CS336 LecTure 3 笔记补充

Attention 机制的核心参数与学习过程

在基础的 Attention 模块中,输入 Input 通常是经过 Embedding 处理后的向量 \(x\)。Query (\(q\))、Key (\(k\)) 和 Value (\(v\)) 均由该输入 \(x\) 乘以各自对应的可学习参数矩阵(如 \(\omega_q, \omega_k, \omega_v\))生成。

  • 相似度计算:通过 Query 和 Key 的点积(Query times Key)来计算序列中元素之间的相似度。
  • 参数学习:需要特别明确的是,\(\omega_q, \omega_k, \omega_v\) 这些权重参数是模型在训练过程中需要不断学习和优化的,而非固定不变的 Embedding 权重。

多头注意力(Multi-Head Attention)

多头的核心作用

多头机制决定了模型能够从多少个不同的“子空间”或角度同时观察输入序列。例如,在一个多头注意力模块中,某一个注意力头可能专门负责关注句子的语法结构,而另一个头则负责捕捉语义信息。

关键维度定义的区别

在多头注意力的公式 MultiHead(Q, K, V) = Concat(head1, ..., headh) 中,涉及几个极易混淆的维度概念:

  • \(d_{model}\)(全局维度/模型维度):这是输入和输出向量的全局长度。例如,一个词(如 "I")经过 Embedding 后可能是一个 768 维的向量,这里的 768 就是 \(d_{model}\)
  • \(h\)(注意力头数):代表将全局维度切分成的独立头的数量。例如,将 768 维切分为 8 个头,则 \(h=8\)
  • \(d_k\)(分头维度/子空间维度):这是每个独立注意力头实际处理的向量长度。其计算公式为 \(d_k = d_{model} / h\)。在上述例子中,每个头只需要处理 64 维(\(768 / 8 = 64\))的数据。\(d_k\) 也是 Query 和 Key 进行点积计算相关性时使用的空间大小,同时决定了每个头输出的特征空间大小。

张量维度的变换过程
在标准的张量运算中,输入数据的维度通常表示为 [Batch Size, Sequence Length, Dimension]

  • Batch Size:批次大小。
  • Sequence Length:序列长度(即句子中 Word 的数量)。
  • Dimension:全局维度 \(d_{model}\)

为了引入多头机制,我们对第三个参数(Dimension)进行扩展与重塑。具体操作是将 \(d_{model}\) 拆分为 \(h \times d_k\)。在代码实现和维度变换时,通常会将代表头数的 \(h\) 提前,使得张量维度变为 [Batch Size, Sequence Length, h, d_k]。这样,每个头就能在独立的 \(d_k\) 局部区域内并行计算注意力,最后再将所有头的结果拼接(Concat)起来,恢复为原始的全局维度。

Embedding、RoPE 位置编码、前馈网络(FFN)、交叉熵损失以及 AdamW 优化器是五个很重要的核心组件,和 CS336 笔记 中的 FLashAttention 结合我们就可以得到一个 Transformer

1. Embedding(词元嵌入层)

Embedding 的本质是一个形状为 (vocab_size, d_model) 的权重矩阵。前向传播时,不需要做矩阵乘法,而是直接将输入的 token id 作为索引,去权重矩阵中“查表”取出对应的行向量。

首先,为什么我们需要 Embedding?

如果没有 Embedding,我们直接给一个词汇表里的各个单词编号,第五号和第六号单词可能天差地别,但是模型会认为二者是接近的(因为五和六离得很近),为了解决这个问题,我们需要建立 Embedding

它本质上是什么?

一个巨大的、可学习的查找表。把它想象成一个巨大的 Excel 表格:

  • 行 : 代表词汇表里的每一个词(或 token)。总共有 vocab_size 行。
  • 列 : 代表每个词的“特征”或“语义”。总共有 embedding_dim 列。

所以,Embedding 层就是一个形状为 (vocab_size, embedding_dim) 的矩阵。

工作流程非常简单,就是查表:

  • 输入: 一个词的 ID,比如 cat 的 ID 是 42。
  • 查找: Embedding 层接收到 42,然后去它的“大表格”里找到第 42 行。
  • 输出: 取出第 42 行的整行数据。这行数据就是一个长度为 embedding_dim 的向量。

