ZeroPoint v0 research prototype: new from-scratch NumPy framework, memory-slot architecture, audited Transformer comparison
5c6c847 verified Download models/zeropoint.py from Holmes2011/ZeroPoint: direct link, hf CLI and curl.
- Browser
- Download file 11 kB
-
https://huggingface.co/Holmes2011/ZeroPoint/resolve/main/models/zeropoint.py
- Command line
-
hf download hf://Holmes2011/ZeroPoint/models/zeropoint.py
-
curl -L -o zeropoint.py https://huggingface.co/Holmes2011/ZeroPoint/resolve/main/models/zeropoint.py
11 kB
| """ZeroPoint:分层记忆槽网络。 | |
| 设计文档见 docs/ZeroPoint.md。核心是 MemoryMixer(代替 Transformer 里的自注意力): | |
| 每层有 H 个头,每个头有 M 个记忆槽 S[m](D 维向量),每个槽有自己的保留率 λ[m]。 | |
| 读到第 t 个字时: | |
| w_t = softmax(x_t Ww) * sigmoid(x_t Wg) 写地址(槽之间竞争)× 写强度 | |
| S[m] ← λ[m] · S[m] + w_t[m] · v_t 所有槽先按自己的速度遗忘,再写入 | |
| a_t = softmax(x_t Wr) 读地址 | |
| r_t = Σ_m a_t[m] · S[m] 从槽里读出 | |
| 两种等价算法: | |
| forward() 分块并行,训练用。序列切成每块 CHUNK 个字: | |
| 块内 r = (A ⊙ 因果掩码) @ v,A[i,j] = Σ_m a_i[m] w_j[m] λ[m]^(i-j) | |
| 利用 λ^(i-j) = λ^i · λ^(-j),一次矩阵乘 | |
| 跨块 r += (a ⊙ λ^(i+1)) @ S_in S_in = 进入这一块时的槽状态 | |
| 块尾 S_out = λ^c ⊙ S_in + (w ⊙ λ^(c-1-j))ᵀ @ v | |
| λ^(-j) 最大 2^(CHUNK-1),不会溢出,所以序列多长都行 | |
| step() 递推形式,推理用:每个字 O(H·M·D),状态大小固定,没有 KV 缓存 | |
| 槽的初始状态 S0 是可学习参数(不是全 0),这样序列开头读出来的向量不会接近 0、被 LayerNorm 放大噪声。 | |
| ema=True(指数滑动平均写入):S[m] ← λ[m]·S[m] + (1-λ[m])·w_t[m]·v_t | |
| 槽的值永远落在"写进去过的值"的范围里,不会随序列变长而越积越大。 | |
| 系统直觉模型要连续跑几天,用这个;实现上只是把写地址 w 乘上 (1-λ),并行和递推两种形式都不用改。 | |
| """ | |
| import numpy as np | |
| from zerocg import Tensor, concat | |
| from zerocg import functional as F | |
| from zerocg import nn | |
| from .transformer import sinusoidal_encoding | |
| LAMBDA_MIN = 0.5 # λ = 0.5 + 0.5·sigmoid(θ) ∈ (0.5, 1) | |
| CHUNK = 32 # 分块并行的块长:块内 λ^(-j) 最大 2^31,float32 绰绰有余 | |
| def _layernorm_np(x, ln): | |
| mu = x.mean(-1, keepdims=True) | |
| var = ((x - mu) ** 2).mean(-1, keepdims=True) | |
| return (x - mu) / np.sqrt(var + ln.eps) * ln.weight.data + ln.bias.data | |
| def _softmax_np(x): | |
| e = np.exp(x - x.max(-1, keepdims=True)) | |
| return e / e.sum(-1, keepdims=True) | |
| def _gelu_np(a): | |
| return 0.5 * a * (1 + np.tanh(np.sqrt(2 / np.pi) * (a + 0.044715 * a ** 3))) | |
| class MemoryMixer(nn.Module): | |
| """参数里的开关只给消融实验用(experiments/ablation.py),默认值就是 ZeroPoint 本身: | |
| gate 写强度门 | |
| write "softmax":写入时槽之间竞争;"sigmoid":每个槽独立决定写不写 | |
| init "hier":半衰期按存储层级铺开;"uniform":所有槽一样 | |
| learn_lambda 保留率 λ 是否可学 | |
| ema 写入按 (1-λ) 缩放(指数滑动平均),长时间运行不漂移 | |
| """ | |
| def __init__(self, n_embd, n_head, n_slot, max_half_life=64.0, | |
| gate=True, write="softmax", init="hier", learn_lambda=True, ema=False): | |
| assert n_embd % n_head == 0 and write in ("softmax", "sigmoid") and init in ("hier", "uniform") | |
| self.H, self.M, self.D = n_head, n_slot, n_embd // n_head | |
| self.write_mode = write | |
| self.ema = ema | |
| self.read = nn.Linear(n_embd, n_head * n_slot) | |
| self.write = nn.Linear(n_embd, n_head * n_slot) | |
| self.gate = nn.Linear(n_embd, n_head) if gate else None | |
| self.value = nn.Linear(n_embd, n_embd) | |
| self.out = nn.Linear(n_embd, n_embd) | |
| self.norm = nn.LayerNorm(self.D) # 每个头读出来的向量各自归一化(不同槽的量级差别很大) | |
| # 初始化成"存储层级":槽 0 的半衰期约 1.5 个字(像寄存器),最后一个约 max_half_life 个字(像内存) | |
| if init == "hier": | |
| half_life = np.geomspace(1.5, max_half_life, n_slot) | |
| else: # 对照:所有槽都取层级的几何中点 | |
| half_life = np.full(n_slot, np.sqrt(1.5 * max_half_life)) | |
| lam = 0.5 ** (1 / half_life) | |
| theta = np.tile(np.log((2 * lam - 1) / (2 - 2 * lam)), (n_head, 1)) # sigmoid 的反函数,(H, M) | |
| self.theta = nn.Parameter(theta) if learn_lambda else Tensor(theta) | |
| self.s0 = nn.Parameter(np.random.randn(n_head, n_slot, self.D) * 0.1) # 可学习的初始状态 | |
| self.usage = None # 最近一次 forward 的 (读地址, 写地址) 平均值,各 (H, M),用来查槽有没有塌缩 | |
| def retention(self): | |
| """每个槽的保留率 λ,形状 (H, M)。""" | |
| return F.sigmoid(self.theta) * (1 - LAMBDA_MIN) + LAMBDA_MIN | |
| def forward(self, x): | |
| B, T, C = x.shape | |
| H, M, D = self.H, self.M, self.D | |
| a = F.softmax(self.read(x).reshape(B, T, H, M), axis=-1).transpose(0, 2, 1, 3) # (B,H,T,M) | |
| w = self.write(x).reshape(B, T, H, M) | |
| w = F.softmax(w, axis=-1) if self.write_mode == "softmax" else F.sigmoid(w) | |
| if self.gate is not None: | |
| w = w * F.sigmoid(self.gate(x)).reshape(B, T, H, 1) | |
| w = w.transpose(0, 2, 1, 3) # (B,H,T,M) | |
| v = self.value(x).reshape(B, T, H, D).transpose(0, 2, 1, 3) # (B,H,T,D) | |
| self.usage = (a.data.mean(axis=(0, 2)), w.data.mean(axis=(0, 2))) | |
| lam = self.retention() # (H, M) | |
| if self.ema: | |
| w = w * (1 - lam).reshape(1, H, 1, M) | |
| log_lam = lam.log().reshape(H, 1, M) | |
| S = self.s0.reshape(1, H, M, D) # 进入第一块时的状态(对 batch 广播) | |
| outs = [] | |
| for s in range(0, T, CHUNK): | |
| e = min(s + CHUNK, T) | |
| c = e - s | |
| ac, wc, vc = a[:, :, s:e], w[:, :, s:e], v[:, :, s:e] | |
| i = Tensor(np.arange(c, dtype=np.float64).reshape(1, c, 1)) | |
| decay = (log_lam * i).exp() # λ^i, (H,c,M) | |
| A = (ac * decay) @ (wc * (log_lam * -i).exp()).transpose(0, 1, 3, 2) # (B,H,c,c) | |
| A = A.masked_fill(np.triu(np.ones((c, c), dtype=bool), 1), 0.0) | |
| outs.append(A @ vc + (ac * decay * lam.reshape(H, 1, M)) @ S) # 块内 + 跨块 | |
| if e < T: | |
| tail = (log_lam * Tensor(np.arange(c - 1, -1, -1, dtype=np.float64).reshape(1, c, 1))).exp() | |
| S = (lam ** c).reshape(1, H, M, 1) * S + (wc * tail).transpose(0, 1, 3, 2) @ vc | |
| r = outs[0] if len(outs) == 1 else concat(outs, axis=2) | |
| return self.out(self.norm(r).transpose(0, 2, 1, 3).reshape(B, T, C)) | |
| # ---------- 递推形式(numpy,无梯度)---------- | |
| def init_state(self, B): | |
| return np.broadcast_to(self.s0.data, (B, self.H, self.M, self.D)).copy() | |
| def step(self, x, S): | |
| """x: (B, C) 一个字的输入;S: (B, H, M, D) 记忆槽。返回 (输出, 新状态)。""" | |
| B = x.shape[0] | |
| H, M, D = self.H, self.M, self.D | |
| lin = lambda layer, z: z @ layer.weight.data + layer.bias.data # noqa: E731 | |
| a = _softmax_np(lin(self.read, x).reshape(B, H, M)) | |
| w = lin(self.write, x).reshape(B, H, M) | |
| w = _softmax_np(w) if self.write_mode == "softmax" else F.sigmoid_np(w) | |
| if self.gate is not None: | |
| w = w * F.sigmoid_np(lin(self.gate, x))[:, :, None] | |
| v = lin(self.value, x).reshape(B, H, D) | |
| lam = self.retention().data | |
| if self.ema: | |
| w = w * (1 - lam)[None] | |
| self.last_addr = (a, w) # 诊断用:这一步的读地址和写地址,(B, H, M) | |
| S = lam[None, :, :, None] * S + w[..., None] * v[:, :, None, :] | |
| r = np.einsum("bhm,bhmd->bhd", a, S) | |
| r = _layernorm_np(r, self.norm).reshape(B, H * D) | |
| return lin(self.out, r), S | |
| class Block(nn.Module): | |
| def __init__(self, n_embd, n_head, n_slot, dropout=0.0, mlp_hidden=None, **mixer_opts): | |
| hidden = mlp_hidden or 4 * n_embd | |
| self.ln1 = nn.LayerNorm(n_embd) | |
| self.mix = MemoryMixer(n_embd, n_head, n_slot, **mixer_opts) | |
| self.ln2 = nn.LayerNorm(n_embd) | |
| self.fc1 = nn.Linear(n_embd, hidden) | |
| self.fc2 = nn.Linear(hidden, n_embd) | |
| self.drop = nn.Dropout(dropout) | |
| def forward(self, x): | |
| x = x + self.drop(self.mix(self.ln1(x))) | |
| return x + self.drop(self.fc2(F.gelu(self.fc1(self.ln2(x))))) | |
| def step(self, x, S): | |
| h, S = self.mix.step(_layernorm_np(x, self.ln1), S) | |
| x = x + h | |
| h = _gelu_np(_layernorm_np(x, self.ln2) @ self.fc1.weight.data + self.fc1.bias.data) | |
| return x + h @ self.fc2.weight.data + self.fc2.bias.data, S | |
| class ZeroX(nn.Module): | |
| """字符级语言模型。默认没有位置编码:位置信息来自各个槽不同的遗忘速度。 | |
| pos="sin" 只给消融实验用:额外加上论文的正弦位置编码,看看还有没有用。 | |
| mlp_hidden 默认 4 × n_embd;消融实验用它把各变体的参数量对齐。""" | |
| def __init__(self, vocab_size, n_embd=64, n_head=4, n_slot=16, n_layer=2, dropout=0.0, | |
| pos="none", mlp_hidden=None, **mixer_opts): | |
| assert pos in ("none", "sin") | |
| self.pos_kind = pos | |
| self.tok = nn.Embedding(vocab_size, n_embd) | |
| self.blocks = [Block(n_embd, n_head, n_slot, dropout, mlp_hidden, **mixer_opts) for _ in range(n_layer)] | |
| self.ln_f = nn.LayerNorm(n_embd) | |
| self.head = nn.Linear(n_embd, vocab_size) | |
| def forward(self, idx): | |
| x = self.tok(idx) | |
| if self.pos_kind == "sin": | |
| x = x + Tensor(sinusoidal_encoding(idx.shape[1], x.shape[-1])) | |
| for b in self.blocks: | |
| x = b(x) | |
| return self.head(self.ln_f(x)) | |
| def init_state(self, B): | |
| return [b.mix.init_state(B) for b in self.blocks] | |
| def step(self, idx_t, states, t=0): | |
| """idx_t: (B,) 第 t 个字。返回 (logits (B, V), 新状态)。""" | |
| x = self.tok.weight.data[idx_t] | |
| if self.pos_kind == "sin": | |
| x = x + sinusoidal_encoding(t + 1, x.shape[-1])[t] | |
| new = [] | |
| for b, S in zip(self.blocks, states): | |
| x, S = b.step(x, S) | |
| new.append(S) | |
| return _layernorm_np(x, self.ln_f) @ self.head.weight.data + self.head.bias.data, new | |
| def forward_recurrent(self, idx): | |
| """逐字递推跑完整个序列,返回 (B, T, V) 的 logits。""" | |
| idx = np.asarray(idx) | |
| states = self.init_state(idx.shape[0]) | |
| out = [] | |
| for t in range(idx.shape[1]): | |
| logits, states = self.step(idx[:, t], states, t) | |
| out.append(logits) | |
| return np.stack(out, axis=1) | |
| def state_floats(self): | |
| """推理时需要保存的状态大小(浮点数个数),和序列长度无关。""" | |
| return sum(b.mix.H * b.mix.M * b.mix.D for b in self.blocks) | |