multimodalart HF Staff commited on
Commit
cc0be50
·
verified ·
1 Parent(s): 9324ec7

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. README.md +10 -7
  2. app.py +184 -345
  3. requirements.txt +1 -1
README.md CHANGED
@@ -1,20 +1,23 @@
1
  ---
2
- title: Arithmetic-SLM Playground
3
  emoji: 🧮
4
  colorFrom: green
5
  colorTo: pink
6
  sdk: gradio
7
  sdk_version: 6.19.0
8
  app_file: app.py
9
- short_description: Tiny 31.7M-param arithmetic model playground
10
  python_version: "3.12"
11
  startup_duration_timeout: 30m
12
  ---
13
 
14
- # Arithmetic-SLM Playground
15
 
16
- A Gradio demo for [WhirlwindAI/Arithmetic-SLM](https://huggingface.co/WhirlwindAI/Arithmetic-SLM),
17
- a tiny 31.7M-parameter language model specialized for arithmetic continuation.
 
 
18
 
19
- Enter an arithmetic expression ending with `=` (e.g. `(10 + 28) * 3 =`) and the model
20
- completes it with the answer. Supports `+`, `-`, `*`, `/`, parentheses, and decimals.
 
 
1
  ---
2
+ title: Arithmetic-SLM
3
  emoji: 🧮
4
  colorFrom: green
5
  colorTo: pink
6
  sdk: gradio
7
  sdk_version: 6.19.0
8
  app_file: app.py
9
+ short_description: Tiny 31.7M SLM that solves arithmetic expressions
10
  python_version: "3.12"
11
  startup_duration_timeout: 30m
12
  ---
13
 
14
+ # Arithmetic-SLM
15
 
16
+ An interactive demo for [WhirlwindAI/Arithmetic-SLM](https://huggingface.co/WhirlwindAI/Arithmetic-SLM),
17
+ a tiny (31.7M parameter) specialized language model that completes arithmetic
18
+ expressions token by token. It handles operator precedence, parentheses, and
19
+ decimals.
20
 
21
+ Inference is ported 1:1 from the model repo's `inference.py` reference
22
+ implementation (custom manual sampling loop with the pure-torch attention
23
+ backend), running on ZeroGPU.
app.py CHANGED
@@ -1,58 +1,44 @@
1
- import spaces # MUST come before torch / any CUDA-touching import
2
- import os
3
- import sys
4
  import random
5
- from typing import List, Optional, Set, Dict
6
-
7
- # Add custom_model to path before importing torch/transformers
8
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
9
 
 
10
  import torch
11
- import torch.nn as nn
12
  import torch.nn.functional as F
13
  import gradio as gr
14
- from transformers import AutoTokenizer
15
- from custom_model.modeling_tiny_gpt import TinyGPTForCausalLM
16
- from custom_model.configuration_tiny_gpt import TinyGPTConfig
17
- from huggingface_hub import hf_hub_download
18
 
19
  MODEL_ID = "WhirlwindAI/Arithmetic-SLM"
20
 
21
  IM_START = "[IM_START]"
22
  IM_END = "[IM_END]"
23
  NO_THINK = "/no think"
 
24
 
25
- # ============================================================
26
- # Model + tokenizer loaded at module scope
27
- # ============================================================
28
- tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
29
 
30
- # Load config from the hub, then instantiate our patched custom model
31
- # We use trust_remote_code=True for the config (to download the custom
32
- # config class) but load weights manually to avoid the remote custom
33
- # modeling code shadowing our patched local version.
34
- config = TinyGPTConfig.from_pretrained(MODEL_ID, trust_remote_code=True)
35
-
36
- # Download the safetensors weights
37
- from safetensors.torch import load_file
38
- weights_path = hf_hub_download(MODEL_ID, "model.safetensors")
39
- state_dict = load_file(weights_path)
40
-
41
- # Instantiate the model from config and load weights
42
- model = TinyGPTForCausalLM(config)
43
- model.load_state_dict(state_dict, strict=False)
44
- model = model.to(dtype=torch.bfloat16).to("cuda")
45
  model.eval()
46
 
47
- # ============================================================
48
- # Sampling utilities (ported from the model's inference.py)
49
- # ============================================================
50
 
51
- def apply_repetition_penalty(logits: torch.Tensor, generated_ids: List[int], penalty: float) -> torch.Tensor:
 
 
 
 
52
  if penalty is None or penalty == 1.0:
53
  return logits
54
- if penalty <= 0:
55
- raise ValueError("repetition_penalty must be > 0")
56
  for tid in set(generated_ids):
57
  if tid < 0 or tid >= logits.numel():
58
  continue
@@ -63,12 +49,7 @@ def apply_repetition_penalty(logits: torch.Tensor, generated_ids: List[int], pen
63
  return logits
64
 
65
 
66
- def apply_frequency_presence_penalty(
67
- logits: torch.Tensor,
68
- generated_ids: List[int],
69
- frequency_penalty: float,
70
- presence_penalty: float,
71
- ) -> torch.Tensor:
72
  if not generated_ids:
73
  return logits
74
  if frequency_penalty == 0.0 and presence_penalty == 0.0:
@@ -86,7 +67,7 @@ def apply_frequency_presence_penalty(
86
  return logits
87
 
88
 
89
- def get_banned_ngram_tokens(generated_ids: List[int], no_repeat_ngram_size: int) -> Set[int]:
90
  n = no_repeat_ngram_size
91
  banned: Set[int] = set()
92
  if n <= 0:
@@ -97,16 +78,14 @@ def get_banned_ngram_tokens(generated_ids: List[int], no_repeat_ngram_size: int)
97
  current_prefix = tuple(generated_ids[-prefix_len:])
98
  ngram_map: Dict[tuple, Set[int]] = {}
99
  for i in range(len(generated_ids) - n + 1):
100
- prefix = tuple(generated_ids[i : i + prefix_len])
101
  next_token = generated_ids[i + prefix_len]
102
- if prefix not in ngram_map:
103
- ngram_map[prefix] = set()
104
- ngram_map[prefix].add(next_token)
105
  banned.update(ngram_map.get(current_prefix, set()))
106
  return banned
107
 
108
 
109
- def apply_no_repeat_ngram(logits: torch.Tensor, generated_ids: List[int], no_repeat_ngram_size: int) -> torch.Tensor:
110
  if no_repeat_ngram_size <= 0:
111
  return logits
112
  banned = get_banned_ngram_tokens(generated_ids, no_repeat_ngram_size)
@@ -116,7 +95,7 @@ def apply_no_repeat_ngram(logits: torch.Tensor, generated_ids: List[int], no_rep
116
  return logits
117
 
118
 
119
- def apply_top_k(logits: torch.Tensor, top_k: int) -> torch.Tensor:
120
  if top_k is None or top_k <= 0:
121
  return logits
122
  top_k = min(top_k, logits.size(-1))
@@ -126,11 +105,11 @@ def apply_top_k(logits: torch.Tensor, top_k: int) -> torch.Tensor:
126
  return logits
127
 
128
 
129
- def apply_top_p(logits: torch.Tensor, top_p: float) -> torch.Tensor:
130
  if top_p is None or top_p >= 1.0:
131
  return logits
132
  if top_p <= 0:
133
- raise ValueError("top_p must be > 0")
134
  sorted_logits, sorted_indices = torch.sort(logits, descending=True)
135
  sorted_probs = F.softmax(sorted_logits, dim=-1)
136
  cumulative = torch.cumsum(sorted_probs, dim=-1)
@@ -142,131 +121,54 @@ def apply_top_p(logits: torch.Tensor, top_p: float) -> torch.Tensor:
142
  return logits
143
 
144
 
145
- def build_stop_sequences(stop_strings: List[str]) -> List[List[int]]:
146
- out: List[List[int]] = []
147
- for s in stop_strings:
148
- ids = tokenizer.encode(s, add_special_tokens=False)
149
- if ids:
150
- out.append(ids)
151
- return out
152
-
153
-
154
- def endswith_sequence(ids: List[int], suffix: List[int]) -> bool:
155
- if not suffix:
156
- return False
157
- if len(ids) < len(suffix):
158
- return False
159
- return ids[-len(suffix) :] == suffix
160
-
161
-
162
- def sample_next_token(
163
- logits: torch.Tensor,
164
- generated_ids: List[int],
165
- temperature: float,
166
- top_k: int,
167
- top_p: float,
168
- repetition_penalty: float,
169
- frequency_penalty: float,
170
- presence_penalty: float,
171
- no_repeat_ngram_size: int,
172
- ) -> int:
173
  logits = logits.float().clone()
174
  logits = apply_repetition_penalty(logits, generated_ids, repetition_penalty)
175
- logits = apply_frequency_presence_penalty(logits, generated_ids, frequency_penalty, presence_penalty)
176
  logits = apply_no_repeat_ngram(logits, generated_ids, no_repeat_ngram_size)
 
177
  if temperature <= 0:
178
  return int(torch.argmax(logits).item())
 
179
  logits = logits / temperature
180
  logits = apply_top_k(logits, top_k)
181
  logits = apply_top_p(logits, top_p)
 
182
  probs = F.softmax(logits, dim=-1)
183
  if torch.isnan(probs).any() or torch.isinf(probs).any() or probs.sum() <= 0:
184
  return int(torch.argmax(logits).item())
185
  return int(torch.multinomial(probs, num_samples=1).item())
186
 
187
 
188
- def model_forward_logits(input_ids: torch.Tensor) -> torch.Tensor:
189
- out = model(input_ids=input_ids)
190
- if hasattr(out, "logits"):
191
- return out.logits
192
- if isinstance(out, tuple):
193
- return out[0]
194
- raise RuntimeError("Could not get logits from model output.")
195
-
196
-
197
- @torch.no_grad()
198
- def generate_manual(
199
- input_ids: torch.Tensor,
200
- max_new_tokens: int,
201
- min_new_tokens: int,
202
- temperature: float,
203
- top_k: int,
204
- top_p: float,
205
- repetition_penalty: float,
206
- frequency_penalty: float,
207
- presence_penalty: float,
208
- no_repeat_ngram_size: int,
209
- ctx_len: int,
210
- stop_strings: List[str],
211
- ) -> List[int]:
212
- idx = input_ids
213
- generated_after_prompt: List[int] = []
214
- stop_sequences = build_stop_sequences(stop_strings)
215
- eos_id = tokenizer.eos_token_id
216
 
217
- for step in range(max_new_tokens):
218
- idx_cond = idx[:, -ctx_len:] if ctx_len > 0 else idx
219
- logits = model_forward_logits(idx_cond)
220
- logits = logits[:, -1, :][0]
221
-
222
- if step < min_new_tokens:
223
- if eos_id is not None and 0 <= eos_id < logits.numel():
224
- logits[eos_id] = -float("inf")
225
- for seq in stop_sequences:
226
- if len(seq) == 1:
227
- tid = seq[0]
228
- if 0 <= tid < logits.numel():
229
- logits[tid] = -float("inf")
230
-
231
- next_id = sample_next_token(
232
- logits=logits,
233
- generated_ids=generated_after_prompt,
234
- temperature=temperature,
235
- top_k=top_k,
236
- top_p=top_p,
237
- repetition_penalty=repetition_penalty,
238
- frequency_penalty=frequency_penalty,
239
- presence_penalty=presence_penalty,
240
- no_repeat_ngram_size=no_repeat_ngram_size,
241
- )
242
 
243
- next_tensor = torch.tensor([[next_id]], dtype=torch.long, device=idx.device)
244
- idx = torch.cat([idx, next_tensor], dim=1)
245
- generated_after_prompt.append(next_id)
246
-
247
- if step >= min_new_tokens:
248
- if eos_id is not None and next_id == eos_id:
249
- break
250
- full_ids = idx[0].tolist()
251
- should_stop = False
252
- for seq in stop_sequences:
253
- if endswith_sequence(full_ids, seq):
254
- should_stop = True
255
- break
256
- if should_stop:
257
- break
258
 
259
- return idx[0].tolist()
260
 
 
 
 
 
 
 
 
 
 
261
 
262
- def build_prompt(expression: str, use_no_think: bool) -> str:
263
- """Build the prompt for the model.
264
 
265
- Args:
266
- expression: arithmetic expression (e.g. '(10 + 28) * 3 =').
267
- use_no_think: if True, wrap in the /no think chat template for production use.
268
- """
269
- if use_no_think:
270
  return (
271
  f"{IM_START}user\n"
272
  f"{expression} {NO_THINK}"
@@ -277,228 +179,165 @@ def build_prompt(expression: str, use_no_think: bool) -> str:
277
  return expression
278
 
279
 
280
- def extract_completion(full_text: str, prompt: str) -> str:
281
- """Extract the completion (model output) from the full generated text."""
282
- if full_text.startswith(prompt):
283
- return full_text[len(prompt):]
284
- pos = full_text.rfind(prompt)
285
- if pos != -1:
286
- return full_text[pos + len(prompt):]
287
- return full_text
288
-
289
-
290
- def strip_after_stop_text(text: str, stop_strings: List[str]) -> str:
291
- """Strip everything after the first stop string."""
292
- best = None
293
- for s in stop_strings:
294
- if not s:
295
- continue
296
- pos = text.find(s)
297
- if pos != -1:
298
- if best is None or pos < best:
299
- best = pos
300
- if best is None:
301
- return text
302
- return text[:best]
303
-
304
-
305
- # ============================================================
306
- # Gradio inference function
307
- # ============================================================
308
-
309
  @spaces.GPU(duration=30)
310
  def solve(
311
  expression: str,
312
- use_no_think: bool,
313
- max_new_tokens: int,
314
- temperature: float,
315
- top_k: int,
316
- top_p: float,
317
- repetition_penalty: float,
318
- frequency_penalty: float,
319
- no_repeat_ngram_size: int,
320
- seed: int,
321
- ):
322
- """Solve an arithmetic expression using the Arithmetic-SLM model.
323
 
324
  Args:
325
- expression: an arithmetic expression ending with '=' (e.g. '59 + 45 =').
326
- use_no_think: use the /no think chat template (recommended for production).
327
- max_new_tokens: maximum number of tokens to generate.
328
- temperature: sampling temperature (lower = more deterministic).
329
- top_k: top-k sampling filter.
330
- top_p: nucleus sampling filter.
331
- repetition_penalty: penalty for repeated tokens.
332
- frequency_penalty: penalty based on token frequency.
333
- no_repeat_ngram_size: ban repeating n-grams of this size.
334
- seed: random seed (-1 for random).
335
  """
336
- if not expression or not expression.strip():
337
- return "Please enter an arithmetic expression ending with '='.", ""
 
338
 
339
- # Set seed
340
- if seed is not None and seed >= 0:
341
- random.seed(seed)
342
- torch.manual_seed(seed)
343
- if torch.cuda.is_available():
344
- torch.cuda.manual_seed_all(seed)
345
- else:
346
- seed = random.randint(0, 2**31 - 1)
347
- random.seed(seed)
348
- torch.manual_seed(seed)
349
  if torch.cuda.is_available():
350
- torch.cuda.manual_seed_all(seed)
351
 
352
- prompt = build_prompt(expression.strip(), use_no_think)
353
- stop_strings = [IM_END, IM_START]
 
 
 
 
354
 
355
  encoded = tokenizer(prompt, return_tensors="pt", add_special_tokens=False)
356
  encoded.pop("token_type_ids", None)
357
- input_ids = encoded["input_ids"].to("cuda")
358
-
359
- output_ids = generate_manual(
360
- input_ids=input_ids,
361
- max_new_tokens=max_new_tokens,
362
- min_new_tokens=1,
363
- temperature=temperature,
364
- top_k=top_k,
365
- top_p=top_p,
366
- repetition_penalty=repetition_penalty,
367
- frequency_penalty=frequency_penalty,
368
- presence_penalty=0.0,
369
- no_repeat_ngram_size=no_repeat_ngram_size,
370
- ctx_len=2048,
371
- stop_strings=stop_strings,
372
- )
373
-
374
- full_text = tokenizer.decode(output_ids, skip_special_tokens=False)
375
- completion = extract_completion(full_text, prompt)
376
- completion = strip_after_stop_text(completion, stop_strings)
377
- completion = completion.strip()
378
-
379
- # The model often repeats the input expression in its output.
380
- # If the completion starts with the expression, extract just the answer.
381
- expr_stripped = expression.strip()
382
- if completion.startswith(expr_stripped):
383
- answer = completion[len(expr_stripped):].strip()
384
- else:
385
- answer = completion
386
-
387
- # Build the result display
388
- full_display = f"{expr_stripped} {answer}".strip()
389
-
390
- if use_no_think:
391
- display = f"**Prompt (no-think template):**\n```\n{prompt}\n```\n\n**Model output:**\n```\n{completion}\n```\n\n**Answer:** `{answer}`"
392
- else:
393
- display = f"**Prompt:** `{prompt}`\n\n**Model output:** `{completion}`\n\n**Answer:** `{answer}`"
394
-
395
- return full_display, display
396
-
397
-
398
- # ============================================================
399
- # Gradio UI
400
- # ============================================================
 
 
 
 
 
 
 
401
 
402
  CSS = """
403
- #col-container { max-width: 900px; margin: 0 auto; }
404
  .dark .gradio-container { color: var(--body-text-color); }
405
  """
406
 
407
- with gr.Blocks() as demo:
408
- gr.Markdown(
409
- "# 🧮 Arithmetic-SLM Playground\n"
410
- "**WhirlwindAI/Arithmetic-SLM** — a tiny 31.7M-parameter model specialized for arithmetic. "
411
- "Enter an expression ending with `=` and the model completes it with the answer.\n\n"
412
- "Supports `+`, `-`, `*`, `/`, parentheses, and decimals. "
413
- "[Model card](https://huggingface.co/WhirlwindAI/Arithmetic-SLM)"
414
- )
415
-
416
  with gr.Column(elem_id="col-container"):
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  with gr.Row():
418
- expression_input = gr.Textbox(
419
  label="Arithmetic expression",
420
- placeholder="e.g. (10 + 28) * 3 =",
 
421
  scale=4,
422
- show_label=False,
423
  )
424
- solve_btn = gr.Button("Solve", variant="primary", scale=1)
425
 
426
- output_text = gr.Textbox(label="Result", interactive=False)
427
- output_detail = gr.Textbox(label="Details", interactive=False, visible=True)
428
 
429
  with gr.Accordion("Advanced settings", open=False):
430
- use_no_think = gr.Checkbox(
431
- label="Use /no think template (recommended)",
432
- value=True,
433
- info="Wraps the expression in the model's production chat template for cleaner outputs.",
434
  )
435
- with gr.Row():
436
- max_new_tokens = gr.Slider(1, 128, value=48, step=1, label="Max new tokens")
437
- temperature = gr.Slider(0.0, 2.0, value=0.6, step=0.05, label="Temperature")
438
- with gr.Row():
439
- top_k = gr.Slider(0, 100, value=50, step=1, label="Top-k")
440
- top_p = gr.Slider(0.01, 1.0, value=0.97, step=0.01, label="Top-p")
441
- with gr.Row():
442
- repetition_penalty = gr.Slider(0.1, 2.0, value=1.0, step=0.05, label="Repetition penalty")
443
- frequency_penalty = gr.Slider(0.0, 2.0, value=0.0, step=0.05, label="Frequency penalty")
444
- with gr.Row():
445
- no_repeat_ngram_size = gr.Slider(0, 10, value=4, step=1, label="No-repeat n-gram size")
446
- seed = gr.Number(label="Seed (-1 for random)", value=-1, precision=0)
447
 
448
  gr.Examples(
449
  examples=[
450
- ["59 + 45 =", True],
451
- ["(10 + 28) * 3 =", True],
452
- ["16 + 4 * 3 =", True],
453
- ["3 * 9 + 12 / 1 =", True],
454
- ["(132 / 12) + (46 - 15) =", True],
455
- ["0.5 * 0.5 =", True],
456
- ["8 * 5 + 4 / 4 =", True],
457
- ["(85 - 45) + 56 =", True],
458
  ],
459
- inputs=[expression_input, use_no_think],
460
- outputs=[output_text, output_detail],
461
  fn=solve,
462
  cache_examples=True,
463
  cache_mode="lazy",
464
  )
465
 
466
- solve_btn.click(
467
- fn=solve,
468
- inputs=[
469
- expression_input,
470
- use_no_think,
471
- max_new_tokens,
472
- temperature,
473
- top_k,
474
- top_p,
475
- repetition_penalty,
476
- frequency_penalty,
477
- no_repeat_ngram_size,
478
- seed,
479
- ],
480
- outputs=[output_text, output_detail],
481
- api_name="solve",
482
- )
483
-
484
- expression_input.submit(
485
- fn=solve,
486
- inputs=[
487
- expression_input,
488
- use_no_think,
489
- max_new_tokens,
490
- temperature,
491
- top_k,
492
- top_p,
493
- repetition_penalty,
494
- frequency_penalty,
495
- no_repeat_ngram_size,
496
- seed,
497
- ],
498
- outputs=[output_text, output_detail],
499
- api_name="solve_submit",
500
- )
501
-
502
 
503
  if __name__ == "__main__":
504
- demo.launch(mcp_server=True, theme=gr.themes.Citrus(), css=CSS)
 
 
 
 
1
  import random
2
+ from typing import Dict, List, Set
 
 
 
3
 
4
+ import spaces
5
  import torch
 
6
  import torch.nn.functional as F
7
  import gradio as gr
8
+ from transformers import AutoModelForCausalLM, AutoTokenizer
 
 
 
9
 
10
  MODEL_ID = "WhirlwindAI/Arithmetic-SLM"
11
 
12
  IM_START = "[IM_START]"
13
  IM_END = "[IM_END]"
14
  NO_THINK = "/no think"
15
+ CTX_LEN = 2048
16
 
17
+ STOP_STRINGS = [IM_END, IM_START]
 
 
 
18
 
19
+ # ---------------------------------------------------------------------------
20
+ # Load model + tokenizer once, at module scope, moved eagerly to CUDA so
21
+ # ZeroGPU can pack the weights and stream them into VRAM on the first call.
22
+ # The model uses custom code (TinyGPTForCausalLM) with the pure-torch attention
23
+ # backend (config: attention_backend="torch", torch_fallback=True) so no flash
24
+ # kernels are needed at runtime.
25
+ # ---------------------------------------------------------------------------
26
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
27
+ model = AutoModelForCausalLM.from_pretrained(
28
+ MODEL_ID,
29
+ dtype=torch.bfloat16,
30
+ trust_remote_code=True,
31
+ ).to("cuda")
 
 
32
  model.eval()
33
 
 
 
 
34
 
35
+ # ---------------------------------------------------------------------------
36
+ # Sampling helpers — ported 1:1 from the model repo's inference.py so output
37
+ # matches the authors' reference path exactly.
38
+ # ---------------------------------------------------------------------------
39
+ def apply_repetition_penalty(logits, generated_ids, penalty):
40
  if penalty is None or penalty == 1.0:
41
  return logits
 
 
42
  for tid in set(generated_ids):
43
  if tid < 0 or tid >= logits.numel():
44
  continue
 
49
  return logits
50
 
51
 
52
+ def apply_frequency_presence_penalty(logits, generated_ids, frequency_penalty, presence_penalty):
 
 
 
 
 
53
  if not generated_ids:
54
  return logits
55
  if frequency_penalty == 0.0 and presence_penalty == 0.0:
 
67
  return logits
68
 
69
 
70
+ def get_banned_ngram_tokens(generated_ids, no_repeat_ngram_size) -> Set[int]:
71
  n = no_repeat_ngram_size
72
  banned: Set[int] = set()
73
  if n <= 0:
 
78
  current_prefix = tuple(generated_ids[-prefix_len:])
79
  ngram_map: Dict[tuple, Set[int]] = {}
80
  for i in range(len(generated_ids) - n + 1):
81
+ prefix = tuple(generated_ids[i:i + prefix_len])
82
  next_token = generated_ids[i + prefix_len]
83
+ ngram_map.setdefault(prefix, set()).add(next_token)
 
 
84
  banned.update(ngram_map.get(current_prefix, set()))
85
  return banned
86
 
87
 
88
+ def apply_no_repeat_ngram(logits, generated_ids, no_repeat_ngram_size):
89
  if no_repeat_ngram_size <= 0:
90
  return logits
91
  banned = get_banned_ngram_tokens(generated_ids, no_repeat_ngram_size)
 
95
  return logits
96
 
97
 
98
+ def apply_top_k(logits, top_k):
99
  if top_k is None or top_k <= 0:
100
  return logits
101
  top_k = min(top_k, logits.size(-1))
 
105
  return logits
106
 
107
 
108
+ def apply_top_p(logits, top_p):
109
  if top_p is None or top_p >= 1.0:
110
  return logits
111
  if top_p <= 0:
112
+ return logits
113
  sorted_logits, sorted_indices = torch.sort(logits, descending=True)
114
  sorted_probs = F.softmax(sorted_logits, dim=-1)
115
  cumulative = torch.cumsum(sorted_probs, dim=-1)
 
121
  return logits
122
 
123
 
124
+ def sample_next_token(logits, generated_ids, temperature, top_k, top_p,
125
+ repetition_penalty, frequency_penalty, no_repeat_ngram_size):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  logits = logits.float().clone()
127
  logits = apply_repetition_penalty(logits, generated_ids, repetition_penalty)
128
+ logits = apply_frequency_presence_penalty(logits, generated_ids, frequency_penalty, 0.0)
129
  logits = apply_no_repeat_ngram(logits, generated_ids, no_repeat_ngram_size)
130
+
131
  if temperature <= 0:
132
  return int(torch.argmax(logits).item())
133
+
134
  logits = logits / temperature
135
  logits = apply_top_k(logits, top_k)
136
  logits = apply_top_p(logits, top_p)
137
+
138
  probs = F.softmax(logits, dim=-1)
139
  if torch.isnan(probs).any() or torch.isinf(probs).any() or probs.sum() <= 0:
140
  return int(torch.argmax(logits).item())
141
  return int(torch.multinomial(probs, num_samples=1).item())
142
 
143
 
144
+ def build_stop_sequences(stop_strings) -> List[List[int]]:
145
+ out = []
146
+ for s in stop_strings:
147
+ ids = tokenizer.encode(s, add_special_tokens=False)
148
+ if ids:
149
+ out.append(ids)
150
+ return out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
+ def endswith_sequence(ids, suffix) -> bool:
154
+ if not suffix or len(ids) < len(suffix):
155
+ return False
156
+ return ids[-len(suffix):] == suffix
 
 
 
 
 
 
 
 
 
 
 
157
 
 
158
 
159
+ def strip_after_stop_text(text, stop_strings) -> str:
160
+ best = None
161
+ for s in stop_strings:
162
+ if not s:
163
+ continue
164
+ pos = text.find(s)
165
+ if pos != -1 and (best is None or pos < best):
166
+ best = pos
167
+ return text if best is None else text[:best]
168
 
 
 
169
 
170
+ def build_prompt(expression: str, use_think_format: bool) -> str:
171
+ if use_think_format:
 
 
 
172
  return (
173
  f"{IM_START}user\n"
174
  f"{expression} {NO_THINK}"
 
179
  return expression
180
 
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  @spaces.GPU(duration=30)
183
  def solve(
184
  expression: str,
185
+ use_think_format: bool = False,
186
+ temperature: float = 0.5,
187
+ top_k: int = 40,
188
+ top_p: float = 0.95,
189
+ max_new_tokens: int = 48,
190
+ seed: int = -1,
191
+ ) -> str:
192
+ """Solve an arithmetic expression with the Arithmetic-SLM model.
 
 
 
193
 
194
  Args:
195
+ expression: An arithmetic expression ending in '=', e.g. '(10 + 28) * 3 ='.
196
+ use_think_format: Use the production [IM_START]/[IM_END] chat template with a '/no think' tag.
197
+ temperature: Sampling temperature (lower = more deterministic).
198
+ top_k: Top-k sampling cutoff.
199
+ top_p: Nucleus (top-p) sampling cutoff.
200
+ max_new_tokens: Maximum number of tokens to generate.
201
+ seed: RNG seed; -1 for random.
202
+
203
+ Returns:
204
+ The model's completion of the expression (typically the solved result).
205
  """
206
+ expression = (expression or "").strip()
207
+ if not expression:
208
+ return "Please enter an arithmetic expression, e.g. '(10 + 28) * 3 ='."
209
 
210
+ if seed is not None and int(seed) >= 0:
211
+ random.seed(int(seed))
212
+ torch.manual_seed(int(seed))
 
 
 
 
 
 
 
213
  if torch.cuda.is_available():
214
+ torch.cuda.manual_seed_all(int(seed))
215
 
216
+ repetition_penalty = 1.05
217
+ frequency_penalty = 0.10
218
+ no_repeat_ngram_size = 4
219
+ min_new_tokens = 1
220
+
221
+ prompt = build_prompt(expression, use_think_format)
222
 
223
  encoded = tokenizer(prompt, return_tensors="pt", add_special_tokens=False)
224
  encoded.pop("token_type_ids", None)
225
+ idx = encoded["input_ids"].to("cuda")
226
+
227
+ stop_sequences = build_stop_sequences(STOP_STRINGS)
228
+ eos_id = tokenizer.eos_token_id
229
+ generated: List[int] = []
230
+
231
+ with torch.no_grad():
232
+ for step in range(int(max_new_tokens)):
233
+ idx_cond = idx[:, -CTX_LEN:]
234
+ out = model(input_ids=idx_cond)
235
+ logits = out.logits[:, -1, :][0]
236
+
237
+ if step < min_new_tokens:
238
+ if eos_id is not None and 0 <= eos_id < logits.numel():
239
+ logits[eos_id] = -float("inf")
240
+ for seq in stop_sequences:
241
+ if len(seq) == 1 and 0 <= seq[0] < logits.numel():
242
+ logits[seq[0]] = -float("inf")
243
+
244
+ next_id = sample_next_token(
245
+ logits, generated, float(temperature), int(top_k), float(top_p),
246
+ repetition_penalty, frequency_penalty, no_repeat_ngram_size,
247
+ )
248
+ idx = torch.cat(
249
+ [idx, torch.tensor([[next_id]], dtype=torch.long, device=idx.device)], dim=1
250
+ )
251
+ generated.append(next_id)
252
+
253
+ if step >= min_new_tokens:
254
+ if eos_id is not None and next_id == eos_id:
255
+ break
256
+ full_ids = idx[0].tolist()
257
+ if any(endswith_sequence(full_ids, seq) for seq in stop_sequences):
258
+ break
259
+
260
+ full_text = tokenizer.decode(idx[0].tolist(), skip_special_tokens=False)
261
+
262
+ if use_think_format:
263
+ # Show the completion after the prompt, cleaned of control markers.
264
+ if full_text.startswith(prompt):
265
+ completion = full_text[len(prompt):]
266
+ else:
267
+ pos = full_text.rfind(prompt)
268
+ completion = full_text[pos + len(prompt):] if pos != -1 else full_text
269
+ completion = strip_after_stop_text(completion, STOP_STRINGS)
270
+ return completion.strip()
271
+
272
+ # Raw mode: return the full continued expression.
273
+ completion = strip_after_stop_text(full_text, STOP_STRINGS)
274
+ return completion.strip()
275
+
276
 
277
  CSS = """
278
+ #col-container { max-width: 820px; margin: 0 auto; }
279
  .dark .gradio-container { color: var(--body-text-color); }
280
  """
281
 
282
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
 
 
 
 
 
 
 
 
283
  with gr.Column(elem_id="col-container"):
284
+ gr.Markdown(
285
+ """
286
+ # 🧮 Arithmetic-SLM
287
+
288
+ A tiny (31.7M parameter) specialized language model that **completes arithmetic
289
+ expressions** — it learned to do math token by token, not with a calculator.
290
+ Handles operator precedence, parentheses, and decimals.
291
+
292
+ Enter an expression ending in `=` and let the model finish it.
293
+
294
+ [Model card](https://huggingface.co/WhirlwindAI/Arithmetic-SLM)
295
+ """
296
+ )
297
  with gr.Row():
298
+ expression = gr.Textbox(
299
  label="Arithmetic expression",
300
+ placeholder="(10 + 28) * 3 =",
301
+ value="(10 + 28) * 3 =",
302
  scale=4,
 
303
  )
304
+ run = gr.Button("Solve", variant="primary", scale=1)
305
 
306
+ output = gr.Textbox(label="Model output", lines=3)
 
307
 
308
  with gr.Accordion("Advanced settings", open=False):
309
+ use_think_format = gr.Checkbox(
310
+ label="Use production /no think chat template",
311
+ value=False,
312
+ info="Wraps the input in the [IM_START]/[IM_END] template with a <think> block.",
313
  )
314
+ temperature = gr.Slider(0.0, 1.0, value=0.5, step=0.05, label="Temperature")
315
+ top_k = gr.Slider(0, 100, value=40, step=1, label="Top-k")
316
+ top_p = gr.Slider(0.1, 1.0, value=0.95, step=0.01, label="Top-p")
317
+ max_new_tokens = gr.Slider(8, 128, value=48, step=1, label="Max new tokens")
318
+ seed = gr.Number(value=-1, precision=0, label="Seed (-1 = random)")
 
 
 
 
 
 
 
319
 
320
  gr.Examples(
321
  examples=[
322
+ ["59 + 45 ="],
323
+ ["16 + 4 * 3 ="],
324
+ ["(16 / 4) + 44 ="],
325
+ ["3 * 9 + 12 / 1 ="],
326
+ ["(132 / 12) + (46 - 15) ="],
327
+ ["0.5 * 0.5 ="],
328
+ ["8 * 5 + 4 / 4 ="],
329
+ ["(85 - 45) + 56 ="],
330
  ],
331
+ inputs=[expression],
332
+ outputs=output,
333
  fn=solve,
334
  cache_examples=True,
335
  cache_mode="lazy",
336
  )
337
 
338
+ inputs = [expression, use_think_format, temperature, top_k, top_p, max_new_tokens, seed]
339
+ run.click(solve, inputs=inputs, outputs=output, api_name="solve")
340
+ expression.submit(solve, inputs=inputs, outputs=output, api_name=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
 
342
  if __name__ == "__main__":
343
+ demo.launch(mcp_server=True)
requirements.txt CHANGED
@@ -1,2 +1,2 @@
1
  transformers
2
- safetensors
 
1
  transformers
2
+ safetensors