opus-max-record / harness /scripts /interpolate.py
simonycl's picture
Upload folder using huggingface_hub
b2ebc95 verified
Raw
History Blame Contribute Delete
3.73 kB
#!/usr/bin/env python3
"""Interpolate two checkpoints in weight space: out = alpha*A + (1-alpha)*B.
Usage: interpolate.py OUT CKPT_A CKPT_B [alpha=0.5]
Why this exists alongside `soup.py`: `soup.py` averages checkpoints *from the same training run*
and assumes they share an index and shard filenames. That assumption does not hold here — the base
model ships shards named `model.safetensors-0000N-of-00004.safetensors` while prime-rl's trainer
writes `model-0000N-of-00004.safetensors`, so `soup.py` dies with `FileNotFoundError` on the base.
This script reads each checkpoint's *own* `model.safetensors.index.json` and matches by tensor
name, so it does not care how either side lays its shards out.
It also does a real weighted interpolation rather than an equal average, and it verifies that both
checkpoints expose exactly the same tensor names and shapes before writing anything — silently
averaging a mismatched pair would produce a file that loads and generates garbage.
Output carries A's shard layout and A's config/tokenizer/processor files.
"""
import json
import os
import shutil
import sys
import torch
from safetensors.torch import load_file, save_file
AUX = (
"config.json", "generation_config.json", "tokenizer.json", "tokenizer_config.json",
"chat_template.jinja", "merges.txt", "vocab.json",
"preprocessor_config.json", "video_preprocessor_config.json",
)
def index_of(ckpt):
"""-> {tensor_name: shard_filename}"""
with open(os.path.join(ckpt, "model.safetensors.index.json")) as f:
return json.load(f)["weight_map"]
def main():
if len(sys.argv) < 4:
sys.exit(__doc__)
out, a, b = sys.argv[1], sys.argv[2], sys.argv[3]
alpha = float(sys.argv[4]) if len(sys.argv) > 4 else 0.5
ia, ib = index_of(a), index_of(b)
missing = set(ia) - set(ib)
if missing:
sys.exit(f"{len(missing)} tensors in A are absent from B, e.g. {sorted(missing)[:3]}")
extra = set(ib) - set(ia)
if extra:
# Expected here: the base ships a 15-tensor MTP (multi-token-prediction) head that
# prime-rl's SFT trainer does not train and does not save. The served artifact is A's
# structure, so B's extras are ignored rather than grafted on — the output must be
# loadable by exactly the command in SUBMISSION.md.
print(f"note: {len(extra)} tensors exist only in B and are ignored "
f"(e.g. {sorted(extra)[:2]})", flush=True)
print(f"{len(ia)} tensors; out = {alpha}*A + {1 - alpha}*B", flush=True)
os.makedirs(out, exist_ok=True)
# Load B once, keyed by tensor name — B's shard layout is irrelevant from here on.
bt = {}
for shard in sorted(set(ib.values())):
bt.update(load_file(os.path.join(b, shard)))
print(f"loaded B ({len(bt)} tensors)", flush=True)
for shard in sorted(set(ia.values())):
sd = load_file(os.path.join(a, shard))
merged = {}
for name, ta in sd.items():
tb = bt[name]
if ta.shape != tb.shape:
sys.exit(f"shape mismatch for {name}: {tuple(ta.shape)} vs {tuple(tb.shape)}")
merged[name] = (alpha * ta.to(torch.float32)
+ (1 - alpha) * tb.to(torch.float32)).to(ta.dtype)
save_file(merged, os.path.join(out, shard), metadata={"format": "pt"})
print(f"wrote {shard} ({len(merged)} tensors)", flush=True)
del sd, merged
shutil.copy(os.path.join(a, "model.safetensors.index.json"), out)
for f in AUX:
src = os.path.join(a, f)
if os.path.exists(src):
shutil.copy(src, out)
print("done:", out, "->", sorted(os.listdir(out)), flush=True)
if __name__ == "__main__":
main()