返回关卡地图
预判 0 / 2 闯关题:0 / 2 XP 0 / 200

Level 08 | 零基础导学关卡

架构技巧

Architecture Tricks

先通过图解和小例子理解原理,再做预测与练习;不懂的地方可以反复尝试,最后把理解写进官方 Notebook。

本课官方 Notebook ↗
08_Architecture_Tricks.ipynb 模型结构架构技巧Transformer
Mission 1 概念练习

Gemma RMSNorm:从纯归一化开始学习缩放

🎯 先猜一猜

self.weight 初始化为全 0,但 forward 仍执行了 RMS 归一化。

此时 output 最准确的描述是什么?

先猜一个答案,再对照下面的讲解。可以随时修改选择,猜错会得到解释。

先补的知识

  • x 的 shape 是 [batch_size, seq_len, hidden_size];RMSNorm 只沿最后的 hidden_size 维计算均方。
  • variance = x.float().pow(2).mean(-1, keepdim=True) 得到 [B,S,1],再通过广播作用到 [B,S,H]。
  • x_norm 已经是 FP32 的归一化结果;self.weight 是长度 H 的可训练参数,会沿 B、S 两维广播。
  • Gemma 的 weight 初始化为 0,所以真正的缩放因子 1 + weight 初始为 1;return 时必须用 type_as(x) 恢复输入 dtype。

图解原理

标准缩放参数若直接乘 weight,就需要把 weight 初始化为 1。Gemma 换一种参数化:保存从 1 的偏移量,实际乘 (1+w)。当 w=0 时,层先只做 RMS 归一化;训练再学习每个隐藏特征应该从 1 向上或向下调整多少。

x[B,S,H],保留原 dtype x.float()用 FP32 统计平方均值 x_norm每个 token 的 RMS 归一化 1 + weight按隐藏特征缩放

shape 与广播

variance[B,S,1] x_norm[B,S,H],FP32 weight[H],每个隐藏特征一个缩放偏移 output[B,S,H],返回时恢复 x.dtype

初始化时发生什么

weight = 0 1 + weight = 1 output = x_norm

这不是恒等映射 output=x;它仍然做了 RMS 归一化,只是没有额外改变归一化后的特征缩放。

测试期望它证明什么
weight=0out 等于手算的 x_norm正确使用了 1+w
weight 非零out2 与 out 不同可训练缩放真正生效
FP16 输入FP16 输出最后恢复了原 dtype
TODO 只需在已有 x_norm 上应用 Gemma 缩放。不要重新计算 variance,也不要在乘法前把 self.weight 强制改成不可训练数据。

语法热身:从基准亮度 1 学习每个通道的偏移

normalized_pixels = torch.tensor([
    [0.5, -1.0, 0.25],
])
channel_offset = torch.tensor([0.0, 0.2, -0.1])

scale = 1 + channel_offset
adjusted = normalized_pixels * scale
print(scale)    # [1.0, 1.2, 0.9]
print(adjusted.shape)

独立例子如何迁移回 Notebook

  • normalized_pixels 对应已经算好的 x_norm
  • channel_offset 对应可训练的 self.weight
  • adjusted 对应 TODO 要创建的 output
  • Notebook 的 return 已负责 type_as(x),因此 TODO 保持 FP32 运算即可。

巩固一下

为什么 variance 使用 keepdim=True?

学完这一段,试着做

用一个小动作确认自己理解了;最后再进入官方题目。

Mission 2 概念练习

Qwen 权重绑定:让两个模块持有同一个 Parameter

🎯 先猜一猜

两个 [V,H] 参数的数值通过 copy_ 变得完全相同。

仅凭 torch.allclose 为 True,能否证明权重已经绑定?

先猜一个答案,再对照下面的讲解。可以随时修改选择,猜错会得到解释。

先补的知识

  • nn.Embedding(vocab_size, hidden_size) 的 weight shape 是 [V,H];nn.Linear(hidden_size, vocab_size, bias=False) 的 weight 也是 [V,H]。
  • shape 相同只说明可以共享,不代表它们已经共享。两个模块刚创建时默认各有一块独立参数内存。
  • 权重绑定是对象引用赋值,不是复制数值;赋值后两个属性指向同一个 nn.Parameter。
  • 测试用 data_ptr() 验证权重和梯度的物理内存地址,而不只是用 allclose 验证数值相等。

图解原理

Embedding 用矩阵按 token id 查行,LM Head 用同一矩阵把 hidden state 投影回词表。绑定不是每次手动同步两张表,而是给同一张表贴上两个入口标签:从任何入口更新,看到的都是同一份参数与同一份梯度。

为什么 shape 正好匹配

Embedding weight[vocab_size, hidden_size] LM Head weight[vocab_size, hidden_size] 输入方向token id -> 查出 hidden 向量 输出方向hidden 向量 -> vocab logits

复制与绑定不是一回事

复制数值当前数值相同,未来更新仍各走各的 绑定参数两个 weight 属性就是同一个 Parameter 测试证据weight.data_ptr() 相同,grad.data_ptr() 也相同
创建 Embedding得到参数 [V,H] 创建 bias=False Linear得到兼容的 [V,H] 重新指向LM Head 使用 Embedding 参数 backward两条路径的梯度汇入同一 weight.grad
权重指针相同 修改 Embedding 后 Head 同步 两端都有 grad 梯度指针相同
不要写 lm_head.weight.data = embed_tokens.weight.data.clone()。clone 明确创建了新内存,无法通过指针共享测试。

语法热身:让两个模块共享同一个可训练温度参数

class SharedTemperature(nn.Module):
    def __init__(self):
        super().__init__()
        self.encoder_scale = nn.Parameter(torch.ones(4))
        self.decoder_scale = self.encoder_scale

module = SharedTemperature()
assert module.encoder_scale.data_ptr() == \
       module.decoder_scale.data_ptr()

独立例子如何迁移回 Notebook

  • encoder_scale 对应 self.embed_tokens.weight
  • decoder_scale 对应 self.lm_head.weight
  • TODO 位于 __init__:让后者属性直接引用前者的 Parameter。
  • 完成后删掉占位的 raise NotImplementedError,否则构造对象时仍会提前中断。

巩固一下

绑定后,为什么 Embedding 路径与 LM Head 路径的梯度会出现在同一个 weight.grad 中?

学完这一段,试着做

用一个小动作确认自己理解了;最后再进入官方题目。

把理解变成自己的代码

准备好,去官方题目试一试

在本页用小例子建立直觉,再去官方 Notebook 完成实现。先运行你自己的测试,遇到困难时再查看官方提示与参考答案。

前往本课官方 Notebook ↗

这节课会遇到的代码对象

summarize_attention_variant · GemmaRMSNorm · forward · QwenTieEmbeddings · forward_embed · forward_lm_head

先找题目里的输入、输出与 TODO,再把本课的手算过程对应进去;以官方题目中的函数说明和测试为准。

练习来源:Datawhale 官方仓库 · 518cc45。这里的入口直接打开官方版本,不读取或分享你的本地 Notebook。

完成 2 道闯关题后,本关即算完成;作业 checklist 用来辅助你回 notebook 练习。
进度只保存在当前浏览器 localStorage,分享 HTML 不会带走你的记录。
上一关:L07 下一关:L09