Skip to content

Commit 5a29c9d

Browse files
crispasr integrationclaude
andcommitted
feat(raon,#387-adj): fix the DiT flash-attn F16 KQ accumulation that broke the 1B
Squash of feat/raon-opentts-1b (tip 925cc3e, 27 commits). The debug trail stays on that branch; main gets one commit. THE BUG. Raon/F5's DiT attention used ggml_flash_attn_ext unconditionally. The fused flash kernel accumulates the KQ product in F16, and on kernels that ignore GGML_PREC_F32 — P100/sm_60 confirmed — the hint is SILENTLY DROPPED. Measured against a torch-f16 mirror oracle (weights .half().float(), f32 arithmetic, mirroring choose_dtype), C++ drifted ~16x more than pure weight-f16 on the 0.3B (velocity maxdiff 0.008 vs 0.0005) and ~85x on the 1B (0.073 vs 0.0009), with a C++-ONLY catastrophe at 1B block 4 (4.14 vs torch-f16's 0.046). Compounded over 32 ODE steps and 28 layers this ran away to NaN, which is why the 1B emitted non-speech while the 22-layer 0.3B stayed intelligible at 0.90. Adding ggml_flash_attn_ext_set_prec(GGML_PREC_F32) FIXED NOTHING: the 0.3B drift stayed BYTE-IDENTICAL at 0.007985 across the change, with build logs confirming f5_tts.cpp recompiled. That null was only readable because the run asserted the code had reached the kernel first — otherwise "no change" and "my fix never ran" are the same number. THE FIX. A manual mul_mat / soft_max_ext / mul_mat SDPA path, F32 throughout and independent of whether any precision hint is honoured. With it the 0.3B velocity drift collapsed 0.008 -> 0.000506, EQUAL to torch-f16 to the last digit, and the per-block plateau 0.21 -> 0.034, also matching. The residue is then pure irreducible weight-f16. The 1B roundtrip PASSES: overlap 1.0, gen == asr. DEFAULT IS NOW MANUAL; CRISPASR_F5_FLASH=1 opts back into fused flash. No hardware allowlist: "force manual on P100" would generalise from one measured device and leave every other hint-ignoring card silently less correct. A runtime probe (compare flash vs manual on fixed inputs at init, select accordingly) is the documented follow-up; correctness-by-default ships first. This also makes the ALREADY-SHIPPED 0.3B strictly more correct — same drift collapse — at the cost of unfused attention. ONE DEFECT FIXED ON THE WAY IN: f5_use_flash_attn() cached its env read in a `static int v`, so the new switch could not be A/B'd on a live context — an in-process flash-vs-manual comparison would have silently compared one path against ITSELF and passed. This repo has shipped that exact bug before (tests/test_sidon_live.cpp compared one RPE mode against itself for its entire existence). Now read per call; getenv is on the graph-construction path, not a per-frame loop. f5_embed_gpu_enabled() has the same shape but is pre-existing (#294) and out of scope here. VERIFIED: builds clean, including the uncached selector. The branch's 1B and 0.3B roundtrips were run on Kaggle GPU. The seven main commits since the branch last merged (13d1a9d a76af3e 0556aae 0919ba5 745a160 a3f2d60 2c9dfd0) are mel-band, #416 tooling and docs, and touch neither f5 nor the raon path. NOT VERIFIED HERE: no raon GGUF exists on this box, so the roundtrip was not re-run locally after the squash. The default path is unchanged by the selector fix by inspection (opt-in absent => manual, as before), but that is an argument, not a measurement. STILL OPEN, stated so it is not swallowed by the good news: Raon's short-prompt handling is UNPORTED. Upstream applies local_speed=0.3 for generated text under 10 bytes plus a 12 char/sec VAD floor; C++ has neither, so one-word --tts is rushed or truncated on ALL f5-family backends (F5-TTS, Raon 0.3B and 1B alike). A passing full-sentence roundtrip does not clear it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent f69ab10 commit 5a29c9d

