Skip to content

[Bugfix][V2] record_stream idx_mapping in the PP draft broadcast - #55745

Merged
njhill merged 3 commits into
vllm-project:mainfrom
eastwood-c:fix/pp-draft-broadcast-record-stream
Sep 9, 2026
Merged

njhill merged 3 commits into
vllm-project:mainfrom
eastwood-c:fix/pp-draft-broadcast-record-stream

Conversation

@eastwood-c

Copy link
Copy Markdown
Contributor

Purpose

PPHandler.broadcast_drafts reads a main-stream tensor on the broadcast stream without a record_stream guard, so the CUDA caching allocator can recycle that tensor's memory while the gather is still in flight. The recycled indices then land out of bounds:

ATen/native/cuda/IndexKernel.cu:111: operator(): block: [0,0,0], thread: [0,0,0]
Assertion `-sizes[i] <= index && index < sizes[i] && "index out of bounds"` failed.

usually surfacing through the pp_broadcast watchdog as CUDA error: device-side assert triggered, which kills EngineCore mid-run.

The site, in vllm/v1/worker/gpu/pp_utils.py:

with torch.cuda.stream(self.broadcast_stream):
    self.broadcast_stream.wait_stream(self.main_stream)
    send = draft_tokens[input_batch.idx_mapping].contiguous()   # main-stream tensor
    torch.distributed.broadcast(send, src=self.last_rank, group=self.broadcast_group)
    send.record_stream(self.broadcast_stream)

input_batch.idx_mapping is allocated on the main stream every step (async_copy_to_gpu -> torch.empty_like). The wait_stream orders the gather after the H2D copy, so the indices are correct at launch; what is missing is the other half of the contract. The last rank never retains idx_mapping -- unlike receive, which parks it in a PendingRecv -- so it is freed at end of step and the allocator may hand the block to the next allocation while the gather is still pending on broadcast_stream.

broadcast, the sampled-token sibling a few lines below, already record_streams every main-stream input it sends. This applies the same guard to the one input broadcast_drafts missed. draft_tokens needs none -- it is RequestStates' persistent buffer, not a per-step allocation.

Reproducing it requires PP>=2, a drafter, and async launches all at once. That is why it has gone unnoticed since #50514: a PP run without a speculator never calls broadcast_drafts, and CUDA_LAUNCH_BLOCKING=1 hides it completely by serialising the gather ahead of any reuse. EAGLE3 + PP is exposed to it, and so is MTP + PP once a drafter is reachable there.

Not a duplicate: no open PR or issue touches pp_utils.py, broadcast_drafts, or this assert. Searched pp_utils, record_stream broadcast_drafts, and IndexKernel index out of bounds pipeline parallel across open PRs and issues.

Test Plan

Unit:

pytest tests/v1/worker/test_pp_utils.py -v

End-to-end, the configuration that reproduces the assert -- GSM8K, 200 prompts, greedy, max_tokens=2048, VLLM_USE_V2_MODEL_RUNNER=1, 2x H200:

# QuantTrio/Qwen3.6-35B-A3B-AWQ, pipeline_parallel_size=2, tensor_parallel_size=1,
# speculative_config={"method": "mtp", "num_speculative_tokens": 1},
# gpu_memory_utilization=0.85, max_model_len=4096

Run before and after the patch on the same tree, same venv, same box, otherwise idle. Acceptance is measured as 1 + accepted/drafts per prompt, mirroring tests/v1/e2e/spec_decode/acceptance_rates/.

Test Result

tests/v1/worker/test_pp_utils.py: 4 passed.

End-to-end, same cell before and after:

tree runs outcome time to failure
before 6 device-side assert 6/6 148s, 160s, 163s, 205s, 212s, 258s
after 4 200/200 clean 4/4 n/a

mean_acceptance_length after the fix: 1.9380, 1.9373, 1.9354, 1.9357.

Unchanged within noise against the two configurations that were already clean, as expected for a change that only affects allocator lifetime:

config mean_acceptance_length
PP=2, async launches, patched 1.9380, 1.9373, 1.9354, 1.9357
PP=2, CUDA_LAUNCH_BLOCKING=1 (assert masked) 1.9352
PP=1, async launches 1.9375

The same assert was also observed on zai-org/GLM-5.2-FP8 at TP=4/PP=2 with MTP k=1 and k=2, so it is not specific to one model or quantisation.


AI assistance was used for this change: to investigate the failure, isolate the faulting path, and draft the patch and this description. The change and its test results have been reviewed line by line before submission.

`PPHandler.broadcast_drafts` gathers the draft block on `broadcast_stream` using
`input_batch.idx_mapping`, which the model runner allocates on the main stream
every step (`async_copy_to_gpu` -> `torch.empty_like`). `wait_stream` orders the
gather after the H2D copy, so the indices are correct at launch, but nothing
tells the caching allocator the block is still in use on another stream. The
last rank does not retain `idx_mapping` -- unlike `receive`, which parks it in a
`PendingRecv` -- so it is freed at end of step and the allocator can hand the
block to the next allocation while the gather is still pending. The indices are
then overwritten with unrelated data:

  ATen/native/cuda/IndexKernel.cu:111: operator(): block: [0,0,0], thread:
  [0,0,0] Assertion `-sizes[i] <= index && index < sizes[i] &&
  "index out of bounds"` failed.

usually surfacing via the `pp_broadcast` watchdog as `CUDA error: device-side
assert triggered`, killing EngineCore.

`broadcast`, the sampled-token sibling, already record_streams every main-stream
input it sends. Apply the same guard to the one input `broadcast_drafts` missed.
`draft_tokens` needs none -- it is `RequestStates`' persistent buffer.

Triggering it requires PP>=2, a drafter, and async launches together, which is
why it has gone unnoticed: `CUDA_LAUNCH_BLOCKING=1` serialises the gather ahead
of any reuse and hides it completely.

Tests: Qwen3.6-35B-A3B-AWQ, GSM8K 200 prompts, PP=2, TP=1, MTP k=1, V2 runner,
2x H200, max_tokens=2048, same tree and venv either side of the patch:

  before: device-side assert 6/6 runs, 148s to 258s in
  after:  200/200 clean 4/4 runs, mean_acceptance_length
          1.9380 / 1.9373 / 1.9354 / 1.9357

Acceptance is unchanged within noise against the two configurations that were
already clean -- 1.9352 with CUDA_LAUNCH_BLOCKING=1 and 1.9375 at PP=1 -- as
expected for a change that only affects allocator lifetime.

tests/v1/worker/test_pp_utils.py: 4 passed.

Signed-off-by: Chris Eastwood <chris.eastwood@pwn4g3.dev>

@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 mrv2 Model Runner V2 specific bug Something isn't working labels Sep 7, 2026
@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 79b04cf0-50ac-40a8-93f6-ac7bff7fddf1

📥 Commits

Reviewing files that changed from the base of the PR and between 70584f6 and 423662c.

📒 Files selected for processing (1)
  • vllm/v1/worker/gpu/pp_utils.py

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Improved stability when broadcasting draft tokens by preventing temporary data from being reused while still in use.

Walkthrough

broadcast_drafts now records the broadcast stream on input_batch.idx_mapping before gathering draft tokens. This prevents premature reuse of the tensor by the caching allocator.

Changes

Broadcast stream safety

Layer / File(s) Summary
Record mapping stream usage
vllm/v1/worker/gpu/pp_utils.py
broadcast_drafts calls record_stream on input_batch.idx_mapping before the draft-token gather.

Estimated code review effort: 2 (Simple) | ~5 minutes

Merge Risk: ⚪ Minimal · up to 42366

This fixes premature CUDA allocator reuse of draft-broadcast indices, preventing pipeline-parallel speculative decoding failures without changing draft-token handling. The current change is ready to merge.

Suggested reviewers: archeychen, yongqinwang-cmd

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies a bug fix for recording the idx_mapping stream in the pipeline-parallel draft broadcast path.
Description check ✅ Passed The description directly explains the CUDA memory-lifetime bug, the record_stream fix, affected configurations, and test results.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@eastwood-c

Copy link
Copy Markdown
Contributor Author

@yewentao256 @njhill This issue blocks full verification that I have been running for closing out #46994

@github-actions

github-actions Bot commented Sep 7, 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 for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream 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.

🚀

@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 Sep 8, 2026
@yewentao256

Copy link
Copy Markdown
Member

/ci run

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

@eastwood-c, 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.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #87720 for commit 423662c95a2d.

@njhill njhill 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 @eastwood-c. I made a small adjustment, including removing another record_stream which is unnecessary (since the send tensor is allocated and used on the same stream).

@njhill

njhill commented Sep 9, 2026

Copy link
Copy Markdown
Member

/ci run

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #87949 for commit 29e82f21f73a.

@eastwood-c

Copy link
Copy Markdown
Contributor Author

@njhill Nice catch, I thought there was something different about the targets so I hadn't removed that. I'll take some time and keep familiarizing myself with the structures.

@njhill
njhill merged commit e8064a9 into vllm-project:main Sep 9, 2026
108 checks passed
@eastwood-c
eastwood-c deleted the fix/pp-draft-broadcast-record-stream branch September 9, 2026 20:03
lucifer1004 added a commit to lucifer1004/vllm that referenced this pull request Sep 10, 2026
Resolve pp_utils.py: upstream vllm-project#55745 re-added a broadcast_drafts near the
top of PPHandler; keep this branch's single definition (the one with the
disabled/max_sample_len guards and the main-stream gather).

Co-authored-by: Kimi Code <noreply@moonshot.cn>
Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
ItsRoy69 pushed a commit to ItsRoy69/vllm that referenced this pull request Sep 10, 2026
…m-project#55745)

Signed-off-by: Chris Eastwood <chris.eastwood@pwn4g3.dev>
Signed-off-by: Nick Hill <nickhill123@gmail.com>
Co-authored-by: Nick Hill <nickhill123@gmail.com>
Signed-off-by: Jyotirmoy Roy <jyotirmoyroy649@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working mrv2 Model Runner V2 specific 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.

3 participants