首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
AI 工具与工作流
发新帖
0
0
[原创]lenovo thinkstation PGX修复与多卡连接
发表于: 2天前
399
[原创]lenovo thinkstation PGX修复与多卡连接
pureGavin
3
2天前
399
# 前言 最近我搞了几台PGX做研究,这是NVIDIA和联想合作的ARM架构个人GPU工作站,实际使用总是会出各种各样的问题,本文主要内容是修复最近拿到的一台操作系统无法启动的PGX,重装系统然后使用购买机器自带的QSFP接口进行多卡连接 # 正文 ## 重装系统 这部分在<a href="elink@f58K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6K6N6i4m8H3L8%4u0@1i4K6u0W2L8r3g2F1L8%4k6G2i4K6u0W2j5$3!0E0i4K6u0r3N6s2N6Q4x3V1k6*7K9q4)9J5c8Y4y4G2L8s2g2@1K9h3!0F1M7#2)9J5c8X3S2@1y4e0p5^5x3o6R3%4i4K6u0V1K9r3!0%4i4K6u0V1N6r3!0Q4x3X3c8A6L8Y4y4@1j5h3I4D9i4K6u0V1k6r3N6^5i4K6u0V1M7%4m8S2M7X3E0Q4x3X3c8G2M7#2)9J5k6s2m8Y4P5q4)9J5k6s2N6G2M7X3E0K6N6r3q4@1K9h3!0F1">联想官方文档</a>中有提到,但是最重要的是没给出重装用的ISO镜像,许多人可能会直接从NVIDIA官网下载镜像,这里我看了一下,NVIDIA提供的GB10机器镜像和Lenovo定制的镜像并不一样,这里我提供一个PGX镜像的<a href="elink@e67K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1L8%4N6F1L8r3!0S2k6q4)9J5k6h3I4W2L8X3!0$3L8#2)9J5k6h3y4G2L8g2)9J5c8X3E0E0i4K6u0r3L8h3g2V1K9h3q4Q4x3V1k6S2N6s2c8S2j5$3S2E0k6h3&6@1i4K6u0r3c8p5N6j5e0#2y4Q4y4h3j5%4i4K6g2X3y4q4)9#2k6U0m8Q4y4h3k6s2b7e0u0Q4y4h3j5H3i4K6g2X3f1p5N6j5i4K6u0W2K9i4y4G2">下载链接</a>,但是如果你的本地网络有问题导致无法访问上面的下载链接,也可以尝试使用我上传到自己服务器的[磁力链接](magnet:?xt=urn:btih:3eec242622ead59b31977c59824c83fc73bd9a12&dn=DGXOS_7_4_0_GA2_0_PGX.iso&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce)进行下载(镜像文件的安全问题请自行斟酌,本人无法做出任何保证,后续任何安全问题与本人无关) 重装系统本身不难,上面给出的Lenovo官方链接也很详细,这里为了防止有些人的网络没法访问到,就简单说一下;假设你已经下载好了PGX的ISO镜像文件,然后再从rufus的<a href="elink@b92K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6H3j5X3q4@1j5i4u0V1i4K6u0r3M7Y4g2X3N6i4x3`.">GitHub官方仓库</a>下载rufus工具,然后插入U盘(容量至少要16G),打开rufus工具,选择U盘,然后再选择下载好的ISO镜像,其他的都不用改,默认就好,然后直接点击开始  一段时间后,U盘就变成启动盘了,然后直接点击关闭就好 PGX的外置接口无论是Type-C还是自带的USB转接口都不能用带2.4G的无线键盘,得用古老的USB连接的键盘才行,开机之后不停地按Delete键进入启动页面,键盘按tab键,选择bss选项,进入硬件启动顺序,选择U盘启动  进入GRUB菜单后选择DGX安装选项  然后点击安装DGX系统  然后操作系统就会自动安装,你只需要等待就行了,安装完成后就会进入熟悉的初始化界面,配置语言、时区和账号密码之类的 ## 多机器连接 虽然起名多机器连接,但事实上我这里是只有两台机器,因为GB10系列机器后面的接口用的是QSFP(200G)接口,这种接口的交换机对于个人来说都不好卖到或者价格很贵,所以我这里只演示两台机器,不过从配置原理来说,应该是支持两台以上连接 整个教程在<a href="elink@efeK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1k6i4k6W2L8r3!0H3k6i4u0Q4x3X3g2F1N6X3W2V1K9h3q4Q4x3X3g2U0L8W2)9J5c8X3u0#2K9h3I4V1i4K6u0V1M7%4m8S2M7X3E0Q4x3V1k6U0L8$3&6F1k6h3y4@1i4K6u0V1N6s2N6G2i4K6u0V1M7%4m8S2M7X3E0K6i4K6t1K6K9e0N6F1K9Y4k6S2K9b7`.`.">NVIDIA官网</a>上也能找到,但是和上面一样,为了部分人可能存在的网络问题,所以我这里也复述一下 第一步就是确认两台机器的用户名是否相同,使用 `whoami` 命令就行,如果两台机器的用户名不同,那么则需要在系统中创建两个相同用户名的用户,Linux基础命令这里就不展示了,大家可以自行搜索 第二步也是很简单,使用QSFP接口连接连接两台机器,注意连接的时候不要使用暴力,接口是防呆的,反着插是插不进去的;连接完成后运行 `ibdev2netdev` 命令,会看到如下结果 ```sh lenovo@thinkstationpgx-3262:~/PGX$ ibdev2netdev rocep1s0f0 port 1 ==> enp1s0f0np0 (Up) rocep1s0f1 port 1 ==> enp1s0f1np1 (Down) roceP2p1s0f0 port 1 ==> enP2p1s0f0np0 (Up) roceP2p1s0f1 port 1 ==> enP2p1s0f1np1 (Down) ``` 第三步则是配置机器的 `netplan` ,官方文档提供了三种配置方式(一种自动配置和两种手动配置),这里我用的是自动配置 首先在 `netplan` 目录下写入yaml配置文件 ```sh sudo tee /etc/netplan/40-cx7.yaml > /dev/null <<EOF network: version: 2 ethernets: enp1s0f0np0: link-local: [ ipv4 ] enp1s0f1np1: link-local: [ ipv4 ] EOF ``` 然后配置相应的权限并部署 ```sh # 配置权限 sudo chmod 600 /etc/netplan/40-cx7.yaml # 应用配置 sudo netplan apply ``` 这一步完成后,使用 `ifconfig` 命令或者 `ip addr` 命令就会看到第二步中对应的网卡已经自动分配了IP地址;注意这一步是两台机器都需要做的 第四步则是配置免密SSH证书,这里NVIDIA官方提供了一个<a href="elink@fe3K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6z5g2V1W2p5d9f1q4Q4x3V1k6V1k6%4S2Q4x3X3c8K6M7r3q4J5K9#2)9J5k6s2m8D9j5i4W2T1L8$3!0C8M7#2)9J5c8X3u0D9L8$3u0Q4x3V1k6E0j5h3W2F1i4K6u0r3L8Y4k6A6k6r3W2S2i4K6u0r3j5$3!0F1L8X3g2U0N6q4)9J5k6s2c8%4L8#2)9J5k6s2y4H3j5i4u0C8M7#2)9J5c8X3q4K6M7$3g2@1M7#2)9J5c8X3c8A6M7$3y4G2N6X3g2J5i4K6u0V1M7%4m8S2M7X3E0K6">脚本</a>,内容如下: ```sh # # SPDX-FileCopyrightText: Copyright (c) 1993-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # #!/bin/env bash # discover-sparks # Discover available systems using avahi-browse and generate MPI hosts file # Searches all active interfaces automatically # # Usage: bash ./discover-sparks set -euo pipefail # Check if running as root or with sudo if [[ $EUID -eq 0 ]] || [[ -n "${SUDO_USER:-}" ]]; then echo "Error: This script should not be run as root or with sudo" echo "Please run as a regular user" exit 1 fi # Dynamically get interface names from ibdev2netdev output # Use ibdev2netdev to list Infiniband devices and their network interfaces. # The awk command searches for lines containing 'Up)' (i.e., interfaces that are up) # and prints the 5th field, which is the interface name (e.g., enp1s0f0np0). # The tr command removes any parentheses from the output. INTERFACES=($(ibdev2netdev | awk '/Up\)/ {print $5}' | tr -d '()')) if [ ${#INTERFACES[@]} -eq 0 ]; then echo "ERROR: No active interfaces found via ibdev2netdev." exit 1 fi # Create temporary file for processing TEMP_FILE=$(mktemp) trap 'rm -f "$TEMP_FILE"' EXIT # Check if avahi-browse is available if ! command -v avahi-browse &> /dev/null; then echo "Error: avahi-browse not found. Please install avahi-utils package." exit 1 fi # Run avahi-browse and filter for SSH services on specified interfaces # -p: parseable output # -r: resolve host names and addresses # -f: terminate after dumping all entries available at startup avahi_output=$(avahi-browse -p -r -f -t _ssh._tcp 2>/dev/null) # Filter for both interfaces found_services=false for interface in "${INTERFACES[@]}"; do if echo "$avahi_output" | grep "$interface" >> "$TEMP_FILE"; then found_services=true fi done if [ "$found_services" = false ]; then echo "Warning: No services found on any specified interface" exit 0 fi # Extract IPv4 addresses from the avahi-browse output # Format: =;interface;IPv4;hostname\032service;description;local;fqdn;ip_address;port; grep "^=" "$TEMP_FILE" | grep "IPv4" | while IFS=';' read -r prefix interface protocol hostname_service description local fqdn ip_address port rest; do # Clean up any trailing data clean_ip=$(echo "$ip_address" | sed 's/;.*$//') # Validate IP address format if [[ $clean_ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then echo "$clean_ip" >> "$TEMP_FILE.sorted" echo "Found: $clean_ip ($fqdn)" else echo "Warning: Invalid IP format: $clean_ip" fi done # Sort and remove duplicates if [[ -s "$TEMP_FILE.sorted" ]]; then sort -u "$TEMP_FILE.sorted" -o "$TEMP_FILE.sorted" else echo "No IPv4 addresses found." exit 1 fi # Generate a shared SSH key if it doesn't exist SHARED_KEY="$HOME/.ssh/id_ed25519_shared" if [[ ! -f "$SHARED_KEY" ]]; then echo "Generating shared SSH key for all nodes..." ssh-keygen -t ed25519 -N "" -f "$SHARED_KEY" -q -C "shared-cluster-key" fi echo "" echo "Setting up shared SSH access across all nodes..." echo "You may be prompted for your password on each node." # Ensure local .ssh directory exists with correct permissions mkdir -p "$HOME/.ssh" chmod 700 "$HOME/.ssh" # Add shared public key to local authorized_keys if ! grep -qF "$(cat "$SHARED_KEY.pub")" "$HOME/.ssh/authorized_keys" 2>/dev/null; then cat "$SHARED_KEY.pub" >> "$HOME/.ssh/authorized_keys" chmod 600 "$HOME/.ssh/authorized_keys" echo " ✓ Added shared public key to local authorized_keys" fi # Distribute shared key to all remote nodes while read -r node_ip; do if [[ -n "$node_ip" ]]; then echo "Configuring $node_ip..." # Copy shared key to remote node and set up authorized_keys if scp -o StrictHostKeyChecking=accept-new "$SHARED_KEY" "$SHARED_KEY.pub" "$USER@$node_ip:~/.ssh/" &>/dev/null; then ssh -n -o StrictHostKeyChecking=accept-new "$USER@$node_ip" " chmod 700 ~/.ssh chmod 600 ~/.ssh/id_ed25519_shared chmod 644 ~/.ssh/id_ed25519_shared.pub # Add shared public key to authorized_keys if not present if ! grep -qF \"\$(cat ~/.ssh/id_ed25519_shared.pub)\" ~/.ssh/authorized_keys 2>/dev/null; then cat ~/.ssh/id_ed25519_shared.pub >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys fi # Create/update SSH config to use shared key by default if ! grep -q 'IdentityFile.*id_ed25519_shared' ~/.ssh/config 2>/dev/null; then echo 'Host *' >> ~/.ssh/config echo ' IdentityFile ~/.ssh/id_ed25519_shared' >> ~/.ssh/config chmod 600 ~/.ssh/config fi " &>/dev/null echo " ✓ Successfully configured $node_ip with shared key" else echo " ✗ Failed to configure $node_ip" fi fi done < "$TEMP_FILE.sorted" # Update local SSH config to use shared key if ! grep -q 'IdentityFile.*id_ed25519_shared' "$HOME/.ssh/config" 2>/dev/null; then touch "$HOME/.ssh/config" echo 'Host *' >> "$HOME/.ssh/config" echo ' IdentityFile ~/.ssh/id_ed25519_shared' >> "$HOME/.ssh/config" chmod 600 "$HOME/.ssh/config" echo " ✓ Updated local SSH config to use shared key" fi echo "" echo "Shared SSH setup complete!" echo "All nodes can now SSH to each other using the shared key (id_ed25519_shared)." ``` 这里如果你遇到了这个问题 ```sh Error: This script should not be run as root or with sudo Please run as a regular user ``` 你需要重启一个终端,然后不要做其他事,直接运行这个脚本 这个脚本执行成功后,你可以使用如下命令测试结果,IP地址就是第三步的输出结果 ```sh # 返回结果是对应IP地址的机器的hostname ssh <ip address> hostname ``` 至此,使用QSFP连接两台PGX的步骤就已经完成了,接下来就是软件层面的事情了 ## 搭建多卡集群 这里涉及到了一些专有名词(Ray集群、NCCL通信),不过本文的目的不是名词解释,而是如何直接使用这些东西(即便如此,还是需要做一些简单的原理介绍);本文的大模型环境是docker+vllm 这里使用的是最新的 `Qwen/Qwen3.8-Flash-Next-FP8` ,主要是因为BF16 在这两台机器上**不可行**——不是普通 OOM,而是把本机拖死重启;最终上线的是 FP8 ,拓扑是跨两台单卡节点的 TP=2,API 在本机 `http://localhost:20001`,对外模型名 `qwen3.8-flash-next` ### 为什么必须双机 GB10 是统一内存架构。`nvidia-smi` 报出来的「显存」和 `/proc/meminfo` 里的物理内存是同一份,每台物理只有 **119 GiB**。所谓「向 GPU 申请显存」,内核路径走的是 `nv_alloc_system_pages`,也就是向系统要页。 Qwen3.8-Flash-Next 的两份检查点: | 检查点 | 权重体积(加载器读到的) | 磁盘占用 | TP=2 每节点权重 | | --- | --- | --- | --- | | `Qwen/Qwen3.8-Flash-Next`(BF16) | 335.28 GiB | 336G | 167.6 GiB | | `Qwen/Qwen3.8-Flash-Next-FP8` | 172.78 GiB | 173G | 86.4 GiB | BF16 每节点 167.6 GiB,比 119 GiB 还多 **48.6 GiB**。缺口不是靠换页能补上的,驱动会在内核里持锁反复回收重试,整机跟着死。这条路已经实测走过(后面再提)。 FP8 每节点权重实测占 **86.33 GiB**。还要加上 KV 和 GDN 状态。`config.json` 里 `num_hidden_layers=48`、`full_attention_interval=4`,也就是 12 个 QSA 全注意力层、36 个 Gated DeltaNet 层;`num_key_value_heads=2`、`head_dim=256`。KV 按 BF16 计: ``` 12 层 × 2 KV 头 × 256 dim × 2(K/V)× 2 字节 = 24 KiB / token TP=2 之后每节点 12 KiB / token 262144 上下文、单条满长序列 ≈ 3 GiB / 节点 ``` GDN 循环状态:36 层 × 48 个 V 头 × 128 × 128 × 2 字节 = 54 MiB / 序列,TP=2 后 27 MiB;`--max-num-seqs 8` 一共 216 MiB,可以忽略。权重 86.4 GiB 加上 KV 和一点宿主开销,大约 95 GiB,119 GiB 里留得下,所以 `--max-model-len 262144` 可以保留。 最终 CUDA graph 那一轮,本机 KV cache **7.62 GiB / 541,401 tokens**,262144 上下文的最大并发 **2.07x**;对端 KV **6.78 GiB**。两边略有偏差,是因为两台当时空闲内存不完全一样,vLLM 按 `--gpu-memory-utilization 0.80`(预算 95.7 GiB)各自切了一刀 ### 拓补只有一种选择 这个架构带 N-gram embedding(`ngram_vocab_size_base=20000000`,挂在第 2 层)。vLLM 对它**不支持流水线并行**。PLE 把 n-gram 表卸到 CPU 在单机也许能试,**分布式模式不支持**。两台都是单卡,所以只剩一种拓扑:张量并行切成两份,每台一张卡。 ```mermaid flowchart LR Client["客户端 :20001"] --> Rank0 subgraph rank0node ["thinkstationpgx-30 / rank 0"] Rank0["APIServer + EngineCore / TP rank 0"] end subgraph rank1node ["thinkstationpgx-32 / rank 1"] Rank1["headless Worker / TP rank 1"] end Rank0 -->|"RoCE NCCL + Gloo"| Rank1 ``` 本机是 master:`169.254.94.252`,`NODE_RANK=0`,起完整的 APIServer 和 EngineCore,HTTP 绑在 `0.0.0.0:20001`。对端是 follower:`169.254.159.206`,`NODE_RANK=1`,只跑 `--headless` 的 Worker,不对外提供 HTTP。NCCL 走 QSFP 上的两条 RoCE 逻辑网卡,Gloo / TCP 控制面走 `enp1s0f0np0`。默认路由仍在 `enP7s7`(`192.168.40.x` 那一段局域网),**不要把默认网关改到 QSFP**。 两台用同一份 compose,靠各自 `.env` 里的 `NODE_RANK` 和 `VLLM_HOST_IP` 区分身份 ### 开工前的三项检查 **1\. 驱动内核态和用户态版本必须一致。** 对端曾经是内核 `580.159.03`、用户态 `580.173.02`,机器已经连续运行五十多天,Docker 里 GPU 初始化失败。重启对端之后才恢复。本机、对端都要能在容器里看到 GPU,不要只在宿主机 `nvidia-smi` 看一眼。 **2\. 防火墙放行 QSFP 网段和 API 端口。** 本机 UFW 默认 DROP,会把 torch 分布式用的 `29501/tcp` 挡掉。需要: ``` sudo ufw allow from 169.254.0.0/16 sudo ufw allow from 192.168.101.0/24 sudo ufw allow 20001/tcp ``` 对端当时 UFW 未启用,不用改。规则要能活过重启。 **3\. 两条 RoCE 逻辑网卡都要通。** 物理口是 QSFP port 0,拆成两条逻辑网卡: | 以太网名 | RoCE 名 | 本机 | 对端 | | --- | --- | --- | --- | | `enp1s0f0np0` | `rocep1s0f0` | `169.254.94.252/16` | `169.254.159.206/16` | | `enP2p1s0f0np0` | `roceP2p1s0f0` | `192.168.101.10/24` | `192.168.101.11/24` | GID index **3** 是 RoCEv2 + IPv4。`ib_write_bw` 两条各跑到 **109 Gb/s**。NCCL 用 `NCCL_IB_HCA==rocep1s0f0,roceP2p1s0f0`(等号是 NCCL 的精确匹配语法)再加 `NCCL_IB_MERGE_NICS=1`,把两条合成一条 200 GbE 量级的传输 ### 镜像与权重 Qwen3.8-Flash-Next 必须用专用镜像 `vllm/vllm-openai:qwen38-flash-next`(arm64)。通用 `v0.28.0` **不认识** 这个架构,拉错镜像会在模型初始化阶段直接退出。这一轮实际跑起来的版本字符串是 `v0.1.dev20073+g8e685d198`。 我这里其中有一台GB10网速很慢,所以镜像和权重都在本机准备,再经 QSFP 推过去。工作目录两边都是 `/home/lenovo/vllm`,Hugging Face 缓存挂在 `/home/lenovo/vllm/huggingface` 下载可以用本机已有的通用镜像跑 `hf download`,缓存目录 bind 到上面那个路径即可,不必在专用镜像里下。HF 缓存属主是 root,`trees/*.json` 经常是 mode `600`,本机用户读不了,rsync 会在这些小文件上失败。传之前先: ``` sudo chmod -R a+rX /home/lenovo/vllm/huggingface/hub/models--Qwen--Qwen3.8-Flash-Next-FP8 ``` 三条注意事项: * 禁止走主机名或 `192.168.40.x`,流量必须走 `169.254.159.206`。 * 禁止 `rsync -L`。HF 的 `snapshots/` 是指向 `blobs/` 的符号链接,解引用会把体积翻倍。 * 禁止给 rsync / SSH 开压缩。200 GbE 上压缩只会把 CPU 打满,带宽用不满。 `docker load` 之后对端的镜像 ID 可能和本机不同,这是 load 重写了本地镜像元数据,RootFS layer 一致就可以用 ### compose的关键设计 完整文件在附录。这里只讲「为什么必须这样写」,每一条都对应过一次故障。 **Host 网络、InfiniBand 设备和锁页权限。** `network_mode: host` 是因为 NCCL 要直接拿到宿主机网卡;容器里映射 `8000:20001` 那种写法会让 NCCL 握手走错命名空间。vLLM 自己 `--host 0.0.0.0 --port 20001`,和本机其它 compose(例如 `qwen3.6-35b-compose.yml`)对外端口一致。`/dev/infiniband`、`IPC_LOCK`、`memlock: -1` 缺了,RoCE 就退回 socket,带宽掉到不可用。 **Gloo 强制 IPv4,并关掉 libuv。** 第一次分布式握手卡死在 c10d 的 IPv6 套接字超时上。环境变量是: ``` NCCL_SOCKET_FAMILY: AF_INET GLOO_SOCKET_FAMILY: AF_INET GLOO_SOCKET_IFNAME: enp1s0f0np0 GLOO_USE_LIBUV: "0" TORCH_GLOO_USE_LIBUV: "0" ``` 控制面走 `enp1s0f0np0`,数据面走两条 RoCE。`NCCL_IB_GID_INDEX=3` 对应 RoCEv2+IPv4;`NCCL_CUMEM_ENABLE=0` 是这个平台上的稳妥选择。 **容器内存硬上限 105G,显式** `restart: "no"`**。** 两台都是 cgroup v2 + systemd driver,驱动分配会计入容器 memcg。超限时先回收页缓存,再 OOM-kill **容器**,而不是拖死整机。`restart: "no"` 避免崩溃后自动拉起、反复冲击内存。`--gpu-memory-utilization 0.80` 给出约 95.7 GiB 的 GPU 预算,和 105G 上限之间留大约 9 GiB 给 Python / torch 宿主。`--distributed-timeout-seconds 3600` 是因为两节点加载 86 GiB 有时间偏斜(对端约 300 秒、本机约 850 秒),默认 600 秒的 NCCL 超时偏紧。 **用 bash 包一层 entrypoint,rank 1 必须加** `--headless`**。** 镜像默认 `ENTRYPOINT` 是 `["vllm","serve"]`。compose 要按各机 `.env` 插值 `NODE_RANK` / `MODEL_ID`,所以改成 `/bin/bash -lc`。rank 1 如果跑完整 EngineCore,会在 `_initialize_kv_caches` 里炸: ``` AssertionError: collective_rpc should not be called on follower node ``` follower 只当 Worker。HTTP 只在 rank 0。 **编译开关。** 先用 `--enforce-eager` 跑通(NVIDIA 论坛双 Spark 实测配置),稳定后再换成 `-cc.mode=0 -cc.cudagraph_mode=FULL_DECODE_ONLY`:跳过会在 GB10 上挂死的 inductor 编译,只保留 decode CUDA graph。当前线上已经是后一种。注意字段名必须是**下划线** `cudagraph_mode`,写成连字符会被 pydantic 拒绝 ### 启动与看门狗 启动前两台都清掉页缓存。之前 BF16 失败时 `buff/cache` 已经到 110 GiB,safetensors 读出来的页缓存和驱动在抢同一份统一内存: ``` sync && echo 3 | sudo tee /proc/sys/vm/drop_caches ``` 先起 rank 0,再起 rank 1: ``` # 本机 cd /home/lenovo/vllm sudo docker compose -f qwen3.8-flash-next-compose.yml --env-file .env up -d --force-recreate --no-deps # 对端,经 QSFP ssh -T -o Compression=no -o BatchMode=yes -c aes128-gcm@openssh.com \ lenovo@169.254.159.206 \ 'cd /home/lenovo/vllm && sudo docker compose -f qwen3.8-flash-next-compose.yml --env-file .env up -d --force-recreate --no-deps' ``` **不要用** `nvidia-smi` **探活。** 驱动一旦卡在 `nv_alloc_system_pages` 并握着 RM 全局写锁,`nvidia-smi` 自己也会被同一把锁堵住——BF16 那次它 blocked for more than 614 seconds。正确的探活是轮询 `/proc/meminfo` 的 `MemAvailable`:低于 **5 GiB** 立刻 `docker stop`,把处置权抢在内核前面。看门狗脚本放在附录。 预期日志顺序: 1. rank 1:`Launching vLLM ... headless multiproc executor` 2. 两边:`rank N in world size 2 is assigned as ... TP rank N` 3. `Loading model from scratch...` 4. `Model loading took 86.33 GiB memory ...` 5. `GPU KV cache size: ... tokens` 6. rank 0:`Graph capturing finished in 5 secs, took 0.36 GiB`(eager 模式没有这一行) 7. rank 0:`Application startup complete.` CUDA graph 这一轮的时间线: | 步骤 | 对端 | 本机 | | --- | --- | --- | | 加载权重 | 300.79 s(模型就绪 307.62 s) | 850.66 s(模型就绪 857.87 s) | | init engine(profile / KV / warmup) | 与本机对齐,卡在集合通信上 | 73.57 s | | CUDA graph 捕获 | 计入上一行 | 5 s / 0.36 GiB | | 启动完成后 MemAvailable | 约 15 GiB | 约 12 GiB | 加载期间 MemAvailable 从 110+ GiB 掉到 17–24 GiB 是正常的,权重进统一内存了。看门狗阈值 5 GiB 仍然留有余量。本机加载明显慢于对端,根因是本机根分区当时更满、顺序读 173G safetensors 更吃页缓存;`--distributed-timeout-seconds 3600` 就是为这个偏斜准备的 ### 验证 只打 rank 0。先看模型是否注册: ``` curl -sS http://localhost:20001/v1/models ``` 返回 `id` 为 `qwen3.8-flash-next`,`root` 为 `Qwen/Qwen3.8-Flash-Next-FP8`,`max_model_len` 为 262144。 出字和 thinking。Qwen3.8 默认 `enable_thinking=True`,vLLM 开了 `--reasoning-parser qwen3`。实测「3 的阶乘」:`completion_tokens=207`,其中 `reasoning_tokens=189`,正文给出「3×2×1=6」。这一版 API 把思考过程放在 `message.reasoning` / `usage.completion_tokens_details.reasoning_tokens`,不一定还有旧字段 `reasoning_content`。 工具调用。`--enable-auto-tool-choice --tool-call-parser qwen3_xml`,给一个 `get_weather` 的 function schema,问「现在天气怎么样?请调用工具查询」: * `finish_reason`:`tool_calls` * `tool_calls[0].function.name`:`get_weather` * `arguments`:`{"city": "北京"}` 这三件事都过,才能认为服务可用,而不是「进程还在、端口能连」 ### 踩坑复盘 这部分是整篇文章真正要留下的部分,三个坑都在生产路径上炸过 #### BF16:不是OOM,是整机死锁 按「先试 BF16,显存不够再 FP8」的顺序,BF16 加载到 `Loading model from scratch...` 之后,计算内核一个都没启动过,GPU 占用 0%,CPU 却被内核态打满。内核日志(boot -1)的调用栈: ``` VLLM::Worker_TP:507121 <writer> 持有 RM 全局 rw-semaphore nvidia_unlocked_ioctl -> RmIoctl -> Nv04AllocWithAccessSecInfo -> rmapiAllocWithSecInfo -> memdescAlloc -> osAllocPagesInternal -> nv_alloc_pages -> nv_alloc_system_pages <-- 卡死在这里 INFO: task nvidia-smi:508972 blocked for more than 614 seconds ``` 统一内存下,167.6 GiB 的权重分配就是向内核要 167.6 GiB 系统页,物理只有 119 GiB。驱动申请不到页,就在持有 RM 写锁的情况下反复回收重试。OOM killer 全程没能介入——它杀的是用户态进程,杀不掉卡在驱动分配循环里的内核路径。当时 `buff/cache` 已经 110 GiB,读权重撑起来的页缓存和驱动抢同一份内存,加剧了死锁。最后本机只能重启。 ```mermaid flowchart TD Load["vLLM 加载权重"] --> Need{"每节点需求 vs 119 GiB"} Need -->|"BF16 167.6 GiB 超出"| Spin["nv_alloc_system_pages 持锁自旋"] Spin --> Dead["CPU 内核态打满 / GPU 0% / 整机重启"] Need -->|"FP8 86.4 GiB 装得下"| Ok["分配成功 -> KV cache -> 服务就绪"] Spin -.->|"新增 memory: 105G"| Killed["cgroup 回收页缓存, 超限则只杀容器"] ``` 护栏的意义在这里:105G 的 memcg 上限让超限变成「只杀容器」。它救不了 BF16——167 GiB 怎么都塞不进 119 GiB——但它能阻止下一次配置失误把整机拖死。`restart: "no"` 保证被杀之后不会自动再冲一次 #### rank 1 漏了 `--headless` 第一轮 FP8 两边都跑完整 EngineCore。权重其实已经装进去了:对端 340.86 s,本机 831.18 s,MemAvailable 掉到 17–23 GiB,看起来很健康。然后对端在 `_initialize_kv_caches` 里: ``` AssertionError: collective_rpc should not be called on follower node ``` 对端一死,本机 NCCL 跟着报 `IBV_WC_RETRY_EXC_ERR`。镜像源码里把 `--headless` 注释成 “headless workers (for multi-node PP/TP)”。compose 用 bash 按 `NODE_RANK` 给 rank 1 补上这个参数之后,KV 初始化一次通过 #### CUDA graph 字段名是下划线 计划里写的是 `--cudagraph-mode FULL_DECODE_ONLY`。这个版本的 CLI 没有这条顶层参数,嵌套写法 `-cc.cudagraph-mode=FULL_DECODE_ONLY` 会被 FlexibleArgumentParser 原样交给 pydantic,报: ``` argument --compilation-config/-cc: 1 validation error for CompilationConfig cudagraph-mode Unexpected keyword argument ``` 容器立刻 exit 2。正确写法: ``` -cc.mode=0 -cc.cudagraph_mode=FULL_DECODE_ONLY ``` 等价的 JSON 也可以:`-cc '{"mode":0,"cudagraph_mode":"FULL_DECODE_ONLY"}'`。日志里确认生效的是 `cudagraph_mode: <CUDAGraphMode.FULL_DECODE_ONLY: (2, 0)>`,捕获尺寸 `[1, 2, 4, 8, 16]`(受 `--max-num-seqs 8` 约束) ### 性能与取舍 同一条「从 1 数到 80」的请求,`temperature=0`、`enable_thinking=False`、`completion_tokens=231`: | 配置 | 耗时 | tok/s | CUDA graph 占用 | | --- | --- | --- | --- | | `--enforce-eager` | 10.211 s | 22.62 | 0 | | `-cc.mode=0` + `FULL_DECODE_ONLY` | 9.905 s | 23.32 | 本机 0.36 GiB | 社区在该架构上报告过数量级的解码提速,我们没看到。原因很具体:decode 已经很快,跨机 TP 的 RoCE allreduce 才是主路径;CUDA graph 只能吃掉本机 kernel launch 的开销,吃不掉网上那一趟。图只多占 0.36 GiB,KV 从 eager 的 503,285 tokens / 1.92x 变成 541,401 tokens / 2.07x,没有挤爆预算,所以线上留着它。 不要在没站稳之前就开图。eager 是验证正确性的底线;图是正确性之后的加分项 # 结尾 本文跨度很长,从维修PGX到搭建双卡大模型中间差不多过去了两个月,但是好在最后把所有坑都踩完了,工具也上线了(中间整台机器卡死一度让我以为又要重复“重装系统”那个步骤了);配置本身并不难,最重要的是排查问题的过程,前面提到的IP地址等需要改成你自己的机器的对应内容;文中内容不少,难免有疏漏之处,请各位读者斧正 # 附录A 两个 `.env` 本机 `/home/lenovo/vllm/.env`(rank 0): ``` MODEL_ID=Qwen/Qwen3.8-Flash-Next-FP8 NODE_RANK=0 VLLM_HOST_IP=169.254.94.252 ``` 对端同名文件: ``` MODEL_ID=Qwen/Qwen3.8-Flash-Next-FP8 NODE_RANK=1 VLLM_HOST_IP=169.254.159.206 ``` `MASTER_ADDR` 写在 compose 里,固定为本机 `169.254.94.252`,不要跟着 `VLLM_HOST_IP` 变 # 附录B 完整compose 文件路径:`/home/lenovo/vllm/qwen3.8-flash-next-compose.yml`。两台内容必须一致,改完用 scp 经 QSFP 推到对端。 ``` # Dual-node TP=2 for Qwen3.8-Flash-Next across two GB10 workstations. # Same file on both machines; NODE_RANK / VLLM_HOST_IP / MODEL_ID come from .env. # host network: vLLM binds :20001 directly (same external port as qwen3.6-35b-compose.yml). services: vllm: image: vllm/vllm-openai:qwen38-flash-next container_name: vllm-qwen38 network_mode: host ipc: host restart: "no" stdin_open: true tty: true volumes: - /home/lenovo/vllm/huggingface:/root/.cache/huggingface - /etc/localtime:/etc/localtime:ro devices: - /dev/infiniband:/dev/infiniband cap_add: - IPC_LOCK - SYS_PTRACE security_opt: - seccomp:unconfined ulimits: memlock: -1 stack: 67108864 deploy: resources: limits: memory: 105G reservations: devices: - driver: nvidia count: all capabilities: [gpu] environment: PYTHONUNBUFFERED: "1" MODEL_ID: ${MODEL_ID} NODE_RANK: ${NODE_RANK} VLLM_HOST_IP: ${VLLM_HOST_IP} MASTER_ADDR: "169.254.94.252" NCCL_IB_HCA: "=rocep1s0f0,roceP2p1s0f0" NCCL_IB_DISABLE: "0" NCCL_IB_GID_INDEX: "3" NCCL_IB_MERGE_NICS: "1" NCCL_SOCKET_IFNAME: enp1s0f0np0 NCCL_SOCKET_FAMILY: AF_INET GLOO_SOCKET_IFNAME: enp1s0f0np0 GLOO_SOCKET_FAMILY: AF_INET GLOO_USE_LIBUV: "0" TORCH_GLOO_USE_LIBUV: "0" TP_SOCKET_IFNAME: enp1s0f0np0 NCCL_NET_PLUGIN: none NCCL_IB_ROCE_VERSION_NUM: "2" NCCL_CUMEM_ENABLE: "0" NCCL_DEBUG: WARN # Rank 1 must be --headless (worker only). Running a full EngineCore on the # follower hits: AssertionError: collective_rpc should not be called on follower node entrypoint: ["/bin/bash", "-lc"] command: - | set -euo pipefail if [ "${NODE_RANK}" = "1" ]; then exec vllm serve "${MODEL_ID}" \ --served-model-name qwen3.8-flash-next \ --tensor-parallel-size 2 \ --nnodes 2 \ --node-rank "${NODE_RANK}" \ --master-addr 169.254.94.252 \ --master-port 29501 \ --distributed-executor-backend mp \ -cc.mode=0 \ -cc.cudagraph_mode=FULL_DECODE_ONLY \ --gpu-memory-utilization 0.80 \ --distributed-timeout-seconds 3600 \ --max-model-len 262144 \ --max-num-seqs 8 \ --enable-prefix-caching \ --no-enable-flashinfer-autotune \ --reasoning-parser qwen3 \ --enable-auto-tool-choice \ --tool-call-parser qwen3_xml \ --host 0.0.0.0 \ --port 20001 \ --headless fi exec vllm serve "${MODEL_ID}" \ --served-model-name qwen3.8-flash-next \ --tensor-parallel-size 2 \ --nnodes 2 \ --node-rank "${NODE_RANK}" \ --master-addr 169.254.94.252 \ --master-port 29501 \ --distributed-executor-backend mp \ -cc.mode=0 \ -cc.cudagraph_mode=FULL_DECODE_ONLY \ --gpu-memory-utilization 0.80 \ --distributed-timeout-seconds 3600 \ --max-model-len 262144 \ --max-num-seqs 8 \ --enable-prefix-caching \ --no-enable-flashinfer-autotune \ --reasoning-parser qwen3 \ --enable-auto-tool-choice \ --tool-call-parser qwen3_xml \ --host 0.0.0.0 \ --port 20001 ``` # 附录C 看门狗脚本 加载期间在本机跑。不要并行开 `nvidia-smi`。 ``` #!/usr/bin/env bash set -euo pipefail THRESHOLD_KB=$((5 * 1024 * 1024)) SSH=(ssh -T -o Compression=no -o BatchMode=yes -c aes128-gcm@openssh.com lenovo@169.254.159.206) emergency_stop() { echo "WATCHDOG STOP: $1" sudo docker stop -t 10 vllm-qwen38 2>/dev/null || true "${SSH[@]}" 'sudo docker stop -t 10 vllm-qwen38 2>/dev/null || true' } for i in $(seq 1 210); do local_avail=$(awk '/MemAvailable/{print $2}' /proc/meminfo) peer_avail=$("${SSH[@]}" "awk '/MemAvailable/{print \$2}' /proc/meminfo") local_status=$(sudo docker inspect vllm-qwen38 --format '{{.State.Status}} {{.State.OOMKilled}} {{.State.ExitCode}}' 2>/dev/null || echo missing) peer_status=$("${SSH[@]}" "sudo docker inspect vllm-qwen38 --format '{{.State.Status}} {{.State.OOMKilled}} {{.State.ExitCode}}' 2>/dev/null || echo missing") echo "[$i] local $((local_avail/1024/1024))GiB $local_status | peer $((peer_avail/1024/1024))GiB $peer_status" if [[ "$local_avail" -lt "$THRESHOLD_KB" || "$peer_avail" -lt "$THRESHOLD_KB" ]]; then emergency_stop "MemAvailable below 5 GiB" exit 2 fi if [[ "$local_status" != running* ]]; then echo "LOCAL CONTAINER NOT RUNNING: $local_status" sudo docker logs vllm-qwen38 2>&1 | tail -60 exit 3 fi if [[ "$peer_status" != running* ]]; then echo "PEER CONTAINER NOT RUNNING: $peer_status" "${SSH[@]}" "sudo docker logs vllm-qwen38 2>&1 | tail -60" exit 3 fi if sudo docker logs vllm-qwen38 2>&1 | grep -q 'Application startup complete'; then echo 'STARTUP COMPLETE' exit 0 fi sleep 10 done echo 'WATCHDOG TIMEOUT' exit 5 ``` transformers 会打两条 `[ERROR] min_frames / max_frames ... not documented`,那是 docstring 噪音,不要当成启动失败 # 附录D 故障排查 | 现象 | 处理 | | --- | --- | | 日志停在 IPv6 / Gloo 超时 | 确认 `GLOO_USE_LIBUV=0`、`NCCL_SOCKET_FAMILY=AF_INET`,UFW 已放行 169.254/16 和 192.168.101.0/24 | | `collective_rpc should not be called on follower node` | rank 1 必须 `--headless` | | `cudagraph-mode Unexpected keyword argument` | 用 `-cc.cudagraph_mode`,不要连字符 | | `CUDA out of memory` / KV cache 不足 | `--max-model-len` 按 262144 → 131072 → 65536 下调 | | Mamba cache 容量错误 | 上调 `--max-num-seqs` | | CPU 打满、GPU 0%、`nvidia-smi` 卡住 | 立刻 `docker stop`,不要再等;核对本机权重是否又换成了 BF16 | | MemAvailable 跌破 5 GiB | 看门狗应已停容器;复盘后再起,不要 `restart: always` | | 对端 Docker 里看不到 GPU | 核驱动内核态 / 用户态版本,不一致就重启对端 | | rsync 在 `trees/*.json` 失败 | `sudo chmod -R a+rX` 对应 hub 目录后再传 | 验证用的 chat 请求示例: ``` curl -sS http://localhost:20001/v1/chat/completions \ -H 'Content-Type: application/json' \ -d '{ "model": "qwen3.8-flash-next", "messages": [{"role": "user", "content": "用一句话回答:3 的阶乘是多少?"}], "max_tokens": 256, "chat_template_kwargs": {"enable_thinking": true} }' ```
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
1
)
wmsuper
雪 币:
9859
活跃值:
(15950)
能力值:
( LV13,RANK:400 )
在线值:
发帖
36
回帖
138
粉丝
358
关注
私信
wmsuper
7
2
楼
rich!!!!
8小时前
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
pureGavin
3
104
发帖
1466
回帖
310
RANK
关注
私信
他的文章
[原创]lenovo thinkstation PGX修复与多卡连接
341
[原创]cursor解禁所有模型
1791
[原创]大模型微调分享
1816
[原创]cursor解禁Claude系列模型方法
2823
[分享]open-terminal搭建记录
2201
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部