- Published on
Stable Diffusion LoRA 합성 후 품질 붕괴 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Stable Diffusion에서 LoRA를 여러 개 쓰다가 최종 배포나 속도 최적화를 위해 합성(merge, bake)했더니 결과물이 갑자기 망가지는 경우가 많습니다. 대표적으로 얼굴이 무너지고(눈/치아/피부 텍스처 붕괴), 원치 않는 스타일이 과도하게 덮이거나, 프롬프트 반응성이 급격히 떨어집니다.
이 글은 “LoRA 합성 후 품질 붕괴”를 재현 가능한 방식으로 진단하고, 원인별로 되돌릴 수 있는 해결책을 제시합니다. 특히 merge는 되돌리기 어려운 작업이므로, 합성 전후를 비교할 수 있는 최소 실험 단위를 먼저 만드는 것이 핵심입니다.
LoRA 합성(merge)이 품질을 망가뜨리는 메커니즘
LoRA는 기본적으로 베이스 모델의 특정 레이어에 대해 저랭크 업데이트(ΔW)를 더하는 방식입니다. 합성은 대략 아래처럼 동작합니다.
- 추론 시:
W' = W + α * ΔW를 런타임에 적용 - 합성 시: 체크포인트 파일 자체에
α * ΔW를 영구 반영
문제는 여러 LoRA를 동시에 합성하거나, 훈련된 베이스/아키텍처가 다른 LoRA를 억지로 합성하면 특정 레이어 업데이트가 누적되어 분포가 무너질 수 있다는 점입니다. 또한 합성은 런타임에서 조절하던 alpha/가중치 스케일을 “영구 고정”하므로, 합성 이후에는 미세 조정 여지가 줄어듭니다.
증상별로 추정 원인 빠르게 매핑하기
아래는 현장에서 가장 자주 보는 증상과 원인 후보입니다.
1) 얼굴/손 디테일이 녹아내림, 뭉개짐
- LoRA 스케일 과다(특히 여러 개 합성)
- 합성된 모델이
fp16/bf16으로 저장되며 정밀도 손실 - VAE/CLIP 설정이 합성 전후로 달라짐
2) 특정 스타일이 과도하게 덮임, 프롬프트 무시
- 스타일 LoRA를 캐릭터/포즈 LoRA와 같이 합성하면서 레이어 충돌
- 텍스트 인코더(TE)까지 합성되어 프롬프트 의미 공간이 틀어짐
3) 합성 전엔 멀쩡했는데 합성 후만 이상
- 합성 도구가 LoRA의 타깃 모듈을 잘못 매핑(예: SDXL과 SD1.5 혼용)
- 안전 텐서 변환/리세이브 과정에서 키 이름이 바뀌거나 누락
합성 전 필수: “최소 재현 세트” 만들기
품질 붕괴를 해결하려면 먼저 같은 조건에서 합성 전후를 비교해야 합니다.
- 동일한 베이스 모델 체크포인트
- 동일한 VAE
- 동일한 샘플러/스텝/CFG/해상도
- 동일한 시드
- 동일한 프롬프트(네거티브 포함)
이 과정은 소프트웨어 트러블슈팅에서 캐시/환경 요인 제거하는 것과 같습니다. 예를 들어 Next.js에서 RSC 캐시가 꼬이면 동일 코드인데도 결과가 달라지듯, 생성 파이프라인도 설정이 조금만 달라져도 원인 추적이 어려워집니다. 내부적으로 “환경 고정”의 중요성은 Next.js App Router RSC 캐시 꼬임 해결법과 사고방식이 유사합니다.
원인 1: LoRA 스케일(가중치) 과다 누적
왜 생기나
런타임에서는 LoRA A를 0.6, LoRA B를 0.4로 섞어 쓰다가, 합성할 때 실수로 둘 다 1.0로 bake해버리는 경우가 흔합니다. 또는 합성 도구가 alpha/multiplier를 다르게 해석해 실제 반영량이 커지기도 합니다.
해결 절차
- 합성은 “한 번에 여러 개”보다 하나씩 단계적으로 진행
- 각 단계마다 최소 재현 세트로 결과 비교
- 합성 스케일을 보수적으로 시작(예:
0.3~0.7)
코드 예시: diffusers로 LoRA를 로드해 스케일 스윕
아래는 합성 전에 런타임에서 스케일을 스윕하며 안전 구간을 찾는 예시입니다.
import torch
from diffusers import StableDiffusionPipeline
base = "runwayml/stable-diffusion-v1-5"
lora_path = "./lora/my_style_lora.safetensors"
pipe = StableDiffusionPipeline.from_pretrained(
base,
torch_dtype=torch.float16,
).to("cuda")
pipe.load_lora_weights(lora_path)
prompt = "portrait photo, 35mm, soft light"
negative = "low quality, bad anatomy"
seed = 1234
g = torch.Generator(device="cuda").manual_seed(seed)
for scale in [0.3, 0.5, 0.7, 0.9, 1.1]:
pipe.set_adapters(["default"], adapter_weights=[scale])
img = pipe(
prompt=prompt,
negative_prompt=negative,
num_inference_steps=28,
guidance_scale=6.5,
generator=g,
).images[0]
img.save(f"./out/scale_{scale}.png")
스윕 결과에서 “품질이 무너지기 시작하는 임계점”이 보이면, 합성은 그보다 낮은 값으로 진행하는 편이 안전합니다.
원인 2: 베이스 모델 불일치(SD1.5, SD2.1, SDXL 혼용)
왜 생기나
LoRA는 학습된 베이스와 구조가 맞아야 합니다. 예를 들어 SDXL용 LoRA를 SD1.5에 억지로 합성하면 키 매핑이 어긋나거나, 일부 레이어만 반영되어 분포가 깨질 수 있습니다.
체크 포인트
- LoRA 카드/메타데이터에 베이스가 명시되어 있는지 확인
- 파일명에
sdxl,v1-5,v2-1등 힌트가 있는지 확인 - 합성 도구가 SDXL의
UNet/text_encoder구조를 지원하는지 확인
해결책
- 베이스가 다르면 합성하지 말고 런타임 로드로만 사용
- 반드시 같은 계열끼리만 합성
- 혼합이 필요하면 “모델 merge(체크포인트 병합)”와 “LoRA merge”를 섞지 말고 단계 분리
원인 3: 텍스트 인코더(TE)까지 합성되어 프롬프트 반응성 붕괴
왜 생기나
일부 LoRA는 UNet뿐 아니라 text_encoder에도 가중치를 가집니다. 합성 시 TE까지 bake되면 프롬프트 의미 공간이 바뀌어, 같은 프롬프트가 전혀 다르게 해석될 수 있습니다.
해결책
- 가능한 경우 UNet만 합성하고 TE는 런타임 LoRA로 유지
- 합성 도구 옵션에서
text_encodermerge를 끄기
코드 예시: LoRA state에서 TE 키 존재 여부 점검
SafeTensors를 읽어 키 네임으로 TE 포함 여부를 대략 확인할 수 있습니다.
from safetensors.torch import load_file
sd = load_file("./lora/my_lora.safetensors")
keys = list(sd.keys())
has_te = any("text_encoder" in k or "lora_te" in k for k in keys)
has_unet = any("unet" in k or "lora_unet" in k for k in keys)
print("has_unet:", has_unet)
print("has_text_encoder:", has_te)
TE가 포함된 LoRA는 합성 시 더 보수적으로 접근하는 것이 좋습니다.
원인 4: 정밀도/리세이브 과정에서 생기는 손실(fp16 저장, 클립 스킵, VAE 불일치)
정밀도 이슈
합성 과정에서 체크포인트를 fp16으로 저장하면 용량은 줄지만, 특정 레이어에서 미세 디테일이 손상되어 “뭉개짐”이 증가할 수 있습니다.
- 합성은 가능하면
fp32로 수행 후, 최종 배포 단계에서만fp16변환을 고려 - 변환 전후 최소 재현 세트로 비교
VAE/CLIP 불일치
합성 전에는 커스텀 VAE를 쓰다가 합성 후 기본 VAE로 돌아가거나, clip_skip 값이 바뀌면 결과가 확 달라집니다. 이건 합성 품질 문제가 아니라 파이프라인 조건 불일치입니다.
- 합성 전후 동일 VAE 강제
clip_skip을 고정하고 비교
원인 5: 여러 LoRA 합성 시 레이어 충돌(특히 스타일+인물+디테일 동시)
왜 생기나
서로 다른 목적의 LoRA가 같은 레이어를 강하게 당기면, 합성 시 특정 특성이 과포화됩니다. 런타임에서는 가중치를 낮추거나 토글로 회피할 수 있지만, bake 후에는 빠져나오기 어렵습니다.
해결책: “역할 분리” 전략
- 캐릭터/의상/포즈: 합성 후보
- 강한 스타일/렌더링 룩: 런타임 LoRA로 유지
- 디테일 보정(피부/필름그레인 등): 합성하더라도 약하게
또는 합성 자체를 포기하고, 런타임에서 어댑터를 관리하는 편이 운영적으로 더 안전할 때가 많습니다. 메모리나 OOM 때문에 합성을 고려했다면, 합성 대신 경량화/오프로딩을 택할 수도 있습니다. 로컬 추론에서 메모리 문제가 핵심이라면 Transformers 로컬 LLM OOM 해결 - 4bit+오프로딩의 접근(정밀도/오프로딩으로 운영 안정성 확보)도 참고할 만합니다.
실전 트러블슈팅 체크리스트(우선순위 순)
아래 순서대로 하면 대개 30분 내에 원인 범위를 좁힐 수 있습니다.
- 합성 전후 조건 고정: 시드, VAE, 샘플러, 스텝, CFG, 해상도,
clip_skip - 베이스 일치 확인: SD1.5/2.1/SDXL 혼용 여부
- LoRA 스케일 재검토: 합성 시 반영된 스케일이 런타임과 같은지
- TE 포함 여부 확인: TE까지 bake했는지, 필요 없는지
- 정밀도 점검: 합성/저장 시
fp16로 내려가며 손실이 생겼는지 - 단계적 합성: LoRA 하나씩 합성하고 매 단계 결과 비교
이 방식은 복잡한 시스템에서 “한 번에 여러 변경을 하지 말라”는 원칙과 같습니다. 병렬화나 최적화를 무리하게 한 번에 적용하면 원인 추적이 어려워지는 것처럼, LoRA도 한 번에 여러 개 bake하면 붕괴 원인을 찾기 힘듭니다. 성능 튜닝에서 함정을 단계적으로 제거하는 접근은 Java Stream 병렬화가 느린 7가지 함정과 해법과도 닮아 있습니다.
추천 워크플로: “합성 가능한 LoRA”만 선별해 안전하게 굽기
1) 런타임에서 먼저 조합을 확정
- 최종 프롬프트 세트 5~10개를 정해 대표 이미지를 뽑기
- LoRA 스케일을 고정하고 결과를 저장
2) 합성은 하나씩, 스케일은 낮게 시작
- LoRA A를
0.5로 합성한 모델 생성 - 동일 조건으로 이미지 비교
- 문제 없으면 다음 LoRA B를
0.3~0.6범위로 합성
3) TE는 가급적 합성하지 않기
- 프롬프트 반응성이 중요한 서비스/워크플로라면 특히 금지
4) 최종 산출물은 두 벌로 보관
model_merged_fp32.safetensorsmodel_merged_fp16.safetensors
그리고 최소 재현 세트로 둘을 비교해, fp16에서 붕괴가 생기면 배포는 fp32 유지 또는 레이어별 정밀도 전략을 고려합니다.
마무리
LoRA 합성 후 품질 붕괴는 “LoRA가 나빠서”가 아니라, 대부분 스케일 누적, 베이스 불일치, TE 합성, 정밀도/파이프라인 조건 변화 같은 공학적인 이유로 발생합니다. 해결의 핵심은 합성을 한 번에 끝내려 하지 말고, 조건을 고정한 최소 재현 세트로 단계적으로 bake하며 임계점을 찾는 것입니다.
원한다면 사용 중인 환경(예: AUTOMATIC1111, ComfyUI, diffusers), 베이스 모델(SD1.5/SDXL), LoRA 개수와 용도(스타일/캐릭터/디테일), 합성 도구와 옵션을 알려주면, 그 조합에 맞춘 “안전한 합성 순서”와 권장 스케일 범위를 더 구체적으로 잡아드릴 수 있습니다.