Skip to content

fix(vllm): handle removed EngineCoreOutput.num_cached_tokens on vLLM >= 0.20 - #20821

Open
uhmin99 wants to merge 3 commits into
mainfrom
hyunmin.yoo/MLOS-950-vllm-num-cached-tokens
Open

uhmin99 wants to merge 3 commits into
mainfrom
hyunmin.yoo/MLOS-950-vllm-num-cached-tokens

Conversation

@uhmin99

@uhmin99 uhmin99 commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Fixes MLOS-950. With vLLM >= 0.20, the first inference request fails with HTTP 500 and the vLLM engine shuts down:

File ".../ddtrace/contrib/internal/vllm/extractors.py", line 88, in extract_request_data
    num_cached_tokens=engine_core_output.num_cached_tokens,
AttributeError: 'EngineCoreOutput' object has no attribute 'num_cached_tokens'

Root cause: vLLM 0.20.0 removed EngineCoreOutput.num_cached_tokens and replaced it with prefill_stats: PrefillStats | None. PrefillStats.num_cached_tokens holds the value. The exception is raised in our OutputProcessor.process_outputs wrapper, which runs in vLLM's engine output loop, so it takes down the whole engine.

prefill_stats is only attached to a request's first emitted output (Request.take_prefill_stats() is one-shot). Swapping the attribute access alone would therefore report num_cached_tokens=0 for nearly every multi-token completion, because spans are built from the final output. vLLM itself copies the value onto RequestState.num_cached_tokens (present since 0.10.2), so we fall back to that.

Changes:

  • extractors.get_num_cached_tokens() resolves cached tokens in this order: legacy EngineCoreOutput.num_cached_tokens (< 0.20), then prefill_stats.num_cached_tokens (first output, >= 0.20), then RequestState.num_cached_tokens (later outputs).
  • traced_output_processor_process_outputs now catches and logs instrumentation errors before and after calling the wrapped function. A future upstream schema change then drops spans instead of crashing the customer's inference server.

Testing

  • New no-GPU unit tests:
    • tests/contrib/vllm/test_extractors.py: legacy attribute, prefill_stats, RequestState fallback, and extract_request_data on a >= 0.20-shaped output.
    • tests/contrib/vllm/test_vllm_patch.py: both wrapper helpers raising still return the original result.
  • Checked the extractor logic in a standalone script. I could not run the pytest venv locally (Docker unavailable).
  • Verified the upstream schema against vLLM v0.19.0, v0.20.0 and v0.26.0 source (vllm/v1/engine/__init__.py, output_processor.py, core/sched/scheduler.py).
  • Note: the llmobs::vllm suite is skip: true and GPU-gated in tests/llmobs/suitespec.yml, which is why CI didn't catch this regression.

Risks

Low. Behavior on vLLM < 0.20 is unchanged. The new try/except only affects the error path.

Additional Notes

Customer workaround until this ships: DD_TRACE_VLLM_ENABLED=false, or pin vLLM <= 0.19.

🤖 Generated with Claude Code

…>= 0.20 [MLOS-950]

vLLM 0.20 replaced EngineCoreOutput.num_cached_tokens with prefill_stats,
which is only attached to a request's first output. Reading the old
attribute raised AttributeError inside the OutputProcessor.process_outputs
wrapper, which shut down the vLLM engine.

Read cached tokens from the legacy attribute, prefill_stats, or the value
RequestState keeps across iterations, and guard the process_outputs wrapper
so instrumentation errors are logged instead of crashing the engine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Dependency direction analysis

⚠️ Existing dependency direction violations

There are 201 dependency direction violations that already exist on the base branch and have not been changed by this PR.

Show existing violations (showing 5 of 201 highest severity)
ddtrace.internal.tracemethods -×-> ddtrace.trace  (internal-core -> product:tracing, score=132)
ddtrace.llmobs._integrations.vertexai -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=130)
ddtrace.llmobs._telemetry -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=130)
ddtrace.internal.ci_visibility.git_client -×-> ddtrace.trace  (product:ci_visibility -> product:tracing, score=130)
ddtrace.llmobs._integrations.base -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=130)

To see all violations, download the layers-base.json and layers-pr.json artifacts from this CI job and run:

uv run --script scripts/import-analysis/layers.py compare layers-base.json layers-pr.json

@cit-pr-commenter-54b7da

Copy link
Copy Markdown

Circular import analysis

⚠️ Existing circular imports

There are 1 circular imports that already exist on the base branch and have not been changed by this PR.

ddtrace.errortracking._handled_exceptions.bytecode_injector -> ddtrace.errortracking._handled_exceptions.callbacks -> ddtrace.errortracking._handled_exceptions.collector -> ddtrace.errortracking._handled_exceptions.bytecode_reporting -> ddtrace.errortracking._handled_exceptions.bytecode_injector

@cit-pr-commenter-54b7da

Copy link
Copy Markdown

Codeowners resolved as

Resolved from the full PR diff against main using the target branch CODEOWNERS file.
CODEOWNERS team requests not listed below are not required by the current file set.

ddtrace/contrib/internal/vllm/extractors.py                             @DataDog/ml-observability
ddtrace/contrib/internal/vllm/patch.py                                  @DataDog/ml-observability
releasenotes/notes/fix-vllm-num-cached-tokens-attribute-error-95727019ead126bd.yaml  @DataDog/apm-python
tests/contrib/vllm/test_extractors.py                                   @DataDog/ml-observability
tests/contrib/vllm/test_vllm_patch.py                                   @DataDog/ml-observability

@uhmin99
uhmin99 marked this pull request as ready for review October 6, 2026 01:02
@uhmin99
uhmin99 requested review from a team as code owners October 6, 2026 01:02
@uhmin99
uhmin99 requested review from ZStriker19 and removed request for a team October 6, 2026 01:02
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-06T08:01:55.980205Z c36bfcc New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Comment on lines +176 to +181
try:
model_name = get_model_name(instance)
spans_data = _capture_request_states(instance, engine_core_outputs)
except Exception:
logger.warning("Failed to capture vLLM request state for tracing", exc_info=True)
return func(*args, **kwargs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need the try catch there and below ? This seems like over protecting as it seems the fix is above

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review! I'd like to keep both guards, for these reasons:

  1. The fix above only covers one attribute. The crash in MLOS-950 was raised from _capture_request_states, which nothing guards. This is the second time a vLLM release has broken this integration by changing internal structures (MLOS-854 was the processor → input_processor rename). The llmobs::vllm suite is skip: true and GPU-gated, so CI won't catch the next upstream change.
  2. The failure mode is unusually severe. This wrapper runs inside vLLM's engine output loop (AsyncLLM.output_handler). An exception there doesn't just fail one request; it shuts down the whole inference server, which is what the customer hit. Dropping spans with a warning seems much better than taking down production inference.
  3. The second block isn't redundant. BaseLLMIntegration.llmobs_set_tags already catches its own errors, but _create_finished_spans also calls create_span, extract_latency_metrics and set_latency_metrics, which aren't guarded. That's the same pattern other LLM integrations use (e.g. openai/_realtime.py).

Happy to narrow the scope (e.g. only guard _capture_request_states) if you feel strongly about it.

@KowalskiThomas KowalskiThomas left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants