File size: 8,386 Bytes
17a55cc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | """Export: ONNX graphs for the production path and the Bend weight file.
groot-export onnx --model runs/groot-1-s/final --out export/onnx
groot-export bend --model runs/groot-1-s/final --out export/groot.gbw [--prune-vocab 65536 --corpus data/train.jsonl]
groot-export prune --model ... --out ... --keep 65536 --corpus data/train.jsonl
Bend weight file (.gbw):
magic "GBW1" | u32 header_len | header JSON (utf-8) | zero padding to 64 B | float32 LE tensors
header = {"config": {...}, "tensors": [{"name", "shape", "offset", "numel"}], "vocab_remap": optional}
The Bend engine (bend/) maps this file and reads each tensor as a flat Array<F32>.
"""
from __future__ import annotations
import argparse
import json
import struct
from collections import Counter
from pathlib import Path
import numpy as np
import torch
from .data import read_jsonl
from .model import GrootDecisionModel
from .schema import render_content
MAGIC = b"GBW1"
# ------------------------------------------------------------------- bend
def write_gbw(model: GrootDecisionModel, path: str | Path, vocab_remap: np.ndarray | None = None, keep_ids: np.ndarray | None = None) -> dict:
tensors = []
blobs = []
offset = 0
state = model.state_dict()
for name, t in state.items():
arr = t.detach().cpu().float().numpy()
if keep_ids is not None and name.endswith("embeddings.tok_embeddings.weight"):
arr = arr[keep_ids]
arr = np.ascontiguousarray(arr, dtype="<f4")
tensors.append({"name": name, "shape": list(arr.shape), "offset": offset, "numel": int(arr.size)})
blobs.append(arr.tobytes())
offset += arr.size * 4
header = {
"format": "gbw",
"version": 1,
"config": model.cfg.to_dict(),
"encoder_config": {k: v for k, v in model.encoder.config.to_dict().items() if isinstance(v, (int, float, str, bool))},
"tensors": tensors,
"temperature": model.temperature.tolist(),
"layer_types": list(getattr(model.encoder.config, "layer_types", None) or []),
"vocab_remap": vocab_remap.tolist() if vocab_remap is not None else None,
}
hbytes = json.dumps(header).encode("utf-8")
pad = (-(4 + 4 + len(hbytes))) % 64
with Path(path).open("wb") as f:
f.write(MAGIC)
f.write(struct.pack("<I", len(hbytes)))
f.write(hbytes)
f.write(b"\0" * pad)
for b in blobs:
f.write(b)
return {"tensors": len(tensors), "bytes": 8 + len(hbytes) + pad + offset, "path": str(path)}
def read_gbw_header(path: str | Path) -> dict:
with Path(path).open("rb") as f:
if f.read(4) != MAGIC:
raise ValueError("not a GBW file")
n = struct.unpack("<I", f.read(4))[0]
return json.loads(f.read(n).decode("utf-8"))
# ----------------------------------------------------------- vocab pruning
def token_frequencies(tokenizer, corpus: str | Path, limit: int | None = 200_000) -> Counter:
counts: Counter = Counter()
for i, row in enumerate(read_jsonl(corpus)):
if limit and i >= limit:
break
texts = [render_content(row["state"]), row.get("question") or "", *row.get("options", [])]
for t in texts:
counts.update(tokenizer(t, add_special_tokens=False)["input_ids"])
return counts
def prune_plan(tokenizer, counts: Counter, keep: int) -> tuple[np.ndarray, np.ndarray]:
"""Return (keep_ids sorted, remap[full_vocab] -> compact id or UNK slot)."""
specials = [i for i in (tokenizer.cls_token_id, tokenizer.sep_token_id, tokenizer.mask_token_id, tokenizer.pad_token_id, tokenizer.unk_token_id) if i is not None]
chosen = set(specials)
for tid, _ in counts.most_common():
if len(chosen) >= keep:
break
chosen.add(tid)
keep_ids = np.array(sorted(chosen), dtype=np.int64)
unk = tokenizer.unk_token_id if tokenizer.unk_token_id is not None else specials[0]
remap = np.full(len(tokenizer), -1, dtype=np.int64)
remap[keep_ids] = np.arange(len(keep_ids))
remap[remap < 0] = remap[unk]
return keep_ids, remap
# ------------------------------------------------------------------- onnx
class _EncoderWrapper(torch.nn.Module):
def __init__(self, enc):
super().__init__()
self.enc = enc
def forward(self, input_ids, attention_mask, position_ids):
return self.enc(input_ids=input_ids, attention_mask=attention_mask, position_ids=position_ids).last_hidden_state
class _FusionWrapper(torch.nn.Module):
"""Option set [q_token ; markers] + state memory -> option logits (fusion + scorer)."""
def __init__(self, model: GrootDecisionModel):
super().__init__()
self.m = model
def forward(self, S, S_mask, S_pos, memory, memory_mask):
from .model import rope_cos_sin
m = self.m
cos, sin = rope_cos_sin(S_pos, m.cfg.hidden_size // m.cfg.fusion_heads, m.cfg.rope_theta, S.dtype)
for layer in m.fusion:
S = layer(S, cos, sin, S_mask.bool(), memory, memory_mask.bool())
S = m.final_norm(S)
return m.scorer(S[:, 1:]).squeeze(-1)
def export_onnx(model: GrootDecisionModel, out_dir: str | Path, opset: int = 17) -> dict:
out = Path(out_dir)
out.mkdir(parents=True, exist_ok=True)
model.eval()
d = model.cfg.hidden_size
ids = torch.randint(5, 100, (2, 16))
mask = torch.ones(2, 16, dtype=torch.long)
pos = torch.arange(16)[None].repeat(2, 1)
torch.onnx.export(
_EncoderWrapper(model.encoder), (ids, mask, pos), str(out / "encoder.onnx"), opset_version=opset,
input_names=["input_ids", "attention_mask", "position_ids"], output_names=["last_hidden_state"],
dynamic_axes={"input_ids": {0: "b", 1: "l"}, "attention_mask": {0: "b", 1: "l"}, "position_ids": {0: "b", 1: "l"}, "last_hidden_state": {0: "b", 1: "l"}},
dynamo=False,
)
S = torch.randn(2, 5, d)
S_mask = torch.ones(2, 5, dtype=torch.long)
S_pos = torch.ones(2, 5, dtype=torch.long)
S_pos[:, 0] = 0
memory = torch.randn(2, 40, d)
mmask = torch.ones(2, 40, dtype=torch.long)
torch.onnx.export(
_FusionWrapper(model), (S, S_mask, S_pos, memory, mmask), str(out / "fusion.onnx"), opset_version=opset,
input_names=["set", "set_mask", "set_pos", "memory", "memory_mask"], output_names=["logits"],
dynamic_axes={"set": {0: "q", 1: "k1"}, "set_mask": {0: "q", 1: "k1"}, "set_pos": {0: "q", 1: "k1"}, "memory": {0: "q", 1: "m"}, "memory_mask": {0: "q", 1: "m"}, "logits": {0: "q", 1: "k"}},
dynamo=False,
)
misc = {k: v.detach().cpu().numpy().tolist() for k, v in model.state_dict().items() if k.split(".")[0] in ("tree", "seg_emb", "qtype_emb", "level_emb", "conf_head")}
(out / "tree_and_misc.json").write_text(json.dumps({"weights": misc, "temperature": model.temperature.tolist(), "config": model.cfg.to_dict()}))
return {"encoder": str(out / "encoder.onnx"), "fusion": str(out / "fusion.onnx")}
# -------------------------------------------------------------------- CLI
def main(argv: list[str] | None = None) -> None:
ap = argparse.ArgumentParser(prog="groot-export")
sub = ap.add_subparsers(dest="cmd", required=True)
o = sub.add_parser("onnx")
o.add_argument("--model", required=True)
o.add_argument("--out", required=True)
o.add_argument("--opset", type=int, default=17)
b = sub.add_parser("bend")
b.add_argument("--model", required=True)
b.add_argument("--out", required=True)
b.add_argument("--prune-vocab", type=int, default=None)
b.add_argument("--corpus", default=None)
args = ap.parse_args(argv)
model = GrootDecisionModel.from_pretrained(args.model, device="cpu")
if args.cmd == "onnx":
print(json.dumps(export_onnx(model, args.out, args.opset), indent=2))
else:
keep_ids = remap = None
if args.prune_vocab:
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained(Path(args.model) / "tokenizer")
if not args.corpus:
raise SystemExit("--prune-vocab needs --corpus")
keep_ids, remap = prune_plan(tok, token_frequencies(tok, args.corpus), args.prune_vocab)
print(json.dumps(write_gbw(model, args.out, remap, keep_ids), indent=2))
if __name__ == "__main__":
main()
|