File size: 14,791 Bytes
5c6c847 cf9d631 5c6c847 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 | # ZeroPoint:分层记忆槽网络
> 为 zeroOS 设计的序列模型架构。用它搭的语言模型叫 **ZeroX**,训练框架是 **ZeroCG**。(原名 ZeroMem。)文档结构参照《Attention Is All You Need》(Vaswani et al., 2017),
> 每节开头注明对应论文的哪一节,方便对照阅读。
>
> 代码:[`models/zeropoint.py`](../models/zeropoint.py) ·
> 自检:[`tests/test_zeropoint.py`](../tests/test_zeropoint.py) ·
> 对照实验:[`experiments/compare.py`](../experiments/compare.py) → [`compare.md`](../experiments/compare.md) ·
> 消融实验:[`experiments/ablation.py`](../experiments/ablation.py) → [`ablation.md`](../experiments/ablation.md) ·
> 精确回忆:[`experiments/recall.py`](../experiments/recall.py) → [`recall.md`](../experiments/recall.md) ·
> 历次结果存档:[`experiments/results/`](../experiments/results/)
>
> 状态:**v0(研究原型)**。
---
## 摘要
Transformer 用自注意力取代了循环,换来了训练时的完全并行;代价是推理时每生成一个字都要回看之前所有的字,
KV 缓存随文本长度线性增长。这对 GPU 服务器不是问题,对**没有 GPU、内存按页精打细算的操作系统内核**却是。
我们提出 **ZeroPoint**:用一组**带不同遗忘速度的记忆槽**取代自注意力。每读一个字,模型决定把它**写进**哪个槽、
从哪些槽**读出**;槽的遗忘速度按"寄存器 → 缓存 → 内存"的存储层级初始化,并在训练中学习。
ZeroPoint 有两种数学上等价的算法:训练时用**分块并行**(块长 32,每块一次矩阵乘,块间串行传递状态),
推理时**逐字递推**,状态大小固定,与文本长度无关,也不需要位置编码。
在 **115,240** 参数、1,558,395 字符的字符级语言建模上,ZeroPoint 与同规模 Transformer 的验证 loss
在 2× 种子标准差内无显著差异(1.5214 ± 0.0134 vs 1.5452 ± 0.0100,差 0.0238,判定线 0.0268;**不声称更好**)。
推理状态是后者在第 1024 个字时的 **1/128**(2,048 vs 262,144 个浮点数),
4 倍训练长度的文本上 loss 升高 0.037 nats/字符(64 倍时为 0.219)。
## 1 引言 〔论文 §1〕
论文 §1 的出发点是:循环网络"天生是串行的",第 t 步必须等第 t−1 步算完,没法在一条样本内并行。
自注意力把任意两个位置之间的路径缩短到 O(1),训练可以完全并行。
但论文没有讨论的另一面是**推理**:生成第 t 个字时,自注意力要和前面 t−1 个字的 Key/Value 逐一比较。
这些 Key/Value 必须一直存着(KV 缓存)。对 zeroOS 来说:
| | GPU 服务器 | zeroOS 内核 |
|---|---|---|
| 内存 | 几十 GB 显存 | 内核堆,按 4KB 页分配 |
| 算力 | 上万个并行单元 | 一个 CPU 核,x87 浮点 |
| 运行方式 | 一次处理一大批 | 用户敲一个字处理一个字 |
**我们想要的是两者的好处:训练几乎像 Transformer 一样并行(分块并行,串行步数是长度的 1/32),推理像 RNN 一样只带一个固定大小的状态。**
## 2 背景 〔论文 §2〕
论文 §2 提到 End-to-End Memory Networks [34] 用"循环的注意力"读外部记忆。ZeroPoint 和这条线以及
后来的"线性注意力 / 带衰减的循环模型"家族是近亲,不是凭空出现的:
| 已有工作 | 相同点 | ZeroPoint 的不同 |
|---|---|---|
| 记忆网络 / 神经图灵机 | 软寻址读写记忆槽 | 不需要逐步串行训练;每个槽有自己的遗忘速度 |
| 线性注意力(Katharopoulos 2020) | 并行 / 递推双形式 | 状态是 M 个可寻址的槽,不是 D×D 的外积矩阵 |
| RetNet(Sun 2023) | 衰减 λ^(t−u) 实现双形式、不需要位置编码 | λ 可学且按存储层级初始化;写入时 softmax 竞争选槽 |
| Mamba / 状态空间模型 | 递推推理、状态固定 | 不需要专门的扫描算子,一次矩阵乘就能并行 |
真正属于 ZeroPoint 的是这个组合:**多时间尺度的可学习遗忘 + 写时竞争选槽 + 写强度门 + 一次矩阵乘的并行形式**,
以及"为一个没有 GPU 的内核设计"这个出发点。
## 3 模型结构 〔论文 §3〕
### 3.1 层的堆叠 〔论文 §3.1〕
和论文的解码器一样:N 个相同的层堆叠,每层两个子层——**记忆混合层**(代替多头自注意力)和**逐位置前馈网络**,
每个子层外面套残差连接和 LayerNorm。
两处和论文不同:
- **LayerNorm 放在子层前面(Pre-LN)**:`x + Sublayer(LayerNorm(x))`。论文用的是 Post-LN
`LayerNorm(x + Sublayer(x))`,后来的 GPT-2 等工作发现 Pre-LN 训练更稳,不太依赖学习率预热。
对照组 Transformer 用的也是 Pre-LN,两边一致。
- **只有解码器(自回归)**。论文是做翻译的编码器-解码器;zeroOS 的任务(续写、意图识别)用不上编码器。
### 3.2 记忆混合层 〔论文 §3.2 注意力〕
#### 3.2.1 读与写 〔对应 §3.2.1 缩放点积注意力〕
论文的注意力是"拿查询去和所有的键比"。ZeroPoint 反过来:**每个字在写入时就决定自己存到哪里**,
读的时候只和 M 个槽打交道,不和之前的字直接打交道。
设第 t 个字的向量为 `x_t`,记忆槽为 `S[1..M]`(每个是 D 维向量),保留率为 `λ[1..M]`:
```
写地址 w_t = softmax(x_t · Ww) × sigmoid(x_t · Wg) 写到哪些槽 × 写多重
写入值 v_t = x_t · Wv
更新 S[m] ← λ[m] · S[m] + w_t[m] · v_t 所有槽先按自己的速度遗忘,再写入
读地址 a_t = softmax(x_t · Wr) 从哪些槽读
读出 r_t = Σ_m a_t[m] · S[m]
输出 LayerNorm(r_t) · Wo
```
- **写地址用 softmax(槽之间竞争)**:一个字要么写进短期槽,要么写进长期槽,模型得决定"这个字值不值得长期记住"。
- **写强度门**:空格、换行这类没信息量的字可以几乎不写,不冲掉已有的记忆。
- **读出后做 LayerNorm**:长期槽可能累积了几十个字,短期槽只有一两个,量级差别很大。
这和论文 §3.2.1 除以 √d_k 的动机相同——别让数值落到 softmax / 后续层不好处理的区间。
- **槽的初始状态 S0 可学习**(不是全 0):否则序列开头几个字读出来的向量接近 0,
后面的 LayerNorm 会把噪声放大。
#### 3.2.2 多头 〔对应 §3.2.2 多头注意力〕
和论文一样分 H 个头,每头 D = d/H 维,各自有一套读写地址和 M 个槽,最后拼起来乘 Wo。
论文的理由同样适用:单头会把不同的"记什么"平均到一起。
#### 3.2.3 保留率与存储层级 〔ZeroPoint 独有〕
```
λ = 0.5 + 0.5 · sigmoid(θ) λ ∈ (0.5, 1),θ 可学
```
初始化时,每头的 M 个槽的**半衰期**(旧内容衰减到一半要经过几个字)按等比数列从 1.5 铺到 64:
```
槽 0 λ ≈ 0.63 半衰期 1.5 个字 ← 像寄存器:记"刚才那个字"
槽 1~14 λ 逐渐变大 ← 像缓存:记"这个词""这一句"
槽 15 λ ≈ 0.989 半衰期 64 个字 ← 像内存:记"这一段在讲什么"
```
论文 §3.5 的正弦位置编码"波长从 2π 到 10000·2π 成等比数列",ZeroPoint 的半衰期等比铺开是同一个思路:
用一组不同尺度的分量覆盖从很近到很远的距离。区别是论文把尺度编码进**输入**,ZeroPoint 把尺度编码进**记忆本身**。
### 3.3 逐位置前馈网络 〔论文 §3.3〕
与论文相同的结构 `FFN(x) = act(x·W1 + b1)·W2 + b2`,内层宽度 4d(论文 512 → 2048 也是 4 倍)。
激活函数用 GELU(论文是 ReLU;GPT 系列之后普遍用 GELU),对照组也用 GELU。
### 3.4 嵌入与输出 〔论文 §3.4〕
可学习的字嵌入,输出前 LayerNorm + 线性层 + softmax。没有采用论文的"输入/输出嵌入共享权重",对照组也没有。
### 3.5 位置信息 〔论文 §3.5〕
论文说:"模型里没有循环也没有卷积,所以必须注入位置信息。"
**ZeroPoint 不需要位置编码。** 它本质上是一个(可并行的)循环结构:第 t 个字读到的内容里,
u 个字之前写入的信息被乘上了 λ^(t−u)。不同的槽衰减速度不同,所以"某条信息在哪些槽里还剩多少"
本身就告诉了模型它是多久以前的。消融实验 (G) 专门检验了这一点。
## 4 为什么用记忆槽 〔论文 §4,表 1〕
论文表 1 比较了三个指标:每层计算量、最少串行步数、任意两位置之间的最长路径。
我们加上对内核最重要的两列:推理时每个字的计算量、推理时要保存的状态。
n = 序列长度,d = 宽度,M = 每头槽数(实验里 M = 16,d = 64)。
| 层类型 | 每层计算量(训练) | 串行步数 | 最长路径 | 推理:每字计算 | 推理:状态大小 |
|---|---|---|---|---|---|
| 自注意力 | O(n²·d) | O(1) | O(1) | O(n·d),越往后越慢 | O(n·d),随长度增长 |
| 循环网络 | O(n·d²) | O(n) | O(n) | O(d²) | O(d) |
| **ZeroPoint** | O(n·c·(M+d))(分块并行,c = 32) | O(n/c) | **O(1)**,但信号按 λ^(t−u) 衰减 | **O(d²)** | **O(M·d),固定** |
关键在于 ZeroPoint 有两种等价算法:
**并行形式(训练用)**。把递推展开:
```
r_t = Σ_{u ≤ t} A[t,u] · v_u, A[t,u] = Σ_m a_t[m] · w_u[m] · λ[m]^(t−u)
```
利用 `λ^(t−u) = λ^t · λ^(−u)`:
```
A = ( a ⊙ λ^t ) @ ( w ⊙ λ^(−u) )ᵀ 再把 u > t 的部分清零(因果)
```
只要一次矩阵乘,和注意力的 `Q·Kᵀ` 同样能整段并行。但 `λ^(−u)` 最大是 2^u,float32 到 u ≈ 120 就溢出。
所以实际用的是**分块并行**:序列切成每块 c = 32 个字,进入一块时槽的状态是 `S_in`:
```
块内 r_i = Σ_{j ≤ i} A[i,j] · v_j A 同上,但 i、j 是块内下标,λ^(−j) 最大 2^31
跨块 r_i += (a_i ⊙ λ^(i+1)) @ S_in
块尾 S_out = λ^c ⊙ S_in + (w ⊙ λ^(c−1−j))ᵀ @ v
```
块内是矩阵乘,块和块之间只传一个 M × d 的状态。训练时每层计算量 O(n·c·(M+d)),
**序列多长都行**(自检里 float32 跑 1000 个字不溢出,和递推形式一致)。
**递推形式(推理用)**。逐字更新 `S`,每层只存 M·d 个浮点数,跟文本多长无关。
两种形式结果一致:自检在**所有 64 种开关组合**下、300 个字(跨多个块)上逐位比对,float64 误差 < 1e-8。
另有一份固定的参考数据 `tests/golden/zeropoint_ref.json`,以后任何实现(包括内核里的 C 版本)都拿它对答案。
**训练好的模型可以直接用递推形式放进内核**:不用 KV 缓存,每层固定一块 M × d 的状态,不需要动态分配。
关于"最长路径 O(1)"要说实话:u 处写入的信息确实一步就能被 t 处读到,但已经乘上了 λ^(t−u)。
注意力能"原样找回"任意远处的某个字,ZeroPoint 只能找回**还没忘干净、而且没被其他写入冲淡**的信息。
## 5 训练 〔论文 §5〕
| | 论文 | ZeroPoint 实验 |
|---|---|---|
| 数据 | WMT 2014 英德,450 万句对,BPE 37000 | Python 标准库源码 150 万字符,字符级,词表 96 |
| 批次 | 约 25000 个词元 | 32 × 64 = 2048 个字符 |
| 硬件 | 8 块 P100 | 一台 Mac 的 CPU(numpy,自研框架 zerocg) |
| 优化器 | Adam β=(0.9, 0.98),公式 3(预热 4000 步后按 1/√步数衰减) | AdamW β=(0.9, 0.999),权重衰减 0.01;预热 100 步后余弦退火到 10% |
| 正则化 | 残差 dropout 0.1、标签平滑 0.1 | 无(模型小、训练步数少,没观察到过拟合) |
| 步数 | 10 万步(12 小时) | 2000 步(每个模型约 2 分钟) |
ZeroPoint 和对照组用**完全相同**的训练设置、相同的随机种子(决定初始化和数据顺序)。
## 6 结果 〔论文 §6〕
### 6.1 对照实验 〔论文 §6.1,表 2〕
> 结果见 [`experiments/compare.md`](../experiments/compare.md)(`compare.py` 自动生成)。
### 6.2 模型变体(消融) 〔论文 §6.2,表 3〕
> 结果见 [`experiments/ablation.md`](../experiments/ablation.md)(`ablation.py` 自动生成)。
### 6.3 长度外推 〔论文 §3.5 的猜想〕
论文选正弦位置编码的理由是它"**可能**可以外推到比训练时更长的序列",但没有给出实验。
我们在 64 字上训练 ZeroPoint 和正弦位置编码的 Transformer,然后在 4,096 字的文本上分段测试
(32 条验证序列 × 3 个种子,两种模型用同一种分块并行算法)。
- ZeroPoint:64~255 字(训练长度的 4 倍)loss 为 1.4742 ± 0.0341,比训练长度内(0~63:1.4372)高 **+0.037**;
1024~4095 字(64 倍)为 1.6932 ± 0.0305,高 **+0.219 ± 0.063**。**外推会变差,但没有崩掉。**
- 正弦位置编码的 Transformer:出了训练长度就升到约 4.64(≈ ln 96 = 4.56,即随机水平)。
这是 8 条序列 × 3 个种子的测量,基线权重未保留,无法用 32 条序列复测。
> 早期 8 条序列的结果(1.5550 / 1.6164 / 1.6410 / 1.7040)抽样噪声大,已由上面的 32 条序列结果取代。
> 原始数据见 [`experiments/results/`](../experiments/results/)。
### 6.4 精确回忆 〔无结论,不能用来比较架构〕
> 结果见 [`experiments/recall.md`](../experiments/recall.md)。
`recall.py` 的训练配置(恒定 lr 3e-3)把两个架构都困在"两个候选值各猜一半"的局部解(loss ≈ ln 2)。
诊断显示换成 1e-3 余弦退火后,只训 N=2 的 Transformer 在 8,000 步内到 100%。在按修正后的学习率重跑之前,
这张 20,000 步的表只能说明训练配置有问题,**不能说明 ZeroPoint 或 Transformer 谁更擅长精确回忆**。
## 7 结论与局限 〔论文 §7〕
**局限**(都是实话):
- **规模很小**:只在约 11.5 万参数、150 万字符、单一语料上测过,结论不能外推到大模型。
- **精确回忆**:只有 M 个槽,信息会被压缩、混合。**推测**(未验证)它不擅长原样复制很久以前的内容;现有 recall 实验因学习率配置问题无结论。
- **没有编码器-解码器版本**:论文的核心应用是翻译;ZeroPoint 目前只做了解码器。
**下一步**:
1. 如果精确回忆确实弱:加一层"滑动窗口注意力"(只看最近 32 个字,状态仍然固定)做混合结构;
2. 在 zeroOS 的意图识别任务上训练;
3. 用 C 实现递推形式放进内核(x87 浮点)——这是 ZeroPoint 存在的原因。
|