Skip to content

Latest commit

 

History

History
381 lines (302 loc) · 42 KB

REPORT.md

File metadata and controls

381 lines (302 loc) · 42 KB

Fleet megakernel decode, DeepSeek-Coder-V2-Lite-Base, MI300X: results

Generated by scripts/report.py from the evidence under results/ and the hand-written results/status.yaml. Design: docs/design-final-fixed.md; every GPU session, failure and fix in order: docs/gpu-log.md. The box copy of the repo had no .git, so the JSONs' git field is empty; each file is labelled with its evidence commit, the last project commit that changed it.

Milestone reached

M4, end-to-end 32 tokens (results/status.yaml; milestones as in design §6). The whole decode token runs inside one persistent megakernel launch for 32 tokens; 32/32 greedy tokens match HF, identical on all 5 timed launches. The teacher-forced logit cosine and per-step routing gates fail at M4 and are reported as failing, not relaxed (see End to end).

headline value evidence
correctness error layers 0-26 in one launch: worst layer rel 1.56e-02 (layer 13), min cosine 0.999912; logits cosine 0.999965, argmax 1780 (HF 1780) results/perf/dag_g1_layers/layer_0-26.json
32-token decode 32/32 greedy tokens = HF; teacher-forced argmax equal at 32/32 steps; logit cosine < 0.9995 at step 12 and step 14 and step 22 and step 23 results/m4/e2e.json
latency P50 3.759 / P95 3.770 ms per token (iterations ≥ 2); launch-level P50 3.928 ms results/bench/bench.json
tokens/s 266.0 (HF eager on the same GPU: 62.91 ms/token) results/bench/bench.json
memory traffic, bandwidth 5.24 GB/token, 1.39 TB/s results/bench/bench.json
GPU launches 1 mpk() call per 32 tokens = 3 kernel dispatches, 0.094 per token (HF eager: 3,853) results/profile/rocprof.json

Reference: HF eager bf16 on AMD Instinct MI300X VF, torch 2.14.0+rocm7.2 / hip 7.2.53211, P=1024 prompt tokens, 32 greedy steps, HF eager median 69.35 ms/token.

GPU (results/m0/check_gpu.txt): device0 AMD Instinct MI300X VF arch=gfx942:sramecc+:xnack- CUs=304 mem=191.7 GiB; copy 4 GB: best 3917 GB/s, median 3870 GB/s (peak 5300; expect ~4000-4300); read 4 GB (sum): best 4160 GB/s, median 3604 GB/s

Correctness error

