Skip to content

Add AGS rollout buffer generator for SWE coding-agent RL - #1

Merged
FunJim merged 43 commits into
mainfrom
ags-rollout-buffer
Aug 18, 2026
Merged

Add AGS rollout buffer generator for SWE coding-agent RL#1
FunJim merged 43 commits into
mainfrom
ags-rollout-buffer

Conversation

@FunJim

@FunJim FunJim commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

Adds an AGS-backed rollout-buffer generator for training coding agents on SWE tasks, plus the core-slime fixes that path exposed.

The generator boots one AGS sandbox per rollout, runs a real coding-agent CLI (Claude Code or CodeBuddy Code) against slime's adapter, scores the resulting patch with the task's own eval command, and returns on-policy token-level training data. 39 commits, +8231/-175 across 50 files.

What's here

New: AGS rollout-buffer generator (slime_plugins/rollout_buffer/generator/ags_generator/, 15 modules)

  • rollout.py / entry.py — concurrent rollouts with per-rollout session isolation; SWE_ROLLOUT_CONCURRENCY and SWE_BOOT_CONCURRENCY bound sandbox boot and agent execution separately
  • harnesses.py — harness registry for Claude Code (Anthropic adapter) and CodeBuddy Code (OpenAI adapter), each with exactly two override knobs (SLIME_AGENT_{CC,CBC}_EXTRA_{ARGS,ENVS})
  • swe_task.py / source.py — SWE task setup, patch extraction, eval-command execution, and prompt-source progression that survives requeue and epoch rollover
  • wandb_metrics.py / weave_trace.py — rollout and eval metrics to W&B, plus per-agent Weave trace parsing (payloads gzip+base64-encoded)
  • empty_patch_guard.py — classifies rollouts that exit 0 with an empty diff (a text-only final turn where no tool call was parsed). Attribution only by default: an empty-patch trajectory is still valid on-policy data whose zero reward is correct. SWE_EMPTY_PATCH_GUARD{off, metrics, abort}
  • adapter_service.py — lets eval stand up its own adapter instead of requiring the training one

New: Harbor task converter (tools/harbor_task_to_slime_prompt_data.py)

Converts Harbor SWE tasks to slime prompt-data JSONL, extracting the fields the generator consumes (instance_id, image, workdir, problem_statement, eval_cmd). Prompt and problem_statement are both Harbor's rendered instruction.md — the exact string Harbor hands its own agents — with the raw tests/config.json text kept under metadata.harbor for provenance.

New: launchers (examples/coding_agent_rl_ags/) — 2-node and 4-node Qwen3.5-35B-A3B SWE training for both harnesses, plus eval-only scripts for scoring a single checkpoint.

Core slime changes

These are not incidental — each fixes a defect that only agentic rollout surfaces.

