Skip to content

[tinker] Encode forwarded sample results to proto once and serve them as-is - #15

Closed
avigyabb wants to merge 2 commits into
avi/stack-4-server-knobsfrom
avi/stack-5-proto-once
Closed

[tinker] Encode forwarded sample results to proto once and serve them as-is#15
avigyabb wants to merge 2 commits into
avi/stack-4-server-knobsfrom
avi/stack-5-proto-once

Conversation

@avigyabb

@avigyabb avigyabb commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Stack 5/7. Long-output rollouts made per-result payload work the API server's main cost.

Before. For a 32k-token result (356 KB JSON) the forwarding path spent 4 ms (orjson decode, pydantic validate, pydantic JSON dump) and the proto path the SDK >= 0.25 uses on retrieve_future spent another 7 ms (stdlib json.loads + proto build) inside a single global lock, capping proto delivery near 150 results/s regardless of concurrency. The proto build holds the GIL, so the thread hop bought nothing.

Change.

  • The forwarding client decodes the vLLM body once and encodes straight to SampleResponse wire bytes (serialize_sample_output, shared with the validated path and pinned to it byte-for-byte by tests). No pydantic model or JSON text is built for the result.
  • ExternalFutureStore keeps the proto bytes (8 bytes/token, 26% smaller than the JSON text); retrieve_future passes them through for proto clients and derives JSON lazily, cached, for pre-0.25 clients (sample_output_json_from_proto, float32 logprobs). Results stored as JSON (DB path, errors) keep the encode-in-thread path, now cached per entry.
  • Pending entries no longer retain the request body (never read back on this path; ~100 KB per entry for long prompts).
  • Store TTLs are EngineConfig fields (external_future_retrieved_ttl_sec, external_future_completed_ttl_sec): retention after delivery is the dominant memory term, roughly completion rate x result size x window.

Results (proto client, 2048 cap): 512 concurrent 32k-token results 7.5 s -> 3.5 s server CPU, 73 -> 167 results/s. 131072 requests with 8k-token results, 5 s engine queueing and SDK-style 45 s re-polls: 131072/131072, 0 failures, 267/s, 8.9 GB peak RSS. 32768 x 32k tokens: 32768/32768, 163/s, 9.6 GB.

Stack

  1. [tinker] Route uvicorn's access log to a plain handler instead of Rich #11
  2. [tinker] Forward samples with aiohttp instead of httpx #12
  3. [tinker] Keep an undelivered sample result alive for the SDK's retry #13
  4. [tinker] Survive completion bursts at the socket layer (accept backlog, keep-alive) #14
  5. [tinker] Encode forwarded sample results to proto once and serve them as-is #15
  6. [tinker] Decode vLLM completion bodies straight into numpy with pysimdjson #16
  7. [tinker] Load harness for the API server's sampling path at 131k concurrency #17

🤖 Generated with Claude Code

… as-is

Long-output rollouts made the per-result payload work the API server's main
cost. For a 32k-token result (356KB JSON) the forwarding path spent 4ms
(orjson decode, pydantic validate, pydantic JSON dump) and the proto path the
SDK >= 0.25 uses on retrieve_future spent another 7ms (stdlib json.loads plus
proto build) inside a single global lock, capping proto delivery near 150
results/s regardless of concurrency; the proto build holds the GIL, so the
thread hop bought nothing.

- The forwarding client decodes the vLLM body once and encodes straight to
  SampleResponse wire bytes (serialize_sample_output, shared with the validated
  path and pinned to it byte for byte by tests). No pydantic model or JSON text
  is built for the result.
- ExternalFutureStore keeps the proto bytes (8 bytes/token, 26% smaller than
  the JSON text); retrieve_future passes them through for proto clients and
  derives JSON lazily, cached, for pre-proto clients
  (sample_output_json_from_proto). Results stored as JSON (DB path, errors)
  keep the existing encode-in-thread path, now cached per entry.
- Pending entries no longer retain the request body (never read back on this
  path; ~100KB per entry for long prompts).
- Store TTLs are EngineConfig fields (external_future_retrieved_ttl_sec,
  external_future_completed_ttl_sec): retention after delivery is the
  dominant memory term, roughly completion rate x result size x window.

512 concurrent 32k-token results, proto client: server CPU 7.5s -> 3.5s,
73 -> 167 results/s. 131072 requests with 8k-token results, 2048-way engine
queueing and SDK-style 45s re-polls: 131072/131072, 0 failures, 267/s, peak
RSS 8.9GB. 32768 requests with 32k-token results: 32768/32768, 163/s, 9.6GB.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Avi Basnet <avigyabb@stanford.edu>
(cherry picked from commit ac48818)
Signed-off-by: Avi Basnet <avigyabb@stanford.edu>
@avigyabb
avigyabb force-pushed the avi/stack-4-server-knobs branch from 2f94455 to 27c838e Compare September 4, 2026 01:24
@avigyabb
avigyabb force-pushed the avi/stack-5-proto-once branch from a76b517 to 94b5591 Compare September 4, 2026 01:24
class ExternalFuture:
request_id: int
model_id: str | None
request_data: dict

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

we could retain a hashed version of this for idempotency, see - hershg#1

Removed comments explaining retention and population of result data in ExternalFuture class.
@avigyabb
avigyabb marked this pull request as ready for review September 4, 2026 18:03
@avigyabb

avigyabb commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

Moved upstream: NovaSky-AI#2164

@avigyabb avigyabb closed this Sep 5, 2026
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.

1 participant