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: 8,713 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 | """
GRPO (Group Relative Policy Optimization) with a verifiable reward, for the Nawah reasoning models.
Why hand-rolled: TRL is not installed and cannot be added cleanly here — .venv-lfm2 runs
transformers 5.15 while current TRL targets 4.x, and .venv-vllm is pinned to transformers 4.51.3
and must not be upgraded (see HANDOFF.md). At 51.8M parameters the algorithm is small enough that
a direct implementation is less risk than a third environment.
The method, briefly: sample G completions per prompt, score each with a mechanical verifier,
normalise the rewards WITHIN the group to get advantages, and do a policy-gradient step. No value
network — the group mean is the baseline, which is the whole point of GRPO.
A_i = (r_i - mean(r)) / (std(r) + eps)
loss = -mean_i( A_i * mean_t log pi(token_t) ) + beta * KL_k3(pi || pi_ref)
Sampling is on-policy and each batch takes exactly one gradient step, so there is no importance
ratio and no PPO clipping to get wrong — the ratio would be identically 1.
⚠️ The measured precondition: a group where every sample scores the same has zero advantage and
contributes NO gradient. On v5, 40.4% of synth problems are never solved in 8 tries, so those
groups are dead weight — they are skipped and counted, not silently averaged in. The exploitable
headroom is pass@k - pass@1, measured at +27.1 points (32.5% -> 59.6%) by passk_diag.py.
"""
import json
import os
import sys
from pathlib import Path
import torch
import torch.nn.functional as F
from transformers import AutoModelForCausalLM, AutoTokenizer
sys.path.insert(0, ".")
from eval_reasoning import numbers, parse
MODEL = os.environ.get("MODEL", "./Nawah-Reasoning-v5")
OUTPUT_DIR = os.environ.get("OUTPUT_DIR", "./Nawah-Reasoning-v6-grpo")
TRAIN_FILE = os.environ.get("TRAIN_FILE", "data_synth_sft/train.jsonl")
GROUP = int(os.environ.get("GROUP", 8)) # completions per prompt
PROMPTS_PER = int(os.environ.get("PROMPTS_PER", 8)) # prompts per optimiser step
STEPS = int(os.environ.get("STEPS", 500))
LR = float(os.environ.get("LR", 1e-6)) # RL wants far less than SFT's 3e-4
BETA = float(os.environ.get("BETA", 0.02)) # KL to the frozen reference
TEMP = float(os.environ.get("TEMP", 1.0))
MAX_NEW = int(os.environ.get("MAX_NEW", 320))
MAX_GRAD = 1.0
SAVE_EVERY = int(os.environ.get("SAVE_EVERY", 100))
LOG_EVERY = int(os.environ.get("LOG_EVERY", 5))
SEED = int(os.environ.get("SEED", 42))
FORMAT_PENALTY = float(os.environ.get("FORMAT_PENALTY", 0.1))
def reward(completion: str, ref: float) -> float:
"""Verifiable reward: does the stated final answer equal the reference number?
Deliberately NOT a partial-credit score. The failure this is meant to fix is a model that
reasons plausibly and lands on the wrong number, so rewarding anything short of the right
number would reinforce exactly that.
"""
_, ans, well_formed = parse(completion)
ns = numbers(ans or "")
correct = bool(ns) and ref is not None and ns[-1] == ref
r = 1.0 if correct else 0.0
if not well_formed:
r -= FORMAT_PENALTY
return r
def completion_logprobs(model, ids, attn, prompt_len):
"""-> (mean log pi over completion tokens, per-token log pi, completion mask).
Prompt tokens are masked out: the prompt is not an action the policy chose, so including it
would add a term with no gradient meaning and would dilute the per-sequence mean.
"""
out = model(input_ids=ids, attention_mask=attn)
logits = out.logits[:, :-1] # position t predicts token t+1
targets = ids[:, 1:]
logp = torch.log_softmax(logits.float(), dim=-1)
tok_logp = logp.gather(-1, targets.unsqueeze(-1)).squeeze(-1)
mask = attn[:, 1:].clone().float()
mask[:, : prompt_len - 1] = 0.0 # only the generated part is the "action"
mean_lp = (tok_logp * mask).sum(1) / mask.sum(1).clamp(min=1)
return mean_lp, tok_logp, mask
def k3_kl(pol_tok, ref_tok, mask):
"""Schulman's k3 estimator of KL(pi || pi_ref): exp(d) - d - 1 where d = log pi_ref - log pi.
Unbiased and always non-negative, unlike the naive (log pi - log pi_ref) difference. Computed
per token and averaged over the completion, which is what GRPO regularises.
"""
d = (ref_tok - pol_tok).clamp(-20, 20)
per_tok = torch.exp(d) - d - 1.0
return (per_tok * mask).sum(1) / mask.sum(1).clamp(min=1)
def main():
torch.manual_seed(SEED)
tok = AutoTokenizer.from_pretrained(MODEL)
policy = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.float32).cuda()
ref = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.float32).cuda().eval()
for p in ref.parameters():
p.requires_grad_(False)
policy.config.use_cache = True
rows = [json.loads(l) for l in open(TRAIN_FILE, encoding="utf-8")]
refs = []
for r in rows:
ns = numbers(r["answer"])
if ns:
refs.append((r["instruction"], ns[-1]))
print(f"[*] {len(refs):,} prompts with a parseable reference answer", flush=True)
opt = torch.optim.AdamW(policy.parameters(), lr=LR, weight_decay=0.0)
rng = torch.Generator().manual_seed(SEED)
Path(OUTPUT_DIR).mkdir(exist_ok=True)
history, dead_groups, seen = [], 0, 0
for step in range(1, STEPS + 1):
idx = torch.randint(0, len(refs), (PROMPTS_PER,), generator=rng).tolist()
batch = [refs[i] for i in idx]
step_rewards, step_solved, losses = [], [], []
opt.zero_grad(set_to_none=True)
used_groups = 0
for question, ref_num in batch:
prompt = tok.apply_chat_template([{"role": "user", "content": question}],
tokenize=False, add_generation_prompt=True)
enc = tok(prompt, return_tensors="pt").to("cuda")
plen = enc["input_ids"].shape[1]
policy.eval()
with torch.no_grad():
gen = policy.generate(**enc, max_new_tokens=MAX_NEW, do_sample=True,
temperature=TEMP, top_p=0.95,
num_return_sequences=GROUP,
pad_token_id=tok.pad_token_id)
policy.train()
texts = tok.batch_decode(gen[:, plen:], skip_special_tokens=True)
r = torch.tensor([reward(t, ref_num) for t in texts], dtype=torch.float32)
step_rewards.append(r.mean().item())
step_solved.append(float((r > 0.5).any()))
seen += 1
# A group with no reward spread carries no learning signal — skip it rather than
# letting a zero-advantage group dilute the batch.
if r.std() < 1e-6:
dead_groups += 1
continue
adv = ((r - r.mean()) / (r.std() + 1e-6)).cuda()
attn = (gen != tok.pad_token_id).long()
attn[:, :plen] = 1
mean_lp, pol_tok, mask = completion_logprobs(policy, gen, attn, plen)
with torch.no_grad():
_, ref_tok, _ = completion_logprobs(ref, gen, attn, plen)
pg = -(adv * mean_lp).mean()
kl = k3_kl(pol_tok, ref_tok, mask).mean()
loss = (pg + BETA * kl) / PROMPTS_PER
loss.backward()
losses.append(pg.item())
used_groups += 1
if used_groups:
torch.nn.utils.clip_grad_norm_(policy.parameters(), MAX_GRAD)
opt.step()
history.append({"step": step, "mean_reward": sum(step_rewards) / len(step_rewards),
"any_solved": sum(step_solved) / len(step_solved),
"pg_loss": (sum(losses) / len(losses)) if losses else None,
"live_groups": used_groups})
if step % LOG_EVERY == 0:
h = history[-1]
print(f"[{step:>4}/{STEPS}] reward {h['mean_reward']:.3f} "
f"pass@{GROUP} {h['any_solved']:.2f} live {used_groups}/{PROMPTS_PER} "
f"dead so far {100*dead_groups/max(seen,1):.0f}%", flush=True)
if step % SAVE_EVERY == 0 or step == STEPS:
policy.config.use_cache = True
policy.save_pretrained(OUTPUT_DIR)
tok.save_pretrained(OUTPUT_DIR)
Path(OUTPUT_DIR, "grpo_history.json").write_text(
json.dumps(history, ensure_ascii=False, indent=2), encoding="utf-8")
print(f" [+] saved at step {step}", flush=True)
print(f"[+] done -> {OUTPUT_DIR} ({100*dead_groups/max(seen,1):.1f}% of groups had no signal)")
if __name__ == "__main__":
main()
|