Celsia's picture
CUDA benchmarks on an RTX 5090: checkpoints, agreement runs, shared init weights, and the corrected comparison
eebb8d5 verified
Raw
History Blame Contribute Delete
6.64 kB
"""Compare two `pretrain.py` runs — one machine against another.
python benchmarks/compare.py "MPS bench" "CUDA bench" --out COMPARISON.md
Both runs start from bit-identical weights on bit-identical data, so every
number below is a difference the *arithmetic* produced. What that means
depends on the row:
- **the portable models** (``*_portable_*``, RNNs, conv, attention) run the
same pure-torch code on both machines, so a difference is float
non-associativity — different reduction orders on different hardware.
Expect small and growing-with-steps, not zero.
- **the vendored models** run the authors' *fused* kernels on CUDA and their
reference path on MPS. Those rows are fused-vs-reference, and they are the
ones worth reading: this is the check PLAN.md fixes as the rule for any
fast path.
A loss trajectory is chaotic, so late-step agreement is not the right test —
two runs of the same model can separate simply because gradient descent
amplifies. The honest summary is the *early* divergence and whether both runs
landed in the same place, which is why both are reported.
"""
from __future__ import annotations
import argparse
import json
import math
from pathlib import Path
import torch
def load(path: Path) -> dict:
manifest = json.loads((path / "manifest.json").read_text())
manifest["_dir"] = path
return manifest
def weights_of(run: Path, name: str) -> dict | None:
blob = run / name / "weights.pt"
if not blob.exists():
return None
return torch.load(blob, map_location="cpu", weights_only=True)
def weight_delta(a: dict, b: dict) -> tuple[float, float]:
"""Relative and absolute distance between two trained parameter sets."""
num = 0.0
den = 0.0
worst = 0.0
for key, va in a["model"].items():
vb = b["model"][key]
d = (va.double() - vb.double()).abs()
num += float((d**2).sum())
den += float((va.double() ** 2).sum())
worst = max(worst, float(d.max()))
return (math.sqrt(num / max(den, 1e-30)), worst)
def first_divergence(la: list[float], lb: list[float], tol: float = 1e-6) -> int | None:
"""The first step whose losses differ by more than `tol` relatively.
Says when the two machines stopped agreeing, which is far more informative
than how far apart they ended up: the end of a chaotic trajectory is not a
measurement of anything.
"""
for i, (x, y) in enumerate(zip(la, lb, strict=False)):
scale = max(abs(x), abs(y), 1e-12)
if abs(x - y) / scale > tol:
return i
return None
def _weights_provenance(left: dict, right: dict) -> list[str]:
"""Say — and check — where each run's starting weights came from.
The comparison used to assert that both machines began from bit-identical
weights. For S4 and S4D that was false: `torch.linalg.eigh` fixes
eigenvectors only up to a phase, so their `B` and `P` differ between macOS
and Linux under the same seed, and the comparison reported a 2.6e-01
difference that was two different models rather than two devices.
A claim the instrument cannot check is worth less than one it can, so this
reads what the runs recorded instead of assuming.
"""
def sources(run: dict) -> set[str]:
return {m.get("weights_from", "seed") for m in run["models"] if "error" not in m}
both = sources(left) | sources(right)
if both <= {"written", "loaded"}:
return [
"Both runs load one shared set of starting weights, so the initial",
"conditions are bit-identical and every difference below is arithmetic.",
"",
]
return [
"> **The two runs did not share starting weights** "
f"({', '.join(sorted(both))}). The seed alone does not give identical",
"> S4/S4D weights across platforms — `eigh`'s eigenvectors are fixed only",
"> up to a phase — so differences below may be initialisation rather than",
"> arithmetic. Re-run both sides with `--init <dir>` pointing at the same",
"> directory. See benchmarks/init_weights.py.",
"",
]
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("left")
ap.add_argument("right")
ap.add_argument("--out", default=None, help="write a markdown table here")
args = ap.parse_args()
left, right = load(Path(args.left)), load(Path(args.right))
by_name = {
"left": {m["name"]: m for m in left["models"] if "error" not in m},
"right": {m["name"]: m for m in right["models"] if "error" not in m},
}
shared = [n for n in by_name["left"] if n in by_name["right"]]
lines = [
"# Device comparison",
"",
f"- **{args.left}** — {left['device_name']} · torch {left['torch']}"
f" · torch-dimensions {left['torch_dimensions']}",
f"- **{args.right}** — {right['device_name']} · torch {right['torch']}"
f" · torch-dimensions {right['torch_dimensions']}",
"",
f"Both runs: seed {left['seed']}, {left['steps']} steps, batch {left['batch']}.",
"Data is drawn on CPU from a seeded generator, so both machines see the",
"same batches in the same order.",
"",
*_weights_provenance(left, right),
"| model | loss (left) | loss (right) | Δ loss | first divergence "
"| rel Δw | max Δw | speed |",
"|---|---|---|---|---|---|---|---|",
]
for name in shared:
a, b = by_name["left"][name], by_name["right"][name]
wa, wb = weights_of(left["_dir"], name), weights_of(right["_dir"], name)
if wa and wb:
rel_w, max_w = weight_delta(wa, wb)
wcol, mcol = f"{rel_w:.2e}", f"{max_w:.2e}"
else:
wcol = mcol = "—"
step = first_divergence(a["losses"], b["losses"])
dcol = "identical" if step is None else f"step {step}"
speed = b["steps_per_second"] / max(a["steps_per_second"], 1e-9)
lines.append(
f"| `{name}` | {a['loss_final']:.5f} | {b['loss_final']:.5f} | "
f"{abs(a['loss_final'] - b['loss_final']):.2e} | {dcol} | {wcol} | {mcol} | "
f"{speed:.2f}× |"
)
missing = sorted(set(by_name["left"]) ^ set(by_name["right"]))
if missing:
lines += ["", f"Not in both runs: {', '.join('`' + m + '`' for m in missing)}."]
text = "\n".join(lines) + "\n"
print(text)
if args.out:
Path(args.out).write_text(text)
print(f"wrote {args.out}")
return 0
if __name__ == "__main__":
raise SystemExit(main())