Introduction

Real‑time inference has moved from a research curiosity to a production necessity. From recommendation engines that must react within milliseconds to autonomous‑vehicle perception pipelines that process thousands of frames per second, the demand for low‑latency, high‑throughput model serving is relentless. Traditional approaches—Python‑centric stacks, monolithic REST services, or heavyweight Java frameworks—often hit scalability ceilings because they either:

  1. Introduce unnecessary runtime overhead (e.g., the Python Global Interpreter Lock, heavyweight garbage collection).
  2. Lack fine‑grained control over I/O, memory, and concurrency.
  3. Struggle with back‑pressure when upstream data rates spike.

Enter Rust, a systems‑level language that promises memory safety without a garbage collector, zero‑cost abstractions, and first‑class asynchronous programming. Coupled with modern asynchronous stream processing architectures (e.g., Tokio, async‑std, NATS, Apache Kafka), Rust becomes a compelling platform for building inference pipelines that can scale horizontally while maintaining deterministic latency.

This article dives deep into the why, what, and how of scaling real‑time inference with Rust. We’ll explore the challenges of real‑time model serving, examine Rust’s async ecosystem, walk through a complete example that stitches together data ingestion, preprocessing, model execution, and post‑processing, and finally discuss production‑grade scaling strategies, performance tuning, and deployment considerations.

Note: While the concepts apply to any ML framework, the examples focus on ONNX Runtime (a cross‑platform inference engine) because it offers a C API that can be called directly from Rust without the overhead of Python bindings.


1. Why Rust for Real‑Time Inference?

1.1 Predictable Performance

  • Zero‑cost abstractions: Rust’s compile‑time guarantees mean that abstractions such as iterators, async/await, and trait objects compile down to code that is as efficient as hand‑written C.
  • No garbage collector: Latency spikes caused by GC pauses are eliminated. Memory is reclaimed deterministically via ownership and lifetimes.
  • Fine‑grained control over allocation: You can allocate buffers on the stack, use Vec::with_capacity, or employ arena allocators for high‑throughput workloads.

1.2 Safety Without Sacrificing Speed

Rust’s borrow checker prevents data races at compile time, which is crucial when you are handling thousands of concurrent inference requests. The language forces you to think about ownership, borrowing, and lifetime, resulting in code that is both safe and fast.

1.3 A Mature Async Ecosystem

The Tokio runtime, async‑std, and smol provide robust, production‑grade async primitives:

  • Reactor pattern for non‑blocking I/O.
  • Task scheduling that can be tuned for CPU‑bound vs. I/O‑bound workloads.
  • Built‑in back‑pressure via futures::Stream and Sink traits.

These tools enable the construction of pipeline‑style architectures where each stage runs concurrently, yet the system respects flow control.

1.4 Interoperability with Existing Inference Engines

Rust can call into C/C++ libraries (e.g., ONNX Runtime, TensorRT) using unsafe FFI blocks. The overhead of crossing the language boundary is minimal compared to the cost of model execution, and Rust’s type system can wrap those unsafe calls in safe abstractions.


2. Real‑Time Inference Challenges

ChallengeImpact on SystemTypical Mitigation
High ConcurrencyThousands of simultaneous requests can saturate CPU cores and memory bandwidth.Use async I/O, non‑blocking networking, and multi‑threaded runtimes.
Variable Input Rate (Burstiness)Sudden spikes overload downstream stages, causing queue build‑up and latency spikes.Implement back‑pressure, rate limiting, and auto‑scaling.
Model Loading OverheadLoading a model per request is prohibitive.Keep models in memory, use shared inference sessions.
Cold‑Start LatencyFirst inference after a cold start can be orders of magnitude slower.Warm‑up pipelines, keep workers warm, use lazy loading with caching.
Hardware HeterogeneityCPUs, GPUs, TPUs may be mixed across nodes.Abstract hardware via a trait, schedule tasks based on capability.
ObservabilityHard to pinpoint latency contributors without fine‑grained metrics.Export tracing spans, Prometheus metrics, and structured logs.

Understanding these pain points informs the architectural decisions we’ll make later.


3. Foundations of Asynchronous Stream Processing

3.1 The Stream‑Sink Model

In Rust’s async world, a Stream is an asynchronous iterator (poll_next) that yields items over time, while a Sink consumes items (poll_ready + start_send). Connecting a Stream to a Sink creates a pipeline where each stage can apply transformation, filtering, or side‑effects.

use futures::{StreamExt, SinkExt};

async fn pipeline<S, Si>(mut src: S, mut sink: Si)
where
    S: Stream<Item = InferenceRequest> + Unpin,
    Si: Sink<InferenceResponse> + Unpin,
{
    while let Some(req) = src.next().await {
        let resp = process(req).await;
        sink.send(resp).await.unwrap();
    }
}

3.2 Back‑Pressure

Back‑pressure propagates upstream when a downstream Sink cannot keep up. In the Tokio ecosystem, this is handled automatically: send on a Sink will await until the sink is ready, preventing uncontrolled memory growth.

3.3 Parallelism vs. Concurrency

  • Concurrency (via async) enables many tasks to be interleaved on a few threads.
  • Parallelism (via thread pools) allows CPU‑bound work (e.g., model inference) to run simultaneously on multiple cores.

A typical design uses async I/O for networking and preprocessing, then dispatches inference to a bounded thread pool (rayon, tokio::task::spawn_blocking, or a custom ThreadPool). This hybrid model maximizes CPU utilization while keeping latency low.


4. Rust Async Ecosystem Primer

LibraryPrimary UseNotable Features
TokioFull‑featured async runtimeMulti‑threaded scheduler, TCP/UDP, timers, TcpListener, TcpStream, mpsc, broadcast.
async‑stdSimpler API, mimics std libasync equivalents of fs, net, task.
smolMinimalist runtimeWorks well inside other runtimes, tiny binary size.
towerComposable services & middlewareCommon in gRPC (tonic) and HTTP (hyper) stacks.
tonicgRPC over HTTP/2Async, protobuf‑generated services, built on Tokio & tower.
tracingStructured async‑aware loggingSpans propagate across async boundaries.
prometheusMetrics expositionCounter, gauge, histogram types.

For the rest of this article we’ll use Tokio as the runtime, tonic for gRPC transport (a popular choice for model serving), and tracing for observability.


5. Building a High‑Throughput Inference Pipeline

We’ll construct a four‑stage pipeline:

  1. Ingress – gRPC endpoint receives InferenceRequests.
  2. Pre‑processing – Decode input (e.g., protobuf, image bytes), reshape tensors.
  3. Model Execution – Call ONNX Runtime via a shared session.
  4. Post‑processing – Apply softmax, map class IDs, serialize response.

5.1 Project Layout

src/
├── main.rs               # Entry point, starts Tokio runtime
├── server.rs             # gRPC service implementation
├── preprocess.rs        # Input validation & tensor conversion
├── inference.rs          # ONNX Runtime wrapper
├── postprocess.rs        # Output formatting
└── metrics.rs            # Prometheus exporter

5.2 Defining the Protobuf API

syntax = "proto3";

package inference;

service InferenceService {
  rpc Predict (InferenceRequest) returns (InferenceResponse);
}

message InferenceRequest {
  // Binary representation of raw input (e.g., JPEG, raw audio)
  bytes payload = 1;
  // Optional metadata (e.g., model version)
  string model_id = 2;
}

message InferenceResponse {
  // Probability distribution over classes
  repeated float scores = 1;
  // Predicted class index
  uint32 label = 2;
}

Generate Rust code with tonic-build in build.rs.

5.3 Shared ONNX Runtime Session

ONNX Runtime is thread‑safe when you enable the ORT_ENABLE_THREADS=1 flag. We’ll create a singleton session that lives for the process lifetime.

// inference.rs
use onnxruntime::{environment::Environment, session::Session, GraphOptimizationLevel};
use std::sync::Arc;

pub struct Model {
    session: Arc<Session>,
}

impl Model {
    pub fn load(model_path: &str) -> anyhow::Result<Self> {
        // Create a shared environment (once per process)
        let env = Environment::builder()
            .with_name("rust-inference")
            .with_log_level(onnxruntime::LoggingLevel::Warning)
            .build()?;

        // Configure session options
        let mut sess_builder = env
            .new_session_builder()?
            .with_optimization_level(GraphOptimizationLevel::Basic)?
            .with_number_threads(num_cpus::get() as i32)?;

        // Load the model
        let session = sess_builder.with_model_from_file(model_path)?;

        Ok(Self {
            session: Arc::new(session),
        })
    }

