refactor(async): budget staleness in rollout batches; version in optimizer updates - #310
Open
CjhHa1 wants to merge 11 commits into
Open
refactor(async): budget staleness in rollout batches; version in optimizer updates#310CjhHa1 wants to merge 11 commits into
CjhHa1 wants to merge 11 commits into
Conversation
Replace sync-window/LIFO control with FIFO rollout batches and demand-driven weight sync so freshness has one explicit unit.
CjhHa1
requested review from
Ideny42,
celve,
haonan3,
leviking98z-rgb and
zzhuoxin1508
as code owners
August 3, 2026 14:57
Keep policy-version values in their native integer types so the control path stays explicit and readable.
Remove repository-level async test scaffolding from the pull request while retaining the files in the local worktree for validation.
4 tasks
Preserve the optimizer-update batch control while carrying forward Tencent-Hunyuan#294's independent Agentic sync guard/logging and pure-ODE eval fix.
Share policy versions, launch admission, sync boundaries, and metrics so AR and diffusion keep their distinct loop order without duplicating control logic.
Remove the hidden rollout initialization flag and force only bootstrap/resume syncs at their call sites.
max_policy_lag counted committed optimizer updates, but admission and consumption only ever run at batch boundaries, so the knob quantized to num_updates_per_batch and the same value meant different depths per recipe. max_staleness now counts whole rollout batches (AReaL's unit for eta) and staleness_budget converts once into the update clock, which keeps a partially-committed step honest. Also split the two quantities that were both called lag: staleness is the off-policyness of the data being trained on, publish_lag is weight-sync debt. Shipped recipes are behavior-identical.
The startup notes on async admission lived in both trainers and covered only two of the three ways a value can mislead. Move them next to the admission math and add the missing case: depth past max_inflight + 1 can never become concurrency, because the loop reaps before it launches and so holds at most one completed batch behind the in-flight ones. The surplus only sets the weight-sync period, which is what bagel's max_staleness=11 is really buying. Drop reject_removed_async_keys with it. A shared blacklist misdescribes two of its three keys: buffer_max_staleness is still the agentic consumer-buffer bound, and weight_sync_interval is still live in 69 recipes on the synchronous path. Only max_policy_lag is genuinely gone, and it never shipped outside this branch.
max_staleness is checked when a batch enters training, but the anchor stays frozen while the weights move, so update i of num_updates_per_batch trains at staleness + i - 1 and the worst case any gradient step sees is one short of a full extra batch beyond the budget. That span is the in-batch off-policyness PPO already assumes and the clip range covers. Say so where the two clock quantities are defined, so the budget is not read as a bound on every optimizer step.
log_admission_notes derived the weight-sync period from the staleness budget alone, but next_hard_boundary clamps admission too, so an eval or checkpoint interval below max_staleness + 1 becomes the real period. Measured at max_staleness=11 with eval_interval=10: the run publishes every 10 batches and data never gets past 9 of the configured 11, while the note still reported 12. sync_period_batches now takes the tightest of the three limits, the surplus note quotes it instead of the budget-derived depth, and a budget the boundaries never let you spend gets its own warning. Both trainers report from train() rather than __init__ because save_interval only arrives with the train call; the period also goes to W&B as async config so a run records the cadence it actually ran at. No shipped recipe changes behaviour: the AR recipes are max_staleness=0 (period 1 either way) and BAGEL sets no eval_interval, so its 12 stands.
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
max_staleness) denominated in whole rollout batches so it means the same depth at anynum_updates_per_batchstalenessis the off-policyness of the data being trained on,publish_lagis weight-sync debtRelated Issue
Supersedes the batch AR/diffusion sync-counter and resident-policy eval portions removed from #294.
Test Plan
Static and unit:
--cfg job) for all six async override sets exercised belowControl-plane equivalence and invariants (drives the real pre-rename control
object from
fbefbfaand the current one through one model of thesingle-threaded loop, then compares event traces):
max_policy_lagvs newmax_stalenessproduce byte-identical traces for all three shipped recipes. BAGEL at U=2: 123 identical events, syncs after batches 12/24/36, staleness reaching exactly 22 updates = 11 batches.max_staleness∈ {0,1,3,11} gives sync period {1,2,4,12} batches at everynum_updates_per_batch∈ {1,2,3,4,8}, and staleness in batch units equalsmax_stalenessexactly. The old unit could not express this:max_policy_lag=4is depth 2 at U=4 but depth 3 at U=2 and depth 5 at U=1.sync_period_batcheswas cross-checked against the simulated publication schedule over 8(max_staleness, U, eval_interval, save_interval)combinations and matched every one.GPU, 1x8 H20 per path (4 train + 4 rollout). This is the first GPU coverage of
the control path as it now stands — the earlier smokes ran the pre-
dcf2e93version whose harness cannot even import against this commit. Observable is the
weight publication itself (SGLang
Cache flushed successfully!, vLLM-OmniActivating adapter), since the async metrics only reach W&B:max_stalenessmax_inflightmax_staleness=1at U=4 and U=2, identical period. Spelling A1 with the old knob neededmax_policy_lag=4; carrying that 4 into A2's U=2 gives4//2+1 = 3, so the period would have moved.AsyncBatchControl(max_staleness=11, num_updates_per_batch=2)yieldsstaleness_budget=22/admission_depth=12, reproducing what the BAGEL recipe previously spelled asmax_policy_lag=22.max_staleness=0no-overlap warning; A4 and B2 (depth abovemax_inflight + 1) emitted the surplus note, each self-reporting a 4-batch weight-sync period that the observed schedule then confirmed; A1/A2/B1 emitted nothing.What the budget buys in wall time (measured from the arm logs; the AR arms are a
clean comparison because their policy is static, so per-step compute is identical
and only the publication cadence moves):
max_staleness=0pays it every batch at 11.99s/step; a budget amortises it to 4.2s/batch atmax_staleness=1and 2.1s/batch at 3 (BAGEL 3.0 → 1.3s/batch). Mean AR step time is 78.9% higher at the synchronous floor than at any non-zero budget. A4/A5 reproduce to 0.03s on mean step. At batch_size 4 the fixed cost dominates, so 78.9% is an upper bound — a production step makes the same overhead a smaller fraction.Limits of the GPU evidence, stated so it is not over-read. Two follow-up arms were
run specifically to remove these and both came back null:
grad_normprints 0.00000 in every AR rollout: at 4 prompts x 4 samples on DAPO math a 4B base model gets whole groups wrong, GRPO's within-group advantage vanishes, and the policy does not move — so all arms are bit-identical and the ratio spread (0.9988-1.0329) is the SGLang-vs-FSDP numerical gap, not drift. Re-running at 8 prompts x 8 samples with 2048 new tokens (max_staleness=0vs3) produced within-group reward variance but still byte-identical arms: mean|ratio-1|0.00493 both, clip 0.0000 both. This is the base model's pass rate, not a batch-size problem. Precisely:grad_norm = 0.0isTrainStack's no-backward fallback and that path also setsoptimizer_updates = 0, which would freezepublish_lagand prevent any sync — the observed schedules require U updates per batch, so backward did run and the norm is genuinely below the%.4fprint precision.max_staleness=1mean clip 0.3478 ±0.0439,max_staleness=3mean clip 0.4011 ±0.0735, reward 0.6252 vs 0.6253 — the between-budget gap is 0.91x the within-budget spread, so not separable from run-to-run variation. The single-run gap that first looked like an AReaL-style effect was seed noise.Measured against the shipped budget, 24 rollouts per arm at 8 prompts x 8 samples x
8 SDE steps, same seed, so
max_staleness=11runs two complete publication cycles:iis(i-1) mod 12batches stale by construction — one run contains its own staleness sweep, paired against a single policy trajectory and free of the seed noise above. Clip fraction rises 0.140 → 0.420 across the cycle; grouped, staleness ≤ 2 (n=6) clips 0.1500 against staleness ≥ 7 (n=10) at 0.3170, a factor of 2.11, difference +0.167 ± 0.050, t = 3.33. It cross-checks: the entiremax_staleness=1run sits at clip 0.1550, i.e. at this run's fresh level, so the deep budget's higher overall clip (0.2675) is the same response curve sampled mostly at its stale end. Clip is the instrument rather than the ratio becauseclip_rangeis 1e-5 while the console prints ratio to four decimals, so|ratio-1|reads 0.000000 at every level.max_staleness=1; −0.000795 ± 0.000833, t=−0.95 at 11), and the deep budget's −0.0136 mean gap is 0.47 of one rollout's standard deviation. Direction disfavours the deep budget in both the mean and the slope sign, but establishing it needs a production-length run — enough steps to read a reward trajectory, and ideally the decoupled objective AReaL and verl rely on at this depth, which UniRL does not implement. That is a training question rather than a control-plane one and should not gate this PR.Compatibility / Risk
weight_sync_intervalandbuffer_max_stalenessare no longer read on this path; usemax_staleness, counted in whole rollout batches. Both keys stay live elsewhere —weight_sync_intervalin 69 synchronous recipes,buffer_max_stalenesson the agentic path — so a stale key here is silently ignored rather than rejected; check async recipes by hand when upgrading.max_stalenessis enforced at batch entry. Withnum_updates_per_batch = Uthe anchor stays frozen while the weights move, so updateitrains atstaleness + i - 1and the worst case any gradient step sees isstaleness_budget + U - 1. That extra span is the in-batch off-policyness PPO already assumes andclip_rangecovers.max_staleness + 1becomes the effective sync period, becausenext_hard_boundaryclamps admission too: atmax_staleness=11witheval_interval=10the run publishes every 10 batches and data never gets past 9 of the configured 11. This used to be silent — the startup note reported the budget-derived 12 — and is now reported bysync_period_batches, quoted in the surplus note, warned about when a budget can never be spent, and logged to W&B assync_period_batches. No shipped recipe changes behaviour: the AR recipes aremax_staleness=0(period 1 either way) and BAGEL sets noeval_interval, so its 12 stands.max_staleness=11, i.e. 11 batches between publications. That is past AReaL's recommendedeta <= 8, and FlowGRPO's rollout-anchored ratio is their "naive PPO" arm with no decoupled objective to absorb the drift. The recipe comment says so; lower it first if the ratio starts to swing.Checklist