Paper/

Netra MT 1

A compact bidirectional encoder-decoder transformer for English–Khmer translation

One imaginary flowering herb, painted in ink on paper

Generated with Nonflowers by Lingdong Huang · MIT

Parameters
77M
Training tokens
220M
License
MIT

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.

Diagram of Netra MT 1: a 32K SentencePiece tokenizer and shared embedding feed a deep bidirectional encoder and a two-layer decoder, both with rotary position embeddings and a final RMSNorm; the encoder output feeds the decoder, which ends in a linear head over the vocabulary
Figure 1 — Netra MT 1. Both languages go through the same 32K SentencePiece tokenizer and a shared embedding table. The bidirectional encoder (orange, 12 layers) reads the source; the decoder (yellow, 2 layers) attends to the encoder's output and writes the target one token at a time through a linear head. Both use rotary position embeddings (RoPE) and end in an RMSNorm.

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 ii of a token at position mm by the angle mθim\theta_i, where

θi=10000−2(i−1)/64,i=1,…,32.\theta_i = 10000^{-2(i-1)/64}, \qquad i = 1, \dots, 32.

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.

  1. 1Each position turns the vector a little further

  2. 2Two tokens the same distance apart keep the same angle

Figure 2 — RoPE on the source pieces of “I love my country”. (1) Three of the 32 pairs at each position, drawn with the same vector everywhere so only the rotation differs: pair 1 turns 57° per position, pair 8 turns 7.6° and pair 32 almost not at all. (2) In pair 1, “i” and “my” are two positions apart, and so are “love” and “country”, so both have the same 115° between them. Orange is the rotated vector; the dashed hand is where it started.

Because the query and the key are both rotated, the score between two tokens depends only on how far apart they are:

(Rmq)⊤(Rnk)=q⊤Rn−m k,(R_m q)^\top (R_n k) = q^\top R_{n-m}\, k,

where qq is the query of the token at position mm, kk is the key of the token at position nn, and RmR_m rotates every pair ii by mθim\theta_i. 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:

x←x+Attention⁡(RMSNorm⁡(x)),x←x+FFN⁡(RMSNorm⁡(x)).x \leftarrow x + \operatorname{Attention}(\operatorname{RMSNorm}(x)), \qquad x \leftarrow x + \operatorname{FFN}(\operatorname{RMSNorm}(x)).

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,

RMSNorm⁡(x)=x1d∑j=1dxj2+ϵ⊙g,\operatorname{RMSNorm}(x) = \frac{x}{\sqrt{\frac{1}{d}\sum_{j=1}^{d} x_j^2 + \epsilon}} \odot g,

where d=512d = 512 is the model dimension, ϵ=10−6\epsilon = 10^{-6} avoids dividing by zero, and gg 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:

FFN⁡(x)=W2(SiLU⁡(Wgx)⊙Wvx),SiLU⁡(z)=z⋅σ(z),\operatorname{FFN}(x) = W_2\bigl(\operatorname{SiLU}(W_g x) \odot W_v x\bigr), \qquad \operatorname{SiLU}(z) = z \cdot \sigma(z),

where WgW_g and WvW_v map 512 dimensions to 2,048, W2W_2 maps them back to 512, σ\sigma is the sigmoid and ⊙\odot multiplies element by element.

layer input, 512-dto the next layerRMSNormSelf-attention, RoPERMSNormSwiGLUinside SwiGLUgate512 → 2,048value512 → 2,048SiLU2,048 → 512
Figure 3 — One encoder layer. The input runs down the residual stream on the left. Each sub-layer branches off through an RMSNorm, and its output is added back (⊕). On the right, the SwiGLU block opened up: the gate passes through SiLU and multiplies the value (⊗). Decoder layers add a cross-attention sub-layer in the same pattern. Orange marks what differs from the original Transformer.

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.

  1. 1Encode the source once

  2. 2Write one piece per step, reusing the cache

Figure 4 — The KV cache on a real translation by Netra MT 1: “I love my country” → ខ្ញុំស្រឡាញ់ប្រទេសរបស់ខ្ញុំ. (1) The encoder reads the source once, and its keys and values (yellow) are kept for every step. (2) The decoder writes one piece per step, and each piece it writes is the next step's input. It computes keys and values for the new input only (orange) and reads the earlier ones from the cache (grey).

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 dimension512
Encoder / decoder layers12 / 2
Attention heads8, of 64 dimensions each
Feed-forward size2,048, SwiGLU
Position encodingRoPE in self-attention
Vocabulary32K 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.

  1. 1Input

    No spaces between words

    ខ្ញុំស្រឡាញ់ប្រទេសរបស់ខ្ញុំ

    I love my country

  2. 2khmercut

    Splits the text into words

    ខ្ញុំIស្រឡាញ់loveប្រទេសcountryរបស់ofខ្ញុំme
  3. 3SentencePiece

    On the segmented text: one piece per word

    ខ្ញុំស្រឡាញ់ប្រទេសរបស់ខ្ញុំ
  4. 4Without khmercut

    The same tokenizer on the raw text

    ខ្ញុំស្រឡាញ់ប្រទេសរបស់ខ្ញុំ

    Dashed: a piece that runs across a word boundary

