Furnace
A small, fast HTTP inference server for ONNX models, written entirely in Rust.
Furnace compiles your ONNX model into Rust at build time (with burn-onnx) and serves it from a single ~5 MB binary. No Python, no C++ runtime: the server starts in milliseconds and uses a fraction of the memory of a Python stack.
| ResNet-18 over HTTP, CPU | Batch 1 | Batch 4 | Ready after | Memory (peak private) | Ships as |
|---|---|---|---|---|---|
| Furnace | 8.8 ms | 27.0 ms | 39 ms | 78 MB | 5.6 MB binary + weights |
| FastAPI + ONNX Runtime | 8.6 ms | 28.9 ms | 1,863 ms | 694 MB | Python + 102 MB of packages + model |
Median latency on a 16-thread laptop CPU (AMD Ryzen 9 7940HS). In process, Furnace takes 8.0 ms per image against ONNX Runtime's 7.1 ms, and the same 25 ms for four. Details, more engines and how to reproduce: docs/PERFORMANCE.md.
Features
- Bring your own ONNX model: put
foo.onnxinmodels/, rebuild, runfurnace --model-name foo. No hand-written Rust. - Pure Rust, CPU or GPU:
--backend cpu|wgpu|metal|cuda. - Weights stay outside the binary in a memory-mapped
.bpkfile (or embed them with--features embed-weights). - JSON or raw
float32requests, and optionally JPEG/PNG with ImageNet preprocessing (--features image). - Production basics: request size limits, input validation, backpressure (
503), timeouts (504),/metrics, graceful shutdown.
Install
-
Release binaries for Linux (x86_64, musl, aarch64), macOS (Intel, Apple Silicon) and Windows come with
resnet18.bpkon the releases page. Put both in one directory and run./furnace --model-name resnet18. -
crates.io (package
furnace-server, commandfurnace). Models are compiled in, so point the build at your.onnxfiles:FURNACE_MODELS_DIR=/path/to/modelsBuilds from crates.io use Burn's stock CPU backend, which is several times slower than the release binaries until Furnace's CPU kernels ship in a Burn release.
-
From source: below.
Quick start
Requires Rust 1.95 or later (on Windows, the Visual Studio C++ build tools).
Send it an image (with a server built using --features image), or raw floats:
# "output" holds 1000 logits; the largest is class 258 (Samoyed)
Your own models
Every models/<name>.onnx becomes a built-in model at build time:
A model needs one float32 input and one float32 output, each with the batch as the first
dimension and every other dimension fixed (e.g. [N, 3, 224, 224]). Models that don't fit,
or that burn-onnx can't convert (it supports opsets 1–24), are skipped with a build warning
saying why. If conversion fails, onnx-simplifier
often helps. furnace --help lists the models in a build.
API
| Endpoint | Purpose |
|---|---|
POST /predict |
Inference |
GET /healthz |
Health check |
GET /model/info |
Input/output shapes and statistics |
GET /metrics |
Request, error and latency counters |
/predict takes each input flattened (ResNet-18: 3 × 224 × 224 = 150,528 floats, CHW,
ImageNet-normalized) and returns raw logits:
{ "input": [0.12, -0.53, ...] } → { "output": [-1.59, -0.17, ...], "inference_time_ms": 8.1, ... }
{ "inputs": [[...], [...]] } → { "outputs": [[...], [...]], ... }
For speed, send Content-Type: application/octet-stream with little-endian float32 values
(and X-Batch-Size: N for a batch); add Accept: application/octet-stream to get raw floats
back. Every response carries an X-Request-Id (yours, or a generated one) that also tags the
server's log lines for that request. Full reference, including errors:
docs/API.md; OpenAPI spec: docs/openapi.json.
Options
| Option | Default | |
|---|---|---|
-m, --model-name <NAME> |
the only built-in model | Model to serve |
-w, --weights <PATH> |
<name>.bpk next to the binary, then in models/ |
Weight file |
-b, --backend <BACKEND> |
cpu |
cpu, or wgpu / metal / cuda if built with that feature |
--host <HOST> / -p, --port <PORT> |
127.0.0.1 / 3000 |
Listen address |
--max-concurrent-requests <N> |
100 |
Beyond this, requests get 503 |
--max-body-mb <MB> |
100 |
Request size limit |
--request-timeout-ms <MS> |
none | Beyond this, requests get 504 |
--batch-workers <N> |
4 |
Forward passes running at once; beyond this, concurrent requests are batched (details) |
--max-batch-size <N> |
8 |
Most items per batched pass; 1 turns batching off |
--batch-timeout-us <US> |
2000 |
Longest a batch waits for requests still arriving; a lone request never waits |
--threads <N> |
one per physical core | Inference threads (also RAYON_NUM_THREADS) |
-l, --log-level <LEVEL> |
info |
RUST_LOG sets per-module levels; FURNACE_ENV=production switches to JSON logs |
--config <PATH> |
Read options from a TOML file (also FURNACE_CONFIG); the command line overrides it. See furnace.example.toml |
--model-path <PATH> serves a Burn MLP saved as <PATH>.bpk + <PATH>.json instead (see
examples/basic_mnist_create.rs).
Deploying
A deployment is two files, the binary and the weights:
The weights are memory-mapped, so replace the file by renaming a new one over it rather than
editing it in place (on Windows, stop the server first). For a single file, build with
--features embed-weights.
GPU backends are Cargo features: --features wgpu (Vulkan, DX12, Metal), metal or cuda.
Docker
Each release publishes an image with ResNet-18 and MNIST built in, for x86_64 and ARM64:
To compile in your own models, build from the Dockerfile (distroless, non-root,
~80 MB with ResNet-18's weights):
--build-arg FEATURES=image adds JPEG/PNG input. The server stops cleanly on SIGTERM.
Development
Debug builds optimize the dependencies, so cargo test needs no --release and rebuilds in
seconds after a change. Generated model code is in target/*/build/furnace-server-*/out/models/. Furnace ships a
patched copy of Burn's CPU backend in vendor/burn-flex with faster convolutions; see
vendor/README.md. Contributions are welcome, see
CONTRIBUTING.md, and benchmark results from other CPUs especially
(#52).
License
MIT. Built on Burn and burn-onnx; models from the ONNX Model Zoo.