7深度学习系统:从算法到硬件

分布式训练

多 GPU 与多节点

数据并行模型并行NCCLAllReduce

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第7章 分布式训练 - 多 GPU 与多节点 (Distributed Training: Multi-GPU and Multi-Node)

一、导读

1.1 本章学习目标

通过本章的学习,读者将能够:

理解分布式训练的必要性和基本范式

掌握数据并行的原理、实现及梯度同步机制

深入分析模型并行的两种形式:流水线并行和张量并行

理解混合并行策略的设计原则和实际应用

掌握PyTorch Distributed Data Parallel (DDP) 的使用和优化

了解分布式训练中的通信模式和瓶颈分析

能够诊断和解决分布式训练中的常见问题

设计适合大规模模型的分布式训练方案

1.2 为什么本章如此重要

随着深度学习模型规模的指数级增长,单GPU已无法满足训练需求。从ResNet的数千万参数到GPT-3的1750亿参数,再到如今的万亿参数模型,分布式训练已成为训练现代AI系统的必备技术。

理解分布式训练对于多个方面至关重要。首先,它使得训练大型模型成为可能,通过将计算和内存压力分散到多个设备上。其次,分布式训练可以显著缩短训练时间,通过并行处理加速收敛。第三,随着云GPU实例的普及,掌握分布式训练可以帮助企业更有效地利用弹性计算资源。

此外,分布式训练涉及复杂的系统设计和优化问题。通信开销、负载均衡、故障恢复等挑战需要深入的系统知识才能有效解决。随着大语言模型和生成式AI的兴起,分布式训练的重要性只会继续增加。

1.3 核心问题引导

在学习本章内容时,请思考以下问题:

为什么单GPU无法训练大型模型?限制因素是什么(显存、计算、带宽)?

数据并行的基本原理是什么?梯度同步的AllReduce操作是如何工作的?

DDP(Distributed Data Parallel)与DataParallel有什么区别?为什么DDP性能更好?

流水线并行如何将模型分割到不同设备?流水线气泡(bubble)是如何产生的?如何最小化?

张量并行与流水线并行的区别是什么?在什么场景下应该选择其中一种?

混合并行策略如何组合多种并行方式?Megatron-LM和DeepSpeed采用了什么策略?

分布式训练中的通信瓶颈在哪里?如何估算和优化通信开销?

如何处理分布式训练中的故障恢复?检查点(checkpointing)策略有哪些考虑?


二、核心概念详解

分布式训练的必要性

深度学习的发展呈现出明显的规模化趋势:更大的模型、更多的数据、更长的训练时间。这种趋势背后有坚实的理论支撑——缩放定律(Scaling Laws)表明,模型性能随参数量、数据量和计算量的增加而持续提升。

然而,规模化带来了严峻的工程挑战:

显存限制:如前几章所述,单个GPU的显存容量有限(目前最大约80GB)。一个千亿参数的模型仅存储参数就需要数百GB,远超单卡能力。

计算时间:即使模型能放入单卡显存,训练时间也可能长达数月。这在实践中是不可接受的。

吞吐量限制:单个GPU的计算能力有限,无法充分利用大规模数据集。

分布式训练通过将计算任务分散到多个设备上,同时解决了这三个问题。理论上,使用$N$个设备可以将训练速度提升接近$N$倍(线性扩展),同时将显存容量扩大$N$倍。

数据并行(Data Parallelism)

数据并行是最简单也最常用的分布式训练策略。其核心思想是:在每个设备上保存完整的模型副本,但分配不同的数据子集进行训练。

工作流程:

初始化:在每个设备上加载相同的模型权重

数据分发:将训练数据分片,每个设备处理不同的批次

前向传播:各设备独立执行前向传播,计算本地损失

反向传播:各设备计算本地梯度

梯度同步:通过AllReduce操作在所有设备间同步梯度

参数更新:各设备使用同步后的梯度更新本地参数

由于所有设备保持相同的参数和接收相同的梯度更新(平均后的梯度),它们在训练过程中始终保持同步。

AllReduce操作:

AllReduce是数据并行的核心通信原语,它将所有设备上的梯度进行归约(通常是求平均),然后将结果广播到所有设备。

AllReduce可以通过多种方式实现:

  • Parameter Server架构:引入中心化的参数服务器,各工作节点将梯度发送到参数服务器,参数服务器聚合后返回。这种方法简单但存在单点瓶颈。
  • Ring AllReduce:设备组织成环形拓扑,每个设备只与相邻设备通信。经过$2(N-1)$步后,所有设备获得全局归约结果。Ring AllReduce的带宽利用率高,没有单点瓶颈,是NCCL等库的默认实现。

Ring AllReduce的过程分为两个阶段:

Scatter-reduce:每个设备将数据分块,沿环传递并累加

Allgather:将各设备上的归约结果广播到所有设备

总通信量为$2 \times (N-1) \times S / N$,其中$S$是数据大小,$N$是设备数。当$N$较大时,每设备的通信量趋近于$2S$,与设备数无关。这是Ring AllReduce的关键优势。

数据并行的优点:

  • 实现简单,易于理解
  • 通信开销与模型大小成正比,与批次大小无关
  • 适用于大多数模型类型

数据并行的缺点:

  • 每个设备需要存储完整的模型副本,显存效率低
  • 当模型太大无法放入单卡时无法使用
  • 通信开销可能成为瓶颈,特别是对于大模型

模型并行(Model Parallelism)

当模型太大无法放入单GPU显存时,需要将模型本身分布到多个设备上。模型并行主要有两种形式:流水线并行和张量并行。

流水线并行(Pipeline Parallelism)

流水线并行将模型的层分组分配到不同设备上。例如,对于一个100层的Transformer,可以将前33层放在GPU 0,中间34层放在GPU 1,最后33层放在GPU 2。

工作流程:

输入数据从第一个设备进入

每个设备处理完自己的部分后,将中间结果传递给下一个设备

最终输出从最后一个设备产生

反向传播沿相反方向进行

流水线气泡(Pipeline Bubble):

流水线并行的主要挑战是流水线气泡——设备在某些时刻处于空闲状态,等待上游或下游的数据。例如,在简单的串行流水线中,GPU 0在完成第一批次的处理后,必须等待该批次通过所有后续设备并返回梯度后才能处理第二批次。这段时间内GPU 0处于空闲状态,造成资源浪费。

气泡的大小取决于流水线的深度和批次数量。对于$P$个设备和$M$个微批次(micro-batch),气泡比例为:

$$\text{Bubble ratio} = \frac{P - 1}{M + P - 1}$$

为了减少气泡,可以使用多个微批次(micro-batching):将一个大批次拆分为多个小批次,依次送入流水线。这样可以让各个设备更充分地并行工作。

GPipe和PipeDream:

GPipe是一种改进的流水线并行策略,使用多个微批次来填充流水线。它将一个大批次划分为$M$个微批次,连续送入流水线,从而减少气泡比例。

PipeDream进一步引入了混合调度策略,允许不同设备同时处理不同批次的不同阶段,进一步提高利用率。

流水线并行的优点:

  • 可以训练超出单卡显存的大模型
  • 实现相对简单,只需按层分割

流水线并行的缺点:

  • 流水线气泡降低效率
  • 设备间需要频繁传递中间激活值,通信开销大
  • 负载不均衡可能导致某些设备空闲

张量并行(Tensor Parallelism)

张量并行将单个层的权重矩阵分割到多个设备上,每个设备计算矩阵的一部分。这与数据并行不同,数据并行复制整个模型,而张量并行将模型本身拆分。

工作原理:

以矩阵乘法$Y = XW$为例。假设$W$的形状为$(d_{in}, d_{out})$。我们可以将$W$按列分割为$P$部分,每部分形状为$(d_{in}, d_{out}/P)$。每个设备持有$W$的一部分$W_i$,计算$Y_i = X W_i$。最终输出$Y$需要通过AllGather操作将所有$Y_i$拼接起来。

对于Transformer中的自注意力层,张量并行可以应用于:

  • QKV投影矩阵:按头(head)分割
  • 前馈网络的权重矩阵:按神经元分割
  • 输出投影矩阵:按行或列分割

Megatron-LM的张量并行:

Megatron-LM是NVIDIA提出的大语言模型训练框架,其核心创新是高效的张量并行实现。Megatron-LM的关键洞察是:通过精心设计,可以使张量并行中的通信只在特定位置发生,而不是每层都通信。

具体来说,Megatron-LM将Transformer层内的操作重新排列,使得:

自注意力的QKV投影可以并行计算,无需通信

注意力得分的计算可以在各设备独立完成

只有在注意力输出和前馈网络输出处需要AllReduce同步

这种设计显著减少了通信次数,提高了效率。

张量并行的优点:

  • 可以有效利用大量设备
  • 没有流水线气泡问题
  • 适合超大规模模型

张量并行的缺点:

  • 实现复杂,需要对模型结构有深入了解
  • 通信开销大,每次前向/反向传播都需要多次AllReduce
  • 要求模型维度可以被设备数整除

混合并行策略

在实际应用中,通常结合多种并行方式来达到最佳效果。这就是混合并行(Hybrid Parallelism)。

常见的混合策略:

数据并行 + 流水线并行:适用于中等规模模型。在模型级别使用流水线并行,在每个流水线阶段内使用数据并行。

数据并行 + 张量并行:Megatron-LM采用的策略。在模型内部使用张量并行,在张量并行组之间使用数据并行。

数据并行 + 流水线并行 + 张量并行:用于超大规模模型。三种并行方式同时使用,形成三维并行空间。

并行维度的选择:

假设有$N$个设备,需要决定如何分配给三种并行方式:

  • $D$:数据并行度
  • $P$:流水线并行度
  • $T$:张量并行度

满足$D \times P \times T = N$。

选择的原则是:

  • 张量并行度$T$通常较小(2-8),因为张量并行的通信开销大
  • 流水线并行度$P$取决于模型深度,通常为模型层数的因数
  • 数据并行度$D$占据剩余的设备,用于提高吞吐量

例如,对于512个GPU训练GPT-3:

  • 张量并行:8路
  • 流水线并行:16路
  • 数据并行:512 / (8 × 16) = 4路

分布式训练框架

PyTorch DDP(Distributed Data Parallel):

DDP是PyTorch官方提供的数据并行实现,相比旧的DataParallel有显著优势:

  • 多进程而非多线程:DDP为每个GPU创建一个独立进程,避免了GIL(Global Interpreter Lock)的限制
  • 梯度桶(Gradient Bucketing):将梯度分组打包,减少通信次数
  • 重叠计算和通信:在反向传播的同时进行梯度同步,隐藏通信延迟
  • 支持分布式采样器:确保每个epoch数据的正确分发

使用DDP的基本模式:

python
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP

def setup_ddp(rank, world_size):
    os.environ['MASTER_ADDR'] = 'localhost'
    os.environ['MASTER_PORT'] = '12355'
    
    dist.init_process_group("nccl", rank=rank, world_size=world_size)
    
    model = MyModel().to(rank)
    ddp_model = DDP(model, device_ids=[rank])
    
    return ddp_model

DeepSpeed:

Microsoft DeepSpeed是一个全面的分布式训练优化库,提供了:

  • ZeRO优化器:消除数据并行中的冗余,大幅降低显存占用
  • 流水线调度:自动化的流水线并行
  • 混合精度支持:便捷的AMP集成
  • 检查点和恢复:高效的持久化机制

Megatron-DeepSpeed:

结合Megatron-LM的张量并行和DeepSpeed的ZeRO优化,支持万亿参数模型的训练。

通信模式分析

分布式训练的性能很大程度上取决于通信效率。以下是主要的通信原语:

AllReduce:所有设备贡献数据,归约后所有设备获得结果。用于梯度同步。

AllGather:所有设备贡献数据,所有设备收集全部数据。用于张量并行中拼接结果。

ReduceScatter:所有设备贡献数据,归约后分散到各设备。常用于AllReduce的实现。

Broadcast:一个设备发送数据到所有其他设备。用于参数初始化。

Send/Recv:点对点通信。用于流水线并行中传递中间结果。

通信开销模型:

通信时间可以近似为:

$$T_{comm} = \alpha + \beta \times S$$

其中$\alpha$是延迟(latency),$\beta$是每字节传输时间的倒数(带宽的倒数),$S$是数据大小。

