1. LayerNorm使用介绍

pytorch中的函数定义如下:

torch.nn.LayerNorm(normalized_shape, eps=1e-05, elementwise_affine=True, device=None, dtype=None)

函数参数说明如如下:

normalized_shape: 进行LayerNorm的维度定义,对于一个多维矩阵[N, C, H, W]来说,这里的normalized_shape定义都是要和矩阵最后几个维度保持一致的,这里就是[C, H, W]。对比数学公式,其中的 pytorch 防止nan_深度学习pytorch 防止nan_人工智能_02 的维度都是[C, H, W],pytorch 防止nan_pytorch 防止nan_03pytorch 防止nan_pytorch 防止nan_04 的维度都是[N, C, H, W]。

eps:为了防止计算公式中的分母为0,加上一个极小的数,默认值: 1e-5

elementwise_affine:设为True的时候,进行elementwise的仿射变换, pytorch 防止nan_深度学习pytorch 防止nan_人工智能_02 才会生效,在训练过程中做为参数会被学习更新,为False的话不生效。pytorch 防止nan_深度学习 所有元素初始为1, pytorch 防止nan_人工智能_02 所有元素初始为0的。pytorch 防止nan_深度学习 在代码实现中对应 pytorch 防止nan_pytorch 防止nan_10, pytorch 防止nan_人工智能_02 在代码实现中对应 pytorch 防止nan_python_12

LayerNorm的数学公式定义如下:

pytorch 防止nan_python_13

pytorch使用示例,给定一个[N, C, H, W]的矩阵,在[C, H, W]维上进行LayerNorm操作:

>>> # Image Example
>>> N, C, H, W = 20, 5, 10, 10
>>> input = torch.randn(N, C, H, W)
>>> # Normalize over the last three dimensions (i.e. the channel and spatial dimensions)
>>> # as shown in the image below
>>> layer_norm = nn.LayerNorm([C, H, W])
>>> output = layer_norm(input)

pytorch 防止nan_python_14

2. LayerNorm反向推导公式

为了方便推导,eps先忽略,输入为一维矩阵。对应LayerNorm的数学公式定义如下, 其中pytorch 防止nan_python_15是由pytorch 防止nan_pytorch 防止nan_16组成的一维向量, pytorch 防止nan_人工智能_17是输出向量,维度跟pytorch 防止nan_python_15一样; pytorch 防止nan_pytorch 防止nan_19是期望,简写为pytorch 防止nan_python_20; pytorch 防止nan_深度学习_21是方差【pytorch 防止nan_pytorch 防止nan_22】; 标准差【pytorch 防止nan_深度学习_23】简写为pytorch 防止nan_python_24

pytorch 防止nan_深度学习_25

这里有三个地方需要求梯度(即需要进行求导),分别是对参数gammapytorch 防止nan_python_26和betapytorch 防止nan_pytorch 防止nan_27, 以及输入x的求导, 即pytorch 防止nan_pytorch_28pytorch 防止nan_人工智能_29pytorch 防止nan_深度学习_30。同时在计算 pytorch 防止nan_深度学习_30 时会用到 pytorch 防止nan_pytorch_32pytorch 防止nan_pytorch_33pytorch 防止nan_pytorch 防止nan_34

pytorch 防止nan_pytorch_35

这里 pytorch 防止nan_人工智能_36pytorch 防止nan_人工智能_37 是一一对应的, 所以不用累加;但对于 pytorch 防止nan_人工智能_37 参与了所有 pytorch 防止nan_人工智能_17 的计算,反向的时候计算梯度也需要对涉及的所有的 pytorch 防止nan_pytorch_40

3. 源码实现

代码仓版本:https://github.com/pytorch/pytorch/tree/v2.0.1

3.1 前向计算

在aten/src/ATen/native/native_functions.yaml中的定义如下:

- func: native_layer_norm(Tensor input, SymInt[] normalized_shape, Tensor? weight, Tensor? bias, float eps) -> (Tensor, Tensor, Tensor)
  dispatch:
    CPU: layer_norm_cpu
    CUDA: layer_norm_cuda
    MPS: layer_norm_mps
    CompositeExplicitAutograd: math_native_layer_norm
    NestedTensorCPU, NestedTensorCUDA: nested_layer_norm
  autogen: native_layer_norm.out
  tags: core

这里以layer_norm_cpu的实现为例,layer_norm_cpu定义在aten/src/ATen/native/layer_norm.cpp中。

在layer_norm_cpu的前向函数中,会根据input和normalized_shape进行shape的转换计算,从多维矩阵转为pytorch 防止nan_pytorch_41的二维矩阵,比如input的shape是[2, 3, 4, 5],normalized_shape是[4, 5], 那么M=2*3=6, N=4*5=20;同时还会进行weight(对应pytorch 防止nan_pytorch 防止nan_42)和bias(对应pytorch 防止nan_人工智能_43)矩阵的初始化。

std::tuple<Tensor, Tensor, Tensor> layer_norm_cpu(
    const Tensor& input,
    IntArrayRef normalized_shape, const c10::optional<Tensor>& weight_opt /* optional */, const c10::optional<Tensor>& bias_opt /* optional */,
    double eps) {
  // weight和bias初始化
  c10::MaybeOwned<Tensor> weight_maybe_owned = at::borrow_from_optional_tensor(weight_opt);
  const Tensor& weight = *weight_maybe_owned;
  c10::MaybeOwned<Tensor> bias_maybe_owned = at::borrow_from_optional_tensor(bias_opt);
  const Tensor& bias = *bias_maybe_owned;

  // 计算M和N
  auto M_N = _check_layer_norm_inputs(input, normalized_shape, weight, bias);
  auto M = M_N.first;
  auto N = M_N.second;
  auto X = input.expect_contiguous();
  auto gamma = weight.expect_contiguous();
  auto beta = bias.expect_contiguous();

  // 初始化mean/rstd,维度是M个,每N个input的元素会计算一个mean和rstd
  Tensor mean = at::empty({M}, X->options().dtype(dtype));
  Tensor rstd = at::empty({M}, X->options().dtype(dtype));
  
  // layer_norm_with_mean_rstd_out中会调用前向kernel(LayerNormKernel)
  layer_norm_with_mean_rstd_out(Y, mean, rstd, *X, normalized_shape, *gamma, *beta, eps, M, N);
  return std::make_tuple(std::move(Y), std::move(mean), std::move(rstd));
}

LayerNormKernel定义在aten/src/ATen/native/cpu/layer_norm_kernel.cpp中,实际的实现是LayerNormKernelImplInternal, 定义如下:

template <typename T, typename T_ACC>
void LayerNormKernelImplInternal(
    const Tensor& X,
    const Tensor& gamma,
    const Tensor& beta,
    int64_t M,
    int64_t N,
    T_ACC eps,
    Tensor* Y,
    Tensor* mean,
    Tensor* rstd) {
    ...   
}

在LayerNormKernelImplInternal首先了解at::parallel_for函数的使用,它的基本作用是对输入先进行分块,然后通过多线程进行并行处理,如下函数的定义是对[0, M]分成多段,分别调用匿名函数。

at::parallel_for(0, M, 1, [&](int64_t start, int64_t end) {...})

回顾下前向计算过程:

pytorch 防止nan_人工智能_44

匿名函数逻辑中,对于 M * N的矩阵,每次处理N个元素进行LayerNorm操作。mean对应pytorch 防止nan_python_20, rstd_val和scale对应pytorch 防止nan_人工智能_46, bias对应pytorch 防止nan_pytorch_47, 因此, pytorch 防止nan_人工智能_48

for (const auto i : c10::irange(start, end)) {
      const T* X_ptr = X_data + i * N;
      T* Y_ptr = Y_data + i * N;
      T mean_val;
      T rstd_val;
      // 1. 计算mean_val和rstd_val
      std::tie(mean_val, rstd_val) = RowwiseMoments(X_ptr, N);
      rstd_val = T(1) / std::sqrt(rstd_val + eps);
      
      const T scale = rstd_val;
      const T bias = -rstd_val * mean_val;
      if (gamma_null || beta_null) {
        for (const auto j : c10::irange(N)) {
          const T gamma_v = gamma_null ? T(1) : gamma_data[j];
          const T beta_v = beta_null ? T(0) : beta_data[j];
          Y_ptr[j] = (X_ptr[j] * scale + bias) * gamma_v + beta_v;
        }
      } else {
        // 2. 计算layer norm的前向公式
        vec::map3<T>(
            [scale, bias](Vec x, Vec gamma, Vec beta) {
              return (x * Vec(scale) + Vec(bias)) * gamma + beta;
            },
            Y_ptr,
            X_ptr,
            gamma_data,
            beta_data,
            N);
      }
      if (!mean_null) {
        mean_data[i] = mean_val;
      }
      if (!rstd_null) {
        rstd_data[i] = rstd_val;
      }
    }
  }

