Skip to content

[Bugfix][MiMo] Apply vision attention sinks in the window attention path - #49815

Merged
Isotr0py merged 6 commits into
vllm-project:mainfrom
almogtavor:fix-mimo-vision-sinks
Aug 10, 2026
Merged

Isotr0py merged 6 commits into
vllm-project:mainfrom
almogtavor:fix-mimo-vision-sinks

Conversation

@almogtavor

@almogtavor almogtavor commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Fixes #47864.

MiMoVisionAttention allocates self.sinks only when the block is not in fullatt_block_indexes, which is exactly the set of blocks that run _forward_window_attn. That path never read the parameter, so the sink weights were loaded from the checkpoint and dropped. XiaomiMiMo/MiMo-V2.5 ships visual.blocks.N.attn.sinks for exactly those blocks, 24 of its 28, and none for the full attention blocks [0, 9, 18, 27].

Approach

The reference adds sinks[h] to the logit of each sequence's first key. triton_prefill_attention already accepts sinks, but treats them as a null logit that only inflates the denominator, which is a different operation (see the measurements below). This adds a sinks_bias_key0 mode that biases key 0 instead, and routes the window path through that kernel, so the softmax normalizes over the biased scores in one pass.

An earlier revision of this PR corrected flash attention's output after the fact using the softmax LSE. That worked, but as @Isotr0py noted it carries ongoing maintenance cost, and the kernel change is both simpler and faster.

Numbers

H100, bf16, window 64, 8 heads, head dim 64. Error is against a dense fp32 windowed softmax with the sink on key 0. Latency is the mean of 20 iterations.

tokens LSE correction err this PR err LSE correction this PR
512 2.23e-03 2.14e-03 0.322 ms 0.074 ms
1024 2.19e-03 2.14e-03 0.307 ms 0.072 ms
4096 2.15e-03 2.14e-03 0.324 ms 0.081 ms
16384 0.316 ms 0.179 ms

2.14e-03 is the bf16 noise floor. The kernel path is equal on accuracy, 2x to 4x faster, and drops ~50 lines of correction from the model file.

For reference, the two sink formulations are not interchangeable. Measured earlier on the same setup, against MiMo's key 0 reference and against a null logit reference:

vs key 0 bias vs null logit
main, sinks dropped 0.048 0.022
s_aux on FA3 0.052 0.0020
sinks as null logit on triton 0.052 0.0020
this PR 0.0021 0.053

Test

tests/models/multimodal/test_mimo_v2_omni.py compares _forward_window_attn against the dense fp32 reference for MHA and GQA, with one sequence shorter than the window and one longer. Both pass here at 2.3e-03 and fail on main at 1.2e-01 and 2.1e-01.

tests/kernels/attention/test_triton_prefill_attention.py, 20 passed, so the existing null-logit behaviour is unchanged.

