Documents
Home>Documents>AI>Inference

Multi-LoRA 서빙의 KV 캐시 격리 문제: 어댑터가 바뀌면 캐시는 왜 무효화되는가

12 min readAug 31, 2026Aug 31, 2026

Prefix Caching을 켜 놓고 vLLM을 띄웠는데 히트율 모니터가 0%를 가리킬 때, 서빙 스택에서 Multi-LoRA를 쓰고 있다면 원인은 거의 이 하나다.

왜 Multi-LoRA 서빙이 구조적으로 다른가

LoRA는 사전학습 가중치 W에 저랭크 행렬 A, B를 더하는 방식으로 동작한다. Attention 레이어의 Q, K, V projection은 (W + AB)x 형태로 바뀌는데, A와 B가 어댑터마다 다르므로 동일한 입력 토큰 x에 대해서도 K, V는 어댑터별로 서로 다른 값을 낸다.

KV 캐시는 이 K, V 행렬을 저장해두는 공간이다. 어댑터가 바뀌는 순간, 이전에 저장된 KV는 새 어댑터에서 계산된 KV와 일치하지 않아 재사용할 수 없다. 이 사실이 Prefix Caching의 핵심 전제를 무너뜨린다.

단일 베이스 모델만 서빙할 때는 캐시 키가 "어떤 토큰 시퀀스냐"로만 결정된다. 두 요청이 같은 시스템 프롬프트를 공유하면 그 구간의 KV는 한 번 계산 후 재사용한다. Multi-LoRA에서는 동일한 1024토큰 시스템 프롬프트라도 어댑터 A와 어댑터 B의 KV 값이 달라지기 때문에, 어댑터마다 별도 캐시 엔트리가 생긴다.

Prefix Caching 키 공간이 어댑터 ID로 분열되는 구조

vLLM과 SGLang(arXiv:2312.07104) 모두 Prefix Caching의 핵심으로 Radix Tree를 쓴다. 캐시 블록 하나의 키 구조는 다음과 같다:

block_hash = hash(parent_block_hash, token_ids, extra_keys)
extra_keys  = (lora_name, mm_hash, cache_salt, ...)

vLLM의 kv_cache_utils.py는 아래 함수로 LoRA 이름을 extra_key에 집어넣는다:

def _gen_lora_extra_hash_keys(request: Request) -> list[str]:
    if not request.lora_request:
        return []
    return [request.lora_request.lora_name]

이 구조의 결과는 단순하다. 어댑터 이름이 다른 두 요청은 토큰 시퀀스가 완전히 같아도 block_hash가 달라진다. 어댑터 A, B, C, D가 동일한 시스템 프롬프트를 각각 처음 받으면 캐시 미스가 4번 발생하고 4개의 독립 엔트리가 생긴다.

균등 분포(Uniform)에서 N개 어댑터가 트래픽을 나눠 가지면 캐시 히트가 발생할 확률의 이론 상한은 1/N이다. 어댑터 16개를 균등하게 쓰는 환경에서 Prefix Caching 히트율 상한은 약 6.25%다.

이 1/N 한계는 캐시가 무한하고 프롬프트 재사용률이 높다는 이상적 조건에서도 피할 수 없다. "캐시를 켰는데 히트율이 0이다"는 상황은 정확히 이 조건에 가까운 운용에서 나온다.

어댑터 전환 비용의 실체

어댑터 전환에는 두 종류의 비용이 있다. GPU 메모리 적재 비용과, 배치 분리에서 오는 활용률 하락이다.

어댑터 크기와 메모리 점유

LoRA 어댑터 1개가 차지하는 VRAM은 다음 식으로 추정한다:

adapter_size = rank × 2 × hidden_dim × num_layers × dtype_bytes

Llama-3-8B(hidden_dim=4096, num_layers=32, bfloat16)에 rank=16 어댑터를 붙이면:

16 × 2 × 4096 × 32 × 2 = 8,388,608 bytes ≈ 8 MB   (projection 1개 기준)

q, k, v, o 네 개 attention projection 전체에 LoRA를 적용하면 4배, 약 32MB다. MLP까지 포함하면 50MB를 넘기도 한다.

어댑터 전환 전략은 크게 둘로 나뉜다.

Full-offload: GPU에는 베이스 모델만 상주하고, 요청마다 해당 어댑터를 CPU에서 GPU로 적재한다. PCIe 환경에서 CPU→GPU 대역폭은 12~16 GB/s 수준이므로, 32MB 어댑터 적재에 약 2ms 오버헤드가 발생한다. Punica(arXiv:2310.18547)가 보고한 "어댑터 온디맨드 로딩 시 밀리초 수준 지연"이 이 범위다.

Hot-pool: 자주 쓰는 어댑터 K개를 GPU VRAM에 상주시킨다. 전환 지연은 거의 없지만, 어댑터 가중치가 KV 캐시 공간을 직접 잠식한다. 32MB짜리 어댑터 16개를 hot pool로 유지하면 512MB가 KV 캐시 예산에서 빠진다. A100 80GB에서도 무시 못 할 비율이다.

S-LoRA(arXiv:2311.03285)는 이 충돌을 Unified Paging으로 완화하려 했다. 어댑터 가중치와 KV 캐시를 단일 메모리 풀에서 동적으로 관리해 단편화를 줄이는 방식이다. 단일 GPU에서 수천 개 어댑터를 스케줄링하는 시나리오에서 기존 대비 최대 4배 처리량을 보고했지만, 캐시 히트율 하락 자체는 해결하지 않는다.

배치 분리 문제

단일 베이스 모델 서빙에서는 수십~수백 개 요청을 하나의 배치로 묶어 GPU 텐서 연산을 최대화한다. Multi-LoRA 환경에서 요청마다 어댑터가 다르면 배치를 어댑터 단위로 쪼개야 한다. 트래픽이 N개 어댑터에 균등 분산되면 배치 크기가 1/N이 되고, 배치가 작아질수록 Tensor Core 활용률과 메모리 대역폭 효율이 함께 떨어진다.

Punica의 SGMV(Segmented Gather Matrix-Vector Multiplication) 커널은 이 문제를 직접 공략했다. 서로 다른 어댑터를 쓰는 요청들을 단일 배치로 묶고, 어댑터별 행렬 연산을 segment 단위로 병렬 처리한다. 기존 서빙 시스템 대비 12배 처리량 개선을 보고했는데, 이건 배치 분리 페널티를 줄인 결과다. 캐시 히트율 문제는 다른 차원이다.

Shared-prefix 최적화의 실제 조건

"시스템 프롬프트 KV는 어댑터와 무관하게 공유할 수 있지 않나"는 질문이 자주 나온다. LoRA 설정에 따라 다르다.

Transformer에서 토큰이 처음 거치는 Embedding 레이어는 대부분 LoRA 타깃에서 빠진다. 이 레이어의 출력은 어댑터와 무관하게 동일하다. 그런데 KV는 각 Transformer 레이어마다 새로 계산되며, 레이어 1에 LoRA가 붙어 있으면 그 이후의 모든 레이어 KV가 어댑터 종속이 된다. 실용적인 LoRA 설정(q, k, v 또는 all-attention)은 레이어 1부터 LoRA를 적용하므로, 공유 가능한 KV 구간은 사실상 없다.

모든 레이어에 LoRA를 적용하는 full-rank 설정에서는 공유 구간이 0이다. Embedding 이후 첫 번째 레이어부터 KV가 분기하기 때문이다.

Frozen Embedding + attention 이전 레이어에만 LoRA를 한정하는 특수 설정에서는 일부 레이어 KV를 공유할 수 있다. 이 설정은 파인튜닝 표현력이 제한적이라 범용 서빙에서 거의 쓰이지 않는다.

어댑터 수·요청 분포에 따른 히트율 변화

균등 분포(Uniform)와 Zipf 분포(s=1.0, 실제 서비스에서 자주 관찰되는 요청 편중)를 비교하면 아래와 같다. 히트율은 캐시가 모든 어댑터의 공통 프리픽스를 충분히 담을 수 있다는 조건에서의 이론 상한이다.

어댑터 수요청 분포히트율 이론 상한
N=1~85%
N=4Uniform~21%
N=4Zipf(s=1.0)~40%
N=16Uniform~5%
N=16Zipf(s=1.0)~25%

Zipf 분포에서 히트율이 회복되는 이유는 상위 1~2개 어댑터가 전체 트래픽의 절반 이상을 차지하기 때문이다. N=4, s=1.0 조건에서 조화급수(H₄ ≈ 2.08)를 기반으로 계산하면 상위 어댑터 하나가 전체의 약 48%를 가져간다. 이 어댑터의 캐시는 잘 채워지고 히트율도 높다.

문제는 트래픽이 거의 없는 하위 어댑터들이다. 이들은 캐시 슬롯을 차지하면서 요청이 뜸하게 들어와 캐시가 항상 cold 상태를 유지한다. Zipf 분포에서 히트율이 회복되는 만큼, 그 반대급부로 하위 어댑터의 tail latency가 올라가는 건 피하기 어렵다.

프레임워크 현황과 분리 인스턴스 판단

vLLM은 lora_name을 캐시 키에 포함해 어댑터 간 오염을 막는다. 단 issue #30931이 지적했듯 lora_name이 같고 가중치가 다른 경우(런타임 핫스왑)에는 stale KV가 재사용되는 버그가 있다. 현재 Prefix Caching과 Multi-LoRA 조합은 완전히 안전하지 않다. SGLang도 어댑터 ID를 Radix Tree 키에 포함하는 방향으로 발전 중이지만 격리 구현은 진행 중인 작업이다.

Multi-LoRA 단일 인스턴스 vs. 어댑터별 독립 인스턴스 선택은 대략 이 교차점에서 판단한다:

어댑터별 QPS가 GPU 최대 처리량의 ~60% 이상이면  → 분리 인스턴스
어댑터 수가 많고 개별 트래픽이 낮으면           → Multi-LoRA 공유 인스턴스

SLA가 엄격한 환경(p99 TTFT < 500ms)이라면 어댑터 수가 4개만 넘어도 분리 인스턴스가 먼저 검토 대상이 된다. 캐시 히트율이 25% 아래로 떨어지면 Prefix Caching의 TTFT 개선 효과는 거의 사라지고, 어댑터 전환 오버헤드가 latency를 주도한다.

Tags
LLMInferenceKV 캐시서빙vLLM아키텍처