【BBuf的CUDA笔记】九,使用newbing(chatgpt)解析oneflow softmax相关的fuse优化

这篇具有很好参考价值的文章主要介绍了【BBuf的CUDA笔记】九,使用newbing(chatgpt)解析oneflow softmax相关的fuse优化。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

0x0. 背景

随着年纪越来越大,读代码越来越困难,如果你发现看不懂同事写的代码应该怎么办呢?不要担心,大语言模型的时代了来了,chatgpt和gpt4会教会我们怎么读代码。本篇文章就来展示一下使用newbing(chatgpt)来读oneflow softmax相关的fuse优化kernel的过程。本文的代码解释均由chatgpt生成,我只是手工做了非常少的一点微调来保证对代码解释的正确性。完整代码解释见: https://github.com/BBuf/how-to-optim-algorithm-in-cuda/blob/master/oneflow-cuda-optimize-skills/fused_softmax/fused_scale_mask_softmax.cu#L1452-L2098

我在【BBuf的CUDA笔记】八,对比学习OneFlow 和 FasterTransformer 的 Softmax Cuda实现 中对oneflow以及FasterTransformer做了源码解析,读者可以先了解一下。在oneflow的softmax cuda kernel中已经提到它的每种实现均有LOAD、STORE两个模板参数分别代表输入输出。使用 load.template load<pack_size>(ptr, row_id, col_id);store.template store<pack_size>(ptr, row_id, col_id); 进行读取和写入。使用LOAD和STORE有两个好处:1、可以在CUDA Kernel中只关心计算类型ComputeType,而不用关心具体的数据类型T。2、只需要加几行代码就可以快速支持Softmax和其他Kernel Fuse,减少带宽需求,提升整体性能。普通的SoftmaxKernel直接使用DirectLoad和DirectStore,FusedScaleSoftmaxKernel(也就是本文要介绍的)只需要额外定义一个ScaleMaskLoad结构用于对输入x做Scale预处理以及ScaleMaskStore即可快速完成kernel fuse。

本篇文章在oneflow softmax的基础上将fused_scale_mask_softmax的完整实现抽取到了https://github.com/BBuf/how-to-optim-algorithm-in-cuda/blob/master/oneflow-cuda-optimize-skills/fused_softmax/fused_scale_mask_softmax.cu 文件中并进行解析,读者可以使用 https://github.com/BBuf/how-to-optim-algorithm-in-cuda/blob/master/oneflow-cuda-optimize-skills/fused_softmax/Makefile 文件来编译这个.cu文件并运行,得到正确的结果。

0x1. Pattern介绍

我们先来介绍一下我们要做的这个fuse pattern是什么样子的,其实它就是Transformer解码模块的softmax部分,由于解码时无法看到当前token的后续token,所以需要对输入做一个mask scale操作。具体来说就是把mask tensor中为true的位置对应的原始tensor元素乘以一个常数scale,并且把mask tensor中为false的位置对应的原始tensor元素填充为一个-inf。其实也不一定是inf,只要足够大就可以了,比如经常取10000.0。用python代码表达这个pattern就是这样子的:

import numpy as np
import oneflow as flow

batch_size = 4
num_heads = 8
seq_length = 64
broadcast_dim = 1
fill_value = -10000.0
scale_value = 2.0

x = np.random.randn(batch_size, num_heads, seq_length, seq_length).astype(
        np.float32
)
mask_size = [batch_size, num_heads, seq_length, seq_length]
if broadcast_dim:
    mask_size[broadcast_dim] = 1

mask = np.random.randint(0, 2, size=mask_size, dtype=bool)
fused_x_tensor = flow.tensor(x, dtype=flow.float32).to("cuda")
fused_mask_tensor = flow.tensor(mask, dtype=flow.bool).to("cuda")
fused_x_tensor.requires_grad = True

fused_out = flow._C.fused_scale_mask_softmax(
    fused_x_tensor, fused_mask_tensor, fill_value=fill_value, scale=scale_value,
)

origin_x_tensor = flow.tensor(x).to("cuda")
origin_mask_tensor = flow.tensor(mask, dtype=flow.float32).to("cuda")
origin_out = flow.mul(
    origin_x_tensor, origin_mask_tensor
) * scale_value + fill_value * (1.0 - origin_mask_tensor)
origin_out = flow.softmax(origin_out, dim=-1)
assert(np.allclose(fused_out.numpy(), origin_out.numpy(), atol=1e-4, rtol=1e-4))

这个pattern就是把flow.mul(origin_x_tensor, origin_mask_tensor) * scale_value + fill_value * (1.0 - origin_mask_tensor)里面的操作融合为一个cuda kernel,也就是flow._C.fused_scale_mask_softmax(fused_x_tensor, fused_mask_tensor, fill_value=fill_value, scale=scale_value,) 。额外需要注意的是这个mask是支持广播的,所以oneflow中对softmax kernel中的LOAD和STORE分别定义了BroadcastScaleMaskLoad/ElementwiseScaleMaskLoad以及BroadcastScaleMaskStore/ElementwiseScaleMaskStore 2组实现来进行fused_scale_mask_softmax算子的数据读取和存储。

