PhysiQuanty commited on
Commit
b85c5c9
·
verified ·
1 Parent(s): c49baa1

Create inference.py

Browse files
Files changed (1) hide show
  1. inference.py +643 -0
inference.py ADDED
@@ -0,0 +1,643 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import random
4
+ from typing import Dict, List, Optional, Set
5
+
6
+ import torch
7
+ import torch.nn.functional as F
8
+ from transformers import AutoModelForCausalLM, AutoTokenizer
9
+
10
+
11
+ IM_START = "[IM_START]"
12
+ IM_END = "[IM_END]"
13
+ NO_THINK = "/no think"
14
+
15
+
16
+ # ============================================================
17
+ # UTILS
18
+ # ============================================================
19
+
20
+ def get_dtype(name: str):
21
+ name = str(name).lower()
22
+
23
+ if name in {"bf16", "bfloat16"}:
24
+ return torch.bfloat16
25
+
26
+ if name in {"fp16", "float16", "half"}:
27
+ return torch.float16
28
+
29
+ if name in {"fp32", "float32", "float"}:
30
+ return torch.float32
31
+
32
+ raise ValueError(f"Unknown dtype: {name}")
33
+
34
+
35
+ def set_seed(seed: int):
36
+ if seed is None:
37
+ return
38
+
39
+ if seed < 0:
40
+ seed = random.randint(0, 2**31 - 1)
41
+ print(f"[INFO] random seed: {seed}")
42
+
43
+ random.seed(seed)
44
+ torch.manual_seed(seed)
45
+
46
+ if torch.cuda.is_available():
47
+ torch.cuda.manual_seed_all(seed)
48
+
49
+
50
+ def build_prompt(args) -> str:
51
+ if args.no_think:
52
+ return (
53
+ f"{IM_START}user\n"
54
+ f"{args.prompt} {NO_THINK}"
55
+ f"{IM_END}\n"
56
+ f"{IM_START}assistant\n"
57
+ "<think>\n</think>\n"
58
+ )
59
+
60
+ return args.prompt
61
+
62
+
63
+ def decode(tokenizer, ids: List[int]) -> str:
64
+ return tokenizer.decode(ids, skip_special_tokens=False)
65
+
66
+
67
+ def extract_completion(full_text: str, prompt: str) -> str:
68
+ if full_text.startswith(prompt):
69
+ return full_text[len(prompt):]
70
+
71
+ pos = full_text.rfind(prompt)
72
+ if pos != -1:
73
+ return full_text[pos + len(prompt):]
74
+
75
+ return full_text
76
+
77
+
78
+ def strip_after_stop_text(text: str, stop_strings: List[str]) -> str:
79
+ best = None
80
+
81
+ for s in stop_strings:
82
+ if not s:
83
+ continue
84
+
85
+ pos = text.find(s)
86
+ if pos != -1:
87
+ if best is None or pos < best:
88
+ best = pos
89
+
90
+ if best is None:
91
+ return text
92
+
93
+ return text[:best]
94
+
95
+
96
+ def build_stop_sequences(tokenizer, stop_strings: List[str]) -> List[List[int]]:
97
+ out = []
98
+
99
+ for s in stop_strings:
100
+ ids = tokenizer.encode(s, add_special_tokens=False)
101
+ if ids:
102
+ out.append(ids)
103
+
104
+ return out
105
+
106
+
107
+ def endswith_sequence(ids: List[int], suffix: List[int]) -> bool:
108
+ if not suffix:
109
+ return False
110
+
111
+ if len(ids) < len(suffix):
112
+ return False
113
+
114
+ return ids[-len(suffix):] == suffix
115
+
116
+
117
+ # ============================================================
118
+ # SAMPLING
119
+ # ============================================================
120
+
121
+ def apply_repetition_penalty(
122
+ logits: torch.Tensor,
123
+ generated_ids: List[int],
124
+ penalty: float,
125
+ ) -> torch.Tensor:
126
+ if penalty is None or penalty == 1.0:
127
+ return logits
128
+
129
+ if penalty <= 0:
130
+ raise ValueError("--repetition-penalty must be > 0")
131
+
132
+ for tid in set(generated_ids):
133
+ if tid < 0 or tid >= logits.numel():
134
+ continue
135
+
136
+ if logits[tid] > 0:
137
+ logits[tid] = logits[tid] / penalty
138
+ else:
139
+ logits[tid] = logits[tid] * penalty
140
+
141
+ return logits
142
+
143
+
144
+ def apply_frequency_presence_penalty(
145
+ logits: torch.Tensor,
146
+ generated_ids: List[int],
147
+ frequency_penalty: float,
148
+ presence_penalty: float,
149
+ ) -> torch.Tensor:
150
+ if not generated_ids:
151
+ return logits
152
+
153
+ if frequency_penalty == 0.0 and presence_penalty == 0.0:
154
+ return logits
155
+
156
+ counts: Dict[int, int] = {}
157
+
158
+ for tid in generated_ids:
159
+ counts[tid] = counts.get(tid, 0) + 1
160
+
161
+ for tid, count in counts.items():
162
+ if tid < 0 or tid >= logits.numel():
163
+ continue
164
+
165
+ if frequency_penalty:
166
+ logits[tid] -= frequency_penalty * count
167
+
168
+ if presence_penalty:
169
+ logits[tid] -= presence_penalty
170
+
171
+ return logits
172
+
173
+
174
+ def get_banned_ngram_tokens(
175
+ generated_ids: List[int],
176
+ no_repeat_ngram_size: int,
177
+ ) -> Set[int]:
178
+ n = no_repeat_ngram_size
179
+ banned = set()
180
+
181
+ if n <= 0:
182
+ return banned
183
+
184
+ if len(generated_ids) + 1 < n:
185
+ return banned
186
+
187
+ prefix_len = n - 1
188
+ current_prefix = tuple(generated_ids[-prefix_len:])
189
+
190
+ ngram_map = {}
191
+
192
+ for i in range(len(generated_ids) - n + 1):
193
+ prefix = tuple(generated_ids[i:i + prefix_len])
194
+ next_token = generated_ids[i + prefix_len]
195
+
196
+ if prefix not in ngram_map:
197
+ ngram_map[prefix] = set()
198
+
199
+ ngram_map[prefix].add(next_token)
200
+
201
+ banned.update(ngram_map.get(current_prefix, set()))
202
+ return banned
203
+
204
+
205
+ def apply_no_repeat_ngram(
206
+ logits: torch.Tensor,
207
+ generated_ids: List[int],
208
+ no_repeat_ngram_size: int,
209
+ ) -> torch.Tensor:
210
+ if no_repeat_ngram_size <= 0:
211
+ return logits
212
+
213
+ banned = get_banned_ngram_tokens(
214
+ generated_ids=generated_ids,
215
+ no_repeat_ngram_size=no_repeat_ngram_size,
216
+ )
217
+
218
+ for tid in banned:
219
+ if 0 <= tid < logits.numel():
220
+ logits[tid] = -float("inf")
221
+
222
+ return logits
223
+
224
+
225
+ def apply_top_k(logits: torch.Tensor, top_k: int) -> torch.Tensor:
226
+ if top_k is None or top_k <= 0:
227
+ return logits
228
+
229
+ top_k = min(top_k, logits.size(-1))
230
+ values, _ = torch.topk(logits, top_k)
231
+ cutoff = values[-1]
232
+
233
+ logits[logits < cutoff] = -float("inf")
234
+ return logits
235
+
236
+
237
+ def apply_top_p(logits: torch.Tensor, top_p: float) -> torch.Tensor:
238
+ if top_p is None or top_p >= 1.0:
239
+ return logits
240
+
241
+ if top_p <= 0:
242
+ raise ValueError("--top-p must be > 0")
243
+
244
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True)
245
+ sorted_probs = F.softmax(sorted_logits, dim=-1)
246
+ cumulative = torch.cumsum(sorted_probs, dim=-1)
247
+
248
+ remove = cumulative > top_p
249
+ remove[1:] = remove[:-1].clone()
250
+ remove[0] = False
251
+
252
+ indices_to_remove = sorted_indices[remove]
253
+ logits[indices_to_remove] = -float("inf")
254
+
255
+ return logits
256
+
257
+
258
+ def apply_min_p(logits: torch.Tensor, min_p: float) -> torch.Tensor:
259
+ if min_p is None or min_p <= 0:
260
+ return logits
261
+
262
+ probs = F.softmax(logits, dim=-1)
263
+ max_prob = torch.max(probs)
264
+
265
+ keep = probs >= (min_p * max_prob)
266
+ logits[~keep] = -float("inf")
267
+
268
+ return logits
269
+
270
+
271
+ def apply_typical_p(logits: torch.Tensor, typical_p: float) -> torch.Tensor:
272
+ if typical_p is None or typical_p >= 1.0:
273
+ return logits
274
+
275
+ if typical_p <= 0:
276
+ raise ValueError("--typical-p must be > 0")
277
+
278
+ probs = F.softmax(logits, dim=-1)
279
+ log_probs = F.log_softmax(logits, dim=-1)
280
+
281
+ entropy = -(probs * log_probs).sum()
282
+ shifted_scores = torch.abs((-log_probs) - entropy)
283
+
284
+ sorted_scores, sorted_indices = torch.sort(shifted_scores, descending=False)
285
+ sorted_probs = probs[sorted_indices]
286
+ cumulative_probs = torch.cumsum(sorted_probs, dim=-1)
287
+
288
+ remove = cumulative_probs > typical_p
289
+ remove[1:] = remove[:-1].clone()
290
+ remove[0] = False
291
+
292
+ indices_to_remove = sorted_indices[remove]
293
+ logits[indices_to_remove] = -float("inf")
294
+
295
+ return logits
296
+
297
+
298
+ def apply_bad_words(
299
+ logits: torch.Tensor,
300
+ tokenizer,
301
+ bad_words: List[str],
302
+ ):
303
+ for word in bad_words:
304
+ ids = tokenizer.encode(word, add_special_tokens=False)
305
+ if len(ids) == 1:
306
+ tid = ids[0]
307
+ if 0 <= tid < logits.numel():
308
+ logits[tid] = -float("inf")
309
+
310
+ return logits
311
+
312
+
313
+ def sample_next_token(
314
+ logits: torch.Tensor,
315
+ generated_ids: List[int],
316
+ tokenizer,
317
+ args,
318
+ ) -> int:
319
+ logits = logits.float().clone()
320
+
321
+ logits = apply_bad_words(
322
+ logits=logits,
323
+ tokenizer=tokenizer,
324
+ bad_words=args.bad_words,
325
+ )
326
+
327
+ logits = apply_repetition_penalty(
328
+ logits=logits,
329
+ generated_ids=generated_ids,
330
+ penalty=args.repetition_penalty,
331
+ )
332
+
333
+ logits = apply_frequency_presence_penalty(
334
+ logits=logits,
335
+ generated_ids=generated_ids,
336
+ frequency_penalty=args.frequency_penalty,
337
+ presence_penalty=args.presence_penalty,
338
+ )
339
+
340
+ logits = apply_no_repeat_ngram(
341
+ logits=logits,
342
+ generated_ids=generated_ids,
343
+ no_repeat_ngram_size=args.no_repeat_ngram_size,
344
+ )
345
+
346
+ if args.temperature <= 0:
347
+ return int(torch.argmax(logits).item())
348
+
349
+ logits = logits / args.temperature
350
+
351
+ logits = apply_top_k(logits, args.top_k)
352
+ logits = apply_top_p(logits, args.top_p)
353
+ logits = apply_min_p(logits, args.min_p)
354
+ logits = apply_typical_p(logits, args.typical_p)
355
+
356
+ probs = F.softmax(logits, dim=-1)
357
+
358
+ if torch.isnan(probs).any() or torch.isinf(probs).any() or probs.sum() <= 0:
359
+ return int(torch.argmax(logits).item())
360
+
361
+ return int(torch.multinomial(probs, num_samples=1).item())
362
+
363
+
364
+ # ============================================================
365
+ # MODEL
366
+ # ============================================================
367
+
368
+ def model_forward_logits(model, input_ids: torch.Tensor):
369
+ out = model(input_ids=input_ids)
370
+
371
+ if hasattr(out, "logits"):
372
+ return out.logits
373
+
374
+ if isinstance(out, tuple):
375
+ return out[0]
376
+
377
+ raise RuntimeError("Impossible de récupérer logits depuis la sortie du modèle.")
378
+
379
+
380
+ @torch.no_grad()
381
+ def generate_manual(
382
+ model,
383
+ tokenizer,
384
+ input_ids: torch.Tensor,
385
+ args,
386
+ ) -> torch.Tensor:
387
+ idx = input_ids
388
+ generated_after_prompt: List[int] = []
389
+
390
+ stop_sequences = build_stop_sequences(
391
+ tokenizer,
392
+ stop_strings=args.stop_strings,
393
+ )
394
+
395
+ eos_id = tokenizer.eos_token_id
396
+
397
+ for step in range(args.max_new_tokens):
398
+ idx_cond = idx[:, -args.ctx_len:] if args.ctx_len > 0 else idx
399
+
400
+ logits = model_forward_logits(model, idx_cond)
401
+ logits = logits[:, -1, :][0]
402
+
403
+ if step < args.min_new_tokens:
404
+ if eos_id is not None and 0 <= eos_id < logits.numel():
405
+ logits[eos_id] = -float("inf")
406
+
407
+ for seq in stop_sequences:
408
+ if len(seq) == 1:
409
+ tid = seq[0]
410
+ if 0 <= tid < logits.numel():
411
+ logits[tid] = -float("inf")
412
+
413
+ next_id = sample_next_token(
414
+ logits=logits,
415
+ generated_ids=generated_after_prompt,
416
+ tokenizer=tokenizer,
417
+ args=args,
418
+ )
419
+
420
+ next_tensor = torch.tensor(
421
+ [[next_id]],
422
+ dtype=torch.long,
423
+ device=idx.device,
424
+ )
425
+
426
+ idx = torch.cat([idx, next_tensor], dim=1)
427
+ generated_after_prompt.append(next_id)
428
+
429
+ full_ids = idx[0].tolist()
430
+
431
+ if step >= args.min_new_tokens:
432
+ if eos_id is not None and next_id == eos_id:
433
+ break
434
+
435
+ should_stop = False
436
+
437
+ for seq in stop_sequences:
438
+ if endswith_sequence(full_ids, seq):
439
+ should_stop = True
440
+ break
441
+
442
+ if should_stop:
443
+ break
444
+
445
+ return idx
446
+
447
+
448
+ # ============================================================
449
+ # CLI
450
+ # ============================================================
451
+
452
+ def parse_args():
453
+ p = argparse.ArgumentParser(
454
+ description="Inference script for Arithmetic-SLM using [IM_START]/[IM_END]."
455
+ )
456
+
457
+ p.add_argument(
458
+ "--model",
459
+ default="PhysiQuanty/Arithmetic-SLM",
460
+ help="HF model id or local path.",
461
+ )
462
+
463
+ p.add_argument(
464
+ "--prompt",
465
+ default="59 + 45 =",
466
+ help="Raw arithmetic prompt. With --no-think, inserted in chat template.",
467
+ )
468
+
469
+ p.add_argument(
470
+ "--no-think",
471
+ action="store_true",
472
+ help="Use production no-think template with [IM_START]/[IM_END].",
473
+ )
474
+
475
+ p.add_argument(
476
+ "--device",
477
+ default="cuda" if torch.cuda.is_available() else "cpu",
478
+ )
479
+
480
+ p.add_argument(
481
+ "--dtype",
482
+ default="bfloat16",
483
+ choices=["bfloat16", "bf16", "float16", "fp16", "float32", "fp32"],
484
+ )
485
+
486
+ p.add_argument("--ctx-len", type=int, default=2048)
487
+ p.add_argument("--max-new-tokens", type=int, default=64)
488
+ p.add_argument("--min-new-tokens", type=int, default=1)
489
+
490
+ p.add_argument("--temperature", type=float, default=0.7)
491
+ p.add_argument("--top-k", type=int, default=40)
492
+ p.add_argument("--top-p", type=float, default=0.90)
493
+ p.add_argument("--min-p", type=float, default=0.0)
494
+ p.add_argument("--typical-p", type=float, default=1.0)
495
+
496
+ p.add_argument("--repetition-penalty", type=float, default=1.05)
497
+ p.add_argument("--frequency-penalty", type=float, default=0.10)
498
+ p.add_argument("--presence-penalty", type=float, default=0.0)
499
+ p.add_argument("--no-repeat-ngram-size", type=int, default=4)
500
+
501
+ p.add_argument("--seed", type=int, default=-1)
502
+
503
+ p.add_argument(
504
+ "--stop-string",
505
+ action="append",
506
+ default=None,
507
+ help="Additional stop string. Can be passed multiple times.",
508
+ )
509
+
510
+ p.add_argument(
511
+ "--bad-word",
512
+ action="append",
513
+ default=None,
514
+ help="Single-token word/token to ban. Can be passed multiple times.",
515
+ )
516
+
517
+ p.add_argument(
518
+ "--print-full",
519
+ action="store_true",
520
+ help="Print full prompt + completion.",
521
+ )
522
+
523
+ p.add_argument(
524
+ "--trust-remote-code",
525
+ action="store_true",
526
+ default=True,
527
+ )
528
+
529
+ args = p.parse_args()
530
+
531
+ if args.max_new_tokens <= 0:
532
+ raise ValueError("--max-new-tokens must be > 0")
533
+
534
+ if args.min_new_tokens < 0:
535
+ raise ValueError("--min-new-tokens must be >= 0")
536
+
537
+ if args.temperature < 0:
538
+ raise ValueError("--temperature must be >= 0")
539
+
540
+ if args.top_k < 0:
541
+ raise ValueError("--top-k must be >= 0")
542
+
543
+ if not (0 < args.top_p <= 1.0):
544
+ raise ValueError("--top-p must be in (0, 1]")
545
+
546
+ if args.min_p < 0:
547
+ raise ValueError("--min-p must be >= 0")
548
+
549
+ if not (0 < args.typical_p <= 1.0):
550
+ raise ValueError("--typical-p must be in (0, 1]")
551
+
552
+ if args.repetition_penalty <= 0:
553
+ raise ValueError("--repetition-penalty must be > 0")
554
+
555
+ if args.no_repeat_ngram_size < 0:
556
+ raise ValueError("--no-repeat-ngram-size must be >= 0")
557
+
558
+ stop_strings = [
559
+ IM_END,
560
+ IM_START,
561
+ ]
562
+
563
+ if args.stop_string:
564
+ stop_strings.extend(args.stop_string)
565
+
566
+ args.stop_strings = stop_strings
567
+
568
+ bad_words = []
569
+
570
+ if args.bad_word:
571
+ bad_words.extend(args.bad_word)
572
+
573
+ args.bad_words = bad_words
574
+
575
+ return args
576
+
577
+
578
+ def main():
579
+ args = parse_args()
580
+ set_seed(args.seed)
581
+
582
+ dtype = get_dtype(args.dtype)
583
+
584
+ print(f"[INFO] model: {args.model}")
585
+ print(f"[INFO] device: {args.device}")
586
+ print(f"[INFO] dtype: {args.dtype}")
587
+ print(f"[INFO] template: {'no_think' if args.no_think else 'raw'}")
588
+ print(f"[INFO] IM_START: {IM_START}")
589
+ print(f"[INFO] IM_END: {IM_END}")
590
+ print(f"[INFO] NO_THINK: {NO_THINK}")
591
+ print(f"[INFO] temperature: {args.temperature}")
592
+ print(f"[INFO] top_k: {args.top_k}")
593
+ print(f"[INFO] top_p: {args.top_p}")
594
+ print(f"[INFO] min_p: {args.min_p}")
595
+ print(f"[INFO] typical_p: {args.typical_p}")
596
+ print()
597
+
598
+ tokenizer = AutoTokenizer.from_pretrained(
599
+ args.model,
600
+ trust_remote_code=args.trust_remote_code,
601
+ )
602
+
603
+ model = AutoModelForCausalLM.from_pretrained(
604
+ args.model,
605
+ dtype=dtype,
606
+ trust_remote_code=args.trust_remote_code,
607
+ ).to(args.device)
608
+
609
+ model.eval()
610
+
611
+ prompt = build_prompt(args)
612
+
613
+ encoded = tokenizer(
614
+ prompt,
615
+ return_tensors="pt",
616
+ add_special_tokens=False,
617
+ )
618
+
619
+ encoded.pop("token_type_ids", None)
620
+
621
+ input_ids = encoded["input_ids"].to(args.device)
622
+
623
+ output_ids = generate_manual(
624
+ model=model,
625
+ tokenizer=tokenizer,
626
+ input_ids=input_ids,
627
+ args=args,
628
+ )
629
+
630
+ full_text = decode(tokenizer, output_ids[0].tolist())
631
+
632
+ if args.print_full:
633
+ print(full_text)
634
+ return
635
+
636
+ completion = extract_completion(full_text, prompt)
637
+ completion = strip_after_stop_text(completion, args.stop_strings)
638
+
639
+ print(completion)
640
+
641
+
642
+ if __name__ == "__main__":
643
+ main()