•2025년 6월 20일•조회 0
개인 GPU로 FLUX.1-dev 파인튜닝? QLoRA로 VRAM 10GB 돌파!
이전 게시물인 'Diffusers의 양자화 백엔드 탐색'에서 우리는 FLUX.1-dev와 같은 확산 모델을 축소하여 성능 저하 없이 추론 접근성을 크게 높이는 다양한 양자화 기술을 살펴보았습니다. 비츠앤바이트(bitsandbytes)와 토르차오(torchao) 등이 이미지 생성 시 메모리 사용량을 어떻게 줄이는지 확인했습니다.추론을 수행하는 것도 중요하지만, 이러한 모델을 진정으로 우리만의 것으로 만들려면 미세 조정(Fine-tuning)도 가능해야 합니다. 따라서 이번 게시물에서는 단일 GPU에서 약 10GB 미만의 VRAM만으로 이러한 모델을 효율적으로 미세 조정하는 방법을 다룹니다. 이 글은 디퓨저스(diffusers) 라이브러리를 사용하여 FLUX.1-dev를 QLoRA로 미세 조정하는 과정을 안내할 것입니다. 우리는 NVIDIA RTX 4090에서의 결과를 선보일 예정이며, 토르차오(torchao)를 이용한 FP8 학습이 호환 가능한 하드웨어에서 속도를 더욱 최적화할 수 있음을 강조할 것입니다.목차데이터셋FLUX 아키텍처디퓨저스로 FLUX.1-dev QLoRA 미세 조정하기주요 최적화 기술설정 및 결과FP8 토르차오(torchao)를 이용한 미세 조정학습된 LoRA 어댑터로 추론하기옵션 1: LoRA 어댑터 로드하기옵션 2: LoRA를 기본 모델에 병합하기구글 코랩(Google Colab)에서 실행하기결론데이터셋우리는 작은 데이터셋을 사용하여 알폰스 무하(Alphonse Mucha)의 예술 스타일을 학습시키기 위해 black-forest-labs/FLUX.1-dev 모델을 미세 조정하고자 합니다.FLUX 아키텍처이 모델은 세 가지 주요 구성 요소로 이루어져 있습니다:텍스트 인코더 (CLIP 및 T5)트랜스포머 (메인 모델 - Flux Transformer)변분 오토인코더 (VAE)우리의 QLoRA 접근 방식에서는 트랜스포머 구성 요소만 미세 조정하는 데 집중합니다. 텍스트 인코더와 VAE는 학습 내내 고정된 상태를 유지합니다.디퓨저스로 FLUX.1-dev QLoRA 미세 조정하기우리는 FLUX 모델의 DreamBooth 스타일 LoRA 미세 조정을 위해 설계된 디퓨저스 학습 스크립트(https://github.com/huggingface/diffusers/blob/main/examples/research_projects/flux_lora_quantization/train_dreambooth_lora_flux_miniature.py에서 약간 수정)를 사용했습니다. 또한 이 블로그 게시물(및 구글 코랩에서 사용된)의 결과를 재현하기 위한 단축 버전은 여기에서 확인할 수 있습니다. QLoRA 및 메모리 효율성에 중요한 부분을 살펴보겠습니다.주요 최적화 기술LoRA (Low-Rank Adaptation) 심층 분석: LoRA는 낮은 랭크 행렬로 가중치 업데이트를 추적하여 모델 학습을 더욱 효율적으로 만듭니다. 전체 가중치 행렬 W를 업데이트하는 대신, LoRA는 두 개의 더 작은 행렬 A와 B를 학습합니다. 모델 가중치에 대한 업데이트는 ΔW=BA이며, 여기서 A는 Rr×k에 속하고 B는 Rd×r에 속합니다. r(랭크라고 불림)은 원래 차원보다 훨씬 작으므로 업데이트할 매개변수가 적습니다. 마지막으로, α는 LoRA 활성화에 대한 스케일링 요소입니다. 이것은 LoRA가 업데이트에 미치는 영향에 영향을 미치며, 종종 r과 같거나 그 배수로 설정됩니다. 이는 사전 학습된 모델과 LoRA 어댑터의 영향을 균형 있게 조절하는 데 도움이 됩니다. 개념에 대한 일반적인 소개는 이전 블로그 게시물인 '효율적인 Stable Diffusion 미세 조정을 위한 LoRA 사용'을 참조하십시오.QLoRA: 효율성의 핵심 동력: QLoRA는 사전 학습된 기본 모델을 양자화된 형식(일반적으로 bitsandbytes를 통한 4비트)으로 먼저 로드하여 LoRA를 향상시킵니다. 이는 기본 모델의 메모리 사용량을 크게 줄입니다. 그런 다음 이 양자화된 기본 모델 위에 LoRA 어댑터(일반적으로 FP16/BF16)를 학습시킵니다. 이는 기본 모델을 유지하는 데 필요한 VRAM을 극적으로 낮춥니다.예를 들어, HiDream 4비트 양자화를 사용한 DreamBooth 학습 스크립트에서 bitsandbytes는 LoRA 미세 조정의 최대 메모리 사용량을 ~60GB에서 ~37GB로 줄여 품질 저하가 거의 없습니다. 이와 동일한 원리를 여기에서 소비자 등급 하드웨어에서 FLUX.1을 미세 조정하는 데 적용합니다.8비트 옵티마이저 (AdamW):표준 AdamW 옵티마이저는 각 매개변수에 대해 32비트(FP32)로 첫 번째 및 두 번째 모멘트 추정치를 유지하며, 이는 많은 메모리를 소비합니다. 8비트 AdamW는 블록 단위 양자화를 사용하여 옵티마이저 상태를 8비트 정밀도로 저장하면서 학습 안정성을 유지합니다. 이 기술은 표준 FP32 AdamW에 비해 옵티마이저 메모리 사용량을 약 75% 줄일 수 있습니다. 스크립트에서 이를 활성화하는 것은 간단합니다:if args.use_8bit_adam: optimizer_class = bnb.optim.AdamW8bitelse: optimizer_class = torch.optim.AdamWoptimizer = optimizer_class( params_to_optimize, betas=(args.adam_beta1, args.adam_beta2), weight_decay=args.adam_weight_decay, eps=args.adam_epsilon,)그래디언트 체크포인팅:전방향(forward pass) 중에 중간 활성화는 일반적으로 역방향(backward pass) 그래디언트 계산을 위해 저장됩니다. 그래디언트 체크포인팅은 일부 체크포인트 활성화만 저장하고 역전파 중에 다른 활성화를 재계산함으로써 계산과 메모리를 교환합니다.if args.gradient_checkpointing: transformer.enable_gradient_checkpointing()레이턴트 캐싱:이 최적화 기술은 학습 시작 전에 모든 학습 이미지를 VAE 인코더를 통해 사전 처리합니다. 그 결과로 얻은 잠재 표현을 메모리에 저장합니다. 학습 중에는 이미지를 즉석에서 인코딩하는 대신 캐시된 레이턴트를 직접 사용합니다. 이 접근 방식은 두 가지 주요 이점을 제공합니다:학습 중 불필요한 VAE 인코딩 계산을 제거하여 각 학습 단계의 속도를 높입니다.캐싱 후 VAE를 GPU 메모리에서 완전히 제거할 수 있습니다. 단점은 모든 캐시된 레이턴트를 저장하기 위해 RAM 사용량이 증가하지만, 이는 일반적으로 작은 데이터셋의 경우 관리 가능합니다.if args.cache_latents: latents_cache = [] for batch in tqdm(train_dataloader, desc="Caching latents"): with torch.no_grad(): batch["pixel_values"] = batch["pixel_values"].to( accelerator.device, non_blocking=True, dtype=weight_dtype ) latents_cache.append(vae.encode(batch["pixel_values"]).latent_dist) del vae free_memory()4비트 양자화 설정 (BitsAndBytesConfig):이 섹션에서는 기본 모델에 대한 QLoRA 구성을 보여줍니다:bnb_4bit_compute_dtype = torch.float32if args.mixed_precision == "fp16": bnb_4bit_compute_dtype = torch.float16elif args.mixed_precision == "bf16": bnb_4bit_compute_dtype = torch.bfloat16nf4_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=bnb_4bit_compute_dtype,)transformer = FluxTransformer2DModel.from_pretrained( args.pretrained_model_name_or_path, subfolder="transformer", quantization_config=nf4_config, torch_dtype=bnb_4bit_compute_dtype,)transformer = prepare_model_for_kbit_training(transformer, use_gradient_checkpointing=False)LoRA 구성 정의 (LoraConfig):양자화된 트랜스포머에 어댑터가 추가됩니다:transformer_lora_config = LoraConfig( r=args.rank, lora_alpha=args.rank, init_lora_weights="gaussian", target_modules=["to_k", "to_q", "to_v", "to_out.0"],)transformer.add_adapter(transformer_lora_config)print(f"trainable params: {transformer.num_parameters(only_trainable=True)} || all params: {transformer.num_parameters()}")텍스트 임베딩 사전 계산 (CLIP/T5)QLoRA 미세 조정을 시작하기 전에 텍스트 인코더의 출력을 한 번 캐싱하여 VRAM과 소요 시간을 크게 절약할 수 있습니다.학습 시 데이터 로더는 캡션을 다시 인코딩하는 대신 캐시된 임베딩을 단순히 읽으므로, CLIP/T5 인코더는 GPU 메모리에 상주할 필요가 없습니다.코드import argparseimport pandas as pdimport torchfrom datasets import load_datasetfrom huggingface_hub.utils import insecure_hashlibfrom tqdm.auto import tqdmfrom transformers import T5EncoderModelfrom diffusers import FluxPipelineMAX_SEQ_LENGTH = 77OUTPUT_PATH = "embeddings.parquet"def generate_image_hash(image): return insecure_hashlib.sha256(image.tobytes()).hexdigest()def load_flux_dev_pipeline(): id = "black-forest-labs/FLUX.1-dev" text_encoder = T5EncoderModel.from_pretrained(id, subfolder="text_encoder_2", load_in_8bit=True, device_map="auto") pipeline = FluxPipeline.from_pretrained( id, text_encoder_2=text_encoder, transformer=None, vae=None, device_map="balanced" ) return pipeline@torch.no_grad()def compute_embeddings(pipeline, prompts, max_sequence_length): all_prompt_embeds = [] all_pooled_prompt_embeds = [] all_text_ids = [] for prompt in tqdm(prompts, desc="Encoding prompts."): ( prompt_embeds, pooled_prompt_embeds, text_ids, ) = pipeline.encode_prompt(prompt=prompt, prompt_2=None, max_sequence_length=max_sequence_length) all_prompt_embeds.append(prompt_embeds) all_pooled_prompt_embeds.append(pooled_prompt_embeds) all_text_ids.append(text_ids) max_memory = torch.cuda.max_memory_allocated() / 1024 / 1024 / 1024 print(f"Max memory allocated: {max_memory:.3f} GB") return all_prompt_embeds, all_pooled_prompt_embeds, all_text_idsdef run(args): dataset = load_dataset("Norod78/Yarn-art-style", split="train") image_prompts = {generate_image_hash(sample["image"]): sample["text"] for sample in dataset} all_prompts = list(image_prompts.values()) print(f"{len(all_prompts)=}") pipeline = load_flux_dev_pipeline() all_prompt_embeds, all_pooled_prompt_embeds, all_text_ids = compute_embeddings( pipeline, all_prompts, args.max_sequence_length ) data = [] for i, (image_hash, _) in enumerate(image_prompts.items()): data.append((image_hash, all_prompt_embeds[i], all_pooled_prompt_embeds[i], all_text_ids[i])) print(f"{len(data)=}") embedding_cols = ["prompt_embeds", "pooled_prompt_embeds", "text_ids"] df = pd.DataFrame(data, columns=["image_hash"] + embedding_cols) print(f"{len(df)=}") for col in embedding_cols: df[col] = df[col].apply(lambda x: x.cpu().numpy().flatten().tolist()) df.to_parquet(args.output_path) print(f"Data successfully serialized to {args.output_path}")if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "--max_sequence_length", type=int, default=MAX_SEQ_LENGTH, help="Maximum sequence length to use for computing the embeddings. The more the higher computational costs.", ) parser.add_argument("--output_path", type=str, default=OUTPUT_PATH, help="Path to serialize the parquet file.") args = parser.parse_args() run(args)사용 방법python compute_embeddings.py --max_sequence_length 77 --output_path embeddings_alphonse_mucha.parquet이를 캐시된 VAE 레이턴트(--cache_latents)와 결합하면 활성 모델이 양자화된 트랜스포머 + LoRA 어댑터로만 줄어들어, 전체 미세 조정이 10GB 미만의 GPU 메모리로 충분히 가능해집니다.설정 및 결과이 시연을 위해 우리는 NVIDIA RTX 4090 (24GB VRAM)을 활용하여 성능을 탐색했습니다. 가속화(accelerate)를 사용한 전체 학습 명령은 아래에 나와 있습니다.accelerate launch --config_file=accelerate.yaml train_dreambooth_lora_flux_miniature.py --pretrained_model_name_or_path="black-forest-labs/FLUX.1-dev" --data_df_path="embeddings_alphonse_mucha.parquet" --output_dir="alphonse_mucha_lora_flux_nf4" --mixed_precision="bf16" --use_8bit_adam --weighting_scheme="none" --width=512 --height=768 --train_batch_size=1 --repeats=1 --learning_rate=1e-4 --guidance_scale=1 --report_to="wandb" --gradient_accumulation_steps=4 --gradient_checkpointing --lr_scheduler="constant" --lr_warmup_steps=0 --cache_latents --rank=4 --max_train_steps=700 --seed="0"RTX 4090 구성:RTX 4090에서 우리는 train_batch_size를 1, gradient_accumulation_steps를 4, mixed_precision="bf16", gradient_checkpointing=True, use_8bit_adam=True, LoRA 랭크를 4, 해상도를 512x768로 사용했습니다. 레이턴트는 cache_latents=True로 캐싱되었습니다.메모리 사용량 (RTX 4090):QLoRA: QLoRA 미세 조정을 위한 최대 VRAM 사용량은 약 9GB였습니다.BF16 LoRA: 동일한 설정에서 표준 LoRA(FP16의 기본 FLUX.1-dev 포함)를 실행하면 26GB VRAM을 소비했습니다.BF16 전체 미세 조정: 메모리 최적화 없이 약 120GB VRAM이 예상됩니다.학습 시간 (RTX 4090):알폰스 무하 데이터셋에서 700단계 동안 미세 조정하는 데 RTX 4090에서 train_batch_size 1, 해상도 512x768로 약 41분이 소요되었습니다.출력 품질:궁극적인 측정 기준은 생성된 예술 작품입니다. 다음은 derekl35/alphonse-mucha-style 데이터셋에서 QLoRA 미세 조정 모델로 생성된 샘플입니다:이 표는 주요 bf16 정밀도 결과를 비교합니다. 미세 조정의 목표는 모델에 알폰스 무하의 독특한 스타일을 학습시키는 것이었습니다.프롬프트기본 모델 출력QLoRA 미세 조정 출력 (무하 스타일)"평온한 검은 머리 여성, 달빛 백합, 소용돌이치는 식물, 알폰스 무하 스타일""연못의 강아지, 알폰스 무하 스타일""단풍잎과 열매 목걸이를 한 화려한 여우, 숲의 태피스트리 한가운데, 알폰스 무하 스타일"미세 조정된 모델은 장식적인 모티프와 독특한 색상 팔레트에서 알폰스 무하의 상징적인 아르누보 스타일을 멋지게 포착했습니다. QLoRA 프로세스는 새로운 스타일을 학습하면서도 뛰어난 충실도를 유지했습니다.fp16 비교를 보려면 클릭하십시오.결과는 거의 동일하며, QLoRA가 fp16 및 bf16 혼합 정밀도 모두에서 효과적으로 작동함을 보여줍니다.모델 비교: 기본 vs. QLoRA 미세 조정 (fp16)프롬프트기본 모델 출력QLoRA 미세 조정 출력 (무하 스타일)"평온한 검은 머리 여성, 달빛 백합, 소용돌이치는 식물, 알폰스 무하 스타일""연못의 강아지, 알폰스 무하 스타일""단풍잎과 열매 목걸이를 한 화려한 여우, 숲의 태피스트리 한가운데, 알폰스 무하 스타일"FP8 토르차오(torchao)를 이용한 미세 조정컴퓨트 능력 8.9 이상(예: H100, RTX 4090)을 가진 NVIDIA GPU 사용자의 경우, 토르차오 라이브러리를 통한 FP8 학습을 활용하여 훨씬 더 큰 속도 효율성을 달성할 수 있습니다.우리는 H100 SXM GPU에서 약간 수정된 diffusers-torchao 학습 스크립트를 사용하여 FLUX.1-dev LoRA를 미세 조정했습니다. 다음 명령이 사용되었습니다:accelerate launch train_dreambooth_lora_flux.py --pretrained_model_name_or_path=black-forest-labs/FLUX.1-dev --dataset_name=derekl35/alphonse-mucha-style --instance_prompt="a woman, alphonse mucha style" --caption_column="text" --output_dir=alphonse_mucha_fp8_lora_flux --mixed_precision=bf16 --use_8bit_adam --weighting_scheme=none --height=768 --width=512 --train_batch_size=1 --repeats=1 --learning_rate=1e-4 --guidance_scale=1 --report_to=wandb --gradient_accumulation_steps=1 --gradient_checkpointing --lr_scheduler=constant --lr_warmup_steps=0 --rank=4 --max_train_steps=700 --checkpointing_steps=600 --seed=0 --do_fp8_training --push_to_hub학습 실행 시 최대 메모리 사용량은 36.57GB였고, 약 20분 만에 완료되었습니다.이 FP8 미세 조정 모델의 정성적 결과도 확인할 수 있습니다:토르차오를 이용한 FP8 학습을 활성화하는 주요 단계는 다음과 같습니다:torchao.float8의 convert_to_float8_training을 사용하여 모델에 FP8 레이어를 주입합니다.module_filter_fn을 정의하여 어떤 모듈을 FP8로 변환해야 하고 어떤 모듈을 변환하지 않아야 하는지 지정합니다.더 자세한 가이드와 코드 스니펫은 이 gist와 diffusers-torchao 저장소를 참조하십시오.학습된 LoRA 어댑터로 추론하기LoRA 어댑터를 학습한 후에는 추론을 위한 두 가지 주요 접근 방식이 있습니다.옵션 1: LoRA 어댑터 로드하기한 가지 접근 방식은 기본 모델 위에 학습된 LoRA 어댑터를 로드하는 것입니다.LoRA 로드의 이점:유연성: 기본 모델을 다시 로드하지 않고도 다른 LoRA 어댑터 간에 쉽게 전환할 수 있습니다.실험: 어댑터를 교체하여 여러 예술 스타일 또는 개념을 테스트할 수 있습니다.모듈성: set_adapters()를 사용하여 여러 LoRA 어댑터를 결합하여 창의적인 혼합을 할 수 있습니다.저장 효율성: 단일 기본 모델과 여러 개의 작은 어댑터 파일을 유지할 수 있습니다.코드from diffusers import FluxPipeline, FluxTransformer2DModel, BitsAndBytesConfigimport torch ckpt_id = "black-forest-labs/FLUX.1-dev"pipeline = FluxPipeline.from_pretrained( ckpt_id, torch_dtype=torch.float16)pipeline.load_lora_weights("derekl35/alphonse_mucha_qlora_flux", weight_name="pytorch_lora_weights.safetensors")pipeline.enable_model_cpu_offload()image = pipeline( "a puppy in a pond, alphonse mucha style", num_inference_steps=28, guidance_scale=3.5, height=768, width=512, generator=torch.manual_seed(0)).images[0]image.save("alphonse_mucha.png")옵션 2: LoRA를 기본 모델에 병합하기단일 스타일로 최대 효율성을 원할 경우 LoRA 가중치를 기본 모델에 병합할 수 있습니다.LoRA 병합의 이점:VRAM 효율성: 추론 중 어댑터 가중치로 인한 추가 메모리 오버헤드가 없습니다.속도: 어댑터 계산을 적용할 필요가 없으므로 추론 속도가 약간 더 빠릅니다.양자화 호환성: 병합된 모델을 다시 양자화하여 최대 메모리 효율성을 얻을 수 있습니다.코드from diffusers import FluxPipeline, AutoPipelineForText2Image, FluxTransformer2DModel, BitsAndBytesConfigimport torch ckpt_id = "black-forest-labs/FLUX.1-dev"pipeline = FluxPipeline.from_pretrained( ckpt_id, text_encoder=None, text_encoder_2=None, torch_dtype=torch.float16)pipeline.load_lora_weights("derekl35/alphonse_mucha_qlora_flux", weight_name="pytorch_lora_weights.safetensors")pipeline.fuse_lora()pipeline.unload_lora_weights()pipeline.transformer.save_pretrained("fused_transformer")bnb_4bit_compute_dtype = torch.bfloat16nf4_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=bnb_4bit_compute_dtype,)transformer = FluxTransformer2DModel.from_pretrained( "fused_transformer", quantization_config=nf4_config, torch_dtype=bnb_4bit_compute_dtype,)pipeline = AutoPipelineForText2Image.from_pretrained( ckpt_id, transformer=transformer, torch_dtype=bnb_4bit_compute_dtype)pipeline.enable_model_cpu_offload()image = pipeline( "a puppy in a pond, alphonse mucha style", num_inference_steps=28, guidance_scale=3.5, height=768, width=512, generator=torch.manual_seed(0)).images[0]image.save("alphonse_mucha_merged.png")구글 코랩(Google Colab)에서 실행하기우리는 RTX 4090에서 결과를 선보였지만, 동일한 코드를 구글 코랩에서 무료로 제공되는 T4 GPU와 같은 더 접근하기 쉬운 하드웨어에서도 실행할 수 있습니다. T4에서는 동일한 단계 수에 대해 미세 조정 프로세스가 약 4시간으로 훨씬 더 오래 걸릴 것으로 예상할 수 있습니다. 이는 접근성을 위한 절충안이지만, 고성능 하드웨어 없이도 맞춤형 미세 조정을 가능하게 합니다. 코랩에서 실행하는 경우 사용량 제한에 유의하십시오. 4시간 학습 실행은 제한을 초과할 수 있습니다.결론QLoRA는 디퓨저스(diffusers) 라이브러리와 결합하여 FLUX.1-dev와 같은 최첨단 모델을 사용자 정의할 수 있는 능력을 크게 민주화합니다. RTX 4090에서 시연된 바와 같이, 효율적인 미세 조정은 충분히 가능하며 고품질의 스타일적 적응을 제공합니다. 또한 최신 NVIDIA 하드웨어를 사용하는 사용자의 경우, 토르차오(torchao)는 FP8 정밀도를 통해 훨씬 빠른 학습을 가능하게 합니다.허브에 여러분의 창작물을 공유해주세요!미세 조정된 LoRA 어댑터를 공유하는 것은 오픈 소스 커뮤니티에 기여하는 훌륭한 방법입니다. 이를 통해 다른 사람들이 여러분의 스타일을 쉽게 시도하고, 여러분의 작업을 기반으로 구축하며, 창의적인 AI 도구의 활기찬 생태계를 만드는 데 도움이 됩니다.FLUX.1-dev용 LoRA를 학습했다면 공유하는 것을 권장합니다. 가장 쉬운 방법은 학습 스크립트에 --push_to_hub 플래그를 추가하는 것입니다. 또는 이미 모델을 학습했고 업로드하고 싶다면 다음 스니펫을 사용할 수 있습니다.from huggingface_hub import create_repo, upload_folderrepo_id = "your-username/alphonse_mucha_qlora_flux"create_repo(repo_id, exist_ok=True)upload_folder( repo_id=repo_id, folder_path="alphonse_mucha_qlora_flux", commit_message="Add Alphonse Mucha LoRA adapter")우리의 무하(Mucha) QLoRA https://huggingface.co/derekl35/alphonse_mucha_qlora_flux FP8 LoRA https://huggingface.co/derekl35/alphonse_mucha_fp8_lora_flux를 확인하십시오. 이 컬렉션에서 예시로 두 가지와 다른 어댑터들을 모두 찾을 수 있습니다.여러분이 무엇을 만들어낼지 기대됩니다!출처: Hugging Face Blog