Add repetition penalty diagnostic script

#1
by Compactbot - opened
Files changed (1) hide show
  1. repetition_diagnostic.py +293 -0
repetition_diagnostic.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ repetition_probe.py — A builder tool for tiny model developers.
4
+
5
+ Takes a HuggingFace model and probes how different repetition_penalty values
6
+ affect generation quality. Outputs a comparison table with:
7
+ - Repetition rate (fraction of tokens already seen in the output)
8
+ - Unique token ratio (diversity)
9
+ - Loop detection (repeated n-grams)
10
+ - Sample text
11
+
12
+ Usage:
13
+ python3 repetition_probe.py --model kevin-bretz/Small-Language-Model --prompt "Once upon a time"
14
+ python3 repetition_probe.py --model kevin-bretz/Small-Language-Model --prompts "Once upon a time|The cat sat|I want to"
15
+
16
+ Why this matters:
17
+ Tiny models (<50M params) are extremely prone to degenerate repetition loops.
18
+ The default repetition_penalty=1.0 means NO penalty — the model can output the
19
+ same token forever. Most training scripts don't test generation quality, so
20
+ you discover this at inference time with no idea what to do.
21
+
22
+ This tool gives you a quick diagnostic: sweep the penalty, see where quality
23
+ peaks, and know what to set in your inference config.
24
+ """
25
+
26
+ import argparse
27
+ import json
28
+ import sys
29
+ import time
30
+ from collections import Counter
31
+ from dataclasses import dataclass, field
32
+ from typing import List, Optional
33
+
34
+ import torch
35
+ import torch.nn.functional as F
36
+
37
+
38
+ @dataclass
39
+ class ProbeResult:
40
+ prompt: str
41
+ penalty: float
42
+ text: str
43
+ n_tokens: int
44
+ repetition_rate: float # fraction of output tokens that appeared before
45
+ unique_token_ratio: float # unique tokens / total tokens
46
+ max_loop_len: int # longest repeated trigram
47
+ n_repeated_trigrams: int # count of trigrams appearing 2+ times
48
+ elapsed_s: float
49
+
50
+
51
+ def compute_repetition_rate(token_ids: List[int]) -> float:
52
+ """Fraction of tokens (excluding first) that have appeared earlier in the sequence."""
53
+ if len(token_ids) < 2:
54
+ return 0.0
55
+ seen = set()
56
+ repeated = 0
57
+ for t in token_ids[1:]:
58
+ if t in seen:
59
+ repeated += 1
60
+ seen.add(t)
61
+ return repeated / (len(token_ids) - 1)
62
+
63
+
64
+ def compute_unique_ratio(token_ids: List[int]) -> float:
65
+ """Unique tokens / total tokens."""
66
+ if not token_ids:
67
+ return 0.0
68
+ return len(set(token_ids)) / len(token_ids)
69
+
70
+
71
+ def find_repeated_trigrams(token_ids: List[int]) -> tuple:
72
+ """Find the longest repeated trigram and count of repeated trigrams."""
73
+ if len(token_ids) < 6:
74
+ return 0, 0
75
+ trigrams = []
76
+ for i in range(len(token_ids) - 2):
77
+ trigrams.append(tuple(token_ids[i:i+3]))
78
+ counts = Counter(trigrams)
79
+ repeated = sum(1 for c in counts.values() if c >= 2)
80
+ # Longest repeated trigram: find the longest n-gram that repeats
81
+ max_len = 0
82
+ for n in range(3, min(len(token_ids), 20)):
83
+ ngrams = [tuple(token_ids[i:i+n]) for i in range(len(token_ids) - n + 1)]
84
+ ngram_counts = Counter(ngrams)
85
+ if any(c >= 2 for c in ngram_counts.values()):
86
+ max_len = n
87
+ else:
88
+ break
89
+ return max_len, repeated
90
+
91
+
92
+ def generate_with_penalty(
93
+ model,
94
+ tokenizer,
95
+ prompt: str,
96
+ max_new_tokens: int = 64,
97
+ repetition_penalty: float = 1.0,
98
+ temperature: float = 0.8,
99
+ top_k: int = 50,
100
+ device: str = "cpu",
101
+ ) -> tuple:
102
+ """Generate text with a given repetition penalty. Returns (text, token_ids, elapsed)."""
103
+ model.eval()
104
+ input_ids = tokenizer.encode(prompt, return_tensors="pt").to(device)
105
+ gen_start = input_ids.shape[1]
106
+
107
+ with torch.no_grad():
108
+ generated = input_ids
109
+ for _ in range(max_new_tokens):
110
+ outputs = model(generated)
111
+ logits = outputs.logits[0, -1, :]
112
+
113
+ # Apply repetition penalty
114
+ if repetition_penalty != 1.0:
115
+ for i in range(generated.shape[1]):
116
+ tok = generated[0, i].item()
117
+ if logits[tok] > 0:
118
+ logits[tok] /= repetition_penalty
119
+ else:
120
+ logits[tok] *= repetition_penalty
121
+
122
+ # Temperature
123
+ if temperature != 1.0:
124
+ logits = logits / temperature
125
+
126
+ # Top-k
127
+ if top_k > 0:
128
+ top_k_val = min(top_k, logits.shape[-1])
129
+ indices_to_remove = logits < torch.topk(logits, top_k_val)[0][..., -1:]
130
+ logits[indices_to_remove] = float("-inf")
131
+
132
+ probs = F.softmax(logits, dim=-1)
133
+ next_token = torch.multinomial(probs, num_samples=1).unsqueeze(0)
134
+ generated = torch.cat([generated, next_token], dim=1)
135
+
136
+ # Stop on EOS
137
+ if next_token.item() == tokenizer.eos_token_id:
138
+ break
139
+
140
+ elapsed = None # measured outside
141
+ output_ids = generated[0, gen_start:].tolist()
142
+ text = tokenizer.decode(output_ids, skip_special_tokens=True)
143
+ return text, output_ids
144
+
145
+
146
+ def run_probe(
147
+ model_name: str,
148
+ prompts: List[str],
149
+ penalties: List[float],
150
+ max_new_tokens: int = 64,
151
+ temperature: float = 0.8,
152
+ top_k: int = 50,
153
+ device: str = "cpu",
154
+ seed: int = 42,
155
+ ) -> List[ProbeResult]:
156
+ """Run the full probe sweep."""
157
+ print(f"Loading model: {model_name}")
158
+ from transformers import AutoModelForCausalLM, AutoTokenizer
159
+
160
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
161
+ if tokenizer.pad_token is None:
162
+ tokenizer.pad_token = tokenizer.eos_token
163
+ model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=torch.float32).to(device)
164
+
165
+ torch.manual_seed(seed)
166
+ results = []
167
+
168
+ for prompt in prompts:
169
+ for penalty in penalties:
170
+ torch.manual_seed(seed) # Same seed for fair comparison
171
+ start = time.time()
172
+ text, token_ids = generate_with_penalty(
173
+ model, tokenizer, prompt,
174
+ max_new_tokens=max_new_tokens,
175
+ repetition_penalty=penalty,
176
+ temperature=temperature,
177
+ top_k=top_k,
178
+ device=device,
179
+ )
180
+ elapsed = time.time() - start
181
+
182
+ rep_rate = compute_repetition_rate(token_ids)
183
+ unique_ratio = compute_unique_ratio(token_ids)
184
+ max_loop, n_rep_tri = find_repeated_trigrams(token_ids)
185
+
186
+ results.append(ProbeResult(
187
+ prompt=prompt,
188
+ penalty=penalty,
189
+ text=text,
190
+ n_tokens=len(token_ids),
191
+ repetition_rate=rep_rate,
192
+ unique_token_ratio=unique_ratio,
193
+ max_loop_len=max_loop,
194
+ n_repeated_trigrams=n_rep_tri,
195
+ elapsed_s=elapsed,
196
+ ))
197
+ print(f" [{prompt[:20]:20s}] rp={penalty:.1f} rep={rep_rate:.1%} "
198
+ f"uniq={unique_ratio:.1%} max_loop={max_loop} "
199
+ f"n_rep_tri={n_rep_tri} ({len(token_ids)} tokens)")
200
+
201
+ return results
202
+
203
+
204
+ def format_table(results: List[ProbeResult]) -> str:
205
+ """Format results as a readable table."""
206
+ lines = []
207
+ lines.append(f"{'Prompt':<22} {'RP':>4} {'Tokens':>6} {'Repet':>7} {'Unique':>7} {'MaxLoop':>8} {'RepTri':>7}")
208
+ lines.append("-" * 72)
209
+ for r in results:
210
+ lines.append(
211
+ f"{r.prompt[:20]:<22} {r.penalty:>4.1f} {r.n_tokens:>6} "
212
+ f"{r.repetition_rate:>7.1%} {r.unique_token_ratio:>7.1%} "
213
+ f"{r.max_loop_len:>8} {r.n_repeated_trigrams:>7}"
214
+ )
215
+ lines.append("-" * 72)
216
+ lines.append("")
217
+ lines.append("Sample outputs (first 100 chars):")
218
+ lines.append("")
219
+ for r in results:
220
+ lines.append(f" [{r.prompt[:15]:15s}] rp={r.penalty:.1f}: {r.text[:100]}")
221
+ return "\n".join(lines)
222
+
223
+
224
+ def main():
225
+ parser = argparse.ArgumentParser(description="Repetition penalty probe for tiny models")
226
+ parser.add_argument("--model", required=True, help="HuggingFace model name or path")
227
+ parser.add_argument("--prompt", default="Once upon a time", help="Single prompt")
228
+ parser.add_argument("--prompts", default=None, help="Pipe-separated prompts")
229
+ parser.add_argument("--penalties", default="1.0,1.1,1.2,1.3,1.5",
230
+ help="Comma-separated repetition penalty values")
231
+ parser.add_argument("--max-tokens", type=int, default=64, help="Max new tokens to generate")
232
+ parser.add_argument("--temperature", type=float, default=0.8)
233
+ parser.add_argument("--top-k", type=int, default=50)
234
+ parser.add_argument("--device", default="cpu")
235
+ parser.add_argument("--seed", type=int, default=42)
236
+ parser.add_argument("--output", default=None, help="Save JSON results to file")
237
+ args = parser.parse_args()
238
+
239
+ if args.prompts:
240
+ prompts = [p.strip() for p in args.prompts.split("|")]
241
+ else:
242
+ prompts = [args.prompt]
243
+
244
+ penalties = [float(p) for p in args.penalties.split(",")]
245
+
246
+ print(f"Model: {args.model}")
247
+ print(f"Prompts: {prompts}")
248
+ print(f"Penalties: {penalties}")
249
+ print(f"Max tokens: {args.max_tokens}, Temp: {args.temperature}, Top-k: {args.top_k}")
250
+ print()
251
+
252
+ results = run_probe(
253
+ args.model, prompts, penalties,
254
+ max_new_tokens=args.max_tokens,
255
+ temperature=args.temperature,
256
+ top_k=args.top_k,
257
+ device=args.device,
258
+ seed=args.seed,
259
+ )
260
+
261
+ print()
262
+ print(format_table(results))
263
+
264
+ if args.output:
265
+ out = {
266
+ "model": args.model,
267
+ "prompts": prompts,
268
+ "penalties": penalties,
269
+ "max_tokens": args.max_tokens,
270
+ "temperature": args.temperature,
271
+ "top_k": args.top_k,
272
+ "seed": args.seed,
273
+ "results": [
274
+ {
275
+ "prompt": r.prompt,
276
+ "penalty": r.penalty,
277
+ "text": r.text,
278
+ "n_tokens": r.n_tokens,
279
+ "repetition_rate": round(r.repetition_rate, 4),
280
+ "unique_token_ratio": round(r.unique_token_ratio, 4),
281
+ "max_loop_len": r.max_loop_len,
282
+ "n_repeated_trigrams": r.n_repeated_trigrams,
283
+ }
284
+ for r in results
285
+ ],
286
+ }
287
+ with open(args.output, "w") as f:
288
+ json.dump(out, f, indent=2)
289
+ print(f"\nResults saved to {args.output}")
290
+
291
+
292
+ if __name__ == "__main__":
293
+ main()