RS Infer
Setup

Configuration

Every server, model, and CLI option in a reference table.

The server reads a YAML file (default config.yaml). A fully-commented example lives at configs/config.example.yaml.

The file has two top-level sections: server (global options) and models (a list of model definitions).

CLI

Serve mode (the default) takes a single flag:

FlagShortDefaultDescription
--config <PATH>-cconfig.yamlPath to the YAML config file
rsinfer-server --config configs/config.example.yaml

rsinfer-server download <repo> <out>

Downloads a model's ONNX graph + tokenizer files from the Hugging Face Hub into <out> (created if missing), so it can be referenced with path: and served without any startup download:

rsinfer-server download Xenova/multilingual-e5-small models/e5-small
FlagDefaultDescription
--revision <REF>mainBranch, tag, or commit hash
--file <PATH>autoPin an exact graph path (e.g. onnx/model_fp16.onnx); overrides the model.onnx > fp16 > quantized preference
--subfolder <DIR>unsetSubfolder inside the repo to look in
--tokenizer-hf <REPO>unsetRepo to take tokenizer.json / config.json from, for model-only ONNX repos

HF_HOME, HF_ENDPOINT (Hub mirror) and HF_TOKEN (gated repos) are honored; the download lands in <out> as a self-contained folder (graph keeps its repo-relative path, e.g. onnx/model.onnx + onnx/model.onnx_data, with tokenizer.json and config.json at the folder root).

Verbosity is controlled by the RUST_LOG environment variable (see Logging).

Server options

server:
  bind: 0.0.0.0:8080
  max_body_mb: 32
  request_timeout_ms: 120000
  max_queue: 256
  queue_timeout_ms: 30000
  # hf_cache_dir: .hf-cache
KeyTypeDefaultDescription
bindstring0.0.0.0:8080host:port to bind the HTTP server to.
max_body_mbint32Hard cap on request body size (MiB). Larger requests are rejected with 413.
request_timeout_msint120000Overall per-request timeout (ms); exceeded → 408.
max_queueint256Max HTTP connections waiting for a session slot per model; overflow → 429.
queue_timeout_msint30000How long a request waits for a session slot; slow queue → 503.
hf_cache_dirpath(unset)Directory used for Hugging Face downloads. Defaults to HF_HOME or ~/.cache/huggingface.
threadsint0Size of the ONNX Runtime thread pool shared by all sessions of all models; 0 = number of physical cores. Raising it to all logical cores usually hurts (SMT siblings / efficiency cores slow every parallel op down).

Model options

Each entry in models selects a kind (pipeline) and a model source (hf repo id or a local path). Options common to all kinds:

KeyTypeDefaultDescription
namestring(required)Unique model id; referenced by clients as model in requests.
kindstring(required)Pipeline: embedding, rerank, zeroshot, pii.
hfstring(required if no path)Hugging Face repo id, e.g. Xenova/multilingual-e5-small. Downloaded into the cache at startup.
pathpath(required if no hf)Local directory containing model.onnx + tokenizer.json (+ config.json).
revisionstringmainHF repo branch/tag/commit.
subfolderstring(unset)Subdirectory inside the repo/path to look for files, e.g. onnx.
filestring(unset)Pin an exact graph filename (e.g. onnx/model_quantized.onnx); overrides the automatic model.onnx > fp16 > quantized preference.
tokenizer_hfstringhfRepo to take tokenizer.json/config.json from, for model-only ONNX exports.
dtypestringautoGraph precision. auto: on CPU-only execution, int8 (see INT8 by default); with GPU/CoreML providers, the published graph (fp32 first, else fp16 > quantized). int8: the server's int8 rewrite whatever the providers. fp32 / fp16: the published graph of that format, no quantization.
max_lenint(unset)Tokenizer truncation length. Longer inputs are truncated and a WARN log line reports how many — except for pii models, which scan long texts in windows of max_len tokens overlapping by a quarter, so no PII escapes detection.
replicasint2Number of concurrent ORT sessions (pool replicas) — a memory vs throughput dial.
max_batchint32Max rows per forward pass when batching.enabled is false (or a request is larger than batching.queue_rows): bigger requests run as successive slices, bounding activation memory.
batchingobject(on)Cross-request dynamic batching, for every model kind: enabled (true), max_rows (64) rows and max_tokens (8192) padded tokens (rows × longest row) per forward, queue_rows (1024) queued rows before 429. Rows of concurrent requests are sorted by length and packed into shared forwards; a lone request is dispatched immediately.
intra_threadsint00 = sessions run on the shared pool (server.threads). > 0 = each session of this model gets its own pool of that many threads (e.g. to isolate a model, or on NUMA hosts).
epslist[string][cpu]Execution provider priority list (see Execution providers).
defaultboolfalseMake this model the default for its kind (requests without a model field use it). If unset, the single model of a kind is the default.
ort_log_levelstringwarnORT native log level: verbose, info, warn, error. error silences CoreML/TensorRT graph-partition chatter.
device_idint0GPU device index for CUDA/TensorRT/NVRTX.

Kind-specific options

Embedding (kind: embedding)

KeyTypeDefaultDescription
poolingstringautoauto (from 1_Pooling/config.json), cls, mean, last.
normalizebooltrueL2-normalize output vectors.
dimensionsint(unset)Matryoshka truncation (request may override).

Rerank (kind: rerank)

KeyTypeDefaultDescription
scoringstringautoauto, sigmoid, softmax, yes_no (all → relevance score in (0,1)), or logit (raw, unbounded scores). yes_no covers Qwen3-Reranker-style vocabulary scoring.

Zero-shot / true-false (kind: zeroshot)

KeyTypeDefaultDescription
hypothesis_templatestringThe text is about {label}.Template used for zero-shot classification.
entailment_labelstringentailmentLabel treated as entailment.
contradiction_labelstringcontradictionLabel treated as contradiction.

PII / NER (kind: pii)

KeyTypeDefaultDescription
thresholdfloat0.5Minimum token probability; below it the token is treated as outside any entity.

INT8 by default

On CPU-only execution (dtype: auto, the default) the server runs an int8 version of the model it quantizes itself from the fp32 graph, the first time the model loads:

  • MatMul weights become int8 with one scale per output column (activations are quantized dynamically, like onnxruntime.quantization.quantize_dynamic), and large embedding tables become int8 with one scale per row. Same speed as the per-tensor int8 files published on the Hub, closer to fp32 (e5-small: embedding cosine to fp32 ≥ 0.995 vs ≥ 0.984), and much less memory because the embedding table stays int8 (e5-small, 4 replicas: 384 MB vs 879 MB idle). e5-small vs fp32 on a 14-core i7, 4 replicas: 1018 vs 582 req/s on 1-text requests, 170 vs 130 texts/s on 32-passage requests.
  • A quality check then runs built-in sample inputs through both graphs and keeps int8 only if it stays close to fp32: embedding/logit cosine ≥ 0.99, rerank score within 0.05, PII token labels agreeing ≥ 97% (worst row). The verdict is logged (int8 quality check passed / ... drifts too far) and the model otherwise runs in fp32 — size memory for fp32 if the check may fail.
  • The int8 graph and the verdict are cached under <hf cache>/rsinfer/int8/ (server.hf_cache_dir, else HF_HOME, else ~/.cache/huggingface), so later starts skip both steps. Delete that folder to redo them.

Models measured on CPU: e5-small and ms-marco pass; the ai4privacy DistilBERT PII model and Qwen3-Embedding-0.6B fail and stay in fp32. Decoder-style models have activation outliers that dynamic int8 cannot represent — the Hub's own model_int8.onnx for Qwen3-Embedding-0.6B only reaches cosine 0.77–0.84 to fp32.

Upgrading: int8 embeddings differ slightly from fp32 ones. If an existing vector index was built with fp32 embeddings, either rebuild it or set dtype: fp32 on the model.

