Skip to content

fix(training): EMA updates on skipped optimizer step with AMP - #15173

Open
nrenvo wants to merge 2 commits into
huggingface:mainfrom
nrenvo:fix/ema-skipped-optimizer-step
Open

nrenvo wants to merge 2 commits into
huggingface:mainfrom
nrenvo:fix/ema-skipped-optimizer-step

Conversation

@nrenvo

@nrenvo nrenvo commented Nov 3, 2026 •

Copy link
Copy Markdown

Problem

When AMP skips an optimizer step due to gradient overflow, EMA still updated with unchanged weights. Also, train_loss was reset inside the skipped-step guard, so the accumulator wasn't cleared on overflow.

An overflow at the last batch of an epoch also caused the epoch loop to exit with global_step < max_train_steps.

Fix

Guard EMA update and step bookkeeping with not accelerator.optimizer_step_was_skipped. Move train_loss = 0.0 outside the guard so it resets on every gradient sync.

Replace for epoch in range(...) with while global_step < args.max_train_steps to ensure training always completes the requested steps.

Test

Added a parametrized test that simulates an overflow on step 2 and asserts EMA is never called for a skipped step.

Added test_text_to_image_ema_skipped_step_at_epoch_boundary to cover overflow at epoch boundary.

Fixes #15112

@sayakpaul @linoytsaban @ZihaoW123

…uggingface#15112)

When AMP causes a gradient overflow, accelerate skips the optimizer step
but the previous code still updated EMA, progressed the step counter, and
logged metrics. Guard the EMA update (and all per-step bookkeeping) with
`not accelerator.optimizer_step_was_skipped`. Also move `train_loss = 0.0`
outside that guard so the loss accumulator is still cleared on any
`sync_gradients` boundary, including overflowed ones.

Adds a parametrized unit test that monkeypatches AcceleratedOptimizer to
simulate an overflow on step 2 and asserts EMA is never called for a
skipped step while the applied-step count stays correct.
…izer step

When AMP skips an optimizer step at the last batch of an epoch, the
static `num_train_epochs` ceiling (computed before any overflow occurs)
caused the outer epoch loop to exit with `global_step < max_train_steps`.

Replace `for epoch in range(first_epoch, args.num_train_epochs)` with a
`while global_step < args.max_train_steps` loop so the training script
always completes the requested number of successful optimizer steps,
regardless of where in an epoch the overflow happens.

Add `test_text_to_image_ema_skipped_step_at_epoch_boundary` to pin this
behaviour: 2 steps/epoch, overflow on step 2 (epoch boundary), training
must still reach global_step == 2 by spilling into a third epoch.
@github-actions github-actions Bot added examples fixes-issue size/M PR with diff < 200 LOC labels Nov 3, 2026

@sayakpaul sayakpaul 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.

Please explain the changes and how they relate to EMA updates.

@@ -35,6 +46,174 @@


class TestTextToImage(ExamplesTestsAccelerate):

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.

Could the tests go to https://github.com/huggingface/diffusers/blob/main/tests/others/test_ema.py?

Also, do the other training scripts need to propagate this update too?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The tests here launch the full training script end-to-end via runpy, monkeypatching Accelerator, AcceleratedOptimizer, and EMAModel together. test_ema.py covers EMAModel in isolation (shadow params, decay, serialization) so they feel like different things. Keeping them next to the script they exercise seemed more natural.

Propagated the fix to all other scripts using EMAModel in this PR: train_text_to_image_sdxl, train_instruct_pix2pix, train_instruct_pix2pix_sdxl, train_unconditional, train_amused, train_vqgan.


# Checks if the accelerator has performed an optimization step behind the scenes
if accelerator.sync_gradients:
if accelerator.sync_gradients and not accelerator.optimizer_step_was_skipped:

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.

optimizer_step_was_skipped -- does it warrant any minimum version requirement checks?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Present since at least accelerate v0.10.0, so no version bump needed — the examples requirements.txt already requires >=0.16.0.

progress_bar.update(1)
global_step += 1
accelerator.log({"train_loss": train_loss}, step=global_step)
train_loss = 0.0

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.

Could you explain this change?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

train_loss should reset on every sync_gradients, including skipped steps — otherwise the loss from a skipped step bleeds into the next logged value. Moving it outside the skipped-step guard ensures it always resets on a gradient sync.

Comment on lines -954 to +955
for epoch in range(first_epoch, args.num_train_epochs):
epoch = first_epoch
while global_step < args.max_train_steps:

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 do these changes relate to EMA updates?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Not directly related to EMA. It fixes a separate issue: if an overflow hits the last batch of an epoch, global_step is not incremented and the for-loop exits before reaching max_train_steps. The while loop ensures training always completes the requested number of steps.

@nrenvo

nrenvo commented Nov 5, 2026

Copy link
Copy Markdown
Author

Two independent fixes:

  1. EMA skip guard: when AMP skips an optimizer step due to gradient overflow, weights are unchanged. Updating EMA with unchanged weights biases the moving average, so EMA should only step when the optimizer actually applied gradients.

  2. Epoch boundary fix: if overflow hits the last batch of an epoch, global_step is not incremented and the for-loop exits before reaching max_train_steps. The while loop ensures training always completes the requested steps. Unrelated to EMA correctness but discovered in the same context.

@sayakpaul

Copy link
Copy Markdown
Member

Two independent fixes:

Can we split it into two PRs then?

@nrenvo

nrenvo commented Nov 5, 2026

Copy link
Copy Markdown
Author

Split into two PRs as requested:

@nrenvo

nrenvo commented Nov 9, 2026

Copy link
Copy Markdown
Author

@sayakpaul hi, following up on your suggestion from a few days ago — split #15173 into two separate PRs: #15189 (EMA guard) and #15190 (epoch boundary fix).

@sayakpaul sayakpaul 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.

Let's remove the test. It's verbose and tests something that is not that much used by the community through our scripts.

@nrenvo

nrenvo commented Nov 10, 2026

Copy link
Copy Markdown
Author

@sayakpaul thanks for the approval! Removed the tests from #15189 and #15190 as suggested. Just wanted to check — would you like to proceed with the two split PRs, or merge #15173 directly?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

examples fixes-issue size/M PR with diff < 200 LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Text-to-image example updates EMA when AMP skips the optimizer step

2 participants