这里接上一节 对C程序的中文注解,下面是对 train_gpt2.cu 的注解,所有注解来自ChatGPT4。
1. train_gpt2.cu
1.1 开始
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
/* GPT-2 Transformer Neural Net trained in raw CUDA Non-trivial notes to be aware of: GPT-2 Transformer神经网络在原始CUDA中进行训练 需要注意的非平凡细节: We are being clever in the backward pass to conserve memory. In particular, all parameters use a += in the backward pass, so we can later do gradient accumulation. But all activations have = instead of += because these are faster (just read, no write). This is okay for all activations except for those in the residual stream, where the gradients have to add. We make sure that those parts work out ok and that we do a += as necessary. E.g., the layernorms are connected to the residuals so we += in layernorm backward. 我们在反向传播中巧妙地节省内存。 特别是,所有参数在反向传播中使用 +=,因此我们 可以随后进行梯度累积。但所有激活函数都使用 = 而不是 +=, 因为这样更快(只读,不写)。这对所有激活函数都是可以的, 除了残差流中的那些,其梯度需要相加。我们确保这些部分 可以正确运行,并且在必要时执行 +=。例如, layernorms 与残差相连,所以我们在layernorm的反向传播中执行 +=。 */ #include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <math.h> #include <time.h> #include <assert.h> #include <float.h> #include <string.h> #include <unistd.h> #include <assert.h> #include <cublas_v2.h> #include <cuda_runtime.h> #include <cublasLt.h> #include <cooperative_groups.h> #include <cooperative_groups/reduce.h> |
1.2 CUDA 工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
// ---------------------------------------------------------------------------- // CUDA utils // CUDA 工具 // convenience macro for calculating grid/block dimensions for kernels // 用于计算内核的网格/块尺寸的便捷宏 #define CEIL_DIV(M, N) (((M) + (N)-1) / (N)) // CUDA error checking // CUDA 错误检查 // 这段代码是一个用于检查CUDA函数调用是否成功的工具函数。 // 如果调用失败,它会打印出错误所在的文件和行号,以及错误描述,然后退出程序。 // 这种错误检查是CUDA编程中常用的做法,用以确保CUDA调用正确执行, // 从而便于追踪和处理可能发生的问题。 void cudaCheck(cudaError_t error, const char *file, int line) { if (error != cudaSuccess) { printf("[CUDA ERROR] at file %s:%d:\n%s\n", file, line, cudaGetErrorString(error)); exit(EXIT_FAILURE); } }; #define cudaCheck(err) (cudaCheck(err, __FILE__, __LINE__)) // cuBLAS error checking // cuBLAS 错误检查 // 这段代码是一个用于检查cuBLAS(一个NVIDIA的CUDA基础线性代数子程序库)操作的状态的函数。 // 如果cuBLAS函数调用未能返回CUBLAS_STATUS_SUCCESS,即表示调用出现错误, // 该函数会打印错误代码、文件名和行号,并使程序退出。 // 此函数的主要目的是为了在程序中及时发现和报告cuBLAS库操作的错误,从而便于调试和保证程序的稳定运行。 void cublasCheck(cublasStatus_t status, const char *file, int line) { if (status != CUBLAS_STATUS_SUCCESS) { printf("[cuBLAS ERROR]: %d %s %d\n", status, file, line); exit(EXIT_FAILURE); } } #define cublasCheck(status) { cublasCheck((status), __FILE__, __LINE__); } // cuBLAS workspace. Hardcoding to 32MiB but only Hopper needs 32, for others 4 is OK // cuBLAS工作空间。硬编码为32MiB,但只有Hopper需要32,其他的4MiB就够了 // 这段代码主要涉及CUDA和cuBLAS库的初始化和配置工作。 // 它设置了cuBLAS Lt(一个针对Tensor Core优化的cuBLAS子库)所需的工作空间大小, // 并初始化了几个重要的库句柄和变量。cooperative_groups命名空间则用于CUDA中的线程组协作, // 此处通过cg别名进行引用,以方便在代码中使用。 // // 将工作空间大小设置为32MiB static size_t cublaslt_workspace_size = 32 * 1024 * 1024; // 初始化cuBLAS Lt工作空间指针为NULL static void* cublaslt_workspace = NULL; // 声明一个cuBLAS计算类型变量 static cublasComputeType_t cublas_compute_type; // 声明一个cuBLAS库的句柄 cublasHandle_t cublas_handle; // 声明一个cuBLAS Lt库的句柄 cublasLtHandle_t cublaslt_handle; // 创建别名cg,指向cooperative_groups命名空间 namespace cg = cooperative_groups; |
1.3 文件和内存工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
// ---------------------------------------------------------------------------- // fread convenience utils, with nice handling of error checking using macros // simple replace fopen, fread, fclose with fopenCheck, freadCheck, fcloseCheck // fread方便工具,使用宏进行优雅的错误检查处理 // 简单替换fopen, fread, fclose为fopenCheck, freadCheck, fcloseCheck FILE *fopen_check(const char *path, const char *mode, const char *file, int line) { FILE *fp = fopen(path, mode); if (fp == NULL) { fprintf(stderr, "Error: Failed to open file '%s' at %s:%d\n", path, file, line); fprintf(stderr, "Error details:\n"); fprintf(stderr, " File: %s\n", file); fprintf(stderr, " Line: %d\n", line); fprintf(stderr, " Path: %s\n", path); fprintf(stderr, " Mode: %s\n", mode); exit(EXIT_FAILURE); } return fp; } #define fopenCheck(path, mode) fopen_check(path, mode, __FILE__, __LINE__) void fread_check(void *ptr, size_t size, size_t nmemb, FILE *stream, const char *file, int line) { size_t result = fread(ptr, size, nmemb, stream); if (result != nmemb) { if (feof(stream)) { fprintf(stderr, "Error: Unexpected end of file at %s:%d\n", file, line); } else if (ferror(stream)) { fprintf(stderr, "Error: File read error at %s:%d\n", file, line); } else { fprintf(stderr, "Error: Partial read at %s:%d. Expected %zu elements, read %zu\n", file, line, nmemb, result); } fprintf(stderr, "Error details:\n"); fprintf(stderr, " File: %s\n", file); fprintf(stderr, " Line: %d\n", line); fprintf(stderr, " Expected elements: %zu\n", nmemb); fprintf(stderr, " Read elements: %zu\n", result); exit(EXIT_FAILURE); } } #define freadCheck(ptr, size, nmemb, stream) fread_check(ptr, size, nmemb, stream, __FILE__, __LINE__) void fclose_check(FILE *fp, const char *file, int line) { if (fclose(fp) != 0) { fprintf(stderr, "Error: Failed to close file at %s:%d\n", file, line); fprintf(stderr, "Error details:\n"); fprintf(stderr, " File: %s\n", file); fprintf(stderr, " Line: %d\n", line); exit(EXIT_FAILURE); } } #define fcloseCheck(fp) fclose_check(fp, __FILE__, __LINE__) // ---------------------------------------------------------------------------- // malloc error-handling wrapper util // malloc 错误处理封装工具 void *malloc_check(size_t size, const char *file, int line) { void *ptr = malloc(size); if (ptr == NULL) { fprintf(stderr, "Error: Memory allocation failed at %s:%d\n", file, line); fprintf(stderr, "Error details:\n"); fprintf(stderr, " File: %s\n", file); fprintf(stderr, " Line: %d\n", line); fprintf(stderr, " Size: %zu bytes\n", size); exit(EXIT_FAILURE); } return ptr; } #define mallocCheck(size) malloc_check(size, __FILE__, __LINE__) |
1.4 warpReduceMax
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// all the kernels // 所有的核函数 // warp-level reduction for finding the maximum value // 用于寻找最大值的warp级别归约 __device__ float warpReduceMax(float val) { for (int offset = 16; offset > 0; offset /= 2) { // 使用__shfl_down_sync函数进行同步的shuffle操作 val = fmaxf(val, __shfl_down_sync(0xFFFFFFFF, val, offset)); } // 返回归约后的最大值 return val; } |
这段CUDA设备代码定义了一个名为warpReduceMax
的函数,用于在GPU的warp级别进行并行归约,以找到最大值。在这个函数中,通过迭代减半偏移量并使用CUDA的__shfl_down_sync
内建函数来传递和比较浮点数值。这个内建函数使得线程能够从相同warp中的另一个线程获取变量值,实现高效的数据交换和归约。每一步归约操作都使用fmaxf
函数保证取得两个值中的最大值,从而最终在一个warp内得到最大值。
1.5 warpReduceSum
1 2 3 4 5 6 7 8 9 10 |
// warp-level reduction for summing values // 用于求和的warp级别归约 __device__ float warpReduceSum(float val) { for (int offset = 16; offset > 0; offset /= 2) { // 使用__shfl_down_sync函数进行同步的shuffle操作,累加值 val += __shfl_down_sync(0xFFFFFFFF, val, offset); } // 返回归约后的总和 return val; } |
1.6 encoder_forward_kernel2
这段代码定义了一个名为encoder_forward_kernel2
的CUDA全局内核函数,用于在深度学习模型中的编码器前向传播计算中。函数接收几个参数,包括输出向量、输入索引、词嵌入向量、位置嵌入向量以及一些维度参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// float* out: 输出数据的指针。 // int* inp: 输入索引的指针。 // float* wte: 词嵌入权重的指针。 // float* wpe: 位置嵌入权重的指针。 // int B: batch大小。 // int T: 序列长度。 // int C: 嵌入维度。 __global__ void encoder_forward_kernel2(float* out, int* inp, float* wte, float* wpe, int B, int T, int C) { // 1.索引计算:通过blockIdx.x, blockDim.x和threadIdx.x计算当前线程的全局索引idx。 int idx = blockIdx.x * blockDim.x + threadIdx.x; int N = B * T * C; // 2.边界检查:确保idx在有效范围内,即小于N,其中N = B * T * C为输出数据的总元素数。 if (idx < N) { // 3.映射到三维索引:计算对应的batch索引b,时间步索引t和嵌入维度索引c。 int bt = idx / C; int b = bt / T; int t = bt % T; int c = idx % C; // 4.数据访问:通过给定的输入索引inp[b * T + t]获取词嵌入索引ix。 int ix = inp[b * T + t]; // 5. 权重访问和相加:通过wte和wpe访问相应的词嵌入和位置嵌入,并将它们相加得到输出。 float* out_btc = out + b * T * C + t * C + c; float* wte_ix = wte + ix * C + c; float* wpe_tc = wpe + t * C + c; *out_btc = *wte_ix + *wpe_tc; } } |
该内核函数的作用是对每个输入词汇的词嵌入和对应的位置嵌入进行相加,这是许多基于注意力的神经网络架构(如Transformer)中的常见步骤。通过利用CUDA的并行计算能力,该函数能够高效地处理大规模数据集,适用于处理大型语言模型或其他需要大量并行文本数据处理的应用。
1.7 encoder_backward_kernel
这段代码定义了一个名为encoder_backward_kernel
的CUDA全局内核函数,用于在深度学习模型中的编码器反向传播过程中的梯度更新。这个函数使用了atomicAdd
,一个原子操作函数,以确保在并行执行中对共享数据的修改是安全的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// really bad naive kernel with atomicAdd // 使用atomicAdd的非常初级的内核函数 // // float* dwte: 词嵌入梯度的指针。 // float* dwpe: 位置嵌入梯度的指针。 // const float* dout: 来自后续层的梯度传递的指针。 // const int* inp: 输入索引的指针。 // int B: batch大小。 // int T: 序列长度。 // int C: 嵌入维度。 // 核心计算过程包括: __global__ void encoder_backward_kernel(float* dwte, float* dwpe, const float* dout, const int* inp, int B, int T, int C) { // 1. 索引计算:通过blockIdx.x, blockDim.x和threadIdx.x计算当前线程的全局索引idx。 int idx = blockIdx.x * blockDim.x + threadIdx.x; int N = B * T * C; // 2. 边界检查:确保idx在有效范围内,即小于N,其中N = B * T * C为数据的总元素数。 if (idx < N) { // 3. 映射到三维索引:计算对应的batch索引b,时间步索引t和嵌入维度索引c。 int bt = idx / C; int b = bt / T; int t = bt % T; int c = idx % C; // 4. 数据访问:通过给定的输入索引inp[b * T + t]获取词嵌入和位置嵌入的更新索引ix。 int ix = inp[b * T + t]; // 5. 梯度累加:使用atomicAdd来安全地在dwte和dwpe的对应位置累加梯度值。 const float* dout_btc = dout + b * T * C + t * C + c; float* dwte_ix = dwte + ix * C + c; float* dwpe_tc = dwpe + t * C + c; atomicAdd(dwte_ix, *dout_btc); atomicAdd(dwpe_tc, *dout_btc); } } |
性能考量: 使用atomicAdd
在GPU编程中是出于需要保证并行更新的数据一致性和正确性。然而,atomicAdd
可能导致性能瓶颈,尤其是当多个线程频繁地对同一内存位置进行更新时,这可能导致严重的性能下降。这个内核函数被标记为“naive”(初级的、简单的)主要是因为它在设计上没有优化这种高冲突的写操作,可能会在实际应用中遇到性能问题。在大规模数据和高度并行的情况下,优化这种类型的内核是非常重要的,比如通过设计更高效的数据访问模式或使用更先进的并行归约技术来减少对atomicAdd
的依赖。
1.8 layernorm_forward_kernel3
这段代码定义了一个名为layernorm_forward_kernel3
的CUDA全局内核函数,用于执行层归一化(Layer Normalization)的前向计算。该函数使用了NVIDIA Cooperative Groups库来进行并行计算,优化了内存访问和数据归约过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
// float* __restrict__ out: 输出数组的指针。 // float* __restrict__ mean: 均值数组的指针。 // float* __restrict__ rstd: 逆标准差数组的指针。 // const float* __restrict__ inp: 输入数组的指针。 // const float* __restrict__ weight: 权重数组的指针。 // const float* __restrict__ bias: 偏差数组的指针。 // int N: 处理的数据行数。 // int C: 每行的数据数量。 __global__ void layernorm_forward_kernel3(float* __restrict__ out, float* __restrict__ mean, float* __restrict__ rstd, const float* __restrict__ inp, const float* __restrict__ weight, const float* __restrict__ bias, int N, int C) { // 1. 线程块和分区:使用cooperative_groups库的功能来定义线程块和分区,以确保更有效的数据处理和归约。 cg::thread_block block = cg::this_thread_block(); cg::thread_block_tile<32> warp = cg::tiled_partition<32>(block); int idx = blockIdx.x * warp.meta_group_size() + warp.meta_group_rank(); if(idx >= N) { return; } // the row of input that this group of threads is responsible for // 这组线程负责的输入行 const float* x = inp + idx * C; // mean // 2. 均值计算:每个warp计算自己负责的输入行的元素之和,然后使用cg::reduce函数进行归约得到均值。 float sum = 0.0f; for (int i = warp.thread_rank(); i < C; i += warp.size()) { sum += x[i]; } sum = cg::reduce(warp, sum, cg::plus<float>{}); float m = sum / C; if(warp.thread_rank() == 0 && mean != nullptr) { __stcs(mean + idx, m); } // rstd // 3. 逆标准差计算:计算每个元素与均值的差的平方和,再次使用cg::reduce进行归约,最后计算逆标准差。 sum = 0.0f; for (int i = warp.thread_rank(); i < C; i += warp.size()) { float diff = x[i] - m; sum += diff * diff; } sum = cg::reduce(warp, sum, cg::plus<float>{}); float s = rsqrtf(sum / C + 1e-5f); if(warp.thread_rank() == 0 && rstd != nullptr) { __stcs(rstd + idx, s); } // final normalization and scaling by weight/bias // 最终的归一化和通过权重/偏差的缩放处理 // 4. 归一化和缩放:对输入数据进行归一化,然后乘以权重并加上偏差,计算最终的输出值。 float* o = out + idx * C; for (int c = warp.thread_rank(); c < C; c += warp.size()) { // load and store using the .cs "streaming" hint to the compiler, // indicating that this data will not be reused soon, and can be streamed through the caches // this allows the threads to get more cache-hits for the (shared) weight and bias parameters float n = s * (__ldcs(x+c) - m); __stcs(o+c, n * weight[c] + bias[c]); } } |
性能优化措施:
- 缓存提示:使用
.cs
(cache streaming)指示符来告诉编译器可以通过缓存进行数据流处理,这有助于提高内存访问效率。 - 并行归约:使用
cooperative_groups
中的归约操作来优化并行归约的性能。
这种实现方式不仅保证了计算的准确性,也优化了执行效率,特别适用于处理大规模数据集,如在深度学习模型的训练和推理中常见的场景。
既然ChatGPT4 提出了优化,那继续让ChatGPT4修改代码,下面是ChatGPT4 的优化代码,由于涉及到共享内存,那自然逻辑就变了,后面看是否可以解决这个问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
__global__ void layernorm_forward_kernel3(float* __restrict__ out, float* __restrict__ mean, float* __restrict__ rstd, const float* __restrict__ inp, const float* __restrict__ weight, const float* __restrict__ bias, int N, int C) { extern __shared__ float shared_data[]; float* shared_weight = shared_data; float* shared_bias = &shared_data[C]; cg::thread_block block = cg::this_thread_block(); cg::thread_block_tile<32> warp = cg::tiled_partition<32>(block); int idx = blockIdx.x * blockDim.x + threadIdx.x; if(idx >= N) { return; } if (threadIdx.x < C) { shared_weight[threadIdx.x] = weight[threadIdx.x]; shared_bias[threadIdx.x] = bias[threadIdx.x]; } __syncthreads(); // 确保所有数据都被加载到共享内存 const float* x = inp + idx * C; float sum = 0.0f; for (int i = warp.thread_rank(); i < C; i += warp.size()) { sum += x[i]; } sum = cg::reduce(warp, sum, cg::plus<float>{}); float m = sum / C; if(warp.thread_rank() == 0 && mean != nullptr) { __stcs(mean + idx, m); } // 逆标准差计算 sum = 0.0f; for (int i = warp.thread_rank(); i < C; i += warp.size()) { float diff = x[i] - m; sum += diff * diff; } sum = cg::reduce(warp, sum, cg::plus<float>{}); float s = rsqrtf(sum / C + 1e-5f); if(warp.thread_rank() == 0 && rstd != nullptr) { __stcs(rstd + idx, s); } // 归一化和缩放 float* o = out + idx * C; for (int c = warp.thread_rank(); c < C; c += warp.size()) { float n = s * (__ldcs(x+c) - m); __stcs(o+c, n * shared_weight[c] + shared_bias[c]); } } |
这段代码确保:
- 权重和偏置是通过共享内存进行访问的,从而减少了全局内存的访问次数和提高了访问效率。
- 使用cooperative_groups的warp级别归约来优化归约操作的效率。
- 使用缓存流指示符
.cs
来优化数据加载和存储操作。
请注意,共享内存的大小需要在内核调用时指定,这通常是在内核调用的配置参数中设置,如 <<<numBlocks, blockSize, sharedMemSize>>>
。在这种情况下,sharedMemSize
应设置为 2 * C * sizeof(float)
,以确保为权重和偏置分配足够的共享内存空间。
1.9 permute_kernel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
__global__ void permute_kernel(float* q, float* k, float* v, const float* inp, int B, int N, int NH, int d) { // okay so now, this kernel wants Q,K,V to all be of shape (B, NH, N, d) // but instead, we have a single tensor QKV (inp) of shape (B, N, 3, NH, d) // 此内核函数的目的是将Q, K, V的形状变为 (B, NH, N, d) // 但原始的输入张量QKV (inp) 的形状为 (B, N, 3, NH, d) int idx = blockIdx.x * blockDim.x + threadIdx.x; // Q[b][nh_][n][d_] = inp[b][n][0][nh_][d_] // 计算Q, K, V的索引,即 Q[b][nh_][n][d_] = inp[b][n][0][nh_][d_] if (idx < B * NH * N * d) { // 计算batch索引 b int b = idx / (NH * N * d); int rest = idx % (NH * N * d); // 计算头索引 nh_ int nh_ = rest / (N * d); rest = rest % (N * d); // 计算序列索引 n int n = rest / d; // 计算维度索引 d_ int d_ = rest % d; // 计算inp中对应的索引位置 int inp_idx = (b * N * 3 * NH * d) + (n * 3 * NH * d) + (0 * NH * d) + (nh_ * d) + d_; // 从inp的特定位置加载Query数据到q q[idx] = __ldcs(&inp[inp_idx]); // 从inp的特定位置加载Key数据到k(注意偏移量为NH * d) k[idx] = __ldcs(&inp[inp_idx + NH * d]); // 从inp的特定位置加载Value数据到v(注意偏移量为2 * NH * d) v[idx] = __ldcs(&inp[inp_idx + 2 * (NH * d)]); } } |
解释
- 索引计算:每个线程通过其全局索引
idx
负责处理一个特定的数据元素。全局索引是根据块的索引(blockIdx.x
)、块的维度(blockDim.x
)和线程的索引(threadIdx.x
)计算得出。 - 形状转换:输入张量
inp
的形状是(B, N, 3, NH, d)
,其中3代表合并了Q、K、V三种类型的数据。此函数的目标是将这些数据重新排列为三个分开的张量,每个张量的形状为(B, NH, N, d)
。 - 内存访问:使用
__ldcs
(load from constant memory with caching)函数从常量内存加载数据,这有助于提高内存访问的效率,因为该函数可以利用GPU的缓存机制。
这种实现方式使得内核能高效地从一个复合张量中提取并重排数据到三个独立的张量中,这是在处理基于Transformer架构的神经网络模型中常见的操作,特别是在多头自注意力机制的实现中。
1.10 permute_kernel_backward
这段代码定义了一个名为 permute_kernel_backward
的CUDA内核函数,用于执行反向传播过程中从分散的查询(Query, Q)、键(Key, K)和值(Value, V)张量重新构造回合并张量 dinp
。这个内核函数实质上是 permute_kernel
的逆过程,其中将分开的张量重新组合到一个大的张量中,用于可能的后续梯度传递或参数更新。下面是对代码的详细解释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// float* dinp:输出的合并后的梯度张量,形状为 (B, N, 3, NH, d)。 // const float* dq:输入的Query梯度张量,形状为 (B, NH, N, d)。 // const float* dk:输入的Key梯度张量,形状为 (B, NH, N, d)。 // const float* dv:输入的Value梯度张量,形状为 (B, NH, N, d)。 // int B:批处理大小。 // int N:序列长度。 // int NH:注意力头数。 // int d:每个头的特征维数。 __global__ void permute_kernel_backward(float* dinp, const float* dq, const float* dk, const float* dv, int B, int N, int NH, int d) { // 1. 索引计算:通过blockIdx.x * blockDim.x + threadIdx.x计算当前线程的全局索引idx。 int idx = blockIdx.x * blockDim.x + threadIdx.x; // 2. 边界检查:确保idx在有效范围内(即小于B * NH * N * d)。 if (idx < B * NH * N * d) { // 3. 映射到三维索引:通过一系列的整数除法和取余操作,解析出五维索引中的b(批次索引)、nh_(头索引)、n(序列索引)和d_(维度索引)。 int b = idx / (NH * N * d); int rest = idx % (NH * N * d); int nh_ = rest / (N * d); rest = rest % (N * d); int n = rest / d; int d_ = rest % d; // 4. 合并张量索引计算:计算在输出合并张量dinp中对应位置的索引inp_idx。 int inp_idx = (b * N * 3 * NH * d) + (n * 3 * NH * d) + (0 * NH * d) + (nh_ * d) + d_; // 将Query的梯度dq[idx]赋值到dinp的对应位置。 dinp[inp_idx] = dq[idx]; // 将Key的梯度dk[idx]赋值到dinp的NH * d后的位置。 dinp[inp_idx + NH * d] = dk[idx]; // 将Value的梯度dv[idx]赋值到dinp的2 * NH * d后的位置。 dinp[inp_idx + 2 * (NH * d)] = dv[idx]; } } |
性能考虑:
- 这个内核利用了简单的线性内存操作来重建合并张量,对内存带宽的需求较高。
- 内存访问模式是连续的,这有助于GPU进行高效的内存访问和缓存优化。
- 该内核函数可以并行地由多个线程执行,每个线程独立处理一个数据点,从而实现高并行性。
通过这种方式,permute_kernel_backward
在保证数据正确性的同时实现了高效的内存操作,适合在深度学习模型中进行梯度的反向传播计算,特别是在处理基于Transformer结构的模型时。
1.11 unpermute_kernel
这段CUDA内核函数 unpermute_kernel
的目的是对一个张量进行重新排列(反置换),以符合某个特定的维度顺序。输入张量 inp
的形状为 (B, NH, N, d)
,而输出张量 out
的期望形状为 (B, N, NH, d)
。这种变换在处理深度学习模型中的数据时很常见,尤其是在需要改变数据布局以适应不同操作或层的要求时。
以下是对代码的详细解释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// float* inp:输入张量,其形状为 (B, NH, N, d)。 // float* out:输出张量,其目标形状为 (B, N, NH, d)。 // int B:批处理大小。 // int N:序列长度。 // int NH:注意力头数。 // int d:每个头的特征维数。 __global__ void unpermute_kernel(float* inp, float *out, int B, int N, int NH, int d) { // out has shape (B, nh, N, d) but we need to unpermute it to (B, N, nh, d) // 1. 索引计算:通过 blockIdx.x * blockDim.x + threadIdx.x 计算当前线程的全局索引 idx,这一索引代表 inp 中的线性位置。 int idx = blockIdx.x * blockDim.x + threadIdx.x; // out[b][n][nh_][d_] <- inp[b][nh_][n][d_] // 2. 边界检查:确保 idx 在有效范围内(即小于 B * NH * N * d)。 if (idx < B * NH * N * d) { // 3. 映射到四维索引:通过整数除法和取余操作,将一维线性索引 idx 转换成四维索引 (b, nh_, n, d_),这四个索引分别对应批次、头索引、序列索引和维度索引。 int b = idx / (NH * N * d); int rest = idx % (NH * N * d); int nh_ = rest / (N * d); rest = rest % (N * d); int n = rest / d; int d_ = rest % d; // 4. 计算输出张量的索引:根据输入的四维索引计算输出张量 out 的对应索引 other_idx。这一步是将 nh_(头索引)和 n(序列索引)的位置互换,以符合输出张量的期望形状。 int other_idx = (b * NH * N * d) + (n * NH * d) + (nh_ * d) + d_; // 5. 数据赋值:将输入张量 inp 中的数据根据计算出的新索引赋值到输出张量 out 中。使用 __ldcs (从常量内存加载数据并缓存)可以优化内存访问 out[other_idx] = __ldcs(&inp[idx]); } } |
性能考虑:
- 内存访问模式:由于数据重新排列通常涉及非连续的内存访问模式,这可能导致内存访问效率降低。使用
__ldcs
可以帮助减少这种影响,因为它允许更有效地使用GPU的缓存。 - 并行度:内核的设计允许完全并行的执行,每个线程独立处理一个数据点,这有助于高效利用GPU的并行处理能力。
此内核适用于在需要将数据从一个布局转换为另一个布局的场景中,常见于处理多头自注意力机制输出的数据转换,特别是在将这些数据传递到后续层之前。
1.12 unpermute_kernel_backward
这段CUDA内核函数 unpermute_kernel_backward
用于执行反向过程的数据重排列(反向置换)。它从变换后的梯度张量 dout
中读取数据,并将其正确放回原始梯度张量 dinp
的相应位置。这通常是在深度学习模型的反向传播过程中需要的,以确保梯度可以正确地传递到适当的层。该函数是 unpermute_kernel
的逆操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// float* dinp:输出张量,需要将梯度数据写入的张量,其形状为 (B, NH, N, d)。 // const float* dout:输入张量,来源于前向传播的反置换,其形状为 (B, N, NH, d)。 // int B:批处理大小。 // int N:序列长度。 // int NH:注意力头数。 // int d:每个头的特征维数。 __global__ void unpermute_kernel_backward(float* dinp, const float *dout, int B, int N, int NH, int d) { // 1. 索引计算:通过 blockIdx.x * blockDim.x + threadIdx.x 计算当前线程的全局索引 idx,这一索引表示 dinp 中的线性位置。 int idx = blockIdx.x * blockDim.x + threadIdx.x; // 2. 边界检查:确保 idx 在有效范围内(即小于 B * NH * N * d)。 if (idx < B * NH * N * d) { // 3. 解析四维索引:通过整数除法和取余操作,将一维线性索引 idx 转换成四维索引 (b, nh_, n, d_),这四个索引分别对应批次、头索引、序列索引和维度索引。 int b = idx / (NH * N * d); int rest = idx % (NH * N * d); int nh_ = rest / (N * d); rest = rest % (N * d); int n = rest / d; int d_ = rest % d; // 4. 计算输入张量的索引:根据输入的四维索引计算输入张量 dout 的对应索引 other_idx。这里互换了 nh_ 和 n 的位置,以匹配 dout 的形状 (B, N, NH, d)。 int other_idx = (b * NH * N * d) + (n * NH * d) + (nh_ * d) + d_; // 5. 数据赋值:将输入张量 dout 中的数据根据计算出的索引 other_idx 赋值到输出张量 dinp 的对应位置。 dinp[idx] = dout[other_idx]; } } |
性能考虑:
- 内存访问模式:由于数据重排通常涉及非连续的内存访问,这可能导致内存访问效率降低。合理利用内存访问模式和缓存可以帮助改善性能。
- 并行度:内核的设计使得可以并行地执行,每个线程独立处理一个数据点,从而高效利用GPU的并行处理能力。
这种内核函数在深度学习的反向传播中特别有用,因为它确保梯度可以正确地按原始前向传播时的布局反向传递,这对于基于模型参数的正确更新至关重要。
1.13 vec_at
这两个CUDA设备函数 vec_at
是为了提供对 float4
类型向量中单个元素的访问。float4
是一个内置的CUDA数据类型,它封装了四个浮点数。这些函数通过对 float4
对象使用 reinterpret_cast
来作为 float
数组进行处理,从而可以直接访问其单个元素。这样的实现增加了灵活性,允许以数组索引的方式访问 float4
的各个组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 非常量版本 // 这个版本返回一个对 float4 中相应浮点数的引用。这允许你直接修改 float4 实例中的相应值。 // 例如,可以使用这个函数来设置 float4 中某个特定位置的值。 __device__ float& vec_at(float4& vec, int index) { return reinterpret_cast<float*>(&vec)[index]; } // 常量版本 // 这个版本适用于不需要修改 float4 实例内容的场景。它返回的是一个值,而不是引用, // 这保证了函数使用的安全性,避免了不小心修改数据的风险。 __device__ float vec_at(const float4& vec, int index) { return reinterpret_cast<const float*>(&vec)[index]; } |
应用场景:
这些函数在需要对 float4
数据结构进行更细粒度操作时非常有用,特别是在处理图形和物理计算中,这些计算可能需要对单独的向量组件进行读取或修改。例如,当你需要根据运算结果动态修改某个组件而不影响其他组件时,这种方法非常适用。
性能和安全性:
- 性能:由于这些函数只涉及类型转换和基本的索引操作,它们的性能开销非常低。
reinterpret_cast
在运行时几乎没有成本,因为它仅是在编译器层面上重新解释已有的数据。 - 安全性:尽管
reinterpret_cast
用法简单直接,但使用时需要保证不会越界访问,即索引值必须在 0 到 3 之间。超出这个范围,行为是未定义的,可能会导致错误或数据损坏。
1.14 softmax_forward_kernel5
这段CUDA内核 softmax_forward_kernel5
主要用于执行带有温度因子的Softmax函数的前向计算,特别是在自注意力机制中处理序列数据。它利用块、线程和分块的CUDA特性来高效地计算Softmax。此内核计算 (N, T, T)
形状张量的Softmax,其中 N
通常是批大小与头数的乘积,T
是序列长度。
核心特性与操作:
- 温度因子调整:通过
inv_temperature
(温度的倒数)调整Softmax的敏感度。 - 反向迭代:为了缓存优化,内核从后向前计算,以便在接下来的矩阵乘法操作中更好地利用缓存。
- 块与线程使用:
- 使用 Cooperative Groups (
cg
) 库来管理线程之间的协作,包括数据归约。 - 通过
warp
将线程分组,以减少同步和归约操作的复杂度。
- 使用 Cooperative Groups (
- 在线Softmax计算:采用在线算法逐步计算Softmax,即边读取边计算,有效避免了一次性读入整行数据可能引起的内存压力和计算延迟。
计算细节:
- 最大值和求和:为了数值稳定性,首先在Softmax计算前找出最大值,然后根据最大值调整所有数值进行指数运算,并累加。
- 归约操作:使用
cg::reduce
进行跨线程的最大值和求和归约。 - 标准化:将归约后得到的求和值用于归一化所有指数值,以得到Softmax的输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
// float* out: 输出数组,存储计算完成后的Softmax结果,形状为 (N, T, T)。 // float inv_temperature: 温度因子的倒数。在Softmax计算中,温度因子用于调整输出分布的“尖锐度”,其中较低的温度使输出更尖锐(更集中于最大值)。 // const float* inp: 输入数组,存储Softmax计算之前的原始得分或对数概率,形状同样为 (N, T, T)。 // int N: 第一维的大小,通常是批大小(B)与头数(NH)的乘积,即 N = B * NH。 // int T: 序列长度或时间步的数量,同样用于表示 inp 和 out 的第二和第三维。 __global__ void softmax_forward_kernel5(float* out, float inv_temperature, const float* inp, int N, int T) { // inp, out shape: (N, T, T), where N = B * NH // fuses the multiplication by scale inside attention // directly autoregressive, so we only compute the lower triangular part // uses the online softmax algorithm // inp, out形状:(N, T, T),其中N = B * NH // 在注意力计算中融合了缩放乘法 // 直接自回归,因此我们只计算下三角部分 // 使用在线Softmax算法 assert(T % 4 == 0); cg::thread_block block = cg::this_thread_block(); cg::thread_block_tile<32> warp = cg::tiled_partition<32>(block); // micro-optimization: we iterate backwards so that // after the softmax backward operation completes, the cache retains the // part of the matrix close to the upper left corner, which benefits the // matmul operation that immediately follows. // int idx = blockIdx.x * warp.meta_group_size() + warp.meta_group_rank(); // forward order // 微优化:我们反向迭代,这样 // 在Softmax反向操作完成后,缓存会保留接近左上角的 // 矩阵部分,这对紧接着的矩阵乘法操作有益。 // int idx = blockIdx.x * warp.meta_group_size() + warp.meta_group_rank(); // 正向顺序 // idx 反向计算索引以利用缓存,改善性能。这里调整了索引计算方式,以从数据末尾向开头进行迭代,帮助后续的矩阵乘法操作中缓存使用。 int idx = (gridDim.x - blockIdx.x - 1) * warp.meta_group_size() + warp.meta_group_rank(); // backward order if(idx >= N * T) { return; } int own_pos = idx % T; int pos_by_4 = own_pos / 4; // one row of inp, i.e. inp[idx, :] of shape (T,) // inp的一行,即 inp[idx, :] 的形状是 (T,) const float* x = inp + idx * T; // not INF, so we don't get NaNs accidentally when subtracting two values. // 不是无穷大,这样在减去两个值时不会意外得到NaN。 float maxval = -FLT_MAX; float sumval = 0.0f; // 计算Softmax的核心步骤,包括最大值搜索、指数求和和归一化 const float4* x_vec = reinterpret_cast<const float4*>(x); for (int i = warp.thread_rank(); i < pos_by_4; i += warp.size()) { float4 v = x_vec[i]; float old_maxval = maxval; for(int k = 0; k < 4; ++k) { // 更新最大值 maxval = fmaxf(maxval, vec_at(v, k)); } // 调整旧的求和值 sumval *= expf(inv_temperature * (old_maxval - maxval)); for(int k = 0; k < 4; ++k) { // 累加新的指数值 sumval += expf(inv_temperature * (vec_at(v, k) - maxval)); } } if(4*pos_by_4 + warp.thread_rank() <= own_pos) { // 单个元素的处理,以处理不能整除4的剩余元素 float old_maxval = maxval; maxval = fmaxf(maxval, x[4*pos_by_4 + warp.thread_rank()]); sumval *= expf(inv_temperature * (old_maxval - maxval)); sumval += expf(inv_temperature * (x[4*pos_by_4 + warp.thread_rank()] - maxval)); } float global_maxval = cg::reduce(warp, maxval, cg::greater<float>{}); sumval *= expf(inv_temperature * (maxval - global_maxval)); float sum = cg::reduce(warp, sumval, cg::plus<float>{}); float norm = 1.f / sum; // divide the whole row by the sum for (int i = warp.thread_rank(); i <= own_pos; i += warp.size()) { // recalculation is faster than doing the round-trip through memory. float ev = expf(inv_temperature * (__ldcs(x + i) - global_maxval)); __stcs(out + idx * T + i, ev * norm); } } |
性能优化:
- 矢量化访问:通过使用
float4
进行矢量化内存访问来提高带宽利用率。 - 局部计算与延迟加载:通过在需要时才加载并计算数据,减少了内存访问次数,提高了计算效率。
- 缓存优化:通过反向迭代顺序,尽量保留在缓存中频繁访问的数据。
这个内核在设计上非常适合用于处理大规模数据集合,在例如Transformer模型中处理自注意力的Softmax层时能够提供高效的计算性能。
1.15 residual_forward_kernel
这段CUDA内核函数 residual_forward_kernel
用于计算残差连接的输出。在深度学习中,尤其是在像Transformer这样的网络结构中,残差连接是一种常用的技术,它有助于减少梯度消失问题,允许更深的网络结构进行训练。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// float* out: 输出数组,存储结果数据。这个数组的长度应该与输入数组 inp1 和 inp2 一致。 // float* inp1: 第一个输入数组,其中包含一些前一层或操作的输出数据。 // float* inp2: 第二个输入数组,通常包含另一层或操作的输出数据,这两个数组在相同索引处的元素将被相加。 // int N: 数组 inp1、inp2 和 out 的元素数量,指明了在这三个数组中有多少元素需要进行处理。 __global__ void residual_forward_kernel(float* out, float* inp1, float* inp2, int N) { // 1. 索引计算:通过 blockIdx.x * blockDim.x + threadIdx.x 计算当前线程的全局索引 idx,这一索引用于访问输入和输出数组中的元素。 int idx = blockIdx.x * blockDim.x + threadIdx.x; // 2. 边界检查:确保当前线程的索引 idx 不超过数组的大小 N,这是为了防止数组越界访问。 if (idx < N) { // 使用 __ldcs(&inp1[idx]) 从第一个输入数组 inp1 读取数据,__ldcs 函数从常量内存加载数据,这有助于利用缓存以提高数据访问速度。 // 使用 __ldcs(&inp2[idx]) 从第二个输入数组 inp2 读取数据。 // 将两个数据相加,并将结果存储到输出数组 out 的相应位置。 out[idx] = __ldcs(&inp1[idx]) + __ldcs(&inp2[idx]); } } |
性能优化:
- 使用
__ldcs
:该函数假设数据可能存在于常量缓存中,这可以减少全局内存的访问延迟。当数据实际上不在常量缓存中时,这个函数仍然从全局内存加载数据,但通常不会比普通的全局内存加载更慢。 - 简洁的数据操作:该核函数仅进行加法操作和数据存取,使得整体计算非常高效。
示例应用场景:
这个内核函数可以在执行深度神经网络中的前向传播时使用,特别是在那些使用残差连接的网络架构中。例如,在每个Transformer编码器或解码器的层后添加残差连接,可以帮助保持不同层间信息的传递,提高网络的训练稳定性。
1.16 gelu_forward_kernel
这段CUDA内核函数 gelu_forward_kernel
用于计算Gaussian Error Linear Unit (GELU) 激活函数的输出。GELU 激活函数是在深度学习模型中常用的非线性激活函数,特别是在Transformer和BERT等自然语言处理模型中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// float* out: 输出数组,存储计算后的GELU激活函数的结果。 // const float* inp: 输入数组,包含应用GELU函数前的原始值。 // int N: 输入和输出数组的元素数量,指明了在这两个数组中有多少元素需要进行处理。 __global__ void gelu_forward_kernel(float* out, const float* inp, int N) { // 1. 索引计算:通过 blockIdx.x * blockDim.x + threadIdx.x 计算当前线程的全局索引 i, int i = blockIdx.x * blockDim.x + threadIdx.x; // 2. 边界检查:确保当前线程的索引 i 不超过数组的大小 N,这是为了防止数组越界访问。 if (i < N) { // 3. GELU激活计算: // 首先读取输入值 xi。 // 计算 xi 的三次方,并乘以固定系数 0.044715f 得到 cube。 // 使用预定义的 GELU_SCALING_FACTOR(基于 sqrt(2.0 / π) 的常量)和 tanh 函数来计算GELU激活函数的主体。 // 最终激活值通过 0.5 * xi * (1.0 + tanhf(GELU_SCALING_FACTOR * (xi + cube))) 计算得出,并存储在输出数组 out 中。 float xi = inp[i]; float cube = 0.044715f * xi * xi * xi; out[i] = 0.5f * xi * (1.0f + tanhf(GELU_SCALING_FACTOR * (xi + cube))); } } |
性能优化:
- 并行化处理:此内核高度并行化,每个线程独立处理一个数组元素,大幅提高处理效率。
- 简洁的数据操作:通过直接计算相关数学表达式,避免了不必要的内存访问和计算,优化了执行速度。
示例应用场景:
GELU激活函数广泛应用于各种深度学习模型中,特别是在自然语言处理领域。其在BERT和GPT等模型中的使用,帮助模型在各种任务中实现更好的性能。这个内核函数可以直接用于这些模型的前向传播过程中,处理激活函数部分。
通过这种方式,gelu_forward_kernel
在保证计算正确性的同时,提供了高效的执行方式,适合在需要高性能计算的深度学习应用中使用。
1.17 gelu_backward_kernel
这段CUDA内核函数 gelu_backward_kernel
用于计算Gaussian Error Linear Unit (GELU) 激活函数的梯度。在深度学习中,这一步骤通常在模型的反向传播阶段进行,用于计算损失函数关于每个输入节点的偏导数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// float* dinp: 输出梯度数组,存储计算后的GELU激活函数的梯度结果。 // const float* inp: 输入数组,包含应用GELU函数前的原始值。 // const float* dout: 输入梯度数组,包含从上游传递下来的梯度。 // int N: 输入和输出数组的元素数量,指明了在这两个数组中有多少元素需要进行处理。 __global__ void gelu_backward_kernel(float* dinp, const float* inp, const float* dout, const int N) { // 1. 索引计算:通过 blockIdx.x * blockDim.x + threadIdx.x 计算当前线程的全局索引 i, int i = blockIdx.x * blockDim.x + threadIdx.x; // 2. 边界检查:确保当前线程的索引 i 不超过数组的大小 N,这是为了防止数组越界访问。 if (i < N) { // 3. GELU梯度计算: // 首先读取输入值 x。 // 计算 x 的三次方,并乘以固定系数 0.044715f 得到 cube。 // 构造 tanh 函数的参数 tanh_arg。 // 使用 tanhf 和 coshf 函数来计算 tanh 输出值 tanh_out 和 cosh 输出值 coshf_out。 // 计算 sech 的平方 (sech_out),即 1.0 / (coshf_out * coshf_out)。 // 根据GELU的梯度公式,计算本地梯度 local_grad: // 将计算得到的本地梯度乘以上游传来的梯度 dout[i],得到最终的梯度值,存储在 dinp[i] 中。 float x = inp[i]; float cube = 0.044715f * x * x * x; float tanh_arg = GELU_SCALING_FACTOR * (x + cube); float tanh_out = tanhf(tanh_arg); float coshf_out = coshf(tanh_arg); float sech_out = 1.0f / (coshf_out * coshf_out); float local_grad = 0.5f * (1.0f + tanh_out) + x * 0.5f * sech_out * GELU_SCALING_FACTOR * (1.0f + 3.0f * 0.044715f * x * x); dinp[i] = local_grad * dout[i]; } } |
性能优化:
- 并行化处理:此内核高度并行化,每个线程独立处理一个数组元素,大幅提高处理效率。
- 精确的数学函数使用:通过精确计算
tanh
和cosh
及其倒数,确保了梯度计算的准确性,这对于模型训练的稳定性和最终性能至关重要。
示例应用场景:
这个内核函数在执行深度神经网络中的反向传播时非常有用,特别是在那些使用GELU激活函数的网络架构中,例如BERT和Transformer。正确的梯度计算对于网络权重的有效更新和模型训练的收敛至关重要。通过这种方式,gelu_backward_kernel
提供了高效且准确的执行方式,适合在需要高性能计算的深度学习应用中使用。