
Modal로 LLM 배포하기: pip install부터 프로덕션 엔드포인트까지
LLM 자체 호스팅에 대한 대부분의 가이드는 가장 어려운 부분인 인프라 구성을 대충 넘깁니다. CUDA 드라이버와 씨름하고, Docker 이미지를 관리하며, 자동 스케일링을 설정해도 결국 새벽 3시에 유휴 상태인 GPU 비용을 지불하게 되는 경우가 많습니다. Modal은 이러한 모든 번거로움을 제거합니다. 파이썬 코드를 작성하고 배포하면 URL이 즉시 제공됩니다.
이 가이드에서는 추론 엔진으로 vLLM을 사용하여 Modal에서 오픈소스 LLM을 배포하는 과정을 안내합니다. 끝날 때쯤이면 아무도 사용하지 않을 때는 자동으로 종료되어 비용이 발생하지 않는(scale-to-zero), H100 GPU에서 실행되는 실시간 OpenAI 호환 API 엔드포인트를 갖게 될 것입니다.
Modal이란 무엇이며 왜 LLM에 사용해야 할까요?
Modal은 AI 워크로드를 위해 특별히 구축된 서버리스 컴퓨팅 플랫폼입니다. AWS Lambda와 비슷하지만 GPU 지원, 초 단위 과금, 그리고 파이썬 중심의 개발자 경험을 제공한다는 점이 다릅니다. YAML 파일도, Dockerfile도, 쿠버네티스도 필요 없습니다. 전체 인프라를 파이썬 스크립트로 정의하고 단일 명령어로 배포할 수 있습니다.
LLM 배포의首选 선택지가 된 이유는 다음과 같습니다:
- 사용량 기반 과금(Scale-to-zero), 엔드포인트가 요청을 처리하지 않을 때는 요금이 전혀 발생하지 않습니다
- 초 단위 GPU 가격, H100은 시간당 약 $3.95, A100 80GB는 시간당 약 $2.50이며 초 단위로 청구됩니다
- 1초 미만의 콜드 스타트, 특히 메모리 스냅샷을 사용하면 컨테이너가 빠르게 시작됩니다
- 월 $30 무료 크레딧, 신용카드 등록 없이도 충분히 실험해 볼 수 있는 금액입니다
- DevOps 불필요, Docker 빌드, Terraform, 클러스터 관리가 필요 없습니다
로컬에서 LLM을 실행해 보았지만 서버 관리 없이 적절한 API를 제공하고 싶다면, Modal이 가장 빠른 해결책입니다.
Modal vs. RunPod vs. Lambda 비교
| 기능 | Modal | RunPod | Lambda |
|---|---|---|---|
| 과금 모델 | 초 단위, 사용량 기반(scale-to-zero) | 초 단위, 최소 요금 적용 | 시간 단위, 항상 켜짐(always-on) |
| 콜드 스타트 | 2-4초 | 6-12초 (대형 인스턴스) | 해당 없음 (지속적) |
| GPU 가용성 | H100, A100, L40S, T4 | A100, H100, A6000 | H100, A100 |
| 인프라 구성 | 순수 파이썬, 설정 파일 불필요 | Docker 기반, 더 많은 제어 권한 | 전체 VM 접근 가능 |
| 무료 티어 | 월 $30 크레딧 | 없음 | 없음 |
| 용도 | 변동성이 큰 작업/개발 환경 | 안정적인 추론 트래픽 | 높은 활용도의 학습 작업 |
결론: Modal은 변동성이 큰 워크로드와 개발 환경에서 우위를 점합니다. GPU 활용률이 지속적으로 40%를 초과한다면 RunPod나 Lambda의 전용 인스턴스가 더 저렴합니다. 프로토타이핑, 간헐적인 API, 데모 등 그 외의 모든 경우에서는 Modal의 사용량 기반 과금 모델이 실질적인 비용을 절감해 줍니다.
사전 준비 사항
시작하기 전에 다음 세 가지가 필요합니다:
- 로컬에 설치된 Python 3.10+
- Modal 계정, modal.com에서 무료로 가입하세요
- Hugging Face 계정, 모델 접근용 (대부분의 모델은 접근 제한이 있음)
이게 전부입니다. 로컬 머신에 GPU가 없어도, CUDA 툴킷이나 Docker가なくても 됩니다.
1단계: Modal 설치 및 인증
터미널을 열고 Modal 파이썬 패키지를 설치합니다:
pip install modal그런 다음 설정 명령어를 실행하여 로컬 환경을 Modal 계정에 연결합니다:
modal setup이 명령어는 인증을 위해 브라우저 창을 엽니다. 확인을 완료하면 Modal이 로컬에 토큰을 저장합니다. 다시 이 과정을 반복할 필요는 없습니다.
2단계: 컨테이너 이미지 정의
Modal 컨테이너는 파이썬으로 정의됩니다. 기본 이미지를 지정하고, 의존성을 설치하며, 환경 변수를 설정하는 모든 작업을 코드로 수행합니다. app.py라는 파일을 생성하세요:
import modal
# Define the container image with CUDA, Python, and vLLM
vllm_image = (
modal.Image.from_registry(
"nvidia/cuda:12.8.0-devel-ubuntu22.04", add_python="3.12"
)
.entrypoint([])
.pip_install(
"vllm==0.13.0",
"huggingface-hub==0.36.0",
)
)
app = modal.App("llm-endpoint", image=vllm_image)몇 가지 주목할 점이 있습니다. Dockerfile이 없다는 점인데, modal.Image 체인이 이를 완전히 대체합니다. 기본 이미지에는 Ubuntu 22.04와 NVIDIA CUDA 12.8이 포함되어 있으며, 그 위에 vLLM과 Hugging Face Hub 클라이언트를 설치합니다.
3단계: 볼륨을 활용한 모델 스토리지 구성
LLM 가중치는 매우 큽니다(7B 파라미터 모델은 fp16 기준 약 14GB). 컨테이너가 시작될 때마다 다운로드할 수는 없습니다. Modal 볼륨(Volumes)은 컨테이너에 직접 마운트되는 영구 스토리지를 제공합니다:
# Persistent volumes for caching model weights
hf_cache = modal.Volume.from_name("huggingface-cache", create_if_missing=True)
vllm_cache = modal.Volume.from_name("vllm-cache", create_if_missing=True)
MODEL_NAME = "Qwen/Qwen3-4B-Thinking-2507-FP8"
MODEL_REVISION = "953532f942706930ec4bb870569932ef63038fdf"여기서는 빠르고 성능이 뛰어나며 단일 GPU에 적합한 양자화된 40억 파라미터 모델인 Qwen3-4B-Thinking (FP8)을 사용합니다. 이를 Llama 3.1 8B, Mistral 7B 또는 vLLM이 지원하는 다른 Hugging Face 모델로 교체할 수 있습니다.
왜 FP8일까요? FP8은 fp16 대비 메모리 사용량을 거의 절반으로 줄여주므로, 동일한 GPU에서 더 큰 모델을 실행하거나 더 저렴한 GPU에서 작은 모델을 실행할 수 있습니다. 양자화의 장단점에 대해 궁금하다면, 정밀도 포맷을 자세히 다루는 LLM 로컬 실행 가이드를 참조하세요.
4단계: vLLM 서버 함수 생성
이 부분이 Modal의 핵심 마법이 작동하는 곳입니다. 파이썬 함수에 GPU 요구 사항, 스케일링 설정, 웹 서버 어노테이션을 데코레이터로 추가합니다. 나머지는 Modal이 모두 처리합니다:
N_GPU = 1
MINUTES = 60
VLLM_PORT = 8000
@app.function(
gpu=f"H100:{N_GPU}",
scaledown_window=15 * MINUTES,
timeout=10 * MINUTES,
volumes={
"/root/.cache/huggingface": hf_cache,
"/root/.cache/vllm": vllm_cache,
},
)
@modal.concurrent(max_inputs=32)
@modal.web_server(port=VLLM_PORT, startup_timeout=10 * MINUTES)
def serve():
import subprocess
cmd = [
"vllm", "serve",
MODEL_NAME,
"--revision", MODEL_REVISION,
"--served-model-name", MODEL_NAME,
"--host", "0.0.0.0",
"--port", str(VLLM_PORT),
"--tensor-parallel-size", str(N_GPU),
"--enforce-eager", # Faster cold starts
]
subprocess.Popen(" ".join(cmd), shell=True)주요 데코레이터를 살펴보면:
gpu="H100:1", 단일 H100 GPU를 요청합니다. 더 저렴한 추론을 위해"A100-80GB:1"로 변경하거나, 70B 이상 모델을 위해"H100:2"로 변경할 수 있습니다scaledown_window=15 * MINUTES, 마지막 요청 후 15분 동안 컨테이너를 유지한 다음 사용량 기반으로 종료(scale-to-zero)합니다@modal.concurrent(max_inputs=32), 컨테이너당 최대 32개의 동시 요청을 허용합니다(vLLM이 내부적으로 배칭을 처리함)@modal.web_server(port=8000), vLLM HTTP 서버를 Modal 웹 엔드포인트로 직접 노출합니다--enforce-eager, 더 빠른 콜드 스타트를 위해 CUDA 그래프 컴파일을 건너뜹니다(단, 최대 처리량은 약간 낮아질 수 있음)
scaledown_window는 비용 조절의 주요 레버입니다. 개발용으로는 5분으로, 정기적인 트래픽이 예상되는 프로덕션 API에는 15-30분으로 설정하세요.
5단계: 프로덕션에 배포
명령어 하나면 충분합니다:
modal deploy app.pyModal이 컨테이너 이미지를 빌드하고 레지스트리에 푸시한 후 실시간 URL을 반환합니다:
✓ Created objects.
├── 🔨 Created mount /app.py
├── 🔨 Created volume huggingface-cache
├── 🔨 Created volume vllm-cache
└── 🔨 Created web function serve => https://your-workspace--llm-endpoint-serve.modal.run첫 배포는 볼륨에 모델 가중치를 다운로드하므로 몇 분 정도 소요됩니다. 가중치가 캐시되므로 이후 배포(및 콜드 스타트)는 훨씬 빠릅니다.
개발 중에는 대신 modal serve app.py를 사용하세요. 파일 변경 시 핫 리로드(hot-reload)되며 임시 URL을 제공합니다.
6단계: 엔드포인트 호출하기 (OpenAI 호환)
배포된 vLLM 서버는 /v1/chat/completions에서 OpenAI 호환 API를 노출합니다. 표준 OpenAI 파이썬 SDK를 사용하여 호출할 수 있으며, base URL만 Modal 엔드포인트로 지정하면 됩니다:
from openai import OpenAI
client = OpenAI(
api_key="not-needed", # vLLM doesn't require auth by default
base_url="https://your-workspace--llm-endpoint-serve.modal.run/v1",
)
response = client.chat.completions.create(
model="Qwen/Qwen3-4B-Thinking-2507-FP8",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain what vLLM is in two sentences."},
],
temperature=0.7,
max_tokens=256,
)
print(response.choices[0].message.content)curl로도 작동합니다:
curl -X POST https://your-workspace--llm-endpoint-serve.modal.run/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen3-4B-Thinking-2507-FP8",
"messages": [{"role": "user", "content": "Hello!"}],
"max_tokens": 128
}'OpenAI 호환 API를 지원하는 모든 도구(LangChain, LlamaIndex, 자체 앱 등)에서 사용할 수 있습니다. 여러 LLM 엔드포인트 간에 요청을 라우팅한다면, 장애 조치(failover)와 로드 밸런싱을 관리하는 데 LLM 게이트웨이 도구가 도움이 될 수 있습니다.
비용 최적화 팁
Modal의 초 단위 과금은 이미 시간 단위 과금보다 효율적이지만, 다음과 같은 방법으로 비용을 더욱 절감할 수 있습니다:
1. FP8 양자화 사용
FP8 모델은 fp16 버전 대비 VRAM 사용량을 약 절반으로 줄입니다. FP8의 Qwen3-8B는 단일 H100에 적합하지만, fp16 버전은 해당 GPU의 80GB 대부분을 필요로 합니다. VRAM 사용량이 적으면 소형 모델에 더 저렴한 GPU(A100 40GB, L40S)를 사용할 수 있습니다.
2. Scaledown Window 조정
scaledown_window 매개변수는 마지막 요청 후 컨테이너가 얼마나 오랫동안 대기 상태(warm)로 유지될지 결정합니다:
| 시나리오 | 권장 시간 | 이유 |
|---|---|---|
| 개발/테스트 | 5분 | 비용 절감, 콜드 스타트 허용 가능 |
| 내부 API (간헐적) | 10-15분 | 비용과 지연 시간 균형 |
| 프로덕션 (정기 트래픽) | 20-30분 | 콜드 스타트 최소화 |
| 고트래픽 프로덕션 | min_containers=1 사용 | 항상 하나의 컨테이너 유지 |
3. 적절한 GPU 선택
무조건 H100을 선택하지 마세요. 작은 모델은 그럴 필요가 없습니다:
| 모델 크기 | 권장 GPU | 시간당 예상 비용 |
|---|---|---|
| 1-4B 파라미터 | L4 또는 T4 | $0.59 - $0.80 |
| 7-8B 파라미터 | A10 또는 L40S | $1.10 - $1.95 |
| 13-14B 파라미터 | A100 40GB | $2.10 |
| 30-70B 파라미터 | A100 80GB 또는 H100 | $2.50 - $3.95 |
| 70B+ 파라미터 | H100 x2 | $7.90 |
4. 프롬프트 캐싱 활성화
워크로드에 반복적인 시스템 프롬프트나 공유 접두사가 포함된 경우, vLLM의 자동 접두사 캐싱(prefix caching)이 지연 시간과 컴퓨팅 비용을 크게 줄일 수 있습니다. vLLM serve 명령어에 --enable-prefix-caching를 추가하여 활성화할 수 있습니다. 다양한 제공업체에서의 캐싱 작동 방식에 대해 더 깊이 알아보려면 LLM 프롬프트 캐싱 가이드를 확인하세요.
5. 콜드 스타트 최적화를 위해 --enforce-eager 사용
기본적으로 vLLM은 시작 시 CUDA 그래프를 컴파일하는데, 이는 추가로 1-3분이 소요됩니다. --enforce-eager 플래그는 이 컴파일을 건너뜹니다. 최대 처리량의 약 10-15%를 희생하는 대신 콜드 스타트 시간을 획기적으로 단축합니다. 원시 처리량보다 지연 시간이 더 중요한 변동성 있는 워크로드의 경우, 거의 항상 올바른 선택입니다.
한 걸음 더: 파인튜닝된 모델
기본 모델 배포에 익숙해지면, 자연스럽게 다음 단계는 자체 파인튜닝 버전을 배포하는 것입니다. 워크플로는 동일하며, MODEL_NAME을 Hugging Face 레포지토리 또는 파인튜닝된 가중치가 포함된 Modal 볼륨으로 지정하기만 하면 됩니다.
Modal은 자사의 GPU에서 파인튜닝 작업을 직접 실행하는 것도 지원합니다. Modal에서 LoRA 어댑터를 학습하고, 볼륨에 저장한 후, 병합된 모델을 배포하는 모든 과정을 플랫폼 밖으로 나가지 않고 수행할 수 있습니다. 학습 측면에 대한 자세한 내용은 LLM 파인튜닝 가이드를 참조하세요.
전체 app.py 코드
복사해서 바로 사용할 수 있는 전체 배포 스크립트는 다음과 같습니다:
import modal
# --- Image Definition ---
vllm_image = (
modal.Image.from_registry(
"nvidia/cuda:12.8.0-devel-ubuntu22.04", add_python="3.12"
)
.entrypoint([])
.pip_install("vllm==0.13.0", "huggingface-hub==0.36.0")
)
# --- Volumes for Model Caching ---
hf_cache = modal.Volume.from_name("huggingface-cache", create_if_missing=True)
vllm_cache = modal.Volume.from_name("vllm-cache", create_if_missing=True)
# --- Model Config ---
MODEL_NAME = "Qwen/Qwen3-4B-Thinking-2507-FP8"
MODEL_REVISION = "953532f942706930ec4bb870569932ef63038fdf"
app = modal.App("llm-endpoint", image=vllm_image)
N_GPU = 1
MINUTES = 60
VLLM_PORT = 8000
@app.function(
gpu=f"H100:{N_GPU}",
scaledown_window=15 * MINUTES,
timeout=10 * MINUTES,
volumes={
"/root/.cache/huggingface": hf_cache,
"/root/.cache/vllm": vllm_cache,
},
)
@modal.concurrent(max_inputs=32)
@modal.web_server(port=VLLM_PORT, startup_timeout=10 * MINUTES)
def serve():
import subprocess
cmd = [
"vllm", "serve",
MODEL_NAME,
"--revision", MODEL_REVISION,
"--served-model-name", MODEL_NAME,
"--host", "0.0.0.0",
"--port", str(VLLM_PORT),
"--tensor-parallel-size", str(N_GPU),
"--enforce-eager",
]
subprocess.Popen(" ".join(cmd), shell=True)modal deploy app.py로 배포하고, MODEL_NAME을 원하는 Hugging Face 모델로 바꾸면 즉시 사용 가능합니다.
자주 묻는 질문 (FAQ)
Modal에서 LLM을 실행하는 데 드는 비용은 얼마인가요?
GPU 종류와 엔드포인트가 대기 상태(warm)로 유지되는 시간에 따라 달라집니다. H100에서의 Qwen3-4B는 활성 사용 시 시간당 약 $3.95가 발생합니다. 사용량 기반 과금과 15분의 scaledown window를 적용하면, 사용량이 적은 엔드포인트의 월 비용은 $5-15 정도일 수 있습니다. 월 $30 무료 크레딧으로 상당한 실험이 가능합니다.
Modal은 사용량 기반(scale-to-zero)으로 확장되나요?
네, 이것이 주요 장점 중 하나입니다. scaledown_window 기간 동안 요청이 없으면 컨테이너가 종료되고 과금이 중단됩니다. 다음 요청이 들어오면 콜드 스타트가触发됩니다(모델 크기와 --enforce-eager 사용 여부에 따라 일반적으로 2-10초 소요).
Modal에 Llama 3.1이나 Mistral을 배포할 수 있나요?
물론입니다. MODEL_NAME 상수를 vLLM이 지원하는 모든 모델(meta-llama/Llama-3.1-8B-Instruct, mistralai/Mistral-7B-Instruct-v0.3 등 Hugging Face의 수백 가지 모델)로 교체하면 됩니다. 70B 이상 모델의 경우 N_GPU를 2로 변경하고 gpu="H100:2"를 사용하세요.
콜드 스타트 시간은 RunPod와 비교해 어떻게 되나요?
Modal의 콜드 스타트는 컨테이너 자체에 대해 일반적으로 2-4초가 소요되며, 여기에 모델 로딩 시간이 추가됩니다. 볼륨에 캐시된 모델 가중치와 --enforce-eager를 사용하면 7-8B 모델의 총 소요 시간은 10-30초 정도입니다. RunPod의 서버리스 콜드 스타트는 캐시된 경우 200ms 미만에서 대형 컨테이너의 경우 6-12초까지 다양하지만, 항상 켜져 있는(always-on) 모델은 콜드 스타트가 전혀 없습니다.
Modal의 vLLM 엔드포인트는 정말 OpenAI와 호환되나요?
네. vLLM은 OpenAI가 사용하는 것과 동일한 /v1/chat/completions, /v1/completions, /v1/models 엔드포인트를 구현합니다. 공식 openai 파이썬 SDK의 URL을 Modal URL로 지정하면 별도 설정 없이 바로 작동합니다. 스트리밍, 함수 호출, JSON 모드도 모두 지원됩니다.
로컬 머신에 GPU가 필요하나요?
아니요. 로컬 머신은 Modal CLI만 실행합니다. 모든 GPU 작업은 Modal의 클라우드 인프라에서 발생합니다. 필요하면 크롬북에서도 배포할 수 있습니다.
엔드포인트에 인증을 추가하려면 어떻게 해야 하나요?
Modal 웹 엔드포인트는 기본적으로 공개됩니다. 프로덕션 환경에서는 애플리케이션 코드에 간단한 API 키 확인 로직을 추가하거나, Modal의 내장 웹 인증 기능을 사용하세요. 또한 인증, 속도 제한(rate limiting) 및 라우팅을 처리하는 LLM 게이트웨이를 프록시 계층으로 설정할 수도 있습니다.
modal serve와 modal deploy의 차이점은 무엇인가요?
modal serve는 코드 편집 시 핫 리로드되는 임시 엔드포인트를 생성하므로 개발에 적합합니다. modal deploy는 안정적인 URL을 가진 지속적이고 프로덕션 준비가 된 엔드포인트를 생성합니다. 반복 작업 중에는 serve를 사용하고, 출시 준비가 되면 deploy를 사용하세요.
vLLM 대신 SGLang을 사용할 수 있나요?
네. Modal 문서에는 vLLM과 함께 SGLang 예제도 포함되어 있습니다. SGLang은 디코드(decode) 중심의 워크로드와 소형 모델에서 오버헤드가 더 낮은 경향이 있습니다. vLLM은 프리필(prefill)이 많은 혼합 워크로드에서 일반적으로 더 낫습니다. 둘 다 OpenAI 호환 엔드포인트를 생성합니다.
Railway나 Render에 배포하는 것과 비교하면 어떻게 되나요?
Railway, Render, Fly.io와 같은 플랫폼은 웹 앱에 훌륭하지만 GPU 인스턴스를 제공하지 않습니다. Modal은 초 단위 과금과 자동 스케일링을 갖춘 GPU 워크로드를 위해 특화되어 설계되었습니다. LLM을 서빙해야 한다면 Modal(또는 RunPod)이 적합한 도구이며, 전통적인 PaaS 플랫폼으로는 이를 수행할 수 없습니다.