对于大规模训练,带宽项$\beta \times S$通常占主导,因此优化重点是减少传输数据量和提高带宽利用率。


三、程序员的视角

3.1 在实际项目中的应用

选择合适的并行策略:根据模型大小和设备数量选择并行策略。小模型用纯数据并行;中等模型加流水线并行;超大模型加张量并行。

调试分布式代码:分布式训练的调试比单机困难。常用技巧包括:

  • 在每个进程中打印rank信息
  • 使用torch.distributed.barrier()同步调试点
  • 检查各进程的梯度是否一致

性能调优:监控通信时间和计算时间的比例。如果通信占比过高,考虑:

  • 增大批次大小
  • 使用梯度压缩
  • 调整并行策略

3.2 常见陷阱与误区

忘记设置sampler:在DDP中,需要使用DistributedSampler确保每个进程看到不同的数据。否则会导致数据重复。

进程组初始化错误:确保所有进程正确初始化进程组,并且rank和world_size设置正确。

死锁:不一致的集体调用顺序可能导致死锁。确保所有进程以相同顺序执行通信操作。

梯度不同步:某些条件下(如动态图、条件执行),不同进程的梯度可能不同步。需要使用find_unused_parameters=True或确保一致的图结构。

3.3 与其他层级的联系

分布式训练依赖于底层通信库(如NCCL)的高效实现,同时也影响上层框架的设计。编译器需要感知分布式策略,生成正确的通信代码。


四、代码实战

示例1:PyTorch DDP完整示例

python
import os
import torch
import torch.nn as nn
import torch.distributed as dist
from torch.utils.data import DataLoader, DistributedSampler
from torch.nn.parallel import DistributedDataParallel as DDP

def setup_ddp(rank, world_size):
    """初始化分布式环境"""
    os.environ['MASTER_ADDR'] = 'localhost'
    os.environ['MASTER_PORT'] = '12355'
    
    dist.init_process_group("nccl", rank=rank, world_size=world_size)
    torch.cuda.set_device(rank)

def cleanup_ddp():
    """清理分布式环境"""
    dist.destroy_process_group()

class SimpleModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.network = nn.Sequential(
            nn.Linear(784, 256),
            nn.ReLU(),
            nn.Linear(256, 128),
            nn.ReLU(),
            nn.Linear(128, 10)
        )
    
    def forward(self, x):
        return self.network(x)

def train_ddp(rank, world_size):
    """DDP训练主函数"""
    setup_ddp(rank, world_size)
    
    # 创建模型
    model = SimpleModel().to(rank)
    ddp_model = DDP(model, device_ids=[rank])
    
    # 创建数据和分布式采样器
    from torchvision import datasets, transforms
    
    transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
    ])
    
    dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
    sampler = DistributedSampler(dataset, num_replicas=world_size, rank=rank)
    dataloader = DataLoader(dataset, batch_size=64, sampler=sampler)
    
    # 训练
    optimizer = torch.optim.Adam(ddp_model.parameters(), lr=0.001)
    criterion = nn.CrossEntropyLoss()
    
    for epoch in range(3):
        ddp_model.train()
        total_loss = 0
        
        for batch_idx, (data, target) in enumerate(dataloader):
            data, target = data.to(rank).view(-1, 784), target.to(rank)
            
            optimizer.zero_grad()
            output = ddp_model(data)
            loss = criterion(output, target)
            loss.backward()
            optimizer.step()
            
            total_loss += loss.item()
        
        if rank == 0:  # 只在主进程打印
            avg_loss = total_loss / len(dataloader)
            print(f"Epoch {epoch+1}, Loss: {avg_loss:.4f}")
    
    cleanup_ddp()

def spawn_ddp():
    """启动多进程DDP训练"""
    world_size = torch.cuda.device_count()
    if world_size < 2:
        print("Need at least 2 GPUs for DDP")
        return
    
    torch.multiprocessing.spawn(
        train_ddp,
        args=(world_size,),
        nprocs=world_size,
        join=True
    )

if __name__ == "__main__":
    spawn_ddp()

示例2:简单的流水线并行实现

python
import torch
import torch.nn as nn

