【Deepspeed-DeepSpeedZeroOptimizer-01】ZeRO源码精读01:DeepSpeedZeroOptimizer(ZeRO-1,ZeRO-2)

DeepSpeedZeroOptimizer

这篇博客将对DeepSpeedZeroOptimizer的Step1和Step2进行一个详尽的描述,这里的Step1和Step2相对于论文汇中的描述叫ZeRO-1,ZeRO-2。

  • ZeRO-1,分区优化器状态
  • ZeRO-2,在1的基础上分区梯度

至于为什么没有把ZeRO-3一起写,暂时不清楚Deepspeed团队的设计,ZeRO3是在一个单独的文件中的,我们将在别的博客中进行展开。本文将聚焦ZeRO-1和ZeRO-2的设计。

本文详细的拆分了这个大类的方法,这个大类有2000多行,涉及了90+个方法,有的是核心的方法,有的时候简单的get/set以及辅助的方法,本文将做一个详尽的描述,用作学习笔记备份。而整理出的精简、梳理部分,我打算另开一文来述。

我将对每个方法进行详尽的解释,可参见注释与代码后的中文注解。
代码可以在https://github.com/microsoft/DeepSpeed/tree/master/中找到
代码的版权信息如下(只在这里写一次,后面代码块中将不重复出现)

// Copyright (c) Microsoft Corporation.
// SPDX-License-Identifier: Apache-2.0

// DeepSpeed Team

以下为正文

方法清单(以下是本类的方法列表,由于观赏需要,我把他们放到了Markdown的列表中。):

列1列2列3列4列5
init_enable_universal_checkpoint_create_param_mapping_link_all_hp_paramsis_moe_group
_configure_moe_settings_update_model_bit16_weights_round_robin_reorder_release_ipg_buffersinitialize_optimizer_states
reduce_gradientsget_first_param_indexinitialize_gradient_partitioning_data_structuresindependent_gradient_partition_epiloguereset_partition_gradient_structures
initialize_gradient_partitionoverlapping_partition_gradients_reduce_epiloguefill_grad_accum_attributeget_gradient_for_reductionget_param_gradient_attribute
clear_grad_attributecreate_reduce_and_remove_grad_hooksget_param_idreport_ipg_memory_usageflatten_dense_tensors_aligned
reduce_independent_p_g_buckets_and_remove_gradsprint_rank_0gradient_reduction_w_predivideaverage_tensorget_grad_position
update_overflow_tracker_for_param_grad_get_offload_gradient_dictasync_accumulate_grad_in_cpu_via_gpuset_norm_for_param_gradset_norm_for_param_grad_in_gpu
async_inplace_copy_grad_to_fp32_buffer_from_gpucomplete_grad_norm_calculation_for_cpu_offloadcopy_grads_in_partitionreduce_ipg_gradsreduce_ready_partitions_and_remove_grads
zero_reduced_gradientsflatten_and_printget_grads_to_reducesequential_executionset_none_gradients_to_zero
allreduce_bucket_clear_previous_reduced_gradsallreduce_and_copyallreduce_no_retainbuffered_reduce_fallback
get_data_parallel_partitionsget_partition_infozero_grad_model_parallel_all_reduceget_grad_norm_direct
get_flat_partitionfree_grad_in_param_listreset_cpu_buffersset_lrget_lr
override_loss_scalescaled_global_normget_bit16_param_group_optimizer_stepstep
update_lp_params_average_expert_grad_normsunscale_and_clip_grads_check_overflowhas_overflow_serial
has_overflow_partitioned_grads_serialhas_overflow_has_inf_or_nanbackwardcheck_overflow
_update_scale_get_state_set_state_get_param_groups_set_param_groups
_get_loss_scale_set_loss_scale_get_groups_without_padding_get_state_without_padding_get_base_optimizer_state
state_dict_restore_from_elastic_fp32_weights_restore_from_bit16_weightsrefresh_fp32_params_partition_base_optimizer_state
_restore_base_optimizer_stateget_ep_ranks_restore_elastic_base_optimizer_stateload_state_dict_load_universal_checkpoint
param_groups_load_hp_checkpoint_state_load_legacy_checkpoint
  1. __init__: 用于设置初始状态。

init():

# 定义初始化函数
def __init__(self,
             init_optimizer,  # 初始化优化器
             param_names,  # 参数名称
             timers,  # 计时器,用于测量代码的运行时间
             static_loss_scale=1.0,  # 静态损失缩放,用于控制损失的缩放比例
             dynamic_loss_scale=False,  # 是否使用动态损失缩放,当为True时,损失会根据训练过程动态调整
             dynamic_loss_args=None,  # 动态损失缩放的参数
             verbose=True,  # 是否打印详细的日志信息
             contiguous_gradients=True,  # 是否连续存储梯度,有助于提高内存效率
             reduce_bucket_size=500000000,  # reduce操作的bucket大小,用于梯度聚合
             allgather_bucket_size=5000000000,  # allgather操作的bucket大小,用于同步所有节点的梯度
             dp_process_group=None,  # 数据并行的进程组
             expert_parallel_group=None,  # 专家并行的进程组
             expert_data_parallel_group=None,  # 专家数据并行的进程组
             reduce_scatter=True,  # 是否使用reduce_scatter进行梯度聚合,可以减少通信次数,提高效率
             overlap_comm=False,  # 是否重叠通信和计算,当为True时,可以在计算梯度的同时进行梯度通信,提高效率
             offload_optimizer_config=None,  # offload优化器的配置,用于在设备间移动模型参数和优化器状态,以节省设备内存
             mpu=None,  # 模型并行单元,用于处理模型并行的相关操作
             clip_grad=0.0,  # 梯度裁剪值,用于防止梯度爆炸
             gradient_accumulation_dtype=torch.float32,  # 梯度累积的数据类型
             communication_data_type=torch.float16,  # 通信时的数据类型,使用float16可以减少通信带宽,提高效率
             postscale_gradients=True,  # 是否在计算完梯度后进行缩放,可以防止数值溢出
             gradient_predivide_factor=1.0,  # 在梯度累积前的预缩放因子
             gradient_accumulation_steps=1,  # 梯度累积步数,通过累积梯度可以模拟大批量训练,提高训练稳定性
             ignore_unused_parameters=True,  # 是否忽略未使用的参数,当为True时,未使用的参数不会被优化器更新
             partition_grads=True,  # 是否分区梯度,当为True时,梯度会被分区存储,可以节省内存
             round_robin_gradients=False,  # 是否进行轮询梯度,当为True时,各个设备会轮流进行梯度计算,可以平衡设备负载
             has_moe_layers=False,  # 是否包含moe层,moe层是一种用于大规模模型训练的技术
             fp16_master_weights_and_gradients=False,  # 是否使用fp16存储主权重和梯度,可以节省内存
             elastic_checkpoint=False):  # 是否使用弹性检查点,当为True时,可以在训练过程中动态保存和加载模型,提高训练的容错性

    # 如果优化器配置中开启了offload,并且offload的设备不为None,那么设置cpu_offload为True
    if offload_optimizer_config is not None and offload_optimizer_config.device != OffloadDeviceEnum.none:
        self.cpu_offload = True
        self.cpu_offload_pin_memory = offload_optimizer_config.pin_memory
    else:
        self.cpu_offload = False
        self.cpu_offload_pin_memory = False

    # 如果当前是主节点,打印一些设置的日志信息
    if dist.get_rank() == 0:
        logger.info(f"Reduce bucket size {reduce_bucket_size}")
        logger.info(f"Allgather bucket size {allgather_bucket_size}")
        logger.info(f"CPU Offload: {self.cpu_offload}")
        logger.info(
            f'Round robin gradient partitioning: {round_robin_gradients}')

    # 设置一些属性
    self.elastic_checkpoint = elastic_checkpoint
    self.param_names = param_names
    self.mpu = mpu

    # 如果没有检测到计算加速器,则抛出异常
    if not get_accelerator().is_available():
        raise SystemError(
            "Accelerator is not detected, cannot perform low precision training (e.g., fp16, bf16).")

    # 初始化的优化器赋值给self.optimizer
    self.optimizer = init_optimizer

    # 设置参数展平和反展平的函数
    self.flatten = _flatten_dense_tensors
    self.unflatten = _unflatten_dense_tensors

    # 设置是否进行梯度分区,和根据是否进行梯度分区设置ZeRO的阶段
    self.partition_gradients = partition_grads
    self.zero_stage_string = "ZeRO-2" if partition_grads else "ZeRO-1"

    self.timers = timers

    self.reduce_scatter = reduce_scatter

    self.overlap_comm = overlap_comm

    self.deepspeed_adam_offload = self.cpu_offload

    # 获取当前设备,如果开启了CPU offload,那么设备为cpu,否则为当前设备
    self.device = get_accelerator().current_device_name() if not self.cpu_offload else 'cpu'

    self.dp_process_group = dp_process_group

    # 专家并行的进程组
    self.ep_process_group = expert_parallel_group

    # 专家数据并行的进程组
    self.expert_dp_process_group = expert_data_parallel_group

    # 数据并行的大小
    dp_size = dist.get_world_size(group=self.dp_process_group)

    # 对于MoE模型,这可能对于不同的参数组是不同的
    # 它将在init中的MoE设置过程中被修改
    self.real_dp_process_group = [
        dp_process_group for i in range(len(self.optimizer.param_groups))]
    self.partition_count = [dp_size for i in range(
        len(self.optimizer.param_groups))]

    self.is_gradient_accumulation_boundary = True

    # CPU-Offload需要连续的梯度
    self.contiguous_gradients = contiguous_gradients or self.cpu_offload

    self.has_moe_layers = has_moe_layers
    if self.has_moe_layers:
        self._configure_moe_settings()
    self._global_grad_norm = 0.

    # 首先检查mpu是否为None,如果是,那么模型并行的相关设置都为默认值
    if mpu is None:
        self.model_parallel_group = None
        self.model_parallel_world_size = 1
        self.model_parallel_rank = 0
    else:
        # 否则,从mpu中获取模型并行的相关信息
        self.model_parallel_group = mpu.get_model_parallel_group()
        self.model_parallel_world_size = mpu.get_model_parallel_world_size()
        self.model_parallel_rank = bwc_tensor_model_parallel_rank(mpu)

    # 初始化一些参数
    self.overflow = False   # 是否溢出
    self.clip_grad = clip_grad   # 是否剪辑梯度
    self.communication_data_type = communication_data_type   # 通信数据类型
    self.gradient_predivide_factor = gradient_predivide_factor   # 梯度预分割因子
    self.postscale_gradients = postscale_gradients   # 是否在梯度后缩放
    self.gradient_accumulation_steps = gradient_accumulation_steps   # 梯度累积步数
    self.micro_step_id = 0   # 微步id
    self.ignore_unused_parameters = ignore_unused_parameters   # 是否忽略未使用的参数
    self.round_robin_gradients = round_robin_gradients   # 是否使用循环梯度

    self.extra_large_param_to_reduce = None   # 需要减小的额外大参数
    self.fp16_master_weights_and_gradients = fp16_master_weights_and_gradients   # 是否使用fp16主权重和梯度

    # 如果使用fp16主权重和梯度,检查cpu负载和优化器类型是否满足要求
    if self.fp16_master_weights_and_gradients:
        assert self.cpu_offload and type(self.optimizer) in [DeepSpeedCPUAdam], \
            f"fp16_master_and_gradients requires optimizer to support keeping fp16 master and gradients while keeping the optimizer states in fp32."\
            f"Currently only supported using ZeRO-Offload with DeepSpeedCPUAdam. But current setting is ZeRO-Offload:{self.cpu_offload} and optimizer type {type(self.optimizer)}." \
            f"Either disable fp16_master_weights_and_gradients or enable {self.zero_stage_string} Offload with DeepSpeedCPUAdam."

    # 如果支持reduce scatter,检查通信数据类型、梯度预分割因子和梯度预缩放是否满足要求
    if self.reduce_scatter:
        valid_reduce_scatter_dtypes = (
            torch.float16, torch.bfloat16, torch.float32)
        assert self.communication_data_type in valid_reduce_scatter_dtypes, f"{self.zero_stage_string} supports {valid_reduce_scatter_dtypes} communication_data_type with reduce scatter enabled. Got: '{self.communication_data_type}'"
        assert self.gradient_predivide_factor == 1.0, "gradient_predivide_factor != 1.0 is not yet supported with {self.zero_stage_string} with reduce scatter enabled"
        assert self.postscale_gradients, "pre-scale gradients is not yet supported with {self.zero_stage_string} with reduce scatter enabled"

    # 初始化一些参数列表
    self.bit16_groups = []   # 按组划分的参数
    self.bit16_groups_flat = []   # 扁平化的参数组
    self.parallel_partitioned_bit16_groups = []   # 并行划分的参数组
    self.single_partition_of_fp32_groups = []   # 每个进程将更新的单个32位参数部分
    self.params_not_in_partition = []   # 不会由此进程直接更新的参数
    self.params_in_partition = []   # 将由此进程直接更新的参数
    self.first_offset = []   # 第一个参数的偏移量
    self.partition_size = []   # 每个组的分区元素数量
    self.nccl_start_alignment_factor = 2   # NCCL all-gather发送缓冲区的4字节对齐
    self.all_reduce_print = False   # 是否打印all_reduce的输出
    self.dtype = self.optimizer.param_groups[0]['params'][0].dtype   # 参数数据类型
    self.gradient_accumulation_dtype = gradient_accumulation_dtype   # 梯度累积数据类型

    # 检查数据类型和梯度累积数据类型是否相同
    if self.dtype != self.gradient_accumulation_dtype:
        self.use_separate_grad_accum = True  # 如果不同,开启单独的梯度累积
    else:
        self.use_separate_grad_accum = False  # 如果相同,不开启单独的梯度累积

    # 检查是否使用单独的梯度累积并且没有分段梯度
    if self.use_separate_grad_accum and not self.partition_gradients:
        self.use_grad_accum_attribute = True  # 如果是,使用梯度累积属性
    else:
        self.use_grad_accum_attribute = False  # 如果不是,不使用梯度累积属性

    self.round_robin_bit16_groups = []  # 初始化空的round_robin_bit16_groups
    self.round_robin_bit16_indices = []  # 初始化空的round_robin_bit16_indices

    # 用于对齐分区的填充
    self.groups_padding = []

    # 遍历优化器的参数组
    for i, param_group in enumerate(self.optimizer.param_groups):
        # 获取当前分区的id
        partition_id = dist.get_rank(group=self.real_dp_process_group[i])

        # 存储需要训练的参数
        trainable_parameters = []
        for param in param_group['params']:
            # 如果参数需要梯度,则存储到trainable_parameters中
            if param.requires_grad:
                param.grad_accum = None
                trainable_parameters.append(param)
        # 将需要训练的参数添加到bit16_groups中
        self.bit16_groups.append(trainable_parameters)

        # 移动所有参数到cpu,以释放GPU空间,用于创建平坦缓冲区
        move_to_cpu(self.bit16_groups[i])
        empty_cache()

        # 对组参数进行重排序,以实现梯度分区在backward过程中的负载平衡
        # 通过这种方式,可以确保梯度的减少方式使所有权在等级之间轮流进行
        if self.round_robin_gradients:
            round_robin_tensors, round_robin_indices = self._round_robin_reorder(
                self.bit16_groups[i], dist.get_world_size(group=self.real_dp_process_group[i]))
        else:
            round_robin_tensors = self.bit16_groups[i]
            round_robin_indices = list(range(len(self.bit16_groups[i])))

        # 将round robin tensors和indices添加到对应的列表中
        self.round_robin_bit16_groups.append(round_robin_tensors)
        self.round_robin_bit16_indices.append(round_robin_indices)

        # 在CPU中创建平坦缓冲区并移动到GPU
        self.bit16_groups_flat.append(
            self.flatten_dense_tensors_aligned(
                self.round_robin_bit16_groups[i],
                self.nccl_start_alignment_factor * dist.get_world_size(group=self.real_dp_process_group[i])).to(
                    get_accelerator().current_device_name()))

        # 记录对齐所需的填充
        if partition_id == dist.get_world_size(group=self.real_dp_process_group[i]) - 1:
            # 如果是最后一个分区,计算填充值
            padding = self.bit16_groups_flat[i].numel() - sum(
                [t.numel() for t in self.round_robin_bit16_groups[i]])
        else:
            # 否则,填充为0
            padding = 0
        self.groups_padding.append(padding)

        # 更新模型的bit16权重
        self._update_model_bit16_weights(i)

        # 将平坦权重划分为近等的分区,等于数据并行度
        # 每个进程将在分区的不同部分进行计算
        data_parallel_partitions = self.get_data_parallel_partitions(
            self.bit16_groups_flat[i], i)
        self.parallel_partitioned_bit16_groups.append(data_parallel_partitions)


    # 验证数据分区起始位置是否为4字节对齐
    for partitioned_data in data_parallel_partitions:
        assert (partitioned_data.data_ptr() %
                (2 * self.nccl_start_alignment_factor) == 0)

    # 创建一个fp32主权重的分区,这个分区会被这个进程更新。
    # 注意,single_partition_of_fp32_groups中的参数是从模型的原始参数中克隆和分离出来的。
    if not fp16_master_weights_and_gradients:
        self.single_partition_of_fp32_groups.append(self.parallel_partitioned_bit16_groups[i][partition_id].to(
            self.device).clone().float().detach())
    else:
        self.single_partition_of_fp32_groups.append(self.parallel_partitioned_bit16_groups[i][partition_id].to(
            self.device).clone().half().detach())

    # 将本地优化器设置为拥有自己分区的扁平参数。
    # 之后,本地优化器将只包含其自己分区的参数。
    # 在这种情况下,本地优化器只保存与其分区参数有关的状态(动量、方差等)。
    self.single_partition_of_fp32_groups[
        i].requires_grad = True  # 保留这个,以防内部优化器使用它
    param_group['params'] = [self.single_partition_of_fp32_groups[i]]

    # 计算分区大小和分区内的参数信息
    partition_size = len(
        self.bit16_groups_flat[i]) / dist.get_world_size(group=self.real_dp_process_group[i])
    params_in_partition, params_not_in_partition, first_offset = self.get_partition_info(
        self.round_robin_bit16_groups[i], partition_size, partition_id)

    # 存储分区大小和参数信息
    self.partition_size.append(partition_size)
    self.params_in_partition.append(params_in_partition)
    self.params_not_in_partition.append(params_not_in_partition)
    self.first_offset.append(first_offset)

    # 设置一些基本参数和流
    self.reduce_bucket_size = int(reduce_bucket_size)
    self.allgather_bucket_size = int(allgather_bucket_size)
    self.reduction_stream = None if get_accelerator(
    ).is_synchronized_device() else get_accelerator().Stream()

    # 初始化一些参数和缓存列表
    self.callback_queued = False
    self.param_dict = {}
    self.is_param_in_current_partition = {}
    self.grads_in_ipg_bucket = []
    self.params_in_ipg_bucket = []
    self.elements_in_ipg_bucket = 0
    self.params_already_reduced = []
    self._release_ipg_buffers()
    self.previous_reduced_grads = None
    self.ipg_bucket_has_moe_params = False

    # 简化参数id
    self.param_id = {}

    # 对每个参数进行唯一标识
    largest_param_numel = 0
    count = 0
    for i, params_group in enumerate(self.bit16_groups):
        for param in params_group:
            unique_id = id(param)
            self.param_id[unique_id] = count
            self.param_dict[count] = param
            self.params_already_reduced.append(False)
            if param.numel() > largest_param_numel:
                largest_param_numel = param.numel()
            count = count + 1

    # 标记参数是否在当前分区
    for param_group in self.params_in_partition:
        for param in param_group:
            self.is_param_in_current_partition[self.get_param_id(param)] = True
    for param_group in self.params_not_in_partition:
        for param in param_group:
            self.is_param_in_current_partition[self.get_param_id(param)] = False

    # 如果开启了CPU offload的功能
    if self.cpu_offload:
        # 初始化一些参数,用来在CPU和GPU之间进行梯度转移
        self.accumulated_grads_in_cpu = {}  # 在CPU中累积的梯度
        self.norm_for_param_grads = {}  # 每个参数梯度的规范
        self.local_overflow = False  # 是否发生了溢出
        self.grad_position = {}  # 梯度的位置
        # 在设备上创造一个全零的tensor,用于CPU offload
        self.temp_grad_buffer_for_cpu_offload = torch.zeros(largest_param_numel,
                                                            device=self.device,
                                                            dtype=self.dtype)
        if self.cpu_offload_pin_memory:
            # 如果启用了内存锁页,就将数据固定在内存中,防止被交换到硬盘,加快数据传输速度
            self.temp_grad_buffer_for_cpu_offload = get_accelerator().pin_memory(
                self.temp_grad_buffer_for_cpu_offload)
        # 在设备上创造一个全零的tensor,用于GPU offload
        self.temp_grad_buffer_for_gpu_offload = torch.zeros(largest_param_numel,
                                                            device=get_accelerator().current_device_name(),
                                                            dtype=self.dtype)
        for i, params_group in enumerate(self.bit16_groups):
            self.get_grad_position(
                i, self.params_in_partition[i], self.first_offset[i], self.partition_size[i])

    # 初始化一些用于梯度分区的参数
    self.param_to_partition_ids = {}  # 参数到它所在分区的映射
    self.is_partition_reduced = {}  # 存储分区是否已经进行了reduce操作
    self.remaining_grads_in_partition = {}  # 分区内还需要计算的梯度数量
    self.total_grads_in_partition = {}  # 分区内总共的梯度数量
    self.is_grad_computed = {}  # 存储分区内的梯度是否已被计算
    self.grad_partition_insertion_offset = {}  # 存储在分区中插入梯度所需要的偏移量
    self.grad_start_offset = {}  # 存储在分区开始的地方插入梯度所需要的偏移量
    self.averaged_gradients = {}  # 存储分区所需的平均梯度
    self.offload_gradient_dict = {}  # 用于CPU offload,存储分区所需的平均梯度
    self.first_param_index_in_partition = {}  # 存储分区中第一个参数的索引

    # 初始化实现梯度分区的所有数据结构
    self.initialize_gradient_partitioning_data_structures()

    # 重置数据结构的值以便下一次反向传播
    self.reset_partition_gradient_structures()

    # 如果启用了梯度分区或者通信重叠,创建后向钩子
    if self.partition_gradients or self.overlap_comm:
        self.create_reduce_and_remove_grad_hooks()

    self.custom_loss_scaler = False
    self.external_loss_scale = None

    # 创建损失缩放器,可能是静态或者动态的
    self.loss_scaler = CreateLossScaler(dtype=self.dtype,
                                        static_loss_scale=static_loss_scale,
                                        dynamic_scaling=dynamic_loss_scale,
                                        dynamic_loss_args=dynamic_loss_args)
    self.dynamic_loss_scale = self.loss_scaler.dynamic

    # 只有当数据类型为float16时,才会使用动态损失缩放
    if self.dtype != torch.float16:
        assert self.loss_scaler.cur_scale == 1.0
        assert not self.dynamic_loss_scale

    # 初始化优化器状态
    self.initialize_optimizer_states()

    # 如果是主节点,则打印优化器状态初始化成功的信息
    if dist.get_rank() == 0:
        logger.info(f"optimizer state initialized")

    # 如果是数据并行处理组的主节点,打印ZeRO优化器初始化后的内存使用情况
    if dist.get_rank(group=self.dp_process_group) == 0:
        see_memory_usage(f"After initializing ZeRO optimizer", force=True)

    # 链接所有超参数
    self._link_all_hp_params()
    # 启用通用检查点
    self._enable_universal_checkpoint()
    # 创建参数映射
    self._param_slice_mappings = self._create_param_mapping()

详细流程如下:

  • 参数设置:首先,函数接收了大量的参数,这些参数主要包括了优化器的类型、参数名称、计时器、损失函数的缩放比例、是否使用动态损失缩放、是否打印详细日志、是否连续存储梯度、梯度聚合的bucket大小等等。

    1. 损失函数的缩放比例(Loss Scaling):损失函数的缩放比例主要是用在训练深度学习模型使用低精度(比如FP16)的情况下。由于低精度数据类型的数值范围较小,直接使用可能会导致数值下溢(变为0)的问题。损失缩放就是在计算梯度前对损失函数的值进行放大,然后在反向传播过程完成后再缩小回来,以避免在低精度计算中丢失梯度信息。
    2. 是否使用动态损失缩放:动态损失缩放是指在训练过程中动态调整损失缩放的大小。一开始设定一个较大的损失缩放值,如果在反向传播过程中发现没有发生梯度溢出,那么就增大损失缩放值;如果发现发生了梯度溢出,那么就减小损失缩放值。这样可以在保证计算精度的同时,尽可能地大幅度减小因为低精度计算带来的有效梯度值丢失。
    3. 梯度聚合的bucket(Gradient Bucketing):在分布式训练中,梯度通信是一个重要的瓶颈。为了减少通信次数,一种常见的做法是将多个梯度值放入一个“桶”(Bucket)中,然后一次性进行通信。这就是所谓的梯度聚合或者梯度Bucketing。这样可以减少通信次数,提高训练效率,但是如果Bucket的大小设置得过大,可能会增加GPU的内存占用。
  • 设备检测:根据是否开启了CPU offload,确定使用的设备是CPU还是GPU。

  • 参数组初始化:遍历优化器的参数组,对于需要训练的参数进行存储,并且按照数据并行的大小进行分区。

    在这个初始化过程中,“参数组初始化”主要是遍历所有的参数组,把需要训练(即requires_grad=True)的参数存储起来,以便后续进行梯度计算和权重更新。存储的方式是采用了一个列表self.bit16_groups,每个元素都是一个参数列表,对应一个参数组。然后,考虑到这个优化器会在分布式环境下使用,所以这些参数会被进一步按照数据并行的大小进行分区。在这个过程中,每个计算设备只需要存储和计算一部分的参数和梯度,这样就可以在不增加单个设备内存负担的前提下,训练更大的模型和批次数据,提高训练的速度和效率。这里的“分区”就是把参数组划分到不同的计算设备上,每个设备上的参数组形成一个分区。

  • 优化器参数设置:创造一个fp32主权重的分区,这个分区会被这个进程更新。并且将本地优化器设置为拥有自己分区的扁平参数。

    "扁平参数"是一种优化深度学习模型中参数存储和操作的方式。在深度学习模型中,参数通常是分布在各层网络中的,形状和大小各不相同,这样在进行参数更新和传输时,需要对每个参数单独进行操作,这在计算和存储上都会带来一定的效率损失。

    "扁平参数"是把所有参数拉平成一个一维向量,把原本分散在各层的参数集中存储和管理。这样在进行参数更新和传输时,可以一次性操作所有参数,大大提高了效率。并且,在GPU等设备上,对连续存储的数据进行操作通常都会有更好的性能。

    在这个初始化过程中,"扁平参数"指的就是把每个设备上的参数分区拉平成一个一维向量,然后把这个一维向量赋给本地优化器,让优化器在更新参数时,直接对这个一维向量进行操作。这样既简化了参数管理,也提高了计算效率。

  • 梯度分区设置:初始化并设置一些用于梯度分区的参数和数据结构。

  • 创建钩子:如果启用了梯度分区或者通信重叠,就会创建相应的后向钩子。

  • 损失缩放器创建:创建损失缩放器,可能是静态或者动态的。

  • 优化器状态初始化:初始化优化器的状态。

  • 其他设置:链接所有超参数、启用通用检查点、创建参数映射。

    "链接所有超参数"的含义是,将所有的超参数(比如学习率、权重衰减等)进行统一管理,以便后续进行调整和优化。通过某种数据结构(比如Python的字典)将所有的超参数和它们的值进行关联,以便后续进行查找和修改。这种具体的实现方式会依赖于整个优化器的设计和实现。

    “平坦缓冲区"是一种在深度学习训练中常见的优化技术。原则上,深度学习模型包含许多参数,这些参数在内存中可能是分散存储的,并且形状和大小各不相同。然而,在GPU等硬件设备上进行并行计算时,连续的内存布局往往能获得更好的性能。所以,我们通常会将这些分散的参数"平坦化”,也就是将它们连续地放入一个一维向量或者称之为"缓冲区"中。这样可以提高内存的利用率,同时也能加速计算。

    在这个代码中的"在CPU中创建平坦缓冲区并移动到GPU",是指首先在CPU内存中创建一个连续的缓冲区,并将模型的参数复制到这个缓冲区中,然后再将这个缓冲区的内容传输到GPU设备上。这样做的目的是为了减少GPU内存的占用,因为在创建缓冲区和复制参数的过程中,我们可以及时释放不再需要的内存,从而节省GPU的内存资源。

  1. _enable_universal_checkpoint_load_universal_checkpoint: 用于开启和加载通用的模型检查点。

    def _enable_universal_checkpoint(self):
        # 遍历bit16_groups中的所有参数组
        for lp_param_group in self.bit16_groups:
            # 对每个参数组启用通用检查点
            enable_universal_checkpoint(param_list=lp_param_group)
    
    # 定义一个方法来加载通用检查点
    def _load_universal_checkpoint(self, checkpoint_folder, load_optimizer_states, load_from_fp32_weights):
        # 从检查点文件夹中加载超参数检查点状态
        self._load_hp_checkpoint_state(checkpoint_folder)
    
  2. _create_param_mapping: 用于创建参数映射。

    def _create_param_mapping(self):
        # 初始化一个空列表,用于保存参数映射
        param_mapping = []
        # 使用枚举函数遍历优化器的参数组,i是索引,_是参数组的内容(这里我们不需要使用内容,因此使用_作为占位符)
        for i, _ in enumerate(self.optimizer.param_groups):
            # 对于每一个参数组,我们使用一个有序字典来保存该组的参数映射
            param_mapping_per_group = OrderedDict()
            # 遍历bit16_groups中的每一个元素,这里的lp代表一个模型的层或参数
            for lp in self.bit16_groups[i]:
                # 检查lp是否有_hp_mapping属性,如果有,说明它有一些需要映射的超参数
                if lp._hp_mapping is not None:
                    # 获取该层或参数的名称
                    lp_name = self.param_names[lp]
                    # 在有序字典中添加一个新的键值对,键是层或参数的名称,值是超参数的映射地址
                    param_mapping_per_group[lp_name] = lp._hp_mapping.get_hp_fragment_address()
            # 将该参数组的映射添加到整体的参数映射列表中
            param_mapping.append(param_mapping_per_group)
    
        # 返回参数映射列表
        return param_mapping
    

    这个_create_param_mapping函数的主要功能是创建一个用于记录模型中各个参数和其对应超参数映射关系的数据结构。

    过程如下:

    • 初始化参数映射:函数首先初始化了一个名为param_mapping的空列表,用于保存所有参数组的参数映射。
  • 遍历参数组:然后,函数遍历优化器的所有参数组,对于每一个参数组,创建一个有序字典param_mapping_per_group来保存该组的参数映射。

  • 创建参数映射:在每个参数组中,遍历bit16_groups的每个元素(一个模型的层或参数)。首先检查这个元素是否有_hp_mapping属性,如果有,说明它有一些需要映射的超参数。然后获取这个元素的名称,并在param_mapping_per_group字典中添加一个新的键值对:键是这个元素的名称,值是这个元素的超参数映射地址。

  • 保存参数映射:将当前参数组的param_mapping_per_group字典添加到param_mapping列表中。

  • 返回参数映射:最后,函数返回含有所有参数组映射信息的param_mapping列表。

这个函数的主要目的是建立模型中各个参数和其对应超参数的映射关系,这样在后续的训练过程中,如果需要调整某个参数的超参数,就可以直接通过这个映射找到对应的超参数,然后进行修改,大大提高了效率。

  1. _link_all_hp_params: 用于链接所有的超参数。这个函数的目标看起来是链接所有的半精度(16位)参数和单精度(32位)参数。它主要用于分布式训练,特别是在使用CPU offload和数据并行性(Data Parallelism)时。

    def _link_all_hp_params(self):
        # 获取分布式处理过程中的世界大小
        dp_world_size = dist.get_world_size(group=self.dp_process_group)
        
        # 如果启用了CPU卸载,获取卸载梯度字典
        if self.cpu_offload:
            self._get_offload_gradient_dict()
    
        # 遍历优化器的参数组
        for i, _ in enumerate(self.optimizer.param_groups):
            # 在分区中链接bit16和fp32参数
            # 获取实际分布式处理过程组的排名作为分区id
            partition_id = dist.get_rank(group=self.real_dp_process_group[i])
            # 计算分区大小,即bit16群组中的元素数量除以世界大小
            partition_size = self.bit16_groups_flat[i].numel() // dp_world_size
            # 获取fp32群组的单个分区
            flat_hp_partition = self.single_partition_of_fp32_groups[i]
            
            # 链接超参数(params)
            link_hp_params(
                # bit16参数列表
                lp_param_list=self.bit16_groups[i],
                # fp32参数的单个分区
                flat_hp_partition=flat_hp_partition,
                # 平均梯度字典
                gradient_dict=self.averaged_gradients,
                # 卸载梯度字典
                offload_gradient_dict=self.offload_gradient_dict,
                # 是否使用CPU卸载
                use_offload=self.cpu_offload,
                # 参数组索引
                param_group_index=i,
                # 分区开始的位置,由分区id乘以分区大小得出
                partition_start=partition_id * partition_size,
                # 分区大小
                partition_size=partition_size,
                # 分区优化器状态,由优化器的状态和fp32参数的单个分区确定
                partition_optimizer_state=self.optimizer.state[flat_hp_partition],
                # 分布式处理过程组
                dp_group=self.real_dp_process_group[i]
            )
    

    这段代码的主要目标是在分布式训练中同步和链接模型的参数。这段代码主要包括以下几个步骤:

    • 获取分布式处理过程中的世界大小(dp_world_size),即正在进行分布式训练的节点的数量。

    • 如果启用了CPU卸载,该函数会获取卸载梯度的字典。这对于在深度学习训练中节省GPU内存非常有用,它允许一些计算在CPU上进行,从而释放GPU资源。

    • 遍历优化器的参数组。这些参数组通常对应于不同的模型层或有着不同学习率的参数集合。

    • 对于每个参数组,函数计算了一个"分区",即该参数组在所有节点上的平均分布。这是通过计算bit16参数组的元素数量并将其除以世界大小(节点数量)来完成的。

    • 然后,函数获取了fp32参数组的单个分区。这是高精度(32位浮点数)的参数版本,用于精确的计算和更新。

    • 最后,函数调用link_hp_params方法来链接和同步所有的参数。这包括bit16参数列表,fp32参数的单个分区,平均梯度字典,卸载梯度字典,参数组索引,分区开始的位置,分区大小,分区优化器状态,以及分布式处理过程组。

    简单来说,这个函数的目的是在分布式训练中同步和管理模型的参数,这对于确保所有节点在训练过程中保持一致性是非常重要的。

    在训练过程中,计算出的梯度会被卸载到CPU的内存中,只在更新模型参数时才将其移回GPU。

    链接和同步所有的参数是分布式深度学习训练中的关键步骤。它的主要作用和方法如下:

    作用:

    在分布式训练中,模型的参数分布在多个计算节点上。每个节点都会对其部分的参数进行更新,这需要保证所有节点上的参数在更新后保持一致。如果不进行参数同步,每个节点会对各自的数据进行学习,而各自的模型参数可能会发散,这将导致模型的性能下降,甚至无法收敛。

    方法:

    链接和同步参数通常涉及以下几个步骤:

    1. **前向传播和反向传播:**在每个节点上,根据其所拥有的一部分训练数据进行前向传播和反向传播,计算出梯度。

    2. **梯度收集:**收集所有节点上的梯度。这可以通过各种通信操作完成,如归约(reduce)操作(将所有节点的数据聚集到一个节点上)或者全归约(all-reduce)操作(将所有节点的数据聚集起来,然后再广播回所有节点)。

    3. **梯度平均:**将收集到的梯度进行平均,得到一个全局的梯度。这个全局梯度是所有节点局部梯度的平均,它可以更好地反映整个训练集的信息。

    4. **参数更新:**使用这个全局梯度来更新每个节点上的参数。这样,所有节点上的参数在每次更新后都会保持一致。

    以上就是链接和同步所有参数的基本方法。在实际操作中(本文不清楚,拓展一下知识),可能还会涉及到一些优化手段,如梯度压缩(为了减少通信开销),梯度稀疏化(只更新重要的梯度)等。

  2. is_moe_group: 检查是否为MOE(Mixture of Experts)组。

        def is_moe_group(self, group):
            return 'moe' in group and group['moe']
    
  3. _configure_moe_settings: 用于配置MOE设置检查。

    def _configure_moe_settings(self):
            # 如果我们使用的是ZeRO阶段2,确保使用连续梯度
            if self.partition_gradients:
                # ZeRO阶段2中的连续梯度必须设置为True以便于MoE。其他代码路径未与MoE一起测试
                assert self.contiguous_gradients, "Contiguous Gradients in ZeRO Stage 2 must be set to True for MoE. Other code paths are not tested with MoE"
            # 注意:要运行ZeRO阶段1与MoE,我们需要设置self.contiguous_gradients为True或忽略此断言
            if not self.partition_gradients and not self.contiguous_gradients:
                logger.warn(
                    # ZeRO阶段1与MoE的配合尚未得到充分测试。这个配置仍然是实验性的
                    "ZeRO Stage 1 has not been thoroughly tested with MoE. This configuration is still experimental.")
            # ZeRO阶段2中的Reduce Scatter必须设置为True以便于MoE。其他代码路径未与MoE一起测试
            assert self.reduce_scatter, "Reduce Scatter in ZeRO Stage 2 must be set to True for MoE. Other code paths are not tested with MoE"
    
            # 模型具有MoE层,但是没有参数组被标记为MoE。在创建优化器之前,创建一个参数组,其中'moe'键设置为True
            assert any(
                [self.is_moe_group(group) for group in self.optimizer.param_groups]
            ), "The model has moe layers, but None of the param groups are marked as MoE. Create a param group with 'moe' key set to True before creating optimizer"
            self.is_moe_param_group = []
            for i, group in enumerate(self.optimizer.param_groups):
                # 如果这是一个MoE参数组
                if self.is_moe_group(group):
                    # MoE组中的所有参数都必须是MoE参数
                    assert all([is_moe_param(param)
                                for param in group['params']]), "All params in MoE group must be MoE params"
                    # 设置真实的数据并行进程组
                    self.real_dp_process_group[i] = self.expert_dp_process_group[group['name']]
                    # 设置分区数量
                    self.partition_count[i] = dist.get_world_size(group=self.expert_dp_process_group[group['name']])
                    # 标记为MoE参数组
                    self.is_moe_param_group.append(True)
                else:
                    # 不是MoE参数组
                    self.is_moe_param_group.append(False)
    
            # 专家数据并行组应当在MoE中配置
            assert self.expert_dp_process_group is not None, "Expert data parallel group should be configured with MoE"
            # 专家并行组应当在MoE中配置
            assert self.ep_process_group is not None, "Expert parallel group should be configured with MoE"
    
    • 检查ZeRO配置:这里主要检查ZeRO阶段2的配置是否符合MoE的要求,并对ZeRO阶段1的使用给出警告(没有经过详尽的测试)。
    • 检查参数组:模型参数通常会被划分为多个参数组,并且每个参数组可以有不同的优化设置。这段代码要求模型中至少有一个参数组被标记为MoE,并且这个参数组中的所有参数都必须是MoE参数。
    • 设置数据并行进程组和并行分区:对于每个被标记为MoE的参数组,设置对应的数据并行进程组和并行分区数量。
    • 检查并行组的配置:最后,代码检查专家数据并行组和专家并行组是否已经在MoE中被正确配置。
  4. _update_model_bit16_weights: 更新16位浮点数权重的模型。

    def _update_model_bit16_weights(self, group_index):
            # 解压缩16位小组的数据
            updated_params = self.unflatten(self.bit16_groups_flat[group_index],
                                            self.round_robin_bit16_groups[group_index])
            # 遍历原始小组和更新的参数,用更新的参数来更新原始参数
            for p, q in zip(self.round_robin_bit16_groups[group_index], updated_params):
                p.data = q.data
    
            # 将模型的16位权重设置为重新排序的扁平缓冲区的切片
            for param_index, param in enumerate(self.bit16_groups[group_index]):
                # 获取新的索引
                new_index = self.round_robin_bit16_indices[group_index][param_index]
                # 使用新的索引更新参数数据
                param.data = self.round_robin_bit16_groups[group_index][new_index].data
    
    • 它首先解压缩或解平化16位权重组(bit16_groups_flat)的数据,得到更新的参数(updated_params)。
    • 然后,它遍历原始的16位权重组(round_robin_bit16_groups)和更新后的参数。在这个过程中,它将原始参数(p.data)更新为新的参数值(q.data)。
    • 最后,它将模型的16位权重(bit16_groups)更新为重新排序后的扁平缓冲区(round_robin_bit16_groups)的切片。通过从新的索引(round_robin_bit16_indices)获取新的参数值,然后将这些新的参数值赋给对应的模型参数。
  5. _round_robin_reorder: 用于在多个设备间重新排序数据。

    def _round_robin_reorder(self, tensor_list, num_partitions):
            # 如果需要调试某个问题,可以禁用round robin
            # return tensor_list, list(range(len(tensor_list)))
    
            # 创建一个字典来存储每个分区的张量
            partition_tensors = {}
    
            # 遍历张量列表,按照round-robin算法将张量分配到各个分区
            for i, tensor in enumerate(tensor_list):
                # 计算当前张量应该分配到哪个分区
                j = i % num_partitions
                # 如果该分区还没有被分配过张量,就在字典中为这个分区创建一个空列表
                if not j in partition_tensors:
                    partition_tensors[j] = []
                # 将当前张量添加到对应分区的列表中
                partition_tensors[j].append((i, tensor))
    
            # 创建一个列表来存储重排序后的张量
            reordered_tensors = []
            # 创建一个字典来存储重排序后的索引
            reordered_indices = {}
    
            # 遍历每个分区
            for partition_index in partition_tensors.keys():
                # 遍历该分区的所有张量
                for i, (original_index, tensor) in enumerate(partition_tensors[partition_index]):
                    # 记录当前张量的新索引
                    reordered_indices[original_index] = len(reordered_tensors)
                    # 将当前张量添加到重排序后的张量列表中
                    reordered_tensors.append(tensor)
    
            # 返回重排序后的张量列表和索引字典
            return reordered_tensors, reordered_indices
    

    轮询(Round Robin)算法是一种非常常用的调度算法,主要用于处理资源分配和负载均衡等问题。

轮询算法的基本思想很简单。例如,假设我们有一个任务列表和有限的资源,我们需要按照某种顺序分配这些资源来处理任务。在轮询算法中,我们会按照一种固定的顺序,从任务列表中挑选任务来执行,每个任务被分配到相同的资源,并且执行相同的时间。当一个任务完成后,资源会被释放,并被分配给下一个等待的任务。这个过程会一直重复,直到所有的任务都被处理完。

这种算法的优点是公平和简单。每个任务都会得到相等的机会来获取资源,没有任务会被饿死

  1. _release_ipg_buffers: 释放一些用于异步梯度累积的缓冲区。

    def _release_ipg_buffers(self):
            # 如果梯度是连续的
            if self.contiguous_gradients:
                # 释放IPG缓冲区,这个缓冲区一般用于存储临时的梯度数据
                self.ipg_buffer = None
                # 释放分区中的梯度,这个一般用于存储在模型分区中的梯度数据
                self.grads_in_partition = None
                # 重置分区中的梯度偏移量,这个一般用于记录当前处理到哪个梯度
                self.grads_in_partition_offset = 0
    

    “梯度是连续的”,这是一个特性,描述的是在机器学习模型的训练过程中,梯度的存储方式。如果梯度是连续的,那么就意味着梯度值在内存中是连续存放的,这样可以帮助提高内存访问的效率,因为连续的内存访问速度通常比非连续的内存访问要快。

    这个"ipg_buffer"被用于存储可能需要在计算过程中用到的一些梯度数据。

    IPG的意思是Independent parameter partition,独立的梯度分区。因为有些梯度不是ZeRO管理的

  2. initialize_optimizer_states: 初始化优化器的状态。

    def initialize_optimizer_states(self):
        # 遍历 bit16_groups,i 是索引,group 是组
        for i, group in enumerate(self.bit16_groups):
            # 创建一个全零的张量,大小等于 partition_size[i],数据类型和 device 都与 single_partition_of_fp32_groups[i] 一致
            single_grad_partition = torch.zeros(int(self.partition_size[i]),
                                                dtype=self.single_partition_of_fp32_groups[i].dtype,
                                                device=self.device)
            # 如果 cpu_offload_pin_memory 为真,则将 single_grad_partition 的内存映射到 GPU 上,否则直接使用
            self.single_partition_of_fp32_groups[i].grad = get_accelerator().pin_memory(
                single_grad_partition) if self.cpu_offload_pin_memory else single_grad_partition
    
        # 如果优化器是 Adagrad,那么就用 single_partition_of_fp32_groups 来初始化它
        # 这是因为 Adagrad 在创建时就会初始化状态,而其他优化器则在第一次调用 step 方法时才会初始化状态
        if isinstance(self.optimizer, torch.optim.Adagrad):
            self.optimizer = torch.optim.Adagrad(self.single_partition_of_fp32_groups, **self.optimizer.defaults)
        else:
            # 其他类型的优化器则直接调用 step 方法
            self.optimizer.step()
    
        # 如果不进行 cpu_offload,那么就将 single_partition_of_fp32_groups 中的每个组的梯度设置为 None
        if not self.cpu_offload:
            for group in self.single_partition_of_fp32_groups:
                group.grad = None  # 初始化类
    
        return
    

    这段代码是在初始化优化器的状态。

    这段代码主要包含以下步骤:

    • 遍历所有 bit16_groups:这些组可能是模型的参数或梯度,被分成了16-bit的小组,以节省内存。
    • 对于每个组,创建一个全零的张量 single_grad_partition,其大小等于 partition_size[i]。这个张量用于存储该组的梯度。
    • 如果 cpu_offload_pin_memory 为真,则将 single_grad_partition 的内存映射到 GPU 上;否则,直接在 CPU 上使用它。
    • 根据优化器的类型初始化优化器的状态。如果优化器是 Adagrad,那么会立即初始化状态;否则,调用优化器的 step 方法,该方法通常会在第一次调用时初始化状态。
    • 如果不进行 cpu_offload (即所有计算都在 GPU 上完成),那么将 single_partition_of_fp32_groups 中的每个组的梯度设置为 None

    总的来说,这段代码的目的是为优化器的状态(包括梯度和其他可能的状态)分配内存,并根据需要将内存映射到 GPU 或者保留在 CPU 上。

  3. reduce_gradients: reduce-梯度。

    def reduce_gradients(self, pipeline_parallel=False):
            # 获取集群中的计算节点总数
            world_size = dist.get_world_size(self.dp_process_group)
            # 获取当前计算节点的排名
            my_rank = dist.get_rank(self.dp_process_group)
    
            # 如果使用pipeline并行并且使用连续的梯度,我们需要创建ipg缓冲区,因为在这种情况下,反向传播是在zero之外处理的
            if pipeline_parallel and self.contiguous_gradients:
                # 创建ipg缓冲区
                self.ipg_buffer = []
                # 创建一个空的tensor,大小等于reduce_bucket_size,数据类型为self.dtype,设备为当前设备
                buf_0 = torch.empty(int(self.reduce_bucket_size),
                                    dtype=self.dtype,
                                    device=get_accelerator().current_device_name())
                # 将这个tensor添加到ipg缓冲区中
                self.ipg_buffer.append(buf_0)
                # 设置ipg索引为0
                self.ipg_index = 0
    
            # 如果不使用通信重叠
            if not self.overlap_comm:
                # 遍历bit16组
                for i, group in enumerate(self.bit16_groups):
                    # 遍历组内的每个参数
                    for param in group:
                        # 获取用于reduce操作的梯度
                        grad_reduc = self.get_gradient_for_reduction(param)
                        # 如果梯度不为空
                        if grad_reduc is not None:
                            # 减少准备好的分区并移除梯度
                            self.reduce_ready_partitions_and_remove_grads(param, i)
            # 在hook或non-hook情况下,reduce任何待处理的梯度
            self.overlapping_partition_gradients_reduce_epilogue()
    ```
    
  • 获取集群信息: 函数首先获取集群中的计算节点总数(world_size)以及当前计算节点的排名(my_rank)。这些信息对于并行化计算和分布式训练非常重要。
  • 创建 IPG 缓冲区: 如果使用管道并行(pipeline_parallel)并且使用连续的梯度(contiguous_gradients),函数会创建一个 IPG(Inter-Process Gradient)缓冲区。IPG 缓冲区中的每个元素是一个梯度,用于在多个计算节点之间进行梯度的通信。
  • reduce梯度: 这是这个函数的核心部分。函数会遍历各个参数,并获取用于reduce操作的梯度,然后执行梯度的reduce操作。这个步骤是分布式训练中的关键步骤,每个计算节点会计算其自己的梯度,然后这些梯度将会被汇总(reduce)到单个梯度,这个梯度会被用于更新模型的参数。
  • 处理待处理的梯度: 最后,函数会调用 overlapping_partition_gradients_reduce_epilogue 方法,处理任何还未处理的梯度。
  1. get_first_param_index: 获取第一个参数的索引。

    def get_first_param_index(self, group_id, param_group, partition_id):
        # 遍历参数组中的每一个参数
        for index, param in enumerate(param_group):
            # 获取参数的ID
            param_id = self.get_param_id(param)
            # 检查当前的参数ID是否在指定的分区ID内
            # 如果在,就返回当前的索引
            if partition_id in self.param_to_partition_ids[group_id][param_id]:
                return index
        # 如果没有找到满足条件的参数,就返回None
        return None
    

    这是一个辅助函数。

  2. initialize_gradient_partitioning_data_structures: 初始化梯度分区的数据结构。

    def initialize_gradient_partitioning_data_structures(self):
    
        # 遍历所有的参数组
        for i, param_group in enumerate(self.round_robin_bit16_groups):
            # 获取分区的总数,这是通过获取分布式处理组的大小来决定的
            total_partitions = dist.get_world_size(group=self.real_dp_process_group[i])
    
            # 为每个参数组初始化相关的数据结构
            self.param_to_partition_ids[i] = {}
            self.is_partition_reduced[i] = {}
            self.total_grads_in_partition[i] = {}
            self.remaining_grads_in_partition[i] = {}
            self.is_grad_computed[i] = {}
            self.grad_partition_insertion_offset[i] = {}
            self.grad_start_offset[i] = {}
            self.first_param_index_in_partition[i] = {}
    
            # 为每个分区初始化相关的数据结构
            for partition_id in range(total_partitions):
                self.is_grad_computed[i][partition_id] = {}
                self.grad_partition_insertion_offset[i][partition_id] = {}
                self.grad_start_offset[i][partition_id] = {}
                # 初始时每个分区中的梯度总数为0
                self.total_grads_in_partition[i][partition_id] = 0
                # 初始化每个分区的梯度值
                self.initialize_gradient_partition(i, param_group, partition_id)
                # 初始化每个分区的缩减状态为False
                self.is_partition_reduced[i][partition_id] = False
                # 获取并存储每个分区的第一个参数的索引
                self.first_param_index_in_partition[i][partition_id] = self.get_first_param_index(
                    i, param_group, partition_id)
    

    这个函数的主要目标是初始化一系列的数据结构,这些数据结构用于跟踪和管理分布式训练过程中的梯度和参数。

    以下是这个函数的主要工作:

    • 遍历所有的参数组:函数首先遍历所有的参数组,这些参数组是模型训练过程中的一组参数。每个参数组都有一个相关的数据结构,用于跟踪和管理这些参数。
    • 初始化每个参数组的数据结构:对于每个参数组,函数初始化一系列的数据结构,包括:
      • param_to_partition_ids:这是一个映射,将每个参数映射到其所在的分区的ID。
      • is_partition_reduced:这是一个字典,用于跟踪每个分区是否已经进行了梯度的减少操作。
      • total_grads_in_partitionremaining_grads_in_partition:这两个字典用于跟踪每个分区中的总梯度数和剩余的梯度数。
      • is_grad_computed:这是一个字典,用于跟踪每个分区的梯度是否已经计算过。
      • grad_partition_insertion_offsetgrad_start_offset:这两个字典用于在分区中插入和开始梯度计算的偏移量。
      • first_param_index_in_partition:这是一个字典,用于存储每个分区的第一个参数的索引。
    • 初始化每个分区的数据结构:对于每个分区,函数进一步初始化一系列的数据结构,包括初始化每个分区的梯度值,初始化每个分区的减少状态为 False,以及获取和存储每个分区的第一个参数的索引。

    总的来说,这个函数的目的是初始化一系列的数据结构,这些数据结构用于在分布式训练过程中跟踪和管理梯度和参数。

  3. independent_gradient_partition_epilogue: IGP的梯度reduce操作。

    def independent_gradient_partition_epilogue(self):
        # 报告在执行梯度reduce之前的内存使用情况
        self.report_ipg_memory_usage(f"In ipg_epilogue before reduce_ipg_grads", 0)
        # 执行梯度reduce
        self.reduce_ipg_grads()
        # 报告在执行梯度缩减之后的内存使用情况
        self.report_ipg_memory_usage(f"In ipg_epilogue after reduce_ipg_grads", 0)
    
        # 将所有参数的已缩减标记设置为False
        for i in range(len(self.params_already_reduced)):
            self.params_already_reduced[i] = False
    
        # 如果开启了通信的并行处理
        if self.overlap_comm:
            # 等待所有的运算都完成
            get_accelerator().synchronize()
            # 清理之前已经缩减过的梯度的数据
            self._clear_previous_reduced_grads()
    
        # 如果不进行CPU卸载
        if self.cpu_offload is False:
            # 对于所有的参数组
            for i, _ in enumerate(self.bit16_groups):
                # 如果该参数组还没有平均梯度,就计算并存储
                if not i in self.averaged_gradients or self.averaged_gradients[i] is None:
                    self.averaged_gradients[i] = self.get_flat_partition(
                        self.params_in_partition[i],
                        self.first_offset[i],
                        self.partition_size[i],
                        dtype=self.gradient_accumulation_dtype,
                        device=get_accelerator().current_device_name(),
                        return_tensor_list=True)
                # 如果该参数组已经有平均梯度,就将新的平均梯度添加到已有的梯度上
                else:
                    avg_new = self.get_flat_partition(self.params_in_partition[i],
                                                      self.first_offset[i],
                                                      self.partition_size[i],
                                                      dtype=self.gradient_accumulation_dtype,
                                                      device=get_accelerator().current_device_name(),
                                                      return_tensor_list=True)
    
                    for accumulated_grad, new_avg_grad in zip(self.averaged_gradients[i], avg_new):
                        accumulated_grad.add_(new_avg_grad)
    
        # 释放independent parameter gradient (ipg)相关的内存
        self._release_ipg_buffers()
    
        # 没有必要再保留梯度信息了,因为所有步骤所需的梯度都已存储在self.averaged_gradients中。
        # 清空所有的梯度
        self.zero_grad(set_to_none=True)
        # 报告在执行完independent_gradient_partition_epilogue函数后的内存使用情况
        see_memory_usage(f"End ipg_epilogue")
    
    • 报告内存使用情况:在执行梯度reduce之前和之后,报告内存使用情况,以便了解内存消耗情况。
    • 执行梯度reduce:reduce操作是指将所有节点上的梯度聚合起来,以计算全局梯度。
    • 清除梯度重复标记:对于已经进行了reduce的参数,将其标记清除,以便下一次计算。
    • 处理并行通信:如果开启了通信的并行处理,将等待所有的运算都完成,然后清理之前已经缩减过的梯度的数据。
    • 处理CPU卸载:如果未开启CPU卸载,将对每个参数组进行处理。如果参数组还没有平均梯度,就计算并存储;如果已经有平均梯度,就将新的平均梯度添加到已有的梯度上。
    • 释放内存:释放与独立参数梯度(independent parameter gradient, ipg)相关的内存。
    • 清空所有梯度:在所有步骤所需的梯度都已存储在self.averaged_gradients中后,清空所有的梯度。这是因为梯度信息已经不再需要,可以释放这部分内存。
    • 报告内存使用情况:在执行完这个函数后,再次报告内存使用情况,以便对比和理解内存的变化。
  4. reset_partition_gradient_structures: 重置与每个分区相关的梯度结构。

    def reset_partition_gradient_structures(self):
        # 遍历所有的参数组
        for i, _ in enumerate(self.bit16_groups):
            # 获取分区的总数,这是通过获取分布式处理组的大小来决定的
            total_partitions = dist.get_world_size(group=self.real_dp_process_group[i])
            
            # 遍历所有的分区
            for partition_id in range(total_partitions):
                # 将每个分区的缩减状态设为False
                self.is_partition_reduced[i][partition_id] = False
                # 将每个分区剩余的梯度数量设置为每个分区的梯度总数
                self.remaining_grads_in_partition[i][partition_id] = self.total_grads_in_partition[i][partition_id]
    
                # 遍历分区中每个参数的梯度计算状态
                for param_id in self.is_grad_computed[i][partition_id]:
                    # 将每个参数的梯度计算状态设为False
                    self.is_grad_computed[i][partition_id][param_id] = False
    

    这个函数的主要目的是重置与每个分区相关的梯度结构。它遍历所有的参数组和分区,并将每个分区的缩减状态、剩余的梯度数量以及每个参数的梯度计算状态进行重置。这个函数通常在完成一次训练迭代后调用,为下一次迭代做准备。

  5. initialize_gradient_partition: 初始化reduce分区。

    def initialize_gradient_partition(self, i, param_group, partition_id):
        # 定义一个函数,用于在字典中设置键值对,如果键已经存在,就在值列表中添加新值,否则创建一个新列表
        def set_key_value_list(dictionary, key, value):
            if key in dictionary:
                dictionary[key].append(value)
            else:
                dictionary[key] = [value]
    
        # 定义一个函数,用于在字典中递增相应键的值,如果键不存在,就设置为1
        def increment_value(dictionary, key):
            if key in dictionary:
                dictionary[key] += 1
            else:
                dictionary[key] = 1
    
        # 获取分区大小
        partition_size = self.partition_size[i]
    
        # 计算分区的起始和结束索引
        start_index = partition_size * partition_id
        end_index = partition_size * (partition_id + 1)
    
        current_index = 0
        first_offset = 0
    
        # 遍历参数组
        for param in param_group:
            # 获取参数的大小和ID
            param_size = param.numel()
            param_id = self.get_param_id(param)
    
            # 如果当前索引在分区范围内
            if start_index <= current_index < end_index:
                # 更新各种字典和列表
                set_key_value_list(self.param_to_partition_ids[i], param_id, partition_id)
                increment_value(self.total_grads_in_partition[i], partition_id)
                self.is_grad_computed[i][partition_id][param_id] = False
                self.grad_partition_insertion_offset[i][partition_id][param_id] = current_index - start_index
                self.grad_start_offset[i][partition_id][param_id] = 0
    
            # 如果当前参数跨越了分区的起始边界
            elif current_index < start_index < (current_index + param_size):
                assert (first_offset == 0), "这个情况应该只会发生一次,因为这必须是分区中的第一个张量"
                first_offset = start_index - current_index
    
                # 更新各种字典和列表
                set_key_value_list(self.param_to_partition_ids[i], param_id, partition_id)
                increment_value(self.total_grads_in_partition[i], partition_id)
    
                self.is_grad_computed[i][partition_id][param_id] = False
                self.grad_partition_insertion_offset[i][partition_id][param_id] = 0
                self.grad_start_offset[i][partition_id][param_id] = first_offset
    
            # 更新当前索引
            current_index = current_index + param_size
    ```
    

这段代码是在初始化一个梯度分区。具体来说,它将一组参数(可能是神经网络的权重和偏置)分成不同的“分区”,每个分区内部包含一部分参数。这种分区策略在大规模并行计算情况下很常用,特别是在分布式训练中,可以将计算任务分解到多个处理器或机器上。

以下是它具体的工作流程:

  • 代码首先定义了两个内部函数,set_key_value_listincrement_value,它们被用于更新几个字典数据结构。
  • 然后,代码计算了每个分区的大小和每个分区的起始结束索引。
  • 之后,代码遍历了所有的参数。对于每一个参数,它计算了该参数的大小和ID。
  • 如果当前参数完全属于某个分区,代码会更新几个字典,记录参数ID、参数在分区中的插入偏移量和梯度开始的偏移量。
  • 如果当前参数跨越了分区的起始边界,代码会进行类似的操作,但是会考虑到偏移量。

总的来说,这段代码的目标是为分布式训练任务的并行化创建一个映射,这个映射可以指导如何将梯度分配到各个分区。这有助于后续的梯度计算和参数更新的并行化操作。

  1. overlapping_partition_gradients_reduce_epilogue: 调用IGP的梯度reduce操作

        def overlapping_partition_gradients_reduce_epilogue(self):
            self.independent_gradient_partition_epilogue()
    
  2. fill_grad_accum_attribute: 填充梯度累积属性。

    def fill_grad_accum_attribute(self):
        # 遍历所有的16位分组
        for group in self.bit16_groups:
            # 在每个分组中遍历所有的参数
            for param in group:
                # 如果参数的梯度不为空
                if param.grad is not None:
                    # 如果参数的grad_accum属性为空
                    if param.grad_accum is None:
                        # 将梯度转换为累积梯度的数据类型并存储在grad_accum中
                        param.grad_accum = param.grad.to(self.gradient_accumulation_dtype)
                    else:
                        # 如果grad_accum已经有值,那么将新的梯度值加到grad_accum上
                        param.grad_accum.add_(
                            param.grad.to(self.gradient_accumulation_dtype).view(param.grad_accum.shape))
                    # 清空参数的梯度,以便于下一次计算
                    param.grad = None
    

    这段代码的主要功能是对模型的梯度进行累积。在训练深度学习模型时,由于GPU内存的限制,有时候无法一次性将所有的数据放入模型中进行训练,这时就需要将数据分成多个小批次(batch)进行训练。每个小批次训练完后,模型的参数会有一个梯度值,这个梯度值表示了模型参数应该如何更新以减少损失函数。这段代码就是用来将每个小批次的梯度进行累积,然后在所有小批次训练完后,使用累积的梯度来更新模型参数,这种方法可以有效地减少计算资源的消耗,并提高训练的稳定性。

  3. get_gradient_for_reduction: 获取reduce的梯度。

    def get_gradient_for_reduction(self, param):
        # 如果使用grad_accum属性进行梯度累积
        if self.use_grad_accum_attribute:
            # 如果参数的grad_accum属性不为空,则返回转化为指定数据类型的grad_accum
            # 如果参数的grad_accum属性为空,则返回None
            return param.grad_accum.to(self.dtype) if param.grad_accum is not None else None
        else:
            # 如果不使用grad_accum属性进行梯度累积,则直接返回参数的梯度
            return param.grad
    

    这段代码的主要功能是根据是否使用梯度累积属性(grad_accum)来获取用于减小的梯度。如果使用梯度累积属性,它会返回累积的梯度(如果存在),并将其转换为指定的数据类型。如果不使用梯度累积属性,它将直接返回参数的梯度。这样可以在不同的情况下灵活地获取梯度,为模型的训练提供更大的灵活性。

  4. get_param_gradient_attribute: 获取参数的梯度属性。

    def get_param_gradient_attribute(self, param):
        # 如果 self.use_grad_accum_attribute 为真,返回 param 的 grad_accum 属性
        # 否则,返回 param 的 grad 属性
        return param.grad_accum if self.use_grad_accum_attribute else param.grad
    
  5. clear_grad_attribute: 清除梯度属性。

    def clear_grad_attribute(self, param):
        # 如果使用了梯度累积属性
        if self.use_grad_accum_attribute:
            # 清空参数的梯度累积属性
            param.grad_accum = None
        else:
            # 否则,清空参数的梯度属性
            param.grad = None
    
  6. create_reduce_and_remove_grad_hooks: 创建并删除梯度钩子。

    def create_reduce_and_remove_grad_hooks(self):
        # 初始化一个用于存储梯度累积函数的列表
        self.grad_accs = []
        # 遍历所有的16位分组
        for i, param_group in enumerate(self.bit16_groups):
            # 在每个分组中遍历所有的参数
            for param in param_group:
                # 如果参数需要计算梯度
                if param.requires_grad:
    
                    # 定义一个闭包函数,用于注册梯度钩子
                    def wrapper(param, i):
                        # 创建一个与参数形状相同的临时参数
                        param_tmp = param.expand_as(param)
                        # 获取梯度函数的下一个函数
                        grad_acc = param_tmp.grad_fn.next_functions[0][0]
    
                        # 定义一个函数,用于在需要的时候减少分区并移除梯度
                        def reduce_partition_and_remove_grads(*notneeded):
                            self.reduce_ready_partitions_and_remove_grads(param, i)
    
                        # 为梯度函数注册一个钩子,当梯度计算完成时会自动调用这个钩子
                        grad_acc.register_hook(reduce_partition_and_remove_grads)
                        # 将梯度函数添加到列表中
                        self.grad_accs.append(grad_acc)
    
                    # 调用闭包函数,注册梯度钩子
                    wrapper(param, i)
    

    这段代码的主要功能是为需要计算梯度的参数创建梯度钩子。在PyTorch中,梯度钩子是一种特殊的函数,可以在梯度计算完成后自动执行。这里的梯度钩子函数reduce_partition_and_remove_grads会在梯度计算完成后自动调用reduce_ready_partitions_and_remove_grads方法,减少计算分区并移除梯度,从而节省内存。这种方法在处理大规模深度学习模型时非常有用,可以有效地减少GPU内存的使用。

  7. get_param_id: 获取参数ID。

    def get_param_id(self, param):
        # 使用内置的id函数获取param的唯一id。
        # id函数返回对象的唯一标识符,此标识符是在该对象的生命周期内恒定的。
        unique_id = id(param)
        
        # 从self.param_id字典中获取对应的值。
        # self.param_id应该是一个字典,存储了参数(param)的唯一id和对应的值。
        # 此行代码返回的是与param的唯一id对应的值。
        return self.param_id[unique_id]
    
  8. report_ipg_memory_usage: 报告IPG(Independent Parallel Gradient)内存使用情况。

    def report_ipg_memory_usage(self, tag, param_elems):
            # 计算当前在 IPG 桶中的元素数量加上传入的 param_elems 参数的总数
            elem_count = self.elements_in_ipg_bucket + param_elems
            # 计算 elem_count 占总桶大小的百分比
            percent_of_bucket_size = (100.0 * elem_count) // self.reduce_bucket_size
            # 调用 see_memory_usage 方法,输出内存使用情况的字符串信息
            see_memory_usage(
                # 字符串格式化,包含了标签,IPG桶中元素的数量,参数元素的数量和占总桶大小的百分比
                f"{tag}: elems in_bucket {self.elements_in_ipg_bucket} param {param_elems} max_percent {percent_of_bucket_size}"
            )
    
  9. flatten_dense_tensors_aligned: 按照指定的对齐方式首先进行对齐,然后再将对齐后的张量扁平化。

    def flatten_dense_tensors_aligned(self, tensor_list, alignment):
        # 这个函数接受两个参数,一个是 tensor_list,是一个包含多个张量(tensor)的列表,另一个是 alignment,表示对齐方式
        # 这个函数的目标是将 tensor_list 中的所有张量首先进行对齐,然后再进行扁平化处理
    
        # 调用 align_dense_tensors 函数,对 tensor_list 中的每一个张量进行对齐,
        # align_dense_tensors 函数的返回值是一个新的张量列表,其中的每个张量都已经根据 alignment 对齐
        aligned_tensors = align_dense_tensors(tensor_list, alignment)
        
        # 调用 flatten 函数,对经过对齐处理的张量列表进行扁平化处理
        # flatten 函数的返回值是一个新的扁平化后的张量
        flattened_tensor = self.flatten(aligned_tensors)
    
        # 返回扁平化处理后的张量
        return flattened_tensor
    
    • 对齐张量:首先,函数调用了一个名为align_dense_tensors的函数,该函数将tensor_list中的每一个张量按照alignment的指定方式进行对齐。对齐的目的可能是确保所有张量具有相同的尺寸或形状,这样才能进行后续的操作。
    • 扁平化张量:在所有张量对齐之后,函数将对齐后的张量列表传给flatten函数进行扁平化处理。扁平化处理的目的是将多维张量转换成一维张量,以便于进行后续的运算或处理。
    • 返回结果:函数最后返回扁平化后的张量。
  10. reduce_independent_p_g_buckets_and_remove_grads: reduce ipg的梯度桶并删除梯度。

    def reduce_independent_p_g_buckets_and_remove_grads(self, param, i):
        # 获取用于减少的梯度
        grad_reduc = self.get_gradient_for_reduction(param)
        # 如果当前bucket中的元素数量加上参数的元素数量超过了bucket的大小
        if self.elements_in_ipg_bucket + param.numel() > self.reduce_bucket_size:
            # 报告当前的内存使用情况
            self.report_ipg_memory_usage("In ipg_remove_grads before reduce_ipg_grads", param.numel())
            # 减少ipg的梯度
            self.reduce_ipg_grads()
            # 如果开启了连续的梯度并且开启了通信重叠,那么交换ipg_index的值
            if self.contiguous_gradients and self.overlap_comm:
                self.ipg_index = 1 - self.ipg_index
            # 报告当前的内存使用情况
            self.report_ipg_memory_usage("In ipg_remove_grads after reduce_ipg_grads", param.numel())
    
        # 获取参数的id
        param_id = self.get_param_id(param)
        # 检查该参数是否已经被减少,如果已经被减少,则抛出异常
        assert self.params_already_reduced[param_id] == False, \
            f"The parameter {param_id} has already been reduced. \
            Gradient computed twice for this partition. \
            Multiple gradient reduction is currently not supported"
    
        # 如果开启了连续的梯度
        if self.contiguous_gradients:
            # 如果参数的元素数量大于bucket的大小,那么将该参数设置为待减少的参数
            if param.numel() > self.reduce_bucket_size:
                self.extra_large_param_to_reduce = param
            else:
                # 保持梯度连续,以防止内存碎片化,并且避免展平
                new_grad_tensor = self.ipg_buffer[self.ipg_index].narrow(0, self.elements_in_ipg_bucket, param.numel())
                new_grad_tensor.copy_(grad_reduc.view(-1))
                grad_reduc.data = new_grad_tensor.data.view_as(grad_reduc)
    
        # 更新bucket中的元素数量
        self.elements_in_ipg_bucket += param.numel()
    
        # 检查用于减少的梯度是否为None,如果为None则抛出异常
        assert grad_reduc is not None, f"rank {dist.get_rank()} - Invalid to reduce Param {param_id} with None gradient"
    
        # 将用于减少的梯度添加到bucket中
        self.grads_in_ipg_bucket.append(grad_reduc)
        # 将参数添加到bucket中
        self.params_in_ipg_bucket.append((i, param, param_id))
    
        # 确保平均张量函数知道如何平均梯度
        if is_moe_param(param):
            self.ipg_bucket_has_moe_params = True
    
        # 报告当前的内存使用情况
        self.report_ipg_memory_usage("End ipg_remove_grads", 0)
    

    这段代码的主要功能是将参数的梯度添加到一个bucket中,并在bucket的大小超过预定的大小时,对bucket中的梯度进行减少操作(通常是通过计算梯度的平均值或累加值),然后移除已经减少的梯度,从而节省内存。这种方法在处理大规模深度学习模型时非常有用,可以有效地减少GPU内存的使用,并且可以加快模型的训练速度,因为可以将多个小的梯度减少操作合并为一个大的梯度减少操作,从而减少了通信的开销。

  11. print_rank_0: 打印Rank 0的信息,辅助函数。

    def print_rank_0(self, message):
        # `dist.get_rank()`是获取当前进程的标识
        # 当标识为0时,代表这是主进程
        if dist.get_rank() == 0:
            logger.info(message)
    
  12. gradient_reduction_w_predivide: 实现了分布式训练中的梯度同步,确保每个进程的模型参数更新是一致的

    def gradient_reduction_w_predivide(self, tensor):
        # 获取当前分布式处理组的大小
        dp_world_size = dist.get_world_size(group=self.dp_process_group)
    
        tensor_to_allreduce = tensor
    
        # 如果通信数据类型与张量数据类型不同,将张量转换为通信数据类型
        if self.communication_data_type != tensor.dtype:
            tensor_to_allreduce = tensor.to(self.communication_data_type)
    
        if self.postscale_gradients:
            # 如果存在预分割因子,将张量乘以预分割因子的倒数
            if self.gradient_predivide_factor != 1.0:
                tensor_to_allreduce.mul_(1. / self.gradient_predivide_factor)
                
            # 在所有设备上执行全部规约操作,即在所有设备上求和
            dist.all_reduce(tensor_to_allreduce, group=self.dp_process_group)
    
            # 如果预分割因子不等于处理组的大小,将张量乘以预分割因子与处理组大小的比值
            if self.gradient_predivide_factor != dp_world_size:
                tensor_to_allreduce.mul_(self.gradient_predivide_factor / dp_world_size)
        else:
            # 如果不进行后缩放梯度,将张量除以处理组的大小
            tensor_to_allreduce.div_(dp_world_size)
            dist.all_reduce(tensor_to_allreduce, group=self.dp_process_group)
    
        # 如果通信数据类型与张量数据类型不同,并且张量与待规约张量不是同一个,将待规约张量的值复制到原始张量
        if self.communication_data_type != tensor.dtype and tensor is not tensor_to_allreduce:
            tensor.copy_(tensor_to_allreduce)
    
        return tensor
    

    这个函数是用来处理在分布式训练中的梯度同步问题的。

    在分布式训练中,每个进程都会对模型的部分数据进行前向和反向传播,计算出模型的梯度。然后,这些梯度需要在不同的进程之间进行同步,以保证每个进程的模型参数更新是一致的。否则,每个进程的模型参数可能会发散,导致训练失败。

    这个函数的主要步骤如下:

    • 获取当前分布式处理组的大小: 这是为了知道有多少个进程需要进行梯度同步。
    • 类型转换: 如果通信数据类型与张量数据类型不同,将张量转换为通信数据类型。
    • 预分割梯度: 如果设置了预分割梯度,那么在进行梯度同步之前,会先将梯度除以预分割因子。
    • 梯度规约: 使用dist.all_reduce函数进行梯度规约,即在所有进程上对梯度进行求和。
    • 后缩放梯度: 如果设置了预分割因子,那么在梯度规约之后,会将梯度乘以预分割因子与处理组大小的比值。如果没有设置预分割因子,那么在梯度规约之后,会将梯度除以处理组大小。
    • 类型转换回复: 如果通信数据类型与张量数据类型不同,并且张量与待规约张量不是同一个,将待规约张量的值复制到原始张量。

    这个函数的目的是实现了分布式训练中的梯度同步,确保每个进程的模型参数更新是一致的。

  13. average_tensor: 计算张量的平均值。

    def average_tensor(self, tensor):
        # 如果允许重叠通信
        if self.overlap_comm:
            stream = self.reduction_stream
            # 如果当前设备不同步
            if not get_accelerator().is_synchronized_device():
                # 等待当前设备完成任务
                stream.wait_stream(get_accelerator().current_stream())
        else:
            # 如果不允许重叠通信,使用当前设备的流
            stream = get_accelerator().current_stream()
    
        # 设置当前设备的流
        with get_accelerator().stream(stream):
            # 如果不进行reduce scatter操作,直接对tensor进行梯度减少操作,然后返回
            if not self.reduce_scatter:
                self.gradient_reduction_w_predivide(tensor)
                return
    
            # 进行reduce scatter操作的逻辑
            rank_and_offsets = []
            real_dp_process_group = []
            curr_size = 0
            prev_id, prev_process_group = -1, None
    
            process_group = self.dp_process_group
            for i, param, param_id in self.params_in_ipg_bucket:
                # 获取梯度进行减少操作
                grad_reduc = self.get_gradient_for_reduction(param)
    
                if self.ipg_bucket_has_moe_params:
                    # 如果参数是moe类型,需要特别处理
                    process_group = self.expert_dp_process_group[param.group_name] if is_moe_param(param) else self.dp_process_group
                    # 对梯度进行平均
                    grad_reduc.data.div_(dist.get_world_size(group=process_group))
    
                partition_ids = self.param_to_partition_ids[i][param_id]
                partition_size = self.partition_size[i]
                partition_ids_w_offsets = []
                for partition_id in partition_ids:
                    offset = self.grad_start_offset[i][partition_id][param_id]
                    partition_ids_w_offsets.append((partition_id, offset))
                # 根据offset对partition排序
                partition_ids_w_offsets.sort(key=lambda t: t[1])
    
            for idx in range(len(partition_ids_w_offsets)):
                # 获取当前的分区ID和偏移量
                partition_id, offset = partition_ids_w_offsets[idx]
    
                # 如果是最后一个分区,那么计算的元素数量为参数总元素数量减去偏移量
                if idx == len(partition_ids_w_offsets) - 1:
                    numel = param.numel() - offset
                else:
                    # 如果不是最后一个分区,那么计算的元素数量为下一个分区的偏移量减去当前分区的偏移量
                    numel = partition_ids_w_offsets[idx + 1][1] - offset
    
                # 如果当前的分区ID和上一个分区ID相同,并且处理组也相同
                if partition_id == prev_id and process_group == prev_process_group:
                    # 获取上一个分区的ID,大小和元素数量
                    prev_pid, prev_size, prev_numel = rank_and_offsets[-1]
                    # 更新上一个分区的元素数量,加上当前分区的元素数量
                    rank_and_offsets[-1] = (prev_pid, prev_size, prev_numel + numel)
                else:
                    # 如果分区ID或处理组和上一个不同,那么将当前的分区ID,当前的大小和元素数量添加到名次和偏移量的列表中
                    rank_and_offsets.append((partition_id, curr_size, numel))
                    # 将当前的处理组添加到真实的dp处理组列表中
                    real_dp_process_group.append(process_group)
                # 更新当前的大小,加上当前的元素数量
                curr_size += numel
                # 更新上一个分区的ID和处理组为当前的分区ID和处理组
                prev_id, prev_process_group = partition_id, process_group
    
            if not self.ipg_bucket_has_moe_params:
                # 对tensor进行平均操作
                tensor.div_(dist.get_world_size(group=self.dp_process_group))
    
            tensor_to_reduce = tensor
            if self.communication_data_type != tensor.dtype:
                # 如果通信数据类型和tensor数据类型不一致,进行转换
                tensor_to_reduce = tensor.to(self.communication_data_type)
    
            # 进行reduce操作,将结果分散存储在不同的节点上
            async_handles = []
            for i, (dst, bucket_offset, numel) in enumerate(rank_and_offsets):
                # 对需要减少的张量进行切片,获取需要进行reduce操作的部分
                grad_slice = tensor_to_reduce.narrow(0, int(bucket_offset), int(numel))
                # 获取目标节点的全局rank
                dst_rank = dist.get_global_rank(real_dp_process_group[i], dst)
                # 异步进行reduce操作,将grad_slice的数据减少到目标节点上,这是一个非阻塞操作,会立即返回一个句柄
                async_handle = dist.reduce(grad_slice, dst=dst_rank, group=real_dp_process_group[i], async_op=True)
                # 将异步操作的句柄添加到列表中,以便后续等待所有的reduce操作都完成
                async_handles.append(async_handle)
    
            # 等待所有的reduce操作完成
            for handle in async_handles:
                handle.wait()
    
            # 如果通信数据类型和tensor数据类型不一致,将tensor_to_reduce的数据复制到tensor中
            if self.communication_data_type != tensor.dtype:
                tensor.copy_(tensor_to_reduce)
    

    这段代码在进行一个称为 “Reduce-Scatter” 的并行计算操作,它是分布式计算中常见的一种操作。这种操作通常在深度学习的训练中使用,用于在多个计算节点之间同步模型参数的更新。以下是具体步骤:

    • 检查并设置通信流:首先,代码检查是否允许重叠通信(即在GPU执行计算任务的同时进行数据传输)。如果允许,那么它会使用一个专门的流来进行通信。否则,它会使用当前设备的默认流。

    • 对 tensor 进行平均操作:如果不进行reduce scatter操作,直接对 tensor 进行梯度减少操作,然后返回。

    • 处理不同类型的参数:对于某些特殊类型的参数(如 MoE 参数),代码会对其进行特殊处理。例如,它可能会改变用于数据传输的进程组,或者对参数进行预处理。

    • 准备进行 Reduce-Scatter 操作:代码会计算出每个参数应该分配给哪个分区,并得出每个分区的大小。然后,它会根据这些信息,以及参数在 tensor 中的位置,来切分 tensor。

    • 进行 Reduce-Scatter 操作:代码会对每个分区进行 Reduce 操作,即将所有节点上的相同数据合并在一起,然后将结果散列(Scatter)到所有节点。这是一个异步操作,即代码会立即向下执行,不会等待操作完成。

    • 等待 Reduce-Scatter 操作完成:代码会等待所有的 Reduce-Scatter 操作完成。这通常是必要的,因为后续的操作可能依赖于这些操作的结果。

    • 数据类型转换:如果通信数据类型和 tensor 数据类型不一致,将经过 reduce scatter 操作后的 tensor 转换回原本的数据类型。

    总的来说,这段代码的目的是在多个计算节点之间同步和分散 tensor 数据,以便在分布式环境中进行并行计算。

  14. get_grad_position: 获取梯度位置。

    def get_grad_position(self, group_id, tensor_list, first_offset, partition_size):
            current_offset = 0  # 当前已处理的元素偏移量
    
            for i, tensor in enumerate(tensor_list):  # 遍历所有张量
                param_id = self.get_param_id(tensor)  # 获取当前张量的ID
                param_start_offset = 0  # 设定当前张量的起始偏移量
    
                num_elements = tensor.numel()  # 获取当前张量的元素总数
    
                # 我们需要偏移以获取到正确的元素
                if i == 0 and first_offset > 0:  # 如果是第一个张量且first_offset大于0
                    tensor_offset = first_offset  # 张量偏移量为first_offset
                    num_elements = num_elements - tensor_offset  # 张量元素总数要减去偏移量
                    param_start_offset = first_offset  # 当前张量的起始偏移量为first_offset
    
                # 我们不需要张量的所有元素
                if num_elements > (partition_size - current_offset):  # 如果当前张量的元素总数大于分区剩余空间
                    num_elements = partition_size - current_offset  # 张量元素总数为分区剩余空间
    
                # 记录当前张量的信息到grad_position中
                # 其中包括:组ID,起始偏移量,当前偏移量,元素总数
                self.grad_position[param_id] = [
                    int(group_id), int(param_start_offset),
                    int(current_offset), int(num_elements)
                ]
                
                # 更新当前已处理的元素偏移量
                current_offset += num_elements
    

    这个函数的主要目的是计算并存储每个张量的梯度位置信息。

    具体来说,它遍历输入的tensor_list列表中的每个张量,并为每个张量计算其在梯度中的位置信息。

    • group_id: 这是参数组的ID,用于区分不同的参数组。
    • tensor_list: 这是需要处理的张量列表。
    • first_offset: 这是第一个张量的元素偏移量,用于确定从哪个元素开始处理。
    • partition_size: 这是每个分区的大小,用于决定每个张量可以处理的元素数量。

    在遍历每个张量时,它首先获取张量的ID(param_id)和元素总数(num_elements)。然后,如果是第一个张量并且first_offset大于0,则会更新num_elements和张量的起始偏移量(param_start_offset)。如果num_elements大于分区剩余空间,则会更新num_elements

    最后,它将张量的信息(包括组ID,起始偏移量,当前偏移量,元素总数)存储在self.grad_position[param_id]中,并更新当前已处理的元素偏移量(current_offset)。

    总的来说,这个函数是用来计算和存储每个张量在梯度中的位置信息的。

  15. update_overflow_tracker_for_param_grad: 更新参数梯度的溢出跟踪器。

    def update_overflow_tracker_for_param_grad(self, param):
        # 为给定的参数获取其梯度属性
        grad_accum = self.get_param_gradient_attribute(param)
        
        # 如果梯度不为空并且数据中存在无穷或NaN值
        if grad_accum is not None and self._has_inf_or_nan(grad_accum.data):
            # 设置本地溢出标志为True
            self.local_overflow = True
    
  16. _get_offload_gradient_dict: 获取卸载梯度的字典。

    def _get_offload_gradient_dict(self):
        # 遍历优化器的所有参数组
        for param_group_index, _ in enumerate(self.optimizer.param_groups):
            # 初始化当前参数组的梯度字典
            self.offload_gradient_dict[param_group_index] = []
            # 遍历当前参数组的所有参数
            for lp_param in self.params_in_partition[param_group_index]:
                # 获取参数的ID
                param_id = self.get_param_id(lp_param)
                # 获取参数的梯度位置信息
                [_, _, dest_offset, num_elements] = self.grad_position[param_id]
                # 根据位置信息,从单个分区的fp32组中获取对应的梯度张量
                dest_tensor = self.single_partition_of_fp32_groups[param_group_index].grad.view(-1).narrow(
                    0, dest_offset, num_elements)
                # 将梯度张量添加到当前参数组的梯度字典中
                self.offload_gradient_dict[param_group_index].append(dest_tensor)
    

    这段代码是在构建一个名为offload_gradient_dict的字典,该字典用于存储优化器中每个参数组的梯度信息。这个过程主要包括以下步骤:

    • 遍历优化器的所有参数组。

    • 对于每个参数组,首先在offload_gradient_dict中为其创建一个空列表。

    • 然后遍历该参数组中的所有参数。

    • 对于每个参数,首先获取其ID,然后根据ID从grad_position中获取该参数梯度的位置信息。

    • 根据位置信息,从single_partition_of_fp32_groups中的相应参数组中获取梯度张量。

    • 将这个梯度张量添加到offload_gradient_dict中相应参数组的列表中。

    这个过程的目的是为了将每个参数的梯度信息保存在一个方便访问的字典结构中,这样在优化器进行参数更新时,可以直接从这个字典中获取每个参数的梯度信息,这将大大提高参数更新的效率。

  17. async_accumulate_grad_in_cpu_via_gpu: 通过GPU在CPU上异步累积梯度。

    def async_accumulate_grad_in_cpu_via_gpu(self, param):
        param_id = self.get_param_id(param)  # 获取参数的ID
    
        [i, source_offset, dest_offset, num_elements] = self.grad_position[param_id]  # 获取梯度的位置信息
    
        # 复制到一个预先存在的缓冲区,以避免内存分配的开销
        dest_buffer = self.temp_grad_buffer_for_gpu_offload.view(-1).narrow(0, 0, param.numel()) 
    
        # 为在CPU中存储此参数的梯度创建缓冲区
        def buffer_to_accumulate_to_in_cpu():
            if not self.fp16_master_weights_and_gradients:
                # 如果不是使用16位浮点数的权重和梯度,则创建一个缓冲区,大小为参数元素的数量,数据类型为参数的数据类型,设备为当前设备
                buffer = torch.zeros(param.numel(), dtype=param.dtype, device=self.device)
                # 如果启用了CPU回传固定内存,则将缓冲区固定在内存中,否则直接返回缓冲区
                return get_accelerator().pin_memory(buffer) if self.cpu_offload_pin_memory else buffer
            else:
                # 如果是使用16位浮点数的权重和梯度,则返回属于这个分区的fp32组的梯度,视图改为1维,并且截取从dest_offset开始,长度为num_elements的部分
                return self.single_partition_of_fp32_groups[i].grad.view(-1).narrow(0, dest_offset, num_elements)
    
        # 将梯度累积到param.grad_accum或者属于这个分区的部分
        def accumulate_gradients():
            grad_accum = self.get_param_gradient_attribute(param)  # 获取参数的梯度属性
            if not self.fp16_master_weights_and_gradients:
                # 如果不是使用16位浮点数的权重和梯度
                # 那么将CPU中已累积的梯度(将其视图改为1维)复制到目标缓冲区中,这个过程是非阻塞的
                dest_buffer.copy_(self.accumulated_grads_in_cpu[param_id].view(-1), non_blocking=True)
                # 然后将目标缓冲区中的内容添加到参数梯度的数据中(将其视图改为1维)
                grad_accum.data.view(-1).add_(dest_buffer)
            else:
                # 如果是使用16位浮点数的权重和梯度
                # 那么将CPU中已累积的梯度(将其视图改为1维)复制到目标缓冲区的指定部分中,这个过程是非阻塞的
                dest_buffer.narrow(0, source_offset, num_elements).copy_(self.accumulated_grads_in_cpu[param_id].view(-1), non_blocking=True)
                # 然后将目标缓冲区中的指定部分的内容添加到参数梯度的数据的指定部分中
                grad_accum.data.view(-1).narrow(0, source_offset, num_elements).add_(dest_buffer.narrow(0, source_offset, num_elements))
    
    
        # 将累积的梯度移回到CPU
        def copy_gradients_to_cpu():
            grad_accum = self.get_param_gradient_attribute(param)  # 获取参数的梯度属性
        if not self.fp16_master_weights_and_gradients:
            # 如果不是使用16位浮点数的权重和梯度,则将参数梯度的数据(将其视图改为1维)复制到CPU中已累积的梯度中,这个过程是非阻塞的
            self.accumulated_grads_in_cpu[param_id].data.copy_(grad_accum.data.view(-1), non_blocking=True)
        else:
            # 如果是使用16位浮点数的权重和梯度,则将参数梯度的数据的指定部分(将其视图改为1维并截取指定部分)复制到CPU中已累积的梯度中,这个过程是非阻塞的
            self.accumulated_grads_in_cpu[param_id].data.copy_(grad_accum.data.view(-1).narrow(0, source_offset, num_elements), non_blocking=True)
    
        if param_id not in self.accumulated_grads_in_cpu:
            # 如果CPU中还没有当前参数的已累积梯度,则创建一个缓冲区用于在CPU中累积梯度
            self.accumulated_grads_in_cpu[param_id] = buffer_to_accumulate_to_in_cpu()
    
        if self.micro_step_id > 0:
            # 如果微步长大于0,则累积梯度
            accumulate_gradients()
    
        # 在边界处,我们将直接发送32位
        if not self.is_gradient_accumulation_boundary:
            copy_gradients_to_cpu()
    

    这段代码是用于在利用 GPU 进行深度学习训练时,异步地将梯度累积(accumulate)在 CPU 中。这是一种优化训练过程的技术,可以提高内存使用效率和计算性能。

    代码的主要步骤是:

    • 获取参数的 ID 和梯度的位置信息。
    • 在 CPU 中创建或找到一个缓冲区用于存储参数的梯度。
    • 如果条件满足(比如,不是第一个微步或在梯度累积的边界),则累积梯度或将累积的梯度复制回 CPU。

    这段代码可能出现在一种称为模型并行化(Model Parallelism)的技术中,这是一种分布式训练模型的策略,可以在多个设备上分片处理模型的不同部分,以此提高训练效率。

  18. set_norm_for_param_grad: 为参数梯度设置范数。

    def set_norm_for_param_grad(self, param):
            # 获取参数的ID
            param_id = self.get_param_id(param)
            
            # 获取参数的梯度属性
            grad_accum = self.get_param_gradient_attribute(param)
            
            # 根据梯度累积步骤的数量,来决定使用哪种方式来获取累积梯度
            # 如果梯度累积步骤大于1,则从存储在CPU中的累积梯度列表中获取,否则直接使用grad_accum
            accumulated_grad = self.accumulated_grads_in_cpu[
                param_id] if self.gradient_accumulation_steps > 1 else grad_accum
    
            # 从梯度位置列表中获取相关信息
            [i, source_offset, dest_offset, num_elements] = self.grad_position[param_id]
    
            # 定义开始位置
            start = source_offset
            
            # 对累积梯度进行调整,首先将其视图(view)调整为1维,然后从start开始,获取num_elements个元素
            accumulated_grad = accumulated_grad.view(-1).narrow(0, start, num_elements)
    
            # 计算调整后的累积梯度的2范数(即欧几里得范数,或者叫做欧氏距离),并将其设置为对应参数梯度的范数
            self.norm_for_param_grads[param_id] = accumulated_grad.data.double().norm(2)
    

    这段代码是一个异步函数,用于在GPU计算完成后将梯度累积到CPU中。这在内存有限但CPU资源充足的情况下非常有用,可以在一定程度上提高训练的效率。具体来说,这个函数的作用可以分解为以下几个步骤:

    1. 获取参数的ID和梯度位置信息。

    2. 创建一个临时的缓冲区用于存放将要复制的梯度。

    3. 定义一个内部函数buffer_to_accumulate_to_in_cpu,用于在CPU中创建一个用于存储参数梯度的缓冲区。这个缓冲区的创建方式取决于是否使用16位浮点数的权重和梯度。

    4. 定义一个内部函数accumulate_gradients,用于将梯度累积到参数的梯度属性param.grad_accum中,或者累积到属于这个分区的部分。这个累积的方式也取决于是否使用16位浮点数的权重和梯度。

    5. 定义一个内部函数copy_gradients_to_cpu,用于将累积的梯度从GPU复制回到CPU。这个复制的方式同样取决于是否使用16位浮点数的权重和梯度。

    6. 如果CPU中还没有当前参数的已累积梯度,那么就使用buffer_to_accumulate_to_in_cpu函数创建一个缓冲区用于在CPU中累积梯度。

    7. 如果微步长大于0,那么就使用accumulate_gradients函数累积梯度。

    8. 如果当前参数的梯度不在梯度累积的边界处,那么就使用copy_gradients_to_cpu函数将累积的梯度从GPU复制回到CPU。

  19. set_norm_for_param_grad_in_gpu: 在GPU中为参数梯度设置范数。

    def set_norm_for_param_grad_in_gpu(self, param):
            # 获取参数的ID
            param_id = self.get_param_id(param)
            
            # 获取参数的梯度属性
            grad_accum = self.get_param_gradient_attribute(param)
            
            # 如果没有累积的梯度,就使用参数的梯度,否则就使用已经累积的梯度
            if grad_accum is None:
                accumulated_grad = param.grad
            else:
                accumulated_grad = grad_accum
    
            # 获取梯度的位置信息
            [i, source_offset, dest_offset, num_elements] = self.grad_position[param_id]
    
            # 从源偏移的位置开始,获取指定数量的元素
            start = source_offset
            accumulated_grad = accumulated_grad.view(-1).narrow(0, start, num_elements)
    
            # 计算梯度的2范数,并将其设置为对应参数梯度的范数
            self.norm_for_param_grads[param_id] = accumulated_grad.data.double().norm(2)
    

    这段代码的目的是计算并设置神经网络模型中各参数的梯度的2范数(Euclidean norm)。在神经网络训练过程中,参数的梯度用于指导参数的更新方向和步长,而梯度的2范数(也就是梯度向量的长度)反映了参数更新的“强度”或“速度”。

    具体来说,这个函数的流程如下:

    1. 获取参数的唯一标识ID。
    2. 尝试获取已经累积的参数梯度。如果没有累积的梯度,就使用参数当前的梯度。
    3. 根据参数ID,从grad_position中获取梯度在内存中的位置信息,包括起始偏移和元素数量等。
    4. 根据上一步获取的信息,提取相应的梯度值。
    5. 计算梯度的2范数,将其转换为双精度浮点数,并保存到norm_for_param_grads字典中,键为参数ID。

    这个函数可能是在神经网络的反向传播过程中使用,用于计算每个参数的梯度范数,从而进行梯度裁剪或其他处理。例如,如果某参数的梯度范数过大,可能会导致训练过程中的梯度爆炸问题,此时可以通过梯度裁剪来避免这个问题。

  20. async_inplace_copy_grad_to_fp32_buffer_from_gpu: 从GPU异步复制梯度到FP32缓冲区。

    def async_inplace_copy_grad_to_fp32_buffer_from_gpu(self, param):
            # 根据参数获取其id
            param_id = self.get_param_id(param)
    
            # 从grad_position字典中获取参数的一些属性,包括所在的组i,源偏移量,目标偏移量,元素数量
            [i, source_offset, dest_offset, num_elements] = self.grad_position[param_id]
    
            # 获取fp32_groups的第i组的梯度,然后调整形状为一维,然后从dest_offset开始,获取num_elements个元素,得到目标张量
            dest_tensor = self.single_partition_of_fp32_groups[i].grad.view(-1).narrow(0, dest_offset, num_elements)
    
            # 获取参数的梯度属性
            grad_accum = self.get_param_gradient_attribute(param)
            if grad_accum is None:
                # 如果梯度属性为空,则将其视为一维张量,从source_offset开始,获取num_elements个元素,得到源张量
                src_tensor = grad_accum.view(-1).narrow(0, source_offset, num_elements)
            else:
                # 如果梯度属性不为空,则将其视为一维张量,从source_offset开始,获取num_elements个元素,得到源张量
                src_tensor = grad_accum.view(-1).narrow(0, source_offset, num_elements)
            if not self.fp16_master_weights_and_gradients:
                # 如果没有启用fp16主权重和梯度,则将源张量转换为float类型
                src_tensor = src_tensor.float()
    
            # 将源张量的内容复制到目标张量中,此操作为非阻塞操作
            dest_tensor.copy_(src_tensor, non_blocking=True)
            # 将参数的梯度设为None,此步骤为了减少GPU上的内存使用
            param.grad = None
    

    这段代码是在进行深度学习模型训练中的一步操作,特别是在使用深度学习框架(如PyTorch)训练神经网络时的一部分。

    具体来说,这段代码的功能是异步地将GPU上的梯度数据复制到CPU上的32位浮点数(fp32)缓冲区。这是为了节省GPU的内存资源。因为当我们在GPU上进行大规模的深度学习训练时,GPU的内存很容易就会被用完。为了减轻这种压力,我们通常会将一些数据(如梯度)临时地存储在CPU的内存中,而不是在GPU上。当需要这些数据时,我们再将它们从CPU复制回GPU。

    这段代码的操作步骤大致如下:

    1. 根据输入的参数获取其在模型中的ID。
    2. 从一个名为grad_position的字典中获取这个参数的一些属性,包括它在一个名为fp32_groups的列表中的位置、在GPU和CPU内存中的偏移量、以及它包含的元素数量。
    3. 获取fp32_groups中对应位置的梯度,并将其调整为一维张量。然后从指定的偏移量开始,获取指定数量的元素,作为目标张量。
    4. 类似地,获取参数在GPU上的梯度,并将其调整为一维张量。然后从指定的偏移量开始,获取指定数量的元素,作为源张量。
    5. 如果没有启用16位浮点数(fp16)的主权重和梯度,那么将源张量转换为32位浮点数。
    6. 将源张量的内容复制到目标张量中,这个操作是异步的,也就是说,它不会阻塞其他操作。
    7. 最后,将参数在GPU上的梯度设为None,这是为了释放GPU上的内存资源。
  21. complete_grad_norm_calculation_for_cpu_offload: 完成CPU卸载的梯度范数计算。

    def complete_grad_norm_calculation_for_cpu_offload(self, params):
            total_norm = 0.0  # 初始化总梯度范数为0
            norm_type = 2.0  # 设置范数类型为2,即L2范数
            for p in params:  # 遍历所有的参数
                # Pipeline parallelism may replicate parameters. Avoid multi-counting.
                # 流水线并行可能会复制参数,我们需要避免重复计数
                if hasattr(p, PIPE_REPLICATED) and p.ds_pipe_replicated:
                    continue
    
                if is_model_parallel_parameter(p) or (self.model_parallel_rank == 0):  # 如果参数是并行模型的参数或模型并行等级为0
                    param_id = self.get_param_id(p)  # 获取参数的ID
                    # as some model have trainable parameters but skipped in training,
                    # their backward hooks in self.create_reduce_and_remove_grad_hooks() will not run,
                    # so they have no norm_for_param_grads
                    # 一些模型可能有可训练的参数但在训练中被跳过,
                    # 它们在self.create_reduce_and_remove_grad_hooks()中的反向钩子将不会运行,
                    # 所以它们没有norm_for_param_grads
                    if param_id in self.norm_for_param_grads:  # 如果参数ID在norm_for_param_grads中
                        param_norm = self.norm_for_param_grads[param_id]  # 获取参数的范数
                        total_norm += param_norm.item()**2  # 将参数的范数的平方累加到总范数
                    else:
                        # As unused parameters in modules may not be expected sometimes,
                        # add an explicit error msg when it occurred and an option to
                        # avoid the error
                        # 有时,模块中未使用的参数可能是预料之外的,
                        # 当出现这种情况时,添加一个明确的错误消息,并提供一个选项来避免该错误
                        assert self.ignore_unused_parameters, """
                            This assert indicates that your module has parameters that
                            were not used in producing loss.
                            You can avoid this assert by
                            (1) enable ignore_unused_parameters option in zero_optimization config;
                            (2) making sure all trainable parameters and `forward` function
                                outputs participate in calculating loss.
                        """
            # Sum across all model parallel GPUs.
            # 跨所有模型并行的GPU进行求和
            total_norm_cuda = get_accelerator().FloatTensor([float(total_norm)])
            dist.all_reduce(total_norm_cuda, op=dist.ReduceOp.SUM, group=self.dp_process_group)
    
            self._model_parallel_all_reduce(tensor=total_norm_cuda, op=dist.ReduceOp.SUM)
    
            total_norm = total_norm_cuda[0].item()**(1. / norm_type)  # 计算总范数的开方,即L2范数
    
            if total_norm == float('inf') or total_norm == -float('inf') or total_norm != total_norm:  # 如果总范数为无穷或者不是数字
                total_norm = -1  # 将总范数设置为-1
    
            return total_norm  # 返回总范数
    

    这段代码是在计算模型参数的L2范数,主要用于CPU离线计算中。L2范数(或欧几里得范数)是指向量元素平方和的平方根。

    具体步骤如下:

    1. 初始化总的范数(total_norm)为0。
    2. 遍历所有的参数(params),对每个参数:
      • 如果参数已经被复制(在流水线并行中可能发生),则跳过当前参数;
      • 否则,获取参数的id,如果参数在norm_for_param_grads(保存了每个参数的范数)中,就获取它的范数并加到总范数上,如果没有在norm_for_param_grads中,就检查是否设置了忽略未使用的参数,如果没有设置,会触发一个断言错误。
    3. 使用分布式操作all_reduce将所有并行计算设备上的total_norm求和。
    4. 计算平方根以得到L2范数。
    5. 检查计算出的L2范数是否为无穷大或非数字,如果是,则将其设置为-1。
    6. 返回L2范数。

    这个函数的主要用途是在训练神经网络时,用来监控参数的变化大小。如果梯度范数过大或者过小,可能会导致训练过程不稳定,甚至无法收敛。

  22. copy_grads_in_partition: 在分区中复制梯度。

    def copy_grads_in_partition(self, param):
        # 检查是否启用了CPU offload
        if self.cpu_offload:
            # 如果梯度累积步数大于1
            if self.gradient_accumulation_steps > 1:
                # 从GPU异步地累积梯度到CPU
                self.async_accumulate_grad_in_cpu_via_gpu(param)
    
            # 如果当前步骤是梯度累积的边界
            if self.is_gradient_accumulation_boundary:
                # 在GPU中设置参数梯度的范数
                self.set_norm_for_param_grad_in_gpu(param)
    
                # 更新参数梯度的溢出跟踪器
                self.update_overflow_tracker_for_param_grad(param)
    
                # 从GPU异步地复制梯度到fp32缓冲区
                self.async_inplace_copy_grad_to_fp32_buffer_from_gpu(param)
    
            return
    
        # 如果分区中没有梯度
        if self.grads_in_partition is None:
            self.grads_in_partition_offset = 0
            total_size = 0
            # 计算分区中所有参数的总元素数量
            for group in self.params_in_partition:
                for param_in_partition in group:
                    total_size += param_in_partition.numel()
    
            # 打印复制梯度前的内存使用情况
            see_memory_usage(f"复制{total_size}个梯度到分区前的内存使用情况")
            # 创建一个新的空tensor用于存储梯度,大小为total_size,数据类型与当前对象一致,设备为当前设备
            self.grads_in_partition = torch.empty(int(total_size),
                                                  dtype=self.dtype,
                                                  device=get_accelerator().current_device_name())
            # 打印复制梯度后的内存使用情况
            see_memory_usage(f"复制{total_size}个梯度到分区后的内存使用情况")
    
        # 获取待进行聚合操作的梯度
        grad_reduc = self.get_gradient_for_reduction(param)
        # allreduce缓冲区将被重写,将分区中的梯度复制到新的缓冲区
        new_grad_tensor = self.grads_in_partition.view(-1).narrow(0, self.grads_in_partition_offset, param.numel())
        # 使用原始的梯度更新新的梯度tensor
        new_grad_tensor.copy_(grad_reduc.view(-1))
        # 更新待进行聚合操作的梯度的数据
        grad_reduc.data = new_grad_tensor.data.view_as(grad_reduc)
        # 更新分区的梯度偏移量
        self.grads_in_partition_offset += param.numel()
    

