- L0 CUDA graph capture PASSES on B200 - All compute-forward sync violations fixed - 3/5 Section C hazards done, 2 deferred to Phase 2 - Full violation fix log with commits - Next steps: extend to all 61 layers + replay verification
129 lines
8.2 KiB
Markdown
129 lines
8.2 KiB
Markdown
# 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()` (was 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 * padded_rows` → 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)
|
||
|
||
The per-layer compute is split:
|
||
- **Captured** (in CUDA graph): mHC pre_block → RMSNorm + quantize → attention projections → o_proj → mHC post_block → FFN mHC → Router → MoE → SE → mHC post_block
|
||
- **Eager** (outside graph): Compressor → Indexer → KV gather → FMHA → inverse RoPE
|
||
- **Rationale**: FMHA has dynamic sequence length; compressor/KV are data-dependent. Capturing the compute-heavy parts eliminates ~94ms of Python dispatch overhead per step.
|
||
- **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** — L0 capture PASSES. Need to extend to all 61 layers + lm_head + replay verification.
|
||
4. ⬜ **Gate every commit on the capture test** — Not yet implemented.
|
||
|
||
### Next Steps
|
||
1. Extend graph capture from L0 to all 61 layers
|
||
2. Capture hc_head + norm + lm_head graph on cuda:0
|
||
3. Implement replay loop and verify bit-for-bit match with eager
|
||
4. Benchmark: measure speedup from graph capture vs eager decode
|
||
5. Gate commits on capture test
|
||
6. 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 |
|