vllm - ✅(Solved) Fix Test Failure: test_run_eagle_dp[FLASH_ATTN] produces non-deterministic outputs with EAGLE speculative decoding [3 pull requests, 5 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vllm-project/vllm#38234Fetched 2026-04-08 01:37:08
View on GitHub
Comments
5
Participants
2
Timeline
9
Reactions
0
Author
Timeline (top)
commented ×5cross-referenced ×3closed ×1

The distributed EAGLE data parallel test tests/v1/distributed/test_eagle_dp.py::test_run_eagle_dp[FLASH_ATTN] is failing because the token outputs generated with EAGLE speculative decoding differ from those generated without it, despite using temperature=0 for deterministic sampling.

Root Cause

The distributed EAGLE data parallel test tests/v1/distributed/test_eagle_dp.py::test_run_eagle_dp[FLASH_ATTN] is failing because the token outputs generated with EAGLE speculative decoding differ from those generated without it, despite using temperature=0 for deterministic sampling.

Fix Action

Fixed

PR fix notes

PR #37460: [Core][Metrics][BugFix] Replace num_cached_tokens/num_external_computed_tokens with PrefillStats

Description (problem / solution / changelog)

Related to the discussion in #36859 and the Counters can only be incremented by non-negative amounts error with the vllm:prompt_tokens_by_source_total metric.

In OutputProcessor, we take the first EngineCoreOutput as a signal that prefill has completed, and record certain statistics about it.

On the scheduler side, because of preemption, we might have prefills that are scheduled but never completed, or we might need to recompute an already completed prefill. To add clarity, we use PrefillStats to track these stats until the first prefill is completed, return this to the frontend via EngineCoreOutput, and then stop tracking PrefillStats.

num_cached_tokens was previously used for KV transfer failure recovery, but this is no longer true as of #38096.

Changed files

  • tests/v1/core/test_async_scheduler.py (modified, +18/-11)
  • tests/v1/engine/test_output_processor.py (modified, +10/-4)
  • tests/v1/engine/utils.py (modified, +27/-6)
  • tests/v1/kv_connector/unit/test_invalid_blocks_correctness.py (modified, +59/-0)
  • tests/v1/metrics/test_stats.py (modified, +62/-29)
  • vllm/v1/core/sched/scheduler.py (modified, +16/-12)
  • vllm/v1/engine/__init__.py (modified, +4/-5)
  • vllm/v1/engine/output_processor.py (modified, +7/-3)
  • vllm/v1/metrics/stats.py (modified, +66/-24)
  • vllm/v1/request.py (modified, +14/-5)

PR #34789: [Bugfix] Offload blocking tokenizer ops to shared thread pool to unblock event loop

Description (problem / solution / changelog)

Purpose

Fix event loop blocking caused by multimodal request preprocessing (base64 decoding, image transforms, HF processor operations) and chat template rendering. Under high concurrency, these synchronous CPU-bound operations block the asyncio event loop, causing /health, /v1/models, and /metrics endpoints to become unresponsive (P95 latency >200ms, with spikes over 1s).

Changes:

  • Add a shared ThreadPoolExecutor on BaseRenderer (size controlled by --preprocessing-thread-pool-workers, default 1)
  • Always offload multimodal preprocessing to the shared thread pool to keep the event loop responsive
  • Wrap chat template rendering in HfRenderer, MistralRenderer, DeepseekV32Renderer, and Grok2Renderer with the shared executor via make_async
  • Consolidate MistralRenderer's separate ThreadPoolExecutor into the shared one
  • Serialize clear_mm_cache through the shared executor to avoid races with concurrent process_inputs on the mm_processor_cache

Test Plan

Benchmarked on 1x NVIDIA A100-SXM4-80GB using vllm bench serve with --request-rate 20 --num-prompts 200 and a custom high-concurrency benchmark with PaddleOCR-VL-1.5.

Tests performed:

  1. vllm bench serve with Llama-3.1-8B-Instruct (text-only, --request-rate 20 --num-prompts 200)
  2. vllm bench serve with Qwen2.5-VL-7B-Instruct (multimodal, --request-rate 20 --num-prompts 200)
  3. Custom high-concurrency benchmark with PaddleOCR-VL-1.5 (500 real OmniDocBench images, 300 concurrency)
  4. --preprocessing-thread-pool-workers comparison (1 vs 2 vs 4) with PaddleOCR-VL-1.5

Test Results

1. Text-Only (meta-llama/Llama-3.1-8B-Instruct)

vllm bench serve --request-rate 20 --num-prompts 200

MetricThis PRMainDiff
Throughput (req/s)14.7714.80-0.2%
Output tok/s1,889.971,893.27-0.2%
Mean TTFT (ms)278.02273.71+1.6%
P99 TTFT (ms)518.97525.17-1.2%
Mean TPOT (ms)54.1453.79+0.7%

No regression. All metrics within noise.

2. Multimodal (Qwen/Qwen2.5-VL-7B-Instruct)

vllm bench serve --request-rate 20 --num-prompts 200 --backend openai-chat --dataset-name random-mm

MetricThis PRMainDiff
Throughput (req/s)6.746.73+0.1%
Output tok/s862.46861.79+0.1%
Mean TTFT (ms)7,489.437,483.03+0.1%
P99 TTFT (ms)17,005.2816,974.36+0.2%
Mean TPOT (ms)125.91126.07-0.1%

No regression. All metrics within noise.

3. PaddleOCR-VL-1.5 High Concurrency (500 prompts, 300 concurrency)

Custom benchmark with real OmniDocBench document images. --max-num-batched-tokens 131072 --no-enable-prefix-caching --mm-processor-cache-gb 0 --gpu-memory-utilization 0.5

MetricThis PRMainDiff
Throughput (req/s)5.885.67+3.7%
Token throughput (tok/s)4,712.974,504.46+4.6%
TTFT mean (ms)31,117.7133,058.79-5.9%
TTFT P99 (ms)47,273.2251,613.86-8.4%
/health median (ms)0.70222.44318x better
/health P99 (ms)18.881,641.5387x better

Event loop stays fully responsive under high multimodal concurrency. The /health endpoint drops from 222ms to <1ms median.

4. --preprocessing-thread-pool-workers Comparison (PaddleOCR-VL-1.5, 500 prompts, 300 concurrency)

Metricworkers=1workers=2workers=4
Throughput (req/s)5.865.925.87
Token throughput (tok/s)4,624.784,559.034,554.05
TTFT mean (ms)31,31930,97431,294
TTFT P99 (ms)47,53647,08447,830
/health median (ms)0.670.620.67
/health P99 (ms)17.2017.4720.13

All worker counts perform identically. This is consistent with https://github.com/vllm-project/vllm/pull/34789#issuecomment-3055653700. The key improvement comes from offloading preprocessing off the event loop (so /health stays responsive), not from parallelizing it. Default of workers=1 is sufficient.

Summary

WhatResult
Event loop liveness (/health)318x improvement (222ms → 0.7ms median)
Request throughput (high concurrency)+3.7% (5.67 → 5.88 req/s)
TTFT (high concurrency)-5.9% (33.1s → 31.1s mean)
Text-only regressionNone (-0.2% throughput, within noise)
Multimodal regressionNone (+0.1% throughput, within noise)
"Already borrowed" errorsZero across all tests

<details> <summary> Essential Elements of an Effective PR Description Checklist </summary>
  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan, such as providing test command.
  • The test results, such as pasting the results comparison before and after, or e2e results
  • (Optional) The necessary documentation update, such as updating supported_models.md and examples for a new model.
  • (Optional) Release notes update. If your change is user facing, please update the release notes draft in the Google Doc.
</details>

Changed files

  • tests/entrypoints/openai/chat_completion/test_chat_error.py (modified, +1/-0)
  • tests/entrypoints/openai/chat_completion/test_serving_chat.py (modified, +1/-0)
  • tests/entrypoints/openai/completion/test_completion_error.py (modified, +1/-0)
  • tests/entrypoints/openai/completion/test_lora_resolvers.py (modified, +1/-0)
  • tests/renderers/test_completions.py (modified, +1/-0)
  • tests/renderers/test_mistral.py (modified, +1/-0)
  • vllm/config/model.py (modified, +4/-0)
  • vllm/engine/arg_utils.py (modified, +6/-0)
  • vllm/renderers/base.py (modified, +118/-6)
  • vllm/renderers/deepseek_v32.py (modified, +18/-4)
  • vllm/renderers/grok2.py (modified, +18/-4)
  • vllm/renderers/hf.py (modified, +20/-9)
  • vllm/renderers/mistral.py (modified, +1/-3)
  • vllm/utils/async_utils.py (modified, +3/-1)
  • vllm/v1/engine/async_llm.py (modified, +1/-1)
RAW_BUFFERClick to expand / collapse

Summary

The distributed EAGLE data parallel test tests/v1/distributed/test_eagle_dp.py::test_run_eagle_dp[FLASH_ATTN] is failing because the token outputs generated with EAGLE speculative decoding differ from those generated without it, despite using temperature=0 for deterministic sampling.

Test Details

  • Test file: tests/v1/distributed/test_eagle_dp.py::test_run_eagle_dp[FLASH_ATTN]
  • Date: 2026-03-25T06:22:37Z
  • Model: meta-llama/Llama-3.1-8B-Instruct (target) with yuhuili/EAGLE-LLaMA3.1-Instruct-8B (draft)
  • Configuration: DP_SIZE=2, data_parallel_backend="mp", VLLM_BATCH_INVARIANT=1
  • Attention backend: FLASH_ATTN

Expected Behavior

With temperature=0 and batch invariant mode enabled, both engine configurations (with and without EAGLE) should produce identical token sequences.

Actual Behavior

The generated token sequences completely diverge from the first token:

Without EAGLE: [323, 10344, 13, 578, 1296, 374, 311, 1629, 264, 4382, 13790, 31649, 1646, 389, 264, 10550, 315, 220, 1041, 15]

With EAGLE: [198, 2028, 374, 264, 1296, 315, 828, 15638, 449, 60989, 198, 2028, 374, 264, 1296, 315, 828, 15638, 449, 60989]

First token difference: 198 vs 323

Related Issues

https://github.com/vllm-project/vllm/issues/31913

extent analysis

Fix Plan

To fix the issue, we need to ensure that the EAGLE speculative decoding is deterministic when temperature=0. We can achieve this by setting the random seed for the EAGLE model.

Steps to Fix

  • Set the random seed for the EAGLE model using the torch.manual_seed() function.
  • Ensure that the torch.manual_seed() function is called before the EAGLE model is initialized.

Example Code

import torch

# Set the random seed
torch.manual_seed(42)

# Initialize the EAGLE model
model = EAGLEModel(...)

# Use the model for speculative decoding
output = model.generate(..., temperature=0)

Alternatively, you can also set the random seed using the random.seed() function from the random module.

import random

# Set the random seed
random.seed(42)

# Initialize the EAGLE model
model = EAGLEModel(...)

# Use the model for speculative decoding
output = model.generate(..., temperature=0)

Verification

To verify that the fix worked, run the test_run_eagle_dp[FLASH_ATTN] test again and check that the generated token sequences are identical with and without EAGLE.

Extra Tips

  • Make sure to set the random seed before initializing the EAGLE model to ensure deterministic behavior.
  • You can use a fixed random seed value, such as 42, to ensure reproducibility.
  • If you are using a distributed training setup, make sure to set the random seed on each worker process to ensure consistent results.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING