Skip to content

feat(weight-transfer): add NIXL + ModelExpress synchronization#3060

Merged
S1ro1 merged 51 commits into
mainfrom
feat/nixl-mx-composed-transfer
Jul 22, 2026
Merged

feat(weight-transfer): add NIXL + ModelExpress synchronization#3060
S1ro1 merged 51 commits into
mainfrom
feat/nixl-mx-composed-transfer

Conversation

@S1ro1

@S1ro1 S1ro1 commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the initial production path for direct trainer-to-vLLM weight synchronization over NIXL and ModelExpress:

trainer-native weights -> declarative trainer-to-HF graph -> typed BF16/FP32 wire arenas -> recorded HF-to-vLLM loader graph -> final vLLM kernel tensors

This composes the trainer conversion graph from #2797 with vLLM loader recording from #2779. #2598 was used only as a reference for the ModelExpress transport shape.

Key properties:

  • declarative trainer-to-HF conversion chains shared by every registered PrimeRL custom model family
  • BF16 wire tensors by default, with model-declared FP32 preservation for tensors stored as FP32 in the source checkpoint
  • reusable CUDA staging arenas registered with NIXL/UCX on both trainer and inference ranks
  • lazy recording and replay of vLLM loaders, including view, split, stack, concatenate, and dtype-conversion paths
  • bounded group-by-group transfer with independent trainer and inference sharding topologies
  • ModelExpress coordination for startup and per-policy synchronization
  • no vLLM source changes
  • NIXL-specific transport code is isolated under prime_rl.nixl_weight_transfer

Correctness and failure handling

  • Any trainer, inference, transport, or coordination failure fails the synchronization; there is no fallback path.
  • Typed trainer tables carry exact tensor shapes, wire dtypes, and shard address ranges.
  • Each policy version becomes live only after all participating inference ranks complete the update.
  • The initial broadcast and cleanup lifecycle match the existing NCCL behavior.
  • FP32 wire declarations were audited against the actual Hugging Face safetensors headers rather than runtime module allocation dtypes.
  • NIXL inference identities use the local CUDA device index, preserving unique ranks when one admin endpoint owns multiple workers.
  • The ModelExpress watcher stops advancing after the final trainer-published in-memory policy version.

Validation

Ruff 0.13.0 check, Ruff format check, and git diff --check pass after rebasing onto current main. CPU and GPU workflows are delegated to PR CI.
Existing Qwen3.5-MoE, Qwen3.5-MoE VLM, and Nemotron-H tests use the instance conversion API that owns the model config.

WhiteFiber Qwen3-MoE E2E

Validated Qwen3-30B-A3B-Instruct-2507 in nixl-modelexpress-nvl72-e2e:

  • trainer: 2x4 GPUs, FSDP + EP8, FP32 master weights, FA4
  • inference: 1x4 GPUs, TP4 with expert parallelism, eager mode
  • max_off_policy_steps=1
  • unquantized BF16/FP32 path
  • 50 synchronized steps for both NIXL and matched NCCL controls on the same GPU nodes
Backend Successful updates KL mean KL median KL p95 KL max Rank-0 sync mean
NIXL + ModelExpress 50/50 0.01742160 0.01398600 0.03868628 0.08084155 4.02599 s
NCCL control 50/50 0.02322200 0.01630000 0.06676000 0.09850000 8.140388 s

NIXL was 2.02196x faster for end-to-end rank-0 synchronization.

The NCCL-minus-NIXL mean KL difference was +0.0058004, with an approximate 95% interval of [-0.0009297, +0.0125306]; the reverse-text workload did not resolve a backend-related KL regression.

The NIXL run had zero rollout errors and zero pod restarts. NCCL had three expected stale-rollout cancellations at off-policy level 1 due to slower synchronization, with no transfer or inference failures.

W&B:

Current scope

  • NIXL uses the UCX backend.
  • A ModelExpress control-plane deployment is required externally.
  • Inference ranks currently receive the complete model weight set, including all experts.
  • vLLM private loading APIs are intentionally used.

Note

High Risk
Large new distributed weight-sync path on the critical RL training loop (trainer, orchestrator, vLLM workers) with NIXL/UCX and ModelExpress coordination; conversion refactors touch every custom model family used for broadcast.

Overview
Introduces weight_broadcast.type = "nixl" as a third in-memory transport (with NCCL and filesystem), wired through shared RL/trainer/orchestrator/inference configs, modelexpress==0.3.0 as a core dependency, and trainer NIXLWeightBroadcast plus vLLM NIXLWeightUpdateWorker for UCX/NIXL pulls coordinated by ModelExpress (session_id, gRPC port, READY/INITIALIZING rendezvous).

The orchestrator WeightWatcher can advance policy versions from ModelExpress status instead of waiting on filesystem STABLE markers; startup and per-step sync paths treat NIXL like NCCL for dispatch gating and worker-count auto-setup. LoRA remains filesystem-only for in-memory transports.

Model weight conversion is refactored: imperative per-family converters become conversion_ops chains (Rename, Stack, routed_experts_op, etc.) with instance convert_to_hf / convert_to_prime on PreTrainedModelPrimeRL, plus keep_in_fp32_for_weight_transfer hooks where wire dtype must stay FP32.

Smaller changes: drop the optional modelexpress uv extra from Docker/entrypoint sync, relax inference quantization typing and remove the mxfp8 SM100 config validator, and pass session_id through /init_broadcaster for NCCL compatibility.

Reviewed by Cursor Bugbot for commit 4484cb1. Bugbot is set up for automated code reviews on this repo. Configure here.

@S1ro1
S1ro1 force-pushed the feat/nixl-mx-composed-transfer branch 6 times, most recently from 4de7d09 to 3969547 Compare July 19, 2026 01:10
@S1ro1
S1ro1 marked this pull request as ready for review July 19, 2026 05:31
Comment thread src/prime_rl/orchestrator/watcher.py
Comment thread src/prime_rl/inference/vllm/worker/nixl.py Outdated
@S1ro1
S1ro1 force-pushed the feat/nixl-mx-composed-transfer branch from 24eea1d to 771a362 Compare July 19, 2026 05:35
@S1ro1 S1ro1 changed the title feat(weight-transfer): add NIXL + ModelExpress sync for Qwen3 MoE feat(weight-transfer): add NIXL + ModelExpress synchronization Jul 19, 2026
Comment thread src/prime_rl/orchestrator/watcher.py
"""GPU memory utilization. Forwarded as ``--gpu-memory-utilization``."""

quantization: Literal["mxfp8", "fp8_per_block"] | None = None
quantization: str | None = None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why str ?

Comment thread packages/prime-rl-configs/src/prime_rl/configs/inference.py
Comment thread packages/prime-rl-configs/src/prime_rl/configs/rl.py

@samsja samsja left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can we add docs on:

  • the new conversion format for moe
  • the way nixl.py file work and record the transformation

I will review the pr in depth, doing step by step

@samsja samsja left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how much did we tested rl run ? I am worried about the potential change in bf16/fp32 for some of the layers, I know that before it was not super clearly definw who was doing what in dtype

@samsja samsja left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can we rename the ht_to_tt function to hf_to_prl ? kinda not tt anymore no ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

would it make sens to have this in a prime_rl.broadcast.nixl submodule. next to fileystem and nccl ideally

Comment thread src/prime_rl/trainer/models/glm4_moe/modeling_glm4_moe.py
ops: list[ConvOp] = [
Rename(f"{p}.gate.weight", f"{p}.router.gate.weight"),
Rename(f"{p}.gate.e_score_correction_bias", f"{p}.expert_bias"),
routed_experts_op(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why is this snake case when the rest is pascal case?

Comment thread src/prime_rl/trainer/models/laguna/converting_laguna.py

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4484cb1. Configure here.

Comment thread src/prime_rl/trainer/models/laguna/modeling_laguna.py
@S1ro1
S1ro1 merged commit c505e07 into main Jul 22, 2026
18 checks passed
mikasenghaas added a commit that referenced this pull request Jul 22, 2026
The merge of #3060 kept this branch's on_version_pending/on_new_version
bodies and dropped main's ModelExpress status updates — the READY
signal the NIXL trainer waits on before each broadcast and the
INITIALIZING reset after it. Post-startup NIXL runs would hang in the
handshake. Both signals are back, ordered as on main (READY pre-update,
INITIALIZING post-update) around this branch's staleness/wake logic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
samsja pushed a commit that referenced this pull request Jul 23, 2026
…#3050)

* fix(orchestrator): enforce target lag strictly at ship time

The dispatch gate only stops new rollouts; on tiny fast envs the sink
keeps forming batches from buffered rollouts, so the orchestrator ships
past TARGET_LAG, finishes, and tears down the watcher while the trainer
still has pending NCCL broadcasts. Those broadcasts then block forever
in the STABLE->NCCL_READY handshake (no watcher to trigger the inference
receive) and the launcher never exits.

hold_for_target_lag blocks shipping batch N until the trainer has
published v{N-1-TARGET_LAG}, bounding in-queue staleness and keeping the
orchestrator alive for every broadcast the trainer will make. Under NCCL
the last published version is max_steps-2, exactly what the final batch
requires, so the hold never waits on an unpublishable version.

Also logs a per-batch staleness decomposition (pre-queue = versions
elapsed during generation, in-queue = versions spent waiting in the
sink) under staleness/*.

Fixes RES-1077

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

* remove staleness metrics from ship path

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

* feat(orchestrator): step-based staleness with demand-driven dispatch

Measure a rollout's off-policy lag against the batch that consumes it —
batch N trains on policy v{N-1}, so lag = (N-1) - generation version,
queue time included. The old off_policy_steps counter only counted
weight updates during generation, so a rollout that finished fast but
sat buffered across steps trained arbitrarily stale while reporting 0.

- max_off_policy_steps now bounds consumption lag: in-flight groups are
  cancelled pre-pause (cancel_stale_train_groups) and buffered sink
  survivors are swept whenever the cutoff moves (ship, weight update).
  Groups share one generation version, so stale groups drop whole.
- Dispatch is demand-driven: the orchestrator injects train_demand
  (batch shortfall minus buffered + partial-group + in-transit
  rollouts), re-evaluated per scheduled group, replacing the lead-based
  dispatch gate and the #2990 final-batch NCCL exemption it needed.
  out_q backlog counts as coverage so ship-holds don't over-provision.
- Max Off-Policy in logs is now the honest consumption lag, stamped at
  ship (reverse_text: steady-state max 3 vs a dishonest 0 before).

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

* review pass: bool dispatch contract, oversampling-aware demand, inline single-use helpers

- train_demand -> train_needed: the dispatcher only thresholds the value,
  and the magnitude isn't even unit-stable across batch modes, so the
  injected contract is a bool predicate.
- Demand target is max(batch_size, max_inflight_rollouts): explicit
  over-provisioning (oversampling_factor > 1 or a raised max_inflight)
  keeps racing batch stragglers instead of being silently capped at one
  batch; capacities below the batch keep a batch_size target so the
  batch can still complete.
- Reject max_off_policy_steps=0 with NCCL broadcast + max_steps: the
  final batch necessarily trains one version off-policy (the trainer
  never broadcasts the last versions), so a zero budget would hang the
  run collecting the final batch.
- Inline single-call helpers (hold_for_target_lag, stale_cutoff, the
  dispatcher lag gauges) into their call sites.

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

* require max_off_policy_steps >= 1 at the field

The last NCCL batch necessarily trains one version off-policy (the
trainer never broadcasts the final versions), so a zero budget can never
be met and would hang the run collecting the final batch. A field floor
covers this without a conditional validator.

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

* fix CI regressions: partial-group dispatch deadlock + serialized weight applies

Two regressions surfaced by the PR's GPU CI run:

- alphabet_sort/rl_opd deadlocked at 126/128 (+3 buffered, 0 inflight):
  demand hit zero mid-group, so an open group's remaining rollouts were
  never scheduled — its buffered members count as batch coverage but the
  group can never finalize. Already-open groups now keep scheduling
  regardless of demand (try_schedule allow_fresh); only fresh groups are
  demand-gated.
- reverse_text timed out (~26s/step vs ~19s on main): the ship hold
  waited on the applied version, serializing every inference weight
  reload into the ship path. Filesystem broadcast decouples publish from
  apply and the trainer never blocks on the watcher there, so the hold
  now releases on the published version (tracked in on_version_pending).
  NCCL keeps waiting for the apply — the trainer blocks in the receive
  handshake until then, so releasing on publish could strand its final
  handshake at teardown.

Verified: reverse_text e2e clean on both broadcast types (filesystem
6-7s steps, NCCL exits clean), 485 unit tests pass.

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

* raise multi-run integration timeout to 600s

Resumed orchestrators now pace to the shared trainer — a batch only
ships once the trainer has produced the required policy version — so
with three runs round-robining one trainer, beta_resume's tail (steps
21-25) can legitimately exceed the old 300s wait while its versions
queue behind the other runs. Matches the 600s budget of the other RL
integration tests.

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

* retract stale-dropped rollouts from the payload-size estimate

drop_stale now mirrors what entering pending_batch added to
_payload_total/_payload_count, keeping the token-mode demand estimate
consistent with the buffered set. Also expand the allow_fresh comment
with the group-finalization invariant it protects.

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

* upload integration run logs as artifacts on failure

The multi-run test's failures only surface a sampled tail of one
orchestrator's output; the copied per-run orchestrator/trainer/inference
logs are what's needed to debug them (currently: beta_resume wedging on
a policy version the shared trainer never publishes after resume).

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

* keep CI test outputs for the failure-artifact upload

The output_dir fixture rmtree'd the whole outputs dir at session
teardown, so the upload step found nothing. Skip the cleanup under CI
(the workspace is discarded with the job) and match log files directly
instead of a directory glob.

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

* dispatch lookahead: cover TARGET_LAG extra batches when freshness allows

Exact-one-batch demand synchronized dispatch into one 128-rollout wave
per batch: each ship opened demand 0->128 at once, the wave stampeded
the env server together (MCP timeouts + backoff retries in CI
alphabet_sort), and inference idled ~60s per cycle while the whole wave
sat in env round-trips — tripling the step cadence vs main (artifact
logs: vLLM alternating 30s busy / 60s '0 running, 0 waiting').

With up to TARGET_LAG batches of lookahead — allowed only while a
rollout landing that far ahead still fits max_off_policy_steps —
steady-state dispatch is continuous (each arrival frees demand for the
next start), keeping the engine fed through env-phase gaps. Costs one
version of staleness on the lookahead batch (reverse_text plateau 3->4,
within the default cap of 8).

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

* raise reverse_text mismatch-KL budget to 0.02

Mismatch KL compares generation-time inference logprobs against the
training weights, so it includes async training's policy-version gap on
top of trainer/inference numerics. With strict pacing + dispatch
lookahead the steady-state consumption lag is 2-3 versions (visible and
bounded, vs unmeasured before), which puts the last-steps average at
~0.011-0.012 against the old 0.01 budget calibrated for main's profile.

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

* raise alphabet_sort and multi-run integration budgets

Artifacts from the failed runs show no wedges — every process was
mid-progress at the deadline: alphabet_sort cycles at ~70s on slow vm
runners (33s on fast ones; trainer MFU varies 2-6% across the pool) and
the multi-run trainer was mid-micro-step serving three runs
round-robin. With strict pacing the orchestrators ride the trainer's
pace instead of finishing early on stale buffers, so wall time tracks
the slowest component.

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

* raise multi-run inner milestone waits to 600s

The wait_for_log/wait_for_file 300s defaults were calibrated for
orchestrators that sprint ahead of the shared trainer on buffered
batches; with strict pacing three orchestrators ride the trainer's
actual speed, so every internal milestone (alpha steps, checkpoint
STABLE markers, resume completion) lands slower and a different 300s
wait tripped on each CI round.

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

* count group-scored partial arrivals as batch coverage

buffered_count hides group-scoring envs' partial groups for the
pipeline log's honesty, but demand must count them — otherwise members
sitting between arrival and group completion invite bounded
over-dispatch. Also document why a held batch intentionally does not
count as coverage (dispatch-ahead keeps inference busy through
trainer-bound holds; staleness stays bounded by lookahead + cutoff).

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

* guard token-export STABLE markers against deleted run dirs

The multi-run trainer crashed with FileNotFoundError touching
run_gamma/token_exports/step_20/STABLE: the multi-run integration test
rmtree's a run's dir once its orchestrator finishes, while the trainer
is still flushing that run's tail steps. The broadcast paths already
skip deleted runs on FileNotFoundError; mark_stable now does the same.

This was the root cause of the recurring multi_run CI failure: the
crashed trainer never published the resumed run's next version, so
beta_resume's ship-hold waited forever ('failed with code None'). On
main the same crash risk exists but the orchestrator never waited on
the trainer, so nothing surfaced.

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

* recalibrate multi-run early-step reward floor for strict pacing

Batch N's data now comes from v{N-2}..v{N-4} instead of main's
effectively-fresher buffer profile, so reward-at-step-7 reads 1-2
policy versions staler (observed 0.14-0.20 across four CI rounds vs the
0.2 floor). The final-reward thresholds still enforce learning quality;
the early floor drops to 0.1 — comfortably above a broken run's flat
~0.05-0.08.

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

* raise rl_sft integration budget to 900s

Same class as the other RL integration budgets: the trainer was
SIGTERM'd at step 14 with the orchestrator already finished — killed by
the 600s deadline steps from completion on a slow runner.

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

* lower multi-run final reward floor to 0.6

With strict pacing the reward-vs-step-index curve sits 1-2 policy
versions lower throughout; gamma — the most trainer-contended of the
five runs — landed at 0.627 while its siblings cleared 0.69-0.75. A
broken run still reads ~0.1-0.3.

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

* bound dispatch freshness at DISPATCH_LAG instead of the drop cap

Root cause of the reward-at-step gap vs main: the old lead gate doubled
as the dispatch freshness bound (rollouts only started at version >=
step-2, landing lag <= 2). The demand rewrite left freshness gated only
by max_off_policy_steps (8) plus a deliberately-ahead lookahead batch,
so mean consumption lag ran 2-4 versions vs main's 1-2 — visible as
Max Off-Policy 3-4, mismatch KL ~2x main's, and reward-at-step lower by
1-2 versions of learning.

Dispatch now pauses when new rollouts would train more than
DISPATCH_LAG = TARGET_LAG+1 versions behind their generation policy,
with the lookahead scaled inside that budget — the tightest bound that
still keeps inference busy during ship-holds. Landing lag returns to
<= 2 (spill 3); local e2e: Max Off-Policy plateau 4 -> 3, mismatch KL
mean ~0.012 -> ~0.008. max_off_policy_steps returns to being purely the
drop safety net.

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

* ci: retrigger GPU tests

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

* tolerate trainer tail-flush when deleting run dirs in multi-run test

The fixture's rmtree races the shared trainer's token-export writes,
which mkdir the run dir back mid-deletion (OSError: Directory not
empty). Retry until the flush for the finished run drains.

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

* restore NIXL ModelExpress handshake signals in the version hooks

The merge of #3060 kept this branch's on_version_pending/on_new_version
bodies and dropped main's ModelExpress status updates — the READY
signal the NIXL trainer waits on before each broadcast and the
INITIALIZING reset after it. Post-startup NIXL runs would hang in the
handshake. Both signals are back, ordered as on main (READY pre-update,
INITIALIZING post-update) around this branch's staleness/wake logic.

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

* reduce to the minimal ship-hold fix + staleness docs

Strip the demand-driven dispatch, consumption-lag drop rework, and test
recalibrations accumulated on this branch back to main's behavior, and
keep only what the spec (docs/staleness.md) strictly requires:

- ADVANCE rule: batch N ships only once the trainer has published
  v{N-1-TARGET_LAG}, so the orchestrator cannot finish and tear down the
  weight watcher while the trainer still has in-memory broadcast
  handshakes pending (RES-1077). Main's dispatch gate (START) and
  in-flight drop aging (DIE) are untouched, so generation freshness and
  throughput are unchanged in both pacing regimes — the hold only moves
  where a finished batch waits, never when the trainer consumes it.
- policy.version advances at publish confirmation (inference applies
  immediately and pauses during the update, so published and applied
  are indistinguishable to consumers), and WeightWatcher.stop() drains
  an in-flight apply before cancelling — teardown can never strand the
  trainer mid-handshake.
- Keep the trainer-side token-export guard, the multi-run test's
  deletion-race helper, and the CI failure-artifact pipeline from the
  earlier iterations; drop everything else.

Verified on the original repro (reverse_text, fast env, NCCL): 10/10
batches, clean trainer exit, 2 holds at the buffer-fed tail.

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

* fold staleness spec into docs/training.md

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

* drop the docs section for now, trim the hold comment

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

* never delete the test output dir; clean at run start instead

The output_dir fixture deleted the whole workspace at session teardown,
racing the failure-artifact upload (and hiding logs from post-mortems
generally). Keep it unconditionally and make fresh starts responsible
for their own hygiene: alphabet_sort and reverse_text_moe now pass
--clean-output-dir like the other rl-launcher tests. Resume runs are
unaffected — validate_output_dir returns before cleaning when resuming.
The multi-run orchestrators manage per-run dirs the test creates (and
the standalone orchestrator entrypoint has no clean flag).

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

* stamp true consumption staleness on shipped rollouts

Batch N trains on policy v{N-1}, so a rollout generated from v{k} is
(N-1)-k versions off-policy — queue time included. The dispatcher's
in-flight counter only sees weight updates during generation, which
undercounts by the queue-wait aging and the dispatch-time baseline gap;
the ship-time stamp overwrites it so Max Off-Policy in logs and the
rollout records report the real number. The counter itself (and the
max_off_policy_steps drop rule it feeds) is unchanged.

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

* use the deletion-retry helper for alpha's run dir too

Alpha is SIGTERM'd mid-run, so the trainer is actively flushing its
token-export tail when the dir is deleted — the recreate race is most
likely at exactly this site.

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

---------

Co-authored-by: Claude Fable 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.

3 participants