LoRA는 체크포인트 위에 덧대는 패치다
ComfyUI 공식 문서는 LoRA(Low-Rank Adaptation)를 Stable Diffusion 같은 큰 생성 모델을 효율적으로 미세 조정하는 기법이라고 소개한다. 예제 페이지는 조금 더 구체적으로, LoRA가 메인 MODEL과 CLIP 모델 위에 덧대는 패치라고 적었다.
«덧대는 패치»라는 말의 뜻은 LoRA 원논문에서 나온다. 2021년에 공개된 이 논문은 미리 학습된 모델의 가중치는 얼려 둔 채, 각 층에 학습 가능한 낮은 랭크의 분해 행렬을 끼워 넣는 방법을 제안했다. 논문은 GPT-3 규모 모델에서 이 방식이 학습할 파라미터 수를 10,000분의 1로, GPU 메모리 요구량을 3분의 1로 줄이면서도 추론 지연을 더하지 않았다고 보고했다. 원논문은 언어 모델을 대상으로 했지만, 같은 생각이 그림 모델의 미세 조정에도 널리 쓰이게 됐다.
그래서 LoRA 파일은 체크포인트를 통째로 대신하지 않는다. 체크포인트가 가진 가중치에 «이만큼 더하라»는 차이 정보를 담고 있을 뿐이다. 이 점을 기억해 두면 아래의 강도 칸, 겹치기, 경고 메시지가 모두 같은 원리로 읽힌다.
노드 이름부터 — Load LoRA가 두 개인 이유
ComfyUI 소스 코드 nodes.py에는 LoRA를 불러오는 노드가 두 개 있다.
| 내부 이름 | 화면에 보이는 이름 | 입력 | 출력 |
|---|---|---|---|
| LoraLoader | Load LoRA (Model and CLIP) | model · clip · lora_name · strength_model · strength_clip | MODEL · CLIP |
| LoraLoaderModelOnly | Load LoRA | model · lora_name · strength_model | MODEL |
표의 이름과 칸은 소스 코드의 정의와 표시 이름 목록에서 옮겼다. 공식 튜토리얼은 강도 두 칸이 있는 노드를 «Load LoRA»라고 부르지만, 지금 소스 코드의 표시 이름으로는 두 칸짜리가 «Load LoRA (Model and CLIP)», 모델 한 칸짜리가 «Load LoRA»다. 노드 검색 창에 lora만 쳐도 둘 다 나오므로, 강도 칸이 몇 개인지를 보고 고르면 헷갈리지 않는다.
모델 한 칸짜리 노드는 내부에서 두 칸짜리 노드의 기능을 그대로 부르되, CLIP 자리에 아무것도 넘기지 않고 CLIP 강도를 0으로 둔다. 텍스트 인코더를 따로 건드리지 않는 모델이나, 모델 쪽에만 학습된 LoRA를 쓸 때 선을 하나 덜 이을 수 있는 노드다.
예제 페이지는 LyCORIS, LoHa, LoKr, LoCon 같은 여러 LoRA 변형도 같은 방식으로 쓴다고 적었다. 파일 형식에 따라 별도 노드를 찾을 필요는 없다.
선 잇기 — MODEL과 CLIP을 함께 통과시킨다
공식 튜토리얼의 예제는 체크포인트 dreamshaper_8.safetensors를 ComfyUI/models/checkpoints에, LoRA blindbox_V1Mix.safetensors를 ComfyUI/models/loras에 넣고 시작한다. 예제 페이지도 LoRA 파일은 models/loras 폴더에 둔다고 안내한다. 노드의 lora_name 칸은 바로 이 loras 폴더의 파일 목록에서 고른다.
선은 이렇게 잇는다. 기본 txt2img 워크플로에서 Load Checkpoint의 MODEL과 CLIP 출력을 곧바로 KSampler와 CLIP Text Encode로 보내던 것을, 중간에 LoRA 노드를 거치게 바꾸는 것이다.
- Load Checkpoint의 MODEL과 CLIP을 Load LoRA (Model and CLIP)의 model · clip 입력에 잇는다.
- LoRA 노드의 MODEL 출력을 KSampler의 model 입력에 잇는다.
- LoRA 노드의 CLIP 출력을 긍정 · 부정 CLIP Text Encode 두 개의 clip 입력에 모두 잇는다.
- VAE는 LoRA 노드를 거치지 않고 Load Checkpoint에서 그대로 VAE Decode로 간다. 노드 출력이 MODEL과 CLIP 두 개뿐이기 때문이다.
- Queue 버튼이나 Ctrl(맥은 Cmd) + Enter로 실행한다.
놓치기 쉬운 곳은 3번이다. 3번에서 CLIP Text Encode 하나만 LoRA 쪽 CLIP에 잇고, 다른 하나는 Load Checkpoint의 CLIP에 그대로 두는 것이다. 이렇게 되면 긍정 프롬프트와 부정 프롬프트가 서로 다른 텍스트 인코더로 해석된다. 의도한 것이 아니라면 두 개 모두 LoRA 노드의 CLIP 출력에 잇는다.
강도 두 칸이 각각 바꾸는 곳
nodes.py에서 strength_model과 strength_clip은 모두 기본 1.0, 범위 -100.0부터 100.0까지, 0.01 단위로 고르는 값이다. 두 칸의 설명 문구는 각각 «확산 모델을 얼마나 강하게 바꿀지», «CLIP 모델을 얼마나 강하게 바꿀지»이며, 둘 다 «음수도 될 수 있다»는 말이 붙어 있다.
공식 튜토리얼의 설명도 같은 방향이다. strength_model은 LoRA가 모델 가중치에 미치는 영향의 크기로, 값이 클수록 LoRA의 스타일이 강해진다. strength_clip은 LoRA가 CLIP 텍스트 임베딩에 미치는 영향의 크기다.
두 칸을 나눠 생각하면 이렇다.
- strength_model — 그림을 실제로 그리는 확산 모델(노이즈를 걷어 내는 쪽)의 가중치를 얼마나 바꿀지. 화풍 · 질감 · 형태처럼 그림에 직접 드러나는 쪽이다.
- strength_clip — 프롬프트 글자를 숫자로 바꾸는 텍스트 인코더를 얼마나 바꿀지. LoRA를 학습할 때 텍스트 인코더도 함께 학습했다면, 트리거 워드 같은 단어를 어떻게 해석할지가 여기서 달라진다.
모든 LoRA 파일이 두 쪽을 다 담고 있지는 않다. 모델 쪽만 학습된 파일이라면 strength_clip을 어떻게 바꿔도 결과가 달라지지 않을 수 있다. 이 경우 위에서 본 모델 한 칸짜리 노드를 써도 된다.
소스 코드로 본 강도 — 더하는 양에 곱하는 수
강도 숫자가 실제로 어디에 곱해지는지는 세 파일을 차례로 따라가면 보인다.
첫째, nodes.py의 LoraLoader는 loras 폴더에서 파일을 읽은 뒤 comfy.sd.load_lora_for_models에 모델 · CLIP · LoRA 내용 · 두 강도를 그대로 넘긴다.
둘째, comfy/sd.py의 load_lora_for_models는 체크포인트의 확산 모델과 CLIP에서 «LoRA 키 이름 → 실제 가중치 이름» 지도를 만들고, 모델과 CLIP을 각각 복제한 다음, 복제본에 LoRA 패치를 붙이면서 strength_model과 strength_clip을 따로 넘긴다. 원래 체크포인트를 고치는 것이 아니라 복제본에 패치를 붙이는 구조라, 같은 Load Checkpoint 출력을 LoRA를 거치지 않는 다른 갈래에 이어도 그쪽은 원래 모델 그대로다.
셋째, 패치가 실제로 계산되는 곳은 comfy/weight_adapter/lora.py다. 이 파일은 LoRA의 두 행렬(up · down)을 곱해 원래 가중치와 같은 모양의 차이를 만든다. 그리고 일반적인 경우 원래 가중치에 «강도 × alpha × 차이»를 더한다.
alpha와 rank
위 식의 alpha는 그대로 쓰지 않는다. comfy/lora.py는 LoRA 파일 안에 «키 이름.alpha» 값이 있으면 그것을 읽어 둔다. weight_adapter/lora.py는 이 값을 down 행렬의 첫 번째 크기, 곧 rank로 나눈 값을 alpha로 쓰고, 파일에 alpha가 없으면 1.0으로 둔다. 정리하면 실제로 더해지는 양은 «강도 × (alpha ÷ rank) × 두 행렬의 곱»이다.
이 계산에서 알 수 있는 것이 있다. 강도 1.0은 «LoRA를 만든 사람이 학습할 때의 비율 그대로»라는 뜻이지, 모든 LoRA에서 같은 세기라는 뜻이 아니다. alpha와 rank를 어떻게 잡고 학습했는지에 따라 같은 1.0이라도 그림이 바뀌는 정도가 다르다. 모델 카드에 권장 강도가 적혀 있다면 그 숫자가 출발점으로 가장 믿을 만한 이유다.
같은 파일에는 DoRA라는 변형을 위한 분기도 있다. 파일에 dora_scale 값이 있으면 단순히 더하는 대신 weight_decompose라는 별도 함수로 가중치를 다시 계산한다. 아래의 «여러 개를 겹치면 더해진다»는 설명은 일반 LoRA를 기준으로 한 것이다.
여러 개 겹치기 — 사슬로 잇고 강도를 나눠 갖는다
LoRA를 두 개 이상 쓰는 방법은 하나다. 노드를 사슬로 잇는다. 예제 페이지는 LoraLoader 노드를 여러 개 이어 여러 LoRA를 적용할 수 있다고 적었고, 노드 자체의 설명 문구에도 «여러 LoRA 노드를 서로 이을 수 있다»는 말이 들어 있다.
공식 Multiple LoRAs 튜토리얼은 체크포인트 dreamshaper_8.safetensors 뒤에 첫 번째 Load LoRA로 blindbox_V1Mix.safetensors, 두 번째 Load LoRA로 MoXinV1.safetensors를 잇는 예를 든다. 튜토리얼은 두 노드의 strength_model 값을 각각 조절해 LoRA마다 영향을 정하라고 안내하고, 결과는 두 스타일이 섞인 그림이 된다고 설명한다.
사슬로 이을 때 선은 이렇게 간다.
- Load Checkpoint → LoRA 1의 model · clip
- LoRA 1의 MODEL · CLIP 출력 → LoRA 2의 model · clip 입력
- LoRA 2의 MODEL → KSampler, LoRA 2의 CLIP → CLIP Text Encode 두 개
마지막 LoRA 노드의 출력만 샘플러와 텍스트 인코더로 간다. 중간 노드의 출력을 샘플러에 이으면 그 뒤의 LoRA는 적용되지 않는다.
순서는 결과를 바꾸나
소스 코드를 기준으로 보면, 일반 LoRA의 패치는 원래 가중치에 «강도 × alpha × 차이»를 더하는 방식이다. 노드를 사슬로 이으면 앞 노드가 붙인 패치 위에 뒤 노드의 패치가 다시 더해진다. 더하기는 순서를 바꿔도 합이 같으므로, 일반 LoRA끼리라면 노드 순서가 결과를 크게 바꾸지 않는 구조다. 반대로 위의 DoRA처럼 가중치 자체를 다시 계산하는 변형이 섞이면 이 설명이 그대로 맞지 않을 수 있다.
순서보다 중요한 것은 합쳐진 세기다. 두 LoRA가 같은 층을 비슷한 방향으로 바꾼다면, 각각 1.0으로 두는 것은 한 방향으로 두 번 미는 것과 비슷하다. 그래서 여러 개를 겹칠 때는 하나씩 강도를 낮춰 나눠 갖게 하는 것이 무난한 출발점이다. 정확한 값은 LoRA마다 alpha · rank · 학습 방식이 달라 공식 문서가 정해 두지 않으므로 실험으로 정한다.
강도 0과 음수
두 칸을 모두 0으로 두면 LoraLoader는 파일을 읽지도 않고 받은 모델과 CLIP을 그대로 돌려준다. 사슬 중간의 LoRA 하나를 잠시 빼고 비교하고 싶을 때, 선을 끊지 않고 두 강도만 0으로 두면 된다는 뜻이다. 한 칸만 0이면 파일은 읽고, 0이 아닌 쪽에만 패치가 붙는다.
강도는 음수도 된다. 범위가 -100.0부터이고 설명 문구에도 음수가 될 수 있다고 적혀 있다. 위 식에서 강도가 음수이면 차이를 더하는 대신 빼는 셈이다. LoRA가 학습한 방향의 반대로 미는 것이다. 다만 반대 방향이 늘 «깔끔한 제거»를 뜻하지는 않으므로, 모델 카드에 음수 사용 안내가 없다면 기대한 결과가 나오지 않을 수 있다.
같은 파일의 강도만 바꿔 다시 돌릴 때 LoraLoader는 마지막으로 읽은 파일의 경로와 내용을 기억해 두었다가, 같은 파일이면 다시 읽지 않고 그대로 쓴다. 강도 실험을 여러 번 돌려도 파일을 매번 디스크에서 새로 읽지 않는 이유다.
안 먹힐 때 — 명령 창의 NOT LOADED 줄
LoRA를 끼웠는데 그림이 전혀 달라지지 않는다면 ComfyUI를 띄운 명령 창부터 본다. 소스 코드에는 두 종류의 경고 줄이 있다.
comfy/lora.py는 LoRA 파일 안의 키 가운데 체크포인트의 키 지도에 맞지 않아 읽지 못한 것을lora key not loaded:로 시작하는 줄로 남긴다.comfy/sd.py는 읽은 패치 가운데 모델에도 CLIP에도 붙지 못한 것을NOT LOADED로 시작하는 줄로 남긴다.
이 줄이 수십, 수백 개 쏟아진다면 원인은 대개 하나다. LoRA를 학습한 기반 모델과 지금 불러온 체크포인트의 계열이 다르다. 키 지도는 지금 체크포인트의 구조에서 만들어지므로, 다른 구조를 기준으로 만든 LoRA의 키는 짝을 찾지 못한다. 이 경우 강도를 올려도 소용이 없고, 모델 카드의 기반 모델 칸을 확인해 같은 계열의 체크포인트로 바꾸는 것이 해결책이다. 계열을 확인하는 법은 체크포인트와 LoRA 고르기에서 다뤘다.
경고가 없는데도 차이가 작다면 다음을 차례로 본다.
- KSampler가 마지막 LoRA 노드의 MODEL을 받고 있는가. 중간 노드나 Load Checkpoint에서 바로 받고 있으면 LoRA가 빠진다.
- CLIP Text Encode 두 개가 LoRA 노드의 CLIP을 받고 있는가. 트리거 워드가 필요한 LoRA라면 이 선이 특히 중요하다.
- 트리거 워드를 프롬프트에 넣었는가. 모델 카드에 트리거 워드가 적혀 있다면 CLIP Text Encode에 직접 적는다.
- 강도가 너무 낮지 않은가. alpha ÷ rank가 작은 파일은 1.0에서도 변화가 작을 수 있다.
강도 실험은 한 칸씩
공식 문서와 소스 코드는 «정답 강도»를 정해 두지 않는다. 문서가 말하는 것은 값이 클수록 LoRA의 스타일이 강해진다는 방향뿐이다. 그래서 값은 실험으로 정하되, 실험의 틀을 정해 두면 빨리 끝난다.
- 모델 카드의 권장 강도에서 시작한다. 위의 alpha ÷ rank 이야기 때문에, 제작자가 적은 숫자가 가장 믿을 만한 출발점이다.
- 시드 · steps · 프롬프트를 고정한다. 강도만 움직여야 차이가 강도 때문인지 가려진다.
- strength_model부터 움직인다. strength_clip은 일단 같은 값에 묶어 두고, 그림이 원하는 쪽으로 가는지 본다.
- 그다음 strength_clip을 따로 움직인다. 트리거 워드가 너무 세게 먹거나 프롬프트의 다른 단어가 묻힌다면 이 칸을 낮춰 본다.
- 여러 개라면 하나씩 더한다. 첫 LoRA의 값을 정한 뒤 두 번째를 붙이고, 겹친 결과가 과하면 두 강도를 함께 낮춘다. 비교할 때는 위의 «두 칸 모두 0» 방법으로 하나를 잠시 꺼 둔다.
실험한 강도 값은 결과 그림과 함께 메모해 둔다. 체크포인트 이름, LoRA 파일 이름, 두 강도, 시드를 한 줄로 남겨 두면 나중에 같은 자리에서 다시 시작할 수 있다.
LoRA도 체크포인트처럼 제작자가 정한 이용 조건이 있다. 결과 그림을 어디에 쓸지에 따라 LoRA와 기반 체크포인트 양쪽의 라이선스를 함께 확인한다. 특정 인물을 흉내 내도록 학습된 LoRA는 초상권 문제가 생길 수 있다.
자주 묻는 것
strength_model과 strength_clip은 같은 값으로 두어야 하나요?
꼭 그럴 필요는 없습니다. 두 칸은 각각 확산 모델과 CLIP 텍스트 인코더에 따로 들어갑니다. 처음에는 같은 값으로 묶어 두고 strength_model로 전체 세기를 정한 뒤, 트리거 워드가 과하거나 약할 때 strength_clip만 따로 조절하는 순서가 편합니다.
Load LoRA 노드가 두 개 보여요. 무엇을 써야 하나요?
강도 칸이 두 개인 «Load LoRA (Model and CLIP)»는 모델과 CLIP을 함께 바꾸고, 칸이 하나인 «Load LoRA»는 모델만 바꿉니다. 일반적인 Stable Diffusion LoRA라면 두 칸짜리를 쓰고, 모델 쪽만 학습된 LoRA나 CLIP 선을 따로 다루는 워크플로에서는 한 칸짜리를 씁니다.
LoRA를 여러 개 쓸 때 순서가 중요한가요?
일반 LoRA는 원래 가중치에 차이를 더하는 방식이라 순서를 바꿔도 결과가 크게 달라지지 않는 구조입니다. 순서보다는 강도의 합이 중요하므로, 여러 개를 겹칠 때는 각각의 강도를 낮춰 나눠 갖게 합니다.
LoRA를 넣었는데 그림이 그대로예요.
ComfyUI 명령 창에 lora key not loaded나 NOT LOADED 경고가 많이 나오는지 먼저 봅니다. 많다면 LoRA의 기반 모델과 체크포인트 계열이 다른 것이므로 같은 계열의 체크포인트로 바꿉니다. 경고가 없다면 KSampler와 CLIP Text Encode가 마지막 LoRA 노드의 출력을 받고 있는지 확인합니다.
강도를 음수로 넣어도 되나요?
소스 코드상 범위가 -100부터라 넣을 수는 있고, 이때는 LoRA가 학습한 차이를 더하는 대신 빼게 됩니다. 결과가 깔끔한 반대가 된다는 보장은 없으므로 모델 카드에 음수 사용 안내가 있는 LoRA에서 쓰는 것이 안전합니다.
참고 자료
- ComfyUI 공식 문서. LoRA 튜토리얼 — LoRA 정의, 예제 파일 dreamshaper_8 · blindbox_V1Mix와 폴더, strength_model · strength_clip 설명, 실행 단축키, 사슬 연결 안내. docs.comfy.org
- ComfyUI 공식 문서. Multiple LoRAs 튜토리얼 — Load LoRA 두 개를 잇는 순서, 두 번째 예제 파일 MoXinV1, 노드마다 strength_model로 영향 조절, 섞인 스타일. docs.comfy.org
- ComfyUI Examples. Lora Examples — 메인 MODEL과 CLIP 위에 덧대는 패치라는 설명, models/loras 폴더, LoraLoader 사슬 연결, LyCORIS · LoHa · LoKr · LoCon도 같은 방법. comfyanonymous.github.io
- ComfyUI 소스 코드 nodes.py — LoraLoader · LoraLoaderModelOnly 입력 칸 기본값 · 범위 · 단위 · 설명 문구, 두 강도가 0이면 그대로 돌려주는 코드, 마지막 파일 캐시, 노드 표시 이름. github.com
- ComfyUI 소스 코드 comfy/sd.py — load_lora_for_models — 키 지도 만들기, model · clip 복제, add_patches에 강도 전달, NOT LOADED 경고. github.com
- ComfyUI 소스 코드 comfy/lora.py — alpha 값 읽기, lora key not loaded 경고. github.com
- ComfyUI 소스 코드 comfy/weight_adapter/lora.py — alpha ÷ rank 계산, alpha가 없으면 1.0, 두 행렬 곱, 강도 × alpha × 차이를 원래 가중치에 더하는 줄, DoRA 분기. github.com
- Hu EJ, et al. LoRA: Low-Rank Adaptation of Large Language Models(arXiv 2106.09685) 초록 · Hugging Face Papers — 원래 가중치를 얼리고 낮은 랭크 분해 행렬을 끼우는 방식, GPT-3 기준 학습 파라미터 10,000분의 1 · GPU 메모리 3분의 1, 추론 지연 없음. huggingface.co