File Change
slime/agent/adapters/openai.py Keep text and reasoning on the manager message even alongside tool_calls. Dropping reasoning_content made the re-render stop reproducing the sampled ids, so _SampleBuilder saw token drift and either REALIGNed a real trained response to loss_mask=0 or forked the trajectory — measured at ~25 samples per rollout where Claude Code produced 1. Also reads reasoning under either reasoning_content or reasoning, since the spec standardised neither and CodeBuddy echoes ours back under the other key.
slime/agent/adapters/common.py A harness's own sampling params no longer override the trainer's. CodeBuddy sends temperature=1 on every request, so --eval-temperature never reached sglang; the CLI's value now applies only where open_session's sampling_defaults left that key unset. Adds /_slime/{open,finish,drop}_session control routes for out-of-process session management.
slime/utils/data.py Gate vision extraction on multimodal_keys, not on the mere existence of a processor. AutoProcessor returns one for any VL-capable checkpoint (Qwen3.5-35B-A3B yields a Qwen3VLProcessor), which forced a conversation-shaped prompt on text-only datasets and made --apply-chat-template mandatory — which in turn rewrote Sample.prompt into a literal <|im_start|>user ... string that agent rollouts handed straight to the CLI.
slime/utils/metric_utils.py, slime/backends/megatron_utils/data.py New compute_grouped_pass_rate: agentic rollouts emit multiple train samples per attempt (one per root-to-leaf chain), and those fan-out siblings are training segments, not independent pass@k samples. Deduplicates by (group_index, rollout_id) before computing pass@k. The fixed-shape path now warns and returns {} instead of asserting.
train.py Skip the weight update and KV onload after the final training step when no rollout or eval follows — otherwise the last step pays a full onload for nothing.
slime_plugins/rollout_buffer/buffer.py Job-scoped rollouts: each start request gets a rollout_job_id, and writes carrying a stale id are dropped so a previous job's late data can't leak into the current training rollout. Replaces BackgroundTasks with an explicit RolloutJob thread that can be stopped and waited on.
slime/ray/rollout.py offload() collects handles across server_groups and ray.gets them, so offload actually completes before training proceeds.
slime/backends/sglang_utils/sglang_engine.py Pause generation around offload and resume only when the resume actually restores generation capacity (KV cache or CUDA graph tags present).
slime/agent/sandbox.py terminate_process_group() — SIGTERM the detached setsid group, wait out a grace period, then SIGKILL, and fail loudly if anything survives. Leaked agent background processes otherwise outlive the rollout.
slime/rollout/data_source.py Accept sample groups larger than n_samples_per_prompt (fan-out) instead of asserting exact equality; assert element type instead.
slime/utils/arguments.py --rollout-buffer-num-epoch, --rollout-buffer-stop-timeout-sec, --enable-token2text
requirements.txt weave

Tests

~2100 lines of new CPU-only tests: test_ags_generator.py (1147), test_ags_empty_patch_guard.py (239), test_ags_prompt_source.py (152), test_harbor_task_to_slime_prompt_data.py (150), test_passrate_metrics.py (97), plus adapter test expansion (test_adapters.py +186, new _fakes.py).

test_ags_prompt_source.py and test_ags_empty_patch_guard.py are registered in the num_gpus: 0 CI matrix (pr-test.yml + .j2). The other new tests are not yet wired in.

Testing

Not run at PR-refresh time — no pytest available in the authoring environment. The AGS generator itself has been exercised on 2- and 4-node Qwen3.5-35B-A3B SWE runs on H20, which is where the adapter, sampling, pass@k, and offload findings above were measured.

Note on the base branch

This targets main, which currently also serves as the fork's mirror of THUDM/slime. Merging 39 commits into it permanently diverges the mirror and makes every future upstream sync a two-way merge. Consider retargeting to a long-lived integration branch and letting main stay fast-forward-only (upstream-sync now tracks upstream/main for this purpose).

FunJim added 30 commits July 7, 2026 16:22
Use fresh adapter session IDs for each AGS attempt and isolate per-sample failures so one bad rollout does not stop the generator.
Convert Harbor SWE-style tasks into AGS prompt data while preserving the canonical verifier layout, and run eval_cmd as root so Harbor test.sh can execute unmodified in the evaluator sandbox.
Log AGS rollout calls, trajectory events, and sample payloads to Weave when W&B online logging is enabled, with an opt-in flag to decode token IDs for trace readability.
Route trajectory parsing through the configured AGS agent so CodeBuddy Code stream JSON can be traced without assuming Claude Code timestamps.
Large SWE-bench PASS_TO_PASS lists made the inline test.sh/config.json
heredocs blow past AGS/E2B argv limits. Embed the files as gzip+base64
blobs and decode them with a python3 heredoc inside the sandbox instead.
Compute passrate from prompt groups and rollout attempts when rollout fan-out produces multiple training samples per attempt, and avoid failing training on mismatched fixed-shape passrate inputs.
Allow AGS adapters to publish a full public base URL so proxy paths can be used, and skip unnecessary rollout onload/update work after the final rollout when no eval remains.
Stop each rollout-buffer generation job once enough samples have been collected so colocated SGLang engines are not offloaded while AGS sandboxes can still issue generation requests. Also pause generation before releasing SGLang memory and add tests for one-epoch buffer requests and stale job writes.
Keep the two-node launcher aligned with the scalable setup by enabling checkpoint saves, shuffled chat-templated rollouts, configurable rollout steps, and run-local W&B logs.
Add lightweight adapter control endpoints so eval rollouts can reuse the
already-running AGS adapter instead of binding a second service on the same
port.
Pass rollout_id through the rollout-buffer payload and initialize the AGS
prompt source offset from it so standalone generator runs do not repeatedly
start from the first prompt groups.
Raise the two- and four-node AGS rollout defaults to the validated
concurrency settings, and pass the configured WandB entity explicitly
when tracking is enabled.
Keep incomplete prompt groups out of the training buffer while preserving
flattened multi-segment agent outputs. Include the staged regression coverage
for AGS buffering and Harbor prompt conversion.
Avoid resetting converted tasks to base_commit unless explicitly requested, so task-image compatibility patches remain available to AGS rollouts.
Support same-sandbox grading by default, stop timed-out agent process groups before evaluation, and make Harbor prompt conversion preserve image state with reproducible payloads.
Use the Harbor TCR eval set, align evaluation sampling, and reduce default sandbox resources for both launchers.
AGSPromptSource seeks RolloutDataSource to the absolute group index the
trainer sends (rollout_id * rollout_batch_size), but only restored
sample_offset. Once that index passed one pass over the dataset, the
source stayed on epoch 0's permutation, so a resumed run drew a
different prompt sequence than the uninterrupted one. Derive epoch_id
from the group index and reshuffle to it when --rollout-shuffle is set.
FunJim added 9 commits July 28, 2026 18:56
CodeBuddy's auto-memory feature injects a `<system-reminder data-role="memory">`
block into every prompt. In SWE rollouts that block is pure overhead -- there is
no cross-session continuity to build up -- and it teaches the model to emit
`<system-reminder>` narration of its own, which no tool-call parser can read.

Verified on a 100-instance SWE-bench Verified subset: 0/100 trajectories carry
memory markers afterwards, against 48/50 in the previous run. Note this does not
by itself change the empty-patch rate (35.0% -> 33.0%, paired McNemar p=1.0);
it is a prompt-hygiene fix, not a scoring fix.

Also collapses three multi-line f-string concatenations onto single lines, as
the repo formatter produces.
Some coding-agent CLIs treat a text-only final turn as a finished answer: the
model stops mid-task, no tool call is parsed, the CLI exits 0, and the rollout
yields an empty diff. Measured with CodeBuddy Code on SWE-bench Verified that
accounts for roughly a third of all rollouts, and it is indistinguishable in the
artifacts from a genuine "no change needed" outcome.

empty_patch_guard.py classifies those rollouts and splits them by why they
stopped (mid-task narration / claimed completion / no final text /
unclassified). It reuses the existing iter_trajectory_events reader rather than
adding a third trajectory parser, and it never raises: a malformed trajectory
degrades to "not triggered" so classification can never cost an otherwise usable
rollout.

SWE_EMPTY_PATCH_GUARD selects the policy and defaults to "metrics", which only
labels and counts. "abort" is deliberately not the default: an empty-patch
trajectory still holds real on-policy tokens whose zero reward is correct, i.e.
the negative half of a GRPO group, so masking those out at a ~33% rate would
bias the group baseline toward successes. It also only requeues under
fully_async_rollout -- on the rollout_buffer path the AGS scripts actually use,
an aborted sample ships with its loss masked and is never retried.

Verified across five runs: the empty-patch rate reproduces at 32-36%, and on a
full 500-instance Claude Code run the guard fired on exactly the 3 of 42
zero-byte patches that had exit code 0, with no false positives or negatives
(a nonzero exit is an honestly reported failure and needs no extra label).

