Stable Diffusion 파이프라인의 추론 가속화 및 VRAM 최적화 엔지니어링 가이드

1. 성능 프로파일링 및 벤치마크 환경 구축

모델 최적화를 진행하기 전, 현재 시스템의 리소스 사용량과 처리 시간을 정확히 측정하는 벤치마크 환경을 구축해야 합니다. 이를 통해 이후 적용할 최적화 기법들의 정량적 효과를 검증할 수 있습니다.

1.1 파이프라인 초기화 및 측정 유틸리티

diffusers 라이브러리를 사용하여 Stable Diffusion 파이프라인을 로드하고, 추론 시간과 VRAM 피크 사용량을 측정하는 유틸리티 함수를 작성합니다. 정확한 시간 측정을 위해 GPU 동기화 작업을 포함합니다.

import torch
from diffusers import StableDiffusionPipeline
import time

def initialize_pipeline(model_repo: str, dtype: torch.dtype = torch.float32) -> StableDiffusionPipeline:
    device = "cuda" if torch.cuda.is_available() else "cpu"
    pipeline = StableDiffusionPipeline.from_pretrained(model_repo, torch_dtype=dtype)
    return pipeline.to(device)

def profile_inference(pipe: StableDiffusionPipeline, prompt: str, steps: int = 50, resolution: int = 512):
    torch.cuda.reset_peak_memory_stats()
    torch.cuda.synchronize()
    
    start_t = time.perf_counter()
    with torch.inference_mode():
        output = pipe(
            prompt=prompt, 
            num_inference_steps=steps, 
            height=resolution, 
            width=resolution
        )
    torch.cuda.synchronize()
    elapsed = time.perf_counter() - start_t
    
    peak_vram = torch.cuda.max_memory_allocated() / (1024 ** 3)
    return output.images[0], elapsed, peak_vram

1.2 베이스라인 측정

최적화가 적용되지 않은 FP32 정밀도 환경에서 기준 지표를 수집합니다.

MODEL_ID = "runwayml/stable-diffusion-v1-5"
base_pipe = initialize_pipeline(MODEL_ID, torch.float32)
base_pipe.set_progress_bar_config(disable=True)

test_prompt = "traditional ink wash painting, mountains, pine trees, elegant style"
base_img, base_time, base_vram = profile_inference(base_pipe, test_prompt)

print(f"[Baseline] Time: {base_time:.2f}s | Peak VRAM: {base_vram:.2f}GB")

2. 추론 지연 시간(Latency) 단축 기법

이미지 생성 파이프라인의 병목 현상은 주로 행렬 연산량과 메모리 대역폭에서 발생합니다. 이를 해결하기 위한 핵심 접근 방식을 살펴봅니다.

2.1 부동소수점 정밀도 다운캐스팅 (FP16/BF16)

현대 GPU의 Tensor Core를 활용하기 위해 가중치와 연산 정밀도를 반정밀도(FP16) 또는 BF16으로 변환합니다. 이는 VRAM 사용량을 절반으로 줄이면서 연산 속도를 크게 향상시킵니다.

def get_optimal_dtype():
    if not torch.cuda.is_available():
        return torch.float32
    capability = torch.cuda.get_device_capability()
    # Compute Capability 8.0 이상은 BF16을, 그 이하는 FP16을 권장
    return torch.bfloat16 if capability[0] >= 8 else torch.float16

optimal_dtype = get_optimal_dtype()
fast_pipe = initialize_pipeline(MODEL_ID, optimal_dtype)
fast_pipe.set_progress_bar_config(disable=True)

_, fp16_time, fp16_vram = profile_inference(fast_pipe, test_prompt)
print(f"[{optimal_dtype}] Time: {fp16_time:.2f}s | Speedup: {base_time/fp16_time:.2f}x")

2.2 메모리 효율적 어텐션 (Memory-Efficient Attention)

UNet의 셀프 어텐션 레이어는 시퀀스 길이의 제곱에 비례하는 메모리를 소모합니다. xformers 또는 PyTorch 2.0의 scaled_dot_product_attention을 활성화하여 어텐션 연산의 메모리 풋프린트와 연산 시간을 대폭 축소할 수 있습니다.

# xformers 활성화 (설치 필요: pip install xformers)
try:
    fast_pipe.enable_xformers_memory_efficient_attention()
    print("Xformers memory-efficient attention enabled.")
except ImportError:
    # PyTorch 2.0 이상 환경에서는 기본으로 SDPA(Scaled Dot-Product Attention)가 활성화됨
    print("Falling back to PyTorch native SDPA.")

2.3 동적 배치 프로세싱 (Dynamic Batching)

단일 이미지 생성 요청을 순차적으로 처리하는 대신, 여러 프롬프트를 배치로 묶어 GPU의 병렬 연산 능력을 극대화합니다.

def process_batch(pipe: StableDiffusionPipeline, prompts: list, batch_sz: int = 4, **kwargs):
    results = []
    for i in range(0, len(prompts), batch_sz):
        chunk = prompts[i:i+batch_sz]
        with torch.inference_mode():
            chunk_outputs = pipe(prompt=chunk, **kwargs).images
        results.extend(chunk_outputs)
    return results

batch_prompts = [
    "ink painting of a lone boat",
    "gongbi style peony and butterfly",
    "poet drinking under the moon",
    "blue-green landscape in spring"
]

batch_start = time.perf_counter()
batch_results = process_batch(fast_pipe, batch_prompts, batch_sz=2, num_inference_steps=30)
batch_duration = time.perf_counter() - batch_start

sequential_estimate = len(batch_prompts) * fp16_time
print(f"Batch Throughput Gain: {sequential_estimate / batch_duration:.2f}x")

3. VRAM 풋프린트 최소화 전략

고해상도 이미지 생성이나 제한된 VRAM 환경(예: 8GB 이하 GPU)에서 OOM(Out of Memory) 오류를 방지하기 위한 메모리 최적화 기법입니다.

3.1 활성화 메모리 최적화 (Gradient Checkpointing)

추론 과정에서는 역전파가 필요 없음에도 불구하고, 일부 프레임워크 설정이나 특정 커스텀 레이어에서 불필요한 활성화 맵이 메모리에 적재될 수 있습니다. Gradient Checkpointing을 활성화하여 메모리 할당을 최소화합니다.

# UNet의 불필요한 활성화 메모리 할당 방지
fast_pipe.unet.enable_gradient_checkpointing()

3.2 모델 오프로딩 (CPU/Model Offloading)

VRAM이 극도로 부족할 경우, 현재 연산에 사용되지 않는 모듈(Text Encoder, VAE 등)을 시스템 RAM으로 오프로드합니다. 이는 PCIe 버스 대역폭을 사용하므로 추론 시간이 증가하는 트레이드오프가 있습니다.

# 순차적 CPU 오프로딩 적용 (가장 보수적인 메모리 사용)
fast_pipe.enable_sequential_cpu_offload()

# 또는 모델 전체를 VRAM에 올리기 어렵다면 특정 컴포넌트만 오프로드
# fast_pipe.enable_model_cpu_offload()

3.3 고급 스케줄러 및 스텝 축소

노이즈 제거(Denoising) 스텝 수를 줄이면서도 품질을 유지하기 위해 수렴 속도가 빠른 ODE 솔버 기반의 스케줄러를 사용합니다.

from diffusers import DPMSolverMultistepScheduler

# DPM-Solver++로 스케줄러 교체
fast_pipe.scheduler = DPMSolverMultistepScheduler.from_config(fast_pipe.scheduler.config)

# 20 스텝으로 축소하여 프로파일링
_, dpm_time, dpm_vram = profile_inference(fast_pipe, test_prompt, steps=20)
print(f"[DPM-Solver 20 steps] Time: {dpm_time:.2f}s | VRAM: {dpm_vram:.2f}GB")

4. 통합 파이프라인 구성 및 성능 분석

앞서 논의한 기법들을 종합하여 프로덕션 환경에 배포할 수 있는 최적화된 파이프라인 팩토리를 구현하고, 각 설정에 따른 성능 매트릭을 비교합니다.

def build_production_pipeline(repo_id: str) -> StableDiffusionPipeline:
    dtype = get_optimal_dtype()
    pipe = StableDiffusionPipeline.from_pretrained(repo_id, torch_dtype=dtype)
    
    # 메모리 및 연산 최적화 적용
    try:
        pipe.enable_xformers_memory_efficient_attention()
    except Exception:
        pass
        
    pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
    pipe.set_progress_bar_config(disable=True)
    
    # VRAM 용량에 따라 오프로딩 전략 동적 결정 (예: 8GB 이하)
    vram_capacity = torch.cuda.get_device_properties(0).total_memory / (1024**3)
    if vram_capacity < 8.0:
        pipe.enable_model_cpu_offload()
    else:
        pipe.to("cuda")
        
    return pipe

prod_pipe = build_production_pipeline(MODEL_ID)

# 최종 벤치마크 실행
final_img, final_time, final_vram = profile_inference(prod_pipe, test_prompt, steps=20)

print("--- Final Production Metrics ---")
print(f"Inference Latency: {final_time:.2f} seconds")
print(f"Peak VRAM Usage: {final_vram:.2f} GB")
print(f"Overall Speedup vs Baseline: {base_time / final_time:.2f}x")
print(f"VRAM Reduction: {100 - (final_vram / base_vram * 100):.1f}%")

태그: StableDiffusion PyTorch Diffusers VRAM최적화 추론가속화

8월 7일 15:00에 게시됨