import random from typing import Dict, List, Set import spaces import torch import torch.nn.functional as F import gradio as gr from transformers import AutoModelForCausalLM, AutoTokenizer MODEL_ID = "WhirlwindAI/Arithmetic-SLM" IM_START = "[IM_START]" IM_END = "[IM_END]" NO_THINK = "/no think" CTX_LEN = 2048 STOP_STRINGS = [IM_END, IM_START] # --------------------------------------------------------------------------- # Load model + tokenizer once, at module scope, moved eagerly to CUDA so # ZeroGPU can pack the weights and stream them into VRAM on the first call. # The model uses the pure-torch attention backend (config: # attention_backend="torch", torch_fallback=True) so no flash kernels are # needed at runtime. # --------------------------------------------------------------------------- tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( MODEL_ID, dtype=torch.bfloat16, trust_remote_code=True, ).to("cuda") model.eval() # --------------------------------------------------------------------------- # Sampling helpers — ported 1:1 from the model repo's inference.py so output # matches the authors' reference path exactly. # --------------------------------------------------------------------------- def apply_repetition_penalty(logits, generated_ids, penalty): if penalty is None or penalty == 1.0: return logits for tid in set(generated_ids): if tid < 0 or tid >= logits.numel(): continue if logits[tid] > 0: logits[tid] = logits[tid] / penalty else: logits[tid] = logits[tid] * penalty return logits def apply_frequency_presence_penalty(logits, generated_ids, frequency_penalty, presence_penalty): if not generated_ids: return logits if frequency_penalty == 0.0 and presence_penalty == 0.0: return logits counts: Dict[int, int] = {} for tid in generated_ids: counts[tid] = counts.get(tid, 0) + 1 for tid, count in counts.items(): if tid < 0 or tid >= logits.numel(): continue if frequency_penalty: logits[tid] -= frequency_penalty * count if presence_penalty: logits[tid] -= presence_penalty return logits def get_banned_ngram_tokens(generated_ids, no_repeat_ngram_size) -> Set[int]: n = no_repeat_ngram_size banned: Set[int] = set() if n <= 0: return banned if len(generated_ids) + 1 < n: return banned prefix_len = n - 1 current_prefix = tuple(generated_ids[-prefix_len:]) ngram_map: Dict[tuple, Set[int]] = {} for i in range(len(generated_ids) - n + 1): prefix = tuple(generated_ids[i:i + prefix_len]) next_token = generated_ids[i + prefix_len] ngram_map.setdefault(prefix, set()).add(next_token) banned.update(ngram_map.get(current_prefix, set())) return banned def apply_no_repeat_ngram(logits, generated_ids, no_repeat_ngram_size): if no_repeat_ngram_size <= 0: return logits banned = get_banned_ngram_tokens(generated_ids, no_repeat_ngram_size) for tid in banned: if 0 <= tid < logits.numel(): logits[tid] = -float("inf") return logits def apply_top_k(logits, top_k): if top_k is None or top_k <= 0: return logits top_k = min(top_k, logits.size(-1)) values, _ = torch.topk(logits, top_k) cutoff = values[-1] logits[logits < cutoff] = -float("inf") return logits def apply_top_p(logits, top_p): if top_p is None or top_p >= 1.0: return logits if top_p <= 0: return logits sorted_logits, sorted_indices = torch.sort(logits, descending=True) sorted_probs = F.softmax(sorted_logits, dim=-1) cumulative = torch.cumsum(sorted_probs, dim=-1) remove = cumulative > top_p remove[1:] = remove[:-1].clone() remove[0] = False indices_to_remove = sorted_indices[remove] logits[indices_to_remove] = -float("inf") return logits def sample_next_token(logits, generated_ids, temperature, top_k, top_p, repetition_penalty, frequency_penalty, no_repeat_ngram_size): logits = logits.float().clone() logits = apply_repetition_penalty(logits, generated_ids, repetition_penalty) logits = apply_frequency_presence_penalty(logits, generated_ids, frequency_penalty, 0.0) logits = apply_no_repeat_ngram(logits, generated_ids, no_repeat_ngram_size) if temperature <= 0: return int(torch.argmax(logits).item()) logits = logits / temperature logits = apply_top_k(logits, top_k) logits = apply_top_p(logits, top_p) probs = F.softmax(logits, dim=-1) if torch.isnan(probs).any() or torch.isinf(probs).any() or probs.sum() <= 0: return int(torch.argmax(logits).item()) return int(torch.multinomial(probs, num_samples=1).item()) def build_stop_sequences(stop_strings) -> List[List[int]]: out = [] for s in stop_strings: ids = tokenizer.encode(s, add_special_tokens=False) if ids: out.append(ids) return out def endswith_sequence(ids, suffix) -> bool: if not suffix or len(ids) < len(suffix): return False return ids[-len(suffix):] == suffix def strip_after_stop_text(text, stop_strings) -> str: best = None for s in stop_strings: if not s: continue pos = text.find(s) if pos != -1 and (best is None or pos < best): best = pos return text if best is None else text[:best] def build_prompt(expression: str, use_think_format: bool) -> str: if use_think_format: return ( f"{IM_START}user\n" f"{expression} {NO_THINK}" f"{IM_END}\n" f"{IM_START}assistant\n" "\n\n" ) return expression @spaces.GPU(duration=30) def solve( expression: str, use_think_format: bool = True, temperature: float = 0.5, top_k: int = 40, top_p: float = 0.95, max_new_tokens: int = 48, seed: int = -1, ) -> str: """Solve an arithmetic expression with the Arithmetic-SLM model. Args: expression: An arithmetic expression ending in '=', e.g. '(10 + 28) * 3 ='. use_think_format: Use the production [IM_START]/[IM_END] chat template with a '/no think' tag. temperature: Sampling temperature (lower = more deterministic). top_k: Top-k sampling cutoff. top_p: Nucleus (top-p) sampling cutoff. max_new_tokens: Maximum number of tokens to generate. seed: RNG seed; -1 for random. Returns: The model's completion of the expression (typically the solved result). """ expression = (expression or "").strip() if not expression: return "Please enter an arithmetic expression, e.g. '(10 + 28) * 3 ='." if seed is not None and int(seed) >= 0: random.seed(int(seed)) torch.manual_seed(int(seed)) if torch.cuda.is_available(): torch.cuda.manual_seed_all(int(seed)) repetition_penalty = 1.05 frequency_penalty = 0.10 no_repeat_ngram_size = 4 min_new_tokens = 1 prompt = build_prompt(expression, use_think_format) encoded = tokenizer(prompt, return_tensors="pt", add_special_tokens=False) encoded.pop("token_type_ids", None) idx = encoded["input_ids"].to("cuda") stop_sequences = build_stop_sequences(STOP_STRINGS) eos_id = tokenizer.eos_token_id generated: List[int] = [] with torch.no_grad(): for step in range(int(max_new_tokens)): idx_cond = idx[:, -CTX_LEN:] out = model(input_ids=idx_cond) logits = out.logits[:, -1, :][0] if step < min_new_tokens: if eos_id is not None and 0 <= eos_id < logits.numel(): logits[eos_id] = -float("inf") for seq in stop_sequences: if len(seq) == 1 and 0 <= seq[0] < logits.numel(): logits[seq[0]] = -float("inf") next_id = sample_next_token( logits, generated, float(temperature), int(top_k), float(top_p), repetition_penalty, frequency_penalty, no_repeat_ngram_size, ) idx = torch.cat( [idx, torch.tensor([[next_id]], dtype=torch.long, device=idx.device)], dim=1 ) generated.append(next_id) if step >= min_new_tokens: if eos_id is not None and next_id == eos_id: break full_ids = idx[0].tolist() if any(endswith_sequence(full_ids, seq) for seq in stop_sequences): break full_text = tokenizer.decode(idx[0].tolist(), skip_special_tokens=False) if use_think_format: # Show the completion after the prompt, cleaned of control markers. if full_text.startswith(prompt): completion = full_text[len(prompt):] else: pos = full_text.rfind(prompt) completion = full_text[pos + len(prompt):] if pos != -1 else full_text completion = strip_after_stop_text(completion, STOP_STRINGS) return completion.strip() # Raw mode: return the full continued expression, matching the reference # inference script's behavior exactly (strip only at [IM_END]/[IM_START]). completion = strip_after_stop_text(full_text, STOP_STRINGS) return completion.strip() CSS = """ #col-container { max-width: 820px; margin: 0 auto; } .dark .gradio-container { color: var(--body-text-color); } """ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo: with gr.Column(elem_id="col-container"): gr.Markdown( """ # 🧮 Arithmetic-SLM A tiny (31.7M parameter) specialized language model that **completes arithmetic expressions** — it learned to do math token by token, not with a calculator. Handles operator precedence, parentheses, and decimals. Enter an expression ending in `=` and let the model finish it. [Model card](https://huggingface.co/WhirlwindAI/Arithmetic-SLM) """ ) with gr.Row(): expression = gr.Textbox( label="Arithmetic expression", placeholder="(10 + 28) * 3 =", value="(10 + 28) * 3 =", scale=4, ) run = gr.Button("Solve", variant="primary", scale=1) output = gr.Textbox(label="Model output", lines=3) with gr.Accordion("Advanced settings", open=False): use_think_format = gr.Checkbox( label="Use production /no think chat template (recommended)", value=True, info=( "Wraps the input in the [IM_START]/[IM_END] template with a " " block — the format the model was trained for, and " "the most reliable. Uncheck for raw free-continuation mode." ), ) temperature = gr.Slider(0.0, 1.0, value=0.5, step=0.05, label="Temperature") top_k = gr.Slider(0, 100, value=40, step=1, label="Top-k") top_p = gr.Slider(0.1, 1.0, value=0.95, step=0.01, label="Top-p") max_new_tokens = gr.Slider(8, 128, value=48, step=1, label="Max new tokens") seed = gr.Number(value=-1, precision=0, label="Seed (-1 = random)") gr.Examples( examples=[ ["59 + 45 ="], ["16 + 4 * 3 ="], ["(16 / 4) + 44 ="], ["3 * 9 + 12 / 1 ="], ["(132 / 12) + (46 - 15) ="], ["0.5 * 0.5 ="], ["8 * 5 + 4 / 4 ="], ["(85 - 45) + 56 ="], ], inputs=[expression], outputs=output, fn=solve, cache_examples=True, cache_mode="lazy", ) inputs = [expression, use_think_format, temperature, top_k, top_p, max_new_tokens, seed] run.click(solve, inputs=inputs, outputs=output, api_name="solve") expression.submit(solve, inputs=inputs, outputs=output, api_name=False) if __name__ == "__main__": demo.launch(mcp_server=True)