/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
文章目录
-
- 1. 引言与结论速览
-
- 结论速览
- 2. 部署环境准备
-
- 2.1 硬件配置要求
- 2.2 操作系统与系统软件版本
- 2.3 依赖软件安装清单
- 3. 详细部署步骤
-
- 3.1 拓扑与节点分工
- 3.2 节点基础配置
- 3.3 高速网络(RoCE)设置与验证
- 3.4 容器镜像与工具链准备
- 3.5 模型权重准备(含内网受限环境的替代通道)
- 3.6 启动服务(完整命令)
- 3.7 就绪判定与功能验证
- 3.8 停止、切换与回滚
- 4. 常见问题排查与解决方案
-
- 4.1 排错总表(先查这张表)
- 4.2 重点案例:服务能起、输出乱码——加载器静默丢掉 FP8 qkv 权重
- 4.3 重点案例:加载期崩溃 `1856 vs 1792`——fused-QKV 几何
- 4.4 其它实战坑(按出现频率排序)
- 5. 性能测试建议
-
- 5.1 测试口径(保证数据可比、可复现)
- 5.2 实测结果(调优后配置)
- 5.3 两项单变量调优的取舍(每项先过质量探针再进基准)
- 5.4 建议的测试流程
- 6. 使用注意事项
- 7. 小结与参考
关键词 / 建议标签:DGX Spark、GB10、MiMo-V2.6-Flash、vLLM、MoE、TP=2、Ray、MXFP4、FP8、RoCE、大模型部署、推理性能
说明:文中主机名、内网地址、用户名均已做脱敏处理(以 HEAD_IP / WORKER_IP 等占位符表示),命令与参数保持可直接参考的完整性。
1. 引言与结论速览
MiMo-V2.6-Flash(小米)是一个 309B 总参 / 15B 激活 的稀疏 MoE 模型:48 层、256 个路由专家(top-8)、专家权重 MXFP4 存储 + FP8 计算、原生 1M 上下文,官方推荐 SGLang、vLLM 次选,并明确提示 “Stable vLLM may lag the architecture”——这句话在本次部署中应验了。
我们的目标是在两台 DGX Spark GB10(单机 128GB 统一内存)上,用一个 vLLM(TP=2)实例把这个 178GB 的模型跑起来。过程并不顺利:服务能起、HTTP 200、但输出乱码这个问题卡了很久,最后定位到镜像自带加载器的一处静默丢键缺陷。本文把完整的环境准备、部署步骤、排错记录、性能数据与使用注意事项整理成可复现的文档,供同行参考。
结论速览
| 部署形态 | 双机 TP=2(vLLM + Ray 跨节点),单机装不下 178GB 权重 |
| 关键前提 | 镜像需含该模型架构支持与 triton_attn_diffkv 后端;必须用标准加载器 + fused-QKV 补丁 |
| 就绪耗时 | 约 10–12 分钟(65 个分片的标准加载 + 内核编译) |
| KV 池 | 739,270 tokens(256K 上下文约 2.82× 并发) |
| 功能验证 | “中国的首都是哪里?” → 北京(推理型模型,先输出思维链) |
| 实测吞吐(调优后) | 单流 24.4 / 中等并发 54.2 / 突发 C16 87.6 / 32K 长上下文 4.6 tok/s |
| 主要坑 | ① 加载器静默丢掉 FP8 qkv_proj.weight(输出乱码)② fused-QKV 几何不匹配(加载期崩溃)③ –kv-cache-dtype fp8 静默降级 |
2. 部署环境准备
2.1 硬件配置要求
| 节点数 | 2 台(Head + Worker) | 每台 1 张 GPU,TP>1 只能跨节点 |
| SoC / 架构 | NVIDIA GB10,aarch64,Blackwell SM121 | 内核需支持 sm_121a 相关算子 |
| 内存 | 128 GB LPDDR5x 统一内存(UMA),带宽约 273 GB/s | CPU/GPU 共享内存池,推理占用直接挤压系统内存 |
| 互联 | ConnectX-7,QSFP 口以太网模式,200 Gb/s(每物理口 = 2 个逻辑口) | 两机 CX7 直连 RoCE,无交换机 |
| 磁盘 | 每节点预留 ≥ 250 GB | 权重约 178 GB(单份)+ 容器镜像 + 编译缓存 |
| 实测 RDMA 带宽 | 单逻辑口 ib_write_bw 约 99.4 Gb/s(4 QP) | 若只跑出 ~20 Gb/s,多半是 NCCL 静默退化为 TCP |
为什么必须双机:模型权重约 178 GB(91 个文件、65 个 safetensors 分片),单机 128 GB 统一内存(其中可供推理约 121 GB)装不下,且运行时还要为 KV Cache 留出空间。因此采用 TP=2 跨节点张量并行,两节点各持一份完整权重(每份按 TP rank 切分加载)。
2.2 操作系统与系统软件版本
以下为本机实测基线(DGX Spark 出厂系统 + 常规更新):
| 操作系统 | Ubuntu 24.04.3 LTS(DGX OS) |
| 内核 | 6.17.0-1026-nvidia |
| NVIDIA 驱动 | 580.159.03 |
| Docker | 29.2.1(建议开机自启:systemctl is-enabled docker → enabled) |
| NVIDIA Container Toolkit | 1.20.0-1(容器透传 GPU / /dev/infiniband 必需) |
| RDMA 工具 | rdma-core(rdma、ibdev2netdev、ib_write_bw 等) |
2.3 依赖软件安装清单
| 容器镜像 | vllm-node-b12x:latest(上游 eugr/spark-vllm-b12x) | 运行时本体:vLLM 0.1.dev20759+gb40673cd0(2026-09-13 构建)、Ray 2.58.0、CUDA 13 系、FlashInfer |
| 启动工具链 | git clone 上游 spark-vllm-docker 仓库 | launch-cluster.sh(跨节点启动器)、run-recipe.sh、hf-download.sh、mods/ 补丁机制 |
| fused-QKV 补丁 | mods/fix-mimo-v2-qkv-block(自研,随仓库分发) | 修复镜像对融合 QKV 的分组假设错误(见 4.3 节),必需 |
| 模型权重 | XiaomiMiMo/MiMo-V2.6-Flash-RL,约 178 GB | HuggingFace 不可达时改用 ModelScope 下载(见 3.5 节) |
| 权重分发 | rsync(系统自带) | Head → Worker 限速同步(–bwlimit),避免挤占生产链路 |
| 宿主 Python 依赖 | modelscope(pip install –user) | 权重下载替代通道 |
| 验证工具 | curl、容器内 vllm bench serve | 就绪判定、质量验证与性能基准 |
镜像选型要点:该镜像已内置 MiMo-V2 的架构支持(含 MiMoV2FlashForCausalLM 注册)、triton_attn_diffkv 注意力后端(MiMo 在 sm_121a 上必需,因 FA3 不可用)、mimo 的 reasoning / tool-call 解析器与 deep_gemm MoE 后端。若你的镜像不含这些,需先升级镜像或对照社区方案打补丁。
3. 详细部署步骤
3.1 拓扑与节点分工
#mermaid-svg-TMnUtccuPds1KGNR{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TMnUtccuPds1KGNR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TMnUtccuPds1KGNR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TMnUtccuPds1KGNR .error-icon{fill:#552222;}#mermaid-svg-TMnUtccuPds1KGNR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TMnUtccuPds1KGNR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TMnUtccuPds1KGNR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TMnUtccuPds1KGNR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TMnUtccuPds1KGNR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TMnUtccuPds1KGNR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TMnUtccuPds1KGNR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TMnUtccuPds1KGNR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TMnUtccuPds1KGNR .marker.cross{stroke:#333333;}#mermaid-svg-TMnUtccuPds1KGNR svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TMnUtccuPds1KGNR p{margin:0;}#mermaid-svg-TMnUtccuPds1KGNR .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-TMnUtccuPds1KGNR .cluster-label text{fill:#333;}#mermaid-svg-TMnUtccuPds1KGNR .cluster-label span{color:#333;}#mermaid-svg-TMnUtccuPds1KGNR .cluster-label span p{background-color:transparent;}#mermaid-svg-TMnUtccuPds1KGNR .label text,#mermaid-svg-TMnUtccuPds1KGNR span{fill:#333;color:#333;}#mermaid-svg-TMnUtccuPds1KGNR .node rect,#mermaid-svg-TMnUtccuPds1KGNR .node circle,#mermaid-svg-TMnUtccuPds1KGNR .node ellipse,#mermaid-svg-TMnUtccuPds1KGNR .node polygon,#mermaid-svg-TMnUtccuPds1KGNR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TMnUtccuPds1KGNR .rough-node .label text,#mermaid-svg-TMnUtccuPds1KGNR .node .label text,#mermaid-svg-TMnUtccuPds1KGNR .image-shape .label,#mermaid-svg-TMnUtccuPds1KGNR .icon-shape .label{text-anchor:middle;}#mermaid-svg-TMnUtccuPds1KGNR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TMnUtccuPds1KGNR .rough-node .label,#mermaid-svg-TMnUtccuPds1KGNR .node .label,#mermaid-svg-TMnUtccuPds1KGNR .image-shape .label,#mermaid-svg-TMnUtccuPds1KGNR .icon-shape .label{text-align:center;}#mermaid-svg-TMnUtccuPds1KGNR .node.clickable{cursor:pointer;}#mermaid-svg-TMnUtccuPds1KGNR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TMnUtccuPds1KGNR .arrowheadPath{fill:#333333;}#mermaid-svg-TMnUtccuPds1KGNR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TMnUtccuPds1KGNR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TMnUtccuPds1KGNR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TMnUtccuPds1KGNR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TMnUtccuPds1KGNR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TMnUtccuPds1KGNR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TMnUtccuPds1KGNR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TMnUtccuPds1KGNR .cluster text{fill:#333;}#mermaid-svg-TMnUtccuPds1KGNR .cluster span{color:#333;}#mermaid-svg-TMnUtccuPds1KGNR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TMnUtccuPds1KGNR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TMnUtccuPds1KGNR rect.text{fill:none;stroke-width:0;}#mermaid-svg-TMnUtccuPds1KGNR .icon-shape,#mermaid-svg-TMnUtccuPds1KGNR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TMnUtccuPds1KGNR .icon-shape p,#mermaid-svg-TMnUtccuPds1KGNR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TMnUtccuPds1KGNR .icon-shape .label rect,#mermaid-svg-TMnUtccuPds1KGNR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TMnUtccuPds1KGNR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TMnUtccuPds1KGNR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TMnUtccuPds1KGNR :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Worker 节点 · 推理工作节点
Head 节点 · 主控 + 推理入口
HTTP :8000
CX7 直连 RoCENCCL/RAY 通信
SSH · 权重/补丁分发
vllm_node 容器 :8000ray head · TP rank0
vllm_node 容器ray worker · TP rank1
客户端 / 网关model 统一填 vllm
管理链路
- Head:执行所有部署/启动命令,运行 Ray head 与 API 服务;
- Worker:被 Head 通过 SSH 驱动拉起容器,运行 Ray worker;
- 两节点必须位于同一管理网络,并具备 免密 SSH。
3.2 节点基础配置
# 在 Head 与 Worker 各自执行:主机名与 hosts 解析(示例值)
sudo hostnamectl set-hostname head-node # Worker 侧改成 worker-node
# /etc/hosts 两机均写入(地址用实际值替换)
echo "HEAD_IP head-node" | sudo tee -a /etc/hosts
echo "WORKER_IP worker-node" | sudo tee -a /etc/hosts
# Head 侧配置到 Worker 的免密 SSH(启动器依赖)
ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519
ssh-copy-id spark@WORKER_IP # 用户名按实际替换
ssh -o BatchMode=yes spark@WORKER_IP 'hostname' # 应免密返回 worker-node
3.3 高速网络(RoCE)设置与验证
双机 TP=2 的每一步通信都跑在这条链路上,因此先验网络,再起服务。
| 数据面网卡 | 两端 CX7 口直连(示例 enp1s0f1np1),各配一个同网段 IP |
| MTU | 两端必须一致(建议 9000;改完两端同时生效) |
| RoCE 设备 | 示例 rocep1s0f1,两端可用 ibdev2netdev 确认映射 |
| GID 索引 | 不要硬编码:RoCEv2 的 GID index 会随重启漂移,启动前自动探测 |
GID 自动探测(写入两端启动脚本,避免建链失败):
for i in $(seq 0 15); do
t=$(cat /sys/class/infiniband/rocep1s0f1/ports/1/gid_attrs/types/$i 2>/dev/null || true)
g=$(cat /sys/class/infiniband/rocep1s0f1/ports/1/gids/$i 2>/dev/null || true)
if [ "$t" = "RoCE v2" ] && [ -n "$g" ]; then
case "$g" in *ffff:*) NCCL_GID_INDEX=$i; break;; esac
fi
done
链路验证四连(全部通过再继续):
# 1) RoCE 设备状态:两端应为 ACTIVE
rdma link && ibdev2netdev
# 2) RDMA 带宽实测:单逻辑口应接近 100 Gb/s
ib_write_bw WORKER_IP -d rocep1s0f1 –report_gbits -q 4 -R –force-link IB
# 3) MTU 一致性
ip link show enp1s0f1np1 | head -1
ping -M do -s 8972 -c 3 WORKER_IP # 计划用 9000 MTU 时先验证大包不丢
# 4) 关键:容器内 NCCL 必须真的走 IB。启动后检查容器日志
# docker logs vllm_node 2>&1 | grep -i "NET/IB"
# 没有 NET/IB 字样 = NCCL 静默退化为 TCP(吞吐直接腰斩),检查容器是否透传 /dev/infiniband
3.4 容器镜像与工具链准备
# 1) 镜像(两节点均需,或由 Head 分发)
docker images | grep vllm-node-b12x # 期望存在 vllm-node-b12x:latest
# 2) 启动工具链(仅 Head 需要,供 launch-cluster.sh 驱动两端)
cd ~/github
git clone <spark-vllm-docker 上游仓库地址> spark-vllm-docker
# 3) fused-QKV 补丁就位(集群模式下 -v / mod 挂载在每个节点生效,
# 因此补丁文件必须在两节点的完全相同路径存在)
ls ~/github/spark-vllm-docker/mods/fix-mimo-v2-qkv-block/run.sh
3.5 模型权重准备(含内网受限环境的替代通道)
若 HuggingFace 不可达,走 ModelScope 下载,再限速同步到 Worker:
# 1) 宿主机安装下载器(容器内无需安装)
pip install –user modelscope
# 2) 下载到「容器已挂载的 HF 缓存目录」,容器内直接按路径加载
modelscope download –model XiaomiMiMo/MiMo-V2.6-Flash-RL \\
–local_dir ~/.cache/huggingface/hub/models–XiaomiMiMo–MiMo-V2.6-Flash-RL
# 3) 限速同步到 Worker 同路径(实测 530MB/s,占 200Gb 链路 <3%,不干扰在役服务)
rsync -a –bwlimit=530000 –info=progress2 \\
~/.cache/huggingface/hub/models–XiaomiMiMo–MiMo-V2.6-Flash-RL/ \\
spark@WORKER_IP:~/.cache/huggingface/hub/models–XiaomiMiMo–MiMo-V2.6-Flash-RL/
# 4) 两节点逐文件校验(大小/文件数一致),随后全程离线加载:HF_HUB_OFFLINE=1
要点:
- 权重约 178 GB / 91 个文件 / 65 个 safetensors 分片,下载与同步都是"长动作",建议在低峰执行;
- 受限环境下给 Python 加 PYTHONDONTWRITEBYTECODE=1,避免写系统缓存被拦导致命令中断;
- 两节点各持一份完整权重(TP=2 时各取所需分片),漏同步 Worker 会直接启动失败。
3.6 启动服务(完整命令)
在 Head 节点执行(工具链根目录):
cd ~/github/spark-vllm-docker
./launch-cluster.sh –ray \\
-t vllm-node-b12x:latest \\
-e HF_HUB_OFFLINE=1 \\
-e CUTE_DSL_ARCH=sm_121a \\
-e VLLM_USE_AOT_COMPILE=1 \\
-e VLLM_USE_BREAKABLE_CUDAGRAPH=0 \\
-e VLLM_USE_MEGA_AOT_ARTIFACT=1 \\
-e VLLM_USE_FLASHINFER_SAMPLER=1 \\
-e VLLM_USE_V2_MODEL_RUNNER=1 \\
-e NCCL_CUMEM_ENABLE=0 \\
-e NCCL_NVLS_ENABLE=0 \\
-e VLLM_USE_RAY_V2_EXECUTOR_BACKEND=1 \\
-d \\
–apply-mod mods/fix-mimo-v2-qkv-block \\
exec vllm serve /root/.cache/huggingface/hub/models–XiaomiMiMo–MiMo-V2.6-Flash-RL \\
–host 0.0.0.0 –port 8000 \\
–trust-remote-code \\
–tensor-parallel-size 2 \\
–load-format safetensors \\
–moe-backend deep_gemm \\
–attention-backend triton_attn_diffkv \\
–gpu-memory-utilization 0.88 \\
–max-model-len 262144 \\
–max-num-seqs 16 \\
–max-num-batched-tokens 8192 \\
–enable-prefix-caching \\
–reasoning-parser mimo \\
–tool-call-parser mimo \\
–enable-auto-tool-choice \\
–served-model-name vllm MiMo-V2.6-Flash
参数逐项说明(按重要性排序):
| –load-format | safetensors | ⚠️ 最关键。镜像自带的 instanttensor 快速加载器不投递 FP8 的 qkv_proj.weight,而该模型融合 QKV 走"权重与 scale 成对到达才写入",配对永不完成 ⇒ qkv 权重全零 ⇒ 输出与提示无关的固定乱码(服务却能正常起、HTTP 200)。代价是加载慢(约 10 分钟),换来正确性 |
| mods/fix-mimo-v2-qkv-block | 必须挂 | 镜像按 num_kv_heads 分组切分 FP8 融合 QKV 的 scale,隐含假设"每组行数是 128 的整数倍";本模型真实布局是 4 分块、块内 [Q|K|V],不打补丁会在加载期崩溃(详见 4.3 节) |
| –tensor-parallel-size | 2 | 178GB 权重必须双机;启动器加 –ray 自动注入 Ray 分布式执行后端 |
| –moe-backend | deep_gemm | 映射到 DEEPGEMM_MXFP4,且其激活方案(FP8 e4m3 动态、128 块)与本检查点 config 完全一致,是本机唯一数值方案对口的选择 |
| –attention-backend | triton_attn_diffkv | MiMo 在 sm_121a(GB10)上 FA3 不可用,DiffKV(K/V 头数异构)必需;不加时模型也会自动选它,显式指定更稳 |
| –max-num-seqs | 16 | 实测调优值:突发 C16 吞吐 +32%、TTFT −73%(详见 5.3 节) |
| –gpu-memory-utilization | 0.88 | 与其它栈一致;实测 KV 池 739,270 tokens |
| –max-model-len | 262144 | 原生支持 1M,第一阶段先用 256K |
| –max-num-batched-tokens | 8192 | 与长上下文/显存平衡,后续可继续调 |
| –enable-prefix-caching | 开 | 共享前缀场景收益明显(注意:它会"污染"基准测试,见 4.4 节) |
| NCCL_CUMEM_ENABLE=0、NCCL_NVLS_ENABLE=0 | 关 | 双机无 NVLink,禁用对称内存路径,避免 TP all-reduce 间歇死锁 |
| VLLM_USE_RAY_V2_EXECUTOR_BACKEND=1 | 开 | Ray V2 执行器;V1 在长跑中偶发死锁 |
| –served-model-name | vllm MiMo-V2.6-Flash | 常驻别名 vllm + 专用名,方便与其它模型栈做同端口切换 |
若使用自建的切换脚本,注意两点:① 启动前必须 docker rm -f vllm_node(launcher 检测到同名容器会复用,改参数不生效,见 4.4 节);② 就绪等待超时要放宽到 ≥1200s(标准加载器 + 首次编译约 10–12 分钟)。
3.7 就绪判定与功能验证
就绪判据:/v1/models 返回 HTTP 200(容器 Up ≠ 服务可用)。启动时间线参考:
| 容器拉起 → Ray 组网 | 约 1 分钟 |
| 权重加载(65 分片,标准加载器) | 约 9–10 分钟 |
| 引擎初始化 + 内核编译(首次含 JIT) | 1–2 分钟 |
| 合计就绪 | 约 10–12 分钟 |
# 1) 就绪判定
curl -s -o /dev/null -w '%{http_code}\\n' http://127.0.0.1:8000/v1/models # 期望 200
# 2) 功能验证:事实问答(本模型是推理型,reasoning 字段先出思维链,max_tokens 建议 ≥256)
curl -s http://127.0.0.1:8000/v1/chat/completions -H 'Content-Type: application/json' \\
-d '{"model":"vllm","messages":[{"role":"user","content":"中国的首都是哪里?"}],"max_tokens":256,"temperature":0}'
# 期望 content 含「北京」
# 3) 健康信号:关注这些加载期日志行
docker logs vllm_node 2>&1 | grep -E "Loading weights took|GPU KV cache size|Resolved architecture|TRITON_ATTN_DIFFKV|DEEPGEMM"
# 期望:Loading weights took 约 9–10 分钟、KV cache 约 739k tokens、attention 后端为 TRITON_ATTN_DIFFKV
验收清单(建议逐项打勾):
| 1 | /v1/models | HTTP 200,且含 vllm 与 MiMo-V2.6-Flash 两个别名 |
| 2 | 事实问答 | 输出含"北京" |
| 3 | 退化检测 | 输出无超长重复串(最长同字符连串 <6、字符去重率 ≥0.25) |
| 4 | KV 池 | 约 739,270 tokens |
| 5 | 加载健康 | Loading weights took 正常结束、无 WAITING/缺键告警 |
3.8 停止、切换与回滚
# 停止(两节点都要执行,否则统一内存与 8000 端口不会释放)
docker rm -f vllm_node
ssh spark@WORKER_IP 'docker rm -f vllm_node'
# 回滚到其它模型栈(示例:切换到既有生产模型)
./switch-model.sh deepseek # 就绪约 3–4 分钟
若使用切换脚本,其流程为「停旧栈 → 起新栈 → 轮询就绪 → 质量探针 → 失败自动回滚原栈」。切换脚本有两个已知陷阱:① 改配置后必须停容器再启动(否则复用旧容器);② “目标栈已在运行则直接退出”——改完参数要主动停一次。
4. 常见问题排查与解决方案
4.1 排错总表(先查这张表)
| 服务能起、HTTP 200,但输出与提示无关的固定乱码/重复串 | –load-format instanttensor 不投递 FP8 qkv_proj.weight,配对加载永不完成 → qkv 权重全零 | 改 –load-format safetensors(见 4.2 节) |
| 加载首分片约 2% 处崩溃:RuntimeError: The size of tensor a (1856) must match the size of tensor b (1792) | 镜像按 num_kv_heads 分组切 fused-QKV 的 scale,与检查点"4 分块、块内 [Q|K|V]"真实布局不符 | 挂 mods/fix-mimo-v2-qkv-block 补丁(见 4.3 节) |
| 启动参数改了不生效 | launcher 复用同名容器 | 启动前 docker rm -f vllm_node(两节点) |
| 集群模式补丁/挂载路径报不存在 | -v / mod 挂载在每个节点生效,Worker 缺同路径文件 | 把文件同步到 Worker 的完全相同路径 |
| –kv-cache-dtype fp8 貌似没生效 | DiffKV 后端只声明 auto/bfloat16,fp8 被静默降级 | 不要依赖 fp8 KV;需更长上下文先确认后端支持(KV 池规模可佐证) |
| –moe-backend flashinfer_cutlass 直接报错 | 本机内核不支持其 BF16 激活变体 | 本机可用:deep_gemm(推荐)/ marlin / humming / b12x |
| 首次启动特别慢(多等几分钟) | 首次内核 JIT 编译 | 编译缓存持久化挂载(如 ~/.cache/flashinfer),后续启动恢复常规耗时 |
| 基准测试数据好得离谱(TTFT 0.5s) | 同容器重复跑命中 prefix cache,prefill 被跳过 | 每轮用唯一随机 seed,或换容器后冷启动(见 5.1 节) |
| 引擎启动即崩、日志停在归一化预热 | 少数镜像对特定模型的预热断言 bug(如 Qwen 系 GDN 预热) | 对应模型单独打 No-op 补丁;MiMo 不受影响 |
| TP 通信慢、吞吐腰斩 | NCCL 静默退化为 TCP(容器缺 /dev/infiniband) | 启动后用 NCCL_DEBUG=INFO 搜 NET/IB;容器需透传 RDMA 设备 |
| 网关报 system memory overloaded,全量请求被拒 | 网关内存水位默认阈值 90%,而推理运行时统一内存本就吃满(实测 93.4%) | 阈值调到约 97(保留对真实内存耗尽的保护) |
| 权重同步挤占生产链路 | 178GB 同步与在役服务的 TP 通信共用同一条 200Gb RoCE | rsync –bwlimit 限速(实测占链路 ❤️%) |
4.2 重点案例:服务能起、输出乱码——加载器静默丢掉 FP8 qkv 权重
现象(最迷惑人的一点:服务完全"正常"):
- 权重加载正常完成、/v1/models 返回 200;
- 但生成的是固定吸引子(如 0,0,0,…),finish_reason=length、无 EOS;
- 两条同长度、不同内容的提示,生成 512 token 逐位完全相同;长度不同则是同一序列的后缀;
- 换 MoE 后端(b12x / marlin / deep_gemm)、关 lm_head 量化、改 QKV 行布局解释——输出逐位不变(只有 MoE 后端会换一个吸引子外观)。
定位路径(每步只引入一个变量的排查法,非常好用):
根因:镜像的 instanttensor 加载器(快速加载:525s → 23.4s)不投递 FP8 的 qkv_proj.weight 键,只投递其 weight_scale_inv;而模型侧 _try_load_fp8_qkv_proj 是"权重与 scale 成对到达才写入"的语义,配对缺失时既不报错也不回退——这是本次故障最难定位的原因。
解法与验证:
# 修复:启动参数改一行
–load-format instanttensor → –load-format safetensors
# 复核(诊断模式下)
docker exec vllm_node grep -c PAIRED /tmp/attn-dbg.log # 期望 48(故障时 0)
docker exec vllm_node grep -m1 'qkv-load.*qkv_proj.weight ' /tmp/attn-dbg.log # 应有输出
普适教训:含"成对加载逻辑"的模型(融合 QKV + FP8)不宜使用 instanttensor 类快速加载器;同时,加载器/模型侧对"配对超时/缺键"应当显式报错而不是静默跳过——静默是这次排错耗时的大头。
4.3 重点案例:加载期崩溃 1856 vs 1792——fused-QKV 几何
现象:引擎初始化约 1 分钟后失败,死在权重加载首分片约 2% 处:
RuntimeError: The size of tensor a (1856) must match the size of tensor b (1792) at non-singleton dimension 0
File "…/vllm/model_executor/models/mimo_v2.py", line 525, in _shard_fp8_qkv_proj
根因:镜像处理 FP8 融合 QKV 反量化时,用 scale_rows_per_group = s_full.shape[0] // num_kv_heads 计算每组的 scale 行数,隐含假设"每组行数是 128(FP8 块大小)的整数倍"。而该模型检查点里:
| 全局注意力(4 KV 头) | [13568, 4096] F8_E4M3 | [108, 32] F32 | 3392 = 3072(Q)+192(K)+128(V) | ⌈3392/128⌉=27 → 4×27=108 ✓ |
| 滑窗注意力(8 KV 头) | [14848, 4096] F8_E4M3 | [116, 32] F32 | 3712 = 3072(Q)+384(K)+256(V) | 3712/128=29 → 4×29=116 ✓ |
两个恒等式(4×块行数 = 总行数、4×⌈块行/128⌉ = scale 行数)共同证明:融合 QKV 是"4 分块、块内 [Q|K|V]“,而不是全局 [Q|K|V]、也不是"num_kv_heads 个连续组”。滑窗层按原假设取 116 // 8 = 14 块 → 展开 1792 行 ≠ 真实 1856 行 → 直接抛错。
解法:使用 mods/fix-mimo-v2-qkv-block 补丁——按分块窗口反量化 → 重组全局 [Q|K|V] → 按 TP rank 取行 → 重新量化,并含布局自检与形状校验。修复后 PAIRED=48、输出正确。
4.4 其它实战坑(按出现频率排序)
| 1 | launcher 复用同名容器 | 改了挂载/参数重启,改动完全不生效。启动前强制 docker rm -f vllm_node 并轮询确认容器消失(内存互斥场景下也是必需动作) |
| 2 | 卷/补丁挂在每个节点生效 | 双机模式下 -v 与 mod 在每个被启动的节点生效,文件必须两节点同路径齐备;只同步 Head 会挂载失败 |
| 3 | –kv-cache-dtype fp8 被静默降级 | DiffKV 后端声明 supported_kv_cache_dtypes=["auto","bfloat16"];实测 fp8 轮 KV 池 745,197 vs bf16 739,270 tokens(非 ≈2× 关系,说明确实没生效)。不要依赖该参数 |
| 4 | 基准测试的 prefix cache 污染 | 同容器重复跑基准会命中 prefix cache,prefill 被跳过,产生"32K 输入 TTFT 0.5s"这类假数据。对策:每轮唯一随机 seed(SEED=$((RANDOM+100000))),或换容器冷启动 |
| 5 | flashinfer_cutlass 不可用 | 该名字在本镜像只保留 BF16 激活变体,而本机内核不支持 BF16 激活,直接报错;请用 deep_gemm |
| 6 | 推理型模型的"空输出" | MiMo 先输出思维链(reasoning 字段),短 max_tokens 下 content 可能为空——冒烟测试建议 max_tokens ≥ 256,或只校验 HTTP 200 与 choices 存在 |
| 7 | 首次 JIT 编译慢 | 首次启动 FlashInfer/内核编译需数分钟,编译缓存(如 ~/.cache/flashinfer)持久化后可恢复到常规耗时 |
| 8 | 隐蔽的 NCCL 退化 | 容器缺 /dev/infiniband 时 NCCL 无报错退化为 TCP,带宽 ~9.8 GB/s → ~2.1 GB/s,推理吞吐 65.4 → 34.6 tok/s;不看 NCCL_DEBUG=INFO 发现不了 |
5. 性能测试建议
5.1 测试口径(保证数据可比、可复现)
| 工具 | 容器内 vllm bench serve –backend openai-chat |
| 数据集 | –dataset-name random,固定输入/输出长度,–ignore-eos(避免提前终止干扰同口径比较) |
| 指标 | –percentile-metrics ttft,tpot,itl,e2el,–metric-percentiles 50,95,99 |
| 冷启动 | 每轮基准使用唯一随机 seed(或新容器),确保 prefill 不被 prefix cache 跳过——否则数据全部失真 |
| 环境一致 | 各配置同机顺序测试、间隔散热、GPU 利用率同步采样;单轮样本仅作方向参考 |
| 用例 | s1 单流(1K/256/C1)· s2 中等并发(1K/256/C4)· s3 突发(2K/256/C16)· s4 长上下文(32K/64/C4) |
# 用例示例(容器内)
SEED=$(( RANDOM + 100000 )) # 关键:每轮唯一 seed
vllm bench serve –backend openai-chat –model vllm \\
–base-url http://127.0.0.1:8000 \\
–dataset-name random –random-input-len 2048 –random-output-len 256 \\
–num-prompts 16 –max-concurrency 16 –ignore-eos –seed $SEED \\
–percentile-metrics ttft,tpot,itl,e2el –metric-percentiles 50,95,99 \\
–save-result –result-dir /tmp/vllm-bench/mimo
5.2 实测结果(调优后配置)
| s1 单流 1K/256/C1 | 24.4 | 574 | 38.8 | 37.3 |
| s2 中等并发 1K/256/C4 | 54.2 | 908 | 70.4 | 68.6 |
| s3 突发 2K/256/C16 | 87.6 | 4,890(p99 7,670) | 163.5 | 71.7(TTFT 10,508) |
| s4 长上下文 32K/64/C4 | 4.6 | 33,000 | — | 3.8(TTFT 43,727) |
读法:
- 单流/小并发:MiMo 的 TPOT 38.8ms(≈26 tok/s 等效)比对照栈慢,后者开了 n=3 投机解码;
- 突发并发:–max-num-seqs 16 后 MiMo 反超(87.6 vs 71.7 tok/s,TTFT 4.9s vs 10.5s);
- 长上下文:48 层中 39 层是窗口 128 的滑窗注意力,32K 输入场景 MiMo 略优;
- "有效产出"打折:推理型模型同样 max_tokens 下先输出思维链,用户可见答案占比更低,选型时按"每千 token 可用答案"折算。
5.3 两项单变量调优的取舍(每项先过质量探针再进基准)
| –max-num-seqs 8→16 | 突发 C16:吞吐 66.4 → 87.6 tok/s(+32%)、TTFT 18.0 → 4.9s(−73%)、p99 34.7 → 7.7s;C4 吞吐 +17%;单流 +15%;长上下文持平 | 采纳(已固化) |
| MTP 投机解码(method=mtp, n=1) | server 指标显示 draft 接受率仅 12–30%;逐用例 s1 +11% / s2 −2.6% / s3 −14% / s4 −3.5% | 不采纳(已回退,草稿计算基本被浪费) |
方法论提醒:这两项结论都建立在"改一个变量→先跑质量探针(事实问答+退化指标)→再跑冷启动基准"的流程上;跳过质量探针直接看吞吐,很可能把"坏掉的快"当成"优化成功"。
5.4 建议的测试流程
6. 使用注意事项
7. 小结与参考
一句话总结:在两台 DGX Spark 上跑 MiMo-V2.6-Flash,真正的门槛不在硬件也不在参数,而在**“版本边界”**——镜像(2026-09-13 的 dev 构建)与模型(2026-09-22 发布)之间存在两处不兼容:加载器丢 FP8 融合 QKV 权重(静默乱码)、fused-QKV 分组假设错误(加载崩溃)。两者分别用 –load-format safetensors 与 fused-QKV 补丁 规避后,模型双机 TP=2 稳定运行,突发并发与长上下文场景下吞吐已可用于生产。
参考来源:
- 模型主页(官方部署说明,SGLang cookbook + vLLM recipe):https://huggingface.co/XiaomiMiMo/MiMo-V2.6-Flash-RL
- spark-vllm-docker(镜像与跨节点启动工具链):https://github.com/eugr/spark-vllm-docker
- 社区 MiMo 系列双机 Spark 案例(MiMo-V2.5 配方,思路可参考):
- https://github.com/tonyd2wild/MiMo-V2.5-TP2-1M-NVFP4-KV-2xDGX-Spark
- NVIDIA Developer Forum 长贴:https://forums.developer.nvidia.com/t/mimo-v2-5-new-model/368097
- vLLM Issue #35329(MXFP4 MoE 相关,用于对照排除):https://github.com/vllm-project/vllm/issues/35329
本文基于 2026-09-22/23 的两次真实部署与一轮性能调优整理;文中版本号、参数与实测数据均为本机读数,模型与镜像更新后请重新验证。

