왜 테넌트마다 모델을 띄우면 안 되는가
테넌트(고객사)별로 파인튜닝한 모델을 각각 독립 프로세스로 올리면 가장 큰 병목은 GPU 메모리다. 7B 모델을 fp16으로 올리면 약 14GB를 차지한다. 테넌트가 30개면 30장의 GPU가 필요하지만, 실제로 동시에 요청이 몰리는 테넌트는 몇 개 되지 않는다. 유휴 GPU가 대부분인 구조가 된다.
LoRA(Low-Rank Adaptation)는 베이스 모델의 가중치를 그대로 두고, 각 레이어에 저랭크 행렬 두 개(A, B)만 추가로 학습한다. 어댑터 하나의 크기는 보통 수십~수백 MB에 불과하다. 따라서 베이스 모델 한 벌을 GPU에 상주시키고, 요청마다 테넌트의 어댑터만 갈아 끼우는(hotswap) 구조가 성립한다.
핫스왑의 핵심 아이디어
추론 시점의 가중치는 개념적으로 W = W_base + (B·A)·(α/r) 형태다. 여기서 W_base는 공유되고 B·A만 테넌트별로 다르다. 두 가지 전략이 있다.
- Merge 방식: 어댑터를 베이스에 합쳐 단일 가중치로 만든다. 추론 오버헤드는 0이지만 스왑할 때 merge/unmerge 비용이 크고, 배치 안에 여러 테넌트를 섞을 수 없다.
- Unmerged(동적) 방식: 어댑터를 분리 유지하고 추론 중
B·A를 더한다. 약간의 연산 오버헤드가 있지만, 서로 다른 테넌트 요청을 한 배치에 묶는 것이 가능하다. vLLM의 Multi-LoRA가 이 방식이다.
| 항목 | 테넌트별 전체 모델 | LoRA 핫스왑 |
|---|---|---|
| 테넌트당 추가 메모리 | ~14GB | 수십~수백 MB |
| 배치 혼합 | 불가 | 가능(unmerged) |
| 신규 테넌트 온보딩 | 모델 로드 | 어댑터 파일 등록 |
vLLM으로 다중 어댑터 서빙하기
vLLM은 요청 단위로 어댑터를 지정할 수 있어 별도 로직 없이 멀티테넌트 서빙이 된다. --max-loras는 GPU에 동시 상주시킬 어댑터 수, --max-lora-rank는 허용 랭크 상한이다.
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3.1-8B \
--enable-lora \
--max-loras 8 \
--max-lora-rank 32 \
--max-cpu-loras 64 \
--lora-modules \
tenant_a=/models/adapters/tenant_a \
tenant_b=/models/adapters/tenant_b
요청에서는 model 필드에 어댑터 이름을 넣으면 해당 테넌트의 어댑터로 응답한다.
curl http://localhost:8000/v1/completions \
-H "Content-Type: application/json" \
-d '{
"model": "tenant_a",
"prompt": "고객 문의를 요약해줘: ...",
"max_tokens": 256
}'
런타임에 어댑터 추가/제거
테넌트가 늘어날 때마다 서버를 재시작하면 다른 테넌트의 요청이 끊긴다. vLLM은 무중단으로 어댑터를 등록/해제하는 API를 제공한다(VLLM_ALLOW_RUNTIME_LORA_UPDATING=1 필요).
curl -X POST http://localhost:8000/v1/load_lora_adapter \
-H "Content-Type: application/json" \
-d '{"lora_name": "tenant_c", "lora_path": "/models/adapters/tenant_c"}'
# 사용 종료된 테넌트 해제(CPU 캐시에서 내림)
curl -X POST http://localhost:8000/v1/unload_lora_adapter \
-H "Content-Type: application/json" \
-d '{"lora_name": "tenant_c"}'
max_loras를 넘는 어댑터는 요청 시 CPU→GPU로 스왑인된다. 이 스왑 비용을 줄이려면 활성 테넌트를 GPU에 유지하는 LRU 정책을 앞단에서 관리하는 것이 좋다.
어댑터 레지스트리 설계
어떤 테넌트가 어떤 어댑터 버전을 쓰는지 추적하려면 가벼운 메타데이터 테이블이 필요하다. 롤백과 A/B 테스트를 위해 버전을 분리해 두는 것이 핵심이다.
CREATE TABLE tenant_adapters (
tenant_id TEXT NOT NULL,
version INT NOT NULL,
path TEXT NOT NULL,
rank INT NOT NULL,
base_model TEXT NOT NULL,
is_active BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT now(),
PRIMARY KEY (tenant_id, version)
);
-- 활성 어댑터만 조회해 게이트웨이가 라우팅
SELECT tenant_id, path, rank
FROM tenant_adapters
WHERE is_active = TRUE AND base_model = 'Llama-3.1-8B';
운영 시 주의점
- 베이스 모델이 반드시 같아야 한다. 어댑터는 학습 당시의 베이스에 종속된다. 게이트웨이에서
base_model불일치 요청은 거부해야 조용한 품질 저하를 막는다. - 랭크가 섞이면 배치 효율이 떨어진다.
max_lora_rank는 상한이므로 낮은 랭크로 통일할수록 메모리와 커널 효율이 좋다. 테넌트별로 r=8, r=64가 뒤섞이면 상한을 크게 잡아야 해 손해다. - 콜드 스왑 지연을 측정하라. 처음 호출되는 테넌트는 어댑터 로드 지연이 first-token latency에 더해진다. p99를 볼 때 스왑인 비용을 분리해 관측해야 한다.
- 테넌트 격리. 배치 혼합은 성능상 이득이지만, 어댑터 이름 검증과 요청 인증을 분리해 다른 테넌트의 어댑터가 지정되지 않도록 게이트웨이에서 강제한다.
정리하면, 활성 테넌트가 소수이고 베이스 모델을 공유할 수 있는 환경에서 LoRA 핫스왑은 GPU 대수를 테넌트 수가 아니라 동시 부하에 비례하게 만든다. 다만 베이스 일치·랭크 통일·콜드 스왑 관측이라는 세 가지 운영 규칙을 지키는 것이 전제다.