Skip to content

Studio: serialize non-streaming responses once and pool the proxy client#6393

Merged
danielhanchen merged 5 commits into
mainfrom
studio-api-perf-serialization-pooling
Jun 18, 2026
Merged

Studio: serialize non-streaming responses once and pool the proxy client#6393
danielhanchen merged 5 commits into
mainfrom
studio-api-perf-serialization-pooling

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

Summary

Two safe latency wins on the OpenAI/Anthropic-compatible endpoints. Both leave the streaming generation paths untouched, which keep Connection: close and max_keepalive_connections=0 so a client disconnect still tears down the upstream socket and stops GPU decode (#5749).

Changes

  1. Non-streaming responses used JSONResponse(content=model.model_dump()), which builds a dict and then re-runs json.dumps. They now serialize once with model.model_dump_json() via a small _model_json_response helper. The body is byte-identical (nulls preserved).

  2. The non-streaming completions and embeddings proxies built a fresh httpx.AsyncClient per request. They now use one pooled client (core/inference/llama_http) closed on shutdown. Streaming generation keeps its own per-request close-only client.

Benchmark (local, indicative)

old new speedup
serialize ChatCompletion (median) 11.5 us 3.8 us 3.0x
POST to local llama-server (median) 5514 us 1093 us 5.0x

Testing

  • studio/backend/tests/test_api_perf_serialization.py: _model_json_response body decodes identically to the old JSONResponse(model_dump()) (nulls kept), correct media type and status.
  • Existing inference, anthropic, and tool-passthrough route tests pass unchanged.
  • Verified live: non-streaming chat completion and /v1/completions return valid responses through the new serialization and the pooled client.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces performance optimizations for non-streaming inference requests. It implements a shared, pooled httpx.AsyncClient for non-streaming proxy calls to the local llama-server, replacing per-request client creation and ensuring proper cleanup during application shutdown. Additionally, it introduces a helper function _model_json_response to serialize Pydantic models directly to JSON, avoiding the overhead of dictionary round-trips and double serialization in Starlette's JSONResponse. Unit tests are also added to verify the serialization behavior. I have no feedback to provide as there are no review comments.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Two safe latency wins on the OpenAI/Anthropic-compatible endpoints that leave
the streaming generation paths untouched (they keep Connection: close and
max_keepalive_connections=0 so a client disconnect still stops GPU decode).

1. Non-streaming responses used JSONResponse(content=model.model_dump()), which
   builds a dict and then re-runs json.dumps. Serialize once with
   model.model_dump_json() via a small _model_json_response helper. The body is
   byte-identical (nulls preserved), about 3x faster to encode in a microbench.

2. The non-streaming completions and embeddings proxies built a fresh
   httpx.AsyncClient per request. Route them through one pooled client
   (core/inference/llama_http) closed on shutdown; streaming generation keeps
   its own per-request close-only client. About 5x faster per call to the
   local llama-server in a microbench.

The existing API-monitor tests for the non-streaming completions, embeddings
and passthrough paths now patch nonstreaming_client instead of httpx.AsyncClient
to match the pooled client, so they stay deterministic.
@danielhanchen danielhanchen force-pushed the studio-api-perf-serialization-pooling branch from 53f3a1b to c0158f4 Compare June 17, 2026 01:44
danielhanchen and others added 2 commits June 17, 2026 11:35
Review follow-up on the shared httpx client. It was a single module-global
instance, which has two lifecycle problems the per-request client did not:

1. After aclose() in lifespan shutdown, nonstreaming_client() kept handing back
   the closed client, so a second lifespan in the same process (repeated
   TestClient, embedded restart) failed with "client has been closed".

2. An httpx client binds its transport to the loop it first runs on, so reuse
   from another loop could raise "Event loop is closed".

Hold one client per running loop in a WeakKeyDictionary, recreate when missing
or closed, and close all on shutdown. Single-loop production is unchanged.
@danielhanchen

Copy link
Copy Markdown
Member Author

Thanks for the review. Pushed a follow-up in 7aa8995.

The shared client is now held per running event loop in a WeakKeyDictionary, recreated when missing or closed, and all are closed on shutdown. This fixes both lifecycle gaps the single global had: returning a closed client after a lifespan restart in the same process, and reusing a transport bound to a finished event loop. Single-loop production behavior is unchanged. Serialization parity was reconfirmed byte-identical. Tests cover same-loop reuse, recreate-after-close, and per-loop isolation.

@danielhanchen

Copy link
Copy Markdown
Member Author

/gemini review

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces performance optimizations for non-streaming inference requests. It implements a pooled, event-loop-scoped httpx.AsyncClient utility to reuse connections for non-streaming calls to the local llama-server, replacing per-request client instantiation. Additionally, it introduces a _model_json_response helper to serialize Pydantic responses directly using model_dump_json(), avoiding the overhead of double serialization in Starlette's JSONResponse. Corresponding unit tests and test mocks have been updated to support these changes. There are no review comments, so I have no feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 34878e2c1f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danielhanchen danielhanchen merged commit a35fbe2 into main Jun 18, 2026
27 of 33 checks passed
@danielhanchen danielhanchen deleted the studio-api-perf-serialization-pooling branch June 18, 2026 05:38
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