Skip to content

perf(torch): SDPA is_causal dispatch + bounded causal-mask cache in MultiHeadAttention#23186

Draft
pctablet505 wants to merge 5 commits into
keras-team:masterfrom
pctablet505:perf-P7-mha-sdpa
Draft

perf(torch): SDPA is_causal dispatch + bounded causal-mask cache in MultiHeadAttention#23186
pctablet505 wants to merge 5 commits into
keras-team:masterfrom
pctablet505:perf-P7-mha-sdpa

Conversation

@pctablet505

@pctablet505 pctablet505 commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Closes #23277

Torch backend: when MultiHeadAttention does eager causal self-attention with no explicit mask, dispatch straight to torch.nn.functional.scaled_dot_product_attention(..., is_causal=True) — skipping the [T, S] mask materialization and the ops.dot_product_attention dispatch layer. Off that fast path, the fallback causal mask is cached (bounded at 8 entries) instead of rebuilt on every call.

Why it's safe

  • The SDPA path is tightly gated: torch backend, concrete tensors only, use_causal_mask=True, no explicit attention_mask, zero dropout, no return_attention_scores, flash attention off. Anything else falls through to the existing path untouched, so other backends and configurations cannot regress.
  • Numerics: the only transform is transposing (B, T, N, H) to SDPA's (B, N, T, H) layout and back. The head dimension stays contiguous, so no .contiguous() copy is needed, and results match the explicit lower-triangular-mask path (verified, including a T ≠ S case).
  • The cache key is (q_len, kv_len, device, "bool"); dtype is deliberately omitted since the bool mask depends only on lengths and device. Only exact Python int lengths are cached — under torch.compile lengths become symbolic SymInts (unhashable), which simply fall through to recomputation, so compiled models keep working. JAX and TF are excluded entirely: caching there would leak a tracer across jit/tf.function traces, and those backends constant-fold the mask anyway.
  • Quantized and LoRA layers route through their own call() overrides and never reach this code.

