Skip to content

rl/syncer: one syncer with async multi-island sync for LoRA and full-parameter RL - #292

Draft
michaellchung wants to merge 263 commits into
s26-e11-clusterfrom
s27-unified
Draft

michaellchung wants to merge 263 commits into
s26-e11-clusterfrom
s27-unified

Conversation

@michaellchung

@michaellchung michaellchung commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR moves multi-island RL to one syncer service. The syncer serves LoRA runs and full-parameter runs. The base stays s26-e11-cluster (#288).

This update moves the head from 869078a5 to 718b2f14. The new head adds these parts:

  1. Faster asynchronous full-parameter rounds: background pushes, a per-fragment boundary, staggered first pushes and an ACK-only final cut.
  2. Full-parameter sync at data parallel (DP) > 1.
  3. Engines on a multi-node island, with the Miles NCCL weight update to the engine nodes.
  4. An automatic per-island data split, data positions and one tape event per round with the trained data rows.
  5. The language-only bridge for the Nemotron-H Super VL checkpoint.
  6. --rl-export-hf-rounds, which writes an island's model as HF directories, and a fix for its upload to a Modal Volume.
  7. Ten branches from other open draft PRs (S26-E0.5: remove the self-written Codex harness #280, S26-E9: robustness fixes (preemption marker, verl cuts, data slots, one-command verl pairs) #282, S26-E7: reject verl with expert parallelism; require fsdp2 strategy #283, S26 E2: async buffer depth defaults to the policy-age limit; opt-in adaptive TIS #285, S26 E0.3: bias-stacking ablation tooling for Qwen3-0.6B on Miles #286, S26 E1: single-island overhead (background round cuts, publish fast path, profilers) #287, S26 E4/E5: phase timing, analysis tools, release writer Volume optimization #289, S26 E6: FP8 quantized rollout and full-parameter dedicated-engine island #290, Move fork and image references from michaellchung to agentenv #291).

A 2-island asynchronous full-parameter run of Nemotron-3 Nano 30B-A3B ran to the end on 32 x H200 at 33cce368. Its first GPU checks of the new mechanisms passed. One HF export of the 1-island control run failed (see "GPU validation"). Held-out pass@1 went from 0.521 (base) to 0.739 (merged global model). 718b2f14 adds only the HF export fix. That fix has CPU tests only.

What the new head merges

The first-parent history from 869078a5 to 718b2f14 has these merges and commits.

Branches from other open draft PRs. Where a branch conflicts with this PR, the merge keeps this PR's behavior.

Branch PR What it brings
s26-e04-imgassert none Harbor layer version lock and a start check.
s26-e7-verl-megatron #283 verl refuses EP > 1 and strategies other than FSDP2.
s26-e45 #289 Release phase timings. A release writer that keeps its own LATEST and commits once. An optional LoRA copy check (--rl-sampler-fused-stage). Scale report tools.
s26-e1-overhead #287 Merge record only. The tree already had this work. The default cut interval did not change to 4.
s26-e6-quant #290 Rollout quantization tiers (--rl-rollout-quant-tier, default 0 = BF16), an FP8 GPU allow list and tier launch checks. This PR keeps its own Volume writer guard, pins and single-island full-parameter rule.
s26-e03-ablation #286 Ablation drivers and task files (tools and data only).
s26-e2-async #285 --rl-async-buffer-rounds. With overlapped generation the buffer depth defaults to --rl-max-policy-age. An optional adaptive TIS.
s26-e9-robust #282 A rank > 0 preemption signal goes to rank 0. verl fully_async cuts save only optimizer-held parameters. Sampler data slots. FINISHED on an exhausted buffer. The launcher follows rescheduled Modal containers. One verl launch runs trainer and sampler islands. All roster checks count the syncer's learners.
s26-agentic-merge none (contains #280) Removes the self-written Codex harness. Adds the Harbor agentic path (suspend between rounds, curriculum, compaction), a boat reuse pool, Codex through the yeto model provider and verl fully_async agentic runs.
s26-agentenv-migrate #291 Only the origin compatibility of the pins (empty former list, no behavior change). Fork and image references still point at michaellchung.

Feature branches and direct commits:

Branch or commit What it brings
s27-sync-overlap (922f3880) Background pushes, zero-copy full-parameter chunks, a faster int4 encoder.
s27-task-assign (f9318d15) Automatic per-island row split, data positions, tools/eval/data_split_check.py.
s27-dp-sync (d93c97fe) Full-parameter sync at DP > 1. Nemotron-H training defaults.
s27-120b-gaw with s27-120b-prep (b95e1f96) Nemotron-H Super VL bridge wiring, a latent MoE check, multi-node full-parameter islands with dedicated engines, an offline Nemotron-H layout tool, a --moe-layer-freq mask without spaces.
s27-disagg-fast (4dc9acac, with 4c702a55 reverted) Staged background releases, narrow integer buffer payloads, parallel prefetch, a Modal port range fix, no relaunch into a stopped app, a timed default cut interval. The revert keeps sampler islands on data slots only.
s27-sync-boundary (0c4750a2) Per-fragment boundary, staggered first pushes, ACK-only final cut.
3b0bafc6 The offline layout tool supports DP replica owners (--dp).
63afafaf The rl_round_data_rows tape event.
76f067d3, 72e9eef4 Miles and image pins (see "Pins and image").
fadbffc3, 33cce368 --rl-export-hf-rounds. The warning goes to stderr, so the --dry-run JSON stays valid.
718b2f14 HF export fix: the first export of each round to a Modal Volume no longer fails.

Behavior changes and new options

Asynchronous full-parameter rounds

  • Background push. A worker thread prepares and sends each planned push while the island trains. The island applies broadcasts once per boundary. This path runs when every role encodes ahead (no int4 error feedback, no stochastic rounding). YETO_FULL_PARAMETER_BACKGROUND_PUSH=0 keeps the synchronous push. YETO_FULL_PARAMETER_PUSH_WORKERS (default 3) sets the number of workers.
  • Zero-copy chunks. With Miles 8edea35a7, full-parameter chunks go through Ray as numpy buffers (one copy on put, mapped on get). Before, Ray pickled CPU tensors at about 0.30 GB/s per thread.
  • int4 encoder. The encoder works in cache-sized row batches and splits large inputs over YETO_PUSH_ENCODE_THREADS threads (default 4). The bytes are identical to the old encoder. For a 0.5B-value fragment the encoder took 4.3 s before and 0.65 s after (CPU).
  • Per-fragment boundary. When the Miles transport reports the lazy_export feature, a local round reads no whole master. The island hashes, NaN-checks and model-checks each pushed fragment when it materializes the fragment. Miles commits per parameter (partial commit). Every 16th round still checks the whole model (YETO_FULL_PARAMETER_FULL_CHECK_EVERY, 0 = never). YETO_FULL_PARAMETER_LAZY_CAPTURE=0 turns the lazy path off.
  • Staggered first pushes. At stream start every fragment has the same anchor. The first push of fragment f now waits f * H // P more rounds. Later pushes keep the H gate. YETO_FULL_PARAMETER_STAGGER_FIRST_PUSH=0 turns this off.
  • ACK-only final cut. New syncer option --final-cut-payload {full,manifest}. New launcher option --rl-sync-final-cut {auto,ack,download} (full-parameter preset only).
    • auto gives ack in --rl-sync-mode async without --rl-eval-interval. It gives download in all other cases. The strict mode keeps download, so its commands do not change.
    • With ack, the syncer sends only the final manifest. Each island drains its pushes and sends its final ACK. No island downloads the f32 final cut (126 GB for a 30B model).
    • The merged final parameters stay in the syncer's final checkpoint. Each island keeps its own local model.
    • The option is part of the semantic profile hash only when it is on. Existing hashes do not change.

Full-parameter sync at DP > 1

  • Miles 921cde8c7 gives each parameter one owner rank per replica group. The replicas apply the owner's checked bytes. The layout lists each parameter once.
  • yeto accepts DP > 1 only when the pinned Miles lineage has 921cde8c7.
  • At DP > 1 the island keeps the non-distributed optimizer (whole FP32 masters on every replica). It refuses --rl-optimizer-offload-fraction.
  • A sampler island builds no trainer, so it skips this check.
  • New Nemotron-H training defaults: selective recompute, 8192 x TP tokens per GPU, and the flex dispatcher with DeepEP when DeepEP is present.
  • tools/nemotron_h_offline_layout.py --dp N computes the layout hash before launch (scope: TP 1, EP = DP). Nano at TP1/EP8/DP8 gives 64 fragments and layout c2c3cb49…. An island on GPU accepted this hash.

Engines on a multi-node island

  • A full-parameter island with dedicated engines may span several nodes (--rl-island-gpus-per-node). The launcher lays out the trainer and the engines over the nodes, as for a LoRA fixed partition.
  • The trainer updates the engines with the Miles NCCL weight update. The engines can be on other containers of the same island. The weights do not go through a store.
  • In a Modal container the island sets ip_local_port_range to 32768-60999 when the range starts lower. Before, an ephemeral port could take a port that Miles picks for an engine, a router or the trainer master. Then one engine of several failed with "address already in use".
  • One island uses one GPU type. Trainer nodes and engine nodes must use the same card.

Data split and data rows

  • --rl-data-split auto (new default, for N >= 2 training islands). Island k of N reads the rows i with i % N == k. The launcher sends --rl-data-slot k/N. --rl-island-tasks overrides the split. shared keeps the old behavior and prints a warning. External learners fall back to shared with the same warning.

  • Passes. Pass 0 reads the rows in data order. Miles reshuffles each later pass with a seed from --seed and the pass number.

  • Data positions. A synced full-parameter island with --rl-checkpoint-store gets --rl-data-position-store. It records its data cursor after each sync boundary, one file per record. A process that joins a running pool continues after the last record. A refused write goes on the tape, and the island trains on.

  • rl_round_data_rows event. The driver tapes one event per island per round when --rl-observe-timeline is on and the island reads a row slot. Fields: rollout_id, policy_version, samples, row_count, rows (arithmetic runs [[first, last, step], ...]). verl does not report the rows. Then the driver writes no event.

  • tools/eval/data_split_check.py. The tool reads the island tapes of a run. It checks these conditions:

    1. The islands hold distinct slots.
    2. No two islands train the same row.
    3. Each trained row lies in its island's slot.
    4. A relaunched island restored its data position.

    The tool prefers rl_round_data_rows and falls back to rl_trajectory_reward for older tapes.

Nemotron-H Super VL

  • yeto reads a NemotronH_Omni_Reasoning_V3 VL config as its language model. The FLOPs count only the language model. The frozen vision names now include vision_projector.* and sound_model.*.
  • For a VL checkpoint or a checkpoint with MTP, the island loads the Miles bridge plugins before AutoBridge. The bridge drops MTP (unless MILES_NEMOTRONH_KEEP_MTP is set). It reads a VL checkpoint as its language model and does not build the vision part.
  • The island refuses an image whose Miles has no language-only VL bridge. The error names the Miles commit to use.
  • Nemotron-3 Nano keeps the upstream provider.
  • models.py registers nemotron35-super-vl (249 GB).

--rl-export-hf-rounds

  • Syntax: --rl-export-hf-rounds R[,R...][,final]. Round R is the island's R-th trained round.
  • final is the round whose boundary stops the island. On a synced full-parameter island, the export runs after the final manifest arrives and the pushes drain, before the final ACK. The syncer waits for the island within its final-ACK timeout. Without sync, final is the last round.
  • Requirements: --rl-checkpoint-store, Miles, full parameter, sample role both. The launcher refuses verl, --rl-elastic and other sample roles. The island refuses LoRA. The launcher prints a warning on stderr for a round past --total-steps x --local-rl-rounds-per-sync.
  • Layout: <store>/hf/island<k>/round-<r>/. Each complete directory has a .complete marker. The final export also writes <store>/hf/island<k>/final.json.
  • Order: the ranks write into a local staging directory. The island puts the directory (one commit). Then it puts the marker and, for final, final.json (second commit). So a directory with the marker is committed.
  • One rl_hf_export tape event per export. A failed export goes on the tape, and the island trains on.
  • Fix in 718b2f14. A Modal Volume reports the removal of a missing path as InvalidError: No such file or directory. The store did not treat this as "missing". So the first export of each round failed at its upload, and the island removed the staging copy. Now:
    1. remove_tree raises only when the path is still there after the removal.
    2. The upload runs up to 3 times (waits of 10 s and 30 s). The tape event gets upload_attempts and upload_errors.
    3. When the last attempt fails, the staging copy stays with its marker (staging_kept). Each island process keeps at most 2 such copies (staging_dropped).

Trainer and sampler islands

  • A trainer island exports each full-weight release into a local staging directory. Every rank writes a share. A background thread uploads the release while the next step trains. --rl-release-inline keeps the old inline path. YETO_RL_RELEASE_EXPORT=miles uses the Miles export.
  • A Modal Volume store uploads a directory with Volume.batch_upload.
  • An unsynced full-parameter trainer releases nothing for version 0. The samplers start from the base model.
  • Buffer payloads store integer arrays in the narrowest type (R3 expert ids as uint8). The trainer prefetches in parallel.
  • The default round-cut interval is now timed: prior MTBF 21600 s, with cuts capped at 5% of the wall time. --rl-cut-every N and --rl-cut-auto-mtbf-s replace the default as before. verl keeps one cut per round.

Island relaunch

  • The default does not change. The launcher relaunches a failed island for --recover-timeout seconds (default 1200). --no-island-relaunch or --modal-retries 0 turns this off.
  • New: when a relaunch fails with "App not found" and the app is stopped, the launcher abandons the island at once. Before, it retried until the recovery timeout ended.
  • New: a relaunched synced full-parameter island with a checkpoint store continues at its next data row.

Pins and image

  • Miles 59aea07f9 (michaellchung/miles, branch yeto/s27-closing). It adds to the previous pin d1616ba12: 8edea35a7 (zero-copy chunks), 921cde8c7 (DP replicas), d78bf39e4 (pinned CPU masters), b7868074c (language-only VL bridge) and 63cb372b3 (per-fragment boundary).
  • SGLang 2fa880182 (unchanged).
  • Image ghcr.io/michaellchung/yeto-miles-ports:59aea07-2fa8801@sha256:d54dd4458d08fb2c6012278ac48c285b89bb51b1e50584ad9a032fc18b6ef18a. The image check (scripts/check_miles_ports_image.sh) passes all 13 checks.
  • 718b2f14 changes only yeto files. It needs no new image.

GPU validation of the new head

Closing run (code 33cce368, image 59aea07-2fa8801)

Model: Nemotron-3 Nano 30B-A3B. Backend: Miles, full parameter. Both runs started within one minute of each other on Modal H200.

Item A (s27-bb-a1) B (s27-bb-b1)
Islands 2. Each island has one 8 x H200 trainer container (TP1/EP8/DP8, no optimizer offload) and one 8 x H200 engine container (4 TP2 SGLang engines). 1, same island shape.
Sync One syncer on an AWS head. Async, quorum 1, adaptive grace up to 300 s, max base lag 1, H = 8, pipeline 20, rda, tokens2-over-steps weights, int4 pushes, 64 fragments. 768 outer merges. None (--rl-single-island-no-sync).
GPUs 32 16
Rounds 152 / 157 per island 105 (fixed in advance)
Training 32 prompts x 16 samples per island round. Responses up to 2048 tokens. Constant lr 1e-6. Rollout routing replay. numeric_answer reward. First 8192 rows of DAPO-Math-17k (A: split by i % 2). Same.

Evaluation: 675 held-out problems (AIME24 29, AIME25 25, DAPO held-out 283, MATH-500 numeric answers 338). pass@1 uses n = 8 at temperature 1.0. Errors are paired bootstrap standard errors over problems. Two evaluations of the base model differ by -0.0004 ± 0.0068.

Model Rounds per island pass@1 Greedy
Base 0 0.521 0.643
A island-local, round 56 56 0.609 / 0.606 0.664 / 0.652
A global, merge 385 (mid-run syncer checkpoint) 78-82 0.639 0.686
B final 105 0.697 0.724
A island-local, final 152 / 157 0.729 / 0.727 0.735 / 0.759
A global, merge 768 (final syncer checkpoint) 152-157 0.739 0.756
  • Every trained model is above the base by more than 2 standard errors.
  • A global final minus B final: +0.042 ± 0.006. A trained more: 152/157 rounds per island against 105, and 158,208 samples against 53,760. A took 187.7 min to merge 768, and B took 129.6 min to round 105.
  • A global mid minus B final: -0.058 ± 0.006. A's mid point came 107.1 min after start, and B's final point came 129.6 min after start.
  • A global final minus A island-local final: +0.010 ± 0.005 and +0.012 ± 0.005.

Mechanisms that ran on GPU for the first time (run A):

Mechanism Result
Per-fragment boundary Steady median boundary 10.8 s and 10.4 s per round. Whole-model checks ran exactly every 16th round. 1176 of 1184 commits were partial, with 0 refusals.
Staggered first pushes No first push came earlier than its planned round.
ACK-only final cut The syncer wrote the final checkpoint at merge 768. Both islands sent the final ACK 184 s and 186 s later. The head job ended with rc = 0.
Mid-run syncer checkpoint (--rl-sync-checkpoint-every 385) The write took 233 s. The next merge came 262 s after the previous merge. No stream stalled, no island reconnected and no island exited.
DP > 1 sync with in-island NCCL weight update update_weights succeeded every round (153 and 158 calls per island). The engine weight_version matched the round number. The publish phase took a median 3.3-3.5 s per round.
Data split and rl_round_data_rows data_split_check passed. All rows came from the new event. No row was trained by both islands.
--rl-export-hf-rounds 4 of 4 exports in A succeeded (63.2 GB and 23 files each, write 54-63 s, upload 124-136 s). This needed a workaround: placeholder directories on the Volume before the first export. B lost its round-56 export to the bug that 718b2f14 fixes.
Global model check The step-385 and step-768 syncer checkpoints convert to HF with the base names, shapes and dtypes, and all values are finite. Teacher-forced logprobs, global against island-local final: mean abs diff 0.0112 and 0.0116, p99 0.179 and 0.188.

Health:

  • No OOM, ENOSPC, NCCL error or Modal reschedule (exit 7) in A or B.
  • Train/rollout logprob gap: A 0.007-0.011, B 0.008-0.012.
  • Trainer GPU memory peak: A 139.7 GiB of 140 GiB, B 112.1 GiB.
  • Health check: B had 0 FAIL. A had 1 FAIL and 1 WARN. The FAIL is one of 280 merge boundaries. At round 146, island 1 accuracy fell by 0.41 (0.72 to 0.31), more than the 0.35 limit. The logprob gap did not change at that boundary. We infer batch variance, not a merge fault.
  • Merges: 710 of 768 had both islands. 46 late pushes went into later merges. The syncer rejected no push.
  • Steady round wall time: A 63.9 s and 60.7 s, B 57.4 s. Actor MFU: A 11.5-11.7%, B 19.4%. B uses the distributed optimizer. A cannot use it at DP > 1.

Other GPU runs since 869078a5

  • s27-ap-c tested the background push, zero-copy chunks and int4 encoder on 2 islands x 4 x H200 (Nano). The median sync per round was 63 s and 54 s. In s27-gr-j it was 403 s and 538 s. The 180 merges completed. Island 1 did not ACK in time while it downloaded the 126 GB final cut. This result led to the ACK-only final cut.
  • s27-ba-a (closing shape with 2 engine containers per island, DP8, Nano): the island accepted the offline layout (64 fragments, c2c3cb49…). The DP replica checks had 0 errors. The NCCL weight update took a median 3.0 s. Health: 13 PASS, 0 WARN, 0 FAIL.
  • s27-aw-b (Nemotron-3.5 Super VL 120B-A12B, 1 island of 5 x 8 H200, TP8/PP4/EP8, no sync, 2 steps): the island built the language-only bridge. The per-rank parameter counts matched the offline layout. All 28 update_weights_from_distributed calls returned 200. Logprob gap 0.0074 and 0.0095. Trainer GPU memory peak 84.7 GiB.

Testing

  • 33cce368, remote full CPU suite without Miles: 7431 passed, 0 failed, 128 skipped, 25 deselected, 3 xfailed.
  • 33cce368, cargo test: 226 passed.
  • Local targeted tests at 63afafaf..33cce368:
    • files that use the pins: 1113 passed, 8 skipped.
    • data rows (85 files): 1698 passed, 1 skipped. 4 errors came from a missing cargo on PATH. With cargo on PATH they passed.
    • launcher, run flags, island entry, driver and HF export (230 files): 4332 passed. 2 failures came from new help and error texts. After the fix the affected files passed.
    • tests/test_rl_hf_export.py: 20 passed, 1 skipped (the skipped test passes with Miles on the path).
  • With Miles 59aea07f9 on the path (25 files): 335 passed, 14 failed. All 14 failures have known causes:
    • 6 upstream parse tests need sglang_router.
    • 1 manifest probe test fails on its evidence path check.
    • 1 test needs ray.
    • 3 tests fail because the Miles tools package hides the yeto tools package.
    • 3 tests in test_neutral_rewards fail the same way at the merge base 6504d537.
  • 718b2f14: 25 targeted files (Modal store, remove_tree, HF export, release store): 483 passed, 3 skipped, 0 failed. The full suite did not run on 718b2f14.
  • After the ten merges from other draft PRs (467d1d91), the remote full suite had 7202 passed and 0 failed. On the same host, 869078a5 had 2 failures in tests that depend on timing.

Known limitations

Results:

  • Each GPU result is one run with one seed.
  • At about the same wall time, the single unsynced island (B final) scored higher than the 2-island global model (A mid). A was ahead only at its final point, with more rounds and samples. At an equal number of rounds per island, we infer that A is close to B, possibly 1-3 points lower. This is an interpolation, not a measurement.
  • A two-island merge moved the weights about as far as a one-island merge (median norm ratio 1.07). We infer that the second island mostly lowers variance and does not double the progress per round.
  • The syncer merged about 5 times per island round, not 8. Most pushes carried 10-11 local steps, not 8. We infer that the cause is the step order of the merges. We did not check it.
  • 58 of 768 merges had one island. They fell at the start (grace not calibrated yet), during the round-56 HF export and after the mid-run checkpoint. 12 of them have no matching late push. The cause is not known.

Sync and layout:

  • The syncer writes a checkpoint on its main loop (233 s mid-run, 248 s final). It does not merge during the write.
  • DP > 1 full-parameter sync needs the non-distributed optimizer. In the closing run the actor step took 13.5 s at DP8, and 7.6 s on the unsynced island with the distributed optimizer.
  • The trainer GPU memory peak in the closing shape is 139.7 of 140 GiB at 8192 tokens per GPU.
  • The background push applies only to codecs that encode ahead (no int4 error feedback, no stochastic rounding).
  • The lead cap (--rl-sync-max-local-rounds-ahead) counts the island's own unmerged pushes. With a deep pipeline this makes the rounds synchronous (H = 1). It is off by default.
  • No tool in this PR converts the syncer's final checkpoint into an HF model. The closing run used a script outside this repository.

Shapes not tested on GPU:

  • A trainer that spans nodes with sync. The 120B island with sync (272 fragments, offline layout 05c1fc0c…). Only the 120B one-island probe without sync ran.
  • Island relaunch and peer recovery in the closing shape. Run A allowed them (--modal-retries 1 --recover-timeout 1200, peer recovery on), but no island exited.
  • 262k-token sequences on two islands. Only one island without sync ran at 262k.
  • Context parallel > 1 with Mamba sequence packing.

Refused or missing:

  • The launcher refuses sampler islands together with full-parameter DiLoCo trainer islands. Three launch rules conflict:
    1. The full-parameter preset needs a fixed partition.
    2. A fixed partition needs engine GPUs.
    3. A trainer island must have 0 engine GPUs.
  • One island cannot mix GPU types.
  • For Nemotron-H the default tokens per GPU is 8192 x TP. At TP8 this gives 65536, so a 120B run must set --rl-max-tokens-per-gpu 8192.
  • verl writes no rl_round_data_rows and cannot export HF rounds. verl has the Nemotron-H parameter rules and a smoke tool only. Full-parameter multi-island RL runs on Miles only.
  • --local-rl-rounds-per-sync has no effect on a single island.
  • The FP8 rollout tiers 1 and 2 on Miles need SGLang 754605964 and a new image. The current pin does not have them.
  • Fork and image references still point at michaellchung. The move to agentenv needs the Miles commits and the image in agentenv first.

Unsynced and HF export paths:

  • B's launch ended with rc = 4. After its final export, B committed a round cut in its finish phase. No training step came for 900 s, so the stall guard of the run script stopped the app.
  • The launcher adds --rl-resume-store to an unsynced full-parameter island. So run B wrote 3 round cuts of 473 GB each (about 140 s per save). 2 of 3 cut commits hit the 600 s timeout.
  • An unsynced full-parameter island still computes a full digest every round (about 8.5 s).
  • B's train phase had about 13 s per round that the Miles timers do not explain. The cause is not known.
  • The 718b2f14 fix has CPU tests only.
  • We infer that a relaunched synced full-parameter island counts its export rounds from 0 again.

Merged branches:

  • Git treats s26-e1-overhead and s26-agentenv-migrate as merged. The first merge kept the tree unchanged, so the default cut interval of 4 is not in the tree. The second merge took only the origin compatibility. A later change that wants the interval of 4 or the reference move must make it by hand.
  • The one-commit release writer caches LATEST and assumes one writer per release directory. We infer a risk of a duplicate sequence number after a fenced publisher restarts. No test covers this.

Other:

  • The launcher counts a Modal app that is still "stopping..." during teardown as a failed head job (rc = 1 after a successful run).
Earlier content of this PR (head 869078a)

Nemotron-H async full-parameter RL

  • Nemotron-H recipe (Miles). yeto/rl/adapters/miles/models/nemotron_h.py writes the Megatron model flags from the Megatron-Bridge HybridModelProvider. RL keeps the routing statistics fixed (--moe-router-bias-update-rate 0, --moe-aux-loss-coeff 0). The backend registry loads the recipe (role nemotron_h_recipe), so the import boundary check passes.
  • Ungated experts. The expert partition check accepts squared-ReLU experts, whose fc1 has FFN rows instead of 2 x FFN.
  • Hybrid parameter rules and FLOPs. yeto/rl/hybrid_params.py names the Mamba fp32 parameters, the frozen vision tower and the 512-expert layout. yeto/rl/hybrid_flops.py gives the Nemotron-H model FLOPs for MFU. A verl Megatron GRPO plan and tools/nemotron_verl_smoke.py are included. The verl path is LoRA with one GPU per island.
  • Per-fragment apply. An island applies a merged fragment by touching only that fragment. It skips fragments that the masters already hold (skip_resident).
  • Deferred capture and batched applies. A local round asks Miles for descriptors only. The island materializes only the fragments that the next push needs. Distinct fragments of one partial-quorum commit go to the trainer in one apply. All three changes are bit-identical to the old path in tests. YETO_FULL_PARAMETER_DEFER_CAPTURE=0 and YETO_FULL_PARAMETER_BATCH_APPLY=0 turn them off.
  • Wait timeouts, larger pipelines, terminal retire. --rl-sync-wait-timeout-s (default 900) sets the island wait. Every push restarts the wait. The async fragment pipeline limit is 60. After an island trains no more, the island closes the last round of a fragment without a trainer apply.
  • Island lead cap (opt-in). --rl-sync-max-local-rounds-ahead K makes an island wait at a boundary instead of training on its own unmerged push.
  • Sync diagnostics. Each Miles trainer-group call prints its elapsed time. Each push prints its delta, error-feedback, encode and enqueue seconds. Each island prints its host facts.
  • Trainer speed flags. YETO_MILES_TRAIN_EXTRA_ARGV appends allow-listed Megatron flags. None of them changes the parallel layout.
  • Long sequences. --context-parallel N reaches the island, the trainer layout (DP = world / (TP x PP x CP)), the placement request and the Miles --context-parallel-size.
  • Numeric answer reward. yeto.rl.numeric_answer_reward:score (numeric_answer) grades one-number labels from the last \boxed{} or Answer: line. On 153,210 recorded responses the old gsm8k_grade credited 962 wrong answers and missed 200 right ones. gsm8k_grade did not change.
  • Modal container-set guard. The guard maps containers to island slots from each island's own container line.
  • Observability flags. --observe-training-entropy and --mamba-training-ssm-states-dtype reach the Miles argv.

How it is wired

  • One syncer. A fragment-round server holds a fixed roster and an island pool. Islands join, leave and re-JOIN under a lease. A syncer-epoch fence rejects stale islands.
  • Sync mode. --rl-sync-mode async|strict. Async is the default for multi-island RL. Strict is the control mode. --rl-sync-max-base-lag counts rounds.
  • Full-parameter async sharded rounds. Policy versions, catch-up and quorum start are in place. --syncer-shards shards the head.
  • Late-push carry and grace pacing. In async mode a late push goes into the next round. Grace pacing starts from every push.
  • Island pool with peer recovery. --rl-island-state-source {peer,catch-up,cut} sets where a new island takes its training state. peer copies parameters and inner optimizer state from a healthy peer. catch-up pulls all fragments from the syncer with a fresh optimizer. cut is refused for synced islands. Peer recovery is on by default where it applies.
  • Stop condition with a sharded head. An island stops local training only when the last round of every fragment is pushable or closed.
  • Rejoin catch-up pacing. The syncer sends a rejoin catch-up one fragment per scheduler pass.
  • Island disk and Ray storage. --modal-ephemeral-disk-gib, --rl-island-ray-object-store-gib and --rl-island-ray-spill-dir set the island disk and the Ray object store.
  • Head memory. Pushes fold in a streaming weighted fold. Checkpoints are sharded and use copy-on-write. --rl-sync-checkpoint-every sets the checkpoint interval for async rounds.
  • Push compression. Pushes can use int4 or int8. Islands can keep error feedback.
  • Distributed optimizer and CPU offload. At DP = 1 the full-parameter sync works with the Miles distributed optimizer and optional optimizer CPU offload.
  • MoE. The sync layout handles expert parallelism. Optional per-expert routed-token weights apply to the merge.
  • Round cuts. Synced islands cut rounds in multi-island runs. Offline cut evaluation includes cut_to_hf with a TP/PP/EP merge.
  • Sampler. The full-parameter sampler has an engine partition. SGLang applies ordered weight updates during generation.
  • Outer optimizer. An optional delayed Nesterov optimizer runs in the syncer.

Earlier GPU validation (Nemotron-3 Nano 30B-A3B, 2 islands x 4 x H200, Miles, full parameter, async)

Run Code / image Result
s27-gr-f fdd468ab First run to the final cut. Each of the 60 fragments merged once with both islands. Both islands acknowledged the final cut.
s27-gr-g2 2b25c30f, image aa6b887-2fa8801 120 outer merges, all with both islands. Health: 13 PASS, 1 WARN, 0 FAIL.
s27-gr-h2 lead cap K=1 Both islands acknowledged the final cut. The lead cap made the rounds synchronous.
s27-gr-i2 abac7785, image aa6b887-2fa8801 180 merges, all with both islands. Health: 12 PASS, 2 WARN, 0 FAIL. Logprob gap up to 0.118 (Mamba packing leak).
s27-gr-j 869078a5, image d1616ba-2fa8801 (Mamba seq_idx fix) 180 merges, all with both islands. Health: 13 PASS, 1 WARN, 0 FAIL. Logprob gap 0.010-0.013. Train/rollout KL at most 0.0005. Both islands acknowledged the final cut.
  • Mamba conv/SSM state leaked across packed samples in training before Miles d1616ba12. Runs before s27-gr-j trained with the leak.
  • Long sequences (one island, 8 x H200, TP2 x CP2 / EP4): 262,144 tokens ran for 4 steps with PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True. Peak training memory was about 138 of 140 GiB.
  • With optimizer CPU offload, island host memory peaked at 1034-1155 GiB.

Earlier testing (head 869078a)

  • cargo test: 226 passed.
  • Full CPU suite without Miles: 7406 passed, 52 skipped, 3 xfailed, 1 failed. The failure is a 10 s receive deadline in test_delta_protocol.py. The same test failed on the base and passed 3 of 3 times alone.
  • Targeted tests with Miles on the path: 259 passed, 1 failed (test_rda_manifest_probe, a known evidence-file problem).
  • The LoRA and Qwen3 full-parameter GPU smoke checks of earlier updates still apply.

Draft PR. Review is not requested yet.

🤖 Generated with Claude Code

chowhsu and others added 30 commits October 20, 2026 16:13
…ced trainers and their samplers

- modal_runner: on a multi-node island rank 0's runner listens on a small relay
  port; a rank>0 runner that gets SIGINT/SIGTERM sends the signal there, and
  rank 0 writes the reclaim marker its driver watcher turns into the store's
  preemption notice (signal names the source rank).
- sample_roles: a synced group (--rl-sample-role trainer with a syncer plus
  --rl-island-sample-role M:sampler) starts K synced trainers (learners 0..K-1,
  lanes K) and no-sync sampler islands over the same store in one launch; the
  samplers split K x total-steps rounds, take data slots and buffer shards.
- launcher: the syncer counts only the trainers of a synced group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mes the rank

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Engine sampler islands: the launch layer writes rollout_quant {tier, apply}
  into the sampler spec (stage1.sampler_spec); the entry applies it on the
  node (sampler_engines.quant_tier). Tier 1 on sglang-server fills
  --quantization fp8, --kv-cache-dtype fp8_e4m3 and SGLANG_FP8_IGNORED_LAYERS
  from the checked online plan; vllm-server is refused (stand-alone vLLM online
  FP8 has no ignore list). Tier 2 converts the base to the FP8 scheme and sets
  quant_scheme, expected_format, release_dir and the routed-expert count. A spec
  that already quantizes is refused. Stage 1 refuses a tier on top of its own
  quant section.
- Post-load expert_count: an expected count of 0 (dense model) passes without
  an engine report; the release index count is checked before the load.
- requant: drop a tied lm_head.weight that an exporter writes although the
  base ties it to the embeddings (tie_word_embeddings).
- Tests: the ignore list reaches the process started by Miles'
  launch_bound_subprocess; Miles' convert_qwen2_to_hf names equal the
  Qwen3-4B-Instruct-2507 index names (fixture from the real index).
- Probe (Modal CPU, verl image): Hydra overrides -> RolloutConfig -> verl's
  CLI builder -> vLLM 0.29 argument parser -> Fp8Config. The received
  ignored_layers equal the sent list; qkv_proj and o_proj skipped,
  gate_up_proj and down_proj quantized; kv_cache_dtype fp8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Importing it as a package put the real miles package into sys.modules and
broke a later test that fakes miles (test_rl_reward_success).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eral samplers)

The verl buffer trainer opens its buffer with create_index=False. With named
shards no sampler writes index.jsonl, so the first merge failed with
FileNotFoundError (S26 E9 G1 on Modal). open_trainer_buffer now creates the
index whenever this trainer merges shards: it is the index's only writer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sampler that writes its own buffer shard counts its backlog from the
release's buffer_shards. The verl publisher released only buffer_selected,
so on Modal (S26 E9 G1) both shard samplers stayed at their backlog cap and
the trainer timed out after four rounds. The release (and the release log)
now carries the read cursor per shard, as the Miles trainer does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alsy)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a buffer trainer the queue is the sample buffer. When the samplers ended
and a round could not be filled (groups passed over as older than
max_policy_age), FullyAsyncRolloutPool raised "rollouter ended the queue" and
the island left INFRA_FAILED (S26 E9 G1 on Modal, launcher rc=4). The buffer
ports now mark the pool so that end raises SampleBufferExhausted, which the
driver turns into rl_buffer_exhausted and a FINISHED run (as on Miles).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inalized

The launcher requires every island tape to end with rl_learner_finalized; a
verl pair's sampler islands run the engine sampler, whose tape never had it,
so a run whose islands all succeeded exited 3 (S26 E9 G1 on Modal). A
finished incarnation now writes it; a reclaimed one does not (the next
incarnation continues the island).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The launcher completes a Modal island's event tape from the island's tape
file and accepts only records with island_id. The verl pair's sampler islands
write engine sampler records (sampler_id only), so every record, the new
rl_learner_finalized included, was discarded and the run exited 3 although
all islands succeeded (S26 E9 G1). The verl sampler spec now names the
island (island_id = learner id) and the tape writer adds it to each record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With --rl-max-policy-age > 0 a round cut asks the rollout pool for its
in-flight work. FullyAsyncRolloutPool had no export_in_flight, so every cut
of a fully_async verl island was refused (RebuildRefused; S26 E9 G2 on
Modal: rl_round_cut ok=false for each round). A round keeps or drops every
group it pulls, so nothing pulled is in flight at the cut; the groups still
queued in the rollouter are generated again after a resume from the data
cursor. The export is now empty and the cut is written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dal verl islands default NCCL_SHM_DISABLE=1

The sync path pinned trainer.n_gpus_per_node=1, so a 2-card container trained
on one rank (S26 E9 e9-g3-a). The island now takes the card count from its
island-local Ray. A 2-card verl island on Modal failed attaching NCCL shared
memory (e9-g2-a); the launcher sets NCCL_SHM_DISABLE=1 for multi-card verl
Modal islands unless --modal-env sets it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…samples record their data row

e9-g4-a: both Miles samplers drew the same 16 prompts (prompt text overlap
16 of 16/24). DATA_SLOT_ENV was set in the island process but missing from
ISLAND_ENVS, so the rollout worker never split its data source. Each kept row
now also records its index in the unsplit data as metadata["yeto_data_row"],
which reaches the sample buffer payload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a 2-card verl sync island (e9b-g3-a) both vLLM replicas wrote
readback-v0.json through the same tmp name; the second os.replace raised
FileNotFoundError inside vLLM and the weight update failed. The tmp name now
carries the pid, and a write failure is printed instead of raised into vLLM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… islands

cmd_head sized the head-local syncer with every --gpu island, so a synced group
(2 trainers + 1 sampler, e9b-sync-a) waited for 3 learners and the trainers
hung after connecting. It now uses sample_roles.syncer_learner_count, the same
count launcher.run uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ason

The verl Megatron engine (the only verl path with EP) is not wired into
the yeto adapter; EP runs use the Miles backend. check_expert_parallel
now names the decision and the backend to use, and stage1 refuses a
verl trainer.verl.strategy other than fsdp2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The strict and decoupled presets set --quorum to every --gpu island; in a
synced group (2 trainers + 1 sampler, e9b-sync-b) that is quorum 3 over
--learners 2, and the syncer exits at start. Both now use
sample_roles.syncer_learner_count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…imings (S26 E8)

- BoatSandbox records per-phase start timings (create, ready, pull, run)
  and polls readiness every 0.5 s instead of 2 s.
- BoatSandboxPool recycles a finished VM (containers, volumes, networks
  and staging removed, then checked empty) and hands it to the next task
  with a fresh container from the image; idle TTL, idle cap, reuse cap,
  prewarm. BoatEnvironment uses it with pool=true or YETO_BOAT_POOL=1
  (off by default); a VM with a failed exec or transfer is deleted.
- HarborHarness retries a trial that failed before the agent ran
  (sandbox start, image pull, Codex install) up to HARBOR_SETUP_RETRIES
  times (default 0) with jittered backoff; failures after the agent
  started, policy-limit, config and quota errors are never retried.
- BoatQuota can also hold the hourly start cap
  (YETO_BOAT_STARTS_PER_HOUR, unset by default); create retries jitter.
- scripts/boat_startup_bench.py measures cold vs pooled rounds and the
  Codex install (Harbor install_only) on real boat sandboxes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stall (S26 E8)

With codex_cache on (YETO_BOAT_CODEX_CACHE=1 or provider kwarg
codex_cache=true; off by default), each boat VM fetches the npm
linux-x64 build of Codex 0.145.0 once, checks its sha512 (npm
dist.integrity), unpacks it under /var/cache/yeto-codex/<version>
beside the image pull, and the task container gets it read-only at
/opt/yeto-codex with /usr/local/bin/codex linked to a wrapper. Harbor's
codex agent then finds the pinned version and skips apt + nvm + npm.
The task image is not changed. A failed fetch falls back to Harbor's
own install. Pool recycling keeps the cache; a reused VM takes the new
task's cache choice. The bench script gains --codex-cache and records
`codex --version` as the task's default user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ompactionRL refusal

Suspension between rounds (Miles stage 3 and verl fully-async):
- yeto.rl.agentic.suspend: TurnGate (one per event loop) and SuspendLedger;
  the outer wall-clock cap does not count suspended time; a time limit hit
  after a suspension is ABORTED, never a trainable zero.
- Miles: harbor_agent.suspend/resume hooks for --agentic-suspend-between-turns;
  the agent dials a GateRelay (gateway/gate_relay.py) that holds model calls
  at the closed gate, resends calls the engines aborted at the cut-off, and
  keeps held streams alive with SSE comments. Switched on by
  YETO_HARBOR_GATE_RELAY_PORT; the island and the hook fail closed without it.
  The Harbor agent function is now suspend-capable for policy_age.
- Cut: suspended trajectories are exported as reference-only entries
  (agentic-suspended:<id>); restore discards them as agentic_session_lost
  (the sandbox and session die with the process). Legacy codex-suspended
  references are still recognised.
- verl: VerlRolloutEngine resends a tracked trajectory's calls that the
  rollouter aborted at a weight update (bounded by YETO_VERL_ABORT_RETRY_S),
  counting the waits as suspended time.

TB2 task lists on Harbor task directories (task_lists.py, tools/reward_env/tb2_lists.py):
- hold-out and training split with the recorded WP3 rule; byte-identical to
  data/eval/tb2-holdout.json and tb2-train.json on tb2@2fd12b8; TB2.1 layout
  supported.
- curriculum stages from official difficulty and time estimates, optional
  reference-solution results and measured pass rates (require_signal drops
  tasks with pass rate 0 or 1).

CompactionRL: not supported on the Harbor path; the records-proxy recorder
refuses a compacting proxy, the launch check documents the decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…2.1 dataset rows

- Gate relay: YETO_HARBOR_GATE_RELAY_PORT takes N, N-M (each rollout
  process on a machine binds the first free port of the range) or 0; a range
  with a fixed public base URL is refused. Multi-process test.
- AgentTrajectory.extra_metrics carries the suspension counters, so an
  ABORTED trajectory without a verdict (wall-clock cap, lost sandbox) reports
  them too; the verl and Miles reclassifications keep them.
- task_lists: Miles and verl dataset rows in the formats the adapters read
  (checked with their real parsers); tb2_lists.py rows/manifest commands.
- data: TB2.1 hold-out, training list, curriculum (data/eval/tb2.1-*.json)
  and Miles/verl rows of the training list and curriculum stage s1
  (data/tb2/tb2.1-*.jsonl), with sources and sha256 in
  data/tb2/tb2.1-data.sha256.json.
- tests: Miles-image test of the suspend hooks through Miles' own hook
  loader; the reward-hook test accepts Miles' Status enum; the cold-start
  test no longer reads the caller's HF_HUB_CACHE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bor's install (S26 E8)

Harbor's codex install also leaves curl and ripgrep in the task
container, and many TB2 verifiers call curl. On a cache hit the
container now gets rg from the Codex bundle (when the image has none)
and curl installed the way Harbor installs it (apk/apt-get/yum, only
when missing). The codex link is made last; if any step fails it is
removed, so Harbor falls back to its own full install. Tests run the
setup script against a fake root and check codex, curl and rg match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oes (S26 E8)

Harbor's codex install leaves node/npm in the container (nvm install 22,
or apk nodejs npm on musl), and some TB2 verifiers run node. The VM
cache now also holds the official Node v22.23.3 linux-x64 build
(sha256-checked; the newest 22.x on 2026-10-21, what nvm install 22
resolves to today). On a cache hit, a container without node/npm gets
them linked from the cache, or apk-installed on musl images. If node
does not run (e.g. too old a glibc), the codex link is removed and
Harbor falls back to its full install.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g, BF16 KV)

- scope.with_fused_aliases: the tier 2 compressed-tensors ignore list also names
  qkv_proj / gate_up_proj when all shards are ignored. SGLang 2fa88018 matches
  fused names only through packed_modules_mapping, which Qwen3ForCausalLM does
  not set, so qkv_proj got the FP8 scheme on the card (e6-gate-t2).
- gate: score by token ids when the engine can (SGLangServerEngine.score_tokens);
  text scoring dropped the trailing EOS and left 96% of completions unaligned.
- tier 1: KV cache stays BF16 (SGLang and vLLM). Uncalibrated FP8 KV failed the
  gate on Qwen3-4B (p99 |diff| 0.82 vs 0.46 with BF16 KV, limit 0.5).
- tools/probes/s26_e6: Modal gate and verl RL drivers, a lock wrapper, and an
  image overlay for the SGLang fork's FP8 activation cast in Qwen2/Qwen3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…obes

- verl max_model_len = prompt + response: Qwen3-4B-Instruct-2507's 262144
  context needed 36 GiB of KV next to the actor and vLLM refused to start.
- Tier 1 on verl is refused at launch and on the island. With verl's FP8
  config vLLM treats the BF16 checkpoint as FP8-serialized and generates
  garbage until a base sync re-quantizes it; yeto verl is LoRA-only, so the
  base is never sent (e6-t1-verl-a-r2: 512-token answers, NaN log-probs;
  e6-vllm-fp8-probe). Plain vLLM online FP8 works with LoRA but quantizes
  attention, which failed the gate as SGLang online FP8 of every linear.
- Gate driver: framework-native variants (Miles block FP8 converter, SGLang
  online FP8 without an ignore list); vLLM probe; verl metrics parser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ative + ignore list)

- Pins: SGLang fork 10afe11a7 (Qwen2 MLP / Qwen3 attention no longer cast the
  activation to an FP8 projection's dtype), Miles fork 9462de532 (quantize_params_fp8
  and convert_hf_to_fp8 honor the FP8 ignore list; --rollout-hf-checkpoint), image
  yeto-miles-ports:9462de5-10afe11 @sha256:6e0a97d2. Build record under
  openspec/changes/rl-infra-spec/evidence/ports-image/2026-10-21-9462de5-10afe11/.
- make_layer: Miles tools/ scripts may change in the overlay (nothing is built).
- --rl-rollout-quant-fp8-source {engine,trainer} (tier 1): "trainer" starts the
  SGLang engines on Miles' 128x128 block-FP8 conversion of the base with our
  ignore list in BF16 (entry.prepare_native_fp8_base, scope-checked by
  tiers.check_native_base) and Miles block-quantizes every weight update by that
  config. No SGLang online flag or ignore-list environment in that case.
- tools/probes/s26_e6: image check on a Modal CPU container; gate variant
  miles-native-block-ignore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sampler-snapshot G-L8 records stay at SGLang 2fa880182, where they were
verified; the new pin is refused for that opt-in mode until G-L8 is re-run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chowhsu and others added 30 commits October 24, 2026 06:06
…weights upload

A sampler at its backlog cap starts its next round only when a release records
that the trainer read its previous round. A staged release read the position
when the trainer exported (before the next step took a round), so the cap lifted
only with the release after it: one whole trainer step later.

release_staged now uploads the weight files, then asks position() and puts the
record into the release directory and LATEST in one store write (put_files).
The upload thread reads the position through MilesRolloutPool
._in_executor_from_thread (ray.get on the reference; the driver's event loop may
be running on the main thread).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A staged release is written by every rank (release_plugin.write_full_checkpoint).
With YETO_RL_RELEASE_EXPORT=miles the trainer still stages and uploads in the
background, but Miles' export_hf (rank 0 writes) produces the files: a fallback
for a run where the all-ranks writer fails, without a code change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…range

In s27-at-a a sampler island's SGLang engine failed to bind its HTTP port in 3
of 3 island starts ("[Errno 98] ... address already in use"); Miles then waits
an hour for the missing engine. A Modal (gVisor) container hands out ephemeral
ports from 16000-65535, which covers the ports Miles probes free from 20000 for
engines, routers and the trainer master and binds later: a connection opened in
between (NCCL, torch c10d, a health check) can take one. A CPU probe in a Modal
container: about 1.5% of outgoing connections got a local port in 20000-20999,
and writing ip_local_port_range takes effect.

Before the setup script, the island now sets the container's range to Linux's
default 32768-60999 when it starts lower, and logs the change; a range that
cannot be read or written is reported, not fatal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Megatron's moe_freq_type only accepts a list pattern made of digits,
commas, brackets, +, * and parentheses (_eval_pattern). The Miles argv
used str() of the provider's per-layer mask, which has spaces, so the
Nemotron-H 120B island failed at argument parsing (s27-aw-a:
'argument --moe-layer-freq: invalid moe_freq_type value').

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ter islands keep their row

Same-card training islands of a multi-island launch read every row of --data in
the same order, so in every round each island trained the same prompts, and a
relaunched full-parameter island (its round counter restarts at 0 in every
process) read its data from the first row again.

Data split (yeto.rl.engine.data_split, backend-neutral):
- --rl-data-split auto (default): the island with learner id k of N reads the
  rows i with i % N == k, in data order; the launcher sends --rl-data-slot k/N
  and the island keeps those rows when it writes its prompt file (Miles: local
  JSONL and Hub datasets; verl: the train parquet). Kept rows carry their row
  number as yeto_data_row. --rl-island-tasks overrides; shared keeps the old
  behaviour and warns loudly; external learners fall back to shared with the
  same warning; overlapping task lists warn too.
- Passes: pass 0 is data order, every later pass is reshuffled with a seed from
  --seed and the pass number (Miles, data_passes); verl keeps its own sampler.
- The launch prints the rule, the dry-run plan and the run manifest carry it,
  rl_driver_start records the island's slot. DataSlot moves here from
  sample_roles (re-exported); sampler islands keep their own slots.

Data position (yeto.rl.engine.data_position):
- A synced full-parameter island with --rl-checkpoint-store gets
  --rl-data-position-store and records its data cursor after every sync
  boundary (directory or object store, background commit). A process that joins
  a running pool (FullParameterSync.joined_running_pool: fragments newer than
  version 0, or a peer recovery) seeks to the last record; a process that starts
  the run writes a start record. An island pool without a store warns at launch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On an island that reads a row slot, every rl_trajectory_reward record (tape,
--rl-observe-timeline) now names the sample's row number in the unsplit --data
(data_row), so a run can check that no row is trained by two islands in one
round, also for data without task ids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/eval/data_split_check.py reads every island tape of a run and checks that
the islands hold distinct row slots, that no row is trained by two islands,
that every trained row lies in its island's slot, and that a relaunched island
restored its data position and did not start at a row of a round an earlier
process brought to its sync boundary. For the next multi-island GPU run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ps the round

A bucket mount (and an object store) cannot append, so the data position log
writes one new file per record under data-positions/island<id>/ in a store
directory, as it already did by object key. A record the store refuses goes on
the tape (rl_data_position_write_failed) and the island trains on: losing it
only makes a relaunch read some rows again. Commit failures at the end of the
run go on the tape too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lies

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The control test compares the dry-run plan of today's launcher in strict mode
with the plan of the commit before the sync modes. The default data split now
adds --rl-data-slot to each island command and data_split records to the plan;
the test launches with --rl-data-split shared (every island reads every row, as
before) and drops the data_split records, so it still checks the sync modes only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engines-on-island full-parameter mode refused DP>1 because Miles listed
every parameter once per data-parallel rank and applied a merged fragment only
on the exporting rank. Miles yeto/s27-dp-sync (921cde8c7) gives every
parameter one owner rank per replica group (dense: DP x CP group; expert:
expert data-parallel group) and applies the owner's bytes on every replica,
so the layout lists each parameter once and all replicas stay identical.

- pins.py: MILES_DP_REPLICA_FULL_PARAMETER_COMMIT (not in the image yet).
- run_config_rules.full_parameter_dp_replica_gap: None once the pinned Miles
  lineage reaches it; run_config's engines-on-island rule, the elastic
  benchmark capability check and the Miles optimizer attributes use it.
- Miles optimizer attributes: a full-parameter sync at DP>1 keeps the
  non-distributed optimizer (whole FP32 masters on every replica) and refuses
  --rl-optimizer-offload-fraction (a partial distributed-optimizer range).
- moe_layout: the duplicate-expert error now names the Miles fix.
- rank_stats: export reports also show the replicas' export note.
- tests/test_s27_dp_replicas_miles.py: yeto's adapter on four fake Miles
  ranks (TP1/DP4/EP2) with the real Miles state, plugin and transport; every
  parameter is laid out once and every replica applies the merged bytes
  bit-identically (needs Miles with data-parallel replicas).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On 8 x H200 with real RL samples (Nemotron-3 Nano), TP1/EP8/DP8 without
optimizer offload trained at 9.7k tokens/s/GPU (19.4% actor MFU) with
selective recompute of the MoE and layernorm modules, 8192 tokens per GPU,
the DeepEP flex dispatcher with permute fusion and 512 samples per step.
The Nemotron-H recipe used Megatron's defaults (no recompute, fixed micro
batches, all-gather dispatcher) unless the island environment passed flags.

models/nemotron_h.train_flags now writes those defaults after the model
flags:
- --recompute-granularity selective --recompute-modules moe layernorm,
  unless --rl-activation-recompute keeps full recompute;
- --use-dynamic-batch-size --max-tokens-per-gpu 8192 x TP (never below one
  sample per packed micro batch), unless --rl-max-tokens-per-gpu is set;
- MoE: --moe-token-dispatcher-type flex --moe-flex-dispatcher-backend deepep
  when deep_ep is importable where the island builds its argv (inside the
  training image), else alltoall; --moe-permute-fusion either way.
Pass-through and YETO_MILES_TRAIN_EXTRA_ARGV flags come later in the argv
and win (the latter now also accepts --moe-flex-dispatcher-backend).

train_warnings prints a launch-time [rl] WARNING when an optimizer step
trains fewer than 256 samples, or when the whole optimizer state is offloaded
on a trainer of 8 GPUs or fewer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sampler island (--rl-sample-role sampler) builds no trainer, so it has no
FP32 masters for the full-parameter sync to read; its "DP" is just its GPU
count over TP x PP. The data-parallel replica check of the previous commit
refused such islands at DP>1 on the current Miles pin (the yeto-cpu-test
full suite caught it in test_rl_sampler_island). The check now applies only
to islands that build a trainer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Islands that generate and train on their own rows now read rows
i % N == k by default (--rl-data-split auto). Full-parameter islands
record their data position in the checkpoint store so a relaunched
island continues at its next row. Sampler islands keep DataSlot.

No conflicts: the branch is based on 3b64f08.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ining defaults

Full-parameter islands may run DP > 1 once the pinned Miles lineage has
the replica commit (921cde8c7); each parameter has one owner rank and the
replicas copy the owner's verified bytes. Nemotron-H defaults move to
selective recompute, 8192 x TP tokens per GPU and DeepEP when present.

No conflicts: the branch is based on 3b64f08 and touches no file that
s27-task-assign touches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… support

Brings the language-only VL bridge wiring, the latent MoE check, the
multi-node full-parameter island with dedicated engines, the offline
Nemotron-H layout tool and the space-free --moe-layer-freq mask
(b95e1f9, the s27-aw-a failure). s27-120b-gaw contains s27-120b-prep
(d7cfbbc), so one merge brings both.

Conflicts, resolved:
- pins.py, Dockerfile, test_rl_ports_image.py: keep this branch's Miles
  8edea35a7 and image 8edea35-2fa8801 for now. The 120B pin (Miles
  b7868074c, image b786807-2fa8801) lacks the zero-copy and DP-replica
  commits; a combined Miles commit and image are pinned in a later commit.
  The b786807-2fa8801 build record stays as history.
- models/nemotron_h.py: keep the 120B module text (MTP dropped by the
  Miles bridge, VL language-only bridge) and the G-AN training-default
  text; the EP note now says EP may span DP with a replica-capable pin.
  Imports are the union of both sides.
- run_config.py full_parameter_layout_problem docstring: engines on one
  node or on a multi-node island, and DP>1 with a replica-capable pin.
  The code merged cleanly: the DP>1 gate comes first, then the node rule.
- test_rl_nemotron_h_recipe.py: keep both sides' new tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Miles ebc6519af (michaellchung/miles yeto/s27-closing) merges the
closing-run fork commits: 8edea35a7 zero-copy chunks, 921cde8c7
full-parameter data-parallel replicas, d78bf39e4 pinned
HybridDeviceOptimizer CPU masters, and b7868074c, the language-only
bridge of the Nemotron-H VL checkpoint. SGLang stays 2fa880182.

- pins.py: MILES_NEXT_COMMIT, MILES_FORK_LINEAGE (+ b7868074c, 921cde8c7,
  d78bf39e4, ebc6519af), MILES_NEXT_IMAGE -> @sha256:1151c59a...
  The DP replica commit is now in the lineage, so a full-parameter sync
  at DP > 1 is accepted.
- Dockerfile ARG MILES_COMMIT; test build-record dir.
- evidence/ports-image/2026-10-24-ebc6519-2fa8801: build record and
  manifest (differs from 8edea35-2fa8801 only in Miles commit and tag).
- tests: the DP > 1 refusal tests pin a pre-replica Miles (8edea35a7)
  instead of relying on the current pin.

Image check (scripts/check_miles_ports_image.sh): 13 checks pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This reverts commit 4c702a5.

The integration keeps one rule for sampler islands: DataSlot (row i goes
to sampler island i % S), now in yeto/rl/engine/data_split.py. The start
offset of 4c702a5 on top of DataSlot would make a sampler island skip a
second block inside its own slot. Done before merging s27-disagg-fast
into s27-closing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er islands

Brings G-AO's one-commit sampler rounds and G-AT's fixes: narrow integer
buffer payloads (R3 expert ids as uint8) and parallel prefetch on the
trainer, staged full-weight releases uploaded in the background (no v0
release from a no-sync full-parameter trainer; --rl-release-inline keeps
the old path; YETO_RL_RELEASE_EXPORT=miles), the release record read
after the upload, no relaunch into a stopped Modal app, Modal island
ephemeral ports moved off Miles' port range, and a timed default
round-cut interval.

The merged side is s27-disagg-fast 4dc9aca plus a revert of 4c702a5
(sampler start offsets): sampler islands keep DataSlot only.

How it was merged: s27-disagg-fast carries its own copies of the S26 E4/E5
commits (e90be7b, da6e900, 2c94f60, 2754919; the same patches as
s26-e45). This branch already has s26-e45 with the S27 conflict
resolutions of the S26 integration (one-commit release writer under
hold_writer). So the merge keeps this branch's tree and applies only
2754919..(4dc9aca + revert), the G-AO/G-AT changes, as a three-way
patch. Conflicts, resolved:
- buffer_payload.py: keep both additions (SampleBufferExhausted from
  S26 E9.3 and the narrow-integer helpers).
- buffer_rollout_miles.py: keep the DataSlot import (apply_data_slot);
  the revert removes first_round_cursor, which this branch never had.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Miles ebc6519af pinned, a dense full-parameter layout at DP > 1 is
accepted (data-parallel replicas). The layout-rule, elastic benchmark and
config-schema tests now check both: the current pin accepts DP > 1
(including the closing shape, one trainer node at DP 8 with engine nodes
on a multi-node island), and a pin before 921cde8c7 (8edea35a7) refuses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The closing run trains Nemotron-3 Nano at TP1/EP8/DP8 with the full-
parameter sync, so its island layout hash must be known before launch.
rank_manifests(dp > 1) now lists each parameter on one rank, as Miles
921cde8c7 does: a dense parameter goes to the least-loaded rank of its
stage's data-parallel group (largest first, ties by name, then group
order); with EP = DP every rank owns its own experts. Scope at DP > 1:
TP 1, EP = DP. DP = 1 is unchanged (Nano TP2/EP2 still gives the
measured 60 fragments and 0ec93ba8...).

tools/nemotron_h_offline_layout.py takes --dp. Nano TP1/EP8/DP8 with
rda: 64 fragments, layout c2c3cb49... (offline; not yet checked by an
island on GPU). A test checks the owners against Miles' own
_replica_owners when the Miles checkout is importable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nous rounds

Every island downloaded the f32 final cut (about 4 bytes per parameter,
126 GB for a 30B model) before its final ACK, and the syncer sends it to
one island after another. That download set the end time and the cost of
an asynchronous full-parameter run, while the final parameters already
live in the syncer's final checkpoint.

- syncer --final-cut-payload {full,manifest}: manifest sends only
  FINAL_MANIFEST. Islands must expect it, so it is part of the semantic
  profile hash (domain appended only when on; existing hashes unchanged,
  Python vector checked against Rust).
- SyncerClient / ShardedSyncerClient.wait_for_final_manifest.
- launcher --rl-sync-final-cut {auto,ack,download} (full-parameter preset
  only): auto is ack for asynchronous rounds without --rl-eval-interval
  and download otherwise (the strict sweep keeps the full cut). ack adds
  the syncer flag, the profile field and the island flag
  --sweep-final-cut ack.
- docs: PROTOCOL.md 'Manifest-only final cut', MILES_RL.md finalization.
- tests that compare final f32 values ask for the download explicitly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…CK-only final cut

Boundary (S27 s27-ap-c: 55-65 s per round, mostly whole-model work):
- lazy capture: when the trainer's Miles has the lazy_export transport,
  a local round's capture reads no master (rows carry labels, the policy
  hash names the capture) and its receipt skips the whole-model
  model/master check. Each fragment that is pushed is hashed, NaN-checked
  and model-checked when it is materialized; the role stream keeps its
  content descriptor and the push reads the local chunks through it.
  Every YETO_FULL_PARAMETER_FULL_CHECK_EVERY-th round (default 16, 0 =
  never) still hashes every fragment and checks the whole model.
  YETO_FULL_PARAMETER_LAZY_CAPTURE=0 turns it off. Lazy rounds add
  sync/capture_lazy to the round event.

Startup burst:
- with H > 1 every fragment is anchored at the same round when a stream
  starts, so all fragments with a PULL became pushable at round H
  together. The first push from that shared anchor now waits
  fragment_id * H // P more rounds (the syncer opens rounds in fragment
  order); later pushes keep the H gate. YETO_FULL_PARAMETER_STAGGER_FIRST_PUSH=0
  turns it off.

Final cut:
- RoleStreamConfig.final_cut "ack": wait for the final manifest only,
  drain the pushes and ACK without applying a final cut (final_reached;
  final_applied stays for a downloaded and applied cut). The island entry
  takes --sweep-final-cut. The final ACK event carries sync/final_cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The strict sync mode is the control profile whose commands must not
change, so --rl-sync-final-cut auto resolves to download there. Pin the
new syncer flag literal in the run-flags schema test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s, ACK-only final cut

Brings G-AY's yeto and syncer side: the lazy capture (feature-detected
on the Miles transport), per-fragment materialize descriptors, a
whole-model check every 16 rounds, staggered first pushes from the shared
start anchor, and the manifest-only final cut (syncer
--final-cut-payload manifest, launcher --rl-sync-final-cut, default ACK
in --rl-sync-mode async without island eval). The branch head 0c4750a
contains 8cab8eb.

No text conflicts: the six files both sides changed (island_entry,
chunked_full_parameter, full_parameter_sync, run_flags, rl_island_task,
MILES_RL.md) merged in different places. The adapter calls stay
owner-level, so they hold with data-parallel replicas; the Miles side
of the merge (yeto/s27-closing 59aea07f9) adapts the per-fragment commit
and the replica export note to replicas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rl_trajectory_reward records stop at 256 samples per round, so a
round of 512 samples showed only half of its rows and the data split
check could not prove that the islands trained disjoint rows.

- rollout_meta_hook: with --rl-observe-timeline, the metadata of a round
  on an island that reads a row slot carries round_data_rows:
  {"samples": n, "rows": [[first, last, step], ...]}, the distinct rows
  of every trained sample as arithmetic runs. A pass-0 round of a slot
  is one run; a reshuffled pass needs at most one run per two rows.
- RolloutBatchHandle.round_data_rows (None when not reported; verl does
  not report it). The driver tapes one rl_round_data_rows per round:
  rollout_id, policy_version, samples, row_count, rows (+ the labels).
- data_split: row_runs, rows_of_runs, run_row_count.
- timeline: ROUND_DATA_ROWS_EVENT and validate_round_data_rows.
- data_split_check: per round, the round record when present, else the
  reward records (older tapes); new check round_rows (well formed, and
  holds every rewarded data_row of its round); rows_source counts the
  rounds of each island by source. Split into one helper per check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…07-2fa8801

Miles 59aea07f9 (michaellchung/miles yeto/s27-closing) merges
yeto/s27-sync-boundary into ebc6519af: 63cb372b3 per-fragment
full-parameter round boundary (lazy export, materialize returns fragment
descriptors, partial commit) and ccd85ec73 (test imports). SGLang stays
2fa880182. With this pin the island's feature check finds the Miles
lazy_export transport, so lazy capture is no longer switched off.

- pins.py: MILES_NEXT_COMMIT, MILES_FORK_LINEAGE (+ 63cb372b3 and
  ccd85ec73 before the DP replica commit, as a side branch without
  replicas; + 59aea07f9), MILES_NEXT_IMAGE -> @sha256:d54dd445...
- Dockerfile ARG MILES_COMMIT; test build-record dir.
- evidence/ports-image/2026-10-24-59aea07-2fa8801: build record and
  manifest (differs from ebc6519-2fa8801 only in Miles commit and tag).

Image check (scripts/check_miles_ports_image.sh): 13 checks pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An island without sync writes no full-parameter cut and yeto never calls
Miles save_model, so a run had no HF model of its middle or final rounds
for an evaluation container.

--rl-export-hf-rounds R[,R...][,final] (with --rl-checkpoint-store; Miles,
full parameter, sample role both, no --rl-elastic): at the sync boundary
of round R (the island's R-th trained round), before the next
publication, the island writes its own model to
<store>/hf/island<learner id>/round-R. "final" is the round whose
boundary stops the island: on a synced full-parameter island after the
final manifest arrived and the pushes drained, before the final ACK (the
syncer waits for the island within its final-ACK timeout and the launch
does not end during the export); without sync, the last round.

- engine/hf_export.py (backend-neutral): ExportRounds, HfRoundExports.
  The backend writes into a local staging directory and leaves its
  completion marker (.complete); the island takes the marker out, removes
  an older directory of the round, puts the directory (one commit), then
  puts the marker and, for the final round, hf/island<k>/final.json in a
  second put (second commit). A directory with the marker is complete and
  committed. One rl_hf_export tape event per export: round, final,
  policy_version, path, store_kind, ok, files, bytes, write_s, upload_s,
  seconds, error. A failed export is taped and the island trains on.
- driver: IslandDriver.hf_exports, called after the data position record
  of each round (phase hf_export).
- Miles: release_plugin.full_checkpoint_writer (every rank writes a share
  with the trainer on one node, else export_hf on rank 0), also used by
  the staged policy releases; entry.hf_exports_for; island flags
  --rl-export-hf-rounds / --rl-export-hf-store and their checks.
- launcher: validation (needs --rl-checkpoint-store; refused for verl,
  --rl-elastic and sample roles; WARNING for rounds past --total-steps x
  --local-rl-rounds-per-sync), forwarding of the rounds and <store>/hf,
  and the exports count as a use of the store on a synced launch.
- tests/test_rl_hf_export.py: CPU doubles, no Ray.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stdout carries the --dry-run plan JSON; the warning made it unreadable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…odel when an upload fails

A Modal Volume reports the removal of a missing path as InvalidError
"No such file or directory", which the store did not treat as missing,
so the first export of every round failed at its upload and the staging
copy was deleted with it.

- modal_ckpt_store: remove_tree removes, and on any failure looks the
  path up (listing each parent from the Volume root down); the error is
  raised only when the path is still there. The runner-interpreter script
  is built from the same two functions.
- hf_export: the upload (remove, put, marker) runs up to 3 times (waits
  10 s, 30 s); the tape gets upload_attempts and upload_errors. When the
  last attempt fails the staging copy stays with its marker put back
  (staging_kept), at most 2 per island process (staging_dropped).
- tests: a Volume fake that raises "No such file or directory" for a
  missing path; retries; kept and dropped staging copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants