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