class PipelineStage(nn.Module):
    """流水线的一个阶段"""
    
    def __init__(self, input_size, output_size, num_layers=5):
        super().__init__()
        self.layers = nn.ModuleList([
            nn.Linear(input_size if i == 0 else output_size, output_size)
            for i in range(num_layers)
        ])
        self.relu = nn.ReLU()
    
    def forward(self, x):
        for layer in self.layers:
            x = layer(x)
            x = self.relu(x)
        return x

class SimplePipelineParallel:
    """
    简单的流水线并行实现
    
    注意:这是一个教学示例,实际生产应使用torch.distributed.pipelining
    """
    
    def __init__(self, num_stages, input_size, hidden_size, output_size, devices):
        self.num_stages = num_stages
        self.devices = devices
        
        # 创建流水线阶段
        self.stages = nn.ModuleList()
        
        # 第一阶段
        self.stages.append(PipelineStage(input_size, hidden_size).to(devices[0]))
        
        # 中间阶段
        for _ in range(num_stages - 2):
            self.stages.append(PipelineStage(hidden_size, hidden_size).to(devices[_]))
        
        # 最后阶段
        self.stages.append(PipelineStage(hidden_size, output_size).to(devices[-1]))
    
    def forward_pipeline(self, x, micro_batches=4):
        """
        使用微批次的流水线前向传播
        
        Args:
            x: 输入数据
            micro_batches: 微批次数量
        """
        batch_size = x.size(0)
        micro_batch_size = batch_size // micro_batches
        
        outputs = []
        
        for i in range(micro_batches):
            start = i * micro_batch_size
            end = start + micro_batch_size
            micro_x = x[start:end]
            
            # 通过流水线
            current = micro_x
            for stage_idx, stage in enumerate(self.stages):
                device = self.devices[stage_idx]
                current = current.to(device)
                current = stage(current)
            
            outputs.append(current)
        
        return torch.cat(outputs, dim=0)

def demo_pipeline():
    """演示流水线并行"""
    if torch.cuda.device_count() < 2:
        print("Need at least 2 GPUs")
        return
    
    devices = [torch.device(f'cuda:{i}') for i in range(2)]
    
    pipeline = SimplePipelineParallel(
        num_stages=2,
        input_size=784,
        hidden_size=256,
        output_size=10,
        devices=devices
    )
    
    x = torch.randn(32, 784)
    output = pipeline.forward_pipeline(x, micro_batches=4)
    
    print(f"Output shape: {output.shape}")


demo_pipeline()

示例3:通信性能基准测试

python
import torch
import torch.distributed as dist
import time
import os

def benchmark_allreduce(rank, world_size):
    """基准测试AllReduce性能"""
    os.environ['MASTER_ADDR'] = 'localhost'
    os.environ['MASTER_PORT'] = '12356'
    
    dist.init_process_group("nccl", rank=rank, world_size=world_size)
    
    sizes = [1024, 1024*1024, 10*1024*1024, 100*1024*1024]  # 1K, 1M, 10M, 100M elements
    
    for size in sizes:
        tensor = torch.randn(size, device=f'cuda:{rank}')
        
        # 预热
        for _ in range(5):
            dist.all_reduce(tensor)
        
        torch.cuda.synchronize()
        start = time.time()
        
        iterations = 100
        for _ in range(iterations):
            dist.all_reduce(tensor)
        
        torch.cuda.synchronize()
        elapsed = (time.time() - start) / iterations * 1000  # ms
        
        if rank == 0:
            data_size_mb = size * 4 / 1024 / 1024  # FP32 = 4 bytes
            bandwidth_gbps = data_size_mb / elapsed * 1000 * 8 / 1024
            print(f"Size: {size:>12} elements ({data_size_mb:.1f} MB) | "
                  f"Time: {elapsed:.3f} ms | "
                  f"Bandwidth: {bandwidth_gbps:.2f} Gbps")
    
    dist.destroy_process_group()

def run_benchmark():
    """运行通信基准测试"""
    world_size = torch.cuda.device_count()
    if world_size < 2:
        print("Need at least 2 GPUs")
        return
    
    print(f"\n{'='*70}")
    print(f"AllReduce Benchmark ({world_size} GPUs)")
    print(f"{'='*70}")
    
    torch.multiprocessing.spawn(
        benchmark_allreduce,
        args=(world_size,),
        nprocs=world_size,
        join=True
    )

