Repository navigation
feat(ops)!: key-padding lengths and window metadata for scaledDotProductAttention (#1343, part 1) - #1348
Merged
michalharakal merged 2 commits intoOct 15, 2026
Conversation
…uctAttention (SKaiNET-developers#1343, part 1) Padding (`keyLengths`, a [batch] integer tensor), the sliding-window band (`window`/`rightContext`) and `causal` become rules over positions defined once in `AttentionMask` and shared by the CPU kernel, the recorded graph and the StableHLO lowering. The dense additive mask stays the generic fallback. - `TensorOps.scaledDotProductAttention(…, keyLengths = null, window = 0, rightContext = 0)`, `@Lowering(composite = "skainet.sdpa")`; VoidTensorOps, RecordingExecution, ComputeGraphExecutor, the KSP tracer (`OpAttributeFactory.sdpaMaskKind`) and the graph DSL follow. - Recorded metadata: `maskKind` (none | causal | slidingWindow | keyPadding | combined), `window`, `rightContext`; `keyLengths` is the last operand after the optional dense mask (`ScaledDotProductAttentionOperation.operandLayout`). - CPU: scores only inside each row's attended key range, MASKED (-1e30) elsewhere — bit-identical to the dense mask on the attended keys, finite on a fully padded row. Fixed the dense-mask indexing, which read batch row 0 for every batch; masks now broadcast trailing-aligned. - StableHLO: converter body moved into the emitter-based `SdpaLowering.emit`, which reads shapes from the operand MLIR types; rules lower to one i1 keep mask (iota/compare/and) and select 0 / -1e30. The causal diagonal carries the Sk − Sq offset the CPU uses; a dense mask and `causal` apply together; rules on a dynamic key length fail the conversion instead of emitting invalid iota IR. - Graph DSL: optional tensor operands (mask, bias, keyLengths) are `GraphValue<*>? = null` instead of mandatory positionals. - Tests: AttentionMaskTest, SdpaKeyLengthsTest, SdpaKeyLengthsRecordingTest, SdpaMaskRulesHloTest; api dumps, generated docs, changelog.
Kotlin/JS keeps a Float literal at double precision while a FloatArray element is float32, so the -1e30 equality in AttentionMaskTest failed on the JS leg.
3 of 4 tasks
This was referenced Oct 15, 2026
Merged
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
Part 1 of #1343: key padding, the sliding-window band and
causalbecome rules over positions thatscaledDotProductAttentioncarries as metadata, with one definition shared by the CPU kernel, the recorded graph and the StableHLO lowering.TensorOps.scaledDotProductAttention(…, keyLengths: Tensor<DType, *>? = null, window: Int = 0, rightContext: Int = 0)(@Lowering(composite = "skainet.sdpa")).keyLengthsis a[batch]integer tensor (rowbattends to keys< keyLengths[b]); the window band isMultiHeadAttention's formula (absQ = Sk − Sq + qi, keys in[absQ − window + 1, absQ + rightContext]). Rules intersect; the dense additivemaskstays the generic fallback and is added on top.AttentionMask(sk.ainet.lang.tensor.ops):kindOf(none | causal | slidingWindow | keyPadding | combined),keyRange/attends/endKey,additive(the[b, 1, Sq, Sk]tensor callers built by hand, now the reference the structured paths are tested against),MASKED = -1e30(finite, never-inf).MASKED— bit-identical to the dense-1e30path on the attended keys, uniform (finite) on a fully padded row,causalfolded into the range. Without them the kernel is untouched byte for byte.RecordingExecutionand the KSP tracer (OpAttributeFactory.sdpaMaskKind) recordmaskKind,window,rightContextbesidescale/causal, withkeyLengthsas the last operand after the optional dense mask;ScaledDotProductAttentionOperation.operandLayoutresolves the layout forComputeGraphExecutorand the converter (legacy nodes withoutmaskKindkeep their meaning).SdpaLowering.emit(e: HloEmitter, operands, operandTypes, resultType, params), which reads every shape from the operand MLIR types — the contract astablehlo.compositedecomposition function has (part 2 wiresskainet.sdpaonto the n-ary seam). Rules lower to onei1keep mask (iota/compare/and) andselect 0 / -1e30added to the scores;keyLengthsis a plain[batch] i32input, no[b, 1, Sq, Sk]constant enters the module.skainet-lang-dag): optional tensor operands (mask,bias,keyLengths) are nowGraphValue<*>? = nullinstead of mandatory positionals.Behaviour changes on the export side (flagged in the changelog)
Sk − Sqoffset the CPU kernel uses, so a KV-cache decode step attends to its whole cache.Sq == Skemission is unchanged (the pre-changeSdpaNumericDumpTestdump is reproduced line for line; only the SSA numbering order differs because the builder names values lazily).causal = trueapply together, as on the CPU, instead of the mask silently replacing the rule.?) return a conversion failure instead of invalidiotaIR; a dense mask still exports on the dynamic path.Out of scope, stated
scaledDotProductAttentionBackwardstays the existing stub (null gradients); when a backward is written it takes its mask fromAttentionMasklike the two forward paths.stablehlo-legalize-composite-to-callround trip: nostablehlo-opton the CI/dev machines; part 2 keeps the "composite body ≡ inline modulo SSA names" test instead.Test plan
AttentionMaskTest(lang-core): kinds, causal offset, window band ==MultiHeadAttentionformula, padding ∩ rules, contractSdpaKeyLengthsTest(CPU): lengths == dense mask bit for bit (causal on/off, GQA, Sq ≠ Sk), window/rightContext == dense, combined, padded batch == single passes ≤ 1e-6, fully padded row finite, schedule-independent, argument errorsSdpaKeyLengthsRecordingTest(compile-dag): trace/node metadata, operand layout for every combination, DIRECT vs OPTIMIZED ≤ 1e-6SdpaMaskRulesHloTest(compile-hlo): lengths → iota/compare/select from thei32input, band edges + causal offset,Sq == Skcausal lines unchanged, dense mask + rules, dynamic key length refused / dense mask acceptedSdpaGqaParityTest,SdpaScheduleParityTest,SDPAShapeValidationTest,SdpaGqaHloExportTest,DynamicShapeHloExportTest,ScheduleModuleAttributeTest,NarrowPermuteConverterTest) unchangedapiDump,generateDocs(matrix row),scripts/pr-gate.sh --quick, full gate