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.
Architecture
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.
Special Tokens
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
What It Can Do
Works well
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.
Known Failures and Limitations
Entity tracking — not reliable
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.
Greeting token misfires
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.
Consumer / niche knowledge gaps
FineWeb-Edu is educational text. The model has weak or absent knowledge of:
Consumer electronics (phones, gaming consoles, specific products)
Pop culture, celebrities, recent events
Cooking, recipes, practical how-to tasks
Sports, music, entertainment
Anything post-training-cutoff
The model degrades gracefully — it gives a plausible-sounding response based on semantic context rather than outputting garbage. But the responses are vague.
Numerical precision
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).
Multi-turn context bleeding
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.
Factual confabulation
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.
The Bioluminescence Experiment 🔬
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:
bash
1grep -i "bioluminescen" data/sft_direct.jsonl &&echo"FOUND"||echo"CLEAN"2python inference/inference.py --checkpoint checkpoints/finetune
3# → 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.
Pretrain vs 24M Comparison
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.
SFT Dataset Design (8 categories)
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.
Expected output: ~9,000–10,000 unique examples after light deduplication
Greeting and goodbye categories are HC-only (API generation deduplicates to near-zero)
Verify distribution:
bash
1python3 -c "
2import json
3from collections import Counter
4data = [json.loads(l) for l in open('data/sft_direct.jsonl') if l.strip()]
5cats = Counter(d['_meta']['category'] for d in data)
6toks = Counter()
7for d in data:
8 for t in d['conversations']:
9 if t['role'] == 'sana':
10 c = t['content']
11 for tok in ['<sana_salute>','<sana_happy>','<sana_think>','<sana_sad>','<sana_smug>']:
12 if c.startswith(tok): toks[tok] += 1; break
13print(f'Total: {len(data)}')
14print('Categories:', dict(sorted(cats.items())))
15total_t = sum(toks.values())
16[print(f' {t:<18} {c:>4} ({c/total_t*100:.0f}%)') for t,c in sorted(toks.items(), key=lambda x:-x[1])]
17"
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).
8. Test fine-tuned model
bash
1# Test the sweet spot checkpoint2python test_finetune.py
Pass threshold: 70%. The 100M consistently reaches 90.9% at epoch 3.
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.
python
1# inference/inference.py — SanaInference.__init__2max_history_turns:int=1,# change from 3 to 1
Commands at the prompt:
reset — clear conversation history
quit / exit — exit
Config Files
pretrain/config_100m_h100.yaml
yaml
1model:2hidden_dim:6403num_layers:184num_heads:105ffn_multiplier:2.6676vocab_size:160007max_seq_len:51289training:10batch_size:128# lowered from 256 due to VRAM11learning_rate:2.0e-412warmup_steps:50013bf16:true14use_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.
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.