本地大模型上下文长度(max-model-len)怎么设置最合理?

本地大模型上下文长度(max-model-len)怎么设置最合理?

背景

上周给客户部署一个合同比对Agent,要求能同时喂入两份30页PDF(OCR后约12万token)。我下意识把 vllm--max-model-len 131072 甩进去,结果启动直接报 CUDA out of memory,连模型权重都加载不全。重试时发现:哪怕只设 65536vllm 也卡在 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_sizetotal_kv_cache_memory_mb;若失败,报错形如:

RuntimeError: Cannot allocate block table with 32768 tokens per sequence.
Available blocks: 1280, required: 1320

这时就降档测试:换 --max-model-len 2457616384 → 直到成功。我最终在 --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 → 可行。再试 24576mem_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 GBnvidia-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_sizemax-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 实在多了。