[vLLM] LoRA (Multi-LoRA Serving): 저차원 어댑터 서빙
들어가며
LoRA(Low-Rank Adaptation)는 거대 모델의 가중치를 고정한 채 저차원 행렬 쌍(A, B)만 학습하는 파인튜닝 기법이다. 파라미터 효율적 학습이라는 본래 목적 외에, LoRA는 서빙 측면에서도 혁신적인 가능성을 연다: 하나의 베이스 모델 위에 수백 개의 LoRA 어댑터를 동시에 로드하여, 각 요청마다 다른 어댑터를 적용할 수 있다. vLLM은 이 Multi-LoRA Serving을 네이티브로 지원한다.
- 논문:
- 공식 문서: https://docs.vllm.ai
공식 문서
vLLM 공식 문서: LoRA
핵심 구조/코드 분석
LoRAModel: 어댑터 표현
vllm/lora/lora_model.py에서 개별 LoRA 모델을 표현한다:
class LoRAModel:
"""A LoRA fine-tuned model."""
def __init__(
self,
lora_model_id: int,
rank: int,
loras: dict[str, LoRALayerWeights],
) -> None:
self.id = lora_model_id
assert lora_model_id > 0
self.rank = rank
self.loras: dict[str, LoRALayerWeights] = loras
def get_lora(self, module_name: str) -> LoRALayerWeights | None:
return self.loras.get(module_name, None)
각 LoRAModel은 고유 ID, 랭크, 그리고 모듈 이름별 가중치 딕셔너리를 가진다. clone 메서드는 텐서를 공유하면서 ID만 바꾸는데, 같은 어댑터를 여러 슬롯에 적용할 때 메모리를 절약한다.
PEFTHelper: 설정 관리
vllm/lora/peft_helper.py의 PEFTHelper가 LoRA 설정의 유효성 검증과 스케일링 팩터 계산을 담당한다:
@dataclass
class PEFTHelper:
r: int # LoRA 랭크
lora_alpha: int # 스케일링 알파
target_modules: list[str] | str # 적용 대상 모듈
use_rslora: bool = False # Rank-Stabilized LoRA
use_dora: bool = False # Weight-Decomposed LoRA (미지원)
def __post_init__(self):
if self.use_rslora:
self.vllm_lora_scaling_factor = self.lora_alpha / math.sqrt(self.r)
else:
self.vllm_lora_scaling_factor = self.lora_alpha / self.r
표준 LoRA의 스케일링은 alpha/r이지만, rsLoRA(Rank-Stabilized LoRA)를 사용하면 alpha/sqrt(r)로 바뀐다. DoRA는 아직 미지원 상태이다.
레이어 구현: 15+ 변형
vllm/lora/layers/__init__.py에서 지원하는 LoRA 레이어 변형의 목록을 확인할 수 있다:
__all__ = [
"BaseLayerWithLoRA",
"VocabParallelEmbeddingWithLoRA",
"LogitsProcessorWithLoRA",
"ColumnParallelLinearWithLoRA",
"ColumnParallelLinearWithShardedLoRA",
"MergedColumnParallelLinearWithLoRA",
"MergedColumnParallelLinearWithShardedLoRA",
"MergedQKVParallelLinearWithLoRA",
"MergedQKVParallelLinearWithShardedLoRA",
"QKVParallelLinearWithLoRA",
"QKVParallelLinearWithShardedLoRA",
"RowParallelLinearWithLoRA",
"RowParallelLinearWithShardedLoRA",
"ReplicatedLinearWithLoRA",
"FusedMoEWithLoRA",
"FusedMoE3DWithLoRA",
]
특히 주목할 것은 Sharded 변형들이다. 텐서 병렬(TP)에서 LoRA 가중치를 어떻게 분할할지 결정하는데, Column Parallel은 출력 차원을, Row Parallel은 입력 차원을 분할한다. MergedQKVParallelLinearWithLoRA는 Q/K/V 프로젝션이 하나의 레이어로 합쳐진 경우를 처리한다.
아키텍처 구성
vllm/lora/ 디렉토리의 전체 구조를 보면 Multi-LoRA 서빙의 복잡도를 알 수 있다:
| 파일 | 역할 |
|---|---|
model_manager.py |
LoRA 모델의 로드/언로드/캐싱 |
worker_manager.py |
워커별 LoRA 상태 관리 |
lora_weights.py |
개별 레이어의 A, B 가중치 |
punica_wrapper/ |
Punica 커널 래퍼 (배치 GEMM) |
resolver.py |
어떤 레이어에 어떤 LoRA 변형을 적용할지 결정 |
왜 이 설계인가
1. Punica 커널: Multi-LoRA의 핵심 기술이다. 서로 다른 LoRA 어댑터가 적용된 요청들을 하나의 배치로 처리하려면, 각 토큰이 어떤 어댑터의 A/B 행렬을 사용할지 인덱싱해야 한다. Punica는 이 세그멘티드 GEMM을 GPU에서 효율적으로 수행한다.
2. Sharded vs Non-Sharded: 텐서 병렬 환경에서 LoRA를 적용할 때, 전체 LoRA 가중치를 모든 GPU에 복제(Non-Sharded)하거나 분할(Sharded)할 수 있다. 작은 랭크의 LoRA는 복제가 유리하고, 큰 랭크는 분할이 메모리 효율적이다. 두 방식을 모두 제공하는 것은 이 트레이드오프 때문이다.
3. MoE 호환: FusedMoEWithLoRA는 MoE 모델의 전문가 레이어에도 LoRA를 적용할 수 있게 한다. Mixtral 같은 모델을 LoRA로 파인튜닝한 경우에도 서빙이 가능하다.
4. 동적 로딩: model_manager.py가 LRU 캐싱을 구현하여, 활성 LoRA 수가 GPU 메모리 한계를 넘으면 가장 오래된 어댑터를 언로드한다. 수천 개의 LoRA를 등록해도 실제 GPU에는 동시에 필요한 것만 올라간다.
논문 핵심 내용
S-LoRA 논문은 수천 개의 LoRA 어댑터를 하나의 GPU에서 동시에 서빙하는 시스템을 제시했다. 핵심 기여는 세 가지다: (1) Unified Paging으로 KV 캐시와 LoRA 가중치를 통합 메모리 풀에서 관리, (2) 이기종 배치(heterogeneous batching)에서 효율적인 커스텀 CUDA 커널, (3) 어댑터 클러스터링을 통한 배치 최적화이다.
처리량 벤치마크 (단일 A100 80GB, req/s)
| 설정 (Llama-7B) | 어댑터 수 | S-LoRA | vLLM-packed | PEFT |
|---|---|---|---|---|
| S1 | 5 | 8.05 | 2.04 | 0.88 |
| S1 | 100 | 7.99 | OOM | 0.25 |
| S1 | 1,000 | 7.64 | OOM | - |
| S1 | 2,000 | 7.61 | OOM | - |
| 설정 (Llama-13B) | 어댑터 수 | S-LoRA | vLLM-packed | PEFT |
|---|---|---|---|---|
| S4 | 2 | 4.49 | 3.83 | 0.54 |
| S4 | 100 | 4.28 | OOM | 0.13 |
| S4 | 1,000 | 3.96 | OOM | - |
S-LoRA의 가장 인상적인 결과는 어댑터 수가 5개에서 2,000개로 400배 증가해도 처리량이 8.05에서 7.61로 5.5%만 감소한다는 점이다. 반면 vLLM-packed은 100개부터 OOM이 발생하고, PEFT는 어댑터 수 증가에 따라 처리량이 급격히 하락한다.
핵심 수치
| 항목 | 결과 |
|---|---|
| vs vLLM-packed 처리량 | 최대 4배 향상 |
| vs PEFT 처리량 | 최대 30배 향상 |
| 동시 서빙 가능 어댑터 | 수천 개 (메인 메모리가 허용하는 한) |
| 어댑터 증가에 따른 성능 저하 | 5% 미만 |
PEFT는 어댑터 1개에서 200개로 늘릴 때 레이턴시가 1,021ms에서 1,609ms로 57% 증가하지만, S-LoRA는 Unified Paging 덕분에 어댑터 수가 처리량에 거의 영향을 주지 않는다. 텐서 병렬 환경에서도 LoRA 통신 오버헤드가 베이스 모델 통신 대비 무시할 수 있는 수준이어서, 다중 GPU로의 확장이 자연스럽다.
마무리
vLLM의 Multi-LoRA 서빙은 Punica 커널, 텐서 병렬 호환, 동적 캐싱이라는 세 축 위에 구축되어 있다. 하나의 베이스 모델로 다수의 사용자/태스크를 서빙하는 멀티테넌트 환경에서, 각 테넌트별 커스텀 모델을 GPU 하나로 제공할 수 있다는 점이 핵심 가치이다.
관련 포스트
vLLM 의 다른글
- 이전글 [vLLM] FP8: 8비트 부동소수점 양자화
- 현재글 : [vLLM] LoRA (Multi-LoRA Serving): 저차원 어댑터 서빙
- 다음글 [vLLM] Fused MoE: 라우팅+전문가 연산 융합
댓글