适用场景
手里有一个开源模型(例如 Llama、Qwen、Mistral 系列),想把它变成 HTTP API 给内部系统或产品调用;流量不稳定,白天高峰、夜里几乎没人用,不想为此长期养一台 GPU 服务器;团队没有 Kubernetes 运维精力,希望把镜像构建、GPU 调度、缩容到零都交给平台。Modal 的定位就是这类 Serverless GPU 场景:用 Python 装饰器定义函数,平台负责按请求拉起容器、跑完再回收。
环境与前置条件
- 操作系统:macOS、Linux,或 Windows + WSL2。
- Python:3.10 及以上,具体以 Modal 官方文档当前版本要求为准。
- 本地工具:
pip、git、一个终端。本地不需要 GPU。 - 账号:Modal 账号,首次使用会生成 token 并写入本地配置文件。
- 磁盘:本地只需放代码,模型权重存在 Modal Volume 里,不需要下载到本机。
- 网络:容器内需要能访问 Hugging Face 拉取权重。若网络受限,可在合规前提下配置镜像源,并注意速率限制。
- 显存规划:7B 级别模型做 FP16 推理通常需要 16GB 以上显存;量化后可以更低。具体占用和模型结构、序列长度有关,以实测为准。
分步骤部署
步骤 1:安装 Modal CLI 并登录
```bash
python -m venv .venv
source .venv/bin/activate # Windows 用 .venv\Scripts\activate
pip install modal
modal setup
```
modal setup 会引导浏览器授权。成功后本地会生成 ~/.modal.toml。可以用下面命令确认:
```bash
modal profile list
```
能看到当前 profile 即登录成功。
步骤 2:创建项目文件
```bash
mkdir modal-llm-api && cd modal-llm-api
touch app.py bench.py
```
步骤 3:定义镜像与模型缓存 Volume
编辑 app.py:
```python
import modal
app = modal.App("llm-inference-api")
MODEL_NAME = "Qwen/Qwen2.5-7B-Instruct" # 换成你要部署的模型,以模型卡说明为准
hf_cache = modal.Volume.from_name("hf-cache", create_if_missing=True)
image = (
modal.Image.debian_slim(python_version="3.11") # 以官方支持的版本为准
.pip_install(
"vllm",
"fastapi",
"uvicorn",
"huggingface_hub",
)
.env({"HF_HOME": "/root/.cache/huggingface"})
)
```
这段在做什么:debian_slim 是基础镜像;pip_install 在镜像构建阶段执行,把 vLLM 等依赖装进去;Volume 用来持久化 Hugging Face 缓存,模型权重只下载一次,后续冷启动直接挂载,省掉重复下载时间。首次构建会花几分钟,输出里能看到镜像层构建日志。
步骤 4:写 GPU 函数,启动 OpenAI 兼容服务
在 app.py 里追加:
```python
@app.function(
image=image,
gpu="A10G", # GPU 型号以官方当前可用列表为准,也可选 T4、L4、A100 等
volumes={"/root/.cache/huggingface": hf_cache},
timeout=60 * 30,
scaledown_window=60 * 5, # 空闲 5 分钟缩容到零;旧版本参数名为 container_idle_timeout
min_containers=0, # 需要常驻预热可调大;旧版本参数名为 keep_warm
)
@modal.web_server(port=8000, startup_timeout=60 * 20)
def serve():
import subprocess
subprocess.Popen(
[
"python", "-m", "vllm.entrypoints.openai.api_server",
"--model", MODEL_NAME,
"--host", "0.0.0.0",
"--port", "8000",
"--max-model-len", "8192",
]
)
```
这里的关键点:
gpu指定容器用什么卡,按官方当前可用型号选择。volumes把模型缓存挂进容器。scaledown_window控制空闲多久回收容器。调大能减少冷启动,但会增加空闲成本。min_containers=0表示可以缩到零;如果对首包延迟敏感,可以设为 1 保持一个热实例。@modal.web_server把容器内的 8000 端口暴露成 HTTPS 端点。函数体用Popen启动 vLLM,自己不阻塞,平台会探测端口是否就绪。- vLLM 自带
/v1/chat/completions、/v1/completions、/v1/models,基本兼容 OpenAI 接口。
如果要用 FastAPI 自己写路由,可以把 @modal.web_server 换成 @modal.fastapi_endpoint(旧版本里叫 web_endpoint,以官方文档当前版本为准),然后在函数里返回 FastAPI app。
步骤 5:本地热重载测试
```bash
modal serve app.py
```
终端会打印一个 https://...modal.run 的临时地址。首次运行会构建镜像、拉取模型,等待时间取决于模型大小和网络。看到地址后,另开一个终端:
```bash
curl -s https://<你的临时端点>/v1/models
```
返回 JSON 里包含模型名称,说明服务已经起来。
步骤 6:部署到生产
```bash
modal deploy app.py
```
部署完成后会输出一个持久端点 URL。这个 URL 不会因为本地终端关闭而失效。
步骤 7:用 OpenAI SDK 调用
```python
from openai import OpenAI
client = OpenAI(
base_url="https://<你的持久端点>/v1",
api_key="unused",
若开启了端点鉴权,按官方文档加请求头
default_headers={"Modal-Key": "...", "Modal-Secret": "..."},
)
resp = client.chat.completions.create(
model="Qwen/Qwen2.5-7B-Instruct",
messages=[{"role": "user", "content": "用三句话解释什么是 Serverless。"}],
max_tokens=128,
)
print(resp.choices[0].message.content)
```
如果能打印出模型回复,整条链路就通了。
步骤 8:调冷启动与并发
冷启动优化可以从几处入手:
- 模型权重走 Volume 缓存,避免每次重新下载。
- 镜像依赖尽量稳定,减少重建频率。
- 适当调大
scaledown_window,让空闲容器多留一会儿。 - 对延迟敏感的接口,设置
min_containers=1保持热实例。
并发方面,早期版本用 allow_concurrent_inputs 参数控制单容器并发请求数,新版本提供 @modal.concurrent(max_inputs=N) 装饰器,具体写法以官方文档当前版本为准。vLLM 本身支持AI 词典:连续批处理">连续批处理,单容器并发调高通常能提升 GPU 利用率,但要观察显存和尾延迟。
步骤 9:做一次成本压测
编辑 bench.py:
```python
import time
import modal
import httpx
app = modal.App("llm-bench")
@app.function(timeout=600)
def bench(endpoint: str, n: int = 20):
url = f"{endpoint.rstrip('/')}/v1/chat/completions"
payload = {
"model": "Qwen/Qwen2.5-7B-Instruct",
"messages": [{"role": "user", "content": "写一首四行诗"}],
"max_tokens": 128,
}
with httpx.Client(timeout=120) as client:
t0 = time.time()
for i in range(n):
r = client.post(url, json=payload)
r.raise_for_status()
total = time.time() - t0
print(f"{n} 次请求总耗时 {total:.2f}s,平均 {total / n:.2f}s/次")
@app.local_entrypoint()
def main(endpoint: str, n: int = 20):
bench.remote(endpoint, n)
```
运行:
```bash
modal run bench.py --endpoint https://<你的持久端点> --n 20
```
压测时重点看三个数:首次请求耗时(含冷启动)、稳定后单请求耗时、Modal 控制台里的 GPU 秒数和容器启动次数。如果冷启动占比高,就调整 scaledown_window 或 min_containers;如果吞吐上不去,就提高单容器并发或换更强 GPU。成本没有统一公式,和 GPU 型号、请求量、生成长度都相关,以 Modal 控制台账单页为准。
验证部署是否成功
1. 查看应用状态:
```bash
modal app list
```
能看到应用处于 deployed 状态。
2. 检查模型列表:
```bash
curl -s https://<你的持久端点>/v1/models
```
返回 JSON 中包含配置的模型名称。
3. 发起一次对话请求:
```bash
curl -s https://<你的持久端点>/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"Qwen/Qwen2.5-7B-Instruct","messages":[{"role":"user","content":"你好"}],"max_tokens":32}'
```
返回 JSON 里有 choices 字段和模型输出。
4. 在 Modal 控制台观察容器数量:无请求时缩到零,有请求时拉起,说明按需伸缩生效。
常见报错与解决
报错:torch.cuda.OutOfMemoryError: CUDA out of memory
原因:模型权重、KV 缓存或 max-model-len 超出当前 GPU 显存。
解决:换更大显存的 GPU,或降低 --max-model-len,或使用量化版本模型。改完参数后重新部署:
```bash
modal deploy app.py
```
报错:modal.exception.NotFoundError: Volume 'hf-cache' not found
原因:Volume 还没创建,或者名字写错。
解决:先创建 Volume,再重新部署:
```bash
modal volume create hf-cache
modal deploy app.py
```
报错:端点返回 502 或 Connection refused
原因:容器还没启动完,startup_timeout 太短,或者 vLLM 进程启动失败。
解决:先看日志定位:
```bash
modal app logs <app-id>
```
如果是启动慢,把 startup_timeout 调大;如果是依赖或参数问题,按日志里的报错修正后重新部署。
报错:ModuleNotFoundError: No module named 'vllm'
原因:镜像里没有装对应依赖,或者镜像缓存是旧版本。
解决:确认 image.pip_install 中包含 vllm,然后重新部署触发镜像重建:
```bash
modal deploy app.py
```
报错:401 Unauthorized
原因:端点开启了鉴权,但请求没带凭证。
解决:按官方文档在请求头里加 Modal-Key 和 Modal-Secret,或者在控制台关闭该端点的代理鉴权。
后续维护
- 备份:代码和配置放 Git 仓库;模型权重放在 Volume 里,即使丢失也可以重新下载。定期确认 Volume 名称和挂载路径没有改动。
- 升级:升级模型或依赖时,先在本地用
modal serve app.py验证,再执行modal deploy app.py。大版本升级建议换一个新应用名灰度,确认无误再切流量。 - 日志:用
modal app logs <app-id>看实时日志。把业务侧请求量、错误率、首包延迟也记下来,方便区分是平台问题还是模型问题。 - 监控:在 Modal 控制台关注 GPU 利用率、容器启动次数、冷启动比例。冷启动比例高就调整保活策略;利用率长期偏低就考虑缩小 GPU 或合并请求。
- 成本:设置预算提醒,定期检查
scaledown_window和min_containers是否还符合当前流量。压测数据要定期复跑,流量模式变了,最优参数也会变。 - 安全:不要把密钥硬编码在代码里,使用 Modal Secrets 注入;对外端点建议开启鉴权,并在网关侧加限流。
