Architecture¶
rembed implements the whole transformer forward pass in Go — embeddings, encoder layers, pooling, normalization — and reaches ONNX-Runtime-competitive speed through a measured optimization ladder, every rung validated against a golden reference and A/B-tested in isolation. This page describes how it's built; the numbers are in Benchmarks.
The forward pass¶
internal/model/ holds the encoder: token + position (+ segment) embeddings →
N layers of attention + feed-forward → pooling → L2 normalization. Each text is
embedded independently, so a text's sequence length is exactly its own token
count. That keeps masking trivial and makes the output match a per-text ONNX
reference exactly, and it's why a batch fans out across texts with no padding
waste rather than padding a batch to a common length.
rembed spans eight architectures with their variations: absolute and rotary (RoPE) positions, bidirectional / sliding-window / causal attention with GQA and QK-norm, GELU / GeGLU / SwiGLU activations, LayerNorm and RMSNorm, and mean / CLS / last-token pooling — with an optional two-layer Dense projection head (EmbeddingGemma). See Supported models.
Weights: safetensors, not ONNX¶
rembed reads safetensors — a JSON header plus raw little-endian float blobs,
trivial to parse in Go — never ONNX protobuf. models/convert.py produces a
model directory once (model.safetensors, vocab.txt, manifest.json); the
manifest.json records the architecture's shape (hidden size, layers, heads,
pooling strategy, normalize flag, special tokens, …) so the Go loader needs no
framework. Loading straight from a Hugging Face checkout is also supported, with
the manifest derived on the fly.
The pluggable kernel interface¶
The heart of the performance plan is a single matmul signature with swappable bodies, each behind its own benchmark:
That Bᵀ layout [out, in] is exactly how Hugging Face stores an nn.Linear
weight, so linear layers need no transpose at load — the naive body reduces to
dot products of two contiguous rows. Because every kernel implements one
interface, a ladder rung (naive → cache-blocked → SIMD → int8) can be swapped in
and A/B-tested without touching the model code. A second, packed-weight interface
was added once packing weights at load became the structural win.
The optimization ladder¶
Each rung targets ONNX Runtime and is measured against it (reference laptop, an i9-13900H). Targets vs measured:
| Rung | What it added | Measured |
|---|---|---|
| M0 | Naive fp32 forward pass; build the validation + benchmark harness | matches ONNX within 1e-4 (2.9e-7); ~20× slower single-thread |
| M1 | Alloc-free, cache-blocked matmul | 1.81× (the scalar-Go ceiling) |
| M2 | Goroutine parallelism (heads / matmul row-blocks) | 4.1× |
| M3 | AVX2 SIMD matmul (hand-written plan9 asm) | 3.3× |
| M4 | Packed GEMM (gemm4x16), spinning fork-join pool, fast-math exp/erf |
parity, 1.05× |
| int8 (M5) | Weight-only int8 | ahead of ORT fp32 (0.89×) and ORT quint8 (0.95×) |
Beyond the core ladder, an R-series added breadth and further kernels: ARM64
NEON (R3), the MPNet / RoBERTa+BPE / SentencePiece / ModernBERT / Qwen3 /
XLM-RoBERTa / EmbeddingGemma / nomic models (R4–R14), AVX-VNNI full int8 (R7),
and disk-backed weights (R11) — plus a 512-bit-wide fp32 kernel (gemm4x32) and
a gemm6x16 micro-kernel that closed most of the residual single-core fp32 gap.
int8 quantization¶
- Weight-only (
WithInt8) — dense weights are quantized at load to per-output-channel symmetric int8; activations stay fp32, andgemm4x16i8dequantizes in-register with one per-channel multiply in the epilogue. The win is memory traffic: aseq=12embed streams ~42 MB of fp32 weights; int8 cuts that to ~10.5 MB, which fits in L3. - Full int8 (
WithInt8Activations) — weights as above, plus activations quantized per-row tou8via the+128offset trick, soVPDPBUSDcomputesu8·s8as the true dot plus a precomputed128·colSumcorrection subtracted in the epilogue. One instruction does four multiply-accumulates perint32lane. Kernels exist for AVX-VNNI (256-bit, VEX-encoded) and AVX-512-VNNI (gemm4x16vnni512, EVEX); both are bit-exact against a pure-Go integer reference. Thek > 131072overflow limit is ~85× past any transformer.
Both int8 paths fall back silently (int8-activations → weight-only → fp32) when
the CPU or a matrix shape can't take them — query Quantized() /
QuantizedActivations() when the mode matters.
Correctness: the golden-ONNX harness¶
Built at M0, before any optimization. models/convert.py runs fixed inputs
through ONNX Runtime in Python, applies the manifest's pooling + L2 normalization
in numpy, and writes testdata/golden.json: per input, the text, the
Hugging-Face-tokenizer input_ids, and the final embedding. Storing the ids
separately lets a failure be attributed to the tokenizer vs the
numerics. The input set covers accents, non-ASCII symbols, and CJK.
Every kernel and model change must match within 1e-4 max absolute
difference, in CI. Model weights (~90 MB) are never committed — tests skip when
the model dir is absent. SIMD reductions accumulate in a different order than
scalar, so SIMD paths are held to a relative tolerance rather than bit-identity,
but still match the golden within 1e-4; int8 is bounded per-model by
test-enforced cosine floors (weight-only ≥ 0.988 across models; full int8 lower
on some checkpoints — see choosing a quantization
mode).
Package layout¶
rembed/ (public API) · internal/tensor/ (matmul, softmax, layernorm, gelu) ·
internal/model/ (the encoder) · internal/safetensors/ (weight loader) ·
internal/hub/ (pure-Go Hugging Face fetch, sha256-verified) ·
internal/packfile/ + internal/mmapfile/ (disk-backed weights) · tokenizer/
(+ bpe, sentencepiece, gemma subpackages) · cmd/rembed/ (CLI) · bench/
(harness) · models/convert.py · testdata/ (goldens).