forked from kohya-ss/sd-scripts
-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathtrain_network.py
More file actions
3528 lines (3064 loc) · 173 KB
/
Copy pathtrain_network.py
File metadata and controls
3528 lines (3064 loc) · 173 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import gc
import importlib
import argparse
import math
import os
import typing
from typing import Any, List, Union, Optional
import sys
import random
import time
import json
from multiprocessing import Value
import numpy as np
import ast
import itertools
from tqdm import tqdm
import torch
import torch.nn as nn
from torch.types import Number
from library.device_utils import init_ipex, clean_memory_on_device
from library.edm2_loss_utils import prepare_edm2_loss_weighting, plot_edm2_loss_weighting_check, plot_edm2_loss_weighting
from library.ramtorch_util import apply_ramtorch_to_module
init_ipex()
from accelerate import Accelerator
from diffusers import DDPMScheduler
from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
from library import deepspeed_utils, model_util, sai_model_spec, strategy_base, strategy_sd, sai_model_spec
from library.strategy_sdxl import SdxlTextEncodingStrategy
import library.train_util as train_util
from library.train_util import DreamBoothDataset
from library.focal_frequency_loss import FocalFrequencyLoss
import library.config_util as config_util
from library.config_util import (
ConfigSanitizer,
BlueprintGenerator,
)
import library.huggingface_util as huggingface_util
import library.custom_train_functions as custom_train_functions
from library.adaptive_timestep_sampler import TimestepSamplerNetwork, AdaptiveTimestepManager
from library.custom_train_functions import (
apply_snr_weight,
get_weighted_text_embeddings,
prepare_scheduler_for_custom_training,
scale_v_prediction_loss_like_noise_prediction,
add_v_prediction_like_loss,
apply_debiased_estimation,
apply_masked_loss,
)
from library.utils import setup_logging, add_logging_arguments
# T-LoRA timestep-dependent rank masking support
try:
from lycoris.modules.tlora import set_timestep_mask, clear_timestep_mask, compute_timestep_mask, compute_timestep_mask_batch
TLORA_AVAILABLE = True
except ImportError:
TLORA_AVAILABLE = False
# LoRA² adaptive rank regularization support
try:
from lycoris.modules.lora2 import LoRA2Module
LORA2_AVAILABLE = True
except ImportError:
LORA2_AVAILABLE = False
setup_logging()
import logging
import warnings
warnings.filterwarnings("ignore", category=UserWarning, module="torchao")
logging.getLogger("torch.distributed.elastic.multiprocessing.redirects").setLevel(logging.ERROR)
logger = logging.getLogger(__name__)
class NetworkTrainer:
def __init__(self):
self.vae_scale_factor = 0.18215
self.is_sdxl = False
self.latent_shift = 0.0
# Weight Noising config (inspired by ai-toolkit-perceptual)
self.weight_noise_enabled = False
# T-LoRA timestep-dependent rank masking config
self.tlora_enabled = False
self.tlora_max_rank = 0
self.tlora_min_rank = 1
self.tlora_mask_alpha = 1.0
self.tlora_max_timestep = 1000
self.tlora_use_network_method = False # True → use network.set_timestep_mask()
# LoRA² adaptive rank regularization config
self.lora2_enabled = False
self.lora2_lambda_r = 1e-4
# Adaptive Non-uniform Timestep Sampling
self.adaptive_manager = None
self._adaptive_last_latents = None
self._adaptive_last_noise = None
self._adaptive_last_text_conds = None
self._adaptive_last_args = None
self._adaptive_last_batch = None
self._adaptive_losses_before = None
# Focal Frequency Loss config
self.ffl_enabled = False
self.ffl_module = None
self.ffl_loss_value = None
# Latent Wavelet Diffusion (LWD) masking config
self.wavelet_masking_enabled = False
self.wavelet_dwt = None
self._noisy_latents = None # stored by get_noise_pred_and_target for wavelet map computation
self._wavelet_mask_ratio = 0.0 # fraction of loss elements masked (for logging)
# TODO 他のスクリプトと共通化する
def generate_step_logs(
self,
args: argparse.Namespace,
current_loss,
avr_loss,
lr_scheduler,
lr_descriptions,
optimizer=None,
keys_scaled=None,
mean_norm=None,
maximum_norm=None,
mean_grad_norm=None,
mean_combined_norm=None,
edm2_lr_scheduler=None,
current_loss_scaled=None,
average_loss_scaled=None,
current_loss_edm2=None,
average_loss_edm2=None,
current_val_loss=None,
average_val_loss=None,
current_ffl_loss=None,
current_wav_mask_ratio=None,
current_weight_noise_norm=None,
it_s: float = 0.0,
):
logs = {"loss/current": current_loss, "loss/average": avr_loss}
if current_loss_scaled is not None:
logs["loss/current_scaled"] = current_loss_scaled
logs["loss/average_scaled"] = average_loss_scaled
if current_loss_edm2 is not None:
logs["loss/current_edm2"] = current_loss_edm2
logs["loss/average_edm2"] = average_loss_edm2
if current_ffl_loss is not None:
logs["loss/current_ffl"] = current_ffl_loss
if current_wav_mask_ratio is not None:
logs["loss/wavelet_mask_ratio"] = current_wav_mask_ratio
if current_weight_noise_norm is not None:
logs["weight_noise/noise_norm"] = current_weight_noise_norm
if keys_scaled is not None:
logs["max_norm/keys_scaled"] = keys_scaled
logs["max_norm/max_key_norm"] = maximum_norm
if mean_norm is not None:
logs["norm/avg_key_norm"] = mean_norm
if mean_grad_norm is not None:
logs["norm/avg_grad_norm"] = mean_grad_norm
if mean_combined_norm is not None:
logs["norm/avg_combined_norm"] = mean_combined_norm
if current_val_loss is not None:
logs["loss/current_val_loss"] = current_val_loss
logs["loss/average_val_loss"] = average_val_loss
lrs = lr_scheduler.get_last_lr()
for i, lr in enumerate(lrs):
if lr_descriptions is not None:
lr_desc = lr_descriptions[i]
else:
idx = i - (0 if args.network_train_unet_only else 1)
if idx == -1:
lr_desc = "textencoder"
else:
if len(lrs) > 2:
lr_desc = f"group{i}"
else:
lr_desc = "unet"
logs[f"lr/{lr_desc}"] = lr
if args.optimizer_type.lower().startswith("DAdapt".lower()) or args.optimizer_type.lower().startswith("Prodigy".lower()):
opt = lr_scheduler.optimizers[-1] if hasattr(lr_scheduler, "optimizers") else optimizer
if opt is not None:
logs[f"lr/d*lr/{lr_desc}"] = opt.param_groups[i]["d"] * opt.param_groups[i]["lr"]
if "effective_lr" in opt.param_groups[i]:
logs[f"lr/d*eff_lr/{lr_desc}"] = opt.param_groups[i]["d"] * opt.param_groups[i]["effective_lr"]
# Log scheduled_lr for Polyak step-size optimizers (e.g. AdamWScheduleFreePlus)
opt_for_scheduled = lr_scheduler.optimizers[-1] if hasattr(lr_scheduler, "optimizers") else optimizer
if opt_for_scheduled is not None and "scheduled_lr" in opt_for_scheduled.param_groups[i]:
logs[f"lr/scheduled/{lr_desc}"] = opt_for_scheduled.param_groups[i]["scheduled_lr"]
if edm2_lr_scheduler is not None:
logs[f"lr/edm2"] = edm2_lr_scheduler.get_last_lr()[0]
if it_s > 0:
logs["train/it_s"] = round(it_s, 4)
return logs
def step_logging(self, accelerator: Accelerator, logs: dict, global_step: int, epoch: int):
self.accelerator_logging(accelerator, logs, global_step, global_step, epoch)
def epoch_logging(self, accelerator: Accelerator, logs: dict, global_step: int, epoch: int):
self.accelerator_logging(accelerator, logs, epoch, global_step, epoch)
def accelerator_logging(
self, accelerator: Accelerator, logs: dict, step_value: int, global_step: int, epoch: int):
"""
step_value is for tensorboard, other values are for wandb
"""
tensorboard_tracker = None
wandb_tracker = None
other_trackers = []
for tracker in accelerator.trackers:
if tracker.name == "tensorboard":
tensorboard_tracker = accelerator.get_tracker("tensorboard")
elif tracker.name == "wandb":
wandb_tracker = accelerator.get_tracker("wandb")
else:
other_trackers.append(accelerator.get_tracker(tracker.name))
if tensorboard_tracker is not None:
tensorboard_tracker.log(logs, step=step_value)
if wandb_tracker is not None:
logs["global_step"] = global_step
logs["epoch"] = epoch
wandb_tracker.log(logs)
for tracker in other_trackers:
tracker.log(logs, step=step_value)
def assert_extra_args(
self,
args,
train_dataset_group: Union[train_util.DatasetGroup, train_util.MinimalDataset],
val_dataset_group: Optional[train_util.DatasetGroup],
):
train_dataset_group.verify_bucket_reso_steps(64)
if val_dataset_group is not None:
val_dataset_group.verify_bucket_reso_steps(64)
def load_target_model(self, args, weight_dtype, accelerator) -> tuple[str, nn.Module, nn.Module, Optional[nn.Module]]:
text_encoder, vae, unet, _ = train_util.load_target_model(args, weight_dtype, accelerator)
if args.use_ramtorch:
logger.info("Applying RamTorch to SD model.")
unet = apply_ramtorch_to_module(unet, "unet", accelerator.device, weight_dtype)
text_encoder = apply_ramtorch_to_module(text_encoder, "clip_l", accelerator.device, weight_dtype)
# モデルに xformers とか memory efficient attention を組み込む
train_util.replace_unet_modules(unet, args.mem_eff_attn, args.xformers, args.sdpa)
if torch.__version__ >= "2.0.0": # PyTorch 2.0.0 以上対応のxformersなら以下が使える
vae.set_use_memory_efficient_attention_xformers(args.xformers)
return model_util.get_model_version_str_for_sd1_sd2(args.v2, args.v_parameterization), text_encoder, vae, unet
def load_unet_lazily(self, args, weight_dtype, accelerator, text_encoders) -> tuple[nn.Module, List[nn.Module]]:
raise NotImplementedError()
def get_tokenize_strategy(self, args):
return strategy_sd.SdTokenizeStrategy(args.v2, args.max_token_length, args.tokenizer_cache_dir)
def get_tokenizers(self, tokenize_strategy: strategy_sd.SdTokenizeStrategy) -> List[Any]:
return [tokenize_strategy.tokenizer]
def get_latents_caching_strategy(self, args):
latents_caching_strategy = strategy_sd.SdSdxlLatentsCachingStrategy(
True, args.cache_latents_to_disk, args.vae_batch_size, args.skip_cache_check
)
return latents_caching_strategy
def get_text_encoding_strategy(self, args):
return strategy_sd.SdTextEncodingStrategy(args.clip_skip)
def get_text_encoder_outputs_caching_strategy(self, args):
return None
def get_models_for_text_encoding(self, args, accelerator, text_encoders):
"""
Returns a list of models that will be used for text encoding. SDXL uses wrapped and unwrapped models.
FLUX.1 and SD3 may cache some outputs of the text encoder, so return the models that will be used for encoding (not cached).
"""
return text_encoders
# returns a list of bool values indicating whether each text encoder should be trained
def get_text_encoders_train_flags(self, args, text_encoders):
return [True] * len(text_encoders) if self.is_train_text_encoder(args) else [False] * len(text_encoders)
def get_flow_pixel_counts(self, args, batch, latents):
return None
def is_train_text_encoder(self, args):
return not args.network_train_unet_only
def cache_text_encoder_outputs_if_needed(self, args, accelerator, unet, vae, text_encoders, dataset, weight_dtype):
for t_enc in text_encoders:
t_enc.to(accelerator.device, dtype=weight_dtype)
def call_unet(self, args, accelerator, unet, noisy_latents, timesteps, text_conds, text_masks, batch, weight_dtype, **kwargs):
noisy_latents = noisy_latents.to(weight_dtype)
noise_pred = unet(noisy_latents, timesteps, text_conds[0], text_masks).sample
return noise_pred
def all_reduce_network(self, accelerator, network):
for param in network.parameters():
if param.grad is not None:
param.grad = accelerator.reduce(param.grad, reduction="mean")
def all_reduce_edm2_model(self, accelerator, edm2_model):
"""Manually synchronize EDM2 model gradients across GPUs."""
if edm2_model is None:
return
for param in edm2_model.parameters():
if param.grad is not None:
param.grad = accelerator.reduce(param.grad, reduction="mean")
def sample_images(self, accelerator, args, epoch, global_step, device, vae, tokenizers, text_encoder, unet):
train_util.sample_images(accelerator, args, epoch, global_step, device, vae, tokenizers[0], text_encoder, unet)
# region SD/SDXL
def post_process_network(self, args, accelerator, network, text_encoders, unet):
pass
def get_noise_scheduler(self, args: argparse.Namespace, device: torch.device) -> Any:
noise_scheduler = DDPMScheduler(
beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", num_train_timesteps=1000, clip_sample=False
)
if args.zero_terminal_snr:
custom_train_functions.fix_noise_scheduler_betas_for_zero_terminal_snr(noise_scheduler)
prepare_scheduler_for_custom_training(noise_scheduler, device)
return noise_scheduler
def encode_images_to_latents(self, args, vae: AutoencoderKL, images: torch.FloatTensor) -> torch.FloatTensor:
return vae.encode(images).latent_dist.sample()
def shift_scale_latents(self, args, latents: torch.FloatTensor) -> torch.FloatTensor:
return (latents - self.latent_shift) * self.vae_scale_factor
def get_noise_pred_and_target(
self,
args,
accelerator,
noise_scheduler,
latents,
batch,
text_encoder_conds,
text_encoder_masks,
unet,
network,
weight_dtype,
train_unet,
fixed_timesteps=None,
is_train=True,
):
# Sample noise, sample a random timestep for each image, and add noise to the latents,
# with noise offset and/or multires noise if specified
encoder_attention_mask_bias = text_encoder_masks[1] #[(1 - t.to(dtype=text_encoder_conds[0].dtype)).unsqueeze(1) * -10000.0 for t in text_encoder_masks]
pixel_counts = None
if hasattr(self, "get_flow_pixel_counts"):
pixel_counts = self.get_flow_pixel_counts(args, batch, latents.device)
# Adaptive timestep sampling: use Beta distribution sampler if enabled
adaptive_fixed_timesteps = fixed_timesteps
if is_train and self.adaptive_manager is not None and fixed_timesteps is None:
adaptive_fixed_timesteps = self.adaptive_manager.sample_timesteps(
latents, noise_scheduler.config.num_train_timesteps
)
# Store data for Algorithm 2 (delta computation after optimizer step)
self._adaptive_last_latents = latents.detach()
self._adaptive_last_noise = noise.detach()
self._adaptive_last_args = args
noise, noisy_latents, timesteps = train_util.get_noise_noisy_latents_and_timesteps(
args, noise_scheduler, latents, fixed_timesteps=adaptive_fixed_timesteps, is_train=is_train, pixel_counts=pixel_counts
)
# Store noisy latents for LWD wavelet masking (used in process_batch)
if is_train and getattr(self, "wavelet_masking_enabled", False):
self._noisy_latents = noisy_latents.detach() if isinstance(noisy_latents, torch.Tensor) else None
# ensure the hidden state will require grad
if is_train and args.gradient_checkpointing:
for x in noisy_latents:
x.requires_grad_(True)
for t in text_encoder_conds:
t.requires_grad_(True)
# Set T-LoRA timestep mask before the forward pass.
# Path 1 (full T-LoRA, algo="tlora"): mask is integral to the
# architecture — used at inference time (ComfyUI loader applies
# per-step masking). Always apply, including validation.
# Path 2 (LoCon flag, use_timestep_mask=True): mask is a training
# curriculum technique — checkpoint saves as standard lora_up/lora_down
# with all ranks active at inference. Only apply during training.
if is_train or not self.tlora_use_network_method:
self.apply_tlora_mask(timesteps)
# For inpainting models: concatenate [noisy_latents, mask, masked_latents] -> 9-channel UNet input
unet_latents = noisy_latents
if batch.get("masked_latents") is not None:
mask = torch.nn.functional.interpolate(
batch["masks"].to(weight_dtype), size=noisy_latents.shape[2:]
)
unet_latents = torch.cat([noisy_latents, mask, batch["masked_latents"].to(weight_dtype)], dim=1)
# Predict the noise residual
with torch.set_grad_enabled(is_train), accelerator.autocast():
noise_pred = self.call_unet(
args,
accelerator,
unet,
unet_latents.requires_grad_(train_unet),
timesteps,
text_encoder_conds,
encoder_attention_mask_bias,
batch,
weight_dtype,
)
# Clear T-LoRA mask after the forward pass (matching the guard above)
if is_train or not self.tlora_use_network_method:
self.clear_tlora_mask_if_needed()
# Upcast for grokking
latents = latents.to(torch.float64)
noise = noise.to(torch.float64)
if getattr(args, "flow_model", False):
target = noise - latents
elif args.v_parameterization:
# v-parameterization training
target = noise_scheduler.get_velocity(latents, noise, timesteps)
else:
target = noise
# differential output preservation
if "custom_attributes" in batch:
diff_output_pr_indices = []
for i, custom_attributes in enumerate(batch["custom_attributes"]):
if "diff_output_preservation" in custom_attributes and custom_attributes["diff_output_preservation"]:
diff_output_pr_indices.append(i)
if len(diff_output_pr_indices) > 0:
network.set_multiplier(0.0)
with torch.no_grad(), accelerator.autocast():
noise_pred_prior = self.call_unet(
args,
accelerator,
unet,
noisy_latents,
timesteps,
text_encoder_conds,
encoder_attention_mask_bias,
batch,
weight_dtype,
indices=diff_output_pr_indices,
)
network.set_multiplier(1.0) # may be overwritten by "network_multipliers" in the next step
target[diff_output_pr_indices] = noise_pred_prior.to(target.dtype)
return noise_pred, target, timesteps, None, noise
def post_process_loss(self, loss, args, timesteps: torch.IntTensor, noise_scheduler) -> torch.FloatTensor:
if args.min_snr_gamma:
loss = apply_snr_weight(loss, timesteps, noise_scheduler, args.min_snr_gamma, args.v_parameterization, soft=args.min_snr_gamma_soft)
if args.scale_v_pred_loss_like_noise_pred:
loss = scale_v_prediction_loss_like_noise_prediction(loss, timesteps, noise_scheduler)
if args.v_pred_like_loss:
loss = add_v_prediction_like_loss(loss, timesteps, noise_scheduler, args.v_pred_like_loss)
if args.debiased_estimation_loss:
loss = apply_debiased_estimation(loss, timesteps, noise_scheduler, args.v_parameterization)
return loss
def compute_adaptive_delta_before_step(self, unet, noise_scheduler, weight_dtype, accelerator, global_step):
"""Compute per-timestep losses with theta_k (before optimizer step) for Algorithm 2.
For a single x_0 (used to build the queue), compute losses at ALL T timesteps.
For the full batch, cache losses at the current |S| timesteps so the post-step
hook can compute the full-batch delta at those same |S| timesteps.
"""
if self.adaptive_manager is None:
return
if not self.adaptive_manager.should_update(global_step):
return
latents = self._adaptive_last_latents
noise = self._adaptive_last_noise
text_conds = self._adaptive_last_text_conds
if latents is None or noise is None or text_conds is None:
return
text_masks = text_conds[1] if len(text_conds) > 1 else None
adaptive_args = self._adaptive_last_args
adaptive_batch = self._adaptive_last_batch if self._adaptive_last_batch is not None else {}
def model_fn(noisy_latents, timesteps, wdtype):
noisy_latents_in = noisy_latents.to(wdtype)
encoder_mask_bias = text_masks
return self.call_unet(
adaptive_args, accelerator, unet, noisy_latents_in, timesteps,
text_conds, encoder_mask_bias, adaptive_batch, wdtype,
)
# Compute per-timestep losses for a single x_0 at all T timesteps (for the queue)
self._adaptive_losses_before = self.adaptive_manager.compute_per_timestep_losses(
latents, noise, model_fn, weight_dtype
)
# Cache per-timestep losses for the FULL batch at the current |S| timesteps,
# so that after the optimizer step we can compute the delta for the full batch
# at those same |S| timesteps (Algorithm 2, line 7).
self.adaptive_manager.cache_batch_losses_at_S(
latents, noise, model_fn, weight_dtype
)
def compute_adaptive_delta_after_step(self, unet, noise_scheduler, weight_dtype, accelerator, network):
"""Run Algorithm 2 after optimizer step: compute delta and update sampler.
Uses the full batch at the |S| selected timesteps (if a previous selection
exists) to compute the delta, falling back to the single x_0 otherwise.
"""
if self.adaptive_manager is None or self._adaptive_losses_before is None:
return
latents = self._adaptive_last_latents
noise = self._adaptive_last_noise
text_conds = self._adaptive_last_text_conds
if latents is None or noise is None or text_conds is None:
return
text_masks = text_conds[1] if len(text_conds) > 1 else None
adaptive_args = self._adaptive_last_args
adaptive_batch = self._adaptive_last_batch if self._adaptive_last_batch is not None else {}
def model_fn(noisy_latents, timesteps, wdtype):
noisy_latents_in = noisy_latents.to(wdtype)
encoder_mask_bias = text_masks
return self.call_unet(
adaptive_args, accelerator, unet, noisy_latents_in, timesteps,
text_conds, encoder_mask_bias, adaptive_batch, wdtype,
)
# Algorithm 2: compute delta approximation. When a previous |S| selection
# exists, the full batch at those timesteps is used (paper Algorithm 2 line 7).
delta_approx, selected_indices = self.adaptive_manager.compute_delta_approximation(
model_fn, latents, noise, weight_dtype, self._adaptive_losses_before,
full_batch_latents=latents, full_batch_noise=noise,
)
# Update sampler via policy gradient (Algorithm 1, line 8)
self.adaptive_manager.update_sampler(delta_approx, latents[:1])
# Clear cached data
self._adaptive_losses_before = None
self._adaptive_last_latents = None
self._adaptive_last_noise = None
self._adaptive_last_text_conds = None
self._adaptive_last_args = None
self._adaptive_last_batch = None
def get_adaptive_model_type(self, args) -> str:
"""Return the model type for the adaptive timestep sampler.
Subclasses should override this to return 'flow_matching' for
flow-matching architectures (Flux, SD3, Lumina, Anima, etc.).
"""
return "ddpm"
def get_sai_model_spec(self, args):
return train_util.get_sai_model_spec(None, args, self.is_sdxl, True, False)
def update_metadata(self, metadata, args):
pass
def is_text_encoder_not_needed_for_training(self, args):
return False # use for sample images
def prepare_text_encoder_grad_ckpt_workaround(self, index, text_encoder):
# set top parameter requires_grad = True for gradient checkpointing works
text_encoder.text_model.embeddings.requires_grad_(True)
def prepare_text_encoder_fp8(self, index, text_encoder, te_weight_dtype, weight_dtype):
text_encoder.text_model.embeddings.to(dtype=weight_dtype)
def prepare_unet_with_accelerator(
self, args: argparse.Namespace, accelerator: Accelerator, unet: torch.nn.Module
) -> torch.nn.Module:
return accelerator.prepare(unet)
def on_step_start(self, args, accelerator, network, text_encoders, unet, batch, weight_dtype, is_train: bool = True):
pass
def on_validation_step_end(self, args, accelerator, network, text_encoders, unet, batch, weight_dtype):
pass
def setup_tlora_masking(self, net_kwargs, network_dim, noise_scheduler):
"""
Initialize T-LoRA timestep masking.
Supports two paths:
1. ``algo="tlora"`` — full TLoraModule with SVD-based parameterization
(module-level mask from lycoris.modules.tlora).
2. ``algo="locon"/"lora"`` + ``use_timestep_mask=True`` — lightweight
mask flag on LoConModule, saves as standard lora_up/lora_down.
Reads tlora_min_rank / tlora_mask_alpha (path 1) or
tlora_min_rank / tlora_alpha (path 2) from network_args.
Must be called after the network is created.
"""
algo = (net_kwargs.get("algo", "lora") or "lora").lower()
# Path 2: LoCon flag approach (algo=lora/locon + use_timestep_mask=True)
if algo in ("lora", "locon", "ortholora") and net_kwargs.get("use_timestep_mask"):
self.tlora_enabled = True
self.tlora_use_network_method = True
self.tlora_max_rank = int(network_dim) if network_dim is not None else 4
tlora_min_rank = net_kwargs.get("tlora_min_rank", None)
if tlora_min_rank is None:
self.tlora_min_rank = max(1, self.tlora_max_rank // 2)
else:
self.tlora_min_rank = int(tlora_min_rank)
self.tlora_min_rank = max(0, min(self.tlora_min_rank, self.tlora_max_rank))
self.tlora_mask_alpha = float(net_kwargs.get("tlora_alpha", 1.0))
self.tlora_max_timestep = noise_scheduler.config.num_train_timesteps
logger.info(
f"T-LoRA masking enabled (LoCon flag): max_rank={self.tlora_max_rank}, "
f"min_rank={self.tlora_min_rank}, alpha={self.tlora_mask_alpha}, "
f"max_timestep={self.tlora_max_timestep}"
)
return
# Path 1: Full TLoraModule approach (algo=tlora)
if algo != "tlora":
return
if not TLORA_AVAILABLE:
logger.warning("T-LoRA requested but lyco_tlora is not available. Skipping T-LoRA setup.")
return
self.tlora_enabled = True
self.tlora_use_network_method = False
self.tlora_max_rank = int(network_dim) if network_dim is not None else 4
tlora_min_rank = net_kwargs.get("tlora_min_rank", None)
if tlora_min_rank is None:
self.tlora_min_rank = int(math.ceil(self.tlora_max_rank * 0.5))
else:
self.tlora_min_rank = int(tlora_min_rank)
self.tlora_min_rank = max(0, min(self.tlora_min_rank, self.tlora_max_rank))
self.tlora_mask_alpha = float(net_kwargs.get("tlora_mask_alpha", 1.0))
self.tlora_max_timestep = noise_scheduler.config.num_train_timesteps
logger.info(
f"T-LoRA masking enabled (TLoraModule): max_rank={self.tlora_max_rank}, "
f"min_rank={self.tlora_min_rank}, mask_alpha={self.tlora_mask_alpha}, "
f"max_timestep={self.tlora_max_timestep}"
)
def apply_tlora_mask(self, timesteps: torch.Tensor):
"""
Compute and set the T-LoRA timestep mask for the current batch.
Dispatches to either the network-level method (LoCon flag approach,
which uses a shared GPU buffer aliased across all modules) or the
module-level functions from lycoris.modules.tlora (full TLoraModule).
"""
if not self.tlora_enabled:
return
if self.tlora_use_network_method:
# LoCon flag: use network.set_timestep_mask (shared buffer, no CPU transfer)
self.network.set_timestep_mask(timesteps, self.tlora_max_timestep)
else:
# Full TLoraModule: compute mask tensor, set via module-level function
if timesteps.numel() == 1:
mask = compute_timestep_mask(
timestep=int(timesteps.item()),
max_timestep=self.tlora_max_timestep,
max_rank=self.tlora_max_rank,
min_rank=self.tlora_min_rank,
alpha=self.tlora_mask_alpha,
)
else:
mask = compute_timestep_mask_batch(
timesteps=timesteps,
max_timestep=self.tlora_max_timestep,
max_rank=self.tlora_max_rank,
min_rank=self.tlora_min_rank,
alpha=self.tlora_mask_alpha,
)
set_timestep_mask(mask)
def clear_tlora_mask_if_needed(self):
"""Clear the T-LoRA mask after the forward pass."""
if not self.tlora_enabled:
return
if self.tlora_use_network_method:
self.network.clear_timestep_mask()
else:
clear_timestep_mask()
def setup_lora2_regularization(self, net_kwargs):
"""
Initialize LoRA² rank regularization if the algo is lora2.
Reads lora2_lambda_r from network_args.
Must be called after the network is created.
"""
algo = (net_kwargs.get("algo", "lora") or "lora").lower()
if algo != "lora2":
return
if not LORA2_AVAILABLE:
logger.warning("LoRA² requested but LoRA2Module is not available. Skipping LoRA² setup.")
return
self.lora2_enabled = True
self.lora2_lambda_r = float(net_kwargs.get("lora2_lambda_r", 1e-4))
logger.info(
f"LoRA² rank regularization enabled: lambda_r={self.lora2_lambda_r}"
)
# endregion
def process_batch(
self,
batch,
text_encoders,
unet,
network,
vae,
noise_scheduler,
vae_dtype,
weight_dtype,
accelerator,
args,
text_encoding_strategy: strategy_base.TextEncodingStrategy,
tokenize_strategy: strategy_base.TokenizeStrategy,
is_train=True,
train_text_encoder=True,
train_unet=True,
edm2_model=None
) -> torch.Tensor:
"""
Process a batch for the network
"""
with torch.no_grad():
if "latents" in batch and batch["latents"] is not None:
latents = typing.cast(torch.FloatTensor, batch["latents"].to(device=accelerator.device))
else:
# latentに変換
if args.vae_batch_size is None or len(batch["images"]) <= args.vae_batch_size:
latents = self.encode_images_to_latents(args, vae, batch["images"].to(device=accelerator.device, dtype=vae_dtype))
else:
chunks = [
batch["images"][i : i + args.vae_batch_size] for i in range(0, len(batch["images"]), args.vae_batch_size)
]
list_latents = []
for chunk in chunks:
with torch.no_grad():
chunk = self.encode_images_to_latents(args, vae, chunk.to(device=accelerator.device, dtype=vae_dtype))
list_latents.append(chunk)
latents = torch.cat(list_latents, dim=0)
# NaNが含まれていれば警告を表示し0に置き換える
if torch.any(torch.isnan(latents)):
accelerator.print("NaN found in latents, replacing with zeros")
latents = typing.cast(torch.FloatTensor, torch.nan_to_num(latents, 0, out=latents))
latents = self.shift_scale_latents(args, latents)
# Prepare inpainting masked_latents if batch contains masks
if batch.get("masks") is not None and batch.get("masked_images") is not None:
masked_latents = self.encode_images_to_latents(
args, vae, batch["masked_images"].to(accelerator.device, dtype=vae_dtype)
)
batch["masked_latents"] = self.shift_scale_latents(args, masked_latents)
text_encoder_conds = []
masks_reshaped = []
text_encoder_outputs_list = batch.get("text_encoder_outputs_list", None)
if text_encoder_outputs_list is not None:
text_encoder_conds = text_encoder_outputs_list # List of text encoder outputs
if isinstance(text_encoding_strategy, SdxlTextEncodingStrategy):
masks_reshaped = text_encoder_outputs_list[3:]
if len(text_encoder_conds) == 0 or text_encoder_conds[0] is None or train_text_encoder:
# TODO this does not work if 'some text_encoders are trained' and 'some are not and not cached'
with torch.set_grad_enabled(is_train and train_text_encoder), accelerator.autocast():
# Get the text embedding for conditioning
if args.weighted_captions:
input_ids_list, weights_list = tokenize_strategy.tokenize_with_weights(batch["captions"])
encoded_text_encoder_conds = text_encoding_strategy.encode_tokens_with_weights(
tokenize_strategy,
self.get_models_for_text_encoding(args, accelerator, text_encoders),
input_ids_list,
weights_list,
)
else:
input_ids = [ids.to(accelerator.device) for ids in batch["input_ids_list"]]
if isinstance(text_encoding_strategy, SdxlTextEncodingStrategy):
masks = [mask.to(accelerator.device) for mask in batch["attn_mask_list"]]
encoded_text_encoder_conds, masks_reshaped = text_encoding_strategy.encode_tokens(
tokenize_strategy,
self.get_models_for_text_encoding(args, accelerator, text_encoders),
input_ids,
attn_masks=masks,
)
else:
encoded_text_encoder_conds = text_encoding_strategy.encode_tokens(
tokenize_strategy,
self.get_models_for_text_encoding(args, accelerator, text_encoders),
input_ids
)
if args.full_fp16:
encoded_text_encoder_conds = [c.to(weight_dtype) for c in encoded_text_encoder_conds]
# if text_encoder_conds is not cached, use encoded_text_encoder_conds
if len(text_encoder_conds) == 0:
text_encoder_conds = encoded_text_encoder_conds
else:
# if encoded_text_encoder_conds is not None, update cached text_encoder_conds
for i in range(len(encoded_text_encoder_conds)):
if encoded_text_encoder_conds[i] is not None:
text_encoder_conds[i] = encoded_text_encoder_conds[i]
# Store text encoder conditions and batch for adaptive Algorithm 2
if self.adaptive_manager is not None and is_train:
self._adaptive_last_text_conds = [c.detach() if isinstance(c, torch.Tensor) else c for c in text_encoder_conds]
self._adaptive_last_batch = batch
# sample noise, call unet, get target
noise_pred, target, timesteps, weighting, noise = self.get_noise_pred_and_target(
args,
accelerator,
noise_scheduler,
latents,
batch,
text_encoder_conds,
masks_reshaped,
unet,
network,
weight_dtype,
train_unet,
is_train=is_train,
)
# Cast to float64 (Double Precision) for Grokking
noise_pred = noise_pred.to(dtype=torch.float64)
target = target.to(dtype=torch.float64)
if is_train:
if args.differential_guidance:
target = noise_pred + (float(args.differential_guidance_scale) * (target - noise_pred))
huber_c = train_util.get_huber_threshold_if_needed(args, timesteps, noise_scheduler)
loss = train_util.conditional_loss(noise_pred, target, args.loss_type, "none", huber_c, scale=float(args.loss_scale))
if weighting is not None:
loss = loss * weighting
if args.contrastive_flow_matching and latents.size(0) > 1:
# CRITICAL FIX: Add .detach() to prevent gradients flowing through negative samples
negative_latents = latents.roll(1, 0).detach()
negative_noise = noise.roll(1, 0).detach()
with torch.no_grad():
if getattr(args, "flow_model", False) or getattr(args, "_anima_model", False):
target_negative = negative_noise - negative_latents
else:
target_negative = noise_scheduler.get_velocity(negative_latents, negative_noise, timesteps)
# Handle cast for CFM
target_negative = target_negative.to(dtype=torch.float64)
loss_contrastive = torch.nn.functional.mse_loss(
noise_pred, target_negative, reduction="none"
)
# Store CFM component for logging (before applying lambda)
#loss_cfm = loss_contrastive.mean([1, 2, 3]).mean().detach()
loss = loss - float(args.cfm_lambda) * loss_contrastive
if args.masked_loss or ("alpha_masks" in batch and batch["alpha_masks"] is not None):
loss = apply_masked_loss(loss, batch)
# --- LWD Wavelet Masking: apply spatial mask to element-wise loss ---
if self.wavelet_masking_enabled and self._noisy_latents is not None:
with torch.no_grad():
A = train_util.compute_wavelet_attention_map(self._noisy_latents, self.wavelet_dwt)
# Flow-matching trainers (Flux/SD3/Anima/Lumina/Hunyuan) return
# timesteps in [0, 1]; DDPM trainers return them in [0, T].
# get_adaptive_model_type() is the explicit discriminator
# (overridden to "flow_matching" by all flow trainers, defaults
# to "ddpm" in the base class).
is_flow_matching = self.get_adaptive_model_type(args) == "flow_matching"
M = train_util.get_wavelet_mask(
A,
l=float(getattr(args, "wavelet_mask_l_bound", 0.3)),
T=noise_scheduler.config.num_train_timesteps,
timesteps=timesteps,
flow_matching=is_flow_matching,
)
# M shape: (B, 1, H, W), loss shape: (B, C, H, W) or (B, seq_len)
if loss.ndim == 4 and loss.shape[2:] == M.shape[2:]:
loss = loss * M
self._wavelet_mask_ratio = M.mean().item()
elif loss.ndim == 2:
# For packed/sequence models, skip masking (shape mismatch)
self._wavelet_mask_ratio = 0.0
else:
logger.warning_once(
f"Wavelet mask shape {M.shape} incompatible with loss shape {loss.shape}, skipping mask"
)
self._wavelet_mask_ratio = 0.0
else:
loss = train_util.conditional_loss(noise_pred, target, "l2", "none", None)
loss = loss.mean(dim=list(range(1, loss.ndim))) # mean over all dims except batch
if is_train:
loss_weights = batch["loss_weights"] # 各sampleごとのweight
loss = loss * loss_weights
loss = self.post_process_loss(loss, args, timesteps, noise_scheduler)
if is_train and args.loss_multiplier:
loss.mul_(float(args.loss_multiplier) if args.loss_multiplier is not None else 1.0)
# For logging
pre_scaling_loss = loss.mean()
if is_train and args.edm2_loss_weighting:
loss, loss_scaled = edm2_model(loss, timesteps)
loss_scaled = loss_scaled.mean()
else:
loss_scaled = None
final_loss = loss.mean()
# LoRA²: add rank regularization loss
if is_train and self.lora2_enabled and LORA2_AVAILABLE:
rank_reg_loss = LoRA2Module.get_total_rank_reg_loss()
if rank_reg_loss.item() > 0:
final_loss = final_loss + self.lora2_lambda_r * rank_reg_loss
# Focal Frequency Loss: auxiliary frequency-domain loss on latent space
self.ffl_loss_value = None
if is_train and self.ffl_enabled and self.ffl_module is not None:
ffl_loss = self.ffl_module(noise_pred, target)
self.ffl_loss_value = ffl_loss.detach().item()
final_loss = final_loss + ffl_loss
return final_loss, pre_scaling_loss, loss_scaled
def process_val_batch(
self,
batch,
text_encoders,
unet,
network,
vae,
noise_scheduler,
vae_dtype,
weight_dtype,
accelerator,
args,
text_encoding_strategy: strategy_base.TextEncodingStrategy,
tokenize_strategy: strategy_base.TokenizeStrategy,
train_text_encoder=True,
train_unet=True,
timesteps_list: list = [50, 350, 500, 650, 950]
) -> torch.Tensor:
"""
Process a batch for the network to determine val loss
"""
total_loss = 0.0
with torch.no_grad():
if "latents" in batch and batch["latents"] is not None:
latents = typing.cast(torch.FloatTensor, batch["latents"].to(device=accelerator.device))
else:
# latentに変換
if args.vae_batch_size is None or len(batch["images"]) <= args.vae_batch_size:
latents = self.encode_images_to_latents(args, vae, batch["images"].to(device=accelerator.device, dtype=vae_dtype))
else: