概述
在 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
解决:
- 换 FP8 量化模型(35GB,省了近一半)
- 加
--enforce-eager跳过 torch.compile 和 CUDA graph capture - 降低
--max-model-len减少 profiling 内存峰值
坑 2:pip 安装 vLLM 导致 CUDA 12/13 库混装
现象:venv 里同时存在 nvidia-cublas-cu12 和 nvidia-cublas(cu13) 等成对的包,进程运行时 libcudart.so.12 和 libcudart.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能干净释放所有内存,不会留下僵尸进程
备注
不推荐的做法:
- 不要用 pip venv 直装 vLLM — cu12/cu13 依赖混装难以解决,Docker 更干净
- 不要用 NGC 官方 vLLM 镜像 — 版本滞后,不支持最新模型架构
- 不要在 DGX Spark 上跑 BF16 原版 35B — 内存不够 torch.compile
- 不要省略
--enforce-eager— torch.compile 的内存峰值在统一内存架构上是致命的 - 不要省略
--shm-size— 默认 64MB 不够 vLLM 进程间通信,会报错 - 不要把 gpu-memory-utilization 设太高 — 统一内存没有独立显存保护,留余量给系统
原文链接
原始笔记来源:收藏文章/NVIDIA DGX Spark 部署 Qwen3.5-35B-A3B 踩坑记录与最佳实践.md