理论+实践,带你了解分布式训练

AI摘要

本文介绍大模型分布式训练,因单设备资源不足,需分布式训练系统解决海量计算和内存需求。阐述分布式训练概念、目标及面临的计算墙、显存墙、通信墙挑战,还介绍数据并行、模型并行、混合并行等并行策略,以及计算设备内存优化方法。

​​本文分享自华为云社区《大模型LLM之分布式训练》,作者: 码上开花_Lancer。

随着语言模型参数量和所需训练数据量的急速增长,单个机器上有限的资源已无法满足大语言模型训练的要求。需要设计分布式训练(Distributed Training)系统来解决海量的计算和内存资源要求问题。在分布式训练系统环境下需要将一个模型训练任务拆分成多个子任务,并将子任务分发给多个计算设备,从而解决资源瓶颈。但是如何才能利用包括数万计算加速芯片的集群,训练模型参数量千亿甚至是万亿的大规模语言模型?这其中涉及到集群架构、并行策略、模型架构、内存优化、计算优化等一系列的技术。

    我将详细介绍分布式机器学习系统的基础概念、分布式训练集群架构、分布式训练并行策略,并以DeepSpeed 为例介绍如何在集群上训练大语言模型。

一、分布式训练概述

    分布式训练(Distributed Training)是指将机器学习或深度学习模型训练任务分解成多个子任务,并在多个计算设备上并行地进行训练。图1给出了单个计算设备和多个计算设备的示例,这里计算设备可以是中央处理器(Central Processing Unit,CPU)、图形处理器(Graphics Processing Unit,GPU)、张量处理器(Tensor Processing Unit,TPU)也可以是神经网络处理器(Neural network Processing Unit,NPU)。由于同一个服务器内部的多个计算设备之间内存也可能并不共享,因此无论这些计算设备是否处于一个服务器还是多个服务器中,其系统架构都属于分布式系统范畴。一个模型训练任务往往会有大量的训练样本作为输入,可以利用一个计算设备完成,也可以将整个模型的训练任务拆分成子任务,分发给不同的计算设备,实现并行计算。此后,还需要对每个计算设备的输出进行合并,最终得到与单个计算设备等价的计算结果。由于每个计算设备只需要负责子任务,并且多个计算设备可以并行执行,因此其可以更快速地完成整体计算,并最终实现对整个计算过程的加速。

图1 单计算设备计算和多计算设备示例

    促使人们设计分布式训练系统的一个最重要的原因就是单个计算设备的算力已经不足以支撑模型训练。图2给出了机器学习模型对于算力的需求以及同期单个计算设备能够提供的算力。如图所示,机器学习模型快速发展,从2013 年AlexNet 开始,到2022 年拥有5400 亿参数的PalM 模型,机器学习模型以每18 个月增长56 倍的速度发展。模型参数规模增大的同时,对训练数据量的要求也指数级增长,这更加剧了对算力的需求。然而,近几年CPU 的算力增加已经远低于摩尔定律(Moore’s Law),虽然计算加速设备(如GPU、TPU 等)为机器学习模型提供了大量的算力,但是其增长速度仍然没有突破每18 个月翻倍的摩尔定律。为了能够满足机器学习模型的发展,只
有通过分布式训练系统才可以匹配模型不断增长的算力需求。

图2 机器学习模型参数量增长和计算硬件的算力增长对比

分布式训练的总体目标就是提升总的训练速度,减少模型训练的总体时间。总训练速度可以用如下公式简略估计:

总训练速度∝ 单设备计算速度× 计算设备总量× 多设备加速比

    其中,单设备计算速度主要由单块计算加速芯片的运算速度和数据I/O 能力来决定,对单设备训练效率进行优化,主要的技术手段有混合精度训练、算子融合、梯度累加等;分布式训练系统中计算设备数量越多,其理论峰值计算速度就会越高,但是受到通讯效率的影响,计算设备数量增大则会造成加速比急速降低;多设备加速比则是由计算和通讯效率决定,需要结合算法和网络拓扑结构进行优化,分布式训练并行策略主要目标就是提升分布式训练系统中的多设备加速比。

    大语言模型参数量和所使用的数据量都非常巨大,因此都采用了分布式训练架构完成训练。文献[5] 针对GPT-3 的训练过程仅介绍了训练过程全部使用NVIDIA V100 GPU,文献[31] 介绍了OPT 使用了992 块NVIDIA A100 80G GPU,采用全分片数据并行(Fully Shared Data Parallel)[129]以及Megatron-LM 张量并行(Tensor Parallelism)[130],整体训练时间将近2 个月。BLOOM[33] 模型的研究人员则公开了更多在硬件和所采用的系统架构方面的细节。该模型的训练一共花费3.5 个月,使用48 个计算节点。每个节点包含8 块NVIDIA A100 80G GPU(总计384 个GPU),并且使用4*NVLink 用于节点内部GPU 之间通信。节点之间采用四个Omni-Path 100 Gbps 网卡构建的增强8 维超立方体全局拓扑网络进行通信。文献[37] 并没有给出LLaMA 模型训练中所使用的集群的具体配置和网络拓扑结构,但是给出了不同参数规模的总GPU 小时数。LLaMA 模型训练采用A100-80GB GPU,LLaMA-7B 模型训练需要82432 GPU 小时,LLaMA-13B 模型训练需要135168GPU 小时,LLaMA-33B 模型训练花费了530432 GPU 小时,而LLaMA-65B 模型训练花费则高达1022362 GPU 小时。由于LLaMA 所使用的训练数据量远超OPT 和BLOOM 模型,因此,虽然模型参数量远小于上述两个模型,但是其所需计算量仍然非常惊人。

    通过使用分布式训练系统,大语言模型训练周期可以从单计算设备花费几十年,缩短到使用数千个计算设备花费几十天就可以完成。然而,分布式训练系统仍然需要克服计算墙、显存墙、通
信墙等多种挑战,以确保集群内的所有资源得到充分利用,从而加速训练过程并缩短训练周期。


• 计算墙:单个计算设备所能提供的计算能力与大语言模型所需的总计算量之间存在巨大差异。2022 年3 年发布的NVIDIA H100 SXM 的单卡FP16 算力也只有2000 TFLOPs,而GPT-3
  则需要314 ZFLOPs 的总算力,两者相差了8 个数量级。

• 显存墙:单个计算设备无法完整存储一个大语言模型的参数。GPT-3 包含1750 亿参数,如果采用FP16 格式进行存储,需要700GB 的计算设备内存空间,而NVIDIA H100 GPU 只有
  80 GB 显存。
• 通信墙:分布式训练系统中各计算设备之间需要频繁地进行参数传输和同步。由于通信的延迟和带宽限制,这可能成为训练过程的瓶颈。GPT-3 训练过程中,如果分布式系统中存在128
个模型副本,那么在每次迭代过程中至少需要传输89.6TB 的梯度数据。而截止2023 年8 月,单个InfiniBand 链路仅能够提供不超过800Gb/s 带宽。计算墙和显存墙源于单计算设备的计算和存储能力有限,与模型对庞大计算和存储需求之间存在矛盾。这个问题可以通过采用分布式训练方法来解决,但分布式训练又会面临通信墙的挑战。在多机多卡的训练中,这些问题逐渐显现。随着大模型参数的增大,对应的集群规模也随之增加,这些问题变得更加突出。同时,在大型集群进行长时间训练时,设备故障可能会影响或中断训练过程,对分布式系统的问题性也提出了很高要求。

二、分布式训练并行策略

    分布式训练系统目标就是将单节点模型训练转换成等价的分布式并行模型训练。对于大语言模型来说,训练过程就是根据数据和损失函数,利用优化算法对神经网络模型参数进行更新的过程。单节点模型训练系统结构如图3所示,主要由数据和模型两个部分组成。训练过程会由多个数据小批次(Mini-batch)完成。图中数据表示一个数据小批次。训练系统会利用数据小批次根据损失函数和优化算法生成梯度,从而对模型参数进行修正。针对大语言模型多层神经网络的执行过程,可以由一个计算图(Computational Graph)表示。这个图有多个相互连接的算子(Operator),每个算子实现一个神经网络层(Neural Network Layer),而参数则代表了这个层在训练中所更新的权重。

图3 单设备模型训练系统

    计算图的执行过程可以分为前向计算和反向计算两个阶段。前向计算的过程是将数据读入第一个算子,计算出相应的输出结构,然后依此重复这个前向计算过程,直到最后一个算子结束。反向计算过程,是根据优化函数和损失,每个算子依次计算出梯度,并利用梯度更新本地的参数。在反向计算结束后,该数据小批次的计算完成,系统就会读取下一个数据小批次,继续下一轮的模型参数更新。

根据单设备模型训练系统的流程,可以看到如果进行并行加速,可以从数据和模型两个维度进行考虑。首先可以对数据进行切分(Partition),并将同一个模型复制到多个设备上,并行执行不同的数据分片,这种方式通常被称为数据并行(Data Parallelism,DP)。还可以对模型进行划分,将模型中的算子分发到多个设备分别完成,这种方式通常被称为模型并行(Model Parallelism,MP)。当训练超大规模语言模型时,往往需要同时对数据和模型进行切分,从而实现更高程度的并行,这种方式通常被称为混合并行(Hybrid Parallelism,HP)。

2.1、数据并行

    在数据并行系统中,每个计算设备都有整个神经网络模型的完整副本(Model Replica),进行迭代时,每个计算设备只分配了一个批次数据样本的子集,并根据该批次样本子集的数据进行网络模型的前向计算。假设一个批次的训练样本数为N,使用M 个计算设备并行计算,每个计算设备会分配到N/M 个样本。前向计算完成后,每个计算设备都会根据本地样本计算损失误差得到梯度Gi(i 为加速卡编号),并将本地梯度Gi 进行广播。所有计算设备需要聚合其他加速度卡给出的梯度值,然后使用平均梯度(ΣNi=1Gi)/N 对模型进行更新,完成该批次训练。图4给出了由两个计算设备组成的数据并行训练系统样例。

图4 两节点数据并行训练系统样例

    数据并行训练系统可以通过增加计算设备,有效提升整体训练吞吐量,每秒全局批次数(Global Batch Size Per Second) 。它和单计算设备训练相比,最主要的区别就在于反向计算中的梯度需要在所有计算设备中进行同步,以保证每个计算设备上最终得到的是所有进程上梯度的平均值。常见的神经网络框架中都有数据并行方式的具体实现,包括:TensorFlow DistributedStrategy、PyTorch Distributed、Horovod DistributedOptimizer 等。由于基于Transformer 架构的大语言模型中每个算子都是依赖单个数据而非批次数据,因此数据并行并不会影响其计算逻辑,一般情况下各训练设备中前向计算是独立的,不涉及同步问题。数据并行训练加速比最高,但要求每个设备上都备份一份模型,显存占用比较高。

使用PyTorch DistributedDataParallel 实现单个服务器多加速卡训练代码如下,首先构造DistributedSampler类,将数据集的样本随机打乱并分配到不同计算设备:

class DistributedSampler(Sampler):
  def __init__(self, dataset, num_replicas=None, rank=None, shuffle=True, seed=0):
    if num_replicas is None:
        if not dist.is_available():
            raise RuntimeError("Requires distributed package to be available")
        num_replicas = dist.get_world_size()
    if rank is None:
        if not dist.is_available():
            raise RuntimeError("Requires distributed package to be available")
        rank = dist.get_rank()
    self.dataset = dataset # 数据集
    self.num_replicas = num_replicas # 进程个数默认等于world_size(GPU 个数)
    self.rank = rank # 当前属于哪个进程/哪块GPU
    self.epoch = 0
    self.num_samples = int(math.ceil(len(self.dataset) * 1.0 / self.num_replicas))
    # 每个进程的样本个数
    self.total_size = self.num_samples * self.num_replicas # 数据集总样本的个数
    self.shuffle = shuffle # 是否要打乱数据集
    self.seed = seed

def __iter__(self):
# 1、Shuffle 处理:打乱数据集顺序
    if self.shuffle:
        # 根据epoch 和种子进行混淆
        g = torch.Generator()
        # 这里self.seed 是一个定值,通过set_epoch 改变self.epoch 可以改变我们的初始化种子
        # 这就可以让每一个epoch 中数据集的打乱顺序不同,使每一个epoch 中,
        # 每一块GPU 拿到的数据都不一样,这样可以有利于更好的训练
        g.manual_seed(self.seed + self.epoch)
        indices = torch.randperm(len(self.dataset), generator=g).tolist()
    else:
        indices = list(range(len(self.dataset)))
    # 数据补充
    indices += indices[:(self.total_size - len(indices))]
    assert len(indices) == self.total_size
    # 分配数据
    indices = indices[self.rank:self.total_size:self.num_replicas]
    assert len(indices) == self.num_samples
    return iter(indices)
def __len__(self):
    return self.num_samples
def set_epoch(self, epoch):

    self.epoch = epoch

利用DistributedSampler 构造完整的训练程序样例main.py 如下:

import argparse
import os
import shutil
import time
import warnings
import numpy as np
warnings.filterwarnings('ignore')
import torch
import torch.nn as nn
import torch.nn.parallel
import torch.backends.cudnn as cudnn
import torch.distributed as dist
import torch.optim
import torch.utils.data
import torch.utils.data.distributed
from torch.utils.data.distributed import DistributedSampler
from models import DeepLab
from dataset import Cityscaples
parser = argparse.ArgumentParser(description='DeepLab')
parser.add_argument('-j', '--workers', default=4, type=int, metavar='N',
help='number of data loading workers (default: 4)')
parser.add_argument('--epochs', default=100, type=int, metavar='N',
help='number of total epochs to run')
parser.add_argument('--start-epoch', default=0, type=int, metavar='N',
help='manual epoch number (useful on restarts)')
parser.add_argument('-b', '--batch-size', default=3, type=int,
metavar='N')
parser.add_argument('--local_rank', default=0, type=int, help='node rank for distributed training')
args = parser.parse_args()
torch.distributed.init_process_group(backend="nccl") # 初始化
print("Use GPU: {} for training".format(args.local_rank))
# create model
model = DeepLab()
torch.cuda.set_device(args.local_rank) # 当前显卡
model = model.cuda() # 模型放在显卡上
model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.local_rank],
    output_device=args.local_rank, find_unused_parameters=True) # 数据并行
criterion = nn.CrossEntropyLoss().cuda()
optimizer = torch.optim.SGD(model.parameters(), args.lr,
    momentum=args.momentum, weight_decay=args.weight_decay)
train_dataset = Cityscaples()
train_sampler = DistributedSampler(train_dataset) # 分配数据
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=args.batch_size,
    shuffle=False, num_workers=args.workers, pin_memory=True, sampler=train_sampler)

通过以下命令行启动上述程序:

CUDA_VISIBLE_DEVICES=0,1 python -m torch.distributed.launch --nproc_per_node=2 main.py

2.2 模型并行

    模型并行(Model Parallelism)往往用于解决单节点内存不足的问题。以包含1750 亿参数的GPT-3 模型为例,如果模型中每一个参数都使用32 位浮点数表示,那么模型需要占用700GB(即175G× 4 Bytes)内存,如果使用16 位浮点表示,每个模型副本需要也需要占用350GB 内存。以2022 年3 月NVIDIA 发布的H100 加速卡也仅支持80GB 显存,无法将整个模型完整放入其中。模型并行可以从计算图角度,以下两种形式进行切分:(1)按模型的层切分到不同设备,即层间并行或算子间并行(Inter-operator Parallelism),也称之为流水线并行(Pipeline Parallelism,PP);(2)将计算图层内的参数切分到不同设备,即层内并行或算子内并行(Intra-operator Parallelism),也称之为张量并行(Tensor Parallelism,TP)。两节点模型并行训练系统样例如图4.9所示,左边为流水线并行,模型的不同层被切分到不同的设备中;右边为张量并行,同一个层中的不同的参数被切分到不同的设备中进行计算。

1. 流水线并行

流水线并行(Pipeline Parallelism,PP)是一种并行计算策略,将模型的各个层分段处理,并将每个段分布在不同的计算设备上,使得前后阶段能够流水式、分批进行工作。流水线并行通常应用于大规模模型的并行系统中,以有效解决单个计算设备内存不足的问题。图4.6给出了一个由四个计算设备组成的流水线并行系统,包含了前向计算和后向计算。其中F1、F2、F3、F4 分别代表四个前向路径,位于不同的设备上;而B4、B3、B2、B1 则代表逆序的后向路径,也分别位于四个不同的设备上。然而,从图中可以看出,计算图中的下游设备(Downstream Device)需要长时间持续处于空闲状态,等待上游设备(Upstream Device)的计算完成,才能开始计算自身的任务。

图5 两节点模型并行训练系统样例

这种情况导致了设备的平均使用率大幅降低,形成了模型并行气泡(Model Parallelism Bubble),也称为流水线气泡(Pipeline Bubble)。

图6 流水线并行样例

    朴素流水线策略所产生的并行气泡,使得系统无法充分利用计算资源,降低了系统整体的计算效率。为了能够减少并行气泡,文献[131] 提出了GPipe 方法,将小批次(Mini-batch)进一步划分成更小的微批次(Micro-batch),利用流水线并行方案,每次处理一个微批次的数据。在当前阶段计算完成得到结果后,将该微批次的结果发送给下游设备,同时开始处理后一个微批次的数据,这样可以在一定程度上减少并行气泡。图7GPipe 策略流水线并行样例。如图所示,前向F1计算被拆解为了F11,F12,F13,F14,在计算设备1 中计算完成F11 后,会在计算设备2 中开始进行F21 计算,同时计算设备1 中并行开始F12 的计算。相比于最原始的流水线并行方法,GPipe 流水线方法可以有效降低并行气泡。

图7 GPipe 策略流水线并行样例

    GPipe 策略虽然可以减少一定的并行气泡,但是只有当一个Mini-batch 中所有的前向计算完成后,才能开始执行后向计算。因此还是会产生很多并行气泡,从而降低了系统的并行效率。Megatron-
LM[132] 提出了1F1B 流水线策略,即一个前向通道和一个后向通道。1F1B 流水线策略引入了任务调度机制,使得下游设备能够在等待上游计算的同时执行其他可并行的任务,从而提高设备的利
用率。1F1B 给出了非交错式和交错式两种方式调度方式,如图8所示。

    1F1B 非交错式调度模式可分为三个阶段。首先是热身阶段,在该阶段中,计算设备中进行不同数量的前向计算。接下来的阶段是前向-后向阶段,计算设备按顺序执行一次前向计算,然后进行一次后向计算。最后一个阶段是后向阶段,计算设备在完成最后一次后向计算。相比于GPipe 策略,非交错式调度模式在节省内存方面表现更好。然而,它需要与GPipe 策略一样的时间来完成一轮计算。

    1F1B 交错式调度模式要求micro-batch 的数量是流水线阶段的整数倍。每个设备不再仅负责连续多个层的计算,而是可以处理多个层的子集,这些子集被称为模型块。具体而言,在之前的模式中,设备1 可能负责层1-4,设备2 负责层5-8,以此类推。然而,在新的模式下,设备1 可以处理层1、2、9、10,设备2 处理层3、4、11、12,以此类推。这种模式下,每个设备在流水线中被分配到多个阶段。例如,设备1 可能参与热身阶段、前向计算阶段和后向计算阶段的某些子集任务。每个设备可以并行执行不同阶段的计算任务,从而更好地利用流水线并行的优势。这种模式不仅在内存消耗方面表现出色,还能够提高计算效率,使得大型模型的并行系统能够更高效地
完成计算任务。


图8 1F1B 流水线并行策略样例

PyTorch 中也包含了实现流水线的API 函数Pipe,具体实现参考“torch.distributed.pipeline.sync.Pipe”类。可以使用这个API 构造一个包含两个线性层,分别放置在2 个不同计算设备中的样例如下:
 

{#
Step 0. Need to initialize RPC framework first.
os.environ['MASTER_ADDR'] = 'localhost'
os.environ['MASTER_PORT'] = '29500'
torch.distributed.rpc.init_rpc('worker', rank=0, world_size=1)
# Step 1: build a model including two linear layers
fc1 = nn.Linear(16, 8).cuda(0)
fc2 = nn.Linear(8, 4).cuda(1)
# Step 2: wrap the two layers with nn.Sequential
model = nn.Sequential(fc1, fc2)
# Step 3: build Pipe (torch.distributed.pipeline.sync.Pipe)
model = Pipe(model, chunks=8)
# do training/inference
input = torch.rand(16, 16).cuda(0)
output_rref = model(input)
}

2. 张量并行

    张量并行(Tensor Parallelism,TP)需要根据模型的具体结构和算子类型,解决如何将参数切分到不同设备,以及如何保证切分后数学一致性两个问题。大语言模型都是以Transformer 结构为基础,Transformer 结构主要由以下三种算子构成:嵌入式表示(Embedding)、矩阵乘(MatMul)和交叉熵损失(Cross Entropy Loss)计算构成。这三种类型的算子有较大的差异,都需要设计对应的张量并行策略[130],才可以实现将参数切分到不同的设备。对于嵌入表示(Embedding)算子,如果总的词表数非常大,会导致单计算设备显存无法容纳Embedding 层参数。举例来说,如果词表数量是64000,嵌入表示维度为5120,类型采用32 位精度浮点数,那么整层参数需要的显存大约为64000 × 5120 × 4/1024/1024 = 1250MB,反向梯度同样需要1250MB,仅仅存储就需要将近2.5GB。对于嵌入表示层的参数,可以按照词维度切分,每个计算设备只存储部分词向量,然后通过汇总各个设备上的部分词向量,从而得到完整的词向量。图4.9给出了单节点Embedding 和两节点张量并行的示意图。在单节点上,执行Embedding 操作,bz 是批次大小(batch size),Embedding 的参数大小为[word_size, hidden_size],计算得到[bz,hidden_size] 张量。图4.9中Embedding 张量并行示例将Embedding 参数沿word_size 维度,切分为两块,每块大小为[word_size/2, hidden_size],分别存储在两个设备上。当每个节点查询各自的词表时,如果无法查到,则该词的表示为0,各自设备查询后得到[bz, hidden_size] 结果张量,最后通过AllReduce_Sum 通信¬,跨设备求和,得到完整的全量结果,可以看出,这里的输出结果和单计算设备执行的结果一致。

图9 两节点Embedding 算子张量并行示例

矩阵乘(MatMul)的张量并行要充分利用矩阵了分块乘法原理。举例来说,要实现如下矩阵乘法Y = X ×A,其中X 是维度为M × N 的输入矩阵,A 是维度为N ×K 的参数矩阵,Y 是结果矩阵,维度为M ×K。如果参数矩阵A 非常大,甚至超出单张卡的显存容量,那么可以把参数矩阵A 切分到多张卡上,并通过集合通信汇集结果,保证最终结果在数学计算上等价于单计算设备计算结果。参数矩阵A 存在两种切分方式:
(1) 参数矩阵A 按列切块,将矩阵A 按列切成:A = [A1,A2]

(2) 参数矩阵A 按行切块,将矩阵A 按行切成:

     

图10给出了参数矩阵按列切分的示例,参数矩阵A 分别将A1,A2 放置在两个计算设备上。两个计算设备分别计算Y1 = X ×A1 和Y2 = X ×A2。计算完成后,多计算设备间进行通信,从而获取其它计算设备上的计算结果,并拼接在一起得到最终的结果矩阵Y ,该结果在数学上与单计算设备计算结果上完全等价。

图10 两节点矩阵乘算子张量并行按列切分示例
 

    图11给出了参数矩阵按列行分的示例,为了满足矩阵乘法规则,输入矩阵X 需要按列切分X = [X1|X2]。同时,将矩阵分块,分别放置在两个计算设备上,每个计算设备分别计算Y1 =X1 ×A1 和Y2 = X2 ×A2。计算完成后,多个计算设备间通信获取归约其他卡上的计算结果,可以得到最终的结果矩阵Y 。同样,这种切分方式,既可以保证数学上的计算等价性,并解决单计算设备显存无法容纳,又可以保证单计算设备通过拆分方式可以装下参数A 的问题。

    Transformer 中的FFN 结构均包含两层全连接(FC)层,即存在两个矩阵乘,这两个矩阵乘分别采用上述两种切分方式,如图4.12所示。对第一个FC 层的参数矩阵按列切块,对第二个FC层参数矩阵按行切块。这样第一个FC 层的输出恰好满足第二个FC 层数据输入要求(按列切分),因此可以省去第一个FC 层后的汇总通信操作。多头自注意力机制的张量并行与FFN 类似,因为具有多个独立的头,因此相较于FFN 更容易实现并行,其矩阵切分方式如图4.13所示。具体可以参考文献[130]。

分类网络最后一层一般会选用Softmax 和Cross_entropy 算子来计算交叉熵损失(Cross Entropy Loss)。如果类别数量非常大,会导致单计算设备内存无法存储和计算logit 矩阵。针对这一类算子,可以按照类别维度切分,同时通过中间结果通信,得到最终的全局的交叉熵损失。首先计算

图11 两节点矩阵乘算子张量并行按行切分示例

图12 FNN 结构张量并行示意图

的是softmax 值,公式如下:

其中,p 表示张量并行的设备号。得到Softmax 计算结果之后,同时对标签Target 按类别切分,每个设备得到部分损失,最后再进行一次通信,得到所有类别的损失。整个过程,只需要进行三次小量的通信,就可以完成交叉熵损失的计算。PyTorch 提供了细粒度张量级别的并行API,DistributedTensor。也提供了粗粒度模型层面的API 对“nn.Module”进行张量并行。通过以下几行代码就可以实现对一个大的张量进行分片:

import torch
from torch.distributed._tensor import DTensor, DeviceMesh, Shard, distribute_tensor
# construct a device mesh with available devices (multi-host or single host)
device_mesh = DeviceMesh("cuda", [0, 1, 2, 3])
# if we want to do row-wise sharding
rowwise_placement=[Shard(0)]
# if we want to do col-wise sharding
colwise_placement=[Shard(1)]
big_tensor = torch.randn(888, 12)
# distributed tensor returned will be sharded across the dimension specified in placements
rowwise_tensor = distribute_tensor(big_tensor, device_mesh=device_mesh, placements=rowwise_placement)

对于像“nn.Linear”这样已经有“torch.Tensor”作为参数的模块,也提供了模块级API “distribute_module”在模型层面进行张量并行,参考代码如下:

