CUDA4_使用CUDA实现Transformer结构

CUDA(四):使用 CUDA 实现 Transformer 结构

作者: 紫气东来
原文链接: https://zhuanlan.zhihu.com/p/694416583
发布日期: 2024年


文章概述

本文是 CUDA 系列的第四篇,聚焦于使用 CUDA 从零实现 Transformer 架构。作者基于 OpenAI 科学家 Andrej Karpathy 开源的 llm.c 项目,详细解析了如何脱离深度学习框架,直接使用 CUDA 构建 LLM 的算子模块、训练过程和计算优化。这是一次深入底层的技术探索,旨在帮助开发者理解 Transformer 的实现细节和性能优化技巧。

核心价值


一、关键算子及模块的实现

作者将算子和模块的实现分为基础算子和核心模块两大类,每个部分都有独立的详细文章。

1.1 基础算子的实现

1. LayerNorm 算子

2. Softmax 算子

3. Cross Entropy 损失函数

4. AdamW 优化器

5. GELU 激活函数与残差连接

6. 矩阵乘法(GEMM)

1.2 核心模块的实现

1. Embedding 层与 LM Head 层

2. Self-Attention 机制


二、训练过程的实现

2.1 训练前的准备工作

2.1.1 训练数据下载并分词

使用 Python 脚本进行数据准备:

python prepro_tinyshakespeare.py

执行操作:

  1. 数据下载: 从 GitHub 下载 Tiny Shakespeare 数据集
  2. 分词处理: 使用 GPT-2 tokenizer 进行分词
  3. 数据集划分: 分为训练集和验证集

输出结果:

Saved 32768 tokens to data/tiny_shakespeare_val.bin
Saved 305260 tokens to data/tiny_shakespeare_train.bin

2.1.2 标准模型转换

主要操作:

  1. 加载预训练模型: 加载标准的 GPT-2 模型
  2. 格式转换:
    • 保存为 FP32 和 BF16 两个版本的二进制文件
    • 词表大小从 50257 padding 到 50304(提高矩阵乘法效率)
  3. Tokenizer 转换: 将分词器保存为二进制格式
  4. 调试状态保存: 保存输入和中间状态用于验证

输出文件:

性能基准(PyTorch 版本):

iteration 1, loss: 5.2700, time: 69.203ms, tok/s: 3699.27
iteration 2, loss: 4.0597, time: 48.847ms, tok/s: 5240.88
...
iteration 10, loss: 0.3765, time: 62.727ms, tok/s: 4081.15
final 9 iters avg: 58.621ms
peak memory consumption: 2392 MiB

2.2 训练过程

2.2.1 训练过程的实现

核心代码结构:

// 1. 构建 GPT-2 模型
GPT2 model;
gpt2_build_from_checkpoint(&model, load_filename);

// 2. 初始化分词器
Tokenizer tokenizer;
tokenizer_init(&tokenizer, "gpt2_tokenizer.bin");

// 3. 初始化数据加载器
DataLoader train_loader, val_loader;
dataloader_init(&train_loader, &multi_gpu_config, train_tokens_filename, B, T);
dataloader_init(&val_loader, &multi_gpu_config, val_tokens_filename, B, T);

// 4. 训练循环
for (int step = 0; step < max_steps; step++) {
    dataloader_next_batch(&train_loader);
    gpt2_forward(&model, train_loader.inputs, train_loader.targets, B, T, false);
    gpt2_zero_grad(&model);
    gpt2_backward(&model);
    gpt2_update(&model, learning_rate, 0.9f, 0.999f, 1e-8f, 0.0f, step+1);
}

编译与运行:

make train_gpt2cu
./train_gpt2cu

训练配置:

| Parameter             | Value                                              |
+-----------------------+----------------------------------------------------+
| batch size B          | 4                                                  |
| sequence length T     | 1024                                               |
| learning rate         | 3.000000e-04                                       |
| device                | NVIDIA GeForce RTX 4090                            |
| precision             | BF16                                               |
| num_layers L          | 12                                                 |
| num_heads NH          | 12                                                 |
| channels C            | 768                                                |
| num_parameters        | 124475904                                          |

内存分配:

训练性能(纯 CUDA 实现):

step 1/74: train loss 4.364526 (43.898880 ms, 93305.343750 tok/s)
step 2/74: train loss 4.501646 (41.996288 ms, 97532.406250 tok/s)
...
step 20/74: train loss 3.743915 (42.046463 ms, 97677.789062 tok/s)
val loss 3.702019

生成示例:

Nay, thou amongst I: he never came to hear this, that I shall not entertain 
the will of myself. Call to play, Gregg! Take W.L<|endoftext|>Your pleas 
about my ignorance, Oh, move your way, charge, plunder: plan for This bloody 
Staff expedition:

2.2.2 使用 cuDNN 模块加速

编译与运行:

make train_gpt2cu USE_CUDNN=1
./train_gpt2cu

性能提升:

cuDNN 版本训练日志:

step 1/74: train loss 4.370480 (362.220551 ms, 11308.027344 tok/s)  # 首次编译
step 2/74: train loss 4.505543 (32.261120 ms, 126963.945312 tok/s)
step 3/74: train loss 4.421638 (32.068607 ms, 127354.804688 tok/s)
...
step 20/74: train loss 3.746799 (32.408577 ms, 127089.820312 tok/s)
val loss 3.702071

内存优化:

2.2.3 更大规模训练示例

使用 TinyStories 数据集进行训练:

# 数据准备
python prepro_tinystories.py

# 训练(更大的 batch size)
./train_gpt2cu -i data/TinyStories -v 250 -s 250 -g 144 -o stories.log -b 32

训练配置:

性能表现:

step 1/28248: train loss 2.386457 (780.998657 ms, 41956.539062 tok/s)
step 2/28248: train loss 3.283106 (215.723007 ms, 151898.468750 tok/s)
step 3/28248: train loss 2.365078 (219.082748 ms, 150703.875000 tok/s)
...
step 20/28248: train loss 1.869874 (220.104706 ms, 149131.281250 tok/s)

损失下降趋势:


三、环境配置要求

该项目对软件版本有较高要求,推荐配置:

OS: Ubuntu 22.04
Driver: 550.54.15
CUDA: 12.4
PyTorch: 2.4.0.dev20240513+cu121
cuDNN: 8.9.7.29
cudnn-frontend: 1.4.0

关键依赖:


四、性能分析与优化总结

4.1 性能对比

实现方式 吞吐量 (tok/s) 相对提升 内存占用 (激活)
PyTorch ~4,000 基准 未知
纯 CUDA ~97,000 24x 2,853 MiB
CUDA + cuDNN ~127,000 32x 1,703 MiB

4.2 关键优化技术

1. 算子层面优化

2. 内存优化

3. 计算优化

4. 系统层面优化

4.3 性能瓶颈分析

内存带宽瓶颈:

计算瓶颈:

同步开销:


五、实践建议与最佳实践

5.1 开发建议

  1. 从简单开始: 先实现正确的版本,再进行优化
  2. 逐步优化: 每次优化后验证正确性和性能提升
  3. 使用 profiler: 使用 Nsight Systems/Compute 分析性能
  4. 参考标准实现: 对比 PyTorch 的输出验证正确性
  5. 保存中间状态: 便于调试和验证

5.2 调试技巧

  1. 数值验证: 与 PyTorch 实现对比中间结果
  2. 梯度检查: 使用数值梯度验证反向传播
  3. 单元测试: 为每个算子编写测试
  4. 可视化: 使用 TensorBoard 监控训练过程
  5. 日志记录: 详细记录损失、梯度范数等指标

5.3 性能调优

  1. Profile 驱动: 先分析瓶颈再优化
  2. 内存优化优先: GPU 通常受内存带宽限制
  3. 批量大小调整: 找到最佳的 batch size
  4. 混合精度: 使用 BF16/FP16 加速训练
  5. 库函数优先: 优先使用 cuBLAS、cuDNN 等优化库

六、技术亮点与创新

6.1 项目特色

  1. 完全脱离框架: 不依赖 PyTorch/TensorFlow
  2. 教育价值高: 代码清晰,注释详细
  3. 性能优秀: 接近或超过框架性能
  4. 模块化设计: 易于理解和扩展
  5. 渐进式优化: 展示从基础到高级的优化过程

6.2 学习价值


七、总结与展望

7.1 核心要点

  1. 算子实现是基础: LayerNorm、Softmax、Attention 等算子的高效实现是关键
  2. 优化是系统工程: 需要从算法、实现、硬件多个层面综合考虑
  3. cuDNN 加速显著: 使用优化库可以获得 30% 以上的性能提升
  4. 内存管理重要: 合理的内存分配和复用可以支持更大的模型和 batch size

7.2 未来方向

  1. 更多优化技术: Flash Attention 2、PagedAttention 等
  2. 分布式训练: 多 GPU、多节点训练支持
  3. 更大模型: 支持 GPT-3、LLaMA 等更大规模模型
  4. 推理优化: 量化、剪枝、知识蒸馏等技术
  5. 新硬件支持: 适配 H100、A100 等新一代 GPU

7.3 参考资源


附录:关键代码片段

A.1 前向传播核心流程

void gpt2_forward(GPT2 *model, int* inputs, int* targets, int B, int T, bool training) {
    // 1. Token embedding + Position embedding
    encoder_forward(model->acts.encoded, inputs, model->params.wte, 
                    model->params.wpe, B, T, C);
    
    // 2. Transformer blocks
    for (int l = 0; l < L; l++) {
        // LayerNorm 1
        layernorm_forward(ln1, encoded, ln1w, ln1b, B, T, C);
        
        // Multi-head attention
        attention_forward(atty, att, ln1, l0, l1, B, T, C, NH);
        
        // Residual connection 1
        residual_forward(residual, encoded, atty, B*T*C);
        
        // LayerNorm 2
        layernorm_forward(ln2, residual, ln2w, ln2b, B, T, C);
        
        // MLP
        matmul_forward(fch, ln2, fcw, B, T, C, 4*C);
        gelu_forward(fch_gelu, fch, B*T*4*C);
        matmul_forward(fcproj, fch_gelu, fcprojw, B, T, 4*C, C);
        
        // Residual connection 2
        residual_forward(encoded, residual, fcproj, B*T*C);
    }
    
    // 3. Final LayerNorm
    layernorm_forward(model->acts.lnf, encoded, lnfw, lnfb, B, T, C);
    
    // 4. LM head
    matmul_forward(model->acts.logits, model->acts.lnf, 
                   model->params.wte, B, T, C, V);
    
    // 5. Loss calculation
    if (targets != NULL) {
        softmax_crossentropy_forward(model->acts.losses, model->acts.logits, 
                                      targets, B, T, V);
    }
}

A.2 反向传播核心流程

void gpt2_backward(GPT2 *model) {
    // 1. Loss gradient
    softmax_crossentropy_backward(dlogits, model->acts.losses, 
                                   model->acts.logits, targets, B, T, V);
    
    // 2. LM head backward
    matmul_backward(dlnf, dwte, dlogits, model->acts.lnf, 
                    model->params.wte, B, T, C, V);
    
    // 3. Final LayerNorm backward
    layernorm_backward(dresidual, dlnfw, dlnfb, dlnf, residual, lnfw, B, T, C);
    
    // 4. Transformer blocks backward (reverse order)
    for (int l = L-1; l >= 0; l--) {
        // Residual 2 backward
        residual_backward(dresidual2, dfcproj, dresidual, B*T*C);
        
        // MLP backward
        matmul_backward(dfch_gelu, dfcprojw, dfcproj, fch_gelu, 
                        fcprojw, B, T, 4*C, C);
        gelu_backward(dfch, fch, dfch_gelu, B*T*4*C);
        matmul_backward(dln2, dfcw, dfch, ln2, fcw, B, T, C, 4*C);
        
        // LayerNorm 2 backward
        layernorm_backward(dresidual, dln2w, dln2b, dln2, residual, ln2w, B, T, C);
        
        // Residual 1 backward
        residual_backward(dencoded, datty, dresidual, B*T*C);
        
        // Attention backward
        attention_backward(dln1, dl0, dl1, datty, ln1, l0, l1, B, T, C, NH);
        
        // LayerNorm 1 backward
        layernorm_backward(dencoded, dln1w, dln1b, dln1, encoded, ln1w, B, T, C);
    }
    
    // 5. Embedding backward
    encoder_backward(dwte, dwpe, dencoded, inputs, B, T, C);
}

A.3 AdamW 优化器更新

void adamw_update(float* params, float* grads, float* m, float* v, 
                  int n, float lr, float beta1, float beta2, 
                  float eps, float weight_decay, int t) {
    for (int i = 0; i < n; i++) {
        // Update biased first moment estimate
        m[i] = beta1 * m[i] + (1.0f - beta1) * grads[i];
        
        // Update biased second raw moment estimate
        v[i] = beta2 * v[i] + (1.0f - beta2) * grads[i] * grads[i];
        
        // Compute bias-corrected first moment estimate
        float m_hat = m[i] / (1.0f - powf(beta1, t));
        
        // Compute bias-corrected second raw moment estimate
        float v_hat = v[i] / (1.0f - powf(beta2, t));
        
        // Update parameters with weight decay
        params[i] = params[i] * (1.0f - lr * weight_decay) 
                    - lr * m_hat / (sqrtf(v_hat) + eps);
    }
}

诗句引用:

浅水池塘莲叶香,红尘道路柳阴长。 —— 彭汝砺 《和初夏》


本文档基于 llm.c 项目整理,详细代码请参考原项目仓库。