Files
xserv/docs/17-tensor-parallelism.md
Gahow Wang 76fffb3b68 docs: Phase 17 tensor parallelism design
Megatron-style TP for Qwen3 on the 8x5090 (no-NVLink, PCIe) box: column/row
split per layer, 2 AllReduces/layer, multi-thread one-rank-per-GPU model,
NCCL, sharded weights, and the incremental implementation + verification plan.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 11:10:03 +08:00

123 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 17: Tensor Parallelism (TP)
> 目标:在单机多卡上做 **张量并行**,把 Qwen3-8B 的权重、计算和 KV cache 按
> head / 中间维切分到 TP 个 GPU 上,用 AllReduce 聚合,降低单卡显存压力并提升吞吐。
> 先做 **TP=2 / 4组内**,跳过投机解码(原 Phase 16
## 1. 硬件约束dash5
- 8× RTX 509032GBSM120**无 NVLink**,纯 PCIe Gen5。
- 拓扑GPU 03 一组、47 一组,组内 `PHB`(同 host bridge可 P2P跨组 `NODE`
- **TP 必须在组内**03 或 47否则 AllReduce 走跨组 PCIe延迟更高。
- AllReduce 带宽受限于 PCIe~单向 64GB/s远低于 NVLink通信会是 decode 的主要开销。
## 2. 切分方案Megatron-style
Qwen3-8B`hidden=4096``num_heads=32``num_kv_heads=8``head_dim=128`
`intermediate=12288``layers=36``vocab=151936`。TP=2/4/8 都能整除 32/8/12288。
每个 transformer block 的切分(设 world size = `T`,本 rank = `r`
### Attentioncolumn → row
| 权重 | 原 shape (已转置) | 切分 | 每 rank shape |
|------|-------------------|------|---------------|
| `q_proj_wt` | `[hidden, num_heads·head_dim]` | column按 Q head | `[hidden, (num_heads/T)·head_dim]` |
| `k_proj_wt` | `[hidden, num_kv_heads·head_dim]` | column按 KV head | `[hidden, (num_kv_heads/T)·head_dim]` |
| `v_proj_wt` | 同上 | column | 同上 |
| `o_proj_wt` | `[num_heads·head_dim, hidden]` | **row** | `[(num_heads/T)·head_dim, hidden]` |
- 每个 rank 只算自己的 `num_heads/T` 个 Q head 和对应的 `num_kv_heads/T` 个 KV head
GQA 的 `n_rep = num_heads/num_kv_heads = 4` 在每个 rank 内保持不变。
- `q_norm`/`k_norm``[head_dim]`)逐 head 应用,**复制**到每个 rank。
- RoPE 逐 head、按 position 应用,每个 rank 独立做。
- **KV cache 也切分**:每个 rank 的 paged KV 只存自己的 `num_kv_heads/T` 个 head
→ 每卡 KV 显存降为 1/TTP 的一大收益)。
- `attn = merge_heads(...) @ o_proj_wt` 得到**部分** `[T_tok, hidden]`**AllReduce(sum)** → 完整。
### MLP / SwiGLUcolumn → row
| 权重 | 原 shape | 切分 | 每 rank shape |
|------|----------|------|---------------|
| `gate_proj_wt` | `[hidden, intermediate]` | column | `[hidden, intermediate/T]` |
| `up_proj_wt` | 同上 | column | 同上 |
| `down_proj_wt` | `[intermediate, hidden]` | **row** | `[intermediate/T, hidden]` |
- `silu(gate)*up` 在切分后的 `[T_tok, intermediate/T]` 上逐元素做,无需通信。
- `down = (...) @ down_proj_wt` 得到部分 `[T_tok, hidden]`**AllReduce(sum)** → 完整。
### 复制(不切分)
- 所有 RMSNorm 权重(`input_norm`/`post_norm`/最终 `norm`):每个 rank 在 AllReduce 后
拿到完整 hidden本地用复制的权重归一化。
- **第一版**`embed_tokens``lm_head` 复制(各 ~1.2GB)。
后续优化vocab-parallel embeddinglocal lookup + AllReduce、column-parallel lm_head + AllGather。
### 通信点
每层 **2 次 AllReduce**o_proj 后、down_proj 后)→ 每生成 1 token 共 `2·36 = 72` 次。
decode 时每次 AllReduce 张量是 `[batch, 4096]` BF16单 token batch=1 时 8KB**延迟主导**。
## 3. 进程 / 线程模型
**单进程、多线程**:每个 TP rank 一个 OS 线程,线程启动时 `cudaSetDevice(rank_device)` 并绑定。
选择理由:
- xserv 的 caching allocator 是 `thread_local`,每线程独立池 → 天然契合「一线程一卡一池」。
- CUDA context 隐式按 device/thread 管理;线程内只 set 一次 device、不再切换即可。
- HTTP server / 调度器仍在主线程rank 0 协调),无需多进程 IPC改动最小。
- 单机 8 卡足够多进程torchrun 式)留待真正跨节点时再说。
执行流:主线程调度器准备一个 step 的输入tokens/positions/slots广播给 `T` 个 rank 线程;
每个 rank 线程跑自己的分片 forward含层内 AllReducerank 0 拿到完整 logits 后采样。
用 barrier / channel 同步每个 step。
## 4. 通信库NCCL
**NCCL**dash5 已装:`/usr/lib/x86_64-linux-gnu/libnccl.so.2``/usr/include/nccl.h`)。
- 新建 crate `xserv-distributed`NCCL FFI`ncclGetUniqueId``ncclCommInitRank`
`ncclAllReduce``ncclGroupStart/End`+ `TpContext`rank/world/comm/stream+
`all_reduce_sum(&mut GpuBuffer)` 原语。
- NCCL 多线程模式:主线程生成 `ncclUniqueId`,各 rank 线程用 `ncclCommInitRank(comm, world, id, rank)`
初始化(需 `ncclGroupStart/End` 包裹并发 init
- AllReduce 用 BF16`ncclBfloat16`+ `ncclSum`,在每个 rank 自己的 stream 上。
> **决策点**collective 用 NCCL 还是自己手写 P2P ring/tree AllReduce
> 本项目是「从零构建」,但 collective 属于基础设施(类比我们也用 cuBLAS 作为可切换后端)。
> 推荐先用 NCCL 把 TP 跑通、拿到正确性与加速比,后续可选做手写 AllReduce 作为学习项。
## 5. 权重分片加载
每个 rank 只加载/保留自己的分片,省显存:
- column-parallel 权重:按输出维切片取本 rank 的 `[*, dim/T]` 段。
- row-parallel 权重:按输入维切片取本 rank 的 `[dim/T, *]` 段。
- 复制权重norm/embed/lm_head每个 rank 各留一份。
实现:`loader` 读 safetensorsmmap时按 rank 只搬运需要的切片到该 rank 的 GPU
`Qwen3::from_weights_tp(config, weights, rank, world)` 在转置/切分时按 rank 取段。
## 6. 实现步骤(逐步可验证)
1. **P17.1 — `xserv-distributed` 基础**NCCL FFI + `TpContext` + `all_reduce_sum`
验收2 卡各放一个已知向量AllReduce 后两卡结果都等于和。
2. **P17.2 — 分片权重加载**`from_weights_tp`,每 rank 只持有自己的分片。
验收:各 rank 权重 shape 正确、显存占用约为 1/T+ 复制项)。
3. **P17.3 — TP forward**rank 内 attention/MLP + 层内 AllReduce。
验收:**TP=2 的 logits 与 TP=1 在 BF16 容差内一致**top-1 一致top-5 重合)。
4. **P17.4 — 接入 engine/server**`--tp N`,多线程 rank workers + rank0 调度。
验收:`--tp 2` 端到端可服务;用现有 llama.cpp bench 跑正确性;
测 TP=2 vs TP=1 的吞吐 / TTFT / 每卡显存。
## 7. 预期与风险
- **显存**:每卡权重 + KV 降到约 1/Tembed/lm_head 暂复制。TP=2 时单卡 ~8GB 权重 + 更大 KV/并发空间。
- **吞吐**PCIe AllReduce 延迟会吃掉部分收益decode 是延迟敏感的72 次小 AllReduce/token
可能让 TP=2 的单流 tok/s **不一定线性提升**,但能跑更大 batch / 更长 context。先测实测数。
- **风险**NCCL 多线程初始化的同步、每 rank stream 与现有 kernel stream 的协调、
KV cache 按 rank 切 head 后 paged kernel 的 head 维参数要用 per-rank 值。
- 正确性优先:先 TP=1 等价logits 对齐),再谈性能。
## 8. 不在本阶段范围
- 跨组 TP=8、Pipeline Parallelism、多节点。
- vocab-parallel embedding / lm_head先复制
- 手写 AllReduceNCCL 跑通后可选)。
- 与 CUDA Graph decode 的结合(先走非 graph 路径)。