File size: 9,114 Bytes
8afed03
 
 
 
 
 
 
 
 
 
 
 
4ee10ab
8afed03
 
 
 
 
 
 
4ee10ab
 
 
 
 
 
 
 
 
 
 
8afed03
699a190
 
 
 
 
 
 
 
 
 
 
 
 
 
ce827ed
8afed03
 
 
 
 
 
 
6d0bc7c
8afed03
 
 
ce827ed
8afed03
 
 
ce827ed
8afed03
4468101
 
 
 
 
 
 
ce827ed
4ee10ab
699a190
 
 
 
8afed03
ce827ed
8afed03
6d0bc7c
 
 
8afed03
 
6d0bc7c
8afed03
ce827ed
8afed03
 
 
 
 
 
 
 
 
 
 
6d0bc7c
8afed03
 
 
 
 
 
 
ce827ed
 
 
 
 
8afed03
 
 
ce827ed
8afed03
ce827ed
 
 
6d0bc7c
4ee10ab
 
 
 
 
 
 
699a190
 
 
 
 
 
 
 
8afed03
 
 
6d0bc7c
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
---
license: apache-2.0
base_model: h2oai/h2o-danube3-500m-chat
tags:
  - gguf
  - email
  - triage
  - ollama
  - full-fine-tune
  - unsloth
  - cipher
  - edge
  - voice-intent
language:
  - en
pipeline_tag: text-generation
---

# Cipher Nano

