Repository navigation
🚧 [WIP][llm][npu] Split mixed chunked-prefill FIA into prefill/decode calls on Ascend 950 - #34387
Closed
TallMessiWu wants to merge 5 commits into
Closed
TallMessiWu wants to merge 5 commits into
TallMessiWu wants to merge 5 commits into
Conversation
TallMessiWu
force-pushed
the
junlin_a5_fia_mixed_split
branch
from
August 11, 2026 08:26
1e0e0d6 to
2a8815c
Compare
Prerequisite only: Qwen3.5 cannot start on Ascend 950 without the portable GemmaRMSNorm dispatch from sgl-project#32745, so the FIA work on top of this commit has no way to run without it. No FIA code here. Drop this commit by rebasing onto main once sgl-project#32745 lands.
A mixed chunked-prefill batch runs one npu_fused_infer_attention_score call covering both long causal prefill queries and one-token decode queries. The kernel tiles for the worst case, so the decode span pays the prefill's cost. Split it into two independent FIA calls on Ascend 950, keeping SGLang's [prefill][decode] token order. Following vllm-ascend#11948, which mirrors the layout as [decode][prefill], so the slicing and the cumulative-length rebasing differ here. - ScheduleBatch.mix_with_running() records the prefill/decode boundary before merge_batch() appends the decode requests; ForwardBatch carries it, and _set_fia_mixed_split_metadata() validates it against seq_lens_list_cumsum rather than silently computing the wrong attention. TBO sub-batches reset the boundary and keep the single-call path. - _forward_fia_mixed_split() issues the prefill call over query[:n_prefill], then the decode call with its cumulative query lengths rebased to 0 as the TND layout requires. Both read the full paged KV cache and select pages through their own block-table slice. - ForwardMode.MIXED has to survive to attention dispatch, so two places that erased it are fixed: HybridLinearAttnBackend.forward() overrode the base dispatch without a MIXED branch (hybrid GDN models hold AscendAttnBackend as a child, so mixed batches fell through to forward_extend and the split never ran), and the prefill CUDA graph rewrote MIXED to EXTEND -- can_run_graph() now declines only these batches, matching vllm-ascend, where ACL graphs only capture DecodeOnly. Gated by SGLANG_NPU_FIA_MIXED_SPLIT (default on) and an Ascend 950 SoC probe; a no-op on every other SoC and on the non-FIA path.
TallMessiWu
force-pushed
the
junlin_a5_fia_mixed_split
branch
from
August 13, 2026 03:14
7fd447a to
b56ef22
Compare
6 tasks done
Only conflict is srt/layers/layernorm.py, where main added _NPU_GEMMA_RMS_NORM_TRITON_MAX_HIDDEN_SIZE next to the sgl_kernel_npu Gemma import guard from PR sgl-project#32745. Keep both: the Gemma provider stays selected at sgl-kernel-npu build time (RFC sgl-project#35709), with no runtime dispatch added here.
The split needs Ascend 950, but supports_fia_mixed_split() asked for it by comparing get_soc_version() to 260 inside the NPU utils -- the only runtime SoC branch left in srt/, and the shape RFC sgl-project#35709 wants replaced. Add a minimal srt/hardware_backend/npu/device_op.py holding the runtime-visible generation contracts: the SoC is read once at startup and resolved into a device operator, and feature code reads a named contract (fia_splits_mixed_batch) instead of testing the generation itself. An unnamed SoC, or a failed query, keeps the baseline single-call contract every generation supports. Feature ownership is unchanged: SGLANG_NPU_FIA_MIXED_SPLIT still decides whether the split is wanted, and the device operator only says whether this generation requires it.
Contributor
|
Thanks @TallMessiWu. Closing this because it is still a draft and has not been updated in 38 days. Reopen it if the work is still relevant. Some directories moved recently, so an older branch may need retargeting: |
This was referenced Oct 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Ascend 950 (A5), a mixed chunked-prefill batch currently goes through a single
npu_fused_infer_attention_score(FIA) call that has to cover two very differentshapes at once: long causal prefill queries and one-token decode queries. The
kernel tiles for the worst case, so the decode part pays the prefill's cost.
This PR splits that into two independent FIA calls — one for the prefill span and
one for the decode span — preserving SGLang's
[prefill][decode]token order.The design follows vllm-ascend #11948.
Note the batch layouts are mirrored: vllm-ascend reorders to
[decode][prefill]via
split_decodes_and_prefills, while SGLang'smix_with_running()produces[prefill][decode], so the slicing and the cumulative-length rebasing differ.The 9.4% reported there is a single-FIA-call measurement (4360us → 3949us on one
batch), not a serving result; nothing about it is carried over here.
Dependencies and current validation
Updated on 2026-10-10 with ordinary merges; base remains
main. Branch head:01bb8dc685. Includes official mainc55790d38b.Contains the shared fixes from #32745 at
cf48b477db; Ascend 950 Qwen3.5 requires the paired target wheel from sgl-project/sgl-kernel-npu#638. #36426 is a separate validation dependency. Review the FIA increment against the refreshed #32745 branch.Current-head checks: 70 attention tests executed in isolation against the actual Python definitions with CPU tensors / mocked NPU calls, 8 graph-policy cases (TP/DP, mixed/pure, split enabled/disabled), 8 isolated Gemma tests, shared partial-scale probes, and applicable pre-commit hooks passed. These are CPU/static checks, not 910/950 operator, model-load, warmup, accuracy, or performance validation. Full SRT test collection was unavailable in this environment because its serving dependencies are not installed. Target hardware validation remains required.
Remains Draft. GitHub reports
viewerCanReopen=false; the fork head is updated but reopening needs repository permissions. Hardware measurements below predate this refresh and must be rerun before drawing conclusions about this head.Changes
Split path
AscendAttnBackend.forward_mixed()picks between one FIA call and two._forward_fia_mixed_split()issues the prefill call overquery[:n_prefill]with
block_table[:n_prefill_reqs], then the decode call over the remainder.The decode span's cumulative query lengths are rebased to start at 0, which the
TND layout requires; both calls read the full paged KV cache and select their
own pages through their block-table slice.
_should_split_fia_mixed()gates on: split enabled, FIA in use, non-MLA, notopk_indices,AttentionType.DECODER, not cross-attention, no slidingwindow, GQA head counts divisible, and boundary metadata present.
Mixed-batch boundary plumbing
ScheduleBatch.mix_with_running()recordsmixed_num_prefill_reqs/mixed_num_prefill_tokensbeforemerge_batch()appends the decoderequests, then sets
ForwardMode.MIXED.ForwardBatchcarries both fields;_set_fia_mixed_split_metadata()validatesthem against
seq_lens_list_cumsumand fails loudly on an inconsistentboundary rather than silently computing the wrong attention.
None— their token ranges donot match the parent batch, so they keep the single-call path.
Keeping
ForwardMode.MIXEDalive until attention dispatchTwo places used to erase MIXED before it reached
forward_mixed():ModelRunnerholds an
AscendHybridLinearAttnBackend, not theAscendAttnBackend—the latter is its
full_attn_backendchild.HybridLinearAttnBackend.forward()overrides the base dispatch and had no MIXED branch, so mixed batches fell
through to
forward_extend()and the split never ran. Added the branch plus aforward_mixed()that routes full-attn layers to the child while thelinear/GDN side keeps its extend path.
captured with EXTEND, and that rewrite reaches attention dispatch.
The runner's common
prefer_eager_mixed_prefillpolicy declines these mixedbatches in both schedule-time DP votes and forward-time graph eligibility;
pure prefill batches retain the graph. This mirrors vllm-ascend,
where ACL graphs only capture
DecodeOnlyand a mixed batch is always eager.enable_fia_mixed_splitis declared onAttentionBackend(defaultFalse) sodispatch sites read it directly instead of through a defensive
getattr, andwrappers mirror it from their full-attn child.
Gating
SGLANG_NPU_FIA_MIXED_SPLITTruesupports_fia_mixed_split()get_soc_version() == 260)ASCEND_USE_FIAOn every other SoC and on the non-FIA path this is a no-op.
Key NPU APIs used
torch.ops.npu.npu_fused_infer_attention_scoresparse_mode=3, paged KV viablock_table+block_size=page_sizetorch_npu.npu.get_soc_version()Files Changed
hardware_backend/npu/attention/ascend_backend.pyascend.fia_mixed.*profile rangeshardware_backend/npu/utils.pysupports_fia_mixed_split()SoC probelayers/attention/hybrid_linear_attn_backend.pyforward_mixed()for hybrid GDN wrapperslayers/attention/base_attn_backend.pyenable_fia_mixed_splitmodel_executor/runner/prefill_cuda_graph_runner.pymanagers/schedule_batch.py,model_executor/forward_batch_info.pybatch_overlap/two_batch_overlap.pyenviron.pySGLANG_NPU_FIA_MIXED_SPLITAccuracy Tests
GSM8K on Ascend A5, Qwen3.5-27B, TP=1,
--enable-mixed-chunk,--chunked-prefill-size 4096, 100 concurrent requests, greedy decode:The concurrency is what makes this the relevant test: at that batch size the
mixed batches carry many concurrent decode requests, so the multi-request
slicing of
block_table/seq_lens_cpu_intand the rebased cumulative-lengthlist are exercised on every step, not just the single-decode-request case.
The 0.83-point gap is inside this setup's resolution. Splitting one FIA call
into two changes the bf16 reduction order, one flipped argmax early in a
chain-of-thought answer redirects the rest of it, and GSM8K's own binomial
standard error at this size is ~0.8 points per side.
Operator-level max absolute difference across all 40 shapes in the sweep
below: exactly
0for head_dim=256,4.883e-4for head_dim=128 — bf16rounding order, no systematic drift, and identical whether the prefill span
is 1024 or 3968.
Speed Tests and Profiling
Operator microbenchmark (Ascend 950)
Direct FIA calls, not a serving measurement. One rank, simulated TP by dividing
head counts, 10 warmup + 200 timed iterations, paired per unique local attention
shape.
The shape is taken from a mixed batch this branch actually produces
(Qwen3.5-27B, TP=1,
--chunked-prefill-size 4096, 32 concurrent requests):3968 prefill tokens against 23 decode requests,
kv_len=8192,block_size=128,mask_size=2048— the last one being whatAscendAttnMaskBuilderhardwires, independent of the prefill span, becausesparse_mode=3takes a compressed causal template.Four shapes regress, and they share a signature --
head_dim=256with at mostthree local query heads:
head_dim=128never regresses, not even at 2 local query heads (+4.87%), sothis is not a plain head-count threshold -- the wider head interacts with it.
Full per-configuration table (40 unique local shapes)
Gain vs prefill span
Qwen3.5-27B, TP=1, 23 decode requests,
kv_len=8192:Positive across the whole range. The gain shrinks as the prefill span grows,
since the span increasingly dominates its own tiling cost and the decode
requests distort it proportionally less. The span is capped by
--chunked-prefill-size, not by the request's input length, so this axis is adeployment choice rather than a property of the traffic.
No end-to-end numbers are claimed
Qwen3.5 is a hybrid GDN model:
layers_block_typemarks everyfull_attention_interval-th layer as full attention (4 here), so only a quarterof the layers reach FIA at all. Measured over three runs per side on a workload
where mixed batches are 89% of all scheduler steps, the deltas on throughput,
TTFT, TPOT, ITL and E2E latency all landed within +/-0.4%, against a
run-to-run spread of 1.4-5.3%. That is consistent with the arithmetic — a 16%
kernel win on ~8% of a step's time is ~1.4% — and it is below what this setup
can resolve.
Reporting a serving number from that would be reporting noise, so none is given.
An architecture where every layer is full attention would see proportionally
more of the kernel gain.
Open questions
_should_split_fia_mixed()has nohead-count condition, so the four regressing shapes above take the slower
path.
tp_q_head_num >= 4whenqk_head_dim >= 256would cover all four,at the cost of also excluding one +2.86% shape (TP=4, 4/1, HD=256). Whether
that is worth a hardware-shaped condition in the gate is a judgement call I
would rather have a reviewer make -- the alternative is leaving it and
accepting a regression on 1-3-head TP=8 deployments of small Qwen3.5 models.
the graph for the split on those steps. On the tested A5 configuration the
prefill graph is auto-disabled (
cuda_graph_configresolvesprefill.backend='disabled'), so the trade is currently free — that stopsholding once it is enabled.
Checklist
CI States
Latest PR Test (Base): ❌ Run #32805837805
Latest PR Test (Extra): ❌ Run #32805837633
Latest PR Test (AMD ROCm 7.2): ❌ Run #32805837824