Your current environment
The output of python collect_env.py
GPU0 X SYS 3-63,131-191 0 N/A
NIC0 SYS X
Legend:
X = Self
SYS = Connection traversing PCIe as well as the SMP interconnect between NUMA nodes (e.g., QPI/UPI)
NODE = Connection traversing PCIe as well as the interconnect between PCIe Host Bridges within a NUMA node
PHB = Connection traversing PCIe as well as a PCIe Host Bridge (typically the CPU)
PXB = Connection traversing multiple PCIe bridges (without traversing the PCIe Host Bridge)
PIX = Connection traversing at most a single PCIe bridge
NV# = Connection traversing a bonded set of # NVLinks
NIC Legend:
NIC0: mlx5_bond_0
==============================
Environment Variables
NVIDIA_VISIBLE_DEVICES=GPU-216bfdad-f232-5610-63d8-c1754594308c
VLLM_BUILD_URL=https://buildkite.com/vllm/release-v2/builds/4244
NVIDIA_REQUIRE_CUDA=cuda>=12.9 brand=unknown,driver>=535,driver<536 brand=grid,driver>=535,driver<536 brand=tesla,driver>=535,driver<536 brand=nvidia,driver>=535,driver<536 brand=quadro,driver>=535,driver<536 brand=quadrortx,driver>=535,driver<536 brand=nvidiartx,driver>=535,driver<536 brand=vapps,driver>=535,driver<536 brand=vpc,driver>=535,driver<536 brand=vcs,driver>=535,driver<536 brand=vws,driver>=535,driver<536 brand=cloudgaming,driver>=535,driver<536 brand=unknown,driver>=550,driver<551 brand=grid,driver>=550,driver<551 brand=tesla,driver>=550,driver<551 brand=nvidia,driver>=550,driver<551 brand=quadro,driver>=550,driver<551 brand=quadrortx,driver>=550,driver<551 brand=nvidiartx,driver>=550,driver<551 brand=vapps,driver>=550,driver<551 brand=vpc,driver>=550,driver<551 brand=vcs,driver>=550,driver<551 brand=vws,driver>=550,driver<551 brand=cloudgaming,driver>=550,driver<551 brand=unknown,driver>=560,driver<561 brand=grid,driver>=560,driver<561 brand=tesla,driver>=560,driver<561 brand=nvidia,driver>=560,driver<561 brand=quadro,driver>=560,driver<561 brand=quadrortx,driver>=560,driver<561 brand=nvidiartx,driver>=560,driver<561 brand=vapps,driver>=560,driver<561 brand=vpc,driver>=560,driver<561 brand=vcs,driver>=560,driver<561 brand=vws,driver>=560,driver<561 brand=cloudgaming,driver>=560,driver<561 brand=unknown,driver>=565,driver<566 brand=grid,driver>=565,driver<566 brand=tesla,driver>=565,driver<566 brand=nvidia,driver>=565,driver<566 brand=quadro,driver>=565,driver<566 brand=quadrortx,driver>=565,driver<566 brand=nvidiartx,driver>=565,driver<566 brand=vapps,driver>=565,driver<566 brand=vpc,driver>=565,driver<566 brand=vcs,driver>=565,driver<566 brand=vws,driver>=565,driver<566 brand=cloudgaming,driver>=565,driver<566 brand=unknown,driver>=570,driver<571 brand=grid,driver>=570,driver<571 brand=tesla,driver>=570,driver<571 brand=nvidia,driver>=570,driver<571 brand=quadro,driver>=570,driver<571 brand=quadrortx,driver>=570,driver<571 brand=nvidiartx,driver>=570,driver<571 brand=vapps,driver>=570,driver<571 brand=vpc,driver>=570,driver<571 brand=vcs,driver>=570,driver<571 brand=vws,driver>=570,driver<571 brand=cloudgaming,driver>=570,driver<571
TORCH_CUDA_ARCH_LIST=7.5 8.0 8.6 8.9 9.0 10.0 12.0
NVIDIA_DRIVER_CAPABILITIES=compute,utility
VLLM_IMAGE_TAG=vllm/vllm-openai:v0.26.0-cu129-ubuntu2404
VLLM_USAGE_SOURCE=production-docker-image
CUDA_VERSION=12.9.1
VLLM_ENABLE_CUDA_COMPATIBILITY=0
VLLM_BUILD_PIPELINE=019d130e-464e-4ff7-b84b-492992c0c06b
LD_LIBRARY_PATH=/usr/local/nvidia/lib64:/usr/local/cuda/lib64:/usr/local/cuda/lib64
OMP_NUM_THREADS=8
VLLM_BUILD_COMMIT=ffd46bfab2128bb84146050e98b51a617c6575ab
PYTORCH_NVML_BASED_CUDA_CHECK=1
TORCHINDUCTOR_COMPILE_THREADS=1
TORCHINDUCTOR_CACHE_DIR=/tmp/torchinductor_root
🐛 Describe the bug
I think there is an off-by-one error in the scheduler's self.scheduler_config.disable_chunked_mm_input logic.
_try_schedule_encoder_inputs in Scheduler guards against splitting a multimodal item across prefill chunks like this:
if (
self.scheduler_config.disable_chunked_mm_input
and num_computed_tokens < start_pos # (A)
and (num_computed_tokens + num_new_tokens) < (start_pos + num_encoder_tokens) # (B)
):
num_new_tokens = max(0, start_pos - (num_computed_tokens + shift_computed_tokens))
break
Check (A) is causing this statement to incorrectly return False when num_computed_tokens == start_pos.
If num_computed_tokens == start_pos then the tokens after num_computed_tokens are exactly
the start of the next multi-modal item, but (A) returns False so the (B) isn't checked and the
multi-modal item might get chunked.
This issue is made worse by the fact if the check does fire when num_computed_tokens < start_pos then
num_computed_tokens will become start_pos, and we will therefore hit this issue if we can't
fit the multi-modal item into the token budget on the second attempt.
This can silently break models with bidirectional attention, since incorrect multi-modal
splitting will prevent tokens that should be able to cross-attend from cross-attending.
Reproduction
We can observe the error by testing the function directly:
from types import SimpleNamespace
from vllm.v1.core.sched.scheduler import Scheduler
START, LENGTH = 3, 766
def request():
# Fake multimodal item of length `LENGTH` offset at `START`
pos = SimpleNamespace(offset=START, length=LENGTH, is_embed=None,
get_num_embeds=lambda: LENGTH,
get_embeds_indices_in_range=lambda a, b: (a, b))
return SimpleNamespace(has_encoder_inputs=True, request_id="r0",
mm_features=[SimpleNamespace(mm_position=pos, identifier="img0", modality="image")])
def sched():
# Fake scheduler `self` with disable_chunked_mm_input=True
return SimpleNamespace(
scheduler_config=SimpleNamespace(disable_chunked_mm_input=True),
is_encoder_decoder=False, ec_connector=None,
encoder_cache_manager=SimpleNamespace(
check_and_update_cache=lambda r, i: False,
can_allocate=lambda r, i, b, n: True))
def ask(num_computed_tokens, num_new_tokens):
# Ask how many tokens the scheduler will actually let this request run this step.
# given we have done `num_computed_tokens` and can afford to do up to `num_new_tokens` more
return Scheduler._try_schedule_encoder_inputs(
sched(), request(), num_computed_tokens, num_new_tokens,
100_000, # stubbed encoder_compute_budget
0, # stubbed shift_computed_tokens
)[1]
# Works: We can don't have room for the image so it only allows 3 new tokens
print(ask(0, 500)) # 3
# Breaks: the scheduler says we can go ahead and compute up to token 500, but this will
# split the image since it ends on position 766
print(ask(3, 500)) # 500
Fix
As far as I can tell changing it to num_computed_tokens <= start_pos will fix the issue
Claude thinks (A) can be deleted. I am hesitant to endorse that since I don't fully understand the
logic and I am not sure why (A) was put there in the first place, but I here is its reasoning:
(A) can also just be deleted, and I believe that is equivalent in normal operation. If the guard is
working then the only reachable states are:
num_computed_tokens |
meaning |
(B) |
< start_pos |
before the item |
decides |
== start_pos |
at the item, nothing of it computed |
decides |
>= start_pos + length |
item already fully computed |
already False, since the left side alone is >= the right |
So (B) rules out the "already processed" case by itself and (A) adds nothing except excluding the
boundary. The only state where the two forms differ is start_pos < num_computed_tokens < start_pos + length, i.e. resuming inside an item, which I think is only reachable with prefix
caching. Deferring there seems right too for a bidirectional model -- the remainder of the item
should not be split either -- but that path is reasoned about rather than tested, so <= is the
change I would suggest.
Either way it should not be possible to starve a request: compute_mm_encoder_budget guarantees
max_num_batched_tokens >= max_tokens_per_mm_item whenever disable_chunked_mm_input is set, so a
request scheduled first in a step always has room for a whole item, and a request given zero tokens
hits continue rather than break, so the rest of the batch keeps moving.
Before submitting a new issue...
Your current environment
The output of
GPU0 X SYS 3-63,131-191 0 N/A NIC0 SYS Xpython collect_env.pyLegend:
X = Self
SYS = Connection traversing PCIe as well as the SMP interconnect between NUMA nodes (e.g., QPI/UPI)
NODE = Connection traversing PCIe as well as the interconnect between PCIe Host Bridges within a NUMA node
PHB = Connection traversing PCIe as well as a PCIe Host Bridge (typically the CPU)
PXB = Connection traversing multiple PCIe bridges (without traversing the PCIe Host Bridge)
PIX = Connection traversing at most a single PCIe bridge
NV# = Connection traversing a bonded set of # NVLinks
NIC Legend:
NIC0: mlx5_bond_0
==============================
Environment Variables
NVIDIA_VISIBLE_DEVICES=GPU-216bfdad-f232-5610-63d8-c1754594308c
VLLM_BUILD_URL=https://buildkite.com/vllm/release-v2/builds/4244
NVIDIA_REQUIRE_CUDA=cuda>=12.9 brand=unknown,driver>=535,driver<536 brand=grid,driver>=535,driver<536 brand=tesla,driver>=535,driver<536 brand=nvidia,driver>=535,driver<536 brand=quadro,driver>=535,driver<536 brand=quadrortx,driver>=535,driver<536 brand=nvidiartx,driver>=535,driver<536 brand=vapps,driver>=535,driver<536 brand=vpc,driver>=535,driver<536 brand=vcs,driver>=535,driver<536 brand=vws,driver>=535,driver<536 brand=cloudgaming,driver>=535,driver<536 brand=unknown,driver>=550,driver<551 brand=grid,driver>=550,driver<551 brand=tesla,driver>=550,driver<551 brand=nvidia,driver>=550,driver<551 brand=quadro,driver>=550,driver<551 brand=quadrortx,driver>=550,driver<551 brand=nvidiartx,driver>=550,driver<551 brand=vapps,driver>=550,driver<551 brand=vpc,driver>=550,driver<551 brand=vcs,driver>=550,driver<551 brand=vws,driver>=550,driver<551 brand=cloudgaming,driver>=550,driver<551 brand=unknown,driver>=560,driver<561 brand=grid,driver>=560,driver<561 brand=tesla,driver>=560,driver<561 brand=nvidia,driver>=560,driver<561 brand=quadro,driver>=560,driver<561 brand=quadrortx,driver>=560,driver<561 brand=nvidiartx,driver>=560,driver<561 brand=vapps,driver>=560,driver<561 brand=vpc,driver>=560,driver<561 brand=vcs,driver>=560,driver<561 brand=vws,driver>=560,driver<561 brand=cloudgaming,driver>=560,driver<561 brand=unknown,driver>=565,driver<566 brand=grid,driver>=565,driver<566 brand=tesla,driver>=565,driver<566 brand=nvidia,driver>=565,driver<566 brand=quadro,driver>=565,driver<566 brand=quadrortx,driver>=565,driver<566 brand=nvidiartx,driver>=565,driver<566 brand=vapps,driver>=565,driver<566 brand=vpc,driver>=565,driver<566 brand=vcs,driver>=565,driver<566 brand=vws,driver>=565,driver<566 brand=cloudgaming,driver>=565,driver<566 brand=unknown,driver>=570,driver<571 brand=grid,driver>=570,driver<571 brand=tesla,driver>=570,driver<571 brand=nvidia,driver>=570,driver<571 brand=quadro,driver>=570,driver<571 brand=quadrortx,driver>=570,driver<571 brand=nvidiartx,driver>=570,driver<571 brand=vapps,driver>=570,driver<571 brand=vpc,driver>=570,driver<571 brand=vcs,driver>=570,driver<571 brand=vws,driver>=570,driver<571 brand=cloudgaming,driver>=570,driver<571
TORCH_CUDA_ARCH_LIST=7.5 8.0 8.6 8.9 9.0 10.0 12.0
NVIDIA_DRIVER_CAPABILITIES=compute,utility
VLLM_IMAGE_TAG=vllm/vllm-openai:v0.26.0-cu129-ubuntu2404
VLLM_USAGE_SOURCE=production-docker-image
CUDA_VERSION=12.9.1
VLLM_ENABLE_CUDA_COMPATIBILITY=0
VLLM_BUILD_PIPELINE=019d130e-464e-4ff7-b84b-492992c0c06b
LD_LIBRARY_PATH=/usr/local/nvidia/lib64:/usr/local/cuda/lib64:/usr/local/cuda/lib64
OMP_NUM_THREADS=8
VLLM_BUILD_COMMIT=ffd46bfab2128bb84146050e98b51a617c6575ab
PYTORCH_NVML_BASED_CUDA_CHECK=1
TORCHINDUCTOR_COMPILE_THREADS=1
TORCHINDUCTOR_CACHE_DIR=/tmp/torchinductor_root
🐛 Describe the bug
I think there is an off-by-one error in the scheduler's
self.scheduler_config.disable_chunked_mm_inputlogic._try_schedule_encoder_inputsinSchedulerguards against splitting a multimodal item across prefill chunks like this:Check (A) is causing this statement to incorrectly return False when
num_computed_tokens == start_pos.If
num_computed_tokens == start_posthen the tokens afternum_computed_tokensare exactlythe start of the next multi-modal item, but (A) returns False so the (B) isn't checked and the
multi-modal item might get chunked.
This issue is made worse by the fact if the check does fire when
num_computed_tokens < start_posthennum_computed_tokenswill becomestart_pos, and we will therefore hit this issue if we can'tfit the multi-modal item into the token budget on the second attempt.
This can silently break models with bidirectional attention, since incorrect multi-modal
splitting will prevent tokens that should be able to cross-attend from cross-attending.
Reproduction
We can observe the error by testing the function directly:
Fix
As far as I can tell changing it to
num_computed_tokens <= start_poswill fix the issueClaude thinks (A) can be deleted. I am hesitant to endorse that since I don't fully understand the
logic and I am not sure why (A) was put there in the first place, but I here is its reasoning:
(A) can also just be deleted, and I believe that is equivalent in normal operation. If the guard is
working then the only reachable states are:
num_computed_tokens< start_pos== start_pos>= start_pos + lengthSo (B) rules out the "already processed" case by itself and (A) adds nothing except excluding the
boundary. The only state where the two forms differ is
start_pos < num_computed_tokens < start_pos + length, i.e. resuming inside an item, which I think is only reachable with prefixcaching. Deferring there seems right too for a bidirectional model -- the remainder of the item
should not be split either -- but that path is reasoned about rather than tested, so
<=is thechange I would suggest.
Either way it should not be possible to starve a request:
compute_mm_encoder_budgetguaranteesmax_num_batched_tokens >= max_tokens_per_mm_itemwheneverdisable_chunked_mm_inputis set, so arequest scheduled first in a step always has room for a whole item, and a request given zero tokens
hits
continuerather thanbreak, so the rest of the batch keeps moving.Before submitting a new issue...