Files changed (1) hide show
  1. app.py +170 -224
app.py CHANGED
@@ -1,3 +1,6 @@
 
 
 
1
  import json
2
  import re
3
  import shutil
@@ -11,18 +14,15 @@ import spaces
11
  import torch
12
  from huggingface_hub import HfApi, hf_hub_download
13
  from safetensors.torch import load_file
14
- from tokenizers import Tokenizer
15
 
16
- TITLE = "Promethean Studios Model Playground"
17
 
 
18
  MODELS = {
19
  "Talos 10M": "PrometheanStudio/talos-10m",
20
  "Styx 100M": "PrometheanStudio/styx-100m",
21
  }
22
 
23
- # This is the Talos revision that produced the released model architecture.
24
- # Do not change this to "main": the repository's current main branch has a
25
- # different layout and is not guaranteed to load these older checkpoints.
26
  TALOS_COMMIT = "e230fed9c18a5467e69ed0f15778a5b06d59cb5a"
27
  TALOS_URL = "https://github.com/Promethean-Studios/talos.git"
28
  CACHE = Path("/tmp/promethean-playground")
@@ -32,8 +32,7 @@ CACHE.mkdir(parents=True, exist_ok=True)
32
  MODEL_CACHE = {}
33
  TOKENIZER_CACHE = {}
34
 
35
- # Known architecture presets are used only if a repository's config.json does
36
- # not match its own weights. A candidate is accepted only after strict loading.
37
  FALLBACK_CONFIGS = {
38
  "PrometheanStudio/talos-10m": {
39
  "vocab_size": 1024,
@@ -73,10 +72,7 @@ def repo_files(repo_id):
73
 
74
 
75
  def find_tokenizer_file(files):
76
- candidates = [
77
- name for name in files
78
- if Path(name).name.lower() == "tokenizer.json"
79
- ]
80
  if not candidates:
81
  raise FileNotFoundError("No tokenizer.json was found in the model repository.")
82
  candidates.sort(
@@ -90,98 +86,26 @@ def find_tokenizer_file(files):
90
  return candidates[0]
91
 
92
 
93
- def load_tokenizer(repo_id, files):
94
- if repo_id in TOKENIZER_CACHE:
95
- return TOKENIZER_CACHE[repo_id]
96
-
97
- filename = find_tokenizer_file(files)
98
- downloaded = hf_hub_download(
99
- repo_id=repo_id,
100
- filename=filename,
101
- repo_type="model",
102
- )
103
-
104
- try:
105
- with open(downloaded, "r", encoding="utf-8") as handle:
106
- payload = json.load(handle)
107
- except Exception as exc:
108
- raise RuntimeError(f"Could not read {filename}: {exc}") from exc
109
-
110
- if not isinstance(payload, dict):
111
- raise RuntimeError(f"{filename} must contain a JSON object.")
112
-
113
- # Do not blindly coerce the tokenizer schema. The previous version of
114
- # this app changed numeric versions into strings, which can itself trigger
115
- # a Rust deserialization error depending on the uploaded file/schema.
116
- # Try the original file first, then only try the alternate version type
117
- # when the parser specifically rejects the original serialization.
118
- tokenizer = None
119
- parser_errors = []
120
- candidates = [("original", downloaded, payload)]
121
- version = payload.get("version")
122
-
123
- if isinstance(version, str):
124
- try:
125
- numeric_version = float(version)
126
- if numeric_version.is_integer():
127
- numeric_version = int(numeric_version)
128
- alternate = dict(payload)
129
- alternate["version"] = numeric_version
130
- candidates.append(("numeric-version", None, alternate))
131
- except (ValueError, OverflowError):
132
- pass
133
- elif isinstance(version, (int, float)) and not isinstance(version, bool):
134
- alternate = dict(payload)
135
- alternate["version"] = f"{version:.1f}" if isinstance(version, float) else f"{version}.0"
136
- candidates.append(("string-version", None, alternate))
137
-
138
- for candidate_name, candidate_path, candidate_payload in candidates:
139
- try:
140
- if candidate_path is not None:
141
- tokenizer = Tokenizer.from_file(str(candidate_path))
142
- else:
143
- candidate_path = CACHE / (
144
- f"{repo_id.replace('/', '_')}_tokenizer_{candidate_name}.json"
145
- )
146
- with open(candidate_path, "w", encoding="utf-8") as handle:
147
- json.dump(candidate_payload, handle, ensure_ascii=False)
148
- tokenizer = Tokenizer.from_file(str(candidate_path))
149
- print(f"Tokenizer loaded using {candidate_name} serialization")
150
- break
151
- except Exception as exc:
152
- parser_errors.append(f"{candidate_name}: {type(exc).__name__}: {exc}")
153
-
154
- if tokenizer is None:
155
- preview = json.dumps(payload, ensure_ascii=False)[:240]
156
- raise RuntimeError(
157
- f"Tokenizer parser rejected {repo_id}/{filename}. "
158
- f"Top-level version={version!r}; beginning of parsed JSON={preview!r}. "
159
- + " | ".join(parser_errors)
160
- )
161
-
162
- TOKENIZER_CACHE[repo_id] = tokenizer
163
- print(
164
- f"Tokenizer ready: {repo_id}/{filename}; "
165
- f"vocabulary entries={tokenizer.get_vocab_size(with_added_tokens=True)}"
166
- )
167
- return tokenizer
168
-
169
-
170
  def ensure_talos_source():
171
- """Check out and import the exact Talos revision used for these checkpoints."""
172
  marker = SOURCE / ".talos_commit"
 
 
 
 
 
 
 
173
  source_is_ready = (
174
- marker.exists()
175
  and marker.read_text(encoding="utf-8").strip() == TALOS_COMMIT
176
- and (SOURCE / "model" / "gpt.py").is_file()
177
- and (SOURCE / "model" / "config.py").is_file()
178
  )
179
 
180
  if not source_is_ready:
181
  if SOURCE.exists():
182
  shutil.rmtree(SOURCE)
183
  SOURCE.mkdir(parents=True, exist_ok=True)
184
-
185
  commands = [
186
  ["git", "init", str(SOURCE)],
187
  ["git", "-C", str(SOURCE), "remote", "add", "origin", TALOS_URL],
@@ -189,42 +113,81 @@ def ensure_talos_source():
189
  ["git", "-C", str(SOURCE), "checkout", "--detach", "FETCH_HEAD"],
190
  ]
191
  for command in commands:
192
- result = subprocess.run(
193
- command,
194
- check=False,
195
- capture_output=True,
196
- text=True,
197
- timeout=240,
198
- )
199
- if result.returncode != 0:
200
- details = (result.stderr or result.stdout or "No git output").strip()
201
- raise RuntimeError(
202
- f"Could not prepare Talos source ({' '.join(command)}): {details[-1200:]}"
203
  )
204
-
 
 
 
 
 
 
205
  marker.write_text(TALOS_COMMIT, encoding="utf-8")
206
- print(f"Talos architecture source pinned to {TALOS_COMMIT}")
207
 
208
- # Do this even when source files were cached from a prior app process:
209
- # /tmp may survive a restart while Python's sys.path does not.
210
  source_prefix = str(SOURCE.resolve())
211
- sys.path[:] = [p for p in sys.path if str(Path(p or ".").resolve()) != source_prefix]
 
 
 
212
  sys.path.insert(0, source_prefix)
213
 
214
- # Discard only similarly named modules imported from somewhere else.
215
- # Never unload modules already coming from this pinned checkout.
216
  for name, module in list(sys.modules.items()):
217
- if not (name == "model" or name.startswith("model.") or name == "inference" or name.startswith("inference.")):
 
 
 
218
  continue
219
  module_file = getattr(module, "__file__", None)
220
  try:
221
- belongs_to_source = module_file and Path(module_file).resolve().is_relative_to(SOURCE.resolve())
 
 
222
  except (OSError, ValueError):
223
  belongs_to_source = False
224
  if not belongs_to_source:
225
  del sys.modules[name]
226
 
227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  def config_dict_from(value):
229
  if isinstance(value, dict):
230
  for nested_key in ("model_config", "config", "model"):
@@ -240,11 +203,13 @@ def config_dict_from(value):
240
 
241
 
242
  def config_candidates(repo_id, files, checkpoint):
243
- # Only pass actual ModelConfig fields to the constructor. Hugging Face
244
- # metadata such as "model_type" is not part of Talos ModelConfig.
245
  from model.config import ModelConfig
246
 
247
  allowed = {item.name for item in fields(ModelConfig)}
 
 
 
 
248
  candidates = []
249
 
250
  if "config.json" in files:
@@ -254,35 +219,24 @@ def config_candidates(repo_id, files, checkpoint):
254
  raw = json.load(handle)
255
  normalized = config_dict_from(raw)
256
  if normalized:
257
- clean = {key: val for key, val in normalized.items() if key in allowed}
258
- required = {
259
- "vocab_size", "max_seq_len", "hidden_size", "num_layers",
260
- "num_attention_heads", "num_kv_heads",
261
- }
262
  if required.issubset(clean):
263
  candidates.append(("repository config.json", clean))
264
  except Exception as exc:
265
- print(f"Ignoring unreadable config.json for {repo_id}: {exc}")
266
 
267
  if isinstance(checkpoint, dict):
268
  for key in ("config", "model_config"):
269
  raw = config_dict_from(checkpoint.get(key))
270
  if raw:
271
- clean = {name: val for name, val in raw.items() if name in allowed}
272
- required = {
273
- "vocab_size", "max_seq_len", "hidden_size", "num_layers",
274
- "num_attention_heads", "num_kv_heads",
275
- }
276
  if required.issubset(clean):
277
  candidates.append((f"checkpoint {key}", clean))
278
 
279
- fallback = FALLBACK_CONFIGS.get(repo_id)
280
- if fallback:
281
- candidates.append(("known model preset", fallback))
282
 
283
- # Deduplicate equivalent config dictionaries while preserving priority.
284
- unique = []
285
- seen = set()
286
  for source, cfg in candidates:
287
  signature = json.dumps(cfg, sort_keys=True, default=str)
288
  if signature not in seen:
@@ -296,7 +250,7 @@ def read_weights(repo_id, files):
296
  if safetensors_files:
297
  safetensors_files.sort(
298
  key=lambda name: (
299
- 0 if Path(name).name == "model.safetensors" else 1,
300
  len(name),
301
  name,
302
  )
@@ -306,14 +260,11 @@ def read_weights(repo_id, files):
306
  print(f"Loading inference weights: {repo_id}/{filename}")
307
  return load_file(path, device="cpu"), None
308
 
309
- checkpoint_files = [
310
- name for name in files
311
- if name.endswith((".pt", ".pth", ".bin"))
312
- ]
313
  if not checkpoint_files:
314
- raise FileNotFoundError(f"No .safetensors or PyTorch checkpoint found in {repo_id}.")
315
 
316
- def checkpoint_priority(name):
317
  basename = Path(name).name.lower()
318
  match = re.search(r"step[-_](\d+)", basename)
319
  step = int(match.group(1)) if match else -1
@@ -325,31 +276,27 @@ def read_weights(repo_id, files):
325
  name,
326
  )
327
 
328
- checkpoint_files.sort(key=checkpoint_priority)
329
  filename = checkpoint_files[0]
330
  path = hf_hub_download(repo_id=repo_id, filename=filename, repo_type="model")
331
  print(f"Loading PyTorch checkpoint: {repo_id}/{filename}")
 
332
  try:
333
  checkpoint = torch.load(path, map_location="cpu", weights_only=True)
334
- except TypeError:
335
- # Older PyTorch releases do not expose the weights_only parameter.
336
- checkpoint = torch.load(path, map_location="cpu")
337
  except Exception as safe_exc:
338
- # These two model IDs are fixed to the owner's own public repositories.
339
- # Some legacy training checkpoints carry ordinary Python config/RNG
340
- # objects that weights_only=True rejects. Keep safetensors as the first
341
- # choice; only use the legacy unpickler for these explicit repositories.
342
  if repo_id not in MODELS.values():
343
  raise RuntimeError(
344
- f"Safe loading of {filename} failed: {type(safe_exc).__name__}: {safe_exc}. "
345
- "Prefer publishing model.safetensors for inference."
346
  ) from safe_exc
 
347
  try:
348
  checkpoint = torch.load(path, map_location="cpu", weights_only=False)
349
  except Exception as legacy_exc:
350
  raise RuntimeError(
351
- f"Could not load {filename} (safe load failed with {type(safe_exc).__name__}: "
352
- f"{safe_exc}; legacy load failed with {type(legacy_exc).__name__}: {legacy_exc})."
353
  ) from legacy_exc
354
 
355
  state = checkpoint
@@ -360,27 +307,46 @@ def read_weights(repo_id, files):
360
  state = value
361
  break
362
  if not isinstance(state, dict) or not state or not all(torch.is_tensor(v) for v in state.values()):
363
- raise RuntimeError(f"{filename} did not contain a plain model state dictionary.")
364
  return state, checkpoint if isinstance(checkpoint, dict) else None
365
 
366
 
367
  def normalize_state_dict(state):
368
- # Strip common wrapper prefixes only when they exist on every tensor key.
369
  for prefix in ("module.", "_orig_mod."):
370
  if state and all(key.startswith(prefix) for key in state):
371
  state = {key[len(prefix):]: value for key, value in state.items()}
372
  return state
373
 
374
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  def load_model(repo_id):
376
  if repo_id in MODEL_CACHE:
377
  return MODEL_CACHE[repo_id]
378
 
379
  files = repo_files(repo_id)
380
- tokenizer = load_tokenizer(repo_id, files)
381
- # Prepare the class modules before reading a legacy .pt checkpoint, which
382
- # may contain a pickled ModelConfig object from this exact code revision.
383
  ensure_talos_source()
 
 
384
  state, checkpoint = read_weights(repo_id, files)
385
  state = normalize_state_dict(state)
386
 
@@ -393,59 +359,59 @@ def load_model(repo_id):
393
  config = ModelConfig(**raw_config).derive()
394
  model = TalosGPT(config)
395
  model.load_state_dict(state, strict=True)
 
396
  model.eval()
397
-
398
- vocab = tokenizer.get_vocab()
399
- max_token_id = max(vocab.values(), default=-1)
400
- if max_token_id >= config.vocab_size:
401
- raise RuntimeError(
402
- f"Tokenizer emits token id {max_token_id}, but model vocabulary has "
403
- f"only {config.vocab_size} rows. This repository's tokenizer and weights are incompatible."
404
- )
405
-
406
- count = model.num_parameters()
407
  print(
408
- f"Loaded {repo_id} using {source}: {count:,} parameters; "
409
- f"vocab={config.vocab_size}, context={config.max_seq_len}"
 
410
  )
411
- MODEL_CACHE[repo_id] = (model, tokenizer)
412
- return model, tokenizer
413
  except Exception as exc:
414
  attempts.append(
415
- f"{source} ({raw_config.get('hidden_size')} hidden, "
416
- f"{raw_config.get('num_layers')} layers): {type(exc).__name__}: {str(exc)[:700]}"
 
417
  )
418
 
419
  raise RuntimeError(
420
- f"The weights in {repo_id} did not strictly match the available Talos architecture configs. "
421
- "This is a real checkpoint/config mismatch, not a Transformers model_type problem.\n"
422
  + "\n".join(attempts)
423
  )
424
 
425
 
426
  def get_model(label):
 
 
427
  return load_model(MODELS[label])
428
 
429
 
430
- def choose_next_token(scores, history, temperature, top_p, repetition_penalty):
431
  scores = scores.float().clone()
432
 
433
  if repetition_penalty > 1.0:
434
  for token_id in set(history[-128:]):
435
- if 0 <= token_id < scores.shape[-1]:
436
  if scores[token_id] < 0:
437
  scores[token_id] *= repetition_penalty
438
  else:
439
  scores[token_id] /= repetition_penalty
440
 
 
 
 
 
 
441
  if temperature <= 0:
442
  return scores.argmax(dim=-1).view(1, 1)
443
 
444
  probabilities = torch.softmax(scores / max(float(temperature), 1e-5), dim=-1)
 
 
 
445
  sorted_probs, sorted_ids = torch.sort(probabilities, descending=True)
446
  cumulative = torch.cumsum(sorted_probs, dim=-1)
447
-
448
- # Keep the first token that crosses the nucleus threshold.
449
  remove = cumulative > float(top_p)
450
  remove[1:] = remove[:-1].clone()
451
  remove[0] = False
@@ -453,8 +419,7 @@ def choose_next_token(scores, history, temperature, top_p, repetition_penalty):
453
  total = sorted_probs.sum()
454
  if not torch.isfinite(total) or total <= 0:
455
  return scores.argmax(dim=-1).view(1, 1)
456
- sorted_probs = sorted_probs / total
457
- sampled_index = torch.multinomial(sorted_probs, num_samples=1)
458
  return sorted_ids.gather(0, sampled_index).view(1, 1)
459
 
460
 
@@ -464,25 +429,26 @@ def generate(label, prompt, max_new_tokens, temperature, top_p, repetition_penal
464
  raise gr.Error("Enter a prompt first.")
465
 
466
  try:
467
- model, tokenizer = get_model(label)
468
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
469
  model = model.to(device).eval()
470
 
471
- encoded = tokenizer.encode(prompt)
472
- if not encoded.ids:
473
- raise gr.Error("The tokenizer returned no tokens for that prompt.")
 
474
 
475
  max_new_tokens = int(max_new_tokens)
476
  max_seq_len = int(model.config.max_seq_len)
477
- if max_new_tokens < 1 or max_new_tokens >= max_seq_len:
478
  raise gr.Error(f"New-token count must be between 1 and {max_seq_len - 1}.")
 
 
 
479
 
480
- prompt_ids = encoded.ids
481
- # Reserve room for all requested output tokens, retaining the latest
482
- # prompt tokens if the input is longer than the model's context window.
483
- keep = max_seq_len - max_new_tokens
484
- prompt_ids = prompt_ids[-keep:]
485
  input_ids = torch.tensor([prompt_ids], dtype=torch.long, device=device)
 
 
486
  history = list(prompt_ids)
487
  generated_ids = []
488
 
@@ -498,18 +464,16 @@ def generate(label, prompt, max_new_tokens, temperature, top_p, repetition_penal
498
  float(temperature),
499
  float(top_p),
500
  float(repetition_penalty),
 
501
  )
502
  token_id = int(next_token.item())
 
 
503
  generated_ids.append(token_id)
504
  history.append(token_id)
505
-
506
  if step + 1 < max_new_tokens:
507
- logits = decode_step(
508
- model,
509
- cache,
510
- next_token.to(device),
511
- position=input_ids.shape[1] + step,
512
- )
513
 
514
  return tokenizer.decode(generated_ids)
515
 
@@ -517,9 +481,7 @@ def generate(label, prompt, max_new_tokens, temperature, top_p, repetition_penal
517
  raise
518
  except Exception as exc:
519
  print(f"Generation failure for {label}: {type(exc).__name__}: {exc}")
520
- raise gr.Error(
521
- f"Could not load or run {label}: {type(exc).__name__}: {exc}"
522
- ) from exc
523
 
524
 
525
  with gr.Blocks(title=TITLE) as demo:
@@ -531,16 +493,10 @@ with gr.Blocks(title=TITLE) as demo:
531
 
532
  with gr.Row():
533
  model_choice = gr.Dropdown(
534
- choices=list(MODELS.keys()),
535
- value="Styx 100M",
536
- label="Model",
537
  )
538
  max_tokens = gr.Slider(
539
- minimum=16,
540
- maximum=256,
541
- value=96,
542
- step=16,
543
- label="New tokens",
544
  )
545
 
546
  prompt = gr.Textbox(
@@ -552,24 +508,14 @@ with gr.Blocks(title=TITLE) as demo:
552
  with gr.Accordion("Generation settings", open=False):
553
  with gr.Row():
554
  temperature = gr.Slider(
555
- minimum=0,
556
- maximum=1.5,
557
- value=0.8,
558
- step=0.05,
559
  label="Temperature (0 = greedy)",
560
  )
561
  top_p = gr.Slider(
562
- minimum=0.1,
563
- maximum=1.0,
564
- value=0.9,
565
- step=0.05,
566
- label="Top-p",
567
  )
568
  repetition = gr.Slider(
569
- minimum=1.0,
570
- maximum=1.5,
571
- value=1.1,
572
- step=0.05,
573
  label="Repetition penalty",
574
  )
575
 
@@ -580,10 +526,10 @@ with gr.Blocks(title=TITLE) as demo:
580
  prompt.submit(fn=generate, inputs=inputs, outputs=output)
581
 
582
  gr.Markdown(
583
- "**First use:** the Space downloads model files and pins the matching Talos source. "
584
- "Generation quality is experimental."
585
  )
586
 
587
 
588
  if __name__ == "__main__":
589
- demo.queue().launch()
 
1
+
2
+
3
+
4
  import json
5
  import re
6
  import shutil
 
14
  import torch
15
  from huggingface_hub import HfApi, hf_hub_download
16
  from safetensors.torch import load_file
 
17
 
 
18
 
19
+ TITLE = "Promethean Studios Model Playground"
20
  MODELS = {
21
  "Talos 10M": "PrometheanStudio/talos-10m",
22
  "Styx 100M": "PrometheanStudio/styx-100m",
23
  }
24
 
25
+
 
 
26
  TALOS_COMMIT = "e230fed9c18a5467e69ed0f15778a5b06d59cb5a"
27
  TALOS_URL = "https://github.com/Promethean-Studios/talos.git"
28
  CACHE = Path("/tmp/promethean-playground")
 
32
  MODEL_CACHE = {}
33
  TOKENIZER_CACHE = {}
34
 
35
+
 
36
  FALLBACK_CONFIGS = {
37
  "PrometheanStudio/talos-10m": {
38
  "vocab_size": 1024,
 
72
 
73
 
74
  def find_tokenizer_file(files):
75
+ candidates = [name for name in files if Path(name).name.lower() == "tokenizer.json"]
 
 
 
76
  if not candidates:
77
  raise FileNotFoundError("No tokenizer.json was found in the model repository.")
78
  candidates.sort(
 
86
  return candidates[0]
87
 
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  def ensure_talos_source():
90
+
91
  marker = SOURCE / ".talos_commit"
92
+ required_files = (
93
+ "model/gpt.py",
94
+ "model/config.py",
95
+ "inference/generate.py",
96
+ "tokenizer/tokenizer.py",
97
+ "tokenizer/vocab.py",
98
+ )
99
  source_is_ready = (
100
+ marker.is_file()
101
  and marker.read_text(encoding="utf-8").strip() == TALOS_COMMIT
102
+ and all((SOURCE / filename).is_file() for filename in required_files)
 
103
  )
104
 
105
  if not source_is_ready:
106
  if SOURCE.exists():
107
  shutil.rmtree(SOURCE)
108
  SOURCE.mkdir(parents=True, exist_ok=True)
 
109
  commands = [
110
  ["git", "init", str(SOURCE)],
111
  ["git", "-C", str(SOURCE), "remote", "add", "origin", TALOS_URL],
 
113
  ["git", "-C", str(SOURCE), "checkout", "--detach", "FETCH_HEAD"],
114
  ]
115
  for command in commands:
116
+ try:
117
+ result = subprocess.run(
118
+ command, check=False, capture_output=True, text=True, timeout=240
 
 
 
 
 
 
 
 
119
  )
120
+ except (OSError, subprocess.TimeoutExpired) as exc:
121
+ raise RuntimeError(f"Could not prepare Talos source: {exc}") from exc
122
+ if result.returncode != 0:
123
+ detail = (result.stderr or result.stdout or "No git output").strip()
124
+ raise RuntimeError(f"Talos git checkout failed: {detail[-1200:]}")
125
+ if not all((SOURCE / filename).is_file() for filename in required_files):
126
+ raise RuntimeError("The pinned Talos revision is missing expected source files.")
127
  marker.write_text(TALOS_COMMIT, encoding="utf-8")
128
+ print(f"Talos source pinned to {TALOS_COMMIT}")
129
 
 
 
130
  source_prefix = str(SOURCE.resolve())
131
+ sys.path[:] = [
132
+ entry for entry in sys.path
133
+ if str(Path(entry or ".").resolve()) != source_prefix
134
+ ]
135
  sys.path.insert(0, source_prefix)
136
 
137
+
 
138
  for name, module in list(sys.modules.items()):
139
+ if not (
140
+ name in ("model", "inference", "tokenizer")
141
+ or name.startswith(("model.", "inference.", "tokenizer."))
142
+ ):
143
  continue
144
  module_file = getattr(module, "__file__", None)
145
  try:
146
+ belongs_to_source = bool(
147
+ module_file and Path(module_file).resolve().is_relative_to(SOURCE.resolve())
148
+ )
149
  except (OSError, ValueError):
150
  belongs_to_source = False
151
  if not belongs_to_source:
152
  del sys.modules[name]
153
 
154
 
155
+ def load_tokenizer(repo_id, files):
156
+
157
+ if repo_id in TOKENIZER_CACHE:
158
+ return TOKENIZER_CACHE[repo_id]
159
+
160
+ ensure_talos_source()
161
+ from tokenizer.tokenizer import ByteLevelBPETokenizer
162
+
163
+ filename = find_tokenizer_file(files)
164
+ downloaded = hf_hub_download(repo_id=repo_id, filename=filename, repo_type="model")
165
+
166
+ with open(downloaded, "r", encoding="utf-8") as handle:
167
+ payload = json.load(handle)
168
+ if not isinstance(payload, dict) or payload.get("type") != "ByteLevelBPE":
169
+ raise RuntimeError(
170
+ f"{repo_id}/{filename} is not the native Talos ByteLevelBPE format. "
171
+ "A different tokenizer loader is required."
172
+ )
173
+ if payload.get("version") != 1:
174
+ raise RuntimeError(
175
+ f"Unsupported Talos tokenizer version {payload.get('version')!r} "
176
+ f"in {repo_id}/{filename}."
177
+ )
178
+
179
+ tokenizer = ByteLevelBPETokenizer.from_file(str(downloaded))
180
+
181
+ print(
182
+ f"Tokenizer ready: {repo_id}/{filename}; "
183
+ f"defined tokens={tokenizer.vocab_size}, "
184
+ f"configured vocabulary={tokenizer.config.vocab_size}, "
185
+ f"merges={tokenizer.merge_count}"
186
+ )
187
+ TOKENIZER_CACHE[repo_id] = tokenizer
188
+ return tokenizer
189
+
190
+
191
  def config_dict_from(value):
192
  if isinstance(value, dict):
193
  for nested_key in ("model_config", "config", "model"):
 
203
 
204
 
205
  def config_candidates(repo_id, files, checkpoint):
 
 
206
  from model.config import ModelConfig
207
 
208
  allowed = {item.name for item in fields(ModelConfig)}
209
+ required = {
210
+ "vocab_size", "max_seq_len", "hidden_size", "num_layers",
211
+ "num_attention_heads", "num_kv_heads",
212
+ }
213
  candidates = []
214
 
215
  if "config.json" in files:
 
219
  raw = json.load(handle)
220
  normalized = config_dict_from(raw)
221
  if normalized:
222
+ clean = {key: value for key, value in normalized.items() if key in allowed}
 
 
 
 
223
  if required.issubset(clean):
224
  candidates.append(("repository config.json", clean))
225
  except Exception as exc:
226
+ print(f"Could not use config.json for {repo_id}: {exc}")
227
 
228
  if isinstance(checkpoint, dict):
229
  for key in ("config", "model_config"):
230
  raw = config_dict_from(checkpoint.get(key))
231
  if raw:
232
+ clean = {name: value for name, value in raw.items() if name in allowed}
 
 
 
 
233
  if required.issubset(clean):
234
  candidates.append((f"checkpoint {key}", clean))
235
 
236
+ if repo_id in FALLBACK_CONFIGS:
237
+ candidates.append(("known model preset", FALLBACK_CONFIGS[repo_id]))
 
238
 
239
+ unique, seen = [], set()
 
 
240
  for source, cfg in candidates:
241
  signature = json.dumps(cfg, sort_keys=True, default=str)
242
  if signature not in seen:
 
250
  if safetensors_files:
251
  safetensors_files.sort(
252
  key=lambda name: (
253
+ 0 if name == "model.safetensors" else 1,
254
  len(name),
255
  name,
256
  )
 
260
  print(f"Loading inference weights: {repo_id}/{filename}")
261
  return load_file(path, device="cpu"), None
262
 
263
+ checkpoint_files = [name for name in files if name.endswith((".pt", ".pth", ".bin"))]
 
 
 
264
  if not checkpoint_files:
265
+ raise FileNotFoundError(f"No model weights found in {repo_id}.")
266
 
267
+ def priority(name):
268
  basename = Path(name).name.lower()
269
  match = re.search(r"step[-_](\d+)", basename)
270
  step = int(match.group(1)) if match else -1
 
276
  name,
277
  )
278
 
279
+ checkpoint_files.sort(key=priority)
280
  filename = checkpoint_files[0]
281
  path = hf_hub_download(repo_id=repo_id, filename=filename, repo_type="model")
282
  print(f"Loading PyTorch checkpoint: {repo_id}/{filename}")
283
+
284
  try:
285
  checkpoint = torch.load(path, map_location="cpu", weights_only=True)
 
 
 
286
  except Exception as safe_exc:
287
+
288
+
 
 
289
  if repo_id not in MODELS.values():
290
  raise RuntimeError(
291
+ f"Safe checkpoint loading failed: {safe_exc}. "
292
+ "Publish a model.safetensors file instead."
293
  ) from safe_exc
294
+ print(f"Safe loading failed for {filename}; trying legacy checkpoint reader")
295
  try:
296
  checkpoint = torch.load(path, map_location="cpu", weights_only=False)
297
  except Exception as legacy_exc:
298
  raise RuntimeError(
299
+ f"Could not load {filename}: safe={safe_exc}; legacy={legacy_exc}"
 
300
  ) from legacy_exc
301
 
302
  state = checkpoint
 
307
  state = value
308
  break
309
  if not isinstance(state, dict) or not state or not all(torch.is_tensor(v) for v in state.values()):
310
+ raise RuntimeError(f"{filename} did not contain a model state dictionary.")
311
  return state, checkpoint if isinstance(checkpoint, dict) else None
312
 
313
 
314
  def normalize_state_dict(state):
 
315
  for prefix in ("module.", "_orig_mod."):
316
  if state and all(key.startswith(prefix) for key in state):
317
  state = {key[len(prefix):]: value for key, value in state.items()}
318
  return state
319
 
320
 
321
+ def allowed_token_ids(tokenizer, model_vocab_size):
322
+
323
+
324
+ text_ids = set(tokenizer.vocab.id_to_bytes.keys())
325
+ valid_ids = text_ids | {int(tokenizer.eos_id)}
326
+ if not valid_ids or min(valid_ids) < 0 or max(valid_ids) >= model_vocab_size:
327
+ raise RuntimeError(
328
+ "Tokenizer contains IDs outside the model vocabulary: "
329
+ f"largest={max(valid_ids, default=-1)}, model_vocab={model_vocab_size}"
330
+ )
331
+
332
+
333
+ all_ids = text_ids | set(tokenizer.special_id_map().values())
334
+ if min(all_ids, default=0) < 0 or max(all_ids, default=-1) >= model_vocab_size:
335
+ raise RuntimeError(
336
+ "Tokenizer special-token IDs exceed the model vocabulary: "
337
+ f"largest={max(all_ids)}, model_vocab={model_vocab_size}"
338
+ )
339
+ return frozenset(valid_ids)
340
+
341
+
342
  def load_model(repo_id):
343
  if repo_id in MODEL_CACHE:
344
  return MODEL_CACHE[repo_id]
345
 
346
  files = repo_files(repo_id)
 
 
 
347
  ensure_talos_source()
348
+ tokenizer = load_tokenizer(repo_id, files)
349
+
350
  state, checkpoint = read_weights(repo_id, files)
351
  state = normalize_state_dict(state)
352
 
 
359
  config = ModelConfig(**raw_config).derive()
360
  model = TalosGPT(config)
361
  model.load_state_dict(state, strict=True)
362
+ valid_ids = allowed_token_ids(tokenizer, config.vocab_size)
363
  model.eval()
 
 
 
 
 
 
 
 
 
 
364
  print(
365
+ f"Loaded {repo_id} from {source}: "
366
+ f"{model.num_parameters():,} parameters, "
367
+ f"context={config.max_seq_len}, generatable_ids={len(valid_ids)}"
368
  )
369
+ MODEL_CACHE[repo_id] = (model, tokenizer, valid_ids)
370
+ return MODEL_CACHE[repo_id]
371
  except Exception as exc:
372
  attempts.append(
373
+ f"{source} (hidden={raw_config.get('hidden_size')}, "
374
+ f"layers={raw_config.get('num_layers')}): "
375
+ f"{type(exc).__name__}: {str(exc)[:700]}"
376
  )
377
 
378
  raise RuntimeError(
379
+ f"The weights in {repo_id} did not strictly match any Talos config.\n"
 
380
  + "\n".join(attempts)
381
  )
382
 
383
 
384
  def get_model(label):
385
+ if label not in MODELS:
386
+ raise ValueError(f"Unknown model selection: {label!r}")
387
  return load_model(MODELS[label])
388
 
389
 
390
+ def choose_next_token(scores, history, temperature, top_p, repetition_penalty, allowed_mask):
391
  scores = scores.float().clone()
392
 
393
  if repetition_penalty > 1.0:
394
  for token_id in set(history[-128:]):
395
+ if 0 <= token_id < scores.numel():
396
  if scores[token_id] < 0:
397
  scores[token_id] *= repetition_penalty
398
  else:
399
  scores[token_id] /= repetition_penalty
400
 
401
+ scores = scores.masked_fill(~allowed_mask, float("-inf"))
402
+ scores = torch.where(torch.isnan(scores), -torch.inf, scores)
403
+ if not torch.isfinite(scores).any():
404
+ raise RuntimeError("The model produced no finite logits for decodable tokens.")
405
+
406
  if temperature <= 0:
407
  return scores.argmax(dim=-1).view(1, 1)
408
 
409
  probabilities = torch.softmax(scores / max(float(temperature), 1e-5), dim=-1)
410
+ if not torch.isfinite(probabilities).all():
411
+ raise RuntimeError("Model output probabilities contain NaN or infinity.")
412
+
413
  sorted_probs, sorted_ids = torch.sort(probabilities, descending=True)
414
  cumulative = torch.cumsum(sorted_probs, dim=-1)
 
 
415
  remove = cumulative > float(top_p)
416
  remove[1:] = remove[:-1].clone()
417
  remove[0] = False
 
419
  total = sorted_probs.sum()
420
  if not torch.isfinite(total) or total <= 0:
421
  return scores.argmax(dim=-1).view(1, 1)
422
+ sampled_index = torch.multinomial(sorted_probs / total, num_samples=1)
 
423
  return sorted_ids.gather(0, sampled_index).view(1, 1)
424
 
425
 
 
429
  raise gr.Error("Enter a prompt first.")
430
 
431
  try:
432
+ model, tokenizer, valid_ids = get_model(label)
433
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
434
  model = model.to(device).eval()
435
 
436
+
437
+ prompt_ids = tokenizer.encode(prompt)
438
+ if not prompt_ids:
439
+ raise gr.Error("The tokenizer returned no tokens for the prompt.")
440
 
441
  max_new_tokens = int(max_new_tokens)
442
  max_seq_len = int(model.config.max_seq_len)
443
+ if not 1 <= max_new_tokens < max_seq_len:
444
  raise gr.Error(f"New-token count must be between 1 and {max_seq_len - 1}.")
445
+ prompt_ids = prompt_ids[-(max_seq_len - max_new_tokens):]
446
+ if min(prompt_ids) < 0 or max(prompt_ids) >= model.config.vocab_size:
447
+ raise RuntimeError("Encoded prompt contains token IDs outside the model vocabulary.")
448
 
 
 
 
 
 
449
  input_ids = torch.tensor([prompt_ids], dtype=torch.long, device=device)
450
+ allowed_mask = torch.zeros(model.config.vocab_size, dtype=torch.bool, device=device)
451
+ allowed_mask[list(valid_ids)] = True
452
  history = list(prompt_ids)
453
  generated_ids = []
454
 
 
464
  float(temperature),
465
  float(top_p),
466
  float(repetition_penalty),
467
+ allowed_mask,
468
  )
469
  token_id = int(next_token.item())
470
+ if token_id == tokenizer.eos_id:
471
+ break
472
  generated_ids.append(token_id)
473
  history.append(token_id)
 
474
  if step + 1 < max_new_tokens:
475
+
476
+ logits = decode_step(model, cache, next_token, position=cache.length)
 
 
 
 
477
 
478
  return tokenizer.decode(generated_ids)
479
 
 
481
  raise
482
  except Exception as exc:
483
  print(f"Generation failure for {label}: {type(exc).__name__}: {exc}")
484
+ raise gr.Error(f"Could not load or run {label}: {type(exc).__name__}: {exc}") from exc
 
 
485
 
486
 
487
  with gr.Blocks(title=TITLE) as demo:
 
493
 
494
  with gr.Row():
495
  model_choice = gr.Dropdown(
496
+ choices=list(MODELS.keys()), value="Styx 100M", label="Model"
 
 
497
  )
498
  max_tokens = gr.Slider(
499
+ minimum=16, maximum=256, value=96, step=16, label="New tokens"
 
 
 
 
500
  )
501
 
502
  prompt = gr.Textbox(
 
508
  with gr.Accordion("Generation settings", open=False):
509
  with gr.Row():
510
  temperature = gr.Slider(
511
+ minimum=0, maximum=1.5, value=0.8, step=0.05,
 
 
 
512
  label="Temperature (0 = greedy)",
513
  )
514
  top_p = gr.Slider(
515
+ minimum=0.1, maximum=1.0, value=0.9, step=0.05, label="Top-p"
 
 
 
 
516
  )
517
  repetition = gr.Slider(
518
+ minimum=1.0, maximum=1.5, value=1.1, step=0.05,
 
 
 
519
  label="Repetition penalty",
520
  )
521
 
 
526
  prompt.submit(fn=generate, inputs=inputs, outputs=output)
527
 
528
  gr.Markdown(
529
+ "**First use:** the Space downloads model files and pins the matching "
530
+ "Talos source. Generation quality is experimental."
531
  )
532
 
533
 
534
  if __name__ == "__main__":
535
+ demo.queue().launch()