手撕代码
介绍一些面试中常见的手撕代码题目及其解法,包括基础算法、数据结构、系统设计等内容
基础
Softmax
-
torch.max()不只是返回最大值,它返回的是一个「复合对象」(元组 / 命名元组),包含两个东西:- values:真正的最大值数值
- indices:最大值所在的下标位置
-
实现
def softmax(x, dim=-1): # 减去最大值保证,数值稳定性 x_max = torch.max(x, dim=dim, keepdim=True).values x_exp = torch.exp(x - x_max) x_sum = torch.sum(x_exp, dim=dim, keepdim=True) return x_exp / x_sum
熵
-
熵的公式
-
公式实现
def compute_entropy(logits): log_probs = F.log_softmax(logits, dim=-1) probs = torch.exp(log_probs) # 逐 token 熵 entropy = -(probs * log_probs).sum(dim=-1) # [B, T] # 平均熵 entropy_mean = entropy.mean() return entropy_mean # logits: [B, T, V] logits = model(...) print(compute_entropy(logits))
交叉熵
-
第一步:计算 softmax
-
第二步:取 log 得到 logits
-
第三步:取每个样本在真实标签上的 log 概率,并取平均
-
代码实现
def cross_entropy( inputs: torch.Tensor, labels: torch.Tensor, dim=-1, eps=1e-10, ) -> torch.Tensor: """ inputs: [batch, num_classes] 模型原始 logits(未经过 softmax) labels: [batch] 类别索引(0,1,2...) """ # 1. 稳定 softmax x_max = torch.max(inputs, dim=dim, keepdim=True).values exp_x = torch.exp(inputs - x_max) softmax_x = exp_x / torch.sum(exp_x, dim=dim, keepdim=True) # 2. 取 log log_softmax = torch.log(softmax_x + eps) # 加小epsilon防log(0) # 3. 对真实类别取负对数概率 # one-hot 索引选取 n = inputs.shape[0] # torch.arange(n) 表示行,labels 表示列,从而取出每个样本在真实标签上的 log 概率 loss = -log_softmax[torch.arange(n), labels] # 4. 平均 return loss.mean()
KL 散度
-
公式
- 输入为对数概率时:,
- 指数形式:
-
代码实现
def k3(log_p, log_q): log_ratio = log_p - log_q return torch.exp(log_ratio) - log_ratio - 1 def kl_divergence( p_logits: torch.Tensor, q_logits: torch.Tensor, temperature: float = 1.0, reduction: str = "batchmean", ) -> torch.Tensor: """ KL(p || q) = E_p [log p - log q] 输入为 logits,内部会做 softmax + log_softmax """ p = F.softmax(p_logits / temperature, dim=-1) log_p = F.log_softmax(p_logits / temperature, dim=-1) log_q = F.log_softmax(q_logits / temperature, dim=-1) kl = p * (log_p - log_q) kl = kl.sum(dim=-1) if reduction == "none": return kl elif reduction == "mean": return kl.mean() elif reduction == "sum": return kl.sum() elif reduction == "batchmean": return kl.sum() / p_logits.shape[0] else: raise ValueError(f"Unknown reduction: {reduction}")
LoRA
-
代码实现
class LoRALayer(nn.Module): def __init__(self, in_dim, out_dim, rank: int = 8, alpha: float = 8): super().__init__() self.rank = rank self.alpha = alpha self.scaling = alpha / rank self.weight = nn.Parameter(torch.randn(in_dim, out_dim), requires_grad=False) self.lora_A = nn.Parameter(torch.randn(in_dim, rank)) self.lora_B = nn.Parameter(torch.empty(rank, out_dim)) def forward(self, x): original_out = x @ self.weight lora_out = (x @ self.lora_A) @ self.lora_B return original_out + self.scaling * lora_out -
QLoRA
import bitsandbytes as bnb class QLoRALayer(nn.module): def __init__(self, in_dim, out_dim, rank: int = 8, alpha: float = 8): super().__init__() self.rank = rank self.alpha = alpha self.scaling = alpha / rank self.base_layer = bnb.nn.Linear4bit( in_dim, out_dim, unbiased=False, quant_type="nf4", ) for param in self.base_layer.parameters(): param.requires_grad = False self.lora_A = nn.Parameter(torch.randn(in_dim, rank)) self.lora_B = nn.Parameter(torch.empty(rank, out_dim)) def forward(self, x): original_out = self.base_layer(x) lora_out = (x @ self.lora_A) @ self.lora_B return original_out + self.scaling * lora_out
MLP
-
代码实现
import numpy as np # ----------------------------- # 1. 激活函数与 Softmax # ----------------------------- def relu(x): return np.maximum(0, x) def relu_grad(x): return (x > 0).astype(float) def softmax(x): exp_x = np.exp(x - np.max(x, axis=-1, keepdims=True)) # 防溢出 return exp_x / np.sum(exp_x, axis=-1, keepdims=True) # ----------------------------- # 2. Cross Entropy Loss # ----------------------------- def cross_entropy_loss(pred, label): # pred: softmax输出 [B, C] # label: one-hot [B, C] batch_size = pred.shape[0] log_pred = -np.log(pred + 1e-8) loss = np.sum(label * log_pred) / batch_size return loss # ----------------------------- # 3. MLP 类(手动前向+反向) # ----------------------------- class MLP: def __init__(self, in_dim, hidden_dim, out_dim): # 初始化参数 self.W1 = np.random.randn(in_dim, hidden_dim) * 0.01 self.b1 = np.zeros((1, hidden_dim)) self.W2 = np.random.randn(hidden_dim, out_dim) * 0.01 self.b2 = np.zeros((1, out_dim)) def forward(self, x): # 前向传播,保存中间变量用于反向 self.x = x self.z1 = x @ self.W1 + self.b1 self.h = relu(self.z1) self.z2 = self.h @ self.W2 + self.b2 self.pred = softmax(self.z2) return self.pred def backward(self, label, lr=1e-3): B = label.shape[0] # ----------------------- # 反向传播 手推梯度 # ----------------------- # 1. dz2 = dLoss/dz2 = (pred - label) / B (关键!) dz2 = (self.pred - label) / B # 2. 输出层梯度 dW2 = self.h.T @ dz2 db2 = np.sum(dz2, axis=0, keepdims=True) # 3. 隐藏层梯度 dh = dz2 @ self.W2.T dz1 = dh * relu_grad(self.z1) # 4. 输入层梯度 dW1 = self.x.T @ dz1 db1 = np.sum(dz1, axis=0, keepdims=True) # ----------------------- # 梯度下降更新 # ----------------------- self.W2 -= lr * dW2 self.b2 -= lr * db2 self.W1 -= lr * dW1 self.b1 -= lr * db1 # ----------------------------- # 测试 # ----------------------------- if __name__ == "__main__": # 数据:batch=2, in=3, out=2 x = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) label = np.array([[1, 0], [0, 1]]) # one-hot model = MLP(in_dim=3, hidden_dim=5, out_dim=2) for step in range(1000): pred = model.forward(x) loss = cross_entropy_loss(pred, label) model.backward(label, lr=1e-1) if step % 100 == 0: print(f"step {step}, loss = {loss:.4f}")
Attention
MHA
-
代码实现
class MultiHeadAttention(nn.Module): def __init__(self, hidden_size, num_heads): super().__init__() assert hidden_size % num_heads == 0 self.hidden_size = hidden_size self.num_heads = num_heads self.d_k = hidden_size // num_heads self.W_q = nn.Linear(hidden_size, hidden_size) self.W_k = nn.Linear(hidden_size, hidden_size) self.W_v = nn.Linear(hidden_size, hidden_size) self.W_o = nn.Linear(hidden_size, hidden_size) self.dropout = nn.Dropout(0.1) self.cache_k = None self.cache_v = None def forward(self, x, mask=None, use_cache=False): B, S, _ = x.shape q = self.W_q(x) k = self.W_k(x) v = self.W_v(x) # reshape -> [B, H, S, D] q = q.view(B, S, self.num_heads, self.d_k).transpose(1, 2) k = k.view(B, S, self.num_heads, self.d_k).transpose(1, 2) v = v.view(B, S, self.num_heads, self.d_k).transpose(1, 2) if use_cache: if self.cache_k is not None: self.cache_k = torch.cat([self.cache_k, k], dim=-2) self.cache_v = torch.cat([self.cache_v, v], dim=-2) else: self.cache_k = k self.cache_v = v k = self.cache_k v = self.cache_v # attention scores -> [B, H, S, S] attn_scores = (q @ k.transpose(-2, -1)) * (self.d_k**-0.5) # mask -> [B, 1, 1, S] or [B, 1, S, S] if mask is not None: attn_scores = attn_scores.masked_fill(mask == 0, float("-inf")) # softmax attn_scores = torch.nn.functional.softmax(attn_scores, dim=-1) attn_scores = self.dropout(attn_scores) # attention weights -> [B, H, S, D] attn_weights = attn_scores @ v # concat heads -> [B, S, H*D] attn_weights = attn_weights.transpose(1, 2).contiguous().view(B, S, self.hidden_size) # final linear attn_output = self.W_o(attn_weights) return attn_output
MQA
-
代码实现
class MultiQueryAttention(nn.Module): def __init__(self, hidden_size, num_heads): super().__init__() assert hidden_size % num_heads == 0 self.hidden_size = hidden_size self.num_heads = num_heads self.d_k = hidden_size // num_heads self.W_q = nn.Linear(hidden_size, hidden_size) # K/V shared across all heads self.W_k = nn.Linear(hidden_size, self.d_k) self.W_v = nn.Linear(hidden_size, self.d_k) self.W_o = nn.Linear(hidden_size, hidden_size) self.dropout = nn.Dropout(0.1) def forward(self, x, mask=None): B, S, _ = x.shape q = self.W_q(x) k = self.W_k(x) v = self.W_v(x) # Q: [B, S, H, D] -> [B, H, S, D] q = q.view(B, S, self.num_heads, self.d_k).transpose(1, 2) # K/V: [B, 1, S, D] for broadcasting k = k.unsqueeze(1) v = v.unsqueeze(1) # attention scores -> [B, H, S, S] scores = (q @ k.transpose(-2, -1)) * (self.d_k**-0.5) if mask is not None: scores = scores.masked_fill(mask == 0, float("-inf")) # attention weights -> [B, H, S, S] attn = torch.softmax(scores, dim=-1) attn = self.dropout(attn) # out: [B, H, S, D] out = attn @ v # concat heads -> [B, S, H*D] out = out.transpose(1, 2).contiguous().view(B, S, self.hidden_size) return self.W_o(out), attn
GQA
-
代码实现
class GroupQueryAttention(nn.Module): def __init__(self, hidden_size, num_heads, num_groups): super().__init__() assert hidden_size % num_heads == 0 assert hidden_size % num_groups == 0 assert num_heads % num_groups == 0 self.hidden_size = hidden_size self.num_heads = num_heads self.num_groups = num_groups self.d_k = hidden_size // num_heads self.num_kv_heads = num_heads // num_groups self.W_q = nn.Linear(hidden_size, hidden_size) self.W_k = nn.Linear(hidden_size, num_groups * self.d_k) self.W_v = nn.Linear(hidden_size, num_groups * self.d_k) self.W_o = nn.Linear(hidden_size, hidden_size) self.dropout = nn.Dropout(0.1) def forward(self, x, mask=None): B, S, _ = x.shape q = self.W_q(x) k = self.W_k(x) v = self.W_v(x) # Q: [B, S, H, D] -> [B, H, S, D] q = q.view(B, S, self.num_heads, self.d_k).transpose(1, 2) # K/V: [B, S, G, D] -> [B, G, S, D] k = k.view(B, S, self.num_groups, self.d_k).transpose(1, 2) v = v.view(B, S, self.num_groups, self.d_k).transpose(1, 2) k = k.repeat_interleave(self.num_kv_heads, dim=1) v = v.repeat_interleave(self.num_kv_heads, dim=1) # attention scores -> [B, H, S, S] attn_scores = (q @ k.transpose(-2, -1)) * (self.d_k**-0.5) if mask is not None: attn_scores = attn_scores.masked_fill(mask == 0, float("-inf")) attn_scores = torch.nn.functional.softmax(attn_scores, dim=-1) attn_scores = self.dropout(attn_scores) # attn_weights [B, H, S, d_k] attn_weights = attn_scores @ v attn_weights = attn_weights.transpose(1, 2).contiguous().view(B, S, self.hidden_size) attn_output = self.W_o(attn_weights ) return attn_output
MLA
-
代码实现
LinearAttention
位置编码
Absolute
-
代码实现
class AbsolutePositionalEncoding(nn.Module): def __init__(self, max_seq_len, hidden_size): super().__init__() self.pos_embedding = nn.Embedding(max_seq_len, hidden_size) def forward(self, input_ids): seq_len = input_ids.size(1) device = input_ids.device positions = torch.arange(0, seq_len, dtype=torch.long, device=device).unsqueeze( 0 ) return input_ids + self.pos_embedding(positions)
Sinusoidal
-
代码实现
class SinusoidalPositionalEncoding(nn.Module): def __init__(self, max_seq_len, hidden_size, base=10000): super().__init__() self.max_seq_len = max_seq_len self.hidden_size = hidden_size # 创建一个 [max_seq_len, hidden_size] 的位置编码矩阵 position = torch.arange(0, max_seq_len).unsqueeze(1) # [max_seq_len, 1] div_term = torch.exp( torch.arange(0, hidden_size, 2) * (-torch.log(torch.tensor(base, dtype=torch.float32)) / hidden_size) ) # [hidden_size/2] # [max_seq_len, hidden_size] 的位置编码矩阵 pe = torch.zeros(max_seq_len, hidden_size) # 公式:PE(pos, 2i) = sin(pos / (base^(2i/hidden_size))) # PE(pos, 2i+1) = cos(pos / (base^(2i/hidden_size))) # 0::2 表示步长为 2,从索引 0 开始,1::2 表示步长为 2,从索引 1 开始 pe[:, 0::2] = torch.sin(position * div_term) # 偶数维度使用正弦函数 pe[:, 1::2] = torch.cos(position * div_term) # 奇数维度使用余弦函数 self.register_buffer("pos_embedding", pe) # 将位置编码注册为 buffer,不参与训练 def forward(self, input_ids): seq_len = input_ids.size(1) return input_ids + self.pos_embedding[:seq_len, :].unsqueeze(0).to( input_ids.device )
RoPE
-
代码实现
class LlamaRotaryEmbedding(nn.Module): """ Rotary Positional Embedding (RoPE). 原理: RoPE 通过将 Query(Q) 和 Key(K) 向量对看作是复数空间中的坐标,并对其进行旋转来注入位置信息。 相较于绝对位置编码,RoPE 捕捉的是相对位置关系,具有更好的外推性(Extrapolation)。 具体实现上,它通过将向量两两分组,并在 2D 平面应用旋转矩阵来实现: [cos θ, -sin θ] [x_1] [sin θ, cos θ] [x_2] """ def __init__( self, dim: int, max_position_embeddings: int = 2048, base: float = 10000.0 ): """ 初始化 RoPE 模块 Args: dim: 每个头的维度 (head_dim),通常为 hidden_size / num_heads max_position_embeddings: 预计算的最大序列长度,默认为 2048 base: 频率基数,控制旋转频率,默认为 10000.0 """ super().__init__() self.dim = dim self.max_position_embeddings = max_position_embeddings self.base = base # 计算逆频系数 (Inverse frequencies),即每个维度分组的旋转角度系数 # 公式: inv_freq[i] = 1 / (base^(2*i/dim)),其中 i 是偶数索引 # torch.arange(0, dim, 2):取偶数索引 [0,2,4,...],因为维度两两分组(x0/x1 一组,x2/x3 一组...) inv_freq = 1.0 / ( self.base**(torch.arange(0, self.dim, 2).float() / self.dim) ) # 注册一个不可训练的缓冲区,存储逆频系数,形状为 [dim/2],用于后续计算旋转角度 self.register_buffer("inv_freq", inv_freq, persistent=False) # 预计算并缓存 cos 和 sin 矩阵,以加速前向计算,初始长度为 max_position_embeddings self._set_cos_sin_cache( max_position_embeddings, inv_freq.device, inv_freq.dtype ) def _set_cos_sin_cache(self, seq_len, device, dtype): """ 预计算并缓存 cos 和 sin 矩阵,以加速前向计算 Args: seq_len: 需要预计算的序列长度 device: 预计算张量所在的设备 dtype: 预计算张量的数据类型 """ self.max_seq_len_cached = seq_len # 生成位置索引 t = [0,1,2,...,seq_len-1] t = torch.arange( self.max_seq_len_cached, device=device, dtype=self.inv_freq.dtype ) # 计算对应的频率值,形状为 [seq_len, dim/2] # 公式: freqs[t][i] = t * inv_freq[i] = t * θ_i,其中 t 是位置索引,i 是频率索引 # outer 实现了 “位置索引 × 逆频率” 的外积,快速得到所有位置 × 所有维度分组的旋转角度矩阵 freqs = torch.outer(t, self.inv_freq) # 将频率与其自身拼接以适应 [x_1, x_2, ..., x_n/2, x_1, x_2, ..., x_n/2] 的旋转策略 # 拼接 freq → shape: [seq_len, dim] # 原因:分组维度需要重复 θ_i(如 x0/x1 共享 θ_0,所以 θ_0 要出现在 dim=0 和 dim=1 位置) # 对应公式中高维旋转的“两两分组”逻辑 emb = torch.cat((freqs, freqs), dim=-1) # 预计算 cos 和 sin,增加两个维度 [1,1,seq_len,dim] # 额外维度是为了适配注意力的 [batch_size, num_heads, seq_len, dim] 形状,方便广播 self.register_buffer( "cos_cached", emb.cos()[None, None, :, :].to(dtype), persistent=False ) self.register_buffer( "sin_cached", emb.sin()[None, None, :, :].to(dtype), persistent=False ) def forward(self, x, seq_len: int): """ 获取指定长度的旋转编码嵌入 (cos, sin)。 Args: x: 参考张量,用于确定 device 和 dtype seq_len: 需要生成的序列长度 """ # 如果当前缓存长度不足,则动态重新计算 if seq_len > self.max_seq_len_cached: self._set_cos_sin_cache(seq_len, x.device, x.dtype) # 返回预计算的 cos 和 sin 矩阵,形状为 [1,1,seq_len,dim],并确保数据类型与输入 x 一致 return ( self.cos_cached[:, :, :seq_len, ...].to(dtype=x.dtype), self.sin_cached[:, :, :seq_len, ...].to(dtype=x.dtype), ) -
LinearScaling
# LlamaLinearScalingRotaryEmbedding 是 LlamaRotaryEmbedding 的一个变体,增加了一个 scaling_factor 参数 # scaling_factor 用于调整位置索引 t 的缩放,从而改变旋转频率,公式为 t' = t / scaling_factor # 通过调整 scaling_factor,可以控制 RoPE 的外推能力和对不同长度序列的适应性,适用于需要处理更长序列或希望增强模型泛化能力的场景 class LlamaLinearScalingRotaryEmbedding(LlamaRotaryEmbedding): def __init__( self, dim, max_position_embeddings=2048, base=10000, device=None, scaling_factor=1.0, ): self.scaling_factor = scaling_factor super().__init__(dim, max_position_embeddings, base, device) def _set_cos_sin_cache(self, seq_len, device, dtype): self.max_seq_len_cached = seq_len t = torch.arange( self.max_seq_len_cached, device=device, dtype=self.inv_freq.dtype ) t = t / self.scaling_factor freqs = torch.outer(t, self.inv_freq) # Different from paper, but it uses a different permutation in order to obtain the same calculation emb = torch.cat((freqs, freqs), dim=-1) self.register_buffer("cos_cached", emb.cos().to(dtype), persistent=False) self.register_buffer("sin_cached", emb.sin().to(dtype), persistent=False) -
NTK Scaling
# LlamaDynamicNTKScalingRotaryEmbedding 是 LlamaRotaryEmbedding 的另一个变体,增加了一个 scaling_factor 参数 class LlamaDynamicNTKScalingRotaryEmbedding(LlamaRotaryEmbedding): def __init__( self, dim, max_position_embeddings=2048, base=10000, device=None, scaling_factor=1.0, ): self.scaling_factor = scaling_factor super().__init__(dim, max_position_embeddings, base, device) def _set_cos_sin_cache(self, seq_len, device, dtype): self.max_seq_len_cached = seq_len # 仅当当前序列长度 > 预训练最大长度时,才调整base(动态NTK) if seq_len > self.max_position_embeddings: # 公式为:base' = base * ((scaling_factor * seq_len / max_position_embeddings) - (scaling_factor - 1))^(dim/(dim-2)) base = self.base * ( (self.scaling_factor * seq_len / self.max_position_embeddings) - (self.scaling_factor - 1) )**(self.dim / (self.dim - 2)) # 重新计算逆频系数,公式为 inv_freq[i] = 1 / (base'^(2*i/dim)),其中 i 是偶数索引 inv_freq = 1.0 / ( base**(torch.arange(0, self.dim, 2).float().to(device) / self.dim) ) self.register_buffer("inv_freq", inv_freq, persistent=False) # 和基础版本一样的计算逻辑,但使用可能调整过的 inv_freq t = torch.arange( self.max_seq_len_cached, device=device, dtype=self.inv_freq.dtype ) freqs = torch.outer(t, self.inv_freq) # Different from paper, but it uses a different permutation in order to obtain the same calculation emb = torch.cat((freqs, freqs), dim=-1) self.register_buffer("cos_cached", emb.cos().to(dtype), persistent=False) self.register_buffer("sin_cached", emb.sin().to(dtype), persistent=False) -
工具函数
def rotate_half(x): """ 对向量后半部分取负号并交换前后部分,以配合 RoPE 的复数乘法展开式。 公式: [-x_2, x_1] 对应于复数空间中的 (x_1 + i*x_2) * i = (-x_2 + i*x_1) Args: x: 输入张量,形状为 [..., head_dim] """ x1 = x[..., : x.shape[-1] // 2] x2 = x[..., x.shape[-1] // 2 :] return torch.cat((-x2, x1), dim=-1) def apply_rotary_pos_emb(q, k, cos, sin, position_ids, unsqueeze_dim=1): """ 将旋转位置编码应用到 Q 和 K。 实现公式: O = Q * cos(θ) + rotate_half(Q) * sin(θ) Args: q: Query 张量 [batch, num_heads, seq_len, head_dim] k: Key 张量 [batch, num_kv_heads, seq_len, head_dim] cos: 预计算好的 cos 缓存 sin: 预计算好的 sin 缓存 position_ids: 当前 token 的位置索引 [batch, seq_len] unsqueeze_dim: 需要在 cos 和 sin 上增加的维度位置,以便与 q 和 k 进行广播 Returns: q_embed: 应用 RoPE 后的 Query 张量 k_embed: 应用 RoPE 后的 Key 张量 """ # 按照 position_ids 从预计算的缓存中提取对应的 cos 和 sin # q, k 形状通常为 [batch, num_heads, seq_len, head_dim] # cos, sin 预计算形状为 [1, 1, max_seq, head_dim] cos = cos[:, :, position_ids, :].squeeze(1) # [batch, seq_len, head_dim] sin = sin[:, :, position_ids, :].squeeze(1) # 使用广播机制完成旋转变换 q_embed = (q * cos.unsqueeze(1)) + (rotate_half(q) * sin.unsqueeze(1)) k_embed = (k * cos.unsqueeze(1)) + (rotate_half(k) * sin.unsqueeze(1)) return q_embed, k_embed
归一化
BatchNorm
LayerNorm
-
代码实现
class LayerNorm(nn.Module): def __init__(self, hidden_size, eps=1e-6): super().__init__() self.hidden_size = hidden_size self.eps = eps # gamma 和 beta 是可学习的参数,初始化为 1 和 0 self.gamma = nn.Parameter(torch.ones(hidden_size)) self.beta = nn.Parameter(torch.zeros(hidden_size)) def forward(self, x): # 公式:LayerNorm(x) = gamma * (x - mean) / sqrt(var + eps) + beta # 计算最后一个维度的均值和方差 mean = x.mean(dim=-1, keepdim=True) # [B, S, 1] var = x.var(dim=-1, keepdim=True, unbiased=False) # [B, S, 1] # 标准化 normalized = (x - mean) / torch.sqrt(var + self.eps) # 缩放和平移,*是逐元素乘法,+是逐元素加法 output = self.gamma * normalized + self.beta return output
RMSNorm
-
代码实现
class RMSNorm(nn.Module): def __init__(self, hidden_size, eps=1e-6): super().__init__() self.hidden_size = hidden_size self.eps = eps # RMSNorm 只有 gamma 参数,没有 beta 参数,初始化为 1 self.gamma = nn.Parameter(torch.ones(hidden_size)) def forward(self, x): # 公式:RMSNorm(x) = gamma * x / sqrt(mean(x^2) + eps) rms = torch.sqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps) normalized = x / rms output = self.gamma * normalized return output
Transformer
FFN
-
代码实现
class FFN(nn.Module): def __init__(self, hidden_size, intermediate_size): super().__init__() self.hidden_size = hidden_size self.up_proj = nn.Linear(hidden_size, intermediate_size) self.down_proj = nn.Linear(intermediate_size, hidden_size) self.dropout = nn.Dropout(0.1) def forward(self, x): x = self.up_proj(x) x = F.gelu(x) x = self.down_proj(x) x = self.dropout(x) return x
SwiGLU
-
代码实现
class SwiGLU(nn.Module): def __init__(self, hidden_size, intermediate_size): super().__init__() self.hidden_size = hidden_size self.gate = nn.Linear(hidden_size, intermediate_size) self.up_proj = nn.Linear(hidden_size, intermediate_size) self.down_proj = nn.Linear(intermediate_size, hidden_size) self.dropout = nn.Dropout(0.1) def forward(self, x): return self.down_proj(F.silu(self.gate(x)) * self.up_proj(x))
MoE
-
代码实现
class MoELayer(nn.Module): """ 原理: MoE 层通过门控网络 (Gate) 动态地为输入的每个 token 选择 Top-K 个专家 MoE 的特殊设计包括: 1. 共享专家 (Shared Experts): 处理所有 token 的特征,提取共性知识 2. 稀疏专家 (Routed/Sparse Experts): 只处理选中的 token 公式: Output = Shared_Experts(x) + Gate(x) * TopK_Experts(x) """ def __init__( self, hidden_size: int, # 模型维度 num_experts: int, # 总专家数 num_experts_per_tok: int, # 每个 token 选 Top-K 个专家 intermediate_size: int = None, # FFN 中间维度(默认 4*hidden_size) use_shared_expert: bool = True, # 是否使用共享专家 dropout: float = 0.0 # dropout 概率 balance_loss_coef: float = 0.01 # 平衡损失系数 ): super().__init__() self.hidden_size = hidden_size self.num_experts = num_experts self.top_k = num_experts_per_tok self.balance_loss_coef = balance_loss_coef # 通常 0.01 ~ 0.001 # 门控网络 self.gate = nn.Linear(hidden_size, num_experts, bias=False) # 稀疏路由专家 self.experts = nn.ModuleList([ SwiGLU(hidden_size, intermediate_size, dropout) for _ in range(num_experts) ]) # 共享专家 self.shared_experts = ( SwiGLU(hidden_size, intermediate_size, dropout) if use_shared_expert else None ) def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: batch_size, seq_len, hidden_size = hidden_states.shape # 展平 [B, S, H] -> [B*S, H] x = hidden_states.view(-1, hidden_size) num_tokens = x.shape[0] # 1. 门控路由 + TopK router_logits = self.gate(x) routing_weights = nnf.softmax(router_logits, dim=-1, dtype=torch.float32) weights, selected_experts = torch.topk(routing_weights, self.top_k, dim=-1) # 权重归一化 weights = weights / weights.sum(dim=-1, keepdim=True) weights = weights.to(x.dtype) balance_loss = self._calculate_balance_loss( routing_weights, selected_experts, num_tokens ) # 2. 稀疏专家计算 final_hidden_states = torch.zeros_like(x) for expert_idx in range(self.num_experts): expert = self.experts[expert_idx] # 找到选中当前专家的 token token_mask = (selected_experts == expert_idx).any(dim=-1) if not token_mask.any(): continue token_indices = torch.where(token_mask)[0] expert_out = expert(x[token_indices]) # 加权累加所有 Top-K 路径 for k in range(self.top_k): pos_mask = selected_experts[token_indices, k] == expert_idx if pos_mask.any(): final_hidden_states[token_indices[pos_mask]] += ( weights[token_indices[pos_mask], k, None] * expert_out[pos_mask] ) # 3. 加上共享专家输出 if self.shared_experts is not None: final_hidden_states += self.shared_experts(x) # 恢复形状 output = final_hidden.view(batch_size, seq_len, hidden_size) return output, balance_loss def _calculate_balance_loss(self, routing_weights, selected_experts, num_tokens): """ 标准 MoE 负载均衡损失(Google Switch Transformer) 防止部分专家被过度使用,部分闲置 """ if self.num_experts <= 1: return torch.tensor(0.0, device=routing_weights.device) # 每个 token 分配给专家的概率(mean over tokens) prob_per_expert = routing_weights.mean(dim=0) # [E] # 每个专家被选中的频率(实际分配到的 token 占比) mask = torch.zeros_like(routing_weights) mask.scatter_(1, selected_experts, 1.0) freq_per_expert = mask.sum(dim=0) / num_tokens # [E] # 平衡损失公式 balance_loss = self.num_experts * (prob_per_expert * freq_per_expert).sum() balance_loss = balance_loss * self.balance_loss_coef return balance_loss -
Qwen3-MoE
class Qwen3MoeSparseMoeBlock(nn.Module): """ Qwen3混合专家模型中的稀疏MoE模块,通过路由器选择部分专家处理输入,实现高效计算 """ def __init__(self, config: Qwen3MoeConfig): super().__init__() # 1. 基础配置参数 self.hidden_size = config.hidden_size # 输入特征维度 self.num_experts = config.num_experts # 专家网络总数:128 self.num_experts_per_tok = config.num_experts_per_tok # 每个token激活的专家数:8 # 2. 路由器(Router):决定每个token选择哪些专家 # 输入:[batch_size, seq_len, hidden_size],输出:[batch_size, seq_len, num_experts](专家权重) self.gate = nn.Linear(self.hidden_size, self.num_experts, bias=False) # 3. 初始化专家网络(每个专家为独立的MLP) # 专家网络通常采用与稠密MLP相同的结构(如两次线性变换+激活函数) self.experts = nn.ModuleList([ Qwen3MoeMLP(config) # 复用Qwen3的MLP结构作为专家 for _ in range(self.num_experts) ]) # 4. 损失函数相关(可选,用于训练时平衡专家负载) self.router_aux_loss_coef = config.router_aux_loss_coef # 路由器辅助损失系数 def forward(self, hidden_states: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: """ 前向传播:输入特征 → 路由器选专家 → 专家计算 → 融合输出 Args: hidden_states: 输入特征,形状为 [batch_size, seq_len, hidden_size] Returns: output: 融合后的输出特征,形状同输入 router_aux_loss: 路由器辅助损失(用于训练时优化专家负载均衡) """ # ===== 步骤1:计算路由器输出(专家权重)===== # 输入通过线性层得到每个专家的原始分数(logits) # 形状:[batch_size, seq_len, num_experts] router_logits = self.gate(hidden_states) # ===== 步骤2:选择Top-K专家并计算权重 ===== # 对每个token,选择分数最高的num_experts_per_tok个专家 # top_k_weights: 选中专家的权重(经softmax归一化),形状 [batch_size, seq_len, num_experts_per_tok] # top_k_indices: 选中专家的索引,形状 [batch_size, seq_len, num_experts_per_tok] top_k_weights, top_k_indices = torch.topk(router_logits, self.num_experts_per_tok, dim=-1) top_k_weights = nn.functional.softmax(top_k_weights, dim=-1, dtype=torch.float32) # ===== 步骤3:计算路由器辅助损失(可选,训练用)===== # 目的是鼓励专家负载均衡,避免少数专家被频繁选中 # 计算方式:对router_logits做softmax后取均值,再求负熵(简化实现) if self.training: # 先对专家分数做softmax,得到每个专家被选中的概率 router_probs = nn.functional.softmax(router_logits, dim=-1, dtype=torch.float32) # 计算每个专家的平均负载(跨batch和seq_len) expert_load = torch.mean(router_probs, dim=(0, 1)) # 形状 [num_experts] # 辅助损失:鼓励负载均衡(熵越大,分布越均衡) router_aux_loss = torch.sum(expert_load * torch.log(expert_load + 1e-10)) # 负熵 router_aux_loss *= self.router_aux_loss_coef # 乘以系数 else: router_aux_loss = torch.tensor(0.0, device=hidden_states.device) # 推理时无损失 # ===== 步骤4:准备输入,分发到选中的专家 ===== # 调整输入形状为 [batch_size * seq_len, hidden_size],便于批量处理 batch_size, seq_len, hidden_size = hidden_states.shape hidden_states = hidden_states.view(-1, hidden_size) # 形状 [total_tokens, hidden_size],total_tokens = batch_size * seq_len # 调整选中专家索引形状:[total_tokens, num_experts_per_tok] top_k_indices = top_k_indices.view(-1, self.num_experts_per_tok) # [total_tokens, k] # 调整权重形状:[total_tokens, num_experts_per_tok, 1](便于广播) top_k_weights = top_k_weights.view(-1, self.num_experts_per_tok, 1) # [total_tokens, k, 1] # ===== 步骤5:专家计算与结果融合 ===== # 初始化输出张量 final_output = torch.zeros( (batch_size * seq_len, hidden_size), # 与输入同形状 dtype=hidden_states.dtype, device=hidden_states.device ) # 遍历每个专家,处理所有选中该专家的token for expert_idx in range(self.num_experts): # 找到所有选中当前专家的token索引 # 掩码:[total_tokens, k] → True表示该位置选中了当前专家 expert_mask = (top_k_indices == expert_idx) # [total_tokens, k] # 若没有token选中当前专家,跳过 if not expert_mask.any(): continue # 收集选中当前专家的token及其对应的权重 # 1. 提取这些token的输入特征 expert_input = hidden_states[expert_mask.any(dim=1)] # [num_tokens_for_this_expert, hidden_size] # 2. 提取这些token对当前专家的权重(取第一个匹配的权重,因每个位置最多选k个专家) expert_weights = top_k_weights[expert_mask] # [num_tokens_for_this_expert, 1] # 3. 当前专家处理输入 expert_output = self.experts[expert_idx](expert_input) # [num_tokens_for_this_expert, hidden_size] # 4. 加权:用该专家的权重乘以输出 expert_output = expert_output * expert_weights # [num_tokens_for_this_expert, hidden_size] # 5. 将结果累加至最终输出(对应位置) final_output[expert_mask.any(dim=1)] += expert_output # ===== 步骤6:恢复输出形状并返回 ===== final_output = final_output.view(batch_size, seq_len, hidden_size) # [batch_size, seq_len, hidden_size] return final_output, router_aux_loss
TransformerBlock
-
代码实现
class TransformerBlock(nn.Module): def __init__( self, hidden_size, num_heads, intermediate_size, attention_type="multi_head", num_groups=1, latent_dim=128, norm_type="layer_norm", pre_norm=True, gated_ffn=True, ): super().__init__() self.hidden_size = hidden_size self.num_heads = num_heads self.intermediate_size = intermediate_size self.pre_norm = pre_norm self.gated_ffn = gated_ffn if attention_type == "multi_head": self.attention = MultiHeadAttention(hidden_size, num_heads) elif attention_type == "multi_query": self.attention = MultiQueryAttention(hidden_size, num_heads) elif attention_type == "group_query": self.attention = GroupQueryAttention(hidden_size, num_heads, num_groups) elif attention_type == "multi_latent": self.attention = MultiLatentAttention( hidden_size, num_heads, latent_dim=latent_dim ) elif attention_type == "linear": self.attention = LinearAttention(hidden_size, num_heads) else: raise ValueError(f"Unsupported attention type: {attention_type}") if norm_type == "layer_norm": self.norm1 = LayerNorm(hidden_size) self.norm2 = LayerNorm(hidden_size) elif norm_type == "rms_norm": self.norm1 = RMSNorm(hidden_size) self.norm2 = RMSNorm(hidden_size) else: raise ValueError(f"Unsupported norm type: {norm_type}") if gated_ffn: self.ffn = SwiGLU(hidden_size, intermediate_size) else: self.ffn = FFN(hidden_size, intermediate_size) def forward(self, x, mask=None): # 注意力子层 if self.pre_norm: attn_output, _ = self.attention(self.norm1(x), mask=mask) x = x + attn_output # 残差连接 else: attn_output, _ = self.attention(x, mask=mask) x = self.norm1(x + attn_output) # 残差连接 + 后归一化 # FFN 子层 if self.pre_norm: x = x + self.ffn(self.norm2(x)) # 残差连接 else: x = self.norm2(x + self.ffn(x)) # 残差连接 + 后归一化 return x
Transformer
-
代码实现
class Transformer(nn.Module): def __init__( self, vocab_size, # 词表大小 hidden_size, # 隐藏层维度 num_layers, # Transformer 层数 num_heads, # 注意力头数 intermediate_size, # FFN 中间维度 max_seq_len=512, # 最大序列长度 attention_type="multi_head", num_groups=1, latent_dim=128, norm_type="rms_norm", # 默认 RMSNorm pre_norm=True, gated_ffn=True, dropout=0.1, ): super().__init__() self.hidden_size = hidden_size self.max_seq_len = max_seq_len self.embedding = nn.Embedding(vocab_size, hidden_size) self.pos_embedding = nn.Embedding(max_seq_len, hidden_size) # 绝对位置编码 self.dropout = nn.Dropout(dropout) self.layers = nn.ModuleList( [ TransformerBlock( hidden_size=hidden_size, num_heads=num_heads, intermediate_size=intermediate_size, attention_type=attention_type, num_groups=num_groups, latent_dim=latent_dim, norm_type=norm_type, pre_norm=pre_norm, gated_ffn=gated_ffn, ) for _ in range(num_layers) ] ) self.norm = ( nn.LayerNorm(hidden_size) if norm_type == "layer_norm" else RMSNorm(hidden_size) ) self.lm_head = nn.Linear(hidden_size, vocab_size) self.apply(self._init_weights) def _init_weights(self, module): if isinstance(module, nn.Linear): torch.nn.init.normal_(module.weight, mean=0.0, std=0.02) if module.bias is not None: torch.nn.init.zeros_(module.bias) elif isinstance(module, nn.Embedding): torch.nn.init.normal_(module.weight, mean=0.0, std=0.02) def forward(self, input_ids, mask=None): B, L = input_ids.shape device = input_ids.device # 1. 词嵌入 + 位置编码 positions = torch.arange(0, L, dtype=torch.long, device=device).unsqueeze(0) x = self.embedding(input_ids) + self.pos_embedding(positions) # [B, L, H] x = self.dropout(x) # 2. 过 N 层 Transformer for layer in self.layers: x = layer(x, mask=mask) # 3. 最后归一化 x = self.norm(x) # 4. 语言模型预测(输出 logits) logits = self.lm_head(x) # [B, L, vocab_size] return logits
强化学习
PPO
-
代码实现
import torch import torch.nn.functional as F def masked_mean(tensor, mask): return (tensor * mask).sum() / (mask.sum() + 1e-8) def masked_whiten(tensor, mask, shift_mean=True): mean = masked_mean(tensor, mask) centered = tensor - mean var = masked_mean(centered**2, mask) std = torch.sqrt(var + 1e-8) whitened = (centered / std) if shift_mean else (tensor / std) return whitened * mask # ========================= # GAE Advantage # ========================= def compute_advantages( token_level_rewards, # [B, T] values, # [B, T] gamma=0.99, lambd=0.95, ): with torch.no_grad(): lastgaelam = 0 advantages_reversed = [] gen_len = token_level_rewards.shape[-1] for t in reversed(range(gen_len)): nextvalues = values[:, t + 1] if t < gen_len - 1 else 0.0 delta = token_level_rewards[:, t] + gamma * nextvalues - values[:, t] lastgaelam = delta + gamma * lambd * lastgaelam advantages_reversed.append(lastgaelam) advantages = torch.stack(advantages_reversed[::-1], dim=1) returns = advantages + values # advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8) advantages = masked_whiten(advantages, mask=mask) # 归一化 return advantages, returns # ========================= # PPO Loss # ========================= def ppo_loss( logprobs, # new log probs [B, T] old_logprobs, # old log probs [B, T] values, # new values [B, T] old_values, # old values [B, T] advantages, # [B, T] returns, # [B, T] ref_logprobs, # [B, T] clip_range=0.2, value_clip_range=0.2, kl_coef=0.1, vf_coef=0.5, ent_coef=0.01, ): # ===== ratio ===== ratio = torch.exp(logprobs - old_logprobs) # ===== policy loss ===== surr1 = ratio * advantages surr2 = torch.clamp(ratio, 1 - clip_range, 1 + clip_range) * advantages policy_loss = torch.min(surr1, surr2) policy_loss = - masked_mean(policy_loss, loss_mask) # ===== value loss ===== value_pred_clipped = old_values + torch.clamp( values - old_values, -value_clip_range, value_clip_range ) value_loss_1 = (values - returns)**2 value_loss_2 = (value_pred_clipped - returns)**2 value_loss = 0.5 * torch.max(value_loss_1, value_loss_2) value_loss = masked_mean(value_loss, loss_mask) # ===== entropy ===== entropy = -torch.sum(torch.exp(logprobs) * logprobs, dim=-1) entropy_loss = - torch.mean(entropy) total_loss = policy_loss + vf_coef * value_loss + ent_coef * entropy_loss return total_loss # ========================= # Training Loop(无 minibatch) # ========================= for step in range(num_steps): # ===== 1. Rollout ===== with torch.no_grad(): responses, old_logprobs, old_values = model.generate(prompts) ref_logprobs = ref_model.forward(responses).detach() # ===== 2. Reward ===== with torch.no_grad(): # kl 惩罚直接加在 reward 中,这里使用 k1 估计 kl = ref_logprobs - old_logprobs rewards = reward_model.get_rewards(prompts, responses) - kl_coef * kl # ===== 3. Advantage ===== advantages, returns = compute_advantages( rewards, old_values, ) # ===== 4. PPO 多轮更新(batch)===== for _ in range(ppo_epochs): logprobs, values = model.forward(responses) loss, stats = ppo_loss( logprobs=nlogprobs, # 当前 logprobs old_logprobs=old_logprobs, # 旧策略 logprobs values=values, # 当前 values old_values=old_values, # 旧 values advantages=advantages, # 当前优势 returns=returns, # 回报 ref_logprobs=ref_logprobs, # 参考模型 logprobs ) optimizer.zero_grad() loss.backward() optimizer.step()
GRPO
-
代码实现
# ========================= # GRPO Advantage # ========================= def compute_grpo_advantages(rewards, eps=1e-8): mean = rewards.mean(dim=1, keepdim=True) # 求组内均值 std = rewards.std(dim=1, keepdim=True) # 求组内标准差 adv = (rewards - mean) / (std + eps) # 求组内优势估计 return adv def compute_kl_loss(logprobs, ref_logprobs): log_ratio = logprobs - ref_logprobs kl = (torch.exp(log_ratio) - log_ratio - 1) return kl def masked_mean(tensor, mask): return (tensor * mask).sum() / (mask.sum() + 1e-8) # ========================= # GRPO Loss # ========================= def grpo_loss( logprobs, old_logprobs, ref_logprobs, advantages, loss_mask=None, clip_range=0.2, beta=0.1, ): # Policy loss ratio = torch.exp(logprobs - old_logprobs) surr1 = ratio * advantages surr2 = torch.clamp(ratio, 1 - clip_range, 1 + clip_range) * advantages policy_loss = torch.min(surr1, surr2) # kl loss kl_loss = compute_kl_loss(logprobs, ref_logprobs, beta) per_token_loss = policy_loss - beta * kl_loss total_loss = - masked_mean(per_token_loss, loss_mask) return total_loss for step in range(num_steps): # ===== 1. Rollout (Group Sampling) ===== with torch.no_grad(): responses, old_logprobs = model.generate(prompts, num_samples=K) # [B,K,S] ref_logprobs = ref_model.forward(responses).detach() # ===== 2. Reward ===== with torch.no_grad(): rewards = reward_model.get_rewards(prompts, responses) # [B, K] # ===== 3. Advantage (GRPO) ===== advantages = compute_grpo_advantages(rewards) # [B, K] advantages = advantages.unsqueeze(-1) # [B, K, 1] # ===== 4. PPO update ===== for _ in range(ppo_epochs): logprobs = model.forward(responses) loss = grpo_loss( logprobs, old_logprobs, ref_logprobs, advantages, loss_mask=None, ) optimizer.zero_grad() loss.backward() optimizer.step()
DAPO
-
实现
import torch from typing import Dict def group_advantages( rewards: torch.Tensor, eps: float = 1e-6, ) -> torch.Tensor: """ Args: rewards: [B, G] Returns: adv: [B, G] """ mean = rewards.mean(dim=1, keepdim=True) std = rewards.std(dim=1, keepdim=True, unbiased=False) adv = (rewards - mean) / (std + eps) return adv def dynamic_sampling_filter( rewards: torch.Tensor, positive_threshold: float = 0.0, ) -> torch.Tensor: """ rewards: [B, G] 这里默认 reward > positive_threshold 视作 correct。 返回保留哪些 group 的 mask: [B] DAPO 只保留 0 < num_correct < G 的 group """ correct = (rewards > positive_threshold).to(torch.long) # [B, G] num_correct = correct.sum(dim=1) # [B] G = rewards.size(1) keep_group = (num_correct > 0) & (num_correct < G) return keep_group def dapo_loss( batch, eps_low: float = 0.2, eps_high: float = 0.28, advantage_eps: float = 1e-6, token_level: bool = True, ) -> Dict[str, torch.Tensor]: """ DAPO loss: 1) 先做 dynamic sampling group filter 2) 再做 decoupled clipping 3) 默认 token-level reduction """ logprobs = batch.logprobs # [B, G, T] old_logprobs = batch.old_logprobs # [B, G, T] token_mask = batch.token_mask.float() # [B, G, T] rewards = batch.rewards.float() # [B, G] assert logprobs.shape == old_logprobs.shape == token_mask.shape assert rewards.shape[:2] == logprobs.shape[:2] keep_group = dynamic_sampling_filter(rewards) # [B] if keep_group.sum() == 0: # 一个 batch 都没保正时, 返回零损失, 外面应继续采样补 batch zero = logprobs.sum() * 0.0 return { "loss": zero, "pg_loss": zero, "kept_groups": torch.tensor(0, device=logprobs.device), "total_groups": torch.tensor(logprobs.size(0), device=logprobs.device), } logprobs = logprobs[keep_group] # [B', G, T] old_logprobs = old_logprobs[keep_group] token_mask = token_mask[keep_group] rewards = rewards[keep_group] if batch.seq_mask is not None: seq_mask = batch.seq_mask[keep_group].float() # [B', G] else: seq_mask = torch.ones_like(rewards) adv = group_advantages(rewards, eps=advantage_eps) # [B', G] adv = adv * seq_mask # broadcast to token level: [B', G, T] adv_t = adv.unsqueeze(-1) log_ratio = logprobs - old_logprobs ratio = torch.exp(log_ratio) clipped_ratio = torch.clamp( ratio, min=1.0 - eps_low, max=1.0 + eps_high, ) surr1 = ratio * adv_t surr2 = clipped_ratio * adv_t pg_obj = torch.minimum(surr1, surr2) # [B', G, T] pg_obj = pg_obj * token_mask if token_level: # DAPO 更强调 token-level PG loss denom = token_mask.sum().clamp_min(1.0) pg_loss = -pg_obj.sum() / denom else: # 类 GRPO 的 sample-level reduction token_denom = token_mask.sum(dim=-1).clamp_min(1.0) # [B', G] seq_obj = pg_obj.sum(dim=-1) / token_denom # [B', G] seq_denom = seq_mask.sum().clamp_min(1.0) pg_loss = -seq_obj.sum() / seq_denom return { "loss": pg_loss, "pg_loss": pg_loss, "kept_groups": keep_group.sum(), "total_groups": torch.tensor(batch.rewards.size(0), device=batch.rewards.device), }
DPO
-
代码实现
def dpo_loss(chosen_logprobs,rejected_logprobs,ref_chosen_logprobs,ref_rejected_logprobs,beta): chosen = chosen_logprobs - ref_chosen_logprobs rejected = rejected_logprobs - ref_rejected_logprobs loss = -torch.nn.functional.logsigmoid(beta*(chosen-rejected)) return loss.mean() -
DPO 实现
def sequence_logprob(logits, labels, mask): """ logits: [B, T, V] labels: [B, T] mask: [B, T] """ log_probs = F.log_softmax(logits, dim=-1) token_logp = torch.gather( log_probs, dim=-1, index=labels.unsqueeze(-1) ).squeeze(-1) return (token_logp * mask).sum(dim=-1) # [B] def dpo_loss( policy_chosen_logp, # [B] policy_rejected_logp, # [B] ref_chosen_logp, # [B] ref_rejected_logp, # [B] beta=0.1, ): pi_logratios = policy_chosen_logp - policy_rejected_logp ref_logratios = ref_chosen_logp - ref_rejected_logp logits = beta * (pi_logratios - ref_logratios) loss = -F.logsigmoid(logits).mean() return loss, { "reward_margin": logits.mean().item(), "accuracy": (logits > 0).float().mean().item(), } for step in range(num_steps): # ===== 1. batch ===== batch = next(dataloader) prompt = batch["prompt"] chosen = batch["chosen"] rejected = batch["rejected"] # ===== 2. policy forward ===== chosen_logits = actor_model(prompt, chosen) rejected_logits = actor_model(prompt, rejected) # ===== 3. ref forward(no grad)===== with torch.no_grad(): ref_chosen_logits = ref_model(prompt, chosen) ref_rejected_logits = ref_model(prompt, rejected) # ===== 4. logprob ===== chosen_logp = sequence_logprob( chosen_logits, chosen["labels"], chosen["mask"] ) rejected_logp = sequence_logprob( rejected_logits, rejected["labels"], rejected["mask"] ) ref_chosen_logp = sequence_logprob( ref_chosen_logits, chosen["labels"], chosen["mask"] ) ref_rejected_logp = sequence_logprob( ref_rejected_logits, rejected["labels"], rejected["mask"] ) # ===== 5. loss ===== loss, stats = dpo_loss( chosen_logp, rejected_logp, ref_chosen_logp, ref_rejected_logp, ) optimizer.zero_grad() loss.backward() optimizer.step()
这篇文档有帮助吗?