ZeroPoint / docs /ZeroPoint.md
Holmes2011's picture
Portable golden reference (JSON instead of LFS npz); doc links updated
cf9d631 verified
|
Raw History Blame Contribute Delete
14.8 kB

ZeroPoint:分层记忆槽网络

为 zeroOS 设计的序列模型架构。用它搭的语言模型叫 ZeroX,训练框架是 ZeroCG。(原名 ZeroMem。)文档结构参照《Attention Is All You Need》(Vaswani et al., 2017), 每节开头注明对应论文的哪一节,方便对照阅读。

代码:models/zeropoint.py · 自检:tests/test_zeropoint.py · 对照实验:experiments/compare.py → compare.md · 消融实验:experiments/ablation.py → ablation.md · 精确回忆:experiments/recall.py → recall.md · 历次结果存档: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(compare.py 自动生成)。

6.2 模型变体(消融) 〔论文 §6.2,表 3〕

结果见 experiments/ablation.md(ablation.py 自动生成)。

6.3 长度外推 〔论文 §3.5 的猜想〕

论文选正弦位置编码的理由是它"可能可以外推到比训练时更长的序列",但没有给出实验。 我们在 64 字上训练 ZeroPoint 和正弦位置编码的 Transformer,然后在 4,096 字的文本上分段测试 (32 条验证序列 × 3 个种子,两种模型用同一种分块并行算法)。

  • ZeroPoint:64255 字(训练长度的 4 倍)loss 为 1.4742 ± 0.0341,比训练长度内(063: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/。

6.4 精确回忆 〔无结论,不能用来比较架构〕

结果见 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 存在的原因。