Most machine translation systems that cover Khmer treat it as one language among hundreds. Models like NLLB-200 and MADLAD-400 spread their capacity across every language they support, and they are large enough that running them usually means a GPU or a paid API. For a Khmer speaker on an ordinary laptop, or a developer building for one, neither is a good fit.
We wanted to know how far a small model could get if it were built for this one language pair from the start. Netra MT 1 is the result: a 77M-parameter encoder-decoder transformer that translates in both directions between English and Khmer, trained from scratch on about 220 million tokens of parallel text.
In brief:
- English→Khmer. On the ALT test set, Netra MT 1 scores higher than every open model we compared, including NLLB-200 (1.3B) and MADLAD-400 (3B), on spBLEU, chrF++ and TER. Google Translate still leads on most metrics, and the larger open models score higher on COMET.
- Khmer→English. This direction is clearly weaker. Netra MT 1 trails every working baseline by 11.8–16.7 spBLEU and 7–10 COMET points.
- Speed. With greedy decoding on a desktop CPU it translates a sentence in about 66 ms in PyTorch, 16 times faster than NLLB-200 (600M). The released package, on ONNX Runtime, takes 39 ms.
The model, tokenizer, training code and evaluation outputs are all open source under the MIT licence, and the weights are on Hugging Face.
Architecture#
Layer allocation#
A translation model has two halves with very different costs. The encoder reads the whole source sentence at once, so it runs a single time per sentence. The decoder writes the translation one token at a time, so every one of its layers runs again for every token it writes. Following Kasai et al. (2021), we put most of the depth in the encoder: 12 encoder layers and only 2 decoder layers. The model keeps a large capacity for understanding the source, but each generated token costs very little.

On the CPU, this layout cut median greedy latency from 217 ms to 76 ms per sentence compared with our earlier autoregressive baseline, a 2.85× speedup. We had also tried a non-autoregressive diffusion decoder to cut latency. The new model turned out to be faster than it as well, at 76 ms against 114 ms with 10 refinement steps, while keeping ordinary autoregressive decoding.
Direction tokens#
One model handles both directions. Each source sentence starts with a token naming the target language, <2km> or <2en>, and the decoder takes its cue from that. "I love my country", to be translated into Khmer, reaches the encoder as the pieces ▁ <2km> ▁i ▁love ▁my ▁country. That is the example in Figures 2 and 4.
Rotary position embeddings#
Attention by itself has no sense of word order: it compares every token with every other, wherever they sit. The original Transformer adds a position vector to each token. We use rotary position embeddings (RoPE) instead, which turn the vectors rather than adding to them.
Each attention head works on 64-dimensional queries and keys. RoPE splits them into 32 pairs of dimensions and rotates pair of a token at position by the angle , where
The first pair turns quickly, 57° per position; the last barely moves. Together they act like the hands of a clock, which tell the time from fast and slow hands at once.
The source pieces of “I love my country”, at positions 0 to 5. Each attention head's 64 dimensions form 32 pairs, and each pair is turned by an angle that grows with position: pair 1 by 57 degrees per position, pair 8 by 7.6 degrees, pair 32 by less than a hundredth of a degree. Between “i” at position 2 and “my” at position 4, the angle in pair 1 is 115 degrees; it is the same between “love” at 3 and “country” at 5, because both pairs are two positions apart.
Because the query and the key are both rotated, the score between two tokens depends only on how far apart they are:
where is the query of the token at position , is the key of the token at position , and rotates every pair by . There is no table of positions to run out of, so the model has no hard limit on input length. RoPE is used in the self-attention of both the encoder and the decoder. Cross-attention, where the target reads the source, has no rotation.
Layer structure#
Every layer has the same two sub-layers as the original Transformer, attention and a feed-forward block, but each is built in the modern way (Figure 3).
The normalisation comes before each sub-layer, and the sub-layer's output is added back to the input:
This pre-norm layout keeps a clean path for the signal through all 12 encoder layers, and it kept training stable at the higher learning rate the deeper encoder needed. RMSNorm is a cheaper form of layer normalisation: it only rescales, without first subtracting the mean,
where is the model dimension, avoids dividing by zero, and is a learned scale for each dimension.
The feed-forward block is SwiGLU. It projects the input twice, and one projection decides how much of the other to let through:
where and map 512 dimensions to 2,048, maps them back to 512, is the sigmoid and multiplies element by element.
Shared embeddings#
One embedding table of 32,000 pieces × 512 dimensions, 16.4M parameters, is used three times: to look up the source pieces for the encoder, to look up the pieces the decoder has written so far, and as the output layer, which scores every row of the table to choose the next piece. Both languages share one vocabulary, so one table serves all three. Separate tables would add 32.8M parameters and take the model from 77M to about 110M.
KV cache#
In attention, every token is compared with the others through its key and passes on its value. The decoder writes one piece at a time, and at each step its self-attention needs the keys and values of every piece written so far. Recomputing them all at every step repeats nearly all the work. Instead the decoder computes keys and values for its newest piece only, appends them to a cache, and reads the rest. The encoder's keys and values, which cross-attention reads at every step, are computed once per sentence.
The source “i love my country”, with the marker for Khmer, goes through the twelve encoder layers once, and their keys and values are stored for cross-attention. The decoder then writes ខ្ញុំស្រឡាញ់ប្រទេសរបស់ខ្ញុំ one piece at a time, in six steps. At each step it computes keys and values only for its newest input piece and reads those of the earlier pieces from the cache: one new entry at step 1, one new and five cached at step 6. The written piece becomes the next step's input.
In the six steps of Figure 4, each decoder layer computes 6 sets of keys and values instead of 1 + 2 + … + 6 = 21. The saving grows with the length of the sentence. Together with the 2-layer decoder, this keeps the cost of each new piece small.
| Model dimension | 512 |
| Encoder / decoder layers | 12 / 2 |
| Attention heads | 8, of 64 dimensions each |
| Feed-forward size | 2,048, SwiGLU |
| Position encoding | RoPE in self-attention |
| Vocabulary | 32K SentencePiece unigram, shared by both languages |
| Parameters | ~77M (154 MB in fp16) |
Tokenization#
Khmer is written without spaces between words. A tokenizer given raw Khmer text has to guess word boundaries from character statistics alone, and those guesses often cut across words.
We segment all Khmer text into words with khmercut before training the tokenizer, and again wherever Khmer appears as model input or output. The SentencePiece vocabulary therefore learns pieces aligned to real words. At inference time the segmentation is removed again, so the output reads as natural Khmer.
ខ្ញុំស្រឡាញ់ប្រទេសរបស់ខ្ញុំ
- ខ្ញុំIស្រឡាញ់loveប្រទេសcountryរបស់ofខ្ញុំme
- ខ្ញុំស្រឡាញ់ប្រទេសរបស់ខ្ញុំ
- ខ្ញុំស្រឡាញ់ប្រទេសរបស់ខ្ញុំ
We also lowercase English, but only when it is the source of an English→Khmer pair. "I love Cambodia" and "i love cambodia" should get the same translation, so we stop the model from treating them differently. When English is the target, it keeps its original casing, so Khmer→English output comes out properly capitalised.
Data#
We trained on about 2.4 million unique sentence pairs from four public sources. They mix synthetic, model-generated translations with text crawled from the web.
| Dataset | Type | Pairs | Domains |
|---|---|---|---|
| Netra-Lab/khmer-english-pairs-raw | Synthetic | 200K | Legal, literary, governmental |
| lyfeyvutha/nllb-en-km-316K | Synthetic | 316K | General |
| KrorngAI/ParaCrawl-English-Khmer-v2 | Web crawl | 1.5M | Web, general |
| SeyhaLite/Translate-English-Khmer-All | Mixed | 366K | General |
We removed duplicate and empty pairs, and pairs outside our length limits. Each remaining pair is then used twice, once in each direction, which gives about 4.2 million training examples. The cleaned set is published as Netra-Lab/bilingual-en-km.
Training#
We trained on two RTX 4070 GPUs with AdamW. The learning rate warmed up linearly over 2,000 steps to a peak of 3×10⁻⁴, then followed a cosine decay. We used an effective batch of 128 sentence pairs, label smoothing of 0.05 and mixed precision. The full configuration is in 09_train_student.py.
Results#
We evaluated on the test split of the Asian Language Treebank (ALT): 1,018 sentences, professionally translated, with one reference per sentence. Every system was decoded greedily, and every system was scored with the same metric settings. We compared against four open models, from 350M to 3B parameters, and against Google Translate.
English→Khmer#
| spBLEU | 25.83 (second best in this row) | 18.89 | 9.24 | 16.62 | 16.19 | 28.93 (best in this row) |
|---|---|---|---|---|---|---|
| chrF++ | 38.05 (second best in this row) | 30.07 | 34.38 | 36.14 | 32.24 | 40.21 (best in this row) |
| TER ↓ | 60.47 (best in this row) | 73.89 | 78.68 | 69.90 | 101.99 | 65.56 (second best in this row) |
| COMET | 78.14 | 71.28 | 76.54 | 79.68 | 79.95 (second best in this row) | 82.74 (best in this row) |
| BERTScore | 90.02 (second best in this row) | 87.89 | 88.24 | 89.11 | 89.28 | 90.66 (best in this row) |
Overlap metrics. Netra MT 1 leads every open model on spBLEU, chrF++ and TER, by 7 spBLEU points over the next best. It also has the lowest TER of all six systems, Google Translate included.
Learned metrics. These are less clear-cut. BERTScore puts us second, behind Google Translate. But on COMET, which is designed to track human judgments, NLLB-200 (1.3B) and MADLAD-400 score 1.5–2 points higher than we do. Our outputs share more surface wording with the references, but we can't say from these numbers alone that they are better translations.
Khmer→English#
| spBLEU | 19.47 | 31.28 | 35.15 (second best in this row) | 36.13 (best in this row) | 34.86 |
|---|---|---|---|---|---|
| chrF++ | 46.01 | 54.14 | 57.36 | 58.28 (best in this row) | 57.51 (second best in this row) |
| TER ↓ | 69.79 | 55.34 | 51.14 (second best in this row) | 51.12 (best in this row) | 51.14 (second best in this row) |
| COMET | 77.02 | 84.25 | 85.87 | 86.88 (best in this row) | 86.48 (second best in this row) |
| BERTScore | 91.79 | 93.62 | 94.13 | 94.33 (best in this row) | 94.31 (second best in this row) |
This is the model's clear weakness. Every working baseline beats it on every metric, by 11.8–16.7 spBLEU points. We think the main reason is the English side. The large multilingual models learned English from vastly more text than our 2.4M pairs contain, and a 2-layer decoder has little room to make up for that. Fixing this direction is our first priority for the next version.
Speed#
| CPU, greedy | 65.6 (best in this row) | 550.8 (second best in this row) | 1,074.9 |
|---|---|---|---|
| CPU, beam-5 | 735.2 (best in this row) | 802.7 (second best in this row) | 1,567.5 |
| GPU, greedy | 14.8 (best in this row) | 77.2 (second best in this row) | 150.4 |
| GPU, beam-5 | 144.3 (second best in this row) | 105.2 (best in this row) | 223.0 |
Greedy decoding. On the CPU, Netra MT 1 is 16 times faster than NLLB-200 (600M) and 8 times faster than the 350M English→Khmer model. On the GPU the gap to NLLB-200 (600M) is about tenfold.
Beam search. In this measurement the advantage mostly disappears with five beams. The decoder does five times the work at every step, so the cheap encoder no longer dominates, and on the GPU the 350M model is faster than ours. Greedy decoding is the package's default.
ONNX Runtime#
The released package, netra-nmt 1.0.0, runs the model on ONNX Runtime instead of PyTorch, and its beam search stops early.
| Greedy, ms per sentence ↓ | 62.6 | 39.1 (best in this row) |
|---|---|---|
| Greedy, tokens per second | 328.5 | 518.1 (best in this row) |
| Beam-5, ms per sentence ↓ | 701.1 | 63.5 (best in this row) |
| Beam-5, tokens per second | 29.4 | 303.6 (best in this row) |
Same output. The ONNX model generates exactly the same tokens as the PyTorch model we evaluated, on all 1,018 ALT test sentences, in both directions, with greedy and beam-5 decoding. Every score above holds for the package.
Beam search is 11 times faster. The old beam search ran all 128 steps for every sentence, because it stopped only when every beam had finished. The new one stops as soon as no unfinished beam can still beat the best finished translation. A beam's score can only fall as it grows, so stopping never changes the result: on the ALT test set, early-stopped and full beam search give identical output on all 1,018 sentences in both directions. GPU speed on ONNX Runtime hasn't been measured yet.
Limitations#
- One test set. ALT is a single benchmark of news-style sentences with one reference translation each. Scores on legal, medical or conversational text may differ, and we haven't run a human evaluation.
- Overlap metrics favour us more than COMET does. For English→Khmer, the lexical metrics and the learned metric don't agree on how we compare with the larger models. The honest summary is that we are competitive with them, not that we are better.
- Synthetic training data. At least a fifth of the training pairs were generated by other models. Their errors and stylistic habits are likely to carry over into ours.
- Short inputs. Training sentences were cut to 256 tokens. RoPE lets the model accept longer input, but we haven't measured how quality holds up on long passages, and documents should be translated sentence by sentence.
- Khmer→English. Quality in this direction, covered above, is well behind the alternatives.
- Rare names and terms. These get mistranslated. In the first ALT test sentence, Khmer→English renders “Randwick Racecourse” as “Trophystic Racing Square”.
- Training data terms. Three of the four training sources are third-party and keep their own terms. Check them before using the model commercially.
Usage#
The model is on Hugging Face, and you can try it in the playground. The Python package runs it on ONNX Runtime, without PyTorch, and downloads the 189 MB model on first use:
pip install "netra-nmt[cpu]" # or "netra-nmt[gpu]" for an NVIDIA GPU
from netra_nmt import translate, NetraTranslator
translate("Hello, how are you?") # English→Khmer by default
translate("ខ្ញុំស្រឡាញ់ប្រទេសរបស់ខ្ញុំ។", direction="km2en")
t = NetraTranslator() # one model, kept loaded
t.translate("Good morning, my friend.", mode="beam", beam_size=5)
The package also has a command-line tool, netra-nmt, and a local web app and REST API, netra-nmt serve, which come with pip install "netra-nmt[server,cpu]". A Docker image with the model inside runs offline:
docker run --rm -p 8000:8000 ghcr.io/netra-ai-lab/netra-nmt:latest
To fine-tune the model on your own sentence pairs, one English–Khmer pair per line, the repository has one script for each step:
bash training/setup.sh
bash training/check_data.sh my_pairs.tsv
bash training/finetune.sh my_pairs.tsv --name my-model
bash training/evaluate.sh published training/runs/my-model/checkpoints/best/checkpoint.pt
bash training/export.sh training/runs/my-model/checkpoints/best/checkpoint.pt
Future work#
Each of these follows from a limitation above:
- Khmer→English. Closing the gap to the larger models, most likely through more English-side data and a deeper decoder for this direction.
- More varied test sets. Legal, medical and conversational text, alongside ALT's news-style sentences.
- Human evaluation. Asking Khmer speakers to judge the translations directly, rather than relying only on automatic metrics.
References#
-
Deep Encoder, Shallow Decoder: Reevaluating Non-autoregressive Machine Translation
Jungo Kasai, Nikolaos Pappas, Hao Peng, James Cross, Noah A. Smith.
ICLR 2021.
arXiv:2006.10369 -
RoFormer: Enhanced Transformer with Rotary Position Embedding
Jianlin Su, Yu Lu, Shengfeng Pan, Ahmed Murtadha, Bo Wen, Yunfeng Liu.
Neurocomputing, 2024.
arXiv:2104.09864 -
Root Mean Square Layer Normalization
Biao Zhang and Rico Sennrich.
NeurIPS 2019.
arXiv:1910.07467 -
GLU Variants Improve Transformer
Noam Shazeer.
2020.
arXiv:2002.05202 -
SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing
Taku Kudo and John Richardson.
EMNLP 2018, System Demonstrations.
arXiv:1808.06226 -
No Language Left Behind: Scaling Human-Centered Machine Translation
NLLB Team, Marta R. Costa-jussà, James Cross, et al.
2022.
arXiv:2207.04672 -
MADLAD-400: A Multilingual And Document-Level Large Audited Dataset
Sneha Kudugunta, Isaac Caswell, Biao Zhang, et al.
NeurIPS 2023, Datasets and Benchmarks.
arXiv:2309.04662 -
COMET: A Neural Framework for MT Evaluation
Ricardo Rei, Craig Stewart, Ana C. Farinha, Alon Lavie.
EMNLP 2020.
arXiv:2009.09025 -
BERTScore: Evaluating Text Generation with BERT
Tianyi Zhang, Varsha Kishore, Felix Wu, Kilian Q. Weinberger, Yoav Artzi.
ICLR 2020.
arXiv:1904.09675 -
chrF++: words helping character n-grams
Maja Popović.
WMT 2017.
ACL Anthology W17-4770 -
Introduction of the Asian Language Treebank
Hammam Riza, Michael Purwoadi, Gunarso, et al.
Oriental COCOSDA 2016.
Dataset -
Seanghay Yath. khmercut: Khmer word segmentation [Software]. GitHub.
Cite this paper
@misc{netralab2026netramt,
title = {{Netra MT 1: A compact bidirectional encoder-decoder transformer for English–Khmer translation}},
author = {{Netra Lab}},
year = {2026},
url = {https://netra-lab.example/research/netra-mt}
}