byte-deep-hybrid
appvoid commited on
Commit
62dfd7a
·
verified ·
1 Parent(s): 0e8999e

Upload infer_bytefast60m.py

Browse files
Files changed (1) hide show
  1. infer_bytefast60m.py +940 -0
infer_bytefast60m.py ADDED
@@ -0,0 +1,940 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ infer_bytefast60m.py
4
+
5
+ Correct standalone inference utility for the custom FastDeepHybridLM defined
6
+ by bytefalcon_fast60m.py.
7
+
8
+ This does NOT instantiate Falcon-H1 or any Hugging Face AutoModel class.
9
+ It imports the exact training architecture and calls its load_model_bundle(),
10
+ which reconstructs Fast60MConfig + FastDeepHybridLM and strictly loads model.pt.
11
+
12
+ Expected checkpoint:
13
+ step-XXXXXXXX/
14
+ config.json
15
+ model.pt
16
+ tokenizer.json
17
+ tokenizer_config.json
18
+ ...
19
+
20
+ Rewrite training format:
21
+ instruction
22
+
23
+ "source text"
24
+
25
+ "target output"<eos>
26
+
27
+ For inference, rewrite mode supplies the opening output quote and lets the
28
+ model generate the target text, closing quote, and EOS.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import argparse
34
+ import contextlib
35
+ import importlib.util
36
+ import json
37
+ import os
38
+ import re
39
+ import sys
40
+ import time
41
+ from pathlib import Path
42
+ from types import ModuleType
43
+ from typing import Any
44
+
45
+
46
+ CONTEXT_LENGTH = 4096
47
+
48
+ # Match the training runtime setup before importing the architecture module.
49
+ os.environ.setdefault("USE_HUB_KERNELS", "NO")
50
+ os.environ.setdefault("PYTORCH_ALLOC_CONF", "expandable_segments:True")
51
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
52
+ os.environ.setdefault("USE_ROCM_CK_GEMM", "1")
53
+ os.environ.pop("PYTORCH_HIP_ALLOC_CONF", None)
54
+
55
+
56
+ def is_checkpoint(path: Path) -> bool:
57
+ return (
58
+ path.is_dir()
59
+ and (path / "config.json").is_file()
60
+ and (path / "model.pt").is_file()
61
+ )
62
+
63
+
64
+ def checkpoint_rank(path: Path) -> tuple[int, float, str]:
65
+ matches = re.findall(r"\d+", path.name)
66
+ step = int(matches[-1]) if matches else -1
67
+ try:
68
+ modified = path.stat().st_mtime
69
+ except OSError:
70
+ modified = 0.0
71
+ return step, modified, path.name
72
+
73
+
74
+ def resolve_checkpoint(
75
+ path: Path,
76
+ *,
77
+ extra_bases: list[Path] | None = None,
78
+ ) -> Path:
79
+ """
80
+ Accept an exact checkpoint, a run directory, or its checkpoints directory.
81
+
82
+ Relative paths are searched from:
83
+ 1. the current working directory;
84
+ 2. the inference script directory;
85
+ 3. any supplied extra bases, such as the architecture script directory.
86
+
87
+ Preference within each candidate:
88
+ exact directory -> final/ -> newest immediate checkpoint ->
89
+ newest run/checkpoints checkpoint -> initial/
90
+ """
91
+ raw_path = path.expanduser()
92
+ bases = [
93
+ Path.cwd(),
94
+ Path(__file__).resolve().parent,
95
+ ]
96
+ if extra_bases:
97
+ bases.extend(base.expanduser().resolve() for base in extra_bases)
98
+
99
+ candidate_roots: list[Path] = []
100
+ if raw_path.is_absolute():
101
+ candidate_roots.append(raw_path.resolve())
102
+ else:
103
+ candidate_roots.extend(
104
+ (base / raw_path).resolve()
105
+ for base in bases
106
+ )
107
+
108
+ candidate_roots = list(dict.fromkeys(candidate_roots))
109
+ inspected: list[dict[str, Any]] = []
110
+
111
+ def inspect_root(root: Path) -> Path | None:
112
+ inspected.append(
113
+ {
114
+ "candidate_root": str(root),
115
+ "exists": root.exists(),
116
+ "is_directory": root.is_dir(),
117
+ }
118
+ )
119
+
120
+ if is_checkpoint(root):
121
+ return root
122
+
123
+ final = root / "final"
124
+ if is_checkpoint(final):
125
+ return final
126
+
127
+ direct = (
128
+ sorted(
129
+ (
130
+ child
131
+ for child in root.iterdir()
132
+ if child.is_dir() and is_checkpoint(child)
133
+ ),
134
+ key=checkpoint_rank,
135
+ )
136
+ if root.is_dir()
137
+ else []
138
+ )
139
+ if direct:
140
+ return direct[-1]
141
+
142
+ checkpoint_root = root / "checkpoints"
143
+ nested = (
144
+ sorted(
145
+ (
146
+ child
147
+ for child in checkpoint_root.iterdir()
148
+ if child.is_dir() and is_checkpoint(child)
149
+ ),
150
+ key=checkpoint_rank,
151
+ )
152
+ if checkpoint_root.is_dir()
153
+ else []
154
+ )
155
+ if nested:
156
+ return nested[-1]
157
+
158
+ initial = root / "initial"
159
+ if is_checkpoint(initial):
160
+ print(
161
+ "WARNING: using initial/; this is an untrained model.",
162
+ file=sys.stderr,
163
+ )
164
+ return initial
165
+
166
+ for directory in (root, checkpoint_root):
167
+ if not directory.is_dir():
168
+ continue
169
+ for child in sorted(directory.iterdir()):
170
+ if not child.is_dir():
171
+ continue
172
+ inspected.append(
173
+ {
174
+ "path": str(child),
175
+ "has_config": (child / "config.json").is_file(),
176
+ "has_model_pt": (child / "model.pt").is_file(),
177
+ "files": sorted(
178
+ item.name
179
+ for item in child.iterdir()
180
+ if item.is_file()
181
+ )[:50],
182
+ }
183
+ )
184
+
185
+ return None
186
+
187
+ for candidate_root in candidate_roots:
188
+ resolved = inspect_root(candidate_root)
189
+ if resolved is not None:
190
+ print(
191
+ f"Resolved model path from {candidate_root}",
192
+ file=sys.stderr,
193
+ )
194
+ return resolved
195
+
196
+ raise FileNotFoundError(
197
+ "Could not find a FastDeepHybridLM checkpoint containing both "
198
+ "config.json and model.pt.\n"
199
+ "The supplied --model path was searched relative to the working "
200
+ "directory, inference-script directory, and architecture-script "
201
+ "directory.\n"
202
+ + json.dumps(inspected, indent=2)
203
+ + "\n\nCurrent working directory: "
204
+ + str(Path.cwd())
205
+ + "\nInference script directory: "
206
+ + str(Path(__file__).resolve().parent)
207
+ )
208
+
209
+
210
+ def find_architecture_script(explicit: Path | None) -> Path:
211
+ if explicit is not None:
212
+ path = explicit.expanduser().resolve()
213
+ if not path.is_file():
214
+ raise FileNotFoundError(
215
+ f"Architecture script does not exist: {path}"
216
+ )
217
+ return path
218
+
219
+ here = Path(__file__).resolve().parent
220
+ cwd = Path.cwd()
221
+ candidates = [
222
+ cwd / "bytefalcon_fast60m.py",
223
+ cwd / "bytefalcon.py",
224
+ here / "bytefalcon_fast60m.py",
225
+ here / "bytefalcon.py",
226
+ ]
227
+
228
+ for candidate in candidates:
229
+ if not candidate.is_file():
230
+ continue
231
+ source = candidate.read_text(
232
+ encoding="utf-8",
233
+ errors="replace",
234
+ )
235
+ required = (
236
+ "class Fast60MConfig",
237
+ "def create_model_classes",
238
+ "def load_model_bundle",
239
+ )
240
+ if all(marker in source for marker in required):
241
+ return candidate.resolve()
242
+
243
+ raise FileNotFoundError(
244
+ "Could not locate the custom architecture script. Pass it explicitly:\n"
245
+ " --architecture-script /path/to/bytefalcon_fast60m.py"
246
+ )
247
+
248
+
249
+ def load_architecture_module(path: Path) -> ModuleType:
250
+ module_name = "_bytefast60m_architecture"
251
+ specification = importlib.util.spec_from_file_location(
252
+ module_name,
253
+ path,
254
+ )
255
+ if specification is None or specification.loader is None:
256
+ raise RuntimeError(
257
+ f"Could not create an import specification for {path}"
258
+ )
259
+
260
+ module = importlib.util.module_from_spec(specification)
261
+ # Dataclasses and some runtime machinery expect the module to be present.
262
+ sys.modules[module_name] = module
263
+ specification.loader.exec_module(module)
264
+
265
+ required = (
266
+ "Fast60MConfig",
267
+ "create_model_classes",
268
+ "import_training_stack",
269
+ "load_model_bundle",
270
+ "load_tokenizer",
271
+ )
272
+ missing = [
273
+ name for name in required if not hasattr(module, name)
274
+ ]
275
+ if missing:
276
+ raise RuntimeError(
277
+ f"{path} is not the FastDeepHybridLM training script; "
278
+ f"missing definitions: {missing}"
279
+ )
280
+
281
+ return module
282
+
283
+
284
+ def resolve_tokenizer(
285
+ checkpoint: Path,
286
+ explicit: Path | None,
287
+ ) -> Path:
288
+ candidates: list[Path] = []
289
+
290
+ if explicit is not None:
291
+ candidates.append(explicit.expanduser().resolve())
292
+
293
+ candidates.extend(
294
+ [
295
+ checkpoint,
296
+ checkpoint / "tokenizer",
297
+ ]
298
+ )
299
+
300
+ project = Path(__file__).resolve().parent
301
+ candidates.extend(
302
+ [
303
+ project / "artifacts" / "byte-tokenizer",
304
+ Path.cwd() / "artifacts" / "byte-tokenizer",
305
+ ]
306
+ )
307
+
308
+ for parent in list(checkpoint.parents)[:5]:
309
+ candidates.extend(
310
+ [
311
+ parent / "artifacts" / "byte-tokenizer",
312
+ parent / "byte-tokenizer",
313
+ ]
314
+ )
315
+
316
+ candidates = list(dict.fromkeys(candidates))
317
+ for candidate in candidates:
318
+ if (
319
+ candidate.is_dir()
320
+ and (
321
+ (candidate / "tokenizer.json").is_file()
322
+ or (candidate / "tokenizer.model").is_file()
323
+ )
324
+ ):
325
+ return candidate
326
+
327
+ raise FileNotFoundError(
328
+ "Tokenizer not found. Pass --tokenizer explicitly. Checked:\n"
329
+ + "\n".join(f" - {path}" for path in candidates)
330
+ )
331
+
332
+
333
+ def rewrite_prompt(instruction: str, source_text: str) -> str:
334
+ instruction = instruction.strip()
335
+ source = '"' + source_text + '"'
336
+ if instruction:
337
+ return instruction + "\n\n" + source + '\n\n"'
338
+ return source + '\n\n"'
339
+
340
+
341
+ def control_token_id_map(
342
+ tokenizer: Any,
343
+ architecture: ModuleType,
344
+ ) -> dict[str, int]:
345
+ control_tokens = getattr(
346
+ architecture,
347
+ "CONTROL_TOKENS",
348
+ [
349
+ "<pad>",
350
+ "<bos>",
351
+ "<eos>",
352
+ "<unk>",
353
+ "<instruction>",
354
+ "<text>",
355
+ "<output>",
356
+ "<record>",
357
+ "<byte_start>",
358
+ "<byte_end>",
359
+ ],
360
+ )
361
+ result = {}
362
+ for token in control_tokens:
363
+ token_id = tokenizer.convert_tokens_to_ids(token)
364
+ if token_id is None:
365
+ continue
366
+ token_id = int(token_id)
367
+ if token_id >= 0:
368
+ result[token] = token_id
369
+ return result
370
+
371
+
372
+ def blocked_generation_ids(
373
+ tokenizer: Any,
374
+ architecture: ModuleType,
375
+ ) -> list[int]:
376
+ mapping = control_token_id_map(tokenizer, architecture)
377
+ return sorted(
378
+ token_id
379
+ for token, token_id in mapping.items()
380
+ if token != "<eos>"
381
+ )
382
+
383
+
384
+ def apply_repetition_penalty(
385
+ torch: Any,
386
+ logits: Any,
387
+ input_ids: Any,
388
+ penalty: float,
389
+ ) -> Any:
390
+ if penalty == 1.0:
391
+ return logits
392
+
393
+ used = torch.unique(input_ids)
394
+ selected = logits[:, used]
395
+ logits[:, used] = torch.where(
396
+ selected < 0,
397
+ selected * penalty,
398
+ selected / penalty,
399
+ )
400
+ return logits
401
+
402
+
403
+ def sample_next_token(
404
+ torch: Any,
405
+ logits: Any,
406
+ *,
407
+ temperature: float,
408
+ top_k: int,
409
+ top_p: float,
410
+ ) -> Any:
411
+ if temperature <= 0:
412
+ return logits.argmax(dim=-1, keepdim=True)
413
+
414
+ logits = logits / max(temperature, 1e-5)
415
+
416
+ if top_k > 0:
417
+ top_k = min(top_k, logits.shape[-1])
418
+ threshold = torch.topk(
419
+ logits,
420
+ top_k,
421
+ dim=-1,
422
+ ).values[:, -1:]
423
+ logits = logits.masked_fill(
424
+ logits < threshold,
425
+ -float("inf"),
426
+ )
427
+
428
+ probabilities = torch.softmax(logits, dim=-1)
429
+
430
+ if top_p < 1.0:
431
+ sorted_probabilities, sorted_indices = torch.sort(
432
+ probabilities,
433
+ descending=True,
434
+ dim=-1,
435
+ )
436
+ cumulative = sorted_probabilities.cumsum(dim=-1)
437
+ remove = cumulative > top_p
438
+ remove[:, 1:] = remove[:, :-1].clone()
439
+ remove[:, 0] = False
440
+ sorted_probabilities = sorted_probabilities.masked_fill(
441
+ remove,
442
+ 0.0,
443
+ )
444
+ denominator = sorted_probabilities.sum(
445
+ dim=-1,
446
+ keepdim=True,
447
+ ).clamp_min(1e-12)
448
+ sorted_probabilities = (
449
+ sorted_probabilities / denominator
450
+ )
451
+ sampled = torch.multinomial(
452
+ sorted_probabilities,
453
+ num_samples=1,
454
+ )
455
+ return sorted_indices.gather(-1, sampled)
456
+
457
+ return torch.multinomial(probabilities, num_samples=1)
458
+
459
+
460
+ def clean_completion(text: str, rewrite_mode: bool) -> str:
461
+ for marker in ("<eos>", "<record>", "<pad>"):
462
+ position = text.find(marker)
463
+ if position >= 0:
464
+ text = text[:position]
465
+
466
+ if rewrite_mode:
467
+ text = text.rstrip()
468
+ if text.endswith('"'):
469
+ text = text[:-1]
470
+
471
+ return text
472
+
473
+
474
+ def generate(
475
+ *,
476
+ architecture: ModuleType,
477
+ checkpoint: Path,
478
+ tokenizer_path: Path,
479
+ prompt: str,
480
+ max_new_tokens: int,
481
+ temperature: float,
482
+ top_k: int,
483
+ top_p: float,
484
+ repetition_penalty: float,
485
+ seed: int,
486
+ compile_model: bool,
487
+ compile_mode: str,
488
+ stream: bool,
489
+ rewrite_mode: bool,
490
+ allow_control_tokens: bool,
491
+ show_top_tokens: int,
492
+ ) -> tuple[str, dict[str, Any]]:
493
+ (
494
+ _np,
495
+ torch,
496
+ nn,
497
+ F,
498
+ _DataLoader,
499
+ _Dataset,
500
+ ) = architecture.import_training_stack()
501
+
502
+ if not torch.cuda.is_available():
503
+ raise RuntimeError(
504
+ "ROCm PyTorch did not expose the AMD GPU through torch.cuda."
505
+ )
506
+
507
+ torch.manual_seed(seed)
508
+ torch.cuda.manual_seed_all(seed)
509
+
510
+ device = torch.device("cuda")
511
+ tokenizer = architecture.load_tokenizer(tokenizer_path)
512
+ model = architecture.load_model_bundle(
513
+ checkpoint,
514
+ torch,
515
+ nn,
516
+ F,
517
+ )
518
+ model.to(device)
519
+ model.eval()
520
+
521
+ blocked_ids = (
522
+ []
523
+ if allow_control_tokens
524
+ else blocked_generation_ids(tokenizer, architecture)
525
+ )
526
+ blocked_tensor = (
527
+ torch.tensor(
528
+ blocked_ids,
529
+ device=device,
530
+ dtype=torch.long,
531
+ )
532
+ if blocked_ids
533
+ else None
534
+ )
535
+
536
+ active_model = model
537
+ if compile_model:
538
+ active_model = torch.compile(
539
+ model,
540
+ mode=compile_mode,
541
+ fullgraph=False,
542
+ dynamic=False,
543
+ )
544
+
545
+ encoded = tokenizer(
546
+ prompt,
547
+ add_special_tokens=False,
548
+ return_tensors="pt",
549
+ return_token_type_ids=False,
550
+ )
551
+ input_ids = encoded.input_ids.to(device)
552
+
553
+ prompt_tokens = int(input_ids.shape[1])
554
+ maximum_context = int(
555
+ model.config.max_position_embeddings
556
+ )
557
+ if prompt_tokens >= maximum_context:
558
+ raise ValueError(
559
+ f"Prompt has {prompt_tokens} tokens and exceeds the "
560
+ f"{maximum_context}-token context."
561
+ )
562
+
563
+ max_new_tokens = min(
564
+ max_new_tokens,
565
+ maximum_context - prompt_tokens,
566
+ )
567
+
568
+ eos_id = int(tokenizer.eos_token_id)
569
+ generated_ids: list[int] = []
570
+
571
+ torch.cuda.synchronize()
572
+ torch.cuda.reset_peak_memory_stats()
573
+ started = time.perf_counter()
574
+
575
+ # This architecture has no KV/conv recurrent inference cache. It therefore
576
+ # recomputes the active prefix each step, matching the original CLI.
577
+ with torch.inference_mode():
578
+ for _ in range(max_new_tokens):
579
+ model_input = input_ids[
580
+ :, -maximum_context:
581
+ ]
582
+
583
+ with torch.autocast(
584
+ device_type="cuda",
585
+ dtype=torch.bfloat16,
586
+ enabled=True,
587
+ ):
588
+ logits = active_model(
589
+ input_ids=model_input,
590
+ return_last_logits=True,
591
+ ).logits[:, -1, :]
592
+
593
+ if not bool(torch.isfinite(logits).all().item()):
594
+ print(
595
+ "Non-finite BF16 logits; retrying this token in FP32.",
596
+ file=sys.stderr,
597
+ )
598
+ with torch.autocast(
599
+ device_type="cuda",
600
+ enabled=False,
601
+ ):
602
+ logits = model(
603
+ input_ids=model_input,
604
+ return_last_logits=True,
605
+ ).logits[:, -1, :].float()
606
+
607
+ if not bool(torch.isfinite(logits).all().item()):
608
+ logits = torch.nan_to_num(
609
+ logits,
610
+ nan=-float("inf"),
611
+ posinf=1e4,
612
+ neginf=-1e4,
613
+ )
614
+
615
+ if show_top_tokens > 0:
616
+ top_values, top_indices = torch.topk(
617
+ logits,
618
+ min(show_top_tokens, logits.shape[-1]),
619
+ dim=-1,
620
+ )
621
+ report = [
622
+ {
623
+ "id": int(token_id),
624
+ "token": tokenizer.decode(
625
+ [int(token_id)],
626
+ skip_special_tokens=False,
627
+ clean_up_tokenization_spaces=False,
628
+ ),
629
+ "logit": float(value),
630
+ }
631
+ for token_id, value in zip(
632
+ top_indices[0].tolist(),
633
+ top_values[0].float().tolist(),
634
+ )
635
+ ]
636
+ print(
637
+ "raw top tokens: "
638
+ + json.dumps(report, ensure_ascii=False),
639
+ file=sys.stderr,
640
+ )
641
+
642
+ if blocked_tensor is not None:
643
+ logits.index_fill_(
644
+ 1,
645
+ blocked_tensor,
646
+ -float("inf"),
647
+ )
648
+
649
+ logits = apply_repetition_penalty(
650
+ torch,
651
+ logits,
652
+ model_input,
653
+ repetition_penalty,
654
+ )
655
+
656
+ if not bool(torch.isfinite(logits).any().item()):
657
+ next_token = torch.tensor(
658
+ [[int(tokenizer.eos_token_id)]],
659
+ device=device,
660
+ dtype=torch.long,
661
+ )
662
+ else:
663
+ next_token = sample_next_token(
664
+ torch,
665
+ logits,
666
+ temperature=temperature,
667
+ top_k=top_k,
668
+ top_p=top_p,
669
+ )
670
+
671
+ token_id = int(next_token.item())
672
+ if token_id in blocked_ids:
673
+ raise RuntimeError(
674
+ "A reserved control token escaped masking: "
675
+ f"id={token_id}, token={tokenizer.decode([token_id], skip_special_tokens=False)!r}"
676
+ )
677
+ generated_ids.append(token_id)
678
+ input_ids = torch.cat(
679
+ (input_ids, next_token),
680
+ dim=-1,
681
+ )
682
+
683
+ if stream:
684
+ piece = tokenizer.decode(
685
+ [token_id],
686
+ skip_special_tokens=False,
687
+ clean_up_tokenization_spaces=False,
688
+ )
689
+ print(piece, end="", flush=True)
690
+
691
+ if token_id == eos_id:
692
+ break
693
+
694
+ torch.cuda.synchronize()
695
+ elapsed = time.perf_counter() - started
696
+
697
+ raw_completion = tokenizer.decode(
698
+ generated_ids,
699
+ skip_special_tokens=False,
700
+ clean_up_tokenization_spaces=False,
701
+ )
702
+ completion = clean_completion(
703
+ raw_completion,
704
+ rewrite_mode,
705
+ )
706
+
707
+ if stream:
708
+ print()
709
+
710
+ metrics = {
711
+ "architecture": model.config.architecture,
712
+ "model_type": model.config.model_type,
713
+ "checkpoint": str(checkpoint),
714
+ "tokenizer": str(tokenizer_path),
715
+ "parameters": sum(
716
+ parameter.numel()
717
+ for parameter in model.parameters()
718
+ ),
719
+ "prompt_tokens": prompt_tokens,
720
+ "generated_tokens": len(generated_ids),
721
+ "elapsed_seconds": elapsed,
722
+ "tokens_per_second": (
723
+ len(generated_ids) / elapsed
724
+ if elapsed > 0
725
+ else None
726
+ ),
727
+ "peak_vram_gib": (
728
+ torch.cuda.max_memory_allocated() / (1024**3)
729
+ ),
730
+ "compiled": compile_model,
731
+ "blocked_control_token_ids": blocked_ids,
732
+ "note": (
733
+ "Generation recomputes the active prefix because this custom "
734
+ "architecture does not implement an incremental inference cache."
735
+ ),
736
+ }
737
+
738
+ return completion, metrics
739
+
740
+
741
+ def build_parser() -> argparse.ArgumentParser:
742
+ parser = argparse.ArgumentParser(
743
+ description=(
744
+ "Inference for the custom byte-deep-hybrid FastDeepHybridLM."
745
+ ),
746
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
747
+ )
748
+
749
+ parser.add_argument(
750
+ "--model",
751
+ type=Path,
752
+ default=Path("runs/bytefast-60m"),
753
+ help=(
754
+ "Exact checkpoint, run directory, or checkpoints directory. "
755
+ "Relative paths are searched from the shell, script, and "
756
+ "architecture-script directories."
757
+ ),
758
+ )
759
+ parser.add_argument(
760
+ "--architecture-script",
761
+ type=Path,
762
+ help=(
763
+ "Path to bytefalcon_fast60m.py. Automatically discovered "
764
+ "when omitted."
765
+ ),
766
+ )
767
+ parser.add_argument(
768
+ "--tokenizer",
769
+ type=Path,
770
+ help=(
771
+ "Tokenizer directory. The checkpoint tokenizer is preferred."
772
+ ),
773
+ )
774
+
775
+ input_group = parser.add_mutually_exclusive_group(required=True)
776
+ input_group.add_argument(
777
+ "--prompt",
778
+ help="Raw language-model prompt.",
779
+ )
780
+ input_group.add_argument(
781
+ "--text",
782
+ help="Source text for rewrite mode.",
783
+ )
784
+ parser.add_argument(
785
+ "--instruction",
786
+ default="Rewrite this clearly and naturally.",
787
+ help="Instruction used with --text.",
788
+ )
789
+
790
+ parser.add_argument("--max-new-tokens", type=int, default=128)
791
+ parser.add_argument("--temperature", type=float, default=0.7)
792
+ parser.add_argument("--top-p", type=float, default=0.95)
793
+ parser.add_argument("--top-k", type=int, default=50)
794
+ parser.add_argument(
795
+ "--repetition-penalty",
796
+ type=float,
797
+ default=1.1,
798
+ )
799
+ parser.add_argument("--seed", type=int, default=42)
800
+ parser.add_argument(
801
+ "--stream",
802
+ action=argparse.BooleanOptionalAction,
803
+ default=True,
804
+ )
805
+ parser.add_argument(
806
+ "--metrics",
807
+ action=argparse.BooleanOptionalAction,
808
+ default=True,
809
+ )
810
+ parser.add_argument("--compile", action="store_true")
811
+ parser.add_argument(
812
+ "--compile-mode",
813
+ choices=[
814
+ "default",
815
+ "reduce-overhead",
816
+ "max-autotune",
817
+ ],
818
+ default="reduce-overhead",
819
+ )
820
+ parser.add_argument(
821
+ "--show-prompt",
822
+ action="store_true",
823
+ )
824
+ parser.add_argument(
825
+ "--allow-control-tokens",
826
+ action="store_true",
827
+ help="Allow structural tokens such as <pad>; disabled by default.",
828
+ )
829
+ parser.add_argument(
830
+ "--show-top-tokens",
831
+ type=int,
832
+ default=0,
833
+ help="Print the raw top-N logits before control-token masking.",
834
+ )
835
+
836
+ return parser
837
+
838
+
839
+ def validate_args(args: argparse.Namespace) -> None:
840
+ if args.max_new_tokens <= 0:
841
+ raise ValueError("--max-new-tokens must be positive.")
842
+ if args.temperature < 0:
843
+ raise ValueError("--temperature cannot be negative.")
844
+ if not 0 < args.top_p <= 1:
845
+ raise ValueError("--top-p must be in (0, 1].")
846
+ if args.top_k < 0:
847
+ raise ValueError("--top-k cannot be negative.")
848
+ if args.repetition_penalty <= 0:
849
+ raise ValueError(
850
+ "--repetition-penalty must be positive."
851
+ )
852
+ if args.show_top_tokens < 0:
853
+ raise ValueError("--show-top-tokens must be non-negative.")
854
+
855
+
856
+ def main() -> int:
857
+ args = build_parser().parse_args()
858
+ validate_args(args)
859
+
860
+ architecture_path = find_architecture_script(
861
+ args.architecture_script
862
+ )
863
+ checkpoint = resolve_checkpoint(
864
+ args.model,
865
+ extra_bases=[architecture_path.parent],
866
+ )
867
+ architecture = load_architecture_module(
868
+ architecture_path
869
+ )
870
+ tokenizer_path = resolve_tokenizer(
871
+ checkpoint,
872
+ args.tokenizer,
873
+ )
874
+
875
+ rewrite_mode = args.text is not None
876
+ prompt = (
877
+ rewrite_prompt(args.instruction, args.text)
878
+ if rewrite_mode
879
+ else args.prompt
880
+ )
881
+ assert prompt is not None
882
+
883
+ print(
884
+ json.dumps(
885
+ {
886
+ "checkpoint": str(checkpoint),
887
+ "architecture_script": str(architecture_path),
888
+ "tokenizer": str(tokenizer_path),
889
+ "rewrite_mode": rewrite_mode,
890
+ },
891
+ indent=2,
892
+ ),
893
+ file=sys.stderr,
894
+ )
895
+
896
+ if args.show_prompt:
897
+ print(
898
+ "----- PROMPT -----\n"
899
+ + prompt
900
+ + "\n----- END PROMPT -----",
901
+ file=sys.stderr,
902
+ )
903
+
904
+ completion, metrics = generate(
905
+ architecture=architecture,
906
+ checkpoint=checkpoint,
907
+ tokenizer_path=tokenizer_path,
908
+ prompt=prompt,
909
+ max_new_tokens=args.max_new_tokens,
910
+ temperature=args.temperature,
911
+ top_k=args.top_k,
912
+ top_p=args.top_p,
913
+ repetition_penalty=args.repetition_penalty,
914
+ seed=args.seed,
915
+ compile_model=args.compile,
916
+ compile_mode=args.compile_mode,
917
+ stream=args.stream,
918
+ rewrite_mode=rewrite_mode,
919
+ allow_control_tokens=args.allow_control_tokens,
920
+ show_top_tokens=args.show_top_tokens,
921
+ )
922
+
923
+ if not args.stream:
924
+ print(completion)
925
+
926
+ if args.metrics:
927
+ print(
928
+ "\n" + json.dumps(metrics, indent=2),
929
+ file=sys.stderr,
930
+ )
931
+
932
+ return 0
933
+
934
+
935
+ if __name__ == "__main__":
936
+ try:
937
+ raise SystemExit(main())
938
+ except KeyboardInterrupt:
939
+ print("\nInterrupted.", file=sys.stderr)
940
+ raise SystemExit(130)