align / code /scripts /ckpt_convert.py
viai957's picture
Add files using upload-large-folder tool
bcc8d35 verified
Raw History Blame Contribute Delete
5.67 kB
#!/usr/bin/env python
"""Sharded checkpoint tools: inspect, verify, export to HF safetensors, reshard.
python scripts/ckpt_convert.py inspect RUN/checkpoints/micro_N
python scripts/ckpt_convert.py verify RUN/checkpoints/micro_N
python scripts/ckpt_convert.py export RUN/checkpoints/micro_N OUT_DIR
python scripts/ckpt_convert.py reshard RUN/checkpoints/micro_N NEW_DIR --world-size 2
CPU only, no GPU, no process group, no heavy-job lock: `export` is a streaming
byte copy (peak RSS ~ a few MiB plus the Python runtime, independent of model
size) and `reshard` re-cuts the weights the same way. The ONE part that holds
real memory is the optimizer re-cut inside `reshard` (one NEW rank's optimizer
state, e.g. 12 B/param/new_ws for fp32-master AdamW); it prints that estimate
and refuses to start when the box cannot afford it (CLAUDE.md: 45 GB
MemAvailable floor for anything heavy, no swap).
WHY THIS EXISTS. `train.checkpoint_format = "sharded"` writes per-rank files
that eval, the rollout weight sync and external tools cannot read. `export`
turns one into exactly the directory the gathered writer would have produced
(byte-identical; tested by file hash) without ever holding the model in RAM --
the gathered path's rank-0 gather measured +28 GB RSS on OLMoE (docs/
MEASUREMENTS.md, Track M). `reshard` is the ONLY way to resume at a different
world size: resume refuses it, because sharded weights are rank-local row
slices and silently loading them under another partitioning trains plausibly
on scrambled weights.
Exit status: 0 ok, 1 refused/corrupt (message on stderr), 2 usage.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
os.environ.setdefault("CUDA_VISIBLE_DEVICES", "") # never touch a GPU from a converter
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from align.train import sharded_ckpt as sc # noqa: E402
def _mem_available_gib() -> float:
with open("/proc/meminfo") as f:
for line in f:
if line.startswith("MemAvailable:"):
return int(line.split()[1]) / 2**20
return 0.0
def _optimizer_bytes(src: str, ws: int) -> int:
return sum(os.path.getsize(os.path.join(src, f"optimizer_rank{r}.pt")) for r in range(ws) if os.path.exists(os.path.join(src, f"optimizer_rank{r}.pt")))
def cmd_inspect(a) -> int:
m = sc.read_manifest(a.src)
total = sum(f["bytes"] for f in m["files"].values())
print(
json.dumps(
{
"world_size": m["world_size"],
"tensors": len(m["tensors"]),
"weight_bytes": total,
"export_family": (m.get("export") or {}).get("family"),
"resharded_from": m.get("resharded_from"),
"problem": sc.sharded_problem(a.src, lambda p: None),
},
indent=1,
)
)
return 0
def cmd_verify(a) -> int:
m = sc.read_manifest(a.src)
bad = []
for r in range(m["world_size"]):
p = sc.verify_rank_file(a.src, r)
print(f"rank {r}: {'OK' if p is None else p}")
if p:
bad.append(p)
return 1 if bad else 0
def cmd_export(a) -> int:
from align.utils.guard import assert_paths_off_root
assert_paths_off_root([os.path.dirname(os.path.abspath(a.dst))])
res = sc.export_hf(a.src, a.dst, verify=not a.no_verify)
print(json.dumps({**res, "peak_rss_kb": sc.peak_rss_kb()}))
return 0
def cmd_reshard(a) -> int:
from align.utils.guard import assert_paths_off_root
assert_paths_off_root([os.path.dirname(os.path.abspath(a.dst))])
m = sc.read_manifest(a.src)
if not a.drop_optimizer:
est = _optimizer_bytes(a.src, m["world_size"]) / max(1, a.world_size) / 2**30
avail = _mem_available_gib()
print(f"optimizer re-cut peak ~ {est:.1f} GiB; MemAvailable {avail:.0f} GiB", file=sys.stderr)
if not a.allow_large and avail < est + 45:
print(
f"refusing: need MemAvailable >= {est + 45:.0f} GiB (estimate + the 45 GiB box floor) and have "
f"{avail:.0f}. Wait for the box, or pass --allow-large if you accept the risk, or --drop-optimizer "
"(fresh moments; NOT lossless).",
file=sys.stderr,
)
return 1
res = sc.reshard(a.src, a.dst, a.world_size, drop_optimizer=a.drop_optimizer)
print(json.dumps({**res, "peak_rss_kb": sc.peak_rss_kb()}))
return 0
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
sub = ap.add_subparsers(dest="cmd", required=True)
for name in ("inspect", "verify"):
p = sub.add_parser(name)
p.add_argument("src")
p = sub.add_parser("export")
p.add_argument("src")
p.add_argument("dst")
p.add_argument("--no-verify", action="store_true", help="skip the per-file sha256 pass (not recommended)")
p = sub.add_parser("reshard")
p.add_argument("src")
p.add_argument("dst")
p.add_argument("--world-size", type=int, required=True)
p.add_argument("--drop-optimizer", action="store_true", help="omit optimizer files: resume starts FRESH moments (NOT lossless)")
p.add_argument("--allow-large", action="store_true")
a = ap.parse_args(argv)
try:
return {"inspect": cmd_inspect, "verify": cmd_verify, "export": cmd_export, "reshard": cmd_reshard}[a.cmd](a)
except (sc.ShardedCheckpointError, FileExistsError, FileNotFoundError, KeyError) as e:
print(f"{type(e).__name__}: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())