Files changed (1) hide show
  1. app.py +412 -0
app.py ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import re
5
+ import time
6
+ from dataclasses import dataclass
7
+ from typing import Optional, Tuple
8
+
9
+ import gradio as gr
10
+ import torch
11
+ from transformers import AutoModel, AutoTokenizer
12
+
13
+ MODEL_ID = "fromziro/JetonCount"
14
+ TOKENIZER_ID = "AxiomicLabs/GPT-X2-125M"
15
+
16
+ # Fill these from your training artifact if you want exact parity.
17
+ FEATURE_MEAN = None
18
+ FEATURE_STD = None
19
+ TARGET_OFFSET = 0.0
20
+
21
+ DEFAULT_VOCAB_SIZE = 32_000
22
+
23
+ TEXT = """
24
+ orfffffffdhdeheorihrhehryh3ureh {
25
+ "actual_token_count": 6,
26
+ "prediction": -5.110997403079409e-09,
27
+ "model_latency_ms": 0.35677890000033585,
28
+ "tokenizer_latency_ms": 0.10765600000013364,
29
+ "model_id": "fromziro/JetonCount",
30
+ "tokenizer_id": "fromziro/Er-Tiny-1.3M",
31
+ "vocab_size": 2564,
32
+ "features": {
33
+ "chars": 19.0,
34
+ "words": 4.0,
35
+ "avg_chars_per_word": 3.75,
36
+ "punctuation_ratio": 0.05263157894736842,
37
+ "symbol_ratio": 0.0,
38
+ "longest_word_chars": 4.0,
39
+ "vocab_size": 2564.0
40
+ }
41
+ }{
42
+ "actual_token_count": 6,
43
+ "prediction": -5.110997403079409e-09,
44
+ "model_latency_ms": 0.35677890000033585,
45
+ "tokenizer_latency_ms": 0.10765600000013364,
46
+ "model_id": "fromziro/JetonCount",
47
+ "tokenizer_id": "fromziro/Er-Tiny-1.3M",
48
+ "vocab_size": 2564,
49
+ "features": {
50
+ "chars": 19.0,
51
+ "words": 4.0,
52
+ "avg_chars_per_word": 3.75,
53
+ "punctuation_ratio": 0.05263157894736842,
54
+ "symbol_ratio": 0.0,
55
+ "longest_word_chars": 4.0,
56
+ "vocab_size": 2564.0
57
+ }
58
+ }
59
+ """
60
+
61
+ TOKENIZER_ROUNDS = 100
62
+ MODEL_ROUNDS = 1000
63
+
64
+ PUNCTUATION_CHARS = set(r""".,!?;:'"`~@#$%^&*()-_=+[]{}<>/\|""")
65
+ SYMBOL_CHARS = set(r"""@#$%^&*()-_=+[]{}<>/\|~`""")
66
+
67
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
68
+
69
+
70
+ @dataclass
71
+ class TextStats:
72
+ chars: float
73
+ words: float
74
+ avg_chars_per_word: float
75
+ punctuation_ratio: float
76
+ symbol_ratio: float
77
+ longest_word_chars: float
78
+ vocab_size: float
79
+
80
+
81
+ def _safe_int(value: object, default: int) -> int:
82
+ try:
83
+ return int(float(value))
84
+ except Exception:
85
+ return default
86
+
87
+
88
+ def _clean_tokenizer_id(tokenizer_id: str | None) -> str:
89
+ return (tokenizer_id or "").strip()
90
+
91
+
92
+ @functools.lru_cache(maxsize=2)
93
+ def load_model(model_id: str) -> AutoModel:
94
+ model = AutoModel.from_pretrained(
95
+ model_id,
96
+ trust_remote_code=True,
97
+ )
98
+ model.eval()
99
+ model.to(DEVICE)
100
+ return model
101
+
102
+
103
+ @functools.lru_cache(maxsize=32)
104
+ def load_tokenizer(tokenizer_id: str):
105
+ return AutoTokenizer.from_pretrained(tokenizer_id, use_fast=True)
106
+
107
+
108
+ def get_tokenizer_vocab_size(tokenizer) -> int:
109
+ vocab_size = getattr(tokenizer, "vocab_size", None)
110
+ if isinstance(vocab_size, int) and vocab_size > 0:
111
+ return vocab_size
112
+ try:
113
+ return int(len(tokenizer))
114
+ except Exception:
115
+ return DEFAULT_VOCAB_SIZE
116
+
117
+
118
+ def compute_text_stats(text: str, vocab_size: int) -> TextStats:
119
+ chars = len(text)
120
+ words_list = re.findall(r"\b\w+\b", text, flags=re.UNICODE)
121
+ words = len(words_list)
122
+
123
+ total_word_chars = sum(len(w) for w in words_list)
124
+ avg_chars_per_word = (total_word_chars / words) if words else 0.0
125
+ longest_word_chars = max((len(w) for w in words_list), default=0)
126
+
127
+ if chars:
128
+ punctuation_count = sum(1 for ch in text if ch in PUNCTUATION_CHARS)
129
+ symbol_count = sum(1 for ch in text if ch in SYMBOL_CHARS)
130
+ punctuation_ratio = punctuation_count / chars
131
+ symbol_ratio = symbol_count / chars
132
+ else:
133
+ punctuation_ratio = 0.0
134
+ symbol_ratio = 0.0
135
+
136
+ return TextStats(
137
+ chars=float(chars),
138
+ words=float(words),
139
+ avg_chars_per_word=float(avg_chars_per_word),
140
+ punctuation_ratio=float(punctuation_ratio),
141
+ symbol_ratio=float(symbol_ratio),
142
+ longest_word_chars=float(longest_word_chars),
143
+ vocab_size=float(vocab_size),
144
+ )
145
+
146
+
147
+ def build_feature_tensor(stats: TextStats) -> torch.Tensor:
148
+ base = torch.tensor(
149
+ [
150
+ stats.chars,
151
+ stats.words,
152
+ stats.avg_chars_per_word,
153
+ stats.punctuation_ratio,
154
+ stats.symbol_ratio,
155
+ stats.longest_word_chars,
156
+ stats.vocab_size,
157
+ ],
158
+ dtype=torch.float32,
159
+ )
160
+
161
+ chars, words, avg_chars_per_word, punctuation_ratio, symbol_ratio, longest_word_chars, vocab_size = base
162
+ eps = 1e-6
163
+
164
+ extra = torch.tensor(
165
+ [
166
+ chars / max(words.item(), 1.0),
167
+ words / max(chars.item(), 1.0),
168
+ torch.log1p(torch.clamp(chars, min=0.0)).item(),
169
+ torch.log1p(torch.clamp(words, min=0.0)).item(),
170
+ torch.log1p(torch.clamp(vocab_size, min=0.0)).item(),
171
+ (chars * punctuation_ratio).item(),
172
+ (chars * symbol_ratio).item(),
173
+ (words * avg_chars_per_word).item(),
174
+ (words * punctuation_ratio).item(),
175
+ (longest_word_chars * punctuation_ratio).item(),
176
+ ((avg_chars_per_word + longest_word_chars) * (1.0 + punctuation_ratio + symbol_ratio)).item(),
177
+ ((chars + eps) * (punctuation_ratio + symbol_ratio + eps)).item(),
178
+ ],
179
+ dtype=torch.float32,
180
+ )
181
+
182
+ return torch.cat([base, extra], dim=0)
183
+
184
+
185
+ def standardize_features(x: torch.Tensor) -> torch.Tensor:
186
+ if FEATURE_MEAN is None or FEATURE_STD is None:
187
+ return x
188
+
189
+ mean = torch.tensor(FEATURE_MEAN, dtype=x.dtype, device=x.device)
190
+ std = torch.tensor(FEATURE_STD, dtype=x.dtype, device=x.device)
191
+
192
+ safe_std = torch.where(torch.isfinite(std) & (std != 0), std, torch.ones_like(std))
193
+ safe_mean = torch.where(torch.isfinite(mean), mean, torch.zeros_like(mean))
194
+ return (x - safe_mean) / safe_std
195
+
196
+
197
+ def benchmark_tokenizer(tokenizer, text: str, rounds: int = 100) -> Tuple[int, float]:
198
+ _ = tokenizer(text, add_special_tokens=False)
199
+ start = time.perf_counter()
200
+ actual_count = 0
201
+ for _ in range(rounds):
202
+ ids = tokenizer(text, add_special_tokens=False).input_ids
203
+ actual_count = len(ids)
204
+ elapsed_ms = (time.perf_counter() - start) * 1000.0 / rounds
205
+ return actual_count, elapsed_ms
206
+
207
+
208
+ @torch.inference_mode()
209
+ def benchmark_model(model, feature_tensor: torch.Tensor, rounds: int = 1000) -> Tuple[float, float]:
210
+ x = standardize_features(feature_tensor).unsqueeze(0).to(DEVICE)
211
+
212
+ _ = model(input_features=x)
213
+
214
+ start = time.perf_counter()
215
+ pred = 0.0
216
+ for _ in range(rounds):
217
+ out = model(input_features=x)
218
+ pred = float(out.logits.squeeze().item())
219
+ elapsed_ms = (time.perf_counter() - start) * 1000.0 / rounds
220
+ return pred, elapsed_ms
221
+
222
+
223
+ def update_vocab_lock(tokenizer_repo_id: str):
224
+ repo_id = _clean_tokenizer_id(tokenizer_repo_id)
225
+
226
+ if not repo_id:
227
+ return (
228
+ gr.update(value=DEFAULT_VOCAB_SIZE, interactive=True),
229
+ f"Using manual vocab size. Default tokenizer: `{TOKENIZER_ID}`.",
230
+ None,
231
+ )
232
+
233
+ try:
234
+ tokenizer = load_tokenizer(repo_id)
235
+ vocab_size = get_tokenizer_vocab_size(tokenizer)
236
+ return (
237
+ gr.update(value=vocab_size, interactive=False),
238
+ f"Tokenizer locked to `{repo_id}` with vocab size `{vocab_size}`.",
239
+ vocab_size,
240
+ )
241
+ except Exception as exc:
242
+ return (
243
+ gr.update(interactive=True),
244
+ f"Could not load tokenizer `{repo_id}`: {exc}",
245
+ None,
246
+ )
247
+
248
+
249
+ def run_benchmark(
250
+ text: str,
251
+ vocab_size_value: object,
252
+ tokenizer_repo_id: str,
253
+ locked_vocab_size: Optional[int],
254
+ ):
255
+ text = text or ""
256
+ repo_id = _clean_tokenizer_id(tokenizer_repo_id)
257
+
258
+ try:
259
+ if repo_id:
260
+ tokenizer = load_tokenizer(repo_id)
261
+ resolved_vocab_size = (
262
+ locked_vocab_size if locked_vocab_size is not None else get_tokenizer_vocab_size(tokenizer)
263
+ )
264
+ tokenizer_source = repo_id
265
+ tokenizer_locked = True
266
+ else:
267
+ tokenizer = load_tokenizer(TOKENIZER_ID)
268
+ resolved_vocab_size = _safe_int(vocab_size_value, DEFAULT_VOCAB_SIZE)
269
+ tokenizer_source = TOKENIZER_ID
270
+ tokenizer_locked = False
271
+
272
+ resolved_vocab_size = int(resolved_vocab_size)
273
+ stats = compute_text_stats(text, resolved_vocab_size)
274
+ feature_tensor = build_feature_tensor(stats)
275
+
276
+ actual_count, tokenizer_latency_ms = benchmark_tokenizer(tokenizer, text, rounds=TOKENIZER_ROUNDS)
277
+ model = load_model(MODEL_ID)
278
+ prediction, model_latency_ms = benchmark_model(model, feature_tensor, rounds=MODEL_ROUNDS)
279
+
280
+ result = {
281
+ "actual_token_count": actual_count,
282
+ "prediction": prediction + TARGET_OFFSET,
283
+ "model_latency_ms": model_latency_ms,
284
+ "tokenizer_latency_ms": tokenizer_latency_ms,
285
+ "model_id": MODEL_ID,
286
+ "tokenizer_id": tokenizer_source,
287
+ "vocab_size": resolved_vocab_size,
288
+ "tokenizer_locked": tokenizer_locked,
289
+ "features": {
290
+ "chars": stats.chars,
291
+ "words": stats.words,
292
+ "avg_chars_per_word": stats.avg_chars_per_word,
293
+ "punctuation_ratio": stats.punctuation_ratio,
294
+ "symbol_ratio": stats.symbol_ratio,
295
+ "longest_word_chars": stats.longest_word_chars,
296
+ "vocab_size": stats.vocab_size,
297
+ },
298
+ }
299
+
300
+ status = (
301
+ f"Benchmarked `{tokenizer_source}` on `{DEVICE.type}`. "
302
+ f"Vocab size used: `{resolved_vocab_size}`."
303
+ )
304
+ return result, status
305
+
306
+ except Exception as exc:
307
+ return None, f"Benchmark failed: {exc}"
308
+
309
+
310
+ CSS = """
311
+ :root {
312
+ --background-fill-primary: #000000 !important;
313
+ --background-fill-secondary: #0b0b0b !important;
314
+ --block-background-fill: #0b0b0b !important;
315
+ --body-text-color: #f4f4f5 !important;
316
+ --link-text-color: #ffffff !important;
317
+ --button-primary-background-fill: #ffffff !important;
318
+ --button-primary-text-color: #000000 !important;
319
+ }
320
+
321
+ body, .gradio-container {
322
+ background: #000 !important;
323
+ color: #f4f4f5 !important;
324
+ }
325
+
326
+ .gradio-container {
327
+ max-width: 1200px !important;
328
+ }
329
+
330
+ h1, h2, h3, h4, p, label, span, div {
331
+ color: #f4f4f5 !important;
332
+ }
333
+
334
+ textarea, input, .wrap, .prose, .block, .block-container, .panel, .container {
335
+ background-color: #090909 !important;
336
+ color: #f4f4f5 !important;
337
+ }
338
+
339
+ textarea, input {
340
+ border: 1px solid #2b2b2b !important;
341
+ }
342
+
343
+ button {
344
+ border-radius: 14px !important;
345
+ }
346
+
347
+ #status_box {
348
+ border: 1px solid #222 !important;
349
+ background: #080808 !important;
350
+ padding: 12px !important;
351
+ border-radius: 16px !important;
352
+ }
353
+ """
354
+
355
+
356
+ with gr.Blocks(
357
+ theme=gr.themes.Monochrome(),
358
+ css=CSS,
359
+ title="JetonCount Gradio Bench",
360
+ ) as demo:
361
+ gr.Markdown("# JetonCount Gradio Bench")
362
+ gr.Markdown(
363
+ "Paste text, choose a vocab size, and optionally provide a tokenizer HF repo ID. "
364
+ "When a tokenizer repo ID is present, the vocab field locks to that tokenizer's vocab size for a fair benchmark."
365
+ )
366
+
367
+ locked_vocab_state = gr.State(None)
368
+
369
+ with gr.Row():
370
+ text_in = gr.Textbox(
371
+ label="Text",
372
+ value=TEXT,
373
+ lines=16,
374
+ placeholder="Paste text here...",
375
+ interactive=True,
376
+ )
377
+
378
+ with gr.Row():
379
+ vocab_in = gr.Number(
380
+ label="Vocab size",
381
+ value=DEFAULT_VOCAB_SIZE,
382
+ interactive=True,
383
+ )
384
+ tokenizer_in = gr.Textbox(
385
+ label="Tokenizer HF Repo ID (optional)",
386
+ value="",
387
+ placeholder="e.g. AxiomicLabs/GPT-X2-125M",
388
+ interactive=True,
389
+ )
390
+
391
+ status = gr.Markdown(
392
+ value=f"Using manual vocab size. Default tokenizer: `{TOKENIZER_ID}`.",
393
+ elem_id="status_box",
394
+ )
395
+
396
+ run_btn = gr.Button("Benchmark", variant="primary")
397
+ result_out = gr.JSON(label="Result")
398
+
399
+ tokenizer_in.blur(
400
+ fn=update_vocab_lock,
401
+ inputs=tokenizer_in,
402
+ outputs=[vocab_in, status, locked_vocab_state],
403
+ )
404
+
405
+ run_btn.click(
406
+ fn=run_benchmark,
407
+ inputs=[text_in, vocab_in, tokenizer_in, locked_vocab_state],
408
+ outputs=[result_out, status],
409
+ )
410
+
411
+ if __name__ == "__main__":
412
+ demo.launch()