0x2. 使用chatgpt来读代码实现

接下来我们就用chatgpt来读一读BroadcastScaleMaskLoad/ElementwiseScaleMaskLoad以及BroadcastScaleMaskStore/ElementwiseScaleMaskStore的实现。

首先来看ElementwiseScaleMaskLoad的实现:

template<typename SRC, typename DST, typename MASK>
struct ElementwiseScaleMaskLoad {
  ElementwiseScaleMaskLoad(const SRC* src, const MASK* mask, ElementwiseMaskSoftmaxParams param)
      : src(src), mask(mask), param(param) {}
  template<int N>
  __device__ void load(DST* dst, int64_t row, int64_t col) {
    Pack<SRC, N> pack;
    const int64_t offset = (row * param.row_size + col) / N;
    pack.storage = *(reinterpret_cast<const PackType<SRC, N>*>(src) + offset);
    Pack<int8_t, N> mask_pack;
    mask_pack.storage = *(reinterpret_cast<const PackType<MASK, N>*>(mask) + offset);
#pragma unroll
    for (int i = 0; i < N; ++i) {
      if (mask_pack.elem[i] == 0) {
        dst[i] = static_cast<DST>(param.fill);
      } else {
        dst[i] = static_cast<DST>(pack.elem[i]) * static_cast<DST>(param.scale);
      }
    }
  }
  const SRC* src;
  const MASK* mask;
  ElementwiseMaskSoftmaxParams param;
};

我们让chatgpt来帮我们分析一下这个结构体,chatgpt为我们分析了这个结构体并给出了以下的信息:

// 它的功能是定义一个模板类,用于在kernel函数中加载逐元素掩码softmax的输入。它的模板参数有:
// SRC: 源张量的数据类型
// DST: 目标张量的数据类型
// MASK: 掩码张量的数据类型
// 它的构造函数接受以下参数:
// src: 源张量的指针
// mask: 掩码张量的指针
// param: 逐元素掩码softmax的参数,是一个ElementwiseMaskSoftmaxParams结构体的实例
// load: 一个模板函数,用于从源张量和掩码张量中加载N个元素,并将它们存储到目标张量中。它接受以下参数:
// dst: 目标张量的指针
// row: 当前的行号
// col: 当前的列号
// 它的内部逻辑是:
// 根据行号和列号计算源张量和掩码张量的偏移量
// 从源张量和掩码张量中读取N个元素,分别存储到pack和mask_pack中
// 遍历N个元素,如果掩码张量的元素为0,就将目标张量的元素设为填充值,否则就将源张量的元素乘以缩放因子,并转换为目标张量的数据类型
// 这个模板类的成员变量有:
// src: 源张量的指针
// mask: 掩码张量的指针
// param: 逐元素掩码softmax的参数,是一个ElementwiseMaskSoftmaxParams结构体的实例
// 这个模板类的目的是为了方便在kernel函数中使用逐元素掩码softmax的输入,提高深度学习的性能。

可以看到chatgpt给出了非常详细的逻辑解释,并且还指出提高深度学习性能。提高深度学习性能这句话应该是猜出来的,毕竟从这段代码chatgpt是不可能了解到这个结构体是和softmax fuse kernel相关的,但能感觉到chatgpt是真的有理解这段代码已经非常厉害了。接着我们问问#pragma unroll这一行代码是干什么的?

【BBuf的CUDA笔记】九,使用newbing(chatgpt)解析oneflow softmax相关的fuse优化我们再问一下这里的Pack是什么意思?

【BBuf的CUDA笔记】九,使用newbing(chatgpt)解析oneflow softmax相关的fuse优化
可以看到chatgpt还是较为准备的为我们解释了这里Pack的含义以及指出是便于在GPU上进行高效的内存访问。

最后再拷打一下pack.storage = *(reinterpret_cast<const PackType<SRC, N>*>(src) + offset)这句话涉及到的逻辑:

【BBuf的CUDA笔记】九,使用newbing(chatgpt)解析oneflow softmax相关的fuse优化

太强了,根本难不倒他。当然了,一般element_wise相关的定义都是略显简单的,如果我们加入广播的逻辑也就是让chatgpt来读一下BroadcastScaleMaskLoad看看效果如何:

