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 个元素
V3 B×T×C 向量化

关键洞察

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 并行
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 算子的实现中。