Skip to content

server: emit GPU ttft/generation/decode_tps in stream usage - #570

Closed
ai-hpc wants to merge 1 commit into
mainfrom
feat/stream-usage-gpu-timings
Closed

server: emit GPU ttft/generation/decode_tps in stream usage#570
ai-hpc wants to merge 1 commit into
mainfrom
feat/stream-usage-gpu-timings

Conversation

@ai-hpc

@ai-hpc ai-hpc commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Measure GPU decode on the continuous-batch worker around forward_token (excluding SSE/network backpressure).
  • Expose ttft_ms, generation_ms, and decode_tps on the final streaming usage chunk so UIs (e.g. sparkinfer-web) can show accurate tok/s.
  • Additive OpenAI-compatible fields only; existing prompt_tokens / completion_tokens / total_tokens unchanged.

Pipeline impact

  • Safe for existing clients — extra JSON fields are ignored by OpenAI SDKs and current bench scripts (they compute their own client-side decode_tps).
  • No change to scheduling, KV, sampling, eval bots, or native bench binaries.
  • Negligible overhead — one steady_clock sample around each decode forward.
  • Left kMaxOutputTokens at the upstream default (4096); deployment max-out remains an init/CLI concern.

Test plan

  • Rebuild sparkinfer_server and stream a short chat; confirm usage includes decode_tps / ttft_ms / generation_ms
  • Smoke existing API benches still parse usage (bench_api_vs_native.py)
  • Confirm non-stream /v1/chat/completions still returns standard usage
  • sparkinfer-web chat shows non-zero GPU tok/s

Measure decode on the batch-engine worker around forward_token (excluding SSE
backpressure) and expose timings on the final chat.completion usage chunk so
UIs can show accurate tok/s without client-side wall-clock math.
@ai-hpc
ai-hpc force-pushed the feat/stream-usage-gpu-timings branch from 71ba1e3 to c10c9a9 Compare July 21, 2026 16:44

@skyrocket2026 skyrocket2026 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review

Nice additive change — streaming usage with ttft_ms / generation_ms / decode_tps is the right shape for the chat UI, and measuring around forward_token (before on_token) correctly excludes SSE backpressure.

Blocker: last_timing_ is racy under continuous batching

ModelEngine::complete_streaming writes last_timing_ after the job finishes, then sparkinfer_server reads engine.last_timing() on a later line. httplib serves concurrent requests and the batch engine interleaves them, so this can happen:

  1. Request A finishes → sets last_timing_
  2. Request B finishes → overwrites last_timing_
  3. Request A’s handler reads last_timing()B’s timings

Worse: if the stolen values are >= 0, the per-request wall-clock fallback in sparkinfer_server.cpp is skipped, so the UI can show another session’s tok/s.

last_timing() also returns an unlocked reference (unlike last_error() which takes mu_).

Suggested fix: return timings with the completion result (extend the return of complete_streaming, or an out-param / pair), and use that local value in the stream handler. Avoid a process-global “last timing” side channel.

Non-blocking notes

  • Non-stream /v1/chat/completions still emits standard usage only (fine if intentional).
  • Single-token completions fall back to wall-clock decode_tps (decode_forwards == 0); OK.
  • Prefill seed token correctly excluded from decode_forwards (decode tok/s after first token).

Please fix the timing handoff, then this looks good to merge.

@skyrocket2026 skyrocket2026 added the area:runtime subsystem (emission weight 0.26) label Jul 21, 2026
@skyrocket2026 skyrocket2026 reopened this Jul 21, 2026
@skyrocket2026 skyrocket2026 added the hold Maintainer override: never auto-merge this PR label Jul 21, 2026
@skyrocket2026

Copy link
Copy Markdown
Member

Maintainer hold: non-speed runtime/server change — keep open; do not auto-close for missing RTX 5090 greenlight.

@skyrocket2026

Copy link
Copy Markdown
Member

Closed — RTX 5090 checkbox not ticked

This PR was auto-closed because the template includes Tested on RTX 5090 as - [ ] (unchecked). Evaluation is opt-in — tick the box only after a real 5090 run.

To submit for review:

  1. Edit this PR description (you can edit while closed): change to - [x] Tested on RTX 5090
  2. Fill the decode and/or prefill before → after tables with real bench/scripts/bench.sh numbers showing improvement
  3. Reopen this PR

