#!/usr/bin/env python3 """Mistral-Small-3.1-24B-Base-2503 -> thrasher base: strip vision, claim ChatML. python prep_base.py --src --out [--init copy|mean|none] python prep_base.py --src --out --tokenizer-only One streaming pass over the shards (CPU-only, ~one tensor in memory at a time; fine on the box or locally). """ from __future__ import annotations import argparse import json import shutil import sys from pathlib import Path CLAIMS = { # id -> (old, new, donor_id) 20: ("", "<|im_start|>", 1), # donor 21: ("", "<|im_end|>", 2), # donor } BROKEN_REGEX = (r"(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+" r"|\p{N}{1,3}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+") FIXED_REGEX = (r"[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]*" r"[\p{Ll}\p{Lm}\p{Lo}\p{M}]+|[^\r\n\p{L}\p{N}]?" r"[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]+[\p{Ll}\p{Lm}\p{Lo}\p{M}]*" r"|\p{N}| ?[^\s\p{L}\p{N}]+[\r\n/]*|\s*[\r\n]+|\s+(?!\S)|\s+") DROP_PREFIXES = ("vision_tower.", "multi_modal_projector.") LM_PREFIX = "language_model." ROW_KEYS = ("model.embed_tokens.weight", "lm_head.weight") def replace_deep(obj, mapping: dict[str, str]): if isinstance(obj, str): return mapping.get(obj, obj) if isinstance(obj, list): return [replace_deep(x, mapping) for x in obj] if isinstance(obj, dict): return {k: replace_deep(v, mapping) for k, v in obj.items()} return obj def jload(p: Path): with open(p) as f: return json.load(f) def jdump(obj, p: Path): with open(p, "w") as f: json.dump(obj, f, indent=2, ensure_ascii=False) f.write("\n") def prep_tokenizer(src: Path, out: Path, template_text: str) -> None: strmap = {old: new for old, new, _ in CLAIMS.values()} tj = jload(src / "tokenizer.json") renamed = 0 for tok in tj["added_tokens"]: if tok["content"] in strmap: tok["content"] = strmap[tok["content"]] renamed += 1 vocab = tj["model"]["vocab"] for old, new, _ in CLAIMS.values(): assert old in vocab, f"{old} not in vocab — wrong base?" assert new not in vocab, f"{new} already in vocab" vocab[new] = vocab.pop(old) assert renamed == len(CLAIMS), f"renamed {renamed} added_tokens, expected {len(CLAIMS)}" split = tj["pre_tokenizer"]["pretokenizers"][0]["pattern"] assert split["Regex"] == BROKEN_REGEX, "pre_tokenizer not the known-broken pattern — re-diff before baking" split["Regex"] = FIXED_REGEX jdump(tj, out / "tokenizer.json") tc = replace_deep(jload(src / "tokenizer_config.json"), strmap) tc["eos_token"] = "<|im_end|>" tc["chat_template"] = template_text jdump(tc, out / "tokenizer_config.json") sm = replace_deep(jload(src / "special_tokens_map.json"), strmap) eos = sm.get("eos_token") if isinstance(eos, dict): eos["content"] = "<|im_end|>" else: sm["eos_token"] = "<|im_end|>" jdump(sm, out / "special_tokens_map.json") (out / "chat_template.jinja").write_text(template_text) # round-trip proof, not guess from tokenizers import Tokenizer tok = Tokenizer.from_file(str(out / "tokenizer.json")) ids = tok.encode("<|im_start|>user\nhi<|im_end|>\n").ids assert ids[0] == 1 and 20 in ids and 21 in ids, f"claim round-trip failed: {ids}" assert tok.decode([20, 21], skip_special_tokens=False) == "<|im_start|><|im_end|>" print(f"[tokenizer] claimed: " + ", ".join( f"{new}={i}" for i, (_, new, _) in CLAIMS.items())) print(f"[tokenizer] round-trip ids for ChatML probe: {ids}") def prep_configs(src: Path, out: Path) -> None: cfg = jload(src / "config.json") text = cfg["text_config"] text.update({ "architectures": ["MistralForCausalLM"], "model_type": "mistral", "torch_dtype": cfg.get("torch_dtype", "bfloat16"), "tie_word_embeddings": False, "bos_token_id": 1, "eos_token_id": 21, }) jdump(text, out / "config.json") gen = {"bos_token_id": 1, "eos_token_id": [21]} if (src / "generation_config.json").exists(): g = jload(src / "generation_config.json") g.update(gen) g.pop("pad_token_id", None) gen = g gen["transformers_version"] = None gen = {k: v for k, v in gen.items() if v is not None} jdump(gen, out / "generation_config.json") print("[config] MistralForCausalLM, untied, eos_token_id=[21]") def prep_weights(src: Path, out: Path, init: str) -> None: import torch from safetensors import safe_open from safetensors.torch import save_file index = jload(src / "model.safetensors.index.json") wmap = index["weight_map"] shards: dict[str, list[str]] = {} for key, shard in wmap.items(): shards.setdefault(shard, []).append(key) new_map: dict[str, str] = {} total = 0 n_drop = n_keep = 0 donor_rows: dict[str, dict[int, torch.Tensor]] = {} # row_key -> {donor_id: row} shard_names = sorted(shards) for si, shard in enumerate(shard_names, 1): out_name = f"model-{si:05d}-of-{len(shard_names):05d}.safetensors" tensors: dict[str, torch.Tensor] = {} with safe_open(src / shard, framework="pt") as f: for key in sorted(shards[shard]): if key.startswith(DROP_PREFIXES): n_drop += 1 continue assert key.startswith(LM_PREFIX), f"unexpected key {key}" nk = key[len(LM_PREFIX):] t = f.get_tensor(key) if nk in ROW_KEYS: t = claim_rows(nk, t, init, donor_rows) tensors[nk] = t n_keep += 1 if not tensors: continue save_file(tensors, str(out / out_name), metadata={"format": "pt"}) for nk, t in tensors.items(): new_map[nk] = out_name total += t.numel() * t.element_size() print(f"[weights] {shard} -> {out_name} ({len(tensors)} tensors)") jdump({"metadata": {"total_size": total}, "weight_map": new_map}, out / "model.safetensors.index.json") print(f"[weights] kept {n_keep}, dropped {n_drop}, total {total/1e9:.2f} GB") assert n_keep == 363 and n_drop == 222, "key census mismatch vs 2026-08-26 index" def claim_rows(name: str, t, init: str, donor_rows) -> "torch.Tensor": import torch live = t[1000:] # rows past the control block are all trained BPE tokens live_norm = live.float().norm(dim=1) print(f"[liveness] {name}: live rows norm mean {live_norm.mean():.4f} " f"(p5 {live_norm.quantile(0.05):.4f})") for tid, (_, new, donor) in CLAIMS.items(): print(f"[liveness] row {tid} ({new}): norm {t[tid].float().norm():.4f}, " f"donor row {donor}: {t[donor].float().norm():.4f}") if init == "none": return t t = t.clone() for tid, (_, _, donor) in CLAIMS.items(): t[tid] = t[donor] if init == "copy" else live.float().mean(0).to(t.dtype) return t def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--src", required=True, type=Path) ap.add_argument("--out", required=True, type=Path) ap.add_argument("--template", type=Path, default=Path(__file__).parent.parent / "configs" / "thrasher.jinja") ap.add_argument("--init", choices=("copy", "mean", "none"), default="copy") ap.add_argument("--tokenizer-only", action="store_true") args = ap.parse_args() args.out.mkdir(parents=True, exist_ok=True) template_text = args.template.read_text() prep_tokenizer(args.src, args.out, template_text) if args.tokenizer_only: print("[done] tokenizer-only") return prep_configs(args.src, args.out) prep_weights(args.src, args.out, args.init) print("[done] prepped base at", args.out) if __name__ == "__main__": main()