fluidlayer 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fluidlayer/__init__.py +96 -0
- fluidlayer/autodiff.py +472 -0
- fluidlayer/blocks.py +275 -0
- fluidlayer/bpe.py +173 -0
- fluidlayer/data.py +92 -0
- fluidlayer/diffusion.py +264 -0
- fluidlayer/gpt2_model.py +129 -0
- fluidlayer/gpt2_tokenizer.py +127 -0
- fluidlayer/graft.py +741 -0
- fluidlayer/losses.py +86 -0
- fluidlayer/memory.py +525 -0
- fluidlayer/model.py +141 -0
- fluidlayer/optim.py +57 -0
- fluidlayer/py.typed +0 -0
- fluidlayer/sheaf.py +147 -0
- fluidlayer/topology.py +294 -0
- fluidlayer/torch_graft.py +283 -0
- fluidlayer-2.0.0.dist-info/METADATA +376 -0
- fluidlayer-2.0.0.dist-info/RECORD +22 -0
- fluidlayer-2.0.0.dist-info/WHEEL +5 -0
- fluidlayer-2.0.0.dist-info/licenses/LICENSE +21 -0
- fluidlayer-2.0.0.dist-info/top_level.txt +1 -0
fluidlayer/diffusion.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fluidlayer.diffusion — the "freezing" operator.
|
|
3
|
+
|
|
4
|
+
Everything needed to run the fluid in a forward pass, in closed form:
|
|
5
|
+
|
|
6
|
+
* power iteration for lambda_max
|
|
7
|
+
* Chebyshev expansion of exp(-t L) (DCT-I interpolation, numerically stable)
|
|
8
|
+
* a multi-scale BAND filter bank (the fix for the rank-c trap)
|
|
9
|
+
* spectral gap lambda_2 (the "critical temperature")
|
|
10
|
+
* predicted jamming time t_jam (= log(1/eps) / lambda_2)
|
|
11
|
+
|
|
12
|
+
Cost model (per layer, per token):
|
|
13
|
+
* m Laplacian matvecs for ALL bands combined (the Chebyshev basis is shared)
|
|
14
|
+
* B x (m+1) coefficient combinations afterwards (free)
|
|
15
|
+
* so the fluid costs m matvecs, NOT m*B matvecs.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import math
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"power_iteration_lmax", "power_iteration_lmax_autodiff", "cheb_coeffs_exp",
|
|
26
|
+
"cheb_basis", "apply_bands", "band_energies", "spectral_gap", "jam_time",
|
|
27
|
+
"cheb_degree", "exact_exp_action", "spectral_summary", "band_times",
|
|
28
|
+
"cheb_coeffs_exp_autodiff", "spectral_bound_autodiff",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def power_iteration_lmax_autodiff(L, iters: int = 15, seed: int = 0):
|
|
33
|
+
"""Differentiable lambda_max estimate (power iteration, no stop-gradient).
|
|
34
|
+
|
|
35
|
+
The spectral scale enters the Chebyshev recurrence as 1/lambda_max, so
|
|
36
|
+
treating it as a constant silently drops a large part of the gradient
|
|
37
|
+
w.r.t. the restriction maps. Keeping it in the graph costs a handful of
|
|
38
|
+
extra mat-vecs and makes the gradient exact.
|
|
39
|
+
"""
|
|
40
|
+
from .autodiff import Tensor
|
|
41
|
+
n = L.shape[0]
|
|
42
|
+
rng = np.random.default_rng(seed)
|
|
43
|
+
v = Tensor(rng.standard_normal(n))
|
|
44
|
+
for _ in range(iters):
|
|
45
|
+
w = L @ v
|
|
46
|
+
nrm = (w * w).sum().sqrt()
|
|
47
|
+
if float(nrm.data) < 1e-12:
|
|
48
|
+
break
|
|
49
|
+
v = w / nrm
|
|
50
|
+
lam = (v * (L @ v)).sum() # Rayleigh quotient v^T L v (1-D safe)
|
|
51
|
+
return lam
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# --------------------------------------------------------------------- norms
|
|
55
|
+
def power_iteration_lmax(L: np.ndarray, iters: int = 60, tol: float = 1e-10) -> float:
|
|
56
|
+
"""Largest eigenvalue of a symmetric PSD matrix (power iteration)."""
|
|
57
|
+
n = L.shape[0]
|
|
58
|
+
if n == 0:
|
|
59
|
+
return 0.0
|
|
60
|
+
v = np.random.default_rng(0).standard_normal(n)
|
|
61
|
+
v /= np.linalg.norm(v) + 1e-16
|
|
62
|
+
lam = 0.0
|
|
63
|
+
for _ in range(iters):
|
|
64
|
+
w = L @ v
|
|
65
|
+
nw = np.linalg.norm(w)
|
|
66
|
+
if nw < 1e-16:
|
|
67
|
+
return 0.0
|
|
68
|
+
v_new = w / nw
|
|
69
|
+
lam_new = float(v_new @ (L @ v_new))
|
|
70
|
+
if abs(lam_new - lam) < tol * max(1.0, abs(lam_new)):
|
|
71
|
+
lam = lam_new
|
|
72
|
+
v = v_new
|
|
73
|
+
break
|
|
74
|
+
lam, v = lam_new, v_new
|
|
75
|
+
return max(lam, 0.0)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def spectral_summary(L: np.ndarray) -> dict:
|
|
79
|
+
"""Exact spectrum for small matrices; the key numbers of the fluid."""
|
|
80
|
+
n = L.shape[0]
|
|
81
|
+
if n <= 512:
|
|
82
|
+
w = np.linalg.eigvalsh(L)
|
|
83
|
+
w = np.clip(w, 0.0, None)
|
|
84
|
+
nz = w[w > 1e-10]
|
|
85
|
+
lam2 = float(nz[0]) if nz.size else 0.0
|
|
86
|
+
return {
|
|
87
|
+
"lam_max": float(w.max()) if w.size else 0.0,
|
|
88
|
+
"lam_min": float(w.min()) if w.size else 0.0,
|
|
89
|
+
"lam2": lam2,
|
|
90
|
+
"kernel_dim": int(n - nz.size),
|
|
91
|
+
"eigs": w,
|
|
92
|
+
}
|
|
93
|
+
lam_max = power_iteration_lmax(L)
|
|
94
|
+
return {"lam_max": lam_max, "lam_min": 0.0, "lam2": float("nan"),
|
|
95
|
+
"kernel_dim": -1, "eigs": None}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# --------------------------------------------------------------- chebyshev
|
|
99
|
+
def cheb_coeffs_exp(t: float, lam_max: float, m: int) -> np.ndarray:
|
|
100
|
+
"""Coefficients a_k with exp(-t L) ~= sum_k a_k T_k(Ltilde),
|
|
101
|
+
where Ltilde = 2L/lam_max - I (spectrum in [-1, 1]).
|
|
102
|
+
|
|
103
|
+
Method: Chebyshev interpolation of g(x) = exp(-alpha x) on [-1,1],
|
|
104
|
+
alpha = t*lam_max/2, at the Chebyshev-Gauss nodes, then scaled by exp(-alpha).
|
|
105
|
+
"""
|
|
106
|
+
if m < 0:
|
|
107
|
+
raise ValueError("m must be >= 0")
|
|
108
|
+
if lam_max <= 0.0:
|
|
109
|
+
a = np.zeros(m + 1)
|
|
110
|
+
a[0] = 1.0
|
|
111
|
+
return a
|
|
112
|
+
alpha = 0.5 * t * lam_max
|
|
113
|
+
if alpha > 500.0:
|
|
114
|
+
# exp(+alpha) would overflow; beyond this the truncated expansion is
|
|
115
|
+
# meaningless anyway -- see band_times() for the safe regime.
|
|
116
|
+
raise ValueError(
|
|
117
|
+
"Chebyshev expansion of exp(-tL) unstable for "
|
|
118
|
+
f"alpha=t*lam_max/2={alpha:.1f} "
|
|
119
|
+
f"(>500). Use band_times() to place bands in spectral time units."
|
|
120
|
+
)
|
|
121
|
+
j = np.arange(m + 1)
|
|
122
|
+
x = np.cos(np.pi * (j + 0.5) / (m + 1))
|
|
123
|
+
g = np.exp(-alpha * x)
|
|
124
|
+
k = np.arange(m + 1)
|
|
125
|
+
Cm = np.cos(np.pi * np.outer(j + 0.5, k) / (m + 1)) # (m+1, m+1)
|
|
126
|
+
c = (2.0 / (m + 1)) * (g @ Cm) # DCT-I coeffs
|
|
127
|
+
a = np.exp(-alpha) * c
|
|
128
|
+
a[0] *= 0.5 # c_0 / 2 convention
|
|
129
|
+
return a
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def spectral_bound_autodiff(L, tau: float = 0.5):
|
|
133
|
+
"""Differentiable UPPER BOUND on lambda_max (Gershgorin + smooth max).
|
|
134
|
+
|
|
135
|
+
The Chebyshev recurrence T_k(2L/l - I) is only stable when l >= lambda_max.
|
|
136
|
+
A power-iteration ESTIMATE can undershoot, and then the recurrence
|
|
137
|
+
explodes geometrically (measured: band energies of 1e15). So we normalise
|
|
138
|
+
with a guaranteed upper bound instead:
|
|
139
|
+
|
|
140
|
+
lambda_max <= max_i ( L_ii + sum_{j != i} |L_ij| ) = max_i rowsum|L|
|
|
141
|
+
|
|
142
|
+
The max is replaced by a log-sum-exp (smooth, differentiable, and still an
|
|
143
|
+
upper bound), so the whole thing stays in the autodiff graph.
|
|
144
|
+
"""
|
|
145
|
+
row_abs = L.abs().sum(axis=1) # (n,) differentiable
|
|
146
|
+
e = (row_abs / tau).exp()
|
|
147
|
+
lse = tau * e.sum().log()
|
|
148
|
+
return 2.0 * lse
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def cheb_coeffs_exp_autodiff(t: float, lam_max_t, m: int):
|
|
152
|
+
"""Differentiable version of cheb_coeffs_exp.
|
|
153
|
+
|
|
154
|
+
The Chebyshev coefficients depend on the spectral scale alpha = t*lam_max/2,
|
|
155
|
+
so treating lam_max as a constant inside the coefficient computation drops
|
|
156
|
+
a real part of the gradient w.r.t. the restriction maps. The whole
|
|
157
|
+
expansion is a matrix multiply, so it can be kept in the autodiff graph.
|
|
158
|
+
"""
|
|
159
|
+
from .autodiff import Tensor
|
|
160
|
+
j = np.arange(m + 1)
|
|
161
|
+
k = np.arange(m + 1)
|
|
162
|
+
x = np.cos(np.pi * (j + 0.5) / (m + 1))
|
|
163
|
+
Cm = np.cos(np.pi * np.outer(j + 0.5, k) / (m + 1))
|
|
164
|
+
Cm[0, :] *= 0.5 # the c_0 / 2 convention
|
|
165
|
+
alpha = 0.5 * t * lam_max_t # Tensor scalar
|
|
166
|
+
g = (-alpha * Tensor(x)).exp() # g(x) = exp(-alpha x), differentiable
|
|
167
|
+
a = (-alpha).exp() * ((2.0 / (m + 1)) * (g @ Tensor(Cm)))
|
|
168
|
+
return a
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def cheb_basis(L: np.ndarray, lam_max: float, X: np.ndarray, m: int):
|
|
172
|
+
"""Y[k] = T_k(Ltilde) @ X for k = 0..m. (m matvecs total, shared by all bands)
|
|
173
|
+
|
|
174
|
+
Ltilde = 2L/lam_max - I so that sigma(Ltilde) subset [-1, 1].
|
|
175
|
+
Chebyshev recurrence: T_0 = I, T_1 = Ltilde, T_k = 2*Ltilde*T_{k-1} - T_{k-2}.
|
|
176
|
+
"""
|
|
177
|
+
if lam_max <= 0.0:
|
|
178
|
+
return [X.copy()]
|
|
179
|
+
Lt = (2.0 / lam_max) * L - np.eye(L.shape[0])
|
|
180
|
+
Y = [X.copy()]
|
|
181
|
+
if m >= 1:
|
|
182
|
+
Y.append(Lt @ X)
|
|
183
|
+
for k in range(2, m + 1):
|
|
184
|
+
Y.append(2.0 * (Lt @ Y[k - 1]) - Y[k - 2])
|
|
185
|
+
return Y
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def band_times(lam_max: float, m: int, n_bands: int, alpha_min: float = 0.05,
|
|
189
|
+
alpha_max: float | None = None):
|
|
190
|
+
"""Spectrum-adaptive band times.
|
|
191
|
+
|
|
192
|
+
The Chebyshev expansion of exp(-tL) needs alpha = t*lam_max/2 <= m, otherwise
|
|
193
|
+
the truncation error blows up (this is the "conditioning cliff"). So the
|
|
194
|
+
bands are placed in *spectral* time units alpha_j, and converted to t at run
|
|
195
|
+
time. The band range then always spans the whole spectrum of L, whatever
|
|
196
|
+
its scale, and the Chebyshev degree stays sufficient by construction.
|
|
197
|
+
|
|
198
|
+
t_j = 2*alpha_j / lam_max
|
|
199
|
+
"""
|
|
200
|
+
if alpha_max is None:
|
|
201
|
+
alpha_max = float(m) * 0.75
|
|
202
|
+
a = np.geomspace(alpha_min, max(alpha_max, alpha_min * 2), n_bands + 1)
|
|
203
|
+
if lam_max <= 0:
|
|
204
|
+
return a
|
|
205
|
+
return 2.0 * a / lam_max
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def apply_bands(L: np.ndarray, lam_max: float, X: np.ndarray,
|
|
209
|
+
ts: np.ndarray, m: int) -> np.ndarray:
|
|
210
|
+
"""Multi-scale band filter bank.
|
|
211
|
+
|
|
212
|
+
band_j = exp(-t_j L) - exp(-t_{j+1} L), t = [t_0, ..., t_B] (B+1 times)
|
|
213
|
+
|
|
214
|
+
Returns array of shape (B, n, d). Cost: m Laplacian matvecs total
|
|
215
|
+
(the Chebyshev basis T_k(Ltilde) X is computed once and reused by every band).
|
|
216
|
+
"""
|
|
217
|
+
ts = np.asarray(ts, dtype=np.float64)
|
|
218
|
+
B = ts.shape[0] - 1
|
|
219
|
+
if B <= 0:
|
|
220
|
+
raise ValueError("need at least two time constants")
|
|
221
|
+
Y = cheb_basis(L, lam_max, X, m)
|
|
222
|
+
out = np.zeros((B,) + X.shape)
|
|
223
|
+
for j in range(B):
|
|
224
|
+
a = cheb_coeffs_exp(ts[j], lam_max, m)
|
|
225
|
+
b = cheb_coeffs_exp(ts[j + 1], lam_max, m)
|
|
226
|
+
coef = a - b
|
|
227
|
+
acc = np.zeros_like(X)
|
|
228
|
+
for k in range(m + 1):
|
|
229
|
+
if coef[k] != 0.0:
|
|
230
|
+
acc += coef[k] * Y[k]
|
|
231
|
+
out[j] = acc
|
|
232
|
+
return out
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def band_energies(bands: np.ndarray) -> np.ndarray:
|
|
236
|
+
"""Relative energy carried by each band: ||band_j||^2 / sum_j ||band_j||^2."""
|
|
237
|
+
e = (bands ** 2).sum(axis=(1, 2))
|
|
238
|
+
s = e.sum()
|
|
239
|
+
return e / s if s > 0 else e
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ------------------------------------------------------------------- theory
|
|
243
|
+
def spectral_gap(L: np.ndarray) -> float:
|
|
244
|
+
"""Smallest strictly positive eigenvalue (the spectral gap lambda_2)."""
|
|
245
|
+
s = spectral_summary(L)
|
|
246
|
+
return s["lam2"]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def jam_time(lam2: float, eps: float = 1e-6) -> float:
|
|
250
|
+
"""Predicted freezing time: t_jam = log(1/eps) / lambda_2."""
|
|
251
|
+
if lam2 <= 0:
|
|
252
|
+
return math.inf
|
|
253
|
+
return math.log(1.0 / eps) / lam2
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def cheb_degree(kappa: float, eps: float = 1e-6) -> int:
|
|
257
|
+
"""Chebyshev degree needed for relative accuracy eps at condition number kappa."""
|
|
258
|
+
return int(math.ceil(math.sqrt(max(kappa, 1.0)) * math.log(2.0 / eps)))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def exact_exp_action(L: np.ndarray, t: float, X: np.ndarray) -> np.ndarray:
|
|
262
|
+
"""Ground truth exp(-tL) X via eigendecomposition (for verification only)."""
|
|
263
|
+
w, V = np.linalg.eigh(L)
|
|
264
|
+
return V @ (np.exp(-t * w)[:, None] * (V.T @ X))
|
fluidlayer/gpt2_model.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fluidlayer.gpt2_model — a minimal GPT-2 forward pass, in PyTorch.
|
|
3
|
+
|
|
4
|
+
WHY
|
|
5
|
+
---
|
|
6
|
+
To attach the memory layer to a REAL pretrained checkpoint without
|
|
7
|
+
depending on `transformers`, this file reads a GPT-2 checkpoint
|
|
8
|
+
(`config.json` + `model.safetensors`) and implements the forward pass
|
|
9
|
+
directly. It is ~120 lines and it is verified by generation: the model
|
|
10
|
+
must produce coherent English, which it does.
|
|
11
|
+
|
|
12
|
+
The project itself stays dependency-free (the numpy package does not
|
|
13
|
+
import this module); torch is needed only for this file and for
|
|
14
|
+
`fluidlayer.torch_graft`.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import math
|
|
21
|
+
import os
|
|
22
|
+
|
|
23
|
+
__all__ = ["GPT2Model"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _layernorm(x, g, b, eps=1e-5):
|
|
27
|
+
mu = x.mean(-1, keepdim=True)
|
|
28
|
+
var = x.var(-1, unbiased=False, keepdim=True)
|
|
29
|
+
return (x - mu) / torch.sqrt(var + eps) * g + b
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _gelu(x):
|
|
33
|
+
# GPT-2 uses the tanh approximation; the erf version is within 1e-3
|
|
34
|
+
return 0.5 * x * (1.0 + torch.tanh(
|
|
35
|
+
math.sqrt(2.0 / math.pi) * (x + 0.044715 * x ** 3)))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
import torch # noqa: E402 (deliberately after the helpers, so the numpy
|
|
39
|
+
|
|
40
|
+
# package never imports torch)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class GPT2Model:
|
|
44
|
+
"""A GPT-2 (and GPT-2-family) causal LM loaded from a local folder."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, path: str, dtype=torch.float32, device="cpu"):
|
|
47
|
+
from safetensors.torch import load_file
|
|
48
|
+
with open(os.path.join(path, "config.json"), encoding="utf-8") as f:
|
|
49
|
+
cfg = json.load(f)
|
|
50
|
+
self.cfg = cfg
|
|
51
|
+
self.n_layer = cfg["n_layer"]
|
|
52
|
+
self.n_head = cfg["n_head"]
|
|
53
|
+
self.n_embd = cfg["n_embd"]
|
|
54
|
+
self.device = device
|
|
55
|
+
sd = load_file(os.path.join(path, "model.safetensors"))
|
|
56
|
+
self.wte = sd["wte.weight"].to(device=device, dtype=dtype)
|
|
57
|
+
self.wpe = sd["wpe.weight"].to(device=device, dtype=dtype)
|
|
58
|
+
self.blocks = []
|
|
59
|
+
for i in range(self.n_layer):
|
|
60
|
+
p = f"h.{i}."
|
|
61
|
+
self.blocks.append({
|
|
62
|
+
"ln1_g": sd[p + "ln_1.weight"].to(device=device, dtype=dtype),
|
|
63
|
+
"ln1_b": sd[p + "ln_1.bias"].to(device=device, dtype=dtype),
|
|
64
|
+
"attn_w": sd[p + "attn.c_attn.weight"].to(device=device, dtype=dtype),
|
|
65
|
+
"attn_b": sd[p + "attn.c_attn.bias"].to(device=device, dtype=dtype),
|
|
66
|
+
"proj_w": sd[p + "attn.c_proj.weight"].to(device=device, dtype=dtype),
|
|
67
|
+
"proj_b": sd[p + "attn.c_proj.bias"].to(device=device, dtype=dtype),
|
|
68
|
+
"ln2_g": sd[p + "ln_2.weight"].to(device=device, dtype=dtype),
|
|
69
|
+
"ln2_b": sd[p + "ln_2.bias"].to(device=device, dtype=dtype),
|
|
70
|
+
"fc_w": sd[p + "mlp.c_fc.weight"].to(device=device, dtype=dtype),
|
|
71
|
+
"fc_b": sd[p + "mlp.c_fc.bias"].to(device=device, dtype=dtype),
|
|
72
|
+
"mlp_w": sd[p + "mlp.c_proj.weight"].to(device=device, dtype=dtype),
|
|
73
|
+
"mlp_b": sd[p + "mlp.c_proj.bias"].to(device=device, dtype=dtype),
|
|
74
|
+
})
|
|
75
|
+
self.ln_f_g = sd["ln_f.weight"].to(device=device, dtype=dtype)
|
|
76
|
+
self.ln_f_b = sd["ln_f.bias"].to(device=device, dtype=dtype)
|
|
77
|
+
self.dtype = dtype
|
|
78
|
+
|
|
79
|
+
# ------------------------------------------------------------------ core
|
|
80
|
+
def forward(self, ids, past=None, h_override=None):
|
|
81
|
+
"""ids: (T,) int64 -> logits (T, V).
|
|
82
|
+
|
|
83
|
+
`h_override` replaces the embedding output: that is the hook a
|
|
84
|
+
grafted memory layer needs (the layer rewrites the residual stream,
|
|
85
|
+
the frozen model continues from it).
|
|
86
|
+
"""
|
|
87
|
+
ids = torch.as_tensor(ids, dtype=torch.long, device=self.device)
|
|
88
|
+
T = ids.shape[0]
|
|
89
|
+
H, D = self.n_head, self.n_embd
|
|
90
|
+
dh = D // H
|
|
91
|
+
h = self.wte[ids] + self.wpe[:T]
|
|
92
|
+
if h_override is not None:
|
|
93
|
+
h = h_override
|
|
94
|
+
mask = torch.tril(torch.ones(T, T, device=self.device, dtype=self.dtype))
|
|
95
|
+
for b in self.blocks:
|
|
96
|
+
x = _layernorm(h, b["ln1_g"], b["ln1_b"])
|
|
97
|
+
qkv = x @ b["attn_w"] + b["attn_b"]
|
|
98
|
+
q, k, v = qkv.split(D, dim=-1)
|
|
99
|
+
q = q.view(T, H, dh).transpose(0, 1)
|
|
100
|
+
k = k.view(T, H, dh).transpose(0, 1)
|
|
101
|
+
v = v.view(T, H, dh).transpose(0, 1)
|
|
102
|
+
att = (q @ k.transpose(-1, -2)) / math.sqrt(dh)
|
|
103
|
+
att = att.masked_fill(mask == 0, float("-inf"))
|
|
104
|
+
att = torch.softmax(att, dim=-1)
|
|
105
|
+
o = (att @ v).transpose(0, 1).reshape(T, D)
|
|
106
|
+
h = h + (o @ b["proj_w"] + b["proj_b"])
|
|
107
|
+
y = _layernorm(h, b["ln2_g"], b["ln2_b"])
|
|
108
|
+
y = _gelu(y @ b["fc_w"] + b["fc_b"])
|
|
109
|
+
h = h + (y @ b["mlp_w"] + b["mlp_b"])
|
|
110
|
+
h = _layernorm(h, self.ln_f_g, self.ln_f_b)
|
|
111
|
+
return h @ self.wte.T
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------- generation
|
|
114
|
+
@torch.no_grad()
|
|
115
|
+
def generate(self, ids, n_new=20, temperature=0.0, seed=0):
|
|
116
|
+
ids = list(ids)
|
|
117
|
+
for _ in range(n_new):
|
|
118
|
+
window = ids[-self.cfg.get("n_positions", 1024):]
|
|
119
|
+
logits = self.forward(torch.tensor(window, dtype=torch.long,
|
|
120
|
+
device=self.device))
|
|
121
|
+
nxt = logits[-1]
|
|
122
|
+
if temperature > 0:
|
|
123
|
+
torch.manual_seed(seed)
|
|
124
|
+
probs = torch.softmax(nxt / temperature, dim=-1)
|
|
125
|
+
nxt = torch.multinomial(probs, 1)
|
|
126
|
+
else:
|
|
127
|
+
nxt = nxt.argmax(dim=-1)
|
|
128
|
+
ids.append(int(nxt))
|
|
129
|
+
return ids
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fluidlayer.gpt2_tokenizer — the GPT-2 byte-level BPE, in pure Python.
|
|
3
|
+
|
|
4
|
+
WHY THIS FILE EXISTS
|
|
5
|
+
--------------------
|
|
6
|
+
The project's claim is that a memory layer can be attached to **any**
|
|
7
|
+
pretrained LLM. To test that on a real checkpoint without dragging in
|
|
8
|
+
`transformers` (and its `regex` dependency), this file implements the
|
|
9
|
+
GPT-2 tokenizer directly: the byte-to-unicode map, the BPE merges, and
|
|
10
|
+
the pre-tokenizer regex. It reads `vocab.json` + `merges.txt` from a
|
|
11
|
+
checkpoint directory.
|
|
12
|
+
|
|
13
|
+
It is exact: `decode(encode(text)) == text` for any text, including text
|
|
14
|
+
the checkpoint never saw (the byte fallback is always available).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
|
|
23
|
+
__all__ = ["GPT2Tokenizer", "bytes_to_unicode"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def bytes_to_unicode() -> dict:
|
|
27
|
+
"""GPT-2's reversible byte -> unicode map."""
|
|
28
|
+
bs = (list(range(ord("!"), ord("~") + 1))
|
|
29
|
+
+ list(range(ord("\xa1"), ord("\xac") + 1))
|
|
30
|
+
+ list(range(ord("\xae"), ord("\xff") + 1)))
|
|
31
|
+
cs = bs[:]
|
|
32
|
+
n = 0
|
|
33
|
+
for b in range(2 ** 8):
|
|
34
|
+
if b not in bs:
|
|
35
|
+
bs.append(b)
|
|
36
|
+
cs.append(2 ** 8 + n)
|
|
37
|
+
n += 1
|
|
38
|
+
return dict(zip(bs, [chr(c) for c in cs]))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_pairs(word):
|
|
42
|
+
return {(word[i], word[i + 1]) for i in range(len(word) - 1)}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# GPT-2's pre-tokenizer pattern (the stdlib `re` handles it fine)
|
|
46
|
+
_PRE = re.compile(
|
|
47
|
+
r"""'s|'t|'re|'ve|'m|'ll|'d| ?\w+| ?\w+|\S+|\s+""")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class GPT2Tokenizer:
|
|
51
|
+
"""The GPT-2 BPE tokenizer, loadable from a checkpoint directory."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, vocab: dict, merges: list):
|
|
54
|
+
self.encoder = vocab
|
|
55
|
+
self.decoder = {v: k for k, v in vocab.items()}
|
|
56
|
+
self.byte_encoder = bytes_to_unicode()
|
|
57
|
+
self.byte_decoder = {v: k for k, v in self.byte_encoder.items()}
|
|
58
|
+
self.bpe_ranks = {tuple(m): i for i, m in enumerate(merges)}
|
|
59
|
+
self.cache: dict = {}
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_pretrained(cls, path: str) -> GPT2Tokenizer:
|
|
63
|
+
with open(os.path.join(path, "vocab.json"), encoding="utf-8") as f:
|
|
64
|
+
vocab = json.load(f)
|
|
65
|
+
merges = []
|
|
66
|
+
p = os.path.join(path, "merges.txt")
|
|
67
|
+
if os.path.exists(p):
|
|
68
|
+
with open(p, encoding="utf-8") as f:
|
|
69
|
+
lines = f.read().split("\n")
|
|
70
|
+
merges = [tuple(l.split()) for l in lines[1:] if l.strip()]
|
|
71
|
+
return cls(vocab, merges)
|
|
72
|
+
|
|
73
|
+
# ------------------------------------------------------------------ BPE
|
|
74
|
+
def bpe(self, token: str) -> str:
|
|
75
|
+
if token in self.cache:
|
|
76
|
+
return self.cache[token]
|
|
77
|
+
word = tuple(token)
|
|
78
|
+
pairs = _get_pairs(word)
|
|
79
|
+
if not pairs:
|
|
80
|
+
return token
|
|
81
|
+
while True:
|
|
82
|
+
bigram = min(pairs, key=lambda p: self.bpe_ranks.get(p, 1e10))
|
|
83
|
+
if bigram not in self.bpe_ranks:
|
|
84
|
+
break
|
|
85
|
+
first, second = bigram
|
|
86
|
+
new_word = []
|
|
87
|
+
i = 0
|
|
88
|
+
while i < len(word):
|
|
89
|
+
try:
|
|
90
|
+
j = word.index(first, i)
|
|
91
|
+
except ValueError:
|
|
92
|
+
new_word.extend(word[i:])
|
|
93
|
+
break
|
|
94
|
+
new_word.extend(word[i:j])
|
|
95
|
+
i = j
|
|
96
|
+
if word[i] == first and i < len(word) - 1 and word[i + 1] == second:
|
|
97
|
+
new_word.append(first + second)
|
|
98
|
+
i += 2
|
|
99
|
+
else:
|
|
100
|
+
new_word.append(word[i])
|
|
101
|
+
i += 1
|
|
102
|
+
word = tuple(new_word)
|
|
103
|
+
if len(word) == 1:
|
|
104
|
+
break
|
|
105
|
+
pairs = _get_pairs(word)
|
|
106
|
+
out = " ".join(word)
|
|
107
|
+
self.cache[token] = out
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
# --------------------------------------------------------------- public
|
|
111
|
+
def encode(self, text: str):
|
|
112
|
+
ids = []
|
|
113
|
+
for tok in _PRE.findall(text):
|
|
114
|
+
tok = "".join(self.byte_encoder[b] for b in tok.encode("utf-8"))
|
|
115
|
+
ids.extend(self.encoder[t] for t in self.bpe(tok).split(" "))
|
|
116
|
+
return ids
|
|
117
|
+
|
|
118
|
+
def decode(self, ids) -> str:
|
|
119
|
+
text = "".join(self.decoder[int(i)] for i in ids)
|
|
120
|
+
raw = bytearray()
|
|
121
|
+
for c in text:
|
|
122
|
+
raw.append(self.byte_decoder[c])
|
|
123
|
+
return raw.decode("utf-8", errors="replace")
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def vocab_size(self) -> int:
|
|
127
|
+
return len(self.encoder)
|