Figure 5 — One sentence through the tokenizer, with real output from khmercut and our 32K SentencePiece model. After segmentation each word becomes one piece. Given the same sentence unsegmented, the tokenizer makes pieces that run across word boundaries: “I love” and “of me” each come out as a single piece.

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.

DatasetTypePairsDomains
Netra-Lab/khmer-english-pairs-rawSynthetic200KLegal, literary, governmental
lyfeyvutha/nllb-en-km-316KSynthetic316KGeneral
KrorngAI/ParaCrawl-English-Khmer-v2Web crawl1.5MWeb, general
SeyhaLite/Translate-English-Khmer-AllMixed366KGeneral

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#

spBLEU25.83 (second best in this row)18.899.2416.6216.1928.93 (best in this row)
chrF++38.05 (second best in this row)30.0734.3836.1432.2440.21 (best in this row)
TER ↓60.47 (best in this row)73.8978.6869.90101.9965.56 (second best in this row)
COMET78.1471.2876.5479.6879.95 (second best in this row)82.74 (best in this row)
BERTScore90.02 (second best in this row)87.8988.2489.1189.2890.66 (best in this row)
Table 1 — English→Khmer on the ALT test set. Higher is better, except TER. The best score in each row is bold and the second best is in italics. Google Translate is a commercial system of unknown size.

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#

spBLEU19.4731.2835.15 (second best in this row)36.13 (best in this row)34.86
chrF++46.0154.1457.3658.28 (best in this row)57.51 (second best in this row)
TER ↓69.7955.3451.14 (second best in this row)51.12 (best in this row)51.14 (second best in this row)
COMET77.0284.2585.8786.88 (best in this row)86.48 (second best in this row)
BERTScore91.7993.6294.1394.33 (best in this row)94.31 (second best in this row)
Table 2 — Khmer→English on the ALT test set. Higher is better, except TER. Bold is best and italics second best; NLLB-200 (1.3B) and Google Translate tie for second on TER. The NLLB EN-KM (350M) model is trained for English→Khmer only, so it is left out here.

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, greedy65.6 (best in this row)550.8 (second best in this row)1,074.9
CPU, beam-5735.2 (best in this row)802.7 (second best in this row)1,567.5
GPU, greedy14.8 (best in this row)77.2 (second best in this row)150.4
GPU, beam-5144.3 (second best in this row)105.2 (best in this row)223.0
Table 3 — Time to translate one English sentence into Khmer, in milliseconds (median over 20 sentences), on a desktop CPU (Intel i7-13700KF) and a GPU (RTX 3060), with greedy and beam-5 decoding. All three models run in PyTorch; for Netra MT 1 this is the earlier 0.1.0 package, before the beam search in Table 4. Lower is better. The best time in each row is bold and the second best is in italics.

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.639.1 (best in this row)
Greedy, tokens per second328.5518.1 (best in this row)
Beam-5, ms per sentence ↓701.163.5 (best in this row)
Beam-5, tokens per second29.4303.6 (best in this row)
Table 4 — The package before and after the move to ONNX Runtime, on the same desktop CPU (Intel i7-13700KF), English→Khmer, median over 20 sentences. Lower is better for milliseconds per sentence, higher for tokens per second. The better score in each row is bold.

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#

  1. 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

  2. RoFormer: Enhanced Transformer with Rotary Position Embedding
    Jianlin Su, Yu Lu, Shengfeng Pan, Ahmed Murtadha, Bo Wen, Yunfeng Liu.
    Neurocomputing, 2024.
    arXiv:2104.09864

  3. Root Mean Square Layer Normalization
    Biao Zhang and Rico Sennrich.
    NeurIPS 2019.
    arXiv:1910.07467

  4. GLU Variants Improve Transformer
    Noam Shazeer.
    2020.
    arXiv:2002.05202

  5. 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

  6. No Language Left Behind: Scaling Human-Centered Machine Translation
    NLLB Team, Marta R. Costa-jussà, James Cross, et al.
    2022.
    arXiv:2207.04672

  7. 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

  8. COMET: A Neural Framework for MT Evaluation
    Ricardo Rei, Craig Stewart, Ana C. Farinha, Alon Lavie.
    EMNLP 2020.
    arXiv:2009.09025

  9. BERTScore: Evaluating Text Generation with BERT
    Tianyi Zhang, Varsha Kishore, Felix Wu, Kilian Q. Weinberger, Yoav Artzi.
    ICLR 2020.
    arXiv:1904.09675

  10. chrF++: words helping character n-grams
    Maja Popović.
    WMT 2017.
    ACL Anthology W17-4770

  11. Introduction of the Asian Language Treebank
    Hammam Riza, Michael Purwoadi, Gunarso, et al.
    Oriental COCOSDA 2016.
    Dataset

  12. 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}
}

More research