Man in a Turban by Rembrandt

Artwork: Man in a Turban by Rembrandt. The Metropolitan Museum of Art · Public domain

Machine Learning

Choosing an On-Prem SLM Inference Engine for Windows

Running a Retrieval-Augmented Generation (RAG) system efficiently on Windows, without internet access, and under strict latency budgets comes down to one decision that dwarfs the rest: which inference engine you pick. After evaluating ONNX Runtime GenAI, LlamaCPP, Hugging Face Optimum, and Triton, ONNX Runtime GenAI won on token throughput, latency, and wall-clock efficiency for our scenario. This is the first of a two-part series; here we cover the evaluation and the numbers.

The constraints

We needed a RAG system built with LangChain, an SLM, and guardrails that could run in an air-gapped Windows environment with hard requirements:

Those constraints immediately rule some things out. No Docker on the target box means Triton is off the table. Windows-first means anything requiring extensive custom builds (looking at you, LlamaCPP) is a liability.

The candidates

Engine Why we considered it Dealbreaker for us
ONNX Runtime GenAI First-class ONNX + Phi-3 support, Windows binaries
LlamaCPP Fast, portable GGUF inference Needs custom builds on Windows
Hugging Face Optimum Familiar HF ergonomics Limited graph optimization; no Phi-3
Triton Great serving throughput Requires Docker; blocked on-prem

We used Microsoft’s Phi-3 Mini 4k Instruct ONNX model, deployed on an Azure ND96amsr_A100_v4 instance (1× A100 80GB). Benchmarks used a fixed generation length of 256 tokens, 5 warmup runs, and 10 repetitions, driven by the benchmark script that ships in the onnxruntime-genai package.

Reproducing the benchmark

The numbers below come from the package’s own benchmark_e2e.py, swept across prompt lengths at batch size 1, targeting ~99% GPU utilization:

python benchmark_e2e.py \
  -i ./phi3-mini-4k-instruct-onnx \
  -g 256 \            # generation length (tokens)
  -p 16,64,256,1024,2048 \  # prompt lengths swept
  -b 1 \              # batch size
  -w 5 -r 10          # 5 warmup runs, 10 repetitions

We captured five metrics per run: token-generation throughput (tps), token-generation latency, wall-clock throughput, wall-clock latency, and GPU utilization. The last one matters as a sanity check — an engine that can’t saturate the A100 is leaving throughput on the table regardless of its per-token numbers.

Results

Token generation throughput (tokens/sec)

ONNX Runtime GenAI led at every prompt length, and its lead widened as prompts grew — exactly what you want for RAG, where retrieved context inflates the prompt.

Prompt length ORT GenAI LlamaCPP HF Optimum ORT/LlamaCPP ORT/Optimum
16 137.59 109.47 108.35 1.26× 1.27×
64 136.82 110.26 107.14 1.24× 1.28×
256 134.45 109.42 105.76 1.23× 1.36×
1024 127.34 105.60 102.11 1.21× 1.50×
2048 122.62 102.00 99.35 1.20× 1.59×

Hand-drawn line graph of the measured ONNX Runtime GenAI to Hugging Face Optimum throughput ratio rising from 1.27 times at 16 prompt tokens to 1.59 times at 2048 tokens.

Wall-clock latency (seconds)

The number that actually matters for the 5-second budget. ORT GenAI stayed comfortably under it even at 2048-token prompts.

Prompt length ORT GenAI (s) Optimum (s) LlamaCPP (s)
16 2.491 3.526 2.51
64 2.502 3.545 2.52
256 2.571 3.772 2.71
1024 2.790 4.049 3.12
2048 3.073 4.279 3.46

Optimum’s weaker graph optimization — and its lack of Phi-3 support — shows up as roughly a full second of extra latency at long prompts. At a 2048-token prompt that’s 4.279s vs 3.073s — a 1.2-second gap, or ~39% slower, which is the difference between hitting and blowing the budget.

Why ONNX Runtime GenAI is faster

The gap isn’t luck; it’s what the engine does to the graph:

LlamaCPP is competitive on throughput but still trails by ~20%, and on Windows it demands custom builds we didn’t want to own.

Windows vs Linux

Interestingly, ORT GenAI’s wall-clock throughput on Windows matched or beat Linux across the board, which put to rest the assumption that Windows would cost us performance.

Prompt length Windows (tps) Linux (tps)
16 142.18 129.52
256 259.48 235.68
2048 932.03 892.12

Decision

ONNX Runtime GenAI won on the merits:

If you’re targeting offline, Windows-based SLM inference, ONNX Runtime GenAI is the pragmatic default.

In part two we wire the winning engine into a LangChain RAG pipeline — including a custom BaseLLM and the model-building step that produces the optimized ONNX graph.

References

#slm#onnx#rag#inference#windows