/metrics was closed as the oldest T0 debt — but only on the SYNCHRONOUS engine.
The production HTTP server runs AsyncLLM, and that path never feeds the logger,
so a real deployment scrapes a catalog of series that never move.
Evidence
src/vllm/v1/engine/llm_engine.cpp:187-195 does the wiring 1:1 with
llm_engine.py:308-329 — builds IterationStats, threads a pointer through the
step, folds it into the logger:
// llm_engine.py:308 iteration_stats = IterationStats() if self.log_stats...
IterationStats iteration_stats;
IterationStats* iteration_stats_ptr = ...
// (+ fold IterationStats when logging).
src/vllm/v1/engine/async_llm.cpp has exactly one IterationStats, at :277-284,
and its own comment says what it is for:
// IterationStats so req_state timing is populated for the TTFT-split dump
IterationStats async_iteration_stats;
That is the VT_TTFT_DUMP diagnostic. Nothing folds it into a StatLogger.
grep -n "log_stats\|stat_logger\|LoggingStatLogger" src/vllm/v1/engine/async_llm.cpp
returns only that include and those two diagnostic lines.
Consequence
examples/server/main.cpp serves /metrics from the AsyncLLM stack. So on the
real server:
- gauges do not track the running batch,
vllm:* token counters stay at zero,
- the TTFT / ITL / e2e / TPOT histograms never observe,
vllm:request_{queue,prefill,inference}_time_seconds and
vllm:num_preemptions_total stay flat.
The catalog is exposed and correctly named (that part is gated by
test_prometheus_metrics), which makes this worse rather than better: a scraper
sees a well-formed endpoint and concludes the server is idle.
The sync-engine gate (test_llm_engine.cpp case 6, 44 asserts) passes precisely
because it drives LLMEngine — the path a deployment does not use.
Scope
Wire the AsyncLLM step the way LLMEngine is wired: build IterationStats per
step under log_stats, fold Scheduler::make_stats() →
EngineCoreOutputs.scheduler_stats and the OutputProcessor-built stats into the
logger at the AsyncLLM step site, and drive per-request timing from the
EngineCoreEvents already being drained. Reuse the sync path's seams rather than
adding a parallel one.
Out of scope: the config-gated metric families (spec-decode / kv-connector / mm /
LoRA), which are their own residual on the same row.
Row: SERVE-METRICS / SERVE-RESPONSE-METRICS — punch-list item 7
(ROAD-V1-C8) in
specs/roadmap-v1-completion.md,
whose residual names "AsyncLLM serving-path metric wiring" explicitly.
Gate: CPU reference backend, RED-first — drive the ASYNC stack, scrape /metrics,
and assert the same invariants the sync case 6 asserts (gauges track the batch,
token counters equal exact counts, histograms observe with non-zero sums).
/metricswas closed as the oldest T0 debt — but only on the SYNCHRONOUS engine.The production HTTP server runs
AsyncLLM, and that path never feeds the logger,so a real deployment scrapes a catalog of series that never move.
Evidence
src/vllm/v1/engine/llm_engine.cpp:187-195does the wiring 1:1 withllm_engine.py:308-329— buildsIterationStats, threads a pointer through thestep, folds it into the logger:
src/vllm/v1/engine/async_llm.cpphas exactly oneIterationStats, at:277-284,and its own comment says what it is for:
// IterationStats so req_state timing is populated for the TTFT-split dump IterationStats async_iteration_stats;That is the
VT_TTFT_DUMPdiagnostic. Nothing folds it into aStatLogger.grep -n "log_stats\|stat_logger\|LoggingStatLogger" src/vllm/v1/engine/async_llm.cppreturns only that include and those two diagnostic lines.
Consequence
examples/server/main.cppserves/metricsfrom the AsyncLLM stack. So on thereal server:
vllm:*token counters stay at zero,vllm:request_{queue,prefill,inference}_time_secondsandvllm:num_preemptions_totalstay flat.The catalog is exposed and correctly named (that part is gated by
test_prometheus_metrics), which makes this worse rather than better: a scrapersees a well-formed endpoint and concludes the server is idle.
The sync-engine gate (
test_llm_engine.cppcase 6, 44 asserts) passes preciselybecause it drives
LLMEngine— the path a deployment does not use.Scope
Wire the AsyncLLM step the way
LLMEngineis wired: buildIterationStatsperstep under
log_stats, foldScheduler::make_stats()→EngineCoreOutputs.scheduler_statsand theOutputProcessor-built stats into thelogger at the AsyncLLM step site, and drive per-request timing from the
EngineCoreEventsalready being drained. Reuse the sync path's seams rather thanadding a parallel one.
Out of scope: the config-gated metric families (spec-decode / kv-connector / mm /
LoRA), which are their own residual on the same row.
Row:
SERVE-METRICS/SERVE-RESPONSE-METRICS— punch-list item 7(
ROAD-V1-C8) inspecs/roadmap-v1-completion.md,whose residual names "AsyncLLM serving-path metric wiring" explicitly.
Gate: CPU reference backend, RED-first — drive the ASYNC stack, scrape
/metrics,and assert the same invariants the sync case 6 asserts (gauges track the batch,
token counters equal exact counts, histograms observe with non-zero sums).