모델 운영

MLOPS / 60번째 글

어댑터 열 개를 서버 한 대에 태우기

고객사마다 LoRA 어댑터를 따로 만들면 서버도 그만큼 늘어납니다. 베이스 가중치 한 벌에 어댑터 여럿을 얹어 한 배치에서 함께 처리하는 방법과 그 대가를 정리합니다.

PALDYN Team11 MIN READ

지난 글까지는 모델 하나가 너무 커서 카드 여러 장에 나눠 싣는 이야기였다. 실무에서는 정반대 상황도 자주 온다. 모델은 카드 한 장에 들어갈 만큼 작은데, 종류가 많다. 고객사마다 말투를 맞춘 어댑터, 문서 종류마다 다른 추출기, 언어마다 다른 요약기. 스무 개쯤 되면 카드를 스무 장 살 것인가 하는 질문이 나온다.

어댑터마다 서버를 띄우면 무엇을 복사하는가

LoRA는 원래 가중치를 얼려 두고 옆에 작은 행렬 두 개를 붙여 그것만 학습하는 방법이다(LoRA 글에 자세히 적어 두었다). 학습이 끝나면 결과물은 그 작은 행렬 쌍뿐이고, 베이스 모델은 처음 받은 그대로다.

크기를 실제로 세어 보면 격차가 분명하다. 층마다 붙이는 자리가 넷(q,k,v,oq, k, v, o)이고 어댑터 하나의 파라미터 수는

2×r×d×L×s2 \times r \times d \times L \times s

랭크 r=16r = 16, 은닉 차원 d=4096d = 4096, 층 수 L=32L = 32, 붙인 자리 s=4s = 4를 넣으면 약 1,680만 개다. fp16이면 34MB. 같은 조건의 8B 베이스는 16GB이니 어댑터는 베이스의 0.21%다.

그런데 어댑터마다 서버 프로세스를 하나씩 띄우면 그 0.21%를 위해 나머지 99.79%를 통째로 복사한다.

어댑터마다 서버를 띄우면 베이스를 그만큼 복사한다

낭비는 메모리에서 끝나지 않는다. 어댑터 셋의 트래픽이 고를 리 없으므로 하나가 붐빌 때 나머지 두 장은 논다. 카드를 나눠 가진 탓에 서로의 여유를 빌려 쓰지 못한다.

한 서버가 여럿을 드는 방식

발상은 단순하다. 베이스 가중치는 한 벌만 GPU에 올리고, 어댑터는 작으니 여러 개를 함께 올려 둔다. 요청이 들어오면 어느 어댑터를 쓸지 요청 파라미터로 지정한다.

여기서 한 가지를 포기해야 한다. LoRA를 배포할 때 흔히 쓰는 방법이 어댑터를 베이스에 미리 더해 하나의 가중치로 합치는 것인데(병합, merge), 합치는 순간 그 가중치는 특정 어댑터 전용이 된다. 여러 어댑터를 함께 쓰려면 합치지 말고 계산 때마다 따로 더해야 한다.

h=Wx+αrBAxh = W x + \frac{\alpha}{r} B A x

앞항이 모두가 공유하는 베이스 계산이고, 뒷항이 요청마다 달라지는 어댑터 계산이다. 병합을 포기한 대가로 곱셈이 한 번 늘었지만, 대신 한 배치 안에 서로 다른 어댑터를 쓰는 요청이 섞여도 된다.

한 배치에 어댑터가 섞여도 베이스 곱은 한 번이다

핵심은 무거운 쪽이 갈래를 타지 않는다는 것이다. 베이스 행렬곱은 배치 전체를 묶어 한 번에 돌고, 갈래를 타는 것은 폭이 rr밖에 안 되는 작은 곱뿐이다. 같은 어댑터를 부른 요청끼리 묶어 어댑터 수만큼만 작은 곱을 돌리는 이 방식을 그룹 행렬곱이라 부른다 — 요청 하나하나 따로 도는 것보다 훨씬 빠르다.

vLLM의 --enable-lora, SGLang, TGI 모두 이 구조를 쓴다. 연속 배칭과도 잘 맞는다. 걸음마다 배치를 다시 짜는 엔진이니, 그 배치에 어느 어댑터가 섞였는지도 걸음마다 다시 보면 될 뿐이다.

대가는 어디서 치르는가

공짜는 아니다. 어댑터를 함께 태우면 세 가지가 나빠진다.

