| --- |
| license: apache-2.0 |
| language: |
| - sv |
| - da |
| - "no" |
| - nn |
| - fi |
| - is |
| - en |
| tags: |
| - translation |
| - nordic |
| - scandinavian |
| - machine-translation |
| - bifrost |
| - nodenestor |
| pipeline_tag: translation |
| metrics: |
| - chrf |
| --- |
| |
| # Bifrost 1.2B |
|
|
| A from-scratch 1.2B-parameter translation model for the **Nordic languages ↔ English**: |
| Swedish (`sv`), Danish (`da`), Norwegian Bokmål (`nb`), Norwegian Nynorsk (`nn`), |
| Finnish (`fi`), and Icelandic (`is`), plus cross-Nordic directions. |
|
|
| On FLORES-200 devtest it **beats NLLB-200-3.3B and TranslateGemma-12B on the |
| English→Nordic average** — at a fraction of their size. |
|
|
| The teacher of the **Bifrost** Nordic-translation family from |
| [NodeNestor](https://nodenestor.com) — for a ~3× smaller/faster distilled option see |
| [Bifrost Flash 430M](https://huggingface.co/NodeNestor/bifrost-flash-430m). |
|
|
| ## Results — FLORES-200 devtest, chrF++ (sacrebleu, `word_order=2`, n=500) |
| |
| **Headline — English→Nordic average:** |
| |
| | Model | Params | en→Nordic chrF++ | |
| |---|---|---| |
| | **Nordic Translator (this model)** | **1.2B** | **57.4** | |
| | NLLB-200-3.3B | 3.3B | 56.1 | |
| | TranslateGemma-12B | 12B | 55.7 | |
| |
| **Group averages:** |
| |
| | Direction group | chrF++ | |
| |---|---| |
| | English → Nordic | 57.4 | |
| | Nordic → English | 63.6 | |
| | Nordic ↔ Nordic | 54.5 | |
| | **Overall** | **58.1** | |
| |
| **Per-direction (chrF++):** |
| |
| | Dir | score | | Dir | score | |
| |---|---|---|---|---| |
| | en→sv | 63.8 | | sv→en | 67.4 | |
| | en→da | 65.4 | | da→en | 69.3 | |
| | en→nb | 58.2 | | nb→en | 64.9 | |
| | en→nn | 57.8 | | nn→en | 68.9 | |
| | en→fi | 50.1 | | fi→en | 55.3 | |
| | en→is | 49.2 | | is→en | 55.8 | |
| | sv→da | 62.5 | | da→sv | 62.7 | |
| | sv→fi | 51.0 | | fi→sv | 50.5 | |
| | nb→nn | 53.0 | | nn→nb | 55.5 | |
| | fi→da | 50.9 | | is→sv | 50.1 | |
| |
| Strongest relative to the references on the low-resource directions (Nynorsk, |
| Icelandic). NLLB-3.3B still leads on several →English directions and Finnish. |
| |
| ## Usage |
| |
| The model expects a control-token prompt and is decoded greedily: |
| |
| ``` |
| [BOS] [<2{tgt_lang}>] {source_token_ids} [<eos_src>] → generate until [EOS] |
| ``` |
| |
| The **target-language control token placed right after `[BOS]` selects the output |
| language** — the source language is inferred. Control-token IDs (above the 65000 |
| SentencePiece vocab): |
| |
| | token | id | | token | id | |
| |---|---|---|---|---| |
| | `<2en>` | 65000 | | `<2nn>` | 65004 | |
| | `<2sv>` | 65001 | | `<2fi>` | 65005 | |
| | `<2da>` | 65002 | | `<2is>` | 65006 | |
| | `<2nb>` | 65003 | | `<eos_src>` | 65007 | |
| |
| `[BOS]=1`, `[EOS]=2`. Tokenizer: `nordic_unigram_65k.model` (SentencePiece, 65000 |
| pieces + 8 control tokens = vocab 65008). |
| |
| The weights ship as `model.safetensors`, with a self-contained pure-PyTorch |
| implementation in `modeling_nordic.py` (no training-stack dependencies). Three ways |
| to run it: |
| |
| **1. Standalone (pure torch, KV-cached):** |
| |
| ```python |
| import torch, sentencepiece as spm |
| from modeling_nordic import NordicTranslator |
| |
| sp = spm.SentencePieceProcessor(); sp.load("nordic_unigram_65k.model") |
| LANG = {"en":65000,"sv":65001,"da":65002,"nb":65003,"nn":65004,"fi":65005,"is":65006} |
| |
| model = NordicTranslator.from_checkpoint("model.safetensors", device="cuda") |
| ids = model.translate(sp.encode("Hello, how are you?", out_type=int), LANG["sv"]) |
| print(sp.decode(ids)) # -> Hej, hur är det med er? |
| ``` |
| |
| **2. HuggingFace (`trust_remote_code`):** |
| |
| ```python |
| from transformers import AutoModelForCausalLM |
| import torch, sentencepiece as spm |
| sp = spm.SentencePieceProcessor(); sp.load("nordic_unigram_65k.model") |
| m = AutoModelForCausalLM.from_pretrained(".", trust_remote_code=True, |
| dtype=torch.bfloat16).cuda().eval() |
| ids = [1, 65001] + sp.encode("Hello, how are you?", out_type=int) + [65007] # 65001=<2sv> |
| out = m.generate(torch.tensor([ids]).cuda(), max_new_tokens=128, do_sample=False, eos_token_id=2) |
| print(sp.decode([t for t in out[0, len(ids):].tolist() if t < 65000])) |
| ``` |
| |
| **3. vLLM** (custom architecture — register the included plugin): see |
| `vllm_nordic.py` + `vllm_pkg/` and `example_vllm.py`. Install the plugin |
| (`pip install -e vllm_pkg`) inside a vLLM environment, then serve with |
| `--skip-tokenizer-init` and feed control-token prompts. |
|
|
| The control-token prompt is `[BOS] [<2{tgt}>] {source_ids} [<eos_src>]` → generate |
| until `[EOS]`; decode only ids `< 65000`. The FLORES numbers above were produced with |
| the batched, KV-cached standalone path. |
|
|
| ## Model details |
|
|
| - **Architecture:** a grouped-query-attention (GQA) decoder. 18 layers, |
| hidden 2048, FFN 6144 (SwiGLU), 16 query heads / 4 KV heads, head dim 128, RoPE |
| (θ=500000, partial 0.25), RMSNorm, parallel residual, fused QKV. ~1.2B params. |
| - **Context length:** 4096 tokens (trained and evaluated at 4096; longer inputs truncate). |
| - **Precision:** bf16. |
| - **Vocab:** 65008 (`nordic_unigram_65k` SentencePiece + 8 control tokens). |
|
|
| ## Training |
|
|
| - **From scratch.** A ~**120B-token run**: a ~19B-token trunk, then **+~100B |
| tokens of continued training** (clean data, cosine schedule with a monolingual |
| floor + anneal). The **released checkpoint is ~96B into the long run (~115B |
| cumulative)** — on the cosine tail, so quality ≈ the 100B point. |
| - **Data:** parallel + monolingual Nordic/English (Wikipedia parallel, DCLM en↔Nordic, |
| Aya cross-lingual, FineWeb-Edu, Nemotron-CC), balanced en↔Nordic blend. |
| - **Objective:** next-token cross-entropy on the target side. |
|
|
| ## Limitations |
|
|
| - Trained at **4096-token context**; longer inputs are truncated. |
| - **Finnish and Icelandic** (en→) are the weakest directions — lower-resource, |
| morphologically hard. |
| - Greedy decoding; no built-in length/formatting control beyond the prompt. |
| - Not instruction-tuned — it is a dedicated translation model, not a chat model. |
| - May produce occasional off-target output on the hardest low-resource pairs. |
|
|
| ## Acknowledgments |
|
|
| - **Tokenizer** (`nordic_unigram_65k`) developed by a collaborator; included here with |
| permission. |
|
|
| ## Citation |
|
|
| ```bibtex |
| @misc{nodenestor_bifrost_1.2b_2026, |
| title = {Bifrost 1.2B}, |
| author = {Nilsson, Ludvig}, |
| year = {2026}, |
| howpublished = {\url{https://huggingface.co/NodeNestor/bifrost-1.2b}}, |
| note = {NodeNestor} |
| } |
| ``` |
|
|