Skip to content

Optimize flash attention max seqlen computation in vision attention#47170

Merged
vasqu merged 7 commits into
huggingface:mainfrom
ShareLer:optim/flash-max-seqlen-precompute
Jul 21, 2026
Merged

Optimize flash attention max seqlen computation in vision attention#47170
vasqu merged 7 commits into
huggingface:mainfrom
ShareLer:optim/flash-max-seqlen-precompute

Conversation

@ShareLer

@ShareLer ShareLer commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

CI

What does this PR do?

This PR optimizes Flash Attention variable-length paths by precomputing the maximum sequence length once at the model/encoder level after cu_seqlens is built, then passing it down through blocks into each attention layer.

Why

Previously, several vision/audio attention implementations recomputed:

(cu_seqlens[1:] - cu_seqlens[:-1]).max()

inside every attention layer on every forward pass.
For Flash Attention varlen kernels, max_seqlen_q and max_seqlen_k are ultimately consumed as Python integer arguments. In eager mode, passing a 0-dimensional CUDA tensor can therefore move the scalar conversion to the Flash Attention / custom op boundary, which may introduce repeated device-to-host synchronization. For an N-layer encoder, this repeats the same max_seqlen computation and scalar conversion path in every layer.

What changed

Compute max_seqlen once in the model/encoder forward path when Flash Attention is requested.
Compute max_window_seqlen once for sliding-window variants.
Pass the precomputed values down through blocks into attention modules.
Keep the attention-level fallback without .item() for standalone attention calls, preserving previous behavior when max_seqlen is not provided.
Guard the computation behind is_flash_attention_requested, so non-Flash-Attention paths do not pay this cost.

Related

This is related to #44962 and the discussion in #44973. Instead of adding scalar conversion inside each attention layer, this PR lifts the computation to the model/encoder level where cu_seqlens is already available, reducing repeated per-layer work and avoiding repeated per-layer scalar conversions in the normal model forward path.

Experiment

Before

image image block kernel launch image

After

image image image

Code Agent Policy

The Transformers repo is currently being overwhelmed by a large number of PRs and issue comments written by
code agents. We are currently bottlenecked by our ability to review and respond to them. As a result,
we ask that new users do not submit pure code agent PRs at this time.
You may use code agents in drafting or to help you diagnose issues. We'd also ask autonomous "OpenClaw"-like agents
not to open any PRs or issues for the moment.

PRs that appear to be fully agent-written will probably be closed without review, and we may block users who do this
repeatedly or maliciously.

This is a rapidly-evolving situation that's causing significant shockwaves in the open-source community. As a result,
this policy is likely to be updated regularly in the near future. For more information, please read CONTRIBUTING.md.

  • I confirm that this is not a pure code agent PR.

Before submitting

  • This PR fixes a typo or improves the docs (you can dismiss the other checks if that's the case).
  • Did you read the contributor guideline and the
    Pull Request checks?
  • Was this discussed/approved via a Github issue or the forum? Please add a link
    to it if that's the case.
  • Did you make sure to update the documentation with your changes according to the guidelines?
  • Did you write any new necessary tests?

Who can review?

cc @vasqu

@vasqu vasqu left a comment

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.

Imo totally valid and makes sense to avoid a per layer call

Only thing I would change design wise would be to move things to vision utils as well and make similar skips as when the kwarg already exists. cc @IlyasMoutawwakil @zucchini-nlp

@ShareLer
ShareLer force-pushed the optim/flash-max-seqlen-precompute branch from be190be to 16876e4 Compare July 14, 2026 11:31
@ShareLer

Copy link
Copy Markdown
Contributor Author

Imo totally valid and makes sense to avoid a per layer call

Only thing I would change design wise would be to move things to vision utils as well and make similar skips as when the kwarg already exists. cc @IlyasMoutawwakil @zucchini-nlp

Thanks for the suggestion!
I moved the vision logic into get_vision_max_seqlen in vision_utils.py and added the same kwargs-based skip for precomputed values. And audio follows the existing model-local helper pattern.

Comment thread src/transformers/vision_utils.py Outdated
Comment on lines +53 to +66
def get_vision_max_seqlen(cu_seqlens: torch.Tensor, kwargs: dict | None = None, kwarg_name: str = "max_seqlen") -> int:
"""Get the maximum packed sequence length, or pop it from `kwargs` if precomputed.

Args:
cu_seqlens: `(num_sequences + 1,)` cumulative sequence boundaries.
kwargs: optional caller kwargs containing a precomputed maximum sequence length.
kwarg_name: key used to pop the precomputed value from `kwargs`.

Returns:
Maximum packed sequence length as a Python integer.
"""
if kwargs is not None and (max_seqlen := kwargs.pop(kwarg_name, None)) is not None:
return max_seqlen
return int((cu_seqlens[1:] - cu_seqlens[:-1]).max().item())

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.

why not pass the model config to it, to decide whether it should:

  • return it from kwarsgs if it's there
  • return None if unnecessary (non flash attn)
  • compute it and sync
    to avoid the

max_seqlen = None
if falsh_attn(config)..

in multiple places

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.

another thing we can do is extend the old get_vision_cu_seqlens to return a tuple[tensor, int | none] since they are always used together or neither @vasqu

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.

Yea tbh, I don't see why we can't combine them. We should let the single version of cu seqlens live tho to be BC

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.

Agreed, this is cleaner. I updated get_vision_max_seqlen to take the model config and handle the decision internally, and added get_vision_attention_seqlens to return both values while keeping get_vision_cu_seqlens unchanged for BC.

Comment on lines +212 to +213
if max_seqlen is None:
max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()

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.

the goal of the changes is that by this point, if the model uses fa, max seqlens should be already computed

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.

i say let it fail loudly if not

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.

Yep agree, don't like the recompute in any case will make it hard to see on new models whether they used the utils properly

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.

Agreed. When Flash Attention is used, max_seqlen should already have been computed by this point. I removed the recomputation fallback, and it now raises a clear ValueError if the value is missing.

hidden_states: torch.Tensor,
cu_seqlens: torch.Tensor,
position_embeddings: tuple[torch.Tensor, torch.Tensor],
max_seqlen: int | None = None,

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.

can be left in **kwargs: Unpack[TransformersKwargs] since we do pass the kwargs to attn interface

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.

Done, max_seqlen is no longer declared by the pass-through Block/Encoder layers and just flows through **kwargs. It stays explicit only on Attention, where it’s actually consumed.

@IlyasMoutawwakil IlyasMoutawwakil left a comment

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.

left a couple comments that should simplify it a bit, mostly nudging towards the same constrants we agreed upon in #45396:

  • the vision util should abstract the repetitive if this is none
  • the "extra" arguments should stay hidden in kwargs

@ShareLer

Copy link
Copy Markdown
Contributor Author

left a couple comments that should simplify it a bit, mostly nudging towards the same constrants we agreed upon in #45396:

  • the vision util should abstract the repetitive if this is none
  • the "extra" arguments should stay hidden in kwargs

Thanks, addressed the review comments:

  • get_vision_max_seqlen now accepts the model config, consumes a precomputed value from kwargs first, returns None for non-Flash-Attention paths otherwise, and computes a Python int once for Flash Attention.
  • Added get_vision_attention_seqlens to return cu_seqlens and max_seqlen together.
  • Kept the existing tensor-only get_vision_cu_seqlens API unchanged for backward compatibility.
  • Removed max_seqlen from pass-through Block/Encoder signatures and kept it hidden in **kwargs; only the Attention modules that consume it declare it explicitly.
  • Removed the Attention-level recomputation fallback. Flash Attention now raises a clear ValueError if max_seqlen was not precomputed.

Comment on lines +612 to +613
if max_seqlen is None:
raise ValueError("`max_seqlen` must be provided when using Flash Attention.")

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.

doesn't the attn interface raise an error on its own when it's missing ?

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.

I checked the Flash Attention source. The native flash_attn_varlen_func requires cu_seqlens_q/k and max_seqlen_q/k together and would reject an incomplete call.
However, Transformers currently checks in _flash_attention_forward:

is_fa_with_varlen_kwargs = all(
    value is not None
    for value in (cu_seq_lens_q, cu_seq_lens_k, max_length_q, max_length_k)
)

Therefore, when cu_seq_lens_q/k are provided but max_length_q/k are None, Transformers does not call flash_attn_varlen_func and never reaches the native error. Without an attention mask or packed position IDs, it instead falls through to the fixed-length flash_attn_func, which can incorrectly allow attention across packed sequences.
I propose centralizing the validation and routing in _flash_attention_forward:

explicit_varlen_kwargs = (
    cu_seq_lens_q,
    cu_seq_lens_k,
    max_length_q,
    max_length_k,
)
has_any_varlen_kwargs = any(value is not None for value in explicit_varlen_kwargs)
has_all_varlen_kwargs = all(value is not None for value in explicit_varlen_kwargs)

if attention_mask is not None:
    # Derive the complete varlen arguments internally.
elif has_any_varlen_kwargs:
    if not has_all_varlen_kwargs:
        raise ValueError(...)
    # Use the complete precomputed varlen arguments.
elif is_fa_with_position_ids:
    # Derive the complete varlen arguments from position_ids.
else:
    # Fixed-length Flash Attention.

This makes the explicitly provided varlen arguments an all-or-none group, while leaving the attention_mask and packed position_ids paths unchanged. We can then remove the repeated checks from each model Attention implementation.
Would centralizing this validation in _flash_attention_forward and removing the per-model checks align with what you had in mind?

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.

Tbh I'm against too much validation here. It will make the code much more unreadable. This is kind of a power feature and we just have to guarantee that our vision utils do this properly.

For BC purposes we could still calculate the max seq len within the attention module with the vision utils

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.

After thinking it over, I agree that we shouldn’t raise here. For BC, if max_seqlen is missing, we should compute it with the helper. In the standard Transformers model path, it is already precomputed and passed down, so this fallback adds no extra max computation or synchronization. It only runs when users call a Block or Attention module directly, preserving the previous behavior.

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.

+1, seems a bit breaking and shouldn't require validation as we are sure that max_len is passed from the VisionModel

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.

I kept a fallback to preserve BC. The normal model path still precomputes max_seqlen, so the fallback won’t be hit there. It only handles direct Block/Attention calls where max_seqlen isn’t provided. I’ve updated all affected models accordingly.

Comment on lines 154 to 155
max_length_q=max_seqlen,
max_length_k=max_seqlen,

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.

yeh i think we don't need to raise the error in every vision attn, we pass the max cuseqlens here so it should be raised by the attn interface when given None (if it isn't the case already)