它在深度学习训练过程中处理梯度。它是梯度累积,梯度归一化,梯度溢出跟踪,以及梯度的GPU到CPU转移的一部分。此外,还进行了一些针对性能优化的工作,例如异步操作和内存管理。下面是这个函数的具体工作:

  • 梯度累积和梯度复制(CPU offload):如果启用了CPU offload,就需要将梯度从GPU异步累积到CPU。如果设定了梯度累积步数大于1,会使用异步方式从GPU累积梯度到CPU。如果是梯度累积的边界,首先在GPU中为参数梯度设置范数,然后更新参数梯度的溢出跟踪器,最后异步地从GPU将梯度复制到fp32缓冲区。
  • 创建和更新梯度分区:如果没有启用CPU offload,则在分区中创建和更新梯度。如果该分区中没有梯度,则创建一个新的空的tensor来存储梯度,并记录其大小。然后,获取待减少的梯度,并将它们复制到新的缓冲区,更新新的梯度tensor,并更新待减少的梯度的数据,最后更新分区的梯度偏移量。
  1. reduce_ipg_grads: reduce-IPG梯度。

    def reduce_ipg_grads(self):
            # 如果梯度是连续的
            if self.contiguous_gradients:
                # 如果存在超大参数需要进行梯度汇总
                if self.extra_large_param_to_reduce is not None:
                    # 确保只有一个参数在 ipg bucket 中,否则会出现问题
                    assert len(self.params_in_ipg_bucket) == 1, "more than 1 param in ipg bucket, this shouldn't happen"
                    # 获取该参数的id
                    _, _, param_id = self.params_in_ipg_bucket[0]
                    # 确保 ipg bucket 中的参数和 extra-large 参数匹配
                    assert self.get_param_id(self.extra_large_param_to_reduce
                                             ) == param_id, "param in ipg bucket does not match extra-large param"
                    # 获取需要进行汇总的梯度
                    extra_large_grad_reduce = self.get_gradient_for_reduction(self.extra_large_param_to_reduce)
                    # 对梯度进行平均处理
                    self.average_tensor(extra_large_grad_reduce.view(-1))
                    # 清空 extra_large_param_to_reduce
                    self.extra_large_param_to_reduce = None
                else:
                    # 对 ipg buffer 的梯度进行平均处理
                    self.average_tensor(self.ipg_buffer[self.ipg_index])
            else:
                # fallback 策略,对 grads_in_ipg_bucket 进行汇总
                self.buffered_reduce_fallback(None,
                                              self.grads_in_ipg_bucket,
                                              elements_per_buffer=self.elements_in_ipg_bucket)
    
            # 根据是否开启 overlap_comm 和 cpu_offload 选择合适的 stream
            if self.overlap_comm:
                stream = self.reduction_stream
            elif self.cpu_offload:
                # 注意:copy_grad_stream 被禁用了,因为它会和 reduce 产生冲突,这会影响性能,应该修复这个问题
                # get_accelerator().synchronize()
                # stream = self.copy_grad_stream
                stream = get_accelerator().current_stream()
            else:
                stream = get_accelerator().current_stream()
    
            # 在选定的 stream 中执行以下操作
            with get_accelerator().stream(stream):
                for _, param, param_id in self.params_in_ipg_bucket:
                    # 确保该参数没有被汇总过,因为当前不支持多次梯度汇总
                    assert self.params_already_reduced[param_id] == False, \
                        f"The parameter {param_id} has already been reduced. \
                        Gradient computed twice for this partition. \
                        Multiple gradient reduction is currently not supported"
                    # 标记该参数已经被汇总
                    self.params_already_reduced[param_id] = True
    
                    # 如果需要对梯度进行分区
                    if self.partition_gradients:
                        if not self.is_param_in_current_partition[param_id]:
                            if self.overlap_comm and self.contiguous_gradients is False:
                                # 在下一次梯度汇总过程中清空其他分区的梯度
                                # 这样可以避免在汇总完成之前就清空他们
                                if self.previous_reduced_grads is None:
                                    self.previous_reduced_grads = []
                                self.previous_reduced_grads.append(param)
                            else:
                                # 清空该参数的梯度属性
                                self.clear_grad_attribute(param)
                        elif self.contiguous_gradients:
                            # 如果梯度是连续的,复制当前分区的梯度
                            self.copy_grads_in_partition(param)
                    else:  # zero stage 1 - 只分区优化器状态
                        if self.contiguous_gradients and self.is_param_in_current_partition[param_id]:
                            # 如果梯度是连续的,复制当前分区的梯度
                            self.copy_grads_in_partition(param)
    
            # 清空 ipg_bucket 和相关信息
            self.grads_in_ipg_bucket = []
            self.params_in_ipg_bucket = []
            self.ipg_bucket_has_moe_params = False
            self.elements_in_ipg_bucket = 0
    

    这段代码是在处理深度学习模型中的参数梯度,具体是在分布式训练中对于梯度进行汇总和处理的过程。

    以下是这段代码的一些主要功能:

    • 处理大型参数的梯度:如果存在超大的参数需要进行梯度汇总,该代码会获取这个参数的梯度,进行平均处理,然后清空这个参数,以防止它再次被处理。
    • 处理连续的梯度:如果梯度是连续的,该代码会将 IPG buffer 中的梯度进行平均处理。
    • 使用 fallback 策略:如果不满足上述条件,该代码会使用一种 fallback 策略,对 IPG bucket 中的梯度进行汇总。
    • 选择合适的流:代码会根据是否开启 overlap_comm 和 cpu_offload 来选择合适的流来进行操作。
    • 梯度分区:如果需要对梯度进行分区,代码会根据是否梯度连续和参数是否在当前分区进行不同的处理。如果梯度不连续,会在下一次梯度汇总过程中清空其他分区的梯度;如果梯度连续,会复制当前分区的梯度。
    • 清空IPG bucket:在完成所有操作之后,代码会清空 IPG bucket 和相关信息,为下一次操作做准备。
  2. reduce_ready_partitions_and_remove_grads: 减少已准备好的分区并删除梯度。

    def reduce_ready_partitions_and_remove_grads(self, param, i):
        # 如果满足以下两个条件之一,执行操作:
        # 1. 需要对梯度进行分区处理
        # 2. 当前处于梯度累积的边界
        # 该操作包括:
        # 对独立的参数梯度分区桶进行归约,并移除梯度
        if self.partition_gradients or self.is_gradient_accumulation_boundary:
            self.reduce_independent_p_g_buckets_and_remove_grads(param, i)
    
  3. zero_reduced_gradients: 将减少的梯度设置为零。

    def zero_reduced_gradients(self, partition_id, i):
        # 定义一个函数,用于检查与当前参数相关的所有分区是否已经完成了梯度的计算
        def are_all_related_partitions_reduced(params_id):
            # 遍历所有与当前参数相关的分区ID
            for partition_id in self.param_to_partition_ids[i][params_id]:
                # 如果有一个分区还没有完成计算,就返回False
                if not self.is_partition_reduced[i][partition_id]:
                    return False
            # 如果所有相关分区都完成了计算,就返回True
            return True
    
        # 遍历当前分区中所有参数的ID
        for params_id in self.is_grad_computed[i][partition_id]:
            # 如果与当前参数ID相关的所有分区都已经完成了梯度的计算
            if are_all_related_partitions_reduced(params_id):
                # 将当前参数的梯度设为None,这样可以节省存储空间
                self.param_dict[params_id].grad = None
    
  4. flatten_and_print: 压平并打印。

    def flatten_and_print(self, message, tensors, start=0, n=5):
        # 首先,我们将输入的多维张量(tensors)压缩为一维
        flatten_tensor = self.flatten(tensors)
    
        # 定义一个函数,负责打印压缩后的张量的一部分
        def print_func():
            # 这里我们调整了张量的维度以便进行打印,然后使用narrow方法取出要打印的部分
            # narrow函数的第一个参数是压缩的维度(这里是0,即第一维),第二个参数是开始的索引(start),第三个参数是长度(n)
            logger.info(flatten_tensor.contiguous().view(-1).narrow(0, start, n))
    
        # 最后在一个序列执行环境中执行这个打印函数,并传入一个消息(message)
        # 这个消息通常用于指示这次打印的含义,例如"打印压缩后的张量的前5个元素"
        self.sequential_execution(print_func, message)
    

    这段代码定义了一个flatten_and_print方法,这个方法的主要功能是将输入的多维张量(tensors)压平(即变为1维),然后打印出压平后的张量的一部分。

    具体来说,这个方法做的事情包括:

    1. 压平张量:使用self.flatten(tensors)来将输入的多维张量压平为一维。

    2. 定义打印函数:定义了一个名为print_func的内部函数,这个函数的任务是打印一部分压平后的张量。这里使用了narrow方法来选择压平后张量中的一个区间,然后用logger.info将其打印出来。

    3. 执行打印:使用self.sequential_execution(print_func, message)来执行打印函数。这个sequential_execution方法通常是在一个序列执行环境中运行给定的函数,并将message作为额外的信息打印出来。

    总的来说,这段代码的主要用途是用于在处理张量数据时,方便地查看张量的一部分数据,以便进行调试或者了解数据的基本情况。

  5. get_grads_to_reduce: 获取要reduce的梯度。

    def get_grads_to_reduce(self, i, partition_id):
        # 定义一个函数,用于获取可以reduce的梯度部分
        def get_reducible_portion(key):
            # 从参数字典中获取梯度
            grad = self.param_dict[key].grad
            # 获取梯度的总元素数量
            total_elements = grad.numel()
            # 获取开始的偏移量
            start = self.grad_start_offset[i][partition_id][key]
            # 计算元素数量
            num_elements = min(total_elements - start,
                               self.partition_size[i] - self.grad_partition_insertion_offset[i][partition_id][key])
            # 如果不进行正确性测试
            if not pg_correctness_test:
                # 如果元素数量等于总元素数量,返回梯度
                if num_elements == total_elements:
                    return grad
                else:
                    # 否则,返回指定范围内的梯度
                    return grad.contiguous().view(-1).narrow(0, int(start), int(num_elements))
            else:
                # 如果进行正确性测试
                if num_elements == total_elements:
                    return grad.clone()
                else:
                    # 返回克隆并指定范围内的梯度
                    return grad.clone().contiguous().view(-1).narrow(0, int(start), int(num_elements))
    
        # 创建一个空列表,用于存储要redeuce的梯度
        grads_to_reduce = []
        # 遍历已计算梯度的键
        for key in self.is_grad_computed[i][partition_id]:
            # 获取可以reduce的梯度部分
            grad = get_reducible_portion(key)
            # 将梯度添加到列表中
            grads_to_reduce.append(grad)
        # 返回reduce的梯度列表
        return grads_to_reduce
    

    它的主要目的是从深度学习模型的参数中获取梯度部分,这些梯度部分是可以被reduce的。

    • 该方法首先定义了一个内部函数get_reducible_portion,这个函数用于根据给定的键来从模型参数的梯度中获取可reduce的部分。这部分可能是整个梯度,或者是梯度的一部分,这取决于梯度的元素数量和开始的偏移量。
  • 然后,该方法创建了一个空列表grads_to_reduce,用于存储可以被reduce的梯度。
    • 接着,该方法遍历self.is_grad_computed[i][partition_id]中的所有键,这些键代表已经计算了梯度的参数。对于每个键,它都调用get_reducible_portion来获取可reduce的梯度部分,并添加到grads_to_reduce列表中。
  • 最后,该方法返回grads_to_reduce列表,即包含了所有可以被reduce的梯度部分的列表。

"Reduce"在这里通常是指在所有设备上对梯度进行求和,以便在分布式训练中同步梯度更新。这是分布式深度学习中的一个关键步骤,可以确保所有设备上的模型参数保持一致。

  1. sequential_execution: 顺序执行。

    def sequential_execution(self, function, message, group=None):
        # 如果没有指定分组,使用当前对象的dp_process_group作为默认分组
        if group is None:
            group = self.dp_process_group
        # 如果当前进程的等级(rank)是0,记录日志信息
        if dist.get_rank(group=group) == 0:
            logger.info(message)
        # 遍历当前分组中的每个进程
        for id in range(dist.get_world_size(group=group)):
            # 如果当前进程的等级(rank)等于循环变量id,执行传入的函数
            if id == dist.get_rank(group=group):
                function()
            # 确保所有进程都执行到这一点后才能继续往下执行
            dist.barrier(group=group)
    

    这段代码主要用于在分布式计算环境中,按照进程的等级(rank)顺序序列化地执行某个函数。每个进程都会执行这个函数,但是执行的顺序是由它们的等级(rank)决定的。在所有进程执行完这个函数之后,它们会在dist.barrier(group=group)这一行代码处同步,确保所有进程都执行到这个点后才能继续往下执行。

  2. set_none_gradients_to_zero: 将无梯度设置为零。

    def set_none_gradients_to_zero(self, i, partition_id):
        # 遍历在指定分区中的所有参数ID
        for param_id in self.is_grad_computed[i][partition_id]:
            # 从字典中获取该ID对应的参数对象
            param = self.param_dict[param_id]
            # 如果该参数的导数(梯度)为None
            if param.grad is None:
                # 则将其设置为与参数相同形状的零张量
                param.grad = torch.zero_like(param)
    
  3. allreduce_bucket: allreduce操作,是基于桶的。

    def allreduce_bucket(self, bucket, rank=None, log=None):
            # 设置初始rank为None
            rank = None
            # 将bucket中的tensor扁平化
            tensor = self.flatten(bucket)
    
            # 待进行allreduce操作的tensor
            tensor_to_allreduce = tensor
    
            # 在进行pg_correctness_test的情况下,通信数据类型设置为float32
            # 否则,使用预设的通信数据类型
            if pg_correctness_test:
                communication_data_type = torch.float32
            else:
                communication_data_type = self.communication_data_type
    
            # 如果通信数据类型与tensor的数据类型不一致,将tensor转为通信数据类型
            if communication_data_type != tensor.dtype:
                tensor_to_allreduce = tensor.to(communication_data_type)
    
            # 将待allreduce的tensor除以进程组的大小,以进行平均操作
            tensor_to_allreduce.div_(dist.get_world_size(group=self.dp_process_group))
    
            if rank is None:
                # 执行allreduce操作,所有进程共享数据
                dist.all_reduce(tensor_to_allreduce, group=self.dp_process_group)
            else:
                # 获取全局rank
                global_rank = dist.get_global_rank(self.dp_process_group, rank)
                # 执行reduce操作,将数据发送到指定的进程
                dist.reduce(tensor_to_allreduce, global_rank, group=self.dp_process_group)
    
            # 如果通信数据类型与tensor的数据类型不一致,并且tensor不等于tensor_to_allreduce
            # 在rank为None或者等于当前进程的rank的情况下,将tensor_to_allreduce的值复制给tensor
            if communication_data_type != tensor.dtype and tensor is not tensor_to_allreduce:
                if rank is None or rank == dist.get_rank(group=self.dp_process_group):
                    tensor.copy_(tensor_to_allreduce)
    
            # 返回处理后的tensor
            return tensor
    

    这段代码是在进行分布式处理中的集体通信操作,其主要目的是实现多个进程间的数据共享和同步。

    这个allreduce_bucket函数的主要步骤如下:

    1. 扁平化处理:将输入的bucket参数中的tensor数据扁平化,即将多维数组转换为一维数组。

    2. 数据类型转换:根据是否进行pg_correctness_test测试,选择通信的数据类型。如果与tensor的数据类型不一致,则将tensor转换为相应的数据类型。

    3. 数据平均:将待allreduce的tensor除以进程组的大小,以进行平均操作。这是准备进行all-reduce操作的步骤,目的是将多个进程的数据合并并平均。

    4. 通信操作:执行all_reducereduce操作。如果rank参数为None,则对所有进程进行all_reduce操作,即所有进程共享数据;如果rank不为None,则使用reduce操作,将数据发送到指定的进程。

    5. 数据类型还原和数据复制:如果通信过程中做了数据类型转换,且tensortensor_to_allreduce不是同一个对象,那么在rankNone或等于当前进程的rank的情况下,将tensor_to_allreduce的值复制回原tensor

    6. 返回处理后的数据:将处理后的tensor返回。

    总的来说,这段代码主要用于分布式计算

  4. _clear_previous_reduced_grads: 清除先前reduce的梯度。

    def _clear_previous_reduced_grads(self):
        # 如果之前的梯度不为None,即之前计算过梯度
        if self.previous_reduced_grads is not None:
            # 遍历每一个之前计算的梯度
            for param in self.previous_reduced_grads:
                # 清除对应的梯度信息
                self.clear_grad_attribute(param)
            # 清空之前的梯度列表,准备下一次计算
            self.previous_reduced_grads = None
    
  5. allreduce_and_copy: allreduce操作和复制。

    def allreduce_and_copy(self, small_bucket, rank=None, log=None):
            # 如果启用了overlap_comm,进行通信和计算的重叠
            if self.overlap_comm:
                get_accelerator().synchronize()  # 确保所有设备上的运行都已完成
                # 清除其他分区之前reduce的梯度,这是安全的
                self._clear_previous_reduced_grads()
                stream = self.reduction_stream  # 使用专门的流进行reduce操作
            else:
                # 如果没有启用overlap_comm,使用当前设备的当前流
                stream = get_accelerator().current_stream()
    
            # 使用指定的流
            with get_accelerator().stream(stream):
                # 对small_bucket进行allreduce操作,然后返回结果
                allreduced = self.allreduce_bucket(small_bucket, rank=rank, log=log)
                # 如果rank是None(即没有指定),或者rank等于当前进程的rank
                # 就对small_bucket中的每个buf和对应的synced进行copy操作
                if rank is None or rank == dist.get_rank(group=self.dp_process_group):
                    for buf, synced in zip(small_bucket, self.unflatten(allreduced, small_bucket)):
                        buf.copy_(synced)  # 将synced的内容复制到buf中
    

    这段代码是在处理分布式深度学习中的梯度聚合与更新。具体来说,它主要用于进行 All-reduce 操作来同步不同计算设备之间的参数梯度,并将结果复制到原始数据结构中。

    以下是代码的详细解释:

  • 函数allreduce_and_copy定义了一个名为small_bucket的参数,这通常是一个包含了多个参数梯度的列表或者其他数据结构。
  • 如果overlap_comm(通信和计算的重叠)被启用,它将首先同步所有设备以确保所有设备上的运行都已完成,然后清除其他分区之前减少的梯度,并在特定的reduction_stream中执行All-reduce操作。如果overlap_comm未被启用,它将在当前设备的当前流中执行All-reduce操作。
  • allreduce_bucket函数对small_bucket进行All-reduce操作,将所有设备上的相同参数的梯度进行聚合。
  • 如果rank(设备的编号)是None,或者rank等于当前进程的rank,那么它将对small_bucket中的每个buf和对应的synced进行copy_操作,即将聚合后的梯度复制回原来的位置。

