본문으로 건너뛰기

[vLLM] LoRA (Multi-LoRA Serving): 저차원 어댑터 서빙

들어가며

LoRA(Low-Rank Adaptation)는 거대 모델의 가중치를 고정한 채 저차원 행렬 쌍(A, B)만 학습하는 파인튜닝 기법이다. 파라미터 효율적 학습이라는 본래 목적 외에, LoRA는 서빙 측면에서도 혁신적인 가능성을 연다: 하나의 베이스 모델 위에 수백 개의 LoRA 어댑터를 동시에 로드하여, 각 요청마다 다른 어댑터를 적용할 수 있다. vLLM은 이 Multi-LoRA Serving을 네이티브로 지원한다.

공식 문서

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.pyPEFTHelper가 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 의 다른글