AI assistance was used for this change.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@mergify mergify Bot added the bug Something isn't working label Jul 25, 2026
@almogtavor
almogtavor force-pushed the fix-mimo-vision-sinks branch 3 times, most recently from 0babb74 to bbd5d51 Compare July 25, 2026 19:42
@mergify mergify Bot added the multi-modality Related to multi-modality (#4194) label Jul 25, 2026
@DarkLight1337
DarkLight1337 requested a review from Isotr0py July 26, 2026 05:40
Comment on lines 221 to 233
@@ -229,8 +229,68 @@ def _forward_window_attn(
softmax_scale=self.scale,
causal=False,
window_size=[w, w],
return_softmax_lse=self.sinks is not 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.

Why not pass s_aux=self.sinks?

@almogtavor almogtavor Jul 26, 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.

@Isotr0py since FA2 rejects it with NotImplementedError: FA2 does not support s_aux so it'll only work on FA3 / FA4 (flash_attn_interface.py passes it as learnable_sink=s_aux). It's also a different operation since s_aux appends a null logit to the softmax denominator, so every weight shrinks by the same factor and nothing is added to the numerator. MiMoVisionAttention instead adds sinks[h] to the logit of key 0, which rescales key 0's softmax weight by exp(sinks[h]) (head h gets sinks[h] at key 0 for all queries and zero at every other key), so the value vector of key 0 (v0) contributes more or less to the output depending on the sign.

Forcing fa_version=3 so it runs at all, H100 in bf16 against that reference:

error
main, sinks dropped 0.048
s_aux=self.sinks 0.052 (FA3 only)
this PR 0.0022

0.0022 is the bf16 noise floor, the same call with no sinks measures 0.0020 against fp32 SDPA. A real additive bias on key 0 would need new kernel code in vllm-project/flash-attention for FA2, FA3 and FA4, so this PR reconstructs the result that a kernel-native bias would produce, using the softmax LSE that flash attention already returns. It is a little slower than doing it in the kernel of course.

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 think you can fall back to Triton kernels for pre-Hopper devices:

def context_attention_fwd(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
o: torch.Tensor,
b_start_loc: torch.Tensor,
b_seq_len: torch.Tensor,
max_input_len: int,
is_causal: bool = True,
softmax_scale: float | None = None,
sliding_window_q: int | None = None,
sliding_window_k: int | None = None,
sinks: torch.Tensor | None = None,
):

@almogtavor almogtavor Jul 27, 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.

@Isotr0py Thanks, I looked at that kernel. It computes the same thing as s_aux, so it changes where the code can run rather than what it computes.

At lines 99-103 it initialises m_i to the sink and l_i to 1.0 with acc at 0, so the sink is one extra term in the denominator that never reaches the numerator, because no value vector is paired with it and acc starts empty. That is the GPT-OSS null logit, but MiMoVisionAttention adds sinks[h] to the logit of key 0, which does reach the numerator through v0.

H100, bf16, same input, measured against both formulations:

err vs MiMo (key 0 bias), we want it lower err vs null logit (lower / higher doenst matter)
main, sinks dropped 0.048 0.022
s_aux=self.sinks on FA3 (issue suggestion) 0.052 0.0020
sinks=self.sinks on triton (your suggestion) 0.052 0.0020
LSE correction (this PR) 0.0021 0.053

Besides the baseline, each row reproduces either the key 0 bias or the null logit to within bf16 rounding (never both. bf16 rounding is about 0.002). The two sink paths land on the null logit and agree with each other in four digits and both are 0.052 from MiMo, a little further than simply dropping the sinks. As a wiring check i saw the triton kernel with no sinks matches flash attention with no sinks to 0.

We could implement in the triton kernel support for having the key 0 bias directly, but it would mean routing this path through triton on every device including Hopper.

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.

@Isotr0py wdyt?

@Isotr0py Isotr0py Jul 30, 2026

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 think using/correcting the triton kernel should be fine, LSE correction will increase the maintainence effort.

@almogtavor

Copy link
Copy Markdown
Contributor Author

@Isotr0py I tried your suggestion properly and it comes out ahead, so I would like to switch this PR to it.

Two things were needed. The kernel treats sinks as a null logit, so I added a mode that biases key 0 instead, matching MiMoVisionAttention. Separately the kernel iterated every key block and masked afterwards, so a windowed pass cost O(seq_len^2). That is fixed on its own in #50776.

H100, bf16, 8 heads, head dim 64, window 64. Error is against a dense fp32 windowed softmax with the sink on key 0, latency is the mean of 20 iterations:

tokens LSE correction err triton key 0 err LSE ms triton ms
512 2.23e-03 2.14e-03 0.322 0.074
1024 2.19e-03 2.14e-03 0.307 0.072
4096 2.15e-03 2.14e-03 0.324 0.081
16384 0.316 0.179

Same accuracy, roughly 2x to 4x faster, and the model file loses the ~50 line correction. The 20 existing cases in test_triton_prefill_attention.py still pass.

Only caveat is that it moves this path off flash attention and onto the Triton kernel for every device. Happy to rework the PR that way if you prefer it, once #50776 is settled.

`MiMoVisionAttention` allocates `self.sinks` only for blocks that are not
in `fullatt_block_indexes`, which is exactly the set of blocks running
`_forward_window_attn`. That path never read the parameter, so the sink
weights were loaded from the checkpoint and dropped.
`XiaomiMiMo/MiMo-V2.5` ships `visual.blocks.N.attn.sinks` for 24 of its 28
blocks, and none for the full attention blocks [0, 9, 18, 27].

The reference adds `sinks[h]` to the logit of each sequence's first key.
The Triton prefill kernel already carries sinks, but as a null logit in
the denominator, which is a different operation. Add a mode that biases
key 0 instead and use it here, so the softmax normalizes over the biased
scores in one pass with no post-hoc correction.

Measured on H100 in bf16 against a dense fp32 windowed softmax with the
sink on key 0, window 64, 8 heads, head dim 64:

| tokens | error |
|--------|-------|
| 512    | 2.14e-03 |
| 1024   | 2.14e-03 |
| 4096   | 2.14e-03 |

2.14e-03 is the bf16 noise floor. Dropping the sinks gives 1.2e-01 to
2.1e-01 on the regression test's shapes.

Signed-off-by: almogtavor <almogtavor@gmail.com>
@Isotr0py
Isotr0py enabled auto-merge (squash) August 5, 2026 13:53
@github-actions github-actions Bot added the ready ONLY add when PR is ready to merge/full CI is needed label Aug 5, 2026
@almogtavor

Copy link
Copy Markdown
Contributor Author

/ci run

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #82840 for commit f84448665dd7.

@almogtavor

Copy link
Copy Markdown
Contributor Author

/ci retry

@github-actions

Copy link
Copy Markdown

✅ Queued 3 failed job(s) for retry in Buildkite CI #82840.

@almogtavor

Copy link
Copy Markdown
Contributor Author

/ci retry

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #83215 for commit 09d0016d2229, running 2 failed step(s) from Buildkite CI #82840.

@Isotr0py
Isotr0py merged commit c3cac8c into vllm-project:main Aug 10, 2026
12 checks passed
@almogtavor
almogtavor deleted the fix-mimo-vision-sinks branch August 11, 2026 08:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working multi-modality Related to multi-modality (#4194) ready ONLY add when PR is ready to merge/full CI is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]:MiMo Code mimo_v2_omni.py ERROR

2 participants