// 它的功能是定义一个模板类,用于在内核函数中加载广播掩码softmax的输入。它的模板参数有:
// SRC: 源张量的数据类型
// DST: 目标张量的数据类型
// MASK: 掩码张量的数据类型
// num_dims: 张量的维度数
// IndexType: 索引的数据类型
// 它的构造函数接受以下参数:
// src: 源张量的指针
// mask: 掩码张量的指针
// params: 广播掩码softmax的参数,是一个BroadcastMaskSoftmaxParams结构体的实例
// 它的成员函数有:
// load: 一个模板函数,用于从源张量和掩码张量中加载N个元素,并将它们存储到目标张量中。它接受以下参数:
// dst: 目标张量的指针
// row: 当前的行号
// col: 当前的列号
// 它的内部逻辑是:
// 根据行号和列号计算源张量的偏移量
// 根据偏移量计算源张量的多维索引
// 根据多维索引计算掩码张量的偏移量
// 从源张量和掩码张量中读取N个元素,分别存储到pack和mask_pack中
// 遍历N个元素,如果掩码张量的元素为0,就将目标张量的元素设为填充值,否则就将源张量的元素乘以缩放因子,并转换为目标张量的数据类型
// 这个模板类的成员变量有:
// src: 源张量的指针
// mask: 掩码张量的指针
// mask_dims: 掩码张量的每个维度的大小,是一个数组
// params: 广播掩码softmax的参数,是一个BroadcastMaskSoftmaxParams结构体的实例
// 这个模板类的目的是为了方便在内核函数中使用广播掩码softmax的输入,提高深度学习的性能。
template<typename SRC, typename DST, typename MASK, size_t num_dims, typename IndexType>
struct BroadcastScaleMaskLoad {
  BroadcastScaleMaskLoad(const SRC* src, const MASK* mask,
                         BroadcastMaskSoftmaxParams<num_dims, IndexType> params)
      : src(src), mask(mask), params(params) {
    for (int i = 0; i < num_dims; i++) { mask_dims[i] = params.mask_dims[i]; }
  }
  template<int N>
  __device__ void load(DST* dst, int64_t row, int64_t col) {
    Pack<SRC, N> pack;
    Pack<MASK, N> mask_pack;
    const IndexType offset = row * params.row_size + col;
    IndexType input_index[num_dims];
    IndexType mask_index[num_dims];
    params.src_index_helper.OffsetToNdIndex(offset, input_index);
    for (int dim = 0; dim < num_dims; ++dim) {
      if (mask_dims[dim] == 1) {
        mask_index[dim] = 0;
      } else {
        mask_index[dim] = input_index[dim];
      }
    }
    const IndexType mask_offset = params.mask_index_helper.NdIndexToOffset(mask_index);
    pack.storage = *(reinterpret_cast<const PackType<SRC, N>*>(src) + offset / N);
    mask_pack.storage =
        *(reinterpret_cast<const PackType<MASK, N>*>(mask) + mask_offset / N);
#pragma unroll
    for (int i = 0; i < N; ++i) {
      if (mask_pack.elem[i] == 0) {
        dst[i] = static_cast<DST>(params.fill);
      } else {
        dst[i] = static_cast<DST>(pack.elem[i]) * static_cast<DST>(params.scale);
      }
    }
  }
  const SRC* src;
  const MASK* mask;
  int64_t mask_dims[num_dims];
  BroadcastMaskSoftmaxParams<num_dims, IndexType> params;
};

chatgpt给了我们一份看起来非常合理的注释,并且似乎它已经参透了这个结构体类就是为了方便在kernel函数中使用广播掩码softmax的输入。接下来我们依旧拷打一下细节,首先我们来问一下BroadcastMaskSoftmaxParams这个结构体类,并让它给出解释:

// 它的功能是定义一个结构体,用于存储广播掩码softmax的参数。
// 广播掩码softmax是一种在不同形状的张量之间进行softmax运算的方法,
// 它会将较小的张量扩展到较大的张量的形状上,并用一个掩码张量来指定哪些位置需要计算softmax,
// 哪些位置需要填充一个固定的值。这个结构体的成员变量有:
// src_index_helper: 一个用于计算源张量索引的辅助类
// mask_index_helper: 一个用于计算掩码张量索引的辅助类
// mask_dims: 掩码张量的每个维度的大小
// row_size: 每一行的元素个数
// fill: 填充的值
// scale: softmax的缩放因子
// 这个结构体的目的是为了方便在内核函数中使用广播掩码softmax的参数,提高深度学习的性能。
template<size_t num_dims, typename IndexType>
struct BroadcastMaskSoftmaxParams {
  NdIndexOffsetHelper<IndexType, num_dims> src_index_helper;
  NdIndexOffsetHelper<IndexType, num_dims> mask_index_helper;
  const int64_t* mask_dims{};
  int64_t row_size;
  float fill;
  float scale;
};

非常准确!接着我们再问一下NdIndexOffsetHelper这个类的细节,chatgpt给出的回答如下:

// 定义 NdIndexOffsetHelper 类来做多维张量的坐标映射
template<typename T, int N>
class NdIndexOffsetHelper {
 public:
  NdIndexOffsetHelper() = default;

  // 这段代码是一个构造函数模板,它的功能是根据给定的多维数组的维度初始化一个用于计算偏移量的辅助类。
  // 它的参数是一个可变参数模板,表示可以接受任意个数和类型的参数,但是第一个参数必须是一个T类型的值,
  // 后面的参数必须是一个参数包。它的函数体是一个调用另一个函数的语句,这个函数是用于初始化步长数组的。
  // 它使用了一个宏OF_DEVICE_FUNC,这个宏可能是用于指定函数在哪个设备上运行的,比如GPU或者CPU。
  // 这个构造函数模板可能是用于实现一些多维数组的操作,比如插值或者转置。
  template<class... Ts>
  OF_DEVICE_FUNC explicit NdIndexOffsetHelper(T d0, Ts... dims) {
    constexpr int n = 1 + sizeof...(dims);
    static_assert(n <= N, "");
    T dims_arr[n] = {d0, static_cast<T>(dims)...};
    // 初始化strides信息
    InitStrides(dims_arr, n);
  }

  // 从一个类型为T的数组进行构造,并初始化strides信息,注意这里的strides长度设置为N
  OF_DEVICE_FUNC explicit NdIndexOffsetHelper(const T* dims) { InitStrides(dims, N); }

  // 从一个类型为U的数组进行构造,并初始化strides信息,注意这里的strides长度设置为N
  template<typename U>
  OF_DEVICE_FUNC explicit NdIndexOffsetHelper(const U* dims) {
    T dims_arr[N];
    for (int i = 0; i < N; ++i) { dims_arr[i] = dims[i]; }
    InitStrides(dims_arr, N);
  }

  // 从一个类型为T的数组进行构造,并初始化strides信息,注意这里的strides长度自定义为n
  OF_DEVICE_FUNC explicit NdIndexOffsetHelper(const T* dims, int n) { InitStrides(dims, n); }

  // 从一个类型为U的数组进行构造,并初始化strides信息,注意这里的strides长度自定义为n
  template<typename U>
  OF_DEVICE_FUNC explicit NdIndexOffsetHelper(const U* dims, int n) {
    T dims_arr[N];
    for (int i = 0; i < N; ++i) {
      if (i < n) { dims_arr[i] = dims[i]; }
    }
    InitStrides(dims_arr, n);
  }

  // virtual 表示这是一个虚析构函数,用于在删除基类指针时调用派生类的析构函数,避免内存泄漏。
  // ~NdIndexOffsetHelper() 表示这是 NdIndexOffsetHelper 类的析构函数,用于释放类对象占用的资源。
  // = default; 表示这是一个默认的析构函数,没有自定义的操作,让编译器自动生成。
  virtual ~NdIndexOffsetHelper() = default;

  // 这段代码是一个模板函数,用于根据一个N维索引数组计算一个一维偏移量。函数的参数和返回值都是模板类型T,可以是任意数值类型。函数的主要步骤如下:
  OF_DEVICE_FUNC T NdIndexToOffset(const T* index) const {
    // 定义一个变量offset,初始值为0,用于存储最终的偏移量。
    T offset = 0;
#ifdef __CUDA_ARCH__
#pragma unroll
#endif
    // 使用一个循环,从0到N-1,遍历索引数组的每一个元素。在循环中,使用一个数组stride_,
    // 用于存储每一个维度的步长,即每增加一个单位的索引,偏移量增加多少。
    // 将索引数组的第i个元素乘以步长数组的第i个元素,然后累加到offset上。
    for (int i = 0; i < N; ++i) { offset += index[i] * stride_[i]; }
    return offset;
  }
  
  // 类似上面,不过这里是从0到n进行循环
  OF_DEVICE_FUNC T NdIndexToOffset(const T* index, int n) const {
    assert(n <= N);
    T offset = 0;
#ifdef __CUDA_ARCH__
#pragma unroll
#endif
    for (int i = 0; i < N; ++i) {
      if (i < n) { offset += index[i] * stride_[i]; }
    }
    return offset;
  }

  // 这段代码是一个函数模板,它的功能是根据给定的多维索引计算一个一维偏移量。
  // 它的参数是一个可变参数模板,表示可以接受任意个数和类型的参数,但是第一个
  // 参数必须是一个T类型的值,后面的参数必须是一个参数包。它的返回值也是一个T类型的值。
  // 它的函数体是一个循环,用于累加每个维度的索引乘以对应的步长,得到最终的偏移量。
  // 它使用了一个宏OF_DEVICE_FUNC,这个宏可能是用于指定函数在哪个设备上运行的,
  // 比如GPU或者CPU。这个函数模板可能是用于实现一些多维数组的操作,比如插值或者转置。
  template<class... Ts>
  OF_DEVICE_FUNC T NdIndexToOffset(T d0, Ts... others) const {
    constexpr int n = 1 + sizeof...(others);
    static_assert(n <= N, "");
    T index[n] = {d0, others...};
    T offset = 0;
#ifdef __CUDA_ARCH__
#pragma unroll
#endif
    for (int i = 0; i < n - 1; ++i) { offset += index[i] * stride_[i]; }
    if (n == N) {
      offset += index[n - 1];
    } else {
      offset += index[n - 1] * stride_[n - 1];
    }
    return offset;
  }

