initial release

This commit is contained in:
civ
2026-07-05 18:11:23 +07:00
commit 8fb29dac70
2105 changed files with 499091 additions and 0 deletions
+585
View File
@@ -0,0 +1,585 @@
#!/usr/bin/env python3
"""Cossim debug : C++ omnivoice-tts vs Python OmniVoice on voice cloning.
Inputs (relative to CWD = tests/) :
../examples/prompt.txt target text fed to both pipelines
../examples/freeman.txt transcript of the cloning reference
../examples/freeman.wav cloning reference audio, any rate, any layout
Both sides run with seed=42, F32 weights, language=English, no pre or post
process. The reference audio is resampled to 24 kHz mono inside both
pipelines.
Dumps land in cpp/ (C++) and python/ (Python) and are compared pair by
pair. All paths are relative.
"""
import argparse
import os
import struct
import subprocess
import sys
# Strict F32 matmul on both sides. NVIDIA_TF32_OVERRIDE=0 forces full FP32
# mantissa in cuBLAS for both PyTorch and the C++ child via inheritance.
# Must be set BEFORE torch imports so the cuBLAS handle reads it on init.
os.environ["NVIDIA_TF32_OVERRIDE"] = "0"
import numpy as np
import soundfile as sf
import torch
import torch.nn.functional as F
import torchaudio
# Belt and suspenders : disable PyTorch's own TF32 toggles too. Some code
# paths bypass NVIDIA_TF32_OVERRIDE through cudnn or torch internal flags.
torch.backends.cuda.matmul.allow_tf32 = False
torch.backends.cudnn.allow_tf32 = False
from omnivoice import OmniVoice
from omnivoice.utils.common import fix_random_seed
BIN = "../build/omnivoice-tts"
MODEL_LM_T = "../models/omnivoice-base-{q}.gguf"
MODEL_CDC_T = "../models/omnivoice-tokenizer-{q}.gguf"
CKPT = "../checkpoints/OmniVoice"
DUMP_CPP = "cpp"
DUMP_PT = "python"
def ensure_dir(path):
os.makedirs(path, exist_ok=True)
def save_dump(path, data):
if isinstance(data, torch.Tensor):
data = data.detach().to(torch.float32).cpu().numpy()
data = np.ascontiguousarray(data.astype(np.float32))
shape = data.shape
with open(path, "wb") as f:
f.write(struct.pack("i", len(shape)))
for s in shape:
f.write(struct.pack("i", s))
f.write(data.tobytes())
def load_dump(path):
raw = np.fromfile(path, dtype=np.uint8)
ndim = int(np.frombuffer(raw[0:4], dtype=np.int32)[0])
shape = tuple(int(x) for x in np.frombuffer(raw[4:4 + 4 * ndim], dtype=np.int32))
body = np.frombuffer(raw[4 + 4 * ndim:], dtype=np.float32)
return body.reshape(shape), shape
def cos(a, b):
a = a.astype(np.float64).ravel()
b = b.astype(np.float64).ravel()
n = min(len(a), len(b))
a, b = a[:n], b[:n]
# log_probs contains -inf at the audio_mask_id slot. Mask these symmetric
# positions to 0 on both sides so they cancel out of the inner product.
bad = ~(np.isfinite(a) & np.isfinite(b))
if bad.any():
a = np.where(bad, 0.0, a)
b = np.where(bad, 0.0, b)
d = float(np.linalg.norm(a) * np.linalg.norm(b))
return float(np.dot(a, b) / d) if d > 1e-10 else 0.0
def stft_cos(a, b, win=2048, hop=512):
# STFT magnitude cosine. Drops phase, so a constant time shift between
# the two waveforms does not collapse the score. The plain cos() on
# raw samples falls to ~0 the moment chunks land a few samples apart.
a = a.astype(np.float64).ravel()
b = b.astype(np.float64).ravel()
n = min(len(a), len(b))
a, b = a[:n], b[:n]
window = np.hanning(win)
frames = (n - win) // hop + 1
if frames <= 0:
return 0.0
sa = np.zeros((frames, win // 2 + 1))
sb = np.zeros((frames, win // 2 + 1))
for i in range(frames):
s = i * hop
sa[i] = np.abs(np.fft.rfft(a[s:s + win] * window))
sb[i] = np.abs(np.fft.rfft(b[s:s + win] * window))
return cos(sa.ravel(), sb.ravel())
def install_hooks(model, dump_dir):
# Capture the raw input_ids row k=0 for cond and uncond at the first
# forward pass. Lets a divergence in prompt construction (style, text,
# ref_audio tokens) localize before the LM runs.
seen_embed = {"done": False}
orig_prepare = model._prepare_embed_inputs
def hooked_prepare(input_ids, audio_mask):
out = orig_prepare(input_ids, audio_mask)
if not seen_embed["done"] and input_ids.dim() == 3 and input_ids.shape[0] >= 2:
cond_ids = input_ids[0, 0, :].detach().to(torch.float32).cpu().numpy()
uncond_ids = input_ids[1, 0, :].detach().to(torch.float32).cpu().numpy()
save_dump(os.path.join(dump_dir, "prompt-cond-ids.bin"), cond_ids)
save_dump(os.path.join(dump_dir, "prompt-uncond-ids.bin"), uncond_ids)
seen_embed["done"] = True
return out
model._prepare_embed_inputs = hooked_prepare
seen = {"step0": False, "mg_tokens": False, "audio": False}
orig_pred = model._predict_tokens_with_scoring
def hooked_pred(c_logits, u_logits, gen_config):
is_first = not seen["step0"]
if is_first:
c = c_logits.detach().to(torch.float32).cpu().numpy()
u = u_logits.detach().to(torch.float32).cpu().numpy()
if c.ndim == 4:
c = c[0]
if u.ndim == 4:
u = u[0]
save_dump(os.path.join(dump_dir, "lm-logits-step0-cond.bin"), c)
save_dump(os.path.join(dump_dir, "lm-logits-step0-uncond.bin"), u)
pred_tokens, scores = orig_pred(c_logits, u_logits, gen_config)
if is_first:
# Reconstruct log_probs with the same op sequence as the Python
# function so the dump is the actual log_probs the model used.
with torch.no_grad():
if gen_config.guidance_scale != 0:
cl = F.log_softmax(c_logits, dim=-1)
ul = F.log_softmax(u_logits, dim=-1)
lp = torch.log_softmax(cl + gen_config.guidance_scale * (cl - ul), dim=-1)
else:
lp = F.log_softmax(c_logits, dim=-1)
lp[..., model.config.audio_mask_id] = -float("inf")
lp_arr = lp.detach().to(torch.float32).cpu().numpy()
if lp_arr.ndim == 4:
lp_arr = lp_arr[0]
save_dump(os.path.join(dump_dir, "mg-log-probs-step0.bin"), lp_arr)
pt_arr = pred_tokens.detach().to(torch.float32).cpu().numpy()
if pt_arr.ndim == 3:
pt_arr = pt_arr[0]
save_dump(os.path.join(dump_dir, "mg-pred-tokens-step0.bin"), pt_arr)
sc_arr = scores.detach().to(torch.float32).cpu().numpy()
if sc_arr.ndim == 3:
sc_arr = sc_arr[0]
save_dump(os.path.join(dump_dir, "mg-scores-step0.bin"), sc_arr)
seen["step0"] = True
return pred_tokens, scores
model._predict_tokens_with_scoring = hooked_pred
orig_generate = model._generate_iterative
def hooked_generate(task, gen_config):
out = orig_generate(task, gen_config)
if not seen["mg_tokens"]:
save_dump(os.path.join(dump_dir, "mg-tokens.bin"), out[0])
seen["mg_tokens"] = True
return out
model._generate_iterative = hooked_generate
orig_decode = model.audio_tokenizer.decode
def hooked_decode(*args, **kwargs):
out = orig_decode(*args, **kwargs)
if seen["audio"]:
return out
wav = getattr(out, "audio_values", out)
if isinstance(wav, torch.Tensor):
arr = wav.detach().to(torch.float32).cpu().numpy()
else:
arr = np.asarray(wav, dtype=np.float32)
if arr.ndim == 3:
arr = arr[0, 0]
elif arr.ndim == 2:
arr = arr[0]
save_dump(os.path.join(dump_dir, "output-audio.bin"), arr)
seen["audio"] = True
return out
model.audio_tokenizer.decode = hooked_decode
# Capture the full reference audio code matrix [K, T_ref] before it goes
# into the prompt, so a divergence in the codec encoder is visible at the
# source rather than only via prompt-cond-ids row k=0.
orig_encode = model.audio_tokenizer.encode
seen_enc = {"done": False}
def hooked_encode(*args, **kwargs):
out = orig_encode(*args, **kwargs)
if not seen_enc["done"]:
codes = out.audio_codes if hasattr(out, "audio_codes") else out
if isinstance(codes, torch.Tensor):
arr = codes.detach().to(torch.float32).cpu().numpy()
if arr.ndim == 3:
arr = arr[0]
save_dump(os.path.join(dump_dir, "ref-audio-codes.bin"), arr)
seen_enc["done"] = True
return out
model.audio_tokenizer.encode = hooked_encode
# Capture the 16 kHz post resample audio and the HuBERT semantic features
# at the boundary inside the audio tokenizer. Lets us bisect the codec
# encoder : raw 24k -> resample 16k -> hubert -> sem_enc -> RVQ -> codes.
orig_extract = model.audio_tokenizer._extract_semantic_features
seen_sem = {"done": False}
def hooked_extract(input_values):
if not seen_sem["done"]:
cfg = model.audio_tokenizer.config
if cfg.sample_rate != cfg.semantic_sample_rate:
v16 = torchaudio.functional.resample(input_values, cfg.sample_rate, cfg.semantic_sample_rate)
else:
v16 = input_values
arr16 = v16[:, 0, :].squeeze(0).detach().to(torch.float32).cpu().numpy()
save_dump(os.path.join(dump_dir, "ref-audio-16k.bin"), arr16)
out = orig_extract(input_values)
if not seen_sem["done"]:
feat = out.squeeze(0).detach().to(torch.float32).cpu().numpy()
save_dump(os.path.join(dump_dir, "ref-hubert-features.bin"), feat)
seen_sem["done"] = True
return out
model.audio_tokenizer._extract_semantic_features = hooked_extract
# Bisect HuBERT internals. The C++ counterpart marks the same 9 stages as
# graph outputs in pipeline_codec_hubert_features_test and dumps them post
# compute. Names match exactly so the Python compare loop can pair them.
sm = model.audio_tokenizer.semantic_model
seen_hub = {"feat": False, "proj_ln": False, "proj": False, "init": False,
"l0": False, "l5": False, "l7": False, "l9": False, "l11": False}
def hub_hook_feat(module, inputs, output):
# HF feature_extractor returns (B, C=512, T_feat). Squeeze gives
# (C, T_feat) which matches the C++ ne=(T, C) tensor whose linear
# buffer is laid out slow-first as (C, T). No transpose needed.
if seen_hub["feat"]:
return
arr = output[0].detach().to(torch.float32).cpu().numpy()
save_dump(os.path.join(dump_dir, "hubert-feat-extract.bin"), arr)
seen_hub["feat"] = True
sm.feature_extractor.register_forward_hook(hub_hook_feat)
# feature_projection internal split : layer_norm first, then projection
# Linear. The C++ side dumps the post LN tensor before the Linear, so we
# mirror with a forward hook on the LN sub-module.
def hub_hook_proj_ln(module, inputs, output):
if seen_hub["proj_ln"]:
return
out_t = output[0] if isinstance(output, tuple) else output
arr = out_t[0].detach().to(torch.float32).cpu().numpy()
save_dump(os.path.join(dump_dir, "hubert-feat-proj-ln.bin"), arr)
seen_hub["proj_ln"] = True
sm.feature_projection.layer_norm.register_forward_hook(hub_hook_proj_ln)
def hub_hook_proj(module, inputs, output):
# feature_projection returns (B, T_feat, 768) already T-first.
if seen_hub["proj"]:
return
out_t = output[0] if isinstance(output, tuple) else output
arr = out_t[0].detach().to(torch.float32).cpu().numpy()
save_dump(os.path.join(dump_dir, "hubert-feat-proj.bin"), arr)
seen_hub["proj"] = True
sm.feature_projection.register_forward_hook(hub_hook_proj)
# encoder.layer_norm is the LN that follows pos_conv_embed add. Its output
# equals the C++ enc_init output : x + pos_conv(x) -> LN.
def hub_hook_init(module, inputs, output):
if seen_hub["init"]:
return
arr = output[0].detach().to(torch.float32).cpu().numpy()
save_dump(os.path.join(dump_dir, "hubert-enc-init.bin"), arr)
seen_hub["init"] = True
sm.encoder.layer_norm.register_forward_hook(hub_hook_init)
# Mid stack taps : layer 0, 5, 7, 9, 11. Mirror the C++ states[1], [6],
# [8], [10], [12] indexing (states[0] is enc-init dumped above). l7 and
# l9 give the resolution to localize the explosion seen between l5 and l11.
def make_layer_tap(key, fname):
def hook(module, inputs, output):
if seen_hub[key]:
return
h = output[0] if isinstance(output, tuple) else output
arr = h[0].detach().to(torch.float32).cpu().numpy()
save_dump(os.path.join(dump_dir, fname), arr)
seen_hub[key] = True
return hook
sm.encoder.layers[0].register_forward_hook(make_layer_tap("l0", "hubert-l0.bin"))
sm.encoder.layers[5].register_forward_hook(make_layer_tap("l5", "hubert-l5.bin"))
sm.encoder.layers[7].register_forward_hook(make_layer_tap("l7", "hubert-l7.bin"))
sm.encoder.layers[9].register_forward_hook(make_layer_tap("l9", "hubert-l9.bin"))
sm.encoder.layers[11].register_forward_hook(make_layer_tap("l11", "hubert-l11.bin"))
# Bisect the LM forward by dumping per layer hidden states at step 0.
# The C++ pipeline dumps cond and uncond at layers 0, 6, 13, 20 and the
# final norm. Mirror that exactly so a per layer max_abs_diff comparison
# localizes where the drift starts.
seen_lm = {"done": False}
def make_layer_hook(name):
def hook(module, inputs, output):
if seen_lm["done"]:
return
h = output[0] if isinstance(output, tuple) else output
if not isinstance(h, torch.Tensor) or h.ndim < 3:
return
cond_arr = h[0].detach().to(torch.float32).cpu().numpy()
save_dump(os.path.join(dump_dir, f"lm-hidden-step0-cond-{name}.bin" if name else "lm-hidden-step0-cond.bin"), cond_arr)
if h.shape[0] >= 2:
unc_arr = h[1].detach().to(torch.float32).cpu().numpy()
save_dump(os.path.join(dump_dir, f"lm-hidden-step0-uncond-{name}.bin" if name else "lm-hidden-step0-uncond.bin"), unc_arr)
return hook
lm_hook_handles = []
for idx, name in [(0, "l0"), (1, "l1"), (2, "l2"), (3, "l3"), (4, "l4"), (5, "l5"), (6, "l6"),
(13, "l13"), (14, "l14"), (15, "l15"), (16, "l16"), (17, "l17"), (18, "l18"),
(19, "l19"), (20, "l20")]:
if idx < len(model.llm.layers):
lm_hook_handles.append(model.llm.layers[idx].register_forward_hook(make_layer_hook(name)))
lm_hook_handles.append(model.llm.norm.register_forward_hook(make_layer_hook("")))
# Trip seen_lm["done"] right after the first LM forward by hooking the
# outermost forward via the existing _predict_tokens_with_scoring path
# already gated on step0. We instead use an extra one-shot hook on the
# last collected layer (norm) to flip the flag after its dump fires.
def flip_done(module, inputs, output):
seen_lm["done"] = True
lm_hook_handles.append(model.llm.norm.register_forward_hook(flip_done))
# Per-layer hooks above already cover layers 0-6, 13-20 and final norm.
# Add the embed boundary on top and a sub-module bisect inside layer 1 to
# locate where the L0 -> L1 jump (15x in one layer) originates.
orig_prepare_embed = model._prepare_embed_inputs
def hooked_prepare_embed(input_ids, audio_mask):
out = orig_prepare_embed(input_ids, audio_mask)
if not seen_lm["done"]:
arr = out.detach().to(torch.float32).cpu().numpy()
save_dump(os.path.join(dump_dir, "lm-hidden-step0-cond-embed.bin"), arr[0])
if arr.shape[0] >= 2:
save_dump(os.path.join(dump_dir, "lm-hidden-step0-uncond-embed.bin"), arr[1])
return out
model._prepare_embed_inputs = hooked_prepare_embed
# Layer 1 sub-module bisect : dump after input_layernorm, after self_attn
# (pre-residual), after post_attn_layernorm and after mlp (pre-residual).
# Suffixes : -l1-norm1, -l1-attn, -l1-norm2, -l1-mlp.
def make_sub_hook(suffix):
def hook(module, inputs, output):
if seen_lm["done"]:
return
h = output[0] if isinstance(output, tuple) else output
if not isinstance(h, torch.Tensor) or h.ndim < 3:
return
arr = h.detach().to(torch.float32).cpu().numpy()
save_dump(os.path.join(dump_dir, f"lm-hidden-step0-cond-l1-{suffix}.bin"), arr[0])
if arr.shape[0] >= 2:
save_dump(os.path.join(dump_dir, f"lm-hidden-step0-uncond-l1-{suffix}.bin"), arr[1])
return hook
if len(model.llm.layers) > 1:
l1 = model.llm.layers[1]
lm_hook_handles.append(l1.input_layernorm.register_forward_hook(make_sub_hook("norm1")))
lm_hook_handles.append(l1.self_attn.register_forward_hook(make_sub_hook("attn")))
lm_hook_handles.append(l1.post_attention_layernorm.register_forward_hook(make_sub_hook("norm2")))
lm_hook_handles.append(l1.mlp.register_forward_hook(make_sub_hook("mlp")))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--prompt", default="../examples/prompt.txt")
ap.add_argument("--ref-text", default="../examples/freeman.txt")
ap.add_argument("--ref-wav", default="../examples/freeman.wav")
ap.add_argument("--seed", type=int, default=42)
ap.add_argument("--lang", default="English")
ap.add_argument("--duration", type=float, default=None)
ap.add_argument("--quant", default="F32",
help="quantization suffix for GGUF (default: F32, e.g. BF16, Q8_0, Q4_K_M)")
ap.add_argument("--out-cpp", default="cpp/clone-cpp.wav")
ap.add_argument("--out-pt", default="python/clone-python.wav")
args = ap.parse_args()
model_lm = MODEL_LM_T.format(q=args.quant)
model_cdc = MODEL_CDC_T.format(q=args.quant)
for p in (model_lm, model_cdc):
if not os.path.isfile(p):
print(f"[Error] GGUF not found: {p}")
sys.exit(1)
print(f"[Quant] {args.quant} -> {model_lm} + {model_cdc}")
ensure_dir(DUMP_CPP)
ensure_dir(DUMP_PT)
os.makedirs(os.path.dirname(args.out_cpp) or ".", exist_ok=True)
with open(args.prompt, "r", encoding="utf-8") as f:
text = f.read().strip()
with open(args.ref_text, "r", encoding="utf-8") as f:
ref_text = f.read().strip()
print(f"[Input] Prompt: {len(text)} chars: {text[:60]}{'...' if len(text) > 60 else ''}")
print(f"[Input] RefText: {len(ref_text)} chars: {ref_text[:60]}{'...' if len(ref_text) > 60 else ''}")
print(f"[Input] RefWav: {args.ref_wav}")
print(f"[Input] Language: {args.lang}")
print(f"[Input] Seed: {args.seed}")
fix_random_seed(args.seed)
device = "cuda" if torch.cuda.is_available() else "cpu"
model = OmniVoice.from_pretrained(
CKPT,
torch_dtype=torch.float32,
attn_implementation="eager",
).to(device).eval()
install_hooks(model, DUMP_PT)
gen_kwargs = dict(
text=text,
language=args.lang,
ref_text=ref_text,
ref_audio=args.ref_wav,
duration=args.duration,
)
audios = model.generate(**gen_kwargs)
audio_pt = np.asarray(audios[0], dtype=np.float32)
sf.write(args.out_pt, audio_pt, 24000, subtype="FLOAT")
print(f"[Python] Audio: {audio_pt.shape[0]} samples {audio_pt.shape[0] / 24000:.2f}s -> {args.out_pt}")
del model
torch.cuda.empty_cache()
# Strict F32 conformance: disable FlashAttention (eager attention) and
# force strict F32 cuBLAS to match the Python side with allow_tf32=False
# set at the top of this file. Both pipelines encode the reference audio
# independently so every encoder stage gets compared frontally.
cmd = [
BIN,
"--model", model_lm,
"--codec", model_cdc,
"--seed", str(args.seed),
"--ref-wav", args.ref_wav,
"--ref-text", args.ref_text,
"--lang", args.lang,
"--format", "wav32",
"--dump", DUMP_CPP,
"-o", args.out_cpp,
"--no-fa",
]
if args.duration:
cmd += ["--duration", str(args.duration)]
print(f"[GGML] Cmd: {' '.join(cmd)}")
r = subprocess.run(cmd, input=text, text=True)
if r.returncode != 0:
sys.exit(r.returncode)
audio_cpp, sr = sf.read(args.out_cpp)
if audio_cpp.ndim > 1:
audio_cpp = audio_cpp[:, 0]
audio_cpp = audio_cpp.astype(np.float32)
print(f"[GGML] Audio: {audio_cpp.shape[0]} samples {sr} Hz {audio_cpp.shape[0] / sr:.2f}s -> {args.out_cpp}")
# Cossim in pipeline order: prompt-ids -> logits -> tokens -> audio.
# Cond and uncond on the same line so a drift localizes to the originating
# stage. PromptIDs split into zones (style, text, ref-audio, target) when
# the lengths can be inferred from the prompt log.
def pair(name):
a, _ = load_dump(os.path.join(DUMP_CPP, name))
b, _ = load_dump(os.path.join(DUMP_PT, name))
return a, b
ca, cb = pair("prompt-cond-ids.bin")
ua, ub = pair("prompt-uncond-ids.bin")
n = min(ca.size, cb.size)
cai = ca.astype(np.int64).ravel()[:n]
cbi = cb.astype(np.int64).ravel()[:n]
cdiffs = np.where(cai != cbi)[0]
n2 = min(ua.size, ub.size)
uai = ua.astype(np.int64).ravel()[:n2]
ubi = ub.astype(np.int64).ravel()[:n2]
udiffs = np.where(uai != ubi)[0]
print(f"[Cossim] PromptIDs cond exact: {100.0 * (1 - cdiffs.size / max(n, 1)):.2f}% diffs: {cdiffs.size} uncond exact: {100.0 * (1 - udiffs.size / max(n2, 1)):.2f}% diffs: {udiffs.size}")
if cdiffs.size > 0:
print(f"[Cossim] PromptIDs cond first diff range: s={cdiffs[0]}..{cdiffs[-1]}")
if udiffs.size > 0:
print(f"[Cossim] PromptIDs uncond first diff range: s={udiffs[0]}..{udiffs[-1]}")
def maxabs(a, b):
a = a.astype(np.float64).ravel()
b = b.astype(np.float64).ravel()
n = min(len(a), len(b))
if not n:
return 0.0
a, b = a[:n], b[:n]
# log_probs has -inf at the audio_mask_id slot on both sides. Drop
# positions that are non finite on either side so the subtract is safe.
keep = np.isfinite(a) & np.isfinite(b)
if not keep.all():
a = a[keep]
b = b[keep]
return float(np.max(np.abs(a - b))) if a.size else 0.0
ra, rb = pair("ref-audio-16k.bin")
print(f"[Cossim] RefAudio16k max_abs_diff: {maxabs(ra, rb):.3e} cossim: {cos(ra, rb):.6f} samples: {min(ra.size, rb.size)}")
fa, fb = pair("ref-hubert-features.bin")
print(f"[Cossim] HuBERT-features cossim: {cos(fa, fb):.6f} max_abs_diff: {maxabs(fa, fb):.3e} shape_cpp: {fa.shape} shape_pt: {fb.shape}")
# HuBERT bisect taps in pipeline order : feat-extract (post 7 conv1d) ->
# feat-proj-ln (post LN, pre Linear) -> feat-proj (post 512 -> 768 Linear)
# -> enc-init (post pos_conv add + LN) -> l0 -> l5 -> l7 -> l9 -> l11. The
# downsample 2x and the 13-state mean that follow are already covered by
# HuBERT-features above.
for tap in ["hubert-feat-extract", "hubert-feat-proj-ln", "hubert-feat-proj",
"hubert-enc-init", "hubert-l0", "hubert-l5", "hubert-l7", "hubert-l9", "hubert-l11"]:
ta, tb = pair(tap + ".bin")
print(f"[Cossim] {tap} cossim: {cos(ta, tb):.6f} max_abs_diff: {maxabs(ta, tb):.3e} shape_cpp: {ta.shape} shape_pt: {tb.shape}")
ka, kb = pair("ref-audio-codes.bin")
n_k = min(ka.size, kb.size)
kai = ka.astype(np.int64).ravel()[:n_k]
kbi = kb.astype(np.int64).ravel()[:n_k]
kdiffs = np.where(kai != kbi)[0]
print(f"[Cossim] RefAudioCodes exact: {100.0 * (1 - kdiffs.size / max(n_k, 1)):.2f}% diffs: {kdiffs.size} shape_cpp: {ka.shape} shape_pt: {kb.shape}")
if ka.ndim == 2 and kb.ndim == 2 and ka.shape == kb.shape:
per_k = []
for k in range(ka.shape[0]):
d = int(np.sum(ka[k].astype(np.int64) != kb[k].astype(np.int64)))
per_k.append(f"k{k}={100.0 * (1 - d / ka.shape[1]):.1f}%")
print(f"[Cossim] RefAudioCodes per-codebook: {' '.join(per_k)}")
# Per layer LM hidden state drift bisect. C++ dumps cond and uncond at
# 4 mid layers and the final norm. Pairs only print if both files exist.
for layer_name, label in [("l0", "L0"),
("l1-norm1", "L1.norm1"), ("l1-attn", "L1.attn"),
("l1-norm2", "L1.norm2"), ("l1-mlp", "L1.mlp"),
("l1", "L1"), ("l2", "L2"), ("l3", "L3"), ("l4", "L4"), ("l5", "L5"), ("l6", "L6"),
("l13", "L13"), ("l14", "L14"), ("l15", "L15"), ("l16", "L16"), ("l17", "L17"), ("l18", "L18"),
("l19", "L19"), ("l20", "L20"), ("", "Lf")]:
suffix = f"-{layer_name}" if layer_name else ""
cn = f"lm-hidden-step0-cond{suffix}.bin"
un = f"lm-hidden-step0-uncond{suffix}.bin"
try:
ca, cb = pair(cn)
ua, ub = pair(un)
except FileNotFoundError:
continue
# Trim cpp shape (which is c_len) into the larger pt shape (max_c_len)
nc = min(ca.size, cb.size)
nu = min(ua.size, ub.size)
cad = ca.ravel()[:nc].astype(np.float64)
cbd = cb.ravel()[:nc].astype(np.float64)
uad = ua.ravel()[:nu].astype(np.float64)
ubd = ub.ravel()[:nu].astype(np.float64)
max_c = float(np.max(np.abs(cad - cbd))) if nc else 0.0
max_u = float(np.max(np.abs(uad - ubd))) if nu else 0.0
cos_c = cos(cad, cbd)
cos_u = cos(uad, ubd)
print(f"[Cossim] {label} hidden cond: cos={cos_c:.6f} max_abs={max_c:.3e} uncond: cos={cos_u:.6f} max_abs={max_u:.3e}")
ca, cb = pair("lm-logits-step0-cond.bin")
ua, ub = pair("lm-logits-step0-uncond.bin")
print(f"[Cossim] Logits cond: {cos(ca, cb):.6f} uncond: {cos(ua, ub):.6f}")
la, lb = pair("mg-log-probs-step0.bin")
print(f"[Cossim] Step0 log_probs cossim: {cos(la, lb):.6f} max_abs_diff: {maxabs(la, lb):.3e}")
pa, pb = pair("mg-pred-tokens-step0.bin")
n_p = min(pa.size, pb.size)
pai = pa.astype(np.int64).ravel()[:n_p]
pbi = pb.astype(np.int64).ravel()[:n_p]
pdiffs = np.where(pai != pbi)[0]
print(f"[Cossim] Step0 pred_tokens exact: {100.0 * (1 - pdiffs.size / max(n_p, 1)):.2f}% diffs: {pdiffs.size}/{n_p}")
sa, sb = pair("mg-scores-step0.bin")
print(f"[Cossim] Step0 scores cossim: {cos(sa, sb):.6f} max_abs_diff: {maxabs(sa, sb):.3e}")
ta, tb = pair("mg-tokens.bin")
n = min(ta.size, tb.size)
ai = ta.astype(np.int64).ravel()[:n]
bi = tb.astype(np.int64).ravel()[:n]
print(f"[Cossim] Tokens: {cos(ta, tb):.6f} exact: {100.0 * float(np.mean(ai == bi)):.2f}%")
aa, ab = pair("output-audio.bin")
print(f"[Cossim] Audio: {cos(aa, ab):.6f}")
n = min(audio_cpp.size, audio_pt.size)
print(f"[Cossim] WAV stft_cos: {stft_cos(audio_cpp[:n], audio_pt[:n]):.6f} samples: {n}")
if __name__ == "__main__":
main()