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.
@@ -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))
@@ -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)