[tinker] Survive completion bursts at the socket layer (accept backlog, keep-alive) - #2163
Conversation
uvicorn.run: backlog=SKYRL_HTTP_CONNECTION_LIMIT (50k; the effective value is capped by net.core.somaxconn, raise it to match) and timeout_keep_alive=75s. With 131072 outstanding samples and 212k-token results each 2048-result completion burst kept the event loop busy ~16s. uvicorn's 5s keep-alive then closed every idle client connection during the burst, every client reconnected at once, and the 2048-entry accept backlog overflowed, so the kernel refused 109k of 131072 requests. Connections now queue in the kernel while the loop is busy and idle ones survive a burst. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Avi Basnet <avigyabb@stanford.edu>
There was a problem hiding this comment.
Code Review
This pull request configures Uvicorn's connection backlog and keep-alive timeout to better handle connection bursts and prevent clients from reconnecting simultaneously. The review feedback suggests improving deployment flexibility by allowing both the keep-alive timeout and the backlog size to be configured independently via environment variables rather than being hardcoded or coupled to existing limits.
Confidence Score: 5/5The PR appears safe to merge with no concrete blocking or independently actionable non-blocking issue identified. The configured backlog uses the documented integer connection limit already applied to another Uvicorn server, while the longer keep-alive directly addresses the described reconnect storms without changing request correctness.
|
| Filename | Overview |
|---|---|
| skyrl/tinker/api.py | Adds documented Uvicorn backlog and keep-alive settings using an existing repository-wide connection-limit configuration. |
Reviews (1): Last reviewed commit: "[tinker] Survive completion bursts at th..." | Re-trigger Greptile
#2160) Stack 1/7. Independent of the rest. RichHandler renders each access-log record through a rich Table, about 1.5 ms of event-loop CPU per HTTP request. Under rollout load that was 63% of the Tinker API server's CPU: profiling a 4096-sample run showed 12.5 s of its 19.9 s of server CPU inside `rich.logging.emit`. With a plain `StreamHandler` for `uvicorn.access` the same run takes 9.7 s and server throughput goes from 337 to 568 samples/s. Startup and error logging keep the Rich handler. Measured with the load harness in stack 7/7 (`skyrl/benchmarks/load_test_tinker_sampling.py`). **Stack** (each PR retargets to `main` as the one below merges) 1. #2160 2. #2161 3. #2162 4. #2163 5. #2164 6. #2165 7. #2166 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Logging configuration only; no request handling, auth, or data path changes—access output format may look slightly plainer on stderr. > > **Overview** > **HTTP access logging** for the Tinker API (via `get_uvicorn_log_config`) no longer goes through `RichHandler`. A dedicated **`access`** `StreamHandler` on stderr uses the same text formatter, while **`uvicorn`** and **`uvicorn.error`** still use Rich for startup, errors, and tracebacks. > > This targets per-request access log volume: Rich’s table rendering was a major event-loop CPU cost under high QPS. Access lines should read the same format string but without Rich styling. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 254499a. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Signed-off-by: Avi Basnet <avigyabb@stanford.edu> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…2162) Stack 3/7. Fixes the 128x128 `404 Future not found` seen against j316chuck#18. **Chain.** The SDK polls `retrieve_future` with a 45 s client timeout and gives up; the result lands afterwards; the abandoned handler wakes, builds a response nobody receives (uvicorn drops the send to a dead client silently) and starts the short retrieved-TTL clock; the sweeper evicts the result 120 s later; the SDK's retry of the same request_id gets 404, which the SDK treats as fatal. **Fix.** Start the retrieved clock only if `request.is_disconnected()` is false, and raise the retrieved TTL to 300 s so it outlasts the SDK's worst-case re-poll gap (45 s timeout + up to 30 s backoff, twice). `tests/tinker/test_retrieve_future_lost_response.py` reproduces the chain under a real uvicorn socket with shortened TTLs; it fails on `main` and passes here. A second test checks a delivered result still expires on the short clock, so memory stays bounded. Alternative considered: j316chuck#19 drops the retrieved clock and keeps every result for 2048 s. That also fixes the 404 but retains ~35 minutes of results regardless of delivery; with long-output rollouts (hundreds of KB per result) that is tens of GB. Verified at scale: 131072 requests with 5 s engine queueing and 224k SDK-style abandoned polls completed with zero 404s. **Stack** (each PR retargets to `main` as the one below merges) 1. #2160 2. #2161 3. #2162 4. #2163 5. #2164 6. #2165 7. #2166 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes async polling/delivery semantics and HTTP server tuning on the hot `retrieve_future` path; behavior is covered by new integration tests but affects SDK retry reliability under load. > > **Overview** > Fixes fatal **`404 Future not found`** when the SDK abandons a long `retrieve_future` poll (45s client timeout) and retries the same `request_id` after the result is ready. > > **`retrieve_future`** now calls **`mark_retrieved`** (starting the post-delivery eviction clock) only when the client is still connected (`not await req.is_disconnected()`). If the handler finishes building a response after the client disconnected, the short retrieved TTL no longer starts, so the in-memory store keeps the result for a real retry. > > **`ExternalFutureStore`** raises **`_RETRIEVED_TTL_SECONDS`** from 120s to 300s so delivered results still get a grace window that covers worst-case SDK re-poll gaps (timeout + backoff, twice). > > **Uvicorn** startup sets **`timeout_keep_alive=75`** (vs 5s default) and **`backlog=SKYRL_HTTP_CONNECTION_LIMIT`** to reduce idle disconnects and accept-queue overflows during completion bursts. > > Adds **`test_retrieve_future_lost_response.py`** (real socket on Linux) plus test stubs for **`is_disconnected`** on existing API tests. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 36188da. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Signed-off-by: Avi Basnet <avigyabb@stanford.edu> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Stack 2/7. **Why.** httpcore's connection pool rescans every pooled connection and queued request on each event, so the forwarding client's per-request CPU grows with the number of in-flight samples: 14 ms at 512, 27 ms at 2048, 38 ms at 4096 (standalone benchmark against an instant fake router; the profile shows 27.5M `is_idle` calls for 512 requests). At 2048 in flight the API server needed 119 s to forward 2048 instant requests. aiohttp's connector is flat at 0.34 ms. **Change.** `SkyRLTrainInferenceForwardingClient` uses one aiohttp session: - connector limit = `forwarding_inference_max_connections` (0 = unlimited); `sock_read` = `forwarding_inference_timeout_sec`; no total deadline, so requests queued behind the engine never hit the old 300 s pool timeout; - 60 s connect timeout (a saturated router takes tens of seconds to accept; that is queueing, not failure) and Happy Eyeballs off: a wave of cancelled connects under uvloop produced ~800 `File descriptor N is used by transport` errors from aiohappyeyeballs' `sock_connect`, failing unrelated forwards; - connect-phase errors and 5xx rejections (`TransientInferenceError`) are retried once after refreshing the proxy URL; read failures stay final (#2118 stance) since vLLM may still be executing the request; - `forwarding_inference_timeout_sec` default 300 s -> 2048 s: with unlimited connections a large burst waits inside vLLM's queue and 128x128 bursts exceed 300 s there. **Results** (same harness and SDK-shaped client, upstream `main` vs this stack): 2048 small results unlimited connections 71.6 s / 29 per s -> 2.7 s / 747 per s; 32768 with 5 s engine queueing 218 s with 530 failures -> 89 s, 0 failures. Note: unlimited outbound connections cannot exceed ~28k (one source IP's ephemeral ports); set `--forwarding-inference-max-connections` near engine capacity for very large bursts. **Stack** (each PR retargets to `main` as the one below merges) 1. #2160 2. #2161 3. #2162 4. #2163 5. #2164 6. #2165 7. #2166 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches hot-path inference forwarding, connection/retry semantics, and future eviction timing under load; mis-tuned timeouts or disconnect handling could cause duplicate forwards or 404s on SDK retries. > > **Overview** > Replaces **httpx** with **aiohttp** in `SkyRLTrainInferenceForwardingClient` so high fan-out rollouts don’t pay growing per-request pool CPU; the shared session uses connector limits, a 60s connect timeout, no total deadline (queueing waits in the connector), Happy Eyeballs off under uvloop, and **orjson** for response bodies. Retries refresh the proxy URL once on connect failures and on router **5xx** (`TransientInferenceError`); read timeouts stay non-retryable. > > **`forwarding_inference_timeout_sec`** default rises **300 → 2048** to cover long vLLM queue waits on large bursts. > > **Uvicorn** gets **`timeout_keep_alive=75`** and **`backlog=SKYRL_HTTP_CONNECTION_LIMIT`** so SDK polls survive completion bursts without mass reconnect/refused accepts. > > **`retrieve_future`** only calls **`mark_retrieved`** when the client is still connected, so abandoned 45s SDK polls don’t start the short TTL on undelivered results; **`_RETRIEVED_TTL_SECONDS`** is **120 → 300** to cover SDK retry/backoff. Integration tests cover the abandoned-poll → retry path over a real socket. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 3bdbb3d. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Signed-off-by: Avi Basnet <avigyabb@stanford.edu> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Stack 4/7.
uvicorn.run:backlog=SKYRL_HTTP_CONNECTION_LIMIT(50k, as on Chuck's branch; the effective value is capped bynet.core.somaxconn, raise it to match) andtimeout_keep_alive=75.With 131072 outstanding samples and 212k-token results, each 2048-result completion burst kept the event loop busy ~16 s; uvicorn's 5 s keep-alive then closed every idle client connection, all clients reconnected at once and the 2048-entry accept backlog overflowed, refusing 109k of 131072 requests. With these settings the same run completed 130917 of 131072 with zero forwarding errors and zero reconnects (earlier runs on the 5 s keep-alive showed hundreds to thousands).
Neither setting is needed for correctness: the SDK retries refused or dropped connections. They avoid the reconnect storm rather than fix a failure, and with the SDK's per-client in-flight cap the burst that overflowed the backlog does not occur. An earlier revision of this PR also exposed
sample_max_concurrent_requestsfromEngineConfigvia/client/config; that was dropped as unnecessary (its default equalled the SDK default, and no measured run depended on it).Stack (each PR retargets to
mainas the one below merges)🤖 Generated with Claude Code
Note
Low Risk
Server-only uvicorn listen/keep-alive defaults; no API or auth behavior change, with SDK retries as a fallback.
Overview
The Tinker API server now passes uvicorn socket tuning so large completion bursts do not trigger mass client reconnects and accept-queue overflows.
timeout_keep_aliveis set to 75s (viaHTTP_KEEP_ALIVE_TIMEOUT_SECONDS) instead of uvicorn’s 5s default, so idle SDK connections stay open while the event loop is busy for many seconds during bursts.backlogis set toSKYRL_HTTP_CONNECTION_LIMIT(default 50k, overridable by env), so pending connections queue in the kernel instead of being refused when the loop cannot accept fast enough (effective cap isnet.core.somaxconn).These are reliability/performance knobs, not correctness fixes—the SDK already retries refused or dropped connections.
Reviewed by Cursor Bugbot for commit 27c838e. Bugbot is set up for automated code reviews on this repo. Configure here.