Skip to content

row/BENCH-35B-TTFT: TTFT deficit attributed to serving INTAKE (diag instrument, no lever yet) - #32

Merged
localai-bot merged 6 commits into
mainfrom
row/BENCH-35B-TTFT
Aug 5, 2026
Merged

row/BENCH-35B-TTFT: TTFT deficit attributed to serving INTAKE (diag instrument, no lever yet)#32
localai-bot merged 6 commits into
mainfrom
row/BENCH-35B-TTFT

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

Finding

The 35B TTFT deficit (0.93-0.98x, worsening with concurrency) decomposes server-side (c32, ms): INTAKE (arrival→QUEUED, engine-core admission) ours 612 vs vLLM 509 = +103ms and growing 20x from c2→c32; PREFILL +92ms flat (the known task-#61 glue residual); QUEUE (queued→scheduled) ours BETTER by 34ms. At c4 we WIN throughput while LOSING TTFT — TTFT is not slaved to drain rate. Kernels and scheduler are at/near parity.

Mechanism: EngineCoreProc::run_busy_loop (core_proc.cpp:54-63) drains the input queue once per step_with_batch_queue (core.cpp:115), so a newly-arrived request waits ~step-cadence, which grows with batch size. vLLM drains via EngineCoreProc._process_input_queue (core.py:1259-1298). Ruled out: TCP_NODELAY, tokenizer locks, egress, async-vs-sync scheduling (both async).

Contents (diagnostics + records only)

  • VT_TTFT_DUMP-gated per-request TTFT-split dump on the async path (byte-identical unset) — also surfaces a SERVE-RESPONSE-METRICS residual: production async serving passes iteration_stats=nullptr and never stamps EngineCoreOutputs.timestamp, so per-request timing is untracked there.
  • Full attribution in benchmark-record/state/NOW; VT_TTFT_DUMP documented in ENVIRONMENT.

Gates

SACRED test_qwen36_paged_engine PASS on the instrumented binary; agent-preflight exit 0; evidence dgx:~/work/q35-regrid/ttft-attr/.

Next (separate work)

Lever A: mirror vLLM's input-drain cadence in the busy loop (needs finer timing first, being pursued on a stacked branch).

🤖 Generated with Claude Code

https://claude.ai/code/session_014fZAcg1WcU8V629k6HWKys

mudler added 6 commits August 5, 2026 11:49
The async serving frontend calls OutputProcessor::process_outputs with
iteration_stats == nullptr (the production /metrics endpoint is deliberately
404, api_server.cpp:762-765), so the per-request queue/prefill/decode split
that stats.py exposes as vllm:request_{queue,prefill,decode}_time_seconds is
invisible on our serving path. This blocks the queue-vs-execution TTFT
attribution the 35B binding grid needs (TTFT is 2-6% worse than throughput at
c>=2 and worsens with concurrency; that split decides whether the lever is
scheduler admission, front-end overhead, or prefill execution).

VT_TTFT_DUMP=1 emits one "TTFTSPLIT rid=... queued=.. prefill=.. decode=..
e2e=.." stderr line per finished request, reconstructed directly from the
event-populated req_state timestamps (identical arithmetic to stats.py:459-476,
which the async path already fills unconditionally since log_stats_ defaults
true). Reads only; generation is byte-identical when the env var is unset
(one function-local static getenv, no behaviour change on the compute path).

FOLLOWING_AGENTS_PROTOCOL
Assisted-by: Claude Code:claude-opus-4-8 [ClaudeCode]
Empirically, the async serving frontend produced all-zero TTFT-split rows
(queued=prefill=decode=0, e2e negative): two gaps, both the SERVE-RESPONSE-
METRICS residual. (1) EngineCore::step_with_batch_queue never stamps
engine_core_outputs.timestamp (the synchronous step() does, core.cpp:85), so
engine_core_timestamp is 0 and first_token_ts/last_token_ts/TTFT are wrong.
(2) AsyncLLM::RunOutputHandler passes iteration_stats=nullptr to
process_outputs, which gates the ENTIRE per-request timing block (event folding
that sets queued_ts/scheduled_ts, first_token_ts, last_token_ts,
num_generation_tokens — output_processor.cpp:379-426), so req_state timing is
never populated on the serving path.

Both are fixed only under VT_TTFT_DUMP (function-local static getenv): the async
timestamp stamp and a throwaway IterationStats so the timing block runs and the
dump reads real intervals. Unset = instruction-identical to production (nullptr
path, no stamp); generation is byte-identical either way. This is a diagnostic
enabler; the durable fix is to wire the async /metrics stat logger (a separate
observability feature row).

FOLLOWING_AGENTS_PROTOCOL
Assisted-by: Claude Code:claude-opus-4-8 [ClaudeCode]
… dump

Localizes the serving-layer frontend cost: intake = queued_ts - arrival_time is
the async engine-core intake wait (OutputProcessor registration to the
scheduler QUEUED event, same steady clock), isolating the input-queue-drain /
step-cadence ingress cost from tokenize (before arrival_time) and egress. Same
VT_TTFT_DUMP gate; reads only.

FOLLOWING_AGENTS_PROTOCOL
Assisted-by: Claude Code:claude-opus-4-8 [ClaudeCode]
Measured same-tool (vllm bench serve + /metrics vs a VT_TTFT_DUMP per-request
split) on the 35B online-serve workload. The concurrency-growing TTFT deficit
(TTFT ratio c2 0.95 -> c32 0.92) is NOT the prefill kernels or the scheduler
queue (both at/near parity) but the async engine-core INTAKE (arrival->QUEUED
admission latency), +34..103ms and growing 20x c2->c32, plus a ~flat prefill
glue gap (+82..92ms = the task #61 residual). Both engines run async scheduling
(async-vs-sync refuted). Appended benchmark-record + anchored state entry;
refreshed NOW (in budget). Evidence dgx:~/work/q35-regrid/ttft-attr/.

FOLLOWING_AGENTS_PROTOCOL
Assisted-by: Claude Code:claude-opus-4-8 [ClaudeCode]
New read-only observability env var introduced by the async TTFT-split
instrument. Documented under the Diagnostic section (generation byte-identical
when unset). Satisfies check-env-doc.

FOLLOWING_AGENTS_PROTOCOL
Assisted-by: Claude Code:claude-opus-4-8 [ClaudeCode]
# Conflicts:
#	.agents/NOW.md
#	.agents/benchmark-record.md
#	.agents/state.md
@localai-bot
localai-bot merged commit 19fb744 into main Aug 5, 2026
6 of 9 checks passed
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.

2 participants