Also surfaces the pre-existing ill_formed flag, which had no metrics outlet.
manager_message is what re-renders as chat history on the next turn, so
any field it drops stops the re-render from reproducing the ids we
sampled. _SampleBuilder then sees token drift and either REALIGNs --
rewriting a real trained response as loss_mask=0 -- or forks the chain.

_build_reply_parts dropped two fields: reasoning_content was never set on
the leaf at all, and content was blanked whenever tool_calls were
present. Measured over 150 rollouts of a 500-instance SWE-bench Verified
run, that split each CodeBuddy rollout into a median of 25 samples where
Claude Code produced 1, with num_samples predicted exactly by
(wire responses - reasoning-only responses) in 150/150 cases.

The comments justifying both omissions assumed clients rewrite history on
echo. Probing the real CodeBuddy CLI over 40 turns shows it echoes text
alongside tool_calls verbatim, so neither omission was buying anything.
It does replay reasoning under "reasoning" rather than "reasoning_content",
so _translate_messages now accepts either: the field survived the round
trip all along and we were failing to read it (0/15 echoes matched the
leaf before, 15/15 after).

Fixing only the leaf would trade token drift for a message mismatch and
lose more signal, so text and reasoning go on the wire too.

This is a training-signal defect only. Scores from --debug-rollout-only
runs never consume loss_mask and are unaffected; it would have corrupted
GRPO advantage estimation as soon as CodeBuddy drove real training.

Tests drive both real adapters over a real /generate upstream and assert
one sample per clean chain plus the trained-token count, since a leaf that
drops a field can keep the count at 1 while training almost nothing. They
need a tokenizer whose assistant rendering a model could actually emit:
FakeTokenizer renders tool calls as the opaque marker toolcall:<name>,
which carries no arguments and so fabricates drift on every tool turn.
The prompt style decides whether the agent is handed the task text directly
or told to go read PROBLEM_STATEMENT.md, and training and eval want
different answers. Eval should measure the model solving the task, the way
Harbor scores it, rather than its file-discovery turns; training may prefer
it to work for the context. SWE_PROMPT_STYLE now covers training
(instruction) and SWE_EVAL_PROMPT_STYLE covers periodic eval (dataset),
both spelled out in the 2- and 4-node example scripts.

Only "instruction" writes PROBLEM_STATEMENT.md now. Under "dataset" the
prompt already carries the task text, so the file was a stray untracked
artifact that only git_diff's exclude pathspec kept out of the patch, and an
invitation to spend turns reading a restatement of the prompt.

The style is passed to each generate() call rather than stored on the
runner, because _AGSGenerateState is a singleton: the first caller's mode
would otherwise stick for the life of the process and hand eval the training
style, or the reverse.

Drop the "inline" style, which rebuilt the prompt from the raw
problem_statement and is superseded by "dataset". An explicit
SWE_PROMPT_STYLE=inline now fails instead of silently changing the prompt.

The converter loses --prompt-source and always uses Harbor's
instruction.md, for both the prompt and metadata.problem_statement, so the
two styles differ in when the agent sees the task and not in what it reads.
The raw tests/config.json text moves to metadata.harbor.problem_statement:
it keeps upstream CRLF on 252/500 of SWE-bench Verified, which tokenises
differently, so it is provenance rather than something to feed an agent.
…s sampling

Eval reached AGS through RemoteAdapterService, which only proxies an adapter
that someone else already bound. The training rollout is what binds it, inside
the rollout-buffer process, so eval-before-train on rollout 0 and --num-rollout
0 had nothing to talk to and every prompt died on connection refused.

get_adapter_service now health-probes the control URL and reuses the training
adapter when it answers -- that process owns the trajectory trees, and a second
bind on the same port would fail -- otherwise it starts a local adapter on an
ephemeral port. Ephemeral because the training adapter may still claim
ADAPTER_PORT later in the same run.

That fallback lives in the RolloutManager actor, which Ray does not pin to the
head node, so it advertises the local node's own IP and the port it actually
bound rather than ADAPTER_PUBLIC_HOST/ADAPTER_PUBLIC_BASE_URL -- both of which
name the head and a fixed port, and would send sandboxes to the wrong address.

