지난 글까지는 모델 하나가 너무 커서 카드 여러 장에 나눠 싣는 이야기였다. 실무에서는 정반대 상황도 자주 온다. 모델은 카드 한 장에 들어갈 만큼 작은데, 종류가 많다. 고객사마다 말투를 맞춘 어댑터, 문서 종류마다 다른 추출기, 언어마다 다른 요약기. 스무 개쯤 되면 카드를 스무 장 살 것인가 하는 질문이 나온다.
어댑터마다 서버를 띄우면 무엇을 복사하는가
LoRA는 원래 가중치를 얼려 두고 옆에 작은 행렬 두 개를 붙여 그것만 학습하는 방법이다(LoRA 글에 자세히 적어 두었다). 학습이 끝나면 결과물은 그 작은 행렬 쌍뿐이고, 베이스 모델은 처음 받은 그대로다.
크기를 실제로 세어 보면 격차가 분명하다. 층마다 붙이는 자리가 넷()이고 어댑터 하나의 파라미터 수는
랭크 , 은닉 차원 , 층 수 , 붙인 자리 를 넣으면 약 1,680만 개다. fp16이면 34MB. 같은 조건의 8B 베이스는 16GB이니 어댑터는 베이스의 0.21%다.
그런데 어댑터마다 서버 프로세스를 하나씩 띄우면 그 0.21%를 위해 나머지 99.79%를 통째로 복사한다.
낭비는 메모리에서 끝나지 않는다. 어댑터 셋의 트래픽이 고를 리 없으므로 하나가 붐빌 때 나머지 두 장은 논다. 카드를 나눠 가진 탓에 서로의 여유를 빌려 쓰지 못한다.
한 서버가 여럿을 드는 방식
발상은 단순하다. 베이스 가중치는 한 벌만 GPU에 올리고, 어댑터는 작으니 여러 개를 함께 올려 둔다. 요청이 들어오면 어느 어댑터를 쓸지 요청 파라미터로 지정한다.
여기서 한 가지를 포기해야 한다. LoRA를 배포할 때 흔히 쓰는 방법이 어댑터를 베이스에 미리 더해 하나의 가중치로 합치는 것인데(병합, merge), 합치는 순간 그 가중치는 특정 어댑터 전용이 된다. 여러 어댑터를 함께 쓰려면 합치지 말고 계산 때마다 따로 더해야 한다.
앞항이 모두가 공유하는 베이스 계산이고, 뒷항이 요청마다 달라지는 어댑터 계산이다. 병합을 포기한 대가로 곱셈이 한 번 늘었지만, 대신 한 배치 안에 서로 다른 어댑터를 쓰는 요청이 섞여도 된다.
핵심은 무거운 쪽이 갈래를 타지 않는다는 것이다. 베이스 행렬곱은 배치 전체를 묶어 한 번에 돌고, 갈래를 타는 것은 폭이 밖에 안 되는 작은 곱뿐이다. 같은 어댑터를 부른 요청끼리 묶어 어댑터 수만큼만 작은 곱을 돌리는 이 방식을 그룹 행렬곱이라 부른다 — 요청 하나하나 따로 도는 것보다 훨씬 빠르다.
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이다 |
| 어댑터 여럿, 각각 적음 | 한 서버에 함께, 병합하지 않음 |
| 어댑터 수백 개 | 함께 + 스왑, 자주 쓰는 것만 상주 |
| 격리가 요건 | 나누되 카드 단위가 아닌 다른 방법을 본다 |
베이스가 같다는 사실 하나가 카드 열아홉 장을 아낀다. 반대로 병합을 습관처럼 해 두면 그 사실을 쓸 기회가 사라진다 — 어댑터를 만들 때부터 합치지 않은 형태로 보관해 두는 것이 이 선택지를 여는 조건이다.
읽어주셔서 감사합니다. 😊

