The output of python collect_env.py
Collecting environment information...
==============================
System Info
==============================
OS : Ubuntu 22.04.5 LTS (x86_64)
GCC version : (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0
Clang version : Could not collect
CMake version : Could not collect
Libc version : glibc-2.35
==============================
PyTorch Info
==============================
PyTorch version : 2.11.0+cu130
Is debug build : False
CUDA used to build PyTorch : 13.0
ROCM used to build PyTorch : N/A
XPU used to build PyTorch : N/A
==============================
Python Environment
==============================
Python version : 3.12.13 (main, Mar 4 2026, 09:23:07) [GCC 11.4.0] (64-bit runtime)
Python platform : Linux-6.17.0-1013-aws-x86_64-with-glibc2.35
==============================
CUDA / GPU Info
==============================
Is CUDA available : True
CUDA runtime version : 13.0.88
CUDA_MODULE_LOADING set to :
GPU models and configuration : GPU 0: NVIDIA RTX PRO 6000 Blackwell Server Edition
Nvidia driver version : 595.58.03
cuDNN version : Could not collect
HIP runtime version : N/A
MIOpen runtime version : N/A
Is XNNPACK available : True
==============================
CPU Info
==============================
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 46 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 8
On-line CPU(s) list: 0-7
Vendor ID: GenuineIntel
Model name: Intel(R) Xeon(R) Platinum 8559C
CPU family: 6
Model: 207
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
Stepping: 2
BogoMIPS: 4800.00
Hypervisor vendor: KVM
Virtualization type: full
==============================
Versions of relevant libraries
==============================
[pip3] flashinfer-python==0.6.8.post1
[pip3] numpy==2.2.6
[pip3] nvidia-cublas==13.1.0.3
[pip3] nvidia-cuda-cupti==13.0.85
[pip3] nvidia-cuda-nvrtc==13.0.88
[pip3] nvidia-cuda-runtime==13.0.96
[pip3] nvidia-cudnn-cu13==9.19.0.56
[pip3] nvidia-cudnn-frontend==1.18.0
[pip3] nvidia-cufft==12.0.0.61
[pip3] nvidia-cufile==1.15.1.6
[pip3] nvidia-curand==10.4.0.35
[pip3] nvidia-cusolver==12.0.4.66
[pip3] nvidia-cusparse==12.6.3.3
[pip3] nvidia-cusparselt-cu13==0.8.0
[pip3] nvidia-cutlass-dsl==4.4.2
[pip3] nvidia-cutlass-dsl-libs-base==4.4.2
[pip3] nvidia-ml-py==13.595.45
[pip3] nvidia-nccl-cu13==2.28.9
[pip3] nvidia-nvjitlink==13.0.88
[pip3] nvidia-nvshmem-cu13==3.4.5
[pip3] nvidia-nvtx==13.0.85
[pip3] pyzmq==27.1.0
[pip3] torch==2.11.0+cu130
[pip3] torch_c_dlpack_ext==0.1.5
[pip3] torchaudio==2.11.0+cu130
[pip3] torchvision==0.26.0+cu130
[pip3] transformers==5.8.0
[pip3] triton==3.6.0
[conda] Could not collect
==============================
vLLM Info
==============================
ROCM Version : Could not collect
vLLM Version : 0.20.2rc1.dev49+g9b4e83934 (git sha: 9b4e83934)
vLLM Build Flags:
CUDA Archs: 7.5 8.0 8.6 8.9 9.0 10.0 12.0+PTX; ROCm: Disabled; XPU: Disabled
==============================
Environment Variables
==============================
CUDA_VERSION=13.0.2
NVIDIA_VISIBLE_DEVICES=all
NVIDIA_DRIVER_CAPABILITIES=compute,utility
VLLM_ENABLE_CUDA_COMPATIBILITY=0
TORCH_CUDA_ARCH_LIST=7.5 8.0 8.6 8.9 9.0 10.0 12.0+PTX
VLLM_USAGE_SOURCE=production-docker-image
PYTORCH_NVML_BASED_CUDA_CHECK=1
TORCHINDUCTOR_COMPILE_THREADS=1
TORCHINDUCTOR_CACHE_DIR=/tmp/torchinductor_root
🐛 Describe the bug
I hit a streaming tool-calling corruption bug with Gemma4 when MTP speculative decoding is enabled.
Setup:
- Docker image:
vllm/vllm-openai:gemma4-0505-cu130
- vLLM version reported by the server:
0.20.2rc1.dev49+g9b4e83934
- model:
google/gemma-4-31B-it
- server args include:
--enable-auto-tool-choice
--tool-call-parser gemma4
--reasoning-parser gemma4
--speculative-config '{"model": "google/gemma-4-31B-it-assistant", "num_speculative_tokens": 4}'
Observed behavior:
- with
stream=true, when the model emits two tool calls in one turn, the first tool call's arguments are corrupted / dropped in the streamed deltas
- with
stream=false, the same request returns the correct final two tool calls
- if I restart the same server without
--speculative-config, the streaming output becomes correct across repeated runs
So this appears to be a speculative-decoding + streaming + multi-tool-call interaction, not a base-model issue.
I searched and found related issues, but this one seems distinct:
Minimal repro
Server launch (the important part is the speculative config + gemma4 parser):
vllm serve google/gemma-4-31B-it \
--tensor-parallel-size 1 \
--served-model-name gemma-4-31B-it \
--host 0.0.0.0 \
--max-model-len 185000 \
--max-num-batched-tokens 8192 \
--gpu-memory-utilization 0.95 \
--limit-mm-per-prompt '{"image": 0}' \
--enable-prefix-caching \
--enable-prompt-tokens-details \
--enable-auto-tool-choice \
--tool-call-parser gemma4 \
--reasoning-parser gemma4 \
--default-chat-template-kwargs '{"enable_thinking": false}' \
--chat-template examples/tool_chat_template_gemma4.jinja \
--speculative-config '{"model": "google/gemma-4-31B-it-assistant", "num_speculative_tokens": 4}'
Repro request:
curl -N http://localhost:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <REDACTED>' \
-d '{
"model": "gemma-4-31B-it",
"temperature": 0,
"stream": true,
"messages": [
{
"role": "user",
"content": "Cerco i treni disponibili da Milano a Piacenza per oggi"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "getStationInfo",
"description": "Get station info for a city",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"}
},
"required": ["location"]
}
}
}
]
}'
Broken stream=true output with speculative decoding enabled
I reproduced this repeatedly. Example streamed deltas:
[
{
"id": "chatcmpl-tool-95240dbe1f928781",
"type": "function",
"index": 0,
"function": {"name": "getStationInfo", "arguments": ""}
},
{
"id": "chatcmpl-tool-93dace5003b7bfb3",
"type": "function",
"index": 1,
"function": {"name": "getStationInfo", "arguments": ""}
},
{
"index": 1,
"function": {"arguments": "{\"location\": \"Piacenza\"}"}
}
]
The first call (Milano) never receives any argument delta.
Correct stream=false output for the same request
[
{
"id": "chatcmpl-tool-a05437a71d9cce91",
"type": "function",
"function": {
"name": "getStationInfo",
"arguments": "{\"location\": \"Milano\"}"
}
},
{
"id": "chatcmpl-tool-a534c6879f562a1b",
"type": "function",
"function": {
"name": "getStationInfo",
"arguments": "{\"location\": \"Piacenza\"}"
}
}
]
Strong signal that speculative decoding is the trigger
I restarted the same server with the same args except without --speculative-config and reran the same streaming request 5 times.
In all 5 runs, both tool calls streamed correctly, e.g.:
[
{"id": "chatcmpl-tool-86d52864723d5ec0", "type": "function", "index": 0, "function": {"name": "getStationInfo", "arguments": ""}},
{"index": 0, "function": {"arguments": "{\"location\": "}},
{"index": 0, "function": {"arguments": "\"Mil"}},
{"index": 0, "function": {"arguments": "ano"}},
{"index": 0, "function": {"arguments": "\"}"}},
{"id": "chatcmpl-tool-a85ab10d70df516f", "type": "function", "index": 1, "function": {"name": "getStationInfo", "arguments": ""}},
{"index": 1, "function": {"arguments": "{\"location\": "}},
{"index": 1, "function": {"arguments": "\"P"}},
{"index": 1, "function": {"arguments": "iac"}},
{"index": 1, "function": {"arguments": "enza"}},
{"index": 1, "function": {"arguments": "\"}"}}
]
Notes
parallel_tool_calls=false is not a useful workaround here. In my testing it just reduces the result to a single tool call, and in streaming mode I only got the empty header delta.
- Single-tool streaming worked for me even with speculative decoding enabled. The corruption showed up when the model emitted multiple tool calls in one assistant turn.
If helpful, I can also provide the exact raw SSE chunks from both the broken (speculative enabled) and working (speculative disabled) runs.
The output of
python collect_env.py🐛 Describe the bug
I hit a streaming tool-calling corruption bug with Gemma4 when MTP speculative decoding is enabled.
Setup:
vllm/vllm-openai:gemma4-0505-cu1300.20.2rc1.dev49+g9b4e83934google/gemma-4-31B-it--enable-auto-tool-choice--tool-call-parser gemma4--reasoning-parser gemma4--speculative-config '{"model": "google/gemma-4-31B-it-assistant", "num_speculative_tokens": 4}'Observed behavior:
stream=true, when the model emits two tool calls in one turn, the first tool call's arguments are corrupted / dropped in the streamed deltasstream=false, the same request returns the correct final two tool calls--speculative-config, the streaming output becomes correct across repeated runsSo this appears to be a speculative-decoding + streaming + multi-tool-call interaction, not a base-model issue.
I searched and found related issues, but this one seems distinct:
tool_choice=required+ speculative decoding + failed tool calls)Minimal repro
Server launch (the important part is the speculative config + gemma4 parser):
Repro request:
Broken
stream=trueoutput with speculative decoding enabledI reproduced this repeatedly. Example streamed deltas:
[ { "id": "chatcmpl-tool-95240dbe1f928781", "type": "function", "index": 0, "function": {"name": "getStationInfo", "arguments": ""} }, { "id": "chatcmpl-tool-93dace5003b7bfb3", "type": "function", "index": 1, "function": {"name": "getStationInfo", "arguments": ""} }, { "index": 1, "function": {"arguments": "{\"location\": \"Piacenza\"}"} } ]The first call (
Milano) never receives any argument delta.Correct
stream=falseoutput for the same request[ { "id": "chatcmpl-tool-a05437a71d9cce91", "type": "function", "function": { "name": "getStationInfo", "arguments": "{\"location\": \"Milano\"}" } }, { "id": "chatcmpl-tool-a534c6879f562a1b", "type": "function", "function": { "name": "getStationInfo", "arguments": "{\"location\": \"Piacenza\"}" } } ]Strong signal that speculative decoding is the trigger
I restarted the same server with the same args except without
--speculative-configand reran the same streaming request 5 times.In all 5 runs, both tool calls streamed correctly, e.g.:
[ {"id": "chatcmpl-tool-86d52864723d5ec0", "type": "function", "index": 0, "function": {"name": "getStationInfo", "arguments": ""}}, {"index": 0, "function": {"arguments": "{\"location\": "}}, {"index": 0, "function": {"arguments": "\"Mil"}}, {"index": 0, "function": {"arguments": "ano"}}, {"index": 0, "function": {"arguments": "\"}"}}, {"id": "chatcmpl-tool-a85ab10d70df516f", "type": "function", "index": 1, "function": {"name": "getStationInfo", "arguments": ""}}, {"index": 1, "function": {"arguments": "{\"location\": "}}, {"index": 1, "function": {"arguments": "\"P"}}, {"index": 1, "function": {"arguments": "iac"}}, {"index": 1, "function": {"arguments": "enza"}}, {"index": 1, "function": {"arguments": "\"}"}} ]Notes
parallel_tool_calls=falseis not a useful workaround here. In my testing it just reduces the result to a single tool call, and in streaming mode I only got the empty header delta.If helpful, I can also provide the exact raw SSE chunks from both the broken (speculative enabled) and working (speculative disabled) runs.