|
Download README.md from jburtoft/gte-encoder-neuron-kernels: direct link, hf CLI and curl.
- Browser
- Download file 11 kB
-
https://huggingface.co/kernels/jburtoft/gte-encoder-neuron-kernels/resolve/main/README.md
- Command line
-
hf download hf://jburtoft/gte-encoder-neuron-kernels/README.md
-
curl -L -o README.md https://huggingface.co/kernels/jburtoft/gte-encoder-neuron-kernels/resolve/main/README.md
11 kB
| license: apache-2.0 | |
| library_name: kernels | |
| tags: | |
| - kernel | |
| - neuron | |
| - nki | |
| - trainium | |
| - trn2 | |
| - bert | |
| - gte | |
| - reranker | |
| - embeddings | |
| - rope | |
| # 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`: | |
| ```bash | |
| 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 | |
| ``` | |
| ```python | |
| 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`](examples/rerank.py) (reranker) and [`examples/embed.py`](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_CONFIG` before importing the kernels. | |
| * Requires the NewBERT `NewModel` eager path: `unpad_inputs=False` and `use_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`](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=True` for the NewBERT modeling code. | |
| Transformers 5.x does not load these models correctly. | |
| * `kernels`: 0.12.3 with `get_local_kernel` next to Transformers 4.x (verified), or `get_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 | |
| Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE). | |