Spaces:
Running on Zero
Running on Zero
File size: 11,304 Bytes
d35abc6 a6e10d9 d35abc6 5dd4d67 d35abc6 a6e10d9 d35abc6 5dd4d67 d35abc6 a6e10d9 186b0b0 d35abc6 66f1627 d35abc6 186b0b0 d35abc6 5dd4d67 d35abc6 5dd4d67 d35abc6 5dd4d67 d35abc6 a6e10d9 d35abc6 5dd4d67 a6e10d9 d35abc6 a6e10d9 d35abc6 a6e10d9 d35abc6 a6e10d9 d35abc6 a6e10d9 d35abc6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | """
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><think></code>
ุซู
ูุนุทู ุงูุฅุฌุงุจุฉ ุงูููุงุฆูุฉ.<br>
A ~52M-parameter Arabic reasoning model that thinks step by step inside
<code><think></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())
|