File size: 10,819 Bytes
bcc8d35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
"""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())