Sizing memory and throughput

  • Each replica holds its own copy of the weights (ONNX Runtime sessions cannot safely run concurrently, so concurrency comes from replicas). Budget roughly the model's file size plus runtime buffers per replica: e5-small fp32 is ~550 MB per replica (its 250k-token embedding table dominates), Qwen3-Embedding-0.6B int8 ~1 GB, fp32 well over 3 GB. dtype: int8 divides weight memory by ~4.
  • Peak memory under load is driven by the batch size in padded tokens. batching.max_tokens bounds it per forward (times the number of replicas running at once). Lower it for large models: Qwen3-Embedding-0.6B int8 with 2 replicas and max_tokens: 4096 peaks at ~4.6 GB under sustained load.
  • Replicas vs throughput: with batching on, 2–4 replicas keep the CPU busy; on a 14-core i7, 4 replicas served ~30–50% more short requests than 2.
  • Disabling ONNX Runtime's CPU memory arena does not save memory — it made peak usage worse in tests (allocator fragmentation).

Execution providers

eps is a priority list. Entries not compiled into the binary, or absent from the linked ONNX Runtime build, are skipped with a warning; CPU is always the final fallback. GET /v1/models reports what each model actually uses.

ValueRequires cargo featurePlatform
cpu(always)everywhere
coremlep-coremlmacOS
cudaep-cudaLinux + CUDA/cuDNN
tensorrtep-cuda,ep-tensorrtLinux (datacenter GPUs)
nvrtxep-nvrtx,lax-ep-matchingLinux (consumer GeForce/RTX, ORT 1.22+)
openvinoep-openvino (built with --no-default-features)Linux x86_64, Intel CPU/GPU/NPU (experimental)

OpenVINO (experimental). make cpu-openvino builds a server that loads ONNX Runtime at startup instead of linking it: fetch Intel's runtime (ONNX Runtime 1.24 + OpenVINO 2025.4, ~215 MB) with scripts/fetch-openvino-runtime.sh /opt/onnxruntime-openvino and start the server with ORT_DYLIB_PATH=/opt/onnxruntime-openvino/libonnxruntime.so.1.24.1 (Docker image: --build-arg FEATURES=ep-openvino --target runtime-openvino). Models opt in with eps: [openvino, cpu]; openvino_device picks CPU (default), GPU, NPU or AUTO. Models with eps: [cpu] keep running on ONNX Runtime's CPU kernels in the same server, int8 included.

Measured on a 14-core i7-13850HX CPU, it does not beat the default build:

  • OpenVINO runs fp32 graphs ~1.2–1.3x faster than ONNX Runtime at a fixed input shape, but dynamic int8 graphs 2–4x slower, so dtype: auto serves the published fp32 graph on it — and the default build's int8 path is faster still.
  • In the server (batched, varying shapes), a fp32 PII model on OpenVINO with 1 replica (it parallelizes internally; use replicas: 1) matched ONNX Runtime with 2 replicas: 294 vs 330 req/s on 1-text requests, 54 vs 51 texts/s on 32-text requests, with a lower memory peak (1.5 vs 2.6 GB).

Its use is therefore Intel GPUs/NPUs (Core Ultra) or fp32-only models on Intel hosts — measure on your hardware before adopting it.

CoreML caveat: for small BERT-size encoders CoreML often ends up slower than CPU (ORT splits the graph into many CPU↔CoreML partitions). Default to eps: [cpu] for small encoders and benchmark per model.

models:
  - name: e5-small
    kind: embedding
    hf: Xenova/multilingual-e5-small
    max_len: 512
    replicas: 2
    eps: [coreml, cpu]
    pooling: mean
    normalize: true

Logging

Logging uses tracing and is configured via the RUST_LOG environment variable:

RUST_LOG=info ./target/release/rsinfer-server --config config.yaml   # startup + request summaries
RUST_LOG=debug ./target/release/rsinfer-server --config config.yaml  # + EP/session/file-resolution detail
LevelWhat you see
errorErrors only
warnWarnings + errors
info (default)Startup, model loading completion, HTTP request summaries (method, route, status, duration_ms)
debugPer-file hub resolution, session construction, EP probing
traceEverything, incl. tokenizers/ORT internals (noisy)

Per-crate overrides are supported, e.g. RUST_LOG=info,hf_hub=debug to watch downloads only.

On this page