Skip to content

Commit eaa397b

Browse files
committed
Pass min_snr_gamma_soft to apply_snr_weight and apply_snr_weight_for_flow_matching
Wire the --min_snr_gamma_soft flag (args.min_snr_gamma_soft) through to the soft kwarg at all 21 call sites across the codebase: apply_snr_weight (12 sites): train_network, fine_tune, train_db, train_leco, sdxl_train, sdxl_train_leco, sdxl_train_control_net, sdxl_train_control_net_lllite, sdxl_train_control_net_lllite_old, train_control_net, train_textual_inversion, train_textual_inversion_XTI. apply_snr_weight_for_flow_matching (9 sites): flux_train, sd3_train, lumina_train, anima_train, flux_train_network, sd3_train_network, lumina_train_network, hunyuan_image_train_network, anima_train_network. Both functions already accept soft: bool = False. The --min_snr_gamma_soft flag and soft-mode implementations in custom_train_functions.py were already in place. This commit connects the training args to the function calls. Adds test_apply_snr_weight_soft.py with 34 tests covering: - Soft vs hard weight formula correctness for both DDPM and flow matching - Edge cases (sigma=0, sigma=1, near-zero clamping) - Gradient flow through soft weights - Smoothness advantage of soft over hard at gamma threshold - CLI argument parsing for --min_snr_gamma_soft
1 parent 586cec9 commit eaa397b

23 files changed

Lines changed: 383 additions & 45 deletions

anima_train.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,7 @@ def grad_hook(tensor: torch.Tensor):
613613

614614
# Min-SNR-γ for flow matching
615615
if args.min_snr_gamma:
616-
loss = apply_snr_weight_for_flow_matching(loss, sigmas, args.min_snr_gamma)
616+
loss = apply_snr_weight_for_flow_matching(loss, sigmas, args.min_snr_gamma, soft=args.min_snr_gamma_soft)
617617

618618
if getattr(args, "contrastive_flow_matching", False) and latents.size(0) > 1:
619619
# CRITICAL: .detach() prevents gradients flowing through negative samples

anima_train_network.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1126,7 +1126,7 @@ def post_process_loss(self, loss, args, timesteps, noise_scheduler):
11261126
if args.min_snr_gamma:
11271127
# Anima timesteps are already in [0, 1] range (sigmas) — they are divided by 1000
11281128
# in get_noise_pred_and_target before being returned
1129-
loss = apply_snr_weight_for_flow_matching(loss, timesteps, args.min_snr_gamma)
1129+
loss = apply_snr_weight_for_flow_matching(loss, timesteps, args.min_snr_gamma, soft=args.min_snr_gamma_soft)
11301130
return loss
11311131

11321132
def get_sai_model_spec(self, args):

fine_tune.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ def fn_recursive_set_mem_eff(module: torch.nn.Module):
419419
loss = loss.mean([1, 2, 3])
420420

421421
if args.min_snr_gamma:
422-
loss = apply_snr_weight(loss, timesteps, noise_scheduler, args.min_snr_gamma, args.v_parameterization)
422+
loss = apply_snr_weight(loss, timesteps, noise_scheduler, args.min_snr_gamma, args.v_parameterization, soft=args.min_snr_gamma_soft)
423423
if args.scale_v_pred_loss_like_noise_pred:
424424
loss = scale_v_prediction_loss_like_noise_prediction(loss, timesteps, noise_scheduler)
425425
if args.debiased_estimation_loss:

flux_train.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,7 @@ def grad_hook(parameter: torch.Tensor):
679679

680680
# Min-SNR-γ for flow matching
681681
if args.min_snr_gamma:
682-
loss = apply_snr_weight_for_flow_matching(loss, sigmas, args.min_snr_gamma)
682+
loss = apply_snr_weight_for_flow_matching(loss, sigmas, args.min_snr_gamma, soft=args.min_snr_gamma_soft)
683683

684684
if args.masked_loss or ("alpha_masks" in batch and batch["alpha_masks"] is not None):
685685
loss = apply_masked_loss(loss, batch)

flux_train_network.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ def post_process_loss(self, loss, args, timesteps, noise_scheduler):
462462
if args.min_snr_gamma:
463463
# Convert timesteps (in [0, 1000] range) to flow matching sigmas (in [0, 1] range)
464464
sigmas = timesteps / noise_scheduler.config.num_train_timesteps
465-
loss = apply_snr_weight_for_flow_matching(loss, sigmas, args.min_snr_gamma)
465+
loss = apply_snr_weight_for_flow_matching(loss, sigmas, args.min_snr_gamma, soft=args.min_snr_gamma_soft)
466466
return loss
467467

468468
def get_sai_model_spec(self, args):

hunyuan_image_train_network.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ def post_process_loss(self, loss, args, timesteps, noise_scheduler):
604604
if args.min_snr_gamma:
605605
# Convert timesteps (in [0, 1000] range) to flow matching sigmas (in [0, 1] range)
606606
sigmas = timesteps / noise_scheduler.config.num_train_timesteps
607-
loss = apply_snr_weight_for_flow_matching(loss, sigmas, args.min_snr_gamma)
607+
loss = apply_snr_weight_for_flow_matching(loss, sigmas, args.min_snr_gamma, soft=args.min_snr_gamma_soft)
608608
return loss
609609

610610
def get_sai_model_spec(self, args):

library/custom_train_functions.py

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -77,38 +77,65 @@ def enforce_zero_terminal_snr(betas):
7777
noise_scheduler.alphas = alphas
7878
noise_scheduler.alphas_cumprod = alphas_cumprod
7979

80+
def apply_snr_weight(
81+
loss: torch.Tensor,
82+
timesteps: torch.IntTensor,
83+
noise_scheduler,
84+
gamma: float,
85+
v_prediction: bool = False,
86+
soft: bool = False,
87+
):
88+
"""Apply Min-SNR or Soft Min-SNR weighting to the loss.
8089
81-
def apply_snr_weight(loss: torch.Tensor, timesteps: torch.IntTensor, noise_scheduler: DDPMScheduler, gamma: Number, v_prediction=False):
82-
snr = torch.stack([noise_scheduler.all_snr[t] for t in timesteps])
83-
min_snr_gamma = torch.minimum(snr, torch.full_like(snr, gamma))
84-
if v_prediction:
85-
snr_weight = torch.div(min_snr_gamma, snr + 1)
86-
else:
87-
snr_weight = torch.div(min_snr_gamma, snr)
90+
Args:
91+
loss: Per-element loss tensor.
92+
timesteps: Timesteps for each sample in the batch.
93+
noise_scheduler: DDPMScheduler containing the precomputed SNR values.
94+
gamma: Min-SNR clipping value (typically 4.0 or 5.0).
95+
v_prediction: Set to True if the model predicts velocity (v).
96+
soft: Set to True to use the smooth Soft Min-SNR transition from the paper.
97+
"""
98+
# Retrieve SNR values and move to the correct device
99+
snr = torch.stack([noise_scheduler.all_snr[t] for t in timesteps]).to(device=loss.device)
88100

89-
snr_weight = snr_weight.to(dtype=loss.dtype, device=loss.device)
101+
if soft:
102+
if v_prediction:
103+
snr_weight = (snr * gamma) / ((snr + gamma) * (snr + 1))
104+
else:
105+
snr_weight = gamma / (snr + gamma)
106+
else:
107+
min_snr_gamma = torch.minimum(snr, torch.full_like(snr, gamma))
108+
if v_prediction:
109+
snr_weight = torch.div(min_snr_gamma, snr + 1)
110+
else:
111+
snr_weight = torch.div(min_snr_gamma, snr)
90112

91-
loss = loss * snr_weight
92-
return loss
113+
snr_weight = snr_weight.to(dtype=loss.dtype)
93114

115+
# Ensure snr_weight dimensions match loss for proper broadcasting
116+
while snr_weight.ndim < loss.ndim:
117+
snr_weight = snr_weight.unsqueeze(-1)
94118

95-
def apply_snr_weight_for_flow_matching(loss: torch.Tensor, sigmas: torch.Tensor, gamma: float) -> torch.Tensor:
96-
"""Apply Min-SNR-γ weighting for flow matching (rectified flow) models.
119+
return loss * snr_weight
97120

98-
Computes the signal-to-noise ratio from sigma: SNR = (1 - σ)² / σ²
99-
and applies the velocity-prediction weight: min(SNR, γ) / (SNR + 1).
100121

101-
This is the flow-matching analog of apply_snr_weight for DDPM models.
102-
Flow matching velocity prediction (v = ε - x₀) is mathematically analogous
103-
to v-prediction in DDPM, so the v-prediction formula is used.
122+
def apply_snr_weight_for_flow_matching(
123+
loss: torch.Tensor,
124+
sigmas: torch.Tensor,
125+
gamma: float,
126+
soft: bool = False
127+
) -> torch.Tensor:
128+
"""Apply Min-SNR-γ or Soft Min-SNR-γ weighting for flow matching models.
104129
105-
Reference: https://arxiv.org/abs/2303.09556
130+
Computes the signal-to-noise ratio from sigma: SNR = (1 - σ)² / σ²
131+
and applies the velocity-prediction weight.
106132
107133
Args:
108134
loss: Per-element loss tensor (any shape, e.g. (B,) or (B, C, H, W)).
109135
sigmas: Noise levels from the flow matching scheduler.
110136
Can be shape (B, 1, 1, 1), (B,), or broadcastable with loss.
111-
gamma: Min-SNR gamma value. 5.0 is recommended by the paper.
137+
gamma: Min-SNR gamma value.
138+
soft: Set to True to use the smooth Soft Min-SNR transition from the paper.
112139
113140
Returns:
114141
Weighted loss tensor (same shape as input loss).
@@ -119,11 +146,14 @@ def apply_snr_weight_for_flow_matching(loss: torch.Tensor, sigmas: torch.Tensor,
119146
# SNR in flow matching: (1 - σ)² / σ²
120147
snr = ((1.0 - sigma) / sigma) ** 2
121148

122-
# Cap SNR at gamma
123-
min_snr = torch.minimum(snr, torch.full_like(snr, gamma))
124-
125-
# Velocity prediction weight: min(SNR, γ) / (SNR + 1)
126-
snr_weight = min_snr / (snr + 1)
149+
if soft:
150+
# Velocity prediction weight using Soft Min-SNR
151+
snr_weight = (snr * gamma) / ((snr + gamma) * (snr + 1))
152+
else:
153+
# Cap SNR at gamma
154+
min_snr = torch.minimum(snr, torch.full_like(snr, gamma))
155+
# Velocity prediction weight: min(SNR, γ) / (SNR + 1)
156+
snr_weight = min_snr / (snr + 1)
127157

128158
snr_weight = snr_weight.to(dtype=loss.dtype, device=loss.device)
129159

@@ -173,6 +203,13 @@ def add_custom_train_arguments(parser: argparse.ArgumentParser, support_weighted
173203
default=None,
174204
help="gamma for reducing the weight of high loss timesteps. Lower numbers have stronger effect. 5 is recommended by paper. / 低いタイムステップでの高いlossに対して重みを減らすためのgamma値、低いほど効果が強く、論文では5が推奨",
175205
)
206+
207+
parser.add_argument(
208+
"--min_snr_gamma_soft",
209+
action="store_true",
210+
help="Controls if min_snr_gamma uses soft implementation from https://arxiv.org/abs/2401.11605.",
211+
)
212+
176213
parser.add_argument(
177214
"--scale_v_pred_loss_like_noise_pred",
178215
action="store_true",

lumina_train.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,7 @@ def grad_hook(parameter: torch.Tensor):
777777

778778
# Min-SNR-γ for flow matching
779779
if args.min_snr_gamma:
780-
loss = apply_snr_weight_for_flow_matching(loss, sigmas, args.min_snr_gamma)
780+
loss = apply_snr_weight_for_flow_matching(loss, sigmas, args.min_snr_gamma, soft=args.min_snr_gamma_soft)
781781

782782
if args.masked_loss or (
783783
"alpha_masks" in batch and batch["alpha_masks"] is not None

lumina_train_network.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ def post_process_loss(self, loss, args, timesteps, noise_scheduler):
346346
if args.min_snr_gamma:
347347
# Convert timesteps (in [0, 1000] range) to flow matching sigmas (in [0, 1] range)
348348
sigmas = timesteps / noise_scheduler.config.num_train_timesteps
349-
loss = apply_snr_weight_for_flow_matching(loss, sigmas, args.min_snr_gamma)
349+
loss = apply_snr_weight_for_flow_matching(loss, sigmas, args.min_snr_gamma, soft=args.min_snr_gamma_soft)
350350
return loss
351351

352352
def get_sai_model_spec(self, args):

sd3_train.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -856,7 +856,7 @@ def grad_hook(parameter: torch.Tensor):
856856

857857
# Min-SNR-γ for flow matching (applied before spatial mean so sigmas broadcast correctly)
858858
if args.min_snr_gamma:
859-
loss = apply_snr_weight_for_flow_matching(loss, sigmas, args.min_snr_gamma)
859+
loss = apply_snr_weight_for_flow_matching(loss, sigmas, args.min_snr_gamma, soft=args.min_snr_gamma_soft)
860860

861861
loss = loss.mean([1, 2, 3])
862862

0 commit comments

Comments
 (0)