GPUburnout commited on
Commit
f6da1b3
Β·
1 Parent(s): f4c74fc

Add 3B-Chat-v2, switch large models to GGUF, preload default

Browse files

- Add GPUburnout-3B-75K-Chat-v2 (proper recipe, beats 2B on 5/6 benchmarks)
- Mark old GPUburnout-3B-75K-Chat as RETIRED with explanatory description
- Switch 1B, 2B, 3B-old, 3B-v2 to llama-cpp-python GGUF Q4_K_M loaders
(3-5x faster CPU inference than fp32 transformers)
- Preload default model at startup to eliminate first-click lag
- Drop transformers + accelerate (no longer needed for large models)

Files changed (3) hide show
  1. README.md +7 -3
  2. app.py +93 -110
  3. requirements.txt +1 -2
README.md CHANGED
@@ -16,9 +16,13 @@ Compare language models trained from scratch across multiple seasons:
16
 
17
  - **Tiny Shakespeare** (3.2M params) β€” Character-level, trained on Shakespeare
18
  - **GPT-2 Small** (134M params) β€” BPE tokenizer, trained on 2.8B tokens
19
- - **Llama 1B** (1.04B params) β€” Llama architecture, trained on 11.8B tokens for $175
20
- - **GPUburnout-2B-Chat-DPO** (2.06B params) β€” 2B parameter chat model fine-tuned with DPO on 75K examples
 
 
 
 
21
 
22
  Built by [Jun Park](https://gpuburnout.com/about/) | [Read the blog](https://gpuburnout.com) | [GitHub](https://github.com/GPUburnout)
23
 
24
- Last updated: April 5, 2026
 
16
 
17
  - **Tiny Shakespeare** (3.2M params) β€” Character-level, trained on Shakespeare
18
  - **GPT-2 Small** (134M params) β€” BPE tokenizer, trained on 2.8B tokens
19
+ - **Llama 1B** (1.04B params) β€” Llama architecture, Chinchilla-optimal at 20.97B tokens
20
+ - **GPUburnout-2B-Chat-DPO** (2.06B params) β€” 2B chat model with SFT + DPO
21
+ - **GPUburnout-3B-Chat-v2** (3.12B params) β€” 3B chat model with proper chat-template SFT
22
+ - **GPUburnout-3B-Chat (RETIRED)** β€” Original 3B with the format-slip bug. Kept as a historical artifact.
23
+
24
+ Large models run via llama-cpp-python on Q4_K_M GGUF for fast CPU inference.
25
 
26
  Built by [Jun Park](https://gpuburnout.com/about/) | [Read the blog](https://gpuburnout.com) | [GitHub](https://github.com/GPUburnout)
27
 
28
+ Last updated: June 21, 2026
app.py CHANGED
@@ -1,6 +1,11 @@
1
  """
2
  GPUburnout Models β€” Unified Demo
3
- Compare models trained from scratch: GPUburnout-3M β†’ GPUburnout-134M β†’ GPUburnout-1B
 
 
 
 
 
4
  """
5
 
6
  import gc
@@ -37,8 +42,9 @@ MODELS = {
37
  },
38
  "GPUburnout-1B (1.04B)": {
39
  "path": "GPUburnout/GPUburnout-1B-160K",
40
- "arch": "s2_hf",
41
- "description": "Season 2. Llama architecture, 11.8B tokens, $175 total. Final loss 2.494.",
 
42
  "examples": [
43
  "The capital of France is",
44
  "In a shocking discovery, scientists found that",
@@ -48,8 +54,9 @@ MODELS = {
48
  },
49
  "GPUburnout-2B-75K-Chat-DPO (2.06B)": {
50
  "path": "GPUburnout/GPUburnout-2B-75K-Chat-DPO",
51
- "arch": "s2_hf",
52
- "description": "2B parameter chat model fine-tuned with DPO on 75K examples. Optimized for conversational AI.",
 
53
  "examples": [
54
  "What is the meaning of life?",
55
  "Explain quantum computing in simple terms.",
@@ -57,8 +64,40 @@ MODELS = {
57
  "Tell me about the history of artificial intelligence.",
58
  ],
59
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
 
 
 
62
  # ── Current model state (one at a time) ─────────────────────────────────────
63
 
64
  current = {"name": None, "model": None, "tokenizer": None, "config": None}
@@ -85,25 +124,20 @@ def load_model(model_name):
85
  unload_current()
86
 
87
  info = MODELS[model_name]
88
- model_dir = info["path"]
89
-
90
- # Handle HuggingFace Hub models differently
91
- if info["arch"] == "s2_hf":
92
- model, tokenizer = _load_s2_hf(model_dir)
93
- config = {"arch": "s2_hf"}
94
- else:
95
- config_path = os.path.join(model_dir, "config.json")
96
 
 
 
 
 
 
97
  if not os.path.exists(config_path):
98
- raise FileNotFoundError(f"Model not found: {model_dir}")
99
-
100
  with open(config_path) as f:
101
  config = json.load(f)
102
-
103
- if info["arch"] == "s1":
104
- model, tokenizer = _load_s1(model_dir, config)
105
- else:
106
- model, tokenizer = _load_s2(model_dir, config)
107
 
108
  current["name"] = model_name
109
  current["model"] = model
@@ -129,10 +163,8 @@ def _load_s1(model_dir, config):
129
  model.load_state_dict(torch.load(weights_path, map_location="cpu", weights_only=True))
130
  model.eval()
131
 
132
- # Load tokenizer
133
  tokenizer_type = config.get("tokenizer_type", "character")
134
  tokenizer_path = os.path.join(model_dir, "tokenizer.json")
135
-
136
  if tokenizer_type == "bpe":
137
  from s1_tokenizer_bpe import BPETokenizer
138
  tokenizer = BPETokenizer()
@@ -145,59 +177,21 @@ def _load_s1(model_dir, config):
145
  return model, tokenizer
146
 
147
 
148
- def _load_s2(model_dir, config):
149
- """Load Season 2 Llama style model."""
150
- from s2_model import LlamaModel, ModelConfig
151
 
152
- model_config = ModelConfig(
153
- vocab_size=config.get("vocab_size", 32005),
154
- d_model=config.get("d_model", 2048),
155
- n_layers=config.get("n_layers", 16),
156
- n_heads=config.get("n_heads", 32),
157
- n_kv_heads=config.get("n_kv_heads", 8),
158
- d_ff=config.get("d_ff", 8192),
159
- max_seq_len=config.get("max_seq_len", 2048),
 
160
  )
161
-
162
- model = LlamaModel(model_config).to("cpu")
163
- weights_path = os.path.join(model_dir, "pytorch_model.bin")
164
-
165
- # Download from HF model repo if not present locally (Space LFS limit workaround)
166
- if not os.path.exists(weights_path):
167
- from huggingface_hub import hf_hub_download
168
- print("Downloading Llama 1B weights from GPUburnout/GPUburnout-1B-160K...")
169
- weights_path = hf_hub_download(
170
- repo_id="GPUburnout/GPUburnout-1B-160K",
171
- filename="pytorch_model.bin",
172
- local_dir=model_dir,
173
- )
174
-
175
- state_dict = torch.load(weights_path, map_location="cpu", weights_only=True)
176
- model.load_state_dict(state_dict)
177
- model.eval()
178
-
179
- # S2 uses HuggingFace tokenizers library
180
- from tokenizers import Tokenizer
181
- tokenizer = Tokenizer.from_file("tokenizer/bpe_tokenizer.json")
182
-
183
- return model, tokenizer
184
-
185
-
186
- def _load_s2_hf(repo_id):
187
- """Load Season 2 model from HuggingFace Hub using transformers."""
188
- from transformers import AutoModelForCausalLM, AutoTokenizer
189
-
190
- print(f"Loading model from HuggingFace Hub: {repo_id}...")
191
- tokenizer = AutoTokenizer.from_pretrained(repo_id)
192
- model = AutoModelForCausalLM.from_pretrained(
193
- repo_id,
194
- torch_dtype=torch.float32,
195
- device_map="cpu",
196
- low_cpu_mem_usage=True,
197
- )
198
- model.eval()
199
-
200
- return model, tokenizer
201
 
202
 
203
  # ── Generation ──────────────────────────────────────────────────────────────
@@ -224,37 +218,17 @@ def generate_s1(model, tokenizer, config, prompt, max_tokens, temperature, top_k
224
  return tokenizer.decode(tokens[0].tolist())
225
 
226
 
227
- def generate_s2(model, tokenizer, prompt, max_tokens, temperature, top_k):
228
- """Generate text with S2 (Llama) model."""
229
- encoded = tokenizer.encode(prompt)
230
- input_ids = torch.tensor([encoded.ids], dtype=torch.long)
231
-
232
- with torch.no_grad():
233
- output_ids = model.generate(
234
- input_ids,
235
- max_new_tokens=max_tokens,
236
- temperature=temperature,
237
- top_k=top_k if top_k > 0 else None,
238
- )
239
-
240
- return tokenizer.decode(output_ids[0].tolist())
241
-
242
-
243
- def generate_s2_hf(model, tokenizer, prompt, max_tokens, temperature, top_k):
244
- """Generate text with S2 HuggingFace model."""
245
- inputs = tokenizer(prompt, return_tensors="pt")
246
-
247
- with torch.no_grad():
248
- output_ids = model.generate(
249
- inputs.input_ids,
250
- max_new_tokens=max_tokens,
251
- temperature=temperature,
252
- top_k=top_k if top_k > 0 else 50,
253
- do_sample=True,
254
- pad_token_id=tokenizer.eos_token_id,
255
- )
256
-
257
- return tokenizer.decode(output_ids[0], skip_special_tokens=True)
258
 
259
 
260
  def generate_text(model_name, prompt, max_tokens, temperature, top_k):
@@ -273,12 +247,12 @@ def generate_text(model_name, prompt, max_tokens, temperature, top_k):
273
  try:
274
  if info["arch"] == "s1":
275
  return generate_s1(model, tokenizer, config, prompt, int(max_tokens), temperature, int(top_k))
276
- elif info["arch"] == "s2_hf":
277
- return generate_s2_hf(model, tokenizer, prompt, int(max_tokens), temperature, int(top_k))
278
  else:
279
- return generate_s2(model, tokenizer, prompt, int(max_tokens), temperature, int(top_k))
280
  except Exception as e:
281
- return f"Error during generation: {e}\n\nPlease try with different parameters or a different model."
282
 
283
 
284
  def get_status(model_name):
@@ -372,6 +346,15 @@ dark_theme = gr.themes.Base(
372
  slider_color_dark="#22d3ee",
373
  )
374
 
 
 
 
 
 
 
 
 
 
375
  # ── Gradio UI ───────────────────────────────────────────────────────────────
376
 
377
  with gr.Blocks(
@@ -383,7 +366,7 @@ with gr.Blocks(
383
  gr.HTML("""
384
  <div class="header-text">
385
  <h1>GPUburnout Models</h1>
386
- <p>Compare language models I trained from scratch β€” from 3.2M to 2 billion parameters.</p>
387
  <p>
388
  <a href="https://gpuburnout.com" target="_blank">Read the blog</a> Β·
389
  <a href="https://github.com/GPUburnout" target="_blank">GitHub</a> Β·
@@ -396,7 +379,7 @@ with gr.Blocks(
396
  with gr.Column(scale=1):
397
  model_selector = gr.Dropdown(
398
  choices=list(MODELS.keys()),
399
- value="GPUburnout-134M (134M)",
400
  label="Select Model",
401
  )
402
 
 
1
  """
2
  GPUburnout Models β€” Unified Demo
3
+ Compare models trained from scratch, 3.2M β†’ 3.12B parameters.
4
+
5
+ Architecture notes:
6
+ - s1: tiny custom GPT-2 (3M and 134M) β€” kept in PyTorch
7
+ - gguf: all Llama-family models (1B, 2B, 3B old + new) run via llama-cpp-python
8
+ for fast CPU inference. Q4_K_M quantization.
9
  """
10
 
11
  import gc
 
42
  },
43
  "GPUburnout-1B (1.04B)": {
44
  "path": "GPUburnout/GPUburnout-1B-160K",
45
+ "gguf_file": "GPUburnout-1B-160K-Q4_K_M.gguf",
46
+ "arch": "gguf",
47
+ "description": "Season 2 base model. Llama architecture, 20.97B tokens (Chinchilla-optimal), $175 total.",
48
  "examples": [
49
  "The capital of France is",
50
  "In a shocking discovery, scientists found that",
 
54
  },
55
  "GPUburnout-2B-75K-Chat-DPO (2.06B)": {
56
  "path": "GPUburnout/GPUburnout-2B-75K-Chat-DPO",
57
+ "gguf_file": "GPUburnout-2B-Chat-DPO-Q4_K_M.gguf",
58
+ "arch": "gguf",
59
+ "description": "2B chat model fine-tuned with SFT + DPO on 75K examples. Clean conversational output.",
60
  "examples": [
61
  "What is the meaning of life?",
62
  "Explain quantum computing in simple terms.",
 
64
  "Tell me about the history of artificial intelligence.",
65
  ],
66
  },
67
+ "GPUburnout-3B-75K-Chat-v2 (3.12B)": {
68
+ "path": "GPUburnout/GPUburnout-3B-75K-Chat-v2",
69
+ "gguf_file": "GPUburnout-3B-75K-Chat-v2-Q4_K_M.gguf",
70
+ "arch": "gguf",
71
+ "description": (
72
+ "The 3B chat model with the CORRECT chat-template SFT recipe. "
73
+ "Beats the 2B on 5/6 benchmarks. See blog Ch.4 for the backstory."
74
+ ),
75
+ "examples": [
76
+ "What is the capital of France?",
77
+ "Explain how photosynthesis works to a 10-year-old.",
78
+ "Write a Python function to check if a string is a palindrome.",
79
+ "What is 247 x 18?",
80
+ ],
81
+ },
82
+ "GPUburnout-3B-75K-Chat (3.12B) β€” RETIRED": {
83
+ "path": "GPUburnout/GPUburnout-3B-75K-Chat",
84
+ "gguf_file": "GPUburnout-3B-75K-Chat-Q4_K_M.gguf",
85
+ "arch": "gguf",
86
+ "description": (
87
+ "RETIRED. Original 3B chat model trained with a buggy plain-concat SFT formatter. "
88
+ "Loops and underperforms the 2B on ARC-Easy. Kept as a historical artifact β€” use v2 instead."
89
+ ),
90
+ "examples": [
91
+ "What is the capital of France?",
92
+ "Write a Python function to check if a string is a palindrome.",
93
+ "Explain relativity in three sentences.",
94
+ "Which of two trains traveled farther by noon?",
95
+ ],
96
+ },
97
  }
98
 
99
+ DEFAULT_MODEL = "GPUburnout-134M (134M)"
100
+
101
  # ── Current model state (one at a time) ─────────────────────────────────────
102
 
103
  current = {"name": None, "model": None, "tokenizer": None, "config": None}
 
124
  unload_current()
125
 
126
  info = MODELS[model_name]
127
+ arch = info["arch"]
 
 
 
 
 
 
 
128
 
129
+ if arch == "gguf":
130
+ model, tokenizer = _load_gguf(info)
131
+ config = {"arch": "gguf"}
132
+ elif arch == "s1":
133
+ config_path = os.path.join(info["path"], "config.json")
134
  if not os.path.exists(config_path):
135
+ raise FileNotFoundError(f"Model not found: {info['path']}")
 
136
  with open(config_path) as f:
137
  config = json.load(f)
138
+ model, tokenizer = _load_s1(info["path"], config)
139
+ else:
140
+ raise ValueError(f"Unknown arch: {arch}")
 
 
141
 
142
  current["name"] = model_name
143
  current["model"] = model
 
163
  model.load_state_dict(torch.load(weights_path, map_location="cpu", weights_only=True))
164
  model.eval()
165
 
 
166
  tokenizer_type = config.get("tokenizer_type", "character")
167
  tokenizer_path = os.path.join(model_dir, "tokenizer.json")
 
168
  if tokenizer_type == "bpe":
169
  from s1_tokenizer_bpe import BPETokenizer
170
  tokenizer = BPETokenizer()
 
177
  return model, tokenizer
178
 
179
 
180
+ def _load_gguf(info):
181
+ """Load a GGUF model via llama-cpp-python, pulling from HF Hub on first use."""
182
+ from llama_cpp import Llama
183
 
184
+ repo_id = info["path"]
185
+ filename = info["gguf_file"]
186
+ print(f"Loading GGUF {filename} from {repo_id}...")
187
+ model = Llama.from_pretrained(
188
+ repo_id=repo_id,
189
+ filename=filename,
190
+ n_ctx=2048,
191
+ n_threads=os.cpu_count() or 2,
192
+ verbose=False,
193
  )
194
+ return model, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
 
196
 
197
  # ── Generation ──────────────────────────────────────────────────────────────
 
218
  return tokenizer.decode(tokens[0].tolist())
219
 
220
 
221
+ def generate_gguf(model, prompt, max_tokens, temperature, top_k):
222
+ """Generate text with a GGUF model via llama-cpp-python."""
223
+ out = model(
224
+ prompt,
225
+ max_tokens=max_tokens,
226
+ temperature=temperature,
227
+ top_k=top_k if top_k > 0 else 50,
228
+ stop=["<|im_end|>", "<|endoftext|>"],
229
+ echo=True,
230
+ )
231
+ return out["choices"][0]["text"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
 
233
 
234
  def generate_text(model_name, prompt, max_tokens, temperature, top_k):
 
247
  try:
248
  if info["arch"] == "s1":
249
  return generate_s1(model, tokenizer, config, prompt, int(max_tokens), temperature, int(top_k))
250
+ elif info["arch"] == "gguf":
251
+ return generate_gguf(model, prompt, int(max_tokens), temperature, int(top_k))
252
  else:
253
+ return f"Unknown arch: {info['arch']}"
254
  except Exception as e:
255
+ return f"Error during generation: {e}\n\nTry different parameters or a different model."
256
 
257
 
258
  def get_status(model_name):
 
346
  slider_color_dark="#22d3ee",
347
  )
348
 
349
+ # ── Preload default model at startup ────────────────────────────────────────
350
+
351
+ print(f"Preloading default model: {DEFAULT_MODEL}...")
352
+ try:
353
+ load_model(DEFAULT_MODEL)
354
+ print(f" Loaded.")
355
+ except Exception as e:
356
+ print(f" Preload failed (will lazy-load on first generate): {e}")
357
+
358
  # ── Gradio UI ───────────────────────────────────────────────────────────────
359
 
360
  with gr.Blocks(
 
366
  gr.HTML("""
367
  <div class="header-text">
368
  <h1>GPUburnout Models</h1>
369
+ <p>Compare language models I trained from scratch, 3.2M to 3.12 billion parameters.</p>
370
  <p>
371
  <a href="https://gpuburnout.com" target="_blank">Read the blog</a> Β·
372
  <a href="https://github.com/GPUburnout" target="_blank">GitHub</a> Β·
 
379
  with gr.Column(scale=1):
380
  model_selector = gr.Dropdown(
381
  choices=list(MODELS.keys()),
382
+ value=DEFAULT_MODEL,
383
  label="Select Model",
384
  )
385
 
requirements.txt CHANGED
@@ -3,6 +3,5 @@ torch>=2.0.0
3
  tokenizers>=0.13.0
4
  gradio>=4.0.0
5
  huggingface_hub
6
- transformers>=4.30.0
7
  numpy
8
- accelerate
 
3
  tokenizers>=0.13.0
4
  gradio>=4.0.0
5
  huggingface_hub
 
6
  numpy
7
+ llama-cpp-python