if __name__ == "__main__":
    run_benchmark()

五、深入思考

5.1 历史背景

分布式深度学习训练的发展经历了多个阶段。早期的Caffe支持基于Parameter Server的数据并行,但扩展性有限。2016年,Google发布了DistBelief和随后的TensorFlow,推动了大规模分布式训练的研究。

Ring AllReduce算法在2017年左右成为主流,被Baidu的AllReduce框架和后来的NCCL采用。PyTorch的DDP在1.0版本后逐渐成熟,成为研究社区的标准选择。

2020年以来,随着大语言模型的兴起,混合并行策略成为研究热点。Megatron-LM、DeepSpeed、ColossalAI等框架不断推动分布式训练的规模和效率边界。

5.2 前沿方向

3D并行:结合数据并行、流水线并行和张量并行的统一框架,自动化选择最优并行策略。

通信优化:梯度压缩、稀疏更新、量化通信等技术减少通信开销。

弹性训练:支持动态添加/移除设备,适应云环境的资源变化。

绿色训练:优化能源效率,减少碳足迹。

5.3 延伸阅读

  • "Efficient Large-Scale Language Model Training on GPU Clusters Using Megatron-LM" - NVIDIA论文
  • DeepSpeed官方文档 - ZeRO和分布式训练的详细说明
  • PyTorch分布式教程 - DDP和RPC的使用指南
  • "GPipe: Efficient Training of Giant Neural Networks using Pipeline Parallelism" - Google论文
  • NCCL文档 - 通信原语的底层实现

六、实验笔记

实验1:比较DataParallel和DDP的性能

目标:量化DDP相比DataParallel的性能优势

步骤:

使用相同的模型和数据集

分别用DataParallel和DDP训练

测量每轮训练时间和显存占用

扩展到不同GPU数量

预期结果:DDP应该有更好的扩展性,特别是在多节点场景

分析要点:

  • 性能差距随GPU数量如何变化?
  • 通信开销的差异来自哪里?
  • 是否存在DDP不适用的场景?

实验2:测试不同并行策略的扩展效率

目标:评估纯数据并行、数据+流水线、数据+张量并行的扩展性

步骤:

选择一个中等规模的Transformer模型

分别用三种策略在8-64 GPU上训练

测量吞吐量和扩展效率

分析通信和计算的时间占比

预期结果:混合并行在大规模下表现更好,但小规模下纯数据并行更简单高效

分析要点:

  • 各种策略的拐点在哪里?
  • 通信瓶颈何时出现?
  • 最优的并行组合是什么?

七、本章小结

本章深入探讨了分布式训练的核心概念和技术,从基本的数据并行到复杂的混合并行策略。我们分析了各种并行方式的原理、优缺点和适用场景,并通过代码实战掌握了DDP的使用和性能测试方法。

分布式训练是现代深度学习不可或缺的技术,它使得训练超大规模模型成为可能。理解分布式系统的原理和优化方法,使我们能够设计高效的训练方案,充分利用计算资源。


八、习题

推导Ring AllReduce的通信复杂度。为什么它的带宽利用率优于Parameter Server架构?

解释流水线气泡的产生原因。如果有$P$个设备和$M$个微批次,气泡比例是多少?如何最小化?

比较张量并行和流水线并行的适用场景。在什么情况下应该选择其中一种而非另一种?

DDP为什么比DataParallel性能更好?列举至少三个原因。

在一个512 GPU的集群上训练万亿参数模型,设计一个混合并行策略。说明每种并行度的选择理由。

解释ZeRO优化器如何消除数据并行中的冗余。ZeRO-3与标准数据并行相比,通信开销有何变化?

分布式训练中如何处理故障恢复?设计一个高效的检查点策略。

阅读Megatron-LM论文,解释其张量并行如何减少通信次数。关键的设计洞察是什么?

估算AllReduce操作的理论带宽上限。在实际系统中,哪些因素会导致达不到理论值?

设计一个实验来诊断分布式训练中的通信瓶颈。你需要测量哪些指标?如何定位问题?

7/12分布式训
0%