总的来说,这段代码确保了在分布式深度学习训练中,梯度能够在不同的设备之间正确同步,并且更新到各自设备的模型参数中。

  1. allreduce_no_retain: allreduce且不保留。

    def allreduce_no_retain(self, bucket, numel_per_bucket=500000000, rank=None, log=None):
        # 初始化一个空的小桶
        small_bucket = []
        # 初始化元素数量为0
        numel = 0
        # 对bucket中的每一个tensor进行遍历
        for tensor in bucket:
            # 将当前tensor添加到小桶中
            small_bucket.append(tensor)
            # 计算小桶中所有tensor的元素总数
            numel = numel + tensor.numel()
            # 如果小桶中的元素总数超过了设定的阈值,那么就对小桶进行allreduce_and_copy操作
            # 然后清空小桶,为接下来的tensors做准备
            if numel > numel_per_bucket:
                self.allreduce_and_copy(small_bucket, rank=rank, log=None)
                small_bucket = []
        # 如果bucket中的所有tensors都已经被处理完,但是小桶中还剩下一些tensors没有被处理
        # 就对这些剩下的tensors进行allreduce_and_copy操作
        if len(small_bucket) > 0:
            self.allreduce_and_copy(small_bucket, rank=rank, log=log)
    

    这段代码是在做一个称为"allreduce"的操作。

    函数allreduce_no_retain的主要目标是从一个大的数据桶(bucket)中取出数据,然后分组到一个小的数据桶(small_bucket)中,每个小数据桶的元素数量不超过numel_per_bucket。然后,对每个小数据桶执行allreduce_and_copy操作。这个操作可以是任何形式的reduce操作,比如求和、求平均等。在这个操作后,每个参与者都会有一个完整的结果,这个结果是所有参与者的数据的reduce结果。

    这种方法的一个优点是可以在大数据集上进行reduce操作,而不会一次性占用太多内存。通过控制numel_per_bucket,可以调整每次执行reduce操作的数据量,这样可以在内存使用和计算效率之间找到一个平衡。

    这里的allreduce_and_copy函数是前面介绍的那个

  2. buffered_reduce_fallback: 缓冲reduce,fallback那个钩子(也许?fallback,回调,也许在某个地方调用了)。

    def buffered_reduce_fallback(self, rank, grads, elements_per_buffer=500000000, log=None):
        # 将 grads 分为半精度浮点数和双精度浮点数两部分
        split_buckets = split_half_float_double(grads)
    
        # 遍历每一个 bucket
        for i, bucket in enumerate(split_buckets):
            # 进行全局归约操作,这是一种并行算法,用于在所有进程中累加每个进程的输入值
            # 并将结果返回给所有进程
            self.allreduce_no_retain(bucket, numel_per_bucket=elements_per_buffer, rank=rank, log=log)
    

    这段代码的主要目的是对输入的梯度进行全局归约操作。在这个过程中,每个进程将其输入值加在一起,并将结果返回给所有进程。

  3. get_data_parallel_partitions: 获取数据并行分区。

    def get_data_parallel_partitions(self, tensor, group_id):
        partitions = [] # 初始化一个空列表,用于存储每个分区的数据
    
        dp = dist.get_world_size(group=self.real_dp_process_group[group_id]) 
        # 获取分布式处理(dp)的大小,即有多少个处理单元参与分布式计算
    
        total_num_elements = tensor.numel() 
        # 计算张量(tensor)中的总元素数量
    
        base_size = total_num_elements // dp 
        # 计算每个处理单元应分配的基本元素数量,这里使用了整数除法
    
        remaining = total_num_elements % dp 
        # 计算不能被均匀分配的剩余元素数量
    
        start = 0 
        # 初始化起始索引为0,这个索引表示当前处理的张量部分的起始位置
    
        for id in range(dp): 
            # 遍历每个处理单元
    
            partition_size = base_size 
            # 默认每个处理单元分配的元素数量为base_size
    
            if id < remaining: 
                # 如果当前处理单元的id小于剩余元素数量remaining,那么就给这个处理单元多分配一个元素
                partition_size = partition_size + 1 
    
            partitions.append(tensor.narrow(0, start, partition_size)) 
            # 使用narrow函数从张量中抽出一部分数据,0表示要操作的维度(这里是第一维),start表示开始的索引,partition_size表示长度
            # 抽出的部分数据作为一个分区,添加到partitions列表中
    
            start = start + partition_size 
            # 更新开始索引,准备处理下一个分区
    
        return partitions
        # 返回分区列表,列表中的每个元素都是一个张量,表示一个分区的数据
    

    这段代码的功能是将一个给定的张量(tensor)数据平均分配到多个分布式处理单元(dp)中。它首先计算出每个处理单元应该分配到的基本元素数量,然后如果有剩余的元素,会优先分配给编号较小的处理单元。

    这种数据分区的方法通常在分布式计算中使用,尤其是在处理大规模数据或者需要并行计算的场景下。例如,在深度学习训练中,我们可能会将一个大的数据集分成多个小的分区,并将每个分区分配给一个处理单元(如一个GPU或一个CPU核心)进行处理,这样可以大大加速训练过程。

    每个处理单元处理完自己的数据分区后,可能会将结果回传给主节点,进行聚合或者其他进一步的处理。这就是分布式计算的基本思想:将一个大问题分解成多个小问题,每个小问题在一个处理单元上独立处理,最后再将各个小问题的结果合并,得到最终的解答。

  4. get_partition_info: 获取分区信息。

    def get_partition_info(self, tensor_list, partition_size, partition_id):
            # 初始化两个列表,用于存储在分区内的参数和不在分区内的参数
            params_in_partition = []
            params_not_in_partition = []
    
            # 计算分区的起始和结束索引
            start_index = partition_size * partition_id
            end_index = partition_size * (partition_id + 1)
    
            # 初始化当前索引和第一个偏移值
            current_index = 0
            first_offset = 0
    
            # 遍历tensor列表
            for tensor in tensor_list:
                # 获取tensor的元素数量
                tensor_size = tensor.numel()
    
                # 如果当前索引在分区的范围内,将tensor添加到params_in_partition列表中
                if start_index <= current_index < end_index:
                    params_in_partition.append(tensor)
    
                # 如果当前索引小于分区起始索引,且分区起始索引在tensor范围内,将tensor添加到params_in_partition列表中
                elif current_index < start_index < (current_index + tensor_size):
                    params_in_partition.append(tensor)
    
                    # 确保first_offset只被设置一次,因为这必须是分区中的第一个tensor
                    assert (first_offset == 0
                            ), "This can happen either zero or only once as this must be the first tensor in the partition"
                    first_offset = start_index - current_index
    
                # 否则,将tensor添加到params_not_in_partition列表中
                else:
                    params_not_in_partition.append(tensor)
    
                # 更新当前索引
                current_index = current_index + tensor_size
    
            # 返回在分区内的参数、不在分区内的参数以及第一个偏移值
            return params_in_partition, params_not_in_partition, first_offset
    

    这段代码是一个函数 get_partition_info,它将一个大的张量(Tensor)列表划分为多个小的分区。其主要应用场景是在处理大规模的深度学习模型时,由于模型太大,无法在一个设备上完全加载,因此需要将其切分为多个小的分区,分别在不同的设备上进行处理。

    具体来说,函数的参数包括:

    • tensor_list:一个张量列表,每个张量包含一些参数。
    • partition_size:每个分区应该包含的参数数量。
    • partition_id:当前分区的ID。

    函数需要做的事情是:

    • 通过计算,得到当前分区的起始和结束索引。
    • 遍历整个张量列表,根据每个张量的大小和位置,将其分配到正确的分区中。
    • 返回两个列表,一个包含在当前分区内的张量,另一个包含不在当前分区内的张量,以及第一个偏移值,这是当前分区内第一个张量的起始位置。

    这样,我们就可以通过多次调用这个函数,将整个大的张量列表划分为多个小的分区,然后在不同的设备上并行处理这些分区,从而提高计算效率。

  5. zero_grad: 将所有模型参数的梯度设置为零。

    def zero_grad(self, set_to_none=True):
        """
        清零 FP16 参数的梯度。
        """
        # FP32 的梯度永远不应该存在。
        # 出于速度的考虑,默认情况下将模型 FP16 的梯度设置为 None
        # 清零所有指向梯度张量的指针
        for group in self.bit16_groups:
            for p in group:
                if set_to_none:
                    p.grad = None  # 在尾声和步骤中
                    p.grad_accum = None
                else:
                    if p.grad is not None:
                        p.grad.detach_()  # 分离梯度
                        p.grad.zero_()  # 清零梯度
    

    这段代码实现的是一个名为 zero_grad 的方法,它用于清除模型中所有参数的梯度。这在训练神经网络时非常常见,因为在每一个训练步骤(batch)中,你可能需要计算每个参数的梯度,然后使用这些梯度进行参数更新。然后在下一个步骤开始之前,你需要清零所有的梯度,否则新的梯度会和旧的梯度累加起来,这会导致错误的结果。

    该函数的主要步骤如下:

  • 遍历所有位于 bit16_groups 的参数组。这个变量可能是一个包含了模型所有需要优化的参数的列表。
  • 对于每个参数 p,根据 set_to_none 参数的值来决定如何清零梯度。如果 set_to_noneTrue,则将该参数的梯度直接设为 None。如果为 False,则先检查梯度是否存在,如果存在,则先调用 detach_() 方法来分离梯度,然后调用 zero_() 方法来清零梯度。
  1. _model_parallel_all_reduce: 执行模型并行的allreduce操作。

    def _model_parallel_all_reduce(self, tensor, op):
            """在模型并行组内执行allreduce操作,如果有的话。
            """
            # 如果模型并行组不存在或模型并行世界大小等于1
            if self.model_parallel_group is None or self.model_parallel_world_size == 1:
                pass  # 不执行任何操作
            else:
                # 否则,使用 dist.all_reduce 函数在模型并行组内对 tensor 进行allreduce操作
                dist.all_reduce(tensor=tensor, op=op, group=self.model_parallel_group)
    
  2. get_grad_norm_direct: 直接获取梯度的范数(L2范数)。

    def get_grad_norm_direct(self, gradients, params, norm_type=2):
            """
    		计算并裁剪参数的梯度范数。
    
            本函数是从torch.nn.utils.clip_grad.clip_grad_norm_中调整而来,
            其中加入了处理模型并行参数的功能。注意,梯度将在原地被修改。
    
            参数:
                gradients (Iterable[Tensor] or Tensor): 需要进行梯度范数计算的张量的迭代器或单个张量
                params (Iterable[Tensor] or Tensor): 需要进行梯度范数计算的参数的迭代器或单个张量
                norm_type (float or int): 使用的p-范数类型。可以是 ``'inf'``表示无穷范数。
    
            返回:
                参数的总体范数(视为单个向量)。
            """
            norm_type = float(norm_type)
            if norm_type == inf:
                # 找到所有梯度中绝对值最大的
                total_norm = max(g.data.abs().max() for g in gradients)
                # 将梯度的最大值转为张量
                total_norm_cuda = get_accelerator().FloatTensor([float(total_norm)])
                # 使用all_reduce进行跨设备的梯度同步,取最大值
                dist.all_reduce(total_norm_cuda, op=dist.ReduceOp.MAX, group=self.dp_process_group)
    
                # 在所有GPUs之间取最大值。
                self._model_parallel_all_reduce(tensor=total_norm_cuda, op=dist.ReduceOp.MAX)
                total_norm = total_norm_cuda[0].item()
            else:
                total_norm = 0.0
                for g, p in zip(gradients, params):
                    # 管道并行化可能会复制参数。避免多次计数。
                    if hasattr(p, PIPE_REPLICATED) and p.ds_pipe_replicated:
                        continue
                    # 如果参数是模型并行的参数,或者当前设备是主设备,则计算该参数的梯度范数
                    if is_model_parallel_parameter(p) or (self.model_parallel_rank == 0):
                        param_norm = g.data.double().norm(2)
                        total_norm += param_norm.item()**2
                # 将梯度的平方和转为张量
                total_norm_cuda = get_accelerator().FloatTensor([float(total_norm)])
                # 使用all_reduce进行跨设备的梯度同步,取和
                dist.all_reduce(total_norm_cuda, op=dist.ReduceOp.SUM, group=self.dp_process_group)
    
                # 在所有模型并行的GPUs之间进行求和
                self._model_parallel_all_reduce(tensor=total_norm_cuda, op=dist.ReduceOp.SUM)
    
                # 计算梯度的总体范数
                total_norm = total_norm_cuda[0].item()**(1. / norm_type)
    
            # 如果总体范数是无穷大,负无穷大,或不是一个数,则将其设置为-1
            if total_norm == float('inf') or total_norm == -float('inf') or total_norm != total_norm:
                total_norm = -1
    
            return total_norm
    ​```
    

    这段代码是用于计算并剪辑参数的梯度范数,这个函数是从torch.nn.utils.clip_grad.clip_grad_norm_中调整而来的,其中加入了处理模型并行参数的功能。

    梯度范数是在训练神经网络时常用的一个概念,它通常用来衡量梯度的大小。计算梯度范数的目的有两个:一是用来检查网络是否正在经历"梯度爆炸"或"梯度消失"的问题;二是用于梯度裁剪,即限制梯度的大小,以防止在训练过程中出现数值不稳定的问题。

    在这个函数中,首先确定了范数类型(无穷范数或其他),然后根据范数类型使用不同的方式计算梯度范数。如果参数是模型并行的参数,或者当前设备是主设备,则计算该参数的梯度范数。

    函数返回的是参数的总体范数。如果总体范数是无穷大,负无穷大,或不是一个数,则将其设置为-1。

    这个函数中使用了dist.all_reduceself._model_parallel_all_reduce,它们是用来在多个设备之间进行梯度同步的,以确保所有设备上的模型参数保持一致。

    总体来说,这个函数主要用于计算并同步多个设备上的模型参数的梯度范数。

  3. get_flat_partition: 获取拉平的分区。

    def get_flat_partition(self, tensor_list, first_offset, partition_size, dtype, device, return_tensor_list=False):
            # 初始化一个空列表,用于存储处理后的tensor
            flat_tensor_list = []
            # 记录当前已处理tensor的元素数量
            current_size = 0
    
            # 遍历每个传入的tensor
            for i, tensor in enumerate(tensor_list):
                # 获取tensor的梯度属性
                grad_accum = self.get_param_gradient_attribute(tensor)
                # 如果没有梯度属性,用0填充
                if grad_accum is None:
                    grad_accum = torch.zeros_like(tensor, dtype=dtype)
    
                # 将梯度属性作为处理的目标tensor
                tensor = grad_accum
                # 获取tensor的元素数量
                num_elements = tensor.numel()
                tensor_offset = 0
    
                # 对于列表中的第一个tensor,如果有偏移量,根据偏移量调整元素数量和偏移值
                if i == 0 and first_offset > 0:
                    tensor_offset = first_offset
                    num_elements = num_elements - tensor_offset
    
                # 如果当前tensor的元素数量超过了分区大小,调整元素数量以适应分区
                if num_elements > (partition_size - current_size):
                    num_elements = partition_size - current_size
    
                # 根据偏移量和元素数量,获取tensor的视图并添加到列表
                if tensor_offset > 0 or num_elements < tensor.numel():
                    flat_tensor_list.append(tensor.contiguous().view(-1).narrow(0, int(tensor_offset), int(num_elements)))
                else:
                    flat_tensor_list.append(tensor)
    
                # 更新当前已处理的元素数量
                current_size = current_size + num_elements
    
            # 如果当前处理的元素数量小于分区大小,需要填充0
            if current_size < partition_size:
                flat_tensor_list.append(torch.zeros(int(partition_size - current_size), dtype=dtype, device=device))
    
            # 如果需要返回tensor列表,直接返回
            if return_tensor_list:
                return flat_tensor_list
    
            # 否则,返回压平后的单个tensor
            return self.flatten(flat_tensor_list)
    ```
    

    这段代码的主要功能是将输入的tensor列表处理成一个平坦的,即一维的tensor,或者一个包含平坦tensor的列表。它的工作流程是这样的:

    1. 对输入的tensor列表中的每个tensor进行遍历。对于每个tensor,它首先尝试获取tensor的梯度属性。如果没有梯度属性,那么它会创建一个全零的tensor作为梯度。

    2. 对于每个tensor,它计算元素数量(numel)。然后,它检查是否有需要处理的偏移量,例如,如果这是列表中的第一个tensor,并且存在first_offset,那么它会从偏移量开始处理tensor。如果当前tensor的元素数量加上已处理的元素数量超过了指定的分区大小,那么它会减少处理的元素数量以适应分区大小。

    3. 对于每个tensor,它将其处理为一维的(使用.view(-1)方法),并根据需要的元素数量创建一个子tensor(使用.narrow()方法)。这个子tensor被添加到flat_tensor_list中。

    4. 如果在处理完所有的tensor后,已处理的元素数量小于分区大小,那么它会添加一些零来填充剩余的空间。

    5. 最后,根据return_tensor_list参数的值,它要么返回一个包含所有处理过的tensor的列表(return_tensor_list=True),要么返回一个由所有处理过的tensor连接而成的一维tensor(return_tensor_list=False)。

    总的来说,这个函数的目标是将输入的一组tensor整理为一个一维tensor或一组一维tensor,其中每个一维tensor的元素数量不超过指定的分区大小。

  4. free_grad_in_param_list: 释放参数列表中的梯度。

    def free_grad_in_param_list(self, param_list):
            for p in param_list:  # 遍历参数列表中的每一个参数
                p.grad = None  # 将参数的梯度设置为None,表示清除该参数的梯度
                p.grad_accum = None  # 清除累积的梯度
    
  5. reset_cpu_buffers: 重置CPU缓冲区。

    def reset_cpu_buffers(self):
        self.norm_for_param_grads = {}
        # 用于标识是否发生了本地溢出
        self.local_overflow = False
    
  6. set_lr: 设置学习率。

    def set_lr(self, lr):
        # 遍历优化器中的所有参数组
        for param_group in self.optimizer.param_groups:
            # 对每个参数组设置新的学习率
            param_group["lr"] = lr
    
  7. get_lr: 获取当前的学习率。

    def get_lr(self):
        # 这个函数的目的是返回当前的学习率。
        # 从优化器的参数组中获取第一个参数组的学习率
        return self.optimizer.param_groups[0]["lr"]
    
  8. override_loss_scale: 覆盖损失比例。

    def override_loss_scale(self, loss_scale):
            # 如果给定的loss_scale不等于当前的外部loss_scale,我们需要更新它
            if loss_scale != self.external_loss_scale:
                # 使用logger.info打印一条信息,说明正在从原来的外部loss_scale更新到新的loss_scale
                logger.info(f'[deepspeed] setting loss scale from {self.external_loss_scale} -> {loss_scale}')
            # 设置custom_loss_scaler为True,表示我们正在使用自定义的loss_scaler
            self.custom_loss_scaler = True
            # 更新外部loss_scale为给定的loss_scale
            self.external_loss_scale = loss_scale
    
  9. scaled_global_norm: 计算缩放全局范数。

    def scaled_global_norm(self, norm_type=2):
        # 断言:仅支持L2范数
        assert norm_type == 2, "only L2 norm supported"
        # 初始化范数组
        norm_groups = []
        # 遍历位组
        for i, group in enumerate(self.bit16_groups):
            # 获取分区ID
            partition_id = dist.get_rank(group=self.real_dp_process_group[i])
            # 如果进行了CPU卸载
            if self.cpu_offload:
                # 计算并添加对应的梯度范数
                norm_groups.append(self.complete_grad_norm_calculation_for_cpu_offload(self.params_in_partition[i]))
                # 获取单个分区的梯度
                single_grad_partition = self.single_partition_of_fp32_groups[i].grad
            else:
                # 直接获取并添加对应的梯度范数
                norm_groups.append(self.get_grad_norm_direct(self.averaged_gradients[i], self.params_in_partition[i]))
    
        # 如果存在moe层
        if self.has_moe_layers:
            # 平均专家梯度范数
            self._average_expert_grad_norms(norm_groups)
    
        # 注意,get_global_norm函数仅支持l2范数
        # 获取全局范数并返回
        return get_global_norm(norm_list=norm_groups)
    

    这段代码是在计算分布式环境下的全局梯度范数。

    首先,它会遍历所有的16位参数组(bit16_groups),并对每组参数进行处理。对每一组参数,它会根据是否进行了CPU卸载来采取不同的方式计算梯度范数。如果进行了CPU卸载,则会调用self.complete_grad_norm_calculation_for_cpu_offload方法来计算梯度范数;否则,会调用self.get_grad_norm_direct方法直接计算梯度范数。

    然后,如果存在混合专家(moe)层,它会对这些专家层的梯度范数进行平均处理。

    最后,它会调用get_global_norm函数,将之前计算得到的所有梯度范数组合起来,计算并返回全局梯度范数。

    这通常用于深分布式训练环境下。全局梯度范数可以用于梯度裁剪(gradient clipping),防止梯度爆炸或梯度消失,从而帮助改善模型的训练效果。

  10. get_bit16_param_group: 获取16位参数组。

    def get_bit16_param_group(self, group_no):
            # 获取16位参数组中的某个组的分区
            bit16_partitions = self.parallel_partitioned_bit16_groups[group_no]
            # 获取当前分区ID,这是根据分布式处理组中的排名来获取的
            partition_id = dist.get_rank(group=self.real_dp_process_group[group_no])
            # 返回对应分区ID的参数组
            return [bit16_partitions[dist.get_rank(group=self.real_dp_process_group[group_no])]]
    
  11. _optimizer_step: 执行优化器的步骤。

    def _optimizer_step(self, group_no):
            # 获取优化器的参数组
            original_param_groups = self.optimizer.param_groups
            # 将优化器的参数组设置为特定的参数组
            self.optimizer.param_groups = [original_param_groups[group_no]]
            # 这段代码被禁用,因为C++端的复制和同步功能无法正确工作
            #from deepspeed.ops.adam import DeepSpeedCPUAdam
            #if type(self.optimizer) == DeepSpeedCPUAdam and self.dtype == torch.half:
            #    self.optimizer.step(fp16_param_groups=[self.get_bit16_param_group(group_no)])
            #else:
            #    self.optimizer.step()
            # 执行优化器的步进
            self.optimizer.step()
            # 将优化器的参数组还原为原始参数组
            self.optimizer.param_groups = original_param_groups
    
  12. step: 优化器的step步骤,真正执行的方法体。

    def step(self, closure=None):
        """
        不支持闭包。
        """
        self.micro_step_id = -1
    
        see_memory_usage(f"In step before checking overflow") # 在检查溢出之前查看内存使用情况
    
        # 首先计算所有组的规范,以便我们知道是否有溢出
        self.check_overflow()
    
        prev_scale = self.loss_scale
        self._update_scale(self.overflow) # 更新规模
        if self.overflow:
            see_memory_usage('After overflow before clearing gradients') # 溢出后,清除梯度之前,查看内存使用情况
            self.zero_grad(set_to_none=True) # 清除梯度
            if self.cpu_offload:
                self.reset_cpu_buffers() # 重置CPU缓冲区
            else:
                self.averaged_gradients = {}
    
            see_memory_usage('After overflow after clearing gradients') # 溢出后,清除梯度之后,查看内存使用情况
    
            for timer in OPTIMIZER_TIMERS:
                self.timers(timer).start()
                self.timers(timer).stop()
            return
    
        # 步骤 1:- 使用 bit-16 梯度计算梯度规范
        see_memory_usage('Before norm calculation') # 在计算规范之前查看内存使用情况
        scaled_global_grad_norm = self.scaled_global_norm()
        self._global_grad_norm = scaled_global_grad_norm / prev_scale
        see_memory_usage('After norm before optimizer') # 在优化器之前,规范后查看内存使用情况
    
        # 步骤 2:- 同时运行优化器和上升规模
        for i, group in enumerate(self.bit16_groups):
            self.timers(OPTIMIZER_GRADIENTS_TIMER).start()
            partition_id = dist.get_rank(group=self.real_dp_process_group[i])
            if self.cpu_offload:
                single_grad_partition = self.single_partition_of_fp32_groups[i].grad
                self.unscale_and_clip_grads([single_grad_partition], scaled_global_grad_norm)
    
                self.timers(OPTIMIZER_GRADIENTS_TIMER).stop()
                self.timers(OPTIMIZER_STEP_TIMER).start()
                self._optimizer_step(i)
    
                bit16_partitions = self.parallel_partitioned_bit16_groups[i]
                fp32_partition = self.single_partition_of_fp32_groups[i]
                bit16_partitions[partition_id].data.copy_(fp32_partition.data)
    
                self.timers(OPTIMIZER_STEP_TIMER).stop()
            else:
                # 释放所有这个进程不更新的参数的梯度(ZeRO stage2)
                self.free_grad_in_param_list(self.params_not_in_partition[i])
    
                # 创建一个为这个进程更新的参数的平坦梯度
                if partition_id == dist.get_world_size(group=self.real_dp_process_group[i]) - 1:
                    single_grad_partition = self.flatten_dense_tensors_aligned(
                        self.averaged_gradients[i],
                        int(self.partition_size[i])).to(self.single_partition_of_fp32_groups[i].dtype)
                else:
                    single_grad_partition = self.flatten(self.averaged_gradients[i]).to(
                        self.single_partition_of_fp32_groups[i].dtype)
                assert single_grad_partition.numel() == self.partition_size[i], \
                    "averaged gradients have different number of elements that partition size {} {} {} {}".format(
                        single_grad_partition.numel(), self.partition_size[i], i, partition_id)
    
                self.single_partition_of_fp32_groups[i].grad = single_grad_partition
                # 释放所有梯度,因为我们已经在dp_grad_partition中创建了必要的副本(ZeRO stage2)
                self.free_grad_in_param_list(self.params_in_partition[i])
    
                self.averaged_gradients[i] = None
    
                self.unscale_and_clip_grads([single_grad_partition], scaled_global_grad_norm)
    
                self.timers(OPTIMIZER_GRADIENTS_TIMER).stop()
    
                # 步骤 3:- 运行优化器(如果没有卸载)
                self.timers(OPTIMIZER_STEP_TIMER).start()
                self._optimizer_step(i)
                # Step 4:- get rid of the fp32 gradients. Not needed anymore
                # 第四步:去掉 fp32 梯度,不再需要它们
                self.single_partition_of_fp32_groups[i].grad = None
                del single_grad_partition
    
                bit16_partitions = self.parallel_partitioned_bit16_groups[i]
                fp32_partition = self.single_partition_of_fp32_groups[i]
    
                # 将fp32分区的数据拷贝到bit16分区
                bit16_partitions[partition_id].data.copy_(fp32_partition.data)
    
                # 停止定时器
                self.timers(OPTIMIZER_STEP_TIMER).stop()
    
        # 查看内存使用情况
        see_memory_usage('After optimizer before all-gather')
    
        # 如果开启了CPU offload,重置CPU buffer
        if self.cpu_offload:
            self.reset_cpu_buffers()
    
        # 启动定时器
        self.timers(OPTIMIZER_ALLGATHER_TIMER).start()
    
        # 从每个节点收集更新后的权重。
        # 然后,所有分区的模型参数都更新完成,准备好进行下一轮的前向传播。
        all_gather_dp_groups(partitioned_param_groups=self.parallel_partitioned_bit16_groups,
                             dp_process_group=self.real_dp_process_group,
                             start_alignment_factor=self.nccl_start_alignment_factor,
                             allgather_bucket_size=self.allgather_bucket_size)
    
        # 停止定时器
        self.timers(OPTIMIZER_ALLGATHER_TIMER).stop()
    
        # 循环更新模型的bit16权重(虽然可能并不需要,但为了保险起见)
        for i in range(len(self.bit16_groups)):
            self._update_model_bit16_weights(i)
    
        # 日志记录优化器的定时信息
        self.timers.log(OPTIMIZER_TIMERS)
    
        # 查看内存使用情况
        see_memory_usage('After zero_optimizer step')
    
        # 结束执行,返回
        return
    

    以下是这段代码的主要步骤:

    1. 溢出检查:首先检查在计算梯度时是否有数值溢出。如果有溢出,它会清除所有的梯度并退出这次更新。
    2. 梯度规范:如果没有溢出,代码将计算梯度的全局规范(也就是所有参数梯度的L2范数)。
    3. 更新优化器:然后根据梯度和损失比例(用于数值稳定性)来更新优化器。
    4. 优化步骤:该方法中的一个关键部分是其对"bit16_groups"的处理。这些组可能包含在使用半精度(FP16)训练时的模型参数。在每个组上,它会执行优化器的步骤并更新参数。
    5. 梯度清除和复制:在每次优化步骤之后,该方法会清除FP32的梯度(因为它们不再需要),并将更新后的FP32参数复制回FP16参数。这是为了节省内存,因为FP16参数使用的内存更少。
    6. 权重聚集:然后,代码将在所有节点上收集更新后的权重。这是在分布式训练中常见的步骤,用于同步所有计算节点上的模型参数。
    7. 更新模型权重:最后,代码将使用收集的权重更新模型。

    在整个过程中,代码使用了一些定时器来测量不同部分的执行时间,并在结束时打印内存使用情况,以帮助用户识别可能的性能瓶颈和内存问题。

    总的来说,这个step方法对FP16和FP32参数进行了精心的管理,以允许在有限的硬件资源上进行大规模的深度学习训练。

  13. update_lp_params: 更新LP参数。

    def update_lp_params(self):
        # 遍历bit16和fp32的分区组
        for i, (bit16_partitions, fp32_partition) in enumerate(
                zip(self.parallel_partitioned_bit16_groups, self.single_partition_of_fp32_groups)):
            # 获取当前分区的ID
            partition_id = dist.get_rank(group=self.real_dp_process_group[i])
            # 将fp32分区的数据复制到相应的bit16分区中
            bit16_partitions[partition_id].data.copy_(fp32_partition.data)
           
    
        # 将所有分区的数据集合在一起,以便在分布式处理过程中使用
        all_gather_dp_groups(partitioned_param_groups=self.parallel_partitioned_bit16_groups,
                             dp_process_group=self.real_dp_process_group,
                             start_alignment_factor=self.nccl_start_alignment_factor,
                             allgather_bucket_size=self.allgather_bucket_size)
    

    具体来说,这段代码的工作流如下:

    1. 这段代码首先遍历了包含16位(bit16)和32位(fp32)参数的分区组。这是为了在训练过程中节省内存和计算资源,因为使用16位浮点数进行计算会比使用32位浮点数更快,但可能会损失一些精度。
    2. 在每个分区组中,它获取当前分区的ID。这在分布式训练中很重要,因为每个处理器只处理数据的一部分。dist.get_rank()函数获取了当前设备在分布式处理组中的排名(即ID)。
    3. 然后,它将32位参数的数据复制到相应的16位参数分区中。这是为了在训练过程中节省内存和计算资源。
    4. 最后,它使用all_gather_dp_groups()函数将所有设备的参数集合在一起。这是在分布式训练中必要的步骤,因为每个设备只有模型的一部分参数,但在每个训练步骤结束时,所有设备都需要获取完整的模型参数以进行同步。
  14. _average_expert_grad_norms: 平均专家梯度范数。

    def _average_expert_grad_norms(self, norm_groups):
        # 遍历norm_groups中的每个元素,并获取其索引和值
        for i, norm in enumerate(norm_groups):
            # 如果当前索引对应的参数组是moe参数组
            if self.is_moe_param_group[i]:
                # 计算规模化的梯度norm,这是通过对原始norm除以分布式进程组的大小来实现的
                scaled_norm = norm * 1.0 / float(dist.get_world_size(group=self.real_dp_process_group[i]))
                # 将规模化的norm转换为tensor格式,并确保其在正确的设备上,并且具有正确的数据类型
                scaled_norm_tensor = torch.tensor(scaled_norm,
                                                  device=get_accelerator().device_name(),
                                                  dtype=torch.float)
                # 使用all_reduce进行跨所有设备的归约操作,所有设备上的scaled_norm_tensor将会被加起来
                dist.all_reduce(scaled_norm_tensor, group=self.real_dp_process_group[i])
                # 将归约后的结果(一个tensor)转换为Python数值,并存回norm_groups中对应的位置
                norm_groups[i] = scaled_norm_tensor.item()
    
  15. unscale_and_clip_grads: 取消缩放并裁剪梯度。

    def _average_expert_grad_norms(self, norm_groups):
        # 遍历每个 norm_groups 中的元素,这些元素可能是模型的参数梯度的范数
        for i, norm in enumerate(norm_groups):
            # 如果当前的参数组是 MoE 参数组(MoE 是一种模型并行方法,全称 Mixture of Experts)
            if self.is_moe_param_group[i]:
                # 将当前的范数 norm 除以分布式环境下的进程总数,这是为了计算分布式环境下的平均梯度范数
                scaled_norm = norm * 1.0 / float(dist.get_world_size(group=self.real_dp_process_group[i]))
                # 将计算后的平均梯度范数转换为 PyTorch 的 Tensor 格式,并指定其在 GPU 或 CPU 上
                scaled_norm_tensor = torch.tensor(scaled_norm,
                                                  device=get_accelerator().device_name(),
                                                  dtype=torch.float)
                # 使用分布式通信操作 all_reduce 来在所有进程之间共享和累加这个平均梯度范数
                dist.all_reduce(scaled_norm_tensor, group=self.real_dp_process_group[i])
                # 更新 norm_groups 中的元素为计算后的平均梯度范数
                norm_groups[i] = scaled_norm_tensor.item()
    
  16. _check_overflow: 检查是否有溢出。

    def _check_overflow(self, partition_gradients=True):
        # 这个方法用于检查是否有溢出
        # partition_gradients 参数决定是否在不同的设备上分割梯度,默认为True
        # has_overflow 是一个方法,检查是否有任何梯度超出了表示范围,如果有,返回 True
        self.overflow = self.has_overflow(partition_gradients)
    
  17. has_overflow_serial: 检查是否串行溢出。

    # 定义一个方法,用于检测是否有溢出
    def has_overflow_serial(self, params, is_grad_list=False):
            # 遍历传入的参数
            for p in params:
                # 如果参数的梯度不为None,且该梯度的数据包含无穷大或者NaN值
                if p.grad is not None and self._has_inf_or_nan(p.grad.data):
                    # 返回True,表示有溢出
                    return True
    
            # 如果遍历所有参数都没有发现溢出,返回False
            return False
    
  18. has_overflow_partitioned_grads_serial: 检查分区的梯度是否串行溢出。

    def has_overflow_partitioned_grads_serial(self):
            # 遍历每一个16位组
            for i in range(len(self.bit16_groups)):
                # 在每个组内遍历每一个平均梯度
                for j, grad in enumerate(self.averaged_gradients[i]):
                    # 如果当前梯度不为空,并且梯度数据中存在无穷大或者NaN(不是一个数字)
                    # 则返回True,表示存在溢出的分区梯度
                    if grad is not None and self._has_inf_or_nan(grad.data, j):
                        return True
            # 如果所有的梯度都被检查过,并且没有发现溢出的情况,那么返回False
            return False
    
  19. has_overflow: 检查是否溢出。

    def has_overflow(self, partition_gradients=True):
            # 如果分区梯度为真
            if partition_gradients:
                # 如果执行CPU offload,则获取本地溢出,否则获取分区梯度的溢出
                overflow = self.local_overflow if self.cpu_offload else self.has_overflow_partitioned_grads_serial()
                # 将溢出值转换为GPU可处理的字节张量
                overflow_gpu = get_accelerator().ByteTensor([overflow])
                '''这将捕获所有数据并行和专家并行过程中的溢出
                由于专家并行过程是数据并行过程的子集'''
                # 将溢出值在数据并行过程组中进行归约处理,获取最大值
                dist.all_reduce(overflow_gpu, op=dist.ReduceOp.MAX, group=self.dp_process_group)
    
            else:
                # 初始化参数列表
                params = []
                # 对于每一个16位组
                for group in self.bit16_groups:
                    # 对于组中的每一个参数
                    for param in group:
                        # 将参数添加到参数列表中
                        params.append(param)
    
                # 获取参数列表中的溢出值
                overflow = self.has_overflow_serial(params, is_grad_list=partition_gradients)
                # 将溢出值转换为GPU可处理的字节张量
                overflow_gpu = get_accelerator().ByteTensor([overflow])
    
            # 由于每个模型并行的GPU只携带模型的一部分
            # 确保溢出标志在所有的模型并行GPU中同步
            self._model_parallel_all_reduce(tensor=overflow_gpu, op=dist.ReduceOp.MAX)
    
            # 获取字节张量中的溢出值
            overflow = overflow_gpu[0].item()
            # 返回溢出值的布尔值,如果溢出则返回True,否则返回False
            return bool(overflow)
    

    在深度学习中,"溢出"通常指的是数值计算中的两种常见问题:下溢出和上溢出。

    1. 下溢出: 当一个非常小的浮点数接近于0,并且小到计算机无法表示时,就会发生下溢出。这在深度学习中经常发生,比如在计算一个非常小的概率,或者一个非常大的负对数概率时。
    1. 上溢出: 当一个非常大的数超过计算机可以表示的最大浮点数时,就会发生上溢出。深度学习中的某些操作,如softmax,可能会导致上溢出。

    这段代码中的has_overflow函数是检查在进行深度学习模型的参数更新时,是否发生了数值溢出。这是非常重要的,因为如果发生溢出,可能会导致模型学习到的参数无效,从而影响模型的性能。

    为了避免上溢出和下溢出,通常会采用一些数值稳定的技巧,例如对输入进行规范化,使用对数概率,或者使用更大范围的数值类型(如使用64位浮点数代替32位浮点数)。在某些情况下,也可以采用特殊的数值格式(如半精度浮点数或定点数),或者使用特定的硬件支持,来更有效地处理数值溢出问题。

  20. _has_inf_or_nan: 检查是否有无穷大或NaN。

    def _has_inf_or_nan(x, j=None):
            try:
                # 如果 x 是半精度浮点数(half),.float()会引发额外的深拷贝,但是如果
                # Pytorch的 .sum() 创建一个与x类型相同的单元素张量
                # (对于一些最近版本的pytorch是这样的),那么这个操作就是必要的。
                cpu_sum = float(x.float().sum())
                # 如果 .sum() 返回一个Python标量,可以使用更高效的版本
                # cpu_sum = float(x.sum())
            except RuntimeError as instance:
                # 我们想要检查这个异常是否真的是溢出异常。
                # RuntimeError 可能来自不同的错误。
                # 如果是这样,我们仍然希望异常能够传播。
                if "value cannot be converted" not in instance.args[0]:
                    raise
                return True
            else:
                # 如果 cpu_sum 是正无穷、负无穷或者不是一个数字(NaN),
                # 那么我们就返回True,意味着x中含有无穷或者NaN
                if cpu_sum == float('inf') or cpu_sum == -float('inf') or cpu_sum != cpu_sum:
                    return True
                # 否则我们返回False,意味着x中没有无穷或者NaN
                return False
    
  21. backward: 执行反向传播。

    
    def backward(self, loss, retain_graph=False):
        """
        :attr:`backward` 执行以下步骤:
    
        1. fp32_loss = loss.float()  # 将损失转换为浮点类型
        2. scaled_loss = fp32_loss*loss_scale  # 对损失进行缩放
        3. scaled_loss.backward()  # 对缩放后的损失进行反向传播,这会将缩放的梯度累积到模型的 fp16 叶子节点的 ``.grad`` 属性中
        """
        self.micro_step_id += 1  # 微步进ID增加1
    
        # 如果启用连续梯度
        if self.contiguous_gradients:
            self.ipg_buffer = []  # 初始化 ipg 缓冲区
            # 创建一个大小为 reduce_bucket_size 的空tensor,数据类型为 self.dtype,设备为当前设备
            buf_0 = torch.empty(int(self.reduce_bucket_size),
                                dtype=self.dtype,
                                device=get_accelerator().current_device_name())
            self.ipg_buffer.append(buf_0)  # 将 buf_0 添加到 ipg 缓冲区中
    
            # 如果启用了 overlap_comm,使用双缓冲区以避免在启用 overlap_comm 时出现数据访问冲突
            if self.overlap_comm:
                buf_1 = torch.empty(int(self.reduce_bucket_size),
                                    dtype=self.dtype,
                                    device=get_accelerator().current_device_name())
                self.ipg_buffer.append(buf_1)  # 将 buf_1 添加到 ipg 缓冲区中
            self.ipg_index = 0  # 初始化 ipg 索引
    
        # 如果使用自定义损失缩放器
        if self.custom_loss_scaler:
            scaled_loss = self.external_loss_scale * loss  # 将损失按照外部损失缩放因子进行缩放
            scaled_loss.backward()  # 对缩放后的损失进行反向传播
        else:
            # 如果没有使用自定义损失缩放器,使用 loss_scaler 对损失进行反向传播
            self.loss_scaler.backward(loss.float(), retain_graph=retain_graph)
    
        # 仅在 Stage 1, Mode 2 中使用
        if self.use_grad_accum_attribute:
            self.fill_grad_accum_attribute()  # 填充 grad_accum 属性
    
    

    这个函数主要处理以下任务:

  • 更新微步进ID(micro_step_id):这通常用于跟踪模型训练的步骤或阶段。
  • 初始化连续梯度(contiguous_gradients):如果启用了连续梯度,函数会创建一个或两个新的缓存(取决于是否启用了overlap_comm)来存储连续梯度。这些缓存被称为ipg_buffer
  • 处理损失:函数首先检查是否使用了自定义损失缩放器(custom_loss_scaler)。如果使用了自定义损失缩放器,函数会根据提供的外部损失缩放因子(external_loss_scale)对损失进行缩放,然后进行反向传播。如果没有使用自定义损失缩放器,函数会直接调用损失缩放器(loss_scaler)对损失进行反向传播。
  • grad_accum_attribute的填充:如果函数处于特定的阶段和模式(Stage 1, Mode 2),它会填充一个名为grad_accum的属性。这个属性可能用于存储梯度的累积值,以便在训练过程中使用。

