Download code/scripts/bench_7b_end_to_end.py from viai957/align: direct link, hf CLI and curl.
- Browser
- Download file 10.8 kB
-
https://huggingface.co/viai957/align/resolve/main/code/scripts/bench_7b_end_to_end.py
- Command line
-
hf download hf://viai957/align/code/scripts/bench_7b_end_to_end.py
-
curl -L -o bench_7b_end_to_end.py https://huggingface.co/viai957/align/resolve/main/code/scripts/bench_7b_end_to_end.py
10.8 kB
| """Benchmark #2: end-to-end step time on the REAL 7B, triton_fa2 vs SDPA. | |
| Run (GPU 3 window; pin BEFORE torch import): | |
| CUDA_VISIBLE_DEVICES=3 TMPDIR=/apps/development/rlhf/nano-mistral-o1/.tmp \ | |
| uv run python scripts/bench_7b_end_to_end.py | |
| WHAT THIS MEASURES, AND WHAT IT DELIBERATELY DOES NOT | |
| ----------------------------------------------------- | |
| A full-parameter 7B training step does NOT fit on one A10G: bf16 weights are | |
| 13.5 GiB and bf16 parameter gradients are another 13.5 GiB, so 27 GiB against a | |
| 23 GiB card. Real SFT gets around this with 4-way FSDP plus an offloaded | |
| optimizer (section-07/11); that is not reproducible inside a single-GPU window, | |
| and pretending otherwise would be measuring a different thing than the label. | |
| So this benchmark freezes every parameter and requires grad on `inputs_embeds` | |
| instead. Backward still flows through every attention and MLP in all 32 layers, | |
| computing the full activation-gradient chain -- dQ/dK/dV included, which is | |
| exactly the part the attention backend controls. What is skipped is the | |
| per-parameter weight-gradient GEMM, which is IDENTICAL for both backends. | |
| The consequence is worth stating precisely, because it decides how to read the | |
| result: the absolute per-step delta (triton minus sdpa, in ms) is exactly what | |
| it would be in a full step, since the omitted work is backend-independent and | |
| additive. The delta as a PERCENTAGE is inflated, because the denominator is | |
| missing that same backend-independent work. **This benchmark therefore reports | |
| an UPPER BOUND on the true percentage cost of choosing triton_fa2.** A number | |
| that bounds the answer from the safe side is worth more here than a number that | |
| requires four GPUs and a training run to obtain. | |
| Both no-checkpointing and activation-checkpointing configurations are measured: | |
| under checkpointing the attention forward runs a second time during backward, | |
| so the backend-attributable share is larger, and SFT is expected to checkpoint. | |
| """ | |
| from __future__ import annotations | |
| import glob | |
| import json | |
| import os | |
| import sys | |
| import time | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| from align.utils.guard import apply_resource_guards | |
| # Light job by the box rules: no heavy lock (other GPU jobs are live), no | |
| # dataloaders, no multi-GB writes. | |
| apply_resource_guards(cpu_threads=8, heavy=False, label="bench-7b-attn") | |
| import torch | |
| from torch.utils.checkpoint import checkpoint as ckpt_fn | |
| from align.model import checkpoint as ckpt | |
| from align.model.mistral import NmoMistralForCausalLM | |
| REPO = "/apps/development/rlhf/nano-mistral-o1" | |
| OUT = f"{REPO}/docs/bench_7b_end_to_end.json" | |
| SNAPSHOT = ( | |
| "/apps/voxtral/.cache/huggingface/hub/" | |
| "models--mistralai--Mistral-7B-v0.3/snapshots/" | |
| "caa1feb0e54d415e2df31207e5f4e273e33509b1" | |
| ) | |
| SEQ = 1024 | |
| BATCH = 1 | |
| #: Hard wall-clock budget. The GPU window is shared and SFT launches the moment | |
| #: the golden gate passes; a benchmark that overstays blocks a training run. | |
| DEADLINE_S = float(os.environ.get("NMO_BENCH_DEADLINE_S", 1500)) | |
| _START = time.time() | |
| def _left() -> float: | |
| return DEADLINE_S - (time.time() - _START) | |
| def _load_7b(attn_impl: str, state_dict, cfg): | |
| model = NmoMistralForCausalLM.from_state_dict(cfg, state_dict, attn_implementation=attn_impl) | |
| model = model.to(torch.bfloat16).to("cuda").eval() | |
| # Freeze: no 13.5 GiB parameter-gradient buffer. See module docstring. | |
| for p in model.parameters(): | |
| p.requires_grad_(False) | |
| return model | |
| def _blocks(model): | |
| inner = model.model if hasattr(model, "model") else model | |
| for name in ("layers", "blocks", "h"): | |
| if hasattr(inner, name): | |
| return getattr(inner, name) | |
| raise AttributeError("cannot find the decoder block list on the model") | |
| def _timed(fn, iters, warmup): | |
| for _ in range(warmup): | |
| fn() | |
| torch.cuda.synchronize() | |
| t0 = time.perf_counter() | |
| for _ in range(iters): | |
| fn() | |
| torch.cuda.synchronize() | |
| return (time.perf_counter() - t0) / iters * 1e3 | |
| def _run_config(model, embeds, labels, use_ckpt: bool, iters: int): | |
| """fwd-only and fwd+bwd timings for one model/checkpointing combination.""" | |
| blocks = _blocks(model) | |
| originals = [b.forward for b in blocks] | |
| if use_ckpt: | |
| for b in blocks: | |
| fwd = b.forward | |
| b.forward = lambda *a, _f=fwd, **kw: ckpt_fn(_f, *a, use_reentrant=False, **kw) | |
| try: | |
| def fwd_only(): | |
| with torch.no_grad(): | |
| model(inputs_embeds=embeds.detach()) | |
| def fwd_bwd(): | |
| if embeds.grad is not None: | |
| embeds.grad = None | |
| logits = model(inputs_embeds=embeds) | |
| loss = torch.nn.functional.cross_entropy( | |
| logits.float().view(-1, logits.shape[-1]), labels.view(-1) | |
| ) | |
| loss.backward() | |
| torch.cuda.reset_peak_memory_stats() | |
| f = _timed(fwd_only, iters, 2) | |
| fb = _timed(fwd_bwd, iters, 2) | |
| peak = torch.cuda.max_memory_allocated() / 2**30 | |
| finally: | |
| # `b.forward = original_bound_method` would leave the instance holding a | |
| # bound method that holds the instance -- a reference cycle the caching | |
| # allocator cannot reclaim on `empty_cache()`, which OOM'd the second | |
| # model load. Deleting the instance attribute restores the class method | |
| # and leaves no cycle. | |
| for b in blocks: | |
| if "forward" in b.__dict__: | |
| del b.__dict__["forward"] | |
| del originals | |
| return {"fwd_ms": f, "fwd_bwd_ms": fb, "peak_gib": peak} | |
| def main() -> int: | |
| if torch.cuda.device_count() != 1: | |
| print( | |
| f"{torch.cuda.device_count()} devices visible; pin exactly one " | |
| "(shared box, three other GPU jobs live)", | |
| file=sys.stderr, | |
| ) | |
| return 1 | |
| free_gib = torch.cuda.mem_get_info()[0] / 2**30 | |
| print(f"device: {torch.cuda.get_device_name(0)} free {free_gib:.1f} GiB") | |
| if free_gib < 20.0: | |
| print( | |
| "refusing: need ~20 GiB free for 13.5 GiB of bf16 weights plus " | |
| "activations; another job is resident on this device.", | |
| file=sys.stderr, | |
| ) | |
| return 1 | |
| from align.eval.golden import resolve_model_config | |
| cfg = resolve_model_config(SNAPSHOT) | |
| print( | |
| f"config: {cfg.n_layers}L hidden={cfg.hidden_size} " | |
| f"{cfg.n_heads}q:{cfg.n_kv_heads}kv head_dim={cfg.head_dim} " | |
| f"vocab={cfg.vocab_size} sliding_window={cfg.sliding_window}" | |
| ) | |
| import safetensors.torch as st | |
| t0 = time.time() | |
| hf_sd = {} | |
| for shard in sorted(glob.glob(os.path.join(SNAPSHOT, "*.safetensors"))): | |
| hf_sd.update(st.load_file(shard)) | |
| nmo_sd = ckpt.hf_to_nmo(hf_sd, expected_n_layers=cfg.n_layers) | |
| del hf_sd | |
| print(f"loaded + remapped {len(nmo_sd)} tensors in {time.time() - t0:.1f}s") | |
| torch.manual_seed(0) | |
| labels = torch.randint(0, cfg.vocab_size, (BATCH, SEQ), device="cuda") | |
| results = {} | |
| # One backend per PROCESS when an arg is given. 13.5 GiB of weights leaves | |
| # no room for a second model, and process exit is the only VRAM release | |
| # that cannot be defeated by a stray reference. Partials are merged below. | |
| only = sys.argv[1] if len(sys.argv) > 1 else None | |
| impls = (only,) if only else ("sdpa", "triton_fa2") | |
| for impl in impls: | |
| if _left() < 240: | |
| print(f"\ndeadline guard: {_left():.0f}s left, skipping {impl}", file=sys.stderr) | |
| break | |
| print(f"\n=== {impl} ===", flush=True) | |
| model = _load_7b(impl, nmo_sd, cfg) | |
| embeds = torch.randn( | |
| BATCH, SEQ, cfg.hidden_size, device="cuda", dtype=torch.bfloat16, requires_grad=True | |
| ) | |
| results[impl] = {} | |
| for use_ckpt in (False, True): | |
| if _left() < 120: | |
| print( | |
| f"deadline guard: {_left():.0f}s left, skipping {impl}/ckpt={use_ckpt}", | |
| file=sys.stderr, | |
| ) | |
| break | |
| r = _run_config(model, embeds, labels, use_ckpt, iters=5) | |
| key = "activation_ckpt" if use_ckpt else "no_ckpt" | |
| results[impl][key] = r | |
| print( | |
| f" {key:16s} fwd {r['fwd_ms']:8.1f} ms " | |
| f"fwd+bwd {r['fwd_bwd_ms']:8.1f} ms peak {r['peak_gib']:5.2f} GiB", | |
| flush=True, | |
| ) | |
| del model, embeds | |
| import gc | |
| gc.collect() | |
| torch.cuda.empty_cache() | |
| # Merge any partial written by a sibling process, so running one backend | |
| # per process still produces a single complete receipt. | |
| partial = f"{REPO}/.tmp/bench7b_partial.json" | |
| if only: | |
| prev = {} | |
| if os.path.isfile(partial): | |
| with open(partial) as f: | |
| prev = json.load(f) | |
| prev.update(results) | |
| with open(partial, "w") as f: | |
| json.dump(prev, f, indent=2) | |
| results = prev | |
| print(f"\npartial results for {sorted(results)} -> {partial}") | |
| payload = { | |
| "note": "parameters FROZEN; grad flows via inputs_embeds. Absolute ms " | |
| "deltas are exact; percentages are an UPPER BOUND on the true " | |
| "cost, because the omitted weight-grad GEMMs are " | |
| "backend-independent and would enlarge the denominator only. " | |
| "See the module docstring.", | |
| "device": torch.cuda.get_device_name(0), | |
| "torch": torch.__version__, | |
| "model": "mistralai/Mistral-7B-v0.3 (real weights)", | |
| "batch": BATCH, | |
| "seq": SEQ, | |
| "dtype": "bfloat16", | |
| "results": results, | |
| } | |
| if "sdpa" in results and "triton_fa2" in results: | |
| payload["comparison"] = {} | |
| print("\n=== triton_fa2 vs sdpa on the real 7B ===") | |
| for key in ("no_ckpt", "activation_ckpt"): | |
| if key in results["sdpa"] and key in results["triton_fa2"]: | |
| s = results["sdpa"][key]["fwd_bwd_ms"] | |
| t = results["triton_fa2"][key]["fwd_bwd_ms"] | |
| c = { | |
| "sdpa_ms": s, | |
| "triton_ms": t, | |
| "delta_ms": t - s, | |
| "ratio": t / s, | |
| "upper_bound_pct": 100.0 * (t - s) / s, | |
| } | |
| payload["comparison"][key] = c | |
| print( | |
| f" {key:16s} sdpa {s:8.1f} ms -> triton {t:8.1f} ms " | |
| f"({c['delta_ms']:+.1f} ms, {c['upper_bound_pct']:+.1f}% " | |
| "UPPER BOUND)" | |
| ) | |
| os.makedirs(os.path.dirname(OUT), exist_ok=True) | |
| with open(OUT, "w") as f: | |
| json.dump(payload, f, indent=2) | |
| f.write("\n") | |
| print(f"\nwrote {OUT} ({_left():.0f}s of window left)") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |