欢迎光临
我们一直在努力

两台 DGX Spark 部署 MiMo-V2.6-Flash 全记录:309B MoE 双机 TP=2 环境准备、部署步骤、排错与性能测试

/* 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 后端会换一个吸引子外观)。

定位路径(每步只引入一个变量的排查法,非常好用):

  • 判据实验:构造等长、仅第 0 个内容 token 不同的两条提示,比较 prompt_logprobs——发现位置 4(前一个 token 不同)预测不同、位置 5 之后完全相同 ⇒ 信息无法跨 ≥2 个位置传递,说明注意力输出≈0,模型退化为"逐位置 MLP";
  • 前向打点:在注意力层记录量级,|hs|=7.7e-01(输入正常)但 |q|=|k|=|v|=|attn_out|=0(精确 0);
  • 参数打点:qkv_proj.weight(FP8)均值 0、零占比 1.0(仍是初始化值,从未写入);
  • 加载期打点:48 层全部 WAITING … have=['weight_scale_inv'],PAIRED 计数 = 0 ⇒ 只有 scale 到达,qkv_proj.weight 从未出现在加载流中;
  • 归因确认:换 –load-format safetensors 后同一打点立即 PAIRED=48,模型输出正确答案。
  • 根因:镜像的 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 块大小)的整数倍"。而该模型检查点里:

    层型(按注意力窗口分)qkv_proj.weightweight_scale_inv4 分块中每块行数每块 128 行块数
    全局注意力(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 实测结果(调优后配置)

    用例输出吞吐(tok/s)TTFT 均(ms)TPOT 均(ms)参考:另一生产栈(DeepSeek-V4-Flash)
    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 建议的测试流程

  • 服务就绪后先跑质量探针("中国的首都是哪里?"→ 北京 + 退化指标),通过后再进基准;
  • 每个配置一轮冷启动基准(唯一 seed) + GPU 利用率采样,原始 JSON 与日志归档留档;
  • 任何参数变更(并发上限、加载器、KV dtype、MoE 后端)后:重跑质量探针 + 重跑基准,不做跨配置对比;
  • 记录就绪耗时与 KV 池容量——它们是"参数是否真的生效"最直接的证据。

  • 6. 使用注意事项

  • 它是推理型模型:响应里 reasoning 字段先输出思维链,content 是最终答案;客户端 max_tokens 建议 ≥256,短输出可能只有思维链。
  • 模型别名:服务注册 vllm(常驻别名)与 MiMo-V2.6-Flash 两个名字;若与其它模型栈共用端口做切换,客户端统一使用 vllm 即可零改动。
  • 加载慢是特性不是故障:标准加载器(正确性优先)约 10 分钟,就绪等待务必设 ≥1200s;不要为了"快 20 倍"换回 instanttensor——代价是静默输出乱码。
  • 改任何加载相关配置后必须复核:PAIRED=48(诊断模式)+ 事实问答正确,两者都过才算通过;只"服务能起"不等于可用。
  • 参数固化原则:–load-format safetensors、fused-QKV 补丁、deep_gemm + triton_attn_diffkv、NCCL 两个环境变量,这五项是已验证组合,不要随意改动;–max-num-seqs 16 为实测最优,MTP 不建议启用。
  • 资源互斥:该模型双机运行时吃满两节点统一内存并独占 8000 端口;与其它模型栈只能互斥运行,切换前两节点都要 docker rm -f vllm_node。
  • KV 池与上下文:实测 KV 池 739,270 tokens(256K 上下文约 2.82× 并发);如需更大并发,优先调 –max-num-batched-tokens 与 –gpu-memory-utilization,但改后须重跑第 5 节流程。
  • 离线加载:容器内固定 HF_HUB_OFFLINE=1,权重以挂载路径直接加载;换机/重装后记得两节点重新同步权重(178GB)与补丁文件。
  • 经网关接入时的联动:渠道模型清单、路由 abilities 表、令牌白名单三处都要加别名,缺一处会"列表里有、调用报无可用渠道";网关内存水位阈值建议提到约 97(否则统一内存吃满会被误判过载,全量请求被拒)。
  • 低峰维护:冷启动首次编译、权重同步、大版本切换都建议在低峰窗口执行,并保留回滚路径(另一套栈的就绪约 3–4 分钟)。

  • 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 的两次真实部署与一轮性能调优整理;文中版本号、参数与实测数据均为本机读数,模型与镜像更新后请重新验证。

    赞(0)
    未经允许不得转载:171主机测评 » 两台 DGX Spark 部署 MiMo-V2.6-Flash 全记录:309B MoE 双机 TP=2 环境准备、部署步骤、排错与性能测试
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址