Skip to content

/metrics is dead on the production server: AsyncLLM never folds IterationStats into the logger #277

Description

@localai-bot

/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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions