Repository navigation
Conversation
…ce per forward, not per layer FlashMLA MegaAttention, the default DeepSeek-V4.1 attention backend on SM100/SM103 with an FP8 KV cache, takes its query in a packed layout, (tokens, 32, padded heads, 16). pack_query built it with new_zeros plus a copy in every layer, so each layer paid a memset of the whole padded buffer; at TP4 the 16 local heads are padded to 64, so three quarters of it is padding. Keep one zeroed buffer per attention metadata, the scheme _zero_padded_q already uses for q_pad_buffer, and have pack_query write only the real heads. The padding is zeroed when the buffer is allocated and nothing writes it afterwards: pack_query writes only [:, :, :heads], and the mega decode and prefill kernels only read q (both sgl-flash-mla 1.0.0 kernel sets load it with ld.global.nc and never store to it; the SM100 kernel test now asserts q is unchanged). The buffer has its own metadata field because the packed layout puts real heads where q_pad_buffer pads. Buffer lifetime: - Eager forwards, and decode and verify graphs that build their metadata inside the capture, hold it for one forward; such a graph zeroes it once per replay instead of once per layer. - Metadata that a captured graph keeps for the server's lifetime (each breakable prefill graph shape, each early verify graph) share one buffer: _share_q_mega_pad_buffer starts each from the buffer of the one kept before it. Graphs replay one at a time, so they can share it, and prefill shapes are captured largest first, so they all share one buffer, allocated and zeroed in warmup. - The late-layer tail metadata (decoder SWA bounded replay) reuses the forward's buffer. Growing a buffer after a CUDA graph captured it asserts, as for q_pad_buffer. The packed query is bitwise identical, so numerics do not change. B200, one GPU, 40 packs per forward, 16 -> 64 heads, A/B in one process after bitwise checks (computelab job 5245283). CUDA-graph time per forward, old -> new: decode, 8 tokens: 137.4 -> 96.3 us verify, 8 x 6 = 48 tokens: 163.9 -> 108.5 us extend, 8K-token chunk: 11.46 -> 6.15 ms B200 TP4 server, cookbook low-latency recipe plus breakable prefill graphs (58 shapes up to 8,192 tokens), nsc-svg job 2355924: prefill graph capture memory 7.33 GiB on main, 7.43 GiB here (14.36 GiB when each kept metadata held its own buffer). sgl-eval gsm8k (200 examples): 97.5% on main and here, and 97.5% / 97.0% with the cookbook recipe as is (job 2355445). test_mega_attention.py gains cases that compare the packed query bitwise with the fresh-zeros pack over several layers and shrinking and growing token counts; two CUDA-graph cases (a buffer allocated before capture, and one allocated inside it and poisoned with NaN between replays); and prefill-graph shapes captured largest first with a late-layer tail, which share one buffer and replay exactly in any order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ked-query buffer chain
_install_early_verify_metadata now starts each early verify graph's metadata
from the MegaAttention packed-query buffer of the one kept before it, so the
SimpleNamespace backend in test_early_verify_metadata.py raised AttributeError
('_share_q_mega_pad_buffer') on the first install. Give the stub the field and
the real method, and assert that a later early verify graph shares the buffer of
the one captured before it instead of holding its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… packed query unchanged The packed-query buffer is zeroed once per forward and reused by every layer, which is only correct if no MegaAttention kernel writes q. Check decode and prefill, on the legacy (block-split cache) and the current (interleaved cache) kernel sets, for 16 -> 64, 64 -> 64 and 128 -> 128 heads: after each call q is bitwise the fresh-zeros pack of the real query. This replaces the single legacy-decode assertion in test_wo_a_projection_matches_reference. est_time 30 -> 60 s (the file measured 41-51 s on B200). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and cover decode without an extra cache - test_early_verify_graphs_share_one_packed_query_buffer now checks that both early verify graphs hold the buffer the first one allocated. Comparing the two buckets with each other also passed when _share_q_mega_pad_buffer assigned in the wrong direction (both None). - test_mega_kernels_leave_q_unchanged adds decode without an extra cache (extra_kv=None), the call the model makes on its compress_ratio 0 layers and on the SWA-only DSpark draft blocks: 18 subtests, was 12. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uffer when the forward ends An eager forward builds its attention metadata anew, and with it the MegaAttention packed-query buffer (tokens x 64 padded heads x 512 bf16; 1 GiB per rank for a 16K-token chunk at TP4). The backend keeps that metadata until the next forward, so the buffer stayed allocated between forwards. DeepseekV4Model.forward now drops it from the forward and tail metadata when the forward ends (DeepseekV4AttnBackend.release_q_mega_pad_buffer). An eager forward allocates and zeroes the buffer once either way; only when it is freed changes. A buffer a CUDA graph captured stays, and in capture mode (graph warmup and capture) the forward keeps its buffer for the capture after it. B200, one GPU, 40 packs of 16 -> 64 heads per forward, every forward through DeepseekV4Model.forward, one process, median of 5 interleaved rounds (main's per-layer pack / previous commit / this commit): decode, 8 tokens, CUDA graph: 145.4 / 98.3 / 98.3 us decode, 8 tokens, eager: 541.5 / 462.6 / 464.2 us extend, 8K chunk, CUDA graph: 11,350 / 5,904 / 5,904 us extend, 8K chunk, eager: 11,554 / 6,126 / 6,125 us held between eager forwards: 0 / 512 MiB / 0 (8K chunk) peak during a forward: 512 MiB in all three The packed queries are bitwise equal to main's pack in every arm. Tests: an eager forward through DeepseekV4Model.forward leaves nothing allocated (on the previous commit it fails with 67108864 != 0, the 1024-token buffer), and the end of a forward keeps a graph warmup's buffer and a captured one (a captured graph still replays exactly after an idle forward's end, with the freed memory of the warmup stream poisoned). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…into their own zeros The decoder tail graphs (deepseek_v4_replay_tail) capture on first use, inside an eager prefill's late-layer tail, and keep the tail metadata they captured with. That metadata shares the eager forward's MegaAttention packed-query buffer, so the graphs kept the buffer of the forward they captured in for the server's lifetime (1 GiB per GPU after a 16K-token chunk at TP4), and the end of the forward could not free it. The tail graphs now pack into fresh zeros, as every layer did before this PR; the memset sits in the graph, over the bucket's rows. Eager layers, prefill graphs and decode/verify graphs keep the shared buffer. The new test captures a tail graph through RowBucketGraphRunner inside an eager forward and checks that the forward's buffer is freed when the forward ends, and that the graph still packs exactly after that memory is reused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
|
/tag-and-rerun-ci |
|
/tag-and-rerun-ci |
Picks up sgl-project#47078, which repairs main's lint and HiCache imports that failed this PR's CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nvpohanh
left a comment
There was a problem hiding this comment.
[by Claude Code] This review is done by agents. Please feel free to disagree if they do not make sense.
Picks up sgl-project#47127, which fixes the KV layer router regression that stopped this PR's CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/rerun-failed-ci |
… release it on the MegaAttention backend Review follow-up, behavior unchanged: - MqaAttentionBase._zero_padded_q is back to main's version. MegaAttention's packed query uses a new MQALayer._zero_packed_q, which takes a DSV4Metadata (narrowed with isinstance in _forward_mega) and reads and writes q_mega_pad_buffer / q_mega_pad_captured directly instead of through getattr / setattr on field names. - DeepseekV4Model sets use_mega_attention in __init__ (the condition under which the CUDA dsv4 backend is DeepseekV4MegaAttnBackend). The end of the forward asserts that backend type and calls release_q_mega_pad_buffer directly, so a wrapper such as TboAttnBackend fails instead of skipping the release. Non-MegaAttention servers no longer look the method up each forward. - The comments in _share_q_mega_pad_buffer, release_q_mega_pad_buffer and _forward_mega are one or two lines. Test: test_forward_end_rejects_a_wrapped_backend runs DeepseekV4Model.forward with a TboAttnBackend around the MegaAttention backend and expects the assertion; the test stand-in backend is now a DeepseekV4MegaAttnBackend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndary Comment-only: the three files parse to the same AST as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The optimization make sense. But since the padded head slots are never written, could we keep one persistent zeroed buffer on the backend (sized to the max rows, zeroed once at init) and slice it everywhere? That would need much less code than the per-metadata buffers, sharing chain and release logic, and it avoids allocating and |
|
@akhilg-nv Could you address @weireweire 's comment above? |
…t buffer per backend DeepseekV4MegaAttnBackend owns one packed-query buffer, zeroed when the first pack allocates it and never reallocated, sized at startup to the largest forward the backend can see: a decode or verify batch and, outside a PD decode server, a prefill chunk or a prefill-graph or decoder-tail graph bucket. Every pack writes the real heads of a row slice of it, so the padded heads stay zero and eager forwards, captured graphs and the decoder tail all use the same memory. A forward larger than the buffer raises. flashmla_mega requires chunked prefill except on a PD decode server: with chunking disabled a single prompt has no size bound to allocate for. Without PD disaggregation, `auto` picks flashmla there instead; a PD prefill server must match its decode peer's backend, so it fails at startup. test_mega_kernels_leave_q_unchanged checks that no MegaAttention kernel writes its query, which the buffer's zero padding relies on for the server's lifetime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
[by Claude Code] @weireweire Agreed, and done: Edit: the first version of this comment had a larger forward raise, which made |
This comment has been minimized.
This comment has been minimized.
…r into fresh zeros A MegaAttention forward with more rows than the backend's packed-query buffer now gets no buffer and packs into a freshly zeroed tensor, as every forward did before the buffer existed, instead of raising. The buffer is left untouched, so captured graphs keep its address. With chunked prefill disabled no startup bound covers a prefill forward, and long prompts take this path; flashmla_mega therefore no longer requires chunked prefill, and `auto` and the --dsv4-attn-backend help text are as on main. The test imports ForwardMode from sglang.srt.model_executor.forward_mode, the module that defines it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/rerun-failed-ci |
[by Claude Code]
Motivation
On SM100/SM103 (B200/B300), DeepSeek-V4.1 Flash can run attention through FlashMLA MegaAttention
(
--dsv4-attn-backend flashmla_mega, FP8 KV cache), whose kernels take the query packed as(tokens, 32, padded_heads, 16)with the heads padded to 64. With attention TP > 1 each GPU holds fewer heads (16 of64 at TP4), and
pack_queryrebuilt the packed tensor in each of the 40 attention layers: a zero-fill of the wholepadded tensor, three quarters of it padding at TP4, then a copy of the real heads. This PR gives the MegaAttention
backend one packed-query buffer, zeroed once when it is first used and never reallocated. Each layer writes only its
real heads into a row slice of it, and nothing writes the padding, so it stays zero. Its size is fixed at startup
from the largest forward the server bounds there: a prefill chunk, a CUDA-graph bucket, or a decode or verify batch.
Eager forwards, captured CUDA graphs and the decoder tail graphs (CUDA graphs of the late layers that eager prefill
chunks replay) pack into this one buffer, so its address never changes. A forward with more rows, such as a long
prompt with chunked prefill disabled, packs into fresh zeros as on main and leaves the buffer as it is. The packed
values are bitwise identical and no kernel changes. On current main, MegaAttention is the default in PD prefill and
decode servers and in aggregated servers without attention TP; aggregated servers with attention TP > 1 default to
flashmlasince #46263 and use this path with--dsv4-attn-backend flashmla_mega.Impact
rows, eager, in CUDA-graph replays with eager forwards in between, and in a forward larger than the buffer).
sgl-eval GSM8K (200 questions) on 4x B200 TP4: 97.0% and 97.5% with the PR's first revision, 97.5% on main.
graph 145.4 -> 98.3 us; an 8K-token prefill chunk 11,494 -> 5,990 us in a CUDA graph and 11,717 -> 6,072 us eager.
TTFT 285.0 -> 275.5 ms (-3.3%) with the PR's first revision; 289.7 -> 271.0 ms (-6.5%) with this revision in the
memory run below, a smaller sample on a later main. Decode TPOT showed no regression with the first revision.
memory is main + 1.0 GiB at all times. The peak during a prefill is main + 1.0 GiB, as with the previous revision of
this PR. The KV pool is unchanged: the buffer is allocated after the pool is sized.
Modifications
+66 / -6 runtime code (2 files), +352 / -3 tests (1 file). No kernel changes.
DeepseekV4MegaAttnBackend.packed_query_buffer(rows, padded_heads, dtype)(deepseek_v4_mega_backend.py) returnsrows
[:rows]of the backend's buffer. The first call allocates it withtorch.zeros(asserted outside CUDA-graphcapture). A call with more rows than the buffer returns
None, and the caller packs into fresh zeros as on main;the buffer is never reallocated, because captured graphs keep its address.
Size, computed in
__init__bymax_packed_query_rows: rows x padded heads x 1 KiB (bf16), where rows isthe largest of
max(max running requests, decode CUDA-graph max bs), padded for MLP sync, xspeculative draft tokens (1 without speculation);
(
max_prefill_buffer_tokens(), 0 with chunked prefill disabled), the prefill CUDA-graph max bs, andSGLANG_DSV4_DECODER_TAIL_GRAPH_MAX_ROWS(2,048 by default; the decoder tail graphs capture every bucket upto it).
At TP4 with 16K-token chunks that is 16,384 x 64 x 1 KiB = 1 GiB per GPU; on a PD decode server with 256 running
requests and 6 draft tokens, 1,536 x 64 x 1 KiB = 96 MiB. With chunked prefill disabled it covers the decode and
verify batches and the CUDA-graph buckets, and longer prompts pack into fresh zeros.
Allocated on the first pack, in practice the first warmup forward at startup, after the KV pool is sized.
Ranks that never pack allocate nothing: those whose
wq_brows are permuted for MegaAttention (64 or 128 localheads with a FlashInfer MXFP8
wq_b), and DSpark draft backends.pack_query(q, padded_heads, out=None)(deepseek_v4_mega_backend.py): givenout, it writes only the real heads'slots. Without
outit works as before.MQALayer._forward_mega(deepseek_v4.py) packs intoattn_backend.packed_query_buffer(...), or into freshzeros when that returns
None.Why the padding stays zero. The buffer is zeroed when it is allocated, and nothing writes its padding after that:
pack_querywrites only[:, :, :heads].the token-interleaved V4.1 cache added in [Perf] dsv41: Serve token-interleaved V4.1 KV caches with sgl-flash-mla #45893. Both load q into registers with
ld.global.ncand never storeto it (
core_attn/kernel.cuh), andtest_mega_kernels_leave_q_unchangedchecks on SM100 that every kernel of bothsets leaves q bitwise unchanged at 16 -> 64, 64 -> 64 and 128 -> 128 heads.
Why one buffer per backend is safe. Packs run in
_forward_megaon the forward's stream, and CUDA graphs replayone at a time on that stream; each pack writes every real-head slot of its rows before the attention reads them.
Backends that can run at the same time on different streams (PD multiplexing's prefill and decode SM groups) are
separate instances with separate buffers. DeepSeek-V4.1 rejects two-batch overlap and pipeline parallelism.
Related:
pack_querycomes from #43922.Tests, in
test/registered/kernels/ops/attention/dsv4/test_mega_attention.py(expected values from the fresh-zerospack written out in the test, and hand-derived row bounds):
TestMegaAttention.test_mega_kernels_leave_q_unchanged, on SM100: each MegaAttention kernel (decode with theextra cache, decode without it, prefill), in both kernel sets, leaves its packed query bitwise equal to the
fresh-zeros pack, at 16 -> 64, 64 -> 64 and 128 -> 128 heads.
TestMegaPackedQueryBuffer, on any CUDA GPU:64 (the buffer's capacity) and 1 rows; 16 -> 64 at 8,192, 1,000 and 4,096 rows; 3 layers each. Each pack lands
in the buffer's storage and the padding is zero at the end. The allocator is filled with NaN before the buffer is
allocated, so a buffer that is not zeroed fails.
40-row graphs are captured, then three rounds of replays with new inputs run with a 17-row eager forward between
replays. Every pack is bitwise equal, the address never changes, and the padding stays zero.
NaN, a 49-row forward is bitwise equal to the fresh-zeros pack and does not use the buffer's storage; the buffer
keeps its address, its contents and its zero padding, and the next 48-row forward packs into it again.
TestMaxPackedQueryRows: the realmax_packed_query_rowson a published config, six hand-derived cases: thechunk dominates; the 2,048-row tail bucket outgrows a 1,024-token chunk; an 8,192-token prefill-graph bucket
outgrows a 4,096-token chunk; chunked prefill disabled, where no chunk term enters and the 2,048-row tail bucket
sets the size; a PD decode server with 1,000 eager requests x 4 tokens; 50 requests padded to 56 for MLP sync.
Each assertion was checked against a broken version of the code on B200, and each broken version fails exactly the
tests named:
torch.emptyinstead oftorch.zeros: the bitwise, graph and larger-forward tests;_forward_megapacking into fresh zeros again: the bitwise and larger-forward tests;test;
MLP-sync padding, or with a context-length-sized prefill term when chunked prefill is disabled: the bound test;
set (6 versions): exactly that kernel's 3 subtests of the kernel test.
Accuracy Tests
The packed query is bitwise identical and the kernels do not change. To check the integration, B200 TP4 servers ran
the DeepSeek-V4.1 Flash cookbook low-latency recipe on main (535aeae) and on this PR's first revision, when TP4
servers defaulted to MegaAttention: TP4, EP4 (the experts split over the same 4 GPUs), DSpark speculative decoding
(DeepSeek-V4.1's bundled draft model) and decode CUDA graphs. A second pair of servers added the prefill CUDA-graph
flags of the DeepSeek-V4.1 CI tests (
--cuda-graph-backend-prefill breakable --cuda-graph-max-bs-prefill 8192 --cuda-graph-max-seq-len-prefill 16384 --chunked-prefill-size 8192).dsv41_flash_common.py):200 examples, 64 threads,
max_tokens4096, thinking off (no chat template kwargs), temperature 0.0, top_p 0.95.0.5-point gap is one question.
replayed, with the old pack bitwise. In the memory run below, every server of this revision served all its requests,
and the DSpark accept length was 3.45 and 3.31 against 3.35 and 3.31 on main.
test_mega_attention.pytest_flashmla_v41.py(also callspack_query)unit/model_executor/runner/test_early_verify_metadata.pyattention/unittests/dsv4/test_deepseek_v4.pytest_num_token_non_padded_localization.py(RowBucketGraphRunner)Speed Tests and Profiling
Where on the Pareto curve, and why a static workload. The change removes a zero-fill per attention layer whose
size is rows x padded heads x 1 KiB, so its saving depends only on how many rows a forward has, not on the request
mix. It is largest in prefill chunks and matters most where a chunk's time is the latency: long prompts at low
concurrency (TTFT at the low-latency end). In decode it saves about 1 us per layer. A static workload measures that
directly; the end-to-end checks below are the low-latency end of the 4x B200 TP4 recipe.
The microbenchmark and the memory run below measured this PR before a forward larger than the buffer fell back to
fresh zeros (it raised instead). No forward in either is larger than the buffer, so they ran the same buffer path as
this revision.
Microbenchmark on one B200: each tree's own
MQALayer._forward_mega, called throughDeepseekV4Model.forwardwith the attention call stubbed out, 40 packs per forward of a (tokens, 16, 512) bf16 query into the 64-head layout.
Exact checks against main's pack run first, eagerly and on CUDA-graph replays with new inputs, and all passed. One
process per tree in the order main, previous revision, this PR, this PR, previous revision, main; 5 rounds per
process; us per forward, median of each tree's 10 rounds:
Eager, the 8K-token forward is 139 us faster, about one 512 MiB allocation and zero-fill that it no longer does;
eager decode is 42 us faster in both processes of each tree (host time; not profiled further).
run back to back; in serving, the projection GEMM writes q right before the pack and attention reads it right
after. The 8K-token pack (512 MiB) is far larger than L2.
the buffer once per replay: decode of 8 tokens 126.5 -> 90.1 us (B300) and 124.6 -> 83.7 us (VR200), an 8K-token
chunk 11,201 -> 5,897 us and 8,699 -> 6,332 us.
End to end (the PR's first revision). 4x B200, the cookbook low-latency recipe (TP4, EP4, DSpark, decode CUDA
graphs, chunked prefill 16,384), on main 55f23a8, when TP4 servers defaulted to MegaAttention (on current main,
add
--dsv4-attn-backend flashmla_mega). Main and the PR on one node in the order main, PR, PR, main:three), so the decode gain is not resolved.
End to end, this revision (from the memory run below). Main 68e57f4, the previous revision and this PR, one
server each in the order main, previous, this, this, previous, main, 6 prompts of 32K tokens per server at
concurrency 1 (one or two of each server's first two requests took 370-500 ms, in every arm):
with either revision.
the two runs used different main commits, and the difference is not explained.
Memory
In this recipe every 16K-token prefill chunk runs eagerly, and its packed query is 16,384 x 64 x 512 x 2 B = 1 GiB
per GPU. Main allocated it per layer and freed it after the layer's attention; the previous revision allocated it
once per eager forward and freed it when the forward ended; this PR allocates it once and keeps it.
4x B200 TP4, the recipe above plus
--dsv4-attn-backend flashmla_mega --enable-metrics, one node, a fresh server perrun, in the order main 68e57f4, previous revision, this PR, this PR, previous revision, main. A hook in every
scheduler records
torch.cudamemory once a second and resets the peaks at each phase; nvidia-smi is read every200 ms. Phases: 6 x 32K-token prompts at concurrency 1 (as above with
--num-prompts 6), 5 s idle, ShareGPT 64 x 512at concurrency 8 (
--num-prompts 64), idle. Rank 0, MiB:max_total_num_tokensare within 1 MiB of rank 0 for allocated memory and within 26 MiB for reserved memory after the prefills. Every
server resolved
flashmla_megaand served 6/6 and 64/64 requests.the previous revision held it only during an eager forward.
where main's per-layer copy lived only through each layer's attention.
The extra is cached by PyTorch's allocator and free for later PyTorch allocations; we have not attributed it to
specific allocations. During the decode phase the reserved memory falls back in every server, and this PR ends at
main + 1 GiB.
free GPU memory the server logs after start-up (
available_gpu_mem) is 26.37 GB with this PR against 27.35 GB onmain.
Checklist
Review and Merge Process
/tag-and-rerun-ci,/tag-run-ci-label,/rerun-failed-ci🤖 Generated with Claude Code
CI States
Latest PR Test (Base): ⏳ Run #42127536643
Latest PR Test (Extra): ❌ Run #42127536579
Latest PR Test (AMD ROCm 10): ⏳ Run #42127536517
Tip
Base CI only covers the core cases, and its workload may not exercise your change. Use
/rerun-testto run the tests your change touches, or copy this prompt for an agent: