gte-encoder-neuron-kernels
NKI kernels for NewBERT-architecture encoders on AWS Trainium2 (trn2) with PyTorch Native
(torch.compile(backend="neuron")). NewBERT is the Alibaba-NLP "new-impl" modeling code used by the gte
model family: post-LayerNorm, rotate-half RoPE with NTK scaling, and a gated GELU MLP.
Each layer runs as two fused kernels instead of dozens of small ops:
| Kernel | Replaces |
|---|---|
attn_block |
QKV projection + RoPE + masked softmax attention + output projection + residual + LayerNorm (L <= 1024) |
gated_mlp_ln |
gated GELU MLP + residual + LayerNorm (weights resident in SBUF, or streamed when they do not fit) |
ln / res_ln |
(residual +) LayerNorm, using the Vector engine's bn_stats / bn_aggr (embedding LayerNorm) |
Other lengths of 768 or more (for example 1536) use attention_cte from the Neuron NKI library (nkilib) for
the attention core, which beats the compiler's own attention at those lengths.
Validated models:
| Model | Hidden / heads / layers | Head |
|---|---|---|
Alibaba-NLP/gte-multilingual-reranker-base |
768 / 12 / 12 | sequence classification (reranker) |
Alibaba-NLP/gte-multilingual-base |
768 / 12 / 12 | embeddings |
Alibaba-NLP/gte-base-en-v1.5 |
768 / 12 / 12 | embeddings |
Alibaba-NLP/gte-large-en-v1.5 |
1024 / 16 / 24 | embeddings |
Usage
The NewBERT modeling code needs Transformers 4.x (it loads incorrect position ids and RoPE tables on 5.x).
Transformers 4.x pins huggingface_hub < 1.0, and kernels releases that can resolve kernel-type Hub repos
need huggingface_hub >= 1.10. So in the model's environment, load the kernels from a clone of this repo with
get_local_kernel:
git clone https://huggingface.co/kernels/jburtoft/gte-encoder-neuron-kernels
pip install "transformers>=4.53,<5" "kernels==0.12.3" # 0.12.3: newest kernels compatible with hub < 1.0
import os
os.environ.setdefault("NEURON_LOGICAL_NC_CONFIG", "2") # read once, when the kernels are imported
from pathlib import Path
import torch
from kernels import get_local_kernel
from transformers import AutoModel
gte = get_local_kernel(Path("gte-encoder-neuron-kernels"), "gte_encoder_neuron_kernels")
model = AutoModel.from_pretrained("Alibaba-NLP/gte-large-en-v1.5", trust_remote_code=True,
attn_implementation="eager").eval()
for mod in model.modules(): # the fused MLP uses the tanh GELU (see Accuracy)
for name, child in mod.named_children():
if "GELU" in type(child).__name__:
setattr(mod, name, torch.nn.GELU(approximate="tanh"))
model = model.to(torch.bfloat16)
gte.patch_model(model) # in place; returns the number of layers patched
model = torch.compile(model.to("neuron"), backend="neuron", dynamic=False)
out = model(input_ids=ids.to("neuron"), attention_mask=mask.to("neuron")) # right-padded, fixed L
In an environment with huggingface_hub >= 1.10 and kernels >= 0.14, for example when you call the ops
from your own model code, get_kernel works directly:
gte = get_kernel("jburtoft/gte-encoder-neuron-kernels", version=1, trust_remote_code=True).
Full scripts: examples/rerank.py (reranker) and examples/embed.py
(embeddings). Pad every batch to a fixed length (for example 128 / 256 / 512) so that each (length,
batch) shape compiles once.
patch_model(model) patches every NewLayer and the embedding LayerNorm. Shapes that the kernels do not
support fall back to the stock modules at trace time, so patching is always safe:
- fused attention block: L in {128, 256, 384, 512, 768, 1024}; batch divisible by the number of logical cores, or (L / 128) divisible by it (an odd batch at LNC=2 splits the queries across the two cores);
- MLP and LayerNorm kernels: batch x L divisible by 128 x LNC;
- other L >= 768: stock attention projections + nkilib
attention_cte+ the NKI LayerNorm.
The raw kernels (attn_block_kernel, gated_mlp_ln_kernel, gated_mlp_ln_stream_kernel,
res_ln_kernel) and the host-side weight packer (pack_attn_block) are exported too, for use from other
frameworks or under nki.simulate.
Performance (trn2.3xlarge, bf16)
Measured 2026-10 on PyTorch Native Beta 6 (torch 2.13.0, torch-neuronx 2.13.3, NKI 0.7.0b1) with real
Hugging Face weights. "Stock" is the same model, also with the tanh GELU, compiled with
torch.compile(backend="neuron") without these kernels, in the same container on the same instance.
Whole instance (one process per logical core, all cores busy at once; best LNC for each column):
| Model | L x batch | Stock (seq/s) | These kernels (seq/s) | Speedup |
|---|---|---|---|---|
| gte-multilingual-reranker-base | 128 x 32 | 8,463 | 12,427 | 1.47x |
| gte-multilingual-reranker-base | 256 x 32 | 3,864 | 5,865 | 1.52x |
| gte-base-en-v1.5 | 128 x 32 | 8,407 | 12,397 | 1.47x |
| gte-base-en-v1.5 | 256 x 32 | 3,879 | 5,831 | 1.50x |
| gte-large-en-v1.5 | 128 x 32 | 2,645 | 3,427 | 1.30x |
| gte-large-en-v1.5 | 256 x 32 | 1,278 | 1,622 | 1.27x |
One logical core (single process, LNC=2, ms per batch, median):
| Model | L x batch | Stock | These kernels | Speedup |
|---|---|---|---|---|
| gte-base-en-v1.5 | 128 x 32 | 16.3 | 10.1 | 1.62x |
| gte-base-en-v1.5 | 256 x 32 | 35.8 | 20.2 | 1.77x |
| gte-base-en-v1.5 | 512 x 16 | 39.4 | 22.8 | 1.73x |
| gte-base-en-v1.5 | 512 x 1 | 4.0 | 3.2 | 1.24x |
| gte-large-en-v1.5 | 128 x 32 | 44.8 | 30.5 | 1.47x |
| gte-large-en-v1.5 | 256 x 32 | 104.4 | 61.1 | 1.71x |
| gte-large-en-v1.5 | 512 x 16 | 111.7 | 68.0 | 1.64x |
| gte-large-en-v1.5 | 512 x 1 | 9.2 | 7.9 | 1.17x |
Long sequences (single process, ms per batch, median; v1.0.0 used attention_cte + unfused projections at
these lengths, v1.1.0 runs the fused attention block):
| Model | LNC | L x batch | Stock | v1.0.0 | v1.1.0 | vs stock |
|---|---|---|---|---|---|---|
| gte-multilingual-reranker-base | 2 | 768 x 8 | 40.0 | 33.3 | 20.1 | 1.99x |
| gte-multilingual-reranker-base | 2 | 1024 x 8 | 54.7 | 46.6 | 29.3 | 1.87x |
| gte-multilingual-reranker-base | 2 | 1024 x 1 | 8.0 | 7.7 | 5.7 | 1.41x |
| gte-multilingual-reranker-base | 1 | 1024 x 8 | 107.2 | 82.9 | 54.0 | 1.98x |
| gte-large-en-v1.5 | 2 | 768 x 8 | 108.0 | 95.3 | 62.2 | 1.74x |
| gte-large-en-v1.5 | 2 | 1024 x 8 | 155.7 | 127.9 | 87.3 | 1.78x |
| gte-large-en-v1.5 | 2 | 1024 x 1 | 21.5 | 18.8 | 15.1 | 1.43x |
| gte-large-en-v1.5 | 1 | 1024 x 8 | 307.5 | 242.0 | 168.7 | 1.82x |
gte-large's MLP weights (1024 x 8192 + 4096 x 1024 per layer) do not fit in SBUF next to the working set, so its MLP kernel streams them. With every core busy, that streaming competes for HBM bandwidth, which is why its whole-instance gain is lower than its single-core gain.
Accuracy
Checked against the stock Hugging Face model on CPU in fp32 with the exact GELU, on 64-72 real-sentence inputs per shape with ragged lengths (6 tokens up to the padded length), at L = 128 / 256 / 512 / 768 / 1024, LNC=1 and LNC=2 (768 / 1024 measured on the reranker and gte-large). "Stock" is stock bf16 PyTorch Native on the same inputs.
| Model | Metric | Stock bf16 | These kernels |
|---|---|---|---|
| gte-multilingual-reranker-base | score Pearson | 0.99997-0.99999 | 0.99997-0.99998 |
| gte-multilingual-reranker-base | top-1 passage per query = fp32 | same as stock | same as stock |
| gte-multilingual-base | CLS cosine, mean | 0.99984-0.99987 | 0.99992-0.99993 |
| gte-base-en-v1.5 | CLS cosine, mean | 0.99991-0.99992 | 0.99993 |
| gte-large-en-v1.5 | CLS cosine, mean | 0.99986-0.99991 | 0.99990-0.99992 |
| all embedding models | retrieval top-1 = fp32 | 0.88-1.00 | 0.88-1.00 (never below stock) |
The kernels keep the residual sum and the LayerNorm statistics in fp32, so on average they are slightly closer to fp32 than stock bf16. The worst single input is on par with stock: the lowest per-text cosine was 0.99900-0.99991 with the kernels against 0.99834-0.99984 for stock. On gte-large it trailed stock's worst by up to 2.3e-4 in some configurations. Padding is exact: a sequence's output is bit-identical whatever the other sequences in its batch contain.
GELU. The MLP kernel computes gelu_tanh (the tanh approximation), while the models specify the exact
(erf) GELU. All accuracy numbers above are against the exact-GELU fp32 model, so they include this
difference. The usage snippet swaps the stock GELU module for the tanh version too, so that fallback shapes
compute the same function.
Constraints
- Inference only (eval mode, no dropout, no
head_mask). - bf16 activations and weights; hidden size a multiple of 128; head_dim 64; number of heads a multiple of 4.
- Right padding only, with a standard 0/1
attention_mask. - Set
NEURON_LOGICAL_NC_CONFIGbefore importing the kernels. - Requires the NewBERT
NewModeleager path:unpad_inputs=Falseanduse_memory_efficient_attention=False(the defaults of all four validated models).
Tests
tests/ has simulator tests (pytest -m sim, kernels under nki.simulate against NumPy fp32 references),
a CPU test of the weight packing against the Hugging Face layer (-m cpu), and on-device accuracy tests
(-m neuron). See tests/README.md. A simulator pass is not device evidence: the
simulator does not model SBUF or PSUM capacity, and some DMA access patterns behave differently on
hardware.
Requirements
- trn2, PyTorch Native (TorchNeuron) with NKI 0.7 and nkilib. Verified on Beta 6 (torch 2.13.0, torch-neuronx 2.13.3, NKI 0.7.0b1, neuronx-cc 2.0.404056).
- Transformers 4.x (verified on 4.53.3) with
trust_remote_code=Truefor the NewBERT modeling code. Transformers 5.x does not load these models correctly. kernels: 0.12.3 withget_local_kernelnext to Transformers 4.x (verified), orget_kernel(..., version=1, trust_remote_code=True)with kernels 0.17.2 / huggingface_hub 2.x (verified on trn2).
Repository layout
build/torch-neuron/
├── __init__.py # public API
├── metadata.json
├── torch_ops.py # torch custom ops (gte_enc::*), NkiLN / NkiMlpLN / NkiAttnBlock, patch_model
├── attn_block_kernel.py # fused attention block
├── attn_block_pack.py # host-side weight packing (NumPy)
├── gated_mlp_kernel.py # fused gated MLP + residual + LayerNorm (resident and streaming)
└── res_ln_kernel.py # (residual +) LayerNorm
examples/ # rerank.py, embed.py
tests/ # simulator, CPU and device tests
License
- Downloads last month
- -