本文记录在一台 2× NVIDIA H100 NVL(每卡约 96GB,合计 ~192GB 显存) 的服务器上,用 vLLM 部署 DeepSeek‑V4‑Flash(原生 1M 上下文)的完整过程:环境搭建、模型下载、服务化、性能优化,以及把 codex / qwen‑code / opencode 三个命令行编程 Agent 接到这个本地端点上的配置与压测。重点是我踩过的几个坑——尤其是一个让推理速度慢了 13 倍 的低级失误。


0. TL;DR(先看结论)

  • DeepSeek‑V4‑Flash 用 MLA(多头潜在注意力),KV cache 极省,1M 上下文 + CUDA graphs 可以同时成立
  • 千万别图省事加 --enforce-eager:它会关掉 CUDA graphs,把解码从 ~67 tok/s 打成 ~5 tok/s
  • agentic CLI(codex/qwen‑code/opencode)需要服务端开 --enable-auto-tool-choice --tool-call-parser deepseek_v4,否则直接 HTTP 400。
  • codex 0.142+ 必须用 wire_api = "responses"(不再支持 chat),好在 vLLM 暴露了 /v1/responses
  • Agent 与模型服务尽量同机部署:流式输出是"延迟敏感型",隔着网络会明显变慢。

最终稳定配置:1M 上下文 + FP8 KV + CUDA graphs + 工具调用,解码 ~67 tok/s,显存 ~91GB/卡。


1. 硬件与软件环境

项目 版本 / 规格
GPU 2× NVIDIA H100 NVL(~96GB/卡,合计 ~192GB)
OS Ubuntu 24.04 LTS
NVIDIA 驱动 580.x
CUDA 13.0(/usr/local/cuda-13.0)
vLLM 0.23.0
PyTorch 2.11.0 + cu130
transformers 5.12.1
flashinfer 0.6.12
huggingface‑hub 1.21.x

经验:为不同模型族准备相互独立的 venv。例如旧的 Qwen 系列用 vLLM 0.21,新的 DeepSeek‑V4 需要 vLLM 0.23——并存、互不干扰,升级一个不会搞坏另一个。


2. 搭建 vLLM 环境(独立 venv)

# 为新模型单独建一个 venv,避免污染已有环境
python3 -m venv /opt/vllm-env-next
/opt/vllm-env-next/bin/pip install --upgrade pip
/opt/vllm-env-next/bin/pip install vllm==0.23.0

坑 1:用户级 site‑packages 污染。 如果机器上别处装过别的 torch,运行时容易串库。统一在启动脚本里:

unset PYTHONPATH
export PYTHONNOUSERSITE=1

这样强制只用当前 venv 的包。

CUDA 工具链也要对齐(某些 FP8/FP4 kernel 的 JIT 编译需要较新的 nvcc):

export CUDA_HOME=/usr/local/cuda-13.0
export PATH=$CUDA_HOME/bin:$PATH
export LD_LIBRARY_PATH=$CUDA_HOME/lib64:${LD_LIBRARY_PATH:-}

3. 下载模型

DeepSeek‑V4‑Flash 是 FP8 量化(quantization = deepseek_v4_fp8),磁盘约 149GB,架构 DeepseekV4ForCausalLM,max_position_embeddings = 1048576(原生 1M)。

# 注意:新版本里 huggingface-cli 已弃用,改用 hf
export HF_HUB_DISABLE_XET=1   # 关键:见下方坑 2
/opt/vllm-env-next/bin/hf download <org>/DeepSeek-V4-Flash \
    --local-dir /data/models/DeepSeek-V4-Flash

坑 2:Xet 高速传输在未登录时会"假死"。 用默认的 Xet 通道下载大模型,未带 HF token 时常常下到几十 GB 就卡死不动(进程还在,但字节数一动不动)。表现极具迷惑性。 解决:export HF_HUB_DISABLE_XET=1 退回标准 HTTPS(支持断点续传、稳定),或配置 HF_TOKEN 提高额度。再写个 while ... hf download ... && break 的重试循环更稳。

坑 3:huggingface-cli download 已经不工作了。 新版 huggingface_hub 里它只打印一段"请改用 hf"的提示就退出,不会真的下载。一定要用 hf download


4. 启动脚本与 systemd 服务

4.1 启动脚本 start-vllm-dsv4.sh

#!/usr/bin/env bash
# vLLM OpenAI 服务:DeepSeek-V4-Flash(MLA + FP8 权重 / FP4 专家)
set -euo pipefail

unset PYTHONPATH
export PYTHONNOUSERSITE=1

VENV=/opt/vllm-env-next
MODEL_PATH=/data/models/DeepSeek-V4-Flash

export HF_HOME=/data/huggingface
export HF_HUB_OFFLINE=1                 # 权重已在本地,启动时不必联网
export VLLM_CACHE_ROOT=$HOME/.cache/vllm

export CUDA_VISIBLE_DEVICES=0,1
export CUDA_HOME=/usr/local/cuda-13.0
export PATH=$CUDA_HOME/bin:$PATH
export LD_LIBRARY_PATH=$CUDA_HOME/lib64:${LD_LIBRARY_PATH:-}

# 这一组是针对该模型 FP8/FP4 kernel + 当前驱动/CUDA 组合调出来的,
# 属于"模型 + 硬件相关"的开关,换模型/换卡可能需要重新验证。
export VLLM_BLOCKSCALE_FP8_GEMM_FLASHINFER=0
export VLLM_USE_DEEP_GEMM=1
export VLLM_USE_DEEP_GEMM_E8M0=1
export VLLM_USE_FLASHINFER_MOE_FP4=1
export VLLM_MXFP4_USE_MARLIN=1

MAXLEN=${MAXLEN:-1048576}               # 1M 上下文;OOM 时调小

exec "$VENV/bin/vllm" serve "$MODEL_PATH" \
    --host 0.0.0.0 --port 8000 \
    --tensor-parallel-size 2 \
    --max-model-len "$MAXLEN" \
    --kv-cache-dtype fp8 \
    --gpu-memory-utilization 0.95 \
    --max-num-seqs 16 \
    --trust-remote-code \
    --enable-auto-tool-choice \
    --tool-call-parser deepseek_v4 \
    --served-model-name DeepSeek-V4-Flash

4.2 systemd 服务 vllm-dsv4.service

[Unit]
Description=vLLM OpenAI server (DeepSeek-V4-Flash, 1M ctx, 2x H100)
After=network-online.target
Wants=network-online.target
# 若同机上有多个互斥的模型服务,用 Conflicts 防止它们同时抢 GPU/端口
# Conflicts=other-vllm.service

[Service]
Type=simple
User=youruser
WorkingDirectory=/data
ExecStart=/data/start-vllm-dsv4.sh
StandardOutput=append:/data/vllm-dsv4.log
StandardError=append:/data/vllm-dsv4.log
Restart=on-failure
RestartSec=15
TimeoutStartSec=1800          # 1M 上下文 + 149GB 权重,启动给足时间
LimitNOFILE=1048576
LimitMEMLOCK=infinity

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now vllm-dsv4.service

5. 性能优化:最重要的一课

5.1 --enforce-eager 是个大坑

一开始为了"稳",我在启动参数里带了 --enforce-eager。结果解码只有 ~5 tok/s,任何稍长的回答都慢到像卡死。

--enforce-eager 会关闭 CUDA graphs(以及 torch.compile)。对 DeepSeek‑V4 这种大 MoE,没有 CUDA graphs,每步解码都被 kernel 启动开销和 Python 逐层调度拖垮。

之所以当初敢加,是潜意识里以为"1M 上下文太吃显存,CUDA graphs 放不下"。这个假设是错的:DeepSeek‑V4 用 MLA,KV cache 极其紧凑,1M 上下文的 KV 池才占 ~10GB/卡,完全留得出空间给 CUDA graphs。

去掉 --enforce-eager 后:

配置 解码速度
带 --enforce-eager ~5 tok/s
去掉(开启 CUDA graphs) ~67 tok/s(约 13×)

5.2 上下文长度 vs 并发:两个独立的上限

可用上下文 = min( 模型自身上限 , 显存里 KV 池能装下的 token 数 )

  • 模型上限:由训练时的 max_position_embeddings 决定(这里是 1,048,576)。超过它要靠 YaRN 外推,质量会掉。
  • 显存上限:每个 token 占一份 KV cache;可用 KV 显存决定所有并发请求共享的总 token 容量

vLLM 启动日志会直接告诉你这两个数,例如:

GPU KV cache size: 1,491,011 tokens
Maximum concurrency for 1,048,576 tokens per request: 1.42x

含义:KV 池能装约 149 万 token;一条 1M 的请求会吃掉其中约 70%,所以"满长请求"的并发约 1.42 路。--max-model-len 只是设"单条最长",它本身不额外占显存——显存在 token 真正进来时才按量消耗。

实测在本机的几档:

max-model-len KV 池 满长并发 解码速度
256K ~70 万 token 2.66× 64.7 tok/s
1M(原生) ~149 万 token 1.42× 66.9 tok/s

因为 MLA 的 KV 太省,直接拉满原生 1M 也几乎不影响速度,显存约 91GB/卡。

5.3 其它优化点

  • --kv-cache-dtype fp8:KV 再砍一半,等于把可用上下文/并发翻倍。
  • --gpu-memory-utilization 0.95:在不 OOM 的前提下尽量榨干显存给 KV 池。
  • --max-num-seqs:按你的并发需求设,单人交互 8–16 足够。

6. 踩坑汇总(speedrun)

  1. --enforce-eager → 慢 13 倍。 见上,DeepSeek‑V4(MLA)1M + CUDA graphs 可兼得。
  2. 缺工具调用参数 → agentic CLI 报 400。 必须加 --enable-auto-tool-choice --tool-call-parser deepseek_v4(vLLM 0.23 自带这个 parser)。否则 qwen‑code 等会直接 400 "auto" tool choice requires --enable-auto-tool-choice and --tool-call-parser
  3. 重启时 GPU 显存释放有竞态。 旧实例还没退干净就 start,新实例做显存探测会报 ValueError: Free memory ... less than desired。正确姿势:先 stop,轮询 nvidia-smi 等显存降到接近 0,再 start
  4. 多进程 worker 不随主进程一起死。 手动起的 vLLM,有时杀了主进程,VLLM::Worker_TP* 还占着几十 GB 显存。用进程组或按确切 PID 收尾,并核对 nvidia-smi --query-compute-apps
  5. 重复/残留的 systemd 服务在抢资源。 我曾有个 enabled 且 Restart=on-failure 的重复服务,因端口被占不断崩溃→30 秒后又被拉起,反复抢 GPU。排查:systemctl list-unit-files | grep -i vllm,把多余的 disable --now
  6. HF 下载:用 hf 不要用 huggingface-cli;未登录的 Xet 会卡死 → HF_HUB_DISABLE_XET=1
  7. codex exec 通过 SSH 非交互执行会卡在读 stdin → 加 < /dev/null

7. 部署编程 Agent

三个 Agent 都走 OpenAI 兼容协议 接到本地 http://localhost:8000/v1强烈建议把 Agent 和模型服务装在同一台机器上(原因见第 8 节)。

7.1 codex(OpenAI Codex CLI)

npm install -g @openai/codex

~/.codex/config.toml:

model = "DeepSeek-V4-Flash"
model_provider = "vllm_local"
model_context_window = 1000000

[model_providers.vllm_local]
name = "Local vLLM DeepSeek-V4-Flash"
base_url = "http://localhost:8000/v1"
env_key = "OPENAI_API_KEY"     # codex 从该环境变量读 key
wire_api = "responses"          # 关键:见下
export OPENAI_API_KEY=sk-local-anything   # 本地服务不校验,但 codex 要求非空

坑 8:codex 0.142+ 不再支持 wire_api = "chat",会直接报错要你改成 "responses"。所幸 vLLM 暴露了 OpenAI Responses API(/v1/responses),所以 wire_api = "responses" 能正常对接。

