Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Skip attention_mask.all() GPU-CPU sync during generation#43088

Open
AmitMY wants to merge 2 commits into
huggingface:mainfrom
AmitMY:skip-mask-sync-generation
Open

Skip attention_mask.all() GPU-CPU sync during generation#43088
AmitMY wants to merge 2 commits into
huggingface:mainfrom
AmitMY:skip-mask-sync-generation

Conversation

@AmitMY

@AmitMY AmitMY commented Jan 3, 2026

Copy link
Copy Markdown
Contributor

I guess there could be cases where people define a custom mask, but I don't know of any.


Summary

  • Adds a context variable _attention_mask_all_true to track if attention_mask is known to be all-True during generation
  • Sets the context variable at the start of generate() based on the initial mask state
  • Modifies _ignore_causal_mask_sdpa() to skip the expensive .all() check when the context variable indicates no padding

Problem

During autoregressive generation, _ignore_causal_mask_sdpa() in masking_utils.py calls padding_mask.all() on every generated token to check if the mask contains any padding. This causes a GPU-CPU synchronization that adds ~2-3ms per token, significantly impacting generation latency.

Solution

Since during generation we only append ones (1s) to the attention mask, if the mask starts all-True (no padding), it will remain all-True throughout generation. We check this once at the start and use a context variable to skip subsequent .all() checks.

Results

  • Reduces .all() calls from N+1 to 1 for N generated tokens
  • Example: 201 calls → 1 call for 200 tokens generated
  • Eliminates per-token GPU-CPU sync overhead (~2-3ms per token)

Test plan

  • Verified outputs are identical with and without optimization
  • Verified .all() call count reduced from 201 to 1 for 200 tokens
  • Run existing tests

🤖 Generated with Claude Code

During autoregressive generation, `_ignore_causal_mask_sdpa()` calls
`padding_mask.all()` on every token to check if the mask contains any
padding. This causes a GPU-CPU synchronization that adds ~2-3ms per
token, significantly impacting generation latency.

This PR adds a context variable `_attention_mask_all_true` that is set
at the start of generation based on the initial attention_mask. If the
mask is all-True (no padding), the context variable allows subsequent
calls to skip the expensive `.all()` check, since during generation we
only append ones to the mask.

This reduces `.all()` calls from N+1 to 1 for N generated tokens (e.g.,
from 201 to 1 for 200 tokens).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Comment on lines +2648 to +2652
# Check if attention_mask is all-True to avoid per-token GPU-CPU sync in masking utils.
# During generation, if the mask starts all-True and we only append ones, it stays all-True.
attention_mask = model_kwargs.get("attention_mask")
mask_all_true = attention_mask is None or bool(attention_mask.all())
mask_token = _attention_mask_all_true.set(mask_all_true)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

hm, we can also set the mask to None and avoid the sync in masking utils

@AmitMY AmitMY Jan 5, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

meaning, we check once at the start?

if bool(model_kwargs.get("attention_mask").all()):
  model_kwargs.set("attention_mask", None)

(or otherwise, of course, feel free to implement it as you want, sounds like a very minimal change)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yeah, once in the beginning. Also want to cc @vasqu for attn masks, maybe we don't even need to tweak generation loop

@vasqu vasqu Jan 13, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Only sdpa and flash attention can skip in those cases. Eager and flex attention (I think) will still need their mask to work correctly - at least when we have a causal model

Note that the way we skip mask creation atm is data-flow dependent, meaning this could indeed help in compiled cases:

SDPA

def _ignore_causal_mask_sdpa(

Flash
if attention_mask.all():
attention_mask = None

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks @vasqu ! I clearly do not understand the complexities for each attention implementation, but I now added a breakdown per implementation: #43089 (comment)
_update_causal_mask affects eager and sdpa (63 syncs), then _ignore_causal_mask_sdpa affects sdap with 64 syncs, and finally _update_causal_mask affects flash attention with 64 syncs.

My understanding is that if the attention mask is all true, we could ignore it for all these cases

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If everything is true, we can ignore for SDPA and FA - they have a flag to enable proper causality without a mask.
However, flex attention and eager attention cannot do that. They require a mask at all times (at least when we have causal models and these are the majority of models).

So we need to differentiate per attention whether we can avoid these sync

  • SDPA + FA: Yes
  • Flex + Eager: No

Then we also have a few special models like PaliGemma which have special mixed masks which we would not be able to skip in any case. I assume that should be a small number of models tho

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants