概述

在 NVIDIA DGX Spark(GB10, 128GB 统一内存, aarch64)上部署 Qwen3.5-35B-A3B 的完整踩坑与解决路径。核心结论:BF16 原版模型在统一内存架构上因 torch.compile 内存峰值不可控,必须切换到 FP8 量化 + enforce-eager + Docker 方案才能稳定运行。

> 硬件:NVIDIA DGX Spark (GB10, 128GB 统一内存, aarch64) > 系统:Ubuntu 24.04.4 LTS, CUDA 13.0, Driver 580.142 > 最终方案:vLLM 0.19.2 nightly (Docker) + Qwen3.5-35B-A3B-FP8

关键要点

  • FP8 量化是唯一稳定选择:BF16 权重 64.69 GiB,加载后仅剩 ~56 GiB,torch.compile profiling 阶段必然 OOM;FP8 权重 34.22 GiB,剩余 ~87 GiB,余量充足
  • 必须 enforce-eager:torch.compile + CUDA graph capture 的内存峰值在统一内存架构上致命,--enforce-eager 跳过该阶段
  • Docker 优于 pip venv:vLLM nightly 依赖 cu13,torch 内部仍链接 cu12 库,pip 环境下两套共存且无法拆分;Docker 镜像 vllm/vllm-openai:cu130-nightly-aarch64 已解决依赖
  • NGC 官方镜像版本滞后nvcr.io/nvidia/vllm:26.01-py3 不支持 qwen3_5_moe 架构,需用社区 nightly 镜像
  • 内存看门狗必备:128GB 统一内存无独立显存保护,OOM 可致系统卡死;Docker 方案下 docker stop 可干净释放所有子进程和内存
项目 踩坑方案 最终方案
模型精度 BF16 原版 (65GB) FP8 量化版 (35GB)
运行方式 pip venv 直装 Docker 容器
编译模式 torch.compile (默认) --enforce-eager
上下文长度 32768 16384 (可调至 32K)
gpu-memory-utilization 0.80 0.75

技术细节

踩坑记录

坑 1:BF16 模型加载后内存不够 torch.compile

现象:模型权重加载完成(64.69 GiB),torch.compile 编译图完成后,进入 profiling/warmup 阶段,内存从 61% 飙升到 92%+,触发 OOM 或被 watchdog 杀掉。

原因

  • BF16 模型权重占 64.69 GiB,128GB 内存只剩 ~56 GiB
  • torch.compile 编译 + CUDA graph capture 需要大量临时内存
  • profiling 阶段的 dummy run 会按 max-model-len 分配临时 KV cache

解决

  1. 换 FP8 量化模型(35GB,省了近一半)
  2. --enforce-eager 跳过 torch.compile 和 CUDA graph capture
  3. 降低 --max-model-len 减少 profiling 内存峰值

坑 2:pip 安装 vLLM 导致 CUDA 12/13 库混装

现象:venv 里同时存在 nvidia-cublas-cu12nvidia-cublas(cu13) 等成对的包,进程运行时 libcudart.so.12libcudart.so.13 同时被加载。

原因

  • vLLM nightly wheel 依赖 cu13 的 nvidia 包
  • torch 2.11+cu130 的 wheel 内部仍然链接了部分 cu12 的 .so(如 libcudnn、libcusparseLt)
  • pip 解析依赖时两套都装了

教训:尝试卸载 cu12 包会导致 torch 无法启动(ImportError: libcudnn.so.9 / libcusparseLt.so.0),因为 torch 运行时确实需要这些 cu12 的库。cu12 包不能删。

解决:放弃 pip venv 方案,改用 Docker 镜像 vllm/vllm-openai:cu130-nightly-aarch64,镜像内依赖已经配好,不存在混装问题。

坑 3:Watchdog 杀不干净 vLLM 进程

现象:watchdog 发送 SIGTERM/SIGKILL 给 vLLM 主进程后,内存没有释放,EngineCore 子进程仍在运行。

原因:vLLM 会 fork 出 EngineCore 工作进程,杀主进程不会自动杀子进程。

解决(两种方案):

  • venv 方案:watchdog 遍历 /proc 收集整个进程树的 PID,逐个杀掉
  • Docker 方案(推荐):直接 docker stop 容器,容器运行时会清理所有子进程,内存完全释放

坑 4:NGC 官方 vLLM 镜像版本太旧

现象nvcr.io/nvidia/vllm:26.01-py3 镜像内的 vLLM 版本不支持 Qwen3.5 的 MoE 架构(qwen3_5_moe model type 未注册)。

解决:使用 vLLM 社区的 nightly 镜像 vllm/vllm-openai:cu130-nightly-aarch64,版本 0.19.2rc1+,已支持 Qwen3.5。

坑 5:Docker Hub 拉取超时

现象:国内网络直连 Docker Hub 的 TLS 握手超时。

解决:在 /etc/docker/daemon.json 中配置国内镜像源:

{
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.xuanyuan.me",
"https://docker.m.daocloud.io"
]
}

配置后 systemctl restart docker,拉取速度从超时变为正常。

最终部署方案

下载 FP8 模型

使用 ModelScope 国内源加速下载:

pip install modelscope
modelscope download --model Qwen/Qwen3.5-35B-A3B-FP8 --local_dir /home/rico/models/Qwen3.5-35B-A3B-FP8

模型大小约 35GB,14 个 safetensors 分片。

Docker 启动命令

docker run -d
--name qwen35-fp8
--runtime nvidia
--gpus all
--shm-size 16g
-p 8000:8000
-v /home/rico/models/Qwen3.5-35B-A3B-FP8:/model
-e PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
-e TORCHINDUCTOR_COMPILE_THREADS=1
vllm/vllm-openai:cu130-nightly-aarch64
--model /model
--served-model-name qwen3.5-35b
--host 0.0.0.0
--port 8000
--dtype auto
--max-model-len 16384
--gpu-memory-utilization 0.75
--max-num-seqs 16
--max-num-batched-tokens 16384
--enforce-eager
--enable-prefix-caching
--reasoning-parser qwen3
--enable-auto-tool-choice
--tool-call-parser qwen3_coder
--trust-remote-code

关键参数说明

参数 说明
--dtype auto auto → bfloat16 FP8 权重自动识别,计算用 BF16
--max-model-len 16384 16K 保守值,可调至 32K(需 util=0.80)
--gpu-memory-utilization 0.75 75% 预分配给 KV cache 的显存比例
--enforce-eager 跳过 torch.compile,避免内存峰值
--enable-prefix-caching 相同前缀的对话复用 KV cache,节省计算
--max-num-seqs 16 16 最大并发请求数
--shm-size 16g 16GB Docker 共享内存,vLLM 进程间通信需要

运行时资源占用

指标
模型加载 34.22 GiB
稳态内存(空闲) ~100 GiB / 128 GiB (82%)
KV cache blocks 2567 blocks × 16 tokens = 41,072 tokens
推理时 GPU 利用率 ~60-70%
推理速度 ~33-35 tok/s (单请求)
启动时间 ~3 分钟(加载 + profiling)

上下文长度调优

模型原生支持 262,144 tokens (256K),但受内存限制需要权衡:

gpu-memory-utilization 预估 KV 容量 建议 max-model-len
0.75 (当前) ~41K tokens 16384
0.80 ~45K tokens 32768
0.85 ~50K tokens 40960
0.90 (激进) ~54K tokens 49152

util 越高,留给系统的内存越少,OOM 风险越大。建议不超过 0.85。

BF16 vs FP8 对比

BF16 原版 FP8 量化版
模型大小 64.69 GiB 34.22 GiB
加载后剩余内存 ~56 GiB ~87 GiB
torch.compile 内存爆炸 可以但没必要
enforce-eager 勉强能跑 稳定运行
精度损失 基准 <0.5%
推理速度 基准 快 40-60%

在 128GB 统一内存设备上,FP8 是唯一稳定的选择。

内存安全:Watchdog 机制

DGX Spark 的 128GB 统一内存被 CPU 和 GPU 共享,没有独立显存保护。一旦内存耗尽,系统可能直接卡死。因此部署了内存看门狗:

  • 启动阶段(前 5 分钟):阈值 96%,容忍 profiling 峰值
  • 正常阶段:阈值 90%,超过则 docker stop 容器
  • Docker 方案的优势:docker stop 能干净释放所有内存,不会留下僵尸进程

备注

不推荐的做法:

  1. 不要用 pip venv 直装 vLLM — cu12/cu13 依赖混装难以解决,Docker 更干净
  2. 不要用 NGC 官方 vLLM 镜像 — 版本滞后,不支持最新模型架构
  3. 不要在 DGX Spark 上跑 BF16 原版 35B — 内存不够 torch.compile
  4. 不要省略 --enforce-eager — torch.compile 的内存峰值在统一内存架构上是致命的
  5. 不要省略 --shm-size — 默认 64MB 不够 vLLM 进程间通信,会报错
  6. 不要把 gpu-memory-utilization 设太高 — 统一内存没有独立显存保护,留余量给系统

原文链接

原始笔记来源:收藏文章/NVIDIA DGX Spark 部署 Qwen3.5-35B-A3B 踩坑记录与最佳实践.md