多 GPU 与多节点
一个模型,多张 GPU——当一台机器不够用时,还可以是多台机器。TensorSharp 按 Megatron-LM 列/行并行范式实现了张量并行,并通过点对点 TCP 网格把它扩展到多台主机。
一句话上手:加上 --tp N 即可在本机 N 张 GPU 上运行;在此基础上再加 --tp-node-id 与 --tp-peers 就能跨机器扩展。TensorSharp.Cli 与 TensorSharp.Server 都支持这些参数,可运行在 Direct cuda 后端以及 GGML CUDA / Vulkan 后端上。
什么时候需要它?
- 模型装不进一张 GPU。每张 GPU 只持有
1/N的切分权重,因此单卡显存放不下的模型,切到两张或四张卡上就能从容运行。 - 模型装不进一台机器。分布式 TP 把多台主机的 GPU 汇聚成一个逻辑并行组——2 节点 × 4 GPU 即全局并行度 8。
- 希望每个 token 获得更多显存带宽。每张 GPU 处理每个投影的自有切片,逐层矩阵乘是被切分的,而不是排队串行执行。
在 GGML 后端上,融合的按 rank block 计算图让 --tp 2 在本来就装得下单卡的模型上也能快于单 GPU —— Gemma 4 E4B Q8_0 上 51.7 对 37.3 tok/s(2× RTX 2000 Ada)。如果你要的是更高的并发吞吐而非更低的单流延迟,请先看 连续批处理——它默认开启,零成本即可尝试。
张量并行的工作原理
每个 transformer block 被改写为一对互补的切分方式,因此每半个 block 只需要一次集合通信:
-
列并行投影
QKV 与 gate/up 沿输出维度切分——注意力头或 FFN 中间维度分配到各 GPU。此处无需通信:每个 rank 只产出属于自己的那部分激活。
-
各 rank 独立计算
注意力(或激活函数)在每张 GPU 上只针对该 GPU 拥有的头运行,使用该 GPU 自己的 KV 缓存。
-
行并行投影 + AllReduce
output 与 down 投影沿输入维度切分,因此每个 rank 得到一个部分和。一次 AllReduce 把这些部分和相加,block 结束时每个 rank 都持有完全相同的隐藏状态。
归一化层、词嵌入与 LM head 在各 rank 上复制而非切分——它们体积很小,复制可以从关键路径上省掉一次集合通信。
本地 TP —— 单进程,多张 GPU
本地 TP 运行在单个进程内。在 Direct cuda 后端上,由一个线程顺序向所有 GPU 下发命令,真正的并行由 CUDA stream 提供,AllReduce 通过点对点设备拷贝加逐元素加法内核完成。在 GGML 后端上,则由一个 rank 工作线程池并发驱动各张 GPU —— GGML 的一次算子调用同时完成提交与同步,顺序循环各 rank 会让显卡严格串行执行。
# CLI —— 把模型切分到 2 张 GPU
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model model.gguf --backend cuda --tp 2
# GGML CUDA 后端上同理
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model model.gguf --backend ggml_cuda --tp 2
# 指定各 rank 对应的物理 GPU
TENSORSHARP_TP_DEVICES=0,2 dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll \
--model model.gguf --backend ggml_cuda --tp 2
# 服务端 —— 同样的参数(TENSORSHARP_TP_DEGREE=2 环境变量也可用)
dotnet TensorSharp.Server/bin/TensorSharp.Server.dll \
--model model.gguf --backend ggml_cuda --tp 2
也可以写进配置文件:
{ "model": "model.gguf", "backend": "cuda", "tp": 2 }
分布式 TP —— 多台机器
每个节点在自己的本地 GPU 上运行独立进程,并通过带长度前缀的分帧协议与其他所有节点建立 TCP 连接。AllReduce 是分层的:先在节点内通过 P2P 做本地归约,再由各节点代表之间走 TCP 交换,最后逐层广播回去——因此只有 1/tp_local 的数据需要穿过网络。
# 2 节点 × 每节点 2 GPU = 全局 TP 度 4
# 节点 0:
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model model.gguf --backend cuda --tp 2 \
--tp-node-id 0 --tp-peers "192.168.1.10:9500,192.168.1.11:9500"
# 节点 1 —— peer 列表相同,节点 ID 不同:
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model model.gguf --backend cuda --tp 2 \
--tp-node-id 1 --tp-peers "192.168.1.10:9500,192.168.1.11:9500"
服务端可以作为这样一个集群的前端,但只能是节点 0 —— 即负责采样并对外提供 HTTP 的 driver。其余每个节点都运行一个 TensorSharp.Cli worker,使用相同的模型、后端与 peer 列表。TENSORSHARP_TP_* 环境变量可以替代这些参数。
# 节点 0 —— 服务端 / driver:
dotnet TensorSharp.Server/bin/TensorSharp.Server.dll --model model.gguf --backend cuda \
--tp 2 --tp-node-id 0 --tp-peers "192.168.1.10:9500,192.168.1.11:9500"
# 节点 1 —— CLI worker:
dotnet TensorSharp.Cli/bin/TensorSharp.Cli.dll --model model.gguf --backend cuda --tp 2 \
--tp-node-id 1 --tp-peers "192.168.1.10:9500,192.168.1.11:9500"
所有节点必须传入完全相同的 --tp-peers 列表(顺序也要一致),并各自使用唯一的 --tp-node-id(0 起始,对应该列表中的下标)。示例中的端口 9500 不是默认值:请自行选定,并确保所有节点之间可达。节点间流量没有鉴权也没有加密,请把集群放在可信的内网中。
节点通常由人工间隔数秒到数分钟启动,因此先起来的节点会持续重试对外连接(最长 120 秒),而不是在第一次连接被拒绝时就失败。网格建立完成后,每个节点会打印 [TcpCommunicator] Rank r/N connected to all peers.。
支持的架构
TensorSharp 中的每一种自回归架构都可以在 TP 下运行;异构层各有自己的切分策略。
| 架构 | TP | 策略 / 说明 |
|---|---|---|
| Qwen 3 | ✅ | 参考实现 —— 标准列/行并行 QKV + FFN。 |
| Mistral 3 | ✅ | 融合与分离两种 QKV 布局,YaRN RoPE。 |
| Gemma 3 | ✅ | 分离 Q/K/V、GELU、滑动窗口注意力。 |
| Gemma 4 | ✅ | 稠密与 MoE、逐层 head 维度;多模态嵌入会注入 TP 路径,因此视觉 / 音频提示不会丢失。GGML 上融合的整模 MoE 主干在每个专家内部切分(gate/up 列并行、down 行并行),从而保留全局专家 id —— TS_GEMMA4_TP_FUSED_MOE=0 可回退到逐算子的整专家路径。Direct CUDA 使用逐专家切分。 |
| Qwen 3.5 / 3.6 family | ✅ | GatedDeltaNet 循环层采用块循环 V-head 归属 —— 每个 rank 维护自己的、常驻设备的 delta/conv 状态,循环路径无需跨 rank 通信。GGML 上整个 GDN block 作为一个打包的按 rank 内核运行,MoE 采用专家并行(每个 rank 持有整个专家,shared expert 按 Megatron 切分),LM head 为列并行且完全不需要集合通信。Direct CUDA 使用专家切分。 |
| GPT OSS | ✅ | MoE 专家切分、attention sink、YaRN。GGML 后端同样可运行,但 GGML 上的 MoE 路径目前仍按 token 逐个遍历专家,尚未使用专家并行。 |
| Nemotron-H | ✅ | MoE 专家切分;Mamba2 SSM 层在 rank 0 上计算后广播。GGML 上的 MoE 限制与 GPT OSS 相同。 |
| DiffusionGemma | — | 不适用(文本扩散采样器,非自回归 decode)。 |
| Qwen-Image-Edit | — | 不适用(MMDiT 图像生成)。 |
MoE 有两种切分方式。专家切分(Direct CUDA,以及各后端上的 GPT OSS / Nemotron-H)让每张 GPU 持有每一个专家权重的 1/tp,router 复制——因此无论 token 选中哪些专家,负载都保持均衡。专家并行(GGML 上的 Qwen 3.5/3.6)则划分整个专家,使每个 rank 每个投影仍然只需一次批量 ggml_mul_mat_id 调度,而不是按 (token, 专家) 循环。
要求与约束
- 仅限 GPU 后端。TP 可运行在
cuda、ggml_cuda与ggml_vulkan上;MLX 与 CPU 后端为单设备。 - 整除性。
numHeads、numKVHeads与intermediateSize都必须能被 TP 度整除;量化权重的行并行切分还要求ne0能被tp × blockSize整除。 - TP 下的批处理前向目前实现于 Qwen 3 与 Mistral 3。MoE 模型(Gemma 4、Qwen 3.5/3.6、GPT OSS、Nemotron-H)在 TP 激活时回退到按序列前向,因此连续批处理在这些模型上的增益较小。
- 本地并行度 ≤ 可见 GPU 数。请求的 rank 数超过主机设备数时,启动阶段就会直接失败。
- 多节点仍走逐算子前向。融合的整模 TP 计算图是按 rank、单进程的;分布式运行会回退到逐算子执行。
实测结果
2× RTX 2000 Ada(各 16 GB,PCIe,无 NVLink),--backend ggml_cuda --tp 2,prefill 512 / decode 64,单位 tok/s:
| 模型 | 单 GPU | --tp 2 |
|---|---|---|
| Gemma 4 E4B Q8_0 | 2760 / 37.3 | 2488 / 51.7 |
| Gemma 4 26B-A4B IQ4_XS | 1845 / 48.5 | 2537 / 51.2 |
| Qwen 3.5-9B Q8_0 | 1461 / 23.1 | 399 / 24.4 |
| Qwen 3.5-35B-A3B IQ4_XS | 装不下 | 184 / 18.1 |
Decode —— TP 本该受益的访存瓶颈部分 —— 在 Gemma 4 E4B 上达到单卡的 1.39×,在 Qwen 3.5-9B 上为 1.06×,且两个 Gemma 4 模型的输出与单卡运行逐字节一致。Prefill 受计算约束且要承担集合通信开销,因此在单卡装得下的模型上持平或略低于单卡。Qwen 3.5-35B-A3B 在 16 GB 卡上根本装不下 —— 只有 TP 能跑,两张卡上的显存占用为 9.4 + 8.0 GB。
调优与诊断
本地 AllReduce 优先使用 CUDA 点对点(P2P)DMA。启动时,并行组会为每一对报告支持 P2P 的设备启用 peer access,随后做一次往返自检——部分拓扑(挂在某些 PCIe 交换机后的 L4、开启 IOMMU 的主机、BAR1 窗口过小)虽然声称支持 P2P,却会静默传输损坏数据,未通过自检的设备对会被永久降级为主机中转。完全不支持 peer access 的硬件(A16 vGPU 配置、大多数消费级显卡)则从一开始就走主机内存中转。这些回退都是自动的;下面的开关用于人为强制走某条路径以便定位问题。
| 变量 | 默认值 | 作用 |
|---|---|---|
TENSORSHARP_TP_DEGREE | 1 | 本机切分使用的 GPU 数(等价于 CLI 与服务端的 --tp)。 |
TENSORSHARP_TP_DEVICES | 0..tp-1 | 各 rank 使用的 GPU 序号,例如 0,2。用于 GGML 后端。 |
TENSORSHARP_TP_NODE_ID | 未设置 | 本节点的 0 起始编号(等价于 --tp-node-id),需与 peer 列表一起设置。 |
TENSORSHARP_TP_PEERS | 未设置 | 所有节点的 host:port 列表,逗号分隔(等价于 --tp-peers)。 |
TENSORSHARP_TP_CONNECT_TIMEOUT_SECONDS | 120 | 节点向 peer 重试连接的时长。编排系统启动节点间隔较大时可调大。 |
TENSORSHARP_TP_RECV_TIMEOUT_SECONDS | 300 | peer 套接字的单次接收超时,让卡住的 peer 直接让集合通信失败,而不是挂在操作系统 TCP keepalive(往往 2 小时以上)上。 |
TENSORSHARP_TP_DISABLE_P2P | 关闭 | 1 表示所有跨 GPU 传输一律经主机内存中转——与无 P2P 硬件的路径完全一致。用于判断某个多 GPU 缺陷是否出在 P2P DMA 路径上。 |
TENSORSHARP_TP_HOST_ALLREDUCE | 关闭 | 1 表示本地 AllReduce 改为设备→主机、CPU 求和、主机→设备。更慢,但与已知可靠的多节点归约路径完全一致。 |
TS_GGML_TP_DEVICE_AR_THRESHOLD | 262144 | 超过该元素数量时 AllReduce 走 ggml 的设备集合通信,否则在主机内存中归约。GGML 的激活本来就在主机内存里,小载荷在那里求和更划算。 |
TS_GGML_TP_PARALLEL | 开启 | 0 表示顺序而非并发地驱动各 rank —— 诊断用,会显著变慢。 |
TS_GEMMA4_TP_FUSED_MOE | 开启 | 0 表示 Gemma 4 从融合的 MoE 主干回退到逐算子的整专家路径。 |
GGML_CUDA_ALLREDUCE | 自动 | nccl / internal / none,直接透传给 ggml 的集合通信选择。 |
GGML_CUDA_AR_BF16_THRESHOLD | 1 MB | ggml 在多大载荷以上把 F32 集合通信转成 BF16。TensorSharp 提高了 ggml 自身的默认值(总是转换),使 decode 规模的归约保持精确;设为 0 则完全禁用转换。 |
启动日志会明确给出拓扑:本地并行组打印 Tensor parallelism: N GPUs (<设备名>),设备对被降级时打印 TP: P2P disabled… 或自检告警,每个节点在网格建立完成后打印一条 [TcpCommunicator] Rank r/N connected to all peers.。
集群内的共享状态
当多个服务端进程共同承载同一批负载时,服务端可以选择把两类状态放到 Redis,而不是各自的进程内存中:
- KV 缓存层——分页 KV 块被持久化,因此某个进程算出的前缀可以被另一个进程复用(
--redis-url/--paged-kv-redis-url,环境变量TS_KV_CACHE_REDIS_URL;条目在TS_KV_CACHE_REDIS_TTL_MINUTES后过期,默认 24 小时)。 - Responses API 存储——OpenAI Responses 存储从内存变为持久化且共享(
TS_RESPONSES_STORE_REDIS_URL;--redis-url一次性设置这两项)。
# 两个层共用一个 Redis
dotnet TensorSharp.Server/bin/TensorSharp.Server.dll --model model.gguf --backend cuda \
--redis-url localhost:6379
# 仅 KV 缓存,TTL 12 小时
dotnet TensorSharp.Server/bin/TensorSharp.Server.dll --model model.gguf --backend cuda \
--paged-kv-redis-url localhost:6379 --paged-kv-redis-ttl 720
故障排查
| 现象 | 可能原因与处理 |
|---|---|
| 启动失败:请求的 TP 度超过设备数 | 进程看到的 CUDA 设备少于 --tp 所要求的数量。检查 CUDA_VISIBLE_DEVICES 与驱动。 |
指定了 --tp 2,模型却只加载到一张 GPU | 后端是 mlx、cpu 或 ggml_cpu/ggml_metal。TP 适用于 cuda、ggml_cuda 与 ggml_vulkan。 |
| 某个维度无法被 TP 度整除 | 选择能整除 numHeads、numKVHeads 与 intermediateSize 的并行度,通常取 2 的幂。 |
| 节点一直等待 peer | 各节点的 peer 列表、顺序或端口不一致,或者防火墙拦截了该端口。若只是节点启动间隔较久,调大 TENSORSHARP_TP_CONNECT_TIMEOUT_SECONDS。 |
| 只有多 GPU 时输出乱码 | 怀疑 P2P DMA 路径。先用 TENSORSHARP_TP_HOST_ALLREDUCE=1 重跑,再试 TENSORSHARP_TP_DISABLE_P2P=1;如果输出恢复正常,问题出在该拓扑的 peer DMA 上。 |
| 多 GPU 比单 GPU 更慢 | Prefill 受计算约束且要承担集合通信开销,在本来就装得下单卡的模型上可能落后于单卡。GGML 后端上的 decode 应当更快 —— 如果不是,请确认融合路径处于开启状态(未设置 TS_GEMMA4_TP_FUSED_MOE 与 TS_GGML_TP_FUSED_MATMUL),并确认是单进程运行,因为多节点会回退到逐算子前向。 |
仓库中的参考资料:USAGE_zh-cn.md → 张量并行与分布式推理、FEATURES_zh-cn.md,以及 TensorSharp.Distributed 项目。