  // 这段代码是一个成员函数模板,它的功能是根据给定的一维偏移量计算一个多维索引。
  // 它的参数是一个T类型的值,表示偏移量,和一个T类型的指针,表示索引数组。
  // 它的函数体是一个循环,用于逐个维度地计算索引值,然后更新剩余的偏移量。
  // 它使用了一个宏OF_DEVICE_FUNC,这个宏可能是用于指定函数在哪个设备上运行的,比如GPU或者CPU 。
  // 这个成员函数模板可能是用于实现一些多维数组的操作,比如插值或者转置。
  OF_DEVICE_FUNC void OffsetToNdIndex(T offset, T* index) const {
    T remaining = offset;
#ifdef __CUDA_ARCH__
#pragma unroll
#endif
    for (int i = 0; i < N - 1; ++i) {
      const T idx = remaining / stride_[i];
      index[i] = idx;
      remaining = remaining - idx * stride_[i];
    }
    index[N - 1] = remaining;
  }

  // 这段代码是用C++语言编写的,它定义了一个名为OffsetToNdIndex的函数,
  // 该函数的功能是将一维的偏移量转换为高维的索引1。这个函数是OneFlow的内部类,
  // OneFlow是一个深度学习框架,它支持分布式训练和推理。这个函数的参数有三个,分别是:
  // offset: 一个整数,表示一维的偏移量。
  // index: 一个整数数组,用于存储转换后的高维索引。
  // n: 一个整数,表示高维的维度数,不能超过N,N是一个常量。
  // 函数的主要逻辑是:
  // 首先,用一个变量remaining存储offset的值。
  // 然后,用一个循环遍历从0到N-1的整数i。
  // 在循环中,如果i小于n,那么就用remaining除以stride_[i]得到一个整数idx,这个stride_[i]是一个预定义的数组,表示每个维度的步长。
  // 然后,将idx赋值给index[i],并用remaining减去idx乘以stride_[i],更新remaining的值。
  // 最后,结束循环。
  // 这个函数的作用是将一维的偏移量映射到高维的索引,这在深度学习中有很多应用,比如Unfold和Fold算子,它们可以将图像的局部区域转换为一维的向量,或者反过来。
  OF_DEVICE_FUNC void OffsetToNdIndex(T offset, T* index, int n) const {
    assert(n <= N);
    T remaining = offset;
#ifdef __CUDA_ARCH__
#pragma unroll
#endif
    for (int i = 0; i < N; ++i) {
      if (i < n) {
        const T idx = remaining / stride_[i];
        index[i] = idx;
        remaining = remaining - idx * stride_[i];
      }
    }
  }

  // 它定义了一个名为OffsetToNdIndex的函数模板,该函数模板的功能和之前的函数类似,
  // 也是将一维的偏移量转换为高维的索引,但是它可以接受不同数量的参数。这个函数模板的参数有两个,分别是:
  // offset: 一个整数,表示一维的偏移量。
  // d0, others: 一系列的整数引用,用于存储转换后的高维索引。
  // 函数模板的主要逻辑是:
  // 首先,用一个常量n表示参数的个数,它等于1加上others的个数。
  // 然后,用一个静态断言检查n是否小于等于N,N是一个常量。
  // 然后,用一个指针数组index存储d0和others的地址。
  // 然后,用一个变量remaining存储offset的值。
  // 然后,用一个循环遍历从0到n-2的整数i。
  // 在循环中,如果i小于n-1,那么就用remaining除以stride_[i]得到一个整数idx,这个stride_[i]是一个预定义的数组,表示每个维度的步长。
  // 然后,将idx赋值给index[i]所指向的变量,并用remaining减去idx乘以stride_[i],更新remaining的值。
  // 最后,根据n和N的关系,分两种情况处理最后一个参数:
  // 如果n等于N,那么就将remaining赋值给index[n-1]所指向的变量。
  // 如果n小于N,那么就用remaining除以stride_[n-1]得到一个整数,赋值给index[n-1]所指向的变量。
  // 这个函数模板的作用是将一维的偏移量映射到高维的索引,它可以根据不同的参数个数进行重载,这是C++的一种泛型编程的特性
  template<class... Ts>
  OF_DEVICE_FUNC void OffsetToNdIndex(T offset, T& d0, Ts&... others) const {
    constexpr int n = 1 + sizeof...(others);
    static_assert(n <= N, "");
    T* index[n] = {&d0, &others...};
    T remaining = offset;
#ifdef __CUDA_ARCH__
#pragma unroll
#endif
    for (int i = 0; i < n - 1; ++i) {
      const T idx = remaining / stride_[i];
      *index[i] = idx;
      remaining = remaining - idx * stride_[i];
    }
    if (n == N) {
      *index[n - 1] = remaining;
    } else {
      *index[n - 1] = remaining / stride_[n - 1];
    }
  }

  OF_DEVICE_FUNC constexpr int Size() const { return N; }

 protected:
  // 这段代码也是用C++语言编写的,它定义了一个名为InitStrides的函数,
  // 该函数的功能是初始化stride_数组,该数组表示每个维度的步长。这个函数的参数有两个,分别是:
  // dims: 一个整数数组,表示高维的维度大小。
  // n: 一个整数,表示高维的维度数,不能超过N,N是一个常量。
  // 函数的主要逻辑是:
  // 首先,用一个循环遍历从n-1到N-1的整数i。
  // 在循环中,将stride_[i]赋值为1。
  // 然后,用一个循环遍历从n-2到0的整数i。
  // 在循环中,将stride_[i]赋值为dims[i+1]乘以stride_[i+1]。
  // 这个函数的作用是计算每个维度的步长,这在之前的OffsetToNdIndex函数中有用到,它可以根据不同的维度大小和维度数进行初始化。

  OF_DEVICE_FUNC void InitStrides(const T* dims, const int n) {
    for (int i = n - 1; i < N; ++i) { stride_[i] = 1; }
    for (int i = n - 2; i >= 0; --i) { stride_[i] = dims[i + 1] * stride_[i + 1]; }
  }

  T stride_[N];
};

可以看到chatgpt不仅理解了NdIndexOffsetHelper是在做坐标映射,甚至把每个构造函数的功能以及涉及到的c++语法比如可变参数模板都给我们展示了。

看到这里,chatgpt已经帮助我们理解了BroadcastScaleMaskLoad/ElementwiseScaleMaskLoad这两个结构体类的功能和细节。基本没有出错,非常让我震撼。接下来,在fused_scale_mask_softmax的启动过程中还调用了一个广播维度简化的函数SimplifyBroadcastDims:https://github.com/BBuf/how-to-optim-algorithm-in-cuda/blob/master/oneflow-cuda-optimize-skills/fused_softmax/fused_scale_mask_softmax.cu#L2136 。我们来看看chatgpt是否可以合理的解释这个函数呢?

// 这个函数的功能是简化两个张量的广播维度,使它们能够进行算术运算。
// 广播是一种在不同形状的张量之间进行运算的方法,它会将较小的张量扩展到较大的张量的形状上。
// 这段代码的输入参数有:
// num_a_dims: 张量A的维度数
// a_dims: 张量A的每个维度的大小
// num_b_dims: 张量B的维度数
// b_dims: 张量B的每个维度的大小
// simplified_num_dims: 简化后的广播维度数
// simplified_a_dims: 简化后的张量A的每个维度的大小
// simplified_b_dims: 简化后的张量B的每个维度的大小
// 这段代码的主要逻辑是:
// 首先,找到两个张量的最大维度数,然后用一个闭包函数MakeGetDim来获取每个张量在每个维度上的大小,如果维度数不足,就用1来填充。
// 然后,遍历每个维度,计算两个张量在该维度上的最大值,作为广播后的维度大小。
// 如果该维度大小为1,就跳过,否则就判断是否可以和上一个维度合并,如果可以,就乘以上一个维度的大小,
// 如果不可以,就添加到简化后的维度数组中,并记录是否是广播维度。
// 最后,返回简化后的广播维度数和两个张量的简化后的维度大小。
// 这段代码的目的是为了减少广播运算的开销,提高深度学习的性能。
inline void SimplifyBroadcastDims(size_t num_a_dims, const int64_t* a_dims, size_t num_b_dims,
                                  const int64_t* b_dims, size_t* simplified_num_dims,
                                  int64_t* simplified_a_dims, int64_t* simplified_b_dims) {
  const size_t num_max_dims = std::max(num_a_dims, num_b_dims);
  auto MakeGetDim = [num_max_dims](size_t num_dims, const int64_t* dims) {
    const int64_t num_padding_dims = num_max_dims - num_dims;
    return [num_padding_dims, dims](size_t index) {
      return index < num_padding_dims ? 1 : dims[index - num_padding_dims];
    };
  };
  auto GetADim = MakeGetDim(num_a_dims, a_dims);
  auto GetBDim = MakeGetDim(num_b_dims, b_dims);
  *simplified_num_dims = 0;
  bool prev_broadcast_a = false;
  bool prev_broadcast_b = false;
  for (int64_t i = 0; i < num_max_dims; ++i) {
    const int64_t a_dim = GetADim(i);
    const int64_t b_dim = GetBDim(i);
    const int64_t broadcast_dim = std::max(a_dim, b_dim);
    // CHECK_GT(broadcast_dim, 0);
    const bool broadcast_a = (a_dim == 1);
    const bool broadcast_b = (b_dim == 1);
    // CHECK((a_dim == broadcast_dim) || broadcast_a);
    // CHECK((b_dim == broadcast_dim) || broadcast_b);
    if (broadcast_dim == 1) {
      continue;
    } else if (*simplified_num_dims != 0
               && (prev_broadcast_a == broadcast_a && prev_broadcast_b == broadcast_b)) {
      simplified_a_dims[*simplified_num_dims - 1] *= a_dim;
      simplified_b_dims[*simplified_num_dims - 1] *= b_dim;
    } else {
      simplified_a_dims[*simplified_num_dims] = a_dim;
      simplified_b_dims[*simplified_num_dims] = b_dim;
      *simplified_num_dims += 1;
      prev_broadcast_a = broadcast_a;
      prev_broadcast_b = broadcast_b;
    }
  }
}

可以看到chatgpt给出的注释也是非常准确的,可以指出这段代码是为了减少广播运算的开销,提高深度学习的性能。

让chatgpt帮读代码到这里就结束了,BroadcastScaleMaskStore/ElementwiseScaleMaskStore的生成结果我也放到了 https://github.com/BBuf/how-to-optim-algorithm-in-cuda/blob/master/oneflow-cuda-optimize-skills/fused_softmax/fused_scale_mask_softmax.cu#L2136 。这次体验让我不得不感叹chatgpt的逻辑理解能力已经非常接近人类了,虽然写代码的能力相对还差一些但相信经过openai的数据以及参数迭代写代码的能力也将突飞猛进。

0x3. 性能表现

接着我们来实测一下这个fused_scale_mask_softmax的效果吧。就以Pattern这一节的代码为例子,使用nsight compute来观测一下fused_scale_mask_softmax相比于原始实现的性能。profile的代码如下:

import numpy as np
import oneflow as flow

batch_size = 4
num_heads = 8
seq_length = 64
broadcast_dim = 1
fill_value = -10000.0
scale_value = 2.0

x = np.random.randn(batch_size, num_heads, seq_length, seq_length).astype(
        np.float32
)
mask_size = [batch_size, num_heads, seq_length, seq_length]
if broadcast_dim:
    mask_size[broadcast_dim] = 1

mask = np.random.randint(0, 2, size=mask_size, dtype=bool)

flow._oneflow_internal.profiler.RangePush('loop begin')
for i in range(100):
   fused_x_tensor = flow.tensor(x, dtype=flow.float32).to("cuda")
   fused_mask_tensor = flow.tensor(mask, dtype=flow.bool).to("cuda")

   flow._oneflow_internal.profiler.RangePush('fused_scale_mask_softmax')
   fused_out = flow._C.fused_scale_mask_softmax(
      fused_x_tensor, fused_mask_tensor, fill_value=fill_value, scale=scale_value,
   )
   flow._oneflow_internal.profiler.RangePop()

   origin_x_tensor = flow.tensor(x).to("cuda")
   origin_mask_tensor = flow.tensor(mask, dtype=flow.float32).to("cuda")
   flow._oneflow_internal.profiler.RangePush('origin scale mask softmax')
   origin_out = flow.mul(
      origin_x_tensor, origin_mask_tensor
   ) * scale_value + fill_value * (1.0 - origin_mask_tensor)
   origin_out = flow.softmax(origin_out, dim=-1)
   flow._oneflow_internal.profiler.RangePop()
flow._oneflow_internal.profiler.RangePop()
assert(np.allclose(fused_out.numpy(), origin_out.numpy(), atol=1e-4, rtol=1e-4))

这里循环了100次计算并且使用内置api flow._oneflow_internal.profiler.RangePush/Pop来埋点。获得nsys文件之后使用NVIDIA Nsight Systems打开并进行分析。原始实现对应的时序图如红色框所示,计算一次的总时间大约100us:

【BBuf的CUDA笔记】九,使用newbing(chatgpt)解析oneflow softmax相关的fuse优化而用上fused_scale_mask_softmax之后的时序图如下所示,可以看到整个计算过程只有一个kernel并且只花了8个us,是非常大的性能提升。
【BBuf的CUDA笔记】九,使用newbing(chatgpt)解析oneflow softmax相关的fuse优化

0x4. 总结

这篇文章展示了一下使用chatgpt阅读oneflow softmax相关的fuse优化(fused_scale_mask_softmax),在惊叹于chatgpt的代码逻辑理解能力的同时也可以体会到cuda中做kernel fuse相比于原始实现所能带来的性能优势。实际上这个fuse在attention中只是局部fuse,已经相对比较落后了。目前TensorRT/Xformers等都提供了多头注意力的完整fuse实现使得多头注意力部分的计算效率更高,oneflow中也集成了上述两种fmha的实现并且做了很多扩展提升易用性,后续有时间我将继续解读。文章来源地址https://www.toymoban.com/news/detail-429710.html

到了这里,关于【BBuf的CUDA笔记】九,使用newbing(chatgpt)解析oneflow softmax相关的fuse优化的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 一个关于宏定义的问题,我和ChatGPT、NewBing、Google Bard、文心一言 居然全军覆没?

    今天在问答模块回答了一道问题,要睡觉的时候,又去看了一眼,发现回答错了。 问题描述 :下面的z的值是多少。 我当时的回答是:z=4+5=9 但是编译运行的结果是:8 又看了一眼,恍然大悟,我曾经在文章中写过: 宏的替换是简单的文本替换(请好好理解这句话),它在预

    2024年02月10日
    浏览(30)
  • 用嘴写代码?继ChatGPT和NewBing之后,微软又开始整活了,Github Copilot X!

    AI盛行的时代来临了,在这段时间,除了爆火的 GPT3.5 后, OpenAI 发布了 GPT4 版本,同时微软也在 Bing 上开始加入了 AI ,也就是 NewBing 。 除了这些,国内大厂也开始AI风暴了,发版的言论不计其数,成果目前简单最成功的是百度推出的文心一言和文心一格,分别对应语言模型和

    2024年02月02日
    浏览(36)
  • 机器学习|Softmax 回归的数学理解及代码解析

    Softmax 回归是一种常用的多类别分类算法,适用于将输入向量映射到多个类别的概率分布。在本文中,我们将深入探讨 Softmax 回归的数学原理,并提供 Python 示例代码帮助读者更好地理解和实现该算法。 Softmax 函数将输入向量的线性得分转换为每个类别的概率。给定一个输入向

    2024年02月12日
    浏览(28)
  • 常见大模型对比[ChatGPT(智能聊天机器人)、Newbing(必应)、Bard(巴德)、讯飞星火认知大模型(SparkDesk)、ChatGLM-6B]

    目录 1 引言 2 选取常见的大模型作为对比项 2.1什么是大模型 2.2 常见大模型 3 相关的大模型介绍和功能 3.1 ChatGPT 3.1.1 ChatGPT的介绍 3.1.2 ChatGPT的原理 3.1.3 ChatGPT的特点 3.2 Newbing 3.2.1 Newbing的介绍 3.2.2 GPT-4的原理 3.2.3 Newbing的特点 3.3 ChatGLM-6B 3.3.1 ChatGLM的介绍 3.3.2 ChatGLM的原理 3.3

    2024年02月11日
    浏览(39)
  • 解决Edge Dev更新后NewBing侧边栏消失的问题,并使用NewBing作画

    问题来源 今天听说NewBing更新了, 支持多模态输出,可以给出聊天文字+图片 可以支持图像输出了,调用的DALL E作画 有保存的历史记录 (听说历史记录在侧边栏,但是我没看到历史记录) 支持插件 (我也没有) 我迫不及待更新了Edge Dev,但是更新之后发现NewBing侧边栏消失了

    2024年02月06日
    浏览(29)
  • 【深度学习笔记】Softmax 回归

    本专栏是网易云课堂人工智能课程《神经网络与深度学习》的学习笔记,视频由网易云课堂与 deeplearning.ai 联合出品,主讲人是吴恩达 Andrew Ng 教授。感兴趣的网友可以观看网易云课堂的视频进行深入学习,视频的链接如下: 神经网络和深度学习 - 网易云课堂 也欢迎对神经网

    2024年02月15日
    浏览(30)
  • (4)深度学习学习笔记-Softmax

    softmax和cross-entorpy 显示图片在pycharm上需要在Animator类中add函数加上花圈的两行。(ctrl+左键对准train_ch3,然后ctrl+f找到animator类) b站 跟李沐学AI 动手学深度学习v2 09

    2024年02月12日
    浏览(35)
  • 评测:对接GPT-4的NewBing使用体验变化

    GPT-4是OpenAI发布的最新一代语言模型,于2023年3月14日正式发布,并通过API和ChatGPT Plus平台向用户开放。 微软也证实,在GPT-4正式发布之前,就已经在部分版本的Bing中使用GPT-4技术。本文旨在对使用GPT-4技术的NewBing进行中立、专业的使用评测,探讨其使用体验的变化。 早于2月

    2024年02月07日
    浏览(38)
  • CMU DLSys 课程笔记 2 - ML Refresher / Softmax Regression

    本节 Slides | 本节课程视频 这一节课是对机器学习内容的一个复习,以 Softmax Regression 为例讲解一个典型的有监督机器学习案例的整个流程以及其中的各种概念。预期读者应当对机器学习的基本概念有一定的了解。 CMU DLSys 课程笔记 2 - ML Refresher / Softmax Regression 目录 机器学习基

    2024年02月03日
    浏览(35)
  • 【AI】《动手学-深度学习-PyTorch版》笔记(十一):分类问题-softmax回归

    1)连续值与离散值 线性回归模型,适用于输出为连续值的情景。 softmax回归模型,适用于输出为离散值的情景。例如图像类别,就需要对离散值进行预测。softmax回归模型引入了softmax运算,使输出更适合离散值的预测和训练。 2)输出个数 线性回归模型,输出单元为1个,而

    2024年02月14日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包