무엇이 얼마나 왜
걸음마다의 계산 5~15% 느려짐 어댑터 곱과 덧셈이 층마다 추가된다
커널 효율 어댑터 종류가 많을수록 나빠짐 그룹이 잘게 쪼개져 작은 곱이 여러 번 돈다
GPU 메모리 어댑터 수 × 34MB KV 캐시가 쓸 자리를 그만큼 먹는다

세 번째가 조용히 아프다. 어댑터 서른 개면 1GB인데, 그 1GB는 KV 캐시에서 빼 온 것이다. 동시에 담을 수 있는 요청 수가 줄어 처리량이 떨어지는데, 원인이 어댑터라는 것이 지표에 드러나지 않는다.

두 번째도 짚어 둘 만하다. 배치 64개에 어댑터가 둘 섞였으면 32개씩 두 그룹이라 곱이 넉넉히 크지만, 어댑터가 서른둘이면 두 개씩 서른두 그룹이 된다. 같은 64개 요청인데 커널이 서른두 번 도는 셈이라 GPU가 제대로 안 찬다. 어댑터 수보다 어댑터당 동시 요청 수가 성능을 정한다.

어댑터를 다 올려 둘 것인가

수백 개가 되면 다 올려 둘 수 없다. 그때는 필요할 때 올리는 방식으로 간다.

  • 상주: 자주 쓰는 어댑터는 GPU에 붙박이로 둔다
  • 스왑: 나머지는 CPU 메모리나 디스크에 두고 요청이 오면 올린다

스왑의 비용은 생각보다 작다. 34MB를 PCIe로 올리는 데 몇 ms면 되고, 디스크에서 읽어도 수십 ms다. 카드 한 장을 새로 띄우는 데 몇 분이 걸리던 것과 견주면 다른 세계다.

다만 첫 요청은 그 시간을 그대로 맞는다. 하루에 한 번 쓰는 어댑터라면 그 한 번이 매번 느리다는 뜻이다. 대응은 캐시와 같다 — 최근 쓴 것을 GPU에 남기고(LRU), 트래픽 패턴을 아는 어댑터는 미리 올려 둔다.

랭크가 섞이면

어댑터를 여럿 태울 때 자주 걸리는 실무 제약이 있다. 엔진은 보통 가장 큰 랭크에 맞춰 자리를 잡는다. 랭크 8짜리 스무 개에 랭크 64짜리 하나를 섞으면 스물한 개 전부가 64인 것처럼 메모리를 쓰는 구현이 흔하다.

그래서 어댑터를 만들 때부터 랭크를 맞춰 두는 편이 낫다. 굳이 큰 랭크가 필요한 어댑터가 하나뿐이라면, 그것만 따로 서버를 두는 쪽이 전체로는 싸다.

언제 이 방식이 안 맞나

한 서버에 몰아 태우는 것이 늘 답은 아니다.

  • 베이스가 다르면 아예 안 된다. 8B용 어댑터와 70B용 어댑터는 같은 서버에 못 올린다. 같은 크기라도 베이스 버전이 다르면 다른 모델이다
  • 어댑터 하나가 카드 한 장을 꽉 채울 만큼 붐비면 함께 태울 이유가 없다. 격리도 잃고 계산 대가만 남는다
  • 고객사끼리 격리 요건이 있으면 같은 프로세스에 태우는 것 자체가 문제가 된다. 하드웨어로 칸을 나누는 방법은 카드를 나눠 쓰는 방식 쪽에 적어 두었다

바꿔 말하면 이 방식이 빛나는 자리는 어댑터는 많은데 각각의 트래픽은 적을 때다. 꼬리가 긴 분포일수록 이득이 크다. 어댑터 스무 개 중 열여덟 개가 하루에 몇 백 건씩만 부른다면, 그 열여덟 개를 위해 카드를 열여덟 장 둘 이유가 없다.

정리

상황 고를 것
어댑터 하나, 트래픽 많음 병합해서 배포 — 계산 대가가 0이다
어댑터 여럿, 각각 적음 한 서버에 함께, 병합하지 않음
어댑터 수백 개 함께 + 스왑, 자주 쓰는 것만 상주
격리가 요건 나누되 카드 단위가 아닌 다른 방법을 본다

베이스가 같다는 사실 하나가 카드 열아홉 장을 아낀다. 반대로 병합을 습관처럼 해 두면 그 사실을 쓸 기회가 사라진다 — 어댑터를 만들 때부터 합치지 않은 형태로 보관해 두는 것이 이 선택지를 여는 조건이다.


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

LATEST

모델 운영의 최신 글

모델 운영2026.09.04

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

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

16 MIN
모델 운영2026.09.04

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

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

21 MIN
모델 운영2026.09.03

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

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

15 MIN