Initial release

This commit is contained in:
civ
2026-08-16 18:24:52 +07:00
commit 876886a39a
13244 changed files with 2353959 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
CXX ?= g++
CXXFLAGS ?= -std=c++17 -O2
INCLUDES = -I../src
test-philox: test-philox.cpp ../src/philox.h
$(CXX) $(CXXFLAGS) $(INCLUDES) -o $@ $< -lm
clean:
rm -f test-philox philox-noise.f32
.PHONY: clean
+204
View File
@@ -0,0 +1,204 @@
#!/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())
+501
View File
@@ -0,0 +1,501 @@
#!/usr/bin/env python3
"""GGML vs Python cosine similarity comparison for ACE-Step DiT.
Run from tests/ directory. All paths relative to CWD.
Usage:
cd tests/
./debug-dit-cossim.py # turbo BF16
./debug-dit-cossim.py --quant Q6_K # turbo Q6_K
./debug-dit-cossim.py --mode sft # SFT BF16
./debug-dit-cossim.py --mode xl-turbo # XL turbo BF16
./debug-dit-cossim.py --mode all # all 4 models
"""
import os, sys, subprocess, struct, shutil, argparse, json
import numpy as np
SEED = 42
MODE_CONFIG = {
"turbo": {
"gguf_base": "acestep-v15-turbo",
"config_path": "acestep-v15-turbo",
"steps": 8, "shift": 3.0, "guidance": 0.0, "n_layers": 24,
},
"sft": {
"gguf_base": "acestep-v15-sft",
"config_path": "acestep-v15-sft",
"steps": 50, "shift": 1.0, "guidance": 1.0, "n_layers": 24,
},
"xl-turbo": {
"gguf_base": "acestep-v15-xl-turbo",
"config_path": "acestep-v15-xl-turbo",
"steps": 8, "shift": 3.0, "guidance": 0.0, "n_layers": 32,
},
"xl-sft": {
"gguf_base": "acestep-v15-xl-sft",
"config_path": "acestep-v15-xl-sft",
"steps": 50, "shift": 1.0, "guidance": 1.0, "n_layers": 32,
},
}
def load_request():
if not os.path.isfile("request0.json"):
print("[Error] request0.json not found in CWD")
sys.exit(1)
with open("request0.json") as f:
req = json.load(f)
print(f"[Request] Loaded request0.json")
return req
def save_dump(path, data):
import torch
if isinstance(data, torch.Tensor):
data = data.detach().float().cpu().numpy()
data = np.ascontiguousarray(data.astype(np.float32))
shape = data.shape
header = struct.pack("i", len(shape))
for s in shape:
header += struct.pack("i", s)
with open(path, "wb") as f:
f.write(header)
f.write(data.tobytes())
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 _cos_flat(a, b):
n = min(len(a), len(b))
if n == 0:
return 0.0
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 cos(a, b, shape_a=None, shape_b=None):
if shape_a and shape_b and len(shape_a) == 2 and len(shape_b) == 2:
if shape_a[0] == shape_b[1] and shape_a[1] == shape_b[0]:
ra = a.reshape(shape_a)
rb = b.reshape(shape_b)
c_normal = _cos_flat(ra.flatten(), rb.flatten())
c_transposed = _cos_flat(ra.T.flatten(), rb.flatten())
if c_transposed > c_normal:
return c_transposed
return c_normal
return _cos_flat(a, b)
def stft_cos(a, b, win=2048, hop=512):
n = min(len(a), len(b))
a, b = a[:n], b[:n]
window = np.hanning(win)
frames = (n - win) // hop + 1
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_flat(sa.flatten(), sb.flatten())
def codes_to_python_format(codes_csv):
"""Convert '43316,18426,...' to '<|audio_code_43316|><|audio_code_18426|>...'"""
if not codes_csv:
return ""
return "".join(f"<|audio_code_{c.strip()}|>" for c in codes_csv.split(",") if c.strip())
# GGML runner
def run_ggml(dump_dir, req, cfg, gguf_path, adapter_dir=None):
ggml_bin = "../build/ace-synth"
if not os.path.isfile(ggml_bin):
print(f"[GGML] binary not found: {ggml_bin}")
return False
os.makedirs(dump_dir, exist_ok=True)
# Build request from input, override mode-specific params
merged = dict(req)
merged["seed"] = SEED
merged["inference_steps"] = cfg["steps"]
merged["guidance_scale"] = cfg["guidance"]
merged["shift"] = cfg["shift"]
merged["thinking"] = False
request_json = os.path.join(dump_dir, "request0.json")
with open(request_json, "w") as f:
json.dump(merged, f, indent=4)
cmd = [
ggml_bin,
"--dit", gguf_path,
"--embedding", "../models/Qwen3-Embedding-0.6B-BF16.gguf",
"--vae", "../models/vae-BF16.gguf",
"--request", request_json,
"--dump", dump_dir,
]
if adapter_dir:
cmd += ["--adapter", adapter_dir]
print(f"[GGML] Running {os.path.basename(gguf_path)}...")
r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=None, text=True)
n = len([f for f in os.listdir(dump_dir) if f.endswith(".bin")])
if r.returncode != 0:
if n > 0:
print(f"[GGML] WARNING: exit {r.returncode} but {n} dump files exist, continuing")
else:
print(f"[GGML] FAILED (exit {r.returncode})")
if r.stdout:
print(r.stdout[-500:])
return False
print(f"[GGML] Done, {n} dump files")
return True
# Python runner
def run_python(dump_dir, req, cfg, adapter_dir=None):
sys.path.insert(0, "../../ACE-Step-1.5")
from acestep.handler import AceStepHandler
os.makedirs(dump_dir, exist_ok=True)
has_cfg = cfg["guidance"] > 1.0
caption = req["caption"]
lyrics = req.get("lyrics", "")
bpm = req.get("bpm", 0)
duration = req["duration"]
language = req.get("vocal_language", "en")
print(f"[Python] Initializing {cfg['config_path']}...")
handler = AceStepHandler()
handler.initialize_service(
project_root="..",
config_path=cfg["config_path"],
device="cuda",
)
if adapter_dir:
# torch.nn forbids '.' in module names, PEFT derives the adapter name
# from the directory basename. Sanitize so directory names like
# 'ACE-Step-v1.5-chinese-new-year-LoRA' do not abort Python ref load.
adapter_name = os.path.basename(os.path.normpath(adapter_dir)).replace(".", "_") or "default"
lr = handler.add_lora(adapter_dir, adapter_name=adapter_name)
print(f"[Python] LoRA: {lr}")
model = handler.model
_dumps = {}
orig_text = handler.infer_text_embeddings
def hooked_text(*a, **kw):
r = orig_text(*a, **kw)
_dumps["text_hidden"] = r[0].clone()
return r
handler.infer_text_embeddings = hooked_text
orig_lyric = handler.infer_lyric_embeddings
def hooked_lyric(*a, **kw):
r = orig_lyric(*a, **kw)
_dumps["lyric_embed"] = r[0].clone()
return r
handler.infer_lyric_embeddings = hooked_lyric
orig_cond = model.prepare_condition
def hooked_prepare(*a, **kw):
r = orig_cond(*a, **kw)
enc_hs, enc_mask, ctx = r
_dumps["enc_hidden"] = enc_hs[0].clone()
_dumps["context"] = ctx[0].clone()
if has_cfg:
null_expanded = model.null_condition_emb.expand_as(enc_hs)
_dumps["null_enc_hidden"] = null_expanded[0].clone()
return r
model.prepare_condition = hooked_prepare
orig_noise = model.prepare_noise
def hooked_noise(*a, **kw):
n = orig_noise(*a, **kw)
_dumps["noise"] = n[0].clone()
return n
model.prepare_noise = hooked_noise
decoder = model.decoder
_step = [0]
orig_fwd = decoder.forward
def hooked_fwd(*args, **kwargs):
xt_in = args[0] if args else kwargs.get('hidden_states')
step = _step[0]
if step > 0 and xt_in is not None:
_dumps[f"dit_step{step - 1}_xt"] = xt_in[0].clone()
out = orig_fwd(*args, **kwargs)
vt = out[0]
if has_cfg and vt.shape[0] == 2:
_dumps[f"dit_step{step}_vt_cond"] = vt[0].clone()
_dumps[f"dit_step{step}_vt_uncond"] = vt[1].clone()
else:
_dumps[f"dit_step{step}_vt_cond"] = vt[0].clone()
if not has_cfg:
_dumps[f"dit_step{step}_vt"] = vt[0].clone()
_step[0] += 1
return out
decoder.forward = hooked_fwd
if has_cfg:
gen_globals = model.generate_audio.__func__.__globals__
_apg_step = [0]
orig_apg = gen_globals['apg_forward']
def hooked_apg(*args, **kwargs):
result = orig_apg(*args, **kwargs)
_dumps[f"dit_step{_apg_step[0]}_vt"] = result[0].clone()
_apg_step[0] += 1
return result
gen_globals['apg_forward'] = hooked_apg
_dumps["null_condition_emb"] = model.null_condition_emb.squeeze().clone()
_hooks = []
def make_hook(name, step_filter=0):
def hook(module, input, output):
if _step[0] == step_filter:
out = output[0] if isinstance(output, tuple) else output
_dumps[name] = out[0].clone().float()
return hook
_hooks.append(decoder.proj_in.register_forward_hook(make_hook("hidden_after_proj_in")))
_hooks.append(decoder.condition_embedder.register_forward_hook(make_hook("enc_after_cond_emb")))
_hooks.append(decoder.layers[0].register_forward_hook(make_hook("hidden_after_layer0")))
_hooks.append(decoder.layers[0].self_attn.register_forward_hook(make_hook("layer0_sa_output")))
for li in [6, 12, 18, cfg["n_layers"] - 1]:
_hooks.append(decoder.layers[li].register_forward_hook(make_hook(f"hidden_after_layer{li}")))
_hooks.append(decoder.time_embed.register_forward_hook(make_hook("temb_t")))
# Hook detokenizer (runs during prepare_condition, before diffusion)
if hasattr(model, 'detokenizer'):
def detok_hook(module, input, output):
_dumps["detok_output"] = output[0].clone().float()
_hooks.append(model.detokenizer.register_forward_hook(detok_hook))
gen_kwargs = dict(
captions=caption, lyrics=lyrics, bpm=bpm,
audio_duration=float(duration), seed=str(SEED),
use_random_seed=False, batch_size=1,
inference_steps=cfg["steps"], shift=cfg["shift"],
guidance_scale=cfg["guidance"],
infer_method="ode", vocal_language=language,
audio_code_string=codes_to_python_format(req.get("audio_codes", "")),
key_scale=req.get("keyscale", ""),
time_signature=req.get("timesignature", ""),
)
# When audio_codes are present, Python auto-sets is_covers=True via
# conditioning_masks.py (instruction match + has_code_hint).
# This makes it use decoded codes as context, matching C++ behavior.
# Do NOT patch is_covers to False, that would use silence instead of codes.
tag = f"{cfg['config_path']}, {cfg['steps']} steps"
if has_cfg:
tag += f", CFG {cfg['guidance']}"
print(f"[Python] Generating ({tag})...")
result = handler.generate_music(**gen_kwargs)
if not result.get("success"):
print(f"[Python] Generation failed: {result.get('error', 'unknown')}")
return False
for h in _hooks:
h.remove()
extra = result.get("extra_outputs", {})
if extra.get("pred_latents") is not None:
_dumps["dit_x0"] = extra["pred_latents"][0]
audios = result.get("audios", [])
if audios and "tensor" in audios[0]:
_dumps["vae_audio"] = audios[0]["tensor"].squeeze(0)
audio_np = audios[0]["tensor"].squeeze(0).cpu().numpy()
wav_path = os.path.join(dump_dir, "output.wav")
import wave
n_samples = audio_np.shape[1]
interleaved = np.empty(2 * n_samples, dtype=np.float32)
interleaved[0::2] = audio_np[0]
interleaved[1::2] = audio_np[1]
pcm = (np.clip(interleaved, -1, 1) * 32767).astype(np.int16)
with wave.open(wav_path, 'w') as wf:
wf.setnchannels(2)
wf.setsampwidth(2)
wf.setframerate(48000)
wf.writeframes(pcm.tobytes())
print(f"[Python] Wrote {wav_path}: {n_samples} samples ({n_samples/48000:.2f}s @ 48kHz stereo)")
for name, tensor in sorted(_dumps.items()):
save_dump(os.path.join(dump_dir, f"{name}.bin"), tensor)
print(f"[Python] Done, {len(_dumps)} dump files")
return True
# comparison
def build_stages(cfg):
has_cfg = cfg["guidance"] > 1.0
steps = cfg["steps"]
stages = [
"text_hidden", "lyric_embed", "enc_hidden", "detok_output", "context", "noise",
"temb_t", "hidden_after_proj_in", "enc_after_cond_emb",
"layer0_sa_output", "hidden_after_layer0",
"hidden_after_layer6", "hidden_after_layer12", "hidden_after_layer18",
f"hidden_after_layer{cfg['n_layers'] - 1}",
]
if has_cfg:
stages += ["null_condition_emb", "null_enc_hidden"]
if steps <= 8:
step_indices = list(range(steps))
else:
step_indices = list(range(0, steps, 5))
if (steps - 1) not in step_indices:
step_indices.append(steps - 1)
for si in step_indices:
if has_cfg:
stages.append(f"dit_step{si}_vt_cond")
if si < 2:
stages.append(f"dit_step{si}_vt_uncond")
stages.append(f"dit_step{si}_vt")
if si < steps - 1:
stages.append(f"dit_step{si}_xt")
stages += ["dit_x0", "vae_audio"]
return stages
def compare(dirs, stages, tag):
labels = sorted(dirs.keys())
pairs = [(labels[i], labels[j]) for i in range(len(labels)) for j in range(i+1, len(labels))]
print(f"[{tag}] Cosine similarities GGML vs Python")
print(f" {'stage':30s}", end="")
for a, b in pairs:
print(f" {a+' vs '+b:>14s}", end="")
print()
for stage in stages:
data = {}
for label, d in dirs.items():
f = os.path.join(d, stage + ".bin")
if os.path.isfile(f):
data[label] = load_dump(f)
if not data:
continue
print(f" {stage:30s}", end="")
for a, b in pairs:
if a in data and b in data:
da, sa = data[a]
db, sb = data[b]
c = cos(da, db, sa, sb)
print(f" {c:>14.6f}", end="")
else:
print(f" {'N/A':>14s}", end="")
print()
vae_data = {}
for label, d in dirs.items():
f = os.path.join(d, "vae_audio.bin")
if os.path.isfile(f):
vae_data[label] = load_dump(f)
if len(vae_data) >= 2:
print(f" {'vae_audio (STFT cosine)':30s}", end="")
for a, b in pairs:
if a in vae_data and b in vae_data:
c = stft_cos(vae_data[a][0], vae_data[b][0])
print(f" {c:>14.6f}", end="")
else:
print(f" {'N/A':>14s}", end="")
print()
if len(pairs) > 0:
a_label, b_label = pairs[0]
a_dir, b_dir = dirs[a_label], dirs[b_label]
xt_stages = [s for s in stages if "_xt" in s]
if xt_stages:
print(f"[{tag}] Error growth GGML vs Python")
print(f" {'stage':22s} {'cos':>10s} {'max_err':>10s} {'mean_err':>10s}"
f" {'mean_A':>10s} {'std_A':>10s} {'mean_B':>10s} {'std_B':>10s}")
for stage in xt_stages:
fa = os.path.join(a_dir, stage + ".bin")
fb = os.path.join(b_dir, stage + ".bin")
if os.path.isfile(fa) and os.path.isfile(fb):
da_raw, sa = load_dump(fa)
db_raw, sb = load_dump(fb)
if len(sa) == 2 and len(sb) == 2 and sa[0] == sb[0] and sa[1] == sb[1]:
da = da_raw.reshape(sa)
db = db_raw.reshape(sb)
c_flat = _cos_flat(da.flatten(), db.flatten())
c_trans = _cos_flat(da.T.flatten(), db.flatten())
if c_trans > c_flat:
da = da.T
da, db = da.flatten(), db.flatten()
else:
da, db = da_raw, db_raw
n = min(len(da), len(db))
da, db = da[:n], db[:n]
c = _cos_flat(da, db)
diff = np.abs(da - db)
print(f" {stage:22s} {c:10.6f} {diff.max():10.6f} {diff.mean():10.6f}"
f" {da.mean():10.6f} {da.std():10.6f} {db.mean():10.6f} {db.std():10.6f}")
else:
missing = []
if not os.path.isfile(fa): missing.append(a_label)
if not os.path.isfile(fb): missing.append(b_label)
print(f" {stage:22s} missing: {', '.join(missing)}")
# main
def run_mode(mode_name, cfg, req, gguf_path, adapter_dir=None):
dump_ggml = f"ggml-{mode_name}"
dump_python = f"python-{mode_name}"
tag = mode_name.upper() if mode_name == "sft" else mode_name.capitalize()
cfg_str = f"steps={cfg['steps']}, shift={cfg['shift']}"
if cfg['guidance'] > 1.0:
cfg_str += f", CFG={cfg['guidance']}"
print(f"[{tag}] {cfg_str} | {os.path.basename(gguf_path)}")
if os.path.isdir(dump_ggml):
shutil.rmtree(dump_ggml)
if not run_ggml(dump_ggml, req, cfg, gguf_path, adapter_dir):
print(f"[{tag}] GGML failed")
return False
if os.path.isdir(dump_python):
shutil.rmtree(dump_python)
if not run_python(dump_python, req, cfg, adapter_dir):
print(f"[{tag}] Python failed")
return False
stages = build_stages(cfg)
compare({"GGML": dump_ggml, "Python": dump_python}, stages, tag)
return True
def main():
ap = argparse.ArgumentParser(description="GGML vs Python cosine similarity comparison")
ap.add_argument("--mode", default="turbo", choices=list(MODE_CONFIG.keys()) + ["all"],
help="which model to test (default: turbo)")
ap.add_argument("--quant", default="BF16",
help="quantization suffix for GGUF (default: BF16, e.g. Q6_K, Q8_0)")
ap.add_argument("--adapter", default=None,
help="path to adapter directory (optional)")
args = ap.parse_args()
req = load_request()
modes = list(MODE_CONFIG.keys()) if args.mode == "all" else [args.mode]
ok = True
for m in modes:
cfg = MODE_CONFIG[m]
gguf_path = f"../models/{cfg['gguf_base']}-{args.quant}.gguf"
if not os.path.isfile(gguf_path):
print(f"[Error] GGUF not found: {gguf_path}")
ok = False
continue
if not run_mode(m, cfg, req, gguf_path, args.adapter):
ok = False
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
for backend in CUDA0 Vulkan0; do
for quant in BF16 Q8_0 Q6_K Q5_K_M Q4_K_M; do
GGML_BACKEND=$backend ./debug-dit-cossim.py --mode all --quant $quant \
2>&1 | tee ${backend}-${quant}.log
done
done
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""Compare first-token logits: GGML vs PyTorch for ace-lm LM"""
import sys, struct, json, os
import numpy as np
# Load safetensors + run one forward pass in PyTorch
def test_pytorch_logits(model_dir, prompt_tokens):
import torch
from safetensors.torch import load_file
config_path = os.path.join(model_dir, "config.json")
with open(config_path) as f:
cfg = json.load(f)
# Load weights (single file or sharded)
st_single = os.path.join(model_dir, "model.safetensors")
if os.path.isfile(st_single):
weights = load_file(st_single)
else:
import glob
shards = sorted(glob.glob(os.path.join(model_dir, "model-*.safetensors")))
assert shards, f"no safetensors found in {model_dir}"
weights = {}
for s in shards:
weights.update(load_file(s))
H = cfg["hidden_size"]
V = cfg["vocab_size"]
n_layers = cfg["num_hidden_layers"]
n_heads = cfg["num_attention_heads"]
n_kv_heads = cfg["num_key_value_heads"]
head_dim = cfg["head_dim"]
inter = cfg["intermediate_size"]
rope_theta = cfg.get("rope_theta", 1000000.0)
eps = cfg.get("rms_norm_eps", 1e-6)
device = "cuda" if torch.cuda.is_available() else "cpu"
dtype = torch.float32 # match GGML f32 compute
# Move weights to device
for k in weights:
weights[k] = weights[k].to(device=device, dtype=dtype)
tokens = torch.tensor([prompt_tokens], dtype=torch.long, device=device)
S = tokens.shape[1]
# Embedding
hidden = weights["model.embed_tokens.weight"][tokens[0]] # [S, H]
# Positions
positions = torch.arange(S, device=device)
# Precompute RoPE freqs
freqs = 1.0 / (rope_theta ** (torch.arange(0, head_dim, 2, device=device, dtype=torch.float32) / head_dim))
t = positions.float()
freqs = torch.outer(t, freqs) # [S, D/2]
cos_f = torch.cos(freqs)
sin_f = torch.sin(freqs)
def rms_norm(x, w):
rms = torch.sqrt(torch.mean(x ** 2, dim=-1, keepdim=True) + eps)
return (x / rms) * w
def apply_rope(x, cos_f, sin_f):
# x: [S, Nh, D] -> NEOX layout
D = x.shape[-1]
x1 = x[..., :D//2]
x2 = x[..., D//2:]
# Broadcast cos/sin: [S, 1, D/2]
c = cos_f.unsqueeze(1)
s = sin_f.unsqueeze(1)
return torch.cat([x1 * c - x2 * s, x2 * c + x1 * s], dim=-1)
# Causal mask
mask = torch.triu(torch.full((S, S), float('-inf'), device=device), diagonal=1)
for l in range(n_layers):
prefix = f"model.layers.{l}"
# Pre-attn norm
normed = rms_norm(hidden, weights[f"{prefix}.input_layernorm.weight"])
# QKV
q = normed @ weights[f"{prefix}.self_attn.q_proj.weight"].T # [S, Nh*D]
k = normed @ weights[f"{prefix}.self_attn.k_proj.weight"].T # [S, Nkv*D]
v = normed @ weights[f"{prefix}.self_attn.v_proj.weight"].T # [S, Nkv*D]
q = q.view(S, n_heads, head_dim)
k = k.view(S, n_kv_heads, head_dim)
v = v.view(S, n_kv_heads, head_dim)
# QK-norm
q = rms_norm(q, weights[f"{prefix}.self_attn.q_norm.weight"])
k = rms_norm(k, weights[f"{prefix}.self_attn.k_norm.weight"])
# RoPE
q = apply_rope(q, cos_f, sin_f)
k = apply_rope(k, cos_f, sin_f)
# GQA: expand KV heads
rep = n_heads // n_kv_heads
if rep > 1:
k = k.unsqueeze(2).expand(-1, -1, rep, -1).reshape(S, n_heads, head_dim)
v = v.unsqueeze(2).expand(-1, -1, rep, -1).reshape(S, n_heads, head_dim)
# Attention: [S, Nh, D] -> [Nh, S, D]
q = q.transpose(0, 1)
k = k.transpose(0, 1)
v = v.transpose(0, 1)
scale = 1.0 / (head_dim ** 0.5)
attn_w = torch.matmul(q, k.transpose(-1, -2)) * scale + mask
attn_w = torch.softmax(attn_w, dim=-1)
attn_out = torch.matmul(attn_w, v) # [Nh, S, D]
attn_out = attn_out.transpose(0, 1).reshape(S, n_heads * head_dim) # [S, Nh*D]
# O proj
attn_out = attn_out @ weights[f"{prefix}.self_attn.o_proj.weight"].T
# Residual
hidden = hidden + attn_out
# Post-attn norm + MLP
normed = rms_norm(hidden, weights[f"{prefix}.post_attention_layernorm.weight"])
gate = normed @ weights[f"{prefix}.mlp.gate_proj.weight"].T
up = normed @ weights[f"{prefix}.mlp.up_proj.weight"].T
mlp_out = (torch.nn.functional.silu(gate) * up)
mlp_out = mlp_out @ weights[f"{prefix}.mlp.down_proj.weight"].T
hidden = hidden + mlp_out
# Final norm
hidden = rms_norm(hidden, weights["model.norm.weight"])
# Logits (last token)
logits = hidden[-1] @ weights["model.embed_tokens.weight"].T # [V]
return logits.cpu().numpy()
def main():
if len(sys.argv) < 4:
print("Usage: debug-lm-logits.py <model_dir> <ggml_logits.bin> <tokens.csv>")
print(" 1) ace-lm --dump-logits logits.bin --dump-tokens tokens.csv ...")
print(" 2) python3 tests/debug-lm-logits.py checkpoints/acestep-5Hz-lm-0.6B logits.bin tokens.csv")
return
model_dir = sys.argv[1]
ggml_logits_path = sys.argv[2]
tokens_path = sys.argv[3]
with open(tokens_path, 'r') as f:
prompt_tokens = [int(x) for x in f.read().strip().split(',')]
print(f"[Test] Prompt: {len(prompt_tokens)} tokens, first 10: {prompt_tokens[:10]}")
# PyTorch reference
pt_logits = test_pytorch_logits(model_dir, prompt_tokens)
print(f"[Python] logits: min={pt_logits.min():.4f} max={pt_logits.max():.4f}")
print(f"[Python] argmax: {pt_logits.argmax()} (val={pt_logits.max():.4f})")
print(f"[Python] top5: {np.argsort(pt_logits)[-5:][::-1]}")
# GGML logits
if ggml_logits_path and os.path.exists(ggml_logits_path):
with open(ggml_logits_path, 'rb') as f:
ggml_logits = np.frombuffer(f.read(), dtype=np.float32)
print(f"[GGML] logits: min={ggml_logits.min():.4f} max={ggml_logits.max():.4f}")
print(f"[GGML] argmax: {ggml_logits.argmax()} (val={ggml_logits.max():.4f})")
print(f"[GGML] top5: {np.argsort(ggml_logits)[-5:][::-1]}")
# Cosine similarity
dot = np.dot(pt_logits, ggml_logits)
norm_pt = np.linalg.norm(pt_logits)
norm_gg = np.linalg.norm(ggml_logits)
cos = dot / (norm_pt * norm_gg + 1e-12)
print(f"[Test] Cosine similarity Python<>GGML: {cos:.6f}")
# Top-k agreement
pt_top10 = set(np.argsort(pt_logits)[-10:])
gg_top10 = set(np.argsort(ggml_logits)[-10:])
print(f"[Test] Top-10 overlap: {len(pt_top10 & gg_top10)}/10")
if __name__ == "__main__":
main()
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
cp ../examples/simple.json .
cp ../examples/partial.json .
cp ../examples/full.json .
../build/ace-lm --request simple.json \
--lm ../models/acestep-5Hz-lm-4B-Q8_0.gguf \
--dump-logits logits.bin --dump-tokens tokens.csv
python3 debug-lm-logits.py ../checkpoints/acestep-5Hz-lm-4B logits.bin tokens.csv
../build/ace-lm --request partial.json \
--lm ../models/acestep-5Hz-lm-4B-Q8_0.gguf \
--dump-logits logits.bin --dump-tokens tokens.csv
python3 debug-lm-logits.py ../checkpoints/acestep-5Hz-lm-4B logits.bin tokens.csv
../build/ace-lm --request full.json \
--lm ../models/acestep-5Hz-lm-4B-Q8_0.gguf \
--dump-logits logits.bin --dump-tokens tokens.csv
python3 debug-lm-logits.py ../checkpoints/acestep-5Hz-lm-4B logits.bin tokens.csv
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""Compare C++ vs Python FSQ tokenizer, code by code.
Runs ace-understand --dump to get C++ VAE latents + FSQ codes,
then runs the Python tokenizer on the same latents and compares.
Run from tests/ directory:
./debug-tok-cossim.py # turbo, 1s sine
./debug-tok-cossim.py --mode sft # SFT
./debug-tok-cossim.py --duration 5 # 5s test audio
./debug-tok-cossim.py --wav input.wav # custom WAV
"""
import sys, os, subprocess, argparse, struct, shutil, math
import numpy as np
FSQ_LEVELS = [8, 8, 8, 5, 5, 5]
ACE_BIN = "../build/ace-understand"
VAE_GGUF = "../models/vae-BF16.gguf"
MODE_CONFIG = {
"turbo": {
"dit_gguf": "../models/acestep-v15-turbo-BF16.gguf",
"config_path": "acestep-v15-turbo",
},
"sft": {
"dit_gguf": "../models/acestep-v15-sft-BF16.gguf",
"config_path": "acestep-v15-sft",
},
}
def generate_test_wav(path, duration=1.0, sr=48000):
"""Generate a short stereo WAV (440Hz sine) for testing."""
ns = int(sr * duration)
t = np.arange(ns, dtype=np.float64) / sr
mono = (np.sin(2 * math.pi * 440 * t) * 16000).astype(np.int16)
nch = 2
data = np.column_stack([mono, mono]).tobytes()
with open(path, 'wb') as f:
f.write(b'RIFF')
f.write(struct.pack('<I', 36 + len(data)))
f.write(b'WAVEfmt ')
f.write(struct.pack('<IHHIIHH', 16, 1, nch, sr, sr * nch * 2, nch * 2, 16))
f.write(b'data')
f.write(struct.pack('<I', len(data)))
f.write(data)
def load_dump(path):
"""Load debug.h format: [ndim:i32] [shape:i32*ndim] [data:f32*numel]."""
raw = np.fromfile(path, dtype=np.float32)
ndim = struct.unpack('i', struct.pack('f', raw[0]))[0]
shape = [struct.unpack('i', struct.pack('f', raw[1 + i]))[0] for i in range(ndim)]
data = raw[1 + ndim:]
return data.reshape(shape)
def fsq_decode_index(index):
dims = []
for L in FSQ_LEVELS:
dims.append(index % L)
index //= L
return dims
def run_cpp(wav_path, dit_gguf, dump_dir):
"""Run ace-understand --dump (tok-only, no LM). Stderr goes to terminal."""
if os.path.isdir(dump_dir):
shutil.rmtree(dump_dir)
os.makedirs(dump_dir)
cmd = [ACE_BIN,
"--src-audio", wav_path,
"--dit", dit_gguf,
"--vae", VAE_GGUF,
"--dump", dump_dir]
print("[GGML] Running ace-understand --dump...")
r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=None, text=True)
lat_path = os.path.join(dump_dir, "tok_latents.bin")
cod_path = os.path.join(dump_dir, "tok_codes.bin")
if r.returncode != 0 or not os.path.isfile(lat_path):
print(f"[GGML] FAILED (exit {r.returncode})")
return None, None
latents = load_dump(lat_path)
codes = np.fromfile(cod_path, dtype=np.int32)
print(f"[GGML] Done, {latents.shape[0]} latent frames -> {len(codes)} codes")
return latents, codes
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--mode", default="turbo", choices=["turbo", "sft"])
parser.add_argument("--duration", type=float, default=1.0, help="Test audio duration (seconds)")
parser.add_argument("--wav", type=str, default=None, help="Custom WAV file instead of generated")
args = parser.parse_args()
cfg = MODE_CONFIG[args.mode]
wav_path = args.wav
if not wav_path:
wav_path = "tok-test-input.wav"
generate_test_wav(wav_path, args.duration)
print(f"[Input] Generated {args.duration:.1f}s 440Hz stereo WAV")
# Step 1: C++ (ace-understand --dump)
dump_dir = "tok-dump"
latents, cpp_codes = run_cpp(wav_path, cfg["dit_gguf"], dump_dir)
if latents is None:
return 1
# Step 2: Python tokenizer on the same latents
print("[Python] Loading model...")
import torch
sys.path.insert(0, '../../ACE-Step-1.5')
from acestep.handler import AceStepHandler
from einops import rearrange
handler = AceStepHandler()
handler.initialize_service(
project_root="..",
config_path=cfg["config_path"],
device='cpu',
)
tokenizer = handler.model.tokenizer.float()
T_25Hz = latents.shape[0]
pad = (5 - (T_25Hz % 5)) % 5
lat_np = latents
if pad > 0:
sl_bin = os.path.join("..", "checkpoints", cfg["config_path"], "silence_latent.bin")
silence = np.fromfile(sl_bin, dtype=np.float32).reshape(-1, 64)
lat_np = np.concatenate([lat_np, silence[:pad]], axis=0)
lat_t = torch.tensor(lat_np, dtype=torch.float32).unsqueeze(0)
x = rearrange(lat_t, 'n (t_patch p) d -> n t_patch p d', p=5)
with torch.no_grad():
_, indices = tokenizer(x)
py_codes = indices.squeeze().cpu().numpy().flatten()
print(f"[Python] {len(py_codes)} codes")
# Step 3: Compare
n = min(len(cpp_codes), len(py_codes))
matches = sum(1 for i in range(n) if cpp_codes[i] == py_codes[i])
pct = 100.0 * matches / n if n > 0 else 0
print(f"[Compare] GGML vs Python ({n} codes)")
print(f"match: {matches}/{n} ({pct:.1f}%)")
mismatches = [(i, int(cpp_codes[i]), int(py_codes[i]))
for i in range(n) if cpp_codes[i] != py_codes[i]]
if mismatches:
off_by_one = 0
for _, c, p in mismatches:
cd, pd = fsq_decode_index(c), fsq_decode_index(p)
diffs = [abs(cd[j] - pd[j]) for j in range(6)]
if sum(1 for d in diffs if d != 0) == 1 and max(diffs) == 1:
off_by_one += 1
print(f"off-by-1 in 1 dim: {off_by_one}/{len(mismatches)}")
for i, c, p in mismatches[:5]:
cd, pd = fsq_decode_index(c), fsq_decode_index(p)
diff_dims = [j for j in range(6) if cd[j] != pd[j]]
print(f"code[{i}]: GGML={c} Python={p} dims={diff_dims}")
print(f"[Summary]")
if pct == 100:
print(f"PASS: all {n} codes match")
elif pct >= 80:
print(f"WARN: {pct:.0f}% match (precision diffs at FSQ boundaries)")
else:
print(f"FAIL: {pct:.0f}% match")
if not args.wav:
os.remove(wav_path)
return 0 if pct == 100 else 1
if __name__ == '__main__':
sys.exit(main())
+19
View File
@@ -0,0 +1,19 @@
{
"caption": "An upbeat and anthemic pop-rock track driven by bright, slightly overdriven",
"lyrics": "# Lyric\n[Intro - Guitar Riff]\n[Verse 1]\nDans le monde des tutos virtuels\nG ta toise en nouvelle passion\nAvec Ggendoline et Pumbé à midi\nLa communauté, c'est l'unité\nQuel joie, une clé\n\n[Chorus]\nDans le monde des tutos virtuels\nGândoline et Pumbé à midi\nUne famille à connecter, c'est vrai\nD'un enfant qui voit toi fusionner\n\n[Guitar Solo]\n\n[Verse 2]\nDans le monde des tutos virtuels\nGândoline, Pumbé à midi\nUne famille à connecter, c'est vrai\nD'un enfant qui voit toi fusionner",
"bpm": 83,
"duration": 88.0,
"keyscale": "G major",
"timesignature": "4",
"vocal_language": "fr",
"seed": 158961132,
"thinking": true,
"lm_temperature": 0.85,
"lm_cfg_scale": 2.0,
"lm_top_p": 0.90,
"lm_negative_prompt": "NO USER INPUT",
"inference_steps": 8,
"guidance_scale": 1.0,
"shift": 3.0,
"audio_codes": "43316,18426,13366,59455,17783,49303,7423,29855,37158,37157,62317,61455,12847,19583,57031,34656,20254,10770,11416,15905,31413,23339,47091,12198,49531,37355,33090,38645,40707,16324,61436,46095,13941,5287,2239,13975,63815,2757,4862,13571,63495,39,29887,49426,12696,50847,40498,61056,25666,12989,23987,54763,25485,31683,28554,25355,16373,28995,2351,1655,7940,55831,34359,15350,15277,11717,20476,52239,5015,19807,24087,3559,20471,34193,32552,60999,29360,25338,38873,16768,17912,27584,24008,1528,449,25563,52684,53223,42183,37215,12343,39431,26055,28148,57286,38382,28863,7191,58397,18991,7695,30716,36784,12687,8707,25649,33718,3202,23035,10747,26354,63965,16260,11223,45679,14343,8679,49351,52927,2535,19207,46447,49615,12694,21110,46597,60991,27711,49751,54656,30448,33125,13585,29256,5161,42434,11753,39402,60354,21953,39532,14282,52160,34248,16304,4671,14172,5127,25991,15343,8583,61902,16328,31700,48415,28879,11215,52715,25541,11203,7695,63951,33803,40453,17750,28006,8231,40464,3136,51006,23839,18711,18711,18711,18711,3343,3279,2823,16071,3271,2823,2319,55815,40260,16215,12047,16631,26927,21863,20060,10166,51070,39,12099,63440,18418,25271,10792,2128,44166,53750,41263,44247,61287,42303,27614,21997,24879,38799,12648,38341,36833,19408,11769,2979,63979,44239,25559,27591,17626,44087,33796,4901,53176,57399,37180,38024,9216,63485,2005,13656,15914,45576,29194,45624,62332,53237,63988,40332,20486,31367,10951,46207,22231,63479,38877,17262,49335,42045,57388,49679,39382,53712,9111,33811,59560,25603,14787,53495,2871,12159,39374,11007,23173,35702,8879,12383,4719,48237,14980,63909,43475,42213,767,44183,55791,53759,27141,26278,43838,13250,10152,62214,15001,17496,16000,31992,8057,53552,56305,40256,53577,26952,38191,1075,824,37948,27071,12637,20327,61907,8327,2807,34015,6102,43413,10839,25862,53122,8082,21280,6327,35456,7776,34112,8384,62816,53456,60868,54280,59240,14390,17664,9917,10400,2174,5127,23845,33871,9463,14255,13519,16079,44037,4887,15894,20031,17264,55256,8687,52775,30280,15384,52141,52943,27455,22279,25903,15868,40911,38383,38535,54734,20019,12769,47552,29096,20458,61890,36838,23983,3311,59887,17590,4639,56839,32205,22842,41671,24968,34816,25147,32206,6134,2376,29886,47107,38434,62506,14276,28490,39372,2511,8238,10687,13487,27735,54975,37335,2463,30307,30471,2367,7695,34375,62931,33104,6183,14856,25296,44854,5582,48047,24319,62967,12098,25355,52525,51140,51335,3311,62839,62782,25839,32303,37495,48727,12599,319,34063,27863,19974,35335,8726,53481,2808,17926,16912,19976,23112,12832,13480,20152,25446"
}
+34
View File
@@ -0,0 +1,34 @@
// test-philox.cpp
// Dump Philox noise to binary file for comparison with PyTorch CUDA.
// Build: make (see Makefile)
// Usage: ./test-philox [seed] [count] [output.f32]
#include "philox.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
int main(int argc, char ** argv) {
if (argc > 1 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) {
fprintf(stderr, "usage: %s [seed=42] [count=48000] [output=philox-noise.f32]\n", argv[0]);
return 0;
}
int64_t seed = argc > 1 ? atoll(argv[1]) : 42;
int count = argc > 2 ? atoi(argv[2]) : 48000;
const char * path = argc > 3 ? argv[3] : "philox-noise.f32";
float * out = new float[count];
philox_randn(seed, out, count, true);
FILE * f = fopen(path, "wb");
if (!f) {
fprintf(stderr, "cannot open %s\n", path);
return 1;
}
fwrite(out, sizeof(float), count, f);
fclose(f);
delete[] out;
fprintf(stderr, "wrote %d floats (seed=%lld) to %s\n", count, (long long) seed, path);
return 0;
}
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Verify C++ Philox matches PyTorch CUDA torch.randn(dtype=bf16).
Run from tests/ directory:
./test-philox.py
"""
import subprocess, sys, os, random
import numpy as np
COUNT = 64 * 25 * 120 # 64ch * 25Hz * 120s = 192000 (2 minutes, max duration)
def build():
if not os.path.isfile("test-philox.cpp"):
print("ERROR: test-philox.cpp not found (run from tests/ directory)")
sys.exit(1)
r = subprocess.run(["make", "test-philox"], capture_output=True, text=True)
if r.returncode != 0:
print(f"Build failed:\n{r.stderr}")
sys.exit(1)
def compare(seed):
import torch
if not torch.cuda.is_available():
print("ERROR: CUDA required")
sys.exit(1)
subprocess.run(["./test-philox", str(seed), str(COUNT), "philox-noise.f32"],
capture_output=True)
cpp = np.fromfile("philox-noise.f32", dtype=np.float32)
gen = torch.Generator(device="cuda").manual_seed(seed)
py = torch.randn([COUNT], generator=gen, device="cuda", dtype=torch.bfloat16)
py = py.float().cpu().numpy()
n = min(len(cpp), len(py))
exact = int(np.sum(cpp[:n] == py[:n]))
diff = np.abs(cpp[:n] - py[:n])
d = np.linalg.norm(cpp[:n]) * np.linalg.norm(py[:n])
cos = float(np.dot(cpp[:n], py[:n]) / d) if d > 0 else 0.0
diffs = n - exact
status = "PERFECT" if exact == n else "OK" if cos > 0.9999 else "FAIL"
print(f" seed={seed:<12d} {exact}/{n} ({100*exact/n:.2f}%) "
f"cos={cos:.8f} max_diff={diff.max():.8f} diffs={diffs} {status}")
return status != "FAIL"
def main():
os.chdir(os.path.dirname(os.path.abspath(__file__)))
build()
print(f"Philox4x32-10 vs PyTorch CUDA bf16 | {COUNT} floats "
f"({COUNT//64}frames, {COUNT//64//25}s @ 25Hz, 64ch)")
print(f"Press Enter for random seed, type a number for specific seed, q to quit.\n")
ok = True
while True:
try:
line = input("> ").strip()
except (EOFError, KeyboardInterrupt):
print()
break
if line in ("q", "quit", "exit"):
break
seed = int(line) if line.lstrip('-').isdigit() else random.randint(0, 2**63 - 1)
if not compare(seed):
ok = False
sys.exit(0 if ok else 1)
if __name__ == "__main__":
main()