201 lines
7.7 KiB
Python
201 lines
7.7 KiB
Python
#!/usr/bin/env python
|
|
"""dump_bs_roformer_goldens.py — PyTorch reference activations for the GGML port.
|
|
|
|
Runs ZFTurbo's no-STFT BS-RoFormer under torch.no_grad at a SHORT time length
|
|
and dumps intermediate tensors, so engine/src/bs-roformer-ggml.h can be
|
|
validated stage by stage instead of only at the output.
|
|
|
|
MEMORY
|
|
------
|
|
Deliberately runs at T=256 (~3 s of audio) rather than the model's trained
|
|
T=1722. A no_grad forward frees activations as it goes, so peak is bounded by
|
|
the largest few tensors (~23 MB each at T=256) plus the 268 MB of weights.
|
|
Numerics do not depend on sequence length, so a short T validates the graph
|
|
just as well. Do NOT raise --time-steps to 1722 "to be thorough" — that is not
|
|
what broke before (tracing was), but there is no reason to pay for it either.
|
|
|
|
WHAT IT DUMPS
|
|
-------------
|
|
Full tensors (for elementwise comparison):
|
|
input [1, T, F*C*2]
|
|
band_split [1, T, n_bands, dim]
|
|
layer_00 [1, T, n_bands, dim] after layers[0] (time + freq)
|
|
layer_01 [1, T, n_bands, dim]
|
|
layer_last [1, T, n_bands, dim]
|
|
final_norm [1, T, n_bands, dim]
|
|
mask [1, S, F*C, T, 2] the graph's actual output
|
|
|
|
Per-layer summary stats for all `depth` layers (mean/std/absmax), enough to
|
|
localise which layer a divergence starts in without storing 16 full tensors.
|
|
|
|
USAGE
|
|
-----
|
|
$py = "d:\\Ace-Step-Latest\\hot-step-9000\\.venv\\Scripts\\python.exe"
|
|
& $py tools\\dump_bs_roformer_goldens.py `
|
|
--config models\\supersep-ckpt\\Xe\\leap_xe_config_voc.yaml `
|
|
--ckpt models\\supersep-ckpt\\Xe\\bs_leap_xe_voc.ckpt `
|
|
--output models\\supersep-ckpt\\goldens_voc.npz
|
|
"""
|
|
|
|
import argparse
|
|
import importlib.util
|
|
import os
|
|
import sys
|
|
|
|
import numpy as np
|
|
import torch
|
|
import yaml
|
|
|
|
DEFAULT_MSS_REPO = os.path.normpath(os.path.join(
|
|
os.path.dirname(os.path.abspath(__file__)), "..", "..", "MSS_ONNX_TensorRT"))
|
|
|
|
|
|
def load_no_stft_class(mss_repo, arch):
|
|
"""Import the amputated model class for `arch` ('bs' or 'mel')."""
|
|
fname, cls = (("bs_roformer_no_stft.py", "BSRoformer") if arch == "bs"
|
|
else ("mel_band_roformer_no_stft.py", "MelBandRoformer"))
|
|
mod_path = os.path.join(mss_repo, "models_without_stft", fname)
|
|
if not os.path.isfile(mod_path):
|
|
raise SystemExit(f"Could not find {mod_path}\nPass --mss-repo <checkout>.")
|
|
if mss_repo not in sys.path:
|
|
sys.path.insert(0, mss_repo)
|
|
spec = importlib.util.spec_from_file_location(fname[:-3], mod_path)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return getattr(mod, cls)
|
|
|
|
|
|
def read_config(path):
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
return yaml.load(f, Loader=yaml.UnsafeLoader)
|
|
|
|
|
|
def build_model(BSRoformer, cfg, ckpt_path):
|
|
kwargs = dict(cfg["model"])
|
|
kwargs["flash_attn"] = False # match what the GGML graph computes
|
|
kwargs["use_torch_checkpoint"] = False
|
|
model = BSRoformer(**kwargs)
|
|
|
|
sd = torch.load(ckpt_path, map_location="cpu", weights_only=False)
|
|
for k in ("state_dict", "model", "model_state_dict"):
|
|
if isinstance(sd, dict) and k in sd and isinstance(sd[k], dict):
|
|
sd = sd[k]
|
|
break
|
|
if sd and all(k.startswith("model.") for k in sd):
|
|
sd = {k[len("model."):]: v for k, v in sd.items()}
|
|
|
|
missing, unexpected = model.load_state_dict(sd, strict=False)
|
|
if missing:
|
|
raise SystemExit(f"missing weights: {missing[:6]}")
|
|
if unexpected:
|
|
print(f"[warn] unexpected: {unexpected[:6]}")
|
|
model.eval()
|
|
return model
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(
|
|
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("--config", required=True)
|
|
ap.add_argument("--ckpt", required=True)
|
|
ap.add_argument("--output", required=True)
|
|
ap.add_argument("--mss-repo", default=DEFAULT_MSS_REPO)
|
|
ap.add_argument("--time-steps", type=int, default=256,
|
|
help="T to run at (default 256; see MEMORY in the docstring)")
|
|
ap.add_argument("--seed", type=int, default=1234)
|
|
args = ap.parse_args()
|
|
|
|
cfg = read_config(args.config)
|
|
mc = cfg["model"]
|
|
arch = "bs" if "freqs_per_bands" in mc else "mel"
|
|
ModelCls = load_no_stft_class(os.path.abspath(args.mss_repo), arch)
|
|
model = build_model(ModelCls, cfg, args.ckpt)
|
|
|
|
n_ch = 2 if mc.get("stereo", True) else 1
|
|
depth = mc["depth"]
|
|
T = args.time_steps
|
|
|
|
torch.manual_seed(args.seed)
|
|
# Scaled to roughly the magnitude of a real STFT bin so activations land in
|
|
# a representative range rather than an artificially tiny one.
|
|
if arch == "bs":
|
|
# BSRoformer.forward takes the flattened [b, t, (f c)] directly.
|
|
in_dim = (mc["stft_n_fft"] // 2 + 1) * n_ch * 2
|
|
x = torch.randn(1, T, in_dim) * 0.05
|
|
model_in = x
|
|
else:
|
|
# MelBandRoformer.forward takes [b, f, t, c] and flattens it itself,
|
|
# where f is the GATHERED index count (bands overlap), so derive it
|
|
# from the model's own freq_indices buffer rather than from n_fft.
|
|
n_gathered = int(model.freq_indices.numel())
|
|
in_dim = n_gathered * 2
|
|
model_in = torch.randn(1, n_gathered, T, 2) * 0.05
|
|
# Flattened view is what the C++ side feeds: [t, (f c)]
|
|
x = model_in.permute(0, 2, 1, 3).reshape(1, T, in_dim).contiguous()
|
|
print(f"[info] arch={arch} in_dim={in_dim}")
|
|
|
|
out = {}
|
|
stats = []
|
|
|
|
def store(name, t):
|
|
out[name] = t.detach().to(torch.float32).contiguous().numpy()
|
|
|
|
# Forward hooks capture intermediates without touching the model source.
|
|
captured = {}
|
|
|
|
def hook(tag):
|
|
def fn(_mod, _inp, output):
|
|
captured[tag] = output.detach()
|
|
return fn
|
|
|
|
handles = [model.band_split.register_forward_hook(hook("band_split"))]
|
|
# Mel-Band Karaoke has norm_output=True on each Transformer and NO
|
|
# final_norm; hooking the last layer stands in for it there.
|
|
has_final_norm = not isinstance(model.final_norm, torch.nn.Identity) \
|
|
if hasattr(model, "final_norm") else False
|
|
if has_final_norm:
|
|
handles.append(model.final_norm.register_forward_hook(hook("final_norm")))
|
|
for i, block in enumerate(model.layers):
|
|
# block is ModuleList([time_transformer, freq_transformer]); hooking the
|
|
# freq transformer captures the state after the full layer.
|
|
handles.append(block[-1].register_forward_hook(hook(f"L{i}")))
|
|
|
|
print(f"[info] running T={T}, in_dim={in_dim}, depth={depth}")
|
|
with torch.no_grad():
|
|
mask = model(model_in)
|
|
|
|
for h in handles:
|
|
h.remove()
|
|
|
|
store("input", x)
|
|
store("mask", mask)
|
|
store("band_split", captured["band_split"])
|
|
# With no final_norm the C++ stage depth+1 is a no-op passthrough, so the
|
|
# last layer's output is exactly what it should produce.
|
|
store("final_norm", captured["final_norm"] if has_final_norm
|
|
else captured[f"L{depth - 1}"])
|
|
store("layer_00", captured["L0"])
|
|
store("layer_01", captured["L1"])
|
|
store("layer_last", captured[f"L{depth - 1}"])
|
|
|
|
for i in range(depth):
|
|
t = captured[f"L{i}"].float()
|
|
stats.append([t.mean().item(), t.std().item(), t.abs().max().item()])
|
|
out["layer_stats"] = np.asarray(stats, dtype=np.float32) # [depth, 3]
|
|
|
|
out["meta"] = np.asarray([T, in_dim, depth, mask.shape[1], mask.shape[2]],
|
|
dtype=np.int64)
|
|
|
|
os.makedirs(os.path.dirname(os.path.abspath(args.output)) or ".", exist_ok=True)
|
|
np.savez(args.output, **out)
|
|
|
|
print(f"[ok] {args.output} ({os.path.getsize(args.output)/1024/1024:.1f} MB)")
|
|
print(f"[ok] mask shape {tuple(mask.shape)} "
|
|
f"range [{mask.min():.4f}, {mask.max():.4f}]")
|
|
print("[ok] per-layer absmax: " +
|
|
" ".join(f"{s[2]:.2f}" for s in stats))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|