这个向量,就是 "cat" 这个词的 Embedding。

import torch
import torch.nn as nn
import triton
import triton.language as tl
import math

import torch
import torch.nn as nn

class Embedding(nn.Module):
    # 定义一个名为 Embedding 的类,它继承自 PyTorch 的 nn.Module
    # 这意味着它是一个标准的神经网络模块,可以被添加到模型中,并且它的参数可以被自动追踪和优化

    def __init__(self, num_embeddings: int, embedding_dim: int):
        # 初始化
        # num_embeddings: 词汇表的大小 (Vocab Size)
        # embedding_dim: 每个词向量的维度 (Embedding Dimension)
        super().__init__()
        # 调用父类 nn.Module 的初始化函数  继承时的标准写法

        self.num_embeddings = num_embeddings
        self.embedding_dim = embedding_dim
        # 保存传入的参数,方便后续使用或调试

        self.W = nn.Parameter(torch.empty(num_embeddings, embedding_dim))
        # 创建权重矩阵 W
        # torch.empty(...) 创建一个形状为 (词汇表大小, 向量维度) 的空矩阵
        # nn.Parameter(...) 告诉 PyTorch:这个矩阵 W 是模型的可学习参数,在训练时,自动计算它的梯度并更新

        std = 1.0 / (embedding_dim ** 0.5)
        # 计算初始化的标准差
        # 这是一个非常经典的初始化技巧(Xavier 初始化的一种变体)
        # 目的是让初始化的数值不要太大也不要太小,保持方差稳定,防止梯度消失或爆炸
        # 维度越高,每个数就要分得越小,所以除以根号下维度

        nn.init.trunc_normal_(self.W, mean=0.0, std=std, a=-3.0 * std, b=3.0 * std)
        # 对矩阵 W 进行“截断正态分布”初始化
        # 意思是 从均值为 0,标准差为 std 的正态分布中随机取值填入 W
        # 如果有数值超过了 [-3*std, 3*std] 的范围,就扔掉重取。
        # 避免初始权重中出现极端的离群值,让训练更稳定

    def forward(self, token_ids: torch.Tensor) -> torch.Tensor:
        # 前向传播函数

        return self.W[token_ids]
        # 查表
        # 在 PyTorch 中,直接用一个张量作为索引(self.W[...]),会自动进行“高级索引”操作
        # 它会根据 token_ids 里的数字,去 W 矩阵里把对应的行抠出来
        # 比如 token_ids 是 [10, 25],它就会把 W 的第 10 行和第 25 行取出来,拼成一个新的张量返回
        # 输入形状: (Batch, Seq_Len) -> 输出形状: (Batch, Seq_Len, Embedding_Dim)

2. RoPE(旋转位置编码)

RoPE 的核心思想是将 Q 和 K 的向量每两个维度看作二维平面上的点,根据 token 的位置施加不同角度的旋转。这样在计算注意力点积时,结果自然只与相对位置差有关。

在 Transformer 模型中,注意力机制(Self-Attention)本身是“无序”的,它不知道哪个词在前面,哪个词在后面。因此,我们需要给词加上“位置信息”。
传统的做法(绝对位置编码)是给每个词加上一个固定的位置向量,但这种方式在处理超长文本时效果不好。而 RoPE 巧妙地结合了绝对位置和相对位置的优点,它的核心目标是:通过绝对位置编码的方式,实现相对位置编码

RoPE 的灵感来源于几何学中的二维旋转

  • 旋转机制:它不是把位置信息“加”到词向量上,而是把词向量在二维平面中“旋转”一个与位置相关的角度。比如,第一个位置的向量旋转 \(\theta\) 角度,第二个位置旋转 \(2\theta\) 角度,依此类推。
  • 保留相对距离:这种旋转有一个非常优雅的数学性质——当两个词(Query 和 Key)进行点积计算注意力分数时,它们各自的旋转角度会相互抵消一部分,最终结果只与这两个词的相对距离(即位置差)有关
  • 不改变向量长度:旋转操作不会改变原始向量的长度(模长),这有助于保持模型训练的稳定性。
def apply_rope(x, cos, sin):
    # 将特征维度对半拆分,相当于把高维向量看作多个二维平面的组合
    x1, x2 = x.chunk(2, dim=-1)
    
    # 二维旋转的数学公式:(x1, x2) -> (-x2, x1)
    rotated_x = torch.cat([-x2, x1], dim=-1)
    
    # 利用预计算好的正余弦值,完成旋转操作
    return x * cos + rotated_x * sin

注:在实际 Transformer Block 中,通常预先生成一张包含所有位置的正余弦表(缓存机制),在前向传播时根据当前序列长度切片使用。

3. FFN(前馈网络 - SwiGLU 变体)

现代 LLM通常使用 SwiGLU 替代传统的 ReLU FFN。它包含三个线性层(参数量是传统 FFN 的 1.5 倍),通过门控机制控制信息流。

Attention 层只是把相关词汇的信息聚合在了一起,但它并没有对这些信息进行深度的非线性处理。FFN 的作用就是对 Attention 提取出的特征进行逐位置的独立加工。

Transformer 模型中绝大部分的参数和学到的知识(比如语法规则、世界常识)都储存在 FFN 层中。

class SwiGLU_FFN(nn.Module):
    def __init__(self, D_MODEL: int, D_FF: int):
        super().__init__()
        # W1: 门控分支 (Gate) - 决定“开多大阀门”
        self.w1 = nn.Linear(D_MODEL, D_FF, bias=False)
        # W3: 值分支 (Value) - 提供“实际的水流(信息)”
        self.w3 = nn.Linear(D_MODEL, D_FF, bias=False)
        # W2: 输出投影 - 将处理后的信息映射回原来的维度
        self.w2 = nn.Linear(D_FF, D_MODEL, bias=False)

    def forward(self, x):
        # 门控分支:通过 SiLU 激活函数(一种平滑的 Sigmoid 变体)生成门控信号
        gate = torch.nn.functional.silu(self.w1(x))
        
        # 值分支:提取原始信息
        value = self.w3(x)
        
        # 门控机制:将门控信号与值分支逐元素相乘
        # 门控信号的值通常在 0 到 1 之间,相当于一个“过滤器”
        # 乘以 gate 后,不重要的信息会被抑制(接近0),重要的信息会被保留
        return self.w2(gate * value)

4. Cross Entropy(交叉熵损失)

在语言模型中,计算交叉熵时需要将 logits 转为 float 以避免精度和溢出问题,同时将 target 转为 int64 以使用 gather 高效提取正确类别的概率。

模型的任务是“预测下一个词”

  • 假设当前上下文中,下一个词真实应该是“猫”。
  • 模型经过一系列计算后,输出了一个概率分布,它认为下一个词是“猫”的概率只有 10%,是“狗”的概率是 90%。

这时候,模型“考砸了”。交叉熵损失函数的作用,就是把这种“考砸了”的程度量化成一个具体的数字(Loss 值)。预测得越离谱,Loss 值越大;预测得越准,Loss 值越小。模型在训练时,唯一的目标就是通过反向传播(loss.backward())去不断降低这个 Loss 值。

交叉熵的本质来源于信息论,它的公式是:
Loss = - log(模型预测正确类别的概率)

具体可以看CS336 笔记

  • 如果模型对正确答案的预测概率是 0.9,Loss = -log(0.9) ≈ 0.105 (惩罚很小,说明模型很棒)
  • 如果模型对正确答案的预测概率是 0.1,Loss = -log(0.1) ≈ 2.302 (惩罚很大,说明模型很差)
  • 如果模型对正确答案的预测概率是 0.01,Loss = -log(0.01) ≈ 4.605 (惩罚极大)
def cross_entropy_loss(logits, targets, ignore_index=-100):
    """
    Args:
        logits: 模型输出的原始分数 (Batch, Seq_Len, Vocab_Size)
        targets: 真实的下一个 Token ID (Batch, Seq_Len)
        ignore_index: 需要被忽略的 Token ID(通常 Padding 会被设为 -100)
    """
    # 获取张量的形状
    BATCH, N_CTX, VOCAB_SIZE = logits.shape

    # 提升精度并展平
    # 将 logits 转为 float32 防止后续 exp() 计算时发生半精度溢出
    # 将形状从 (BATCH, N_CTX, VOCAB_SIZE) 展平为 (BATCH * N_CTX, VOCAB_SIZE)
    logits = logits.float().view(-1, VOCAB_SIZE)

    # 将 targets 转为 int64 (long) 类型,这是 PyTorch 索引操作的硬性要求
    # 形状从 (BATCH, N_CTX) 展平为 (BATCH * N_CTX,)
    targets = targets.long().view(-1)

    # 过滤掉需要忽略的 Token
    # 生成一个布尔掩码,标记出哪些位置是真实的 Token
    valid_mask = targets != ignore_index

    # 处理极端边界情况
    # 如果当前 Batch 中全都是 Padding(即没有任何有效的 Token)
    if not valid_mask.any():
        # 直接返回一个值为 0.0 的标量张量
        # 必须加上 requires_grad=True,否则后续 loss.backward() 会报错
        return torch.tensor(0.0, device=logits.device, requires_grad=True)

    # 利用掩码提取出所有有效的 logits 和 targets
    # 这一步将原本包含 Padding 的二维矩阵,压缩成了一维的有效数据
    logits = logits[valid_mask]
    targets = targets[valid_mask]

    # 减去当前行的最大值
    # 保持维度 (N, 1) 以便后续能正确广播
    max_logits, _ = torch.max(logits, dim=-1, keepdim=True)
    # 减去最大值,这在数学上等价于原公式,但能完美防止 exp() 溢出
    logits = logits - max_logits

    # 计算 LogSoftmax
    # 计算分母的对数形式:log(sum(exp(logits)))
    log_sum_exp = torch.log(torch.sum(torch.exp(logits), dim=-1))
    # 计算每个类别的对数概率:logits - log_sum_exp
    log_probs = logits - log_sum_exp.unsqueeze(-1)

    # 获取真实类别的对数概率
    # targets.unsqueeze(-1) 将形状变为 (N, 1) 以配合 gather 的索引要求
    # gather 沿着最后一个维度(Vocab_Size),根据 targets 提供的索引精准提取概率
    # squeeze(-1) 去掉多余的维度,恢复为 (N,)
    target_log_probs = log_probs.gather(dim=-1, index=targets.unsqueeze(-1)).squeeze(-1)

    # 计算最终的 Loss
    # 加上负号(因为我们要最大化对数概率,等价于最小化负对数概率)
    # 对所有有效 Token 的 Loss 求平均,得到当前 Batch 的最终损失值
    return -target_log_probs.mean()

5. AdamW 优化器

在传统的 Adam 优化器中,为了防止模型过拟合(比喻一下就是死记硬背),通常会加入 L2 正则化(即权重衰减)。但 Adam 在实现时,错误地把权重衰减加到了梯度计算里,导致当学习率发生变化时(比如使用 Warmup 或余弦退火),正则化的强度也会跟着剧烈波动,严重削弱了模型的泛化能力。

AdamW 的核心突破在于,将“权重衰减”与“梯度更新”彻底解耦。它让 Adam 专心负责高效地寻找最优解,而让权重衰减独立地在每次更新后对参数进行惩罚,从而更好地控制模型复杂度,防止过拟合。

AdamW 在每次更新参数时会进行如下操作运行:

  1. 自适应学习率与动量
    • 一阶矩(动量):记录梯度的历史方向,让模型在平坦的误差曲面上加速,减少震荡。
    • 二阶矩(自适应学习率):记录梯度的历史平方,为每个参数提供个性化的学习率。更新频繁的参数学习率会变小,更新少的参数学习率会变大。
    • 偏差修正:因为模型刚开始训练时,历史矩估计为 0,会产生偏差。AdamW 会进行数学修正,让初始阶段的更新更准确。
  2. 解耦的权重衰减
    在参数更新时,不经过梯度,而是直接用参数本身乘以一个衰减系数(1 - 学习率 * 权重衰减系数)。这确保了正则化的强度是恒定可控的,不受学习率调度策略的干扰。
  3. 参数更新
    将上述两步的结果结合起来,完成最终的参数更新。
from torch.optim import Optimizer

class AdamW(Optimizer):
    def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-8, weight_decay=0.01):
        # 将超参数打包成字典
        defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay)
        # 调用父类初始化,将模型参数和超参数绑定
        super().__init__(params, defaults)

    @torch.no_grad()
    def step(self):
        # 关闭自动求导 节省大量显存和计算资源
        for group in self.param_groups:
            # 从参数组中提取超参数
            beta1, beta2 = group['betas']

            # 遍历该组中的每一个可学习参数(如权重矩阵 W、偏置 b 等)
            for p in group['params']:
                # 如果该参数没有梯度(例如被冻结的层),直接跳过
                if p.grad is None:
                    continue

                # 获取该参数的内部状态(用于存储历史梯度的动量)
                state = self.state[p]

                # 状态初始化(仅在第一次遇到该参数时执行)
                if len(state) == 0:
                    state['step'] = 0
                    # 一阶矩估计:记录梯度的历史均值
                    state['exp_avg'] = torch.zeros_like(p)
                    # 二阶矩估计:记录梯度的历史方差(平方均值)
                    state['exp_avg_sq'] = torch.zeros_like(p)

                # 更新步数计数器
                state['step'] += 1
                # 获取当前参数的梯度
                grad = p.grad

                # 更新一阶矩和二阶矩(使用 In-place 原地操作,节省显存)
                # 一阶矩 = 0.9 * 旧一阶矩 + 0.1 * 当前梯度
                state['exp_avg'].mul_(beta1).add_(grad, alpha=1 - beta1)
                # 二阶矩 = 0.999 * 旧二阶矩 + 0.001 * 当前梯度的平方
                state['exp_avg_sq'].mul_(beta2).addcmul_(grad, grad, value=1 - beta2)

                # 偏差修正
                # 因为初始时刻矩估计为 0,会导致初期更新步长偏小,这里进行数学修正
                bias_correction1 = 1 - beta1 ** state['step']
                bias_correction2 = 1 - beta2 ** state['step']

                # 计算修正后的学习率
                step_size = group['lr'] / bias_correction1

                # 计算分母:sqrt(二阶矩 / 修正因子) + eps
                # eps 是为了防止分母为 0 导致除零错误
                denom = (state['exp_avg_sq'].sqrt() / (bias_correction2 ** 0.5)).add_(group['eps'])

                # AdamW 的核心:解耦的权重衰减
                # 直接在参数上乘以 (1 - 学习率 * 衰减系数)
                p.mul_(1 - group['lr'] * group['weight_decay'])

                # 参数更新:p = p - step_size * (一阶矩 / 分母)
                # addcdiv_ 表示 add(当前值, 分子 / 分母 * value)
                p.addcdiv_(state['exp_avg'], denom, value=-step_size)

Transformer

将所有代码结合在一起我们就得到了需要的 Transformer

Show Code
 # transformer
import torch
import torch.nn as nn
import triton
import triton.language as tl
import math


# ================= Embedding =================
class Embedding(nn.Module):
    def __init__(self, num_embeddings: int, embedding_dim: int):
        super().__init__()
        self.num_embeddings = num_embeddings
        self.embedding_dim = embedding_dim
        self.W = nn.Parameter(torch.empty(num_embeddings, embedding_dim))
        std = 1.0 / (embedding_dim ** 0.5)
        nn.init.trunc_normal_(self.W, mean=0.0, std=std, a=-3.0 * std, b=3.0 * std)

    def forward(self, token_ids: torch.Tensor) -> torch.Tensor:
        return self.W[token_ids]


# ================= RoPE =================
def apply_rope(x, cos, sin):
    """
    x: (BATCH, N_HEADS, N_CTX, D_HEAD)
    cos, sin: (N_CTX, 1, 1, D_HEAD)
    """
    x1, x2 = x.chunk(2, dim=-1)
    rotated_x = torch.cat([-x2, x1], dim=-1)
    return x * cos + rotated_x * sin


# ================= SwiGLU FFN =================
class SwiGLU_FFN(nn.Module):
    def __init__(self, D_MODEL: int, D_FF: int):
        super().__init__()
        self.w1 = nn.Linear(D_MODEL, D_FF, bias=False)  # Gate
        self.w3 = nn.Linear(D_MODEL, D_FF, bias=False)  # Value
        self.w2 = nn.Linear(D_FF, D_MODEL, bias=False)  # Output

    def forward(self, x):
        gate = torch.nn.functional.silu(self.w1(x))
        value = self.w3(x)
        return self.w2(gate * value)


# ================= Cross Entropy Loss =================
def cross_entropy_loss(logits, targets, ignore_index=-100):
    """
    logits: (BATCH, N_CTX, VOCAB_SIZE)
    targets: (BATCH, N_CTX)
    """
    BATCH, N_CTX, VOCAB_SIZE = logits.shape
    logits = logits.float().view(-1, VOCAB_SIZE)
    targets = targets.long().view(-1)
    # 过滤掉 ignore_index 的 token
    valid_mask = targets != ignore_index
    if not valid_mask.any():
        return torch.tensor(0.0, device=logits.device, requires_grad=True)
    logits = logits[valid_mask]
    targets = targets[valid_mask]
    max_logits, _ = torch.max(logits, dim=-1, keepdim=True)
    logits = logits - max_logits
    log_sum_exp = torch.log(torch.sum(torch.exp(logits), dim=-1))
    log_probs = logits - log_sum_exp.unsqueeze(-1)
    target_log_probs = log_probs.gather(dim=-1, index=targets.unsqueeze(-1)).squeeze(-1)
    return -target_log_probs.mean()


# ================= AdamW Optimizer =================
from torch.optim import Optimizer


class AdamW(Optimizer):
    def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-8, weight_decay=0.01):
        defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay)
        super().__init__(params, defaults)

    @torch.no_grad()
    def step(self):
        for group in self.param_groups:
            beta1, beta2 = group['betas']
            for p in group['params']:
                if p.grad is None:
                    continue
                state = self.state[p]
                if len(state) == 0:
                    state['step'] = 0
                    state['exp_avg'] = torch.zeros_like(p)
                    state['exp_avg_sq'] = torch.zeros_like(p)
                state['step'] += 1
                grad = p.grad
                state['exp_avg'].mul_(beta1).add_(grad, alpha=1 - beta1)
                state['exp_avg_sq'].mul_(beta2).addcmul_(grad, grad, value=1 - beta2)
                bias_correction1 = 1 - beta1 ** state['step']
                bias_correction2 = 1 - beta2 ** state['step']
                step_size = group['lr'] / bias_correction1
                denom = (state['exp_avg_sq'].sqrt() / (bias_correction2 ** 0.5)).add_(group['eps'])
                # 解耦的权重衰减
                p.mul_(1 - group['lr'] * group['weight_decay'])
                p.addcdiv_(state['exp_avg'], denom, value=-step_size)


# ================= Triton Flash Attention =================
@triton.jit
def _fwd_kernel(
        Q, K, V, sm_scale, Out,
        stride_qz, stride_qh, stride_qm, stride_qk,
        stride_kz, stride_kh, stride_kn, stride_kk,
        stride_vz, stride_vh, stride_vn, stride_vk,
        stride_oz, stride_oh, stride_om, stride_ok,
        Z, H, N_CTX,
        BLOCK_M: tl.constexpr, BLOCK_DMODEL: tl.constexpr, BLOCK_N: tl.constexpr,
):
    start_m = tl.program_id(0)
    off_hz = tl.program_id(1)
    off_z = off_hz // H
    off_h = off_hz % H
    qvk_offset = off_z.to(tl.int64) * stride_qz + off_h.to(tl.int64) * stride_qh
    v_offset = off_z.to(tl.int64) * stride_vz + off_h.to(tl.int64) * stride_vh
    o_offset = off_z.to(tl.int64) * stride_oz + off_h.to(tl.int64) * stride_oh
    Q_block_ptr = tl.make_block_ptr(base=Q + qvk_offset, shape=(N_CTX, BLOCK_DMODEL), strides=(stride_qm, stride_qk),
                                    offsets=(start_m * BLOCK_M, 0), block_shape=(BLOCK_M, BLOCK_DMODEL), order=(1, 0))
    K_block_ptr = tl.make_block_ptr(base=K + qvk_offset, shape=(BLOCK_DMODEL, N_CTX), strides=(stride_kk, stride_kn),
                                    offsets=(0, 0), block_shape=(BLOCK_DMODEL, BLOCK_N), order=(0, 1))
    V_block_ptr = tl.make_block_ptr(base=V + v_offset, shape=(N_CTX, BLOCK_DMODEL), strides=(stride_vn, stride_vk),
                                    offsets=(0, 0), block_shape=(BLOCK_N, BLOCK_DMODEL), order=(1, 0))
    O_block_ptr = tl.make_block_ptr(base=Out + o_offset, shape=(N_CTX, BLOCK_DMODEL), strides=(stride_om, stride_ok),
                                    offsets=(start_m * BLOCK_M, 0), block_shape=(BLOCK_M, BLOCK_DMODEL), order=(1, 0))
    m_i = tl.zeros([BLOCK_M], dtype=tl.float32) - float("inf")
    l_i = tl.zeros([BLOCK_M], dtype=tl.float32)
    acc = tl.zeros([BLOCK_M, BLOCK_DMODEL], dtype=tl.float32)
    # 增加 boundary_check 避免 N_CTX 不整除 BLOCK_SIZE 时的越界
    q = tl.load(Q_block_ptr, boundary_check=(0, 1))
    for start_n in range(0, N_CTX, BLOCK_N):
        k = tl.load(K_block_ptr, boundary_check=(0, 1))
        v = tl.load(V_block_ptr, boundary_check=(0, 1))
        qk = tl.dot(q, k) * sm_scale
        col_offsets = start_n + tl.arange(0, BLOCK_N)
        row_offsets = start_m * BLOCK_M + tl.arange(0, BLOCK_M)
        # Causal Mask 和 序列长度边界 Mask
        causal_mask = col_offsets[None, :] > row_offsets[:, None]
        ctx_mask = col_offsets[None, :] < N_CTX
        qk = tl.where(causal_mask | ~ctx_mask, float("-inf"), qk)
        m_ij = tl.maximum(m_i, tl.max(qk, 1))
        alpha = tl.exp(m_i - m_ij)
        l_ij = tl.exp(qk - m_ij[:, None])
        l_i = l_i * alpha + tl.sum(l_ij, 1)
        acc = acc * alpha[:, None] + tl.dot(l_ij.to(v.dtype), v)
        m_i = m_ij
        K_block_ptr = tl.advance(K_block_ptr, (0, BLOCK_N))
        V_block_ptr = tl.advance(V_block_ptr, (BLOCK_N, 0))
    acc = acc / l_i[:, None]
    tl.store(O_block_ptr, acc.to(Out.type.element_ty), boundary_check=(0, 1))


def triton_flash_attention(Q, K, V, causal=True):
    BATCH, N_HEADS, N_CTX, D_HEAD = Q.shape
    sm_scale = 1.0 / math.sqrt(D_HEAD)
    O = torch.empty_like(Q)
    BLOCK_M, BLOCK_N = 64, 64
    grid = (triton.cdiv(N_CTX, BLOCK_M), BATCH * N_HEADS)
    _fwd_kernel[grid](
        Q, K, V, sm_scale, O,
        Q.stride(0), Q.stride(1), Q.stride(2), Q.stride(3),
        K.stride(0), K.stride(1), K.stride(2), K.stride(3),
        V.stride(0), V.stride(1), V.stride(2), V.stride(3),
        O.stride(0), O.stride(1), O.stride(2), O.stride(3),
        BATCH, N_HEADS, N_CTX,
        BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N, BLOCK_DMODEL=D_HEAD
    )
    return O


# ================ Transformer =================
class RMSNorm(nn.Module):
    def __init__(self, dim: int, eps: float = 1e-6):
        super().__init__()
        self.eps = eps
        self.weight = nn.Parameter(torch.ones(dim))

    def forward(self, x):
        return x / torch.sqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) * self.weight


class TransformerBlock(nn.Module):
    def __init__(self, D_MODEL: int, N_HEADS: int, D_FF: int):
        super().__init__()
        self.D_MODEL = D_MODEL
        self.N_HEADS = N_HEADS
        self.D_HEAD = D_MODEL // N_HEADS
        self.attn_norm = RMSNorm(D_MODEL)
        self.ffn_norm = RMSNorm(D_MODEL)
        self.wq = nn.Linear(D_MODEL, D_MODEL, bias=False)
        self.wk = nn.Linear(D_MODEL, D_MODEL, bias=False)
        self.wv = nn.Linear(D_MODEL, D_MODEL, bias=False)
        self.wo = nn.Linear(D_MODEL, D_MODEL, bias=False)
        # 使用 SwiGLU FFN
        self.ffn = SwiGLU_FFN(D_MODEL, D_FF)

    def forward(self, x, cos, sin):
        BATCH, N_CTX, _ = x.shape
        norm_x = self.attn_norm(x)
        Q = self.wq(norm_x).view(BATCH, N_CTX, self.N_HEADS, self.D_HEAD).transpose(1, 2)
        K = self.wk(norm_x).view(BATCH, N_CTX, self.N_HEADS, self.D_HEAD).transpose(1, 2)
        V = self.wv(norm_x).view(BATCH, N_CTX, self.N_HEADS, self.D_HEAD).transpose(1, 2)
        # 应用 RoPE
        Q = apply_rope(Q, cos, sin)
        K = apply_rope(K, cos, sin)
        # 调用 Triton Flash Attention
        attn_output = triton_flash_attention(Q, K, V, causal=True)
        attn_output = attn_output.transpose(1, 2).contiguous().view(BATCH, N_CTX, self.D_MODEL)
        x = x + self.wo(attn_output)
        # 使用 SwiGLU FFN
        x = x + self.ffn(self.ffn_norm(x))
        return x


class DecoderOnlyTransformer(nn.Module):
    def __init__(self, VOCAB_SIZE: int, D_MODEL: int, N_HEADS: int, N_LAYERS: int, D_FF: int, MAX_SEQ_LEN: int):
        super().__init__()
        # 使用 Embedding
        self.tok_embeddings = Embedding(VOCAB_SIZE, D_MODEL)
        self.layers = nn.ModuleList([TransformerBlock(D_MODEL, N_HEADS, D_FF) for _ in range(N_LAYERS)])
        self.norm = RMSNorm(D_MODEL)
        self.output = nn.Linear(D_MODEL, VOCAB_SIZE, bias=False)
        # 预计算 RoPE (修复维度拼接问题)
        D_HEAD = D_MODEL // N_HEADS
        freqs = 1.0 / (10000.0 ** (torch.arange(0, D_HEAD, 2).float() / D_HEAD))
        t = torch.arange(MAX_SEQ_LEN, dtype=torch.float32)
        freqs = torch.outer(t, freqs)  # (MAX_SEQ_LEN, D_HEAD/2)
        # 将 freqs 在最后一维拼接,使其大小为 D_HEAD,从而与 x 的最后一维匹配
        freqs_cis = torch.cat([freqs, freqs], dim=-1)  # (MAX_SEQ_LEN, D_HEAD)
        self.register_buffer('cos_cached', torch.cos(freqs_cis).view(MAX_SEQ_LEN, 1, 1, D_HEAD))
        self.register_buffer('sin_cached', torch.sin(freqs_cis).view(MAX_SEQ_LEN, 1, 1, D_HEAD))

    def forward(self, token_ids):
        BATCH, N_CTX = token_ids.shape
        h = self.tok_embeddings(token_ids)
        cos = self.cos_cached[:N_CTX].to(h.device)
        sin = self.sin_cached[:N_CTX].to(h.device)
        for layer in self.layers:
            h = layer(h, cos, sin)
        h = self.norm(h)
        return self.output(h)


# ================= test =================
if __name__ == "__main__":
    # 初始化模型、优化器
    model = DecoderOnlyTransformer(VOCAB_SIZE=1000, D_MODEL=256, N_HEADS=4, N_LAYERS=4, D_FF=512, MAX_SEQ_LEN=128).to(
        'cuda').half()
    optimizer = AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)
    # 模拟 Batch 的数据
    input_ids = torch.randint(0, 1000, (2, 64), device='cuda')
    targets = torch.randint(0, 1000, (2, 64), device='cuda')
    # 前向传播
    logits = model(input_ids)
    # 计算 Loss
    loss = cross_entropy_loss(logits, targets)
    print(f"Initial Loss: {loss.item():.4f}")
    # 反向传播与 AdamW 更新
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    print("Forward & Backward pass with custom components completed successfully!")



posted @ 2026-07-07 15:55  PassName  阅读(5)  评论(0)    收藏  举报