Text Generation
Transformers
Safetensors
Arabic
llama
arabic
reasoning
chain-of-thought
math
gsm8k
small-language-model
slm
sft
conversational
text-generation-inference
oddadmix's picture
training code: data generation, SFT, eval, GRPO
867d0f3 verified
Raw History Blame Contribute Delete
11.3 kB
"""
Nawah-Math-Reasoning โ€” Gradio demo.
The prompt rendering (ChatML + BOS prepend) is IDENTICAL to train_reasoning.py. A 51M model
is very sensitive to format drift, so do not change render_prompt() without changing training.
Runs on ZeroGPU. The model is only ~52M parameters and works on CPU too, but ZeroGPU keeps
responses snappy. `import spaces` must come BEFORE torch so it can patch the CUDA calls.
The model emits <think>โ€ฆ</think> before its answer, so the stream is split live into two
panels: the reasoning trace and the final answer.
Deploy: push this + requirements.txt + README.md to a Gradio Space. The released model is
public, so no token is needed; MODEL_HF_TOKEN is still read for pointing MODEL_ID at a private
checkpoint.
"""
import os
import re
import threading
import spaces # import BEFORE torch so it can patch CUDA calls
import gradio as gr
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
# โ”€โ”€ Config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
MODEL_ID = os.environ.get("MODEL_ID", "oddadmix/Nawah-Math-Reasoning")
# Unused for the public release; needed only if MODEL_ID is repointed at a private repo.
# HF_TOKEN is reserved by Spaces (a secret set under that name does not reach the container),
# so MODEL_HF_TOKEN is the one to set.
HF_TOKEN = os.environ.get("MODEL_HF_TOKEN") or os.environ.get("HF_TOKEN")
IM_START, IM_END = "<|im_start|>", "<|im_end|>"
THINK_OPEN, THINK_CLOSE = "<think>", "</think>"
MAX_NEW_TOKENS_CAP = 1500
# โ”€โ”€ Load (once, at startup) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
print("[*] token env vars present:",
[k for k in ("MODEL_HF_TOKEN", "HF_TOKEN") if os.environ.get(k)] or "NONE")
print(f"[*] Loading {MODEL_ID} ...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, token=HF_TOKEN)
model = AutoModelForCausalLM.from_pretrained(MODEL_ID, dtype=torch.bfloat16, token=HF_TOKEN)
model.to("cuda").eval()
CTX = getattr(model.config, "max_position_embeddings", 2048)
_eos = {tokenizer.eos_token_id} if tokenizer.eos_token_id is not None else set()
_im_end = tokenizer.convert_tokens_to_ids(IM_END)
if isinstance(_im_end, int) and _im_end >= 0:
_eos.add(_im_end)
EOS_IDS = list(_eos) or None
PAD_ID = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else tokenizer.eos_token_id
print(f"[+] {model.num_parameters():,} params | eos_ids={EOS_IDS} | ctx={CTX}")
# โ”€โ”€ Prompt rendering โ€” must match train_reasoning.py โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def render_prompt(question: str) -> str:
# Single-turn only: the model was trained on one user turn per sample, so a chat
# history would be out of distribution.
return f"{IM_START}user\n{question.strip()}{IM_END}\n{IM_START}assistant\n"
STRIP_RE = re.compile(r"<\|im_end\|>|</s>|<pad>|<s>")
def split_stream(text: str):
"""-> (reasoning_so_far, answer_so_far). Handles the partial state mid-stream."""
text = STRIP_RE.sub("", text)
if THINK_CLOSE in text:
reasoning, answer = text.split(THINK_CLOSE, 1)
return reasoning.replace(THINK_OPEN, "").strip(), answer.strip()
return text.replace(THINK_OPEN, "").strip(), ""
# โ”€โ”€ Generate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@spaces.GPU(duration=60)
def solve(question, max_new_tokens, temperature, repetition_penalty):
question = (question or "").strip()
if not question:
yield "", "", ""
return
ids = tokenizer(render_prompt(question), add_special_tokens=False)["input_ids"]
if tokenizer.bos_token_id is not None:
ids = [tokenizer.bos_token_id] + ids # match training's explicit BOS
input_ids = torch.tensor([ids], device=model.device)
# skip_special_tokens must stay False โ€” <think>/</think> are real special tokens
# in this tokenizer, and stripping them would destroy the split.
streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=False)
kwargs = dict(
input_ids=input_ids,
attention_mask=torch.ones_like(input_ids),
max_new_tokens=int(max_new_tokens),
repetition_penalty=float(repetition_penalty),
eos_token_id=EOS_IDS,
pad_token_id=PAD_ID,
streamer=streamer,
)
if temperature and temperature > 0:
kwargs.update(do_sample=True, temperature=float(temperature), top_p=0.95)
else:
kwargs.update(do_sample=False) # greedy โ€” how the model was evaluated
threading.Thread(target=model.generate, kwargs=kwargs).start()
out = ""
for chunk in streamer:
out += chunk
reasoning, answer = split_stream(out)
yield reasoning, (answer or "โ€ฆ"), out
reasoning, answer = split_stream(out)
if not answer:
answer = "โš ๏ธ ู„ู… ูŠูุบู„ู‚ ุงู„ู†ู…ูˆุฐุฌ ูˆุณู… ุงู„ุชููƒูŠุฑ โ€” ุฌุฑู‘ุจ ุณุคุงู„ู‹ุง ุฃู‚ุฑุจ ู„ุฃู…ุซู„ุฉ ุงู„ุชุฏุฑูŠุจ.\n" \
"(The model never closed `</think>` โ€” try a question closer to its training distribution.)"
yield reasoning, answer, out
# โ”€โ”€ UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
DESCRIPTION = """
<div style="text-align:center">
<h1>๐Ÿง  Nawah-Math-Reasoning</h1>
<p>ู†ู…ูˆุฐุฌ ุงุณุชุฏู„ุงู„ ุนุฑุจูŠ ุตุบูŠุฑ (~52M ุจุงุฑุงู…ุชุฑ) ูŠููƒู‘ุฑ ุฎุทูˆุฉ ุจุฎุทูˆุฉ ุฏุงุฎู„ ูˆุณู… <code>&lt;think&gt;</code>
ุซู… ูŠุนุทูŠ ุงู„ุฅุฌุงุจุฉ ุงู„ู†ู‡ุงุฆูŠุฉ.<br>
A ~52M-parameter Arabic reasoning model that thinks step by step inside
<code>&lt;think&gt;</code> before answering.</p>
<p><i>ู†ู…ูˆุฐุฌ ุตุบูŠุฑ ุจู…ุง ูŠูƒููŠ ู„ูŠุนู…ู„ ุญุชู‰ ุนู„ู‰ ุงู„ู…ุนุงู„ุฌ (CPU).<br>
Small enough to run on a CPU โ€” this Space uses ZeroGPU for snappier responses.</i></p>
<p>
<a href="https://huggingface.co/oddadmix/Nawah-Math-Reasoning">Model</a> ยท
<a href="https://huggingface.co/oddadmix/Nawah-Math-Reasoning/tree/main/code">Training code</a> ยท
<a href="https://huggingface.co/datasets/oddadmix/arabic-math-reasoning-synth">Synthetic dataset</a> ยท
<a href="https://huggingface.co/datasets/oddadmix/gsm8k-reasoning-ar">GSM8K-ar dataset</a>
<br><i>Weights, both datasets and the full training code are open โ€” Apache 2.0.</i>
</p>
</div>
"""
NOTE = """
### ๐Ÿ“Š ุงู„ู†ุชุงุฆุฌ / Results
Number agreement, greedy decoding, on held-out splits โ€” the same rows for every version of the
model, so the numbers are comparable.
| eval set | n | score |
|---|---:|---:|
| GSM8K-ar | 600 | **79.0%** |
| Arabic_Reasoning | 400 | **73.0%** |
| synthetic math | 1000 | **40.4%** |
| synthetic relational (`ุถุนู`, `ู†ุตู`, `ุฃูƒุซุฑ ุจู€โ€ฆ`) | 400 | **52.2%** |
### โš ๏ธ ุญุฏูˆุฏ ุงู„ู†ู…ูˆุฐุฌ / Limitations
ู†ู…ูˆุฐุฌ ุชุฌุฑูŠุจูŠ ุจุญุฌู… 52M: ูŠุฌูŠุฏ **ุดูƒู„** ุงู„ุงุณุชุฏู„ุงู„ ุงู„ุนุฑุจูŠ ูˆูŠุญู„ู‘ ู…ุณุงุฆู„ ุงู„ู†ูู‘ุณุจ ูˆุงู„ุญุณุงุจ ุงู„ุจุณูŠุทุฉุŒ
ู„ูƒู†ู‡ **ูŠุฎุทุฆ ููŠ ุงู„ุญุณุงุจ ูƒุซูŠุฑู‹ุง** โ€” ุบุงู„ุจู‹ุง ุฎุทูˆุงุช ุงู„ุญู„ ุณู„ูŠู…ุฉ ุซู… ุชู‚ุน ุบู„ุทุฉ ููŠ ุนู…ู„ูŠุฉ ุญุณุงุจูŠุฉ ูˆุงุญุฏุฉ
ูˆูŠูƒู…ู„ ุงู„ู†ู…ูˆุฐุฌ ุนู„ู‰ ุฑู‚ู…ู‡ ุงู„ุฎุงุทุฆ. ุงู„ุฃุณุฆู„ุฉ ุงู„ู…ูุชูˆุญุฉ ูˆุบูŠุฑ ุงู„ุญุณุงุจูŠุฉ ุฎุงุฑุฌ ู†ุทุงู‚ู‡ุŒ ูˆุงู„ุญูˆุงุฑ ู…ุชุนุฏุฏ
ุงู„ุฃุฏูˆุงุฑ ูƒุฐู„ูƒ.
A 52M proof of concept. It reliably produces the *shape* of Arabic step-by-step reasoning, but
**arithmetic errors are the dominant failure mode**: the derivation is usually structurally
right, one computation is wrong, and the model then stays faithful to its own bad number. The
40.4% and 52.2% above are the honest ceiling on multi-step problems. Single-turn only;
open-ended and non-mathematical questions are out of distribution.
"""
EXAMPLES = [
"ุฅุฐุง ูƒุงู† ู„ุฏูŠูƒ 1500 ุฑูŠุงู„ ูˆุฃู†ูู‚ุช 20% ู…ู†ู‡ุง ุนู„ู‰ ุงู„ูƒุชุจุŒ ููƒู… ุชุจู‚ู‰ ู…ุนูƒุŸ",
"ููŠ ู…ุตู†ุน ุชู… ุฅู†ุชุงุฌ 5000 ูˆุญุฏุฉุŒ ูˆูƒุงู†ุช ู†ุณุจุฉ ุงู„ูˆุญุฏุงุช ุงู„ู…ุนูŠุจุฉ 2%ุŒ ูู…ุง ุนุฏุฏ ุงู„ูˆุญุฏุงุช ุงู„ุณู„ูŠู…ุฉุŸ",
"ููŠ ู…ุฏุฑุณุฉ ุจู‡ุง 500 ุทุงู„ุจุŒ ุฅุฐุง ูƒุงู†ุช ู†ุณุจุฉ ุงู„ุฐูƒูˆุฑ 55%ุŒ ูู…ุง ุนุฏุฏ ุงู„ุทุงู„ุจุงุชุŸ",
"ู„ุฏู‰ ุชุงุฌุฑ 240 ูƒูŠู„ูˆุบุฑุงู…ู‹ุง ู…ู† ุงู„ุฃุฑุฒุŒ ุจุงุน ู…ู†ู‡ุง 35%ุŒ ููƒู… ูƒูŠู„ูˆุบุฑุงู…ู‹ุง ุชุจู‚ู‰ ู„ุฏูŠู‡ุŸ",
"ุฅุฐุง ูƒุงู† ุนู…ุฑ ุฃุญู…ุฏ 12 ุณู†ุฉ ูˆุนู…ุฑ ุฃุฎูŠู‡ ุถุนู ุนู…ุฑู‡ุŒ ูู…ุง ู…ุฌู…ูˆุน ุนู…ุฑูŠู‡ู…ุงุŸ",
"ููŠ ุญุฏูŠู‚ุฉ 80 ุญูŠูˆุงู†ู‹ุงุŒ 25% ู…ู†ู‡ุง ุทูŠูˆุฑุŒ ูˆู†ุตู ุงู„ุทูŠูˆุฑ ุจูŠุถุงุก. ูƒู… ุนุฏุฏ ุงู„ุทูŠูˆุฑ ุงู„ุจูŠุถุงุกุŸ",
"ุฌู…ุน ุณุงู…ูŠ 45 ุตุฏูุฉุŒ ูˆุฌู…ุน ุฃุฎูˆู‡ ุถุนู ู‡ุฐุง ุงู„ุนุฏุฏ. ูƒู… ุตุฏูุฉ ุฌู…ุนุง ู…ุนู‹ุงุŸ",
"ู„ุฏู‰ ู„ูŠู„ู‰ 60 ุฌู†ูŠู‡ู‹ุงุŒ ูˆู„ุฏู‰ ู†ุฏู‰ ุฃู‚ู„ ู…ู†ู‡ุง ุจู€ 18 ุฌู†ูŠู‡ู‹ุง. ูƒู… ู…ุนู‡ู…ุง ู…ุนู‹ุงุŸ",
]
with gr.Blocks(title="Nawah-Math-Reasoning") as demo:
gr.HTML(DESCRIPTION)
with gr.Row():
with gr.Column(scale=3):
question = gr.Textbox(
label="ุงู„ุณุคุงู„ / Question", rtl=True, lines=3,
placeholder="ุงูƒุชุจ ู…ุณุฃู„ุฉ ุญุณุงุจูŠุฉ ู‡ู†ุงโ€ฆ",
)
with gr.Row():
submit = gr.Button("๐Ÿงฎ ุญู„ / Solve", variant="primary")
clear = gr.Button("ู…ุณุญ / Clear")
with gr.Accordion("โš™๏ธ ุฅุนุฏุงุฏุงุช ุงู„ุชูˆู„ูŠุฏ / Generation settings", open=False):
max_new_tokens = gr.Slider(32, MAX_NEW_TOKENS_CAP, value=320, step=8,
label="ุฃู‚ุตู‰ ุนุฏุฏ ุชูˆูƒู†ุฒ / Max new tokens")
temperature = gr.Slider(0.0, 1.5, value=0.0, step=0.05,
label="ุฏุฑุฌุฉ ุงู„ุญุฑุงุฑุฉ / Temperature (0 = greedy, as evaluated)")
repetition_penalty = gr.Slider(1.0, 1.5, value=1.0, step=0.01,
label="ุนู‚ูˆุจุฉ ุงู„ุชูƒุฑุงุฑ / Repetition penalty")
with gr.Column(scale=4):
answer_box = gr.Textbox(label="โœ… ุงู„ุฅุฌุงุจุฉ / Answer", rtl=True, lines=3)
with gr.Accordion("๐Ÿง  ุงู„ุชููƒูŠุฑ / Reasoning trace", open=True):
reasoning_box = gr.Textbox(label="", rtl=True, lines=12)
with gr.Accordion("๐Ÿ” ุงู„ู…ุฎุฑุฌุงุช ุงู„ุฎุงู… / Raw output", open=False):
raw_box = gr.Textbox(label="", lines=8)
gr.Examples(examples=EXAMPLES, inputs=question, label="ุฃู…ุซู„ุฉ / Examples")
gr.Markdown(NOTE)
inputs = [question, max_new_tokens, temperature, repetition_penalty]
outputs = [reasoning_box, answer_box, raw_box]
submit.click(solve, inputs=inputs, outputs=outputs)
question.submit(solve, inputs=inputs, outputs=outputs)
clear.click(lambda: ("", "", "", ""), outputs=[question] + outputs)
if __name__ == "__main__":
demo.queue().launch(theme=gr.themes.Soft())