fused_ops: typed flash backward (FusedSDPA + VJP), not a generic CustomCall#13
Conversation
CustomCall is a backend-specific escape hatch to a target the backend recognizes by name (e.g. cuDNN flash attention, "__cudnn$fmhaSoftmax"), bypassing the normal op set. It is multi-output. Backends without custom-call support return ErrNotImplemented so callers can fall back to a decomposed implementation.
|
So, while I expected Also, define the In case we actually wanted the XLA escape hatch, I would suggest creating it on a separate |
…e hatch Drop CustomCall/CustomCallSpec from the Function interface (the cuDNN target mapping belongs in the xla backend, not the cross-backend surface). Instead: - FusedScaledDotProductAttention now returns (output, softmaxStats, err); softmaxStats is optional (nil) and carries the log-sum-exp the fused backward needs. - new FusedScaledDotProductAttentionVJP(query,key,value,mask,...,output,softmaxStats,dOutput) -> (dQuery,dKey,dValue); ErrNotImplemented when the backend has no fused backward, so the caller differentiates the decomposed attention. Regenerated opsregistration; hand-stubbed the multi-return notimplemented methods (the generator only templates (Value,error)). Go backend returns nil stats (decomposed gradient).
|
The PR looks all good to me. I'll wait for your thoughts on extending the FusedSDPA to accept optional |
…p loop limits - equalOptions: drop Value-typed QuerySeqLen/KeyValueSeqLen from == comparison (non-comparable concrete types panic; input operand identity handles dedup) - execFusedScaledDotProductAttention: use comma-ok type assertion for seqlen tensors; return clear error on wrong dtype or mismatched length vs batch - sdpaMultiHeadGeneric: clamp per-batch qLimit into [0, seqLen] and kvLimit into [0, kvLen] before using as loop bounds - fused_ops.go: clarify ScaledDotProductAttentionConfig doc: correctness fields require ErrNotImplemented when unsupported; optimization hints may be ignored Tests: TestSDPADirect_OptionEqualityNoValuePanic, TestSDPADirect_SeqLenWrongDtype, TestSDPADirect_SeqLenClamped (7/7 pass)
Stage 1: seqlen config fields + CPU reference + fp8 gate
|
Stage 1 of the fused-attention work just landed here. Terminology: Stage 1 is the config fields and CPU reference for sequence-length padding masks, this PR. Stage 2 is the additive-bias config field and reference, a separate cumulative PR: #15. What the review asked for:
|
janpfeifer
left a comment
There was a problem hiding this comment.
Thanks @guygrigsby !!
There is one larger request: softmaxStats Value -> statesForVJP []Value. I think this will be more stable if other fusion SDPA are implemented (at least for the Go backend there will be a few with SIMD support).
And then a few smaller ones, that if you are changing the API, maybe it's a good time to do it.
Let me know what you think, and if it's ok.
| // (log-sum-exp), shape [batch, numHeads, seqLen] f32, which the VJP needs. Backends | ||
| // without a fused backward return nil here and ErrNotImplemented from the VJP, so the | ||
| // caller differentiates the decomposed attention instead. | ||
| FusedScaledDotProductAttention( |
There was a problem hiding this comment.
Since we are changing this a couple of suggestions (but I can do them later):
- I think the
numHeadsandnumKVHeadsare redundant, since they can be inferred from the shapes, given theaxesLayout. Maybe we just remove them ? (the Go backend can trivially be changed to read these from the shape, and the XLA doesn't need them I think) - Move
mask,causal,scaleand eventuallybiasto theScaled.DotProductAttentionConfigsince they are also optional. And make it such thatscale == 0(the default) means no scale.
There was a problem hiding this comment.
Deferring these to a follow-up PR to keep the statesForVJP refactor isolated: drop numHeads/numKVHeads (infer from shape + axesLayout), and move mask/causal/scale/bias into ScaledDotProductAttentionConfig with scale == 0 meaning the default. No behavior change intended, just the ergonomics pass you suggested.
|
|
||
| // FusedScaledDotProductAttentionVJP computes the gradients (dQuery, dKey, dValue) of | ||
| // FusedScaledDotProductAttention given the forward output, the softmaxStats it returned, and | ||
| // the output gradient dOutput. The query/key/value/mask and numHeads..options parameters are |
There was a problem hiding this comment.
Nit: "the adjoint output gradient dOutput" (that's how I've been calling the V in VJP, following the convention I found in the related literature).
There was a problem hiding this comment.
Renamed: the VJP doc now calls it "the adjoint output gradient dOutput". guygrigsby@e815ee8
| // Output: same shape as query. | ||
| // Outputs: | ||
| // - output: same shape as query. | ||
| // - softmaxStats: optional (may be nil). When the backend supports a fused backward |
There was a problem hiding this comment.
Apologies, but I just realized something: the softmaxStats is very specific to the cudnn implementation. Actually, I'm surprised it also doesn't want the max-exp of the attention logits, since having it could speed-up the online softmax, which I assume it re-calculates in the VJP (?).
But, I was going to suggest to change softmaxStats Value to statesForVJP []Value. And the FusedScaledDotProductAttentionVJP would take that as input.
I think the cudnn implementation works as a gradient checkpoint, it regenerates some forward values to save temporary memory. But other implementations (for CPU, where memory is cheaper) may find a different balance and decide to keep more state in temporary memory to avoid having to regenerate them on the VJP. Hence the change from a single Value to a []Value.
Actually, I asked the AI, it mentions that some version of the cudnn FMHA also requires a byte tensor called workspace as additional output that needs to be passed back to the backward (VJP) function.
Any thoughts ?
There was a problem hiding this comment.
Double checked with the AI and looks like this is the right call. I'll add statesForVJP []Value in place of softmaxStats Value.
There was a problem hiding this comment.
Done: softmaxStats Value -> statesForVJP []Value on the forward return and the VJP input, threaded through the interface, the notimplemented default, and the go backend reference. An empty slice keeps the old nil meaning (no fused backward -> decomposed VJP). guygrigsby@502e5de
FusedScaledDotProductAttention/VJP hardcoded a single cuDNN-shaped softmax-stats tensor. Widen it to a slice so other backends (and cuDNN FMHA variants needing a workspace tensor) can carry whatever backward state they need. nil/empty slice keeps the old "no fused backward" meaning. Regenerated gen_opsregistration.go via go generate ./internal/gobackend/...; no behavior change.
janpfeifer
left a comment
There was a problem hiding this comment.
Thank you Guy! All looks good. Going to review the other 2 PRs and I'll merge when those are also ready!
|
Following up on the fixes in gomlx/gomlx#427 I did a few changes here:
This allows the 3 PRs to be committed. |
Stage 2 of the fused-attention work: additive attention bias. This is cumulative on top of Stage 1 (#13), which is the prerequisite, so the Stage 1 commits show here too until #13 merges to main, after which this shrinks to just the bias delta. Adds ScaledDotProductAttentionConfig.Bias plus the go-backend CPU reference: bias added to the scores before softmax, combining with causal and seqlen masking. The bias is deduped via operand identity (equality compares the presence flag, not the Value), validated for shape (clear error, not a panic). Capability stays off; exercised by direct unit tests. Validated on an RTX 3070 Ti through the go-xla and gomlx bias PRs: cuDNN ScaleBias applies the bias forward and backward.
Per the design discussion in gomlx/gomlx#422: keep the cuDNN target mapping off the cross-backend surface and express flash attention as typed fused ops instead of a generic escape hatch.
FusedScaledDotProductAttentionnow returns(output, softmaxStats, err).softmaxStatsis optional (may be nil); a backend with a fused backward returns the log-sum-exp the backward needs, others return nil.FusedScaledDotProductAttentionVJP(query, key, value, mask, ..., output, softmaxStats, dOutput) (dQuery, dKey, dValue, err). ReturnsErrNotImplementedwhen the backend has no fused backward, so the graph layer differentiates the decomposed attention instead.CustomCall/CustomCallSpecfrom theFunctioninterface. The cuDNN custom-call mapping now lives in the xla backend (compute/xla: cuDNN flash as typed FusedSDPA + VJP go-xla#37), not on the shared interface.Regenerated opsregistration; the two multi-return methods are hand-stubbed in
notimplemented(the generator only templates(Value, error)). The Go backend returns nil stats, so its attention gradient stays decomposed.Base of the flash stack: gomlx/go-xla#37 and gomlx/gomlx#427 build on it.
Context: gomlx/gomlx#422.