
BitMamba 1B — training the first SSM model
Context
The TatuEngine’s hybrid CPU/GPU pipeline was already working — 252× speedup, Block Codec compressing 36 GB into 281 MB (128×), 41.9 µs per token. But there was a critical gap: the model couldn’t generate coherent text.
BitMamba-1B is a PyTorch implementation of Mamba-2, with 48 layers. ~1.4B parameters total (1.446B). Compared to a Transformer of similar size — 7B, 70B — it seems small, but for a pure SSM it’s still a 7-headed beast.
The problem is that Mamba doesn’t follow the same transfer learning path as Transformers. You don’t take a Llama-2 checkpoint and fine-tune it. There’s no from_pretrained that works — the representation space is fundamentally different.
The training timeline
Full Warmup — 200 pretrain steps
The first checkpoint, full_warmup, was trained with pure pretrain data (train_ids.pt, 96MB). 200 steps, batch 2, GPT-2 tokenizer. Result: a 1.94 GB checkpoint that knows token statistics but hasn’t formed language — perplexity ~444M on holdout.
# Loading checkpoint full_warmup/best
# Auto-detect: dims read from checkpoint
# Parameters: 1,446,747,136 total
# VRAM: 2.93 GB
Phase 2 — SFT with cold start
I took full_warmup and tried supervised fine-tuning with 1000 reasoning examples (code, systems, deduction). Three attempts:
| Attempt | Dataset | Batch | Result |
|---|---|---|---|
| v1 | 167 ex | 1 | Loss ~100 — won’t descend |
| v2 | 1000 ex | 2 | CUDA error — OOM on backward |
| v3 | — | — | Didn’t complete |
An average loss of 100 means the model was guessing almost uniformly among 50K vocabulary tokens. log(50288) ≈ 10.8 would be the loss of a random model. Our loss of 100+ indicated that the model wasn’t just failing to learn — it was actively sabotaging itself, probably through exploding activations or NaN gradients.
# Sample generated at step 100 (Phase 2):
# "372366 � partName� startlingOpt elevate Gemuchs charitychelamer Ideally..."
# Format [THOUGHT]:
# Format [ANSWER]:
This isn’t language. It’s token noise with positional bias.
Phase 3 — Continuous SFT
I continued training from the best Phase 2 checkpoint. Evaluation result:
EVALUATION REPORT
──────────────────────────────────
Average loss: 19.9131
Perplexity: 444,768,275.92
Tokens evaluated: 84,844
Examples: 201
By domain:
code: Loss 19.87 PPL 424M
deduction: Loss 19.33 PPL 249M
systems: Loss 19.78 PPL 388M
Perplexity of 444 million. For reference: a random-guessing model has perplexity ~50K (log perplexity ~vocab size). 444M means the model’s token distribution is worse than random — it actively assigns low probability to the correct tokens.
Sample generated by the model:
"K Moon acoustic sign Ossvertis0 ParkM em anINa� usel�vertis scoutingl Pac..."
Diagnosis: what went wrong?
Three problems combined:
1. Under-trained pretrain
200 warmup steps isn’t enough for a 1.4B parameter model. For reference, Mamba-2 models in the literature are trained on hundreds of billions of tokens. We had maybe 10M tokens.
The warmup serves to stabilize activations — A_log, dt_bias, norms — but not to form linguistic representations. Semantics emerge at data scales 3-4 orders of magnitude larger.
2. SFT too early
I skipped the continued pretrain phase and went straight to SFT with 1000 examples. The result is catastrophic because:
- The model has no internal language representation to fine-tune
- The SFT loss pushes the model in directions that shallow pretrain can’t sustain
- With batch 1 (VRAM limitation), the gradient is extremely noisy
3. Mixing semantic data with a non-semantic model
It’s like trying to teach quantum physics to someone who hasn’t learned to count. SFT assumes the model already “understands” language — it just needs to be adjusted for a specific format (user: ...\nassistant: ...). Without the foundation, SFT destroys instead of builds.
The solution: Phase 4 — Hybrid Pipeline
The resurrection plan (Phase 4) attacks all 3 problems at once:
Phase 1 (steps 0000-0300): PURE PRETRAIN
train_ids.pt, pretrain only
Goal: bring down perplexity + stabilize cross-entropy
Phase 2 (steps 0301-2000): CO-TRAINING 3:1
3 pretrain examples : 1 SFT example per batch
Loss = w_pretrain * L_pretrain + w_sft * L_sft
w_pretrain: 0.70 → 0.50 (linear decay)
Natural language learning anchors the SFT
Phase 4 hyperparameters
| Parameter | Value | Reason |
|---|---|---|
| Batch size | 4 | Maximum that fits in 12GB VRAM |
| Max seq len | 768 | Larger context than Phase 2 (512) |
| Learning rate | 5e-5 | More aggressive than Phase 2 (5e-6) |
| Warmup | 200 steps | Tenth of total training |
| Sequence dtype | bf16 | Saves VRAM (vs fp32) |
| Grad clip | 1.0 | Prevents gradient explosion |
The magic: 4 parameter groups with separate LR
The most important trick is treating each part of the model with different learning rates:
groups = [
{'name': 'embed', 'lr': lr * 0.3, 'weight_decay': 0.01}, # vocabulary
{'name': 'state', 'lr': lr * 10.0, 'weight_decay': 0.0}, # A_log, D, dt_bias
{'name': 'residual', 'lr': lr * 0.5, 'weight_decay': 0.01}, # MLP + norm
{'name': 'base', 'lr': lr, 'weight_decay': 0.01}, # mixer, proj
]
- Embeddings at 0.3×: The vocabulary was already reasonably trained in warmup — no need for drastic changes
- State params at 10×: This is the shock to the hippocampus. A_log, D, dt_bias are the parameters that control the SSM’s memory. If they’re not pushed hard, the model never learns to carry state between tokens
- Residual at 0.5×: The MLP + RMSNorm layers already have some structure — change slowly to avoid breaking
- Base at 1.0×: Linear projections (in_proj, out_proj, conv1d) — the model’s engine
Complete reasoning dataset structure
For Phase 4, the SFT dataset has 1000 examples balanced by domain using sample weights:
| Domain | Examples | Weight | Effect |
|---|---|---|---|
| code | 392 | 0.85 | Light oversample |
| systems | 340 | 0.98 | Neutral |
| deduction | 268 | 1.24 | Oversample to compensate minority |
Each example follows the format:
user: <prompt>
assistant: <response with [THOUGHT] and [ANSWER]>
Co-training loss weighting
The key insight of co-training is that the model can’t forget pretrain while learning SFT:
co_progress = (global_step - PHASE1_STEPS) / max(1, PHASE2_STEPS)
w_pretrain = 0.70 + (0.50 - 0.70) * co_progress # 70% → 50%
w_sft = 1.0 - w_pretrain # 30% → 50%
loss = w_pretrain * loss_pretrain + w_sft * loss_sft
It starts with 70% weight on pretrain (to maintain the foundation), and gradually drops to 50% (giving more room for SFT to shape the output).
Gradient telemetry at step 1
An addition I paid dearly to learn: measure the gradient on the first step.
if global_step == 1:
raw_norm = clip_grad_norm_(model.parameters(), max_norm=inf)
zero_count = sum((p.grad.abs() < 1e-8).all() for p in model.parameters()...)
print(f"Grad norm: {raw_norm:.6f}")
print(f"{zero_count}/{total_grad} params with grad ≈ 0")
This immediately detects if:
- The gradient is exploding (norm > 100)
- The gradient is dead (80%+ of params with grad ≈ 0)
- Some subgroup isn’t receiving gradients (e.g., frozen state params)
A_log telemetry at step 50
A_log controls the SSM’s decay — essentially the model’s “memory”:
if global_step == 50:
mean_A = exp(mean_a_log)
mem_tau = 1.0 / max(1e-10, mean_A)
print(f"Mean A_log: {mean_a_log:.4f} | |A|: {mean_A:.4f} | τ: ~{mem_tau:.0f} tok")
If τ < 1, the model forgets everything between consecutive tokens — impossible to learn long-range dependencies. The ideal is τ ≈ 100-500 tokens.
Lessons learned
1. Mamba is more sensitive to hyperparameters than Transformer
A Transformer with SGD + simple warmup converges to a reasonable loss even with insufficient data. Mamba SSM has 3 coupled subsystems (temporal conv1d + recurrent SSM + linear projections) that need to be balanced. The warmup needs to be longer, the LR needs to be lower, and gradient clipping is mandatory.
2. Batch size 1 is a waste of time
With 12GB of VRAM, the maximum I could achieve was batch 4 (seq_len 768) or batch 8 (seq_len 512). Batch 1, which I used in Phase 2, produces such noisy gradients that loss doesn’t descend — even with grad_accum 4.
3. Parameter groups with separate LR aren’t optional — they’re what saves you
Without the state group at 10× LR, the SSM’s memory never learns to propagate information. Without embed at 0.3× LR, the vocabulary corrupts in 50 steps. This separation by function (vocab, memory, reasoning, projection) was the most expensive insight — it cost ~3 weeks of failed training.
4. Perplexity of 444M is worse than random — but it’s diagnosable
Log perplexity > log vocab_size = the model is actively wrong. Almost certain cause: diverging activations combined with softmax that pushes probability toward specific tokens (usually the first ones in the vocabulary, positions 0-1000). Solution: activation clipping + monitoring mean A_log.
5. Co-training is fragile — needs an isolated pretrain checkpoint
If the model enters co-training without finishing pretrain (stable loss, stabilized A_log, gradient norm < 10), the SFT contaminates the pretrain and both suffer. Pure pretrain needs to reach loss < 5-6 before any SFT.
The numbers (so far)
| Phase | Steps | Batch | Loss | Perplexity | Status |
|---|---|---|---|---|---|
| Full Warmup | 200 | 2 | ~4.5 | ~90 | Base checkpoint |
| Phase 2 v1 | 130/200 | 1 | ~100 | — | Loss won’t descend |
| Phase 2 v2 | 0/200 | 4 | — | — | CUDA OOM |
| Phase 3 SFT | evaluated | — | 19.9 | 444M | Gibberish |
| Phase 4 | 2000 | 4 | ? | ? | ** In progress** |
What’s next
The tatu_phase4_ressurection.py script is ready, linted, and waiting for execution. The next step is:
- Run Phase 4 with 2000 steps (~3-4 days on RTX 3060)
- Validate whether perplexity drops below 500 (intermediate milestone)
- If it works, expand the pretrain dataset with more data (code, docs, wikis)
- Try generation with the post-Phase 4 checkpoint
If it doesn’t work… well, then I’m going back to Transformer. But the SSM premise still seems right for edge devices: 1.4B params in 2.93GB of VRAM, inference at 41.9µs with Block Codec. The potential exists — it’s the training that’s missing.
# The lesson that will stick:
# Training SSM is 修真 — every advance costs blood, sweat, and CUDA errors.
# But when the gradient passes cleanly on step 1, you know the path exists.
TL;DR: I trained a Mamba-2 with 1.4B parameters in 12GB of VRAM. Warmup works, SFT fails, the 444M perplexity is the diagnosis that the pretrain was too shallow. Phase 4 attacks with 2000 steps of hybrid co-training — 300 pure pretrain + 1700 with weighted loss 70%→50%. If it works, we open the path to trainable SSMs on consumer hardware. If not, at least the logs are documented for the next attempt.