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
+293
View File
@@ -0,0 +1,293 @@
#!/usr/bin/env python
"""convert-bs-roformer-gguf.py — BS-RoFormer .ckpt → GGUF for the HOT-Step engine.
A pure state-dict repack: reads the checkpoint tensor by tensor and writes GGUF.
There is NO graph tracing, so peak memory is roughly the size of the checkpoint
(~270 MB for Leap Xe) rather than the tens of gigabytes that torch.onnx.export
needs to hold a fully unrolled 16-layer axial transformer at T=1722.
Consumed by engine/src/bs-roformer-ggml.h.
TENSOR NAMING
-------------
Flat and index-addressable so the C++ side can build names with snprintf:
band_split.{b}.norm [dim_in] RMSNorm gamma
band_split.{b}.w [dim_in, dim] Linear weight
band_split.{b}.b [dim] Linear bias
blk.{i}.{time|freq}.rope_freqs [dim_head/2] rotary inv-freqs
blk.{i}.{time|freq}.attn_norm [dim]
blk.{i}.{time|freq}.qkv [dim, 3*dim_inner] fused, no bias
blk.{i}.{time|freq}.gates_w [dim, heads]
blk.{i}.{time|freq}.gates_b [heads]
blk.{i}.{time|freq}.out [dim_inner, dim] no bias
blk.{i}.{time|freq}.ff_norm [dim]
blk.{i}.{time|freq}.ff1_w [dim, ff_inner]
blk.{i}.{time|freq}.ff1_b [ff_inner]
blk.{i}.{time|freq}.ff2_w [ff_inner, dim]
blk.{i}.{time|freq}.ff2_b [dim]
final_norm [dim]
mask.{s}.{b}.w1 [dim, ff_inner] Linear
mask.{s}.{b}.b1 [ff_inner]
mask.{s}.{b}.w2 [ff_inner, dim_in*2] Linear (GLU halves it)
mask.{s}.{b}.b2 [dim_in*2]
Shapes above are in GGML order (ne[0] innermost). A torch Linear weight is
(out, in); gguf reverses numpy dims on write, so passing it through unchanged
yields ne = [in, out], which is what ggml_mul_mat wants.
Note `band_split[b]` input width is freqs_per_bands[b] * 2 (stereo) * 2
(complex) — the yaml lists frequency bins, not feature width.
USAGE
-----
$py = "d:\\Ace-Step-Latest\\hot-step-9000\\.venv\\Scripts\\python.exe"
& $py tools\\convert-bs-roformer-gguf.py `
--config models\\supersep-ckpt\\Xe\\leap_xe_config_voc.yaml `
--ckpt models\\supersep-ckpt\\Xe\\bs_leap_xe_voc.ckpt `
--output models\\supersep\\bs_leap_xe_voc-F32.gguf
"""
import argparse
import re
import os
import numpy as np
import torch
import yaml
import gguf
ARCH = "bs-roformer"
def read_config(path):
with open(path, "r", encoding="utf-8") as f:
# freqs_per_bands uses !!python/tuple, so safe_load will not do.
return yaml.load(f, Loader=yaml.UnsafeLoader)
def load_state_dict(path):
sd = torch.load(path, map_location="cpu", weights_only=False)
for key in ("state_dict", "model", "model_state_dict"):
if isinstance(sd, dict) and key in sd and isinstance(sd[key], dict):
sd = sd[key]
break
if sd and all(k.startswith("model.") for k in sd):
sd = {k[len("model."):]: v for k, v in sd.items()}
return sd
class Repacker:
"""Pulls named tensors out of the state dict, tracking what was consumed."""
def __init__(self, sd, writer):
self.sd = sd
self.writer = writer
self.used = set()
self.written = 0
def put(self, dst, src, expect_dims=None):
if src not in self.sd:
raise SystemExit(f"missing tensor in checkpoint: {src}")
t = self.sd[src]
if expect_dims is not None and t.dim() != expect_dims:
raise SystemExit(
f"{src}: expected {expect_dims}-D, got {tuple(t.shape)}")
arr = t.detach().to(torch.float32).contiguous().numpy()
self.writer.add_tensor(dst, arr)
self.used.add(src)
self.written += 1
def report(self):
leftover = sorted(set(self.sd) - self.used)
if leftover:
raise SystemExit(
f"{len(leftover)} checkpoint tensors were not written, e.g. "
f"{leftover[:6]}\nThe converter does not understand this "
"checkpoint layout — refusing to emit a partial model.")
print(f"[ok] wrote {self.written} tensors, none left over")
def convert(cfg, sd, out_path):
mc = cfg["model"]
dim = mc["dim"]
depth = mc["depth"]
heads = mc["heads"]
dim_head = mc["dim_head"]
n_stems = mc.get("num_stems", 1)
n_fft = mc["stft_n_fft"]
hop = mc["stft_hop_length"]
win = mc.get("stft_win_length", n_fft)
stereo = bool(mc.get("stereo", True))
n_ch = 2 if stereo else 1
mlp_mult = mc.get("mlp_expansion_factor", 4)
chunk = cfg["audio"]["chunk_size"]
n_freqs = n_fft // 2 + 1
if "freqs_per_bands" in mc:
# BS-RoFormer: contiguous bands that tile the spectrum exactly.
arch = "bs"
freqs_per_bands = list(mc["freqs_per_bands"])
if sum(freqs_per_bands) != n_freqs:
raise SystemExit(
f"freqs_per_bands sums to {sum(freqs_per_bands)}, expected "
f"{n_freqs} (n_fft//2+1)")
else:
# Mel-Band RoFormer: OVERLAPPING bands from a mel filterbank. The bands
# do not tile — the caller gathers freq_indices before the graph and
# scatters the mask back afterwards (see mel_band_tables.inc), so the
# per-band widths come from how many bins each mel filter touches and
# their sum is the *gathered* length, not n_freqs.
arch = "mel"
from librosa import filters
num_bands = mc["num_bands"]
fb = filters.mel(sr=mc.get("sample_rate", 44100), n_fft=n_fft, n_mels=num_bands)
fb = np.asarray(fb)
# Matches MelBandRoformer.__init__: force the first/last bins on so
# every frequency is covered by at least one band.
fb[0][0] = 1.0
fb[-1, -1] = 1.0
per_band = (fb > 0)
if not per_band.any(axis=0).all():
raise SystemExit("mel filterbank leaves some frequencies uncovered")
freqs_per_bands = per_band.sum(axis=1).tolist()
n_bands = len(freqs_per_bands)
# Feature width per band: bins * channels * 2 (real/imag).
band_widths = [int(f) * n_ch * 2 for f in freqs_per_bands]
dim_inner = heads * dim_head
ff_inner = dim * mlp_mult
target = cfg.get("training", {}).get("target_instrument", "unknown")
print(f"[info] dim={dim} depth={depth} heads={heads} dim_head={dim_head}")
print(f"[info] bands={n_bands} stems={n_stems} target={target}")
print(f"[info] n_fft={n_fft} hop={hop} chunk={chunk} -> T={chunk // hop + 1}")
if mc.get("linear_transformer_depth", 0) != 0:
raise SystemExit("linear_transformer_depth != 0 is not supported")
if mc.get("time_transformer_depth", 1) != 1 or \
mc.get("freq_transformer_depth", 1) != 1:
raise SystemExit("only time/freq_transformer_depth == 1 is supported")
if mc.get("skip_connection", False):
raise SystemExit("skip_connection=True is not supported")
# Structural variations between checkpoints, detected from the state dict
# rather than trusted from the yaml (the Mel-Band Karaoke config claims
# mask_estimator_depth 2 but its MLPs actually have 3 Linears).
has_out_norm = any(re.match(r"layers\.\d+\.\d+\.norm\.", k) for k in sd)
has_final_norm = any(k.startswith("final_norm") for k in sd)
mask_idx = sorted({int(k.split(".")[5]) for k in sd
if k.startswith("mask_estimators.") and k.endswith(".weight")})
mask_layers = len(mask_idx)
if mask_layers == 0:
raise SystemExit("no mask_estimators found in checkpoint")
print(f"[info] arch={arch} mask_layers={mask_layers} (indices {mask_idx}) "
f"out_norm={has_out_norm} final_norm={has_final_norm}")
w = gguf.GGUFWriter(out_path, ARCH)
w.add_string("bs_roformer.arch", arch)
w.add_uint32("bs_roformer.mask_layers", mask_layers)
w.add_bool("bs_roformer.has_out_norm", has_out_norm)
w.add_bool("bs_roformer.has_final_norm", has_final_norm)
w.add_uint32("bs_roformer.dim", dim)
w.add_uint32("bs_roformer.depth", depth)
w.add_uint32("bs_roformer.heads", heads)
w.add_uint32("bs_roformer.dim_head", dim_head)
w.add_uint32("bs_roformer.dim_inner", dim_inner)
w.add_uint32("bs_roformer.ff_inner", ff_inner)
w.add_uint32("bs_roformer.n_bands", n_bands)
w.add_uint32("bs_roformer.n_stems", n_stems)
w.add_uint32("bs_roformer.n_channels", n_ch)
w.add_uint32("bs_roformer.n_fft", n_fft)
w.add_uint32("bs_roformer.hop_length", hop)
w.add_uint32("bs_roformer.win_length", win)
w.add_uint32("bs_roformer.chunk_size", chunk)
w.add_array("bs_roformer.band_widths", band_widths)
w.add_string("bs_roformer.target_instrument", str(target))
r = Repacker(sd, w)
# ── Band split: 90 × [RMSNorm(dim_in) -> Linear(dim_in, dim)] ──────────
for b in range(n_bands):
p = f"band_split.to_features.{b}"
r.put(f"band_split.{b}.norm", f"{p}.0.gamma", 1)
r.put(f"band_split.{b}.w", f"{p}.1.weight", 2)
r.put(f"band_split.{b}.b", f"{p}.1.bias", 1)
# ── Axial transformer body ────────────────────────────────────────────
# layers[i][0] = time transformer, layers[i][1] = freq transformer.
# Each has depth 1, so exactly one (Attention, FeedForward) pair, and
# norm_output is False (no trailing per-Transformer norm — confirmed by
# the checkpoint's tensor count reconciling exactly without one).
for i in range(depth):
for axis_idx, axis in ((0, "time"), (1, "freq")):
src = f"layers.{i}.{axis_idx}.layers.0"
dst = f"blk.{i}.{axis}"
r.put(f"{dst}.rope_freqs", f"{src}.0.rotary_embed.freqs", 1)
r.put(f"{dst}.attn_norm", f"{src}.0.norm.gamma", 1)
r.put(f"{dst}.qkv", f"{src}.0.to_qkv.weight", 2)
r.put(f"{dst}.gates_w", f"{src}.0.to_gates.weight", 2)
r.put(f"{dst}.gates_b", f"{src}.0.to_gates.bias", 1)
r.put(f"{dst}.out", f"{src}.0.to_out.0.weight", 2)
# FeedForward Sequential: 0=RMSNorm 1=Linear 2=GELU 3=Dropout
# 4=Linear 5=Dropout
r.put(f"{dst}.ff_norm", f"{src}.1.net.0.gamma", 1)
r.put(f"{dst}.ff1_w", f"{src}.1.net.1.weight", 2)
r.put(f"{dst}.ff1_b", f"{src}.1.net.1.bias", 1)
r.put(f"{dst}.ff2_w", f"{src}.1.net.4.weight", 2)
r.put(f"{dst}.ff2_b", f"{src}.1.net.4.bias", 1)
if has_out_norm:
r.put(f"{dst}.out_norm", f"layers.{i}.{axis_idx}.norm.gamma", 1)
if has_final_norm:
r.put("final_norm", "final_norm.gamma", 1)
# ── Mask estimators: per stem, per band ───────────────────────────────
# to_freqs[b] = Sequential(MLP, GLU); MLP = Sequential(Linear, Tanh, Linear)
for s in range(n_stems):
for b in range(n_bands):
p = f"mask_estimators.{s}.to_freqs.{b}.0"
d = f"mask.{s}.{b}"
for n, idx in enumerate(mask_idx):
r.put(f"{d}.w{n + 1}", f"{p}.{idx}.weight", 2)
r.put(f"{d}.b{n + 1}", f"{p}.{idx}.bias", 1)
r.report()
w.write_header_to_file()
w.write_kv_data_to_file()
w.write_tensors_to_file()
w.close()
size_mb = os.path.getsize(out_path) / (1024 * 1024)
print(f"[ok] {out_path} ({size_mb:.1f} MB)")
def main():
ap = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--config", required=True, help="Model yaml")
ap.add_argument("--ckpt", required=True, help="Checkpoint .ckpt")
ap.add_argument("--output", required=True, help="Destination .gguf")
args = ap.parse_args()
cfg = read_config(args.config)
sd = load_state_dict(args.ckpt)
print(f"[info] checkpoint: {len(sd)} tensors, "
f"{sum(v.numel() for v in sd.values()):,} params")
os.makedirs(os.path.dirname(os.path.abspath(args.output)) or ".", exist_ok=True)
convert(cfg, sd, args.output)
if __name__ == "__main__":
main()
+248
View File
@@ -0,0 +1,248 @@
#!/usr/bin/env python
"""convert-mdx23c-gguf.py — MDX23C (TFC-TDF v3) .ckpt → GGUF for HOT-Step.
A pure state-dict repack, like convert-bs-roformer-gguf.py: no tracing, peak
memory ~= the checkpoint (~440 MB for DrumSep).
Consumed by engine/src/mdx23c-ggml.h.
ARCHITECTURE (models_without_stft/mdx23c_tfc_tdf_v3_no_stft.py)
---------------------------------------------------------------
A 5-scale conv U-Net over the complex spectrogram, with subband folding:
x = cac2cws(x) fold num_subbands into channels
mix = x
first_conv_out = x = first_conv(x) Conv2d(dim_c -> c, 1x1)
x = x.transpose(-1, -2) [b, c, f, t] -> [b, c, t, f]
for s in scales: x = tfc_tdf(x); skip.append(x); x = downscale(x)
x = bottleneck(x)
for s in scales: x = upscale(x); x = cat(x, skip.pop()); x = tfc_tdf(x)
x = x.transpose(-1, -2)
x = x * first_conv_out artifact reduction
x = final_conv(cat(mix, x))
x = cws2cac(x) unfold subbands
TFC_TDF sub-block (residual):
s = shortcut(x) Conv2d 1x1
x = tfc1(x) norm -> act -> Conv2d 3x3
x = x + tdf(x) norm->act->Linear->norm->act->Linear
x = tfc2(x) norm -> act -> Conv2d 3x3
x = x + s
`norm` is InstanceNorm2d(affine=True) — normalise each (sample, channel) over
its spatial extent, i.e. GroupNorm with n_groups == n_channels, then an affine
weight/bias per channel. `act` is GELU.
TENSOR NAMING / LAYOUT
----------------------
gguf reverses numpy dims on write, so a torch Conv2d weight (OC, IC, KH, KW)
lands as ne = [KW, KH, IC, OC] — exactly ggml_conv_2d's kernel layout. A torch
ConvTranspose2d weight is (IC, OC, KH, KW) and lands as [KW, KH, OC, IC],
which is ggml_conv_transpose_2d_p0's layout. Both pass through unchanged.
first_conv [1,1,dim_c,c]
enc.{s}.blk.{b}.* per TFC_TDF sub-block (see below)
enc.{s}.down_norm_w / _b [c]
enc.{s}.down_conv [sw,sh,c,c+g]
bot.blk.{b}.*
dec.{s}.up_norm_w / _b [c]
dec.{s}.up_conv [sw,sh,c-g,c] (transposed)
dec.{s}.blk.{b}.*
final1 [1,1,c+dim_c,c]
final2 [1,1,c,n_inst*dim_c]
per TFC_TDF sub-block:
tfc1_norm_w/_b, tfc1_conv [3,3,in_c,c]
tdf_n1_w/_b, tdf_l1 [f, f//bn]
tdf_n2_w/_b, tdf_l2 [f//bn, f]
tfc2_norm_w/_b, tfc2_conv [3,3,c,c]
shortcut [1,1,in_c,c]
USAGE
-----
$py = "d:\\Ace-Step-Latest\\hot-step-9000\\.venv\\Scripts\\python.exe"
& $py scripts\\convert-mdx23c-gguf.py `
--config <SuperSep>\\models\\config_drumsep_mdx23c.yaml `
--ckpt <SuperSep>\\models\\MDX23C-DrumSep-aufr33-jarredou.ckpt `
--output models\\supersep\\mdx23c_drumsep-F32.gguf
"""
import argparse
import os
import torch
import yaml
import gguf
ARCH = "mdx23c"
def read_config(path):
with open(path, "r", encoding="utf-8") as f:
return yaml.load(f, Loader=yaml.UnsafeLoader)
def load_state_dict(path):
sd = torch.load(path, map_location="cpu", weights_only=False)
for key in ("state_dict", "model", "model_state_dict"):
if isinstance(sd, dict) and key in sd and isinstance(sd[key], dict):
sd = sd[key]
break
if sd and all(k.startswith("model.") for k in sd):
sd = {k[len("model."):]: v for k, v in sd.items()}
return sd
class Repacker:
def __init__(self, sd, writer):
self.sd, self.writer = sd, writer
self.used, self.written = set(), 0
def put(self, dst, src, expect_dims=None):
if src not in self.sd:
raise SystemExit(f"missing tensor in checkpoint: {src}")
t = self.sd[src]
if expect_dims is not None and t.dim() != expect_dims:
raise SystemExit(f"{src}: expected {expect_dims}-D, got {tuple(t.shape)}")
self.writer.add_tensor(dst, t.detach().to(torch.float32).contiguous().numpy())
self.used.add(src)
self.written += 1
def report(self):
leftover = sorted(set(self.sd) - self.used)
if leftover:
raise SystemExit(
f"{len(leftover)} checkpoint tensors were not written, e.g. "
f"{leftover[:6]}\nRefusing to emit a partial model.")
print(f"[ok] wrote {self.written} tensors, none left over")
def put_tfc_tdf(r, dst, src, n_blocks):
"""One TFC_TDF stack: n_blocks residual sub-blocks.
Sequential indices in the checkpoint:
tfc1 / tfc2 : 0=norm, 1=act, 2=Conv2d
tdf : 0=norm, 1=act, 2=Linear, 3=norm, 4=act, 5=Linear
"""
for b in range(n_blocks):
s = f"{src}.blocks.{b}"
d = f"{dst}.blk.{b}"
r.put(f"{d}.tfc1_norm_w", f"{s}.tfc1.0.weight", 1)
r.put(f"{d}.tfc1_norm_b", f"{s}.tfc1.0.bias", 1)
r.put(f"{d}.tfc1_conv", f"{s}.tfc1.2.weight", 4)
r.put(f"{d}.tdf_n1_w", f"{s}.tdf.0.weight", 1)
r.put(f"{d}.tdf_n1_b", f"{s}.tdf.0.bias", 1)
r.put(f"{d}.tdf_l1", f"{s}.tdf.2.weight", 2)
r.put(f"{d}.tdf_n2_w", f"{s}.tdf.3.weight", 1)
r.put(f"{d}.tdf_n2_b", f"{s}.tdf.3.bias", 1)
r.put(f"{d}.tdf_l2", f"{s}.tdf.5.weight", 2)
r.put(f"{d}.tfc2_norm_w", f"{s}.tfc2.0.weight", 1)
r.put(f"{d}.tfc2_norm_b", f"{s}.tfc2.0.bias", 1)
r.put(f"{d}.tfc2_conv", f"{s}.tfc2.2.weight", 4)
r.put(f"{d}.shortcut", f"{s}.shortcut.weight", 4)
def convert(cfg, sd, out_path):
mc, ac = cfg["model"], cfg["audio"]
n_scales = mc["num_scales"]
n_blocks = mc["num_blocks_per_scale"]
c0 = mc["num_channels"]
growth = mc["growth"]
bn = mc["bottleneck_factor"]
n_subbands = mc["num_subbands"]
scale = list(mc["scale"])
norm_type = mc.get("norm", "InstanceNorm")
act_type = mc.get("act", "gelu")
n_audio_ch = ac["num_channels"]
dim_c = n_subbands * n_audio_ch * 2
dim_f = ac["dim_f"]
n_fft = ac["n_fft"]
hop = ac["hop_length"]
chunk = ac["chunk_size"]
instruments = cfg.get("training", {}).get("instruments", [])
target = cfg.get("training", {}).get("target_instrument", None)
n_inst = 1 if target else len(instruments)
if norm_type != "InstanceNorm":
raise SystemExit(f"only InstanceNorm is supported, got {norm_type}")
if act_type != "gelu":
raise SystemExit(f"only gelu is supported, got {act_type}")
print(f"[info] scales={n_scales} blocks/scale={n_blocks} c={c0} growth={growth}")
print(f"[info] subbands={n_subbands} dim_c={dim_c} dim_f={dim_f} bn={bn}")
print(f"[info] instruments={n_inst} {instruments}")
print(f"[info] n_fft={n_fft} hop={hop} chunk={chunk} scale={scale}")
w = gguf.GGUFWriter(out_path, ARCH)
w.add_uint32("mdx23c.num_scales", n_scales)
w.add_uint32("mdx23c.blocks_per_scale", n_blocks)
w.add_uint32("mdx23c.num_channels", c0)
w.add_uint32("mdx23c.growth", growth)
w.add_uint32("mdx23c.bottleneck_factor", bn)
w.add_uint32("mdx23c.num_subbands", n_subbands)
w.add_uint32("mdx23c.dim_c", dim_c)
w.add_uint32("mdx23c.dim_f", dim_f)
w.add_uint32("mdx23c.n_fft", n_fft)
w.add_uint32("mdx23c.hop_length", hop)
w.add_uint32("mdx23c.chunk_size", chunk)
w.add_uint32("mdx23c.n_instruments", n_inst)
w.add_uint32("mdx23c.n_audio_channels", n_audio_ch)
w.add_array("mdx23c.scale", scale)
w.add_string("mdx23c.instruments", ",".join(instruments))
r = Repacker(sd, w)
r.put("first_conv", "first_conv.weight", 4)
c = c0
for s in range(n_scales):
put_tfc_tdf(r, f"enc.{s}", f"encoder_blocks.{s}.tfc_tdf", n_blocks)
r.put(f"enc.{s}.down_norm_w", f"encoder_blocks.{s}.downscale.conv.0.weight", 1)
r.put(f"enc.{s}.down_norm_b", f"encoder_blocks.{s}.downscale.conv.0.bias", 1)
r.put(f"enc.{s}.down_conv", f"encoder_blocks.{s}.downscale.conv.2.weight", 4)
c += growth
put_tfc_tdf(r, "bot", "bottleneck_block", n_blocks)
for s in range(n_scales):
r.put(f"dec.{s}.up_norm_w", f"decoder_blocks.{s}.upscale.conv.0.weight", 1)
r.put(f"dec.{s}.up_norm_b", f"decoder_blocks.{s}.upscale.conv.0.bias", 1)
r.put(f"dec.{s}.up_conv", f"decoder_blocks.{s}.upscale.conv.2.weight", 4)
put_tfc_tdf(r, f"dec.{s}", f"decoder_blocks.{s}.tfc_tdf", n_blocks)
c -= growth
r.put("final1", "final_conv.0.weight", 4)
r.put("final2", "final_conv.2.weight", 4)
r.report()
w.write_header_to_file()
w.write_kv_data_to_file()
w.write_tensors_to_file()
w.close()
print(f"[ok] {out_path} ({os.path.getsize(out_path)/1024/1024:.1f} MB)")
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)
args = ap.parse_args()
cfg = read_config(args.config)
sd = load_state_dict(args.ckpt)
print(f"[info] checkpoint: {len(sd)} tensors, "
f"{sum(v.numel() for v in sd.values()):,} params")
os.makedirs(os.path.dirname(os.path.abspath(args.output)) or ".", exist_ok=True)
convert(cfg, sd, args.output)
if __name__ == "__main__":
main()
+200
View File
@@ -0,0 +1,200 @@
#!/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()
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python
"""dump_mdx23c_goldens.py — PyTorch reference output for the MDX23C GGML port.
Runs ZFTurbo's no-STFT TFC_TDF_net under torch.no_grad and writes a flat binary
that engine/tools/mdx23c-test.cpp compares against.
magic "MDXG"
int32 T, dim_f, cin, n_inst
f32 input [T * dim_f * cin] torch [b, cin, dim_f, T], t fastest
f32 output [n_inst * cin * dim_f * T]
Memory is bounded: a no_grad forward at the trained T=256 frees activations as
it goes. No tracing anywhere.
The upstream module does `from utils import prefer_target_instrument`, and that
utils.py drags in the whole MSS model zoo (demucs, scnet, omegaconf). We stub
the one function instead — it is three lines — so this needs only torch.
USAGE
$py = "d:\\Ace-Step-Latest\\hot-step-9000\\.venv\\Scripts\\python.exe"
& $py scripts\\dump_mdx23c_goldens.py `
--config <SuperSep>\\models\\config_drumsep_mdx23c.yaml `
--ckpt <SuperSep>\\models\\MDX23C-DrumSep-aufr33-jarredou.ckpt `
--output models\\supersep-ckpt\\goldens_mdx23c.bin
"""
import argparse
import importlib.util
import os
import struct
import sys
import types
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 prefer_target_instrument(config):
"""Mirror of MSS utils.prefer_target_instrument (avoids importing its utils)."""
if getattr(config.training, "target_instrument", None):
return [config.training.target_instrument]
return config.training.instruments
def load_net_class(mss_repo):
mod_path = os.path.join(mss_repo, "models_without_stft",
"mdx23c_tfc_tdf_v3_no_stft.py")
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)
# Stub `utils` so the module's single import does not pull in the zoo.
stub = types.ModuleType("utils")
stub.prefer_target_instrument = prefer_target_instrument
sys.modules.setdefault("utils", stub)
spec = importlib.util.spec_from_file_location("mdx23c_no_stft", mod_path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod.TFC_TDF_net
def main():
ap = argparse.ArgumentParser()
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("--seed", type=int, default=1234)
args = ap.parse_args()
from ml_collections import ConfigDict
with open(args.config, "r", encoding="utf-8") as f:
cfg = ConfigDict(yaml.load(f, Loader=yaml.UnsafeLoader))
Net = load_net_class(os.path.abspath(args.mss_repo))
model = Net(cfg)
sd = torch.load(args.ckpt, 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
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()
dim_f = cfg.audio.dim_f
cin = cfg.audio.num_channels * 2
T = cfg.audio.chunk_size // cfg.audio.hop_length + 1
n_inst = len(prefer_target_instrument(cfg))
print(f"[info] T={T} dim_f={dim_f} cin={cin} instruments={n_inst}")
torch.manual_seed(args.seed)
x = torch.randn(1, cin, dim_f, T) * 0.05
with torch.no_grad():
y = model(x)
print(f"[info] output shape {tuple(y.shape)} range "
f"[{y.min():.4f}, {y.max():.4f}]")
os.makedirs(os.path.dirname(os.path.abspath(args.output)) or ".", exist_ok=True)
with open(args.output, "wb") as f:
f.write(b"MDXG")
f.write(struct.pack("<4i", T, dim_f, cin, n_inst))
f.write(np.ascontiguousarray(x[0], dtype=np.float32).tobytes())
f.write(np.ascontiguousarray(y[0], dtype=np.float32).tobytes())
print(f"[ok] {args.output} ({os.path.getsize(args.output)/1024/1024:.1f} MB)")
if __name__ == "__main__":
main()
+387
View File
@@ -0,0 +1,387 @@
"""Export AceStep DiT decoder from safetensors to ONNX for native TRT.
Self-contained script: doesn't require the 'acestep' package. Loads the model
definition directly from the model directory's modeling file.
The exported ONNX matches HOT-Step's dit-trt.h I/O layout:
- input_latents: [B, T, 192] fp32 (context[128] + xt[64], pre-concatenated)
- enc_hidden: [B, S, 2048] fp32
- t: [B] fp32
- t_r: [B] fp32
- velocity: [B, T, 64] fp32 (output)
Usage:
python scripts/export_dit_onnx.py ^
--model-dir models/acestep-v15-merge-sft-turbo-xl-ta-0.7 ^
--output-dir models/onnx/dit-stream
"""
from __future__ import annotations
import argparse
import json
import importlib.util
import shutil
import sys
import types
from pathlib import Path
import torch
import torch.nn as nn
# ── Traceable Lambda replacement ────────────────────────────────────────────
class _Transpose12(nn.Module):
"""Drop-in for Lambda(lambda x: x.transpose(1, 2))."""
def forward(self, x: torch.Tensor) -> torch.Tensor:
return x.transpose(1, 2)
# ── Export wrapper ──────────────────────────────────────────────────────────
class DiTForTRTExport(nn.Module):
"""Wraps AceStepDiTModel for ONNX export matching dit-trt.h I/O layout.
Takes pre-concatenated input_latents [B, T, 192] and exposes t/t_r
as separate inputs, matching the C++ engine's tensor layout.
"""
def __init__(self, decoder: nn.Module):
super().__init__()
self.decoder = decoder
self._replace_lambdas()
self.decoder.config._attn_implementation = "sdpa"
self._patch_decoder_for_trace()
def _replace_lambdas(self) -> None:
for seq in (self.decoder.proj_in, self.decoder.proj_out):
for i, mod in enumerate(seq):
if type(mod).__name__ == "Lambda":
seq[i] = _Transpose12()
def _patch_decoder_for_trace(self) -> None:
"""Monkey-patch decoder forward for ONNX traceability."""
try:
import transformers.integrations.sdpa_attention as _sdpa_mod
_sdpa_mod.use_gqa_in_sdpa = lambda *args, **kwargs: False
except (ImportError, AttributeError):
pass
decoder = self.decoder
sliding_window = decoder.config.sliding_window
layer_types = decoder.config.layer_types
time_embed_dim = decoder.time_embed.time_proj.out_features // 6
def _patched_time_embed_forward(self_te, t):
t_freq = self_te.timestep_embedding(t, self_te.in_channels)
temb = self_te.linear_1(t_freq.to(t.dtype))
temb = self_te.act1(temb)
temb = self_te.linear_2(temb)
timestep_proj = self_te.time_proj(self_te.act2(temb)).reshape(-1, 6, time_embed_dim)
return temb, timestep_proj
decoder.time_embed.forward = types.MethodType(
_patched_time_embed_forward, decoder.time_embed)
decoder.time_embed_r.forward = types.MethodType(
_patched_time_embed_forward, decoder.time_embed_r)
def _export_forward(
self_dec, hidden_states, timestep, timestep_r,
attention_mask, encoder_hidden_states, encoder_attention_mask,
context_latents, use_cache=None, past_key_values=None,
cache_position=None, position_ids=None, output_attentions=False,
return_hidden_states=None, custom_layers_config=None,
enable_early_exit=False, **flash_attn_kwargs,
):
temb_t, timestep_proj_t = self_dec.time_embed(timestep)
temb_r, timestep_proj_r = self_dec.time_embed_r(timestep - timestep_r)
temb = temb_t + temb_r
timestep_proj = timestep_proj_t + timestep_proj_r
hidden_states = torch.cat([context_latents, hidden_states], dim=-1)
hidden_states = self_dec.proj_in(hidden_states)
encoder_hidden_states = self_dec.condition_embedder(encoder_hidden_states)
seq_len_pat = hidden_states.shape[1]
cache_position = torch.arange(seq_len_pat, device=hidden_states.device)
position_ids = cache_position.unsqueeze(0)
position_embeddings = self_dec.rotary_emb(hidden_states, position_ids)
indices = cache_position
diff = indices.unsqueeze(0) - indices.unsqueeze(1)
sw_mask = torch.where(
torch.abs(diff) <= sliding_window,
torch.zeros(1, device=hidden_states.device, dtype=hidden_states.dtype),
torch.full((1,), torch.finfo(hidden_states.dtype).min,
device=hidden_states.device, dtype=hidden_states.dtype),
)
sw_mask = sw_mask.unsqueeze(0).unsqueeze(0)
for i, layer_module in enumerate(self_dec.layers):
attn_mask = sw_mask if layer_types[i] == "sliding_attention" else None
layer_outputs = layer_module(
hidden_states, position_embeddings, timestep_proj, attn_mask,
position_ids, None, False, False, cache_position,
encoder_hidden_states, None,
)
hidden_states = layer_outputs[0]
shift, scale = (self_dec.scale_shift_table + temb.unsqueeze(1)).chunk(2, dim=1)
hidden_states = (self_dec.norm_out(hidden_states) * (1 + scale) + shift).type_as(hidden_states)
hidden_states = self_dec.proj_out(hidden_states)
return (hidden_states, None)
decoder.forward = types.MethodType(_export_forward, decoder)
def forward(
self,
input_latents: torch.Tensor, # [B, T, 192]
enc_hidden: torch.Tensor, # [B, S, 2048]
t: torch.Tensor, # [B]
t_r: torch.Tensor, # [B]
) -> torch.Tensor:
context_latents = input_latents[..., :128]
hidden_states = input_latents[..., 128:]
outputs = self.decoder(
hidden_states=hidden_states, timestep=t, timestep_r=t_r,
attention_mask=None, encoder_hidden_states=enc_hidden,
encoder_attention_mask=None, context_latents=context_latents,
use_cache=False, past_key_values=None, output_attentions=False,
)
return outputs[0]
# ── Load model from directory ────────────────────────────────────────────────
def _load_module_from_file(name: str, filepath: str):
"""Load a Python module from a file path."""
spec = importlib.util.spec_from_file_location(name, filepath)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod
def load_model(model_dir: str, device: str = "cpu"):
"""Load AceStepConditionGenerationModel from a safetensors directory.
Handles the 'acestep' module dependency by loading the config class
from the DEMON project or from a standalone copy.
"""
model_dir = Path(model_dir).resolve()
# Load config.json
config_path = model_dir / "config.json"
with open(config_path) as f:
config_dict = json.load(f)
# ── Bootstrap the 'acestep' package from DEMON project ──────────────
# The model dir's shim files (configuration_acestep_v15.py, apg_guidance.py)
# import from 'acestep.models.common.*', which maps to DEMON's 'acestep.models.*'.
# We add DEMON to sys.path AND create a 'common' alias pointing to 'models'.
demon_root = Path("d:/Ace-Step-Latest/Demon")
if not demon_root.exists():
raise RuntimeError(
"DEMON project not found at d:/Ace-Step-Latest/Demon — "
"needed for model class definitions")
if str(demon_root) not in sys.path:
sys.path.insert(0, str(demon_root))
print(f"[Export] Using DEMON project for acestep package: {demon_root}")
# Import the real acestep package so its __init__.py runs
import acestep
import acestep.models
# The model-dir shims import from 'acestep.models.common.*' but DEMON
# puts everything directly in 'acestep.models.*'. Create a 'common'
# alias that points to the real 'acestep.models' module.
sys.modules["acestep.models.common"] = sys.modules["acestep.models"]
# Now load the config
from acestep.models.configuration_acestep_v15 import AceStepConfig
config = AceStepConfig(**config_dict)
print(f"[Export] Config: hidden_size={config.hidden_size}, layers={config.num_hidden_layers}, "
f"heads={config.num_attention_heads}, kv_heads={config.num_key_value_heads}")
# Register the config module under its short name too (the modeling file
# tries 'from configuration_acestep_v15 import AceStepConfig' as fallback)
config_shim = types.ModuleType("configuration_acestep_v15")
config_shim.AceStepConfig = AceStepConfig
sys.modules["configuration_acestep_v15"] = config_shim
# Register apg_guidance similarly (modeling file does 'from apg_guidance import ...')
import acestep.models.apg_guidance as _real_apg
sys.modules["apg_guidance"] = _real_apg
# Also alias 'acestep.models.common.apg_guidance' → the real module
sys.modules["acestep.models.common.apg_guidance"] = _real_apg
sys.modules["acestep.models.common.configuration_acestep_v15"] = sys.modules[
"acestep.models.configuration_acestep_v15"]
# Load modeling file
modeling_file = model_dir / "modeling_acestep_v15_xl_base.py"
if not modeling_file.exists():
raise FileNotFoundError(f"Modeling file not found: {modeling_file}")
print(f"[Export] Loading model definition from {modeling_file.name}...")
# Add model_dir to path for local imports
if str(model_dir) not in sys.path:
sys.path.insert(0, str(model_dir))
model_mod = _load_module_from_file("_acestep_modeling", str(modeling_file))
model_cls = model_mod.AceStepConditionGenerationModel
# Create model on CPU (meta device doesn't work with ResidualFSQ)
print(f"[Export] Creating model on CPU (this uses ~10GB RAM temporarily)...")
model = model_cls(config)
# Load safetensors weights
from safetensors.torch import load_file
safetensors_path = model_dir / "model.safetensors"
if not safetensors_path.exists():
index_path = model_dir / "model.safetensors.index.json"
if index_path.exists():
with open(index_path) as f:
index = json.load(f)
weight_files = set(index["weight_map"].values())
state_dict = {}
for wf in sorted(weight_files):
print(f"[Export] Loading shard: {wf}")
shard = load_file(str(model_dir / wf), device="cpu")
state_dict.update(shard)
else:
raise FileNotFoundError(f"No model.safetensors or index file in {model_dir}")
else:
fsize = safetensors_path.stat().st_size / (1 << 20)
print(f"[Export] Loading weights: {safetensors_path.name} ({fsize:.0f} MB)...")
state_dict = load_file(str(safetensors_path), device="cpu")
print(f"[Export] Loading {len(state_dict)} tensors into model...")
missing, unexpected = model.load_state_dict(state_dict, strict=False)
if missing:
print(f"[Export] WARNING: {len(missing)} missing keys (first 5): {missing[:5]}")
if unexpected:
print(f"[Export] WARNING: {len(unexpected)} unexpected keys (first 5): {unexpected[:5]}")
del state_dict # Free RAM
# Only move the decoder to GPU (we don't need encoders/tokenizer for export)
model.decoder = model.decoder.to(device)
model.eval()
return model, config
def export_onnx(
model_dir: str,
output_dir: str,
device: str = "cuda",
batch_size: int = 1,
seq_len: int = 750,
enc_len: int = 200,
opset: int = 17,
):
"""Export DiT decoder to ONNX in FP32."""
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
print(f"[Export] Loading model from {model_dir}...")
model, config = load_model(model_dir, device=device)
# Extract the decoder (DiT model)
decoder = model.decoder
param_count = sum(p.numel() for p in decoder.parameters()) / 1e6
print(f"[Export] Decoder: {config.num_hidden_layers} layers, "
f"hidden={config.hidden_size}, {param_count:.0f}M params")
# Wrap for export
wrapper = DiTForTRTExport(decoder).float().to(device).eval()
print("[Export] Exporting in FP32 precision")
B, T, S = batch_size, seq_len, enc_len
print(f"[Export] Trace shapes: B={B}, T={T}, S={S}")
example_inputs = (
torch.randn(B, T, 192, device=device, dtype=torch.float32),
torch.randn(B, S, 2048, device=device, dtype=torch.float32),
torch.full((B,), 0.5, device=device, dtype=torch.float32),
torch.full((B,), 0.5, device=device, dtype=torch.float32),
)
input_names = ["input_latents", "enc_hidden", "t", "t_r"]
output_names = ["velocity"]
dynamic_axes = {
"input_latents": {0: "batch", 1: "seq_len"},
"enc_hidden": {0: "batch", 1: "enc_len"},
"t": {0: "batch"},
"t_r": {0: "batch"},
"velocity": {0: "batch", 1: "seq_len"},
}
onnx_path = output_dir / "dit.onnx"
with torch.no_grad():
print("[Export] Running test forward pass...")
test_out = wrapper(*example_inputs)
print(f"[Export] Test output shape: {test_out.shape}, "
f"mean={test_out.float().mean():.6f}, "
f"std={test_out.float().std():.6f}")
if torch.isnan(test_out).any():
print("[Export] ERROR: test output contains NaN! Aborting.")
return None
print("[Export] Exporting to ONNX (this may take a few minutes)...")
torch.onnx.export(
wrapper,
example_inputs,
str(onnx_path),
input_names=input_names,
output_names=output_names,
dynamic_axes=dynamic_axes,
opset_version=opset,
do_constant_folding=True,
dynamo=False,
)
size_mb = onnx_path.stat().st_size / (1 << 20)
print(f"[Export] ONNX saved to {onnx_path} ({size_mb:.1f} MB)")
# Copy config
model_dir_path = Path(model_dir)
for fname in ["config.json", "silence_latent.pt"]:
src = model_dir_path / fname
if src.exists():
shutil.copy2(str(src), str(output_dir / fname))
print(f"[Export] Copied {fname}")
print(f"\n{'='*60}")
print(f"[Export] SUCCESS! ONNX model at: {onnx_path}")
print(f"[Export] Size: {size_mb:.1f} MB")
print(f"{'='*60}")
return onnx_path
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Export AceStep DiT to ONNX")
parser.add_argument("--model-dir", required=True)
parser.add_argument("--output-dir", required=True)
parser.add_argument("--device", default="cuda")
parser.add_argument("--batch-size", type=int, default=1)
parser.add_argument("--seq-len", type=int, default=750)
parser.add_argument("--enc-len", type=int, default=200)
parser.add_argument("--opset", type=int, default=17)
args = parser.parse_args()
export_onnx(
model_dir=args.model_dir,
output_dir=args.output_dir,
device=args.device,
batch_size=args.batch_size,
seq_len=args.seq_len,
enc_len=args.enc_len,
opset=args.opset,
)
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env python
"""goldens_to_bin.py — flatten dump_bs_roformer_goldens.py output for C++.
engine/tools/bs-roformer-test.cpp reads a flat little-endian binary rather than
an .npz, because parsing zip+npy in C++ buys nothing here.
Layout:
magic "BSRG" 4 bytes
int32 T, in_dim, depth, dim, n_bands, n_stems
f32 input [in_dim * T]
f32 band_split [dim * n_bands * T]
f32 layer_00 [dim * n_bands * T]
f32 layer_01 [dim * n_bands * T]
f32 layer_last [dim * n_bands * T]
f32 final_norm [dim * n_bands * T]
f32 mask [n_stems * in_dim * T]
Hidden-state arrays arrive from torch as [1, T, n_bands, dim] and are written
in GGML memory order (dim fastest, then n_bands, then T) so the C++ side can
compare against its debug tensor elementwise without reindexing.
The mask arrives as [1, S, fs, T, 2] and is already in the engine's order.
USAGE
$py = "d:\\Ace-Step-Latest\\hot-step-9000\\.venv\\Scripts\\python.exe"
& $py tools\\goldens_to_bin.py `
--input models\\supersep-ckpt\\goldens_voc.npz `
--output models\\supersep-ckpt\\goldens_voc.bin
"""
import argparse
import struct
import numpy as np
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--input", required=True)
ap.add_argument("--output", required=True)
args = ap.parse_args()
z = np.load(args.input)
T, in_dim, depth, n_stems, fs = (int(v) for v in z["meta"])
band_split = z["band_split"] # [1, T, n_bands, dim]
_, t_chk, n_bands, dim = band_split.shape
assert t_chk == T, f"T mismatch {t_chk} vs {T}"
print(f"T={T} in_dim={in_dim} depth={depth} dim={dim} "
f"bands={n_bands} stems={n_stems} fs={fs}")
expect = T * n_bands * dim
def hidden(name):
# Rank varies: band_split/final_norm are hooked outside the pack and
# come back [1, T, n_bands, dim]; the per-layer hooks sit on the freq
# transformer, which sees the packed [(b t), n_bands, dim]. Batch is 1,
# so both flatten to the same GGML order (dim fastest, n_bands, T) —
# just flatten whatever rank arrived and check the count.
a = np.ascontiguousarray(z[name], dtype=np.float32).reshape(-1)
if a.size != expect:
raise SystemExit(
f"{name}: {a.size} elements, expected {expect} "
f"(shape {z[name].shape})")
return a
with open(args.output, "wb") as f:
f.write(b"BSRG")
f.write(struct.pack("<6i", T, in_dim, depth, dim, n_bands, n_stems))
# input is [1, T, in_dim]; GGML wants in_dim fastest then T — same order.
f.write(np.ascontiguousarray(z["input"][0], dtype=np.float32).tobytes())
for name in ("band_split", "layer_00", "layer_01", "layer_last", "final_norm"):
f.write(hidden(name).tobytes())
# mask [1, S, fs, T, 2] -> linear [s][fs][t][2], already correct
f.write(np.ascontiguousarray(z["mask"][0], dtype=np.float32).tobytes())
import os
print(f"[ok] {args.output} ({os.path.getsize(args.output)/1024/1024:.1f} MB)")
if __name__ == "__main__":
main()