Downloads ยท 30 days
0
Hera-Berg/sana-100M-instruct
sana-100M-instruct is a machine learning model from Hera-Berg. Use it for the machine learning task on the model card, and read the license before you ship it in a product.
A ~100M parameter language model trained from scratch. No Hugging Face transformers. No SentencePiece. No flash-attn. Every component hand-implemented in pure PyTorch. Scale-up from the 24M edition.
Downloads ยท 30 days
0
Access
Public
Updated Jul 1, 2026
Repo size
1.2 GB
Likes
0
Public
Click a slice to open those files.
.pt1.2 GB ยท 100%
From the Hugging Face model README
A ~100M parameter language model trained from scratch. No Hugging Face transformers. No SentencePiece. No flash-attn. Every component hand-implemented in pure PyTorch. Scale-up from the 24M edition.
| Component | Detail |
|---|---|
| Parameters | ~98.75M unique (lm_head weight tied to embedding) |
| Hidden dim | 640 |
| Layers | 18 TransformerBlocks |
| Attention heads | 10 |
| Head dim | 64 (640 / 10) โ same as 24M |
| FFN intermediate | 1,707 (640 ร 2.667 rounded, SwiGLU) |
| Vocabulary | 16,000 tokens (BPE, pure Python) โ same tokenizer as 24M |
| Max sequence | 512 tokens |
| Position encoding | RoPE (Rotary Position Embeddings), base=10000 |
| Normalisation | RMSNorm (pre-norm architecture) |
| Activation | SwiGLU (gated FFN) |
| KV Cache | Yes (per-layer, allocated during generation) |
| Tied embeddings | Yes (input/output share one weight matrix) |
| Dropout | 0.1 during pretrain, 0.0 during SFT |
Why num_heads=10: gives head_dim=64, a clean power of 2, same as the 24M model. Attention math is identical โ only width and depth changed.
Identical to the 24M model. Tokenizer is fully shared.
| ID | Token | Purpose |
|---|---|---|
| 0 | <|pad|> | Padding |
| 1 | <|user|> | User turn marker |
| 2 | <|sana|> | Sana turn marker |
| 3 | <|end|> | End of turn / EOS |
| 8 | <sana_salute> | Sana emotion: ๐ชผ greeting |
| 9 | <sana_happy> | Sana emotion: โจ happy |
| 10 | <sana_think> | Sana emotion: ๐ค thinking |
| 11 | <sana_sad> | Sana emotion: ๐ sad |
| 12 | <sana_smug> | Sana emotion: ๐ smug |
Factual questions on topics covered by FineWeb-Edu
You: what is a black hole
Sana: ๐ค Black holes are regions in spacetime where gravity is so strong that nothing,
not even light, can escape. They're formed from collapsing stars.
You: explain photosynthesis
Sana: ๐ค Photosynthesis is the process by which plants convert sunlight, carbon dioxide,
and water into glucose and oxygen. It's basically the way we use energy.
You: what is nuclear fusion
Sana: ๐ค It's the process where light collapses into energy, releasing vast amounts of
energy. Pretty fascinating.
Emotional routing
You: im sad
Sana: ๐ That's rough. What's going on?
You: i got the job!!
Sana: โจ Nice one. When do you start?
You: cool!
Sana: โจ Glad you think so!
Identity
You: who are you
Sana: ๐ I'm Sana. Compact AI. Short answers, dry delivery.
You: tell me your name
Sana: ๐ค Sana. What do you need?
You: what do you like
Sana: โจ Jellyfish. Octopuses. Black holes. Things that shouldn't work but do.
Graceful degradation on out-of-domain queries
You: how do i cook a potato
Sana: ๐ค You can use my own recipe or mix it with some spice. Your choice.
You: what is a nintendo switch
Sana: ๐ค A switch for moving parts. Sounds familiar?
The model doesn't know consumer electronics but infers from semantic context rather than hallucinating a different topic. It knows "Nintendo Switch" is a thing, it just doesn't know what kind of thing specifically.
Jellyfish (Sana's speciality)
You: tell me about jellyfish
Sana: ๐ค Jellyfish have been around for over 500 million years. They're basically immortal.
Their tentacles can revert to their juvenile form in the ocean.
You: anything else?
Sana: โจ Jellyfish have been around for over 500 million years. They're basically the
ultimate survivors.
The model was trained on only 8 handcoded entity-tracking examples. It cannot consistently remember names introduced earlier in a conversation.
You: my cat is named Lumi
Sana: ๐ That's cool! What's going on?
You: what is my cat's name
Sana: ๐ค I'm Sana. Hi, question. How can I find you? โ forgets Lumi
Fix for next iteration: expand HC_ENTITY to 50+ examples covering pets, people, and self-introduction across 3-4 turn conversations.
The model correctly produces greeting-appropriate content but sometimes uses the wrong emotion token. "hi" occasionally produces โจ instead of ๐ชผ. The content routing is correct; only the emoji is wrong.
You: hi
Sana: โจ Hey. What do you need? โ should be ๐ชผ, content is right
Root cause: greeting responses (avg 5 tokens) have ~4ร less gradient signal per example than empathy responses (avg 20 tokens). Fix requires first-token loss weighting in finetune.py.
FineWeb-Edu is educational text. The model has weak or absent knowledge of:
The model degrades gracefully โ it gives a plausible-sounding response based on semantic context rather than outputting garbage. But the responses are vague.
The pretrain data contained unicode artifacts (e28099, \x10, \x12) from pre-unicode-fix downloads. Some numerical outputs contain garbled characters.
You: what is the speed of sound
Sana: ๐ค Sound can travel at about 1/)th that of a vacuum. โ \x00 artifact
Fix: retrain pretrain on clean data (unicode fix was applied to download_data.py and tokenize_data.py but the 100M pretrain used older-downloaded shards).
With max_history_turns=3, responses from several turns ago can influence current responses in unexpected ways. Set max_history_turns=1 in inference.py to mitigate.
The model produces confident-sounding but wrong answers, especially on specific facts (dates, numbers, proper names).
You: who is aristotle
Sana: ๐ค Aristotles are ancient Greek philosophers with profound knowledge and wisdom.
Their works have been around for over 500 years. โ 2,400 years, not 500
At 100M parameters trained on 7.8B tokens this is expected. RAG is stubbed out in inference.py for future implementation.
Same experiment as the 24M edition. Demonstrates that pretrain knowledge survives SFT.
"bioluminescence" does not appear anywhere in data/sft_direct.jsonl. Ask the model anyway:
grep -i "bioluminescen" data/sft_direct.jsonl && echo "FOUND" || echo "CLEAN"
python inference/inference.py --checkpoint checkpoints/finetune
# โ What is bioluminescence?
Expected: a response describing light from chemical reactions in living organisms, using <sana_think>, despite never appearing in SFT data. Passes consistently at epoch 3.
| Metric | 24M | 100M |
|---|---|---|
| Parameters | 23.85M | 98.75M |
| Pretrain tokens | 2.09B | 7.84B |
| Pretrain val loss | 2.048 | 2.053 |
| Pretrain test score | 68โ76% | 72โ84% |
| SFT examples | ~1,200 | ~9,690 |
| SFT epochs (sweet spot) | 5 | 3 |
| SFT optimizer steps | ~375 | ~1,800 |
| SFT lr | 1.5e-5 | 1.0e-5 |
| Best SFT test score | 81.8% | 90.9% |
The 100M pretrain val loss (2.053) matches the 24M (2.048) despite 4ร more parameters because the pretrain data still contained unicode artifacts from pre-fix downloads. The test score improvement (76% โ 84%) shows the 100M learned more despite identical val loss.
The most noticeable practical difference is response coherence. The 24M degrades to plausible-sounding nonsense on topics outside its confident knowledge โ it will confidently produce grammatically correct sentences with wrong or invented content. The 100M, trained on 7.8B tokens vs 2B and with a larger SFT dataset, maintains coherent reasoning even when it doesn't know the answer. As long as the user input is clean (no garbled text, no extreme prompt injection), the 100M will not produce word salad โ it either gives a reasonable answer, hedges, or stays on topic. Out-of-domain queries degrade gracefully to vague-but-coherent responses rather than collapsing entirely.
The 100M uses an 8-category SFT design versus the 24M's 3-category design.
| Category | Target % | Token | Source |
|---|---|---|---|
| greeting | 12% | <sana_salute> | HC only |
| goodbye | 8% | <sana_salute> | HC only |
| identity | 10% | <sana_smug> | HC + API |
| smalltalk | 10% | <sana_smug> | HC + API |
| factual | 35% | <sana_think> | HC + API |
| empathy_sad | 15% | <sana_sad> | HC + API |
| empathy_happy | 8% | <sana_happy> | HC + API |
| entity | 2% | any | HC only |
Greeting and goodbye are HC-only because API generation for these categories collapses under deduplication โ "user sends 'hi'" causes GPT to always output user: "hi", leaving one unique example per seed. The hardcoded set covers all variants.
pip install torch pyyaml datasets numpy tqdm openai requests
python pretrain/download_data.py \
--output_dir data/pretrain_raw \
--max_tokens 4500
python tokenizer/train_tokenizer.py \
--data_dir data/pretrain_raw \
--vocab_size 16000 \
--sample_lines 100000 \
--output tokenizer/sana_tokenizer/tokenizer.json \
--force
Critical: do NOT use --dialogue_dir. That flag corrupts BPE merges and causes subword fragmentation ("ing vironment" instead of "environment").
python pretrain/tokenize_data.py \
--data_dir data/pretrain_raw \
--output_dir data/pretrain_bin \
--shard_size 200 \
--workers 32
H100 (80GB) โ recommended:
python pretrain/pretrain.py --config pretrain/config_100m_h100.yaml
ls checkpoints/pretrain/ckpt_*.pt | sort | head -n -3 | xargs rm -f
Resume from checkpoint after crash:
python pretrain/pretrain.py \
--config pretrain/config_100m_h100.yaml \
--resume checkpoints/pretrain/ckpt_XXXXXX.pt
RTX 4060 (8GB) โ not recommended for 100M: Training a 100M model on a 4060 takes ~5 days for a full run. Use H100 or a cloud GPU.
Expected loss curve (H100, 100M):
| Step | Val loss |
|---|---|
| 16,000 | ~2.286 |
| 30,000 | ~2.217 |
| 62,000 | ~2.133 |
| 119,630 | ~2.053 |
python test_pretrain.py --checkpoint checkpoints/pretrain
Pass threshold: 60%. The 100M scores 72โ84% depending on temperature sampling variance.
export OPENAI_API_KEY="sk-..."
python pretrain/generate_direct_sft.py \
--output data/sft_direct.jsonl \
--repeat 3 \
--n_api 15000 \
--target 15000 \
--workers 30
Verify distribution:
python3 -c "
import json
from collections import Counter
data = [json.loads(l) for l in open('data/sft_direct.jsonl') if l.strip()]
cats = Counter(d['_meta']['category'] for d in data)
toks = Counter()
for d in data:
for t in d['conversations']:
if t['role'] == 'sana':
c = t['content']
for tok in ['<sana_salute>','<sana_happy>','<sana_think>','<sana_sad>','<sana_smug>']:
if c.startswith(tok): toks[tok] += 1; break
print(f'Total: {len(data)}')
print('Categories:', dict(sorted(cats.items())))
total_t = sum(toks.values())
[print(f' {t:<18} {c:>4} ({c/total_t*100:.0f}%)') for t,c in sorted(toks.items(), key=lambda x:-x[1])]
"
Target token distribution: smug ~25%, sad ~22%, think ~20%, happy ~15%, salute ~14%.
Update pretrain_checkpoint in finetune/config_100m.yaml to point at your best pretrain checkpoint, then:
python finetune/finetune.py --config finetune/config_100m.yaml
checkpoints/finetune/Note on reported avg loss: finetune.py reports avg_loss at ~4ร the real value due to gradient accumulation interaction. Real epoch loss = reported รท 4. Real sweet spot: 0.45โ0.55 real avg loss (reported: ~1.8โ2.2).
# Test the sweet spot checkpoint
python test_finetune.py
Pass threshold: 70%. The 100M consistently reaches 90.9% at epoch 3.
python inference/inference.py \
--checkpoint checkpoints/finetune \
--temperature 0.4
Important: set max_history_turns=1 in inference.py before running. The SFT training data is mostly single-turn โ 3 turns of history causes context bleeding from previous exchanges.
# inference/inference.py โ SanaInference.__init__
max_history_turns: int = 1, # change from 3 to 1
Commands at the prompt:
reset โ clear conversation historyquit / exit โ exitpretrain/config_100m_h100.yamlmodel:
hidden_dim: 640
num_layers: 18
num_heads: 10
ffn_multiplier: 2.667
vocab_size: 16000
max_seq_len: 512
training:
batch_size: 128 # lowered from 256 due to VRAM
learning_rate: 2.0e-4
warmup_steps: 500
bf16: true
use_compile: true
Batch size was lowered from 256 to 128 during the actual run due to VRAM constraints. This automatically doubled the step count to maintain the same token coverage.
finetune/config_100m.yamltraining:
pretrain_checkpoint: checkpoints/pretrain/ckpt_119630.pt
learning_rate: 1.0e-5
num_epochs: 7
batch_size: 4
gradient_accumulation: 4 # effective batch = 16
warmup_steps: 200
Identical to the 24M edition plus the 100M-specific configs:
sana/
โโโ model/
โ โโโ model.py
โโโ tokenizer/
โ โโโ train_tokenizer.py
โ โโโ tokenizer.py
โ โโโ sana_tokenizer/
โ โโโ tokenizer.json
โโโ pretrain/
โ โโโ download_data.py
โ โโโ tokenize_data.py
โ โโโ pretrain.py
โ โโโ generate_direct_sft.py โ 8-category design for 100M
โ โโโ config.yaml โ 24M RTX 4060
โ โโโ config_h100.yaml โ 24M H100
โ โโโ config_100m_h100.yaml โ 100M H100 โ new
โ โโโ config_100m_4060.yaml โ 100M RTX 4060 (not recommended)
โโโ finetune/
โ โโโ finetune.py
โ โโโ config.yaml โ 24M SFT config
โ โโโ config_100m.yaml โ 100M SFT config โ new
โโโ inference/
โ โโโ inference.py
โโโ test_pretrain.py
โโโ test_finetune.py
โโโ README.md โ 24M edition
โโโ README_100M.md โ this file
More SFT data is required. The 24M worked at ~1,200 examples ร 5 epochs = 375 optimizer steps. The 100M has 4ร more parameters and needs proportionally more SFT signal. This run used ~9,700 examples ร 7 epochs = ~4,200 optimizer steps. The sweet spot is epoch 3 (~1,800 steps) โ the model learns the behavioral format cleanly before starting to overfit.
SFT teaches format, not facts (same principle, more important at scale). At 100M the pretrain knowledge is richer and harder to override. Heavy SFT (more epochs, more repetition) degrades factual quality more than at 24M. Keep epoch count conservative and use the earliest checkpoint that passes the behavioral tests.
Greeting/goodbye require HC-only training. Generic API seeds for greetings collapse under deduplication โ GPT generates user: "hi" for any seed phrased as "user sends hi", leaving one unique example per seed. The HC set covers all variants (hi, hello, hey, sup, yo, good morning, etc.) with repeat=3 ร 5 = 15 copies each, which is sufficient.
Token-level loss imbalance. Short responses (greetings: 1-3 tokens) have much less gradient signal than long responses (factual: 15-30 tokens). The greeting emotion token (<sana_salute>) is systematically under-trained relative to <sana_think>. This causes occasional greeting token misfires (โจ instead of ๐ชผ) that are cosmetic โ the content routing is correct. A first-token weighted loss in finetune.py would fix this in a future iteration.