四张卡没毛病,是环境一路设卡: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 和激活值。

三、假期前:备齐料,顺手埋个雷

长假前那几天,我干了三件事:

  1. 下载。 131 个分片,下完传到文件存储,挂成 PVC。
  2. 抄菜谱。 翻官方的 vLLM recipes 页,把推荐的并行度和显存比例记下来。
  3. 改镜像。 把之前那套构建镜像的 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

几个参数值得单独说一句:

部署全景:模型文件以 PVC 挂进 vLLM 容器,容器用 TP=4 把权重摊到四张 GPU 卡上,对外暴露 8000 端口。
图 1:链路不长,但每一环都可能卡住你。

Pod 起来了,日志滚了几行,停了。第一句像样的报错是:

cuInit failed: 802

我当时还不知道,802 这三个数字后面,跟着七个版本的镜像。

五、六个坑,一张速查表

先给结论:从第一版镜像到模型跑通,一共填了六个坑,构建了七版镜像。

#报错关键字真正的根因一句话解法
1cuInit ... 802宿主机 fabric manager 没跑重启 GPU 节点,让 fabric manager 恢复
2容器里 torch.version.cuda = 12.5镜像打包了旧 libcuda.so.1,盖住了宿主机的驱动层库不进镜像,用宿主机挂载的
3nvcc: command not found基础镜像没有 CUDA 编译器,内网源也没有包从外网机器取 nvcc,打包进镜像
4cannot execute 'cc1plus'装了 gcc,没装 g++补 build-essential g++
5cuda_runtime.h: No such fileln -sf 的目标目录已存在,链接被建进了目录里先 rm -rf,再 cp -a
6curand.h: No such filenvcc 包里只有 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 时代的旧拷贝,容器启动后加载到的是我这份旧的,宿主机那份更新的就被盖住了。

左图镜像不含驱动库,容器内看到 13.2;右图镜像自带 12.5 的 libcuda.so.1,压住宿主机挂载的同名库,容器内看到 12.5。
图 2:同一个文件名,看它从哪儿来。

打个比方:租车公司给你配了新车,你上车前非要从后备箱掏出一个旧方向盘换上,然后怪车跑不起来。

改法很简单——驱动层的东西一律不进镜像。

# 正确做法:不打包 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 生成的速度有多快,我很想知道。

好了,这一篇就写到这里,欲知后事如何,且听下回分解!

留言

0/1000