
LLM을 자체 호스팅하는 방법에 대한 대부분의 가이드는 가장 어려운 부분, 즉 인프라를 제대로 다루지 않습니다. CUDA 드라이버와 씨름하고, Docker 이미지를 관리하고, 오토스케일링을 설정하고, 어느새 새벽 3시에 유휴 GPU 비용을 내고 있는 자신을 발견하게 됩니다. Modal은 이 모든 것을 제거합니다. Python을 작성하고, 배포하고, URL을 얻습니다.
이 가이드는 vLLM을 추론 엔진으로 하여 Modal에서 오픈소스 LLM을 배포하는 과정을 안내합니다. 끝나갈 때쯤이면 H100 GPU에서 실행되는 라이브 OpenAI 호환 API 엔드포인트를 가지게 되며, 아무도 사용하지 않을 때는 제로 스케일링됩니다.
Modal이란 무엇인가 (그리고 LLM 배포에 왜 사용하나)?
Modal은 AI 워크로드를 위해 특별히 구축된 서버리스 컴퓨트 플랫폼입니다. AWS Lambda처럼 생각하면 되는데, GPU 지원, 초 단위 청구, Python 네이티브 개발자 경험이 추가된 것입니다. YAML도 없고, Dockerfile도 없고, Kubernetes도 없습니다. Python 스크립트에서 전체 인프라를 정의하고 한 명령으로 배포하면 됩니다.
LLM 배포를 위한 선택지가 된 이유는 다음과 같습니다:
- 제로 스케일링 청구 — 엔드포인트가 요청을 처리하지 않을 때는 비용이 들지 않음
- 초 단위 GPU 가격 — H100은 시간당 약 $3.95, A100 80GB는 시간당 약 $2.50, 초 단위로 청구
- 1초 미만의 콜드 스타트 — 특히 메모리 스냅샷으로 컨테이너가 빠르게 시작됨
- 월 $30 무료 크레딧 — 신용카드 청구 없이 실험하기에 충분함
- DevOps 불필요 — Docker 빌드, Terraform, 클러스터 관리 없음
LLM을 로컬에서 실행하다가 서버를 관리하지 않고도 올바른 API를 제공하고 싶다면, Modal이 가장 빠른 경로입니다.
Modal vs. RunPod vs. Lambda
결론: Modal은 버스트성 워크로드와 개발에서 승리합니다. GPU 사용률이 지속적으로 40%를 초과한다면, RunPod나 Lambda의 전용 인스턴스가 더 저렴합니다. 그 외 모든 경우 — 프로토타이핑, 간헐적 API, 데모 — Modal의 제로 스케일링 모델이 실제 비용을 절감합니다.
사전 요구사항
시작하기 전에 세 가지가 필요합니다:
- Python 3.10 이상이 로컬에 설치되어 있음
- Modal 계정 — modal.com에서 무료 가입
- Hugging Face 계정 — 모델 접근용 (대부분의 모델은 게이트됨)
이게 전부입니다. 로컬 머신에 GPU가 없어도, CUDA 도구 키트가 없어도, Docker가 없어도 됩니다.
1단계: Modal 설치 및 인증
터미널을 열고 Modal Python 패키지를 설치하세요:
pip install modal
그런 다음 로컬 환경을 Modal 계정에 연결하는 설정 명령을 실행하세요:
modal setup
이렇게 하면 인증을 위한 브라우저 창이 열립니다. 확인하면 Modal이 토큰을 로컬에 저장합니다. 다시 할 필요가 없습니다.
2단계: 컨테이너 이미지 정의
Modal 컨테이너는 Python에서 정의됩니다. 기본 이미지를 지정하고, 의존성을 설치하고, 환경 변수를 설정합니다 — 모두 코드로. app.py라는 파일을 만드세요:
import modal
image = (
modal.Image.from_registry("nvidia/cuda:12.8.0-devel-ubuntu22.04")
.apt_install("git", "curl")
.pip_install(
"vllm==0.6.3",
"hf-transfer==0.1.8",
"huggingface-hub==0.24.0",
)
.env({"HF_HUB_ENABLE_HF_TRANSFER": "1"})
)
주목할 점이 몇 가지 있습니다. Dockerfile가 없습니다 — modal.Image 체인이 완전히 대체합니다. 기본 이미지에는 NVIDIA CUDA 12.8이 Ubuntu 22.04와 함께 포함되어 있으며, 그 위에 vLLM과 Hugging Face Hub 클라이언트를 설치합니다.
3단계: 볼륨으로 모델 스토리지 구성
LLM 가중치는 크습니다 (7B 파라미터 모델은 fp16에서 약 14GB). 컨테이너가 시작될 때마다 다운로드하고 싶지 않을 것입니다. Modal Volumes는 컨테이너에 직접 마운트되는 영구 스토리지를 제공합니다:
volume = modal.Volume.from_name("llm-weights", create_if_missing=True)
MODEL_NAME = "Qwen/Qwen3-4B-Thinking-FP8"
MODEL_DIR = "/model"
여기서는 Qwen3-4B-Thinking (FP8)을 사용합니다 — 빠르고 유능하며 단일 GPU에 맞는 양자화된 40억 파라미터 모델입니다. 이를 Llama 3.1 8B, Mistral 7B, vLLM이 지원하는 모든 Hugging Face 모델로 바꿀 수 있습니다.
왜 FP8인가요? fp16에 비해 메모리 사용량을 대략 절반으로 줄여줍니다. 이는 같은 GPU에서 더 큰 모델을 실행하거나 더 저렴한 GPU에서 더 작은 모델을 실행할 수 있다는 의미입니다. 양자화 트레이드오프가 궁금하다면, LLM을 로컬로 실행하는 방법에 대한 가이드에서 정밀도 형식을 자세히 다룹니다.
4단계: vLLM 서버 함수 생성
Modal의 마법이 여기서 일어납니다. Python 함수를 GPU 요구사항, 스케일링 설정, 웹 서버 주석으로 장식합니다. Modal이 나머지를 모두 처리합니다:
@modal.App.function(
image=image,
gpu="H100:1",
timeout=3600,
concurrency_limit=1,
volumes={MODEL_DIR: volume},
)
def run_vllm_server():
import subprocess
subprocess.Popen([
"python", "-m", "vllm.entrypoints.openai.api_server",
"--model", MODEL_NAME,
"--tensor-parallel-size", "1",
"--gpu-memory-utilization", "0.9",
"--enforce-eager",
"--port", "8000",
])
import time
time.sleep(float('inf'))
@modal.App.web_server(port=8000)
def web():
return run_vllm_server()
주요 데코레이터를 살펴보겠습니다:
gpu="H100:1"— 단일 H100 GPU를 요청합니다. 더 저렴한 추론을 위해"A100-80GB:1"로 변경하거나, 70B 이상의 모델을 위해"H100:2"로 변경하세요scaledown_window=15 * MINUTES— 마지막 요청 후 15분 동안 컨테이너를 따뜻하게 유지한 다음 제로로 스케일링합니다@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.py
Modal이 컨테이너 이미지를 빌드하고 레지스트리에 푸시한 다음 라이브 URL을 반환합니다:
✓ Deployed app 'llm-server' -> https://your-username--llm-server-web.modal.run
모델 가중치를 볼륨에 다운로드하기 때문에 첫 번째 배포는 몇 분이 걸립니다. 후속 배포 (및 콜드 스타트)는 가중치가 캐시되어 있으므로 훨씬 빠릅니다.
개발의 경우 modal serve app.py를 대신 사용하세요 — 파일 변경 시 핫 리로드되고 임시 URL을 제공합니다.
6단계: 엔드포인트 호출 (OpenAI 호환)
배포된 vLLM 서버는 /v1/chat/completions에서 OpenAI 호환 API를 노출합니다. 표준 OpenAI Python SDK를 사용하여 호출할 수 있습니다 — Modal 엔드포인트에 기본 URL을 지정하기만 하면 됩니다:
from openai import OpenAI
client = OpenAI(
api_key="not-needed",
base_url="https://your-username--llm-server-web.modal.run/v1",
)
response = client.chat.completions.create(
model="Qwen3-4B-Thinking-FP8",
messages=[{"role": "user", "content": "What's 2+2?"}],
temperature=0.7,
)
print(response.choices[0].message.content)
이는 curl에서도 작동합니다:
curl https://your-username--llm-server-web.modal.run/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"Qwen3-4B-Thinking-FP8","messages":[{"role":"user","content":"What is 2+2?"}]}'
OpenAI 호환 API를 지원하는 모든 도구가 작동합니다 — LangChain, LlamaIndex, 자신의 앱. 여러 LLM 엔드포인트에 요청을 라우팅하는 경우, LLM 게이트웨이 도구가 페일오버 및 로드 밸런싱을 관리하는 데 도움이 될 수 있습니다.
비용 최적화 팁
Modal의 초 단위 청구는 이미 시간 단위 가격 책정보다 더 효율적이지만, 더 많은 것을 짜낼 수 있습니다:
1. FP8 양자화 사용
FP8 모델은 fp16 카운터파트의 대략 절반의 VRAM을 사용합니다. Qwen3-8B FP8은 단일 H100에 맞지만, fp16 버전은 그 GPU의 80GB 대부분이 필요합니다. VRAM이 적을수록 더 저렴한 GPU (A100 40GB, L40S)를 더 작은 모델에 사용할 수 있습니다.
2. Scaledown 윈도우 튜닝
scaledown_window 파라미터는 마지막 요청 후 컨테이너가 따뜻하게 유지되는 기간을 제어합니다:
# 개발용: 빠른 재시작, 비용 최소화
@modal.function(scaledown_window=5 * MINUTES)
# 프로덕션: 정기적인 트래픽을 위한 더 적은 콜드 스타트
@modal.function(scaledown_window=30 * MINUTES)
3. 올바른 GPU 선택
H100을 기본으로 사용하지 마세요. 더 작은 모델은 필요하지 않습니다:
| 모델 크기 | 권장 GPU | 시간당 비용 |
|---|---|---|
| 4B-7B | A100 40GB | ~$1.60/hr |
| 13B-30B | H100 | ~$3.95/hr |
| 70B+ | H100x2 또는 A100 80GB | ~$2.50-7.90/hr |
4. 프롬프트 캐싱 활성화
워크로드가 반복되는 시스템 프롬프트나 공유 접두사를 포함하는 경우, vLLM의 자동 접두사 캐싱이 레이턴시와 계산을 크게 줄일 수 있습니다. vLLM 제공 명령에 --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
import os
image = (
modal.Image.from_registry("nvidia/cuda:12.8.0-devel-ubuntu22.04")
.apt_install("git", "curl")
.pip_install(
"vllm==0.6.3",
"hf-transfer==0.1.8",
"huggingface-hub==0.24.0",
)
.env({"HF_HUB_ENABLE_HF_TRANSFER": "1"})
)
volume = modal.Volume.from_name("llm-weights", create_if_missing=True)
MODEL_NAME = "Qwen/Qwen3-4B-Thinking-FP8"
MODEL_DIR = "/model"
app = modal.App(name="llm-server", image=image)
MINUTES = 60
@app.function(
gpu="H100:1",
timeout=3600,
volumes={MODEL_DIR: volume},
)
def run_vllm():
import subprocess
import time
subprocess.Popen([
"python", "-m", "vllm.entrypoints.openai.api_server",
"--model", MODEL_NAME,
"--tensor-parallel-size", "1",
"--gpu-memory-utilization", "0.9",
"--enforce-eager",
"--port", "8000",
])
time.sleep(float('inf'))
@app.web_server(port=8000, startup_timeout=600)
def web():
return run_vllm()
modal deploy app.py로 배포하고, MODEL_NAME을 모든 Hugging Face 모델로 바꾸면 배포됩니다.
자주 묻는 질문
Modal에서 LLM을 실행하는 비용은 얼마나 되나요?
GPU와 엔드포인트가 따뜻하게 유지되는 기간에 따라 다릅니다. H100의 Qwen3-4B는 활성 사용 시간당 약 $3.95가 소요됩니다. 제로 스케일링과 15분 scaledown 윈도우를 사용하면, 가볍게 사용되는 엔드포인트는 월 $5~15가 소요될 수 있습니다. 월 $30 무료 크레딧은 많은 실험을 커버합니다.
...