Skip to content

[Bug]: Gemma4 + MTP speculative decoding drops first tool-call arguments in streaming multi-tool auto-tool-choice #41967

Description

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions