support DeepSpeed.

This commit is contained in:
sayakpaul
2024-12-24 10:59:21 +05:30
parent cf9be17819
commit 6329e9ed07
4 changed files with 140 additions and 52 deletions
+1
View File
@@ -15,6 +15,7 @@ class State:
learning_rate: float = None learning_rate: float = None
train_batch_size: int = None train_batch_size: int = None
generator: torch.Generator = None generator: torch.Generator = None
num_update_steps_per_epoch: int = None
# Hub state # Hub state
repo_id: str = None repo_id: str = None
+81 -51
View File
@@ -1,10 +1,8 @@
import inspect
import json import json
import logging import logging
import math import math
import os import os
import random import random
import shutil
from datetime import timedelta from datetime import timedelta
from typing import Any, Dict from typing import Any, Dict
from pathlib import Path from pathlib import Path
@@ -46,10 +44,11 @@ from .dataset import BucketSampler, PrecomputedDataset, VideoDatasetWithResizing
from .models import get_config_from_model_name from .models import get_config_from_model_name
from .state import State from .state import State
from .utils.data_utils import should_perform_precomputation from .utils.data_utils import should_perform_precomputation
from .utils.file_utils import find_files, delete_files, string_to_filename from .utils.file_utils import string_to_filename
from .utils.optimizer_utils import get_optimizer, gradient_norm from .utils.optimizer_utils import get_optimizer
from .utils.memory_utils import get_memory_statistics, free_memory, make_contiguous from .utils.memory_utils import get_memory_statistics, free_memory, make_contiguous
from .utils.torch_utils import unwrap_model, align_device_and_dtype from .utils.torch_utils import unwrap_model, align_device_and_dtype
from .utils.checkpointing import sort_out_and_load_latest_ckpt_states, save_intermediate_ckpt_states
logger = get_logger("finetrainers") logger = get_logger("finetrainers")
@@ -389,7 +388,13 @@ class Trainer:
) )
self.transformer.add_adapter(transformer_lora_config) self.transformer.add_adapter(transformer_lora_config)
# TODO: refactor # Enable TF32 for faster training on Ampere GPUs: https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices
if self.args.allow_tf32 and torch.cuda.is_available():
torch.backends.cuda.matmul.allow_tf32 = True
self.register_saving_loading_hooks(transformer_lora_config)
def register_saving_loading_hooks(self, transformer_lora_config):
# create custom saving & loading hooks so that `accelerator.save_state(...)` serializes in a nice format # create custom saving & loading hooks so that `accelerator.save_state(...)` serializes in a nice format
def save_model_hook(models, weights, output_dir): def save_model_hook(models, weights, output_dir):
if self.state.accelerator.is_main_process: if self.state.accelerator.is_main_process:
@@ -415,13 +420,20 @@ class Trainer:
) )
def load_model_hook(models, input_dir): def load_model_hook(models, input_dir):
transformer_ = self.model_config["pipeline_cls"].from_pretrained( if not self.state.accelerator.distributed_type == DistributedType.DEEPSPEED:
self.args.pretrained_model_name_or_path, subfolder="transformer" while len(models) > 0:
) model = models.pop()
transformer_.add_adapter(transformer_lora_config) if isinstance(unwrap_model(self.state.accelerator, model), type(unwrap_model(self.state.accelerator, self.transformer))):
transformer_ = unwrap_model(self.state.accelerator, model)
else:
raise ValueError(f"Unexpected save model: {unwrap_model(self.state.accelerator, model).__class__}")
else:
transformer_ = unwrap_model(self.state.accelerator, self.transformer).__class__.from_pretrained(
self.args.pretrained_model_name_or_path, subfolder="transformer"
)
transformer_.add_adapter(transformer_lora_config)
lora_state_dict = self.model_config["pipeline_cls"].lora_state_dict(input_dir) lora_state_dict = self.model_config["pipeline_cls"].lora_state_dict(input_dir)
transformer_state_dict = { transformer_state_dict = {
f'{k.replace("transformer.", "")}': v f'{k.replace("transformer.", "")}': v
for k, v in lora_state_dict.items() for k, v in lora_state_dict.items()
@@ -447,10 +459,6 @@ class Trainer:
self.state.accelerator.register_save_state_pre_hook(save_model_hook) self.state.accelerator.register_save_state_pre_hook(save_model_hook)
self.state.accelerator.register_load_state_pre_hook(load_model_hook) self.state.accelerator.register_load_state_pre_hook(load_model_hook)
# Enable TF32 for faster training on Ampere GPUs: https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices
if self.args.allow_tf32 and torch.cuda.is_available():
torch.backends.cuda.matmul.allow_tf32 = True
def prepare_optimizer(self) -> None: def prepare_optimizer(self) -> None:
logger.info("Initializing optimizer and lr scheduler") logger.info("Initializing optimizer and lr scheduler")
@@ -519,6 +527,7 @@ class Trainer:
self.state.train_steps = self.state.train_epochs * num_update_steps_per_epoch self.state.train_steps = self.state.train_epochs * num_update_steps_per_epoch
# Afterwards we recalculate our number of training epochs # Afterwards we recalculate our number of training epochs
self.state.train_epochs = math.ceil(self.state.train_steps / num_update_steps_per_epoch) self.state.train_epochs = math.ceil(self.state.train_steps / num_update_steps_per_epoch)
self.state.num_update_steps_per_epoch = num_update_steps_per_epoch
def prepare_trackers(self) -> None: def prepare_trackers(self) -> None:
logger.info("Initializing trackers") logger.info("Initializing trackers")
@@ -547,10 +556,18 @@ class Trainer:
} }
logger.info(f"Training configuration: {json.dumps(info, indent=4)}") logger.info(f"Training configuration: {json.dumps(info, indent=4)}")
# TODO(aryan): handle resume from checkpoint
global_step = 0 global_step = 0
first_epoch = 0 first_epoch = 0
initial_global_step = 0 initial_global_step = 0
# Potentially load in the weights and states from a previous save
initial_global_step, global_step, first_epoch = sort_out_and_load_latest_ckpt_states(
accelerator=self.state.accelerator,
resume_from_checkpoint=self.args.resume_from_checkpoint,
num_update_steps_per_epoch=self.state.num_update_steps_per_epoch,
output_dir=self.args.output_dir
)
progress_bar = tqdm( progress_bar = tqdm(
range(0, self.state.train_steps), range(0, self.state.train_steps),
initial=initial_global_step, initial=initial_global_step,
@@ -682,20 +699,9 @@ class Trainer:
# Checkpointing # Checkpointing
if accelerator.distributed_type == DistributedType.DEEPSPEED or accelerator.is_main_process: if accelerator.distributed_type == DistributedType.DEEPSPEED or accelerator.is_main_process:
if global_step % self.args.checkpointing_steps == 0: if global_step % self.args.checkpointing_steps == 0:
# before saving state, check if this save would set us over the `checkpointing_limit` save_intermediate_ckpt_states(
if self.args.checkpointing_limit is not None: accelerator=accelerator, checkpointing_limit=self.args.checkpointing_limit, step=global_step, output_dir=self.args.output_dir
checkpoints = find_files(self.args.output_dir, prefix="checkpoint") )
# before we save the new checkpoint, we need to have at_most `checkpoints_total_limit - 1` checkpoints
if len(checkpoints) >= self.args.checkpointing_limit:
num_to_remove = len(checkpoints) - self.args.checkpointing_limit + 1
checkpoints_to_remove = checkpoints[0:num_to_remove]
delete_files(checkpoints_to_remove)
logger.info(f"Checkpointing at step {global_step}")
save_path = os.path.join(self.args.output_dir, f"checkpoint-{global_step}")
accelerator.save_state(save_path)
logger.info(f"Saved state to {save_path}")
# Maybe run validation # Maybe run validation
should_run_validation = ( should_run_validation = (
@@ -725,6 +731,7 @@ class Trainer:
accelerator.wait_for_everyone() accelerator.wait_for_everyone()
if accelerator.is_main_process: if accelerator.is_main_process:
# TODO: consider factoring this out when supporting other types of training algos.
self.transformer = unwrap_model(accelerator, self.transformer) self.transformer = unwrap_model(accelerator, self.transformer)
dtype = ( dtype = (
torch.float16 torch.float16
@@ -741,6 +748,13 @@ class Trainer:
transformer_lora_layers=transformer_lora_layers, transformer_lora_layers=transformer_lora_layers,
) )
if self.args.push_to_hub:
upload_folder(
repo_id=self.state.repo_id, folder_path=self.args.output_dir, ignore_patterns=["checkpoint-*"]
)
self.validate(step=global_step, final_validation=True)
del self.tokenizer, self.text_encoder, self.transformer, self.vae, self.scheduler del self.tokenizer, self.text_encoder, self.transformer, self.vae, self.scheduler
free_memory() free_memory()
memory_statistics = get_memory_statistics() memory_statistics = get_memory_statistics()
@@ -748,7 +762,7 @@ class Trainer:
accelerator.end_training() accelerator.end_training()
def validate(self, step: int) -> None: def validate(self, step: int, final_validation: bool = False) -> None:
logger.info("Starting validation") logger.info("Starting validation")
accelerator = self.state.accelerator accelerator = self.state.accelerator
@@ -763,21 +777,34 @@ class Trainer:
memory_statistics = get_memory_statistics() memory_statistics = get_memory_statistics()
logger.info(f"Memory before validation start: {json.dumps(memory_statistics, indent=4)}") logger.info(f"Memory before validation start: {json.dumps(memory_statistics, indent=4)}")
pipeline = self.model_config["initialize_pipeline"]( if not final_validation:
model_id=self.args.pretrained_model_name_or_path, pipeline = self.model_config["initialize_pipeline"](
tokenizer=self.tokenizer, model_id=self.args.pretrained_model_name_or_path,
text_encoder=self.text_encoder, tokenizer=self.tokenizer,
tokenizer_2=self.tokenizer_2, text_encoder=self.text_encoder,
text_encoder_2=self.text_encoder_2, tokenizer_2=self.tokenizer_2,
transformer=unwrap_model(accelerator, self.transformer), text_encoder_2=self.text_encoder_2,
vae=self.vae, transformer=unwrap_model(accelerator, self.transformer),
device=accelerator.device, vae=self.vae,
revision=self.args.revision, device=accelerator.device,
cache_dir=self.args.cache_dir, revision=self.args.revision,
enable_slicing=self.args.enable_slicing, cache_dir=self.args.cache_dir,
enable_tiling=self.args.enable_tiling, enable_slicing=self.args.enable_slicing,
enable_model_cpu_offload=self.args.enable_model_cpu_offload, enable_tiling=self.args.enable_tiling,
) enable_model_cpu_offload=self.args.enable_model_cpu_offload,
)
else:
# `torch_dtype` is manually set within `initialize_pipeline()`.
pipeline = self.model_config["initialize_pipeline"](
model_id=self.args.pretrained_model_name_or_path,
device=accelerator.device,
revision=self.args.revision,
cache_dir=self.args.cache_dir,
enable_slicing=self.args.enable_slicing,
enable_tiling=self.args.enable_tiling,
enable_model_cpu_offload=self.args.enable_model_cpu_offload,
)
pipeline.load_lora_weights(self.args.output_dir)
all_processes_artifacts = [] all_processes_artifacts = []
for i in range(num_validation_samples): for i in range(num_validation_samples):
@@ -811,6 +838,7 @@ class Trainer:
num_frames=num_frames, num_frames=num_frames,
num_videos_per_prompt=self.args.num_validation_videos_per_prompt, num_videos_per_prompt=self.args.num_validation_videos_per_prompt,
generator=self.state.generator, generator=self.state.generator,
# todo support passing `fps` for supported pipelines.
) )
# Remove all hooks that might have been added during pipeline initialization to the models # Remove all hooks that might have been added during pipeline initialization to the models
@@ -845,7 +873,8 @@ class Trainer:
artifact_value = wandb.Image(filename) artifact_value = wandb.Image(filename)
elif artifact_type == "video": elif artifact_type == "video":
logger.debug(f"Saving video to {filename}") logger.debug(f"Saving video to {filename}")
export_to_video(artifact_value, filename, fps=15) # TODO: this should be configurable here as well as in validation runs where we call the pipeline that has `fps`.
export_to_video(artifact_value, filename, fps=15)
artifact_value = wandb.Video(filename, caption=prompt) artifact_value = wandb.Video(filename, caption=prompt)
all_processes_artifacts.append(artifact_value) all_processes_artifacts.append(artifact_value)
@@ -853,9 +882,10 @@ class Trainer:
all_artifacts = gather_object(all_processes_artifacts) all_artifacts = gather_object(all_processes_artifacts)
if accelerator.is_main_process: if accelerator.is_main_process:
tracker_key = "final" if final_validation else "validation"
for tracker in accelerator.trackers: for tracker in accelerator.trackers:
if tracker.name == "wandb": if tracker.name == "wandb":
tracker.log({"validation": all_artifacts}, step=step) tracker.log({tracker_key: all_artifacts}, step=step)
accelerator.wait_for_everyone() accelerator.wait_for_everyone()
@@ -864,11 +894,11 @@ class Trainer:
logger.info(f"Memory after validation end: {json.dumps(memory_statistics, indent=4)}") logger.info(f"Memory after validation end: {json.dumps(memory_statistics, indent=4)}")
torch.cuda.reset_peak_memory_stats(accelerator.device) torch.cuda.reset_peak_memory_stats(accelerator.device)
self.transformer.train() if not final_validation:
self.transformer.train()
def evaluate(self) -> None: def evaluate(self) -> None:
logger.info("Starting evaluation") raise NotImplementedError
# TODO: implement metrics for evaluation
def _init_distributed(self) -> None: def _init_distributed(self) -> None:
logging_dir = Path(self.args.output_dir, self.args.logging_dir) logging_dir = Path(self.args.output_dir, self.args.logging_dir)
+57
View File
@@ -0,0 +1,57 @@
import os
from accelerate.logging import get_logger
from ..constants import FINETRAINERS_LOG_LEVEL
from ..utils.file_utils import find_files, delete_files
logger = get_logger("finetrainers")
logger.setLevel(FINETRAINERS_LOG_LEVEL)
def sort_out_and_load_latest_ckpt_states(
accelerator, resume_from_checkpoint, num_update_steps_per_epoch, output_dir
):
if not resume_from_checkpoint:
initial_global_step = 0
global_step = 0
first_epoch = 0
else:
if resume_from_checkpoint != "latest":
path = os.path.basename(resume_from_checkpoint)
else:
# Get the most recent checkpoint
dirs = os.listdir(output_dir)
dirs = [d for d in dirs if d.startswith("checkpoint")]
dirs = sorted(dirs, key=lambda x: int(x.split("-")[1]))
path = dirs[-1] if len(dirs) > 0 else None
if path is None:
logger.info(
f"Checkpoint '{resume_from_checkpoint}' does not exist. Starting a new training run."
)
resume_from_checkpoint = None
initial_global_step = 0
else:
logger.info(f"Resuming from checkpoint {path}")
accelerator.load_state(os.path.join(output_dir, path))
global_step = int(path.split("-")[1])
initial_global_step = global_step
first_epoch = global_step // num_update_steps_per_epoch
return initial_global_step, global_step, first_epoch
def save_intermediate_ckpt_states(accelerator, checkpointing_limit, step, output_dir):
# before saving state, check if this save would set us over the `checkpointing_limit`
if checkpointing_limit is not None:
checkpoints = find_files(output_dir, prefix="checkpoint")
# before we save the new checkpoint, we need to have at_most `checkpoints_total_limit - 1` checkpoints
if len(checkpoints) >= checkpointing_limit:
num_to_remove = len(checkpoints) - checkpointing_limit + 1
checkpoints_to_remove = checkpoints[0:num_to_remove]
delete_files(checkpoints_to_remove)
logger.info(f"Checkpointing at step {step}")
save_path = os.path.join(output_dir, f"checkpoint-{step}")
accelerator.save_state(save_path)
logger.info(f"Saved state to {save_path}")
+1 -1
View File
@@ -33,7 +33,7 @@ def main():
trainer.prepare_for_training() trainer.prepare_for_training()
trainer.prepare_trackers() trainer.prepare_trackers()
trainer.train() trainer.train()
trainer.evaluate() # trainer.evaluate()
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info("Received keyboard interrupt. Exiting...") logger.info("Received keyboard interrupt. Exiting...")