Sampling defaults from open_session now outrank the request body. Probing the
real CLIs against a recording stub: codebuddy sends temperature=1 on every
/v1/chat/completions call, so --eval-temperature never reached sglang under
SWE_AGENT=codebuddy_code; Claude Code sends none of temperature/top_p/top_k,
which is why only the codebuddy path was affected. Body values still apply for
keys the caller left unset.

AdapterService stays a singleton, so a later caller's args are still ignored --
tearing down a live adapter would lose in-flight trajectories. It now records
its construction inputs and warns when they differ, instead of silently serving
an adapter built with another tokenizer or context budget.
Scores one Megatron checkpoint on the full SWE-bench Verified set with no
training: --num-rollout 0 selects train.py's eval-only branch, so no rollout
buffer is started and the eval path stands up its own AGS adapter. The
non-obvious parts are commented in place -- the LR scheduler asserts that
train_iters=0 triggers, why --no-load-optim is required for checkpoints
written with --optimizer-cpu-offload, and the Ray port overrides these H20
nodes need.

Also gate vision extraction in Dataset on multimodal_keys rather than on the
mere presence of a processor. AutoProcessor returns one for any VL-capable
checkpoint, so Qwen3.5-35B-A3B forced a conversation-shaped prompt on
text-only data, which made --apply-chat-template mandatory and rewrote
Sample.prompt into a templated "<|im_start|>user ..." string. Agent rollouts
hand that prompt straight to a CLI, so they were fed the literal markers.
The examples directory now holds launchers for more than one coding
agent, so move them out of claude_code_ags/ into coding_agent_rl_ags/
and prefix each script with the harness it drives (cc_).
The AGS examples only had Claude Code entry points, so scoring CodeBuddy
meant hand-editing SWE_AGENT. Add run_cbc_* / eval_cbc_* wrappers that set
SWE_AGENT and re-exec the harness-agnostic launcher, rather than copying
~370 lines per harness for the two copies to drift apart.

Reduce each harness's env surface to EXTRA_ARGS + EXTRA_ENVS. The removed
knobs (max turns, per-turn output cap, thinking, tool lists) were either
properties of the harness rather than of a run, or unreachable: none of the
SLIME_AGENT_CBC_* names were in the launchers' Ray env allowlist, so setting
them was silently ignored. Both knobs are now applied last -- EXTRA_ARGS
after the harness's own flags, EXTRA_ENVS after static_env -- so either can
override a default, which relies on both CLIs taking the last occurrence of
a repeated flag.

Deny WebSearch/WebFetch on both harnesses. Web access makes a rollout
unreproducible and lets the agent look up the fix being graded; it was
denied for CodeBuddy but not for Claude Code, which then used those tools on
1-2% of instances. Do it with --disallowedTools, which the CLI enforces, not
--tools, which does not restrict the surface.

Cover the new contract in tests: that EXTRA_ARGS and EXTRA_ENVS are applied
late enough to win, that the web tools are denied on both harnesses, and that
no turn or output cap is imposed. Verified by mutation -- clearing
default_flags or moving EXTRA_ARGS ahead of it turns these red.
Both CLIs clamp an absolute auto-compact window to [100k, 1M], which sits above
our 96k rollout_max_context_len -- so an absolute setting could never fire before
the adapter hard-stops a turn with finish_reason="length". Switch to the
percentage overrides instead, and tell each CLI the real window so the percentage
is of MAX_CONTEXT_LEN on both sides:

- CodeBuddy: CODEBUDDY_AUTOCOMPACT_PCT_OVERRIDE is a percentage of the model's
  maxInputTokens, so CodeBuddyCodeHarness now writes that key into models.json
  from SLIME_AGENT_MAX_INPUT_TOKENS. Without it resolveCompactTriggerAt() falls
  back to the clamped absolute window.
- Claude Code: CLAUDE_CODE_MAX_CONTEXT_TOKENS applies directly for model names it
  does not recognise as Claude models, which covers our "slime-actor" label, so
  no DISABLE_COMPACT is needed. MAX_OUTPUT_TOKENS is pinned to MAX_GEN_LEN rather
  than left at the 32000 default-for-unknown-ids.

Default is 60%, not the CLIs' 70%: the per-turn output reservation means a prompt
cannot grow past MAX_CONTEXT_LEN - MAX_GEN_LEN (~63k), so a 70% trigger (67200)
would never be reached. The launchers now print the resolved trigger point and
warn when it lands above that ceiling.

Also fixes a shell bug in the same block: a JSON literal used as the default in
${VAR:-{...}} is mis-parsed, because the value's own closing brace terminates the
expansion and leaks a stray "}" whenever the variable is already set. The JSON is
built in a named variable first.
@FunJim FunJim changed the title Add AGS rollout buffer support Add AGS rollout buffer generator for SWE coding-agent RL Aug 6, 2026
FunJim added 3 commits August 6, 2026 14:31
Remove the extra `/wandb` subdirectory from `--wandb-dir` in training and evaluation scripts.
test_ags_generator.py was never registered, so its 45 cases only ran
locally. Registering it needed three fixes, because the CI runner invokes
`python <file>` rather than pytest:

  * Add the `__main__` guard the runner relies on. Without it the file is
    merely imported and exits 0 -- a green check that runs nothing.
    test_ags_prompt_source.py had the same gap while already being
    registered, so it has been silently passing; fixed too.
  * Insert REPO_ROOT into sys.path before `from tests.test_agent._fakes`,
    matching what every other agent test already does. Bare `python
    <file>` does not put the repo root on the path.
  * Install openai / transformers / wandb / fastapi / uvicorn for the
    agent-test job. The test imports through the real slime_plugins
    package, whose __init__ chain needs them; all five are in
    requirements.txt but none are in the job's hardcoded install line.
    Uses the template's per-job extra_pip_deps hook so the other CPU
    job's install is untouched.