另:codex 对未知模型会提示 Model metadata not found,无害;可用 model_context_window 告诉它上下文窗口。

7.2 qwen‑code

npm install -g @qwen-code/qwen-code

~/.qwen/.env:

OPENAI_API_KEY=sk-local-anything
OPENAI_BASE_URL=http://localhost:8000/v1
OPENAI_MODEL=DeepSeek-V4-Flash

qwen‑code 直接读这几个环境变量,配置最简单。

7.3 opencode

# 官方安装脚本(或 npm)
curl -fsSL https://opencode.ai/install | bash
opencode upgrade        # 升级到最新

~/.config/opencode/opencode.json(用社区的 @ai-sdk/openai-compatible provider):

{
  "$schema": "https://opencode.ai/config.json",
  "model": "local-vllm/deepseek-v4",
  "permission": { "*": "allow" },
  "provider": {
    "local-vllm": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "Local vLLM (DeepSeek-V4-Flash)",
      "options": {
        "baseURL": "http://127.0.0.1:8000/v1",
        "timeout": 600000
      },
      "models": {
        "deepseek-v4": { "id": "DeepSeek-V4-Flash", "name": "DeepSeek-V4-Flash (Local)" }
      }
    }
  }
}

opencode 还可挂 LSP 和 MCP(filesystem / memory / sequential‑thinking 等)。功能强,但这些 MCP server 会拉高启动开销,首次响应更慢。


8. 性能测试

同一道题(What is 7 times 8?),在模型同机本地实测:

Agent 版本 端到端用时 备注
codex 0.142.x ~1.5s 最快;首请求会预填 ~11K token 的系统提示
qwen‑code 0.15.x ~5s 配置最简单
opencode 1.17.x ~16s 慢在启动多个 MCP server,模型本身很快

服务端纯解码 66.9 tok/s(在线日志里 generation throughput 可印证)。

为什么"同机部署"很重要:流式是延迟敏感型

一个反直觉的发现:LLM 的流式(SSE 逐 token)输出,瓶颈不是带宽,而是延迟。

  • 模型每生成一个 token 就 flush 一个很小的数据块;一次回答就是成百上千个小消息。
  • 在 localhost,每个小块延迟≈0,所以能跑满 ~67 tok/s。
  • 一旦隔着网络代理/远程通道,每个 token 小包都要付一整次往返延迟 + TLS/帧封装开销,流式速度会明显下降(我实测从 ~67 掉到 ~27 tok/s),而一次性(非流式)的大请求几乎不受影响——因为它能把延迟摊薄。

类比:本机直连像"一封信塞 200 页一次寄到";隔网络流式像"拆成 200 张明信片,一张一张寄、还要等到了再寄下一张"——纸张总量(带宽)不是问题,来回跑腿(延迟)才是。

结论:把编程 Agent 直接装在 GPU 主机上、连 localhost,是体验最好的方案;延迟敏感的流式负载尤其如此。


9. 经验总结

  1. MLA 模型(DeepSeek‑V4)KV 极省:别被"1M 上下文"吓到,CUDA graphs 完全放得下——不要无脑加 --enforce-eager
  2. agentic CLI 需要服务端开工具调用:--enable-auto-tool-choice --tool-call-parser <model>,这是"Agent 用不了"的头号原因,比速度问题更隐蔽。
  3. 重启 vLLM 要先确认 GPU 显存释放干净,并清理残留 worker / 重复服务。
  4. codex 用 wire_api = "responses";qwen‑code 走环境变量;opencode 用 openai‑compatible provider。
  5. 流式看延迟、批量看带宽:Agent 与模型同机,省心又快。

把上面这套配好,你就能在两张 H100 上跑一个 1M 上下文、~67 tok/s、支持工具调用 的本地 DeepSeek‑V4‑Flash,并用 codex / qwen‑code / opencode 把它当编程助手用。Happy hacking!


本文所有命令均为示意,实际路径/账号请按你自己的环境替换。模型与硬件相关的环境变量开关,换型号或换卡后建议重新验证。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