24 files changed

Lines changed: 2166 additions & 12 deletions

models/convert-raon-opentts-to-gguf.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,11 +190,8 @@ def main():
190190
n_fft = mel["n_fft"]
191191
mel_type = mel["mel_spec_type"]
192192
assert mel_type == "sbhifigan16k", f"expected sbhifigan16k, got {mel_type}"
193-
head_dim = dim // heads
194193

195194
vocab = [ln.rstrip("\n") for ln in open(args.vocab, encoding="utf-8")]
196-
print(f"arch: dim={dim} depth={depth} heads={heads}x{head_dim} ff_mult={ff_mult} "
197-
f"n_mels={n_mels} sr={sr} vocab={len(vocab)}", flush=True)
198195

199196
print("loading DiT (ema) via mmap …", flush=True)
200197
ckpt = torch.load(str(args.checkpoint), map_location="cpu", weights_only=True, mmap=True)
@@ -203,6 +200,20 @@ def main():
203200
if k.startswith("ema_model.transformer.") and k not in ("initted", "step")}
204201
print(f" {len(dit)} DiT tensors", flush=True)
205202

203+
# head_dim from the ACTUAL q-projection, not dim//heads. F5 Attention sets
204+
# inner_dim = dim_head * heads with dim_head defaulting to 64, independent of
205+
# `dim` — so inner_dim != dim in general (1B: dim=1408, heads=24, but
206+
# inner=1536 → dim_head=64). The 0.3B happened to have inner==dim so the old
207+
# dim//heads was right there; it truncates to 58 on the 1B. Read the truth.
208+
_q = next((v for k, v in dit.items() if k.endswith(".attn.to_q.weight")), None)
209+
if _q is None:
210+
sys.exit("no .attn.to_q.weight in checkpoint — cannot derive head_dim")
211+
inner_dim = int(_q.shape[0])
212+
assert inner_dim % heads == 0, f"inner_dim {inner_dim} not divisible by heads {heads}"
213+
head_dim = inner_dim // heads
214+
print(f"arch: dim={dim} depth={depth} heads={heads}x{head_dim} (inner={inner_dim}) "
215+
f"ff_mult={ff_mult} n_mels={n_mels} sr={sr} vocab={len(vocab)}", flush=True)
216+
206217
# The trained text-embedding row count is authoritative for text_num_embeds
207218
# (= vocab_size + 1). The repo vocab.txt can be larger than the checkpoint
208219
# (0.3B: vocab.txt has 5559 chars but the embed is 5556 = 5555 + 1); trust