3.2 反向计算

对于多维矩阵求反向,可以看成是M个大小为N的向量,以一个5维向量为例,向量维度为pytorch 防止nan_pytorch 防止nan_49,layer_norm的维度是pytorch 防止nan_深度学习_50,对应的pytorch 防止nan_深度学习_51, pytorch 防止nan_python_52

在aten/src/ATen/native/native_functions.yaml中的定义如下:

- func: native_layer_norm_backward(Tensor grad_out, Tensor input, SymInt[] normalized_shape, Tensor mean, Tensor rstd, Tensor? weight, Tensor? bias, bool[3] output_mask) -> (Tensor, Tensor, Tensor)
  dispatch:
    CPU: layer_norm_backward_cpu
    CUDA: layer_norm_backward_cuda
    MPS: layer_norm_backward_mps
  autogen: native_layer_norm_backward.out
  tags: core

这里以layer_norm_backward_cpu的实现为例,layer_norm_backward_cpu定义在aten/src/ATen/native/layer_norm.cpp中。跟layer_norm_cpu类似,在backward中初始化相关tensor,和进行kernel的调用。

std::tuple<Tensor, Tensor, Tensor> layer_norm_backward_cpu(
    const Tensor& dY,
    const Tensor& input,
    IntArrayRef normalized_shape,
    const Tensor& mean,
    const Tensor& rstd,
    const c10::optional<Tensor>& weight_opt /* optional */,
    const c10::optional<Tensor>& bias_opt /* optional */,
    std::array<bool, 3> grad_input_mask) {
  ......
  if (M > 0) {
    LayerNormBackwardKernel(
        kCPU, dY, *X, mean, rstd, *gamma, M, N, &dX, &dgamma, &dbeta);
  }
  return std::make_tuple(std::move(dX), std::move(dgamma), std::move(dbeta));   
}

为了方便和后续pytorch源码实现中对应,对上面推导公式的最后结果中做下相应的展开,展开如下:pytorch 防止nan_python_53

kernel的实现在是aten/src/ATen/native/cpu/layer_norm_kernel.cpp文件的LayerNormBackwardKernelImplInternal函数中,实现分为两个阶段:

  1. 初始化一个shape大小为{2, max_threads, N}的buffer矩阵,对应其中的buffer[0]用于dgamma_buffer, buffer[1]用于dbeta_buffer。多线程分别计算dY和X。
  2. 对dgamma/dbeta的值进行累加操作,复用X[i]和dY[i]

对于代码实现是通过两层嵌套进行的,对于第一步来说,最外面是对 pytorch 防止nan_python_54 的矩阵按行进行多线程并行,每个线程处理 pytorch 防止nan_pytorch 防止nan_55 个元素;第二步是按N列进行元素的累加。layer_norm_backward_frame函数中包含了主要的计算逻辑, 后面进一步分析。

template <typename T>
void LayerNormBackwardKernelImplInternal(
    const Tensor& dY,
    const Tensor& X,
    const Tensor& mean,
    const Tensor& rstd,
    const Tensor& gamma,
    int64_t M,
    int64_t N,
    Tensor* dX,
    Tensor* dgamma,
    Tensor* dbeta) {
  ......
  // 第一步:计算dgamma/dbeta and dX
  at::parallel_for(0, M, 1, [&](int64_t start, int64_t end) {
    int tid = at::get_thread_num();
    TORCH_CHECK(
        tid < num_threads,
        "expect thread id smaller than ",
        num_threads,
        ", got thread id ",
        tid);
    T* dgamma_buffer_ptr = dgamma_null ? nullptr : buffer_data + tid * N;
    T* dbeta_buffer_ptr =
        dbeta_null ? nullptr : buffer_data + num_threads * N + tid * N;
    for (const auto i : c10::irange(start, end)) {
      layer_norm_backward_frame<T, T2, T_ACC>(dY_data, X_data, mean_data, rstd_data, gamma_data, dX_data, dgamma_buffer_ptr, dbeta_buffer_ptr, scale, gamma_null, dX_null, dgamma_null, dbeta_null, N, i);
    }
  });

  // 第二步:计算dgamma/dbeta的累加
  if (buffer_data != nullptr) {
    parallel_for(0, N, 1, [&](int64_t start, int64_t end) {
      for (const auto j : c10::irange(start, end)) {
        T_ACC dgamma_v = T_ACC(0);
        T_ACC dbeta_v = T_ACC(0);
        for (const auto i : c10::irange(num_threads)) {
          dgamma_v += buffer_data[i * N + j];
          dbeta_v += buffer_data[num_threads * N + i * N + j];
        }
        if (!dgamma_null) {
          // NOLINTNEXTLINE(clang-analyzer-core.NullDereference)
          dgamma_data[j] = dgamma_v;
        }
        if (!dbeta_null) {
          // NOLINTNEXTLINE(clang-analyzer-core.NullDereference)
          dbeta_data[j] = dbeta_v;
        }
      }
    });
  }
  ......
}

layer_norm_backward_frame函数中计算dgamma的逻辑如下,对应公式:pytorch 防止nan_深度学习_56, 其中 pytorch 防止nan_python_57, pytorch 防止nan_pytorch_58

if (!dgamma_null) {
    const T_ACC a = rstd_data[i];
    const T_ACC b = -a * mean_data[i];
    // Scalar math:
    // for (const auto j : c10::irange(N)) {
    //   dgamma_data[j] += dY_ptr[j] * (a * X_ptr[j] + b);
    // }
    vec::map3<T>(
        [a, b](Vec dgamma, Vec dy, Vec x) {
          return dgamma + dy * (Vec(a) * x + Vec(b));
        },
        dgamma_buffer_ptr,
        dgamma_buffer_ptr,
        dY_ptr,
        X_ptr,
        N);
  }

layer_norm_backward_frame函数中计算dbeta的逻辑如下,对应公式:pytorch 防止nan_深度学习_59

if (!dbeta_null) {
    // Scalar math:
    // for (const auto j : c10::irange(N)) {
    //   dbeta_data[j] += dY_ptr[j];
    // }
    vec::map2<T>(
        [](Vec dbeta, Vec dy) { return dbeta + dy; },
        dbeta_buffer_ptr,
        dbeta_buffer_ptr,
        dY_ptr,
        N);
  }

layer_norm_backward_frame函数中计算dx的逻辑如下,对应公式:

pytorch 防止nan_人工智能_60

layer_norm_backward_frame函数核心代码实现如下:

if (gamma_null) {
      ......
    } else {
      ds = vec::map3_reduce_all<T>(
          [](Vec x, Vec y, Vec z) { return x * y * z; },
          [](Vec x, Vec y) { return x + y; },
          dY_ptr,
          X_ptr,
          gamma_data,
          N);
      db = vec::map2_reduce_all<T>(
          [](Vec x, Vec y) { return x * y; },
          [](Vec x, Vec y) { return x + y; },
          dY_ptr,
          gamma_data,
          N);
    }
    const T_ACC a = rstd_data[i];
    const T_ACC b = (db * mean_data[i] - ds) * a * a * a * scale;
    const T_ACC c = -b * mean_data[i] - db * a * scale;
    if (gamma_null) {
      ......
    } else {
      vec::map3<T>(
          [a, b, c](Vec dy, Vec gamma, Vec x) {
            return Vec(a) * dy * gamma + Vec(b) * x + Vec(c);
          },
          dX_ptr,
          dY_ptr,
          gamma_data,
          X_ptr,
          N);
    }
  }

代码中变量与公式对应关系如下:

  • ds对应pytorch 防止nan_pytorch_61
  • db对应pytorch 防止nan_pytorch_62
  • a对应pytorch 防止nan_人工智能_63
  • scale对应pytorch 防止nan_pytorch 防止nan_64
  • b对应pytorch 防止nan_pytorch_65
  • c对应pytorch 防止nan_深度学习_66
  • 最终结果:dx = Vec(a) * dy * gamma + Vec(b) * x + Vec(c)

4. 参考资料

  • Vector, Matrix, and Tensor Derivatives
  • 手推公式之“层归一化”梯度
  • 矩阵求导浅析(一)
  • 矩阵求导术(上)
  • 矩阵求导术(下)
  • 道理我都懂,但是神经网络反向传播时的梯度到底怎么求?
  • 神经网络反向传播的数学原理
  • layernorm 反向传播推导及代码
  • pytorch-LAYERNORM
  • Layer Normalization-paper