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()