
1. 서론: 왜 단일 GPU에서 다중 어댑터 서빙인가?
최근 초거대 언어 모델(LLM)의 보급으로 인해 기업들은 특정 도메인에 특화된 모델을 필요로 하고 있습니다. 하지만 모든 서비스마다 수십 기가바이트(GB)에 달하는 모델 전체 파라미터를 개별적으로 로드하는 것은 인프라 비용 측면에서 매우 비효율적입니다. 이러한 문제를 해결하기 위해 등장한 것이 바로 PEFT(Parameter-Efficient Fine-Tuning), 그중에서도 가장 널리 쓰이는 LoRA(Low-Rank Adaptation)입니다. 본 가이드에서는 단일 GPU 환경에서 하나의 Base Model을 공유하면서, 서로 다른 역할을 수행하는 여러 개의 어댑터를 동시에 서빙하여 하드웨어 효율을 극대화하는 실무적인 방법론을 제시합니다.
2. 기존 방식 vs PEFT 기반 다중 어댑터 서빙 차이 비교
기존의 개별 모델 서빙 방식과 PEFT 기반의 멀티 어댑터 서빙 방식이 하드웨어 자원 활용 측면에서 어떤 차이를 보이는지 아래 표를 통해 확인하십시오.
| 비교 항목 | Full Model 개별 서빙 | PEFT 멀티 어댑터 서빙 | 비고 |
|---|---|---|---|
| VRAM 소모 | 모델 수 × 모델 크기 (매우 높음) | Base Model + (어댑터 수 × 소량) | 최대 90% 절감 가능 |
| 서빙 유연성 | 낮음 (모델 교체 시 재로딩 필요) | 매우 높음 (실시간 어댑터 전환 가능) | Hot-swapping 지원 |
| 처리 속도(Latency) | 개별 인스턴스 기준 빠름 | 어댑터 전환 오버헤드 미미함 | 배치 처리 최적화 필수 |
| 인프라 비용 | 모델당 GPU 1장 이상 필요 | 단일 GPU로 수십 개 서비스 가능 | 운영 비용 대폭 감소 |
3. 핵심 기술: LoRA와 Adapter Switching의 원리
LoRA는 모델의 가중치를 고정한 채 일부 저차원 행렬(Low-rank matrices)만을 학습시킵니다. 서빙 시에는 이 행렬들만 Base Model에 더해주면(merge) 됩니다. 다중 어댑터 서빙의 핵심은 이 더해지는 행렬을 요청(Request)마다 동적으로 교체하거나, 여러 요청을 한꺼번에 처리할 때 각 토큰마다 다른 어댑터를 적용하는 Multi-LoRA Inference 기술에 있습니다.
4. 실무 적용 가능한 7가지 코드 예제 (Sample Examples)
개발자가 실무에서 바로 복사하여 사용할 수 있도록 Python 기반의 Hugging Face `peft` 및 `vLLM` 라이브러리를 활용한 예제를 제공합니다.
Example 1: 기본 Base Model 및 다중 LoRA 로딩
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
base_model_name = "meta-llama/Llama-2-7b-hf"
tokenizer = AutoTokenizer.from_pretrained(base_model_name)
base_model = AutoModelForCausalLM.from_pretrained(base_model_name, torch_dtype=torch.float16, device_map="auto")
# 첫 번째 어댑터 로드 (마케팅 문구 생성용)
model = PeftModel.from_pretrained(base_model, "./adapters/marketing_lora", adapter_name="marketing")
# 두 번째 어댑터 로드 (코드 요약용)
model.load_adapter("./adapters/code_summary_lora", adapter_name="coder")
print(model.active_adapters)
Example 2: 실시간 어댑터 전환(Switching) 추론
# 마케팅 어댑터로 전환
model.set_adapter("marketing")
inputs = tokenizer("Summer Sale Promotion:", return_tensors="pt").to("cuda")
output_marketing = model.generate(**inputs, max_new_tokens=50)
# 코드 요약 어댑터로 즉시 전환
model.set_adapter("coder")
inputs = tokenizer("def hello_world(): print('hi')", return_tensors="pt").to("cuda")
output_code = model.generate(**inputs, max_new_tokens=50)
Example 3: vLLM을 이용한 고성능 멀티 어댑터 서빙 설정
from vllm import LLM, SamplingParams
from vllm.lora.request import LoRARequest
# LoRA 지원을 활성화하여 모델 시작
llm = LLM(model="meta-llama/Llama-2-7b-hf", enable_lora=True)
sampling_params = SamplingParams(temperature=0.7, top_p=0.95)
# 요청별로 다른 LoRA 적용
prompts = [
("English to Korean translation: Apple ->", "korean_lora", 1),
("Explain Quantum Physics in simple terms:", "science_lora", 2),
]
outputs = []
for prompt, lora_name, lora_id in prompts:
outputs.extend(llm.generate(
prompt,
sampling_params,
lora_request=LoRARequest(lora_name, lora_id, f"./adapters/{lora_name}")
))
Example 4: 특정 레이어에 어댑터 비중 조절 (Weighted Adapters)
# 두 어댑터를 섞어서 하이브리드 결과물 생성 (앙상블 효과)
model.add_weighted_adapter(
adapters=["marketing", "coder"],
weights=[0.6, 0.4],
adapter_name="hybrid_v1",
combination_type="linear"
)
model.set_adapter("hybrid_v1")
Example 5: 어댑터 로딩/언로딩 동적 관리 클래스
class AdapterManager:
def __init__(self, model):
self.model = model
self.loaded_adapters = set(["default"])
def ensure_adapter(self, adapter_path, adapter_name):
if adapter_name not in self.loaded_adapters:
self.model.load_adapter(adapter_path, adapter_name=adapter_name)
self.loaded_adapters.add(adapter_name)
self.model.set_adapter(adapter_name)
manager = AdapterManager(model)
manager.ensure_adapter("./adapters/finance", "finance_bot")
Example 6: FastAPI를 이용한 멀티 어댑터 API 서버 구축
from fastapi import FastAPI
app = FastAPI()
@app.post("/generate/{service_type}")
async def generate_text(service_type: str, prompt: str):
if service_type == "creative":
model.set_adapter("marketing")
else:
model.set_adapter("coder")
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
with torch.no_grad():
output = model.generate(**inputs, max_new_tokens=100)
return {"result": tokenizer.decode(output[0])}
Example 7: VRAM 최적화를 위한 어댑터 오프로딩 전략
# 사용하지 않는 어댑터를 삭제하여 VRAM 확보
def garbage_collect_adapters(model, keep_adapter):
all_adapters = list(model.peft_config.keys())
for adapter in all_adapters:
if adapter != keep_adapter:
model.unload_adapter(adapter)
print(f"Unloaded {adapter}")
garbage_collect_adapters(model, "marketing")
5. 해결 방법: 서빙 중 발생하는 주요 이슈와 대응 전략
문제 1: 어댑터 전환 시의 미세한 지연(Latency)
해결: vLLM이나 LoRAX 같은 엔진을 사용하면 전용 CUDA 커널을 통해 어댑터 가중치를 실시간으로 적용하므로 전환 지연 시간을 거의 제로(0)에 가깝게 유지할 수 있습니다.
문제 2: VRAM 단편화 현상
해결: 다수의 어댑터를 로드하면 관리 포인트가 늘어납니다. 주기적으로 사용되지 않는 어댑터를 캐시에서 제거하거나, bitsandbytes 4-bit 양자화를 Base Model에 적용하여 기초 메모리 점유율을 낮추는 것이 필수적입니다.
6. 결론
단일 GPU에서 다중 어댑터를 서빙하는 기술은 현대 LLM 인프라 구축의 핵심입니다. PEFT 라이브러리와 최신 서빙 프레임워크를 적절히 조합하면, 고가의 GPU 자원을 추가로 구매하지 않고도 수십 가지의 특화된 AI 서비스를 동시에 운영할 수 있습니다. 위에서 제시한 7가지 예제를 토대로 여러분의 환경에 맞는 최적의 AI 서빙 아키텍처를 설계해 보시기 바랍니다.
7. 출처 및 참고문헌
- Hugging Face, "PEFT: Parameter-Efficient Fine-Tuning of Billion-Scale Models", 2023.
- Edward J. Hu et al., "LoRA: Low-Rank Adaptation of Large Language Models", ICLR 2022.
- vLLM Project, "Multi-LoRA Serving Documentation", 2024.
- Predibase, "LoRAX: Multi-LoRA Inference Server", GitHub Repository.