Skip to main content

furnace/
lib.rs

1//! An inference server and nervous system in pure Rust, on axum and tokio.
2//!
3//! Furnace serves models with what a production inference server needs: the KServe / Triton
4//! Open Inference Protocol over HTTP and gRPC (so Triton and KServe clients work unchanged),
5//! dynamic batching, several models per process with reloads that don't drop a request,
6//! Prometheus metrics, readiness after a warm-up, API keys, rate limits and TLS. It serves LLMs
7//! with the OpenAI API (the `llm` module), and connects sensors, models, the LLM and actuators
8//! through its [`nerve`] module. See the [README](https://github.com/Gilfeather/furnace#readme).
9//!
10//! This crate runs no model itself: an engine implements [`TensorModel`]. Burn is the engine
11//! furnace is built around: the `furnace-burn` crate serves a Burn model's `forward`, a Burn
12//! model of named tensors, weights from Burnpack files and ONNX models compiled to Burn, and
13//! the `furnace` command (crate `furnace-cli`) serves ONNX models from the command line.
14//!
15//! # Serving a model
16//!
17//! Implement [`TensorModel`] for a model and serve it with [`Server`]. It answers
18//! `POST /predict` and `POST /v2/models/{name}/infer`, and concurrent requests are batched
19//! together. With Burn, `furnace_burn::from_fn` does this for a `forward` function.
20//!
21//! ```no_run
22//! use furnace::{HostTensor, IoSpec, Model, Server, TensorModel, Values};
23//!
24//! /// Sums each row of four values
25//! #[derive(Debug)]
26//! struct RowSum;
27//!
28//! impl TensorModel for RowSum {
29//!     fn name(&self) -> &str { "rowsum" }
30//!     fn backend(&self) -> &str { "cpu" }
31//!     fn inputs(&self) -> Vec<IoSpec> { vec![IoSpec::fp32("input", &[4])] }
32//!     fn outputs(&self) -> Vec<IoSpec> { vec![IoSpec::fp32("output", &[1])] }
33//!     fn run(&self, inputs: Vec<HostTensor>) -> furnace::Result<Vec<HostTensor>> {
34//!         // Checked already: one FP32 tensor of [batch, 4]
35//!         let Values::F32(values) = &inputs[0].values else { unreachable!() };
36//!         let sums: Vec<f32> = values.chunks(4).map(|row| row.iter().sum()).collect();
37//!         Ok(vec![HostTensor { shape: vec![sums.len(), 1], values: Values::F32(sums) }])
38//!     }
39//! }
40//!
41//! #[tokio::main]
42//! async fn main() -> furnace::Result<()> {
43//!     // Logs to stderr: where the server listens, what it serves, why it stopped
44//!     tracing_subscriber::fmt::init();
45//!     Server::new().port(3000).model(Model::custom(RowSum)).serve().await
46//! }
47//! ```
48//!
49//! # Mounting it in your own axum app
50//!
51//! [`Server::router`] returns the inference API as an axum `Router`. Merge it with your own
52//! routes and serve the result as usual: furnace answers `/predict`, `/v2/...`, `/healthz`,
53//! `/readyz` and `/metrics`, with batching and backpressure, and your routes do the rest.
54//!
55//! ```no_run
56//! use axum::{Json, Router, routing::get};
57//! use furnace::{Model, Server};
58//!
59//! # use furnace::{HostTensor, IoSpec, TensorModel, Values};
60//! # #[derive(Debug)]
61//! # struct Digits;
62//! # impl TensorModel for Digits {
63//! #     fn name(&self) -> &str { "digits" }
64//! #     fn backend(&self) -> &str { "cpu" }
65//! #     fn inputs(&self) -> Vec<IoSpec> { vec![IoSpec::fp32("input", &[784])] }
66//! #     fn outputs(&self) -> Vec<IoSpec> { vec![IoSpec::fp32("output", &[10])] }
67//! #     fn run(&self, inputs: Vec<HostTensor>) -> furnace::Result<Vec<HostTensor>> {
68//! #         let batch = inputs[0].batch();
69//! #         Ok(vec![HostTensor { shape: vec![batch, 10], values: Values::F32(vec![0.0; batch * 10]) }])
70//! #     }
71//! # }
72//! // One of your own routes: the class names for the 10 logits /predict returns
73//! async fn labels() -> Json<Vec<String>> {
74//!     Json((0..10).map(|digit| digit.to_string()).collect())
75//! }
76//!
77//! #[tokio::main]
78//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
79//!     tracing_subscriber::fmt::init();
80//!     let app = Router::new()
81//!         .route("/labels", get(labels))
82//!         .merge(Server::new().model(Model::custom(Digits)).router()?);
83//!
84//!     let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
85//!     axum::serve(listener, app).await?;
86//!     Ok(())
87//! }
88//! ```
89//!
90//! # Several inputs or outputs, integer tensors
91//!
92//! A model names its tensors in [`TensorModel::inputs`] and [`TensorModel::outputs`]: a text
93//! classifier taking token ids and a mask, a detector returning boxes and scores. The server
94//! checks every request against them before [`TensorModel::run`] sees it, and serves the model
95//! on `/v2/models/{name}/infer`, batching requests when every input has a fixed shape.
96//!
97//! ```
98//! use furnace::{DataType, IoSpec};
99//!
100//! let inputs = vec![
101//!     IoSpec::new("input_ids", DataType::Int64, &[Some(128)]),
102//!     IoSpec::new("attention_mask", DataType::Int64, &[Some(128)]),
103//! ];
104//! let outputs = vec![IoSpec::fp32("logits", &[2])];
105//! # assert_eq!(inputs[0].v2_shape(), [-1, 128]);
106//! # assert_eq!(outputs[0].v2_shape(), [-1, 2]);
107//! ```
108//!
109//! # The nervous system
110//!
111//! The [`nerve`] module connects receptors (an HTTP route, a channel, a timer, a stream) to
112//! neurons (a closure, a Burn model, an LLM) and effectors (a webhook, a command, a file).
113//! Most signals stop at a fast reflex; only what needs it reaches the slow brain.
114//! [`Server::nerve`] serves the neurons next to the models, on the same port.
115//!
116//! ```no_run
117//! use furnace::Server;
118//! use furnace::nerve::prelude::*;
119//! use serde_json::json;
120//!
121//! #[tokio::main]
122//! async fn main() -> furnace::Result<()> {
123//!     tracing_subscriber::fmt::init();
124//!     let mut b = Fabric::builder();
125//!     // POST /events: each body is a signal
126//!     let events = b.receptor(Http::new("events", "/events").text());
127//!     // A reflex: alerts become a command to the `alert` effector, the rest stop here
128//!     let triage = b.neuron(FnNeuron::reflex("triage", |signal, _| {
129//!         let text = signal.payload.as_text()?.to_string();
130//!         text.contains("ALERT")
131//!             .then(|| signal.command("alert", json!({ "text": text })))
132//!     }));
133//!     b.effector(Webhook::new("alert", "http://127.0.0.1:9000/hook")?);
134//!     b.reflex(events, triage);
135//!     Server::new().port(3000).nerve(b).serve().await
136//! }
137//! ```
138//!
139//! See [docs/NERVE.md](https://github.com/Gilfeather/furnace/blob/main/docs/NERVE.md) for
140//! habituation, thresholds, deliberative pathways and the brain.
141//!
142//! # Configuration
143//!
144//! The common settings have their own methods: [`Server::host`], [`Server::port`],
145//! [`Server::max_batch_size`], [`Server::request_timeout`]. [`Server::configure`] reaches
146//! every [`ServerConfig`] setting: limits, timeouts, batching, gRPC (feature `grpc`), TLS
147//! (feature `tls`), API keys, rate limits, the admin token for [`Server::reloadable_model`].
148//! [`Server::router`] returns the HTTP API as an axum `Router` to mount in your own application. The HTTP API is described in
149//! [docs/API.md](https://github.com/Gilfeather/furnace/blob/main/docs/API.md).
150//!
151//! # Features
152//!
153//! None are on by default.
154//!
155//! - `grpc`: the Open Inference Protocol over gRPC as well.
156//! - `tls`: HTTPS and mutual TLS, with a pure-Rust crypto provider.
157//! - `image`: JPEG / PNG input on `/predict`, with ImageNet preprocessing.
158//! - `llm`: serve LLMs with the OpenAI chat and completions API, streaming included, next to
159//!   the other models; see the `llm` module.
160//! - `test-models`: `model::DummyModel` and `model::DummyTensorsModel`, constant models for
161//!   testing and benchmarking the server itself.
162
163pub mod access;
164pub mod api;
165pub mod batching;
166pub mod error;
167mod json_fast;
168#[cfg(feature = "llm")]
169pub mod llm;
170pub mod model;
171pub mod nerve;
172#[cfg(feature = "image")]
173pub mod preprocess;
174pub mod tensor;
175// RFC 3339 timestamps; public for furnace-burn's model metadata
176#[doc(hidden)]
177pub mod time;
178#[cfg(feature = "tls")]
179pub mod tls;
180
181pub use api::{Server, ServerConfig};
182pub use error::{ApiError, FurnaceError, ModelError, Result};
183pub use model::{Model, ModelInfo, TensorModel};
184pub use tensor::{DataType, HostTensor, IoSpec, TensorSpec, Values};
185
186// The Rust examples in the Markdown documents compile as doc-tests, so an API change that breaks
187// one fails CI. docs/NERVE.md uses the `llm` mock engine; README.md and examples/README.md use
188// Burn and compile in furnace-burn, docs/LLM.md uses furnace-candle and compiles there.
189#[cfg(all(doctest, feature = "llm"))]
190#[doc = include_str!("../docs/NERVE.md")]
191mod nerve_md {}
192#[cfg(doctest)]
193#[doc = include_str!("../docs/API.md")]
194mod api_md {}
195#[cfg(doctest)]
196#[doc = include_str!("../docs/MODELS.md")]
197mod models_md {}
198#[cfg(doctest)]
199#[doc = include_str!("../docs/PERFORMANCE.md")]
200mod performance_md {}
201#[cfg(doctest)]
202#[doc = include_str!("../docs/SERVERLESS.md")]
203mod serverless_md {}
204#[cfg(doctest)]
205#[doc = include_str!("../CONTRIBUTING.md")]
206mod contributing {}