fix(server_utils): enable TCP keepalive on global aiohttp connector - #1959
Conversation
Detect silently dropped upstream sockets that otherwise cause long-running rollouts to stall forever. _TCPKeepAliveConnector sets SO_KEEPALIVE + TCP_KEEPIDLE/INTVL/CNT via _wrap_create_connection (aiohttp<3.11 compat); detection budget ~90s. Signed-off-by: Martina G. Vilas <mgonzalezvil@nvidia.com>
|
/claude review |
|
|
||
|
|
||
| class _TCPKeepAliveConnector(TCPConnector): | ||
| _KEEPALIVE_IDLE_SECONDS: int = 60 |
There was a problem hiding this comment.
can we parameterize this via GlobalAIOHTTPAsyncClientConfig? and what are the defaults for these values if we don't specify them, can we default to those same defaults or at least display them in a comment here?
There was a problem hiding this comment.
Good idea. I moved the three TCP keepalive values (TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT) to GlobalAIOHTTPAsyncClientConfig fields so they're overridable per deployment. Linux kernel defaults are 7200s / 75s / 9 probes, so ~2h15m before TCP gives up. I think that's longer than most benchmarks need, so I used shorter times.
|
SHIP WITH CARE — the fix is correct and well-targeted; one failure-mode asymmetry is worth reconsidering before merge. The diagnosis is right: Two things (both inline):
Nothing here corrupts scores or breaks the public server API — it's transport-layer reliability. Fix #1 and it's a clean ship. |
| transport, protocol = await super()._wrap_create_connection(*args, **kwargs) | ||
| sock = transport.get_extra_info("socket") | ||
| if sock is None: | ||
| raise RuntimeError("_TCPKeepAliveConnector: transport has no underlying socket") |
There was a problem hiding this comment.
RISK — fail-hard on a keepalive optimization inverts the intended failure mode.
WHAT BREAKS: _wrap_create_connection runs on every new connection. If get_extra_info("socket") ever returns None for a transport type in production, this raise propagates out of client.request(), gets caught by the generic except Exception in request() (line 227), retries MAX_NUM_TRIES, then re-raises — permanently. The condition is deterministic per transport type, so it wouldn't fail one request, it would fail all of them.
BLAST RADIUS: a feature added to prevent stalled rollouts becomes a total-outage switch. Losing keepalive is a degraded state (back to today's behavior); losing all HTTP is an outage. The downside of the guard is strictly worse than the problem it guards against.
FIX: degrade gracefully instead of raising — log/warn once and return transport, protocol without setting sockopts. Keep the loud raise only in the test (assert the warning), not in the hot path. On Linux+asyncio/uvloop with TCP/SSL transports socket is reliably present, so this is defense-in-depth, but the asymmetry (trivial fix, catastrophic failure mode) argues for not raising.
There was a problem hiding this comment.
Switched the raise RuntimeError in _wrap_create_connection to a log-once warning that returns the transport unchanged. If a transport ever exposes no underlying socket, we now just skip keepalive on that connection instead of taking down the whole client through the retry loop. Test updated to assert graceful degradation and a single warning per connector.
Move idle/interval/probe from hardcoded connector attrs to GlobalAIOHTTPAsyncClientConfig fields. Defaults unchanged (60s / 10s / 3). Signed-off-by: Martina G. Vilas <mgonzalezvil@nvidia.com>
…no socket Raising RuntimeError from _wrap_create_connection is caught by client.request()'s retry loop and would fail every outbound request on a transport type that doesn't expose a socket. Log once and skip keepalive for that transport instead, so the worst case is losing the optimization, not the client. Signed-off-by: Martina G. Vilas <mgonzalezvil@nvidia.com>
Drop the _TCPKeepAliveConnector subclass and its _wrap_create_connection override in favor of TCPConnector's public socket_factory= parameter (aiohttp >= 3.11). Same sockopts applied with the same values; stops depending on a private aiohttp method. Signed-off-by: Martina G. Vilas <mgonzalezvil@nvidia.com>
…VIDIA-NeMo#1959) ## Symptom Long-running Gym benchmarks (e.g. AA-Omniscience, MMLU-Pro) occasionally freeze with a handful of rollouts stuck "in flight" forever, until the Slurm job hits its time limit. ## Root cause The global aiohttp client uses a default `TCPConnector` with no TCP keepalive enabled. When an upstream socket is silently dropped, it is not noticed, so the request blocks forever. Not sure whether this was intentional. The connector's existing `keepalive_timeout=15.0` is not TCP keepalive, it only recycles unused connections in the pool, so it doesn't help with stuck in-flight requests. ## Fix `_TCPKeepAliveConnector`: a `TCPConnector` subclass that turns on `SO_KEEPALIVE` on every new connection with tight timings (idle 60s, probe every 10s, give up after 3 probes). Once the kernel drops the socket, aiohttp raises `ClientOSError`, which the existing `request()` retry loop already handles. Implemented via `_wrap_create_connection` rather than `TCPConnector(socket_options=…)` so it works on aiohttp<3.11 too. Linux-only sockopts are guarded so the code also runs on macOS/BSD. Happy to bump or lower the specific timings (60s / 10s / 3 probes). ## Tests Three unit tests cover: happy path (all keepalive sockopts set), non-Linux platform (only `SO_KEEPALIVE` set, no crash), and fail-loud when the transport has no underlying socket. --------- Signed-off-by: Martina G. Vilas <mgonzalezvil@nvidia.com>
Symptom
Long-running Gym benchmarks (e.g. AA-Omniscience, MMLU-Pro) occasionally freeze with a handful of rollouts stuck "in flight" forever, until the Slurm job hits its time limit.
Root cause
The global aiohttp client uses a default
TCPConnectorwith no TCP keepalive enabled. When an upstream socket is silently dropped, it is not noticed, so the request blocks forever.Not sure whether this was intentional. The connector's existing
keepalive_timeout=15.0is not TCP keepalive, it only recycles unused connections in the pool, so it doesn't help with stuck in-flight requests.Fix
_TCPKeepAliveConnector: aTCPConnectorsubclass that turns onSO_KEEPALIVEon every new connection with tight timings (idle 60s, probe every 10s, give up after 3 probes). Once the kernel drops the socket, aiohttp raisesClientOSError, which the existingrequest()retry loop already handles.Implemented via
_wrap_create_connectionrather thanTCPConnector(socket_options=…)so it works on aiohttp<3.11 too. Linux-only sockopts are guarded so the code also runs on macOS/BSD.Happy to bump or lower the specific timings (60s / 10s / 3 probes).
Tests
Three unit tests cover: happy path (all keepalive sockopts set), non-Linux platform (only
SO_KEEPALIVEset, no crash), and fail-loud when the transport has no underlying socket.