Fix SDPA output dtype following config.floatX instead of the input dtype - #165
cetagostini wants to merge 2 commits into
Conversation
The softmax scale and the causal mask in _sdpa_graph used config.floatX to type their values. When config.floatX is float64 (the default), a float32 (q @ k^T) gets promoted to float64 through the scale multiplication, and the causal mask keeps it there, so the attention output is float64 regardless of the input dtype. Use q.dtype instead, which tracks the caller's precision. The value delta is <2e-7 (float32 scale vs float64 scale on a typical d_k=64), well within existing test tolerances (atol=1e-5).
| k = _repeat_kv(k, q.type.shape[-3], k.type.shape[-3]) | ||
| v = _repeat_kv(v, q.type.shape[-3], v.type.shape[-3]) | ||
|
|
||
| compute_dtype = q.dtype |
There was a problem hiding this comment.
| compute_dtype = q.dtype | |
| input_dtype = q.dtype |
A bit more clear
| MultiheadAttention("mha", **kwargs) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("is_causal", [False, True], ids=["full", "causal"]) |
There was a problem hiding this comment.
This is way too many tests, probably you can just add a check to the existing ones
|
Both points addressed in
The check still catches the bug it is for: reverting Downstream (pymc-labs/pytensor.cpp) this unblocks removing our boundary-cast workaround; it currently pins this commit because no release post-dates v0.2.2. No rush implied — just flagging that a release containing it is what lets us unpin. |
Fix: SDPA output dtype follows the input dtype, not
config.floatXCloses #164.
Root cause
_sdpa_graphusedpytensor.config.floatXfor three tensors: the softmax scale (both the default1/sqrt(d_k)path and the explicitscale=path) and the causal mask. Whenconfig.floatXis"float64"(the default), afloat32(q @ k^T)is promoted tofloat64by the scale multiplication, and thefloat64causal mask keeps it there.Fix
Replace
config.floatXwithq.dtype(namedinput_dtype) at the three call sites.qis the right reference: q/k/v come from the same projections in every realistic call, andq @ k^Talready promotes mixed dtypes per NumPy rules.Minimal reproducer
Value delta
The only numerical change is that the softmax scale is now computed in
float32instead offloat64. On a typicald_k = 64, the float32 scale1/sqrt(64)differs from the float64 value by< 2e-7. The existing SDPA reference and additive-mask tests now also assert that the output dtype matches the input dtype.Tests
Dtype assertions are folded into the existing
test_sdpa_matches_referenceandtest_sdpa_additive_masktests, rather than adding standalone tests. Their six existing cases use float32 inputs withconfig.floatX="float64", covering default/custom scales, causal/non-causal attention, and additive masks. The existing numerical comparisons are retained.pytest tests/test_attention.py -q: 25 passed.config.floatXbehavior fails all six cases at the dtype assertion; the fixed implementation passes all six. The control comparison usedPYTENSOR_FLAGS=cxx=to avoid a local C-linker configuration issue; the full 25-test run above used the normal configuration.Pre-existing quirks NOT changed by this PR
1.0in1.0 / pt.sqrt(...)is typedfloat32by PyTensor, so the scale isfloat32even whenqisfloat16. This is pre-existing and out of scope.float64mask, NumPy promotion widens the scores. Also pre-existing and out of scope.Same-class defects (follow-ups, not fixed here)
layers/dropout.py:87—mask.astype(config.floatX)layers/conv.py:1314-1317—pt.cast(extent, config.floatX)in the interpolation path📚 Documentation preview 📚: https://pytensor-ml--165.org.readthedocs.build/en/165/