#!/usr/bin/env python3 """Compare C++ vs Python detokenizer, step by step. Runs ace-synth with --dump, then Python detokenizer, and compares. Also validates Python intermediates against manual math to isolate bugs. Usage: ./debug-detok-cossim.py Expects request0.json in CWD with audio_codes (run ace-lm first). """ import sys, os, json, struct, subprocess, shutil import numpy as np SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(SCRIPT_DIR) GGML_BIN = os.path.join(ROOT, "build", "ace-synth") DIT_GGUF = os.path.join(ROOT, "models", "acestep-v15-sft-BF16.gguf") QWEN_GGUF = os.path.join(ROOT, "models", "Qwen3-Embedding-0.6B-BF16.gguf") VAE_GGUF = os.path.join(ROOT, "models", "vae-BF16.gguf") FSQ_LEVELS = [8, 8, 8, 5, 5, 5] def cos(a, b): a, b = a.flatten().astype(np.float64), b.flatten().astype(np.float64) n = min(len(a), len(b)) a, b = a[:n], b[:n] d = np.linalg.norm(a) * np.linalg.norm(b) return float(np.dot(a, b) / d) if d > 1e-10 else 0.0 def stats(name, a, b): c = cos(a, b) a_f, b_f = a.flatten(), b.flatten() n = min(len(a_f), len(b_f)) diff = np.abs(a_f[:n] - b_f[:n]) tag = "OK" if c > 0.999 else "BAD" if c < 0.99 else "WARN" print(f"{name:25s} cos={c:.6f} maxdiff={diff.max():.6f} meandiff={diff.mean():.6f} [{tag}]") return c def load_dump(path): raw = np.fromfile(path, dtype=np.float32) ndim = int(struct.unpack('i', struct.pack('f', raw[0]))[0]) shape = [int(struct.unpack('i', struct.pack('f', raw[1+i]))[0]) for i in range(ndim)] data = raw[1 + ndim:] return data, shape def fsq_decode_index(index): out = np.zeros(6, dtype=np.float32) stride = 1 for d in range(6): L = FSQ_LEVELS[d] level_idx = (index // stride) % L half_L = (L - 1) / 2.0 out[d] = level_idx / half_L - 1.0 stride *= L return out def run_ggml(request_path, dump_dir): if not os.path.isfile(GGML_BIN): print(f"[GGML] binary not found: {GGML_BIN}") return False if os.path.isdir(dump_dir): shutil.rmtree(dump_dir) os.makedirs(dump_dir) cmd = [ GGML_BIN, "--dit", DIT_GGUF, "--embedding", QWEN_GGUF, "--vae", VAE_GGUF, "--request", request_path, "--dump", dump_dir, ] print(f"[GGML] Running ace-synth...") r = subprocess.run(cmd, stderr=subprocess.PIPE, text=True) detok_path = os.path.join(dump_dir, "detok_output.bin") if not os.path.isfile(detok_path): print(f"[GGML] FAILED: no detok_output.bin (exit {r.returncode})") if r.stderr: for line in r.stderr.strip().split('\n')[-10:]: print(f" {line}") return False print(f"[GGML] Done") return True def main(): if not os.path.isfile("request0.json"): print("[Error] request0.json not found in CWD") return 1 request_path = "request0.json" req = json.load(open(request_path)) if 'audio_codes' not in req or not req['audio_codes']: print("ERROR: request has no audio_codes (run ace-lm first)") return 1 codes = [int(x) for x in req['audio_codes'].split(',')] T_5Hz = len(codes) print(f"[Input] {T_5Hz} codes, first 5: {codes[:5]}") # Step 1: Run GGML dump_dir = os.path.join(SCRIPT_DIR, "detok-dump") if not run_ggml(request_path, dump_dir): return 1 ggml_data, ggml_shape = load_dump(os.path.join(dump_dir, "detok_output.bin")) T_25Hz = ggml_shape[0] ggml_out = ggml_data.reshape(T_25Hz, 64) print(f"[GGML] detok_output: [{T_25Hz}, 64]") # Step 2: Run Python print("[Python] Loading model...") import torch sys.path.insert(0, os.path.join(ROOT, '..', 'ACE-Step-1.5')) from acestep.handler import AceStepHandler handler = AceStepHandler() handler.initialize_service( project_root=ROOT, config_path='acestep-v15-sft', device='cuda', ) model = handler.model detok = model.detokenizer codes_tensor = torch.tensor([codes], dtype=torch.long, device='cuda').unsqueeze(-1) with torch.no_grad(): # FSQ dequant + project_out lm_hints_5Hz = model.tokenizer.quantizer.get_output_from_indices(codes_tensor) py_after_proj = lm_hints_5Hz[0].float().cpu().detach().numpy() # embed_tokens py_embedded = detok.embed_tokens(lm_hints_5Hz) py_embed_np = py_embedded[0].float().cpu().detach().numpy() # special_tokens + broadcast B, T, D = py_embedded.shape x = py_embedded.unsqueeze(2).repeat(1, 1, 5, 1) special = detok.special_tokens.expand(B, T, -1, -1) py_after_special = (x + special)[0, 0].float().cpu().detach().numpy() # Full detokenize lm_hints_25Hz = model.detokenize(lm_hints_5Hz) py_out = lm_hints_25Hz[0].float().cpu().detach().numpy() print(f"[Python] detok output: {py_out.shape}") # Step 3: GGML vs Python final comparison print(f"[Compare] GGML vs Python ({T_25Hz} frames)") n = min(len(ggml_out), len(py_out)) stats("detok_output (full)", ggml_out[:n], py_out[:n]) for t in range(min(5, T_5Hz)): g = ggml_out[t*5:(t+1)*5] p = py_out[t*5:(t+1)*5] stats(f"token {t} (5 frames)", g, p) print(f"Frame 0 (ch 0-7):") print(f"GGML: {ggml_out[0, :8]}") print(f"Python: {py_out[0, :8]}") # Step 4: Validate Python math (isolate which stage could break C++) print(f"[Math validation] Python intermediates vs manual compute") # FSQ decode fsq_manual = np.array([fsq_decode_index(c) for c in codes]) fsq_layer = model.tokenizer.quantizer.layers[0] idx_tensor = torch.tensor([[[codes[0]]]], dtype=torch.long, device='cuda') raw_fsq = fsq_layer.indices_to_codes(idx_tensor) raw_fsq_np = raw_fsq[0, 0, 0].float().cpu().detach().numpy() stats("FSQ decode tok0", fsq_manual[0], raw_fsq_np) # project_out proj_w = model.tokenizer.quantizer.project_out.weight.float().cpu().detach().numpy() proj_b = model.tokenizer.quantizer.project_out.bias.float().cpu().detach().numpy() manual_proj = fsq_manual[0] @ proj_w.T + proj_b stats("project_out tok0", manual_proj, py_after_proj[0]) # embed_tokens embed_w = detok.embed_tokens.weight.float().cpu().detach().numpy() embed_b = detok.embed_tokens.bias.float().cpu().detach().numpy() manual_embed = py_after_proj[0] @ embed_w.T + embed_b stats("embed_tokens tok0", manual_embed, py_embed_np[0]) # special_tokens special_np = detok.special_tokens[0].float().cpu().detach().numpy() manual_after_special = np.tile(manual_embed, (5, 1)) + special_np stats("special_tokens tok0", manual_after_special, py_after_special) print(f"[Summary]") c_final = cos(ggml_out[:n], py_out[:n]) if c_final > 0.999: print(f"PASS: cos={c_final:.6f}") elif c_final > 0.99: print(f"WARN: cos={c_final:.6f} (precision issue, check bf16 vs f32)") else: print(f"FAIL: cos={c_final:.6f}") print(f"If math validation OK above, bug is in C++ 2L encoder (attn/MLP).") print(f"If math validation BAD, check weight loading / FSQ / projections.") return 0 if __name__ == '__main__': sys.exit(main())