If this PR does not need GPU eval (e.g. docs-only), remove the proof-of-speedup section from the description instead of leaving an unchecked box.

CONTRIBUTING.md

Automated by eval bot / rtx5090-required CI.

@skyrocket2026

Copy link
Copy Markdown
Member

closed

@skyrocket2026

Copy link
Copy Markdown
Member

Reopened + hold. This is a non-speed runtime/server PR — do not auto-close for missing RTX 5090 greenlight. Eval bot incorrectly closed it despite hold; fixing that path.

skyrocket2026 added a commit that referenced this pull request Aug 11, 2026
…shutdown, metrics, live metadata, capacity

Closes the production-readiness gaps identified against an OpenRouter-style
provider checklist:

- Request cancellation: streaming on_token now returns bool; a disconnected
  client (DataSink::is_writable() goes false) stops generation immediately
  instead of running to completion for nobody.
- Overload handling: SPARKINFER_MAX_QUEUE_DEPTH admission cap returns 429
  before any KV allocation is attempted, instead of failing later.
- Per-request timeout: SPARKINFER_REQUEST_TIMEOUT_S (default disabled --
  measured ~90s TTFT at 32k context, so no safe aggressive default exists)
  returns 504 when exceeded.
- Graceful shutdown: SIGTERM/SIGINT stop accepting new work and let in-flight
  requests drain, bounded by SPARKINFER_SHUTDOWN_GRACE_S (default 30s) since
  svr.stop() only closes the listening socket -- a client that vanishes
  without a clean TCP close can otherwise block the drain indefinitely.
- GET /metrics (Prometheus text format): request/error counters by outcome,
  token totals, active requests, free KV blocks, uptime.
- GET /v1/capacity: live occupancy for orchestrator/load-balancer polling.
- /v1/models, /v1/info, /v1/tokenize now report live engine max_seq instead
  of hardcoded constants.
- TTFT/generation_ms/decode_tps on the OpenAI usage object (additive fields,
  ignored by standard SDKs) -- ports PR #570's idea but rebuilt on current
  main's by-value Result/CompletionResult pattern; #570 predates and
  conflicts with the concurrent-error-bleed fix in #740.

Found and fixed a real pre-existing concurrency bug while load-testing the
overload path: ModelEngine::complete_streaming called
model_->clear_prefix_cache() for every non-prefix-matching request without
checking exclusivity, but clear_prefix_cache() frees whatever session is
currently active on the shared Qwen35Model -- and the continuous-batch
worker thread mutates that same active-session state from its own thread
independent of ModelEngine's mutex. Reproduced directly: concurrent requests
without prefix caching configured (the common case) could free an unrelated
in-flight request's KV blocks mid-decode, corrupting the KV cache and
poisoning the CUDA context ("illegal memory access", every subsequent call
failing). Fixed by gating the clear with the same prefix_exclusive check the
"use prefix" branch already had.

Verified end-to-end on RTX 5090: full build, ctest 10/10, and a live server
smoke test covering streaming/non-streaming completions with real timing
fields, context-overflow 400, cancellation (metrics-confirmed), overload 429
under genuine concurrent load, timeout 504, and bounded graceful shutdown --
including reproducing and confirming the fix for the KV corruption bug under
the same concurrent load that first exposed it.
@skyrocket2026

Copy link
Copy Markdown
Member

Superseded by 3c1758a on main, which ships the same TTFT/generation_ms/decode_tps idea from this PR (plus request cancellation, overload/429, timeouts, graceful shutdown, /metrics, live /v1/info /v1/capacity) as part of closing out the server's production-readiness gaps.

This PR can't be merged as-is: it predates #740 (concurrent-error-bleed fix) and reintroduces the shared-mutable-last_error_/last_timing_ race that fix removed — ModelEngine now returns CompletionResult by value specifically so concurrent requests can't observe or clobber each other's state. It's also currently showing CONFLICTING against main.

Closing as superseded — thanks for the original idea, it's in main now (rebuilt on the current architecture).

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

Labels

area:runtime subsystem (emission weight 0.26) hold Maintainer override: never auto-merge this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants