Repository navigation
rl/syncer: one syncer with async multi-island sync for LoRA and full-parameter RL - #292
Merged
Merged
Conversation
…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>
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>
michaellchung
marked this pull request as ready for review
October 26, 2026 14:58
This was referenced Oct 26, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
869078a5to718b2f14. The new head adds these parts:--rl-export-hf-rounds, which writes an island's model as HF directories, and a fix for its upload to a Modal Volume.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).718b2f14adds only the HF export fix. That fix has CPU tests only.What the new head merges
The first-parent history from
869078a5to718b2f14has these merges and commits.Branches from other open draft PRs. Where a branch conflicts with this PR, the merge keeps this PR's behavior.
s26-e04-imgasserts26-e7-verl-megatrons26-e45--rl-sampler-fused-stage). Scale report tools.s26-e1-overheads26-e6-quant--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-ablations26-e2-async--rl-async-buffer-rounds. With overlapped generation the buffer depth defaults to--rl-max-policy-age. An optional adaptive TIS.s26-e9-robusts26-agentic-merges26-agentenv-migratemichaellchung.Feature branches and direct commits:
s27-sync-overlap(922f3880)s27-task-assign(f9318d15)tools/eval/data_split_check.py.s27-dp-sync(d93c97fe)s27-120b-gawwiths27-120b-prep(b95e1f96)--moe-layer-freqmask without spaces.s27-disagg-fast(4dc9acac, with4c702a55reverted)s27-sync-boundary(0c4750a2)3b0bafc6--dp).63afafafrl_round_data_rowstape event.76f067d3,72e9eef4fadbffc3,33cce368--rl-export-hf-rounds. The warning goes to stderr, so the--dry-runJSON stays valid.718b2f14Behavior changes and new options
Asynchronous full-parameter rounds
YETO_FULL_PARAMETER_BACKGROUND_PUSH=0keeps the synchronous push.YETO_FULL_PARAMETER_PUSH_WORKERS(default 3) sets the number of workers.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.YETO_PUSH_ENCODE_THREADSthreads (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).lazy_exportfeature, 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=0turns the lazy path off.fnow waitsf * H // Pmore rounds. Later pushes keep the H gate.YETO_FULL_PARAMETER_STAGGER_FIRST_PUSH=0turns this off.--final-cut-payload {full,manifest}. New launcher option--rl-sync-final-cut {auto,ack,download}(full-parameter preset only).autogivesackin--rl-sync-mode asyncwithout--rl-eval-interval. It givesdownloadin all other cases. The strict mode keepsdownload, so its commands do not change.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).Full-parameter sync at DP > 1
921cde8c7gives each parameter one owner rank per replica group. The replicas apply the owner's checked bytes. The layout lists each parameter once.921cde8c7.--rl-optimizer-offload-fraction.8192 x TPtokens per GPU, and the flex dispatcher with DeepEP when DeepEP is present.tools/nemotron_h_offline_layout.py --dp Ncomputes the layout hash before launch (scope: TP 1, EP = DP). Nano at TP1/EP8/DP8 gives 64 fragments and layoutc2c3cb49…. An island on GPU accepted this hash.Engines on a multi-node island
--rl-island-gpus-per-node). The launcher lays out the trainer and the engines over the nodes, as for a LoRA fixed partition.ip_local_port_rangeto 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".Data split and data rows
--rl-data-split auto(new default, for N >= 2 training islands). IslandkofNreads the rowsiwithi % N == k. The launcher sends--rl-data-slot k/N.--rl-island-tasksoverrides the split.sharedkeeps the old behavior and prints a warning. External learners fall back tosharedwith the same warning.Passes. Pass 0 reads the rows in data order. Miles reshuffles each later pass with a seed from
--seedand the pass number.Data positions. A synced full-parameter island with
--rl-checkpoint-storegets--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_rowsevent. The driver tapes one event per island per round when--rl-observe-timelineis 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:The tool prefers
rl_round_data_rowsand falls back torl_trajectory_rewardfor older tapes.Nemotron-H Super VL
NemotronH_Omni_Reasoning_V3VL config as its language model. The FLOPs count only the language model. The frozen vision names now includevision_projector.*andsound_model.*.MILES_NEMOTRONH_KEEP_MTPis set). It reads a VL checkpoint as its language model and does not build the vision part.models.pyregistersnemotron35-super-vl(249 GB).--rl-export-hf-rounds--rl-export-hf-rounds R[,R...][,final]. RoundRis the island's R-th trained round.finalis 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,finalis the last round.--rl-checkpoint-store, Miles, full parameter, sample roleboth. The launcher refuses verl,--rl-elasticand 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.<store>/hf/island<k>/round-<r>/. Each complete directory has a.completemarker. The final export also writes<store>/hf/island<k>/final.json.final,final.json(second commit). So a directory with the marker is committed.rl_hf_exporttape event per export. A failed export goes on the tape, and the island trains on.718b2f14. A Modal Volume reports the removal of a missing path asInvalidError: 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:remove_treeraises only when the path is still there after the removal.upload_attemptsandupload_errors.staging_kept). Each island process keeps at most 2 such copies (staging_dropped).Trainer and sampler islands
--rl-release-inlinekeeps the old inline path.YETO_RL_RELEASE_EXPORT=milesuses the Miles export.Volume.batch_upload.--rl-cut-every Nand--rl-cut-auto-mtbf-sreplace the default as before. verl keeps one cut per round.Island relaunch
--recover-timeoutseconds (default 1200).--no-island-relaunchor--modal-retries 0turns this off.Pins and image
59aea07f9(michaellchung/miles, branchyeto/s27-closing). It adds to the previous pind1616ba12:8edea35a7(zero-copy chunks),921cde8c7(DP replicas),d78bf39e4(pinned CPU masters),b7868074c(language-only VL bridge) and63cb372b3(per-fragment boundary).2fa880182(unchanged).ghcr.io/michaellchung/yeto-miles-ports:59aea07-2fa8801@sha256:d54dd4458d08fb2c6012278ac48c285b89bb51b1e50584ad9a032fc18b6ef18a. The image check (scripts/check_miles_ports_image.sh) passes all 13 checks.718b2f14changes only yeto files. It needs no new image.GPU validation of the new head
Closing run (code
33cce368, image59aea07-2fa8801)Model: Nemotron-3 Nano 30B-A3B. Backend: Miles, full parameter. Both runs started within one minute of each other on Modal H200.
s27-bb-a1)s27-bb-b1)tokens2-over-stepsweights, int4 pushes, 64 fragments. 768 outer merges.--rl-single-island-no-sync).numeric_answerreward. First 8192 rows of DAPO-Math-17k (A: split byi % 2).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.
Mechanisms that ran on GPU for the first time (run A):
--rl-sync-checkpoint-every 385)update_weightssucceeded every round (153 and 158 calls per island). The engineweight_versionmatched the round number. The publish phase took a median 3.3-3.5 s per round.rl_round_data_rowsdata_split_checkpassed. All rows came from the new event. No row was trained by both islands.--rl-export-hf-rounds718b2f14fixes.Health:
Other GPU runs since
869078a5s27-ap-ctested 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. Ins27-gr-jit 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 28update_weights_from_distributedcalls 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.63afafaf..33cce368:cargoon PATH. Withcargoon PATH they passed.tests/test_rl_hf_export.py: 20 passed, 1 skipped (the skipped test passes with Miles on the path).59aea07f9on the path (25 files): 335 passed, 14 failed. All 14 failures have known causes:sglang_router.ray.toolspackage hides the yetotoolspackage.test_neutral_rewardsfail the same way at the merge base6504d537.718b2f14: 25 targeted files (Modal store,remove_tree, HF export, release store): 483 passed, 3 skipped, 0 failed. The full suite did not run on718b2f14.467d1d91), the remote full suite had 7202 passed and 0 failed. On the same host,869078a5had 2 failures in tests that depend on timing.Known limitations
Results:
Sync and layout:
--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.Shapes not tested on GPU:
05c1fc0c…). Only the 120B one-island probe without sync ran.--modal-retries 1 --recover-timeout 1200, peer recovery on), but no island exited.Refused or missing:
8192 x TP. At TP8 this gives 65536, so a 120B run must set--rl-max-tokens-per-gpu 8192.rl_round_data_rowsand 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-synchas no effect on a single island.754605964and a new image. The current pin does not have them.michaellchung. The move toagentenvneeds the Miles commits and the image inagentenvfirst.Unsynced and HF export paths:
--rl-resume-storeto 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.718b2f14fix has CPU tests only.Merged branches:
s26-e1-overheadands26-agentenv-migrateas 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.Other:
Earlier content of this PR (head 869078a)
Nemotron-H async full-parameter RL
yeto/rl/adapters/miles/models/nemotron_h.pywrites the Megatron model flags from the Megatron-BridgeHybridModelProvider. RL keeps the routing statistics fixed (--moe-router-bias-update-rate 0,--moe-aux-loss-coeff 0). The backend registry loads the recipe (rolenemotron_h_recipe), so the import boundary check passes.fc1hasFFNrows instead of2 x FFN.yeto/rl/hybrid_params.pynames the Mamba fp32 parameters, the frozen vision tower and the 512-expert layout.yeto/rl/hybrid_flops.pygives the Nemotron-H model FLOPs for MFU. A verl Megatron GRPO plan andtools/nemotron_verl_smoke.pyare included. The verl path is LoRA with one GPU per island.skip_resident).YETO_FULL_PARAMETER_DEFER_CAPTURE=0andYETO_FULL_PARAMETER_BATCH_APPLY=0turn them off.--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.--rl-sync-max-local-rounds-ahead Kmakes an island wait at a boundary instead of training on its own unmerged push.YETO_MILES_TRAIN_EXTRA_ARGVappends allow-listed Megatron flags. None of them changes the parallel layout.--context-parallel Nreaches the island, the trainer layout (DP = world / (TP x PP x CP)), the placement request and the Miles--context-parallel-size.yeto.rl.numeric_answer_reward:score(numeric_answer) grades one-number labels from the last\boxed{}orAnswer:line. On 153,210 recorded responses the oldgsm8k_gradecredited 962 wrong answers and missed 200 right ones.gsm8k_gradedid not change.--observe-training-entropyand--mamba-training-ssm-states-dtypereach the Miles argv.How it is wired
--rl-sync-mode async|strict. Async is the default for multi-island RL. Strict is the control mode.--rl-sync-max-base-lagcounts rounds.--syncer-shardsshards the head.--rl-island-state-source {peer,catch-up,cut}sets where a new island takes its training state.peercopies parameters and inner optimizer state from a healthy peer.catch-uppulls all fragments from the syncer with a fresh optimizer.cutis refused for synced islands. Peer recovery is on by default where it applies.--modal-ephemeral-disk-gib,--rl-island-ray-object-store-giband--rl-island-ray-spill-dirset the island disk and the Ray object store.--rl-sync-checkpoint-everysets the checkpoint interval for async rounds.cut_to_hfwith a TP/PP/EP merge.Earlier GPU validation (Nemotron-3 Nano 30B-A3B, 2 islands x 4 x H200, Miles, full parameter, async)
s27-gr-ffdd468abs27-gr-g22b25c30f, imageaa6b887-2fa8801s27-gr-h2K=1s27-gr-i2abac7785, imageaa6b887-2fa8801s27-gr-j869078a5, imaged1616ba-2fa8801(Mambaseq_idxfix)d1616ba12. Runs befores27-gr-jtrained with the leak.PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True. Peak training memory was about 138 of 140 GiB.Earlier testing (head 869078a)
cargo test: 226 passed.test_delta_protocol.py. The same test failed on the base and passed 3 of 3 times alone.test_rda_manifest_probe, a known evidence-file problem).Draft PR. Review is not requested yet.
🤖 Generated with Claude Code