Avoid long-blocking H2D copies in ViT - #51841
Merged
Merged
Conversation
mrope_positions is allocated as [3, max_num_tokens + 1] with a dummy trailing column that keeps it non-contiguous for torch.compile, so the cpu[:, :N] slice is a strided view; copy_() cannot express that as one cudaMemcpyAsync and instead gathers into a contiguous pageable temporary, which makes the transfer ignore non_blocking=True and synchronize the stream. Copying each of the three rows separately keeps the source contiguous inside the pinned allocation, so the copy stays asynchronous. Signed-off-by: Max Hu <hyoung2991@gmail.com>
maxyanghu
requested review from
njhill,
sighingnow and
vadiklyutiy
as code owners
August 11, 2026 16:03
rot_pos_ids() builds its per-image tensors from numpy, so the concatenated pos_ids lives in ordinary pageable host memory, and a pageable H2D copy ignores non_blocking=True and synchronizes the stream before the transfer starts. Pinning the buffer first keeps the copy on the asynchronous path; pin_memory() is host-side only and does not change what is transferred. Signed-off-by: Max Hu <hyoung2991@gmail.com>
maxyanghu
force-pushed
the
max/fix-sync-cudamemcpyasync
branch
from
August 11, 2026 16:09
763a857 to
bb74c9b
Compare
Member
|
/ci run |
|
✅ Triggered Buildkite CI #83395 for commit |
Isotr0py
enabled auto-merge (squash)
August 11, 2026 16:42
njhill
reviewed
Aug 11, 2026
Co-authored-by: Nick Hill <nickhill123@gmail.com> Signed-off-by: Max Hu <hyoung2991@gmail.com>
auto-merge was automatically disabled
August 11, 2026 18:52
Head branch was pushed to by a user without write access
Contributor
|
Hi @maxyanghu, the pre-commit checks have failed. Please run: uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-filesThen, commit the changes and push to your branch. For future commits, |
Strip trailing whitespace introduced by the web editor and reflow the call to match ruff-format. Signed-off-by: Max Hu <hyoung2991@gmail.com>
Member
|
/ci run |
|
✅ Triggered Buildkite CI #83439 for commit |
Member
|
/ci retry |
|
✅ Queued 3 failed job(s) for retry in Buildkite CI #83439. |
zyp2014
pushed a commit
to zyp2014/vllm
that referenced
this pull request
Aug 21, 2026
Signed-off-by: Max Hu <hyoung2991@gmail.com> Co-authored-by: Nick Hill <nickhill123@gmail.com>
This was referenced Aug 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
Two host-to-device copies on the per-iteration critical path can take a very
long time to return. Despite
non_blocking=True, thecudaMemcpyAsynccallblocks the calling thread, and the time it takes tracks the amount of GPU work
that has already been queued. That undoes the host run-ahead that asynchronous
scheduling and CUDA graphs exist to build up.
Neither call site looks suspicious at a glance, which is why this is easy to
miss:
M-RoPE positions (
vllm/v1/worker/gpu_model_runner.py). The staging bufferis pinned. However
mrope_positionsis allocated as[3, max_num_tokens + 1],where the dummy trailing column is deliberately there to keep the tensor
non-contiguous for
torch.compile. That makescpu[:, :N]a strided view of apinned buffer. Using
CudaMemcpyAsyncwith non-contiguous buffer leads to a silent synchronization.Copying the three rows individually — each row being contiguous
within that same pinned allocation — makes the synchronization go away.
Before M-RoPE fix:

After M-RoPE fix:

As shown above, the new three HtoDs became truly pinned and the synchronization goes away. There is still a very long
cudamemcpyasyncbecause we haven't fixed the following vision position ids copies yet.Vision position ids (
vllm/model_executor/models/qwen3_vl.py).rot_pos_ids()builds its per-image tensors from numpy, so the concatenatedpos_idsis not pinned. Calling.pin_memory()before.to(device, non_blocking=True)makes the long copies go away.pin_memory()is a host-side copy only and does not change what is transferred.
Before vision position ids fix:

After vision position ids fix:

After these two fixes,
cudaMemcpyAsyncsynchronizations are totally eliminated.Scope
The M-RoPE change affects models using M-RoPE (Qwen2-VL, Qwen2.5-VL, Qwen3-VL)
on the V1 model runner. Model Runner V2 stages positions through UVA buffers and
does not perform this copy, so it is unaffected.
The vision position id change is specific to Qwen3-VL and is independent of the
model runner.
Test Plan
Existing Qwen-VL correctness tests. The changes affect only how the transfer is
issued, not what is transferred.
Test Result
Output is unchanged. Profiling shows the long-blocking copies are no longer
present at either call site.
What is measured, and what is not
A microbenchmark that issues a single copy behind a fixed, deep queue of GPU
work, sweeping only the copy size, shows a payload threshold. Below it the
cudaMemcpyAsynccall returns in microseconds. Above it the call takes roughlyas long as the GPU work already queued, and stays at that cost as the payload
grows further.
Being explicit about the limits of that result:
reproducible; we cannot yet say why.
have not demonstrated that it is specifically a stream synchronization.
end to end on a production model configuration.
source, so no claim is made about that path here.
What is directly observable is the part this PR relies on: at both sites, issuing
the transfer from pinned, contiguous memory removes the long-blocking copies.
Essential Elements of an Effective PR Description Checklist
supported_models.mdandexamplesfor a new model.