[NPUW] Support GQA sliding-window and smooth-softmax in DecomposeGQA - #37423
Draft
ankitm3k wants to merge 3 commits into
Draft
[NPUW] Support GQA sliding-window and smooth-softmax in DecomposeGQA#37423ankitm3k wants to merge 3 commits into
ankitm3k wants to merge 3 commits into
Conversation
NPUW's GQA decomposition ignored local_window_size, so a sliding-window model on the NPU LLM path was lowered as plain causal attention (silently wrong output). Add the local-window band to both mask branches, and generalize the generate-branch base mask so it is correct for multi-token (speculative) decode as well. The window is applied as an additive mask over NPUW's static, host-managed KV cache; the physical rolling buffer (sliding_window_cache) is intentionally not honored, since masking over the full cache yields the same attention result and fits NPUW's static-shape / host-managed-cache design. The band is branch-specific because NPUW's mask coordinates are not the true absolute positions: - prefill: past and current share one right-aligned frame, so (vert - hori) is the true query-key distance; mask keys with (vert - hori) >= local_window_size. - generate: past keys are left-aligned in [0, past_seqlen) and the current block sits at the fixed capacity slots [C, C+curr), so the mask coordinate is not the query/key position. The window is computed in true absolute positions per query row: q_abs(i) = (total - curr) + i, and k_abs(h) = h for a past slot (h < capacity C) or (total - curr) + (h - C) for a current-block slot; mask when q_abs - k_abs >= local_window_size. The past/current boundary is the capacity C, not seqlens_k (which is wrong once the past overflows the buffer, seqlens_k >= C). The generate base kv-cache mask was also only correct for single-token decode: it kept past slots up to seqlens_k (retaining stale slots for curr > 1) and a single diagonal current slot. It now gates on the resident past length (past_seqlen = seqlens_k + 1 - curr) and the whole current block ([C, C+curr)), with the causal triu providing intra-block causality. Verified numerically against a NumPy reference on CPU (decomposed graph), generate and prefill, across curr = 1 / 2 / 3, local_window_size = -1 / 1 / 3 / large, and past-overflow (seqlens_k >= capacity): the window masks exactly the correct keys (plain-causal at large window, self-only at window 1), stale buffer slots are fully masked, and single-token decode is unchanged.
NPUW's GQA decomposition ignored smooth_softmax and the head_sink input, so those models were lowered as a plain softmax (silently wrong denominator). Wire them onto SDPA's sink input, mirroring the common decomposition: build a [1, num_heads, 1, 1] sink tensor - the head_sink input reshaped (per-head), or a zero broadcast for plain smooth_softmax - and pass it as the 6th input of ScaledDotProductAttention with an explicit scale (op scale, or the default 1/sqrt(head_size)). The extra logit participates in the softmax denominator and is sliced out, matching ONNX Runtime. When neither smooth_softmax nor head_sink is set, the original 4-input (or scaled 5-input) SDPA path is kept unchanged. Verified numerically against a NumPy reference on CPU (decomposed graph), generate and prefill branches: smooth_softmax (zero sink -> denominator 1 + sum exp), per-head head_sink (denominator exp(sink_h) + sum exp), and the no-sink regression path - all match to ~1e-7.
Adds structural coverage for ov::npuw::DecomposeGQA: the internal GroupQueryAttention op must lower to a single ScaledDotProductAttention in both the prefill and generate branches, the local_window_size band must decompose on both, and smooth_softmax / head_sink must wire the SDPA sink (6-input form). Registers the new file in the explicit unit-test source list. Guards against the pass silently failing to fire or dropping the window/sink wiring on future refactors; numerical parity of the windowed coordinates is covered off-tree against a NumPy port of the ORT windowed attention.
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.
Details
Extends NPUW's
DecomposeGQA(decompose_gqa.cpp) to honor twocom.microsoft.GroupQueryAttentionfeatures that were previously ignored on the NPU LLM path, matching the common
GroupQueryAttentionDecompositionbehavior:local_window_size(sliding-window attention) — previously a windowed model was lowered as plaincausal attention (silently wrong output).
smooth_softmax/head_sink— previously lowered with a plain softmax (silently wrong denominator).Two feature commits + one test commit. Touches only
decompose_gqa.cppand adds a unit test.What changed
Sliding window (
local_window_size)Applied as an additive mask band over NPUW's static, host-managed KV cache. The band is branch-specific
because NPUW's mask coordinates are not the true absolute positions:
vert - horiis the true query-keydistance; mask keys with
vert - hori >= local_window_size.[0, past_seqlen)and the current block sits at the fixedcapacity slots
[C, C+curr), so the mask coordinate is not the query/key position. The window is computedin true absolute positions per query row —
q_abs(i) = (total - curr) + i,k_abs(h) = hfor a past slot(
h < C) or(total - curr) + (h - C)for a current-block slot — masking whenq_abs - k_abs >= local_window_size. The past/current boundary is the capacityC, notseqlens_k(
seqlens_kis wrong once the past overflows the buffer,seqlens_k >= C). The generate base KV-cache maskwas also generalized from single-token-only to gate on
past_seqlenand the whole current block, so it iscorrect for multi-token (speculative) decode.
sliding_window_cache(the physical rolling KV buffer) is intentionally not honored: NPUW manages the KVcache statically on the host, so applying the window as a mask over the full cache yields the same attention
result and fits NPUW's static-shape design. The frontend guarantees
local_window_size >= 1wheneversliding_window_cache = 1, so the masking path fully covers the windowed-cache semantics.smooth_softmax / head_sink
Wired onto SDPA's sink input, mirroring the common decomposition: a
[1, num_heads, 1, 1]sink tensor (thehead_sinkinput reshaped per-head, or a zero broadcast for plainsmooth_softmax) passed as the 6th inputof
ScaledDotProductAttentionwith an explicit scale. The extra logit participates in the softmax denominatorand is sliced out. When neither is set, the original 4-/5-input SDPA path is unchanged.
batch > 1is rejected at the internal-op level, so NPUW inherits that guard; quantized KV (i8/i4/f8) is out of scope here — proper VPUX-native support is tracked separately.Testing
Added
npuw_decompose_gqa_test.cpp(ov_npu_unit_tests, 6 cases): the internal op fully decomposes to asingle SDPA in both prefill and generate branches, the
local_window_sizeband decomposes on both, andsmooth_softmax/head_sinkselect the 6-input SDPA (sink) form. All pass.Numerical parity of the windowing coordinates was verified off-tree against a NumPy port of the ORT windowed
attention on the decomposed graph (run on CPU), across generate and prefill,
curr = 1 / 2 / 3,local_window_size = -1 / 1 / 3 / large, and past-overflow (seqlens_k >= capacity): the window masksexactly the correct keys (plain-causal at a large window, self-only at window 1), stale buffer slots are fully
masked, and single-token decode is unchanged; smooth_softmax (zero sink) and per-head head_sink match to
~1e-7. A committable numerical oracle test (replacing the off-tree check) is a tracked follow-up — the
committed structural tests guard against the pass silently not firing or dropping the window/sink wiring.
Notes / scope
host-side KV management (
num_stored_tokens/make_tensor_slice) during a real window-crossingspeculative step is not covered by an automated test.
Tickets:
AI Assistance
Yes — used for mapping the ONNX Runtime windowed-attention semantics onto NPUW's capacity-relative mask
coordinate system, adversarial correctness review of the generate-branch coordinates, and generating the
NumPy reference tensors.