You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
git rev-parse HEAD
python -c "import sys, platform; print(sys.version); print(platform.platform())"
python -c "import torch; print('torch', torch.version, 'cuda', torch.version.cuda); print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'no cuda')"
python -c "import importlib.metadata as m
for pkg in ('unirl', 'vllm', 'vllm-omni', 'sglang', 'torch'):
try:
print(pkg, m.version(pkg))
except m.PackageNotFoundError:
print(pkg, 'not installed')"
Rollout / inference engine
Train-side only
Domain
sft (train_sft)
Recipe / config-name
N/A (generic checkpoint reproducer; first observed in a modified Qwen-Image-Edit SFT recipe)
How you ran it
Official example in examples/ (unmodified recipe)
Modified recipe or my own script
Reproduction
Summary
UniRL's optimizer checkpoint save/load path is not round-trip safe when an
AdamW optimizer is partially initialized:
some parameters have received gradients and own AdamW state;
other parameters are in optimizer param_groups but have never received a
gradient and legitimately have no state entry.
The checkpoint can be saved successfully, but restoring it through set_optimizer_state_dict fails because the loader requires a state entry for
every parameter listed in param_groups.
This is a generic checkpoint-boundary issue. It was first exposed by a
not-yet-merged Qwen-Image-Edit SFT recipe, but it is not caused by that recipe
or by SFT.
Minimal CPU reproduction
This reproducer does not depend on Qwen, LoRA, FSDP, DyRef, a dataset or a GPU:
state 2 params 4 missing ['unused.weight', 'unused.bias']
KeyError: 'unused.weight'
unused.weight and unused.bias are valid AdamW parameters. They simply have
not received a gradient yet, so AdamW has not lazily created step, exp_avg
or exp_avg_sq for them.
PR #440 fixed a related but narrower case: exporting an entirely cold AdamW
optimizer without advancing its first-step clock.
Its guard requires:
notoptimizer.state
That condition is false for a partially initialized optimizer: once any
parameter has been updated, optimizer.state is non-empty even if other
parameters still have no state.
Therefore the partially initialized case passes through the #440 export
boundary unchanged and remains unhandled.
The issue was first observed while resuming a Qwen-Image-Edit LoRA SFT run.
The final QwenImage transformer block still consumes text K/V for image
attention, but its updated text output is discarded after the block. LoRA
parameters attached only to the final text-output path consequently keep grad=None.
The observed optimizer contained:
1440 parameter names in param_groups
1436 entries in state
4 valid, never-initialized entries absent from state
The missing entries were LoRA A/B under these final-block modules:
Their resume paths have not yet been runtime-verified for this failure, so they
are potential affected paths rather than confirmed reproductions.
The custom SFT recipe made the partially initialized state deterministic and
exposed the existing checkpoint assumption; it did not introduce that
assumption.
Workaround validation
Adding only an empty dictionary for each missing optimizer-state entry makes
the same checkpoint load successfully:
load: OK
future lazy init: ['exp_avg', 'exp_avg_sq', 'step']
On the real Qwen run:
only 4 / 1440 empty entries were added;
every existing optimizer tensor was preserved unchanged;
model, scheduler and data-cursor state were unchanged;
resume restored step 2517 and data epoch 5.000;
the first resumed update had finite loss and gradients;
the subsequently saved checkpoint at step 3021 resumed successfully.
This preserves standard AdamW lazy-initialization semantics.
Impact
Any UniRL training path can encounter this when its optimizer contains a
parameter that does not receive a gradient before checkpointing. Possible
causes include:
an intentionally unused terminal branch;
conditional routing;
sparse experts;
optional heads;
parameters activated only by certain batches;
broad LoRA target matching.
The failure is delayed until resume, so training and checkpoint saving both
appear successful.
This does not affect weight-only or exported-adapter inference loading.
The torch-format path is confirmed. The DCP-format path should be audited but
is not yet claimed as runtime-confirmed.
Proposed direction
Normalize valid sparse optimizer state at the shared checkpoint boundary:
use optimizer param_groups as the authoritative parameter inventory;
preserve every existing optimizer-state entry exactly;
materialize {} only when the entire state entry for a listed parameter is
absent;
Torch and DCP checkpoint paths are both audited; any unverified path is
documented explicitly.
Your contribution
I plan to work on a focused root fix and submit a PR after maintainer
confirmation.
I will keep the change in the shared optimizer checkpoint boundary rather than
special-casing Qwen-Image or modifying individual recipes. Verification
commands and results will be included in the PR Test Plan; no one-off test
directory will be committed.
AI assistance: the investigation and proposed fix were developed with AI
assistance. I reviewed the checkpoint contents, the origin/main checkpoint
path, the relationship to #440, and the workaround behavior.
Expected behavior
A checkpoint emitted by UniRL must be consumable by UniRL's corresponding
resume path.
AdamW parameters listed in optimizer param_groups may legitimately have no
state entry when they have never received a gradient. Resume should preserve
such parameters as lazily uninitialized while restoring every existing
optimizer tensor, scheduler state, optimizer-step count, model state and
trainer state unchanged.
The fix should apply at the generic optimizer checkpoint boundary, preserve the
fully cold behavior from #440, remain backward-compatible with existing sparse
checkpoints, and not weaken strict model-state validation.
System info
source /data/unirl-dyref/venv-sglang/bin/activate
git rev-parse HEAD
python -c "import sys, platform; print(sys.version); print(platform.platform())"
python -c "import torch; print('torch', torch.version, 'cuda', torch.version.cuda); print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'no cuda')"
python -c "import importlib.metadata as m
for pkg in ('unirl', 'vllm', 'vllm-omni', 'sglang', 'torch'):
try:
print(pkg, m.version(pkg))
except m.PackageNotFoundError:
print(pkg, 'not installed')"
Rollout / inference engine
Train-side only
Domain
sft (train_sft)
Recipe / config-name
N/A (generic checkpoint reproducer; first observed in a modified Qwen-Image-Edit SFT recipe)
How you ran it
examples/(unmodified recipe)Reproduction
Summary
UniRL's optimizer checkpoint save/load path is not round-trip safe when an
AdamW optimizer is partially initialized:
param_groupsbut have never received agradient and legitimately have no state entry.
The checkpoint can be saved successfully, but restoring it through
set_optimizer_state_dictfails because the loader requires a state entry forevery parameter listed in
param_groups.This is a generic checkpoint-boundary issue. It was first exposed by a
not-yet-merged Qwen-Image-Edit SFT recipe, but it is not caused by that recipe
or by SFT.
Minimal CPU reproduction
This reproducer does not depend on Qwen, LoRA, FSDP, DyRef, a dataset or a GPU:
Observed with Python 3.11.6 and torch 2.5.0:
unused.weightandunused.biasare valid AdamW parameters. They simply havenot received a gradient yet, so AdamW has not lazily created
step,exp_avgor
exp_avg_sqfor them.UniRL code path
UniRL uses this same pair of APIs:
gather_optimizer_state_dict/sharded_optimizer_state_dictcallget_optimizer_state_dict;load_optimizer_state_dict/load_sharded_optimizer_state_dictcallset_optimizer_state_dict;BaseFSDP2Backendsaves the resulting optimizer state and restores itunconditionally during training-state resume.
As a result, UniRL can emit an optimizer checkpoint that its own resume path
cannot consume.
Static audit was performed against:
Relation to PR #440
PR #440 fixed a related but narrower case: exporting an entirely cold AdamW
optimizer without advancing its first-step clock.
Its guard requires:
That condition is false for a partially initialized optimizer: once any
parameter has been updated,
optimizer.stateis non-empty even if otherparameters still have no state.
Therefore the partially initialized case passes through the #440 export
boundary unchanged and remains unhandled.
Related PR:
#440
Qwen-Image trigger
The issue was first observed while resuming a Qwen-Image-Edit LoRA SFT run.
The final QwenImage transformer block still consumes text K/V for image
attention, but its updated text output is discarded after the block. LoRA
parameters attached only to the final text-output path consequently keep
grad=None.The observed optimizer contained:
The missing entries were LoRA A/B under these final-block modules:
Existing merged Qwen-Image and Qwen-Image-Edit training recipes already target
the same terminal
attn.to_add_outmodule, for example:Their resume paths have not yet been runtime-verified for this failure, so they
are potential affected paths rather than confirmed reproductions.
The custom SFT recipe made the partially initialized state deterministic and
exposed the existing checkpoint assumption; it did not introduce that
assumption.
Workaround validation
Adding only an empty dictionary for each missing optimizer-state entry makes
the same checkpoint load successfully:
Validation results:
On the real Qwen run:
This preserves standard AdamW lazy-initialization semantics.
Impact
Any UniRL training path can encounter this when its optimizer contains a
parameter that does not receive a gradient before checkpointing. Possible
causes include:
The failure is delayed until resume, so training and checkpoint saving both
appear successful.
This does not affect weight-only or exported-adapter inference loading.
The torch-format path is confirmed. The DCP-format path should be audited but
is not yet claimed as runtime-confirmed.
Proposed direction
Normalize valid sparse optimizer state at the shared checkpoint boundary:
param_groupsas the authoritative parameter inventory;{}only when the entire state entry for a listed parameter isabsent;
step,exp_avgorexp_avg_sq;This should be a generic optimizer checkpoint fix, not a Qwen-, LoRA-,
algorithm- or recipe-specific exception.
Acceptance criteria
another has never received a gradient.
step,exp_avgandexp_avg_sqtensors are unchanged.first future gradient.
documented explicitly.
Your contribution
I plan to work on a focused root fix and submit a PR after maintainer
confirmation.
I will keep the change in the shared optimizer checkpoint boundary rather than
special-casing Qwen-Image or modifying individual recipes. Verification
commands and results will be included in the PR Test Plan; no one-off test
directory will be committed.
AI assistance: the investigation and proposed fix were developed with AI
assistance. I reviewed the checkpoint contents, the
origin/maincheckpointpath, the relationship to #440, and the workaround behavior.
Expected behavior
A checkpoint emitted by UniRL must be consumable by UniRL's corresponding
resume path.
AdamW parameters listed in optimizer
param_groupsmay legitimately have nostate entry when they have never received a gradient. Resume should preserve
such parameters as lazily uninitialized while restoring every existing
optimizer tensor, scheduler state, optimizer-step count, model state and
trainer state unchanged.
The fix should apply at the generic optimizer checkpoint boundary, preserve the
fully cold behavior from #440, remain backward-compatible with existing sparse
checkpoints, and not weaken strict model-state validation.