    /// Run inference on a single input tensor.
    pub fn infer(&self, input: Vec<f32>, dims: &[usize]) -> anyhow::Result<Vec<f32>> {
        // Convert input into a ndarray tensor
        let input_tensor = ndarray::Array::from_shape_vec(dims, input)?;

        // Prepare input & output names (assumes single I/O)
        let input_name = self.session.inputs[0].name.clone();
        let output_name = self.session.outputs[0].name.clone();

        // Run the session (blocking, offloaded later)
        let outputs = self.session.run(vec![(input_name.as_str(), &input_tensor)])?;

        // Extract the first output tensor
        let output_tensor = outputs[0].try_extract::<f32>()?;
        Ok(output_tensor.to_vec())
    }
}

The infer method is blocking, because ONNX Runtime performs CPU‑bound work. We will offload it to a dedicated thread pool to avoid starving the async runtime.

5.4 Offloading to a Blocking Thread Pool

// inference.rs (continued)
use tokio::task::JoinHandle;

pub struct InferenceWorker {
    model: Arc<Model>,
    // Optional: a bounded semaphore to limit concurrent inferences
    semaphore: Arc<tokio::sync::Semaphore>,
}

impl InferenceWorker {
    pub fn new(model: Arc<Model>, max_concurrent: usize) -> Self {
        Self {
            model,
            semaphore: Arc::new(tokio::sync::Semaphore::new(max_concurrent)),
        }
    }

    /// Public async API – spawns a blocking task.
    pub async fn predict(&self, input: Vec<f32>, dims: &[usize]) -> anyhow::Result<Vec<f32>> {
        // Acquire a permit (back‑pressure)
        let permit = self.semaphore.acquire().await?;
        let model = self.model.clone();

        // Offload the actual inference to a blocking thread
        let handle: JoinHandle<anyhow::Result<Vec<f32>>> = tokio::task::spawn_blocking(move || {
            // Permit is dropped when the future completes
            let _guard = permit;
            model.infer(input, dims)
        });

        // Await the result
        handle.await?
    }
}

The semaphore caps concurrent inference calls, preventing the system from queuing more work than the hardware can handle.

5.5 Pre‑Processing Example (Image Decoding)

// preprocess.rs
use image::io::Reader as ImageReader;
use image::DynamicImage;

/// Decode JPEG bytes into a normalized Float32 tensor (NCHW)
pub fn preprocess_image(payload: &[u8]) -> anyhow::Result<(Vec<f32>, Vec<usize>)> {
    // Decode image (uses libjpeg under the hood)
    let img = ImageReader::new(std::io::Cursor::new(payload))
        .with_guessed_format()?
        .decode()?;

    // Resize to model's expected size (e.g., 224x224)
    let resized = img.resize_exact(224, 224, image::imageops::FilterType::Triangle);

    // Convert to RGB if needed
    let rgb = match resized {
        DynamicImage::ImageRgb8(i) => i,
        other => other.to_rgb8(),
    };

    // Normalize (0‑1) and convert to CHW layout
    let mut tensor = Vec::with_capacity(3 * 224 * 224);
    for c in 0..3 {
        for y in 0..224 {
            for x in 0..224 {
                let pixel = rgb.get_pixel(x, y);
                let value = pixel[c] as f32 / 255.0;
                tensor.push(value);
            }
        }
    }

    // Shape: [1, 3, 224, 224] (batch, channel, height, width)
    Ok((tensor, vec![1, 3, 224, 224]))
}

The function returns both the flattened tensor and its shape vector, ready for ONNX Runtime.

5.6 Post‑Processing (Softmax & Argmax)

// postprocess.rs
pub fn softmax(logits: &[f32]) -> Vec<f32> {
    let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
    let exp_sum: f32 = logits.iter().map(|v| (*v - max).exp()).sum();
    logits.iter().map(|v| (*v - max).exp() / exp_sum).collect()
}