总的来说,这个backward函数被设计用于在训练深度学习模型时处理损失和梯度的计算。

  1. check_overflow: 检查是否溢出。

    def check_overflow(self, partition_gradients=True):
        # 调用内部的 `_check_overflow` 方法
        # 该方法旨在检查计算过程中是否出现了溢出
        # `partition_gradients` 参数决定是否在不同设备上分割梯度计算
        self._check_overflow(partition_gradients)
    
  2. _update_scale: 更新比例。

    def _update_scale(self, has_overflow=False):
        # has_overflow 是一个布尔值,如果为True,表示在前向或者反向传播中发生了梯度溢出
    
        # self.loss_scaler 是一个梯度缩放器,它是用来防止在反向传播过程中梯度小到无法表示的情况
        # update_scale 是 loss_scaler 的一个方法,通过传入 has_overflow 参数,来更新当前的缩放因子
    
        self.loss_scaler.update_scale(has_overflow)
        # 如果发生了梯度溢出,就会减小缩放因子,否则就会增大缩放因子
        # 这样可以保证在训练过程中,梯度既不会太大导致溢出,也不会太小到无法表示
    
  3. _get_state: 获得状态。

        def _get_state(self):
            return self.optimizer.state
    
    
  4. _set_state: 设置状态。

        def _set_state(self, value):
            self.optimizer.state = value
    
    
  5. _get_param_groups: 获取参数组。

        def _get_param_groups(self):
            return self.optimizer.param_groups
    
    
  6. _set_param_groups: 设置参数组。

        def _set_param_groups(self, value):
            self.optimizer.param_groups = value
    
    
  7. _get_loss_scale: 获取损失比例。

    def _get_loss_scale(self):
        """
        本函数的目标是获取损失缩放值。如果存在自定义的损失缩放器,
        则返回外部的损失缩放值。否则,返回当前的损失缩放值。
        """
        if self.custom_loss_scaler:
            # 存在自定义的损失缩放器时,返回外部的损失缩放值
            return self.external_loss_scale
        else:
            # 不存在自定义的损失缩放器时,返回当前的损失缩放值
            return self.loss_scaler.cur_scale
    
  8. _set_loss_scale: 设置损失比例。

        def _set_loss_scale(self, value):
            self.loss_scaler.cur_scale = value
    
    
  9. _get_groups_without_padding: 获取无填充的组。

    def _get_groups_without_padding(self, groups_with_padding):
            # 创建一个空列表,用于存储去除填充后的组
            groups_without_padding = []
            # 使用枚举函数对带填充的组进行迭代,获取每个组的索引和内容
            for i, group in enumerate(groups_with_padding):
                # 计算每个组真实的长度,即组的元素总数减去该组的填充数量
                lean_length = group.numel() - self.groups_padding[i]
                # 从每个组中提取出真实的元素(去除填充),并添加到新的列表中
                groups_without_padding.append(group[:lean_length])
    
            # 返回去除填充后的组列表
            return groups_without_padding
    
  10. _get_state_without_padding: 获取没有填充的状态。

    def _get_state_without_padding(self, state_with_padding, padding):
            # 初始化一个空字典,用于存放没有padding的状态
            lean_state = {}
            # 遍历传入的状态字典
            for key, value in state_with_padding.items():
                # 如果状态的值是一个张量
                if torch.is_tensor(value):
                    # 计算不包含padding的长度
                    lean_length = value.numel() - padding
                    # 截取原张量的前lean_length长度的部分,赋值给新的状态字典
                    lean_state[key] = value[:lean_length]
                else:
                    # 如果状态的值不是张量,直接赋值给新的状态字典
                    lean_state[key] = value
    
            # 返回没有padding的状态字典
            return lean_state
    
  11. _get_base_optimizer_state: 获取基础优化器状态。

    def _get_base_optimizer_state(self):
            # 初始化一个空列表用于存储优化器的状态
            optimizer_groups_state = []
            
            # 遍历优化器的参数组
            for i, group in enumerate(self.optimizer.param_groups):
                # 获取参数组中的第一个参数
                p = group['params'][0]
                
                # 调用函数_get_state_without_padding去掉参数的填充,并获取优化器状态
                # self.groups_padding[i]是获取当前参数组的填充
                lean_optimizer_state = self._get_state_without_padding(self.optimizer.state[p], self.groups_padding[i])
                
                # 将优化器状态添加到列表中
                optimizer_groups_state.append(lean_optimizer_state)
    
            # 返回处理后的优化器状态列表
            return optimizer_groups_state
    
  12. state_dict: 返回优化器的状态字典。

    def state_dict(self):
        """
        返回一个字典,包含当前FP16_Optimizer实例的状态。
        这个字典包含FP16_Optimizer的属性,以及包含的Pytorch优化器的state_dict。
        示例::
            checkpoint = {}
            checkpoint['model'] = model.state_dict()
            checkpoint['optimizer'] = optimizer.state_dict()
            torch.save(checkpoint, "saved.pth")
        """
    
        # 初始化一个空字典用于存储状态
        state_dict = {}
    
        # 存储损失缩放器的状态
        state_dict['loss_scaler'] = self.loss_scaler
        # 存储动态损失缩放的状态
        state_dict['dynamic_loss_scale'] = self.dynamic_loss_scale
        # 存储溢出的状态
        state_dict['overflow'] = self.overflow
        # 存储梯度裁剪的状态
        state_dict[CLIP_GRAD] = self.clip_grad
    
        # 如果启用了弹性检查点
        if self.elastic_checkpoint:
            # 获取基础优化器的状态
            state_dict[BASE_OPTIMIZER_STATE] = self._get_base_optimizer_state()
    
            # 如果优化器的参数组中存在"step"(步数)
            if "step" in self.optimizer.param_groups[0]:
                # 假设"step"是唯一会在训练迭代中改变的项
                # 确保所有的参数组的"step"都和第一个参数组的"step"相同
                assert all(group["step"] == self.optimizer.param_groups[0]["step"]
                           for group in self.optimizer.param_groups), "All param groups must have the same step value"
                # 存储"step"的状态
                state_dict[BASE_OPTIMIZER_STATE_STEP] = self.optimizer.param_groups[0]["step"]
        else:
            # 存储优化器的状态
            state_dict[BASE_OPTIMIZER_STATE] = self.optimizer.state_dict()
    
        # 移除DP对齐的填充,以便于加载其他对齐值
        fp32_groups_without_padding = self._get_groups_without_padding(self.single_partition_of_fp32_groups)
        state_dict[SINGLE_PARTITION_OF_FP32_GROUPS] = fp32_groups_without_padding
    
        # 存储ZeroStage(零阶段)的状态
        state_dict[
            ZERO_STAGE] = ZeroStageEnum.gradients if self.partition_gradients else ZeroStageEnum.optimizer_states
        # 存储分组填充的状态
        state_dict[GROUP_PADDINGS] = self.groups_padding
        # 存储分区计数的状态
        state_dict[PARTITION_COUNT] = self.partition_count
    
        # 存储DeepSpeed版本的状态
        state_dict[DS_VERSION] = version
        # 存储参数切片映射的状态
        state_dict[PARAM_SLICE_MAPPINGS] = self._param_slice_mappings
    
        # 返回状态字典
        return state_dict
    

    用于获取FP16_Optimizer类实例的状态并将其保存为一个字典。FP16_Optimizer类通常在深度学习训练中使用,对模型进行优化并协助训练过程。这个类的实例有许多状态(如损失缩放、动态损失缩放、溢出检测、梯度裁剪等),这些状态在训练过程中可能会发生变化。用于保存这些状态的字典可以用于检查、比较、恢复和迁移优化器的状态。

    这个函数的主要工作如下:

    1. 创建一个空的字典state_dict,用于存储优化器的状态。
    2. 将优化器的各种状态(如loss_scalerdynamic_loss_scaleoverflowclip_grad等)保存到state_dict中。
    3. 根据elastic_checkpoint的值,保存基础优化器的状态。如果启用了弹性检查点,它会获取基础优化器的状态并检查所有的参数组的"step"是否相同。如果没有启用弹性检查点,它会直接保存优化器的状态。
    4. 移除DP对齐的填充,以便于加载其他对齐值。
    5. 保存ZeroStage(零阶段)的状态、分组填充的状态、分区计数的状态、DeepSpeed版本的状态、以及参数切片映射的状态。
    6. 返回填充了优化器状态的state_dict字典。

    这个函数通常会在训练过程中的某些时间点被调用,以保存训练的当前状态,以便于在需要时恢复训练或进行故障诊断。

  13. _restore_from_elastic_fp32_weights: 从弹性FP32权重恢复。

    def _restore_from_elastic_fp32_weights(self, all_state_dict):
            # 初始化一个空列表用于存储FP32分区数据
            merged_single_partition_of_fp32_groups = []
    
            # 遍历 self 中的 FP32 分区组
            for i in range(len(self.single_partition_of_fp32_groups)):
                # 获取当前分区的ID,使用真实的数据并行过程组
                partition_id = dist.get_rank(group=self.real_dp_process_group[i])
                # 从所有的状态字典中获取对应的FP32分区
                merged_partitions = [sd[SINGLE_PARTITION_OF_FP32_GROUPS][i] for sd in all_state_dict]
                # 如果当前优化器的参数组是一个MOE组
                if self.is_moe_group(self.optimizer.param_groups[i]):
                    # 获取EP的排名
                    ranks = self.get_ep_ranks(group_name=self.optimizer.param_groups[i]['name'])
                    # 从合并的分区中选择对应排名的部分
                    merged_partitions = [merged_partitions[i] for i in ranks]
                # 将所有合并的分区数据进行平均,然后根据NCCL开始对齐因子和真实的数据并行过程组的大小进行对齐
                flat_merged_partitions = self.flatten_dense_tensors_aligned(
                    merged_partitions,
                    self.nccl_start_alignment_factor * dist.get_world_size(group=self.real_dp_process_group[i]))
                # 获取数据并行分区
                dp_partitions = self.get_data_parallel_partitions(flat_merged_partitions, i)
                # 将数据并行分区添加到最终的FP32分区组中
                merged_single_partition_of_fp32_groups.append(dp_partitions[partition_id])
    
            # 遍历当前的FP32分区组和合并后的FP32分区组
            for current, saved in zip(self.single_partition_of_fp32_groups, merged_single_partition_of_fp32_groups):
                # 将合并后的数据拷贝到当前的数据中
                current.data.copy_(saved.data)
    

    这段代码是在进行模型参数的恢复,特别是用于处理分布式训练场景中的模型参数恢复。在分布式训练中,模型的参数会被分成若干个分区,并且可能会在不同的设备或节点上进行训练。这段代码的主要目标是将这些在不同设备或节点上的模型参数进行合并和恢复。

    整个流程大致如下:

    1. 初始化一个空列表用于存储合并后的模型参数分区(FP32分区)。

    2. 遍历当前对象(可能是一个优化器或者模型)中的所有FP32分区。

    3. 对于每个FP32分区,获取其在分布式训练中的相应位置(即分区的ID)。

    4. 从所有的状态字典(可能是各个设备或节点上的模型状态)中提取出对应的FP32分区,并进行合并。

    5. 如果当前的参数组是一个MoE(Mixture of Experts,一种模型结构)组,则需要根据EP(Expert Partition,专家分区)的排名进行相应的选择。

    6. 将所有合并的分区数据进行平均,然后根据NCCL开始对齐因子和分布式训练的组大小进行对齐。

    7. 获取数据并行的分区。

    8. 将这些数据并行分区添加到最终的FP32分区组中。

    9. 遍历当前的FP32分区组和合并后的FP32分区组,将合并后的数据拷贝到当前的数据中。

    这个过程能保证在分布式训练中,即使模型的参数被分散在不同的设备或节点上,也能正确地将模型的状态恢复到一个统一的状态,从而保证训练的正确进行。

  14. _restore_from_bit16_weights: 从16位权重恢复。

    def _restore_from_bit16_weights(self):
        # 遍历 bit16_partitions 和 fp32_partition
        for group_id, (bit16_partitions, fp32_partition) in enumerate(
                zip(self.parallel_partitioned_bit16_groups, self.single_partition_of_fp32_groups)):
            # 获取当前进程在进程组中的排名(ID)
            partition_id = dist.get_rank(group=self.real_dp_process_group[group_id])
            # 将bit16_partitions中对应ID的数据复制到fp32_partition中
            fp32_partition.data.copy_(bit16_partitions[partition_id].data)
    
  15. refresh_fp32_params: 刷新FP32参数。

    def refresh_fp32_params(self):
        # 调用内部函数_restore_from_bit16_weights,用于从16位浮点数权重恢复到32位浮点数
        self._restore_from_bit16_weights()
    
  16. _partition_base_optimizer_state: 分区基础优化器状态。

    def _partition_base_optimizer_state(self, state_key, all_partition_states, group_id):
            # 获取当前进程在数据并行组中的排名
            partition_id = dist.get_rank(group=self.real_dp_process_group[group_id])
            # 获取数据并行组的总进程数
            alignment = dist.get_world_size(group=self.real_dp_process_group[group_id])
            # 如果状态是一个张量
            if torch.is_tensor(all_partition_states[0]):
                # 将所有分区状态张量扁平化并对齐
                flat_merged_partitions = self.flatten_dense_tensors_aligned(all_partition_states, alignment)
                # 获取数据并行分区
                dp_partitions = self.get_data_parallel_partitions(flat_merged_partitions, group_id)
                # 返回当前进程对应的分区
                return dp_partitions[partition_id]
            else:
                # 如果状态不是张量,假设它没有分区,并且在所有进程中都是相同的,因此返回第一个状态
                return all_partition_states[0]
    
  17. _restore_base_optimizer_state: 恢复基本优化器状态。

    def _restore_base_optimizer_state(self, base_optimizer_group_states):
        # 如果 base_optimizer_group_states 是字典,我们取出其中的 'state' 键对应的值
        if type(base_optimizer_group_states) == dict:
            base_optimizer_group_states = base_optimizer_group_states['state']
        
        # 遍历优化器中的参数组
        for i, group in enumerate(self.optimizer.param_groups):
            # 取出参数组中的第一个参数
            p = group['params'][0]
            
            # 遍历存储的优化器状态
            for key, saved in base_optimizer_group_states[i].items():
                # 如果优化器的状态是一个张量
                if torch.is_tensor(self.optimizer.state[p][key]):
                    # 获取目标张量
                    dst_tensor = self.optimizer.state[p][key]
                    
                    # 通过_pad_tensor函数获取与目标张量等长的源张量
                    src_tensor = _get_padded_tensor(saved, dst_tensor.numel())
                    
                    # 将源张量的数据复制到目标张量
                    self.optimizer.state[p][key].data.copy_(src_tensor.data)
                else:
                    # 如果优化器的状态不是张量,直接赋值
                    self.optimizer.state[p][key] = saved
    
  18. get_ep_ranks: 获取EP等级。

    def get_ep_ranks(self, rank=0, group_name=None):
        # 导入deepspeed库中的groups模块,该模块提供了并行计算组的管理功能
        from deepspeed.utils import groups
        
        # _get_expert_parallel_world_size函数返回专家并行世界的大小
        # 专家并行世界是指在专家并行(Expert Parallelism)策略下,所有专家(即模型的一部分)所在的并行计算环境
        expert_parallel_size_ = groups._get_expert_parallel_world_size(group_name)
        
        # _get_data_parallel_world_size函数返回数据并行世界的大小
        # 数据并行世界是指在数据并行(Data Parallelism)策略下,所有数据所在的并行计算环境
        world_size = groups._get_data_parallel_world_size()
        
        # _get_expert_parallel_rank函数获取当前处理器在专家并行世界中的排名
        # 排名决定了当前处理器在并行计算中的执行顺序
        rank = groups._get_expert_parallel_rank(group_name)
        
        # range函数生成一个序列,范围从当前处理器的排名开始,到数据并行世界的大小,步长为专家并行世界的大小
        # 这样可以确保每个处理器在其执行顺序上都是均匀分布的
        ranks = range(rank, world_size, expert_parallel_size_)
        
        # 返回一个由处理器排名组成的列表,这个列表可以用于管理处理器的执行顺序
        return list(ranks)
    

    ep是专家并行的缩写 expert parallel

    这个函数的目的是在并行计算环境中获取并返回一组处理器(或者称为计算节点)的排名。这里的排名决定了这些处理器在并行计算中的执行顺序。

    这个函数的工作过程如下:

    1. 获取专家并行世界的大小和数据并行世界的大小,这两个大小分别表示了专家并行和数据并行中的处理器数量。
    2. 获取当前处理器在专家并行世界中的排名。
    3. 生成一个由处理器排名组成的列表。这个列表的范围从当前处理器的排名开始,到数据并行世界的大小,步长为专家并行世界的大小。这样可以确保每个处理器在其执行顺序上都是均匀分布的。
    4. 返回这个列表。

    这个函数的返回值可以用于管理处理器的执行顺序,以实现更有效的并行计算。

  19. _restore_elastic_base_optimizer_state: 恢复弹性基本优化器状态。

    # 定义一个方法,用于恢复基础优化器的状态
    def _restore_elastic_base_optimizer_state(self, all_state_dict):
        # 初始化一个空的列表,用于存储基础优化器的状态
        base_optimizer_group_states = []
        # 对优化器的参数组进行遍历
        for i in range(len(self.optimizer.param_groups)):
            # 初始化一个空字典,用于存储当前参数组的状态
            partition_states = {}
            # 从所有的状态字典中获取当前参数组的状态
            all_partition_group_states = [sd[BASE_OPTIMIZER_STATE][i] for sd in all_state_dict]
    
            # 如果当前参数组是一个 MOE 组(Mixture of Experts,混合专家模型)
            if self.is_moe_group(self.optimizer.param_groups[i]):
                # 获取当前参数组在 EP 中的排名
                ranks = self.get_ep_ranks(group_name=self.optimizer.param_groups[i]['name'])
                # 根据排名,重新获取当前参数组的状态
                all_partition_group_states = [all_partition_group_states[i] for i in ranks]
    
            # 对所有的状态进行遍历,合并分区状态
            for key in all_partition_group_states[0].keys():
                # 获取所有分区的同一状态
                all_partition_states = [all_states[key] for all_states in all_partition_group_states]
                # 合并所有分区的状态
                partition_states[key] = self._partition_base_optimizer_state(key, all_partition_states, i)
            # 将合并后的状态添加到状态列表中
            base_optimizer_group_states.append(partition_states)
    
        # 通过状态列表,恢复基础优化器的状态
        self._restore_base_optimizer_state(base_optimizer_group_states)
    
        # 如果状态字典中包含步骤信息
        if BASE_OPTIMIZER_STATE_STEP in all_state_dict[0]:
            # 确保所有的状态字典都包含相同的步骤值
            assert all(sd[BASE_OPTIMIZER_STATE_STEP] == all_state_dict[0][BASE_OPTIMIZER_STATE_STEP]
                       for sd in all_state_dict), "State dicts of all partitions must have the same step value"
            # 获取步骤值
            loaded_param_groups_step = all_state_dict[0][BASE_OPTIMIZER_STATE_STEP]
            # 将步骤值写入优化器的每一个参数组
            for param_group in self.optimizer.param_groups:
                param_group['step'] = loaded_param_groups_step
    

    这段代码是为了定义一个恢复优化器状态的方法。其中"优化器状态"是指一个记录了优化器所有参数的字典,包含了优化器在训练过程中学习到的所有信息。这个方法的主要步骤如下:

    1. 首先,创建一个空列表base_optimizer_group_states用于存储优化器的所有参数组的状态。

    2. 然后,遍历优化器的所有参数组。对于每一个参数组,从all_state_dict中提取该组的状态,并且把它添加到base_optimizer_group_states列表中。

    3. 如果参数组是属于一个混合专家模型(Mixture of Experts,MOE),则会获取它在EP中的排名,并根据排名重新获取当前参数组的状态。

    4. 然后,对所有的状态进行遍历,并合并分区状态,合并完成后,将其添加到base_optimizer_group_states列表中。

    5. 通过base_optimizer_group_states列表,恢复基础优化器的状态。

    6. 如果all_state_dict中包含步骤信息,那么就确保所有的状态字典都包含相同的步骤值。然后,获取步骤值,并将步骤值写入优化器的每一个参数组。

    简而言之,这个函数的目的是从一个给定的状态字典中恢复优化器的状态,包括每个参数组的状态以及优化器的全局步骤信息。这在恢复训练中断的模型时非常有用,因为它允许你从上次训练结束的地方恢复训练,而不是从头开始。

  20. load_state_dict: 加载优化器状态字典。

    def load_state_dict(self,
                        state_dict_list,                  # 状态字典列表,一般用于存储模型的参数
                        load_optimizer_states=True,       # 是否加载优化器的状态,默认为True
                        load_from_fp32_weights=False,     # 是否从fp32权重加载,默认为False
                        checkpoint_folder=None,           # 检查点文件夹,默认为None
                        load_serial=None):                # 加载序列,默认为None
        # 如果提供了检查点文件夹
        if checkpoint_folder:
            # 从通用检查点加载模型和优化器状态
            self._load_universal_checkpoint(checkpoint_folder, load_optimizer_states, load_from_fp32_weights)
        else:
            # 从传统检查点加载模型和优化器状态
            self._load_legacy_checkpoint(state_dict_list, load_optimizer_states, load_from_fp32_weights)
    
  21. _load_hp_checkpoint_state: 加载HP检查点状态。

    def _load_hp_checkpoint_state(self, checkpoint_dir):
            # 将给定的路径后面添加 "zero" 子目录
            checkpoint_dir = os.path.join(checkpoint_dir, "zero")
            # 获取模型并行的排名
            tp_rank = bwc_tensor_model_parallel_rank(mpu=self.mpu)
            # 获取模型并行的世界大小
            tp_world_size = self.mpu.get_slice_parallel_world_size()
            
            # 遍历优化器的参数组
            for i, _ in enumerate(self.optimizer.param_groups):
                # 遍历16位的参数组
                for lp in self.bit16_groups[i]:
                    # 如果参数有映射
                    if lp._hp_mapping is not None:
                        # 加载检查点状态
                        lp.load_hp_checkpoint_state(
                            # 检查点路径
                            os.path.join(checkpoint_dir, self.param_names[lp]),
                            # 模型并行的排名
                            tp_rank,
                            # 模型并行的世界大小
                            tp_world_size)
    

    这段代码定义了一个名为_load_hp_checkpoint_state的方法,它的功能是从指定的检查点目录加载模型的状态。这个方法主要用于深度学习模型训练过程中的断点续训。当训练过程中断时,可以从前面保存的检查点(checkpoint)恢复,继续训练,以节省时间和计算资源。

    具体来说,代码的工作流如下:

    1. 连接checkpoint_dir和子目录 “zero” 作为检查点文件的路径。
    2. 获取模型并行的排名tp_rank和模型并行的世界大小tp_world_size。这两个参数通常用于并行计算设置,用于确定当前进程在所有并行进程中的位置(排名)和并行进程的总数(世界大小)。
    3. 遍历优化器的参数组,对于每个参数组,再遍历其16位的参数组。
    4. 如果参数有映射,则加载参数的检查点状态。这一步是通过调用lp.load_hp_checkpoint_state方法实现的,这个方法需要提供检查点路径、模型并行的排名和模型并行的世界大小作为参数。

    hp是 “hyperparameters”(超参数)的缩写

  22. param_groups: 参数组。

        def param_groups(self):
            """Forward the wrapped optimizer's parameters."""
            return self.optimizer.param_groups
    
    
  23. _load_legacy_checkpoint: 加载旧版检查点。

​```python
def _load_legacy_checkpoint(self, state_dict_list, load_optimizer_states=True, load_from_fp32_weights=False):
    """
    加载 ZeRO 检查点

    参数:
        state_dict_list: 所有保存的 ZeRO 检查点的列表,每个保存的分区一个。
            注意,保存的分区数量可能与加载的分区数量不同,以支持在保存和加载检查点之间更改 GPU 数量,特别是 DP 世界大小。
        load_optimizer_states: 布尔值,表示是否加载基础优化器状态
        load_from_fp32_weights: 布尔值,表示是否从检查点的 fp32 副本(无精度损失)或模型的 fp16 副本(有精度损失)初始化 fp32 主权重。
    """

    # 获取当前的分布式进程等级
    dp_rank = dist.get_rank(group=self.dp_process_group)
    # 获取当前等级的状态字典
    current_rank_sd = state_dict_list[dp_rank]

    # 获取状态信息,包括损失缩放器,动态损失缩放,溢出状态,梯度裁剪等
    self.loss_scaler = current_rank_sd.get('loss_scaler', self.loss_scaler)
    self.dynamic_loss_scale = current_rank_sd.get('dynamic_loss_scale', self.dynamic_loss_scale)
    self.overflow = current_rank_sd.get('overflow', self.overflow)
    self.clip_grad = current_rank_sd.get(CLIP_GRAD, self.clip_grad)

    # 获取检查点版本,版本信息在加载时需要进行检查以保证兼容性
    ckpt_version = current_rank_sd.get(DS_VERSION, False)
    assert ckpt_version, f"Empty ds_version in checkpoint, not clear how to proceed"
    ckpt_version = pkg_version.parse(ckpt_version)

    # 针对 zero stage 1 模式进行版本检查
    if not self.partition_gradients:
        required_version = pkg_version.parse("0.3.17")
        error_str = f"ZeRO stage 1 changed in {required_version} and is not backwards compatible " \
            "with older stage 1 checkpoints. If you'd like to load an old ZeRO-1 checkpoint " \
            "please use an older version of DeepSpeed (<= 0.5.8) and set 'legacy_stage1': true in your zero config json."
        assert required_version <= ckpt_version, f"Old version: {ckpt_version} {error_str}"

    # 检查状态字典是否为字典类型
    ckpt_is_rigid = isinstance(current_rank_sd[BASE_OPTIMIZER_STATE], dict)

    if load_optimizer_states:
        if ckpt_is_rigid:
            # 如果是固定状态字典,直接加载
            self.optimizer.load_state_dict(current_rank_sd[BASE_OPTIMIZER_STATE])
        else:
            if self.elastic_checkpoint:
                # 如果是弹性检查点,使用对应的恢复方法
                self._restore_elastic_base_optimizer_state(state_dict_list)
            else:
                # 如果是非弹性检查点,使用基础的恢复方法
                self._restore_base_optimizer_state(current_rank_sd[BASE_OPTIMIZER_STATE])

    # 在这一点上,优化器对模型的 fp32 参数的引用是最新的。
    # 优化器的超参数和内部缓冲区也是最新的。
    # 然而,优化器存储的模型的 fp16 参数的 fp32 主副本仍然过时。有两个选择。
    # 1:从模型的 fp16 参数刷新主参数。
    #    这需要更少的存储但会导致精度损失。
    # 2:单独保存和恢复 fp32 主副本。
    #    如果改变 DP 度,我们选择选项 1,否则选择选项 2。

    if load_from_fp32_weights:
        # 选项 2
        if self.elastic_checkpoint and not ckpt_is_rigid:
            self._restore_from_elastic_fp32_weights(state_dict_list)
        else:
            # 对于非弹性检查点,简单地从当前等级的保存权重复制就足够了。
            for current, saved in zip(self.single_partition_of_fp32_groups,
                                      current_rank_sd[SINGLE_PARTITION_OF_FP32这段代码是一个深度学习框架DeepSpeed中的一部分,用于从保存的检查点中加载模型和优化器的状态。下面是这段代码的详细注释:

​```python
# 定义了一个名为 _load_legacy_checkpoint 的函数,这个函数的目的是从旧版的检查点中加载模型和优化器的状态
def _load_legacy_checkpoint(self, state_dict_list, load_optimizer_states=True, load_from_fp32_weights=False):

    # 获取当前进程的等级,这是在分布式训练中使用的
    dp_rank = dist.get_rank(group=self.dp_process_group)
    # 获取当前等级对应的状态字典
    current_rank_sd = state_dict_list[dp_rank]

    # 从状态字典中获取并设置损失缩放器、动态损失缩放、溢出状态和梯度裁剪等信息
    self.loss_scaler = current_rank_sd.get('loss_scaler', self.loss_scaler)
    self.dynamic_loss_scale = current_rank_sd.get('dynamic_loss_scale', self.dynamic_loss_scale)
    self.overflow = current_rank_sd.get('overflow', self.overflow)
    self.clip_grad = current_rank_sd.get(CLIP_GRAD, self.clip_grad)

    # 获取检查点的版本信息,用于检查兼容性
    ckpt_version = current_rank_sd.get(DS_VERSION, False)
    assert ckpt_version, f"Empty ds_version in checkpoint, not clear how to proceed"
    ckpt_version = pkg_version.parse(ckpt_version)

    # 如果当前使用的是 ZeRO stage 1 模式,需要进行版本检查
    if not self.partition_gradients:
        required_version = pkg_version.parse("0.3.17")
        error_str = f"ZeRO stage 1 changed in {required_version} and is not backwards compatible " \
            "with older stage 1 checkpoints. If you'd like to load an old ZeRO-1 checkpoint " \
            "please use an older version of DeepSpeed (<= 0.5.8) and set 'legacy_stage1': true in your zero config json."
        assert required_version <= ckpt_version, f"Old version: {ckpt_version} {error_str}"

    # 检查状态字典中基础优化器状态的数据类型是否为字典
    ckpt_is_rigid = isinstance(current_rank_sd[BASE_OPTIMIZER_STATE], dict)

    if load_optimizer_states:
        # 如果状态字典中基础优化器状态的数据类型是字典,则直接加载状态
        if ckpt_is_rigid:
            self.optimizer.load_state_dict(current_rank_sd[BASE_OPTIMIZER_STATE])
        else:
            # 如果状态字典中基础优化器状态的数据类型不是字典,那么根据是否是弹性检查点来使用不同的恢复方法
            if self.elastic_checkpoint:
                self._restore_elastic_base_optimizer_state(state_dict_list)
            else:
                self._restore_base_optimizer_state(current_rank_sd[BASE_OPTIMIZER_STATE])

    # 在这一点上,优化器对模型的 fp32 参数的引用是最新的。
    # 优化器的超参数和内部缓冲区也是最新的。
    # 然而,优化器存储的模型的 fp16 参数的 fp32 主副本仍然过时。有两个选择。
    # 1:从模型的 fp16 参数刷新主参数。
    #    这需要更少的存储但会导致精度损失。
    # 2:单独保存和恢复 fp32 主副本。
    #    如果改变 DP 度,我们选择选项 1,否则选择选项 2。

    if load_from_fp32_weights:
        # 选择方案2,如果是弹性检查点并且状态字典不是字典类型,使用对应的恢复方法
        if self.elastic_checkpoint and not ckpt_is_rigid:
            self._restore_from_elastic_fp32_weights(state_dict_list)
        else:
            # 对于非弹性检查点,简单地从当前等级的保存权重复制就足够了。
            for current, saved in zip(self.single_partition_of_fp32_groups,
                                      current_rank_sd[SINGLE_PARTITION_OF_FP32_GROUPS]):
                current.data

这段代码是恢复旧版的检查点,应该是后面要不用了,读的简单一些。应该是旧版本的检查点和新版本在设计上有不一样的地方。

具体来说,这段代码做了以下事情:

  1. 加载和设置模型的一些基本状态信息,如损失缩放器,动态损失缩放,溢出状态,梯度裁剪等。
  2. 检查检查点的版本信息,确认是否兼容当前的DeepSpeed版本。
  3. 如果指定了加载优化器状态,那么会根据检查点的类型(弹性或非弹性)采用不同的恢复方法。
  4. 选择如何加载fp32的主权重:从模型的fp16参数刷新(可能会造成精度损失)或者单独保存和恢复fp32的主权重。

在分布式训练中,每个进程都有自己的一份模型参数和优化器状态,因此这个函数会根据当前进程的等级来加载对应的检查点数据。

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值