Cipher Nano is a full-parameter fine-tune of [h2oai/h2o-danube3-500m-chat](https://huggingface.co/h2oai/h2o-danube3-500m-chat), trained on **every LLM-backed feature of a local-first email assistant**: email triage (importance/summary/category JSON), chat, daily-summary synthesis, draft reply, compose assist, and voice-command intent parsing β€” not just prompted for these tasks, actually trained on them.

**Update:** added a 6th task, voice-command intent parsing. Nano's smaller full-fine-tune
capacity means it still sometimes substitutes a plausible-looking wrong address instead of
copying the real one verbatim from the candidate list β€” 3 rounds of targeted oversampling
narrowed but didn't eliminate this. The actual fix ended up being architectural, not a
bigger model: the calling app never trusts an LLM-supplied email address at all anymore,
re-deriving it deterministically in code from the model's extracted name against the real
candidate list instead. That makes this habit irrelevant regardless of which Cipher tier is
active β€” see `generate_voice_intent.py`'s training notes and `eval_voice_intent.py` for the
full detail.

**Second update:** an incremental continue-training round aimed at voice-intent (done on top
of the update above) turned out to have a side effect elsewhere: manual testing found this
model would occasionally fabricate a specific false detail not present in its given context
(e.g. inventing a person's surname that was never mentioned) on the chat task. Rather than
patch that narrowly on top of an already-long chain of incremental updates, this model was
retrained fresh from the base in one pass on a properly-rebalanced dataset covering all 6
tasks together, with substantially richer prompt-injection coverage across draft-reply and
chat (credential-phishing, wire-transfer, and data-exfiltration attempts, not just one
generic case) and an explicit "the question has nothing to do with your email at all"
scenario category to close the specific hallucination gap that was found. Verified through
repeated testing against the real calling application's code path: no compliance with any
injection attempt, and the specific hallucination case now correctly reports the missing
information instead of inventing it.

It's the smallest of the three **Cipher** tiers (`cipher-nano` / `cipher-air` / `cipher-pro`), built specifically to run on modest hardware β€” **verified at 29.4 tok/s on a 10-year-old Intel i3-6100T with 8GB RAM, no GPU**, with accuracy holding essentially flat versus a high-end GPU. Cipher is the local-model engine for an unreleased larger email-assistant project β€” that project isn't public yet, but these weights, the training code, the eval script, and all five dataset generators are fully open now, in this repo.

## Why this exists

Most email triage today means sending your inbox to a third-party API. Cipher runs entirely on your own hardware via [Ollama](https://ollama.com) β€” nothing about your email ever leaves your machine. Cipher Nano exists so that "your own hardware" can mean literally any hardware, not just a machine with a GPU.

## Why Danube3-500M and not something smaller

We tried harder-to-shrink options first and hit real walls:

- **SmolLM2-135M/360M** (49K-token vocab) genuinely shrinks to 100-270MB, but its base pretraining wasn't strong enough to reliably learn category/importance calibration β€” tried LoRA vs. full fine-tune, epoch sweeps, and reshaped training data, and category accuracy stayed stuck in the 40-65% range regardless.
- **Qwen2.5-0.5B / Gemma3** have strong pretraining but 152K/256K-token vocabularies that dominate disk size and don't shrink with quantization (confirmed: Q2_K barely shrank vs Q4_K_M).
- **Danube3-500M** is the first candidate with *both* a small vocabulary (32K tokens) and pretraining strong enough for the task β€” category accuracy jumped to 70%+ on the first fine-tuning attempt, no extensive tuning needed. Its full-parameter fine-tune (100% of weights retrained, vs. `cipher-air`/`cipher-pro`'s LoRA) also gives it total plasticity to fully adopt the required output format, no competing base-model habit to fight against.

## Benchmark

Evaluated on a 29-fixture triage benchmark across five machines, GPU and CPU-only:

| Hardware | Tok/s | JSON-valid | Category acc | Importance-in-band |
|---|---|---|---|---|
| RTX 5070 (GPU) | 901.0 | 100% | 72.4% | 69.0% |
| RX 9070 XT 16GB (GPU) | 398.7 | 100% | 69.0% | 69.0% |
| Ryzen 9800X3D (CPU-only) | 143.0 | 100% | 72.4% | 72.4% |
| Core Ultra 9 285K (CPU-only) | 106.3 | 100% | 69.0% | 69.0% |
| **Intel i3-6100T, 8GB RAM (CPU-only, 10 years old)** | **29.4** | 100% | 69.0% | 72.4% |
| Multi-task retrain (chat/summary/draft/compose added) | 899.9 | 96.6% | 67.9% | 89.3% |
| Voice-intent retrain (6th task added) | 867.4 | 96.6% | 75.0% | 82.1% |
| Full retrain (current version, injection/hallucination hardened) | 106.7* | 100.0% | 82.8% | 79.3% |

\* Tok/s measured while the GPU was concurrently running an unrelated training job β€” not a
like-for-like comparison with the isolated numbers above; accuracy figures are unaffected.

Accuracy holds essentially flat across every machine tested β€” only throughput changes. Even the weakest machine here, a decade-old office PC with no GPU, does ~2 seconds per email, well within range for a background triage task. Reproduce with:

```bash
pip install -r requirements.txt
python eval_triage.py --models cipher-nano:latest --keep
```

## Usage (Ollama)

Danube3's chat template does **not** support a `system` role and uses non-ChatML turn markers β€” the included `Modelfile` sets an explicit `TEMPLATE` to match. Don't rely on Ollama's template autodetection with this base model (confirmed live: without an explicit `TEMPLATE`, Ollama couldn't resolve a working chat format for this GGUF at all).

```bash
ollama create cipher-nano -f Modelfile
```

Query it with grammar-constrained JSON output for reliable parsing:

```bash
curl http://localhost:11434/api/chat -d '{
  "model": "cipher-nano",
  "messages": [
    {"role": "system", "content": "<system prompt from Modelfile>"},
    {"role": "user", "content": "From: alex@acme.com\nSubject: Q3 budget review\n\nBody:\nCan we sync before Friday?"}
  ],
  "format": "json",
  "options": {"temperature": 0.1}
}'
```

If you're integrating this into your own app rather than using Ollama: `llama-server`
(llama.cpp's own server binary) handles this model's real chat template correctly on its
own β€” verified directly, no override needed there, only for Ollama's simpler
auto-detection.

## Training

- Base: `h2oai/h2o-danube3-500m-chat` (plain `LlamaForCausalLM`, 32,000-token vocab), **full fine-tune** (100% of parameters, not LoRA), 3 epochs
- Data: ~4,800 triage examples + ~1,600-2,000 examples each for chat/daily-summary/draft-reply/compose, all matching production prompts exactly β€” generated by the five `generate_*.py` scripts in this repo; training text reformatted to Danube3's native `<|prompt|>`/`<|answer|>` turn format since it has no `system`-role support (see `train_cipher_nano.py`)
- Framework: [Unsloth](https://github.com/unslothai/unsloth) + `trl.SFTTrainer`
- Sequence packing was tried to speed up training (most examples are well under the
  2048-token context window) β€” it crashed outright, an Unsloth/trl version incompatibility,
  not a quality tradeoff. Disabled.
- Reproduce with `train_cipher_nano.py` β†’ `export_gguf_cipher_nano.py`
- Voice-intent retrain: added `generate_voice_intent.py`'s data (weighted heavily toward
  the no-match case and exact-copy-under-distractor-pressure scenarios) to the mix across 3
  rounds of oversampling. Nano's smaller full-fine-tune capacity meant this narrowed but
  never fully eliminated one specific habit β€” substituting a different plausible-looking
  address instead of copying the real one verbatim β€” see the README intro above for how
  this ended up getting solved at the calling-app level instead. Reproduce/verify with
  `eval_voice_intent.py`.
- Full retrain (current version): one clean pass over the base model on a single
  consolidated, properly-balanced dataset covering all 6 tasks, rather than continuing to
  layer incremental patches on top of each other β€” done specifically to fix both the
  injection-compliance issue and the hallucination regression noted above without risking
  a whack-a-mole cycle where each narrow fix nudges a different, previously-working case.
  Followed by one small, targeted continue-train pass (~700 examples, 2 epochs, lr=2e-5)
  reinforcing "report missing information plainly rather than inventing a plausible-
  sounding detail," specifically targeting the exact hallucination case found in testing.

## License

Apache 2.0, inherited from the base model. Weights, training code, and eval harness are fully open.