← 목록으로
파인튜닝한 페르소나, 한 GPU에서 동시에 서빙하기 — vLLM 멀티 LoRA 실전 가이드
gcube · hands-on

파인튜닝한 페르소나, 한 GPU에서 동시에 서빙하기 — vLLM 멀티 LoRA 실전 가이드

on this page

파인튜닝한 페르소나, 한 GPU에서 동시에 서빙하기 — vLLM 멀티 LoRA 실전 가이드

gcube Lab에서는 그동안 파인튜닝과 서빙을 각각 다뤄왔습니다.

  • [Llama 3.1 8B Unsloth/QLoRA 파인튜닝 튜토리얼] — gcube에서 처음으로 QLoRA 파인튜닝을 실습하며 라이브 버그까지 함께 잡았던 편
  • [Axolotl 분산 멀티 GPU 파인튜닝] — RTX5090 두 대로 학습 속도를 끌어올린 편
  • [vLLM vs LMDeploy vs Ollama 동시성 벤치마크] — Qwen3-8B로 서빙 엔진별 처리량을 비교한 편

이번 편은 이 두 갈래를 잇는 이야기입니다. 파인튜닝은 여러 번 해봤는데, 막상 어댑터가 여러 개 쌓이면 이런 고민이 생깁니다.

“페르소나별로 어댑터를 따로 만들었는데, 이걸 서빙할 땐 GPU를 몇 개 띄워야 하지?”

정답은 “하나면 충분하다”입니다. vLLM의 멀티 LoRA 서빙 기능을 쓰면 베이스 모델 하나에 어댑터 여러 개를 동시에 얹어, 요청마다 다른 페르소나로 응답하는 서버를 GPU 한 장으로 운영할 수 있습니다. 이번 글에서는 gcube RTX 5090 32GB 환경에서 이걸 직접 검증한 과정을 공유합니다. (실습 절차는 새 워크로드에서 처음부터 재현까지 완료했습니다.)

왜 멀티 LoRA인가

파인튜닝 프로젝트가 늘어날수록 서빙 방식은 크게 두 갈래로 나뉩니다.

  • 병합(merge) 후 서빙: LoRA를 베이스 모델에 합쳐서 독립된 모델로 배포. 페르소나 수만큼 GPU(혹은 모델 인스턴스)가 필요합니다.
  • 멀티 LoRA 서빙: 베이스 모델은 하나만 로드하고, 어댑터만 여러 개 얹어 요청마다 골라 쓰는 방식. GPU 메모리 대부분을 차지하는 건 베이스 모델이고, LoRA 어댑터는 랭크 32 기준 수십 MB 수준이라 여러 개를 얹어도 부담이 크지 않습니다.

사이드 프로젝트로 만들어둔 페르소나 어댑터가 있다면, 후자가 훨씬 경제적입니다.

실습 환경

항목
GPUgcube Tier2 RTX5090 × 1 (32GB)
베이스 이미지unsloth/unsloth:latest
베이스 모델Qwen2.5-7B-Instruct
프레임워크Unsloth(학습) + vLLM 0.16.1(서빙)

1단계: 서로 다른 어댑터 두 개 준비하기

효과를 눈으로 확인하기 위해 톤이 완전히 다른 어댑터 두 개를 학습했습니다.

  • gyaru: 갸루 말투를 쓰는 캐주얼 페르소나 (기존 gyaru-dataset 3,000개 중 300개 샘플 사용)
  • persona-b: 이모지·반말 없이 격식체만 쓰는 상담 AI 페르소나 (신규 합성 데이터 40개)

핵심은 LoRA를 베이스 모델에 병합하지 않고 어댑터 상태 그대로 저장하는 것입니다.

model = FastLanguageModel.get_peft_model(
    model, r=32,
    target_modules=["q_proj","k_proj","v_proj","o_proj",
                     "gate_proj","up_proj","down_proj"],
    lora_alpha=32, lora_dropout=0, bias="none",
    use_gradient_checkpointing="unsloth",
)
# ... SFTTrainer로 학습 ...
model.save_pretrained(output_dir)      # merge_and_unload() 호출하지 않음
tokenizer.save_pretrained(output_dir)

RTX 5090에서 300 샘플 기준 학습은 약 24초, 40 샘플은 18초 만에 끝났습니다. 저장된 디렉터리를 열어보면 adapter_model.safetensorsadapter_config.json만 있고 전체 모델 가중치는 없습니다 — 딱 필요한 만큼만 저장된 것입니다.

