Commit ·
31653ad
0
Parent(s):
DGPT v1-base release
Browse files- .gitattributes +1 -0
- README.md +142 -0
- config.json +30 -0
- model.npz +3 -0
- src/generate.py +131 -0
- src/model.py +157 -0
- src/tokenizer.py +109 -0
- 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
|
|
|