Dipan04 commited on
Commit
31653ad
·
0 Parent(s):

DGPT v1-base release

Browse files
Files changed (8) hide show
  1. .gitattributes +1 -0
  2. README.md +142 -0
  3. config.json +30 -0
  4. model.npz +3 -0
  5. src/generate.py +131 -0
  6. src/model.py +157 -0
  7. src/tokenizer.py +109 -0
  8. tokenizer/bpe_6000.json +0 -0
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ model.npz filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: unknown
3
+ language:
4
+ - en
5
+ tags:
6
+ - text-generation
7
+ - from-scratch
8
+ - transformer
9
+ - gpt
10
+ - numpy
11
+ - cupy
12
+ - tinystories
13
+ datasets:
14
+ - roneneldan/TinyStories
15
+ pipeline_tag: text-generation
16
+ ---
17
+
18
+ # DGPT v1-base
19
+
20
+ **A small, from-scratch base language model. Not an instruction-tuned assistant.**
21
+
22
+ DGPT v1-base is a 13,049,856-parameter decoder-only Transformer trained from
23
+ scratch (manual forward pass, manual backward pass, manual AdamW — no
24
+ autograd, no PyTorch/JAX/TensorFlow) on the TinyStories dataset. It generates
25
+ short, simple, TinyStories-style children's narratives and nothing more.
26
+
27
+ **Do not expect:** instruction following, multi-turn conversation, reasoning,
28
+ factual world knowledge, or ChatGPT-comparable capability of any kind. This
29
+ model was never trained or tuned for any of those.
30
+
31
+ ## Model description
32
+
33
+ - **Model type:** decoder-only Transformer, Pre-LN, GELU (tanh approx), tied
34
+ token embedding / LM head (no output bias), learned positional embeddings.
35
+ - **Parameters:** 13,049,856
36
+ - **Context length:** 256 tokens
37
+ - **Vocabulary:** 6,000 (locked byte-level BPE, `bpe_6000.json`)
38
+ - **Framework:** none — hand-implemented NumPy/CuPy. Every layer's backward
39
+ pass was independently verified against finite-difference gradient checks
40
+ before training.
41
+
42
+ ## Architecture
43
+
44
+ | param | value |
45
+ |---|---|
46
+ | vocab_size | 6000 |
47
+ | block_size | 256 |
48
+ | d_model | 384 |
49
+ | n_layer | 6 |
50
+ | n_head | 6 |
51
+ | head_dim | 64 |
52
+ | d_ff | 1536 |
53
+ | activation | GELU (tanh approx) |
54
+ | norm | Pre-LN |
55
+ | positions | learned |
56
+ | lm_head | tied to token embedding, no bias |
57
+
58
+ ## Training data
59
+
60
+ [TinyStories](https://huggingface.co/datasets/roneneldan/TinyStories)
61
+ (`TinyStoriesV2-GPT4-train.txt`): 2,717,495 synthetically generated (GPT-3.5/
62
+ GPT-4) short stories using a deliberately small vocabulary
63
+ ([Eldan & Li, 2023](https://arxiv.org/abs/2305.07759)), tokenized to
64
+ 371,525,259 tokens. Licensed by its authors under CDLA-Sharing-1.0 — this
65
+ model card does not redistribute the dataset itself.
66
+
67
+ ## Training procedure
68
+
69
+ - **Optimizer:** manually implemented AdamW (lr, betas, weight decay applied
70
+ only to matrix params — biases/LayerNorm params excluded from decay).
71
+ - **Stage 2 (validation run):** 50k-story subset, 2000 steps, batch size 64,
72
+ LR 3e-4 with warmup, used to gate correctness before full training.
73
+ - **Full run:** resumed from the Stage 2 checkpoint, continued on the full
74
+ 371.5M-token corpus at LR 3e-5, to a **final checkpoint at step 5000**.
75
+ - **Tokenizer:** locked, pre-trained externally, never retrained during model
76
+ training.
77
+
78
+ ## Hardware
79
+
80
+ - 1x NVIDIA Tesla T4 (Turing, SM75, 16 GB VRAM), Kaggle.
81
+ - CuPy 14.0.1 as the GPU numerical execution backend (no autograd usage).
82
+ - Measured throughput: ~3,300–3,440 tokens/sec at batch size 64 (directly
83
+ measured, not extrapolated).
84
+
85
+ ## Intended use
86
+
87
+ - Educational reference for from-scratch Transformer implementation
88
+ (manual forward/backward/AdamW) at small scale.
89
+ - Generating short, TinyStories-style children's narratives from a prompt.
90
+ - Portfolio / ML-engineering demonstration.
91
+
92
+ ## Out-of-scope use
93
+
94
+ - Any production or consumer-facing assistant use case.
95
+ - Instruction following, chat, question answering, factual retrieval,
96
+ reasoning tasks, or code generation.
97
+ - Anything requiring broad world knowledge — the model's effective knowledge
98
+ is bounded by TinyStories' simplified vocabulary and narrative style.
99
+ - Any use that assumes safety alignment or content filtering — **none was
100
+ performed.**
101
+
102
+ ## Evaluation
103
+
104
+ From the training notebook (full-data run, step 5000):
105
+
106
+ - Train loss ≈ 3.3
107
+ - Val loss ≈ 3.3–3.4
108
+ - Val perplexity ≈ 27–29
109
+
110
+ No held-out benchmark suite (e.g. downstream NLP tasks) was run — TinyStories
111
+ train/val loss and perplexity are the only reported metrics. Treat any
112
+ numbers as approximate; see the training notebook's step-by-step log for the
113
+ exact source values.
114
+
115
+ ## Known generation issues
116
+
117
+ - Occasional run-on or abruptly concatenated sentences (short stories
118
+ sometimes blend into the next without a clean boundary).
119
+ - Repetition of simple phrases/character names across generations.
120
+ - No factual grounding — names, objects, and events are generated freely and
121
+ are not to be treated as accurate about anything.
122
+ - Context is capped at 256 tokens; longer prompts are truncated from the
123
+ left before generation.
124
+
125
+ ## How to use
126
+
127
+ ```python
128
+ from src.generate import load_dgpt, generate_text
129
+ from src.tokenizer import BPETokenizer
130
+
131
+ tok = BPETokenizer("tokenizer/bpe_6000.json")
132
+ model, _ = load_dgpt("model.npz")
133
+
134
+ print(generate_text(model, tok, "Once upon a time", max_new_tokens=150))
135
+ ```
136
+
137
+ ## Licensing
138
+
139
+ License is marked `unknown` above deliberately. See this repository's main
140
+ `README.md` → "Licensing" for the full breakdown across code, weights,
141
+ tokenizer, and the TinyStories dataset — the weights and tokenizer do not
142
+ have an established license and none is invented here.
config.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "dgpt",
3
+ "model_name": "DGPT v1-base",
4
+ "architectures": ["DGPT"],
5
+ "vocab_size": 6000,
6
+ "block_size": 256,
7
+ "d_model": 384,
8
+ "n_layer": 6,
9
+ "n_head": 6,
10
+ "head_dim": 64,
11
+ "d_ff": 1536,
12
+ "activation": "gelu_tanh",
13
+ "norm": "pre_ln",
14
+ "positional_embedding": "learned",
15
+ "tie_word_embeddings": true,
16
+ "lm_head_bias": false,
17
+ "parameter_count": 13049856,
18
+ "final_checkpoint_step": 5000,
19
+ "tokenizer": {
20
+ "file": "bpe_6000.json",
21
+ "type": "byte_level_bpe",
22
+ "vocab_size": 6000,
23
+ "schema": "bpt_v1"
24
+ },
25
+ "generation_defaults": {
26
+ "temperature": 0.8,
27
+ "top_k": 40,
28
+ "max_new_tokens": 200
29
+ }
30
+ }
model.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d9bae85b59e1df798b5d93b8047fae7f2dde09083ea5d88ee4ffac7d0d559ef2
3
+ size 143200980
src/generate.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/generate.py | 158 lines
2
+ """
3
+ DGPT v1-base inference: load model.npz + bpe_6000.json, generate text.
4
+
5
+ Usage:
6
+ python src/generate.py \
7
+ --checkpoint checkpoints/model.npz \
8
+ --tokenizer tokenizer/bpe_6000.json \
9
+ --prompt "Once upon a time" \
10
+ --max_new_tokens 200 --temperature 0.8 --top_k 40
11
+
12
+ Accepts either:
13
+ - the full training checkpoint (model.npz), which contains
14
+ `param__*`, `m__*`, `v__*`, and a JSON `__meta__` blob with the model
15
+ config and optimizer state, OR
16
+ - an inference-only weights file (model_weights.npz) produced by
17
+ scripts/extract_weights.py, which contains ONLY `param__*` keys and
18
+ requires --config to be passed explicitly (defaults to
19
+ configs/v1-base.json).
20
+ """
21
+
22
+ import argparse
23
+ import json
24
+ import os
25
+ import sys
26
+
27
+ import numpy as np
28
+
29
+ sys.path.insert(0, os.path.dirname(__file__))
30
+ from model import DGPT, stable_softmax # noqa: E402
31
+ from tokenizer import BPETokenizer # noqa: E402
32
+
33
+ DEFAULT_CONFIG_PATH = os.path.join(
34
+ os.path.dirname(__file__), "..", "configs", "v1-base.json"
35
+ )
36
+
37
+
38
+ def load_config(config_path=None):
39
+ path = config_path or DEFAULT_CONFIG_PATH
40
+ with open(path, "r") as f:
41
+ return json.load(f)
42
+
43
+
44
+ def load_dgpt(checkpoint_path, config_path=None):
45
+ """
46
+ Loads a DGPT model for inference from either a full training checkpoint
47
+ or an inference-only weights file. Returns (model, step_or_none).
48
+ """
49
+ data = np.load(checkpoint_path, allow_pickle=True)
50
+ param_keys = [k for k in data.files if k.startswith("param__")]
51
+ if not param_keys:
52
+ raise ValueError(f"No 'param__*' arrays found in {checkpoint_path}")
53
+
54
+ params = {k[len("param__"):]: data[k] for k in param_keys}
55
+
56
+ if "__meta__" in data.files:
57
+ meta = json.loads(str(data["__meta__"]))
58
+ config = meta["config"]
59
+ step = meta.get("step")
60
+ else:
61
+ config = load_config(config_path)
62
+ step = None
63
+
64
+ model = DGPT(params, config)
65
+ return model, step
66
+
67
+
68
+ def generate_text(model, tokenizer, prompt, max_new_tokens=200, temperature=0.8, top_k=40,
69
+ seed=None):
70
+ """Autoregressive sampling. NumPy only (CPU inference)."""
71
+ rng = np.random.default_rng(seed)
72
+ generated = list(tokenizer.encode(prompt))
73
+
74
+ for _ in range(max_new_tokens):
75
+ context = generated[-model.block_size:]
76
+ idx = np.asarray([context], dtype=np.int64)
77
+
78
+ logits = model.forward(idx)
79
+ logits_last = logits[0, -1].astype(np.float32, copy=False)
80
+
81
+ temperature = max(float(temperature), 1e-6)
82
+ logits_last = logits_last / temperature
83
+
84
+ if top_k is not None and top_k > 0:
85
+ k = min(int(top_k), logits_last.shape[0])
86
+ top_idx = np.argpartition(logits_last, -k)[-k:]
87
+ filtered = np.full_like(logits_last, -1e10)
88
+ filtered[top_idx] = logits_last[top_idx]
89
+ logits_last = filtered
90
+
91
+ probs = stable_softmax(logits_last, axis=-1)
92
+ next_token = int(rng.choice(probs.shape[0], p=probs / probs.sum()))
93
+ generated.append(next_token)
94
+
95
+ return tokenizer.decode(generated)
96
+
97
+
98
+ def main():
99
+ parser = argparse.ArgumentParser(description="DGPT v1-base text generation")
100
+ parser.add_argument("--checkpoint", default="checkpoints/model.npz")
101
+ parser.add_argument("--tokenizer", default="tokenizer/bpe_6000.json")
102
+ parser.add_argument("--config", default=None, help="Only needed for weights-only npz files")
103
+ parser.add_argument("--prompt", default="Once upon a time")
104
+ parser.add_argument("--max_new_tokens", type=int, default=200)
105
+ parser.add_argument("--temperature", type=float, default=0.8)
106
+ parser.add_argument("--top_k", type=int, default=40)
107
+ parser.add_argument("--seed", type=int, default=None)
108
+ args = parser.parse_args()
109
+
110
+ print(f"Loading tokenizer from {args.tokenizer} ...")
111
+ tok = BPETokenizer(args.tokenizer)
112
+ assert tok.vocab_size == 6000, f"Expected vocab_size=6000, got {tok.vocab_size}"
113
+
114
+ print(f"Loading model from {args.checkpoint} ...")
115
+ model, step = load_dgpt(args.checkpoint, args.config)
116
+ print(f"Model loaded. step={step} vocab={model.vocab_size} block_size={model.block_size}")
117
+
118
+ print(f"\nPrompt: {args.prompt!r}\n")
119
+ output = generate_text(
120
+ model, tok, args.prompt,
121
+ max_new_tokens=args.max_new_tokens,
122
+ temperature=args.temperature,
123
+ top_k=args.top_k,
124
+ seed=args.seed,
125
+ )
126
+ print("Output:")
127
+ print(output)
128
+
129
+
130
+ if __name__ == "__main__":
131
+ main()
src/model.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/model.py | 158 lines
2
+ """
3
+ DGPT — from-scratch decoder-only Transformer (inference-only reimplementation).
4
+
5
+ This is a forward-pass-only extraction of the architecture defined in the
6
+ original training notebook (NanoScratchGPT / DGPT). The original notebook
7
+ also hand-implements the backward pass and an AdamW optimizer for training;
8
+ those are intentionally NOT reproduced here since this file is for loading
9
+ `model.npz` and generating text only.
10
+
11
+ Locked architecture (see configs/v1-base.json):
12
+ vocab_size = 6000
13
+ block_size = 256
14
+ d_model = 384
15
+ n_layer = 6
16
+ n_head = 6
17
+ head_dim = 64
18
+ d_ff = 1536
19
+ activation = GELU (tanh approximation, GPT-2 style)
20
+ norm = Pre-LN
21
+ positions = learned
22
+ lm_head = tied to token embedding, no output bias
23
+
24
+ No PyTorch, no Hugging Face Transformers. NumPy only.
25
+ """
26
+
27
+ import numpy as np
28
+
29
+
30
+ def stable_softmax(x, axis=-1):
31
+ x_max = np.max(x, axis=axis, keepdims=True)
32
+ e = np.exp(x - x_max)
33
+ return e / np.sum(e, axis=axis, keepdims=True)
34
+
35
+
36
+ def gelu(x):
37
+ c = np.float32((2.0 / np.pi) ** 0.5)
38
+ inner = c * (x + 0.044715 * x ** 3)
39
+ return 0.5 * x * (1.0 + np.tanh(inner))
40
+
41
+
42
+ class Linear:
43
+ def __init__(self, W, b=None):
44
+ self.W = W
45
+ self.b = b
46
+
47
+ def __call__(self, x):
48
+ out = x @ self.W
49
+ if self.b is not None:
50
+ out = out + self.b
51
+ return out
52
+
53
+
54
+ class LayerNorm:
55
+ def __init__(self, gamma, beta, eps=1e-5):
56
+ self.gamma = gamma
57
+ self.beta = beta
58
+ self.eps = eps
59
+
60
+ def __call__(self, x):
61
+ mu = x.mean(axis=-1, keepdims=True)
62
+ var = x.var(axis=-1, keepdims=True)
63
+ x_hat = (x - mu) / np.sqrt(var + self.eps)
64
+ return self.gamma * x_hat + self.beta
65
+
66
+
67
+ class CausalSelfAttention:
68
+ def __init__(self, params, prefix, n_head):
69
+ self.Wq = Linear(params[f"{prefix}.Wq.W"], params[f"{prefix}.Wq.b"])
70
+ self.Wk = Linear(params[f"{prefix}.Wk.W"], params[f"{prefix}.Wk.b"])
71
+ self.Wv = Linear(params[f"{prefix}.Wv.W"], params[f"{prefix}.Wv.b"])
72
+ self.Wo = Linear(params[f"{prefix}.Wo.W"], params[f"{prefix}.Wo.b"])
73
+ self.n_head = n_head
74
+
75
+ def __call__(self, x):
76
+ B, T, C = x.shape
77
+ H = self.n_head
78
+ hd = C // H
79
+
80
+ Q, K, V = self.Wq(x), self.Wk(x), self.Wv(x)
81
+
82
+ def split_heads(t):
83
+ return t.reshape(B, T, H, hd).transpose(0, 2, 1, 3)
84
+
85
+ Qh, Kh, Vh = split_heads(Q), split_heads(K), split_heads(V)
86
+
87
+ scale = np.float32(1.0 / (hd ** 0.5))
88
+ scores = np.matmul(Qh, Kh.transpose(0, 1, 3, 2)) * scale
89
+
90
+ mask = np.triu(np.ones((T, T), dtype=bool), k=1)
91
+ scores = np.where(mask, np.float32(-1e9), scores)
92
+
93
+ A = stable_softmax(scores, axis=-1)
94
+ ctx = np.matmul(A, Vh)
95
+ ctx_merged = ctx.transpose(0, 2, 1, 3).reshape(B, T, C)
96
+ return self.Wo(ctx_merged)
97
+
98
+
99
+ class FeedForward:
100
+ def __init__(self, params, prefix):
101
+ self.fc1 = Linear(params[f"{prefix}.fc1.W"], params[f"{prefix}.fc1.b"])
102
+ self.fc2 = Linear(params[f"{prefix}.fc2.W"], params[f"{prefix}.fc2.b"])
103
+
104
+ def __call__(self, x):
105
+ return self.fc2(gelu(self.fc1(x)))
106
+
107
+
108
+ class TransformerBlock:
109
+ def __init__(self, params, prefix, n_head):
110
+ self.ln1 = LayerNorm(params[f"{prefix}.ln1.gamma"], params[f"{prefix}.ln1.beta"])
111
+ self.attn = CausalSelfAttention(params, f"{prefix}.attn", n_head)
112
+ self.ln2 = LayerNorm(params[f"{prefix}.ln2.gamma"], params[f"{prefix}.ln2.beta"])
113
+ self.ffn = FeedForward(params, f"{prefix}.ffn")
114
+
115
+ def __call__(self, x):
116
+ x = x + self.attn(self.ln1(x))
117
+ x = x + self.ffn(self.ln2(x))
118
+ return x
119
+
120
+
121
+ class DGPT:
122
+ """Inference-only DGPT. Load parameters with `DGPT.from_params(params, config)`."""
123
+
124
+ def __init__(self, params, config):
125
+ self.config = config
126
+ self.vocab_size = config["vocab_size"]
127
+ self.block_size = config["block_size"]
128
+ self.d_model = config["d_model"]
129
+ self.n_layer = config["n_layer"]
130
+ self.n_head = config["n_head"]
131
+
132
+ self.tok_emb_W = params["tok_emb.W"]
133
+ self.pos_emb_W = params["pos_emb.W"]
134
+ self.blocks = [
135
+ TransformerBlock(params, f"blocks.{i}", self.n_head)
136
+ for i in range(self.n_layer)
137
+ ]
138
+ self.ln_f = LayerNorm(params["ln_f.gamma"], params["ln_f.beta"])
139
+
140
+ def num_parameters(self):
141
+ return sum(v.size for v in self.__dict__.get("_raw_params", {}).values())
142
+
143
+ def forward(self, idx):
144
+ """idx: int array (B, T) with T <= block_size. Returns logits (B, T, vocab_size)."""
145
+ B, T = idx.shape
146
+ assert T <= self.block_size, "sequence length exceeds block_size"
147
+
148
+ tok = self.tok_emb_W[idx]
149
+ pos = self.pos_emb_W[:T]
150
+ x = tok + pos[None, :, :]
151
+
152
+ for blk in self.blocks:
153
+ x = blk(x)
154
+
155
+ x = self.ln_f(x)
156
+ logits = x @ self.tok_emb_W.T # tied weights, no output bias
157
+ return logits
src/tokenizer.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/tokenizer.py | 96 lines
2
+ """
3
+ Loader for the LOCKED byte-level BPE tokenizer (`bpe_6000.json`, vocab=6000).
4
+
5
+ This tokenizer was trained once, outside this repository, and must never be
6
+ retrained or regenerated. This module only deserializes the finished merge
7
+ table and performs encode/decode. There is no training method here by design.
8
+
9
+ On-disk schema (BPT_V1):
10
+ {
11
+ "vocab_size": 6000,
12
+ "merge_order": [
13
+ [[a_id, b_id], new_id],
14
+ ...
15
+ ]
16
+ }
17
+ """
18
+
19
+ import json
20
+
21
+
22
+ class BPETokenizer:
23
+ def __init__(self, path):
24
+ self.path = path
25
+ with open(path, "r", encoding="utf-8") as f:
26
+ data = json.load(f)
27
+
28
+ if "merge_order" not in data:
29
+ raise KeyError(
30
+ f"Unrecognized tokenizer file at {path}: expected a 'merge_order' key "
31
+ f"(BPT_V1 schema). Found top-level keys: {list(data.keys())}. "
32
+ "Do not attempt to regenerate this tokenizer."
33
+ )
34
+
35
+ merge_order = data["merge_order"]
36
+
37
+ # Base vocabulary: raw bytes 0..255
38
+ self.id_to_bytes = {i: (i,) for i in range(256)}
39
+ self.bpe_ranks = {}
40
+
41
+ for rank, entry in enumerate(merge_order):
42
+ a_id, b_id, new_id = self._normalize_merge_entry(entry)
43
+ self.bpe_ranks[(a_id, b_id)] = rank
44
+ self.id_to_bytes[new_id] = (a_id, b_id)
45
+
46
+ self.bytes_to_id = {v: k for k, v in self.id_to_bytes.items()}
47
+
48
+ declared_vocab_size = data.get("vocab_size")
49
+ computed_vocab_size = max(self.id_to_bytes.keys()) + 1
50
+ if declared_vocab_size is not None and declared_vocab_size != computed_vocab_size:
51
+ raise ValueError(
52
+ f"Tokenizer mismatch: declared vocab_size={declared_vocab_size}, "
53
+ f"computed={computed_vocab_size}. The tokenizer artifact is locked; "
54
+ "do not regenerate it."
55
+ )
56
+ self.vocab_size = computed_vocab_size
57
+
58
+ @staticmethod
59
+ def _normalize_merge_entry(entry):
60
+ if (
61
+ isinstance(entry, (list, tuple))
62
+ and len(entry) == 2
63
+ and isinstance(entry[0], (list, tuple))
64
+ and len(entry[0]) == 2
65
+ ):
66
+ return int(entry[0][0]), int(entry[0][1]), int(entry[1])
67
+ raise KeyError(f"Unrecognized merge entry: {entry!r}")
68
+
69
+ def _get_pairs(self, seq):
70
+ return set(zip(seq[:-1], seq[1:]))
71
+
72
+ def _bpe_merge(self, seq):
73
+ seq = list(seq)
74
+ if len(seq) < 2:
75
+ return seq
76
+ while True:
77
+ pairs = self._get_pairs(seq)
78
+ ranked = [(self.bpe_ranks[p], p) for p in pairs if p in self.bpe_ranks]
79
+ if not ranked:
80
+ break
81
+ _, best = min(ranked)
82
+ new_seq, i = [], 0
83
+ while i < len(seq):
84
+ if i < len(seq) - 1 and (seq[i], seq[i + 1]) == best:
85
+ new_seq.append(self.bytes_to_id[(seq[i], seq[i + 1])])
86
+ i += 2
87
+ else:
88
+ new_seq.append(seq[i])
89
+ i += 1
90
+ seq = new_seq
91
+ return seq
92
+
93
+ def encode(self, text):
94
+ raw_bytes = text.encode("utf-8")
95
+ return self._bpe_merge(list(raw_bytes))
96
+
97
+ def _expand(self, token_id):
98
+ if token_id < 256:
99
+ return bytes([token_id])
100
+ out = bytearray()
101
+ for part in self.id_to_bytes[token_id]:
102
+ out.extend(self._expand(part) if part >= 256 else bytes([part]))
103
+ return bytes(out)
104
+
105
+ def decode(self, ids):
106
+ out = bytearray()
107
+ for token_id in ids:
108
+ out.extend(self._expand(int(token_id)))
109
+ return bytes(out).decode("utf-8", errors="replace")
tokenizer/bpe_6000.json ADDED
The diff for this file is too large to render. See raw diff