Skip to content

OpenSandbox sandbox backend for terminal RL, with infra hardening and loss exclusion - #1887

Open
pdasigi wants to merge 54 commits into
omni_agentfrom
pd_open_sandbox
Open

pdasigi wants to merge 54 commits into
omni_agentfrom
pd_open_sandbox

Conversation

@pdasigi

@pdasigi pdasigi commented Sep 8, 2026

Copy link
Copy Markdown
Member

Adds OpenSandboxBackend — RL sandboxes on a self-hosted OpenSandbox service on GKE — as an alternative to on-node Podman and Modal, plus the trainer-side robustness work that a month of production runs on it demanded.

Sandbox backend

  • OpenSandboxBackend (all six SandboxBackend methods), verified live; selected via backend: opensandbox in tool_configs
  • Hardening from production incidents: adopt-on-504 (gateway-cut creates are found by create-id tag and adopted), per-node create throttle (SWERL_OPENSANDBOX_START_CONCURRENCY), terminal SandboxDiedError on mid-episode death (no more silent blank-container restarts — ModalBackend too), registry auth, adoption-failure diagnostics
  • Image pulls route through an Artifact Registry pull-through mirror (SWERL_OPENSANDBOX_IMAGE_PREFIX): one Docker Hub fetch per tag ever, in-region node pulls

Trainer robustness

  • Loss exclusion for infra-failed rollouts (--mask_infra_failed_completions): sandbox deaths / reset failures / breaker trips are excluded from GRPO group mean/std (new valid_mask in compute_group_advantages) and dropped from the batch; val/infra_failed_rate reported always. Without it, Spot preemption's fake zeros (~5–15% of episodes) bias group advantages
  • Rollout circuit breaker (SWERL_MAX_CONSECUTIVE_TOOL_TIMEOUTS): ends rollouts on unresponsive envs instead of grinding through max_steps × timeout
  • Main-loop stall watchdog (MainLoopStallWatchdog): faulthandler stack dumps when the trainer stops advancing — this is what caught the wandb.log hang (Publish wandb metrics from a background thread so a wedged SDK cannot stall training #1841, cherry-picked here)
  • Async wandb logger: wandb.log moved off the main thread; a wedged wandb SDK now costs dropped metrics, not stalled training (three multi-hour production stalls traced to this)
  • Environment pool utilization metrics (pool/<name>/in_use|waiting|acquire_wait_s_mean)
  • mason --min_runtime (Beaker preemption-protection window; needs beaker-py ≥ 2.7.2, pin bumped)

Ops

  • scripts/opensandbox/: egress check, janitor (by app tag), cost estimator (spot/on-demand), GC CronJob manifest, synthetic load test + Beaker batch-job wrapper (validated the service at 2,048 concurrent sandboxes / four ps512 runs' churn)
  • Launch scripts: 4-node prod + 2-node toy for Qwen3.5-4B tmax on OpenSandbox
  • docs/sandbox_management.md (renamed from sandbox_modal_vs_podman.md): all four backends, tuning, incident postmortems
  • Dockerfile: google-cloud-sdkgoogle-cloud-cli (upstream package removed; unbreaks all image builds); build_image_and_launch buildx fallback for Docker 20.10 hosts

Validation

Validated by extended production GRPO runs (4 nodes × 8 GPUs, 512–1024 concurrent sandboxes, 100+ training steps across runs) and a 2×1024-worker synthetic scale test against the live service; 42 unit tests for the backend, 11 for masked advantages, plus watchdog/pool suites.

GPU_TESTS=bypass (infra/backend branch validated by the production runs above; the GRPO loss-path change is unit-tested and flag-gated, default off)

Note for the merger: two known conflicts vs omni_agent (pyproject.toml, uv.lock) from the beaker-py 2.5.7 → 2.7.2 bump — keep >=2.7.2 and re-lock.

🤖 Generated with Claude Code

pdasigi and others added 30 commits June 25, 2026 09:45
Quantify Podman/Docker sandbox failures from a training run's logs
(Beaker experiment or local files): concurrency saturation, host
rotation/cooldown, disk exhaustion, OOM, step/reset timeouts, and the
"running hot" regime (Ray actor/node death + resource exhaustion). Emits
counts and per-rollout rates with FATAL vs recoverable-INFRA totals; each
signature cites the emitting code site.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Broaden node_marked_dead to catch Ray node-death phrasings ("actor's node
  was terminated", "node has died", missed-heartbeats), which the ps768
  crash surfaced -- FATAL was undercounting node/raylet OOM deaths.
- Add --compare to print 2+ logs side-by-side (count + rate/rollout per
  signature, with FATAL/INFRA/rollout totals), so the throttled-vs-running-hot
  profile shift is visible at a glance. Sources are file paths or exp:<id>.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a third SandboxBackend that runs sandboxes as modal.Sandbox
containers instead of on-node Podman/Docker. Selected via
"backend": "modal" in the env config; defaults to the agent-training
Modal environment. File I/O is exec-piped like ApptainerBackend so
exception semantics match the other backends; sandboxes get a hard
lifetime plus an atexit reaper to bound the cost of leaks.

Verified end-to-end against real Modal (ai2-reviz), including an
egress check from ai2/jupiter (scripts/modal/check_modal_egress.sh).
Includes a Modal variant of the tmax 2-node toy launch script and
promotes modal to a core dependency so the training image gets it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SWERLVanilluxSandboxEnvConfig is a fixed-field dataclass, so
"sandbox_lifetime" in tool_configs fails validation at startup.
Use SWERL_MODAL_SANDBOX_LIFETIME_S instead, which ModalBackend
already reads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
modal.Image.from_registry requires Python + pip inside the image and
its build fails otherwise, which is common for task images (the tmax
images are bare Ubuntu 22.04). This bricked the first Modal training
run: every sandbox create failed and the trainer waited on step 0
forever.

Try the image as-is first so images that ship their own Python aren't
shadowed by a standalone one, then retry with add_python (version from
SWERL_MODAL_ADD_PYTHON_VERSION, default 3.12) and remember the outcome
per tag. Verified against the exact task image that failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Modal warns on every blocking call made from an async context; with
hundreds of env actors making sandbox calls this floods training logs
(most of the log volume in the first Modal run). The blocking usage is
deliberate since SandboxBackend is a sync interface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
add_python drops a standalone interpreter into /usr/local/bin, which
shadows the image's own /usr/bin/python3. tmax task verification runs
`python3 -m pytest`, which then hits the bare standalone interpreter
(no pytest, no task deps) and unconditionally writes reward 0 — every
rollout group had zero reward variance, so active sampling discarded
all of them and training never produced a batch (258 rollouts, 0
accepted; the podman baseline accepts its first group within ~74).

After a fallback start, if the image has its own python3 outside
/usr/local/bin, remove the standalone's interpreter/pip links so
resolution returns to the image's python. Verified on the failing
image: python3 -> /usr/bin/python3 (3.10) with pytest 9.0.3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first Modal toy run billed $1,219 vs a ~$15 estimate; the overage
was sandbox-hours nobody was using. Fixes:

- Close the sandbox at episode end (submit and OOM paths) in both
  SWERL envs instead of holding it idle until the env's next reset.
  Also removes most end-of-run leakage since envs finish their last
  episode with no live sandbox.
- ModalBackend.close(): retry terminate once and log failures loudly
  instead of suppressing them (a failed terminate bills until the
  lifetime cap).
- scripts/modal/cleanup_modal_sandboxes.sh: janitor to terminate all
  live sandboxes for an app after killing a job.
- Toy launch script: sandbox lifetime 4h -> 1h (caps worst-case leak
  4x), per-run Modal app name so the janitor can target it.
- Note the measured toy-run cost in the podman-vs-modal doc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Episodes in flight when training completes never reach the submit-path
close, so the pool's sandboxes (122 of 128 in the last run) leak until
their lifetime cap. Chain the janitor after grpo_fast.py so a graceful
job end cleans them up; hard kills still need the janitor run manually.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…service

New SandboxBackend that runs each sandbox as a pod on a self-hosted
OpenSandbox deployment (GKE Autopilot). Unlike Modal, the OCI image is
pulled as-is, so there is no rebuild/fidelity gap. Includes the same
leak protections as ModalBackend (hard lifetime cap, loud kill retry,
atexit reaper, end-of-job janitor), an egress-check feasibility script,
a toy launch script, and unit tests with a fake SDK.

Verified live against the AI2 GKE deployment: warm start ~5.3s,
~1.05s/exec, all six backend methods and error semantics pass. Known
issue documented: cold-start creates can 504 at the ingress's 30s
timeout while still creating the pod server-side (orphan reclaimed by
the janitor via the open_instruct_app metadata tag).

Also renames docs/sandbox_modal_vs_podman.md to
docs/sandbox_management.md, rewrites it to document all four backends
as implemented (present tense, no stale line numbers), and updates all
references.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a create outlasts the ingress's upstream timeout, the client gets
HTTP 504 but the sandbox still comes up server-side (sometimes more
than once), so every cold start both errored AND leaked billed orphan
pods. start() now tags each create with a unique open_instruct_create_id;
on a gateway-timeout error it polls the management API for a sandbox
carrying that tag (bounded by ready_timeout), adopts it via
SandboxSync.connect, and kills any duplicates. If nothing appears, the
original error is re-raised.

Also fixes _sandbox_is_alive comparing against "RUNNING": the SDK's
SandboxState.RUNNING is "Running" (mixed case), so the liveness probe
never matched and any transient exec error would have restarted the
sandbox, wiping episode state mid-rollout.

Live verification against the AI2 GKE deployment: after the LB backend
timeout was raised server-side, a 101s cold create (golang image + node
provisioning) completed on the normal path; the adoption path is covered
by unit tests with the fake SDK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The toy launch script defaulted SWERL_OPENSANDBOX_PROTOCOL to http when
unset in the launching shell, so training jobs spoke plain HTTP to the
HTTPS-only endpoint. The load balancer resets those connections
immediately, which made every sandbox create fail within seconds with
"[Errno 104] Connection reset by peer" / "Server disconnected" and
deadlocked two toy runs at step 0 (identical at pool_size 128 and 16 --
initially misread as a concurrency ceiling). Default to https, and log
an explicit hint from OpenSandboxBackend.start() when a create dies
with a connection reset while protocol is http.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pairs OpenSandboxBackend's "sandbox started"/"Closing" log lines into
per-sandbox lifetimes and prices total sandbox-hours at GKE Autopilot
per-request rates (cpu/memory/lifetime auto-detected from the log).
Handles Ray log dedup by counting "[repeated Nx]" multipliers and
pricing hidden events at the mean observed lifetime, and prices
unclosed sandboxes up to end-of-log bounded by the lifetime cap.
Cross-check against the BigQuery billing export for ground truth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ray log dedup hides Closing lines, so per-id pairing misread most
closed sandboxes as unclosed and priced each up to the lifetime cap
(2.7x overestimate on the first pool-128 run: $44.54 vs $13.84).
Reconcile starts vs closes globally (including dedup multipliers and
end-of-job janitor kills) so only the true start/close gap is priced
as leaked; other unpaired sandboxes get the mean paired lifetime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
EnvironmentPool now counts in-use actors, blocked acquirers, and
acquire wait times; stats() snapshots and resets them. run_training
polls every pool once per training step and logs pool/<name>/in_use,
in_use_peak, waiting, acquires, and acquire_wait_s_mean, so pool_size
saturation (in_use pinned at size with nonzero waiting/wait times) is
visible on wandb instead of requiring log archaeology. Analysis of the
first pool-128 OpenSandbox run showed the pool pinned at 128 for
essentially the whole run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A pool-768 run collapsed at startup: ~768 concurrent creates
overwhelmed the control plane (mass 504s) and forced Autopilot node
provisioning in bulk, so pods sat Pending past the 180s ready_timeout
and nearly all post-504 adoptions gave up on pods that were about to
come up -- env retries then amplified the load (99 sandbox starts in
2h vs 3,205 at pool 128).

Wrap creates (including the adoption poll) in a per-node file-slot
semaphore, SWERL_OPENSANDBOX_START_CONCURRENCY (default 64), so large
pools ramp instead of stampeding -- running sandboxes hold no slot, so
steady-state still reaches full pool size. Raise the ready_timeout
default from 180s to 600s (SWERL_OPENSANDBOX_READY_TIMEOUT_S) so
creates and adoptions survive node-provisioning waves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SWERL_RESET_FAILURE_ZERO_REWARD defaults to off and the toy-script
lineage (podman -> modal -> opensandbox) never set it, so a single env
reset that exhausted its retries (create 504 + failed adoption) crashed
the whole pool-768 training job during step 2. With the flag on, such
resets become zero-reward rollouts and the job survives. Proper
infra-failure semantics (exclude from training instead of reward 0)
remains follow-up work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sandbox image pulls happen on the GKE nodes, not the training job, so
the Podman-style local docker login does nothing for this backend.
Pass credentials through the SDK's SandboxImageSpec/SandboxImageAuth
instead: OpenSandboxBackend reads DOCKERHUB_USERNAME / DOCKER_PAT (the
same convention as the Podman setup) and attaches auth to the create
call when both are set. The toy launch script wires the same Beaker
secret used by the Podman script. Anonymous Docker Hub pulls are
rate-limited per NAT IP, which mass pod scale-out trips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a sandbox vanished during an exec (Spot preemption, crash,
lifetime expiry), OpenSandboxBackend and ModalBackend restarted a fresh
sandbox and retried the command. The replacement is a blank container
-- no task seed files, no agent-built state -- so the episode continued
against a corrupted environment and scored garbage rewards silently
(~66 episodes in the first Spot-backed toy run).

Raise SandboxDiedError (mirroring the SandboxOOMError contract) from
both backends instead, and end the episode in the SWERL envs with
reward 0 and metadata.sandbox_died=True. OpenSandboxBackend.close()
now also skips the kill-retry-and-alarm path when the sandbox is
already gone. This makes Spot sandbox pods safe: a preemption costs
one episode, not signal integrity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The SWERL envs now track sandbox_died and oom_killed per episode
(set by the terminal handlers, cleared on reset) and report them from
get_metrics(). The existing rollout-state aggregation averages these
over each training step's batch, so wandb shows
env/<env_name>/sandbox_died and env/<env_name>/oom_killed as the
fraction of rollouts lost to Spot preemption/expiry and OOM kills.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…aker

A spot-backed toy run silently stalled for 12.5 hours between training
steps 3 and 4 with zero log output, leaving no evidence of where the
main thread was blocked (the step-4 weight sync completed in 0.75s once
whatever it waited on released). Two defenses:

- MainLoopStallWatchdog (utils.py): the training loop heartbeats at
  each phase; a daemon thread dumps all thread stacks via faulthandler
  when no beat arrives for SWERL_MAIN_LOOP_STALL_DUMP_S (default 1800),
  repeating until the loop advances. health_check_fn's silent
  weight-sync handshake poll now also logs every 120s while waiting.

- Circuit breaker (vllm_utils.py): a sandbox on a preempted Spot node
  blackholes the exec stream instead of erroring, so every tool step
  times out while the env actor sits blocked -- observed as hour-long
  zombie rollouts that collapsed generation throughput and likely
  triggered the sync hang. After SWERL_MAX_CONSECUTIVE_TOOL_TIMEOUTS
  (default 3) consecutive tool-step timeouts the rollout ends instead
  of grinding through max_steps x tool_call_timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The create-throttle change added ", semaphore_wait=Xs" to the sandbox
started log line, which the estimator's regex did not match, so it
found zero lifecycle events in newer logs. Match both formats and
subtract the semaphore queue time when backdating the billing start
(billing begins after the slot is acquired, not when start() is
entered).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the planned design (valid_mask in compute_group_advantages +
the existing keep_idxes drop machinery + mask_infra_failed_completions
flag) and why it matters at observed Spot death rates, plus the
retry-in-process_request follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The estimator priced everything at on-demand rates. Sandbox capacity
type is a deployment property invisible to the client, but the run's
endpoint domain (in the Starting log lines) identifies which server it
used. Add --pricing {auto,on-demand,spot}: auto selects spot when the
domain contains a --spot-domain-markers substring, spot applies
--spot-discount (default 70%) to the on-demand rates, and spot output
carries the caveats (preempted pods stop billing before the lifetime
cap; realized discounts vary -- BigQuery Spot SKUs are ground truth).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a post-504 adoption gives up, the training log only said 'no
sandbox appeared', hiding WHY the pod never became Running -- a ps768
run burned an hour on what was likely capacity exhaustion with no
identifiable cause in the log. The failure path now distinguishes
never-admitted creates from stuck-Pending pods, logs each stuck pod's
status state/reason/message, and relays the server's diagnostics API
content (Kubernetes events: quota names, IP_SPACE_EXHAUSTED,
GCE_STOCKOUT, ...). Servers without the diagnostics API (current AI2
deployment answers DIAGNOSTICS_NOT_IMPLEMENTED) get one warning and no
repeated fetch attempts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pdasigi and others added 18 commits August 10, 2026 16:27
Clone of qwen35_4b_base_tmax_10k.sh with the on-node Podman plumbing
(subcontainer perms, podman services, registry mirror, docker_login)
replaced by the OpenSandbox config from the 2-node toy script: endpoint
env vars, registry auth via SandboxImageSpec, create throttle, reset
zero-reward flag, and the end-of-job janitor. pool_size is 512 rather
than 1024: the Spot-backed server's observed capacity ceiling is
~450-520 sandboxes and oversubscribing it slows the run (see the ps768
postmortem notes in docs/sandbox_management.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OpenSandbox's built-in TTL reaper runs inside the control plane it
protects; during the 2026-08-12 overload it stopped reaping and ~2,300
expired-but-running pods plus ~2,300 finished BatchSandbox records
accumulated, exhausting Spot capacity and stalling a production run for
19 hours in a create-fail/retry spiral. This CronJob is scheduled by
Kubernetes and deletes through the Kubernetes API directly, so it keeps
working while the OpenSandbox server/controller are unhealthy: finished
(Succeed/Failed) CRs older than 30 min and any CR older than 2 h are
removed every 10 minutes. Apply once per sandbox cluster with kubectl.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bitnami/kubectl:1.35 does not exist (Bitnami's public catalog was
discontinued); use gcr.io/cloud-builders/kubectl, which also pulls
in-GCP without touching Docker Hub. Scope the Role to the CRD's actual
group (sandbox.opensandbox.io) instead of a wildcard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drives OpenSandboxBackend (the production code path: create throttle,
adopt-on-504, SandboxDiedError handling, registry auth) with N
concurrent workers looping realistic episodes (create -> spaced execs
-> close) to answer whether the deployment sustains multi-run load
without paying for GPUs. Reports create/exec latencies, failure
taxonomy, preemption deaths, and judges pass/fail on steady-state
create p95 and failure rate (cold-cluster ramp creates excluded).
Smoke-tested live: 8 episodes, 22/22 execs, steady create p50 4.5s,
zero sandboxes leaked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prod script: add the WANDB_API_KEY Beaker secret (required in the
oe-agents workspace) and point at that workspace. Toy script: reflect
its current use as the concurrency acceptance test (start concurrency
128, distinct exp_name, description).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… stall training

wandb.log hands its payload to the wandb service process over a local
socket and waits on a future with no timeout. When the service wedges,
the caller blocks indefinitely: three long GRPO runs stalled this way,
each with the main thread stuck inside wandb.log at the end of
one_training_step for 3-12 hours while every GPU idled (stack traces
captured by the stall watchdog were identical across incidents).

Add AsyncWandbLogger (utils.py): log() enqueues to a bounded queue and
returns immediately; a daemon thread is the only wandb.log caller,
preserving step ordering. If the queue fills because the SDK stopped
draining, entries are dropped with a loud warning - losing metrics is
acceptable, losing training time is not. flush() (atexit-registered)
counts in-flight entries so a healthy run's final metrics still land.
Route the two training-loop call sites (per-step metrics in
grpo_fast.one_training_step, eval metrics in grpo_utils.maybe_evaluate)
through it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SWERL_OPENSANDBOX_IMAGE_PREFIX (or image_prefix kwarg) rewrites bare
Docker Hub references to pull through a pull-through mirror -- an
Artifact Registry remote repository in-region with the sandbox cluster.
user/img:tag gains the prefix, official images additionally gain Docker
Hub's implicit library/ namespace, and already registry-qualified
references pass through unchanged. Rewritten pulls skip the Docker Hub
username/PAT (mirror access uses the cluster's own credentials).

Each unique tag is fetched from Docker Hub once ever (by the mirror);
node pulls stay in-region, removing multi-minute cold pulls on fresh
nodes and Docker Hub rate-limit exposure. Verified live against the AI2
mirror with an official and a task image (create + exec both paths).
Launch scripts default the prefix to the AI2 mirror, overridable or
blankable from the launching shell.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Explain what SWERL_OPENSANDBOX_IMAGE_PREFIX does (pull-through cache of
Docker Hub, in-region node pulls, no Hub rate limits) and how to bypass
it; also drop the stale "Autopilot" and "no registry mirror" phrasing
now that the cluster is GKE Standard and mirror pulls exist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
checkpoint_state_freq was set but checkpoint_state_dir was not, so the
save block never ran and the 2026-08-21 run lost all 84 steps (21.5h on
32 GPUs) when Beaker preempted one node; point it at weka so a node
death now costs at most 10 steps.

SWERL_OPENSANDBOX_CPU=2 (overridable): at cpu=1, ~3 sandboxes per
4-vCPU e2 spot node ran nodes at 77-85% CPU and stretched rollouts
~2.4x with ~500 exec timeouts/hr; doubling the reservation is the
contention experiment. Header documents the packing consequence (one
sandbox per e2-standard-4 -> node autoscaler max must clear ~520 nodes
or the machine type must grow).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Workers check stop_event at the loop top, then can spend minutes queued
in the create semaphore inside backend.start(); after --duration-s
expired, queued workers emerged in waves and each ran a full episode,
keeping the test creating sandboxes long past its deadline (observed:
+900 CRs ten minutes after the 1024-worker run ended). Close the fresh
sandbox and exit instead when the stop arrived mid-create.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
launch_load_test_beaker.sh submits a CPU-only mason job (ai2/hammond)
that runs run_load_test_beaker.sh: two side-by-side 1024-worker load
tests with distinct app names, simulating two concurrent training runs'
sandbox churn on the production network path. The job prints both final
summaries, janitor-sweeps both app tags, and exits nonzero if either
process misses the success criteria.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…issing

Docker 20.10 without the buildx plugin reports "unknown flag:
--platform" for the buildx invocation. Detect the plugin and fall back
to DOCKER_BUILDKIT=1 docker build; the platform flag is unnecessary on
linux/amd64 hosts and the registry cache-to/from is buildx-only, so the
fallback builds without the shared cache.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
At full CPU quota there is no warm-node headroom, so every Spot death
routes replacement creates onto freshly provisioned nodes (~2-3 min GCE
boot) and the default 30s create-p95 criterion fails even when the
service is healthy (2026-08-25 run: p50 5.2s, p95 ~140s, failure rate
0.7-0.8%, zero control-plane restarts). Note in the launcher header how
to read the verdict at this scale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… script

mason gains --min_runtime (Beaker duration string, e.g. '90m'): the
scheduler will not preempt the job before it has run this long. Needs
beaker-py >= 2.7.2 (BeakerTaskContext.min_runtime), so bump the pin.
Pass the string through verbatim — a bare int is interpreted as
nanoseconds by the client.

The 4-node OpenSandbox prod script sets BEAKER_MIN_RUNTIME (default 2h):
enough to clear the ~25-40 min first step and reach the first
checkpoint_state save at step 10, so a preemption never sends a retry
back to step 1. Both recent prod attempts died to node preemption.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ompletions)

Spot-preempted sandboxes end episodes with a fake zero reward (~5-15% of
episodes, 30% in spikes), and GRPO's group-relative advantages then push
the policy away from trajectories that were fine until the pod vanished.

The rollout loop now flags info.infra_failed on mid-episode sandbox
death, environment reset failure, and consecutive-timeout circuit
breaker trips. OOM kills are deliberately NOT flagged: the policy's own
commands caused them, so their zero reward is legitimate feedback.

With --mask_infra_failed_completions (default false), group advantage
statistics are computed over surviving samples only via a valid_mask in
compute_group_advantages (masked mean/std; advantage 0 for failed
samples; all-invalid groups get all zeros), and the failed rollouts are
then dropped from the batch through the existing keep_idxes machinery.
val/infra_failed_count|rate are reported on the unfiltered batch whether
or not masking is on. The OpenSandbox prod script enables it
(MASK_INFRA_FAILED=false to opt out); other launch configs unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Google dropped the transitional google-cloud-sdk apt package from
packages.cloud.google.com; installs now fail with "Package
'google-cloud-sdk' has no installation candidate ... replaced by
google-cloud-cli". Same binaries (gcloud, gsutil), new package name.
Breaks every image build from any commit, so this needs to reach main
too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pdasigi
pdasigi requested a review from shatu September 8, 2026 23:52
pdasigi and others added 6 commits September 9, 2026 10:52
…ur override

--pricing auto detected spot from a "spot" substring in the endpoint
domain — a relic of the two-cluster era. The current single-domain
deployment has no marker, so a Spot ps1024 run was silently priced
on-demand (3.3x too high, $1918 vs $575). auto now prints a loud
warning when it falls back to on-demand.

--sandbox-hourly-cost prices sandbox-hours at a flat rate, bypassing
the vCPU/GiB pod model: on GKE Standard each cpu=2 sandbox occupies a
whole e2-standard-4 spot node (~$0.040/hr), which the pod model
undercounts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A killed Beaker job never reaches the trailing janitor; at pool_size
1024 that leaves ~1,000 live sandboxes holding the whole node pool and
CPU quota for up to one lifetime, and an immediately relaunched run
ramps into leftovers (creates 504 while the pool is full — observed
2026-09-09). Sweep with the janitor before relaunching, or wait one
sandbox lifetime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…not resume its predecessor

The checkpoint dir was a hardcoded literal while exp_name was edited
per-experiment; the loss-exclusion run silently auto-resumed the
previous run's step-320 state because the trainer treats an existing
checkpoint dir as its own retry. One EXP_NAME variable now feeds both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Experiment config inherited verbatim from omni_agent's
qwen35_9b_dppo_repro_4node_64k.sh (DPPO tv@0.1, liger loss, 64k
response, 8x32 rollouts, 16 engines / 8+8 learners); only the sandbox
plumbing differs: OpenSandboxBackend with the AR pull-through mirror,
cpu=2, create throttle, loss exclusion (MASK_INFRA_FAILED, default on),
checkpoint_state_dir derived from EXP_NAME, Beaker min_runtime, and a
distinct app tag (swerl-tmax-9b-opensandbox) so the janitor and metrics
never collide with the 4B run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BEAKER_CLUSTER / BEAKER_WORKSPACE env overrides (defaults unchanged:
ai2/jupiter, ai2/oe-agents) so the same script launches on the B300
cluster with BEAKER_CLUSTER=ai2/holmes BEAKER_WORKSPACE=ai2/oe-agents-holmes.
Add ai2/holmes to WEKA_CLUSTERS (matching main) so mason mounts weka
there — the checkpoint_state and rollouts paths need it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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