Skip to content

Reduce KDA prefill OOM risk with opt-in workspace limits - #37535

Open
ormandj wants to merge 2 commits into
sgl-project:mainfrom
ormandj:pr/kda-blocked-extend
Open

ormandj wants to merge 2 commits into
sgl-project:mainfrom
ormandj:pr/kda-blocked-extend

Conversation

@ormandj

@ormandj ormandj commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Motivation

Long or batched prefills can run out of GPU memory inside KDA attention because its temporary workspace grows with every token passed to chunk_kda. Lowering the global prefill chunk size also changes batching for the rest of the model.

This PR lets KDA process a large extend in smaller internal blocks while preserving the global prefill limits. It bounds the workspace allocated by each KDA call, not total model or request memory. An earlier per-sequence revision allowed author-observed GLM-5.3-Flash long/image prefills to complete after OOMs; no full-model receipt for the current grouped implementation is attached.

Modifications

  • Add SGLANG_KDA_EXTEND_BLOCK_TOKENS, disabled by default. Values below 64 retain the original call; larger values round down to a 64-token boundary.
  • Group whole sequences that fit and split longer sequences. Carry split-sequence recurrent state through FP32 scratch, rounding reduced-precision pool state only on the final writeback.
  • Preserve padded-row handling and intermediate-state ordering. Fall back to the original call under graph capture or incompatible host metadata.
  • Cache block-boundary metadata by device, stream, dtype and content. Full-extend output storage and tracked intermediate states still scale with the complete extend.

Accuracy Tests

Author CPU validation at 290173ceef: 8 tests and 14 subtests passed, with CUDA hidden. Its Python runtime and test trees are unchanged at refreshed head e96e6f0d12 on main afe90a8bc9. Earlier GPU and serving results retain their stated source scope.

The real-kernel regression starts the blocked and unblocked paths from identical pool copies, spies on every KDA call to enforce the block limit, and compares outputs, final states and untouched slots. Cases include ragged/grouped sequences, partial final blocks, reduced-precision pools, strided inputs, padded rows and intermediate states. CPU fake-kernel cases check call planning, state carry, metadata caching and fallback.

PYTHONPATH=python python -m pytest -q test/registered/kernel/attention/test_kda_extend_blocked.py

Author-reported SM120 result: 15 passed and 20 subtests. CPU-only run: 8 passed and 14 subtests. Results are bitwise equal when blocking preserves the intra-chunk kernel variant; the dispatch-threshold-crossing case is checked at atol=rtol=1e-2, not exact equality.

Speed Tests and Profiling

Blocking trades extra kernel calls and possible metadata synchronization for lower per-call workspace. Author-reported launch-bound microbenchmarks on one RTX PRO 6000 Blackwell took 0.50-0.65 ms unblocked versus 3.67-4.45 ms with eight 2048-token calls. Those one-head shapes measure call overhead, not model throughput. Peak memory of the current grouped path was not measured.

Checklist

  • Format changed code and add CPU/GPU regression coverage.
  • State the numerical contract, memory scope and overhead.

Developed with AI assistance.


CI States

Latest PR Test (Base): ❌ Run #34282201461
Latest PR Test (Extra): ❌ Run #34282201310
Latest PR Test (AMD ROCm 7.2): ❌ Run #34282201526

@ormandj ormandj changed the title Bound the per-call chunk_kda workspace of the KDA Triton extend with opt-in token blocks Limit KDA prefill workspace without lowering the global prefill chunk size Sep 6, 2026
@ormandj ormandj changed the title Limit KDA prefill workspace without lowering the global prefill chunk size Reduce KDA prefill OOM risk with opt-in workspace limits Sep 6, 2026
@ormandj ormandj mentioned this pull request Sep 6, 2026
11 tasks
@ormandj
ormandj force-pushed the pr/kda-blocked-extend branch from 27e648e to aa05881 Compare September 8, 2026 02:45
…_kda calls

TritonKDAKernel.extend ran chunk_kda over the whole extend in one call, so
the chunk kernels' workspace (l2-normed q/k, gate cumsums, w/u/kg
projections, per-chunk state buffer, intra-chunk A matrices) scaled with the
extend length. The scheduler bounds that length (max_prefill_tokens,
chunked_prefill_size), but the workspace is a per-layer transient on top of
the weights and pools, and on a memory-tight KDA deployment the prefill chunk
that keeps throughput up was too large for these kernels.

Add SGLANG_KDA_EXTEND_BLOCK_TOKENS (opt-in; default 0 and any value below
the 64-token chunk disable, larger values round down to a multiple of 64).
When set, run chunk_kda over blocks of at most that many tokens. Consecutive
whole sequences that fit within the block share one call, so short-sequence
batches stay batched; only longer sequences are split, one call per block,
chaining their recurrent state through a per-sequence fp32 scratch slot
(gathered from the pool, zero for the padded-row sentinel -1, written back
once and masked for -1; the kernel loads initial_state[index] and stores the
final state back, INPLACE_UPDATE, skipping both for -1). The one cast back
to a bf16/fp16 pool is the single call's one final rounding. Block
boundaries fall on chunk boundaries; the output assembles in place in one
contiguous v buffer (o=v); intermediate states go into one preallocated
packed tensor. Each call's cu_seqlens comes from a small content-keyed LRU
cache so repeated layouts reuse one tensor and hit fla's identity cache.
Blocking is skipped when extend_seq_lens_cpu is missing or does not match
the device metadata in count or total (the boundaries agree by construction:
query_start_loc is the device cumsum of the same host list), and under graph
capture. Blocking is off by default because each block is a full kernel
pipeline and each distinct layout costs one metadata sync; that latency
trade has not been measured.

Numerical contract: bitwise equal to the single call when every call
dispatches the same intra-chunk kernel variant (B * num_chunks * H <= 256 in
fla/kda.py); rounding-level differences when blocking moves a call across
that threshold.

Add test_kda_extend_blocked.py: GPU tests compare the blocked path against
the single call (exact where the kernel variant is the same, including
bf16/fp16 pools, ragged final blocks, grouped short sequences and a padded
-1 row with a nonzero slot 0; a spied threshold-crossing 8-head case with a
tolerance), with a spy proving the call plan. CPU tests cover the plan,
grouping, metadata fallback, scratch carry, padded-row zero state and the
cu_seqlens cache bound with a stand-in kernel whose results depend on the
loaded state. GPU tests were not executed for this revision.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant