前四篇已经解释请求如何到达 Scheduler、KV Cache 如何管理,以及 Engine 怎样执行一个 Batch。最后还剩一个问题:几十亿参数怎样分布到多张 GPU,并在每层计算后重新组合?
本文先推导 Tensor Parallel 的基本原理,再分析 Mini-SGLang 的 Layers、Models、Distributed、MoE 和 Kernel 模块。源码基于提交 9a91cfa。
一、为什么需要 Tensor Parallel
如果模型权重或 KV Cache 放不进单张 GPU,最直接的方法是把不同层放到不同 GPU,也就是 Pipeline Parallel。但自回归 Decode 每轮 token 很少,流水线容易出现气泡,而且跨 stage 传递激活会增加延迟。
Tensor Parallel 把同一层的矩阵切到多张 GPU,让所有 GPU 同时计算同一批 token:
同一 Transformer Layer
GPU 0:处理部分权重
GPU 1:处理部分权重
GPU 2:处理部分权重
GPU 3:处理部分权重
↓ collective
组合为完整层输出
它需要更频繁的 GPU 间通信,但能共同承载单层权重,并保持每个 rank 的执行进度一致。
二、Column Parallel Linear
考虑线性层:
Y = XW
按输出维切分权重:
W = [W0 | W1 | ... | Wn]
每个 rank 都拥有完整输入 X,分别计算:
Y0 = XW0
Y1 = XW1
...
输出自然是 Y 的不同列。如果下一层也能消费分片输出,就不需要立即通信。
QKV projection 和 MLP 的 gate/up projection 通常适合 Column Parallel,因为 Attention heads 或中间维可以按 rank 切分。
Mini-SGLang 在 layers/linear.py 中实现对应的列并行 Linear,并在加载权重时只读取本 rank 所需分片。
三、Row Parallel Linear
按输入维切分权重:
W = [W0; W1; ...; Wn]
X = [X0 | X1 | ... | Xn]
每个 rank 计算部分结果:
P0 = X0W0
P1 = X1W1
...
Y = P0 + P1 + ...
最后必须执行 all-reduce sum。
Attention 的 output projection 和 MLP down projection 常用 Row Parallel。它们接收前一列并行层产生的分片激活,并在残差连接前还原完整 hidden states。
一组典型 MLP 因而是:
完整 hidden states
-> Column Parallel gate/up
-> 每 rank 计算局部激活
-> Row Parallel down
-> all-reduce
-> 完整 hidden states
四、Attention head 如何切分
Q heads 通常可以均匀分到各 rank。KV heads 则要考虑 GQA:KV head 数可能少于 TP size。
Mini-SGLang 的配置和 Layer 工具允许 KV heads 在必要时复制,而不是强制每个 rank 至少获得一个独立 KV head。每个 rank 的 KV Cache 只保存本地需要的 heads,因此缓存容量计算使用 local_kv_heads。
Attention 输出经过 Row Parallel output projection 和 all-reduce 后,各 rank 再次得到一致的完整 hidden states。
五、Vocab Parallel Embedding 与 LM Head
词表也可以沿 vocab 维切分。每个 rank 只保存一段 token embedding:
Rank 0:token [0, V/2)
Rank 1:token [V/2, V)
输入 token 不属于本 rank 时,本地输出置零;所有 rank 的 embedding 结果 all-reduce 后得到完整向量。
LM Head 使用相同的词表分片思路,各 rank 产生局部 logits。随后可根据采样实现选择 all-gather 完整词表,或使用分布式选择。Mini-SGLang 的抽象把通信集中在并行 Layer 和 Distributed backend 中,模型代码保持接近普通 Transformer。
六、Distributed 模块的两套实现
distributed/impl.py 提供统一的:
all_reduce();all_gather()。
底层可使用:
torch.distributed+ NCCL;- 项目封装的 PyNCCL;
- 单 GPU 空操作实现。
控制面另有 Gloo group,用于广播 Python 消息、同步配置和检查显存。数据面的模型张量则走 NCCL 类通信。
把控制与计算通信分开,可以避免小型 CPU 消息依赖 GPU collective,也便于 Rank 0 驱动请求广播。
七、模型注册与构建
models/register.py 使用 Hugging Face config 中的 architectures[0] 选择模型类。当前注册:
LlamaForCausalLM;Qwen2ForCausalLM;Qwen3ForCausalLM;Qwen3MoeForCausalLM;MistralForCausalLM;Mistral3ForConditionalGeneration。
模型文件主要负责组装 Layer:Embedding、重复的 Decoder Layer、Norm 和 LM Head。运行时调度、KV Cache 与具体 Attention kernel 并没有复制到每种模型中。
这种结构的扩展路径很明确:新模型如果只是层组合不同,可以复用已有 Layer;只有新的算子或权重布局才需要扩展底层模块。
八、权重加载与分片
models/weight.py 和各 Layer 的 weight loader 负责将 Hugging Face 权重映射到本地参数。
Tensor Parallel 下不能先在每张 GPU 加载完整权重再切分,否则峰值显存过高。更合理的流程是:
读取权重 Tensor
-> 根据参数类型和 TP rank 选取 shard
-> 转换 dtype
-> 复制到本地参数
不同参数的切分维不同:
- Column Parallel 沿输出维切;
- Row Parallel 沿输入维切;
- QKV 合并参数分别处理 Q、K、V;
- GQA 的 KV heads 可能复制;
- Norm 和部分 bias 在各 rank 完整保留。
权重加载代码因此是理解 TP 正确性的关键,而不只是 I/O 工具。
九、RoPE 与位置编码
layers/rotary.py 实现 Rotary Position Embedding。RoPE 将位置相关旋转应用于 Query 和 Key,使注意力分数携带相对位置信息。
Scheduler 为每个 token 准备 position:
Prefill:cached_len ... device_len-1
Decode:当前最后位置
Layer 只消费 positions,不关心请求经历了多少次 chunk。这样 Chunked Prefill 在语义上仍等价于一次完整 Prefill。
十、MoE 的额外路径
Qwen3 MoE 不再让每个 token 经过同一个 MLP,而是由 router 选择少量 experts:
Token hidden state
-> Router logits
-> Top-k experts
-> Expert GEMM
-> 按权重合并
moe 目录定义 backend 接口与 fused 实现,kernel/triton/fused_moe.py 提供 Triton kernel。Engine 根据模型配置选择 MoE backend。
这里的核心性能问题是把按 expert 离散分组的 token 高效送入矩阵乘法,同时避免大量小 kernel 和中间 Tensor。
十一、自定义 Kernel 模块
kernel 目录包含几类底层能力:
index.py:索引和二维映射操作;store.py:将新 K/V 写入缓存;radix.py:Radix 相关 C++ 绑定;pynccl.py:轻量 NCCL 封装;triton/fused_moe.py:融合 MoE;csrc:CUDA/C++ 与 TVM-FFI JIT 代码。
为什么不全部使用 PyTorch 算子?因为调度器经常需要执行“小而特殊”的操作,例如根据二维请求索引生成扁平缓存位置。组合多个通用算子会产生额外中间 Tensor 和 kernel launch,自定义 kernel 可以一次完成。
十二、完整 Transformer 层中的通信点
以常见 Decoder Layer 为例:
完整 hidden states
-> RMSNorm
-> Column Parallel QKV
-> 每 rank 本地 RoPE + Attention
-> Row Parallel Output
-> all-reduce
-> Residual
-> RMSNorm
-> Column Parallel Gate/Up
-> 本地激活
-> Row Parallel Down
-> all-reduce
-> Residual
每层通常有两次主要 all-reduce。TP 越大,单卡计算越少,但通信比例越高。因此 TP size 不是越大越好,还取决于 GPU 互联、模型规模和 batch 形态。
十三、这套模块化设计的意义
Mini-SGLang 将系统拆成几条稳定边界:
Scheduler:选择工作
Cache:管理状态
Engine:组织执行
Model:定义网络结构
Layer:封装并行语义
Backend/Kernel:实现硬件优化
这使读者可以单独替换一个调度策略、缓存算法或 Attention backend,而不必修改整条推理链路。它也是 Mini-SGLang 作为教学型高性能引擎最有价值的地方。
至此,整个系列从 HTTP 请求一直走到了 GPU kernel。建议下一步结合 profiler 实验:分别关闭 Radix Cache、Overlap Scheduling 和 CUDA Graph,观察 TTFT、ITL、吞吐和显存利用率如何变化。
参考源码
python/minisgl/layers/linear.pypython/minisgl/layers/embedding.pypython/minisgl/layers/attention.pypython/minisgl/layers/rotary.pypython/minisgl/layers/moe.pypython/minisgl/models/register.pypython/minisgl/models/weight.pypython/minisgl/distributed/impl.pypython/minisgl/kernelpython/minisgl/moe