[FA4] Harden block_logit: scaled logit, pack_gqa guard, docs, tests#164
Open
ForFishes wants to merge 4 commits into
Open
[FA4] Harden block_logit: scaled logit, pack_gqa guard, docs, tests#164ForFishes wants to merge 4 commits into
ForFishes wants to merge 4 commits into
Conversation
…ogue Add an optional block_logit output to the SM100 FlashAttention forward that, computed in the softmax epilogue at near-zero extra cost, emits the per-(query, key-block) max of the RAW (pre-scale, post-mask) q*k^T logit. This feeds the non-differentiable Top-K block selection used by HySparse sparse attention, letting the full-attention path produce both the attention output and the block score in a single fused pass instead of a separate scoring kernel. - interface._flash_attn_fwd: new block_logit / block_size args, fp32/CUDA validation + dlpack conversion, compile_key + constructor + kernel-call threading. Returned tuple unchanged; block_logit written in place. - flash_fwd_sm100: thread mBlockLogit through __call__ -> kernel -> softmax_loop -> softmax_step; bucket each thread's row fragment into block_size sub-blocks and write per-block maxes mirroring the LSE addressing. No MMA/smem/cross-warp traffic. Handles split-D (write once) and non-split-D (both stages), and skips sub-blocks past seqlen_k to stay in bounds when seqlen_k is not a multiple of block_size. - tests: block-score accuracy across head dims (64/128/256), head counts, block sizes (32/64/128), causal/non-causal, and corner cases (ragged seqlens, seqlen_k not a multiple of block_size, single token, seqlen_q != seqlen_k), plus a check that the fused epilogue leaves out/lse bit-identical.
Follow-up review hardening for the SM100 fused HySparse block-score (block_logit) added in 4a7d31a. No change to the fused reduction math. - interface._flash_attn_fwd: add input guards for block_logit -- assert SM 10.x, non split-KV, fp32, ndim==4, and num_blocks >= ceil(seqlen_k/ block_size) -- so misuse fails loudly instead of passing an extra arg to an SM90 kernel or silently indexing out of bounds. - Docs/comments: correct the "RAW q*k^T" wording. The score is the post-score_mod, post-mask (still pre-softmax-scale) logit, i.e. the actual attention score INCLUDING any score_mod bias -- an attention-importance proxy for block selection, reducing to raw q*k^T only when no score_mod is set. Also document the write contract: the kernel only writes key-blocks the attention loop visits, so fully-masked/future blocks are never written and the caller MUST pre-initialize block_logit to -inf. - tests: add GQA coverage (qhead_per_kvhead>1), a large multi-n-tile non-causal case (cross-tile column addressing + every block written), and a causal contract test pinning that unvisited future blocks are left untouched while reachable blocks are overwritten. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Store the SCALED attention logit (softmax_scale * q.k + score_mod bias) in block_logit instead of the raw q.k. This puts every head on one head-independent scale, so a downstream `block_logit - LSE` gives log(max attention weight in the block), comparable across heads for cross-head Top-K block selection (HySparse). The score_mod path already folds softmax_scale into the logit; the no-score_mod path carries raw q.k, so multiply by softmax_scale (= scale_log2 * ln2) there only. Add an assert rejecting pack_gqa=True with block_logit: block_logit is indexed by the query head and query row, but under pack_gqa the query heads are packed into the M/row dim and head_idx becomes the KV head, so writes would land in wrong locations. pack_gqa defaults to (qhead_per_kvhead > 1), so GQA callers must pass pack_gqa=False explicitly. Update tests: reference now scales by softmax_scale, tolerances scale accordingly, and add a negative test that block_logit + pack_gqa (explicit True and defaulted) raises.
…ence) The fused HySparse block-score epilogue previously bucketed key columns by their ABSOLUTE packed-sequence block (abs_col // block_size). Downstream HySparse selection interprets block ids DOCUMENT-relative to each query row's document start (bos), so packed multi-document inputs (bos > 0) were scored against a shifted grid and selected the wrong blocks -- only bos == 0 matched. Thread an optional per-query bos tensor (block_bos, [B, S] int32) into the SM100 forward and bucket each column by its document-relative block rel = floor((abs_col - bos) / block_size). Because an unaligned bos shifts the block_size grid, one n-tile can straddle one extra relative block, so the per-thread accumulator widens by one slot (n_block_slots = blocks_per_ntile + 1) and the gmem write becomes a guarded same-thread fmax RMW across the n_block loop. When block_bos is absent the kernel falls back to bos == 0, i.e. the original absolute bucketing (byte-identical fast path). This makes packed selection bit-identical to running each document alone (pack-equivalence).
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
Hardens the HySparse block-score (
block_logit) fusion in the SM100/Blackwell FA4 forward kernel. Commits on this branch:4a7d31aFuse HySparse block-score (block_logit) into the SM100 forward epilogue.6f7e843Harden the fused block-score: guards, docs, and an accuracy/contract test suite.ef194c8Unifyblock_logitto the scaled logit + rejectpack_gqa.What changed
block_logitnow stores the SCALED attention logitsoftmax_scale * q·kᵀ (+ score_mod bias)— the exact value fed into softmax — instead of the rawq·kᵀ. This puts every head on one head-independent scale, so downstreamblock_logit - LSEgiveslog(max attention weight in the block), comparable across heads for cross-head Top-K block selection (HySparse eq(3)).softmax_scaleinto the logit (apply_score_mod_inner); the no-score_mod path carries rawq·kᵀ, so we multiply bysoftmax_scale (= scale_log2 · ln2)there only.-infmasked columns stay-inf.not pack_gqawhenblock_logitis requested.block_logitis indexed by the query head (head_idx) and query row (m_block*m_block_size+tidx); underpack_gqa=Truethe query heads are packed into the M/row dim andhead_idxbecomes the KV head, so writes would land in wrong locations. Sincepack_gqadefaults to(qhead_per_kvhead > 1), GQA callers must passpack_gqa=Falseexplicitly.block_logit + LSE; no in-kernel normalization.Downstream: computing the unified-scale Top-K block index
The kernel returns two tensors that together let you recover a head-independent per-block score:
block_logit:[B, H, S, num_blocks]fp32 — per-(query, key-block) max of the scaled logitm̃_block = max_{j∈block} ( softmax_scale · qᵀk_j (+ bias) ).lse:[B, H, S]— the softmax normalizer over the full row (natural log):LSE = m_i + ln(ℓ_i), wherem_i = max_j s_ijandℓ_i = Σ_j exp(s_ij − m_i).Why raw
block_logitis NOT cross-head comparable. The raw per-block max logit has a per-row/per-head offset (each head has its ownm_i,ℓ_i), so a larger logit in head A does not mean a larger attention weight than head B. You cannotmaxraw logits across heads.The unifying step — subtract LSE (log-domain normalization). Because
LSEis the exact softmax denominator of the same scaled logits,i.e.
log_weightis the log of the largest softmax attention weight inside the block. Softmax weights are probabilities in(0, 1], solog_weight ∈ (−∞, 0]lives on one common scale for every head → it can be maxed/compared across heads.exp(log_weight)recovers the actual peak weight. Masked / unvisited blocks are−infand are never selected.Top-K index (cross-head).
logis monotonic, so Top-K onlog_weightequals Top-K on the true weight — no need toexpfirst.Equivalent in the weight domain (same result):
weight = paddle.exp(log_weight), then max-over-heads andtopk.Notes:
lseis natural-log and taken over the SCALED logits, matchingblock_logit's units exactly.block_logitwith-inffor blocks the causal loop never visits (documented contract; seetest_block_logit_unvisited_future_blocks_untouched_causal).Tests
tests/test_block_score_fusion.py:softmax_scale; tolerances scale accordingly.test_block_logit_rejects_pack_gqa(explicitpack_gqa=Trueand defaultedNone) verifies the assert fires.pack_gqa=False).Runs only on SM 10.x (Blackwell).