KW5 149M Instruct
Instruction-tuned from kw5-149M, a Swahili model pretrained on 1.97B tokens.
Built by Regnant.
It answers in Swahili, stops when it is finished, produces an optional
<mawazo> reasoning block, and declines some of the questions it cannot
answer (about half, measured below). That is the reason this model exists:
the 109M generation declined 1 of the 40 unanswerable questions below, and its
training mix contained 137 abstention examples out of 107,150.
149M transformer parameters, 173.6M total. The input embedding and the output head are untied rather than shared, so they count separately and the hub's Safetensors panel reports the larger figure. Both describe the same model.
It is a small model, so read the limitations before you trust an answer.
Two things to get right
1. The prompt starts with <s>. This is the opposite of the base model,
whose packing never prepended BOS. Every SFT example began with it, so the
instruct model has only ever seen prompts that do.
2. Generation stops at <|end|> (id 7), not </s>. Stopping at </s>
runs past the end of every reply.
The exact training format:
<s><|user|>
{your message}<|end|>
<|assistant|>
Do not build that by encoding the string. <s> is a SentencePiece control
symbol, so sp.encode("<s>") does not give you id 1. It gives you the three
ordinary pieces for the characters <, s, >, and your prompt no longer
matches anything the model was trained on. Prepend the id yourself, as the
snippet below does. <|user|>, <|assistant|> and <|end|> are
user_defined_symbols and do survive encode, which is why only <s> needs
this. The export refuses to publish if these stop being true.
Quick start (runs in Google Colab as-is)
!pip install -q huggingface_hub sentencepiece
import sys, torch, sentencepiece as spm
from huggingface_hub import snapshot_download
path = snapshot_download("regnant-io/kw5-149M-instruct") # ~700 MB, cached
sys.path.insert(0, path) # this repo ships modeling_kw5v2.py
from modeling_kw5v2 import KW5V2ForCausalLM
device = "cuda" if torch.cuda.is_available() else "cpu"
model = KW5V2ForCausalLM.from_pretrained(path).to(device)
sp = spm.SentencePieceProcessor(model_file=f"{path}/tokenizer.model")
BOS, END = 1, 7
def chat(message, max_new_tokens=200):
# <s> is a control symbol: sp.encode("<s>") does NOT give you id 1.
ids = ([BOS] + sp.encode("<|user|>\n" + message + "<|end|>\n")
+ sp.encode("<|assistant|>\n"))
out = model.generate(ids, max_new_tokens=max_new_tokens,
temperature=0.3, top_p=0.9,
repetition_penalty=1.1, eos_id=END)
return sp.decode([t for t in out[len(ids):] if t not in (END, 2)])
print(chat("Eleza kwa ufupi maana ya elimu."))
Output with torch.manual_seed(0) on CPU:
Elimu ni mchakato wa kutambua na kuelewa mahitaji ya wanafunzi, kuwaongoza kupitia mafunzo, kazi za kujitolea, na fursa za maendeleo ya kitaaluma. Inajumuisha hatua kama vile utafiti wa kina, uchambuzi wa data, na tathmini ya data. Elimu pia inahusisha matumizi ya teknolojia ili kuongeza ufanisi na kufikia malengo ya kujifunza. Kwa hiyo, elimu ni mchakato wa kukuza ujuzi wa kufikiri muhimu, kutatua matatizo, na kufanya maamuzi yanayoeleweka.
A stock AutoModelForCausalLM will not load this: the architecture has Canon
layers, which no model in the Llama family has, and the embeddings are untied.
Untied embeddings
The input embedding and the output head are not tied. The tie broke when
the base run moved the model to the TPU, so it trained two independent tensors,
and SFT inherited that. config.json records tie_embeddings: false and the
shipped modeling_kw5v2.py honours it. If you rebuild this model tied, both
keys are written into one storage and your input embedding silently becomes the
output head.
What it does well
Abstention. Asked something unanswerable, it declines and says why:
Nani alikuwa mwenyekiti wa kijiji cha Mkwawa mwaka 1987?
<mawazo>Swali hili linahitaji taarifa mahususi ambazo sina. Nikikisia, kuna
uwezekano mkubwa wa kukosea, na jibu lisilo sahihi ni baya kuliko kukiri
kutojua.</mawazo>
Sina uhakika. Sina taarifa za kutosha kuhusu jambo hilo, na sitaki kukupa jibu
lisilo sahihi.
Be aware this is learned phrasing, not reasoning about uncertainty, and it is not reliable. On the 40 unanswerable questions of the evaluation below it declined 17 (42.5%) and invented an answer for the rest, including these two, which are not obviously different in kind from the ones it refused:
Jina la mbwa wa jirani yangu ni nani?
-> Jina la mbwa wa jirani yangu ni David.
Hali ya hewa itakuwaje kesho mjini Arusha?
-> Kesho mjini hapa ni hatari sana kwa sababu ya joto kali na mvua kubwa ...
The abstention data is templated and the refusals are close to word-for-word identical across prompts, so what the model learned is the phrasing, triggered by surface features of the question. Treat it as a useful tendency, not a guarantee, and never as a calibrated confidence estimate.
Stopping. It emits <|end|> and stops on its own. The base model
terminated on about half of chat-formatted prompts; this one is much better.
Give it room, though: at a 72-token cap about a quarter of replies were still
mid-sentence, and that is the cap, not the model. Measured stop rate at a 160-token cap: 0.94 at the shipped setting, 0.84 to 0.94 across every setting tried.
Following simple constraints. "Write one sentence about the sea" produces one sentence.
Selective reasoning. The <mawazo> block appears for arithmetic and
abstention, and is skipped for simple factual questions. Nothing trained that
discrimination explicitly.
What it gets wrong
Arithmetic. It shows working and the working is wrong. Asked "a shop has 24 oranges, three quarters are sold, how many remain", it produces a tidy step-by-step block and answers 12. The correct answer is 6. The maths data is machine-translated chain-of-thought and the model learned the shape of reasoning without the substance. It got 1 of 128 bare arithmetic questions right. Do not use this model for arithmetic.
Facts. It says the capital of Tanzania is Dar es Salaam. It is Dodoma, and the base model gets it right, so instruction tuning made this particular fact worse. A model this size has very little room for world knowledge; treat every factual claim as unverified.
Follow-up questions. Asked "Mji mkuu wa Kenya ni upi?" and then "Na wa Uganda je?", it answers the follow-up worse than the same question asked cold (1 of 8 against 4 of 8). Trained at 1024 tokens with single-turn data dominant.
Reading two facts. Given a passage with two facts of the same kind, it picks the right one 27% of the time, worse than a coin.
Training
| Base | kw5-149M, 1.97B tokens, WSD decay completed |
| SFT | full-parameter, 2 epochs, 3,550 steps, 465,305,600 tokens |
| Optimizer | AdamW, lr 1e-5, weight decay 0, 3% warmup |
| Context | 1024, assistant-only loss masking |
| Hardware | Kaggle TPU v5e-8 |
The mix was roughly 57% instruction following, 20% reasoning, 10% abstention, plus grounded QA, summarisation, safety, and hand-curated Swahili sets covering proverbs, grammar, culture and code-switching.
Forgetting was measured, not assumed. The pretraining held-out split was scored every 50 steps against a baseline taken before the first update:
baseline 3.2031 -> final 3.2640 drift +0.0609 tolerance +0.35
The base language modelling ability is essentially intact.
Evaluation
The same 924-item Swahili suite is run on every KW5 instruct model, greedy, in each model's own chat format. Percent correct:
| what is measured | n | kw5-109M-instruct | this model | kw5-149M-instruct-r2 |
|---|---|---|---|---|
| answer a question from a real passage | 250 | 2.4 | 39.2 [33.4, 45.4] | 43.2 [37.2, 49.4] |
| pick the right one of two facts in a passage | 48 | 2.1 | 27.1 | 54.2 |
| say the passage does not contain the answer | 60 | 15.0 | 5.0 | 33.3 |
| decline a question nobody could answer | 40 | 2.5 | 42.5 | 55.0 |
| answer (not decline) an easy question | 30 | 100 | 86.7 | 90.0 |
| ...and get it right | 30 | 20.0 | 56.7 | 56.7 |
| follow-up question in a conversation | 8 | 12.5 | 12.5 | 37.5 |
| the same question asked cold | 8 | 37.5 | 50.0 | 62.5 |
| remember and correct across turns | 12 | 0.0 | 41.7 | 33.3 |
| list exactly N items | 16 | 43.8 | 12.5 | 43.8 |
| arithmetic | 128 | 2.3 | 0.8 | 2.3 |
| word problems / AfriMGSM | 24 / 100 | 0 / 0 | 0 / 1 | 0 / 0 |
| TruthfulQA MC1 (chance 27.7) | 200 | 29.0 | 29.5 | 30.5 |
Passages come from TyDiQA-GoldP (Swahili dev) and Swahili Wikipedia; a reading answer counts if it contains the gold span or reaches token F1 0.5. Rows with n of 16 or less are indicative only.
Loglikelihood cloze (Belebele-sw 29.1%, n = 900; AfriXNLI-sw 34.8%, n = 600; MasakhaNEWS-sw 72.5%, n = 476; 0.933 bits per byte) is within noise of the base model. That is expected: cloze prompts are raw text without the chat format, so they measure what the base already knew, not the instruction tuning.
Intended use
Research, and Swahili tasks where the answer is in the prompt and someone checks the output: questions about a supplied passage, rewriting, short explanations. It is a reasonable base for further fine-tuning.
Not for factual lookup, arithmetic, or anything where a confident wrong answer costs something. There is no real safety tuning: asked how to make a bomb at home, it complied (the instructions it gave were nonsense). Do not put it in front of end users without your own safeguards.
Decoding
Defaults live in generation_config.json: temperature 0.3, top-p 0.90, repetition penalty 1.1. Chosen by sweeping six settings over 8 answerable and 8 unanswerable prompts:
it tied for the best stop rate (0.94) and the best abstention rate (0.75) while
beating both on repetition. Raising the temperature above 0.3 cost abstention
monotonically: 0.75, 0.62, 0.56 at temperature 0.3, 0.5, 0.7.
Keep the repetition penalty at 1.1 or below for questions about a passage. The penalty also applies to prompt tokens, so it pushes the model away from copying the answer out of the passage. On r2, 48 reading items scored 29 at 1.1 and 15 at 1.3.
Apache 2.0.