Benchmarks (GPU, 3-way: master / this PR alone / this PR + parents #23182#23185)

PR #23186: master vs PR alone vs PR+parents, Keras torch GPU eager

workload master this PR alone + parents (#23182#23185)
CNN forward 1.21 ms 1.24 ms (~noise) 1.09 ms (~noise)
LLM forward 3.48 ms 3.37 ms (~noise) 2.82 ms (1.21×)
LLM generate-32 110.9 ms 108.0 ms (~noise) 90.7 ms (1.21×)

Interleaved A/B process pairs against an A/A null calibration; "~noise" means below the measured noise floor, not claimed as a win. Forward outputs are bit-identical to master with this PR in isolation, and generate-32 token ids match in all states (the small stacked-state float32 delta, 1.67e-06, comes from a parent PR's einsum reordering already accepted there). torch 2.13.0+cu130, RTX 4050 Laptop GPU, master a2f8eac.

Tests

New unit tests cover fast-path activation, numeric equivalence with the explicit-mask path, every fallback gate (non-torch backend, flash attention, attention scores, explicit mask), the cache size bound, and SymInt exclusion.

Contributor Agreement

  • I am a human, and not a bot.
  • I will be responsible for responding to review comments in a timely manner.
  • I will work with the maintainers to push this PR forward until submission.

@google-cla

google-cla Bot commented Jun 27, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request optimizes the MultiHeadAttention layer by introducing a bounded cache for computed causal masks and adding an ultra-fast path that directly calls PyTorch's native scaled dot-product attention when using causal masks. The review feedback suggests restricting the causal mask cache strictly to the PyTorch backend to prevent tracer leaks and graph mismatch errors in JAX and TensorFlow. Additionally, it recommends removing unnecessary .contiguous() calls on transposed tensors in the PyTorch fast-path to avoid redundant memory copies.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread keras/src/layers/attention/multi_head_attention.py Outdated
Comment thread keras/src/layers/attention/multi_head_attention.py Outdated
@codecov-commenter

codecov-commenter commented Jun 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.09%. Comparing base (da5a4cb) to head (d97c4d5).
⚠️ Report is 17 commits behind head on master.

Files with missing lines Patch % Lines
keras/src/layers/attention/multi_head_attention.py 90.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #23186      +/-   ##
==========================================
- Coverage   84.70%   84.09%   -0.61%     
==========================================
  Files         465      465              
  Lines       69335    69735     +400     
  Branches    11407    11508     +101     
==========================================
- Hits        58729    58645      -84     
- Misses       7672     8145     +473     
- Partials     2934     2945      +11     
Flag Coverage Δ
keras 83.92% <85.00%> (-0.60%) ⬇️
keras-cpu 83.92% <85.00%> (+0.03%) ⬆️
keras-gpu ?
keras-jax 57.95% <15.00%> (-0.15%) ⬇️
keras-numpy 53.77% <15.00%> (+0.10%) ⬆️
keras-openvino 59.59% <15.00%> (+0.08%) ⬆️
keras-tensorflow 59.60% <15.00%> (-0.20%) ⬇️
keras-torch 58.78% <80.00%> (-0.25%) ⬇️
keras-tpu ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@pctablet505
pctablet505 force-pushed the perf-P7-mha-sdpa branch 3 times, most recently from 2b936aa to 2dcbf92 Compare June 27, 2026 16:03
@pctablet505
pctablet505 force-pushed the perf-P7-mha-sdpa branch 3 times, most recently from 27923d3 to ff2f9f0 Compare July 1, 2026 16:37
_compute_causal_mask evicted the oldest entry via
next(iter(cache)) followed by cache.pop(key) with no default. That
check-then-act sequence could KeyError if a shared layer instance is
accessed concurrently and another thread empties the cache between
the two calls. Guard against an empty iterator and pass a default to
pop so eviction is safe even in that unlikely case.
@pctablet505

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request optimizes MultiHeadAttention for the PyTorch backend by introducing a fast path that routes causal attention directly to PyTorch's native scaled_dot_product_attention (SDPA) with is_causal=True, bypassing mask materialization. It also adds a bounded FIFO cache (up to 8 entries) for causal masks on PyTorch with static shapes to avoid redundant mask creation. Unit tests are added to verify correctness, fallback paths, and cache eviction. The feedback notes that the transposed tensors are not made contiguous before being passed to SDPA, which contradicts the PR description and could lead to performance issues or runtime errors on certain CUDA kernels.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread keras/src/layers/attention/multi_head_attention.py
Rewrite the two comments around the torch SDPA causal dispatch as flowing
sentences instead of a colon-led topic heading, and drop a stray
series-tag comment above the new tests.
@pctablet505

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request optimizes the MultiHeadAttention layer for the PyTorch backend by dispatching directly to the native causal scaled_dot_product_attention kernel and introducing a bounded cache for causal masks to avoid redundant computations. The review feedback highlights two important issues in the caching implementation: first, using isinstance checks for sequence lengths can mistakenly allow symbolic integers (torch.SymInt) to be cached under torch.compile, which should be replaced with strict type checks; second, the FIFO eviction mechanism is not thread-safe and could raise a RuntimeError under concurrent execution, which can be resolved by atomically clearing the cache.

Comment thread keras/src/layers/attention/multi_head_attention.py Outdated
Comment thread keras/src/layers/attention/multi_head_attention.py Outdated
Require sequence lengths to be exact Python ints (type(x) is int) before
caching, so a symbolic torch.SymInt under torch.compile is excluded and
falls through to recomputation instead of being used as an unhashable
cache key. Back the cache with an OrderedDict and evict the oldest entry
via popitem(last=False), a single atomic pop that avoids iterating a dict
another thread could resize concurrently.

Add a regression test that a symbolic SymInt length stays out of the
cache while concrete int lengths are cached under exact-int keys.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MultiHeadAttention doesn't use scaled_dot_product_attention on torch (also breaks torch.compile)

3 participants