fix(v1,serve): discard delivered intercepts; trim worker arenas at heartbeat cadence - #1610
Conversation
…artbeat cadence Two env-worker memory fixes, measured together at ~74% peak / ~85% resting RSS reduction in a churn simulation of perplexity-shaped browser rollouts (4 concurrent, 45 turns): 1. InterceptionServer.intercepts retained every request's raw body — the full message history INCLUDING in-sandbox base64 screenshots — until rollout unregister, so a long rollout held every turn's request simultaneously (the dominant worker-memory term; the renderer's image offload rewrites a normalized COPY, so the intercept's base64 never left). forward_request now discards the intercept after delivery; the HTTP handler holds its own local reference, so delivery is unaffected. Discard is idempotent and rollout-unregister still sweeps undelivered entries. 2. Env workers had no malloc_trim anywhere (the per-batch trim is orchestrator-only), so freed arena pages ratcheted RSS to ~3x the live set. Trim at the existing 10s stats cadence via ctypes (GIL released; deliberately NOT gc.collect — full collections on fat heaps are what caused the worker heartbeat timeouts). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit e522d36) (cherry picked from commit 6516128)
ApprovabilityVerdict: Approved Memory optimization fix that cleans up intercept data after delivery and trims malloc arenas at heartbeat cadence. Both changes are self-contained, well-documented, and don't alter functional behavior - they only release memory earlier to prevent RSS growth during long rollouts. You can customize Macroscope's approvability policy. Learn more. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 868124c013
ℹ️ 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".
| else: | ||
| deliver_response(request, response, error) | ||
| finally: | ||
| endpoint.discard_request(request_id) |
There was a problem hiding this comment.
Keep failed streaming intercepts until cleanup can unblock them
When an intercepted request has stream: true for any non-chat protocol, this block raises before synthesize_stream() can put an EOF sentinel or resolve the response_future, but the new unconditional discard removes the only entry that InterceptionServer.unregister_rollout() uses to cancel that future and signal the chunk queue. In that failure path (and any synthesize_stream() error before delivery), the aiohttp handler is left waiting/keepalive-looping instead of being unblocked during rollout cleanup; only discard after successful delivery or explicitly unblock the intercept before popping it.
Useful? React with 👍 / 👎.
| # gc.collect(), which must NOT be called here (full collections | ||
| # on fat heaps are what caused worker heartbeat timeouts). | ||
| if libc is not None: | ||
| libc.malloc_trim(0) |
There was a problem hiding this comment.
Move malloc_trim off the event loop
Every EnvWorker already starts the configure_runtime_native_threads() daemon trim loop from _cap_native_threads() during construction, so this adds a second trim path that runs synchronously on the asyncio event-loop thread. Releasing the GIL does not let this same event loop continue processing requests or heartbeats while malloc_trim(0) is executing (or waiting on allocator locks held by the daemon trim), so on the large fragmented heaps this targets it can introduce exactly the worker lag/heartbeat delays the stats loop is meant to report; rely on the existing background trim or offload this call instead.
Useful? React with 👍 / 👎.
Completes the env-worker memory series: #1608 merged the first two fixes (step slimming + registry eviction) just before the commit carrying these last two was pushed to its branch — this PR is that stranded delta, cherry-picked onto the current base.
3. Discard delivered intercepts (dominant term: −74% worker peak)
InterceptionServer.interceptsretained every request's raw body — the full message history including in-sandbox base64 screenshots — until rollout unregister, so a long browser rollout held every turn's request simultaneously. (The renderer's image offload rewrites a normalized copy; the intercept's base64 never left.)forward_requestnow discards the intercept after delivery via a newEndpoint.discard_request: the HTTP handler keeps its own local reference so delivery is unaffected, discard is idempotent, and rollout-unregister still sweeps undelivered entries.4.
malloc_trim(0)at the worker stats cadence (resting RSS ÷3)Env workers had no trim anywhere (the per-batch trim added on prime-rl's side is orchestrator-only); freed arena pages ratcheted worker RSS to ~3× the live set. Trim every 10s in the existing stats loop via ctypes (releases the GIL — never stalls the loop). Deliberately not
gc.collect: full collections on fat heaps are what caused the worker heartbeat-timeout kills.Measurement (churn simulation: 4 concurrent 45-turn browser rollouts/worker, real subprocess RSS)
Production: without these two, the worldsims perplexity env pod climbed to ~90GB and failed even with #1608's fixes; with all four (worldsims pin
e522d36e), runoh7jsedjxix7rbk23sdvsujdis training now.py_compile+ruffclean; discard behavior unit-checked (local ref intact post-discard, idempotent).🤖 Generated with Claude Code
Note
Medium Risk
Touches request-forwarding lifecycle and periodic native memory calls in serve workers; behavior is localized but affects long-running rollout memory and timing under load.
Overview
Env-worker memory: The stats heartbeat loop now calls
malloc_trim(0)on Linux (viactypes+libc.so.6) so freed allocator pages are returned to the OS on the same ~10s cadence as worker stats. The change deliberately avoidsgc.collect()in that loop to prevent heartbeat stalls on large heaps.Intercept retention:
Endpoint.discard_requestremoves each delivered intercept fromInterceptionServer.interceptsafter streaming or non-streaming delivery inforward_request. Raw request bodies (including full multimodal histories) no longer accumulate for every turn until rollout unregister; the handler’s localrequestreference still covers delivery, and rollout unregister continues to sweep anything undelivered.Reviewed by Cursor Bugbot for commit 868124c. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Discard delivered intercepts and trim worker heap memory at heartbeat cadence
Endpoint.discard_requestinendpoint_utils.pyto delete completed intercept entries fromserver.intercepts, reducing retained memory after request delivery.forward_requestwithtry/finallysodiscard_requestis always called, including on error paths.malloc_trim(0)call viactypesinEnvWorker.stats_loopto return freed heap pages to the OS on each stats interval when libc is available.Macroscope summarized 868124c.