2단계: vLLM 멀티 LoRA 서버 기동

vLLM은 --enable-lora 플래그 하나로 멀티 LoRA 서빙을 지원합니다. --lora-modules이름=경로 형식으로 어댑터를 나열하면 됩니다.

vllm serve Qwen/Qwen2.5-7B-Instruct \
  --enable-lora \
  --max-loras 2 \
  --max-lora-rank 32 \
  --lora-modules gyaru=/workspace/adapters/gyaru \
                 persona-b=/workspace/adapters/persona_b \
  --port 8002 \
  --gpu-memory-utilization 0.85

베이스 모델(bf16) 로드에 GPU 메모리 15.4GB가 잡혔고, /v1/models를 호출하면 베이스 모델과 어댑터 2개가 각각 독립된 모델처럼 노출됩니다.

["Qwen/Qwen2.5-7B-Instruct", "gyaru", "persona-b"]

이후 요청에서는 model 필드에 어댑터 이름만 지정하면 됩니다. 별도 서버를 띄울 필요가 없습니다.

curl http://localhost:8002/v1/chat/completions \
  -d '{"model": "gyaru", "messages": [...]}'

curl http://localhost:8002/v1/chat/completions \
  -d '{"model": "persona-b", "messages": [...]}'

3단계: 검증 — 같은 질문, 다른 페르소나

동일한 질문 “오늘 날씨 어때?”를 두 어댑터에 각각 보내봤습니다.

어댑터응답
gyaru“죄송합니다, 저는 실시간 정보를 제공하지 못합니다. 현재 위치의 날씨를 확인하시려면 인터넷 검색을 추천드립니다.”
persona-b“고객님의 문의에 성심껏 답변드리겠습니다. 관련하여 도움을 드릴 수 있어 기쁩니다. 추가로 궁금하신 점이 있으시면 언제든 말씀해 주시기 바랍니다.”

같은 베이스 모델, 같은 GPU, 같은 서버 프로세스에서 나온 응답이라는 게 믿기지 않을 만큼 톤이 다릅니다. 다른 질문(“건강 관리 팁 줘”)으로도 재확인했고, 서로 다른 컨테이너에서 처음부터 재현했을 때도 완전히 동일한 문구로 응답이 나왔습니다.

4단계: 지연시간 · 동시 요청 처리

콜드 스타트: 서버가 뜬 뒤 처음 들어오는 요청은 어댑터별로 CUDA 그래프를 새로 컴파일하기 때문에 느립니다. 첫 gyaru 요청은 35초가 걸렸습니다.

웜 상태: 컴파일이 끝난 뒤로는 요청당 0.6~0.75초로 뚝 떨어집니다.

동시 요청: gyaru와 persona-b에 동시에 요청을 보내도 vLLM의 배칭 스케줄러가 알아서 묶어 처리합니다. 두 요청을 병렬로 보냈을 때 전체 소요시간은 약 1.0초 — 순차로 보냈을 때(약 1.4~1.5초)보다 오히려 빨랐고, 두 응답이 서로 섞이는 문제(cross-contamination)도 없었습니다. 새 컨테이너에서 재현했을 때도 0.997초로 거의 동일한 결과를 얻었습니다.

시나리오소요시간
최초 요청 (LoRA 컴파일 포함)35.0초
웜 상태 단건 요청0.6~0.75초
두 어댑터 동시 요청약 1.0초

정리

  • 파인튜닝한 어댑터를 병합하지 않고 저장하면, vLLM -enable-lora로 한 GPU에서 여러 페르소나를 동시에 서빙할 수 있습니다.
  • gcube RTX5090 32GB 환경에서 베이스 모델(7B, bf16) + 어댑터 2개를 문제없이 함께 로드했고, 동시 요청도 배칭으로 안정적으로 처리됐습니다.
  • 유의할 점은 최초 요청의 컴파일 콜드 스타트(수십 초) 뿐입니다. 실서비스라면 배포 직후 워밍업 요청을 미리 보내두는 것을 권장합니다.

사이드 프로젝트로 파인튜닝한 어댑터가 여러 개 쌓여 있다면, 굳이 GPU를 여러 대 쓰지 않아도 됩니다. gcube 위에서 한 장으로 충분합니다.