并行推理指南¶
本文档详细介绍 TeleFuser 的分布式并行推理架构,包括原理介绍、配置方法和使用示例。 Tensor 数据流、同步协议、transport 职责和性能约束见通信架构。
概述¶
TeleFuser 提供多维度并行推理能力,支持以下并行策略:
| 并行类型 | 描述 | 适用场景 |
|---|---|---|
| 数据并行 (DP) | 复制模型到多个 GPU,并行处理不同数据 | 吞吐量优化 |
| CFG 并行 | 并行计算 positive/negative prompt | CFG 加速 |
| 序列并行 (SP) | 将长序列分割到多个 GPU | 长视频生成 |
| 流水线并行 (PP) | 将模型层分割到多个 GPU | 大模型推理 |
| 张量并行 (TP) | 将张量维度分割到多个 GPU | 大模型推理 |
架构设计¶
Device Mesh 布局¶
TeleFuser 使用 PyTorch DeviceMesh 管理分布式并行,维度顺序为:
from telefuser.distributed import create_device_mesh_from_config
from telefuser.core.config import ParallelConfig
config = ParallelConfig(
device_ids=[0, 1, 2, 3],
dp_degree=1,
cfg_degree=2,
sp_ulysses_degree=2,
sp_ring_degree=1,
pp_degree=1,
tp_degree=1,
)
device_mesh = create_device_mesh_from_config(config)
核心模块¶
telefuser/distributed/
├── device_mesh.py # DeviceMesh 创建和进程组管理
├── collectives.py # 共享的连续 gather 与 reduction 原语
├── pp_comm.py # 流水线并行 P2P 通信
├── ulysses_comm.py # Ulysses All-to-All 通信原语
├── ring.py # Ring Attention P2P 通信
├── parallel_shard.py # 序列并行张量分片/反分片
├── vae_spatial.py # 按高度分片的 VAE halo 交换
├── fsdp.py # FSDP 数据并行
└── tp_parallelize.py # 张量并行工具
模型代码只负责张量布局和重建语义;可复用的 collective buffer 分配与 reduction 提交统一放在 collectives.py,策略专用协议仍保留在各自模块中。
序列并行¶
序列并行用于处理超长序列(如长视频),将序列维度分割到多个 GPU。
Ulysses Attention¶
基于 All-to-All 通信的序列并行:
原理: 1. 每个 GPU 持有序列的一部分 2. 通过 All-to-All 将 heads 重新分配 3. 每个 GPU 拥有完整序列的部分 heads 4. 本地计算注意力后,再通过 All-to-All 恢复
数据流:
输入: (B, S_LOCAL, H_GLOBAL, D)
-> All-to-All QKV -> (B, S_GLOBAL, H_LOCAL, D)
-> 本地注意力计算
-> All-to-All O -> (B, S_LOCAL, H_GLOBAL, D)
特点: - 通信开销:2次 All-to-All(QKV + Output) - 适合中等长度序列 - 需要头数能被 GPU 数整除
在 PyTorch 2.11 及以上版本中,TeleFuser 优先使用 PyTorch Symmetric Memory 分配并 rendezvous 成组 Q/K/V scatter 的对端 target buffer,再由源码编译的 tf-kernel Copy Engine 算子通过一条高优先级 stream 直接 写入这些由 PyTorch 管理的映射。Q、K、V 保持独立提交以保留 projection overlap,同时共享一次 grouped handshake。communicator 及其缓存 buffer 由模型持有,因此模型 offload 或销毁时即可释放资源,无需 worker 在 process-group teardown 阶段执行额外 collective。
主包仍支持 torch>=2.6.0:能力检测采用延迟方式,较旧 PyTorch 会优先回退到源码编译的 CUDA IPC backend, 不可用时再回退到 PyTorch/NCCL。单次 collective 和 output gather 继续使用实测更快的 PyTorch/NCCL 路径。 设计、执行时间线、H100 测量、限制和 Related Work 见 CUDA IPC Ulysses 技术文章。
Ring Attention¶
基于 P2P 通信的序列并行:
原理: 1. 每个 GPU 持有 Q 的一部分和 K/V 的一部分 2. K/V 在 GPU 环中轮转 3. 每个 GPU 依次看到所有 K/V 块 4. 使用在线 softmax 合并注意力输出
算法流程:
for step in range(world_size):
# 1. 计算当前 KV 块的注意力
out, lse = attention(q, k, v)
# 2. 发送当前 KV 到下一个 GPU
# 3. 从上一个 GPU 接收新的 KV
next_k, next_v = send_recv_kv(k, v)
# 4. 使用在线 softmax 合并结果
out, lse = merge_attn_states(prev_out, prev_lse, out, lse)
# 5. 更新 KV
k, v = next_k, next_v
特点: - 支持任意长度序列 - 通信与计算可重叠 - 需要支持 log-sum-exp 的注意力实现
USP (Unified Sequence Parallelism)¶
Ulysses + Ring 组合策略,支持更大规模并行:
原理: 1. Ring 维度:序列分割 2. Ulysses 维度:heads 分割 3. 两种策略互补,支持更多 GPU
配置示例:
# 4 GPU: ring=2, ulysses=2
config = ParallelConfig(
device_ids=[0, 1, 2, 3],
sp_ring_degree=2,
sp_ulysses_degree=2,
)
异步 Ulysses (async_usp_forward)¶
异步 All-to-All 实现,重叠计算和通信:
# 发起异步 All-to-All
q_wait = ulysses_scatter_heads(q, group, tag="q", barrier=False)
k_wait = ulysses_scatter_heads(k, group, tag="k", barrier=False)
v_wait = ulysses_scatter_heads(v, group, tag="v")
# 等待完成
q = q_wait()
k = k_wait()
v = v_wait()
# 计算注意力
x = attention(q, k, v)
# 异步 All-to-All 输出
out_wait = ulysses_gather_heads(x, group, num_heads=num_heads)
out = out_wait()
流水线并行 (PP)¶
将模型层分割到多个 GPU,实现大模型推理。
跨 Worker Tensor 通道¶
相邻 stage 如果属于不同 ParallelWorker group,可以使用 WorkerTensorChannel 连接。CPU tensor 使用 multiprocessing shared memory;CUDA tensor 对每个稳定 tensor profile 使用两个有界的 producer-owned IPC slot,每个 IPC allocation 只打开一次并持续复用。Pool handle 与各 rank 的 offset 作为私有 WorkerTensorRef 元数据沿现有控制路径传输。双向复用的跨进程 CUDA event 既保证 producer staging 完成后 consumer 才 copy,也保证所有 consumer copy 完成后 producer 才覆盖 slot。Consumer 先记录 completion event 再发布 generation ACK,producer staging stream 等待这些 event;整个过程不需要 device-wide synchronization。
from telefuser.worker import ParallelWorker, WorkerTensorChannel
latent_channel = WorkerTensorChannel(consumer_world_size=vae_parallel_config.world_size)
denoise_worker = ParallelWorker(
denoise_stage,
tensor_output_channel=latent_channel,
tensor_output_methods=("denoise",),
)
vae_worker = ParallelWorker(vae_stage, tensor_input_channels=(latent_channel,))
Channel 不会自动创建。拥有 stage 拓扑的 pipeline 必须创建并持有每条 channel,把它传给两端的 ParallelWorker,并在 consumer 和 producer worker 停止后关闭。Actor 或 scheduler edge 只携带 WorkerTensorRef metadata;支持取消的 actor 流程必须通过 consumer worker 释放末端 reference。完整的跨 stage wiring 与生命周期 contract 参见 通信架构。
当 consumer rank 只处理互不重叠的 tensor slice 时可设置 shard_dim。Producer 只 staging 一次,各 rank 仅 copy 自己的 slice。LingBot 空间并行 VAE 使用 shard_dim=-2,因此跨卡 peer-copy 聚合通信量保持为一个 logical latent,而不会随 VAE world size 放大;计入 producer 本地 staging 后,设备搬运预算为两个 logical latent。每个 channel 最多池化八种稳定 CUDA tensor profile;更多动态 profile 会回退到 PyTorch CUDA IPC, 避免 retained HBM 无界增长。
该路径是单 producer、单 consumer group 的点对点 FIFO。只有 tensor 的完整 consumer 集合就是所连接的 worker group 时才能启用。需要在主进程读取 tensor 的调用可以传入 _tensor_transport=False。Consumer 必须保持 producer 顺序;关闭时先停止 consumer、再停止 producer,最后关闭 channel。Scheduler 取消末端 artifact 时,必须按 producer 顺序调用 consumer worker 的 discard_tensor_refs(ref, sync=True)。该操作不会在父进程物化 tensor,并会 立即释放 CPU shared memory 或 CUDA IPC storage;正常 receive 仍会兜底丢弃更早的已取消 FIFO entry。
常规 worker 派发同样只向各 rank 发送 shared-memory 或 CUDA IPC handle,最终 device placement 由接收 rank 完成;主进程不再为每张目标 GPU 分配临时副本。
可使用本地 SGLang checkout 在相同 GPU 和 tensor shape 上运行端到端延迟门禁:
该比较同时计入两种实现的 producer staging、元数据传输、target copy、target 同步与 slot ACK。默认门禁使用 200 个样本:p50 不得比 SGLang 高 5% 以上;p95 上限取 10% 与 0.05 ms 中较宽者,以覆盖亚毫秒级 multiprocessing 调度抖动。
原理¶
Stage 0 (GPU 0): Embedding + Layers [0:N/4]
↓ send hidden states
Stage 1 (GPU 1): Layers [N/4:N/2]
↓ send hidden states
Stage 2 (GPU 2): Layers [N/2:3N/4]
↓ send hidden states
Stage 3 (GPU 3): Layers [3N/4:N] + Head
↓ output
P2P 通信¶
from telefuser.distributed import PipelineP2PComm, get_pp_group
pp_group = get_pp_group(device_mesh)
comm = PipelineP2PComm(pp_group)
# 发送隐藏状态到下一阶段
if not comm.is_last_stage:
comm.send_latent(hidden_states)
# 从上一阶段接收隐藏状态
if not comm.is_first_stage:
hidden_states = comm.recv_latent(shape=latent_shape)
层分配¶
# 自动分配层到各阶段
start_idx, end_idx = comm.get_stage_indices(num_layers)
# 示例:40 层,4 阶段
# Stage 0: [0:10]
# Stage 1: [10:20]
# Stage 2: [20:30]
# Stage 3: [30:40]
PP Forward 实现¶
def pp_forward(self, x, timestep, context, latent_shape, **kwargs):
# 第一阶段:Embedding + 首批层
if self.is_pp_first_stage:
x = self.patch_embedding(x)
x, grid_size = self.patchify(x)
x = self.forward_blocks_pp(x, timestep, context, **kwargs)
self.pp_comm.send_latent(x)
return None
# 中间阶段:接收 + 处理 + 发送
elif not self.is_pp_last_stage:
x = self.pp_comm.recv_latent(shape=latent_shape)
x = self.forward_blocks_pp(x, timestep, context, **kwargs)
self.pp_comm.send_latent(x)
return None
# 最后阶段:接收 + 处理 + 输出
else:
x = self.pp_comm.recv_latent(shape=latent_shape)
x = self.forward_blocks_pp(x, timestep, context, **kwargs)
x = self.head(x)
return x
CFG 并行¶
将 Classifier-Free Guidance 的 positive/negative prompt 并行计算:
原理¶
标准 CFG 计算:
CFG 并行将 positive 和 negative 分配到不同 GPU:
使用方法¶
# 2 GPU CFG 并行
config = ParallelConfig(
device_ids=[0, 1],
cfg_degree=2,
)
# 在模型中启用
dit.enable_cfgp()
实现¶
# 分片
cfg_parallel_shard(device_mesh, [x, timestep, context, ...])
# 根据 CFG rank 选择 positive/negative
cond_flag = False if get_cfg_rank(device_mesh) else True
# 计算
output = model(x, context, cond_flag=cond_flag)
# 合并
output = cfg_parallel_unshard(device_mesh, [output])[0]
数据并行 (DP)¶
使用 FSDP 进行数据并行训练/推理:
FSDP1¶
from telefuser.distributed.fsdp import shard_model
model = shard_model(
model,
device_id=device_id,
sharding_strategy=ShardingStrategy.FULL_SHARD,
wrap_module_names=["blocks"],
param_dtype=torch.bfloat16,
)
FSDP2¶
from telefuser.distributed.fsdp import shard_model_fsdp2
model = shard_model_fsdp2(
model,
wrap_module_names=["blocks"],
param_dtype=torch.bfloat16,
)
张量并行 (TP)¶
将张量维度分割到多个 GPU:
使用方法¶
from telefuser.distributed.tp_parallelize import parallelize_module
from torch.distributed.tensor.parallel import ColwiseParallel, RowwiseParallel
tp_plan = {
"self_attn.q": ColwiseParallel(),
"self_attn.k": ColwiseParallel(),
"self_attn.v": ColwiseParallel(),
"self_attn.o": RowwiseParallel(),
"ffn.0": ColwiseParallel(),
"ffn.2": RowwiseParallel(),
}
model = parallelize_module(model, device_mesh, tp_plan)
注意事项¶
- 在所选 pipeline 支持时,SP 和 TP 可以作为独立的 device mesh 维度同时启用
- 需要确保头数能被 TP 度数整除
Worker 实现¶
ParallelWorker¶
多进程并行 Worker,使用 multiprocessing.spawn:
from telefuser.worker import ParallelWorker
worker = ParallelWorker(stage)
# 调用方法
result = worker.process(latents, ...)
# 关闭
del worker
特点: - 每个 GPU 一个进程 - 自动初始化进程组 - 支持同步/异步调用
RayWorker¶
Ray 集群分布式 Worker:
from telefuser.worker import create_ray_worker
worker = create_ray_worker(stage, enable_parallel=True)
result = worker.process.remote(latents, ...)
配置说明¶
ParallelConfig¶
@dataclass
class ParallelConfig:
device_ids: list | None = None # GPU ID 列表
dp_degree: int = 1 # 数据并行度
cfg_degree: int = 1 # CFG 并行度
sp_ulysses_degree: int = 1 # Ulysses 序列并行度
sp_ring_degree: int = 1 # Ring 序列并行度
pp_degree: int = 1 # 流水线并行度
tp_degree: int = 1 # 张量并行度
enable_fsdp: bool = False # 启用 FSDP
timeout: int = 600 # 超时时间(秒)
queue_with_cpu: bool = False # 使用 CPU 队列
worker_intra_op_threads: int = 1 # 每个 worker 的 CPU intra-op 线程数
默认值 1 与 torchrun 一致,可避免多个 GPU worker 的 CPU 线程过度竞争。只有分布式 stage 包含大量 CPU 计算时才需要提高该值;它不会修改父进程的线程池。
验证规则¶
# 设备数必须等于各并行度乘积
world_size = dp * cfg * sp_ring * sp_ulysses * pp * tp
# SP 和 TP 可以作为独立的 mesh 维度组合。
# 所选 pipeline 必须实现并校验请求的组合。
使用示例¶
单 GPU 推理¶
2 GPU Ulysses 序列并行¶
config = ParallelConfig(
device_ids=[0, 1],
sp_ulysses_degree=2,
)
pipe_config.dit_config.parallel_config = config
pipe_config.enable_denoising_parallel = True
4 GPU CFG + Ulysses¶
4 GPU USP¶
4 GPU 流水线并行¶
8 GPU 混合并行¶
# DP=2, CFG=2, Ulysses=2
config = ParallelConfig(
device_ids=[0, 1, 2, 3, 4, 5, 6, 7],
dp_degree=2,
cfg_degree=2,
sp_ulysses_degree=2,
)
Wan Video 示例¶
from telefuser.pipelines.wan_video.wan21_video import (
Wan21VideoPipeline,
Wan21VideoPipelineConfig,
)
from telefuser.core.config import AttentionConfig, AttnImplType
# 创建 Pipeline
pipe = Wan21VideoPipeline(device="cuda", torch_dtype=torch.bfloat16)
pipe_config = Wan21VideoPipelineConfig()
# 配置并行
if gpu_num > 1:
pipe_config.dit_config.parallel_config.device_ids = list(range(gpu_num))
pipe_config.dit_config.parallel_config.sp_ulysses_degree = 2
pipe_config.enable_denoising_parallel = True
# 配置注意力
pipe_config.dit_config.attention_config = AttentionConfig.dense_attention(
AttnImplType.FLASH_ATTN_2
)
# 初始化
pipe.init(module_manager, pipe_config)
# 推理
video = pipe(
prompt="A stylish girl playing with her dog",
num_inference_steps=40,
num_frames=81,
cfg_scale=6.0,
)
Device Mesh 工具函数¶
from telefuser.distributed import (
# 进程组
get_dp_group, get_dp_rank, get_dp_world_size,
get_cfg_group, get_cfg_rank, get_cfg_world_size,
get_ulysses_group, get_ulysses_rank, get_ulysses_world_size,
get_ring_group, get_ring_rank, get_ring_world_size,
get_pp_group, get_pp_rank, get_pp_world_size,
get_tp_group, get_tp_rank, get_tp_world_size,
# PP 辅助
is_pipeline_first_stage,
is_pipeline_last_stage,
# 策略检测
get_attention_strategy, # 返回 "local", "ulysses", "ring", "usp"
# 通信
ulysses_scatter_heads,
ulysses_gather_heads,
RingP2PComm,
PipelineP2PComm,
merge_attn_states,
ring_attention_forward,
)
# 获取当前注意力策略
strategy = get_attention_strategy(device_mesh)
# "local": 无序列并行
# "ulysses": 仅 Ulysses
# "ring": 仅 Ring
# "usp": Ulysses + Ring 组合
性能优化建议¶
选择并行策略¶
| 场景 | 推荐策略 | 说明 |
|---|---|---|
| 短视频 (81帧) | 单 GPU 或 CFG=2 | 通信开销小 |
| 中等视频 (161帧) | Ulysses=2 | All-to-All 效率高 |
| 长视频 (241+帧) | Ring 或 USP | 支持任意长度 |
| 大模型 (14B) | PP 或 FSDP | 模型分割 |
FSDP vs TP 选择¶
FSDP 和 TP 以不同方式切分模型权重,推理通信的扩展规律也不同。FSDP 会 all-gather 参数单元,TP 会在 张量并行算子后归约激活张量;不能只根据模型大小或 sequence 长度判断哪一种更快。
| 条件 | 候选策略 | 原因 |
|---|---|---|
| 最大 FSDP wrapping unit 加激活可放入单卡 | FSDP 可行 | FSDP 在 all-gather 时必须完整 materialize 一个 wrapped unit。 |
| 单卡无法放下一个 wrapped unit | 需要 TP 或 PP | 仅切分 FSDP 常驻 shard 并不能消除完整单元的 all-gather 峰值。 |
| 参数 all-gather 主导暴露通信时间 | TP | TP 让参数 shard 常驻,转而通信激活。 |
激活归约主导暴露通信时间,尤其 B * S * H 较大时 | FSDP 或 TP + SP | FSDP 参数流量大多与 sequence 长度无关;SP 会降低 TP 的局部 sequence,但自身也会引入通信。 |
| TP group 位于高速 NVLink/NVSwitch 域内 | TP 值得优先评估 | 大量对延迟敏感的激活 collective 可受益于低延迟、高带宽互联。 |
第一轮可以按每张卡、每个 denoising step 估算通信字节数。设 FSDP degree 为 f,每个 wrapped unit 的完整参数字节数为 P_u,每个请求的 all-gather 次数为 r_u:
设 TP degree 为 t、SP degree 为 s、激活元素字节数为 e_a,一次 TP collective j 处理的激活 形状近似为 [B, S / s, H]:
该求和通常包括每个 Transformer block 中的 row-parallel reduction。Ulysses 或 Ring 流量必须单独加入; SP 虽然降低 TP 的局部激活大小,但并不是免费的。应在目标拓扑上把字节数转换成实际决策:
T_comm ~= N_collectives * latency + V_wire / effective_bandwidth
T_step ~= T_compute + exposed(T_comm after overlap)
在实际模型、sequence 长度、dtype、denoising step 数和 mesh degree 下,采集 p50/p95 step time、原始和 暴露通信时间、collective 次数及每卡峰值 HBM。FSDP 可与 PP、SP 组合;所选 pipeline 已实现并校验时,TP 和 SP 也可以组合。
通信优化¶
- 使用异步通信:
async_usp_forward重叠计算和通信 - 批量通信:
batch_isend_irecv减少通信次数 - FP8 量化:减少通信数据量
内存优化¶
- 序列并行:减少每个 GPU 的序列长度
- 流水线并行:减少每个 GPU 的层数
- CPU Offload:将权重卸载到 CPU
故障排除¶
设备数不匹配¶
解决方案:确保 len(device_ids) == dp * cfg * sp_ring * sp_ulysses * pp * tp
Ring Attention 需要 LSE¶
解决方案:使用支持 LSE 的注意力实现(Flash Attention ⅔/4)。
通信超时¶
解决方案:增加 timeout 参数值,或检查网络连接。