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>&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())