#!/usr/bin/env python3 """Average several checkpoints of the same run into one ("model soup" / SWA). All inputs must be my own checkpoints from this run's training tree; averaging along a training trajectory is the classic stochastic-weight-averaging trick and usually buys a little robustness for free. Copies the tokenizer/config/processor files from the first input. Usage: soup.py OUT CKPT1 CKPT2 [CKPT3 ...] """ import json import os import shutil import sys import torch from safetensors.torch import load_file, save_file def main(): out, ckpts = sys.argv[1], sys.argv[2:] if len(ckpts) < 2: sys.exit(__doc__) os.makedirs(out, exist_ok=True) index_path = os.path.join(ckpts[0], "model.safetensors.index.json") index = json.load(open(index_path)) shards = sorted(set(index["weight_map"].values())) print(f"{len(ckpts)} checkpoints, {len(shards)} shards") for shard in shards: acc = None for i, c in enumerate(ckpts): sd = load_file(os.path.join(c, shard)) if acc is None: acc = {k: v.to(torch.float32) for k, v in sd.items()} else: for k in acc: acc[k] += sd[k].to(torch.float32) del sd for k in acc: acc[k] = (acc[k] / len(ckpts)).to(torch.bfloat16) save_file(acc, os.path.join(out, shard), metadata={"format": "pt"}) print("wrote", shard, flush=True) del acc for f in os.listdir(ckpts[0]): if f.endswith(".safetensors") or f == "STABLE": continue shutil.copy2(os.path.join(ckpts[0], f), os.path.join(out, f)) open(os.path.join(out, "STABLE"), "w").close() print("soup ->", out) if __name__ == "__main__": main()