背景
上周给客户部署一个合同比对Agent,要求能同时喂入两份30页PDF(OCR后约12万token)。我下意识把 vllm 的 --max-model-len 131072 甩进去,结果启动直接报 CUDA out of memory,连模型权重都加载不全。重试时发现:哪怕只设 65536,vllm 也卡在 PagedAttention 初始化阶段,GPU 显存占用飙到 23.8/24GB,但 nvidia-smi 显示 compute processes 为 0 —— 这说明不是推理卡住,是预分配失败。我意识到:本地部署里,max-model-len 不是「支持多长」的标称值,而是「提前划多少地」的内存契约。它不光影响 context 能力,更直接决定显存基线、KV Cache 内存布局、甚至是否能冷启动成功。这篇就是我用 Qwen2-7B-Instruct 在单卡 RTX 4090(24GB)上反复烧出来的配置心法。
三步定长法:从显存余量反推 max-model-len
我的做法是:不看模型文档写的「支持200K」,而是用 vllm 自带的 mem_usage 工具反向测算。全程在干净环境操作(无其他进程占显存):
# 1. 先确认基础显存占用(仅加载模型权重,不启用 KV cache 预分配)
vllm-entrypoint serve \
--model Qwen/Qwen2-7B-Instruct \
--tensor-parallel-size 1 \
--gpu-memory-utilization 0.9 \
--enforce-eager \
--max-model-len 8192 \
--disable-log-stats &
sleep 10
nvidia-smi --query-compute-apps=pid,used_memory --format=csv,noheader,nounits
# 输出示例:12345, 12456 MiB
记下这个基础值(我的是 12456 MiB)。然后关掉服务:kill -9 12345。
2. 启动 vllm 内置内存分析器,逐步试探 KV Cache 上限:
vllm-entrypoint mem_usage \
--model Qwen/Qwen2-7B-Instruct \
--dtype b16 \
--tensor-parallel-size 1 \
--gpu-memory-utilization 0.9 \
--max-model-len 32768 \
--num-prompts 1 \
--output-json ./mem_32k.json
如果成功,会输出 kv_cache_block_size 和 total_kv_cache_memory_mb;若失败,报错形如:
RuntimeError: Cannot allocate block table with 32768 tokens per sequence.
Available blocks: 1280, required: 1320
这时就降档测试:换 --max-model-len 24576 → 16384 → 直到成功。我最终在 --max-model-len 16384 时拿到有效输出:
{
"model_config": {"max_model_len": 16384},
"block_size": 16,
"num_gpu_blocks": 1720,
"total_kv_cache_memory_mb": 4212.5
}
3. 关键计算:总显存 = 基础权重占用 + KV Cache 占用 + 安全余量(我留 1200MB)。代入:12456 + 4212.5 + 1200 ≈ 17868 MB < 24576 MB → 可行。再试 24576?mem_usage 报错要求 1920 blocks,但实际只有 1720,说明超了。因此 16384 就是这台机器的硬上限。
最后固化配置(config.yaml):
model: "Qwen/Qwen2-7B-Instruct"
tensor_parallel_size: 1
gpu_memory_utilization: 0.9
max_model_len: 16384
enforce_eager: false
kv_cache_dtype: "auto"
block_size: 16
enable_prefix_caching: true
# 注意:prefix caching 必须配合 block_size=16 才生效,否则报 warning
启动命令:
vllm-entrypoint serve --config config.yaml --host 0.0.0.0 --port 8000
实测记录
用 curl 测吞吐(输入 16K token prompt,生成 512 token):
curl http://localhost:8000/v1/completions \
-H "Content-Type: application/json" \
-d '{"model":"Qwen2-7B-Instruct","prompt":"<|im_start|>system\n你是一个法律助手...<|im_end|>\n<|im_start|>user\n[16384 token 文本]<|im_end|>\n<|im_start|>assistant\n","max_tokens":512,"temperature":0.1}'
结果:
- 首 token 延迟(TTFT):平均 1820 ms(因 KV Cache 初始化耗时)
- 输出 token 平均间隔(ITL):32 ms/token(batch_size=1)
- 显存稳定占用:22.1 GB / 24 GB(
nvidia-smi持续观察 10 分钟无抖动) - 并发测试(3 个请求并行):TTFT 升至 2100ms,ITL 保持 32±2ms,无 OOM —— 证明
block_size=16+prefix_caching确实复用了公共前缀
对比组:若强行设 max-model-len 32768(不跑 mem_usage 直接启动),启动日志报:
ERROR 04-23 10:22:17 [block_manager.py:122] Failed to allocate blocks for sequence xxx. Available blocks: 1720, required: 2048.
服务进程直接退出,PID 都没分配出来。
踩坑备忘
• 坑1:以为 --gpu-memory-utilization 0.95 更好 —— 实测在 4090 上设 0.95 会导致 mem_usage 计算失真,因为 vLLM 会预留更多碎片空间,反而让可用 blocks 数变少。坚持用 0.9 最稳。
• 坑2:忽略 block_size 与 max-model-len 的整除关系 —— 如果 max-model-len=16385(非 16 整数倍),vLLM 会自动向上取整到 16384+16=16400,但此时 num_gpu_blocks 不够,启动失败。务必保证 max-model-len % block_size == 0。
• 坑3:在 GPUStack 里改 max-model-len 不生效 —— GPUStack 0.6.1 的 UI 配置项会覆盖 CLI 参数,必须进容器改 /app/config.yaml,然后 docker exec -it gpu-stack-server bash -c 'supervisorctl restart vllm',不能只点 Web 界面「保存」。
• 坑4:误用 --max-num-seqs 代替 --max-model-len —— 前者控制并发请求数(类似 batch_size),后者才是单请求最大长度。曾有同事调高前者想撑长文本,结果只是并发更多,单个请求仍被截断到默认 8192。
我的结论
本地部署中,max-model-len 不是越大越好,而是「刚好够用且留余量」的精细工程。我的推荐方案:
- 日常 RAG/Agent 场景(输入 8K–12K token):直接设
max-model-len: 16384,配block_size: 16+enable_prefix_caching: true,这是 4090 的黄金组合,平衡启动速度与长文本能力; - 纯摘要/单次问答(输入 <4K):保守设
8192,可把gpu_memory_utilization提到0.92,腾出显存跑更高并发(实测 batch_size=4 时 ITL 仍 <40ms); - 别碰 32K+:除非你有双卡 A100-80G 或 H100,否则在单卡消费级 GPU 上,>16K 的收益远低于 OOM 风险。我试过用
flashinfer替换 PagedAttention,但编译失败三次后放弃——工程时间成本 > 实际收益; - 谁该用这套方法:所有用 vLLM 自建服务的本地开发者,尤其用 Qwen、Phi-3、DeepSeek-Coder 等 7B–14B 模型的朋友;
- 谁不该照搬:用 Ollama 或 LM Studio 的用户(它们自动管理长度),以及生产环境用 Triton 推理的团队(他们走的是完全不同的内存路径)。
最后一句大实话:在本地,max-model-len 的本质不是「我能喂多长」,而是「我敢承诺多长」。承诺之前,先用 vllm-entrypoint mem_usage 把账算清楚——这比调十次 temperature 实在多了。