一台 8 卡平头哥 PPU 一体机,从环境探测到 1966 tok/s 压测的完整部署实录——按「环境 / 流程 / 实测 / 踩坑」四段展开,照着就能复刻。
| 项目 | 配置 |
|---|---|
| 算力 | 8×PPU-ZW610E,每卡 48GB |
| 软件栈 | GPUStack v2.3.0 + vLLM 0.23.0(hggc 后端) |
| 镜像交付 | 离线搬运约 16GB 镜像 |
| 最终状态 | 3 个模型全部上线 |
“国产 GPU 到底能不能跑大模型?”这是被问得最多、也最容易被口号带偏的问题。这篇不谈情怀,只把一台平头哥 PPU 一体机的真实部署过程、十三个踩坑和一组压测数字摊开给你看。
01 部署环境
要在这台 8 卡平头哥一体机上跑大模型,先得把环境探清楚。机器是 8 卡平头哥 PPU-ZW610E(每卡 48GB),软件栈是 GPUStack v2.3.0 + vLLM 0.23.0(hggc 后端)。下面三步实测,决定了后面所有的下载与部署路线。
最终状态(已跑通):
| 模型 | GPU | TP | 副本状态 |
|---|---|---|---|
| qwen3-14b | GPU0 | 1 | 1/1 运行 |
| qwen3-embedding-8b | GPU1 | 1 | 1/1 运行 |
| qwen3-32b | GPU4–7 | 4 | 1/1 运行 |
统一入口:
http://192.168.31.46:8080/v1。32B 加载 62GB 权重加 graph capture 约 12 分钟,三实例修复后 23 分钟零重启。
部署前最该戒掉的习惯,是“照纸面参数办事”。这台机器出厂就枚举了 8 张卡、驱动已预装,原本最担心的“alinux3 无 PPU 驱动”风险直接解除;但另两件事不实测就会埋雷:系统盘装不下 40GB 级镜像,必须把 Docker data-root 迁到 NVMe;整机 80 端口已被自带的 AIStack nginx 占用,GPUStack 只能起在 8080。
还有一项决定后面所有下载路线的关键实测——服务器出口带宽。结论很扎心:服务器出口只有约 1.3 MB/s,大镜像绝不在服务器直下,一律走 Mac 下载 → U 盘 → docker load。
ls /dev/alixpu* # 8 卡出厂已枚举
ppu-smi # 驱动预装,每卡 48GiB
curl -sS -m 10 https://quay.io/v2/ # 401=通
02 部署流程
真正的上线只有三步:把 GPUStack 拉起来、把 8 张 PPU 纳管进集群、再把三个模型部署上去。但前置还有两件“搬运”活儿——镜像和权重必须先就位。下面按实操顺序一步一步来。
1. 先把 Docker data-root 迁到 NVMe
系统盘只剩约 48GB 且已用大半,40GB 级镜像直接 load 必爆根盘。改完重启 Docker 再生效:
{
"data-root": "/bmcp_lvm_fs/docker"
}
systemctl restart docker
2. 镜像与权重离线就位
推理镜像 gpustack/runner:hggc13.0-vllm0.23.0 线上传输约 16.43GB,服务器出口只有约 1.3 MB/s,所以走“Mac 下载 → U 盘 → docker load”。实测三条路线,只有 quay.io 直连能并行(Mac 上 8–16 路 Range 聚合约 12–16 MB/s)。权重由 Mac 从 ModelScope 手动下载,U 盘拷入 /bmcp_lvm_fs/models/hf/models/:
| 模型 | 权重大小 |
|---|---|
| qwen3-14b | 约 28GB |
| qwen3-embedding-8b | 约 15GB |
| qwen3-32b | 约 62GB |
别用代理测速:Mac 默认全流量走本机 Clash 代理,测速前必须加
--noproxy '*'绕开,否则会把代理节点的锅扣在源站头上。
同 digest 可跨源核对:同一镜像在 quay.io 与 Docker Hub 的 digest 完全一致。需要 Hub 镜像时先查 quay.io 有没有同名货,可省一次 16GB 下载。
交付铁律:三次 SHA-256 同值。下载后、拷贝到 U 盘后、
docker load前各算一次 SHA-256,三次同值才算闭环;tar 内置的是裸名 tag,load 后必须补成 quay.io 前缀并删除裸名,否则每次重建部署都会重新拉取 16GB。
3. 启动 GPUStack(Server + Worker 同容器)
整机 80 端口被自带的 AIStack nginx 占用,这里起在 8080;--network=host 让推理容器直接使用宿主网卡,后面多卡通信用得上:
docker run -d --name gpustack \
--restart=unless-stopped \
--network=host --ipc=host \
--device=/dev/alixpu \
--device=/dev/alixpu_ctl \
--device=/dev/alixpu_sep \
--device=/dev/alixpu_ppu0 \
--device=/dev/alixpu_ppu1 \
--device=/dev/alixpu_ppu2 \
--device=/dev/alixpu_ppu3 \
--device=/dev/alixpu_ppu4 \
--device=/dev/alixpu_ppu5 \
--device=/dev/alixpu_ppu6 \
--device=/dev/alixpu_ppu7 \
-v /bmcp_lvm_fs/docker/gpustack:/var/lib/gpustack \
-v /bmcp_lvm_fs/models:/bmcp_lvm_fs/models \
-v /var/run/docker.sock:/var/run/docker.sock \
quay.io/gpustack/gpustack:latest \
--port 8080 --enable-worker \
--cache-dir /bmcp_lvm_fs/models/gpustack-cache
挂载要点:docker.sock 必须挂,否则 GPUStack 无法下发推理容器;models 目录按宿主路径原样同挂,漏挂会落到空目录。ppu0–ppu7 共 8 张卡,上面的命令完整列出全部设备。
4. 纳管与验收
控制台 http://192.168.31.46:8080 → 资源 → GPU,应看到 8×PPU-ZW610E 全在线、vendor=thead。GPUStack 探测 PPU 靠自带捆绑库,不依赖宿主机 ppu-smi——容器内手跑 ppu-smi 报缺库是正常现象,别被带偏。
GPUStack 控制台概览:集群 1 / 节点 1 / GPU 8 / 部署 3,三模型已全部上线。
GPU 列表:8 张 PPU-ZW610E 全部在线,vendor=thead,显存利用率最高 95%。
5. 部署三个模型
走控制台“模型库 / 部署”或 API POST /v2/models(source=local_path):
| 模型 | GPU | TP | 部署要求 |
|---|---|---|---|
| qwen3-14b | GPU0 | 1 | 单卡部署 |
| qwen3-embedding-8b | GPU1 | 1 | 部署类别选 embedding |
| qwen3-32b | GPU4–7 | 4 | 必须配置 NCCL env |
必带项:多卡要改网卡名。32B 四卡 TP=4 起不来,报
NCCL ERROR Bootstrap: no socket interface found——根因是推理容器走 host 网络,却默认注入NCCL_SOCKET_IFNAME=eth0,而本机网卡是bond0。模型 env 覆盖NCCL_SOCKET_IFNAME=bond0(模型级优先)。今后任何多卡部署都必须带这一条。
gpu_ids写法:worker名:类型:序号,本机写作aistack:hggc:4,不是直觉里的1/4- 必须开启
enable_model_route=true,否则网关显示 0 模型 - embedding 部署类别要选
embedding,路由跟着类别走
6. 验收三连
网关 models / chat / embeddings 都通,且 docker ps 无重启循环,才算真的上线:
curl http://192.168.31.46:8080/v1/models \
-H "Authorization: Bearer $KEY"
curl http://192.168.31.46:8080/v1/chat/completions \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"model":"qwen3-32b","messages":[{"role":"user","content":"你好"}],"max_tokens":64}'
curl http://192.168.31.46:8080/v1/embeddings \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"model":"qwen3-embedding-8b","input":"测试文本"}'
一页速查(照此走)
| 环节 | 关键操作 | 验收标准 |
|---|---|---|
| 探测 | 检查 /dev/alixpu*、ppu-smi、系统盘余量和出口带宽 | 以实测结果确定路线 |
| Docker | data-root 先迁 NVMe,GPUStack 使用 8080 端口 | 系统盘不被镜像占满 |
| 镜像 | Mac 走 quay.io 并行分段下载,使用 --noproxy '*' | SHA-256 三次同值,load 后只留一个 quay.io tag |
| 权重 | ModelScope 手动下载 | 落到 /bmcp_lvm_fs/models/hf/models/ |
| 部署 | TP∈{1,2,4,8},锁同一 NUMA 组 | TP>1 带 NCCL_SOCKET_IFNAME=bond0,开启 enable_model_route=true |
| 验收 | 调用 models / chat / embeddings | 三连全通,docker ps 无重启循环 |
03 性能实测
压测走服务端本机直打网关(含 LB / 鉴权,等同客户端真实链路),SSE 流式取真实 TTFT,54 档全部 0 错误。先放一张 14B 与 32B 的峰值对照:
| 模型 | 卡数 | TP | 短文本峰值 |
|---|---|---|---|
| qwen3-32B | 4 | 4 | 1966 tok/s |
| qwen3-14B | 1 | 1 | 1223 tok/s |
下面是完整并发扫描的原始数据(54 档全部 0 错误)。先看短 / 中文本的吞吐与 TPOT 拐点——14B 与 32B 的拐点都在 c=64,再往上只是排队变长,不再多产 token。
表 1:qwen3-14b 并发扫描(单卡 · 短 / 中文本)
| 并发 | 短·吞吐 | 短·TPOT | 中·吞吐 | 中·TPOT |
|---|---|---|---|---|
| 1 | 37.5 | 26.5 | 36.8 | 27.1 |
| 8 | 251.6 | 31.1 | 248.5 | 32.0 |
| 16 | 485.0 | 31.7 | 475.6 | 33.3 |
| 32 | 884.1 | 34.6 | 670.7 | 36.1 |
| 64 | 1219.1 | 49.3 | 921.5 | 51.4 |
| 96 | 1223.3 | 49.3 | 923.6 | 51.3 |
| 128 | 1222.2 | 49.4 | 921.6 | 51.3 |
表 2:qwen3-32b 并发扫描(4 卡 TP=4 · 短 / 中文本)
| 并发 | 短·吞吐 | 短·TPOT | 中·吞吐 | 中·TPOT |
|---|---|---|---|---|
| 1 | 50.9 | 19.4 | 49.4 | 20.2 |
| 8 | 351.3 | 22.1 | 321.8 | 23.1 |
| 16 | 672.9 | 22.8 | 590.6 | 23.7 |
| 32 | 1196.3 | 25.2 | 892.6 | 25.9 |
| 64 | 1965.1 | 29.9 | 1350.3 | 31.4 |
| 96 | 1756.8 | 30.0 | 1342.4 | 31.4 |
| 128 | 1966.3 | 30.2 | 1353.2 | 31.4 |
表 3:长文本(入约 6000 / 出 1500)SLO 上限
SLO:TTFT p95 ≤1s 且 TPOT ≤60ms。
| 模型 | 并发 | 吞吐 | TTFT p95 | TPOT | SLO 达标 |
|---|---|---|---|---|---|
| 14B | 16 | 409.7 | 0.246s | 36.7 | 100% |
| 14B | 32 | 694.6 | 0.404s | 43.7 | 100% |
| 14B | 48 | 642.6 | 0.572s | 54.8 | 29.7% |
| 14B | 64 | 891.4 | 0.635s | 67.7 | 0% |
| 32B | 32 | 835.7 | 0.353s | 29.1 | 100% |
| 32B | 48 | 806.4 | 0.471s | 33.4 | 100% |
| 32B | 64 | 1089.0 | 1.22s | 36.7 | 17.2% |
| 32B | 64(复测) | 1103.5 | 0.627s | 36.8 | 100% |
表 4:qwen3-embedding-8b 向量并发(每请求 8 条、约 512 字)
| 并发 | req/s | 输入 tok/s | 均时延 | p95 时延 |
|---|---|---|---|---|
| 1 | 5.69 | 5,231 | 0.18s | 0.18s |
| 4 | 10.32 | 9,498 | 0.35s | 0.43s |
| 8 | 14.98 | 13,781 | 0.45s | 0.69s |
| 16 | 16.12 | 14,834 | 0.87s | 1.15s |
拐点小结:短 / 中文本 14B、32B 均在 c=64 触顶(再涨仅排队);长文本 14B 上限 32 路(48 路起吞吐反降,KV 抢占重算),32B 上限 48 路(64 路两次复测差异大,判为偶发抖动,对外按 48 路承诺)。SLO 判据:TTFT p95 ≤1s 且 TPOT ≤60ms。
选型结论:短对话场景 32B 不划算——4 倍显存换 1.6 倍吞吐,每卡效率只有 14B 的 40%。但长上下文场景 32B 优势明显放大——SLO 内并发 1.5 倍,且 14B 长文本会因 KV 不足触发抢占重算(吞吐反降 7.5%),32B 四卡 KV 空间大得多。高校 RAG、知识库、长文档优先 32B;短问答与高并发轻量场景用 14B。
对外可承诺运营参数
均按 SLO 内数据、留余量取,上限值已进抖动 / 违约区,不对外承诺:
| 模型 | 建议并发 | 可承诺吞吐 | TTFT | TPOT / 时延 |
|---|---|---|---|---|
| qwen3-14B | 短 / 中 32 路;长文 32 路 | 短 / 中约 880 tok/s;长文约 695 tok/s | ≤0.40s | TPOT ≤44ms |
| qwen3-32B | 短 / 中 32–48 路;长文 48 路 | 短 / 中约 1200–1600 tok/s;长文约 806 tok/s | ≤0.47s | TPOT ≤33ms |
| qwen3-embedding-8B | 向量并发 8 路 | 约 1.38 万输入 tok/s | — | 时延 ≤0.45s |
模型广场:qwen3-32b / qwen3-embedding-8b / qwen3-14b 三模型均显示绿色可用状态。
部署列表:三个模型副本数均为 1/1,运行状态正常。
04 踩坑分享
完整踩坑清单有十三条,这里挑四个最隐蔽、也最容易被反复踩的。
P7:NCCL 找不到网卡
32B 四卡 TP=4 起不来,报:
NCCL ERROR Bootstrap: no socket interface found
根因:GPUStack 推理容器走 host 网络,却默认注入 NCCL_SOCKET_IFNAME=eth0,而本机网卡是 bond0,根本没有 eth0。TP=1 不碰 NCCL,所以 14B 和 Embedding 一直活得好好的,把这个雷完美掩盖。
修复:模型 env 覆盖 NCCL_SOCKET_IFNAME=bond0(PUT /v2/models/{id} 带 env,模型级优先)。今后任何 TP>1 部署都必须带这一条。
P6:Embedding 永远 pending
类别误选 LLM 又开了跨卡分布式,调度要求 TP=7,但 32 个注意力头不能被 7 整除。TP 只能取 1/2/4/8;embedding 单卡足够,且部署类别要选对,路由跟着类别走。
P10:自定义 Runner Source 清空后端
该 source 是整表替换内置后端目录,只放几行 runner 会把后端定义清空,新实例报 Inference backend vllm not found。本地已有镜像时根本不需要 Runner Source,回滚 custom=null 再 reload 即可。
P5:gpu_ids 格式
API 建部署报 Invalid GPU ID,因为 gpu_ids 格式是 worker名:类型:序号(本机写作 aistack:hggc:4),不是直觉里的 1/4。
这条路,已经跑通
国产算力 + GPUStack 这条路,在这台 8 卡平头哥一体机上已经不是“能不能”的问题,而是“怎么稳”的问题。从驱动探测、16GB 镜像离线搬运,到十三个坑的逐一排除,再到 1966 tok/s 的压测背书,整套流程可复刻、可承诺。
也别神话它:思考模式、超 8K 长文、单副本无冗余、32B 长文 64 路偶发抖动,这几项在承诺对外能力前还要补测。先把可承诺的运营参数守住,再谈扩展。
国产 GPU 跑大模型,已经可以从口号变成交付。