Gates (design §5.2): per op max|d| ≤ 2 % of max|ref| + 1e-3 (on the megakernel's own inputs); per layer 5 % and cosine ≥ 0.999; K/V cache rows 2 %; teacher-forced logits cosine ≥ 0.9995, argmax equal unless HF's top-2 margin < 0.1; routing ids exact, or a listed near-tie escape (HF's swap margin between the dropped and the added expert < the measured gate-logit error).

Layers 0-26 in one launch, final kernel -> PASS

results/perf/dag_g1_layers/layer_0-26.json, git 2b4ac23, 2026-09-14T20:16:21: scripts/run_layer.py --layers 0-26 --event-timing --out results/perf/dag_g1_layers; graph 20,494 tasks / 347 events / 298 ops; one launch 35.2 ms. Layer 0 receives HF's input, every later layer the megakernel's own output.

layer out max|d| at max|ref| rel cosine K row rel V row rel routing ids (top-6) ok
0 0.00195 at 1.828 1.07e-03 0.999997 4.71e-08 0.00e+00 - ok
1 0.00391 at 2.391 1.63e-03 0.999989 3.93e-04 4.03e-03 [30, 6, 45, 16, 40, 33] ok
2 0.00391 at 3.125 1.25e-03 0.999984 1.28e-03 1.04e-02 [44, 59, 35, 37, 60, 53] ok
3 0.00781 at 3.641 2.15e-03 0.999980 2.76e-03 8.85e-03 [49, 3, 27, 21, 36, 20] ok
4 0.00781 at 4.688 1.67e-03 0.999983 1.03e-03 1.26e-02 [48, 40, 7, 10, 36, 37] ok
5 0.0312 at 6.469 4.83e-03 0.999982 8.28e-04 7.89e-03 [25, 44, 35, 28, 42, 11] ok
6 0.00781 at 7.875 9.92e-04 0.999979 1.71e-03 1.87e-02 [27, 41, 59, 20, 25, 63] ok
7 0.0156 at 8.125 1.92e-03 0.999972 1.81e-03 1.73e-02 [5, 13, 61, 46, 28, 2] ok
8 0.0156 at 7.938 1.97e-03 0.999962 1.38e-03 1.49e-02 [52, 48, 13, 1, 26, 49] ok
9 0.0156 at 10.56 1.48e-03 0.999973 1.98e-03 1.52e-02 [31, 0, 51, 10, 16, 59] ok
10 0.0156 at 12.62 1.24e-03 0.999972 2.28e-03 1.11e-02 [57, 61, 42, 45, 14, 7] ok
11 0.125 at 16.12 7.75e-03 0.999961 2.31e-03 1.44e-02 [52, 49, 2, 58, 15, 61] ok
12 0.188 at 15.56 1.20e-02 0.999935 3.02e-03 1.18e-02 [8, 56, 22, 23, 52, 35] ok
13 0.25 at 16 1.56e-02 0.999912 5.13e-03 9.80e-03 [15, 61, 53, 35, 45, 54] ok
14 0.125 at 17.88 6.99e-03 0.999948 4.37e-03 1.28e-02 [19, 27, 23, 59, 55, 46] ok
15 0.125 at 19.38 6.45e-03 0.999962 4.88e-03 9.89e-03 [52, 32, 24, 48, 25, 37] ok
16 0.125 at 20.62 6.06e-03 0.999959 2.76e-03 1.35e-02 [60, 37, 33, 9, 40, 53] ok
17 0.125 at 24.62 5.08e-03 0.999967 7.30e-03 1.48e-02 [10, 46, 58, 50, 26, 28] ok
18 0.25 at 25 1.00e-02 0.999957 2.28e-03 9.84e-03 [2, 49, 14, 32, 30, 58] ok
19 0.25 at 24.25 1.03e-02 0.999958 5.35e-03 9.04e-03 [44, 35, 8, 60, 2, 42] ok
20 0.25 at 28 8.93e-03 0.999966 3.06e-03 1.28e-02 [3, 38, 23, 62, 41, 2] ok
21 0.25 at 29.12 8.58e-03 0.999959 3.69e-03 1.39e-02 [58, 26, 15, 17, 0, 62] ok
22 0.25 at 35 7.14e-03 0.999960 6.80e-03 1.15e-02 [43, 14, 42, 8, 54, 5] ok
23 0.25 at 45 5.56e-03 0.999968 4.24e-03 1.58e-02 [58, 10, 28, 34, 45, 0] ok
24 0.25 at 52.25 4.78e-03 0.999968 4.42e-03 1.20e-02 [23, 63, 7, 31, 38, 54] ok
25 0.5 at 92 5.43e-03 0.999983 5.66e-03 1.98e-02 [19, 55, 3, 13, 24, 46] ok
26 1 at 132 7.58e-03 0.999987 3.13e-03 1.09e-02 [43, 29, 33, 25, 40, 0] ok

Logits: cosine 0.999965, max|d| 0.188 at |ref| 27.62. Argmax 1780 (HF 1780, HF top-2 margin 6.75), next token written on device: 1780.

End to end, 32 tokens

results/m4/e2e.json, git 2b4ac23, 2026-09-14T20:17:02: scripts/run_e2e.py --event-timing; graph 20,494 tasks / 347 events / 298 ops. The JSON's overall pass is false: it is the AND of the gates below, and two of them fail; the gates were not relaxed.

gate result
free-running greedy tokens = HF PASS: 32/32 tokens match, first divergence none
determinism PASS: identical tokens on all 5 launches
teacher-forced argmax = HF PASS: equal at 32/32 steps (HF top-2 margins at the cosine misses: 0.50, 7.25, 11.88, 9.25)
teacher-forced logit cosine ≥ 0.9995 FAIL: step 12 0.999128 (max|d| 0.438 at |ref| 25); step 14 0.998947 (max|d| 1.8 at |ref| 32.75); step 22 0.997707 (max|d| 1.97 at |ref| 43); step 23 0.999146 (max|d| 1.22 at |ref| 32.25); the other 28 steps pass, lowest 0.999769 (step 2)
per-step routing = HF's router FAIL: 755/832 MoE layer-steps exact, 76 near-tie escapes, 1 rejected (step 5 layer 23: swap margin 0.033 > gate-logit error 0.024)

Tokens: [1780, 185, 300, 23689, 403, 22961, 575, 27988, 3028, 207, 16, 15, 185, 300, 565, 23689, 1879, 207, 16, 15, 25, 185, 391, 972, 23689, 570, 207, 16, 185, 300, 972, 16145] (identical to HF's). Timing of the same run: iterations ≥ 2 P50 3,727 µs / P95 3,738 µs (n = 155), iteration 1 P50 5,375 µs, launch-level P50 3.894 ms/token.

Routing detail: 7 steps have every layer exact and 25 have at least one escape; escapes also occur at 21 steps whose cosine passes, so the escape rule alone does not show that the dips are routing. By the escape rule of tests/test_sim.py::routing_check (a step is explained if it has at least one accepted escape and no rejected mismatch): step 12 explained, step 14 explained, step 22 explained, step 23 explained.

Forced-routing counterfactual (results/m4/routing_counterfactual.json, evidence commit 826f4ef: scripts/routing_counterfactual.py): for every step HF eager is rerun with HF's own cache rows and token; the control pass reproduces HF's logits bit-exactly at 32/32 steps. Then every MoE gate is forced to the megakernel's top-6 ids (with HF's weights), and to the megakernel's ids and weights:

step HF with our ids vs HF HF with our ids + weights vs HF megakernel vs HF with our ids megakernel vs HF with our ids + weights
12 1.000000 0.999618 0.999905 0.999849
14 0.998877 0.998894 0.999977 0.999981
22 0.998060 0.996574 0.999047 0.999607
23 0.999861 0.999867 0.999968 0.999971

HF takes the dips once it takes the megakernel's routing, and the megakernel agrees with that HF run at ≥ 0.9995 at both steps (lowest over the other 28 steps: 0.999659). Reading (docs/gpu-log.md session 5): step 14 = near-tie swaps at layers 15 and 26; step 22 = a near-tie at layer 2 that cascades through 13 more layers, with the rejected layer-16 swap downstream of it. The logit-cosine and routing gates stay as written and are reported as failing.

History: every kernel change re-validated

run kernel state tasks gate worst layer rel min layer cosine logits cosine, argmax evidence
layer 1 first kernel: superset experts (D2), per-row GEMV, lm_head 320 × 320 rows 5,684 PASS, 13/13 per-op 8.17e-04 (L1) 0.999996 - results/m2/layer_1-1.json (evidence commit 4c4d156)
layers 1-3 first kernel, 3 layers chained 16,202 PASS 4.29e-03 (L3) 0.999982 - results/m3/layer_1-3.json (evidence commit 04cbecc)
layers 0-26 first kernel, whole token 137,946 PASS 1.56e-02 (L13) 0.999933 0.999940, 1780 (HF 1780) results/m3/layer_0-26.json (evidence commit 04cbecc)
layer 1 + moe_topk wave-parallel 5,684 PASS, 13/13 per-op 8.17e-04 (L1) 0.999996 - results/perf/step1_layer_1-1.json (evidence commit 2fbd862)
layer 1 + vectorised GEMV (bit-identical), lm_head 1,600 × 64 (D10b) 6,964 PASS, 13/13 per-op 8.17e-04 (L1) 0.999996 - results/perf/step2_layer_1-1.json (evidence commit 2fbd862)
layer 1 + slot-indexed experts (D10a), gate + top-k one task (D10c) 2,555 PASS, 13/13 per-op 8.17e-04 (L1) 0.999996 - results/perf/step3_layer_1-1.json (evidence commit 2fbd862)
layer 1 + attention PV rewrite (D10d) 2,555 PASS, 13/13 per-op 8.17e-04 (L1) 0.999996 - results/perf/step4_layer_1-1.json (evidence commit 2fbd862)
layers 0-26 steps 1-4, whole token 24,592 PASS 1.20e-02 (L12) 0.999951 0.999978, 1780 (HF 1780) results/perf/step5_layer_0-26.json (evidence commit 2fbd862)
layer 1 + Fleet stream-leak patch; a second init_func before the launch 2,555 PASS 8.17e-04 (L1) 0.999996 - results/m2/s5_reinit/layer_1-1.json (evidence commit 231a35e)
layers 0-26 + lm_head 200 × 512 rows (Q-H) 23,192 PASS 1.20e-02 (L12) 0.999951 0.999978, 1780 (HF 1780) results/perf/s5_lm512/layer_0-26.json (evidence commit b7f0e5e)
layer 1 1,155 PASS, 13/13 per-op 8.17e-04 (L1) 0.999996 - results/perf/gate_l1/layer_1-1.json (evidence commit db09ccd)
layer 1 + expert tile sweep: W13 32 × 7 tiles (11a) 1,071 PASS, 13/13 per-op 8.17e-04 (L1) 0.999996 - results/perf/sweep_gate_l1/layer_1-1.json (evidence commit 7b0a445)
layers 0-26 + expert tile sweep (11a), whole token 21,008 PASS 1.20e-02 (L12) 0.999951 0.999978, 1780 (HF 1780) results/perf/sweep_gate_l0-26/layer_0-26.json (evidence commit 7b0a445)
layer 1 + all three op folds (11b, 8 ops per MoE layer; not merged: slower) 1,135 PASS, 9/9 per-op 8.17e-04 (L1) 0.999998 - results/perf/fold_l1/layer_1-1.json (evidence commit 7b0a445)
layers 0-26 11a + all three op folds, whole token (not merged: slower) 20,494 PASS 1.56e-02 (L13) 0.999912 0.999965, 1780 (HF 1780) results/perf/foldsw_l0-26/layer_0-26.json (evidence commit 7b0a445)
layers 0-26 11a + mla_prep folded into KV_B_PROJ / ATTN_CHUNK (11b) 20,981 PASS 1.20e-02 (L12) 0.999951 0.999978, 1780 (HF 1780) results/perf/mla_l0-26/layer_0-26.json (evidence commit 7b0a445)
layer 1 1,070 PASS 8.17e-04 (L1) 0.999996 - results/perf/dag_s1a_l1/layer_1-1.json (git 4425efc)
layers 0-26 DAG (12a) + L6 folds + kernel switches F1/F3/F2 (FLEET_KNOBS=dag=1,fold=1), session 8 20,494 PASS 1.56e-02 (L13) 0.999912 0.999965, 1780 (HF 1780) results/perf/dag_g1_layers/layer_0-26.json (git 2b4ac23)

Per op, layer 1 on the first kernel (results/m2/layer_1-1.json, evidence commit 4c4d156: scripts/run_layer.py --layers 1 --torch-fallback --event-timing), each op against torch on the megakernel's own inputs:

op max|d| / rel / cosine, or routing ok
RMS_NORM 0.0312 / 1.7e-03 / 0.999996 ok
QKV_PROJ 5.96e-08 / 3.0e-09 / 1.000000 ok
MLA_PREP.q_rope 0 / 0.0e+00 / 1.000000 ok
MLA_PREP.c_kvn 0 / 0.0e+00 / 1.000000 ok
MLA_PREP.k_pe 0 / 0.0e+00 / 1.000000 ok
KV_B_PROJ.K 0 / 0.0e+00 / 1.000000 ok
KV_B_PROJ.V 0 / 0.0e+00 / 1.000000 ok
ATTN_CHUNK+MERGE 0.00195 / 5.3e-03 / 0.999998 ok
O_PROJ_RESIDUAL 0 / 0.0e+00 / 1.000000 ok
RMS_NORM.2 0.00781 / 5.0e-03 / 0.999996 ok
MOE_GATE 3.58e-07 / 1.5e-07 / 1.000000 ok
MOE_TOPK ids exact [6, 16, 30, 33, 40, 45], HF 6th-7th gap 3.4e-03 ok
EXPERT_W13+W2+COMBINE 0.000488 / 2.0e-04 / 1.000000 ok

Fleet-native ops

Every op of the decode token is a task inside the one persistent megakernel launch. Tasks per token of the final graph by registered task type, from the local builder (python -m fleet_ds.graph 1056, lm_head 512-row tiles; op → task type from fleet_ds.mirage_graph.TASK):

task type kernel ops per token tasks per token graph ops
moe_expert_w13_silu ours (kernels/) 26 4,992 EXPERT_W13_SILU
moe_expert_w2 ours (kernels/) 26 4,992 EXPERT_W2
gemv_bf16 ours (kernels/) 108 4,299 Q_PROJ, KVA_PROJ, KV_B_PROJ, EXPERT_W2_SHARED, LM_HEAD
attn_chunk_192_128 ours (kernels/) 27 3,888 ATTN_CHUNK
gemv_bf16_silu ours (kernels/) 27 1,174 GATE_UP_SILU, EXPERT_W13_SHARED
gemv_bf16_res ours (kernels/) 27 864 O_PROJ_RESIDUAL
gemv_splitk ours (kernels/) 1 128 DOWN_SPLITK
argmax_partial Fleet (ref clone) 1 100 ARGMAX_PARTIAL
moe_gate_topk ours (kernels/) 26 26 MOE_GATE_TOPK
moe_combine ours (kernels/) 26 26 MOE_COMBINE_RESIDUAL
embedding Fleet (ref clone) 1 1 EMBEDDING
splitk_reduce_mi300 Fleet (ref clone) 1 1 REDUCE_RESIDUAL
argmax_reduce Fleet (ref clone) 1 1 ARGMAX_REDUCE
TERMINATE, BEGIN_TASK_GRAPH Fleet runtime (control, no work) - 2 -
total 298 20,494

Fleet's own kernels: 103 tasks in 4 ops per token; ours: 20,389 tasks in 294 ops; 0 empty tasks (slot-indexed experts, D10a). tests/test_mirage_graph.py asserts this builder equal, op for op, to fleet_ds.mirage_graph.layer_plan() (the op list the Mirage build consumes), and tests/test_graph.py asserts both equal to design §2.4-2.5 (20,492 work tasks, 298 ops). Cross-check with the Mirage graph compiled on the MI300X for the final run (results/m4/e2e.json): 20,494 tasks / 298 ops, equal to this builder's 20,494 / 298 (both count TERMINATE and BEGIN), and 347 events.

Remaining fallbacks

  • HF eager prefill + one-time KV-cache copy into the arena (by design, design-final-fixed.md 4.2; excluded from decode timing)
  • No decode op runs in torch or on the host (table above). --torch-fallback in the layer-1 runs is an A/B diagnostic (the same arena through fleet_ds.reference on the GPU, used for the per-op gates), not a serving path.

Consecutive layers completed

27 layers pass the per-layer gate consecutively (0-26) across all layer runs; on the final kernel layers 0-26 run chained in one launch (results/perf/dag_g1_layers/layer_0-26.json). Error grows with depth with the residual stream: layer 0 max|ref| 1.83, rel 1.07e-03; layer 26 max|ref| 132, rel 7.58e-03; the worst layer, 1.56e-02, is 2.0 bf16 ulps (2^-7 relative).

GPU launches

  • Megakernel: one mpk() call per 32 tokens (0.03125 per token); each call is prepare_kernel + worker_kernel + scheduler_kernel = 3 dispatches = 0.094 per token (launch_persistent_kernel; results/bench/bench.json, git 2b4ac23).
  • Measured by rocprof --hip-trace around bench.py (results/profile/rocprof.json, evidence commit 1eeebfa): 3 mpk() calls, 9 megakernel dispatches = 3.0 per call (worker_kernel, scheduler_kernel, prepare_kernel); 16,935 HIP dispatches in the whole traced process, torch model load included.
  • HF eager baseline: 3,853 device kernel launches per decode token (torch.profiler CUDA events, 8 steps; results/bench/hf_baseline.json, evidence commit 877474c; 124.2 ms/token under the profiler). Design estimate: ~2,800-3,000 + 26 host syncs per token.

Median and P95 latency

results/bench/bench.json, git 2b4ac23: scripts/bench.py.

  • Per iteration (= per token), iterations ≥ 2 of every launch (MPK_EVENT_TIMING, 3,100 samples): P50 3.759 ms, P95 3.770 ms, min 3.733.
  • Iteration 1 of each launch (the schedulers' pass 1 over all tasks; stated separately, D10e): P50 5.389 ms, P95 5.628 ms.
  • Launch-level (wall time of the launch call ÷ 32, includes prepare_kernel and iteration 1; 100 launches × 32 tokens after 20 warm-up): P50 3.928 ms/token, P95 3.937, min 3.917.
  • HF eager on the same GPU: 62.91 ms/token (reference_prefill.py).
  • Design §7 before measurement: target 2.0-2.5 ms, floor 0.98 ms at 5.3 TB/s, practical floor 1.2-1.3 ms.

Memory traffic

Bytes read or written per token, counted from the attached tensors (KV cache at T = 1,056 rows): weights 4.903 GB + KV cache 0.292 GB + metadata 45 MB = 5.24 GB (design §4.3: 5.2 GB). Hardware counters (FETCH_SIZE / WRITE_SIZE) are not collected: rocprof --pmc serialises dispatches and Fleet's split worker/scheduler launch deadlocks under it. Corrected here: bench.json counted 4.84 GB because its counter skipped the DAG's W_q / W_kva views of the attention input projection (27 × 3,648 × 2,048 × 2 B = 0.40 GB, still read every token); fixed in scripts/bench.py.

Achieved bandwidth

1.39 TB/s = bytes per token ÷ P50 iteration time = 5.24 GB / 3.759 ms, 26 % of the 5.3 TB/s peak. Derived, not counted (see Memory traffic).

Occupancy / GPU resource usage

Compiled resources per megakernel entry point from the gfx942 code object's notes (llvm-readelf --notes, results/profile/codeobj/notes.txt), dispatches and durations from the rocprof trace (results/profile/rocprof.json, evidence commit 1eeebfa: scripts/bench.py --rocprof results/profile/trace --out results/profile). The profiled build is session 5's optimised kernel before the 512-row lm_head retile (1,600 × 64-row tiles); the final build's code object was not re-read before the VM was deleted.

kernel dispatches per mpk() call P50 duration per call VGPR AGPR SGPR VGPR / SGPR spills static LDS scratch per lane
worker_kernel 1 157.7 ms 255 0 106 0 / 57 1,984 B 64 B
scheduler_kernel 1 157.6 ms 68 0 106 0 / 29 4 B 608 B
prepare_kernel 1 3.6 µs 51 0 50 0 / 0 0 B 64 B

The session-4 build (results/profile/codeobj/resources.json, evidence commit 877474c): worker_kernel VGPR 247, SGPR 106, 59 SGPR spills; scheduler_kernel VGPR 68, 29 SGPR spills.

Occupancy: the persistent launch is worker_kernel<<<296,256,57 KB>>> + scheduler_kernel<<<8,128>>> on 304 CUs (design §3.4): one 256-thread workgroup per CU = 4 waves per CU = 1 wave per SIMD, set by the launch; the worker kernel's 255 VGPRs would allow 2 waves per SIMD. 57 KB dynamic LDS per workgroup. Work per worker: 69 tasks per token on the final graph.

Where the remaining time goes, final configuration: diagnostic -DMPK_ENABLE_TIMING build (results/perf/dag_kept_timing/probe.json, git 2b4ac23: scripts/dispatch_probe.py --timing --runs 3 --out results/perf/dag_kept_timing), lane 0 of workers 0-7, 3 × 32-token launches of the 20,494-task DAG, tokens matching [32, 32, 32]; the flag costs 5.6 % (3,952 vs 3,742 µs per iteration, results/perf/dag_b2b_kept/probe.json), so read the shares, not the absolute time. Dependency wait is a worker idle until the op before it signals, so it includes that op's execution on other workers; the chain's timing build (next paragraph) alongside.

worker state final µs per task share chain µs per task share
waiting on the previous op (dependency counter) 37.01 60.9 % 46.31 68.9 %
executing the task 19.19 31.6 % 16.73 24.9 %
waiting on the scheduler (empty-queue poll) 3.73 6.1 % 3.15 4.7 %
signalling the next event 0.85 1.4 % 0.99 1.5 %

2,216 tasks per worker per launch = 69 per iteration. Execution is a third of worker time, so faster kernels alone do not reach the 2.5 ms target; the remaining levers are runtime-level (Recommended next steps).

Where a worker's time went on the chain: diagnostic -DMPK_ENABLE_TIMING build (results/perf/timing/probe.json, evidence commit 1d9f630: scripts/dispatch_probe.py --timing --out results/perf/timing), lane 0 of workers 0-7, 3 × 32-token launches of the 24,592-task graph (64-row lm_head tiles); the flag costs 10.8 % (5,321 vs 4,804 µs per iteration on the same graph, results/perf/lm64/probe.json), so this is not the benchmarked build.

clock64 calibrated at 1,923 cycles/µs against launch wall time (a lower bound: descriptor loads are untimed); 2,659 tasks per worker per launch = 83 per iteration. The graph dispatches once per iteration, striped over 8 schedulers × 37 workers each (design §7, docs/gpu-log.md session 5); tasks wait in the queues on their dependency counter, so the steady-state scheduler pass is not the floor. Scheduler pass 1 (iteration 1 only): 1.67 µs per task per scheduler, from iteration 1 of the 24,592- and 20,981-task probes.

Barrier cost per op boundary: median span of the 1-task RMS_NORM op, 3.3 µs over its 55 instances (results/perf/final/probe.json, evidence commit 7b0a445; it includes the norm's own compute, so it bounds the event round trip from above). docs/gpu-log.md session 5 takes ≈ 3.5 µs from the 1-task ops (RMS_NORM 3.1-3.7, ARGMAX_REDUCE 4.7).

Time per output token and tokens/s

3.759 ms/token (P50 per iteration) = 266 tokens/s; launch-level P50 3.928 ms/token (results/bench/bench.json, git 2b4ac23).

Where the time goes

Per op type per token on the session-6 chain graph, 11a + mla_prep fold (before the DAG) (results/perf/final/probe.json, evidence commit 7b0a445: scripts/dispatch_probe.py --out results/perf/final_probe; flag-free build, 1 warm-up + 3 × 32-token launches; each event is labelled by the op owning its first producer task; P50 per op over iterations ≥ 2, summed over the op's instances). Iteration P50 of this run: 4,276 µs (bench.py on the same kernel, docs/gpu-log.md session 6: 4,309 µs).

op type tasks per instance instances per token µs per token % of P50 iteration
EXPERT_W13_SILU 224 26 1,084 25.3 %
EXPERT_W2 224 26 553 12.9 %
MOE_GATE_TOPK 1 26 445 10.4 %
ATTN_CHUNK 144 27 427 10.0 %
QKV_PROJ 57 27 409 9.6 %
O_PROJ_RESIDUAL 32 27 387 9.1 %
KV_B_PROJ 64 27 315 7.4 %
RMS_NORM 1 55 186 4.3 %
MOE_COMBINE_RESIDUAL 1 26 148 3.5 %
LM_HEAD 200 1 129 3.0 %
ATTN_MERGE 16 27 94 2.2 %
GATE_UP_SILU 342 1 49 1.2 %
DOWN_SPLITK 128 1 23 0.5 %
EMBEDDING 1 1 13 0.3 %
REDUCE_RESIDUAL 1 1 5 0.1 %
ARGMAX_PARTIAL 100 1 5 0.1 %
ARGMAX_REDUCE 1 1 5 0.1 %
sum 301 4,276 100.0 %

Barrier estimate (inside the rows above): 301 boundaries × 3.3 µs ≈ 1.01 ms per token (24 % of the iteration); with gpu-log's 3.5 µs, ≈ 1.05 ms. bench.json's boundary_us_p50 is not used here: its labels are shifted by one op after layer 0 (Bugs, below), so its ≈ 53 µs "L*.EXPERT_W2" entries are EXPERT_W13_SILU.

Performance round: before and after

Per-op numbers are layer 1 of a one-layer launch, one iteration (a few µs of noise; EXPERT_W13_SILU read 184-337 µs across the runs before step 3). "Before" is the last run in which that op's kernel was unchanged. Token numbers are P50 / P95 over iterations ≥ 2.

step change decision revised op µs, before → after gate ms per token P50 / P95 evidence
0 first working kernel: superset experts (5,259 tasks per MoE layer), per-row GEMV, lm_head 320 × 320 rows - one token incl. pass 1: 30,990 µs layers 0-26 PASS 19.31 / 19.84 docs/gpu-log.md session 3 (bench.py killed by its timeout after 1,080 recorded iterations, no JSON); results/m3/layer_0-26.json
1 moe_topk.cuh wave-parallel (6 rounds of wave max over 64 lanes) none (same outputs) MOE_TOPK 88.6 → 7.9 layer 1 PASS, 13/13 per-op, rel 8.17e-04 - results/perf/step1_layer_1-1.json
2 gemv_bf16.cuh vectorised (4 rows per wave in flight, bit-identical); lm_head 1,600 × 64-row tiles D7g → D10b O_PROJ_RESIDUAL 35.7 → 14.7, MOE_GATE 33.8 → 12.8 layer 1 PASS, 13/13 per-op, rel 8.17e-04 - results/perf/step2_layer_1-1.json
3 slot-indexed expert tasks (expert = idx[slot] read at run time); gate + top-k one task D2 → D10a; D10c EXPERT_W13_SILU 183.8 → 53.3, EXPERT_W2 281.1 → 20.6, gate + top-k 20.3 → 16.1 layer 1 PASS, 13/13 per-op, rel 8.17e-04 - results/perf/step3_layer_1-1.json
4 attn_chunk.cuh PV loop: lanes over tokens, 16 B V loads issued together D10d (reordering inside the gates) ATTN_CHUNK 40.2 → 14.9 layer 1 PASS, 13/13 per-op, rel 8.17e-04 - results/perf/step4_layer_1-1.json
5 steps 1-4 together, whole token - one token incl. pass 1: 30,990 → 6,731 µs layers 0-26 PASS, worst rel 1.20e-02, logits 0.999978 4.832 / 4.847 results/perf/step5_layer_0-26.json, results/perf/bench/bench.json
6 lm_head 200 × 512-row tiles, the fastest of a 64-1,024-row sweep (Profiling, below) D10b → Q-H LM_HEAD 151.8 → 129.3 (whole-token probes) layers 0-26 PASS (same numbers), 32/32 tokens 4.796 / 4.811 results/perf/s5_lm512/layer_0-26.json, docs/gpu-log.md session 5
7 expert tile sweep: EXPERT_W13_SILU 32 × 7 tiles (88 / 176 rows), best of 22/32/44 × W2 16/32 8c/8e → 11a EXPERT_W13_SILU 52.3 → 41.6 (whole-token probes, mean of layers 1-26) layers 0-26 PASS 4.485 / 4.496 (probe) results/perf/sweep_w13_32_w2_32/probe.json, results/perf/sweep_gate_l0-26/layer_0-26.json
8a all three op folds (RMSNorm, mla_prep, attn_merge into the next op's prologue; 8 ops per MoE layer) — not merged 11b O_PROJ 14.5 → 28.8, MOE_GATE_TOPK 17.1 → 22.4, QKV 15.0 → 19.5 layers 0-26 PASS 4.617 / 4.630 (probe, slower) results/perf/foldsw_probe/probe.json
8 mla_prep folded: kv_a norm + k_pe RoPE in the KV_B_PROJ tiles, q RoPE in ATTN_CHUNK (11 ops per MoE layer) 11b MLA_PREP 10.5 → 0, KV_B_PROJ 10.2 → 11.6, ATTN_CHUNK 15.1 → 15.5 layers 0-26 PASS, 32/32 tokens (session-6 e2e) 4.309 / 4.321 (bench) results/perf/mla_l0-26/layer_0-26.json, results/perf/final/probe.json, results/bench/bench.json as of docs/gpu-log.md session 6 (overwritten by row 9's bench)
9 the token as an un-serialised DAG (dag=1) + RMSNorm / attention-merge folds (fold=1) + kernel switches F1/F3/F2 (DSK_COMBINE_BLOCK=1 DSK_NORM_FIXED=1 DSK_MERGE_FIXED=2) 12a; 12e-g MoE layer wall 151.8 → 132.5; ops / events per token 301 / 403 → 298 / 347 (back-to-back probes) layers 0-26 PASS, 32/32 tokens 3.759 / 3.770 (bench); back to back 4,250 → 3,741 µs (chain → this row, median of 3 probes) results/perf/dag_g1_layers/layer_0-26.json, results/bench/bench.json, results/perf/dag_session.tsv, results/perf/dag_b2b_chain/probe.json, results/perf/dag_b2b_kept/probe.json

Known failures

  • Teacher-forced logit-cosine gate (0.9995) fails at 4 of 32 steps on the DAG kernel (session 8, results/m4/e2e.json): 12 (0.999128), 14 (0.998951), 22 (0.997714), 23 (0.999138); argmax is equal at all 32 steps and the free-running 32 tokens match HF. The chain kernel missed at 14 (0.99873) and 22 (0.99546) only. Explained by routing near-ties, verified by counterfactual (results/perf/dag_g2_counterfactual): HF with the megakernel's routing ids and weights forced matches ours at 0.999916 / 0.999982 / 0.999976 / 0.999926. Gate not relaxed (docs/gpu-log.md sessions 5 and 8).
  • Per-step routing gate fails for the same cause (session 8 DAG kernel: 1 unexplained mismatch, step 5 layer 23, swap margin 0.033 > gate-logit error 0.024; chain kernel: 759 exact layer-steps, 72 near-tie escapes, 1 rejected by the escape rule at step 22 layer 16, explained by the cascade).
  • Hardware performance counters unavailable: rocprof --pmc deadlocks Fleet's split worker/scheduler launch on the MI300X VF; achieved bandwidth is derived from bytes/token and measured time.
  • rocprofv3 kernel trace fails to register (kineto after its configuration window); rocprof v1 --hip-trace used instead.
  • bench.py counted 4.84 GB/token for the DAG kernel (results/bench/bench.json): the DAG attaches W_qkva as the views W_q / W_kva, which its name filter skipped (27 x 3,648 x 2,048 x 2 B = 0.40 GB). The weights are still read: 5.24 GB/token, 1.39 TB/s at 3,759 us. Counter fixed in scripts/bench.py after the run.
  • DAG attention tiles at attn_rows=256 decode incorrectly: 5/32 tokens on 9/9 launches (FLEET_KNOBS=dag=1,fold=1,attn_rows=256 + F1/F3/F2, docs/gpu-log.md session 8 x6, results/perf/dag_x6_attn256). Not in the kept configuration; the token-match gate caught it; fleet_ds/graph.py refuses attn_rows=256. CPU bit-identity tests cover attention tiles at 16/32/64 rows only (128 rows measured 9x32 on the GPU). Cause not isolated: KV_B_PROJ at one tile per head (TPH 1) writing k_pe, or the 256-row O_PROJ merge prologue.

Bugs found on the device

From docs/gpu-log.md (cause, fix and commit for each).

  • Fleet: worker/scheduler streams leak on re-init. A second init_func in one process created a new stream pair without destroying the old one; HIP's 4 hardware queues ran out, the scheduler stream shared a queue with the running worker kernel and never dispatched, so the launch hung (session-3 e2e and bench stalls; reproduced in session 4 in 30 s, passes with GPU_MAX_HW_QUEUES=8). Workaround: never re-init, set max_seq_length per launch (25819ba). Fix: destroy the previous streams on re-init, upstream PR ROCm/fleet-chiplet-megakernel#9 (standalone reproduction and logs in results/upstream-pr/: vanilla hangs, patched passes), applied here as fleet_glue/persistent_kernel_cuh.patch (b4b4607). Regression check on the model: scripts/run_layer.py --layers 1 --event-timing --reinit passes with the patch (results/m2/s5_reinit/layer_1-1.json, 231a35e).
  • Reported GPU hang after the first layer launch: no hang, Fleet's host/device printf banner was C-stdio buffered under tee and landed after Python's PASS line; launch() now flushes C stdio (f29eeaa).
  • Per-boundary event-timing labels shift by one op after layer 0: gcd-split boundaries emit extra events and the labeller counted plan entries. scripts/dispatch_probe.py labels each event by the op owning its first producer task (1d9f630); bench.json's boundary_us_p50 still carries the shift.
  • rocprofv3 1.1 aborts before the model loads (torch 2.14's kineto registers after the tool's configuration window, "api registration failed with error code 16"); rocprof v1 plain mode does not see the persistent kernels launched on Fleet's own streams. profile.sh trace falls back to rocprof v1 --hip-trace --stats and reads kernel resources from the compiled code object (5e6a09c, 1eeebfa).
  • DeepSeek remote code on transformers: check_imports demands flash_attn even for eager attention (stub package, 62e14a9); eager attention asserts a 4-D mask on decode steps (explicit attention_mask, 5257b10; same fix in bench.py --hf).
  • Box setup: get_configurations_from_gpu needs rank (75e2d42); build_graph.sh used the system python (5ad8de4), re-downloaded the checkpoint (80b1f59) and broke a line continuation (43b292f).

Recommended next steps

  • Lower worker poll latency, so a worker notices its dependency counter sooner (unmeasured estimate: dependency wait is 61 % of worker time on the default configuration, results/perf/dag_kept_timing; how much of it is poll latency rather than upstream execution is not split).
  • Cheaper completion signalling per event (unmeasured estimate: ~4.5 us of poll + signal on each of ~11 critical-path events per layer, docs/gpu-log.md session 8 addendum).
  • Fewer critical-path events through further folds, e.g. the attention path KVA -> KV_B -> chunk -> o_proj (unmeasured estimate: each fold removes one event round trip but adds a prologue; the chain's first folds measured slower, D11b).
  • Carried over, also not done: absorbed MLA cache (-5 % bytes, validated on CPU only), a vLLM batch-1 run on the same MI300X, a path-aware routing near-tie escape rule.

Notes: Decisions revised on measurements: D2 superset-with-skip (19.31 ms P50) -> slot-indexed experts (D10a); D7g 320 x 320 -> 1,600 x 64-row (D10b) -> 200 x 512-row lm_head tiles after a measured sweep (Q-H). Each kernel change passed the layer-1 gates; the combined kernel and the final kernel each passed layers 0-26 and 32/32 tokens (History and Performance round tables).

Profiling commands and results

  • scripts/bench.py → results/bench/bench.json: per-iteration times from MPK_EVENT_TIMING (END_OF_TASK_GRAPH firings), launch wall times, bytes/token. P50 / P95 3,759 / 3,770 µs, 266.0 tokens/s, 1.39 TB/s.

  • scripts/bench.py --hf → results/bench/hf_baseline.json: torch.profiler device events per HF eager decode step: 3,853 per token.

  • scripts/profile.sh trace → results/profile/: tries rocprofv3 --kernel-trace (aborts, see Bugs), then runs rocprof --hip-trace --stats -o results/profile/trace/v1/results.csv .venv/bin/python scripts/bench.py --warmup 1 --runs 2 --no-event-timing --out results/profile; dumps the code object (llvm-objcopy --dump-section .hip_fatbin of permanent_output_dir/test.*.so, clang-offload-bundler --unbundle, llvm-readelf --notes → codeobj/notes.txt); then scripts/bench.py --rocprof results/profile/trace --out results/profile → results/profile/rocprof.json: 3.0 dispatches per mpk() call, worker_kernel VGPR 255 / SGPR 106 (Occupancy, above).

  • FLEET_KNOBS defaults, scripts/dispatch_probe.py --timing --runs 3 --out results/perf/dag_kept_timing → results/perf/dag_kept_timing/probe.json: -DMPK_ENABLE_TIMING worker states on the final configuration (dependency wait 37.0 µs, execute 19.2 µs per task).

  • scripts/dispatch_probe.py --timing --out results/perf/timing → results/perf/timing/probe.json: -DMPK_ENABLE_TIMING worker states (dependency wait 46.3 µs, execute 16.7 µs per task) and exact per-op spans.

  • scripts/dispatch_probe.py --lm-rows N --out results/perf/lmN (N = 64, 128, 256, 320, 512, 1024; 1 warm-up + 3 × 32-token launches each, flag-free build): exact per-op spans, the lm_head tile sweep:

    lm_head rows × tasks graph tasks / events LM_HEAD µs iteration P50 / P95 µs iteration 1 µs tokens per launch
    64 × 1,600 24,592 / 430 151.8 4,804 / 4,817 6,727 [32, 32, 32]
    128 × 800 23,792 / 430 138.0 4,795 / 4,810 6,678 [32, 32, 32]
    256 × 400 23,392 / 430 140.4 4,785 / 4,797 6,570 [32, 32, 32]
    320 × 320 23,312 / 350 165.1 4,789 / 4,801 6,644 [32, 32, 32]
    512 × 200 23,192 / 430 129.3 4,783 / 4,797 6,572 [32, 32, 32]
    1024 × 100 23,092 / 430 173.4 4,835 / 4,852 6,662 [32, 32, 32]
  • scripts/run_e2e.py --event-timing → results/m4/e2e.json: free-running and teacher-forced decode with per-iteration timing and per-step routing.

  • scripts/routing_counterfactual.py → results/m4/routing_counterfactual.json: HF eager with the megakernel's routing forced (reads results/m4/tf_capture.safetensors).

  • scripts/profile.sh counters (rocprof --pmc FETCH_SIZE / WRITE_SIZE): not run; Fleet documents that --pmc serialises dispatches and the split worker/scheduler launch deadlocks under it, so bandwidth is derived.

Reproduction (MI300X, from the repo root)

scripts/deploy.sh mi300x:AMD --git main                           # laptop -> box, committed tree over ssh
scripts/day1.sh      # ~10 min: packages, pinned Fleet + glue incl. the re-init patch, torch, model, Fleet build, HF reference (+ router logits) -> results/m0; --with-tests / --with-graph opt-in
scripts/run_layer.py --layers 1 --torch-fallback --event-timing    # M2, per-op gates -> results/m2
scripts/run_layer.py --layers 1-3 --event-timing                   # M3 -> results/m3
scripts/run_layer.py --layers 0-26 --event-timing --out results/perf/s5_lm512         # whole token, final kernel
scripts/run_layer.py --layers 1 --event-timing --reinit --out results/m2/s5_reinit     # re-init regression check
scripts/run_e2e.py --event-timing                                  # M4 -> results/m4
scripts/routing_counterfactual.py                                  # -> results/m4/routing_counterfactual.json
scripts/bench.py && scripts/bench.py --hf                          # -> results/bench
scripts/profile.sh trace                                           # -> results/profile
scripts/dispatch_probe.py --timing --out results/perf/timing       # diagnostic build
scripts/dispatch_probe.py --lm-rows 512 --out results/perf/lm512   # and 64, 128, 256, 320, 1024
.venv/bin/python scripts/report.py                                 # this file

Mirage caches the compiled megakernel in permanent_output_dir/: remove it after changing build flags or the glue.

Exact command behind every evidence file:

evidence commit command
results/bench/bench.json git 2b4ac23 scripts/bench.py
results/bench/hf_baseline.json 877474c scripts/bench.py --hf
results/m2/layer_1-1.json 4c4d156 scripts/run_layer.py --layers 1 --torch-fallback --event-timing
results/m2/s5_reinit/layer_1-1.json 231a35e scripts/run_layer.py --layers 1 --event-timing --reinit --out results/m2/s5_reinit
results/m3/layer_0-26.json 04cbecc scripts/run_layer.py --layers 0-26 --event-timing
results/m3/layer_1-3.json 04cbecc scripts/run_layer.py --layers 1-3 --event-timing
results/m4/e2e.json git 2b4ac23 scripts/run_e2e.py --event-timing
results/m4/routing_counterfactual.json 826f4ef scripts/routing_counterfactual.py
results/perf/bench/bench.json b1568fa scripts/bench.py --out results/perf/bench
results/perf/dag_g1_layers/layer_0-26.json git 2b4ac23 scripts/run_layer.py --layers 0-26 --event-timing --out results/perf/dag_g1_layers
results/perf/dag_kept_timing/probe.json git 2b4ac23 scripts/dispatch_probe.py --timing --runs 3 --out results/perf/dag_kept_timing
results/perf/dag_s1a_l1/layer_1-1.json git 4425efc scripts/run_layer.py --layers 1 --event-timing --out results/perf/dag_s1a_l1
results/perf/final/probe.json 7b0a445 scripts/dispatch_probe.py --out results/perf/final_probe
results/perf/fold_l1/layer_1-1.json 7b0a445 scripts/run_layer.py --layers 1 --torch-fallback --event-timing --out results/perf/fold_l1
results/perf/foldsw_l0-26/layer_0-26.json 7b0a445 scripts/run_layer.py --layers 0-26 --event-timing --out results/perf/foldsw_l0-26
results/perf/foldsw_probe/probe.json 7b0a445 scripts/dispatch_probe.py --out results/perf/foldsw_probe
results/perf/gate_l1/layer_1-1.json db09ccd scripts/run_layer.py --layers 1 --torch-fallback --event-timing --out results/perf/gate_l1
results/perf/lm1024/probe.json d3675f7 scripts/dispatch_probe.py --lm-rows 1024 --out results/perf/lm1024
results/perf/lm128/probe.json d3675f7 scripts/dispatch_probe.py --lm-rows 128 --out results/perf/lm128
results/perf/lm256/probe.json d3675f7 scripts/dispatch_probe.py --lm-rows 256 --out results/perf/lm256
results/perf/lm320/probe.json d3675f7 scripts/dispatch_probe.py --lm-rows 320 --out results/perf/lm320
results/perf/lm512/probe.json d3675f7 scripts/dispatch_probe.py --lm-rows 512 --out results/perf/lm512
results/perf/lm64/probe.json d3675f7 scripts/dispatch_probe.py --lm-rows 64 --out results/perf/lm64
results/perf/mla_l0-26/layer_0-26.json 7b0a445 scripts/run_layer.py --layers 0-26 --event-timing --out results/perf/mla_l0-26
results/perf/s5_lm512/layer_0-26.json b7f0e5e scripts/run_layer.py --layers 0-26 --event-timing --out results/perf/s5_lm512
results/perf/step1_layer_1-1.json 2fbd862 scripts/run_layer.py --layers 1 --torch-fallback --event-timing --out results/perf
results/perf/step2_layer_1-1.json 2fbd862 scripts/run_layer.py --layers 1 --torch-fallback --event-timing --out results/perf
results/perf/step3_layer_1-1.json 2fbd862 scripts/run_layer.py --layers 1 --torch-fallback --event-timing --out results/perf
results/perf/step4_layer_1-1.json 2fbd862 scripts/run_layer.py --layers 1 --torch-fallback --event-timing --out results/perf
results/perf/step5_layer_0-26.json 2fbd862 scripts/run_layer.py --layers 0-26 --event-timing --out results/perf
results/perf/sweep_gate_l0-26/layer_0-26.json 7b0a445 scripts/run_layer.py --layers 0-26 --event-timing --out results/perf/sweep_gate_l0-26
results/perf/sweep_gate_l1/layer_1-1.json 7b0a445 scripts/run_layer.py --layers 1 --torch-fallback --event-timing --out results/perf/sweep_gate_l1
results/perf/sweep_w13_32_w2_32/probe.json 7b0a445 scripts/dispatch_probe.py --w13-tiles 32 --w2-tiles 32 --out results/perf/sweep_w13_32_w2_32
results/perf/sweep_w13_44_w2_32/probe.json 7b0a445 scripts/dispatch_probe.py --w13-tiles 44 --w2-tiles 32 --out results/perf/sweep_w13_44_w2_32
results/perf/timing/probe.json 1d9f630 scripts/dispatch_probe.py --timing --out results/perf/timing
results/profile/rocprof.json 1eeebfa scripts/bench.py --rocprof results/profile/trace --out results/profile