import torch
from torch.distributed._tensor import DeviceMesh, Shard, distribute_tensor,distribute_module
class MyModule(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(8, 8)
        self.fc2 = nn.Linear(8, 8)
        self.relu = nn.ReLU()
        def forward(self, input):
            return self.relu(self.fc1(input) + self.fc2(input))
    mesh = DeviceMesh(device_type="cuda", mesh=[[0, 1], [2, 3]])
    def shard_params(mod_name, mod, mesh):
        rowwise_placement = [Shard(0)]
        def to_dist_tensor(t): return distribute_tensor(t, mesh, rowwise_placement)
        mod._apply(to_dist_tensor)
    sharded_module = distribute_module(MyModule(), mesh, partition_fn=shard_params)
    def shard_fc(mod_name, mod, mesh):
        rowwise_placement = [Shard(0)]
        if mod_name == "fc1":
            mod.weight = torch.nn.Parameter(distribute_tensor(mod.weight, mesh, rowwise_placement))
    sharded_module = distribute_module(MyModule(), mesh, partition_fn=shard_fc)

2.3.混合并行

混合并行(Hybrid Parallelism,HP)是将多种并行策略如数据并行、流水线并行和张量并行等进行混合使用。通过结合不同的并行策略,混合并行可以充分发挥各种并行策略的优点,以最大程度地提高计算性能和效率。针对千亿规模的大语言模型,通常在每个服务器内部使用张量并行策略,由于该策略涉及的网络通信量较大,需要利用服务器内部的不同计算设备之间进行高速通信带宽。通过流水线并行,将模型的不同层划分为多个阶段,每个阶段由不同的机器负责计算。这样可以充分利用多台机器的计算能力,并通过机器之间的高速通信来传递计算结果和中间数据,以提高整体的计算速度和效率。最后,在外层叠加数据并行策略,以增加并发数量,提升整体训练速度。通过数据并行,将训练数据分发到多组服务器上进行并行处理,每组服务器处理不同的数据批次。这样可以充分利用多台服务器的计算资源,并增加训练的并发度,从而加快整体训练速度。BLOOM 使用了Megatron-DeepSpeed[104] 框架进行训练,主要包含两个部分:Megatron-LM 提供张量并行能力和数据加载原语;DeepSpeed提供ZeRO 优化器、模型流水线以及常规的分布式训练组件。通过这种方式可以实现数据、张量和流水线三维并行,BLOOM 模型训练时采用的并行计算结构如图14所示。BLOOM 模型训练使用了由48 个NVIDIA DGX-A100 服务器组成的集群,每个DGX-A100 服务器包含8 张NVIDIA A100 80GB GPU,总计包含384 张。BLOOM 训练采用的策略是首先将集群分为48 个一组,进行数据并行。接下来,模型整体被分为12 个阶段,进行流水线并行。每个阶段的模型被划分到4 张GPU 中,进行张量并行。同时BLOOM 也使用了ZeRO(零冗余优化器)[134] 进一步降低了模型对显存的占用。用了通过上述四个步骤可以实现数百个GPU 的高效并行计算。

图14 BLOOM 模型训练时采用的并行计算结构

2.4. 计算设备内存优化

当前大语言模型训练通常采用Adam 优化算法,除了需要每个参数梯度之外,还需要一阶动量(Momentum)和二阶动量(Variance)。虽然Adam 优化算法相较SGD 算法通常效果更好也更稳定,但是对计算设备内存的占用显著增大。为了降低内存占用,大多数系统已经采用了混合精度训练(Mixed Precision Training)方式,即同时存在FP16(16 位浮点数)或者BF16(Bfloat16)和FP32(32 位浮点数)两种格式的数值。FP32、FP16 和BF16 表示如图4.15所示。FP32 中第31 位为符号位,第30 到第23 位用于表示指数,第22 到第0 位用于表示尾数。FP16 中第15 位为符号位,第14 到第10 位用于表示指数,第9 到第用于表示尾数。BF16 中第15 位为符号位,第14 到第7 位用于表示指数,第6 到第0 位用于表示尾数。由于FP16 的值区间比FP32 的值区间小很多,所以在计算过程中很容易出现上溢出和下溢出。BF16 相较于FP16 以精度换取更大的值区间范围。但是,由于FP16 和BF16 相较FP32 精度低,训练过程中可能会出现梯度消失和模型不稳定的问题。
    因此,需要使用一些技术来解决这些问题,例如动态损失缩放(Dynamic Loss Scaling)和混合精度优化器(Mixed Precision Optimizer)等。混合精度优化的过程如图4.16所示。Adam 优化器状态包括采用FP32 保存的模型参数备份,一阶动量和二阶动量也都采用FP32 格式存储。假设模型参数量为Φ,模型参数和梯度都是用FP16格式存储,则共需要2Φ + 2Φ + (4Φ + 4Φ + 4Φ) = 16Φ 字节存储。其中Adam 状态占比75%。动态损失缩放反向传播前,将损失变化(dLoss)手动增大2K 倍,因此反向传播时得到的激活函数梯度则不会溢出;反向传播后,将权重梯度缩小2K 倍,恢复正常值。举例来说,对于包含75 亿个参数模型,如果用FP16 格式,只需要15GB 计算设备内存,但是在训练阶段模型状态实际上需要耗费120GB。计算卡内存占用中除了模型状态之外,还有剩余状态(Residual States),包括激活值(Activation)、各种临时缓冲区(Buffer)以及无法使用的显存碎片(Fragmentation)等。由于激活值可以用检查点(Activation Checkpointing)方式使得激活值内存占用大幅度减少,因此如何减少模型状态尤其是Adam 优化器状态是解决内存占用问题的关键。

图16 混合精度优化过程

以上是我简单介绍分布式机器学习系统的基础概念、分布式训练集群架构、分布式训练并行策略,DeepSpeed 为例如何在集群上训练大语言模型在下一次的文章给大家继续介绍,欢迎大家点赞关注支持,您的支持是我创作的动力。

参考内容:

(1) 收藏丨30个大语言模型训练相关的数据集分享 - 知乎. https://zhuanlan.zhihu.com/p/612243919.
(2) 大语言模型训练数据常见的4种处理方法 - 知乎. https://zhuanlan.zhihu.com/p/673045395.

(3)《大规模语言模型:从理论到实践》张奇等著. —北京:电子工业出版社
(4) 大语言模型综述 - Renmin University of China. http://ai.ruc.edu.cn/research/science/20230605100.html.

点击关注,第一时间了解华为云新鲜技术~

相关推荐

鸿蒙案例上新啦 | 购物、听歌、健身:15个模板带你一键激活社交娱乐

页面层级如何组织、跨页共享状态如何同步、列表与详情及个人中心如何形成稳定闭环。鸿蒙开发实践中心将这些高频消费与互动场景沉淀为可参考的标准模板,提供从业务架构到界面实现的快捷实践入口。更多实战案例欢迎前往鸿蒙开发实践中心案例中心查看并开启云端体验。

华为云官方博客 351

鸿蒙案例上新啦 | 出门打车、到家上课:25个模板带你一屏打通日常生活

如何合理拆分页面组件、如何建立单一可信的状态源、如何让用户在每一步操作中都获得明确反馈。鸿蒙开发实践中心以标准化的应用与元服务模板为入口,将清晰的业务架构与可复用的代码结构直观呈现。不妨立即挑选一个感兴趣的方向进入云端环境,从页面布局、组件封装、状态管理到路由组织,亲手跑通你的鸿蒙实战项目。

华为云官方博客 335

LakeAgent:DWS数据仓库的第二大脑,告别 “翻译式运维”,迈入智能自治时代

和普通 SQL 改写工具不同,LakeAgent 自动做多方案代价对比,主动规避负优化陷阱:补充分区键过滤实现剪枝,扫描分区由 37 收敛至 1,执行代价从 758.28 降至 113.74;未来的数据仓库,将不止能算、能存,更能看懂自己、诊断自己、理解自己。长久以来,数仓的智能都依附于人:信号需要人解读,瓶颈需要人研判,经验困在个体里,无法随底座永续生长。CPU 高、CPU 告警、CPU 诊断、CPU 使用率过高、CPU 满。语法、错误码、计费、性能调优、最佳实践、故障处置等全场景内容。

华为云官方博客 561

如何使用RDS Supabase后端BaaS能力:文件存储(File Storage)

• product-images(公开桶,允许公共读取):策略名称"Public read for product-images",允许SELECT,目标角色anon,策略定义bucket_id = 'product-images'。上传/删除需另配INSERT/DELETE策略,详见SQL。• product-specs(私有桶,仅认证用户访问自己的文件夹):建议拆为上传、读取、删除三条策略,策略定义均为 bucket_id = 'product-specs' 且仅限本人文件夹(详见下文 SQL)。

华为云官方博客 482

如何使用RDS Supabase后端BaaS能力:实时订阅与推送(Realtime)

Realtime 把数据库的每次变更同步到客户端:表里增、删、改一行,订阅者无需刷新页面或主动请求即可秒级收到。三大能力各司其职:• Postgres Changes — 监听表的 INSERT/UPDATE/DELETE,变化即通知• Broadcast — 客户端之间直接互发,适合瞬时通知、协同编辑• Presence — 跟踪频道里“谁在线”典型场景有实时数据展示(订单、库存、监控指标即时刷新)、即时通讯(消息收发、在线名单、“对方正在输入”)、协同编辑与实时通知。

华为云官方博客 473

AI Agent如何通过MCP连接到华为云RDS Supabase应用平台

Schema元数据列出所有可访问的表(按schema分组)。Schema元数据列出表的列及元数据(类型/默认值/约束)。Schema元数据列出所有约束(PK/FK/UNIQUE/CHECK/EXCLUDE)。可按schema/表/类型过滤。Schema元数据列出所有外键关系。可按schema/表过滤。Schema元数据列出所有索引及定义和大小。可按schema/表过滤。Schema元数据列出已应用的数据库迁移记录。Schema元数据列出已安装的PostgreSQL扩展。

华为云官方博客 479

华为大数据Flink让每一 CU 算力,跑出开源 5.74 倍的性能

1. 开源Apache Flink 已成为业界流计算事实标准,但在极端重负载场景下存在性能瓶颈。2. 华为大数据Flink通过多项自研深度优化,在执行计划、算子执行、状态存储三个层面构建协同的优化体系。3. Nexmark Benchmark 测试结果:整体性能提升 5.74 倍,极端重负载场景性能领先最高 18.3 倍。4. 100% 兼容 Apache Flink 1.20,业务代码零改造,无缝迁移。

华为云官方博客 383

LakeAgent实战 | 业务分析篇从一句提问到业务分析

本文以“订单金额变化”分析为例,展示LakeAgent如何通过自然语言提问,结合DataArtsStudio的数据资产与专业技能,帮助业务人员快速定位数据、生成SQL、追踪血缘、生成报告。从问题出发,逐步实现数据发现、变化洞察、原因追溯与团队协作,最终形成可复用的分析流程与企业规则,显著提升数据分析效率与一致性。

华为云官方博客 393

华为云RDS Supabase AI应用已开放公测 :30分钟搭建AI应用后端

认证、存储、实时推送、数据库API——4个后端模块,传统开发需要数周。今天,你只需要30分钟。AI创业公司的全栈开发工程师。上周老板说"下周要上线的AI对话应用,后端你来搞"——认证鉴权、数据库接口、文件存储、实时推送,每一个模块都需要数周搭建,而市场不会等你。小张打开华为云RDS Supabase,30分钟搭好认证、数据库API、对象存储,剩下的时间全用来打磨AI交互体验。基于开源Supabase+华为云RDS PostgreSQL全托管能力,为开发者提供。

华为云官方博客 401

如何使用RDS Supabase后端BaaS能力:数据库函数(Database Function)

上一篇介绍了 Database Data API——建表即自动生成增删改查接口,解决单表读写,但聚合统计、跨表事务、字段自动维护等逻辑已超出其能力边界。本篇推进到数据库函数(Database Function):把业务逻辑封装进数据库,前端通过 .rpc() 调用,由触发器自动响应数据变更。上一篇中,小张基于 Data API 完成了产品目录的建表与基础读写。但新需求随之而来:按分类统计产品数与价格区间、上架新品同时写产品表与定价表、产品修改后自动刷新更新时间。

华为云官方博客 467

如何使用RDS Supabase后端BaaS能力:创表即生成API(Database Data API)

认证、存储、实时推送、数据库API——4个后端模块,传统开发要数周。本系列逐个拆解,首篇聚焦数据库API(Database Data API):建表即得 API,无需手写后端。小张,AI创业公司的全栈工程师。老板让他做个产品目录页,展示产品与价格、支持上下架。传统做法要搭后端、写接口、配权限,工期少说1-2周;而在华为云RDS Supabase,建两张表、应用 SDK 直连,几分钟增删改查全跑通。

华为云官方博客 482

从一句话到一个能出去的链接,AI开发者空间做了什么

自行搭建的 Agent、正在使用的开源项目、团队既有的工具,都可以接入,可以通过AI创建或者手动搭建环境;华为云 AI开发者空间——面向开发者的云端一站式开发平台,做的就是把这段路接上:环境免安装,浏览器打开即为完整的 Linux 运行环境,预置的 AI 智能体开箱即用。第一个任务建议选能出结果的:做一个游戏、生成一个页面、创建一台实例,从需求走到可访问的链接,一次就能看清这个环境在做什么。沙箱是一个完整的 Linux 环境,依赖、服务、端口都是真实的,不是模拟。环境释放后,权限随之失效。

华为云官方博客 429

华为云码道配置自定义大模型,四种方式完整实操

案例介绍。

华为云官方博客 632

图数据库:实现穿透式监管的数据基座

穿透式监管标志着国企监管从“管企业”向“管资本”转型过程中的关键一跃。而图数据库正是这一跃的技术支点。从股权穿透到资金链穿透,从供应链穿透到数据穿透,华为GES图数据库正在将“穿透式监管”从理念变为可落地、可操作的实践。无论是天眼查式的全量链路查询,还是某集团财经的税务规则判断,抑或是芯片供应链式的部件风险分析,乃至通过Palantir构建决策知识图谱,其底层逻辑都是一致的——用图的力量,穿透一切形式的“隔离层”,直抵风险本质与责任核心。

华为云官方博客 667

《公共云三十问》正式收官,奔赴企业AI百场景新征程

今年6月底,公共云产业联盟重点推出《公共云三十问》专栏,以每周三问的节奏,连载十周,解读《先进公共云》白皮书,分享国计民生系列案例,旨在凝聚共识、推动落地实践。同时,专栏分析公共云在产业升级、民生福祉、科技创新、全球发展等方面的作用,梳理中国特色公共云发展路径:以普惠云拓展用云广度,以智算云提升用云深度,以自主云夯实用云厚度,以全球云打开用云跨度。这十问从专属云、行业云切入,深入政务、金融、交通、电力、钢铁、化工、油气、制造等国计民生场景,回答公共云“怎么用”。

华为云官方博客 474

开发者小技巧|IAM配置,让码道代码智能体连续30天免登录

案例介绍。

华为云官方博客 648

码道深度对接CodeArts,让需求自动落地为代码

案例介绍。

华为云官方博客 721

【公共云三十问 之三十】拥抱AI,企业如何建设面向智能体的专属云?

企业可分阶段建设覆盖开发、运行、运营及安全的一体化智能体平台——从构建一站式、高低码协同的智能体开发平台,到打造高性能、极致弹性的运行时环境,再到搭建AgentOps驱动的持续运营与运维体系,同时构建端到端、多层次的全链路安全防护体系。在数据湖建设方面,遵循“存量复用、平滑演进、AI导向、分步迭代”原则,分三阶段推进——打造AI多模数据湖核心能力、完成存算分离与统一元数据管理、实现闭环赋能全面支撑智能体规模化落地,在保护既有投资的前提下将传统数据平台转型为AI数据专属供给平台。

华为云官方博客 824

【公共云三十问 之二十九】国计民生之制造:制造云如何重塑数智时代的新质生产力?

平台打通软件交付的完整路径,覆盖需求管理、代码托管、代码检查、编译构建、制品仓库、部署、测试等关键环节,支持大模型能力融入研发全流程,实现从需求到交付的端到端管理。通过构建“研发一朵云”,方案打破地域与组织壁垒,实现跨团队协同开发,有效缩短软件交付周期,助力工业制造企业从“传统研发”走向“敏捷研发”,加速数智化转型进程。当制造云真正成为数据流通的平台、AI创新的底座,公共云的社会价值将从基础设施层向生产场景层持续延伸,为新型工业化建设和制造强国战略注入源源不断的动力,重塑数智时代的中国制造新质生产力。

华为云官方博客 791

告别重复手工迁仓,码道MCP+Skill实现仓库批量迁移

案例介绍。

华为云官方博客 791

【公共云三十问 之二十八】国计民生之油气:智算云如何叩响数智时代的“地心蓝金”?

西南油气田以“向地球深部要资源”为战略方向,联合以华为云Stack为代表的公共云服务商共同打造统一算力底座,建设西南区域数据湖与通用技术平台,构建“勘探开发工程技术、天然气生产过程管控、天然气产运储销”等三个一体化协同应用场景,支撑油气勘探、开发生产、管输储运等全业务链的数智化转型。当智算云真正成为数据流通的平台、AI创新的底座,油气田得以将算力效能、数据价值与智能技术深度融入勘探开发全链条,将“看不见的算力”转化为“摸得着的储量”,以智能化手段持续降低勘探风险、缩短发现周期、提高采收效率,确保生产安全。

华为云官方博客 613

【公共云三十问 之二十七】国计民生之化工:数智云如何重塑传统化工新范式?

在架构方面,方案构建云边协同的统一体系,涵盖AI协同、数据协同与运营协同三大维度——总部集中训练大模型,边缘节点负责推理执行,云边持续迭代,实现“边用边学”的智能闭环,模型平均精度提升20%以上。此外,方案提供一站式企业级工业物联网平台、数据湖与数据治理平台,覆盖工业数据分析“采、存、算、管、用”全流程,打通生产现场与管理系统间的数据壁垒,有效消除数据孤岛。层面,方案充分发挥软硬协同优势,实现计算、存储、网络与云平台的深度协同,训练线性度超95%,为大模型训练推理提供强劲算力支撑。▲化工“数智”新模式。

华为云官方博客 664

【公共云三十问 之二十六】国计民生之钢铁:数智云如何淬炼中国钢铁新力量?

同时,构建云边端协同的统一架构,支撑集团总部与各生产基地两级协同发展——总部集中训练大模型,边缘节点负责推理执行,云边持续迭代,实现训推一体化的资源调度与高效协同,为钢铁行业的数智化转型构建了坚实的算力底座。层面,方案构建覆盖“工业数采→边缘IT/OT融合→统一数据湖→数据治理平台→数据应用”全链路的一站式数据治理与流通平台,打通生产现场与管理系统间的数据壁垒,让数据“采得到、流得动、用得好”,加速数据价值从生产现场向经营决策层流动释放。湘潭钢铁集团的钢铁数智云,是钢铁工业智能云建设的标杆案例。

华为云官方博客 526

LakeWatch——MRS 智能运维Agent

从资源池、队列、作业等多个维度,对集群CPU与内存资源进行精细化分析与对比,直观呈现不同时间段的空闲、满载等使用状态,识别资源浪费,指导治理优化,为扩缩容决策提供数据支撑。对HDFS存储进行多维度深度分析(目录、用户、用户组等),涵盖使用量、增量、文件大小分布及数据热度等指标,帮助快速识别增长较快的目录、长期未访问的文件,统计目录文件数量与小文件占比,评估HDFS总存储量与扩缩容需求,为存储治理与资源规划提供全面数据支撑。Agent主动巡检,识别异常趋势并提前预警,让运维从"事后处理"转向"事前预防"。

华为云官方博客 515

华为云专题文档发布:丰富的AI案例、选型指导和概念解读

发布 “云服务选型”、“云计算概念”专题,提供云服务选型指导,助您选择匹配业务的云服务,解读云计算相关概念,开启云上学习之路。开发者工具和文档专区上线,提供从学习、实践到反馈的全链路支持。帮助中心新增100余篇AI案例。

华为云官方博客 286

一个人,也能用AI做点“星东西”

华为云星耀创想者大赛-2026华为云OPC创意大赛等你来~

华为云官方博客 291

从需求到可执行工作项:MCP+Skill 让智能体规划真正落地

回顾整个体验,以前让智能体帮我拆需求,它是洋洋洒洒给我列了一大篇 Markdown,看着挺像那么回事,但最后还是得我一条条往 CodeArts Req 里搬,搬完再手动挂父子关系、排日期,拆需求五分钟,搬运两小时。现在好了,MCP 给智能体接上了"手",Skill 给它装上了"项目经理的脑子",一句话下去,34 个工作项连带排期、层级关系全给我建好了,甘特图一拉,齐刷刷都在两周内,节省了多少工作量啊,美滋滋。本次实操在码道代码智能体基础上,构建了一套"需求→可执行工作项"的自动化规划方案。

华为云官方博客 576

华为云码道「码力续航计划」更新!

✅上新GLM‑5.3‑Flash,高速响应,代码生成、工程解析、Agent调试高效输出。新增GLM‑5.3‑Flash福利模型,每日解锁1000万免费Tokens。● 本次活动共发放总量价值100万元Tokens,发完即止!把耗算力的编码重活,交给华为云码道代码智能体🚀🚀。● 每日额度准时刷新,当日清零、次日满血重置。✅复杂算法、大项目解析、批量代码生成随便跑。✅不用抠额度、不用算消耗,放开调试大胆迭代。✅无需额外操作,登录即送,直接切换模型使用。● 把算力交给码道,让撸码不再内耗👏🏻。

华为云官方博客 688

【公共云三十问 之二十四】国计民生之交通:交通云如何铺就数智通衢新坦途?

交通行业作为支撑实体经济的关键基础设施,涵盖公路、铁路、机场、港口、地铁等众多子领域,其中,公路网络作为综合交通运输体系的基础骨架,承担着全国超过70%的客货运量,其数字化、智能化转型的需求尤为迫切。方面,提供Agent开发框架、知识库管理、RAG工程、意图识别、静态应用编排及模型评测等能力,支持客户将行业知识、业务流程与AI能力深度融合,快速构建面向收费稽核、路网调度、应急处置等场景的智能应用。开发并实现落地应用,全面覆盖建设、管理、养护、运营、服务、安全、办公七大业务领域。

华为云官方博客 464

【公共云三十问 之二十五】国计民生之电力:电力云如何点亮数智电力新引擎?

方案构建了覆盖数据获取、质量评估到标准化处理的全链路能力。基于公共云服务商智算底座及气象大模型,通过时空图神经网络重新建模产流机制,中国华电建成超50TB多源融合数据集,突破时空建模与机理融合等五大关键技术,短期径流预测精度较传统机理模型提升5个百分点以上,实现15天中期径流预测精度超85%,推理效率达秒级。基于气象大模型的基础架构与预训练能力,客户可直接在大模型之上开展增量训练与微调,充分利用场站所在区域的高精度历史数据(如3公里、1公里分辨率),针对不同场站和区域的地形地貌与气候特征进行模型调优。

华为云官方博客 381
上一篇: 华为云开发者桌面全新发布CodeArts IDE for Python
下一篇: 关于Java Chassis 3的契约优先(API First)开发
华为云开发者联盟
华为云开发者联盟 企业官方账号 企业官方账号
博客等级 码龄10年 72万+粉丝 5985原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值