四张卡没毛病,是环境一路设卡:Qwen3.8-Flash-Next 部署记(一)
预警:这不是一篇十分钟部署教程,而是一份七个镜像版本的翻车记录。
一、接到一个任务
国庆前的那次组会,我接到一个任务:本地化部署一套大模型推理服务。
我当时觉得这事不算太难——下载模型、在 K8s 上拉起一个 Pod、vLLM 的参数照官方文档推荐来。后来的事实证明,这三件事里,只有“下载模型”是真的简单。
二、选型:186G 的行李,四个 95G 的格子
打开 ModelScope,先把公司已经提供的公有云模型排除掉,剩下的标准就四条:够新、够强、塞得下、跑得快。
很快锁定目标:Qwen3.8-Flash-Next 的 FP8 版本。模型页在 Qwen/Qwen3.8-Flash-Next-FP8,选择理由如下:
| 我在意的 | 具体要求 | 这个模型的回答 |
|---|---|---|
| 新鲜度 | 别拿半年前的模型凑数 | Qwen4 的预发布版本,架构同源 |
| 模态 | 文字要能读,图片也要能看 | 原生多模态 |
| 速度 | 在线服务,回答不能磨叽 | Flash 系列,天生冲着延迟去的 |
| 体积 | 四张卡之内解决 | FP8 量化后 186G,131 个分片 |
算笔账。一张 GPU 卡有 97871 MiB,约 95.6 GiB 显存,四张合起来约 382 GiB。186G 权重摊到四张卡上,每张大约 47 GiB,剩下的显存留给 KV cache 和激活值。
三、假期前:备齐料,顺手埋个雷
长假前那几天,我干了三件事:
- 下载。 131 个分片,下完传到文件存储,挂成 PVC。
- 抄菜谱。 翻官方的 vLLM recipes 页,把推荐的并行度和显存比例记下来。
- 改镜像。 把之前那套构建镜像的 Dockerfile 丢给 AI,让它按新模型的要求改一版。
第三条是这次最省事的一步,也是最要命的一步。AI 改得又快又像模像样,我扫了一眼就放行构建了。
这一步埋了一个大坑,只不过我当时浑然不觉。
四、开火:第一次就哑了
假期结束,模型文件就绪,镜像也构建好了。创建 Deployment、挂上 PVC、按官方推荐的参数拉起服务。
容器里真正跑的是下面这条命令:
source /opt/vllm-venv/bin/activate
python3 -m vllm.entrypoints.openai.api_server \
--model /models/Qwen3.8-Flash-Next-FP8 \
--served-model-name qwen38-flash-next \
--tensor-parallel-size 4 \
--dtype auto \
--kv-cache-dtype fp8 \
--max-num-seqs 256 \
--gpu-memory-utilization 0.95 \
--max-model-len 200000 \
--trust-remote-code \
--enable-prefix-caching \
--enable-auto-tool-choice \
--tool-call-parser qwen3_xml \
--reasoning-parser qwen3 \
--port 8000 \
--host 0.0.0.0
几个参数值得单独说一句:
--kv-cache-dtype fp8:KV cache 也用 FP8 存,长上下文时省显存的关键开关。--tool-call-parser qwen3_xml与--reasoning-parser qwen3:一个负责把工具调用从回复文本里解析出来,一个负责把思考过程和正文分开——后面接业务时全靠这两位。
Pod 起来了,日志滚了几行,停了。第一句像样的报错是:
cuInit failed: 802
我当时还不知道,802 这三个数字后面,跟着七个版本的镜像。
五、六个坑,一张速查表
先给结论:从第一版镜像到模型跑通,一共填了六个坑,构建了七版镜像。
| # | 报错关键字 | 真正的根因 | 一句话解法 |
|---|---|---|---|
| 1 | cuInit ... 802 | 宿主机 fabric manager 没跑 | 重启 GPU 节点,让 fabric manager 恢复 |
| 2 | 容器里 torch.version.cuda = 12.5 | 镜像打包了旧 libcuda.so.1,盖住了宿主机的 | 驱动层库不进镜像,用宿主机挂载的 |
| 3 | nvcc: command not found | 基础镜像没有 CUDA 编译器,内网源也没有包 | 从外网机器取 nvcc,打包进镜像 |
| 4 | cannot execute 'cc1plus' | 装了 gcc,没装 g++ | 补 build-essential g++ |
| 5 | cuda_runtime.h: No such file | ln -sf 的目标目录已存在,链接被建进了目录里 | 先 rm -rf,再 cp -a |
| 6 | curand.h: No such file | nvcc 包里只有 cudart 的头文件 | 把 cuRAND/cuBLAS/cuFFT 等头文件也带上 |
下面按踩到的顺序拆。
坑 1:cuInit 802 —— 我先怀疑错了对象
Pod 启动就退出,日志里只有一行 cuInit failed: 802。
我的第一反应是:vGPU 参数没配对?还是卡之间的 NVLink 被切坏了?我甚至在容器里把可见设备列表翻了一遍,越看越觉得“这就是虚拟化层的事”。
转折点是去问了公司对应的 K8s 运维同事。他一句话把这条路堵死了:“我们的 vGPU 方案是 HAMi,不会独立一个驱动栈,没有 guest 和 host 之分。”
也就是说,容器里看到的就是宿主机的驱动,不存在两套驱动打架。那 802 只能往宿主机看。
一看就明白了:宿主机上的 NVIDIA fabric manager 没起来。这类靠 NVLink 互联的多卡机器,fabric manager 管的就是卡与卡之间的内部高速公路;它不在,四张卡等于互不认识。
最后是重启了 GPU 节点,fabric manager 状态正常了,802 才消失。
这个坑的账单是这样的:真正的修复只花了十分钟,前面怀疑 vGPU 花了大半天。教训很朴素——报错写在容器里,根因未必在容器里。 以后遇到 GPU 初始化类报错,先按三层过一遍:宿主机服务(fabric manager)→ 宿主机驱动(nvidia-smi)→ 容器运行时。
坑 2:容器说 12.5,宿主机说 13.2
802 走了以后,报错换了张脸:flashinfer 编译失败,顺手一查,容器里 torch.version.cuda 报 12.5,而宿主机 nvidia-smi 明明白白写着 13.2。
原因在我假期前改的那版 Dockerfile 里:
# 当时的想法:“把需要的库都带上更保险”
COPY libcuda.so.1 /lib/x86_64-linux-gnu/libcuda.so.1
RUN ln -sf /lib/x86_64-linux-gnu/libcuda.so.1 /lib/x86_64-linux-gnu/libcuda.so
COPY cuda-12.5/ /usr/local/cuda-12.5/
RUN ln -sfn /usr/local/cuda-12.5 /usr/local/cuda
问题在于,libcuda.so.1 是驱动层的库,本来就该由宿主机提供:容器运行时会把宿主机上的驱动库挂进容器。我在镜像里又塞了一份 12.5 时代的旧拷贝,容器启动后加载到的是我这份旧的,宿主机那份更新的就被盖住了。
打个比方:租车公司给你配了新车,你上车前非要从后备箱掏出一个旧方向盘换上,然后怪车跑不起来。
改法很简单——驱动层的东西一律不进镜像。
# 正确做法:不打包 libcuda.so.1,也不打包整个 cuda-12.5 目录
# COPY libcuda.so.1 ... ← 删掉
# COPY cuda-12.5/ ... ← 删掉
改完进容器验证两行:
nvidia-smi # 应显示宿主机的 595.91.07 / CUDA 13.2
python3 -c "import torch; print(torch.version.cuda)"
那需要编译(nvcc)怎么办?见坑 3。答案是:不搬整套 toolkit,只搬编译器。
坑 3:nvcc: command not found
库对齐了,vLLM 启动时 flashinfer 要做 JIT 编译,报 nvcc: command not found。
根因有两层:基础镜像是干净的 ubuntu:24.04,没有 CUDA toolkit;内网软件源里也没有 cuda-nvcc 相关的包。
不纠结了,物料自己带。找一台能上外网的机器,下 CUDA 12.6 的 runfile,解包里只留三个组件,打个 72MB 的包带过来:
wget https://developer.download.nvidia.com/compute/cuda/12.6.0/local_installers/cuda_12.6.0_560.28.03_linux.run
chmod +x cuda_12.6.0_560.28.03_linux.run
./cuda_12.6.0_560.28.03_linux.run --extract=$HOME/cuda_extract
cd ~/cuda_extract
tar czf ~/nvcc-12.6.tar.gz cuda_nvcc cuda_cudart cuda_cccl
COPY nvcc-12.6.tar.gz /tmp/nvcc-12.6.tar.gz
RUN tar xzf /tmp/nvcc-12.6.tar.gz -C /usr/local/ && \
ln -sf /usr/local/cuda_nvcc/bin/nvcc /usr/local/bin/nvcc
ENV CUDA_HOME=/usr/local/cuda_nvcc \
PATH="/usr/local/cuda_nvcc/bin:$PATH"
为什么是 12.6,而不是跟宿主机一样的 13.2,或者更新的版本?先说清楚一点:12.6 并不是 CUDA 的最新版,现在 NVIDIA 已经发到 13.x 了。选它纯粹是够用——驱动向下兼容,nvcc 12.6 编出来的 kernel 在 13.2 的驱动上照样跑,而这边缺的只是一个编译器,没必要把整套新 toolkit 搬进镜像。驱动归宿主机,编译器归镜像,铁路警察,各管一段。
坑 4:cc1plus: cannot execute
nvcc 找到了,编译也跑起来了,然后:
gcc: fatal error: cannot execute 'cc1plus': No such file or directory
原因低幼得有点好笑:我装了 gcc,没装 g++。nvcc 编译 .cu 时要去调宿主编译器,C++ 前端就在 g++ 里。
RUN apt-get update -qq && \
DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 LC_ALL=C.UTF-8 \
apt-get install -yqq --no-install-recommends \
ca-certificates curl locales \
build-essential gcc g++ ninja-build cmake pkg-config \
git openssh-client procps net-tools \
python3.12 python3.12-venv python3.12-dev python3-pip
比喻的话:好不容易把厨师请进厨房,结果发现没给他锅铲。
坑 5:cuda_runtime.h 找不到 —— ln -sf 的经典陷阱
编译器齐了,下一句:
fatal error: cuda_runtime.h: No such file or directory
我明明把 cudart 的头文件链进去了:
ln -sf /usr/local/cuda_cudart/targets/x86_64-linux/include /usr/local/cuda_nvcc/include
问题出在 ln -sf 的一个老脾气:当目标路径已经是一个目录时,它不会替换这个目录,而是在目录内部再建一个链接。 解压出来的 tarball 里本来就有个空的 cuda_nvcc/include/,于是实际得到的是:
/usr/local/cuda_nvcc/include/include → cudart headers
nvcc 按 -isystem /usr/local/cuda_nvcc/include 去找,看到的是那个空目录,自然找不到。
比喻:快递员到了一看,门口有个鞋柜,就把包裹塞进鞋柜,然后告诉你“已送达客厅”。
改法:先删,再拷。
RUN rm -rf /usr/local/cuda_nvcc/include /usr/local/cuda_nvcc/lib64 && \
cp -a /usr/local/cuda_cudart/targets/x86_64-linux/include /usr/local/cuda_nvcc/include && \
cp -a /usr/local/cuda_cudart/targets/x86_64-linux/lib /usr/local/cuda_nvcc/lib64
坑 6:curand.h 找不到
cuda_runtime.h 过了,flashinfer 编到 sampling.cu,又缺一个:
fatal error: curand.h: No such file or directory
原因:我带的 nvcc 包里只有 cudart(CUDA 运行时)的头文件,而 flashinfer 还要 cuRAND、cuBLAS、cuFFT、cuSolver、cuSPARSE、NVRTC 这些组件的头文件。
比喻:出门拍胸脯说“东西都带了”,结果只带了米,油盐酱醋一样没有。
解法:在物料机上把完整 CUDA 目录里这些组件的 include 打个小包(558KB),构建时合并进同一个 include 目录。
cd ~/cuda_extract
tar czf ~/extra_cuda_includes.tar.gz \
libcurand/targets/x86_64-linux/include \
libcublas/targets/x86_64-linux/include \
libcufft/targets/x86_64-linux/include \
libcusolver/targets/x86_64-linux/include \
libcusparse/targets/x86_64-linux/include \
cuda_nvrtc/targets/x86_64-linux/include
COPY extra_cuda_includes.tar.gz /tmp/extra_cuda_includes.tar.gz
RUN cd /tmp && tar xzf extra_cuda_includes.tar.gz && \
cp -a /tmp/libcurand/targets/x86_64-linux/include/* /usr/local/cuda_nvcc/include/ && \
cp -a /tmp/libcublas/targets/x86_64-linux/include/* /usr/local/cuda_nvcc/include/ && \
cp -a /tmp/libcufft/targets/x86_64-linux/include/* /usr/local/cuda_nvcc/include/ && \
cp -a /tmp/libcusolver/targets/x86_64-linux/include/* /usr/local/cuda_nvcc/include/ && \
cp -a /tmp/libcusparse/targets/x86_64-linux/include/* /usr/local/cuda_nvcc/include/ && \
cp -a /tmp/cuda_nvrtc/targets/x86_64-linux/include/* /usr/local/cuda_nvcc/include/ && \
rm -rf /tmp/libcurand /tmp/libcublas /tmp/libcufft /tmp/libcusolver /tmp/libcusparse /tmp/cuda_nvrtc
这一版过了。vLLM 开始加载那 131 个分片,日志一屏一屏地刷。
六、跑起来之后
模型加载完,四张卡全被吃住,服务 ready,/v1/models 能正常返回。那一刻比“部署成功”更先涌上来的,是“终于不用再改 Dockerfile 了”。
回头看,这一路真正值钱的不是那几个报错,而是几条边界:
| 教训 | 具体做法 |
|---|---|
| 驱动层不是你的活 | 镜像里不放 libcuda.so.1、不放 cuda-x.y 目录,交给宿主机 |
| 编译期依赖要一次写全 | nvcc、g++、ninja、cmake、CUDA headers,缺一个就多构建一次 |
| 内网构建要有一台“物料机” | 有外网的机器负责下载和切包,镜像只负责 COPY |
| symlink 三步走 | 先 rm -rf、再 cp -a、最后进容器 ls 一眼 |
| 报错先分层 | 宿主机服务 → 宿主机驱动 → 容器运行时 → 应用 |
坦白说,前五个坑每一个都能单独写成一条“五分钟解决”的帖子,合在一起却花掉了我一整个星期。不是问题难,是它们排着队来,而且每次都用上一轮的错误伪装自己。
七、写在最后
推理服务是跑起来了,但 Qwen 这一版 Flash 模型回答时 token 生成的速度有多快,我很想知道。
好了,这一篇就写到这里,欲知后事如何,且听下回分解!
留言
虚位以待,第一条留言会被记入史册。