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 — for a ~3× smaller/faster distilled option see
Bifrost Flash 430M.
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):
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
1import torch, sentencepiece as spm
2from modeling_nordic import NordicTranslator
34sp = spm.SentencePieceProcessor(); sp.load("nordic_unigram_65k.model")5LANG ={"en":65000,"sv":65001,"da":65002,"nb":65003,"nn":65004,"fi":65005,"is":65006}67model = NordicTranslator.from_checkpoint("model.safetensors", device="cuda")8ids = model.translate(sp.encode("Hello, how are you?", out_type=int), LANG["sv"])9print(sp.decode(ids))# -> Hej, hur är det med er?
2. HuggingFace (trust_remote_code):
python
1from transformers import AutoModelForCausalLM
2import torch, sentencepiece as spm
3sp = spm.SentencePieceProcessor(); sp.load("nordic_unigram_65k.model")4m = AutoModelForCausalLM.from_pretrained(".", trust_remote_code=True,5 dtype=torch.bfloat16).cuda().eval()6ids =[1,65001]+ sp.encode("Hello, how are you?", out_type=int)+[65007]# 65001=<2sv>7out = m.generate(torch.tensor([ids]).cuda(), max_new_tokens=128, do_sample=False, eos_token_id=2)8print(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.
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.