pub fn argmax(scores: &[f32]) -> usize {
    scores
        .iter()
        .enumerate()
        .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
        .map(|(idx, _)| idx)
        .unwrap_or(0)
}

5.7 gRPC Service Implementation

// server.rs
use tonic::{Request, Response, Status};
use inference::inference_service_server::InferenceService;
use inference::{InferenceRequest, InferenceResponse};

use crate::{preprocess, postprocess, inference::InferenceWorker};

pub struct InferenceServer {
    worker: InferenceWorker,
}

#[tonic::async_trait]
impl InferenceService for InferenceServer {
    async fn predict(
        &self,
        request: Request<InferenceRequest>,
    ) -> Result<Response<InferenceResponse>, Status> {
        // 1️⃣ Extract payload
        let payload = request.into_inner().payload;

        // 2️⃣ Pre‑process
        let (tensor, shape) = preprocess::preprocess_image(&payload)
            .map_err(|e| Status::invalid_argument(format!("preprocess error: {}", e)))?;

        // 3️⃣ Run inference (async, back‑pressured)
        let logits = self
            .worker
            .predict(tensor, &shape)
            .await
            .map_err(|e| Status::internal(format!("inference error: {}", e)))?;

        // 4️⃣ Post‑process
        let scores = postprocess::softmax(&logits);
        let label = postprocess::argmax(&scores) as u32;

        // 5️⃣ Build response
        let resp = InferenceResponse {
            scores,
            label,
        };
        Ok(Response::new(resp))
    }
}

5.8 Main Entrypoint & Server Startup

// main.rs
use inference::inference_service_server::InferenceServiceServer;
use inference::inference_server::InferenceServer;
use std::{net::SocketAddr, sync::Arc};
use tokio::signal;

mod inference;
mod preprocess;
mod postprocess;
mod server;
mod metrics;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Initialize tracing subscriber (JSON logs for observability)
    tracing_subscriber::fmt()
        .json()
        .with_max_level(tracing::Level::INFO)
        .init();

    // Load the model once (shared across all requests)
    let model = Arc::new(inference::Model::load("models/resnet50.onnx")?);
    let worker = inference::InferenceWorker::new(model, /*max_concurrent=*/ 8);

    // Start Prometheus endpoint
    let metrics_handle = tokio::spawn(metrics::run_metrics_server());

    // Build gRPC server
    let svc = InferenceServiceServer::new(InferenceServer { worker });
    let addr: SocketAddr = "[::1]:50051".parse()?;
    tracing::info!("gRPC server listening on {}", addr);

    // Run server with graceful shutdown
    tonic::transport::Server::builder()
        .add_service(svc)
        .serve_with_shutdown(addr, async {
            signal::ctrl_c().await.expect("failed to install Ctrl+C handler");
            tracing::info!("Shutdown signal received");
        })
        .await?;

    // Wait for metrics server to finish
    metrics_handle.abort(); // optional cleanup
    Ok(())
}

The example demonstrates a complete end‑to‑end pipeline that:

  • Receives requests over gRPC (efficient binary transport).
  • Processes inputs asynchronously while respecting back‑pressure.
  • Offloads heavy inference to a bounded thread pool.
  • Exports metrics and logs for observability.

6. Scaling Strategies

6.1 Horizontal Scaling with Load Balancers

Deploy multiple instances of the service behind a Layer‑4 (TCP) load balancer (e.g., Envoy, HAProxy) or a gRPC‑aware load balancer (e.g., Istio). Because the service is stateless apart from the shared in‑memory model, scaling out is straightforward.

6.1.1 Sticky Sessions vs. Stateless

  • Stateless: Each replica loads the model independently; memory usage scales linearly but you get true fault tolerance.
  • Sticky: Use a shared memory segment (e.g., memfd on Linux) to keep a single model copy across processes. This is advanced and often unnecessary when you have enough RAM.

6.2 Autoscaling on Kubernetes

Define a Horizontal Pod Autoscaler (HPA) based on custom metrics such as request latency or CPU utilization. Export these metrics via Prometheus and use the Prometheus Adapter to feed HPA.

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: inference-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: inference-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Pods
    pods:
      metric:
        name: inference_latency_seconds
      target:
        type: AverageValue
        averageValue: 0.050 # 50ms target

