DualPipe 并行
优化流水线并行,减少气泡
类型: 学习 | 语言: Python | 🏷 前置:《多 Token 预测》(本系列第 17 篇)
学习目标
- 理解流水线并行的原理
- 实现 DualPipe 并行
- 减少流水线气泡
- 优化通信开销
- 分析 DualPipe 的优势
流水线并行概述
流水线并行将模型分成多个阶段,每个阶段在不同的设备上执行。
标准流水线并行的问题
- 气泡:设备空闲等待数据
- 内存峰值:需要存储多个微批次的激活
- 通信开销:阶段间传输激活
DualPipe 的解决方案
DualPipe 通过双缓冲和重叠计算与通信,减少气泡和内存峰值。
DualPipe 实现
基本架构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import torch import torch.nn as nn import torch.distributed as dist
class DualPipe: def __init__(self, model, n_stages, n_microbatches): self.model = model self.n_stages = n_stages self.n_microbatches = n_microbatches self.stages = self.split_model() self.forward_buffers = [None] * n_microbatches self.backward_buffers = [None] * n_microbatches self.double_buffer = { "forward": [None, None], "backward": [None, None] } def split_model(self): """将模型分块""" layers = list(self.model.children()) chunk_size = len(layers) // self.n_stages stages = [] for i in range(self.n_stages): start = i * chunk_size end = start + chunk_size if i < self.n_stages - 1 else len(layers) stage = nn.Sequential(*layers[start:end]) stages.append(stage) return nn.ModuleList(stages)
|
前向传播
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| class DualPipeForward: def __init__(self, pipe, rank, world_size): self.pipe = pipe self.rank = rank self.world_size = world_size self.forward_queue = [] def forward(self, input_ids): """前向传播""" batch_size = input_ids.shape[0] microbatches = torch.chunk(input_ids, self.pipe.n_microbatches) outputs = [] for i, microbatch in enumerate(microbatches): buffer_idx = i % 2 if self.rank < self.pipe.n_stages: if self.rank == 0: output = self.pipe.stages[self.rank](microbatch) else: if self.pipe.double_buffer["forward"][buffer_idx] is not None: microbatch = self.pipe.double_buffer["forward"][buffer_idx] else: microbatch = self.receive_forward(buffer_idx) output = self.pipe.stages[self.rank](microbatch) if self.rank < self.pipe.n_stages - 1: self.pipe.double_buffer["forward"][buffer_idx] = output outputs.append(output) return torch.cat(outputs, dim=0) def receive_forward(self, buffer_idx): """接收前向数据""" if self.rank > 0: recv_tensor = torch.zeros_like(self.pipe.stages[self.rank](torch.zeros(1, 768))) dist.recv(recv_tensor, src=self.rank - 1) return recv_tensor return None
|
反向传播
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class DualPipeBackward: def __init__(self, pipe, rank, world_size): self.pipe = pipe self.rank = rank self.world_size = world_size def backward(self, outputs, labels): """反向传播""" total_loss = 0 for i in range(len(outputs) - 1, -1, -1): output = outputs[i] loss = F.cross_entropy( output.view(-1, output.size(-1)), labels[i].view(-1) ) total_loss += loss total_loss.backward() return total_loss
|
优化技术
重叠计算与通信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| class OverlappedDualPipe: def __init__(self, pipe, rank, world_size): self.pipe = pipe self.rank = rank self.world_size = world_size self.send_handles = [] self.recv_handles = [] def overlapped_forward(self, input_ids): """重叠前向传播""" microbatches = torch.chunk(input_ids, self.pipe.n_microbatches) outputs = [] for i, microbatch in enumerate(microbatches): buffer_idx = i % 2 if self.rank < self.pipe.n_stages - 1: if self.pipe.double_buffer["forward"][buffer_idx] is not None: handle = dist.isend( self.pipe.double_buffer["forward"][buffer_idx], dst=self.rank + 1 ) self.send_handles.append(handle) if self.rank > 0: recv_tensor = torch.zeros_like(microbatch) handle = dist.irecv(recv_tensor, src=self.rank - 1) self.recv_handles.append(handle) if self.rank == 0: output = self.pipe.stages[self.rank](microbatch) else: self.recv_handles[i].wait() output = self.pipe.stages[self.rank](recv_tensor) outputs.append(output) for handle in self.send_handles: handle.wait() return torch.cat(outputs, dim=0)
|
内存优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| class MemoryEfficientDualPipe: def __init__(self, pipe, rank, world_size): self.pipe = pipe self.rank = rank self.world_size = world_size self.checkpoint_interval = 4 def memory_efficient_forward(self, input_ids): """内存优化前向传播""" microbatches = torch.chunk(input_ids, self.pipe.n_microbatches) outputs = [] for i, microbatch in enumerate(microbatches): if i % self.checkpoint_interval == 0: self.save_activation(microbatch, i) output = self.pipe.stages[self.rank](microbatch) outputs.append(output) if i > 0 and (i - 1) % self.checkpoint_interval == 0: self.free_activation(i - 1) return torch.cat(outputs, dim=0) def save_activation(self, tensor, index): """保存激活""" self.activations[index] = tensor.cpu() def free_activation(self, index): """释放激活""" if index in self.activations: del self.activations[index]
|
性能分析
气泡分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| def analyze_bubble_time(pipe, n_microbatches, n_stages): """分析气泡时间""" bubble_time = 0 for i in range(n_microbatches): for j in range(n_stages): if i < n_stages - 1: bubble_time += 1 total_time = n_microbatches * n_stages bubble_ratio = bubble_time / total_time print(f"气泡时间: {bubble_time}") print(f"总时间: {total_time}") print(f"气泡比例: {bubble_ratio:.2%}") return bubble_ratio
|
通信分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| def analyze_communication(pipe, n_microbatches, message_size): """分析通信开销""" forward_comm = n_microbatches * (pipe.n_stages - 1) * message_size backward_comm = n_microbatches * (pipe.n_stages - 1) * message_size total_comm = forward_comm + backward_comm print(f"前向通信: {forward_comm / 1e9:.2f}GB") print(f"反向通信: {backward_comm / 1e9:.2f}GB") print(f"总通信: {total_comm / 1e9:.2f}GB") return total_comm
|
与传统流水线比较
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| def compare_with_traditional(pipe_dual, pipe_traditional, n_microbatches): """与传统流水线比较""" time_dual = measure_time(pipe_dual, n_microbatches) memory_dual = measure_memory(pipe_dual) time_traditional = measure_time(pipe_traditional, n_microbatches) memory_traditional = measure_memory(pipe_traditional) print(f"双缓冲流水线:") print(f" 时间: {time_dual:.4f}s") print(f" 内存: {memory_dual:.2f}MB") print(f"传统流水线:") print(f" 时间: {time_traditional:.4f}s") print(f" 内存: {memory_traditional:.2f}MB") print(f"加速比: {time_traditional / time_dual:.2f}x") print(f"内存减少: {(memory_traditional - memory_dual) / memory_traditional:.2%}")
|
配置优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| def optimize_config(model_size, n_gpus, batch_size): """优化配置参数""" if n_gpus >= 8: n_stages = 8 elif n_gpus >= 4: n_stages = 4 else: n_stages = 2 if batch_size >= 32: n_microbatches = 8 elif batch_size >= 16: n_microbatches = 4 else: n_microbatches = 2 chunk_size = model_size // n_stages print(f"配置:") print(f" 阶段数: {n_stages}") print(f" 微批次数: {n_microbatches}") print(f" Chunk 大小: {chunk_size / 1e6:.2f}M 参数") return { "n_stages": n_stages, "n_microbatches": n_microbatches, "chunk_size": chunk_size }
|
实际应用
大模型训练
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| def train_large_model(model, dataset, config): """使用 DualPipe 训练大模型""" dist.init_process_group("nccl") rank = dist.get_rank() world_size = dist.get_world_size() pipe = DualPipe(model, world_size, config["n_microbatches"]) for epoch in range(config["epochs"]): for batch in dataset: outputs = pipe.forward(batch["input_ids"]) loss = F.cross_entropy(outputs, batch["labels"]) loss.backward() optimizer.step() optimizer.zero_grad()
|
多节点训练
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class MultiNodeDualPipe: def __init__(self, model, n_nodes, n_gpus_per_node): self.model = model self.n_nodes = n_nodes self.n_gpus_per_node = n_gpus_per_node self.intra_pipe = DualPipe(model, n_gpus_per_node) self.inter_comm = InterNodeComm(n_nodes) def train(self, input_ids): """多节点训练""" outputs = self.intra_pipe.forward(input_ids) self.inter_comm.sync(outputs) return outputs
|
总结
DualPipe 通过双缓冲和重叠计算与通信,显著减少了流水线气泡和内存峰值。关键优化包括异步通信、内存优化和配置调整。DualPipe 特别适合训练大型语言模型。
下一步
下一课将深入讲解 DeepSeek-V3 的架构。
📚 本文改编自 AI Engineering from Scratch(MIT License · 作者 Rohit Ghumare),中文内容来自官方中文镜像。原课程共 503 课 · 20 阶段 · 免费开源,教程网站见 aiengineeringfromscratch.com。