feat(sde): implement WindowScheduler decay / exp_decay strategies - #489
Open
ruiling-smartbear wants to merge 3 commits into
Open
ruiling-smartbear wants to merge 3 commits into
ruiling-smartbear wants to merge 3 commits into
Conversation
ruiling-smartbear
requested review from
Jayce-Ping and
haonan3
as code owners
September 18, 2026 21:01
Both strategies were declared in Strategy and described by WindowConfig, but had no resolver, so selecting either raised "Bad strategy configuration". Semantics follow MixGRPO's GRPOTrainingStates. Refs Tencent-Hunyuan#488.
ruiling-smartbear
force-pushed
the
feat/window-scheduler-decay
branch
from
September 20, 2026 05:01
e070eae to
3ad4cc3
Compare
… MixGRPO's sample dict
…ing on the first get_sde_indices call
Jayce-Ping
approved these changes
Sep 22, 2026
This branch has not been deployed
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
Strategydeclaresdecayandexp_decay, andWindowConfigfully describes them (max_iters_per_window,min_iters_per_window,exp_decay_threshold,exp_decay_k, plus thedecaybranch in__post_init__) — butWindowSchedulerhas no resolver for either, so selecting one raisesBad strategy configuration. Four shipped mixgrpo recipes (sd3_mixgrpo,wan21_t2v_mixgrpo,wan22_t2v_14b_mixgrpo,qwen_image_mixgrpo) already set the decay-only fields while runningprogressive, so those lines are currently inert.This adds both resolvers, mapped onto the existing stateless
get_sde_indices(step)contract.Dwell formulas follow MixGRPO's
GRPOTrainingStates(fastvideo/utils/grpo_states.py:55-83), wheretis the window start:decay—iters(t) = max(min, int(max*(1-t/T) + min*t/T))exp_decay—iters(t) = ceil(iters_per_window * exp(-k * relu(t - threshold)))Index space is UniRL's own, not MixGRPO's — the full-window walk
_resolve_progressivehas used since the initial release, withnum_timesteps = num_inference_steps. That difference is inherited rather than introduced here:T-1transitions, somax_timesteps = sampling_steps - 2(train_grpo_flux.py:921) is the last index that dict can yield. That cut comes from DanceGRPO's extra[:, :-1](train_grpo_flux.py:511-519), not from Flow-GRPO (which keeps allT), and carries no comment or commit rationale. The same value is then used as an exclusiverangeend, so the last index actually reached isT-3— one short of the value it names._resolve_sde_windowrejects any index>= num_inference_steps), andprogressivealready used the full0..T-1range.Consequence, stated rather than hidden: with
sd3_mixgrpo(T=10, window_size=4, overlap_size=1, max=10, min=1, roll_back=true)decayyields dwell10/7/4over indices0..9; evaluated over MixGRPO's coordinates the same formula yields10/6/3over0..7. Mirroring MixGRPO's coordinates would also changeprogressive, which all four shipped mixgrpo recipes run — out of scope here, and it wants its own issue.The third commit is separable: it moves window-config validation into
WindowConfig.__post_init__so invalid schedules fail at instantiate time rather than on the firstget_sde_indicescall. It is not required by the new strategies (the same pitfalls already exist forprogressive), so treat it as your call — say the word and I'll rebase it out.Related Issue
Refs #488
Test Plan
Parity harness over 216 parameter combinations (
num_timesteps∈ {10,14,25} ×window_size∈ {2,3,4} ×iters_per_window∈ {4,10,25} ×overlap_size∈ {0,1} ×roll_back∈ {false,true}), replayingGRPOTrainingStates.update_iteration()step by step againstget_sde_indices(step). Both coordinates were evaluated, which is the part the earlier revision of this description got wrong:exp_decaydwell matches the reference on the shared window prefix under either coordinate — it does not depend onmax_timesteps.decaydwell matches on the shared window prefix when the reference is evaluated withmax_timesteps := num_timesteps— 108/108; under the trainer'smax_timesteps = T-2it differs by the denominator (e.g.10/7/4vs10/6/3).roll_back=truecases); UniRL keeps to full windows, matching_resolve_progressive.Resulting dwell with
num_timesteps=10, window_size=4, overlap_size=1:Validation harness (separate commit):
ValueErroratWindowConfigconstruction; all 7 shippedWindowSchedulerrecipes still construct.ZeroDivisionErroron the firstget_sde_indicescall (144) or silently degraded to a single / out-of-range window (72); 0 valid configs rejected.progressive/decay/exp_decay, 0 mismatches.Regression: pre-change vs post-change
WindowSchedulerdriven side by side over 607,824(config, step)pairs (strategy∈ {all,progressive,random}) — 0 mismatches — andAllSDESchedulerover 2,244 pairs — 0 mismatches.Lint:
ruff check unirl/sde/index_schedule.py,ruff format --check unirl/sde/index_schedule.py,python lint/check_docstring_lines.py— clean (CI agrees: thelintjob is green on this head).Training smoke: passed for both strategies on
fa01335d, one H100 80GB, SD3.5-medium BF16 LoRA with thesd3_mixgrporecipe and PickScore (datasets/pickscore/train.txt). Each arm completed 5 rollouts / 10 optimizer steps at 256×256; loss, gradients and updated parameters were finite, every optimizer step had nonzero gradients and changed parameters. Observed window starts:0, 0, 3, 6, 0, covering advancement and wraparound;exp_decayused threshold 0 so decay was active. Training subprocess times: 104s / 81s including model initialization, not performance benchmarks. Modal run. This is an integration smoke, not convergence or image-quality validation.Smoke commands (one-GPU Ray head already running)
Torch 2.11.0+cu130, Transformers 5.6.2, Diffusers 0.40.0, PEFT 0.21.0. External observation-only instrumentation recorded scheduler output, per-update loss/gradients, and parameter deltas; no production source changes were needed.
The harnesses were run but not committed, per
CLAUDE.md.Compatibility / Risk
No API, config-schema, or checkpoint changes;
all/progressive/randombehaviour is unchanged. Validation moves intoWindowConfig.__post_init__, so it fires at Hydra instantiate rather than at the firstget_sde_indicescall, and additionally rejectsoverlap_size >= window_size(previouslyZeroDivisionErroronstride == 0, or a silent single-window scheduler onstride < 0) anditers_per_window < 1(previouslyZeroDivisionErrorforexp_decay); thedecaybound check moved here fromWindowScheduler.__init__. Those two checks are scoped to the strategies that consumestride/iters_per_window(progressive,decay,exp_decay), so the threerandombagel recipes are untouched.Reviewer Notes
Two calls worth a look:
_resolve_progressivealready differs from the reference past the end of a sweep, and the reference's own boundary is not self-consistent (its comment saysT-2, its code reachesT-3, upstream Flow-GRPO keepsT-1). I followed UniRL's convention so the three strategies behave consistently, which meansdecay/exp_decaymatch the reference's formulas but not its index bookkeeping. Happy to switch if you'd rather mirror MixGRPO exactly — note that would also changeprogressive, i.e. the four shipped recipes.AI-assisted. Checked open issues and PRs for overlapping work before opening.
Checklist