모델 운영

MLOPS / 79번째 글

LoRA 어댑터 병합 — 여럿을 하나로 합치는 법

학습을 마친 LoRA 어댑터를 가중치에 합칠지 얹은 채 서빙할지, 그리고 어댑터 여럿을 하나로 합칠 때 값이 서로 상쇄되는 문제를 TIES·DARE 같은 방법으로 어떻게 다루는지 정리합니다.

PALDYN Team12 MIN READ

지난 글까지가 학습에 넣을 것을 준비하는 이야기였다면, 이 글은 학습을 마친 뒤의 이야기다. LoRA로 미세조정하면 결과물은 원래 모델이 아니라 어댑터 — 층마다 붙는 작은 행렬 두 개 — 이고, 파일 크기는 수십 MB에서 수백 MB다. 이제 이것을 어떻게 서빙에 태울지 정해야 한다.

선택은 둘이다. 어댑터를 얹은 채로 돌리거나, 베이스 가중치에 미리 더해 하나로 만들거나.

합친다는 것이 정확히 무슨 뜻인가

LoRA는 원래 가중치 WW 를 건드리지 않고 옆에 작은 행렬 AA 와 BB 를 붙여 학습한다. 추론할 때 실제로 쓰이는 값은 이렇다.

W′=W+αrBAW' = W + \frac{\alpha}{r} BA

여기서 rr 은 어댑터의 랭크, α\alpha 는 학습 때 정한 배율이다. 병합(merge)은 이 덧셈을 추론 때마다 하지 않고 배포 전에 한 번 해서 W′W' 을 저장해 두는 것이다. 그러면 결과물은 어댑터가 없는 평범한 모델 파일이 된다.

얹은 채로 서빙할 것인가, 미리 더할 것인가

어댑터 하나를 합치는 것은 수학적으로 정확한 연산이다. 근사도 손실도 없고, 합친 모델과 얹은 모델의 출력은 같다. 그래서 하나만 쓸 것이라면 병합이 대체로 이득이다.

딱 한 가지 예외가 있다. 베이스 모델이 4비트로 양자화되어 있으면 이야기가 달라진다. QLoRA로 학습한 경우가 그렇다. 양자화된 값에 어댑터를 더하고 다시 양자화하면 반올림 오차가 실제로 쌓인다. 그래서 이때는 원본 정밀도의 베이스 모델을 다시 받아 거기에 합치고, 필요하면 그 결과를 새로 양자화한다. 4비트 가중치에 그대로 합치는 것이 성능이 조용히 떨어지는 흔한 경로다.

언제 합치고 언제 얹어 둘 것인가

상황 어떻게 왜
어댑터 하나만 운영한다 합친다 추론이 단순해지고 어떤 엔진에나 올라간다
고객사마다 어댑터가 다르다 얹어 둔다 베이스 하나에 어댑터 수십 개를 함께 서빙할 수 있다
어댑터를 자주 갱신한다 얹어 둔다 배포가 수십 MB 교체로 끝난다
엣지·온디바이스로 내보낸다 합친다 변환 도구가 어댑터 구조를 모르는 경우가 많다
여러 능력을 한 모델에 담고 싶다 합친다 아래에서 다룰 다중 병합이 이 경우다

두 번째 줄이 LoRA를 쓰는 큰 이유 중 하나다. 어댑터를 얹은 채로 두면 같은 베이스 가중치를 공유하면서 요청마다 다른 어댑터를 적용할 수 있다. 고객사 50곳에 맞춘 모델을 각각 통째로 띄우면 GPU가 50대 필요하지만, 베이스 하나에 어댑터 50개면 한 대에서 돌아간다. 이 구성을 지원하는 추론 엔진이 늘면서 얹어 두는 쪽의 실용성이 많이 올라갔다.

여러 개를 합칠 때 생기는 문제

어댑터 하나를 합치는 것은 정확하지만, 둘 이상을 합치는 것은 근사다. 각각 다른 데이터로 학습했으므로 같은 자리를 서로 다른 방향으로 밀고, 그냥 더하면 밀린 양이 뒤섞인다.

가장 단순한 방법은 가중 평균이다. 어댑터 kk 개의 변화량을 계수 λk\lambda_k 로 섞는다.

ΔW=∑kλk⋅αkrkBkAk\Delta W = \sum_k \lambda_k \cdot \frac{\alpha_k}{r_k} B_k A_k

이것으로도 어느 정도는 된다. 다만 어댑터가 셋을 넘어가면 눈에 띄게 무뎌지는데, 원인은 부호가 반대인 값들이 서로를 깎기 때문이다.

셋을 그냥 평균하면 서로 상쇄된다

이 상쇄를 줄이려고 나온 방법들이 있다. 이름은 달라도 발상은 비슷하다 — 중요하지 않은 변화를 버리고, 방향이 어긋나는 것을 정리한 뒤에 평균한다.

방법 무엇을 하나 어울리는 자리
선형 평균 그대로 가중 평균한다 어댑터 두셋, 성격이 비슷할 때
TIES 작은 값을 버리고 부호를 다수결로 통일한 뒤 평균 넷 이상을 합칠 때
DARE 변화량을 무작위로 대부분 버리고 남은 것을 키운다 TIES와 함께 쓰면 더 낫다
SLERP 두 모델 사이를 구면 위에서 보간한다 정확히 둘을 섞을 때
모델 수프 같은 과제의 여러 체크포인트를 평균한다 같은 학습의 변주를 합칠 때

DARE가 직관에 어긋나 보인다. 변화량의 90% 이상을 무작위로 0으로 만들고 남은 것을 그 비율만큼 키우는데, 그렇게 해도 성능이 거의 안 떨어진다. 미세조정으로 생긴 변화량에 중복이 아주 많다는 뜻이고, 버리고 나면 어댑터끼리 겹치는 자리가 줄어 상쇄도 줄어든다. 실무에서는 DARE로 성기게 만든 뒤 TIES로 합치는 조합을 자주 쓴다.

모델 수프는 성격이 다르다. 서로 다른 능력을 합치는 것이 아니라, 같은 과제를 하이퍼파라미터만 바꿔 학습한 체크포인트들을 평균해 하나로 만드는 것이다. 상쇄 문제가 거의 없고 대체로 조금 좋아지므로, 여러 번 돌린 학습이 있다면 값싸게 얻는 이득이다.

실제로 하는 절차

from peft import PeftModel
from transformers import AutoModelForCausalLM

# 양자화하지 않은 원본 정밀도로 베이스를 불러온다
base = AutoModelForCausalLM.from_pretrained(
    "base-model", torch_dtype="bfloat16", device_map="cpu",
)

model = PeftModel.from_pretrained(base, "./adapter-support")
model = model.merge_and_unload()      # 가중치에 더하고 어댑터 구조를 뗀다
model.save_pretrained("./merged")

merge_and_unload가 두 가지를 함께 한다. 이름 그대로 더하고, 어댑터 층을 떼어 평범한 모델로 되돌린다. 여러 개를 섞을 때는 mergekit 같은 도구가 위 표의 방법들을 설정 파일로 받아 준다.

주의할 곳은 코드가 아니라 그 앞뒤에 있다.

  • 어댑터마다 붙은 층이 같은지 확인한다. 하나는 어텐션에만, 다른 하나는 MLP까지 붙였다면 겹치는 자리에서만 섞이고 나머지는 그대로 들어간다. 결과가 예측하기 어려워진다
  • 랭크와 α\alpha 가 다르면 실제 배율이 다르다. 위 수식의 α/r\alpha/r 을 각각 계산해 놓고 봐야 계수를 제대로 잡을 수 있다
  • 토크나이저와 특수 토큰이 같아야 한다. 어댑터를 학습하며 토큰을 추가했다면 그 정보도 같이 옮겨야 한다
  • 베이스 모델의 버전이 정확히 같아야 한다. 같은 이름의 다른 리비전에 합치면 조용히 이상해진다

합친 뒤에 반드시 재는 것

병합은 학습이 아니라서 손실 곡선이 없다. 잘못돼도 아무 오류가 나지 않는다. 그래서 평가가 유일한 확인 수단이다.

  • 각 어댑터의 원래 과제를 따로 잰다. 셋을 합쳤으면 세 과제 점수를 각각 본다. 「평균은 괜찮은데 하나가 무너진」 경우가 흔하고, 총점만 보면 안 보인다
  • 어느 것도 학습하지 않은 과제도 잰다. 병합이 베이스의 일반 능력을 깎지 않았는지 본다
  • 계수를 훑는다. 두 개를 합칠 때 0.3·0.5·0.7을 각각 만들어 재 본다. 병합은 학습보다 훨씬 싸므로 이 탐색을 몇 번 돌리는 것이 정석이다
  • 얹은 채로 돌린 결과와 대조한다. 어댑터 하나를 합쳤을 뿐인데 점수가 달라졌다면 앞의 양자화 문제이거나 베이스 버전이 어긋난 것이다

마지막 항목이 병합 사고를 가장 빨리 잡아낸다. 하나짜리 병합은 출력이 같아야 하므로, 다르면 그 자체가 설정 오류의 증거다.

정리

어댑터 병합은 두 가지 다른 작업이 한 이름으로 불리는 자리다. 섞어 생각하면 헷갈린다.

  • 하나를 합치는 것은 정확한 연산이고 배포를 단순하게 만든다. 원본 정밀도 베이스에 합치는 것만 지키면 된다
  • 여럿을 합치는 것은 근사이고 능력이 서로 깎일 수 있다. 넷 이상이면 선형 평균 대신 TIES·DARE 계열을 쓰고, 합친 뒤 과제별로 따로 재야 한다

그리고 서빙 쪽 선택은 기술이 아니라 운영이 정한다. 요청마다 다른 어댑터를 쓸 일이 있으면 얹어 두고, 하나로 고정할 것이면 합친다. 여기까지가 미세조정을 마친 결과물을 실제 서비스에 올리기까지의 마지막 단계다.


읽어주셔서 감사합니다. 😊

LATEST

모델 운영의 최신 글

모델 운영2026.09.04

KV 캐시 양자화 — 가중치보다 이쪽이 먼저 넘친다

긴 문맥에서 GPU 메모리를 실제로 잡아먹는 것은 가중치가 아니라 KV 캐시입니다. 캐시 크기를 계산하는 법, K와 V를 다르게 다뤄야 하는 이유, 어디까지 줄여도 되는지를 정리합니다.

16 MIN
모델 운영2026.09.04

양자화 보정 — 데이터 128개가 모델 품질을 정한다

양자화에서 스케일을 정하는 절차가 보정입니다. 무엇을 재는지, 데이터를 어디서 몇 개 뽑아야 하는지, 자르는 지점을 어떻게 고르는지, 그리고 잘못된 보정이 어떤 모양으로 드러나는지를 정리합니다.

21 MIN
모델 운영2026.09.03

과제 특화 증류 — 큰 모델의 답을 작은 모델에 옮긴다

범용 성능이 아니라 우리 과제 하나만 잘하는 작은 모델을 만드는 방법입니다. 교사에게 무엇을 받아야 하는지, 데이터를 어떻게 모으고 거르는지, 손익 분기가 어디인지를 정리합니다.

15 MIN