Comment on lines 181 to 182
max_seqlen: int | None = None,
attention_mask: torch.Tensor | None = None,

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.

for the sake of bc it should probably be added last in order

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.

max_seqlen was already a required argument in MiniCPMV4_6VisionAttention before this PR, in the same position before attention_mask. I only widened its type because non-FA paths can now explicitly pass None. To preserve the original signature more strictly, I’ll remove the default while keeping its existing position, avoiding any change to the positional argument mapping.

I also checked the other Attention signatures and found that some newly added max_seqlen parameters were inserted before existing parameters such as position_embeddings, which does introduce a positional BC issue. I’ll move those newly added optional parameters after the existing ones. Thanks for prompting this broader check.

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.

ah i commented on the wrong one i guess, i saw a max_seqlen added in between two args somewhere

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.

ah yes thanks ! just read the rest of your reply 😅

Comment on lines 591 to 592
max_seqlen: int | None = None,
position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None,

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.

@ShareLer here

@zucchini-nlp zucchini-nlp left a comment

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.

Super nice initiative, thanks! Cosmos3-Edge is close to merge, lets fix it as well as imo it'll be merged first

Seems like we just created a basic VisionRaggedInput data collator for qwen-style models. When we get the torch-ragged attn support, we might merge the input preparation as a single collating class?

Comment on lines 591 to 593
max_seqlen: int | None = None,
position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None,
**kwargs,

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.

ha, i kinda assumed it was already moved to kwargs in prev PR for export

Imo we can put it as Unpack[TransformersKwargs], what do yall think?

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.

Hmm, it is a bit weird but I'd rather keep the full signature here to indicate that this is now required here. Also cu seqlens was needed for the non FA paths so would be weird to kwarg only max seqlen

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.

I think keeping max_seqlen explicit in the Attention signature is clearer here. Flash Attention requires it together with cu_seqlens, so exposing both makes the varlen input contract clear to callers. Keeping it optional preserves BC for direct Block/Attention calls, while still indicating that callers should pass the precomputed value when available. Intermediate Blocks can continue to pass it through **kwargs.

Comment on lines +612 to +613
if max_seqlen is None:
raise ValueError("`max_seqlen` must be provided when using Flash Attention.")

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.

+1, seems a bit breaking and shouldn't require validation as we are sure that max_len is passed from the VisionModel

@vasqu

vasqu commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Seems like we just created a basic VisionRaggedInput data collator for qwen-style models. When we get the torch-ragged attn support, we might merge the input preparation as a single collating class?

One more for my data collator approach, I feel validated 😆 ❤️

@ShareLer
ShareLer force-pushed the optim/flash-max-seqlen-precompute branch from 032aef5 to 2c35c80 Compare July 17, 2026 03:21
@github-actions

Copy link
Copy Markdown
Contributor

Thank you for your contribution 🤗!

CI Security Gate — automatic approval blocked

This PR was not automatically approved for CI because the security gate failed.

Possible reasons:

  • The PR touches 50 or more files — only PRs with fewer than 50 changed files are automatically approved
  • A changed file is outside the allowed directories (src/, tests/, docs/, utils/), has a disallowed extension (only .py, .txt, .md permitted outside tests/ and docs/), or is not .md/.yml inside docs/
  • A new high-severity security issue was detected in the changed Python files (Bandit check)

See the workflow run for the exact violations.

A maintainer can review and manually approve CI if a finding is a false positive.

@ShareLer

ShareLer commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Super nice initiative, thanks! Cosmos3-Edge is close to merge, lets fix it as well as imo it'll be merged first

Seems like we just created a basic VisionRaggedInput data collator for qwen-style models. When we get the torch-ragged attn support, we might merge the input preparation as a single collating class?

Thanks for the review! I rebased onto the latest main and added Cosmos3-Edge to the same precomputation and fallback flow.

@vasqu vasqu left a comment

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.

Just some small comments left imo but this looks very solid now

Comment thread src/transformers/exporters/utils.py Outdated
Comment on lines +449 to +450
if max_seqlen is not None:
inputs["max_seqlen"] = max_seqlen

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.

Suggested change
if max_seqlen is not None:
inputs["max_seqlen"] = max_seqlen
inputs["max_seqlen"] = max_seqlen

not sure but we can just ignore the none guard imo

Comment thread src/transformers/exporters/utils.py Outdated
Comment on lines +462 to +466
max_window_seqlen = get_vision_max_seqlen(
inputs["cu_window_seqlens"], model.config, kwargs=inputs, kwarg_name="max_window_seqlen"
)
if max_window_seqlen is not None:
inputs["max_window_seqlen"] = max_window_seqlen

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.

Suggested change
max_window_seqlen = get_vision_max_seqlen(
inputs["cu_window_seqlens"], model.config, kwargs=inputs, kwarg_name="max_window_seqlen"
)
if max_window_seqlen is not None:
inputs["max_window_seqlen"] = max_window_seqlen
inputs["max_window_seqlen"] = get_vision_max_seqlen(
inputs["cu_window_seqlens"], model.config, kwargs=inputs, kwarg_name="max_window_seqlen"
)

same here no?

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.

Ok happens below as well, but not mentioning anymore everywhere

Comment on lines +401 to +402
if max_seqlen is None:
max_seqlen = get_vision_max_seqlen(cu_seqlens, self.config)

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.

Imo, we should just have

Suggested change
if max_seqlen is None:
max_seqlen = get_vision_max_seqlen(cu_seqlens, self.config)
max_seqlen = get_vision_max_seqlen(cu_seqlens, self.config)

Elsewhere as well then

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.

I.e. we should allow to return the max seqlens that are already prepared within the function (cu seqlen does the same)

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.

get_vision_cu_seqlens implements get-or-compute directly from kwargs. get_vision_max_seqlen already does the same at the model level, but by the time we enter Attention, max_seqlen has been bound to the explicit parameter and is no longer in kwargs.

Therefore, replacing the current guard with:

max_seqlen = get_vision_max_seqlen(cu_seqlens, self.config)

would ignore the precomputed value and recompute it in every layer. To preserve the explicit Attention signature, we would need to pass the already-bound value into the helper:

def get_vision_max_seqlen(
    cu_seqlens,
    config,
    max_seqlen=None,
    kwargs=None,
    kwarg_name="max_seqlen",
):
    if max_seqlen is None and kwargs is not None:
        max_seqlen = kwargs.pop(kwarg_name, None)

    if max_seqlen is not None:
        return max_seqlen

    if not is_flash_attention_requested(config):
        return None

    return int((cu_seqlens[1:] - cu_seqlens[:-1]).max().item())

This would centralize the None check in the helper rather than remove it. Since you also preferred keeping max_seqlen explicit in the Attention signature earlier, I think the current explicit fallback is clearer than adding another way to pass the same value into the helper. Would keeping it as is make more sense to you?

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.

Can we not just slightly adjust the calls like max_seqlen = get_vision_max_seqlen(cu_seqlens, self.config, kwargs={"max_seq_len": max_seq_len})?

My goal here is to avoid the none guard just so that we return the existing max seqlen if possible

Comment on lines +437 to +438
if max_seqlens is not None:
max_seqlens = max_seqlens // 4

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.

This is a bit weird imo we dont need to assume this to not exist here, we already guaranteed it so no need to allow this to be 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.

I think this guard is still needed for non-FA paths. get_vision_max_seqlen intentionally returns None for eager/SDPA, while get_downsampled_inputs is still called when use_vit_merger=True, including with the default SDPA implementation. Removing the guard would evaluate None // 4. Making the value always available would require an otherwise unnecessary max computation and synchronization for non-FA paths, so I’d prefer to keep the guard here.

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.

Gotcha, let's keep a small comment then tho?

Comment thread tests/utils/test_vision_utils.py Outdated

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.

Oh yea we never really had actual tests tbh, I'd rather remove this and just verify with our slow tests

@ShareLer

Copy link
Copy Markdown
Contributor Author

Just some small comments left imo but this looks very solid now

Thanks! I’ve addressed the remaining comments and pushed the updates. Appreciate the thorough review!

@vasqu vasqu left a comment

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.

Happy with this state, @IlyasMoutawwakil if you could take a last look 🤗

return F.pad(after_conv1.cumsum(0), (1, 0), value=0).to(torch.int32)


def get_audio_max_seqlen(cu_seqlens: torch.Tensor, config: PreTrainedConfig, kwargs: dict | None = None) -> int | None:

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.

not on you, I remember audio being a bit too different which is why we opted for per file implementations maybe something we should push for @IlyasMoutawwakil @eustlb @ebezzam

Comment thread src/transformers/exporters/utils.py Outdated
Comment on lines +485 to +492
@@ -480,12 +486,16 @@ def _prepare_navit_vision_inputs(model: torch.nn.Module, inputs: dict[str, Any])
grid_thw, spatial_merge_size=1, window_size=window_kernel_size[0], patch_size=1
)
inputs["merged_shape"] = get_vision_merged_shape(target_sizes, window_kernel_size)
cu_seqlens = torch.nn.functional.pad(
torch.cumsum(target_sizes[:, 0] * target_sizes[:, 1], dim=0, dtype=torch.int32), (1, 0)
)
inputs["max_seqlen"] = get_vision_max_seqlen(cu_seqlens, model.config, kwargs=inputs)

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.

can the max_seqlen be computed from inputs["cu_window_seqlens"] here ?

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.

Not quite: cu_window_seqlens describes the windowed attention used by the ViT merger, while this max_seqlen is for the encoder’s per-image packed attention and is based on the full h * w sequence length. These values differ when an image is larger than one window. The merger already derives its own maximum from window_h * window_w, so I think the current full-image cu_seqlens is still needed here.

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.

i see, thanks !

Comment thread src/transformers/vision_utils.py Outdated
return max_seqlen
if not is_flash_attention_requested(config):
return None
return int((cu_seqlens[1:] - cu_seqlens[:-1]).max().item())

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.

is the int and item both necessary ?

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.

removed int conversion

Comment on lines +570 to +576
def get_audio_max_seqlen(cu_seqlens: torch.Tensor, config: PreTrainedConfig, kwargs: dict | None = None) -> int | None:
"""Get the maximum packed audio sequence length, or pop it from `kwargs` if precomputed."""
if kwargs is not None and (max_seqlen := kwargs.pop("max_seqlen", None)) is not None:
return max_seqlen
if not is_flash_attention_requested(config):
return None
return int((cu_seqlens[1:] - cu_seqlens[:-1]).max().item())

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.

max_seqlen is computed the same way independently of modality so not sure @vasqu maybe we should strip the utility from the keyword "vision" and use it everywhere ?

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.

It's just a bit weird because it would live in vision utils. Maybe we need multimodal_utils, vision, and audio?

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.

this can be just in utils.generic for now, but mm_utils also works, although might stay mostly empty

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.

I moved the modality-agnostic logic to utils.generic.get_max_seqlen, updated both vision and audio paths to use it, and removed the duplicated audio helpers.

@github-actions

Copy link
Copy Markdown
Contributor

[For maintainers] Suggested jobs to run (before merge)

run-slow: cosmos3_edge, ernie4_5_vl_moe, exaone4_5, glm4v, glm4v_moe, glm_image, glm_ocr, hunyuan_vl, kimi_k25, minicpmv4_6, paddleocr_vl, qwen2_5_omni, qwen2_5_vl, qwen2_vl, qwen3_5, qwen3_5_moe

@github-actions

Copy link
Copy Markdown
Contributor

CI recap

Dashboard: View test results in Grafana
Latest run: 29799990094:2
Result: success | Jobs: 15 | Tests: 168,122 | Failures: 0 | Duration: 15h 42m

@vasqu vasqu left a comment

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.

Let's land this 🫡 nice effort, these obvious but huge gains are nice to see

@vasqu
vasqu enabled auto-merge July 21, 2026 13:14
@vasqu
vasqu added this pull request to the merge queue Jul 21, 2026
@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

Merged via the queue into huggingface:main with commit 858d4a2 Jul 21, 2026
106 checks passed
SangbumChoi added a commit to SangbumChoi/transformers that referenced this pull request Jul 22, 2026
* upstream/main: (39 commits)
  Remove deprecated training args and `is_fast` property (huggingface#46917)
  Consistent output shape from `get_image_features` (huggingface#46405)
  Fix multi-device mxfp4 dequantization race in `_convert_moe_packed_tensors` (huggingface#47423)
  fix failed test cases for qwen3_omni_moe model (huggingface#47449)
  Fix Hunyuan-VL PIL image resize parity with reference preprocessing (huggingface#47233)
  Move `value` padding into the attention interfaces that need it (huggingface#47451)
  Simplify function dispatch for linear attention (huggingface#47450)
  [cache] Allow sliding window layers to be roll-backed for speculative decoding (huggingface#47447)
  Fix double-shifted training loss in GitForCausalLM (huggingface#47395)
  Fix CohereASR training-loss double-shift (same as Moonshine fix huggingface#46784) (huggingface#46895)
  Warn when `group_by_length` is silently ignored for iterable datasets (huggingface#47379)
  Update bug report list (huggingface#46607)
  Fix shape mismatch in KyutaiSpeechToText `generate()` last window (huggingface#46952)
  Optimize flash attention max seqlen computation in vision attention (huggingface#47170)
  fix: remove unreachable return in special token builder (huggingface#47420)
  Add Harry to slow CI (huggingface#47454)
  BLT: vectorize patch length processing (huggingface#47385)
  Fix `TrackioCallback` fails to log evaluation metrics after training ends (huggingface#46935)
  [Kimi] add integration tests (huggingface#47383)
  Fix typo in `MusicgenForCausalLM.generate()` (huggingface#46974)
  ...
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.

5 participants