Text Generation
Transformers
Safetensors
Arabic
llama
arabic
reasoning
chain-of-thought
math
gsm8k
small-language-model
slm
sft
conversational
text-generation-inference
Instructions to use oddadmix/Nawah-Math-Reasoning with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use oddadmix/Nawah-Math-Reasoning with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="oddadmix/Nawah-Math-Reasoning") messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("oddadmix/Nawah-Math-Reasoning") model = AutoModelForCausalLM.from_pretrained("oddadmix/Nawah-Math-Reasoning", device_map="auto") messages = [ {"role": "user", "content": "Who are you?"}, ] inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use oddadmix/Nawah-Math-Reasoning with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "oddadmix/Nawah-Math-Reasoning" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "oddadmix/Nawah-Math-Reasoning", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/oddadmix/Nawah-Math-Reasoning
- SGLang
How to use oddadmix/Nawah-Math-Reasoning with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "oddadmix/Nawah-Math-Reasoning" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "oddadmix/Nawah-Math-Reasoning", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "oddadmix/Nawah-Math-Reasoning" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "oddadmix/Nawah-Math-Reasoning", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use oddadmix/Nawah-Math-Reasoning with Docker Model Runner:
docker model run hf.co/oddadmix/Nawah-Math-Reasoning
File size: 10,328 Bytes
867d0f3 | 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 | # Nawah-Math-Reasoning β training code
Everything that produced the model in this repo: two dataset pipelines, the SFT trainer, the eval
harness, a `pass@k` diagnostic, and a GRPO implementation. Run top to bottom and you get the
release; run any single stage and it resumes from what is already on disk.
Paths below assume this directory is the working directory and `$P` is a Python with
`torch`, `transformers>=5.15`, `pyarrow` and `huggingface_hub`.
## 0. Two environments, and why
| env | for | pins |
|---|---|---|
| training / eval / Hub | `train_reasoning.py`, `eval_reasoning.py`, everything else | torch 2.9 + cu126, **transformers 5.15** |
| generation | `translate_gsm.py`, `synth_generate.py` | **vLLM 0.8.5.post1**, torch 2.6 + cu124, **transformers 4.51.3** |
They are not interchangeable. The model configs are transformers-v5 format (`rope_parameters`,
`dtype`, `tokenizer_class: TokenizersBackend`) and transformers 4.x cannot read them β but vLLM
0.8.5 breaks on transformers 5.x (`TokenizersBackend has no attribute
all_special_tokens_extended`), so the generation env must stay at 4.51.3. Do not `pip install -U`
in it.
vLLM 0.8.5 is itself a pin: every release from 0.20.2 up requires torch 2.11, which is a CUDA 13
build needing driver β₯ 580. On a CUDA 12.4 host that is a hard stop, and cu12 wheels do not rescue
it (`ImportError: libcudart.so.13`). 0.8.5 is the last cu124 release.
In transformers v5 `TrainingArguments` it is `eval_strategy` (not `evaluation_strategy`) and
`warmup_steps` (there is no `warmup_ratio`).
## 1. `gsm8k-reasoning-ar` β 142,969 machine-translated rows
```bash
$V translate_gsm.py 150000 # vLLM env; ~95 min on 1x A6000; resumable
$P build_dataset.py # -> out_gsm/*.parquet + rejects.jsonl
$P push_dataset.py --repo <user>/gsm8k-reasoning-ar
```
`Ajhesh7/gsm8k-reasoning-SFT-datas` translated with `ByteDance-Seed/Seed-X-PPO-7B`. The trailing
language tag in the prompt is **mandatory**: `Translate the following English sentence into
Arabic:\n{text} <ar>`.
Only 150k of the source's 600,000 rows are translated, and the sample is stratified by question
pattern with a floor of 13 rows/pattern: the corpus expands from just **2,814** question patterns,
so translating all 600k is ~99% redundant.
**The reason `build_dataset.py` has a validator at all:** Seed-X silently corrupts arithmetic in
~0.6% of segments β `$13751` β `13571`, `4 Γ 44 = 176` β `4 Γ 46 = 176`. These read as fluent
Arabic and pass any fluency check. So every row is numeral-audited, asymmetrically: **reasoning
chains strictly** (every numeral must survive exactly), **questions leniently** (a value β€ 12 may
be verbalised β `6 friends` β `Ψ£Ψ΅Ψ―ΩΨ§Ψ¦ΩΨ§ Ψ§ΩΨ³ΨͺΨ©` β but any number the English never contained is
rejected). Naive exact-multiset matching rejects ~6% of *correct* translations; do not "simplify"
it back to that.
`translate_gsm.py` appends every chunk to `out_gsm/translations.jsonl` and skips cached segments on
restart, so an interruption costs at most one 20k chunk. Do not pass `enable_prefix_caching=True`
β it hangs at startup on this vLLM/V1 engine.
## 2. `arabic-math-reasoning-synth` β 120,462 verified rows
```bash
# general pool: 100,323 rows, ~22 h
GEN_MODEL=<path>/gemma-3-12b-it BACKEND=vllm OUT_DIR=out_synth TARGET=100000 \
BATCH=256 MAX_NEW=1200 MAX_LEN=3072 GPU_UTIL=0.90 \
nohup $V -u synth_generate.py > synth_run.log 2>&1 &
# relational pool: 20,139 rows, ~3.5 h. Disjoint task-id range, on purpose.
GEN_MODEL=<path>/gemma-3-12b-it BACKEND=vllm OUT_DIR=out_synth_rel POOL=relational \
START_TASK=1000000 TARGET=20000 BATCH=256 MAX_NEW=1200 MAX_LEN=3072 GPU_UTIL=0.90 SEED=1234 \
nohup $V -u synth_generate.py > synth_rel.log 2>&1 &
./finish_merge_push.sh # merge primary cache + any node shards -> out_merged_v6/
$P split_synth_v6.py # -> data_synth_v6_sft/{train,eval,eval_rel}.jsonl
$P push_synth_dataset.py --repo <user>/arabic-math-reasoning-synth
```
**Resumability is the design, not a feature.** A task is one generation call asking for 4 problems,
and task *N*'s prompt is a pure function of *N* (`synth_common.build_task`) β nothing about the
plan is persisted, so a restart redraws identical prompts. Finished tasks are appended to
`generations.jsonl` and fsynced; a torn final line is dropped with a warning. The stop condition is
**accepted rows, not tasks**.
**Raw completions are stored, never just the parsed rows.** Every validator change can be re-scored
over the whole cache with no GPU β which is how the accept rate went 48.4% β 64.9% without
regenerating anything.
**The arithmetic audit** (`synth_common.validate`) re-evaluates every `a op b = c` in the reasoning
and one wrong equation rejects the row. Two things it must keep doing, both of which were bugs that
rejected *correct* rows:
1. **Equation chains.** Models write `75 + 15 Γ 5 = 75 + 75 = 150`. Reading only to the first `=`
compares 150 against 75 and rejects a correct chain. Split the whole chain, require every
segment to agree.
2. **Rounding.** `3200 / 60 = 53.33` is arithmetic as people write it. `_close()` forgives rounding
*at the precision the model displayed* (and floor/ceil for integers), so a genuinely wrong
number still fails.
Also rejected: `noop_step` (`63 + 0 = 63`, padding to hit a step count), `meta_commentary`, Latin
residue, missing conclusion marker, and answers that disagree with the last computed value.
**The relational pool is a separate pool** (`RELATIONAL_OPS`), deliberately not appended to
`OPERATIONS`. Appending would change `rng.choice()` for every task id and silently break
reproducibility of the first 100,323 rows. `build_task(task_id, seed, pool)` takes
`pool="default"` or `"relational"`; keep it that way.
Check `unique_templates` in `build_stats.json` rather than assuming the variation grid worked.
## 3. SFT
```bash
$P prepare_data.py # Arabic_Reasoning_Dataset -> data/{train,eval}.jsonl
$P prepare_gsm_sft.py # -> data_gsm_sft/{train,eval}.jsonl
$P prepare_v6_sft.py # three-way mix -> data_v6_sft/{train,eval}.jsonl
BASE_MODEL=<base> OUTPUT_DIR=./Nawah-Math-Reasoning \
TRAIN_FILE=data_v6_sft/train.jsonl EVAL_FILE=data_v6_sft/eval.jsonl \
MAX_LENGTH=768 EPOCHS=5 BATCH_SIZE=64 GRAD_ACCUM=1 WARMUP_STEPS=200 EVAL_STEPS=1000 LOAD_BEST=0 \
$P -u train_reasoning.py 2>&1 | tee train.log # 21,535 steps, ~85 min on 1x A6000
```
**`LOAD_BEST=0` is load-bearing.** Eval loss selects the *worse* checkpoint on this ladder, and
that was measured rather than assumed: on a corpus with no repeated rows, the minimum-loss
checkpoint scored 30.9% where the final scored 35.6%. It held for four consecutive runs. Ship the
final checkpoint.
**Answer styles are not normalised.** GSM8K rows end in a bare numeral, the other two corpora in an
`Ψ₯Ψ°ΩΨ β¦` sentence. Rewriting them into one style deletes what the mix adds, so a `source` tag rides
on every row and eval scores each half on its own terms.
The eval splits are **pinned across versions** β the same 400 `Arabic_Reasoning` rows and the same
600 GSM8K rows since the first model, and `split_synth_v6.py` copies the previous synth eval rows
through verbatim rather than re-shuffling. Re-drawing would have moved 1,955 of 2,000 held-out
items into train.
## 4. Eval
```bash
EVAL_FILE=data_v6_sft/eval.jsonl $P eval_reasoning.py ./Nawah-Math-Reasoning 1800
EVAL_FILE=data_synth_sft/eval.jsonl $P eval_reasoning.py ./Nawah-Math-Reasoning 1000
```
Greedy. Reports well-formedness, **number agreement**, exact match and reasoning length, broken
down per `source` when the rows carry the tag. `EVAL_FILE` comes from the env β pointing it at the
wrong split silently scores against the wrong data.
Score on **number agreement**, not exact match: with two answer styles in the mix, exact match
measures style compliance, not arithmetic.
## 5. RL β run the diagnostic first
```bash
$P -u passk_diag.py ./Nawah-Math-Reasoning # 240 problems x k=8 @ T=1.0, ~25 min
```
RLVR reweights samples the model already produces. A group where every sample is wrong scores
all-zero, the advantage is zero, and there is no gradient β so exploitable headroom is bounded by
`pass@k β pass@1`. On the previous version that was **+27.1 points**, with 40.4% of problems never
solved in 8 tries. Run this before spending a GPU-hour.
That diagnostic is also what produced this release: the dead 40% turned out to be a *data* gap
(relational comparisons were 1.34% of the corpus), so the fix was Β§2's relational pool through
SFT, not RL. **Data first, then RL** β running RL first leaves the dead tail untouched and caps
the gain.
```bash
MODEL=./Nawah-Math-Reasoning OUTPUT_DIR=./Nawah-Math-Reasoning-grpo \
GROUP=8 PROMPTS_PER=8 STEPS=500 LR=1e-6 BETA=0.02 \
$P -u grpo_train.py 2>&1 | tee grpo.log
```
`grpo_train.py` is hand-rolled β TRL is not installable against these pins. Group-normalised
advantage with no value network, binary final-answer reward (not partial credit β the failure being
fixed is fluent reasoning landing on a wrong number), KL to a frozen reference via Schulman's k3
estimator, and no importance ratio or PPO clipping because sampling is on-policy with one step per
batch. Zero-spread groups are skipped **and counted**; the dead-group percentage in the log is the
number to watch.
## 6. Release
```bash
$P build_release_code.py # stage this directory
$P push_release.py --dry-run # render the card only
$P push_release.py
```
Every number in the model card is read from an eval JSON on disk. Nothing is typed by hand, so the
card cannot drift from the measurements.
## Demo
`space/` is the Gradio demo, deployable as-is to a Space. Two things that break it if "cleaned up":
the streamer must use `skip_special_tokens=False` (`<think>`/`</think>` are real special tokens and
stripping them destroys the reasoning/answer split), and `render_prompt()` must stay byte-identical
to the trainer's rendering β a 52M model is very sensitive to format drift.
On ZeroGPU, `import spaces` must come **before** torch, and `model.to("cuda")` at startup must stay
unguarded: no GPU is attached at startup and the call is replayed inside the forked GPU process.
|