상태: 초안 (2026-10-07), 실측 글입니다. 어댑터는 성능 시험용으로 만든 무작위 가중치라서 출력 품질은 의미가 없고, 속도와 자원 거동만 측정했습니다. 계산한 것은 「계산」, 못 잰 것은 「미실측」으로 표시했습니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」으로 표시합니다.
1. 왜 LoRA 를 서빙하는가
모델을 용도마다 미세 조정하면 그만큼 모델 전체를 메모리에 올려야 합니다. LoRA 는 기본 가중치 W 를 그대로 두고 저차원 행렬 두 개(B, A)를 학습해 W + (α/r)·B·A 로 바꾸는 방식이라, 기본 모델 하나에 작은 어댑터 여러 개를 붙여 요청마다 다른 어댑터를 쓸 수 있습니다. 고객별·업무별 모델을 GPU 한 장에서 서비스하려는 이유입니다. vLLM 문서는 어댑터를 요청 단위로 적은 오버헤드로 서빙할 수 있다고 설명합니다. 이 글은 그 「적은」이 이 환경에서 몇 퍼센트인지를 재 봅니다.
2. 환경과 방법
| 항목 | 값 |
|---|---|
| GPU | 8GB급 노트북 GPU 한 장, 다른 작업 없음 |
| 서버 | vLLM 0.31.0, Qwen3-1.7B(bf16), --max-model-len 4096, --max-num-seqs 64, --max-lora-rank 16 |
| 어댑터 | PEFT 로 만든 LoRA 3개(a, b, c). 랭크 8, 대상은 어텐션의 q, k, v, o 투영, lora_B 를 시드가 다른 무작위 값(표준편차 0.02)으로 초기화 |
| 어댑터 크기 | 학습 가능 파라미터 3,211,264개로 기본 모델 1,720,574,976개의 0.19%(측정). bf16 이면 약 6.4MB(계산) |
| 요청 | 입력 Write a long story about a robot., 출력 128토큰, 온도 0, ignore_eos, 요청마다 서버의 모델 이름으로 어댑터를 선택 |
| 반복 | 같은 요청 묶음을 한꺼번에 보내고 총 처리량(출력 토큰 수 ÷ 걸린 시간)을 계산 |
동시 요청 수에 주의하십시오. 이 글의 표는 「한꺼번에 보낸 요청 수」를 적습니다(3개, 18개, 48개). 시험 도구의 인자 이름 때문에 처음에는 「동시 1, 16」이라고 부르다가 실제로는 3배의 요청을 보내고 있음을 확인해 바로잡았습니다.
어댑터가 무작위라서 출력은 의미가 없습니다. 어댑터 행렬의 크기와 연산량은 학습 여부와 무관하므로 속도와 메모리 거동은 학습된 어댑터와 같다고 보지만, 학습된 어댑터에서 확인하지는 않았습니다(미실측).
3. 처리량의 비용
기본 서버(LoRA 끔)와 LoRA 를 켠 서버에서 같은 요청을 보냈습니다.
| 한꺼번에 보낸 요청 수 | 서버 | 요청이 쓰는 모델 | 총 처리량 | 기본 대비 | ITL 중앙 |
|---|---|---|---|---|---|
| 3 | LoRA 끔 | 기본 | 199 tok/s | 기준 | 15ms |
| 3 | LoRA 켬 | 기본(어댑터 없음) | 200 tok/s | +0.5% | 15ms |
| 3 | LoRA 켬 | 어댑터 a | 182 tok/s | −8.5% | 16ms |
| 48 | LoRA 끔 | 기본 | 2,667 tok/s | 기준 | 17ms |
| 48 | LoRA 켬 | 기본(어댑터 없음) | 2,664 tok/s | −0.1% | 17ms |
| 48 | LoRA 켬 | 어댑터 a | 2,190 tok/s | −17.9% | 19ms |
| 48 | LoRA 켬 | a 와 b 를 번갈아 | 2,323 tok/s | −12.9% | 20ms |
| 48 | LoRA 켬 | a, b, 어댑터 없음을 번갈아 | 2,346 tok/s | −12.0% | 20ms |
- LoRA 기능을 켜 두는 것 자체는 공짜였습니다. 어댑터를 쓰지 않는 요청의 처리량은 LoRA 를 끈 서버와 같았습니다(200 대 199, 2,664 대 2,667).
- 어댑터를 쓰는 요청은 처리량이 줄었습니다. 위 한 번씩 잰 값은 3개일 때 8.5%, 48개일 때 17.9% 였지만, 아래 「반복 시험」에서 48개 값은 첫 측정이 낮게 나온 탓에 부풀었음이 드러났습니다(반복하면 약 10%). 문서의 「적은 오버헤드」는 이 환경에서 작지 않은 값이었습니다. 어댑터의 추가 연산은 1.7B 모델에서 가중치 읽기 비용(21편)에 비해 작지 않다는 뜻으로 읽지만, 어느 연산이 얼마를 차지하는지는 분해하지 않았습니다(추정).
- 처음에는 「어댑터를 섞어도 더 나빠지지 않았다」고 적었지만 반복으로 뒤집혔습니다. 한 번씩 잰 값에서는 어댑터 a 만(2,190)보다 a 와 b 를 섞을 때(2,323)가 높았으나, 5회 반복에서는 a 만이 중앙 2,390, 섞으면 2,330 이었습니다. 한 번 잰 2,190 은 서버를 띄우고 처음 부른 어댑터 요청이라 느렸던 값입니다(아래).
- ITL 중앙값은 15에서 20ms 로 늘었습니다. 지연이 늘어난 폭은 처리량 감소와 같은 방향입니다.
- 모델이 크면 비율은 달라질 것입니다. 이 모델은 토큰 하나에 약 15ms 로 작아서 어댑터의 고정 비용이 상대적으로 큽니다. 큰 모델에서의 비율은 재지 않았습니다(미실측).
반복 시험(5회씩, 같은 서버). 요청 48개(동시 16 의 3배)를 어댑터 없음, 어댑터 a, 어댑터 a 와 b 번갈아의 세 조건으로 순서를 섞어 5번씩 보냈습니다(--max-loras 2).
| 조건 | 처리량(tok/s), 5회 | 중앙값 | 기본 대비 |
|---|---|---|---|
| 어댑터 없음 | 2,647, 2,658, 2,669, 2,663, 2,667 | 2,663 | 기준 |
| 어댑터 a | 2,183, 2,398, 2,390, 2,391, 2,390 | 2,390 | -10.3% |
| 어댑터 a 와 b 번갈아 | 2,329, 2,330, 2,332, 2,329, 2,332 | 2,330 | -12.5% |
- 첫 측정만 낮았습니다. 어댑터 a 의 첫 값 2,183 은 서버를 띄운 뒤 처음 어댑터를 부른 측정이고, 이어지는 네 값(2,390~2,398)은 안정적이었습니다. 처음 어댑터를 쓸 때 적재와 초기화 비용이 섞인 것으로 보입니다(분해하지 않음). 앞의 한 번씩 잰 값이 17.9% 로 부풀어 보인 이유입니다. 벤치마크의 첫 값은 예열으로 보고 반복해서 중앙값을 써야 한다는 사례입니다.
- 어댑터 비용은 약 10~12% 입니다. 어댑터가 하나일 때가 둘을 섞을 때보다 약간 쌌습니다(-10.3% 대 -12.5%). 반복 간 흩어짐은 어댑터 없음 ±0.4%, 어댑터 a(첫 값 제외) ±0.2%, a 와 b ±0.1% 로 작아 이 차이는 의미가 있습니다.
- 위 3개 요청 시험의 -8.5%(182 대 199)도 한 번씩 잰 값이라 예열이 섞였을 수 있습니다(반복하지 않음).
4. 어댑터 슬롯: 어댑터 수가 --max-loras 를 넘으면
--max-loras 는 한 배치 안에 들 수 있는 서로 다른 LoRA 의 수입니다(기본 1). 처음에는 「GPU 에 동시에 올려 둘 수 있는 어댑터 수」로 적었지만, 이 글이 쓴 vLLM 0.31.0 이미지의 소스(vllm/config/lora.py)를 직접 읽어 보니 설명이 「Max number of LoRAs in a single batch」이고, CPU 쪽에 둘 어댑터 수는 별도의 --max-cpu-loras(기본은 max_loras 와 같은 값)였습니다. 어댑터 3개로 요청 18개를 한꺼번에 보냈습니다(각 설정 2회 반복. 두 회의 값이 거의 같은 경우가 많았지만 슬롯 2개의 어댑터 a, b 는 886과 984 로 달랐습니다).
| 서버 설정 | 어댑터 없음 | 어댑터 a, b | 어댑터 a, b, c |
|---|---|---|---|
--max-loras 2 |
1,120 tok/s | 886~984 tok/s | 529 tok/s |
--max-loras 4 |
1,131 tok/s | 986~987 tok/s | 950 tok/s |
- 슬롯이 모자라면 처리량이 거의 절반이 됩니다. 슬롯 2개에서 어댑터 3개를 번갈아 쓰자 529 tok/s 로, 슬롯 4개의 950 tok/s 의 56% 였고 어댑터 없는 기준(1,120)의 47% 였습니다. 원인은 소스에서 확인했습니다. 같은 이미지의 스케줄러 소스(
vllm/v1/core/sched/scheduler.py)는 대기 요청을 고를 때 이미 배치에 든 서로 다른 LoRA 수가max_loras에 도달했고 그 요청의 LoRA 가 배치에 없으면 요청을 건너뛰고 대기열에 둡니다(주석은 「Scheduling would exceed max_loras, skip」). 즉 슬롯 2개에서 어댑터 3개가 번갈아 오면 세 번째 어댑터의 요청이 계속 미뤄져 배치가 작아지고, 그만큼 총 처리량이 줄어듭니다. 어댑터를 교체하며 기다리는 시간이 원인이라고 적었던 처음의 추정은 이 소스 사실에 맞춰 고쳤고, 미뤄진 요청의 대기 시간 분포와 실제 배치 크기는 서버 지표로 읽지 않았습니다(미확인). - 슬롯이 충분하면 어댑터 개수가 늘어도 비용이 거의 같습니다. 슬롯 4개에서 어댑터 2개(987)와 3개(950)의 차이는 4% 였습니다.
- 그러므로
--max-loras는 「한 시점에 같은 배치에서 쓰이는 어댑터 수」에 맞춥니다. 어댑터가 수백 개여도 동시에 요청에 등장하는 어댑터가 슬롯 수 안이면 괜찮고, 그렇지 않으면 위의 절반 수준으로 떨어질 수 있습니다. 슬롯은 어댑터 가중치와 연산 공간을 미리 잡으므로 GPU 메모리를 쓰고, 이 서버에서는 슬롯을 켠 서버의 총 사용 메모리가 6,144MiB 로 켜지 않은 서버(6,168MiB)와 비슷했습니다(KV 캐시가 그만큼 줄어드는 식으로 흡수되었을 것으로 보이며 KV 용량은 확인하지 않았습니다).
4-1. 같은 시험을 SGLang 에서 하면
같은 모델과 같은 무작위 어댑터 3개(rank 16, 파라미터의 0.19%)로 SGLang 0.5.21 의 LoRA(--enable-lora --max-loras-per-batch N)를 재었습니다.
| 시험 | SGLang | vLLM(위 3절, 4절) |
|---|---|---|
| 동시 16(요청 48개), 어댑터 없음 | 2,539 tok/s | 2,664(LoRA 켠 서버의 어댑터 없는 요청) |
| 같은 부하, 어댑터 a | 2,516 tok/s(-0.9%, 한 번) | 2,390 tok/s(-10.3%, 5회 중앙) |
| 요청 18개, 어댑터 2개를 섞음, 슬롯 2 | 1,045 tok/s(어댑터 없음 1,027) | 886~984 tok/s(어댑터 없음 1,120) |
| 요청 18개, 어댑터 3개를 섞음, 슬롯 2 | 558 tok/s | 529 tok/s |
| 같은 시험, 슬롯 4 | 1,045 tok/s | 950 tok/s |
- 어댑터 하나당 비용은 SGLang 에서 더 작았습니다(-0.9% 대 -10.3%). SGLang 값은 한 번 잰 것이라 vLLM 처럼 첫 값이 낮을 수 있고, 위 반복 시험의 교훈대로 반복해서 확인해야 합니다(미실측). 두 엔진의 LoRA 커널과 배치 방식이 다르다는 단서이지만 원인은 분해하지 않았습니다(추정). 이 결과는 이 모델과 rank 16 어댑터에서의 값이므로 일반화하지 않습니다.
- 슬롯이 모자란 절벽은 두 엔진에서 같았습니다(이 값들도 한 번씩 잰 것입니다). 한 배치에 서로 다른 어댑터가 슬롯보다 많으면 처리량이 반 가까이 떨어졌고(SGLang 558 대 1,045, vLLM 529 대 950), 슬롯을 어댑터 수 이상으로 늘리면 회복했습니다. 슬롯 수가 어댑터 종류 수보다 작으면 안 된다는 4절의 결론은 엔진과 무관하게 유효했습니다.
- 한계: 같은 무작위 어댑터라 실제 학습된 어댑터와 다를 수 있고, 한 번씩 잰 값입니다(반복은 재지 않음).
5. 실행 중에 어댑터를 올리는 데 걸리는 시간
VLLM_ALLOW_RUNTIME_LORA_UPDATING=True 로 서버를 띄운 뒤 /v1/load_lora_adapter 로 어댑터 c 를 추가했습니다.
| 항목 | 결과 |
|---|---|
| 적재 요청의 응답 | LoRA adapter 'c' added successfully, 호출 전체 0.18초(원격 명령 실행 오버헤드 포함) |
| 적재 직후 첫 요청들 | 3개 요청 182 tok/s, 어댑터 a 와 같은 속도 |
- 어댑터 하나(6.4MB)를 올리는 일은 서버 재시작 없이 0.2초 안에 끝났습니다. 서버 재기동이 35~55초(14편)인 것과 대비됩니다. 어댑터 배포가 모델 배포보다 훨씬 가볍다는 것이 LoRA 서빙의 운영상 이점입니다.
- 다만 이 기능은 운영에 쓰지 말라는 경고가 문서에 있습니다. vLLM 문서는 실행 중 어댑터 변경이 격리되고 완전히 신뢰할 수 있는 환경이 아니면 운영에서 쓰지 말라고 적습니다. 요청이 임의의 경로의 어댑터를 올리게 할 수 있어서 보안상 위험이라고 이해합니다(직접 확인하지 않은 해석입니다). 운영에서는 시작할 때
--lora-modules로 정하거나, 어댑터 배포를 파이프라인으로 통제하는 편이 안전합니다. - 서버 준비 시간은 LoRA 를 켜면 70초로 켜지 않은 서버(35~55초 재기동)보다 길었습니다. LoRA 를 켠 서버는 CUDA 그래프를 더 많은 형태로 포착했습니다(로그에서 38개 대 19개).
6. 운영에서 가져갈 것
- 어댑터를 쓰는 요청의 처리량 비용을 미리 잽니다. 이 환경에서 8~18% 였고 모델과 동시성에 따라 달라집니다.
--max-loras를 동시에 등장하는 어댑터 수에 맞춥니다. 슬롯이 모자라면 절반 수준으로 떨어집니다.- 어댑터 배포는 가볍지만 무통제 적재는 위험합니다. 파이프라인으로 통제하고 검증된 어댑터만 올립니다.
- LoRA 를 켜 두는 비용은 없었습니다. 어댑터를 쓰지 않는 요청은 영향이 없었습니다.
- 큰 모델과 학습된 어댑터에서는 다시 재야 합니다. 이 글의 수치는 작은 모델과 무작위 어댑터에서 얻었습니다.
7. 면접에서 나올 만한 질문
- LoRA 어댑터를 여러 개 서빙하는 이유는? 기본 모델 하나를 공유하고 어댑터(기본 모델의 0.19% 수준)만 바꿔 쓰므로, 용도별 전체 모델을 올리는 것보다 GPU 메모리가 훨씬 적게 든다.
- 어댑터를 쓰면 얼마나 느려지는가? 이 환경에서 1.7B 모델은 요청 48개 기준 약 10~12% 줄었다(반복 5회 중앙값, 처음 한 번씩 잰 17.9% 는 예열이 섞인 값). LoRA 를 켜 두는 것만으로는 비용이 없었다.
--max-loras는 무엇을 정하는가, 모자라면? 한 배치 안에 들 수 있는 서로 다른 LoRA 수(기본 1)다. 어댑터가 이 수를 넘으면 스케줄러가 그 어댑터의 요청을 건너뛰어 배치가 작아진다. 어댑터 3개를 슬롯 2개로 서빙하면 처리량이 529 tok/s 로, 슬롯 4개의 950 의 56% 가 되었다.- 어댑터 배포가 모델 배포보다 쉬운 이유는? 어댑터 6.4MB 를 서버 재시작 없이 0.2초 만에 올렸다. 모델은 재기동 35~55초에 가중치 적재가 더해진다.
- 런타임 LoRA 적재를 운영에서 쓰지 않는 이유는? 문서가 격리된 신뢰 환경이 아니면 쓰지 말라고 경고한다.
외부 근거: 논문과 공식 자료로 확인한 것
외부 수치는 모델, GPU, 어댑터 구성이 달라 직접 비교하지 않고 방향과 자릿수의 일치만 판정했습니다.
| 주제 | 자료와 확인한 위치 | 확인한 내용과 조건 | 이 글과의 관계 |
|---|---|---|---|
| LoRA 의 크기 | Hu 외, 「LoRA: Low-Rank Adaptation of Large Language Models」, arXiv 2106.09685(ICLR 2022), 초록과 4.2절 | GPT-3 175B 에서 학습 파라미터를 최대 10,000배, 체크포인트 350GB 를 35MB 로 줄임(랭크 4, 쿼리와 값 투영만 적용). 추론 지연이 늘지 않는다는 말은 W0 + BA 로 합친 단일 어댑터에만 성립하고, 서로 다른 어댑터를 한 배치에 섞는 것은 합친 상태에서는 어렵다고 논문이 한계에 적음 |
지지. 우리 어댑터는 기본 모델의 0.19%(3,211,264개, 모델 설정에서 계산해 일치). 3절의 8.5~17.9% 감소는 합치지 않고 요청마다 고르는 방식의 비용 |
| 다중 어댑터 서빙의 처리량 | Chen 외, Punica, MLSys 2024, arXiv 2310.18547, 7절 그림 11. Sheng 외, S-LoRA, MLSys 2024, arXiv 2311.03285, 7.2절 표 3 | Punica: A100 80GB 한 장, Llama-2 7B·13B, 랭크 16. 같은 어댑터만 쓸 때 백본 전용 vLLM 보다 7B 약 8.4%, 13B 약 12.2% 낮음(논문 수치로 계산)이고 토큰당 약 2ms 추가 지연. S-LoRA: 어댑터를 호스트 메모리에 두고 실행 중인 요청의 것만 GPU 로 가져와 어댑터 5개에서 100개로 늘어도 처리량 유지, 약간의 감소 | 제한적으로 지지. 어댑터를 쓰는 요청의 처리량 감소와 자릿수가 같음. 「슬롯 초과 시 절반」에 대응하는 외부 수치는 없고, S-LoRA 는 활성 어댑터 수가 비용을 정한다고 설명해 이 현상이 vLLM 의 max_loras 구성에 특유할 수 있음을 시사 |
--max-loras 의미 |
vLLM 0.31.0 소스 config/lora.py, v1/core/sched/scheduler.py(이 글이 쓴 이미지에서 직접 읽음) |
한 배치 안의 서로 다른 LoRA 수(기본 1). 한도에 도달하면 다른 어댑터의 요청을 건너뜀. max_cpu_loras 는 별도(기본 max_loras와 같음) |
처음 서술을 정정. 위 4절 |
| 오버헤드와 런타임 적재 | vLLM 문서 「LoRA Adapters」(2026-09-28 표기) | 어댑터를 요청 단위로 적은 오버헤드로 서빙한다고 적고 정량 수치는 없음. VLLM_ALLOW_RUNTIME_LORA_UPDATING 은 격리된 신뢰 환경이 아니면 운영에서 쓰지 말라는 경고가 있고 구체적 이유는 적혀 있지 않음 |
제한적으로 지지. 「적은 오버헤드」는 이 환경에서 8.5~17.9% 로 작지 않았음 |
확인하지 못한 것
- 학습된 어댑터에서의 속도와 출력 품질. 무작위 어댑터로 속도만 쟀습니다.
- 큰 모델, 더 높은 랭크, 어댑터가 모든 선형층을 대상으로 할 때의 비용.
- 반복 오차. 슬롯 시험만 2회 반복했고 3절은 1회입니다. a 와 b 를 섞은 값이 a 하나보다 높게 나온 차이는 해석하지 않았습니다.
- 미뤄진 요청의 대기 시간과 배치 크기. 서버 지표(Prometheus)를 읽지 않았습니다.
--max-cpu-loras등 CPU 쪽 캐시 설정의 효과와 어댑터 수백 개 규모.- 어댑터가 있을 때 접두사 캐시와의 상호작용(어댑터마다 캐시가 분리되는지).
참고 자료
- vLLM 문서, LoRA Adapters: https://docs.vllm.ai/en/latest/features/lora/
- Hugging Face PEFT, LoRA: https://huggingface.co/docs/peft/package_reference/lora
- 이 시리즈의 14편(vLLM 부하와 서버 준비 시간), 21편(토큰 속도와 메모리 대역폭)
확인한 버전과 날짜
2026-10-07 기준. vLLM 0.31.0, PEFT 0.21.2, accelerate 1.15.0, Qwen3-1.7B(bf16).