ops6_embedding层与LM_head层的CUDA实现

ops(6):embedding 层与 LM head 层的 CUDA 实现

原文链接: https://zhuanlan.zhihu.com/p/695785781
发布时间: 2024-05-11
作者: ifromeast


概述

本文详细介绍了 Transformer 模型中 embedding 层和 LM head 层的 CUDA 实现与优化。这两个层分别构成了整个模型的首和尾,在模型性能中扮演着重要角色。


一、Embedding 层的 CUDA 实现

1.1 Embedding 与 Linear 的区别

基本概念

在语言模型中,embedding 层的作用是将文本的离散语义和位置信息转换为高维向量表示。在标准 GPT 模型中:

Embedding 的本质

Embedding 操作本质上是向量索引过程,可以等价转换为矩阵乘法:

索引方式:

import torch
idx = torch.tensor([2, 3, 1])
embedding = torch.nn.Embedding(4, 5)
output = embedding(idx)  # 直接索引

矩阵乘法方式:

# 将索引转换为 one-hot 编码
onehot = torch.nn.functional.one_hot(idx)
# 使用线性层(权重为 embedding 矩阵的转置)
linear = torch.nn.Linear(4, 5)
linear.weight = torch.nn.Parameter(embedding.weight.T.detach())
output = linear(onehot.float())  # 矩阵乘法

数学表达式:

z = x * W = [W_s1 ... W_sD]

其中 x 是 one-hot 编码,W 是 embedding 矩阵。

反向传播推导

梯度计算:

∂J/∂W_ij = Σ_k (∂J/∂z_k) * (∂z_k/∂W_ij)

关键性质:

因此:

∂J/∂W_sj = ∂J/∂z_j

写成矩阵形式,反向传播是对梯度的索引过程。

1.2 Embedding 层的实现

CPU 基准实现

void encoder_forward_cpu(float* out,
                   const int* inp, const float* wte, const float* wpe,
                   int B, int T, int C) {
    for (int b = 0; b < B; b++) {
        for (int t = 0; t < T; t++) {
            float* out_bt = out + b * T * C + t * C;
            int ix = inp[b * T + t];
            const float* wte_ix = wte + ix * C;
            const float* wpe_t = wpe + t * C;
            for (int i = 0; i < C; i++) {
                out_bt[i] = wte_ix[i] + wpe_t[i];
            }
        }
    }
}

实现逻辑:

  1. 遍历 batch (B) 和 sequence (T) 维度
  2. 根据输入索引 ix 获取对应的 word embedding
  3. 根据位置 t 获取对应的 position embedding
  4. 将两者相加得到最终输出

CUDA 实现 V1:基础并行化

优化策略:

性能数据:

block_size   32 | time 0.3918 ms | bandwidth 256.93 GB/s
block_size   64 | time 0.3921 ms | bandwidth 256.73 GB/s
block_size  128 | time 0.4000 ms | bandwidth 251.64 GB/s
block_size  256 | time 0.7029 ms | bandwidth 143.21 GB/s

CUDA 实现 V2:完全并行化

优化策略:

性能数据:

block_size   32 | time 0.3366 ms | bandwidth 299.10 GB/s
block_size   64 | time 0.1600 ms | bandwidth 629.16 GB/s
block_size  128 | time 0.0853 ms | bandwidth 1180.48 GB/s
block_size  256 | time 0.0745 ms | bandwidth 1350.73 GB/s
block_size  512 | time 0.0746 ms | bandwidth 1350.01 GB/s

性能提升:相比 V1,带宽提升约 5 倍。

CUDA 实现 V3:向量化内存访问

优化策略:

性能数据:

block_size   32 | time 0.0573 ms | bandwidth 1756.26 GB/s
block_size   64 | time 0.0538 ms | bandwidth 1869.95 GB/s
block_size  128 | time 0.0537 ms | bandwidth 1874.82 GB/s
block_size  256 | time 0.0536 ms | bandwidth 1879.17 GB/s
block_size  512 | time 0.0539 ms | bandwidth 1868.99 GB/s

性能提升:相比 V2,带宽再提升约 40%,达到 1879 GB/s。

1.3 Embedding 反向传播实现

CUDA 实现:使用原子操作

关键技术:

性能数据:

block_size   32 | time 0.3296 ms
block_size   64 | time 0.1735 ms
block_size  128 | time 0.1719 ms
block_size  256 | time 0.1728 ms

二、LM Head 层的实现

2.1 LM Head 的作用

在 Transformer 的 Causal Language Model 中,LM head 层负责:

  1. 推理阶段:

    • 根据 logits 通过 softmax 计算概率分布 probs
    • 使用解码算法(如 greedy、beam search、sampling)生成 next token
  2. 训练阶段:

    • 根据 probs 和 labels 计算 loss
    • 计算 logits 的梯度 dlogits,开始反向传播

2.2 融合的 Classifier Kernel

关键技术点

1. Softmax 计算

2. Loss 计算

3. 梯度计算

4. 向量化内存访问

5. 融合操作

性能数据

block_size   32 | time 10.460636 ms
block_size   64 | time 10.547235 ms
block_size  128 | time 9.895740 ms
block_size  256 | time 9.381703 ms
block_size  512 | time 9.059260 ms
block_size 1024 | time 8.828804 ms

性能特点:


三、优化技术总结

3.1 并行化策略

版本 并行维度 每线程工作量 性能
V1 B×T C 个元素 基准
V2 B×T×C 1 个元素 5×
V3 B×T×C 向量化 7×

关键洞察:

3.2 内存访问优化

1. 向量化加载/存储

2. 缓存提示

3. 合并访问

3.3 原子操作

使用场景:

性能影响:

3.4 Kernel 融合

LM Head 融合操作:

  1. Softmax 计算
  2. Loss 计算
  3. 梯度计算

优势:


四、性能分析

4.1 Embedding 层性能演进

版本 最佳带宽 优化技术 提升倍数
V1 256.93 GB/s B×T 并行 1×
V2 1350.73 GB/s B×T×C 并行 5.3×
V3 1879.17 GB/s 向量化访问 7.3×

分析:

4.2 Block Size 选择

Embedding 层:

LM Head 层:

4.3 内存带宽利用率

理论分析:

实际表现:


五、实现要点

5.1 Embedding 层

前向传播:

  1. 根据输入索引获取 word embedding
  2. 根据位置获取 position embedding
  3. 两者相加得到输出
  4. 使用向量化内存访问优化

反向传播:

  1. 将输出梯度累加到对应 embedding 位置
  2. 必须使用原子操作避免竞争
  3. 这是索引操作的反向过程

5.2 LM Head 层

融合 Kernel 设计:

  1. 使用 warp 级别协作组计算 softmax
  2. 单线程计算 loss(避免同步)
  3. 并行计算所有位置的梯度
  4. 可选输出 probs(推理/调试)

数值稳定性:

5.3 代码组织

参数说明:

内存布局:


六、参考资源

代码仓库:

参考项目:


七、关键收获

  1. Embedding 本质:索引操作,可等价为矩阵乘法,但索引更高效

  2. 并行化策略:根据操作特点选择合适的并行维度,Embedding 适合完全并行

  3. 内存优化:向量化访问、缓存提示、合并访问是提升性能的关键

  4. Kernel 融合:减少内存访问,提高数据局部性,显著提升性能

  5. 原子操作:在必要时使用,权衡性能和正确性

  6. 性能分析:理解操作是 compute-bound 还是 memory-bound,针对性优化


总结:本文通过详细的实现和优化过程,展示了如何从基础版本逐步优化到高性能实现,最终达到接近硬件理论性能的水平。这些技术和思路可以推广到其他 CUDA 算子的实现中。