Download tools/maya_export/export_backbone.py from mrwine/maya2-native-fast: direct link, hf CLI and curl.
- Browser
- Download file 12.1 kB
-
https://huggingface.co/mrwine/maya2-native-fast/resolve/main/tools/maya_export/export_backbone.py
- Command line
-
hf download hf://mrwine/maya2-native-fast/tools/maya_export/export_backbone.py
-
curl -L -o export_backbone.py https://huggingface.co/mrwine/maya2-native-fast/resolve/main/tools/maya_export/export_backbone.py
12.1 kB
| """Fused backbone ONNX: 28 Qwen3 decoder layers + final RMSNorm + cb0 head, with KV cache in/out. | |
| Build (one layer resident at a time, so it fits the 4 GB limit): | |
| 1. each layer: transformers Qwen3DecoderLayer loaded from the checkpoint (tools/maya_export/streamed_backbone.py), | |
| exported to ONNX (opset 17, dynamo=False), saved with external data; | |
| for wo8 quantized with MatMulNBitsQuantizer(bits=8, block_size=32, is_symmetric=True, accuracy_level=4). | |
| 2. head: final RMSNorm + cb0_head (fp32). | |
| 3. merge: graph structure only (weights stay in the per-part data files), tensors renamed, then the weight bytes | |
| are copied into one data file next to the merged model. | |
| Output: work_dir()/assets/v1/backbone_{precision}.onnx + backbone_{precision}.data | |
| Per-part files: work_dir()/backbone_build/{precision}/ (kept for the layer-by-layer reference in the tests). | |
| .venv/bin/python tools/memguard.py --rss-limit-mb 4000 --min-free-mb 5000 -- \ | |
| .venv-tts/bin/python -m tools.maya_export.export_backbone --precision wo8 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import shutil | |
| from pathlib import Path | |
| import numpy as np | |
| import onnx | |
| import torch | |
| import torch.nn as nn | |
| from onnx import TensorProto, helper | |
| from tools.maya_export.common import ckpt_paths, work_dir | |
| from tools.maya_export.streamed_backbone import StreamedBackbone, shard_files | |
| N_LAYERS = 28 | |
| N_KV_HEADS = 8 | |
| HEAD_DIM = 128 | |
| D_MODEL = 2048 | |
| N_CB0 = 2049 # 2048 codes + <aeos> | |
| OPSET = 17 | |
| PREFILL = 8 # trace length for the export; dynamic axes cover any T | |
| COPY_CHUNK = 1 << 24 | |
| class LayerKV(nn.Module): | |
| """One decoder layer, KV cache as flat tensors, additive attention mask passed in.""" | |
| def __init__(self, layer): | |
| super().__init__() | |
| self.layer = layer | |
| def forward(self, h, cos, sin, mask, past_k, past_v): | |
| from transformers import DynamicCache | |
| cache = DynamicCache() | |
| cache.update(past_k, past_v, 0) | |
| out = self.layer(h, attention_mask=mask, position_ids=None, past_key_values=cache, | |
| use_cache=True, cache_position=None, position_embeddings=(cos, sin)) | |
| if isinstance(out, tuple): | |
| out = out[0] | |
| return out, cache.layers[0].keys, cache.layers[0].values | |
| class Head(nn.Module): | |
| def __init__(self, norm: nn.Module, cb0_head: nn.Linear) -> None: | |
| super().__init__() | |
| self.norm = norm | |
| self.cb0_head = cb0_head | |
| def forward(self, hid): | |
| x = self.norm(hid) | |
| return x, self.cb0_head(x) | |
| def masks(T: int, past: int) -> torch.Tensor: | |
| """Additive causal mask (1, 1, T, past+T): query i sees keys j <= past + i.""" | |
| i = torch.arange(T)[:, None] | |
| j = torch.arange(past + T)[None, :] | |
| return torch.zeros(T, past + T).masked_fill(j > past + i, float("-inf"))[None, None] | |
| def rope(rot, T: int, past: int) -> tuple[torch.Tensor, torch.Tensor]: | |
| pos = torch.arange(past, past + T)[None] | |
| return rot(torch.zeros(1, T, 1), pos) | |
| def _export(module: nn.Module, args: tuple, path: Path, **kw) -> None: | |
| with torch.inference_mode(): | |
| torch.onnx.export(module, args, str(path), opset_version=OPSET, dynamo=False, | |
| do_constant_folding=True, **kw) | |
| def _save_external(model: onnx.ModelProto, path: Path) -> None: | |
| # onnx appends to an existing data file; remove it so a rebuild does not leave stale bytes behind | |
| path.with_name(path.name + ".data").unlink(missing_ok=True) | |
| onnx.save_model(model, str(path), save_as_external_data=True, all_tensors_to_one_file=True, | |
| location=path.name + ".data", size_threshold=1024) # small tensors inline: ORT shape inference needs them | |
| def _quantize_wo8(model: onnx.ModelProto, exclude: list[str] | None = None) -> onnx.ModelProto: | |
| from onnxruntime.quantization.matmul_nbits_quantizer import MatMulNBitsQuantizer | |
| q = MatMulNBitsQuantizer(model, bits=8, block_size=32, is_symmetric=True, accuracy_level=4, | |
| nodes_to_exclude=exclude or []) | |
| q.process() | |
| return q.model.model | |
| def export_layer(sb: StreamedBackbone, i: int, precision: str, build: Path) -> Path: | |
| sb._load_layer(i) | |
| sb.layer.self_attn.layer_idx = 0 # LayerKV owns a one-slot cache | |
| wrap = LayerKV(sb.layer).eval() | |
| h0 = torch.randn(1, PREFILL, D_MODEL) * 0.02 | |
| cos, sin = rope(sb.rotary, PREFILL, 0) | |
| pk = torch.zeros(1, N_KV_HEADS, 0, HEAD_DIM) | |
| pv = torch.zeros(1, N_KV_HEADS, 0, HEAD_DIM) | |
| tmp = build / f"layer_{i:02d}_raw.onnx" | |
| _export(wrap, (h0, cos, sin, masks(PREFILL, 0), pk, pv), tmp, | |
| input_names=["h", "cos", "sin", "mask", "past_k", "past_v"], | |
| output_names=["out", "new_k", "new_v"], | |
| dynamic_axes={"h": {1: "t"}, "cos": {1: "t"}, "sin": {1: "t"}, "mask": {2: "t", 3: "total"}, | |
| "past_k": {2: "past"}, "past_v": {2: "past"}, | |
| "out": {1: "t"}, "new_k": {2: "total"}, "new_v": {2: "total"}}) | |
| model = onnx.load(str(tmp)) | |
| if precision == "wo8": | |
| model = _quantize_wo8(model) | |
| out = build / f"layer_{i:02d}.onnx" | |
| _save_external(model, out) | |
| tmp.unlink() | |
| tmp.with_suffix(".onnx.data").unlink(missing_ok=True) | |
| return out | |
| def export_head(sb: StreamedBackbone, build: Path) -> Path: | |
| cb0 = nn.Linear(D_MODEL, N_CB0, bias=False) | |
| cb0.weight.data.copy_(sb.weights.get("backbone.cb0_head.weight").float()) | |
| head = Head(sb.norm, cb0).eval() | |
| tmp = build / "head_raw.onnx" | |
| _export(head, (torch.randn(1, PREFILL, D_MODEL),), tmp, input_names=["hid"], | |
| output_names=["hidden", "cb0_logits"], | |
| dynamic_axes={"hid": {1: "t"}, "hidden": {1: "t"}, "cb0_logits": {1: "t"}}) | |
| out = build / "head.onnx" | |
| _save_external(onnx.load(str(tmp)), out) | |
| tmp.unlink() | |
| tmp.with_suffix(".onnx.data").unlink(missing_ok=True) | |
| return out | |
| def _rename(model: onnx.ModelProto, prefix: str, mapping: dict[str, str], keep: set[str]): | |
| """Rename every tensor and node of a part. Names in `mapping` are replaced; names in `keep` are shared | |
| across parts (cos/sin/mask); everything else gets `prefix`.""" | |
| def new(name: str) -> str: | |
| if name == "": | |
| return name | |
| if name in mapping: | |
| return mapping[name] | |
| if name in keep: | |
| return name | |
| return prefix + name | |
| g = model.graph | |
| nodes = [] | |
| for node in g.node: | |
| n = onnx.NodeProto() | |
| n.CopyFrom(node) | |
| del n.input[:] | |
| n.input.extend(new(x) for x in node.input) | |
| del n.output[:] | |
| n.output.extend(new(x) for x in node.output) | |
| n.name = prefix + node.name if node.name else "" | |
| nodes.append(n) | |
| inits = [] | |
| for init in g.initializer: | |
| t = TensorProto() | |
| t.CopyFrom(init) | |
| t.name = new(init.name) | |
| inits.append(t) | |
| return nodes, inits | |
| def merge(layer_paths: list[Path], head_path: Path, out_path: Path, precision: str) -> None: | |
| all_nodes, all_inits, opsets = [], [], {} | |
| for i, path in enumerate(layer_paths): | |
| m = onnx.load(str(path), load_external_data=False) | |
| src_in = "embeds" if i == 0 else f"hid_{i - 1}" | |
| mapping = {"h": src_in, "past_k": f"past_k_{i}", "past_v": f"past_v_{i}", | |
| "out": f"hid_{i}", "new_k": f"present_k_{i}", "new_v": f"present_v_{i}"} | |
| nodes, inits = _rename(m, f"L{i}_", mapping, keep={"cos", "sin", "mask"}) | |
| all_nodes += nodes | |
| all_inits += inits | |
| for op in m.opset_import: | |
| opsets[op.domain] = max(opsets.get(op.domain, 0), op.version) | |
| hm = onnx.load(str(head_path), load_external_data=False) | |
| nodes, inits = _rename(hm, "H_", {"hid": f"hid_{len(layer_paths) - 1}", "hidden": "hidden", | |
| "cb0_logits": "cb0_logits"}, keep=set()) | |
| all_nodes += nodes | |
| all_inits += inits | |
| for op in hm.opset_import: | |
| opsets[op.domain] = max(opsets.get(op.domain, 0), op.version) | |
| data_name = out_path.with_suffix(".data").name | |
| _consolidate(all_inits, layer_paths + [head_path], out_path.parent, data_name) | |
| inputs = [helper.make_tensor_value_info("embeds", TensorProto.FLOAT, [1, "T", D_MODEL]), | |
| helper.make_tensor_value_info("cos", TensorProto.FLOAT, [1, "T", HEAD_DIM]), | |
| helper.make_tensor_value_info("sin", TensorProto.FLOAT, [1, "T", HEAD_DIM]), | |
| helper.make_tensor_value_info("mask", TensorProto.FLOAT, [1, 1, "T", "total"])] | |
| outputs = [helper.make_tensor_value_info("hidden", TensorProto.FLOAT, [1, "T", D_MODEL]), | |
| helper.make_tensor_value_info("cb0_logits", TensorProto.FLOAT, [1, "T", N_CB0])] | |
| n = len(layer_paths) | |
| for i in range(n): | |
| inputs.append(helper.make_tensor_value_info(f"past_k_{i}", TensorProto.FLOAT, [1, N_KV_HEADS, "P", HEAD_DIM])) | |
| inputs.append(helper.make_tensor_value_info(f"past_v_{i}", TensorProto.FLOAT, [1, N_KV_HEADS, "P", HEAD_DIM])) | |
| outputs.append(helper.make_tensor_value_info(f"present_k_{i}", TensorProto.FLOAT, [1, N_KV_HEADS, "total", HEAD_DIM])) | |
| outputs.append(helper.make_tensor_value_info(f"present_v_{i}", TensorProto.FLOAT, [1, N_KV_HEADS, "total", HEAD_DIM])) | |
| graph = helper.make_graph(all_nodes, f"maya2_backbone_{precision}", inputs, outputs, all_inits) | |
| opset_imports = [helper.make_opsetid(d, v) for d, v in sorted(opsets.items())] | |
| ir_version = onnx.load(str(layer_paths[0]), load_external_data=False).ir_version | |
| model = helper.make_model(graph, opset_imports=opset_imports, ir_version=ir_version) | |
| onnx.save_model(model, str(out_path)) | |
| def _consolidate(inits: list[TensorProto], parts: list[Path], out_dir: Path, data_name: str) -> None: | |
| """Copy every external tensor's bytes into one data file, updating offset and location in place.""" | |
| offset = 0 | |
| with open(out_dir / data_name, "wb") as dst: | |
| for t in inits: | |
| if t.data_location != TensorProto.EXTERNAL: | |
| continue | |
| entries = {e.key: e.value for e in t.external_data} | |
| loc = entries["location"] | |
| src = _source_file(loc, parts) | |
| length = int(entries["length"]) | |
| start = int(entries.get("offset", 0)) | |
| with open(src, "rb") as f: | |
| f.seek(start) | |
| remaining = length | |
| while remaining: | |
| chunk = f.read(min(COPY_CHUNK, remaining)) | |
| if not chunk: | |
| raise IOError(f"short read for {t.name} in {src}") | |
| dst.write(chunk) | |
| remaining -= len(chunk) | |
| del t.external_data[:] | |
| for k, v in (("location", data_name), ("offset", str(offset)), ("length", str(length))): | |
| e = t.external_data.add() | |
| e.key, e.value = k, v | |
| offset += length | |
| def _source_file(location: str, parts: list[Path]) -> Path: | |
| for p in parts: | |
| if (p.parent / location).exists(): | |
| return p.parent / location | |
| raise FileNotFoundError(location) | |
| def build(precision: str) -> Path: | |
| if precision not in ("fp32", "wo8"): | |
| raise ValueError(precision) | |
| build_dir = work_dir() / "backbone_build" / precision | |
| build_dir.mkdir(parents=True, exist_ok=True) | |
| paths = ckpt_paths() | |
| sb = StreamedBackbone(paths["backbone_dir"], shard_files(paths["ckpt"]), n_layers=N_LAYERS) | |
| layer_paths = [] | |
| for i in range(N_LAYERS): | |
| layer_paths.append(export_layer(sb, i, precision, build_dir)) | |
| print(f"layer {i:02d} {precision} ok", flush=True) | |
| head = export_head(sb, build_dir) | |
| out_dir = work_dir() / "assets" / "v1" | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| out = out_dir / f"backbone_{precision}.onnx" | |
| merge(layer_paths, head, out, precision) | |
| print(f"wrote {out} and {out.with_suffix('.data').name}", flush=True) | |
| return out | |
| if __name__ == "__main__": | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--precision", choices=("fp32", "wo8"), required=True) | |
| build(ap.parse_args().precision) | |