src/crispasr_model_registry.cpp

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,12 +1093,21 @@ constexpr Entry k_registry[] = {
10931093
// HiFi-GAN, English zero-shot voice cloning. Rides the f5-tts runtime
10941094
// (same DiT; norm_type=rmsnorm is dead metadata with post_norm=False).
10951095
// Single GGUF carries DiT + HiFi-GAN + the shipped slaney mel fb/window.
1096-
// TTS→ASR roundtrip validated on Kaggle (word overlap 0.90). CPU vocoder
1097-
// is slow (~40s/utterance); a GPU build runs the DiT on-device.
1096+
// TTS→ASR roundtrip validated on Kaggle (word overlap 0.90). The HiFi-GAN
1097+
// vocoder runs through the shared GPU-capable core_hifigan graph (#387);
1098+
// the DiT ODE loop is the remaining cost and runs on-device in a GPU build.
10981099
{"raon", "raon-opentts-0.3b-f16.gguf",
10991100
"https://huggingface.co/cstr/raon-opentts-0.3b-GGUF/resolve/main/raon-opentts-0.3b-f16.gguf",
11001101
"~959 MB", nullptr, nullptr, nullptr,
11011102
"CC-BY-NC-4.0 — NON-COMMERCIAL use only (KRAFTON/Raon-OpenTTS-0.3B, "
1103+
"https://huggingface.co/KRAFTON/Raon-OpenTTS-0.3B)"},
1104+
// Raon-OpenTTS 1B: larger DiT (dim=1408 depth=28 heads=24x64 ff_mult=4);
1105+
// same sbhifigan16k mel + HiFi-GAN vocoder as the 0.3B. Roundtrip validated
1106+
// on Kaggle. Use --backend raon-1b (or -m auto with this key).
1107+
{"raon-1b", "raon-opentts-1b-f16.gguf",
1108+
"https://huggingface.co/cstr/raon-opentts-1b-GGUF/resolve/main/raon-opentts-1b-f16.gguf",
1109+
"~2.8 GB", nullptr, nullptr, nullptr,
1110+
"CC-BY-NC-4.0 — NON-COMMERCIAL use only (KRAFTON/Raon-OpenTTS-1B, "
11021111
"https://huggingface.co/KRAFTON/Raon-OpenTTS-1B)"},
11031112
// Irodori-TTS v3 500M: RF-DiT flow-matching TTS with zero-shot voice
11041113
// cloning via DAC-VAE latents. 48 kHz output, Japanese-focused.

src/f5_tts.cpp

Lines changed: 261 additions & 3 deletions
Large diffs are not rendered by default.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"id": "chr1s4/crispasr-raon-convert",
3+
"title": "crispasr-raon-convert",
4+
"code_file": "raon_convert.py",
5+
"language": "python",
6+
"kernel_type": "script",
7+
"is_private": "true",
8+
"enable_gpu": "false",
9+
"enable_internet": "true",
10+
"dataset_sources": ["chr1s4/crispasr-hf-token"],
11+
"competition_sources": [],
12+
"kernel_sources": [],
13+
"model_sources": []
14+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env python3
2+
"""Raon-OpenTTS → GGUF conversion + upload (#387-adj, CC-BY-NC-4.0).
3+
4+
Download-and-convert only — no torch reference synthesis, no C++ build. The
5+
16.7 GB 1B checkpoint cannot be loaded on the 8 GB VPS, so conversion runs
6+
here; our converter mmaps the .pt and only materializes the ema DiT tensors,
7+
so a CPU box (30 GB) is plenty and no GPU slot is burned (the roundtrip kernel
8+
needs those). The reference-fixture dump lives in raon-ref-dump; end-to-end
9+
validation is raon-roundtrip. Emits cstr/raon-opentts-<size>-GGUF.
10+
11+
Set RAON_SIZE=1B (default) or 0.3B. CPU kernel; datasets: crispasr-hf-token.
12+
"""
13+
import json
14+
import os
15+
import subprocess
16+
import sys
17+
import time
18+
from pathlib import Path
19+
20+
TMP = Path("/kaggle/temp")
21+
TMP.mkdir(parents=True, exist_ok=True)
22+
WORK = Path("/kaggle/working")
23+
24+
SIZE = os.environ.get("RAON_SIZE", "1B")
25+
REPO_MODEL = f"KRAFTON/Raon-OpenTTS-{SIZE}"
26+
CKPT_FILE = {"0.3B": "model_225000.pt", "1B": "model_520000.pt"}[SIZE]
27+
CRISPASR_URL = "https://github.com/CrispStrobe/CrispASR.git"
28+
CRISPASR_REF = os.environ.get("CRISPASR_REF", "feat/raon-opentts-1b")
29+
CLONE = TMP / "CrispASR"
30+
31+
32+
def step(name, **kv):
33+
print(f"[{time.strftime('%H:%M:%S')}] {name} " + json.dumps(kv), flush=True)
34+
35+
36+
# clone CrispASR (retry: Kaggle GitHub access is flaky, gotcha #18) — only the
37+
# converter script is needed, so no submodules.
38+
if not CLONE.exists():
39+
for _ in range(4):
40+
r = subprocess.run(["git", "clone", "--depth", "1", "--branch", CRISPASR_REF,
41+
CRISPASR_URL, str(CLONE)], timeout=1800)
42+
if r.returncode == 0:
43+
break
44+
time.sleep(15)
45+
else:
46+
print("clone failed after retries", flush=True); sys.exit(1)
47+
sys.path.insert(0, str(CLONE / "tools" / "kaggle"))
48+
import kaggle_harness as kh # noqa: E402
49+
50+
kh.init_progress()
51+
HF_TOKEN = kh.resolve_hf_token()
52+
# Small deps only; do NOT reinstall torch/torchaudio (Kaggle's match its arch).
53+
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "pyyaml", "huggingface_hub"], check=False)
54+
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "--no-deps", "gguf"], check=False)
55+
from huggingface_hub import hf_hub_download, HfApi # noqa: E402
56+
57+
MODELS = TMP / "models"
58+
MODELS.mkdir(exist_ok=True)
59+
60+
61+
def dl(repo, fname, sub=""):
62+
d = MODELS / sub if sub else MODELS
63+
d.mkdir(parents=True, exist_ok=True)
64+
return hf_hub_download(repo, fname, local_dir=str(d), token=HF_TOKEN or None)
65+
66+
67+
with kh.build_heartbeat("download", interval_s=30):
68+
ckpt = dl(REPO_MODEL, CKPT_FILE, SIZE)
69+
cfg = dl(REPO_MODEL, "config.yaml", SIZE)
70+
vocab = dl(REPO_MODEL, "vocab.txt", SIZE)
71+
gen_ckpt = dl("speechbrain/tts-hifigan-libritts-16kHz", "generator.ckpt", "sbhifigan")
72+
step("downloaded", size=SIZE, ckpt_gb=round(os.path.getsize(ckpt) / 1e9, 1))
73+
74+
out_gguf = WORK / f"raon-opentts-{SIZE.lower()}-f16.gguf"
75+
with kh.build_heartbeat("convert", interval_s=30):
76+
r = subprocess.run(f"{sys.executable} {CLONE}/models/convert-raon-opentts-to-gguf.py "
77+
f"--checkpoint {ckpt} --config {cfg} --vocab {vocab} --hifigan {gen_ckpt} "
78+
f"--output {out_gguf} --quant f16",
79+
shell=True, capture_output=True, text=True, timeout=3600)
80+
print(r.stdout[-3000:], r.stderr[-3000:], flush=True)
81+
step("converted", rc=r.returncode, exists=out_gguf.exists(),
82+
gguf_gb=round(os.path.getsize(out_gguf) / 1e9, 2) if out_gguf.exists() else 0)
83+
if r.returncode != 0 or not out_gguf.exists():
84+
step("CONVERT_FAIL"); sys.exit(1)
85+
86+
api = HfApi(token=HF_TOKEN)
87+
repo_id = f"cstr/raon-opentts-{SIZE.lower()}-GGUF"
88+
api.create_repo(repo_id, exist_ok=True)
89+
with kh.build_heartbeat("upload", interval_s=30):
90+
api.upload_file(path_or_fileobj=str(out_gguf), repo_id=repo_id, path_in_repo=out_gguf.name)
91+
step("uploaded", repo=repo_id, file=out_gguf.name)
92+
(WORK / "raon_convert.json").write_text(json.dumps(
93+
{"size": SIZE, "gguf": out_gguf.name, "repo": repo_id,
94+
"gguf_gb": round(os.path.getsize(out_gguf) / 1e9, 2)}, indent=2))
95+
step("DONE")
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"id": "chr1s4/crispasr-raon-dit-diff",
3+
"title": "crispasr-raon-dit-diff",
4+
"code_file": "raon_dit_diff.py",
5+
"language": "python",
6+
"kernel_type": "script",
7+
"is_private": "true",
8+
"enable_gpu": "true",
9+
"enable_internet": "true",
10+
"dataset_sources": ["chr1s4/crispasr-hf-token", "chr1s4/crispasr-ccache"],
11+
"competition_sources": [],
12+
"kernel_sources": [],
13+
"model_sources": []
14+
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
#!/usr/bin/env python3
2+
"""Raon 1B DiT per-stage diff (#387-adj) — localize the C++ DiT divergence.
3+
4+
The 1B converts+loads+runs but emits a wrong mel (non-speech). Python reference
5+
is perfect and the GGUF is provably correct, so the bug is in our C++ DiT. This
6+
kernel captures ONE reference DiT forward (torch, CPU) — the input-embed output
7+
(hidden), the timestep embedding, every transformer-block output, and the final
8+
velocity — then injects the SAME hidden + t_emb into our C++ via
9+
CRISPASR_F5_DIT_PROBE and diffs each stage. The first block whose cosine drops
10+
is where our port diverges (rope/attn/norm/ffn/adaln all live inside the block).
11+
12+
POSITIVE CONTROL (a green wall must not be the comparator failing to fire):
13+
- identity cos(ref_b0, ref_b0) == 1
14+
- sensitivity cos(ref_b0, ref_b1) < 0.999 (comparator can report MISMATCH)
15+
- injection the C++ MUST at least reproduce block 0 from the injected hidden;
16+
if cpp_block_0 already ~0 or NaN the harness (not the model) is broken.
17+
18+
CPU torch for the reference (P100 lacks torch stft kernels); C++ runs on GPU
19+
(CUDA) to match the failing roundtrip config. RAON_SIZE=1B. chr1s4 datasets.
20+
"""
21+
import json, os, subprocess, sys, time
22+
from pathlib import Path
23+
import numpy as np
24+
25+
WORK = Path("/kaggle/working"); TMP = Path("/kaggle/temp"); TMP.mkdir(exist_ok=True)
26+
PROBE = TMP / "probe"; PROBE.mkdir(exist_ok=True)
27+
SIZE = os.environ.get("RAON_SIZE", "1B")
28+
REPO = f"KRAFTON/Raon-OpenTTS-{SIZE}"
29+
CKPT_FILE = {"0.3B": "model_225000.pt", "1B": "model_520000.pt"}[SIZE]
30+
CRISPASR_URL = "https://github.com/CrispStrobe/CrispASR.git"
31+
CRISPASR_REF = os.environ.get("CRISPASR_REF", "feat/raon-opentts-1b")
32+
RAON_URL = "https://github.com/krafton-ai/Raon-OpenTTS.git"
33+
CLONE = TMP / "CrispASR"; RAON = TMP / "Raon-OpenTTS"
34+
35+
36+
def sh(cmd, **kw): return subprocess.run(cmd, shell=True, capture_output=True, text=True, **kw)
37+
def step(name, **kv): print(f"[{time.strftime('%H:%M:%S')}] {name} " + json.dumps(kv), flush=True)
38+
39+
40+
for url, dst, ref in ((CRISPASR_URL, CLONE, CRISPASR_REF), (RAON_URL, RAON, None)):
41+
if not dst.exists():
42+
for _ in range(4):
43+
cmd = ["git", "clone", "--depth", "1"] + (["--branch", ref] if ref else []) + [url, str(dst)]
44+
if subprocess.run(cmd).returncode == 0:
45+
break
46+
time.sleep(15)
47+
for _ in range(4):
48+
if subprocess.run(["git", "submodule", "update", "--init", "--recursive", "ggml", "third_party/c2pa-audio"],
49+
cwd=str(CLONE)).returncode == 0 or (CLONE / "ggml" / "CMakeLists.txt").exists():
50+
break
51+
time.sleep(15)
52+
sys.path.insert(0, str(CLONE / "tools" / "kaggle"))
53+
import kaggle_harness as kh # noqa: E402
54+
kh.init_progress()
55+
HF_TOKEN = kh.resolve_hf_token()
56+
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "x_transformers", "torchdiffeq", "ema_pytorch",
57+
"loguru", "einops", "jieba", "pypinyin", "hydra-core", "omegaconf", "vocos", "pyyaml",
58+
"soundfile", "huggingface_hub"], check=False)
59+
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "--no-deps", "gguf"], check=False)
60+
sys.path.insert(0, str(RAON / "src"))
61+
from huggingface_hub import hf_hub_download # noqa: E402
62+
import torch, yaml # noqa: E402
63+
64+
MODELS = TMP / "models"; MODELS.mkdir(exist_ok=True)
65+
def dl(repo, f, sub=""):
66+
d = MODELS / sub if sub else MODELS; d.mkdir(parents=True, exist_ok=True)
67+
return hf_hub_download(repo, f, local_dir=str(d), token=HF_TOKEN or None)
68+
69+
70+
# ── build crispasr (GPU, to match the failing config) ──────────────────────
71+
kh.install_build_toolchain()
72+
arch = kh.detect_cuda_arch()
73+
flags = ["-DCMAKE_BUILD_TYPE=Release"] + kh.cuda_build_flags(arch) + kh.cache_and_link_flags()
74+
r = sh(f"cd {CLONE} && cmake -G Ninja -B build " + " ".join(flags), timeout=1200)
75+
if r.returncode != 0:
76+
step("cmake_FAIL", err=r.stderr[-1500:]); sys.exit(1)
77+
with kh.build_heartbeat("build", interval_s=30):
78+
kh.sh_with_progress(f"cmake --build build -j{kh.safe_build_jobs(gpu=True)} --target crispasr-cli", cwd=str(CLONE))
79+
CLI = CLONE / "build" / "bin" / "crispasr"
80+
if not CLI.exists():
81+
c = [p for p in (CLONE / "build").rglob("crispasr") if p.is_file() and os.access(p, os.X_OK)]
82+
CLI = c[0] if c else None
83+
if not CLI:
84+
step("no_binary"); sys.exit(1)
85+
os.environ["LD_LIBRARY_PATH"] = str(CLI.parent) + ":" + os.environ.get("LD_LIBRARY_PATH", "")
86+
gguf = hf_hub_download(f"cstr/raon-opentts-{SIZE.lower()}-GGUF", f"raon-opentts-{SIZE.lower()}-f16.gguf",
87+
local_dir=str(MODELS), token=HF_TOKEN or None)
88+
ref_wav = CLONE / "samples" / "jfk.wav"
89+
step("built", cli=str(CLI), gguf=os.path.basename(gguf))
90+
91+
# ── reference capture: one DiT forward (torch, CPU, b=1, no cfg) ───────────
92+
ckpt = dl(REPO, CKPT_FILE, SIZE); cfg = dl(REPO, "config.yaml", SIZE); vocab = dl(REPO, "vocab.txt", SIZE)
93+
from f5_tts.model.backbones.dit import DiT # noqa: E402
94+
from f5_tts.model.utils import get_tokenizer, list_str_to_idx # noqa: E402
95+
conf = yaml.safe_load(open(cfg)); a = conf["model"]["arch"]; mspec = conf["model"]["mel_spec"]
96+
n_mel = mspec["n_mel_channels"]
97+
sd = torch.load(ckpt, map_location="cpu", weights_only=True)["ema_model_state_dict"]
98+
sd = {k.replace("ema_model.transformer.", ""): v for k, v in sd.items()
99+
if k.startswith("ema_model.transformer.")}
100+
rows = int(sd["text_embed.text_embed.weight"].shape[0])
101+
vmap, _ = get_tokenizer(vocab, "custom")
102+
vmap = {c: i for c, i in vmap.items() if i < rows - 1}
103+
dit = DiT(**{k: a[k] for k in a if k != "name"}, mel_dim=n_mel, text_num_embeds=rows - 1)
104+
missing, unexpected = dit.load_state_dict(sd, strict=False)
105+
dit.eval()
106+
step("dit_loaded", missing=len(missing), unexpected=len(unexpected), dim=a["dim"], depth=a["depth"], heads=a["heads"])
107+
108+
T = 200
109+
torch.manual_seed(387)
110+
x = torch.randn(1, T, n_mel) * 0.3
111+
cond = torch.randn(1, T, n_mel) * 0.3
112+
gen_text = "the quick brown fox jumps over the lazy dog"
113+
text_ids = list_str_to_idx([list(gen_text)], vmap) # (1, nt)
114+
time_t = torch.tensor([0.5])
115+
116+
dim = a["dim"]; text_dim = a["text_dim"]
117+
cap = {}; blk_out = {}
118+
h1 = dit.input_embed.register_forward_hook(lambda m, i, o: cap.__setitem__("hidden", o.detach()))
119+
h2 = dit.time_embed.register_forward_hook(lambda m, i, o: cap.__setitem__("temb", o.detach()))
120+
h4 = dit.text_embed.register_forward_hook(lambda m, i, o: cap.__setitem__("text_embed", o.detach()))
121+
h3 = dit.proj_out.register_forward_hook(lambda m, i, o: cap.__setitem__("velocity", o.detach()))
122+
bh = [dit.transformer_blocks[k].register_forward_hook(
123+
(lambda kk: (lambda m, i, o: blk_out.__setitem__(kk, o.detach())))(k))
124+
for k in range(len(dit.transformer_blocks))]
125+
with torch.no_grad():
126+
_ = dit(x, cond, text_ids, time_t, drop_audio_cond=False, drop_text=False, cfg_infer=False)
127+
for h in [h1, h2, h4, h3] + bh:
128+
h.remove()
129+
130+
hidden = cap["hidden"][0].contiguous().numpy().astype(np.float32) # (T, dim)
131+
temb = cap["temb"].reshape(-1).numpy().astype(np.float32) # (dim,)
132+
text_embed = cap["text_embed"][0].contiguous().numpy().astype(np.float32) # (T, text_dim)
133+
velocity = cap["velocity"][0].contiguous().numpy().astype(np.float32) # (T, mel)
134+
depth = len(blk_out)
135+
ref_blocks = [blk_out[k][0].contiguous().numpy().astype(np.float32) for k in range(depth)]
136+
137+
# ── UNCOND arm capture (CFG null: drop_audio_cond + drop_text) ─────────────
138+
capu = {}
139+
u1 = dit.input_embed.register_forward_hook(lambda m, i, o: capu.__setitem__("hidden", o.detach()))
140+
u4 = dit.text_embed.register_forward_hook(lambda m, i, o: capu.__setitem__("text_embed", o.detach()))
141+
u3 = dit.proj_out.register_forward_hook(lambda m, i, o: capu.__setitem__("velocity", o.detach()))
142+
dit.clear_cache()
143+
with torch.no_grad():
144+
_ = dit(x, cond, text_ids, time_t, drop_audio_cond=True, drop_text=True, cfg_infer=False)
145+
for h in [u1, u4, u3]:
146+
h.remove()
147+
u_text_embed = capu["text_embed"][0].contiguous().numpy().astype(np.float32)
148+
u_hidden = capu["hidden"][0].contiguous().numpy().astype(np.float32)
149+
u_velocity = capu["velocity"][0].contiguous().numpy().astype(np.float32)
150+
tok = text_ids[0].numpy().astype(np.int32)
151+
(PROBE / "shape.txt").write_text(f"{T} {tok.size}")
152+
(PROBE / "t.txt").write_text(str(float(time_t.item())))
153+
x[0].contiguous().numpy().astype(np.float32).tofile(PROBE / "x.bin")
154+
cond[0].contiguous().numpy().astype(np.float32).tofile(PROBE / "cond.bin")
155+
tok.tofile(PROBE / "tokens.bin")
156+
hidden.tofile(PROBE / "hidden.bin"); temb.tofile(PROBE / "temb.bin")
157+
step("captured", T=T, nt=int(tok.size), dim=dim, depth=depth,
158+
hidden_std=float(hidden.std()), velocity_std=float(velocity.std()))
159+
160+
def run(mode):
161+
env = dict(os.environ); env[mode] = str(PROBE)
162+
r = sh(f"{CLI} --backend raon-1b -m {gguf} --voice {ref_wav} --ref-text x --tts probe "
163+
f"--tts-output {WORK/'probe.wav'} -t 4 --i-have-rights -v", env=env, timeout=1200)
164+
print(f"[{mode}]", r.stdout[-500:], r.stderr[-1500:], flush=True)
165+
166+
run("CRISPASR_F5_INPUT_PROBE") # -> cpp_temb / cpp_text_embed / cpp_hidden
167+
run("CRISPASR_F5_DIT_PROBE") # -> cpp_velocity / cpp_block_<k> (injected hidden+temb)
168+
169+
def load(name):
170+
p = PROBE / name
171+
return np.fromfile(p, dtype=np.float32) if p.exists() else None
172+
173+
def compare(ref, cpp):
174+
if cpp is None:
175+
return None
176+
u = np.asarray(ref).reshape(-1).astype(np.float64); v = np.asarray(cpp).reshape(-1).astype(np.float64)
177+
if u.size != v.size:
178+
return {"size_mismatch": [int(u.size), int(v.size)]}
179+
nu, nv = np.linalg.norm(u), np.linalg.norm(v)
180+
cosv = float(u.dot(v) / (nu * nv)) if nu > 0 and nv > 0 else None
181+
return {"cos": None if cosv is None else round(cosv, 5),
182+
"norm_ratio": round(float(nv / nu), 4) if nu > 0 else None, # ||cpp|| / ||ref|| (1.0 = same scale)
183+
"maxabs_ratio": round(float(np.abs(v).max() / (np.abs(u).max() + 1e-12)), 4)}
184+
185+
# positive control on magnitude too: a 1.7x-scaled ref must read norm_ratio≈1.7, cos≈1
186+
pc = compare(hidden, (hidden * 1.7).reshape(-1))
187+
result = {"size": SIZE, "T": T, "depth": depth,
188+
"positive_control_scale1.7": pc,
189+
"cond_input_path": {"temb": compare(temb, load("cpp_temb.bin")),
190+
"text_embed": compare(text_embed, load("cpp_text_embed.bin")),
191+
"hidden": compare(hidden, load("cpp_hidden.bin"))},
192+
"UNCOND_arm": {"text_embed": compare(u_text_embed, load("cpp_text_embed_uncond.bin")),
193+
"hidden": compare(u_hidden, load("cpp_hidden_uncond.bin")),
194+
"velocity": compare(u_velocity, load("cpp_velocity_uncond.bin"))},
195+
"cond_velocity": compare(velocity, load("cpp_velocity.bin")),
196+
"blocks": [{"k": k, **(compare(ref_blocks[k], load(f"cpp_block_{k}.bin")) or {})} for k in range(depth)]}
197+
(WORK / "raon_dit_diff.json").write_text(json.dumps(result, indent=2))
198+
print(json.dumps(result, indent=2), flush=True)
199+
nr = [b.get("norm_ratio") for b in result["blocks"] if b.get("norm_ratio")]
200+
step("DONE", cond_velocity=result["cond_velocity"], uncond=result["UNCOND_arm"]["velocity"], block0_nr=result["blocks"][0].get("norm_ratio"),
201+
blocklast_nr=result["blocks"][-1].get("norm_ratio"),
202+
norm_ratio_span=[min(nr), max(nr)] if nr else None)

0 commit comments

Comments
 (0)