Verified in a clean venv built from exactly the generated dep list: all
seven tests in agent-test pass, 45 of them from this file.
CI pins 3.10, where asyncio.timeout() does not exist -- it is 3.11+. The
two generate() plumbing cases hit rollout.py's `async with
asyncio.timeout(...)`, whose AttributeError is then swallowed by the
rollout's broad except into an abort, so the harness never runs and the
tests fail on a missing capture rather than on the real cause.

Shim it onto a pass-through context manager, mirroring
test_agent/test_agent_rollout_cpu.py, which already does this for the same
reason. The wall-clock guard never fires in these tests.

Verified on a 3.10.20 venv with exactly the generated CI dep list: all
seven tests in agent-test pass. The earlier local verification used 3.14,
which is why this only surfaced on CI.
@FunJim
FunJim marked this pull request as ready for review August 6, 2026 08:19
@Tsing-git

Tsing-git commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Features are basically developed per our agreed requirements, and both deliverables and outcomes have been reviewed. We suggest running an AI review to check code details.

@Tsing-git Tsing-git left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Large PR (39 commits, +8266/−175, 50 files) that adds an AGS-backed rollout-buffer generator for training coding agents on SWE tasks, plus several important core slime fixes exposed by this new workload.

Verdict: Approve with suggestions — the code is well-tested (~2100 lines of new tests) and the commit history is unusually well-documented. A few concerns below.


Key Findings

Positives

  1. Excellent commit messages — every fix commit explains the root cause, how it was measured, and why the chosen fix is correct. This is a reference-quality commit log for a systems project.
  2. Good isolation — the AGS generator is entirely self-contained under slime_plugins/, with clean entry points. Core slime changes are minimal and well-scoped.
  3. Defensive engineeringempty_patch_guard.py defaults to metrics-only (no reward corruption), terminate_process_group fails loudly on leak, stale job writes are rejected by rollout_job_id.

Concerns

  1. PR size — 39 commits across 50 files makes review hard. This is more of a feature branch merge than a single PR. Consider splitting future work into adapter fixes, core metric fixes, and the AGS generator as separate PRs.

  2. Launcher duplicationrun_cc_qwen35_35b_a3b_swe_2nodes.sh and the 4-node variant share ~370 lines verbatim. The CBC wrappers partially address this, but the CC 2-node/4-node scripts themselves will drift. A shared base script sourced by both would reduce this risk.

  3. Sampling priority inversion (common.py) — the fix makes open_session defaults outrank request body values, which fixes codebuddy's temperature=1 problem. But this is a semantic inversion: normally request-level params should override session defaults. The choice is correct for this use case but should be documented as intentional in the docstring, since a future adapter consumer might expect the opposite.

  4. compute_pass_rate silent degradation — replacing the assert with a warning + return {} means a misconfigured run silently reports no pass@k rather than failing loudly. Consider at least logging at ERROR level so it's not missed in long training logs.

  5. Offload race window (sglang_engine.py) — pause_generation() is called inside release_memory_occupation() but there's no guarantee in-flight requests complete before the memory release proceeds. If sglang's pause is not synchronous w.r.t. in-flight batches, this could hit a use-after-free on the KV cache. Worth confirming sglang's contract here.

  6. Test CI coveragetest_ags_generator.py has 1170 lines and is now registered in CI, but the asyncio.timeout shim suggests it was only validated on 3.14 locally before the last commit. The CI matrix should ideally run 3.10 and 3.12+ to catch these gaps earlier.


Minor Nits

  • requirements.txt adds weave without a version pin — this could break in a future weave release.
  • The run_cc_*.sh scripts hardcode paths under /data_train/ericxjzheng/ as defaults — these should probably be documented as needing override, or moved to a .env.example.
  • _REASONING_KEYS in openai.py is a good pattern; consider extracting the key-normalization logic into a shared utility if more adapters need it.

Overall this is solid systems work with clear measurement-driven fixes. The main ask is to keep future PRs smaller and to document the sampling-priority decision more explicitly.

@FunJim
FunJim requested a review from Tsing-git August 7, 2026 03:20
A training step whose micro-batch count is not a multiple of dp_size was
aligned only by splitting multi-sample bins. That is impossible in
long-context runs: once a single sample fills max_tokens_per_gpu * cp_size,
first-fit emits one bin per sample and there is nothing left to split, so an
odd sample count raised and killed the run. This happened after the rollout
phase had fully succeeded -- 48 trajectories graded, patches applied cleanly,
abort rate 0 -- so a rollout's worth of sandbox compute was discarded, and no
AGS metric warned of it. The sample count is unpredictable because
auto-compaction lets one rollout emit several training samples (48 rollouts
produced 62 to 77 samples), which makes per-step parity a coin flip.

Round the count down to the nearest multiple instead, keeping every sample.
Rounding down is only possible with at least align_to bins; below that the
floor is 0 micro-batches, so raise there with an error naming the real cause
rather than blaming the repack.

Repack with longest-processing-time-first so the largest resulting bin stays
as small as possible, since that bin sets peak activation memory. Merging the
two smallest bins repeatedly is the intuitive rule but degrades once more than
one merge is needed: [49000, 49000, 90000, 90000, 90000] merged to 3 bins
peaks at 180000 tokens versus 139000, i.e. 1.88x against 1.45x a 96000 cap.
The two agree at exactly one merge, which is the dp_size=2 case.

Merged bins can exceed the token cap, unavoidably, because first-fit is
already a maximal packing; max_tokens_per_gpu becomes a target on this path
and the repack is logged. Lowering it cannot relieve the overflow -- a merged
micro-batch holds whole samples, so its token count is set by the data -- so
the warning points at rollout_max_context_len and the batch shape instead.

Tests use the token lengths and rollout ids recovered from the run that
crashed, and cover the peak-memory property and the below-threshold error.
@FunJim
FunJim merged commit 3597b60 into main Aug 18, 2026
44 checks passed
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.

4 participants