Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

.nornir/embedder-selection.md

Runtime embedder selection — GPU/CPU/AMD in ONE binary (plan #9)

Internal design + guide for plan item #9: a plain cargo install nornir yields ONE binary that carries both embedder backends and picks the right one at runtime — NVIDIA GPU (CUDA) or AMD GPU (ROCm) when present, pure-Rust CPU otherwise — with no build failure and no panic on a GPU-less / library-less box. This file is a durable .nornir/ design note (not a rendered artifact).

Source: src/vector/select.rs (the selector), src/vector/cuda.rs (NVIDIA + onnxruntime discovery), src/vector/rocm.rs (AMD probe), src/vector/embed.rs (tract CPU), src/vector/embed_ort.rs (ort GPU/CPU). Companion: vector.md (the index), vector-rocm.md.

What changed vs the pre-#9 state

Before #9 the default features shipped only embed-tract (pure-Rust CPU). embed-ort (GPU) was opt-in, so the default binary never touched the GPU even on an RTX 4090. The runtime CUDA/ROCm probes (cuda.rs / rocm.rs) existed but were dead code in the default build (#[cfg(feature = "embed-ort")]).

#9 makes both backends part of default and adds a runtime selector that is the single place deciding which embedder to load on the running machine.

The selection (probe order → backend)

src/vector/select.rs::decide(&Probe) -> Backend is a pure function over a struct of booleans:

  probe onnxruntime present?  ── no ──▶ Backend::TractCpu  (CPU, pure Rust)
         │ yes
  probe NVIDIA CUDA ready?    ── yes ─▶ Backend::OrtCuda   (NVIDIA GPU)
         │ no
  probe AMD  ROCm  ready?     ── yes ─▶ Backend::OrtRocm   (AMD GPU)
         │ no
         └──────────────────────────▶ Backend::TractCpu  (CPU, pure Rust)

The build strategy: ort load-dynamic (why it can't break a CPU-only build)

The hard constraint: embed-ort must NOT break cargo build/cargo install on a box with no onnxruntime and no GPU. A normal embed-ort build links the onnxruntime C++ library (via ort's default download-binaries/static link), so a plain box would fail to build/link — that violates "works out of the box."

The fix is ort's load-dynamic feature (ort/load-dynamic → ort-sys/disable-linking, verified against ort 2.0.0-rc.12 in this repo):

That panic risk is exactly why the selector is probe-first: it never constructs an ort object unless arm_onnxruntime() confirmed a real libonnxruntime.so is loadable (the probe dlopens it and checks the OrtGetApiBase export, then drops the handle). No onnxruntime ⇒ decide() returns TractCpu and the ort module is never entered → no panic, no link.

Cargo.toml default change

default = [..., "embed-tract", "embed-ort", ...]   # both backends, was tract-only
embed-ort = ["vector", "dep:ort", "ort/load-dynamic", "dep:tokenizers", "dep:libloading"]
ort = { version = "2.0.0-rc.12", default-features = false,
        features = ["std", "ndarray", "cuda", "api-24"], optional = true }

default-features = false drops ort's download-binaries/tls-* (not needed under dynamic-load); load-dynamic brings ort-sys/disable-linking. The cuda feature only enables the CUDA EP bindings (no link under disable-linking); embed-ort-rocm adds ort/migraphx + ort/rocm for the AMD EP.

HONEST build-prerequisite note

So #9's "GPU by default" is honest: the binary is capable of GPU out of the box, and uses it the moment a real onnxruntime + GPU are present, with zero build prerequisites for the CPU case.

nornir vector doctor

In the default build, doctor prints the CUDA preflight, then the runtime probe booleans (onnxruntime / cuda_ready / rocm_ready), the selected backend, and a real test embed through it (dims + ms + backend label). It is the operator's single readout of the runtime choice.

Batched CPU embedding (2026-07-02) — ~2–4×, bit-identical

The tract CPU path (src/vector/embed.rs) used to run one forward per text. It now batches: tokenize all texts, length-bucket them (sort by token count so similar lengths pad together, minimizing wasted padding), pack up to MAX_BATCH_ROWS=16 rows / MAX_BATCH_TOKENS=8192 per batch (env NORNIR_EMBED_BATCH_ROWS / NORNIR_EMBED_BATCH_TOKENS), run one (B, pad_len) forward per batch, mask-aware mean-pool + normalize each row, distribute batches across cores via gatling_map_balanced, reassemble in original order.

Tests (LAW: inject-assert)

In src/vector/select.rs (#[cfg(test)]):

Plus the existing rocm::probe_degrades_to_cpu_without_rocm and the ort/tract #[ignore] load+embed tests.