English· Español· Deutsch· Nederlands· Français· 日本語· ქართული· 繁體中文· 简体中文· Português· Русский· العربية· हिन्दी· Italiano· 한국어· Polski· Svenska· Türkçe· Українська· Tiếng Việt· Bahasa Indonesia

un

访客
1 / ?
返回课程列表

局部梯度相乘

Forward & Backward Kernels


正向传播

ANDREA-120M 的正向传播将输入依次通过一系列操作:


x = embed(token_ids)         # token embeddings
for layer in 12_layers:
x = x + attn(LN(x))      # 注意力子层
x = x + mlp(LN(x))       # MLP 子层
logits = LN(x) @ embed.T     # 共享输出投影
loss   = cross_entropy(logits, targets)

每个操作读取输入张量并产生输出张量。正向传播以单个标量结束:该批次的交叉熵损失。


反向传播

训练会按照减少损失的方向更新权重。为了获得更新方向,引擎需要:


模型中每个可学习 W 的 dL/dW

链式法则提供了这个。对于链 loss = f(g(h(x)))


dL/dx = (dL/df) * (df/dg) * (dg/dh) * (dh/dx)

每个因子都是一个局部梯度:当输入小幅变化时,一个操作的输出如何变化。将局部梯度向后通过图相乘,将损失信号传播到每个权重。


反向模式微分

反向传播以逆序计算梯度:从 dL/dlogits = 1 开始,然后向后遍历交叉熵、输出投影、层归一化、十二个 Transformer 块,然后是嵌入。在每一步,将传入梯度乘以局部雅可比矩阵。


当输出是一个单一标量(损失)且有许多输入(权重)时,反向模式是高效的。一次向后传播为模型中的每个权重生成梯度。前向模式需要为每个权重进行一次传播;对于具有 ~120M 权重的 ANDREA-120M,前向模式是不可行的。

为什么使用反向模式

ANDREA-120M 有约 120M 个权重,并在每个训练步骤产生一个标量损失。将反向模式自动微分与前向模式进行比较。说明 (1) 哪种模式能在单次反向传播中产生所有权重梯度;(2) 计算所有 120M 个权重梯度需要多少次前向模式传递;(3) ANDREA 使用哪种模式及原因。

每个前向操作都有一个反向双生

配对规则

microgpt_cuda.cu 为每个操作提供两个 CUDA 内核:一个计算前向输出,一个在给定输出梯度的情况下计算输入梯度。配对是一对一的:


前向内核后向内核操作
k_embed_fwdk_embed_bwd令牌嵌入查找
k_layernorm_fwdk_layernorm_bwd层归一化
k_attn_qkv_fwdk_attn_qkv_bwdQ、K、V 投影
k_attn_fwdk_attn_bwd缩放点积注意力
k_attn_out_fwdk_attn_out_bwd输出投影 W_O
k_mlp_fwdk_mlp_bwdMLP(带 GELU)
k_residual_addk_residual_add_bwd残差连接
k_loss_fwdk_loss_bwd交叉熵损失

八个操作对覆盖了完整的 transformer。还有几个实用内核:k_grad_norm_partialk_grad_norm_finalk_grad_scale 用于梯度裁剪(参见活动 75)。


反向内核的工作

给定从后续层流入的梯度(grad_output),反向内核计算:


1. grad_input:相对于操作输入张量的梯度。这会进一步向后传递。

2. grad_weight:相对于操作中可学习参数的梯度。这会进入优化器状态。


两者都在单个内核启动中计算。CUDA 线程并行协作于梯度张量的瓦片。


保存的张量

反向计算通常需要前向传递中的值。例如,k_layernorm_bwd 需要前向计算的均值和方差;k_mlp_bwd 需要 GELU 预激活。训练引擎在前向过程中将这些存储在专用缓冲区中,然后在反向过程中读取它们。


内存成本:与前向输出的形状大致相同,每个保存的张量。对于 ANDREA-120M,batch=8, seq=1024, d_model=768,一个保存的张量是 8 × 1024 × 768 × 4 bytes = 25 MB。跨越 12 层 & 每层多个保存的张量,激活值在训练期间主导 VRAM(~5-10 GB 在 24 GB 显卡上)。

追踪一个反向步骤

ANDREA-120M 完成一个 transformer 块的前向传播。追踪该同一块的反向传播(pre-norm 结构:`x = x + Attention(LN(x))` 然后 `x = x + MLP(LN(x))`)。按顺序命名反向 kernel,并说明每个与哪个前向 kernel 配对。至少覆盖 4 个 kernel。

梯度在内存中的存储位置

每个权重张量对应一个梯度张量

ANDREA-120M 中的每个可学习权重张量都有一个形状完全相同的匹配梯度张量。对于每个块:


W_Q       [768, 768]     ↔   grad_W_Q       [768, 768]
W_K       [768, 768]     ↔   grad_W_K       [768, 768]
W_V       [768, 768]     ↔   grad_W_V       [768, 768]
W_O       [768, 768]     ↔   grad_W_O       [768, 768]
W_1       [768, 3072]    ↔   grad_W_1       [768, 3072]
W_2       [3072, 768]    ↔   grad_W_2       [3072, 768]
LN1.gamma [768]          ↔   grad_LN1.gamma [768]
LN1.beta  [768]          ↔   grad_LN1.beta  [768]
LN2.gamma [768]          ↔   grad_LN2.gamma [768]
LN2.beta  [768]          ↔   grad_LN2.beta  [768]

加上令牌嵌入、位置嵌入和最终层归一化。梯度缓冲区总内存与权重内存匹配:~120M 个浮点数,在 FP32 下 ~480 MB,在 FP16 下 ~240 MB。


跨微批次的累积

ANDREA 的 batch_size = 8 在 FP16 下适合 VRAM。更大的有效批次需要梯度累积:在小批次上运行多个前向+反向传播,将梯度累加到同一个缓冲区中,然后执行一次优化器步骤。


for microbatch in range(n_microbatches):
forward(微批次)
backward()           # 添加到梯度缓冲区,不会覆盖
scale_grads(1.0 / n_microbatches)  # 跨微批次平均
optimizer_step()
zero_grads()             # 为下一个训练步骤重置

反向传播内核使用 += 语义,而不是 =。每次调用都会将梯度贡献添加到现有缓冲区;缓冲区保持运行总和,直到 zero_grads() 清空它。


优化器状态

AdamW(活动 73)为每个权重额外持有两个缓冲区:一阶矩 m 和二阶矩 v。训练时总内存:


权重:      1× 权重数量
梯度:      1× 权重数量
Adam m:     1× 权重数量
Adam v:     1× 参数数量
保存的激活: ~2-4× 取决于层数和批次
──────────────────────────────────────────
总计:      ~6-8× 参数数量

ANDREA-120M 在 FP16: ~240 MB × 4 个缓冲区 (权重、梯度、m、v) + ~5-10 GB 激活 = ~10-12 GB 总计。舒适地低于 RTX 4090 的 24 GB 上限。ANDREA-12M 在 1.4 GB 中训练;10× 参数缩放带来 ~10× 内存。

梯度缓冲区大小

ANDREA-120M 拥有约 120,000,000 个权重,并在每个训练步骤中使用 4 个微批次的梯度累积。计算:(a) FP16 下的梯度缓冲区大小(MB);(b) FP16 下权重 + 梯度 + Adam m + Adam v 的总内存;(c) 每个训练步骤触发多少个独立的 `forward()` + `backward()` 调用。请展示你的计算过程。

完全控制内存与精度

通用框架的代价

PyTorch 和 JAX 让自动求导变得方便:编写 Python 代码,自动获得梯度。代价是:在你的代码与 CUDA 之间有一个通用的分发层。每个操作都要经过 Python 解释器开销、框架簿记和动态内核选择。对于在一张 GPU 上训练小型语言模型,这种开销很重要。


ANDREA 避免的具体成本:


1. Python 解释器延迟。 每个 PyTorch 操作都会跨越 Python/C++ 边界。对于每个训练步骤约 100 个内核启动,每分钟约 9 个步骤,那就是每分钟约 900 次边界跨越。C 级调度消除了这一点。


2. 框架分配器不可预测性。 PyTorch 的缓存分配器平均吞吐量良好,但峰值内存不可预测。ANDREA 的训练引擎在启动时预分配每个缓冲区;训练期间无重新分配、无碎片化、无 100K 步骤时的意外 OOM。


3. 通用内核选择。 PyTorch 通过启发式在运行时选择内核。ANDREA 在编译时选择内核,针对 RTX 4090 张量核心瓦片大小进行调优。


4. 混合精度管道。 ANDREA-120M 的 FP16 cuBLAS 路径 & ANDREA 的 FP8 E4M3 张量核心实验需要精确控制哪些张量处于哪种精度。通用框架通过分层 API 暴露这种控制;自定义 CUDA 直接写入它。


权衡取舍

自定义 CUDA 的成本:需要编写更多代码,发现更多 bug,没有社区生态系统。ANDREA 的 microgpt_cuda.cu 是约 6000 行手写 CUDA,调试花了数月时间。每个新操作都需要编写前向核、后向核和测试。


ANDREA 获得的好处:


- 完全可重现性。 训练管道仅由一个 C 二进制文件和一个 Python 代理组成。无 PyTorch 版本漂移,无与框架 wheel 的 CUDA 版本不匹配。

- 位精确恢复。 SIGTERM 触发检查点写入,精确捕获 GPU 所见的每个张量。恢复时拾取运行时的相同损失轨迹。

- 可预测内存。 ANDREA-120M 在 200K 步训练中无 OOM。内存在引擎启动时已核算。

- 直接硬件访问。 张量核心瓦片大小、FP8 E4M3 设置、异步内存拷贝:均可在 CUDA 中直接寻址,在通用框架中不透明。


可重现性即使命

ANDREA 白皮书第 9 节列出了完整的可重现性堆栈:


训练引擎:microgpt/microgpt_cuda.cu
训练代理:microgpt/training_proxy.py
实验配置:experiments/ANDREA-*-TRAIN.json
数据管道:scripts/pull-hermes3.py, scripts/prep-megachat.py
仪表板:scripts/live-loss-dashboard.html
Bandit 规范:docs/FIREHOSE-BANDIT.md
模型文档:docs/ANDREA.md

硬件要求:一台配备 ≥8 GB VRAM 的 NVIDIA GPU(RTX 3060 或更好)。任何人都可以从这些工件中重现 ANDREA-12M。自定义 CUDA 路径就是原因之一:没有框架版本冻结,没有五年后依赖惊喜。


信号与检查点

CUDA 训练循环响应两种 POSIX 信号:


- SIGTERM:立即写入检查点,然后退出。用于干净地停止训练。

- SIGUSR1:写入即时检查点,继续训练。在 v3 的 polish pivot 期间使用,以捕获状态而不中断运行。


检查点格式:[int32 step][int32 n_params][n_params × float32 weights][n_params × float32 m][n_params × float32 v]。步骤计数器、权重计数,然后是权重后跟 Adam 动量。精确位恢复。代理在 polish 上单独归档 .samples.json & .state.json.loss.json 从不归档(它累积完整的训练历史)。

为什么不使用 PyTorch

ANDREA 本可以使用 PyTorch 的 autograd 而不是手写 `microgpt_cuda.cu`。给出 ANDREA 选择自定义 CUDA 的两个不同的工程原因。其中一个原因应提及内存或精度控制;另一个应提及可重现性、框架依赖或长期维护。