- Deleted fmha.py (CuTeDSL slow path), FmhaKernel, Python KV merge - Deleted fmha_sm100.cuh, fmha_sm100_tc.cuh, fmha_sm100_launch.cu, fmha_epilogue_sm100.cuh - Moved fmha_qk_verify.cuh to tests/unit/qk_verify_kernel.cuh - Deleted decode_sparse.py, decode_swa.py, kernels/decode/ - Deleted 46 test_d*.py probes, test_smem_*, test_cotiled_*, test_tmem_*, test_smem_p_*, test_ultra_minimal, test_fmha_pv16, test_working_softmax_maybe - Deleted root scratch: debug_linear.py, test_mapping.py, run_router_tests.py - Moved archive/ to archived_plans/code_archive/ - Rewrote production.py: single fast path via 6-warp multi-tile kernel - Added STATUS.md, audit_attention_live.md - Moved NEXT_PRIORITIES*.md to archived_plans/
20 KiB
ROADMAP — Stage E and the path to a runnable model
Method. Every state claim below was verified against the source in this revision. Agent status doc and agent assessment were both read, then ignored where they contradict the code. The doctrine rules from prior issue docs apply: DSL wall → raw CUDA C++ (not Python); raw CUDA ≠ scalar math; print, don't guess; integration over exploration; falsifiable gates only.
TL;DR — where we actually are
The FMHA kernel itself is in genuinely good shape. fmha_6warp_tma_multirow_multitile.cuh
is one file, properly Blackwell-native (tcgen05.mma SS, UMMA descriptors,
TMEM, cp.async.bulk.tensor.2d + mbarrier.arrive.expect_tx, in-kernel
multi-tile rescale via SMEM accumulator, multi-row softmax). The TMA hang from
P4 was correctly diagnosed (missing expect_tx) and resolved. The C-API +
ctypes shim works. P3, P4, P5, P6, P7, P8 numerics are done.
The agent's "Stage E" assessment is correct in spirit but wrong on specifics.
Where it says "AttentionSubBlock has NotImplementedError stubs" — it doesn't.
The layer is structurally complete and imports the right things. The real
problem is one layer down: it calls cache methods like gather_compressed_kv(),
gather_swa_kv(), and properties like cache.num_query_heads / cache.head_dim
that don't exist on LayerCacheHandle. The handle exposes read_*_view()
returning paged FP8 buffers; nothing materializes them into the BF16 dense
tensors dsv4_attention consumes. That's the integration gap, not stubs.
Three structural shortcuts still live in production code. They are not performance-fatal yet but they will be at scale:
- Two
torch.cuda.synchronize()calls remain on the kernel-launch path (fmha_multitile_op.py:130,production.py:279). The second is in the still-resident "slow path." The first is unconditional, even on the fast path. - The slow CuTeDSL+Python-merge path is not deleted, only relabeled
"slow path." 471 lines of code, 48 unit tests bound to its
FmhaKernelsymbol, still imported atproduction.py:53. - The batched call shape (4D q) does a Python for-loop over batch items
at
production.py:380instead of folding batch into the grid.
Cleanup is non-trivial. ~22 dead/stale code paths live alongside the
working ones. Some are old CuTeDSL drafts (ops/decode_sparse.py,
ops/decode_swa.py), some are pre-consolidation Stage D probes
(fmha_sm100.cuh, fmha_sm100_tc.cuh, fmha_sm100_launch.cu,
fmha_qk_verify.cuh, fmha_epilogue_sm100.cuh), some are misplaced root files
(debug_linear.py, test_mapping.py, tests/working_softmax_maybe.py), and 46
of 177 unit tests are pinned to the deprecated FmhaKernel. Left in place,
this is the primary path by which an agent will drift back into the wrong
codebase the next time it context-switches.
PART 1 — CLEANUP (do this first, in this order)
Cleanup is gated before Stage E work because the next stage's failure mode is
"agent picks the wrong file to extend." Right now dsv4/kernels/attention/ has
14 files; only 6 are live. That ratio is the diagnostic.
C1. Confirm what's live with grep, not by reading status docs
Before deleting anything: produce audit_attention_live.md listing each file
under dsv4/kernels/attention/ and dsv4/ops/ with its outbound reference
count from non-archive code (production, layers, model, integration tests).
Anything with 0 refs is a deletion candidate; anything with >0 refs gets
inspected to confirm the reference is live, not a stale import.
Falsifiable gate: the audit table goes into archived_plans/audit_attention_live.md,
file count column adds up to the actual directory listing. Mention deletion
candidates by name with the ref count next to them.
C2. Delete the dead CuTeDSL FMHA scaffolding
These have zero external refs outside their own #include chain and
self-references:
| File | Status | Action |
|---|---|---|
dsv4/kernels/attention/fmha_sm100.cuh |
Phase-1 scalar reference, header says "WORKING cos 0.999999" | Delete. The 6-warp kernel supersedes it. |
dsv4/kernels/attention/fmha_sm100_tc.cuh |
Earlier tensor-core fork before consolidation. | Delete. The 6-warp kernel is its successor. |
dsv4/kernels/attention/fmha_sm100_launch.cu |
Header says "COMPILES but doesn't run via torch.utils.cpp_extension" | Delete. Replaced by fmha_multitile_capi.cu + ctypes. |
dsv4/kernels/attention/fmha_qk_verify.cuh |
Header says "QK GEMM verification" — one test references it. | Move to tests/unit/qk_verify_kernel.cuh (it's a test fixture, not a library kernel). |
dsv4/kernels/attention/fmha_epilogue_sm100.cuh |
Phase-2 epilogue prototype; the multi-tile kernel already does in-kernel rescale via SMEM and doesn't use this. | Delete after confirming it's not #include'd by anything live. Audit table from C1 answers this. |
dsv4/ops/decode_sparse.py |
Old CuTeDSL CSA decode draft, not called from anywhere. | Delete. Path is now layers/attention.py:_forward_csa → kernels/attention.sparse_fmha_with_swa → kernels/attention/production.py. |
dsv4/ops/decode_swa.py |
Old CuTeDSL SWA decode draft, same story. | Delete. |
dsv4/kernels/decode/ |
Two files: __init__.py (empty) and _NOTES_fp8_bf16.md (scratch). |
Delete the whole directory. |
Falsifiable gate: after C2, find dsv4/kernels/attention -name '*.cuh' -o -name '*.cu' | wc -l
returns the value in audit_attention_live.md's live count. No reference
warnings on python -c "import dsv4".
C3. Decide the fate of the CuTeDSL slow path (fmha.py)
fmha.py is 645 lines and is the entire pre-P3-P8 CuTeDSL path. It is currently
imported by production.py:53 (from dsv4.kernels.attention.fmha import FmhaKernel)
and used by the else branch starting at production.py:410 ("SLOW PATH"). It
is also imported by 48 unit tests.
Two options. Pick one, document it, do not maintain both.
- Option A (preferred, matches doctrine). Delete
fmha.py, delete the SLOW PATH branch fromproduction.py, delete the 48test_d*.pyunit tests bound toFmhaKernel. The 6-warp multi-tile kernel covershd∈{64,128,256,512}×T=1decode × anyn_segments. That's the production surface. Everything else is exploration that already paid off — it does not need to ship. - Option B (fallback, only if Option A is blocked). Move
fmha.pyand the 48 tests toarchived_plans/cutedsl_legacy/. Delete the SLOW PATH fromproduction.pyregardless. The kernel and tests stay buildable for reference but are no longer in the active import graph.
Failure mode to refuse: "keep it as fallback in case the 6-warp kernel has a regression." If there's a regression, you fix the 6-warp kernel — that's the production kernel. Carrying a parallel implementation guarantees both rot.
Falsifiable gate: grep -r "FmhaKernel" dsv4/ tests/ scripts/ | wc -l returns
0 (Option A) or only matches inside archived_plans/ (Option B). The
SLOW PATH branch is gone from production.py either way.
C4. Empty the archive/ subdirectory the same way
dsv4/kernels/attention/archive/ currently holds 5 backups
(fmha_backup_pre_epilog.py, fmha_backup_v2.py, fmha_smem_acc.py,
fmha_6warp_tma_driver_api.cuh, fmha_tma_driver_api.cuh). These were correct
to not delete during P8 consolidation. Now: move them out of dsv4/
entirely into archived_plans/code_archive/ and delete dsv4/kernels/attention/archive/.
Active code directories should contain only active code. archived_plans/ is
the right place for everything that isn't.
Falsifiable gate: find dsv4 -name 'archive' -type d | wc -l returns 0.
C5. Move the root-level scratch files
Three files live at the repo root and don't belong there:
debug_linear.py(58 lines, computes expected O for a debug pattern — Stage D probe). →tests/unit/archive/or delete if no longer useful.test_mapping.py(49 lines, SMEM-P coordinate mapping check — Stage D probe). → same.run_router_tests.py(hardcoded path/root/dsv4-nvfp4-workspace/kernel). → Either generalize the path and move toscripts/, or delete. Hardcoded absolute paths in tracked files are bug magnets.tests/working_softmax_maybe.py(496 lines, name speaks for itself). → If it's a draft, archive it. If it's superseded, delete it. The doctrine is "code or archive, nevermaybe."
Falsifiable gate: repo root has no .py files except generated/build
artifacts. tests/ contains no *_maybe.py or working_*.py.
C6. Prune the unit test D-probe explosion
46 test_d*.py files were Stage D debugging probes. Many are bound to
FmhaKernel and will be deleted in C3 if you pick Option A. The remainder that
test components still in use (FmhaTmaMultiRowMultiTile, the indexer, the
compressor, mHC, MoE) should either be promoted to named integration tests
(test_fmha_decode.py, test_csa_indexer.py, etc.) or archived.
Falsifiable gate: ls tests/unit/test_d*.py | wc -l returns ≤ 5
(only the ones genuinely testing currently-live behavior, with descriptive names).
C7. Status doc consolidation
After C1–C6, write one new top-level STATUS.md describing the post-cleanup
state of the production path, and delete NEXT_PRIORITIES.md (this doc replaces it).
archived_plans/ already holds the historical paper trail; do not add to it
during cleanup.
Falsifiable gate: repo root has exactly two living .md files: README.md
and STATUS.md (plus this ROADMAP.md). archived_plans/ holds the history,
unchanged.
PART 2 — STAGE E PRIORITY ORDER (post-cleanup)
The agent is right that Stage E = "make the model run end-to-end." The order below is sequenced so each priority unblocks the next, and so the performance-decisive moves come after end-to-end correctness, not before.
E1. Wire LayerCacheHandle to expose what AttentionSubBlock calls
This is the load-bearing missing piece. dsv4/kernels/attention/__init__.py
calls four things that don't exist on dsv4/cache/handle.py:
cache.gather_compressed_kv(selected_indices) → (k_compressed, v_compressed)BF16 densecache.gather_all_compressed_kv() → (k_compressed, v_compressed)BF16 dense (HCA, no top-k)cache.gather_swa_kv() → (k_swa, v_swa)BF16 densecache.num_query_heads,cache.head_dim(properties)
The handle already exposes the right raw state via
read_classical_view() (FP8 entries + RoPE BF16 + paged block_table) and
read_swa_view() (FP8 entries + RoPE BF16 + positions). The work is the
gather+dequantize+concat path.
Constraints (doctrine):
- Each gather is a single fused kernel, raw CUDA C++ if CuTeDSL can't do block-table-strided FP8→BF16 dequant+gather in one pass. Not a Python loop over selected indices. Not five eager torch ops.
- The kernel uses TMA + warp-level dequant, not scalar loads. The MoE GEMM already proves out the FP8 dequant pattern on Blackwell — reuse it.
- The dequant decodes E4M3 with the inverse-scale stored alongside. Print
the actual layout of
paged.entries_fp8+paged.inv_scalebefore writing the kernel — do not assume the layout matches an MoE convention.
Definition of done:
- The four methods exist on
LayerCacheHandle, implemented indsv4/kernels/cache/gather.{cu,py}. python -c "from dsv4.layers.attention import AttentionSubBlock; ..."runs a forward pass on one layer without AttributeError, NotImplementedError, or shape mismatch.- Numerical parity vs a pure-Python reference (FP32 dequant + dense FMHA over
the gathered tensors):
cos ≥ 0.9995. FP8 dequant loses a little, that's expected. - Launch count for one CSA layer measured with Nsight: gather is ≤ 3 launches (compressed gather, SWA gather, concat). Not 30, not 5+5+cat.
Failure modes to refuse:
- A Python gather loop over the block table. The block table is on GPU; the gather is on GPU. No host trips.
- BF16 materialization "for now" without FP8 dequant. The paged cache stores FP8 because that's the design — bypassing it loses 2x KV memory and the whole point of the cache structure.
E2. End-to-end smoke test through one full layer
After E1, both halves of dsv4/model/layer.py (mhc_attn → attn.forward →
mhc_ffn → ffn.forward) should execute without error. The test is the
forcing function.
Definition of done:
tests/e2e/test_one_layer.pyrunsDSV4Layer.forward(X, token_ids, cache)on synthetic inputs with cached weights loaded fromloader/, returns a tensor of the right shape and dtype, no errors.- The same test with
cachecontaining actual paged FP8 entries (not zeros) produces output withcos ≥ 0.99against a Python reference of the same layer (compressor + indexer + FMHA + mHC, all in eager PyTorch). - Test exists for all three attention types (CSA, HCA, SWA) by toggling the
layer's
attn_type.
Failure mode to refuse: wrapping the test in a try/except to "make it pass." The cos threshold is the gate.
E3. Top-level model/dsv4.py (currently a 2-line TODO)
dsv4/model/dsv4.py is literally:
"""Full DSV4 model."""
# TODO: Phase 1
Build the actual model class: embedding → N×DSV4Layer → final norm → prediction
head. Use layer_schedule.py to alternate CSA/HCA per the V4 spec
(Pro: layers 0–1 HCA, then interleaved; Flash: layers 0–1 SWA, then
interleaved).
Definition of done:
DSV4Model.forward(token_ids, cache_manager) → logitsworks on a single token decode.- Layer schedule matches the V4 paper (verify against
config.py). - MTP module is wired but optional (off by default; doctrine note in code that MTP fine-tuning is a separate workstream).
- End-to-end test:
tests/e2e/test_decode.pygenerates one token from a 3-layer toy config, succeeds, cos ≥ 0.99 vs Python reference.
E4. Delete the two remaining torch.cuda.synchronize() calls on the fast path
After C3 the slow-path sync at production.py:279 is gone with the slow path.
The other sync is at fmha_multitile_op.py:130:
# Synchronize to catch async errors
torch.cuda.synchronize()
return o, lse
This is on the fast path. It's there to catch async kernel errors, but the
correct way to do that is to check the return code from the C-API launch
function (which fmha_multitile_decode_launch already returns) plus
cudaPeekAtLastError, not a full device sync.
Definition of done:
- The sync is removed from
fmha_multitile_op.py. - Error checking moves to a
cudaPeekAtLastErrorcall + the existing return code check. - Nsight measurement on a 4-layer decode: zero
cudaDeviceSynchronizecalls between layer 0 entry and final logits.
E5. Fold the batch loop into the kernel grid
production.py:380–388 is a Python for-loop over batch items. Each iteration
calls back into dsv4_attention on a 3D slice. For batch=8 and 61 layers,
that's 488 launches per decoded step instead of 61.
The multi-tile kernel's grid is currently (1, n_h, 1). It needs to become
(1, n_h, batch) with blockIdx.z used to index batch. The kernel already
has _batch_stride params (read off FmhaTmaMultiRowMultiTileParams); they
just aren't wired to a grid dim.
Definition of done:
production.py:380–388for-loop deleted; the batched path calls the multi-tile kernel once withgridDim.z = batch.- Numerical parity:
cos ≥ 0.999998vs the deleted Python loop. - Launch count for batch=8 decode: 1 per layer, not 8.
E6. NVFP4-1.2: FP4 output fusion for FMHA → wo_a
Right now the FMHA writes BF16 to GMEM, then wo_a re-quantizes it to FP4.
That's a redundant memory pass on the attention output. The epilogue already
has the one-way TMEM→regs→SMEM→GMEM shape — adding an FP4 pack with amax in
registers, gated by a template param, is the same pattern the MoE epilogue
already uses for NVFP4-1.1.
Constraints (doctrine):
- The FP4 pack is part of the same single launch. No second kernel for quantize. Per-128-element block amax reduces via warp shuffle in registers.
- The amax → scale → E2M1 LUT path matches
dsv4/ops/quantize.pybyte for byte. Print the LUT from the Python side; assert equality in a unit test. The indexer LUT bug we already fixed is the cautionary tale. - This is gated on E4 (
epilogue_opslot exists in the multi-tile kernel — let's verify it does before starting E6).
Definition of done:
fmha_6warp_tma_multirow_multitile_kernelaccepts a template argEpilogue(default = identity); aFP4Epiloguespecialization packs E2M1 + emits SF blocks in the layoutwo_aexpects.tests/unit/test_fp4_epilogue_parity.py: BF16 reference vs FP4 fused,cos ≥ 0.999after dequant.- End-to-end CSA layer: BF16 output → FP4 output replaces one full BF16 R/W pass through (T, n_h * hd) memory. Measured bandwidth drop with Nsight.
E7. Stage F: Lightning indexer FP4 tensor-core scoring
Currently indexer_score_topk.cu uses scalar FP32 cores after the LUT dequant.
The paper §5.2.1 is explicit that this path runs in FP4 on tensor cores
("multiplied entirely in FP4 ... 99.7% recall"). The LUT bug fix made the
scalar path correct; this priority makes it fast, the way V4 actually
specifies it.
Constraints (doctrine):
tcgen05.mmawithmxf4nvf4kind. Keys and queries both FP4. Scales E4M3 in shared mem, hardware decodes. No scalar fallback in the live path.- Recall vs the FP32 oracle ≥ 99.7% per paper. The recall test from the LUT fix is the same test; the bar is the same.
- The top-k selector goes warp-level (
__shfl_xorballot) at the same time — the current serial single-thread heap merge at top_k=1024 is its own latency source on Pro decode.
Definition of done:
indexer_score_topk.cuhas atcgen05-MMA FP4 path, gated by a config flag, with the scalar path archived (not deleted, since the FP4 path needs the oracle).- Recall@k ≥ 99.7% for k ∈ {512, 1024} at compressed_blocks ∈ {2k, 8k, 32k}.
- End-to-end CSA layer latency drops measurably (Nsight).
E8. Multi-CTA grid for prefill (Priority 4 from the original ROADMAP)
Decode is single-CTA per head and that's correct. Prefill (T > 1) wants multi-CTA to parallelize over M. The one-way epilogue from E6 is the prerequisite — once it lands, the grid expansion is wiring.
Definition of done:
gridDim.x = ceil_div(M, M_TILE)forT > 1paths.- Prefill latency at T=128 with M-tile=64: 2-CTA grid, measured speedup ≥ 1.5× over single-CTA.
- Decode path unchanged (single CTA, no regression).
E9. CUDA graph capture (Stage E item from the agent's notes)
Once E5 lands and the decode hot path is sync-free, graph capture is straightforward. This is the biggest single perf win on autoregressive decode because it eliminates host launch overhead entirely.
Definition of done:
DSV4Model.decode_step(token, cache)iscudaGraph-capturable.- Replay latency vs uncaptured: ≥ 2× faster on a 3-layer toy at batch=1.
- Memory: graph capture doesn't
torch.zeroson the hot path (E1 already forced this for gather; verify it holds for compressor + indexer + FMHA).
DOCTRINE — applies to every item above
- DSL wall → raw CUDA C++, not Python. The C-API + ctypes pattern in
fmha_multitile_op.pyis the correct shape for new kernels. - Raw CUDA ≠ scalar math. Every kernel above is
tcgen05/ UMMA / TMA / warp reductions. No "temporary scalar dot product" without a labeled replacement target. - Print, don't guess. Two places this round specifically:
- E1 gather kernels: print the FP8 cache layout + inv-scale layout before writing the dequant.
- E6 FP4 pack: print the LUT from
quantize.pyand assert in a test that the kernel produces byte-identical packed nibbles.
- Integration over exploration. Do not create
fmha_6warp_tma_multirow_multitile_v2.cuh. Extend the chosen file or stop and replan. Variant proliferation is the agent's primary failure mode in this repo. - Falsifiable gates only. Every "done" above has a number or a binary check. "Looks fine," "milestone complete," and "Stage X done" without a number are read as "not done."
- The slow path is not a fallback. Once C3 deletes the CuTeDSL+Python-merge path, it stays deleted. If the 6-warp kernel breaks, you fix the 6-warp kernel — not resurrect 471 lines of dead code.