MNN-Pocket-0.3B-RU / README.md
Rahspide's picture
docs: drop forward-looking roadmap, keep released facts only
f3cb38c verified
|
Raw History Blame Contribute Delete
13.2 kB
---
language:
- ru
- en
- zh
license: apache-2.0
base_model: taobao-mnn/MNN-Pocket-0.3B-Base
library_name: transformers
pipeline_tag: text-generation
datasets:
- HuggingFaceFW/fineweb-2
- HuggingFaceFW/fineweb
tags:
- russian
- on-device
- edge
- mnn
- qwen3
- gguf
- keyboard
- work-in-progress
---
![MNN-Pocket-0.3B-RU — a Russian continuation model on a 0.3B base](assets/cover.png)
![params](https://img.shields.io/badge/params-361M-E2543F?style=flat-square)
![tokens](https://img.shields.io/badge/trained%20on-1.2B%20tokens-5FD3B4?style=flat-square)
![context](https://img.shields.io/badge/context-32k%20(base)-6FA8FF?style=flat-square)
![gguf](https://img.shields.io/badge/gguf-Q8__0%20%C2%B7%20Q4__K__M-E2543F?style=flat-square)
![license](https://img.shields.io/badge/license-apache--2.0-8B95A4?style=flat-square)
![status](https://img.shields.io/badge/status-v1%20released-5FD3B4?style=flat-square)
> **Status: v1 released.** 1.2 B tokens, one epoch, weights in this repository, GGUF in `gguf/`.
> Training log, checkpoints and the evaluation script live in the private repo
> [`Rahspide/mnn-pocket-ru-run`](https://huggingface.co/Rahspide/mnn-pocket-ru-run).
A continued-pretraining run of [`taobao-mnn/MNN-Pocket-0.3B-Base`](https://huggingface.co/taobao-mnn/MNN-Pocket-0.3B-Base)
that teaches it Russian while keeping English and Chinese readable. The keyboard is the use case that set the
size and latency budget.
It is trained as a plain causal LM on raw text, so it **continues** a typed prefix instead of answering it.
For dialogue there is a separate checkpoint — [`Rahspide/MNN-Pocket-0.3B-RU-Chat`](https://huggingface.co/Rahspide/MNN-Pocket-0.3B-RU-Chat).
## Two models, two jobs
![One base, two jobs](assets/two_jobs.png)
| | This model — `MNN-Pocket-0.3B-RU` | Sister — `MNN-Pocket-0.3B-RU-Chat` |
|---|---|---|
| Job | continue text | follow instructions |
| Input | a prefix, often mid-sentence | a system + user message |
| Output | the plausible continuation | an answer |
| Trained with | continued pretraining, 1.2 B tokens of raw text | SFT on top of this model, 50 M tokens |
| Choose it for | keyboard suggestions, autocomplete, prefix completion | chat, Q&A, short rewrites |
| Do not choose it for | assistant behaviour: it continues your question instead of answering | autocomplete: it answers instead of continuing |
The two tasks reward opposite behaviour. Trained on both jobs, one checkpoint does each badly: it answers when
it should continue, and continues when it should answer.
## Specs
| | |
|---|---|
| Base | `taobao-mnn/MNN-Pocket-0.3B-Base` (Qwen3 architecture, 303 M non-embedding / 361 M total) |
| Tokenizer | 65,536 tokens, byte-level BPE, `<|endoftext|>` = 65510 |
| Training tokens | 1.2 B — 0.9 B ru + 0.204 B en + 0.096 B zh |
| Method | full fine-tune, 1 epoch, no LoRA |
| Steps | 36,621 × 32,768 tokens |
| Hardware | one Colab T4 16 GB, 4.8 k tok/s, ~69 h |
| Precision | fp32 master weights, fp16 autocast (T4 has no usable bf16) |
## Results
![Bits per character: base vs v1](assets/eval.png)
100 k held-out tokens per language, identical settings for both checkpoints:
| Lang | ppl base → v1 | bits/char base → v1 | top-1 base → v1 | top-5 base → v1 |
|---|---|---|---|---|
| **ru** | 10.42 → **5.05** | 1.625 → **1.123** (−31%) | 46.2% → **61.0%** | 69.3% → **80.6%** |
| en | 21.85 → 41.16 | 1.016 → 1.225 (+21%) | 39.9% → 35.5% | 64.1% → 58.7% |
| zh | 23.95 → 51.86 | 3.274 → 4.071 (+24%) | 41.9% → 37.7% | 62.0% → 55.6% |
The published weights were re-downloaded and re-evaluated independently (CPU, fp32, 20 k tokens per language)
and reproduce the same ordering: ru 1.022 against base 1.519, en 1.337 against 1.124, zh 4.053 against 3.393.
Russian landed past the target (1.123 against a 1.40 gate). English and Chinese regressed 21–24% instead of
the ≤10% the recipe aimed for. a 25% replay mix is too
thin at 0.36 B when the target language is tokenised this badly. A second pass with a 50/25/25 mix at lr 5e-5
was started, got through ~50 M of a planned 200 M tokens, and changed nothing measurable on English
(1.337 → 1.337 on the same eval), so it is **not** published. Treat v1 as Russian-first, with working but
measurably weaker English and Chinese.
## Why the tokenizer shapes the recipe
![Russian characters per token, base vs extended vocabulary](assets/tokenizer.png)
The base vocabulary is a distilled 65 k set with almost no Cyrillic merges, and it is byte-level: the
vocabulary stores UTF-8 bytes remapped to latin-1, so `convert_ids_to_tokens` prints `С` where the text says
`С`. That is a display artifact; `decode` returns the word. The count is what matters:
| Word | Characters | Tokens | Pieces |
|---|---|---|---|
| `Спасибо` | 7 | 6 | `С · п · ас · и · б · о` |
| `привет` | 6 | 3 | `пр · ив · ет` |
| `покупать` | 8 | 4 | `п · ок · уп · ать` |
| `Ёжик` | 4 | 4 | `Ё` lands in two separate byte tokens |
Because the vocabulary is byte-level, a token can end in the middle of a character: in `Ёжик` and `ЩУКА` the
uppercase letters are cut into single bytes. Decoding the whole sequence is still exact, but a streaming client
must hold an incomplete UTF-8 sequence back instead of printing it. `llama.cpp` and `transformers` buffer it for
you; a hand-rolled decoder has to do the same or the output shows replacement characters.
| Tokenizer | RU chars/token | RU tokens per word | EN chars/token |
|---|---|---|---|
| MNN-Pocket / Qwen3-distilled (65,536) | **1.75–1.85** (2.08 on dialogue text) | **≈3.3** | 3.78 |
| +8,192 Cyrillic merges (73,728) | **3.10** | ≈1.8 | — |
| +16,384 Cyrillic merges (81,920) | **3.39** | ≈1.6 | — |
| ruGPT3-small (50 k, Russian BPE) | 4.2 | ≈1.3 | — |
Consequences that shaped the plan:
- the same 1.2 B-token budget buys ~2.2 B Russian characters (~340 M words), roughly 2.2× less text than a
Russian-native tokenizer would fit;
- on a phone one suggested Russian word costs ~3 decode steps instead of ~1, which matters for a keyboard
latency budget;
- vocabulary surgery was measured but kept out of v1, so v1 stays tokenizer-compatible with the base and drops
into the existing MNN / GGUF toolchain. An extended-vocabulary artefact (`model_ext`, 73,728 rows, embeddings
initialised as the mean of their child tokens) exists in the private run repo but is not part of this release.
## Recipe
- Full fine-tune from `MNN-Pocket-0.3B-Base`, fp32 master weights with fp16 autocast.
- Sequence length 1024, random windows over a packed `uint16` token stream, `<|endoftext|>` between documents.
- 32,768 tokens per optimizer step (micro-batch 4 × 1024, gradient accumulation 8).
- AdamW, β = (0.9, 0.95), weight decay 0.1, gradient clip 1.0, lr 2e-4 with 1% linear warmup and cosine decay.
- One epoch over 1.2 B tokens = 36,621 steps.
- Checkpoint every 500 steps to a private HF repo; training auto-resumes from the last checkpoint after a
session restart.
## Data
All sources are public Hugging Face datasets. `*` marks the validation split: the first 100 k tokens of each
stream are held out before training starts, so validation text is never seen.
| Language | Dataset | Config | Tokens | Share |
|---|---|---|---|---|
| ru | [`HuggingFaceFW/fineweb-2`](https://huggingface.co/datasets/HuggingFaceFW/fineweb-2) | `rus_Cyrl` | 0.90 B (100 k held out*) | 75% |
| en | [`HuggingFaceFW/fineweb`](https://huggingface.co/datasets/HuggingFaceFW/fineweb) | `sample-100BT` | 0.204 B (100 k held out*) | 17% |
| zh | [`HuggingFaceFW/fineweb-2`](https://huggingface.co/datasets/HuggingFaceFW/fineweb-2) | `cmn_Hani` | 0.096 B (100 k held out*) | 8% |
Licences: FineWeb / FineWeb-2 are ODC-BY 1.0. No scraped Telegram dumps or other unlicensed chat corpora were
used, so the resulting weights stay publishable.
## Builds
![GGUF sizes](assets/sizes.png)
| Build | Size | Download | Note |
|---|---|---|---|
| Q8_0 | 310 MB | [`MNN-Pocket-0.3B-RU-Q8_0.gguf`](https://huggingface.co/Rahspide/MNN-Pocket-0.3B-RU/blob/main/gguf/MNN-Pocket-0.3B-RU-Q8_0.gguf) | near-lossless, for desktop |
| Q4_K_M | 223 MB | [`MNN-Pocket-0.3B-RU-Q4_K_M.gguf`](https://huggingface.co/Rahspide/MNN-Pocket-0.3B-RU/blob/main/gguf/MNN-Pocket-0.3B-RU-Q4_K_M.gguf) | the size to ship on a phone |
| MNN INT4 | 165 MB | [mnn/MNN-Pocket-0.3B-RU-INT4](https://huggingface.co/Rahspide/MNN-Pocket-0.3B-RU/tree/main/mnn/MNN-Pocket-0.3B-RU-INT4) | MNN runtime, Android |
Both files sit under `gguf/` with the quant tag in the name, which is what llama.cpp matches on:
```bash
llama-cli -hf Rahspide/MNN-Pocket-0.3B-RU:Q4_K_M -p "Сегодня я" -n 40
```
Converted with llama.cpp `convert_hf_to_gguf.py` (f16) and quantised with `llama-quantize`. Measured on a
16-thread x86 CPU, no GPU involved:
| Build | Prompt processing | Generation |
|---|---|---|
| Q4_K_M | ~410 tok/s | **~130 tok/s** |
| Q8_0 | ~525 tok/s | ~99 tok/s |
Phone figures will be several times lower; these are desktop-CPU numbers.
The MNN build runs through MNN's own runtime. Build `llm_demo` from the MNN repository with
`-DMNN_BUILD_LLM=ON`, then point it at the folder:
```bash
cd mnn/MNN-Pocket-0.3B-RU-INT4 && llm_demo config.json prompt.txt
```
165 MB on disk, ~354 tok/s prefill and ~77 tok/s decode on 12 CPU threads here. `llm_config.json` carries no
chat template on purpose: this checkpoint continues the prompt, so the runtime should not wrap it in a dialogue
format.
Two conversion quirks, both already fixed in this repository's `tokenizer_config.json`.
- the base pre-tokenizer does not hash-match in current llama.cpp, so the Qwen pre-tokenizer has to be pinned
by hand (`qwen2`);
- `extra_special_tokens` must not be a list, or transformers v4 refuses to load the tokenizer.
## Reproduce
Everything below runs on CPU. The numbers in this card came from these paths.
```bash
# bits/char, ppl and top-k against the base, 20k tokens per language
python bench/eval_bpc.py --model Rahspide/MNN-Pocket-0.3B-RU --base taobao-mnn/MNN-Pocket-0.3B-Base \
--dataset HuggingFaceFW/fineweb-2 --config rus_Cyrl --tokens 20000
# throughput and a smoke generation on the quantised build
LLAMA_CLI=/path/to/llama-cli bash bench/bench_cpu.sh gguf/MNN-Pocket-0.3B-RU-Q4_K_M.gguf
```
`bench/eval_bpc.py` reports the same three metrics as the tables above (ppl, bits/char, top-1/top-5) and exits
non-zero when the model loses more than a given `--max-bpc-regression` against the base, so it can run as a
regression gate in CI. `bench/bench_cpu.sh` prints prompt and generation tok/s and checks that generation stops
at `<|endoftext|>`.
## Intended use
- On-device next-word and short-phrase suggestion in a keyboard or IME, Russian first, English and Chinese from
the same checkpoint (a keyboard can pick the language from the current word or the layout).
- Text completion, prefix continuation and autocomplete for Russian anywhere a ~220 MB quantised model has to
run locally: chat composers, note apps, search fields, terminal prompts.
- Any pipeline that needs a small causal LM fluent in Russian that can still read English and Chinese, for
example tagging, short rewriting suggestions, or generating Russian seed text.
Not intended for: instruction following, factual question answering, code, long-form reasoning, or translation.
This is a 0.36 B model trained for 1.2 B tokens; its knowledge is narrow by construction. The language mix is a
deliberate trade: Russian is the target, English and Chinese are kept from degrading, neither is improved.
## Limitations
- Russian is tokenised near character level (see above): slower decoding and a larger effective cost per Russian
word than the same model pays for English.
- Token boundaries do not follow character boundaries. Rare and uppercase Cyrillic letters are split into
single bytes, so a streaming decoder must buffer partial UTF-8 sequences; `llama.cpp` and `transformers`
already do this.
- Web text only. Register is mostly written/informal blog Russian; no dialogue corpora, no post-training.
- Capacity is 0.36 B. Expect weak facts, weak arithmetic, and drift on long generations.
- English and Chinese regressed 21–24% against the base. The 25% replay mix limits the damage; it does not
remove it.
- The ChatML template from the base is inherited but untrained here. Do not use this checkpoint in chat mode.
## Releases
| Date | Version | What changed |
|---|---|---|
| 2026-11-10 | **v1.0** | 1.2 B-token continued pretraining released: bf16 weights, GGUF Q8_0 and Q4_K_M, measured ru/en/zh bits-per-char |
| 2026-11-11 | v1.1 | MNN INT4 build for the MNN runtime: 165 MB, ~77 tok/s decode on 12 CPU threads |
| 2026-11-10 | v2.0 (sister) | Assistant checkpoint released: 50 M-token SFT on top of v1, weights and GGUF |
## Licence and attribution
Weights: Apache-2.0, inherited from the base model. Data: ODC-BY 1.0 (FineWeb, FineWeb-2).
If you use this model, credit the base release:
```bibtex
@misc{mnn_pocket_03b,
title = {MNN-Pocket-0.3B},
author = {Taobao MNN team},
year = {2026},
url = {https://huggingface.co/taobao-mnn/MNN-Pocket-0.3B}
}
```