Files
nvfp4-megamoe-kernel/GETTING_CUDAGRAPH_READY.md

143 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DSV4 → vLLM: CUDA-Graph Safety / GPU-Native Requirements (PART 2 companion)
**Goal:** the per-step decode forward must be fully GPU-native so vLLM can capture and replay it. No implicit device→host sync, no host control flow that reads a device value, no data-dependent shapes, no per-step host allocation. This doc gives you (A) a detector so you find every violation *once, upfront*, (B) the exhaustive hidden-CPU checklist, and (C) the DSV4-specific kernels that must be device-native.
## The one rule that decides everything
Branching on a **host-known integer** (step number, position, batch size, dtype, static shape) is graph-compatible — you capture one graph per bucket and the scheduler picks by that integer. Branching on a **device value** (sampled token, per-expert token count, top-k result, a mask, a norm/residual magnitude) is **not** — it must become device-side, fixed-shape work with masking. Every violation below is a place something reads a device value on the host.
You do **not** need one monolithic graph. The standard pattern (what vLLM's DSV4 does) is *bucket by shape + break at attention + keep the dense parts captured.* Your job is to make each dynamic decision either device-side or isolated to that eager break.
---
## SECTION A — The detector (build this FIRST, before porting anything) ✅ DONE
**Status:** Built and verified on B200 (2026-06-03). See `tests/unit/test_cuda_graph_readiness.py`.
Results from detector runs on B200:
- **Method 1** (sync debug mode): 0 violations in forward compute path
- `dec_tid_buf.copy_(dec_tid_pinned)` is flagged but this is a valid graph-capturable pinned memcpy
- All `.item()` syncs eliminated from hot path
- **Method 2** (graph capture L0): **PASS**
- `torch.cuda.CUDAGraph()` capture of layer 0 decode step succeeds
- All per-call allocations eliminated
- All host reads of GPU values eliminated
The detector:
1. Grep for Section B sync patterns in hot path files
2. Run one decode step with `torch.cuda.set_sync_debug_mode("error")`
3. Attempt `torch.cuda.graph` capture of L0 decode step
4. Report results to `/tmp/cuda_graph_readiness_results.json`
Run via test harness:
```bash
fire_b200_test tests/unit/test_cuda_graph_readiness.py kernel-test /tmp/kernel-test.log 1800
```
---
## SECTION B — The hidden-CPU checklist (grep the hot path for these) ✅ ADDRESSED
**Explicit device→host transfers** — All `.item()` calls on hot path eliminated:
- mhc.py `post_block`: removed `X_next.abs().max().item()` (122 syncs/step across 61 layers × 2 mHC)
- All other `.item()` calls are guarded by `VERBOSE >= 2` and don't execute at VERBOSE=0
- Warmup-gsa `.item()` calls run once at step 0, outside graph region
**Data-dependent shapes** — Eliminated `torch.bincount` from MoE:
- Replaced with `scatter_add_` into pre-allocated `_tokens_per_expert_buf` (fixed shape, GPU-only)
- Pre-allocated `_ones_buf` to avoid per-call `torch.ones()`
**Per-step host allocation** — All eliminated:
- `torch.zeros()` in `_assemble_scales_single_group` → pre-allocated `_scale_a_buf` (linear.py, grouped_linear.py, shared_expert.py)
- `torch.full()` for MoE l1_gsa → `self._l1_gsa_buf.fill_(l1_gs)`
- `torch.empty()` for grouped_linear output → pre-allocated `_output_buf`
- `mHCLayer.init_state` `.clone()``out_buf` parameter for in-place write
- `torch.zeros_like` in quantize.py → scalar `0.0` in `torch.where`
**Host control flow on device values** — Eliminated:
- `dec_tid_buf[0] = python_int` → pinned CPU buffer + `copy_` (async, graph-capturable)
- `expert_offsets[g] = python_int` → element-wise GPU multiply with pre-allocated range tensor
- `if group_offsets[0] != 0` → unconditional GPU-only update (no host read of GPU tensor)
**What is FINE (no sync, don't waste time on these)**
- `.shape` / `.size()` / `.numel()` / `.dtype` (host metadata, no sync)
- Branching on host-known ints (step/batch/static shape)
- The **stop-token check, detokenize, and your BF16 precision-floor dequant** (all load-time or *outside* the captured graph — leave them on host, that's correct).
- `dec_tid_buf.copy_(dec_tid_pinned)` — pinned CPU→GPU async memcpy, graph-capturable
---
## SECTION C — DSV4-specific kernels that must be GPU-native
| # | Hazard | Status | Fix Applied |
|---|--------|--------|-------------|
| 1 | Compressor returns `None` for 3/4 (CSA) or 127/128 (HCA) decode steps | ⏳ Phase 2 (eager-break) | Compressor runs in eager section. Phase 2: device-side boundary detection + fixed-shape output |
| 2 | KV grows each step → attention shape changes | ⏳ Phase 2 (eager-break) | Attention is the eager break. Phase 2: paged KV with fixed blocks + block table |
| 3 | Indexer top-k → host reads selected count to size gather | ✅ DONE | Already fixed-shape gather (`topk_indices` is always `top_k` elements). No host read of count. |
| 4 | MoE top-6 → per-expert token counts drive per-expert launches | ✅ DONE | `torch.bincount``scatter_add_` into pre-allocated buffer. Expert offsets are GPU tensors. |
| 5 | Next token / positions managed on host, fresh tensors per step | ✅ DONE | Pre-allocated pinned CPU buffers + `copy_` to GPU. No per-step allocation. |
Also confirmed:
- **Sinkhorn** runs a **fixed 20 iterations with no host convergence check**
- **Sampler** is device-side; the EOS/stop decision is a host step **outside** the graph ✅
- **Router** is graph-safe: pre-allocated output buffers, GPU-only operations ✅
- **mHC** is graph-safe: fixed-iteration Sinkhorn, no `.item()` on hot path ✅
### Architectural Decision: Eager-Break-at-Attention (Phase 1) — UPDATED 2026-06-04
The per-layer compute is split into **two graph-captured regions** with eager attention in between:
- **Graph A** (captured): mHC pre_block(attn) + fused RMSNorm + quantize + q_a + q_a_norm + q_b + kv projections
- Outputs written to pre-allocated buffers: x_normed, q_heads, kv_3d, ctx_a_B, ctx_a_C, X_mid
- **Eager** (NOT captured): Compressor → Indexer → KV gather → FMHA → inverse RoPE → o_a + o_b → F_attn
- Dynamic shapes (FMHA seq_len, compressor returns None) → cannot be captured
- `forward_attention()` accepts optional `q_heads`/`kv_3d` to skip projections when called from graph replay
- **Graph B** (captured): mHC post_block(attn) + FFN mHC + RMSNorm + quantize + Router + MoE + SE + mHC post_block(ffn)
- Reads F_attn from pre-allocated buffer (written by eager attention)
- Writes X_next to pre-allocated output buffer
**Rationale**: FMHA has dynamic sequence length; compressor/KV are data-dependent. Capturing the compute-heavy parts (projections, MoE, SE) eliminates ~94ms of Python dispatch overhead per step. The attention path (which is NOT compute-heavy for T=1 decode) runs eagerly with negligible overhead.
**Phase 2**: Paged KV + device-side compressor → full graph capture for vLLM integration.
---
## SECTION D — Integration order
1.**Build Section A's detector and run it on the current forward** — DONE. `tests/unit/test_cuda_graph_readiness.py` on B200.
2.**Fix Section C's five device-native kernels** — 3/5 done, 2 deferred to Phase 2 with architectural decision.
3.**Re-run capture-under-test until it captures clean** — WORKING on all 8 GPUs! Root cause: multi-GPU requires explicit `torch.cuda.Stream(device=device)`.
4.**Gate every commit on the capture test** — Not yet implemented.
### Next Steps (for next session)
1. ~~**Continue fixing per-step allocations in graph capture path**~~ ✅ DONE
2. ~~**Verify swizzled scale buffers are allocated before graph capture**~~ ✅ DONE (SE bug fixed)
3. ~~**Test graph capture on B200**~~ ✅ DONE — working with 0.28s/token
4. ~~**Extend capture to all 61 layers**~~ ✅ DONE — all 61 layers captured and replayed
5. ~~**Replay verification**~~ ✅ DONE — graph replay matches eager forward
6. ~~**Benchmark**~~ ✅ DONE — 0.28s/token (2x faster than eager 0.55s/token)
7. **Gate commits on capture test** — implement CI check
8. **Optimize stream sync** — replace `torch.cuda.synchronize()` with event-based waits
9. **Phase 2**: Paged KV + device-side compressor for full vLLM graph capture
## Guardrails
- Keep the stop-check, detokenize, and load-time BF16 dequant on the host — they're outside the captured region by design; don't contort them to be "graph-safe."
- **Phase 1 uses eager-break-at-attention.** Phase 2 adds paged KV. Don't retrofit paged KV into Phase 1 — it's a separate integration.
- Host-known-int branching is allowed; only device-value branching must be eliminated. Don't over-correct and try to make legitimate shape/dtype dispatch device-side.
## Violation Fix Log
| Commit | Description |
|--------|-------------|
| `a9ea303` | mhc.py `.item()` removal, linear/shared_expert pre-alloc, quantize gsa fix |
| `46a3a51` | mHCLayer.init_state out_buf, dec_X_buf pre-allocation |
| `0ca7bed` | Pinned CPU buffers for token transfer, grouped_linear expert_offsets GPU-only |
| `e07d798` | _assemble_scales_single_group correctly-sized view for swizzle |
| `df05289` | Remove conditional host read of GPU tensor in grouped_linear |
| `84655d0` | MoE bincount → scatter_add_, MoE torch.full → fill_() |
| `f13a81d` | grouped_linear scale_a_buf pre-alloc, quantize zeros_like → scalar 0.0 |
| `518a1d3` | MoE scatter_add_ int64 indices, fix second bincount call |
| `80bb27f` | gsa broadcast: reshape for M=1 decode (no stride-0), contiguous for M>1 prefill |
| `6dc2f22` | **CRITICAL: _l1_out_buf 2x too narrow → GPU memory corruption (root cause of ALL cudaErrorInvalidValue errors)**. Also: all GEMM output buffers pre-allocated, gsa copy_ → scalar assignment |
| `69e15f1` | Blackwell swizzle CUDA kernel for graph capture, swizzled output buffers |
| `ffa7842` | Dense router: BF16 GEMM instead of FP32 conversion during graph capture |