6.3 Batching Requests

Batching multiple inference inputs into a single ONNX Runtime call can dramatically improve throughput on CPUs and GPUs. Implement a batcher that collects requests for up to N milliseconds or M items, whichever comes first.

// pseudo-code
async fn batcher(mut rx: mpsc::Receiver<BatchItem>) {
    let mut pending = Vec::new();
    loop {
        // Wait for first item or timeout
        let first = tokio::select! {
            Some(item) = rx.recv() => item,
            _ = tokio::time::sleep(Duration::from_millis(2)) => continue,
        };
        pending.push(first);
        // Pull more items without blocking
        while let Ok(item) = rx.try_recv() {
            pending.push(item);
            if pending.len() >= MAX_BATCH_SIZE {
                break;
            }
        }
        // Run batched inference
        let results = model.batch_predict(pending).await;
        // Send back individual responses
        for (item, result) in pending.into_iter().zip(results) {
            let _ = item.responder.send(result);
        }
        pending = Vec::new();
    }
}

Batching is especially effective on GPU where kernel launch overhead dominates.

6.4 Zero‑Copy Data Paths

Avoid copying payloads between stages:

  • Use Arc<[u8]> for request payloads; clones are cheap (just bump the ref count).
  • Pass bytes::Bytes objects which internally share memory.
  • When interfacing with ONNX Runtime, use OrtValue::CreateTensorFromMemory with pre‑allocated buffers if the API permits.

Zero‑copy reduces GC pressure (irrelevant in Rust) and improves cache locality.

6.5 CPU Pinning & NUMA Awareness

On multi‑socket machines, pin inference worker threads to specific cores and allocate model buffers in the corresponding NUMA node. Tokio’s Builder::worker_threads lets you set the number of runtime worker threads, and you can use the numa crate to set thread affinity.

tokio::runtime::Builder::new_multi_thread()
    .worker_threads(num_cpus::get_physical())
    .thread_name("tokio-worker")
    .enable_all()
    .build()?;

6.6 Monitoring & Alerting

  • Latency histograms: Export inference_latency_seconds with buckets [0.001, 0.005, 0.01, 0.05, 0.1, 0.5].
  • Error counters: inference_errors_total labeled by error type (e.g., preprocess, runtime).
  • Resource usage: CPU, memory, and GPU utilization via Node Exporter or NVIDIA DCGM.

Set alerts when p99 latency exceeds SLA or when queue depth (pending requests) grows beyond a threshold.


7. Performance Tuning Checklist

AreaTipExpected Impact
Async RuntimeUse Tokio’s multi‑threaded scheduler; avoid current_thread for CPU‑bound workloads.Better core utilization.
Thread Pool SizeSet InferenceWorker semaphore to ~80 % of logical cores; keep a few cores free for I/O.Reduces contention, improves latency.
Batch SizeExperiment with batch sizes 8‑32 for CPUs, 64‑256 for GPUs.Increases throughput, may increase latency (balance).
Memory AllocationPre‑allocate tensors with Vec::with_capacity and reuse buffers via a pool (bb8, deadpool).Reduces allocator pressure.
Model OptimizationsEnable ONNX Runtime graph optimizations, operator fusion, FP16 if hardware supports.Faster per‑inference compute.
Cache LocalityKeep model weights and input buffers NUMA‑local to the worker threads.Lower memory latency.
Network StackUse gRPC over HTTP/2 with tonic’s built‑in compression (gzip) for large payloads.Lower bandwidth usage, marginal latency gain.
Back‑PressureTune semaphore permits and queue lengths; monitor queue depth.Prevent overload spikes.
ObservabilityEmit trace spans (tracing::instrument) for each stage; visualize with Jaeger.Faster root‑cause analysis.

8. Deploying to Production

8.1 Containerization

Compile with musl for a minimal static binary:

cargo build --release --target x86_64-unknown-linux-musl

Dockerfile:

FROM scratch
COPY target/x86_64-unknown-linux-musl/release/inference-service /usr/local/bin/inference-service
EXPOSE 50051 9090
ENTRYPOINT ["/usr/local/bin/inference-service"]

The resulting image is ~10 MB, ideal for fast rollouts.

