Initial release
This commit is contained in:
@@ -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
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user