feat: disaggregated sandbox backends on OpenSandbox (ns_tools + math_formal_lean) - #2434
feat: disaggregated sandbox backends on OpenSandbox (ns_tools + math_formal_lean)#2434hemildesai wants to merge 5 commits into
Conversation
c59a354 to
70d7a00
Compare
|
/claude review |
|
SHIP WITH CARE — solid, heavily-tested disaggregated-sandbox backend. Scoring/verify contract preserved; async cancellation handling is careful and well-covered. One correctness RISK worth a look before high-concurrency training use. Correctness / scoring (highest scrutiny) — clean
Async correctness — clean
RISK (inline)
NOTE (non-blocking)
|
|
/claude review |
|
/ok to test 6bf85fe81b |
@hemildesai, there was an error processing your request: See the following link for more information: https://docs.gha-runners.nvidia.com/cpr/e/2/ |
|
/claude review |
|
SHIP WITH CARE — solid, opt-in, and unusually well-tested. The default paths are unchanged and the new provider-backed backends are gated behind config, so blast radius for existing users is zero unless they set What I verified holds up:
NOTE (please confirm, couldn't verify here): No BLOCKER or RISK-level findings. Operability caveat is the usual one: the pool/heal/routing machinery is unit-tested but not exercised against a live sandbox service, so watch the first real rollout at concurrency. |
|
Confirmed against NeMo-Skills |
e18f72e to
ace4c1b
Compare
|
/claude review |
|
/ok to test ace4c1b |
|
SHIP WITH CARE — two opt-in OpenSandbox backends (Lean per-verify/pooled compilation; ns_tools disaggregated pod pool) plus core One RISK posted inline ( Notes (author's call, not blocking):
Verifier/scorer logic ( |
| # the key only in proxy mode: a direct sandbox endpoint runs | ||
| # untrusted code and must never see it. | ||
| if self._connection.api_key is not None: | ||
| kwargs["headers"] = {"OPEN-SANDBOX-API-KEY": self._connection.api_key} |
There was a problem hiding this comment.
is this still needed given the this is being set on line 663 now?
| await self._provider.close(handle) | ||
| finally: | ||
| await self._provider.aclose() | ||
| self._closed = True |
| import asyncio | ||
|
|
||
|
|
||
| async def await_cleanup(task: asyncio.Task[None]) -> None: |
There was a problem hiding this comment.
Could we rename this to await_uninterruptibly, since it applies to any task and defers cancellation until the task finishes?
| strip_theorem_from_proof=self.config.strip_theorem_from_proof, | ||
| ) | ||
|
|
||
| def setup_webserver(self): |
There was a problem hiding this comment.
Could we move this lifecycle wiring into SimpleResourcesServer as opt-in behavior, so resource servers needing sandbox warmup don’t have to implement it themselves?
|
Could you post the link to your run? (for the run numbers in https://terryk.gitlab-master-pages.nvidia.com/nemo-html/hemild/nemo-sandbox/rlvr-disagg-sandbox-design.html) |
ace4c1b to
8175b7c
Compare
Adds two opt-in sandbox backends that move code execution off the resources server's own host and onto OpenSandbox pods. Both default to today's behavior: with no environment variables set, the resolved configs are byte-identical to the current ones and neither backend is constructed or imported. ns_tools `sandbox_type: sandbox_pool` A fixed set of long-lived, SHARED pods each running the NeMo-Skills sandbox HTTP protocol, with sessions multiplexed across them by sticky routing. Sharing is what makes large batches feasible: many concurrent sessions ride K pods instead of one pod per session. New sessions pin to the least-loaded healthy pod and stay there, so stateful ipython state survives across tool calls. Slots are filled either by claiming a prewarmed pod from a server-side Pool (extensions.poolRef), which drops warmup to allocation time, or by a direct create; a failed or full pool claim degrades to a direct create by default (pool_fallback). A health loop evicts a pod after three consecutive failed probes and heals it in the same slot under a create-rate limit, and an idle sweep drops stale session pins. Every pod carries per-run attribution labels so an epilogue reaper can delete exactly one run's sandboxes. Transport rides a shared aiohttp session — httpx/httpcore's connection pooling collapses at the concurrency this backend targets — while keeping httpx exception types so the NeMo-Skills client contract is unchanged; infra failures degrade rewards rather than crashing the server. math_formal_lean `sandbox_backend: gym_sandbox` Lean4 compilation on OpenSandbox pods via provider exec, reproducing the NS server's lake/lean invocation and its process_status/stdout/stderr contract exactly. With pool_size=0 each verify gets a fresh pod under a bounded semaphore, destroyed in finally. With pool_size=N a warm pool is built at server startup and reused across verifies, because a cold pod's first `import Mathlib` lazy-pulls several GB of olean files one page fault at a time; pool pods bulk-prefetch that tree once at prepare and then serve verifies back-to-back. Failed pods are replaced in place and the verify retries once elsewhere before degrading. A third value, `ns_http_proxy`, speaks the NS protocol through a full base_url plus headers, which serves as a parity oracle against the default path. Also adds `endpoint()` to the OpenSandbox provider, implementing the existing SupportsSandboxEndpoint protocol: it resolves the SDK's server-proxy route for a declared port, absolutizes a scheme-less URL from the configured domain, and carries the auth header the proxy requires. Config surface is env-fed with empty defaults (domain, api_key, image, pool_ref, sizes), and values that must not arrive as strings go through oc.decode or explicit int()/float() coercion. Selecting either backend with an empty domain, api_key, or image is a hard startup error rather than a silent no-op. Tests: 21 sandbox_pool tests, 14 lean backend tests, 9 provider endpoint tests. Signed-off-by: Hemil Desai <hemild@nvidia.com>
Reuse the public sandbox API for pooled and Lean execution. Make resource ownership and cancellation cleanup explicit, validate backend configuration, and cover lifecycle regressions. Signed-off-by: Hemil Desai <hemild@nvidia.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
8175b7c to
ba67f25
Compare
What this adds
Two opt-in sandbox backends that move code execution off the resources server's own host and onto OpenSandbox pods, plus the provider method they both need.
ns_tools—sandbox_type: sandbox_poolA fixed set of long-lived, shared pods, each running the NeMo-Skills sandbox HTTP protocol, with sessions multiplexed across them by sticky routing. Sharing is what makes large batches feasible: many concurrent sessions ride K pods instead of one pod per session.
ipythonstate survives across tool calls.pool_refis set, a slot is filled by claiming a prewarmed pod from a server-side Pool viaextensions.poolRef, which drops warmup to allocation time. A full or unavailable pool degrades to a direct create by default (pool_fallback: true), or fails the slot when that is turned off. Fallback pods go through the normal prepare step, so pool configs should still carry their setup/service settings.aiohttpsession, per this repo's guidance that httpx/httpcore connection pooling collapses at high concurrency, while keeping httpx exception types so the NeMo-Skills client contract is unchanged. Infra failures normalize into that contract and degrade rewards rather than crashing the server.math_formal_lean—sandbox_backend: gym_sandboxLean4 compilation on OpenSandbox pods via provider exec, reproducing the NS server's
lake env ... leaninvocation and itsprocess_status/stdout/stderrcontract exactly.pool_size: 0— a fresh pod per verify, created under a bounded semaphore and destroyed infinally.pool_size: N— a warm pool built at server startup and reused across verifies. A cold pod's firstimport Mathliblazy-pulls several GB of olean files one page fault at a time (measured at ~900s), while a warmed pod compiles in ~4s, so pool pods bulk-prefetch that tree once at prepare and then serve verifies back-to-back.verify().ns_http_proxy, speaks the NS protocol through a fullbase_urlplus headers. It is useful as a parity oracle against the default path.nemo_gym/sandbox— OpenSandbox providerendpoint()Implements the existing
SupportsSandboxEndpointprotocol for the OpenSandbox provider (Docker already had it). It resolves the SDK's server-proxy route for a declared port, absolutizes a scheme-less URL using the configured domain, and carries the auth header the proxy requires. The key is attached only in server-proxy mode — a direct endpoint terminates at the sandbox itself, which runs untrusted code and must never be handed the credential (the same scoping rule as #2462). This is the only change tonemo_gym/sandbox/— everything else the backends rely on was already onmain.Default behavior is unchanged
Both backends are opt-in and off by default. With no environment variables set, the resolved configs are byte-identical to the current ones, and neither backend module is imported or constructed:
ns_toolsdefaults tosandbox_type: localmath_formal_leandefaults tosandbox_backend: ns_httpThe
opensandboxSDK is declared in each server'srequirements.txt; the rootpyproject.tomlalready pinned it.Config surface
Everything deployment-specific is env-fed with an empty default, so nothing about any particular deployment is baked into the repo:
OPENSANDBOX_BASE_URL,OPENSANDBOX_API_KEYNS_SANDBOX_IMAGENS_SANDBOX_POOL_REF,NS_SANDBOX_POOL_FALLBACKNS_SANDBOX_POOL_SIZE,NS_SANDBOX_TTL_SNS_TOOLS_SANDBOX_TYPE,MATH_FORMAL_LEAN_BACKENDLEAN_SANDBOX_MAX_CONCURRENT,LEAN_SANDBOX_POOL_SIZEValues that must not arrive as strings go through
oc.decodeor explicitint()/float()coercion at the constructor, since Hydra hands env-fed values over as strings and the create API rejects non-string env maps. Selecting either backend with an empty domain, API key, or image is a hard startup error, never a silent no-op.Sizing and prewarming notes
request_timeoutfor creates is raised well above the SDK default, because the first create against a new image tag blocks on image conversion and the default fails the whole cold warmup wave.pool_refor a warmpool_sizein any latency-sensitive setting: the first compile on a genuinely cold pod is dominated by lazy olean pulls, far beyond any verify's admission window.start()kicks pod creation from server lifespan startup and returns immediately, so server boot is never gated on the pool being full.Test coverage
resources_servers/ns_tools/tests/test_sandbox_pool.pyresources_servers/math_formal_lean/tests/test_sandbox_backends.pytests/unit_tests/test_opensandbox_endpoint.pyAll pass, along with the pre-existing suites for the touched servers (41 total in
ns_tools, 56 inmath_formal_lean) andtests/unit_tests -k sandbox(301 passed). No network is required: routing, eviction, and pool-claim logic are driven directly, and transports are faked. Thesandbox_pooltests that neednemo_skillsskip gracefully when it is not installed.🤖 Generated with Claude Code