8.2 Kubernetes Manifest (Simplified)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: inference-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: inference
  template:
    metadata:
      labels:
        app: inference
    spec:
      containers:
      - name: inference
        image: ghcr.io/yourorg/inference-service:latest
        ports:
        - containerPort: 50051
        - containerPort: 9090   # Prometheus metrics
        resources:
          limits:
            cpu: "2"
            memory: "4Gi"
          requests:
            cpu: "1"
            memory: "2Gi"
        env:
        - name: RUST_LOG
          value: "info"
---
apiVersion: v1
kind: Service
metadata:
  name: inference-service
spec:
  selector:
    app: inference
  ports:
  - name: grpc
    port: 50051
    targetPort: 50051
  - name: metrics
    port: 9090
    targetPort: 9090
  type: ClusterIP

Sidecar pattern: You could add a Envoy sidecar to handle TLS termination, retries, and request tracing.

8.3 Edge Deployment

For latency‑critical edge use‑cases (e.g., IoT gateways), compile for ARM64 and run the same binary on devices like the NVIDIA Jetson. The Rust binary’s small footprint and static linking make OTA updates trivial.


9. Advanced Topics

9.1 GPU Acceleration

ONNX Runtime supports CUDA and TensorRT execution providers. To integrate:

  1. Build ONNX Runtime with --use_cuda.
  2. In Rust, enable the cuda feature flag in the onnxruntime crate.
  3. Adjust the InferenceWorker to use a GPU‑specific thread pool (often a single thread per GPU due to driver constraints).

9.2 Model Versioning & A/B Testing

Expose a model_id field in the request and maintain a hash map of model sessions (Arc<HashMap<String, Arc<Model>>>). Use a router middleware to direct traffic based on percentages.

9.3 Server‑Side Streaming

For video analytics, you may want to stream inference results back to the client as frames are processed. Use gRPC server‑streaming:

rpc PredictStream (InferenceRequest) returns (stream InferenceResponse);

In Rust, the handler returns a Pin<Box<dyn Stream<Item = Result<InferenceResponse, Status>> + Send>>. The same back‑pressure mechanisms apply.

9.4 Fault Isolation with Process Sandboxing

If you need to run untrusted models (e.g., user‑provided ONNX files), consider containerizing each model or using Firecracker microVMs. Rust’s small binary size makes it cheap to spin up isolated workers on demand.


10. Benchmarking Results (Sample)

ConfigurationThroughput (req/s)P95 Latency (ms)CPU Utilization
1x CPU (8 cores), batch = 12,8001285 %
1x CPU (8 cores), batch = 167,5002895 %
2x CPU (16 cores total), batch = 15,5001380 %
1x GPU (Tesla T4), batch = 3218,0002270 % GPU
Edge ARM (4 cores, batch = 1)1,2001590 %

Numbers are illustrative; actual performance depends on model size, input dimensions, and hardware.

Key takeaways:

  • Batching dramatically boosts throughput on both CPU and GPU.
  • Horizontal scaling yields near‑linear throughput increase when the workload is I/O‑bound or lightly CPU‑bound.
  • GPU shines when the batch is large enough to amortize kernel launch overhead.

Conclusion

Scaling real‑time inference is no longer the exclusive domain of heavyweight Java or Python ecosystems. Rust, with its blend of safety, zero‑cost abstractions, and a mature async runtime, offers a compelling foundation for building low‑latency, high‑throughput model serving pipelines. By:

  1. Structuring the service as an asynchronous stream processing pipeline,
  2. Offloading heavy model execution to a bounded thread pool,
  3. Applying back‑pressure, batching, and zero‑copy techniques,
  4. Leveraging modern orchestration tools (Kubernetes, Prometheus, Envoy),

you can achieve sub‑50 ms latency at tens of thousands of requests per second, all while keeping the binary footprint small and the codebase maintainable.

The example code presented here serves as a starting point. In production, you’ll iterate on batch sizes, hardware‑specific optimizations, and observability pipelines, but the core architectural principles remain the same: asynchronous, back‑pressured streams + Rust’s performance guarantees = scalable real‑time inference.

Happy coding, and may your inference pipelines be ever fast and reliable!


Resources