diffusers
[Training] Add `datasets` version of LCM LoRA SDXL
#5778
Merged

[Training] Add `datasets` version of LCM LoRA SDXL #5778

sayakpaul merged 109 commits into main from lcm-lora-sdxl-datasets
sayakpaul
sayakpaul1 year ago (edited 1 year ago)👍 1

What does this PR do?

Add a datasets compatible variant of https://github.com/huggingface/diffusers/blob/main/examples/consistency_distillation/train_lcm_distill_lora_sdxl_wds.py. It also adapts a couple of best practices from peft:

  • Instead of initializing two UNets (teacher and student), it leverages the disable_adapters() and enable_adapters() functions. In this case, the teacher is without any adapters and the student is with LoRA. We only update the LoRA params in the student.
  • Makes use of peft utility modules positioning it as a utility library rather than a modelling library.

Running a couple experiments. Will report back the findings. But should be more or less ready to be reviewed.

Basic training command

export MODEL_NAME="stabilityai/stable-diffusion-xl-base-1.0"
export OUTPUT_DIR="lora-lora-sdxl"
export DATASET_NAME="lambdalabs/pokemon-blip-captions"
export VAE_PATH="madebyollin/sdxl-vae-fp16-fix"

accelerate launch train_lcm_distill_lora_sdxl.py \
  --pretrained_teacher_model=${MODEL_NAME}  \
  --pretrained_vae_model_name_or_path=${VAE_PATH} \
  --output_dir=${OUTPUT_DIR} \
  --mixed_precision="fp16" \
  --dataset_name=$DATASET_NAME \
  --resolution=1024 \
  --train_batch_size=4 \
  --gradient_accumulation_steps=1 \
  --gradient_checkpointing \
  --use_8bit_adam \
  --learning_rate=1e-4 --loss_type="huber" \
  --report_to="wandb" \
  --lr_scheduler="constant" \
  --lr_warmup_steps=0 \
  --max_train_steps=1500 \
  --checkpointing_steps=50 \
  --validation_steps=25 \
  --seed="0" \
  --report_to="wandb" \
  --push_to_hub

TODOs

  • Update the PR with results
  • Docs
  • Tests
sayakpaul add: script to train lcm lora for sdxl with 🤗 datasets
ca7f220b
sayakpaul suit up the args.
88efd154
sayakpaul remove comments.
9e49fd2e
sayakpaul fix num_update_steps
728aa8a6
sayakpaul fix batch unmarshalling
bc8cfddf
sayakpaul fix num_update_steps_per_epoch
8c4d4b6a
sayakpaul fix; dataloading.
6d2f7407
sayakpaul fix microconditions.
c7f28284
sayakpaul sayakpaul requested a review from patil-suraj patil-suraj 1 year ago
sayakpaul unconditional predictions debug
df707545
sayakpaul fix batch size.
dd93227e
sayakpaul no need to use use_auth_token
3d4b1da0
pcuenca
pcuenca commented on 2023-11-13
pcuenca1 year ago

Looks good so far, let me know if you need help testing / debugging

sayakpaul
sayakpaul1 year ago

It works, needs hyperparameter tuning. If you could take it to a test run, that would be great!

patil-suraj
patil-suraj approved these changes on 2023-11-13
patil-suraj1 year ago

Thanks a lot for working on this. Just left some comments. Most importantly:

Let's make sure we can load the saved lora with the pipeline for inference. I had some issues with it when I tried.

Everything else looks good !

Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
445 parser.add_argument(
446 "--learning_rate",
447 type=float,
448
default=1e-4,
patil-suraj1 year ago

Since that's the default we use.

Suggested change
default=1e-4,
default=1e-6,
Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
482 parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer")
483 parser.add_argument("--max_grad_norm", default=1.0, type=float, help="Max gradient norm.")
484 # ----Diffusion Training Arguments----
485
parser.add_argument(
486
"--proportion_empty_prompts",
487
type=float,
488
default=0.0,
489
help="Proportion of image prompts to be replaced with empty strings. Defaults to 0 (no prompt replacement).",
490
)
patil-suraj1 year ago

Maybe let's remove this. Not really required here.

patil-suraj1 year ago

I thought about this a bit. As we are distilling with classifier-free-guidance, this option is not necessary and should not be used.

examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
776 # 8. Add LoRA to the student U-Net, only the LoRA projection matrix will be updated by the optimizer.
777 lora_config = LoraConfig(
778 r=args.lora_rank,
779
target_modules=[
780
"to_q",
781
"to_k",
782
"to_v",
783
"to_out.0",
784
"proj_in",
785
"proj_out",
786
"ff.net.0.proj",
787
"ff.net.2",
788
"conv1",
789
"conv2",
790
"conv_shortcut",
791
"downsamplers.0.conv",
792
"upsamplers.0.conv",
793
"time_emb_proj",
patil-suraj1 year ago👍 1

for later:

We could also think of making this an argument.

Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
830 if accelerator.is_main_process:
831 unet_ = accelerator.unwrap_model(unet)
832 lora_state_dict = get_peft_model_state_dict(unet_, adapter_name="default")
833
StableDiffusionXLPipeline.save_lora_weights(os.path.join(output_dir, "unet_lora"), lora_state_dict)
patil-suraj1 year ago

Let's make sure we can load these saved weights for inference. Last time I tried there was a error in loading, but it could be because of my mistake (not fully familer with LoRA logic in diffusers).

I ended up converting the state_dict to kohya format and manually saving it with torch.save

sayakpaul1 year ago

The problem is that we're trying to serialize the peft state dict with save_lora_layers() and that shouldn't be the case because the peft state dict also needs to be supplemented with an adapter config file. This is what's being done in the load model hook where we make use of load_adapter().

For the time being, I have modified it so that we output the state dict in the Kohya format so that we know that it's gonna be compatible with load_lora_weights(). But I will revisit it so that we can output a diffusers-compatible single file checkpoint (by making use of convert_state_dict_to_diffusers()).

IIRC, we cannot yet load a peft LoRA checkpoint directly with load_lora_weights(). This should be fixed in a separate PR if that's the case.

Cc: @pacman100 @younesbelkada

Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
745 revision=args.teacher_revision,
746 )
747
748
# 5. Load teacher U-Net from SD-XL checkpoint
749
teacher_unet = UNet2DConditionModel.from_pretrained(
750
args.pretrained_teacher_model, subfolder="unet", revision=args.teacher_revision
751
)
patil-suraj1 year ago

For later:

We could potentially avoid loading two unets. Since we are doing LoRA, we could disable the LoRA when getting teacher prediction.

sayakpaul1 year ago

@pacman100 we're using peft here. IIRC, there's a nice context manager in peft that lets us run a model without the adapters. Could you provide a reference?

Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
1149
1150 # encode pixel values with batch size of at most 8
1151 latents = []
1152
for i in range(0, pixel_values.shape[0], 8):
patil-suraj1 year ago

Could also make this an argument.

sayakpaul1 year ago

Done in 6b2e42f

Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
1209 # noisy_latents with both the conditioning embedding c and unconditional embedding 0
1210 # Get teacher model prediction on noisy_latents and conditional embedding
1211 with torch.no_grad():
1212
with torch.autocast("cuda"):
patil-suraj1 year ago
Suggested change
with torch.autocast("cuda"):
with torch.autocast("cuda", dtype=weight_dtype):
Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
1257
1258 # Get target LCM prediction on x_prev, w, c, t_n
1259 with torch.no_grad():
1260
with torch.autocast("cuda", enabled=True, dtype=weight_dtype):
patil-suraj1 year ago
Suggested change
with torch.autocast("cuda", enabled=True, dtype=weight_dtype):
with torch.autocast("cuda", dtype=weight_dtype):
Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
1261 target_noise_pred = unet(
1262 x_prev.float(),
1263 timesteps,
1264
timestep_cond=None,
patil-suraj1 year ago
Suggested change
timestep_cond=None,
Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
1190 noise_pred = unet(
1191 noisy_model_input,
1192 start_timesteps,
1193
timestep_cond=None,
patil-suraj1 year ago
Suggested change
timestep_cond=None,
sayakpaul
sayakpaul1 year ago

Let's make sure we can load the saved lora with the pipeline for inference. I had some issues with it when I tried.

Could you post the snippet with which you tried and the error you got?

patil-suraj
patil-suraj1 year ago

Let's make sure we can load the saved lora with the pipeline for inference. I had some issues with it when I tried.

Could you post the snippet with which you tried and the error you got?

When I try to load the lora saved with script using

pipe.load_lora_weights(path_to_saved_lora, weight_name="pytorch_lora_weights.safetensors")

I get

---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In [13], line 1
----> 1 pipe.load_lora_weights("valhalla/lcm-sdxl-distill-lora-k5", weight_name="pytorch_lora_weights.safetensors")

File ~/diffusers/src/diffusers/loaders.py:3233, in StableDiffusionXLLoraLoaderMixin.load_lora_weights(self, pretrained_model_name_or_path_or_dict, adapter_name, **kwargs)
   3230 if not is_correct_format:
   3231     raise ValueError("Invalid LoRA checkpoint.")
-> 3233 self.load_lora_into_unet(
   3234     state_dict, network_alphas=network_alphas, unet=self.unet, adapter_name=adapter_name, _pipeline=self
   3235 )
   3236 text_encoder_state_dict = {k: v for k, v in state_dict.items() if "text_encoder." in k}
   3237 if len(text_encoder_state_dict) > 0:

File ~/diffusers/src/diffusers/loaders.py:1630, in LoraLoaderMixin.load_lora_into_unet(cls, state_dict, network_alphas, unet, low_cpu_mem_usage, adapter_name, _pipeline)
   1627     if "lora_B" in key:
   1628         rank[key] = val.shape[1]
-> 1630 lora_config_kwargs = get_peft_kwargs(rank, network_alphas, state_dict, is_unet=True)
   1631 lora_config = LoraConfig(**lora_config_kwargs)
   1633 # adapter_name

File ~/diffusers/src/diffusers/utils/peft_utils.py:122, in get_peft_kwargs(rank_dict, network_alpha_dict, peft_state_dict, is_unet)
    120 rank_pattern = {}
    121 alpha_pattern = {}
--> 122 r = lora_alpha = list(rank_dict.values())[0]
    124 if len(set(rank_dict.values())) > 1:
    125     # get the rank occuring the most number of times
    126     r = collections.Counter(rank_dict.values()).most_common()[0][0]
sayakpaul Apply suggestions from code review
79672471
sayakpaul make vae encoding batch size an arg
6b2e42f2
sayakpaul final serialization in kohya
d7f632e6
sayakpaul style
e4edb31b
HuggingFaceDocBuilderDev
HuggingFaceDocBuilderDev1 year ago

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint.

sayakpaul Merge branch 'main' into lcm-lora-sdxl-datasets
858009b4
sayakpaul state dict rejigging
6aa2dd8c
sayakpaul feat: no separate teacher unet.
1fd33782
sayakpaul debug
41354149
sayakpaul fix state dict serialization
3b066d26
sayakpaul debug
fc5546fe
sayakpaul debug
ba0d0f25
sayakpaul debug
35e30fbb
sayakpaul remove prints.
53c13f7f
sayakpaul remove kohya utility and make style
cff23edb
sayakpaul fix serialization
ca076c78
sayakpaul fix
808f61ea
sayakpaul
sayakpaul1 year ago

@patil-suraj here's a summary of the changes:

  • No teacher UNet is being loaded separately. Thanks to disable_adapter() context (cc: @younesbelkada)
  • Cleaned the with contexts a bit.
  • Fixed the serialization.

The following should work now:

from diffusers import StableDiffusionXLPipeline
import torch


pipeline_id = "stabilityai/stable-diffusion-xl-base-1.0"
ckpt_id = "sayakpaul/lora-lcm-sdxl-new"

pipeline = StableDiffusionXLPipeline.from_pretrained(pipeline_id, torch_dtype=torch.float16).to("cuda")
pipeline.load_lora_weights(ckpt_id, use_auth_token=True)

We're now serializing the final checkpoint in the diffusers native format.

Currently, conducting an experiment with the following command:

CUDA_VISIBLE_DEVICES=1 accelerate launch train_lcm_distill_lora_sdxl.py \
  --pretrained_teacher_model=${MODEL_NAME}  \
  --pretrained_vae_model_name_or_path=${VAE_PATH} \
  --output_dir="lora-lcm-sdxl-new" \
  --mixed_precision="fp16" \
  --dataset_name=$DATASET_NAME \
  --resolution=1024 \
  --train_batch_size=16 \
  --gradient_accumulation_steps=1 \
  --gradient_checkpointing \
  --use_8bit_adam \
  --learning_rate=1e-6 --loss_type="huber" \
  --report_to="wandb" \
  --lr_scheduler="constant" \
  --lr_warmup_steps=0 \
  --max_train_steps=5000 \
  --checkpointing_steps=100 \
  --validation_steps=50 \
  --seed="0" \
  --report_to="wandb" \
  --push_to_hub

It'd be very nice to have your 👀 on this.

sayakpaul add test
842df25c
sayakpaul add peft dependency.
00276736
sayakpaul add: peft
c6255532
sayakpaul remove peft
c5317ff3
sayakpaul autocast device determination from accelerator
6a690abd
sayakpaul autocast
8c4eaf67
sayakpaul reduce lora rank.
cece7819
patrickvonplaten patrickvonplaten requested a review from patil-suraj patil-suraj 1 year ago
patrickvonplaten
patrickvonplaten1 year ago
patil-suraj
patil-suraj commented on 2023-11-14
patil-suraj1 year ago

Looking very good, just left some suggestions. We should remove the empty prompt embeds options here, since we train the model with CFG it's not necessary. Apart from this, it's in a pretty good state already.

Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
1#!/usr/bin/env python
2# coding=utf-8
3
# Copyright 2023 The HuggingFace Inc. team. All rights reserved.
patil-suraj1 year ago

Let's mention original authors as well

Suggested change
# Copyright 2023 The HuggingFace Inc. team. All rights reserved.
# Copyright 2023 The LCM team and the HuggingFace Inc. team. All rights reserved.
Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
550 " https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices"
551 ),
552 )
553
parser.add_argument(
554
"--cast_teacher_unet",
555
action="store_true",
556
help="Whether to cast the teacher U-Net to the precision specified by `--mixed_precision`.",
557
)
patil-suraj1 year ago
Suggested change
parser.add_argument(
"--cast_teacher_unet",
action="store_true",
help="Whether to cast the teacher U-Net to the precision specified by `--mixed_precision`.",
)

Can be removed since we don't use teacher_unet now.

examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
598 if env_local_rank != -1 and env_local_rank != args.local_rank:
599 args.local_rank = env_local_rank
600
601
if args.proportion_empty_prompts < 0 or args.proportion_empty_prompts > 1:
602
raise ValueError("`--proportion_empty_prompts` must be in the range [0, 1].")
patil-suraj1 year ago

This should be removed.

Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
739 revision=args.teacher_revision,
740 )
741
742
# 5. Load teacher U-Net from SD-XL checkpoint
743
# teacher_unet = UNet2DConditionModel.from_pretrained(
744
# args.pretrained_teacher_model, subfolder="unet", revision=args.teacher_revision
745
# )
patil-suraj1 year ago

Should be removed

Suggested change
# 5. Load teacher U-Net from SD-XL checkpoint
# teacher_unet = UNet2DConditionModel.from_pretrained(
# args.pretrained_teacher_model, subfolder="unet", revision=args.teacher_revision
# )
Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
748 vae.requires_grad_(False)
749 text_encoder_one.requires_grad_(False)
750 text_encoder_two.requires_grad_(False)
751
# teacher_unet.requires_grad_(False)
patil-suraj1 year ago
Suggested change
# teacher_unet.requires_grad_(False)
Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
806 text_encoder_one.to(accelerator.device, dtype=weight_dtype)
807 text_encoder_two.to(accelerator.device, dtype=weight_dtype)
808
809
# Move teacher_unet to device, optionally cast to weight_dtype
810
# teacher_unet.to(accelerator.device)
811
# if args.cast_teacher_unet:
812
# teacher_unet.to(dtype=weight_dtype)
patil-suraj1 year ago
Suggested change
# Move teacher_unet to device, optionally cast to weight_dtype
# teacher_unet.to(accelerator.device)
# if args.cast_teacher_unet:
# teacher_unet.to(dtype=weight_dtype)
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
822 # create custom saving & loading hooks so that `accelerator.save_state(...)` serializes in a nice format
823 def save_model_hook(models, weights, output_dir):
824 if accelerator.is_main_process:
825
unet_ = accelerator.unwrap_model(unet)
826
# save weights in peft format to be able to load them back
827
unet_.save_pretrained(output_dir)
patil-suraj1 year ago👍 1

We should save the intermediate checkpoints in the loadable format as well. Since sometimes intermediate checkpoints are better than final. So it's convenient to be able to just load and do inference with the checkpoint without having to manually convert the peft state dict.

sayakpaul1 year ago

WDYT about saving both in the peft format (as is currently being done during intermediate checkpoints) and also in the diffusers format?

This way:

  • We can easily resume from checkpoints maintaining the cleanliness of peft via load_adapter().
  • We can also immediately load an intermediate checkpoint with load_lora_weights().
patil-suraj1 year ago

Sounds good!

Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
853 "xFormers 0.0.16 cannot be used for training in some GPUs. If you observe problems during training, please update xFormers to at least 0.0.17. See https://huggingface.co/docs/diffusers/main/en/optimization/xformers for more details."
854 )
855 unet.enable_xformers_memory_efficient_attention()
856
# teacher_unet.enable_xformers_memory_efficient_attention()
patil-suraj1 year ago
Suggested change
# teacher_unet.enable_xformers_memory_efficient_attention()
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
1201 # Get teacher model prediction on noisy_latents and conditional embedding
1202 # Notice that we're disabling the adapter layers within the `unet` and then it becomes a
1203 # regular teacher. This way, we don't have to separately initialize a teacher UNet.
1204
with torch.no_grad() and torch.autocast(
1205
str(accelerator.device), dtype=weight_dtype
1206
) and unet.disable_adapter():
patil-suraj1 year ago

Since we are just using one unet now, this autocast might not be necessary. The unet is already prepared with accelerate which wraps the model in autocast when doing mixed-precision training

sayakpaul1 year ago

Kept it because the unet that's being trained is also under autocast (taken from the original script).

patil-suraj
patil-suraj1 year ago

Curious to know how are the results with smaller datasets :)

sayakpaul remove unneeded space
beb8aa2c
sayakpaul Apply suggestions from code review
33cb9d03
sayakpaul style
795cc9f9
sayakpaul remove prompt dropout.
042f3578
sayakpaul also save in native diffusers ckpt format.
283af651
sayakpaul debug
5e099a24
sayakpaul debug
71db43a2
sayakpaul
sayakpaul1 year ago (edited 1 year ago)

@DN6 we can't add pip install peft in the PR test workflow either as that swaps the training backend to use peft which is not fully supported for the other training examples.

I see the following as a potential workaround. In the workflow, we run all the example tests except for test_text_to_image_lcm_lora_sdxl without installing peft. Then we add a command to install peft and then just test test_text_to_image_lcm_lora_sdxl. Does this work for you?

Later, when the examples are fully equipped with peft we can revisit it. LMK.

Update: With #5388, this might already be fixed (cc: @younesbelkada).

sayakpaul debug
e1346d56
sayakpaul better formation of the null embeddings.
dfcf2340
sayakpaul remove space.
5ce6cc19
sayakpaul autocast fixes.
7ee9d5d9
sayakpaul autocast fix.
1b359ae8
sayakpaul hacky
82b628a3
sayakpaul
sayakpaul commented on 2023-11-14
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
1171 # regular teacher. This way, we don't have to separately initialize a teacher UNet.
1172 using_cuda = "cuda" in str(accelerator.device)
1173 with torch.no_grad() and torch.autocast(
1174
str(accelerator.device), dtype=weight_dtype if using_cuda else torch.bfloat16, enabled=using_cuda
sayakpaul1 year ago

@patil-suraj I am not super happy about how I am doing it here but it's the only way I found to make the test pass on a CPU. Let me know if you have better ideas.

younesbelkada
younesbelkada1 year ago

@sayakpaul I can confirm all issues with respect to failing example CI are fixed with #5388. Perhaps we can merge first #5388 and I can work on the saving adapter config as a follow up PR. What do you think?

sayakpaul
sayakpaul1 year ago👍 1

I will have a look at #5388.

younesbelkada
younesbelkada commented on 2023-11-14
Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
798 # be independently loaded via `load_lora_weights()`.
799 peft_state_dict = get_peft_model_state_dict(unet, adapter_name="default")
800 diffusers_state_dict = convert_state_dict_to_diffusers(peft_state_dict)
801
diffusers_state_dict = {
802
f"{module_name.replace('base_model.model.', '')}.{module_name}": param
803
for module_name, param in diffusers_state_dict.items()
804
}
younesbelkada1 year ago
Suggested change
diffusers_state_dict = {
f"{module_name.replace('base_model.model.', '')}.{module_name}": param
for module_name, param in diffusers_state_dict.items()
}

This is not needed in case you don't use the PeftModel interface, only get_peft_model_state_dict(unet) should suffice

younesbelkada
younesbelkada commented on 2023-11-14
Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
761 "time_emb_proj",
762 ],
763 )
764
unet = get_peft_model(unet, lora_config)
younesbelkada1 year ago (edited 1 year ago)❤ 1

you can simply do unet.add_adapter(lora_config) and by-pass the PeftModel interface which is what I am doing in #5388
Although it is possible to use a PeftModel through get_peft_model I would rather encourage users to use add_adapter interface to have an API which is consistent across all examples and inference guidelines

Suggested change
unet = get_peft_model(unet, lora_config)
unet.add_adapter(lora_config)
younesbelkada
younesbelkada commented on 2023-11-14
Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
857
858 # 12. Optimizer creation
859 optimizer = optimizer_class(
860
unet.parameters(),
younesbelkada1 year ago
Suggested change
unet.parameters(),
filter(lambda p: p.requires_grad, unet.parameters()),

to only select trainable parameters

younesbelkada
younesbelkada commented on 2023-11-14
Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
793 if accelerator.is_main_process:
794 unet_ = accelerator.unwrap_model(unet)
795 # save weights in peft format to be able to load them back
796
unet_.save_pretrained(output_dir)
younesbelkada1 year ago
Suggested change
unet_.save_pretrained(output_dir)
unet_.peft_config.save_pretrained(output_dir)

The save_pretrained here will save both adapter_config.json and adapter_model.bin, but the .bin gets ignored since we don't use it when we load lora weights in PEFT + diffusers. Since the lora weights are properly saved below, I propose to only save the adapter config as such

sayakpaul1 year ago

@younesbelkada we're using load_adapter() on the UNet.

sayakpaul1 year ago

Ah I see what you mean. See if the current changes make sense.

younesbelkada
younesbelkada commented on 2023-11-14
Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
1172 using_cuda = "cuda" in str(accelerator.device)
1173 with torch.no_grad() and torch.autocast(
1174 str(accelerator.device), dtype=weight_dtype if using_cuda else torch.bfloat16, enabled=using_cuda
1175
) and unet.disable_adapter():
younesbelkada1 year ago (edited 1 year ago)
Suggested change
) and unet.disable_adapter():
):

In case you would go without PeftModel interface, you need to remove this and accept my two suggestions below

younesbelkada
younesbelkada commented on 2023-11-14
Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
1170 # Notice that we're disabling the adapter layers within the `unet` and then it becomes a
1171 # regular teacher. This way, we don't have to separately initialize a teacher UNet.
1172 using_cuda = "cuda" in str(accelerator.device)
1173
with torch.no_grad() and torch.autocast(
younesbelkada1 year ago
Suggested change
with torch.no_grad() and torch.autocast(
unet.disable_adapters()
with torch.no_grad() and torch.autocast(
younesbelkada
younesbelkada commented on 2023-11-14
Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
1179 encoder_hidden_states=prompt_embeds.to(weight_dtype),
1180 added_cond_kwargs={k: v.to(weight_dtype) for k, v in encoded_text.items()},
1181 ).sample
1182
cond_pred_x0 = predicted_origin(
younesbelkada1 year ago
Suggested change
cond_pred_x0 = predicted_origin(
# re-enable unet adapters
unet.enable_adapters()
cond_pred_x0 = predicted_origin(
younesbelkada
younesbelkada commented on 2023-11-14
younesbelkada1 year ago

Hi @sayakpaul
Thanks for your great efforts!
I left few comments regarding the current state of the fine-tuning script, right now the script deviates a bit from the PEFT integration in diffusers as it utilises the PeftModel interface. Although it works fine, it might lead to some confusion (why load_adapter for inference that modifies in-place the model and get_peft_model that suddenly returns a PeftModel for training? ). IMO when using PEFT integration (in diffusers and/or transfomers) we should see peft as an utility library rather than a modeling library
I propose to slightly refactor the training logic, which should also work as expected, following #5388 - If that's something we want to ship asap that's totally fine and can be re-worked in a follow up PR, let me know if you need any help!

sayakpaul remove lora_sayak
17d5c0dd
sayakpaul Apply suggestions from code review
fea95e0f
sayakpaul style
83801a69
sayakpaul make log validation leaner.
0c5d9348
sayakpaul Merge branch 'main' into lcm-lora-sdxl-datasets
3b034bea
sayakpaul move back enabled in.
0f42185e
sayakpaul fix: log_validation call.
41f19258
sayakpaul add: checkpointing tests
bf5c5d6b
sayakpaul
sayakpaul1 year ago (edited 1 year ago)

@younesbelkada I resolved all your comments. Could you take a look again? Thanks so much in advance! In particular, how I am disabling and enabling the adapters.

sayakpaul
sayakpaul1 year ago

Tests will pass after #5388 is merged.

sayakpaul Merge branch 'main' into lcm-lora-sdxl-datasets
64063c7a
sayakpaul
sayakpaul1 year ago

Also, @patil-suraj, I launched an experiment yesterday:

export MODEL_NAME="stabilityai/stable-diffusion-xl-base-1.0"
export OUTPUT_DIR="lora-lora-sdxl-new"
export DATASET_NAME="lambdalabs/pokemon-blip-captions"
export VAE_PATH="madebyollin/sdxl-vae-fp16-fix"

CUDA_VISIBLE_DEVICES=1 accelerate launch train_lcm_distill_lora_sdxl.py \
  --pretrained_teacher_model=${MODEL_NAME}  \
  --pretrained_vae_model_name_or_path=${VAE_PATH} \
  --output_dir="lora-lcm-sdxl-new" \
  --mixed_precision="fp16" \
  --dataset_name=$DATASET_NAME \
  --resolution=1024 \
  --train_batch_size=16 \
  --gradient_accumulation_steps=1 \
  --gradient_checkpointing \
  --use_8bit_adam \
  --learning_rate=1e-5 --loss_type="huber" \
  --report_to="wandb" \
  --lr_scheduler="constant" \
  --lr_warmup_steps=0 \
  --max_train_steps=5000 \
  --checkpointing_steps=100 \
  --validation_steps=50 \
  --seed="0" \
  --report_to="wandb" \
  --push_to_hub

The results (https://wandb.ai/sayakpaul/text2image-fine-tune/runs/ahqu0wvp) are random noise. Anything crucial I am missing? Any usual suspect?

sayakpaul Merge branch 'main' into lcm-lora-sdxl-datasets
53cf0e76
pcuenca
pcuenca commented on 2023-11-17
pcuenca1 year ago

A couple of things I saw while testing:

  • I ran out of GPU memory while running the second evaluation. I verified my max batch size by running the script using --validation_steps=5 to verify that I could train, evaluate and go back to training successfully. But when I launched the training run for real with evaluation and checkpointing every 100 steps, it crashed at the 200 step mark. I'm not sure what could be the reason for this. I decreased my batch size (slightly, 12 to 10) after that, and the same thing happened again
  • There are many images smaller than 1024 pixels in the pokemon-blip-captions dataset (didn't count how many). Would that be a problem for SDXL training?
sayakpaul
sayakpaul1 year ago (edited 1 year ago)

I ran out of GPU memory while running the second evaluation. I verified my max batch size by running the script using --validation_steps=5 to verify that I could train, evaluate and go back to training successfully. But when I launched the training run for real with evaluation and checkpointing every 100 steps, it crashed at the 200 step mark. I'm not sure what could be the reason for this. I decreased my batch size (slightly, 12 to 10) after that, and the same thing happened again

Didn't see this with the following (I am using a single GPU from the DGX):

export MODEL_NAME="stabilityai/stable-diffusion-xl-base-1.0"
export OUTPUT_DIR="lora-lora-sdxl-new"
export DATASET_NAME="lambdalabs/pokemon-blip-captions"
export VAE_PATH="madebyollin/sdxl-vae-fp16-fix"

CUDA_VISIBLE_DEVICES=1 accelerate launch train_lcm_distill_lora_sdxl.py \
  --pretrained_teacher_model=${MODEL_NAME}  \
  --pretrained_vae_model_name_or_path=${VAE_PATH} \
  --output_dir="lora-lcm-sdxl-new" \
  --mixed_precision="fp16" \
  --dataset_name=$DATASET_NAME \
  --resolution=1024 \
  --train_batch_size=16 \
  --gradient_accumulation_steps=1 \
  --gradient_checkpointing \
  --use_8bit_adam \
  --lora_rank=16 \
  --learning_rate=1e-5 --loss_type="huber" --adam_weight_decay=0.0 \
  --report_to="wandb" \
  --lr_scheduler="constant" \
  --lr_warmup_steps=0 \
  --max_train_steps=5000 \
  --checkpointing_steps=100 \
  --validation_steps=50 \
  --seed="0" \
  --report_to="wandb" \
  --push_to_hub

There are many images smaller than 1024 pixels in the pokemon-blip-captions dataset (didn't count how many). Would that be a problem for SDXL training?

Could be but SDXL doesn't really excel at lower resolutions than that from what I have seen. Currently, I don't have a good workaround other than suggesting the use of an upscaler to upsample the images to 1024x1024.

sayakpaul Merge branch 'main' into lcm-lora-sdxl-datasets
de958dc6
younesbelkada
younesbelkada approved these changes on 2023-11-17
younesbelkada1 year ago

Impressive work @sayakpaul ! All good on PEFT side!

Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
1167 # Notice that we're disabling the adapter layers within the `unet` and then it becomes a
1168 # regular teacher. This way, we don't have to separately initialize a teacher UNet.
1169 using_cuda = "cuda" in str(accelerator.device)
1170
unet.disable_adapters()
younesbelkada1 year ago👍 1

I just realized we do have a duplicated method disable_lora / enable_lora here: https://github.com/huggingface/diffusers/blob/main/src/diffusers/loaders/unet.py#L536 let's just keep that in mind and only use disable_adapters in the future

patrickvonplaten patrickvonplaten requested a review from patil-suraj patil-suraj 1 year ago
ArturFormella
ArturFormella1 year ago

What about other files with the same problem?

  • train_lcm_distill_lora_sd_wds.py
  • train_lcm_distill_sd_wds.py
  • train_lcm_distill_sdxl_wds.py

More info:
huan085128/lcm_lora#1
https://github.com/huggingface/diffusers/blob/main/examples/consistency_distillation/train_lcm_distill_lora_sdxl_wds.py#L506C15-L506C15

sayakpaul Merge branch 'main' into lcm-lora-sdxl-datasets
5824fa3b
sayakpaul
sayakpaul1 year ago

@ArturFormella this PR should provide a good reference for anyone willing to adjust the scripts accordingly. Plus @pcuenca is working on #5908.

sayakpaul Merge branch 'main' into lcm-lora-sdxl-datasets
f52cb6e7
sayakpaul taking my chances to see if disabling autocasting has any effect?
5534b0c2
sayakpaul
sayakpaul1 year ago

@BenjaminBossan pinging you since Sourab is busy and Younes is OOO.

In this PR, I am doing what I described here: #5778 (comment). The training, however, seems to be completely off as it's yielding just noise: https://wandb.ai/sayakpaul/text2image-fine-tune/runs/ax6w9q0x?workspace=user-sayakpaul. I don't think it's just a matter of hyperparameters.

I suspect some peft related changes that I did in this PR might have caused it. I am unable to see any potential suspects in comparison to what we have in https://github.com/huggingface/diffusers/blob/main/examples/consistency_distillation/train_lcm_distill_lora_sdxl_wds.py (don't worry about the dataloading code). Would you be able to take a look and advise regarding the peft related changes?

BenjaminBossan
BenjaminBossan1 year ago

Hi @sayakpaul I took a look at the script and also compared it to the other one you linked. I have no experience in training diffusion models, so I cannot judge if the generated output could be solved by some better hyper-parameters or if something else is completely off. The diff between the two scripts is quite large, so I could not pinpoint what difference causes the trouble.

The main difference that involves PEFT seems to be the way that the teacher network is being used via disable_adapters(). IIUC, the teacher net is completely frozen, whereas the student net is teacher + LoRA parameters on top, the latter being updated. Maybe something worth checking would be if requires_grad is set correctly on all params after disable_adapters() is called, and whether it is re-set correctly after enable_adapters() is being called. The diffusers implementation of these methods looks correct to me, but if training issues are PEFT related, this is what I would check first.

sayakpaul
sayakpaul1 year ago

Thanks @BenjaminBossan for your inputs. I will get to them and let you know.

sayakpaul resolve conflicts
3bacd82d
sayakpaul start debugging
1da3071f
sayakpaul name
bd4d1c43
sayakpaul name
26f16c18
sayakpaul name
91740272
sayakpaul more debug
92ba868d
sayakpaul more debug
1fba251b
sayakpaul index
3751ca9b
sayakpaul remove index.
63649d35
sayakpaul print length
05de5422
sayakpaul print length
5e604a8a
sayakpaul print length
8fecdda2
sayakpaul
sayakpaul1 year ago

@BenjaminBossan sorry for the delay here. Big log file here: https://huggingface.co/datasets/sayakpaul/sample-datasets/blob/main/logs.txt.

Some findings:

sayakpaul move unet.train() after add_adapter()
023866f8
sayakpaul disable some prints.
07c28de8
sayakpaul enable_adapters() manually.
c6a61dac
BenjaminBossan
BenjaminBossan1 year ago👍 1

Somewhat surprisingly, length of params_to_optimize (when packed as a list) is zero. This is not expected, no?

The issue here is that the result of filter is lazy, like a generator expression. Since you pass the same expression to the optimizer right after, it is exhausted, so when you call list on it after that, it is empty. Almost certainly, it would contain the same number if you cast it to a list before passing it to the optimizer.

The other numbers are, as you say, pretty much what's expected, so my suspicion that the issue could be related to requires_grad is very unlikely at this point.

Instead of initializing two UNets (teacher and student), it leverages the disable_adapters() and enable_adapters() functions. In this case, the teacher is without any adapters and the student is with LoRA. We only update the LoRA params in the student.

Do you have a reference for using this trick for training? I wonder if there could be some kind of flaw in that approach which could explain the findings.

sayakpaul
sayakpaul1 year ago

Do you have a reference for using this trick for training? I wonder if there could be some kind of flaw in that approach which could explain the findings.

This is taken from https://huggingface.co/blog/stackllama. It's implemented in TRL too:
https://github.com/huggingface/trl/blob/baa8f09cb35057c03b33d898f90c7e8ff958ed9b/trl/trainer/ppo_trainer.py#L468

@lvwerra could you comment more on the above with respect to:

Instead of initializing two UNets (teacher and student), it leverages the disable_adapters() and enable_adapters() functions. In this case, the teacher is without any adapters and the student is with LoRA. We only update the LoRA params in the student.

lvwerra
lvwerra1 year ago

I think @younesbelkada would be the right person to answer this :)

sayakpaul remove prints.
ec33085e
sayakpaul Merge branch 'main' into lcm-lora-sdxl-datasets
d14dd411
sayakpaul some changes.
ed7969d2
sayakpaul fix params_to_optimize
8c549e4b
sayakpaul more fixes
94460666
sayakpaul debug
0153665f
sayakpaul debug
b9891ffb
sayakpaul remove print
b11b0a6d
sayakpaul disable grad for certain contexts.
539bda39
sayakpaul
sayakpaul1 year ago

@BenjaminBossan the main culprit was not putting torch.no_grad() where it was needed. I added that in 539bda3.

Now, things are slowly showing progress :-)

BenjaminBossan
BenjaminBossan1 year ago

the main culprit was not putting torch.no_grad()

Awesome that you figured it out 🎉

patil-suraj Merge branch 'main' into lcm-lora-sdxl-datasets
dfe916dd
patil-suraj patil-suraj marked this pull request as ready for review 1 year ago
fabiorigano Add support for IPAdapterFull (#5911)
d5a40cde
yiyixuxu Fix a bug in `add_noise` function (#6085)
e3d76c47
apolinario [Advanced Diffusion Script] Add Widget default text (#6100)
472c3974
apolinario [Advanced Training Script] Fix pipe example (#6106)
373d3923
charchit7 IP-Adapter for StableDiffusionControlNetImg2ImgPipeline (#5901)
be46b6eb
a-r-r-o-w IP adapter support for most pipelines (#5900)
c7a87ca7
sayakpaul
sayakpaul1 year ago

Need to propagate the changes from #6145 once it's merged.

sayakpaul resolve conflicts
556b7977
sayakpaul Merge branch 'main' into lcm-lora-sdxl-datasets
a8d97858
sayakpaul fix: lora_alpha
47abcf6b
sayakpaul make vae casting conditional/
b7c0f95f
sayakpaul param upcasting
7a1d6c90
sayakpaul propagate comments from https://github.com/huggingface/diffusers/pull…
87f87a70
sayakpaul sayakpaul changed the title [WIP][Training] Add `datasets` version of LCM LoRA SDXL [Training] Add `datasets` version of LCM LoRA SDXL 1 year ago
sayakpaul
sayakpaul1 year ago

@dg845 as well if you want to give this a look :-)

dg845
dg845 commented on 2023-12-21
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
192
193
194# From LCMScheduler.get_scalings_for_boundary_condition_discrete
195
def scalings_for_boundary_conditions(timestep, sigma_data=0.5, timestep_scaling=10.0):
196
c_skip = sigma_data**2 / ((timestep / 0.1) ** 2 + sigma_data**2)
197
c_out = (timestep / 0.1) / ((timestep / 0.1) ** 2 + sigma_data**2) ** 0.5
198
return c_skip, c_out
dg8451 year ago

We could make this a little more general by using the timestep_scaling argument (and perhaps expose this as an argument):

Suggested change
def scalings_for_boundary_conditions(timestep, sigma_data=0.5, timestep_scaling=10.0):
c_skip = sigma_data**2 / ((timestep / 0.1) ** 2 + sigma_data**2)
c_out = (timestep / 0.1) / ((timestep / 0.1) ** 2 + sigma_data**2) ** 0.5
return c_skip, c_out
def scalings_for_boundary_conditions(timestep, sigma_data=0.5, timestep_scaling=10.0):
scaled_timestep = timestep_scaling * timestep
c_skip = sigma_data**2 / (scaled_timestep**2 + sigma_data**2)
c_out = scaled_timestep / (scaled_timestep**2 + sigma_data**2) ** 0.5
return c_skip, c_out

(The current function is the same as in examples/consistency_distillation/train_lcm_distill_lora_sdxl_wds.py so if we make this change we should probably also propagate it to the WebDataset scripts.)

sayakpaul1 year ago👍 1

Would prefer reflecting that after it's changed in WDS so that it's easier to track.

patil-suraj1 year ago

+1, let's adapt this for both scripts.

dg845
dg845 commented on 2023-12-21
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
789
790 # 9. Add LoRA to the student U-Net, only the LoRA projection matrix will be updated by the optimizer.
791 lora_config = LoraConfig(
792
r=args.lora_rank,
793
lora_alpha=args.lora_rank,
dg8451 year ago

Would it make sense to allow lora_alpha to be set independently of the r/lora_rank, to allow the LoRA layer scaling to be controlled?

sayakpaul1 year ago👍 1

Maybe in a future PR. Since this PR is almost just a copy-paste of the WDS version.

patil-suraj1 year ago

makes sense.

dg845
dg845 commented on 2023-12-21
Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
1194 # predicted noise eps_0 and predicted original sample x_0, then run the ODE solver using these
1195 # estimates to predict the data point in the augmented PF-ODE trajectory corresponding to the next ODE
1196 # solver timestep.
1197
unet.disable_adapters()
dg8451 year ago

I think it would be a little more clear if a comment was added here to indicate that when the LoRA adapter is disabled, unet is exactly the teacher model U-Net, so we can get the teacher outputs from it.

patil-suraj1 year ago

+1

sayakpaul1 year ago

Done in 4c689b2.

dg845
dg845 commented on 2023-12-21
Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
1257 # Note that the DDIM step depends on both the predicted x_0 and source noise eps_0.
1258 x_prev = solver.ddim_step(pred_x0, pred_noise, index).to(unet.dtype)
1259
1260
# re-enable unet adapters
1261
unet.enable_adapters()
dg8451 year ago

Following https://github.com/huggingface/diffusers/pull/5778/files#r1434412096, it might also make sense to modify the comment here to indicate that re-enabling the LoRA adapter turns unet back into the student model.

patil-suraj1 year ago

+1

sayakpaul1 year ago

Done in 4c689b2.

sayakpaul Merge branch 'main' into lcm-lora-sdxl-datasets
404351fa
sayakpaul
sayakpaul1 year ago

@patil-suraj @dg845 ran a recent experiment with the following:

export MODEL_NAME="stabilityai/stable-diffusion-xl-base-1.0"
export OUTPUT_DIR="lora-lcm-sdxl-new"
export DATASET_NAME="lambdalabs/pokemon-blip-captions"
export VAE_PATH="madebyollin/sdxl-vae-fp16-fix"

CUDA_VISIBLE_DEVICES=1 accelerate launch train_lcm_distill_lora_sdxl.py \
  --pretrained_teacher_model=${MODEL_NAME}  \
  --pretrained_vae_model_name_or_path=${VAE_PATH} \
  --output_dir="lora-lcm-sdxl-new" \
  --mixed_precision="fp16" \
  --dataset_name=$DATASET_NAME \
  --resolution=1024 \
  --train_batch_size=24 \
  --gradient_accumulation_steps=1 \
  --gradient_checkpointing \
  --use_8bit_adam \
  --lora_rank=64 \
  --learning_rate=1e-4 \
  --report_to="wandb" \
  --lr_scheduler="constant" \
  --lr_warmup_steps=0 \
  --max_train_steps=10000 \
  --checkpointing_steps=3000 \
  --validation_steps=50 \
  --seed="0" \
  --report_to="wandb" \
  --push_to_hub

WandB: https://wandb.ai/sayakpaul/text2image-fine-tune/runs/tv3zw00t
Weights: https://huggingface.co/sayakpaul/pokemons-lora-lcm-sdxl

Given it's only 10k steps and I haven't ablated the hyperparameters, I'd say it's still good enough. WDYT? I'd be keen on merging this soon as it reduces the memory requirements significantly by following good PEFT practices.

WDYT?

patil-suraj
patil-suraj1 year ago

Sounds good! Usually in my experiment 2-3k steps were enough, 10k results in overfitting with large BS.

patil-suraj
patil-suraj approved these changes on 2023-12-26
Conversation is marked as resolved
Show resolved
examples/consistency_distillation/train_lcm_distill_lora_sdxl.py
242 return out.reshape(b, *((1,) * (len(x_shape) - 1)))
243
244
245
def tokenize_prompt(tokenizer, prompt):
patil-suraj1 year ago

Is this used ?

sayakpaul1 year ago

Removed in c24626a.

kashif [Peft] fix saving / loading when unet is not "unet" (#6046)
4c7e983b
kashif [Wuerstchen] fix fp16 training and correct lora args (#6245)
0bb9cf02
sayakpaul [docs] fix: animatediff docs (#6339)
11659a6f
sayakpaul add: note about the new script in readme_sdxl.
f645b87e
sayakpaul Revert "[Peft] fix saving / loading when unet is not "unet" (#6046)"
fd64acf9
sayakpaul Revert "[Wuerstchen] fix fp16 training and correct lora args (#6245)"
121567b0
sayakpaul Revert "[docs] fix: animatediff docs (#6339)"
c24626ae
sayakpaul remove tokenize_prompt().
4c689b29
sayakpaul assistive comments around enable_adapters() and diable_adapters().
1b49fb92
sayakpaul
sayakpaul1 year ago (edited 1 year ago)

@patil-suraj I added the following for the reference training command when training on a very small dataset like Pokemons:

export MODEL_NAME="stabilityai/stable-diffusion-xl-base-1.0"
export DATASET_NAME="lambdalabs/pokemon-blip-captions"
export VAE_PATH="madebyollin/sdxl-vae-fp16-fix"

accelerate launch train_lcm_distill_lora_sdxl.py \
  --pretrained_teacher_model=${MODEL_NAME}  \
  --pretrained_vae_model_name_or_path=${VAE_PATH} \
  --output_dir="pokemons-lora-lcm-sdxl" \
  --mixed_precision="fp16" \
  --dataset_name=$DATASET_NAME \
  --resolution=1024 \
  --train_batch_size=24 \
  --gradient_accumulation_steps=1 \
  --gradient_checkpointing \
  --use_8bit_adam \
  --lora_rank=64 \
  --learning_rate=1e-4 \
  --report_to="wandb" \
  --lr_scheduler="constant" \
  --lr_warmup_steps=0 \
  --max_train_steps=3000 \
  --checkpointing_steps=500 \
  --validation_steps=50 \
  --seed="0" \
  --report_to="wandb" \
  --push_to_hub

This has 3k steps instead of 10k. The training dynamics for this aren't clear but https://wandb.ai/sayakpaul/text2image-fine-tune/runs/tv3zw00t definitely shows progress IMO.

sayakpaul Merge branch 'main' into lcm-lora-sdxl-datasets
9b3dbaaf
sayakpaul
sayakpaul1 year ago🎉 1

Will merge after the CI is green.

sayakpaul sayakpaul merged 6683f979 into main 1 year ago
sayakpaul sayakpaul deleted the lcm-lora-sdxl-datasets branch 1 year ago

Login to write a write a comment.

Login via GitHub

Assignees
No one assigned
Labels
Milestone