Skip to content

[Refactor] Make some flash-attention utils more readable - #49071

Merged
remi-or merged 5 commits into
mainfrom
flash_modeling_fixes
Oct 1, 2026
Merged

remi-or merged 5 commits into
mainfrom
flash_modeling_fixes

Conversation

@remi-or

@remi-or remi-or commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

CPU CI GPU run-slow

This PR refactors two functions present in modeling_flash_attention_utils.py, not to change their behaviour but to make them more readable. It also fixes one bug.

_lazy_imports

  • introduce fa_fallback_version to make the fallback system more readable: we fall back to the highest version of FA avaiable
  • collapse the if/elif/else chain in that function: it used to be that in the else, there was another if/elif/else branch. Now everything is in the same branch
  • moved the NPU-specific block before the FA block, where we try FA2, 3 and 4. First, it seems more logical with the block comment. Second, unless mutilples device types are available, this is does not change the behavior because those branches are exclusive: if is_npu_available is True, is_flash_attention_x_available if False
  • added a tiny tuple[Callable, ...] return type hint

_flash_attention_forward

This is the change that matters the most IMO. This function is mainly about preparing the sequence-defining attributes (cu_seq_lens_q, cu_seq_lens_k, max_length_q, max_length_k) before calling the right flavor of flash. Currently, there are 4 cases:

  1. there is an attention_mask, which we use to unpad the sequences and compute the sequence-defining attributes
  2. there is no attention mask but the sequence-defining attributes are already passed
  3. there is no attention mask, no sequence-defining attributes passed, and the position_ids show that the sequence is packed, therefore we compute the sequence-defining attributes from positions_ids
  4. there is no attention mask, no sequence-defining attributes passed, and the sequence are not packed. In this case and this case only we run flash and not flash_varlen

Cases 1, 2 and 3 only differ in the way they pre-process and post-process the input / output of flash_varlen. Yet the currently, the main body of those cases is not shared: we duplicate some mps check, the call to the function, and checking for tuple.
Furthermore, the check for case 3. is always run. If positions_ids is passed, the check is expensive: GPU ops + D2H sync (incompatible with cuda graph), while totally avoidable if we are in case 1 or 2.
Finally, there is a "hole" in the checks: though we check if all sequence_defining attributes are passed, we only compute them from position_ids if the cu_seqlens are missing: this means that if a user does not pass max_length, the flash_varlen call will crash.

To fix all this:

  • we separate the pre-processing, call to flash and post-processing into 3 different blocks. The only special case is 4.: since there is not pre or post processing to do, we return early in the pre-processing block
  • we stop pre-computing is_fa_with_position_ids and only compute it case 1. and 2. fail.
  • we stop repeating the mps check, call flash_varlen and tuple stripping block
  • there is a tiny rename inside the function scope from flash_kwargs to flash_kwargs_fn because it's not a dict and it makes everything fit to one line when we call it

Side note: the diff on GH is quite ugly, I would recommend checking it out with Gitlens or smt. We can see there is much less change there.

@remi-or
remi-or requested a review from vasqu September 24, 2026 06:41
@remi-or remi-or self-assigned this Sep 24, 2026
@remi-or
remi-or added this pull request to stack #49072 September 24, 2026 06:41
@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.

@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.

Not completely opposed just wondering in combination with my other FA PR and how that fits in

return pad_fn(out, indices_q, query_states.size(0), query_length)

return out
return out.view(query_states.size(0), -1, out.size(-2), out.size(-1))

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.

Thinking of #45153 and how this would play into your refactor here because we would need to support base FA via varlen (it's less accurate using varlen which is why I dont want to drop the base FA completely)

Also opinionated but how is this more readable? I know before we repeated quite a bit but now we have quite a few branches we need to remember like the last pad on the attention mask 😅 maybe we can adopt one function per scenario as in the other PR so we see what does what?

@remi-or remi-or Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

we would need to support base FA via varlen (it's less accurate using varlen which is why I dont want to drop the base FA completely)

We still do? For that path the behavior has not changed, we still run base FA and exit if this is not varlen

Also opinionated but how is this more readable?

Well we just have a block to process the sequence defining variables, then the call, then the post processing. On the opposite I think it would make the code less readable to have 3 fused paths (4 with paged attention) and it will lead to repetitions inside the code. Plus we will still pass a lot of duplicate kwargs around. I think an if / else on the out is ok for users!

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.

We still do? For that path the behavior has not changed, we still run base FA and exit if this is not varlen

No I think you misunderstood, torch varlen only has FA varlen so there is no base FA and we need a workaround for that

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ha ok! I don't understand how it relates to this PR though, sorry

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 I was trying to be preemptive here: So we would introduce another prepare fa varlen path right? But yea no worries

@remi-or remi-or Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

So we would introduce another prepare fa varlen path right

Depends on what arguments are given! I guess the torch varlen will have similar arguments, 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.

Yep we basically dummy create them because you can just infer them based on the bsz / seq len but should be easy to integrate 🫡

@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.

Should be fine, I dont want to fight against it and I feel like im just becoming an opinionated slug :D

@remi-or
remi-or force-pushed the flash_modeling_fixes branch 2 times, most recently from 626eed2 to 2f83e48 Compare September 25, 2026 11:19
@remi-or
remi-or force-pushed the flash_modeling_fixes branch 2 times, most recently from 9fe211e to a052c9f Compare September 29, 2026 06:42
@remi-or
remi-or force-pushed the flash_modeling_fixes branch from a052c9f to 45bfb18 Compare October 1, 2026 04:56
@remi-or
remi-or added this pull request to the merge queue Oct 1, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 1, 2026
Base automatically changed from cb-little-fixes to main October 1, 2026 09:47
@remi-or
remi-or force-pushed the flash_modeling_fixes branch from 45bfb18 to f27d79a Compare October 1, 2026 09:48
@remi-or
remi-or force-pushed the flash_modeling_fixes branch from f27d79a to 3409c56 Compare October 1, 2026 11:44
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

CI recap

Dashboard: View test results in Grafana
Latest run: 36857242507:1
Result: success | Jobs: 16 | Tests: 178,870 | Failures: 0 | Duration: 13h 45m

@remi-or
remi-or added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit e7cceb9 Oct 1, 2026
114 checks passed
@remi-or
remi-or deleted the flash_modeling_fixes branch October 1, 2026 12:29
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