Skip to main content

TinyML inference engine for embedded devices โ€” Rust no_std core with Python bindings and quantization utilities

Project description

๐Ÿง  NANO-RUST-AI

TinyML Inference Engine โ€” Train in PyTorch, Run on Microcontrollers

PyPI Version License: MIT

Train (PyTorch, GPU) โ†’ Quantize (float32 โ†’ int8) โ†’ Verify (Python) โ†’ Deploy (ESP32/STM32)

๐Ÿ“ฆ Installation

pip install nano-rust-py

That's it. No Rust toolchain needed for using the library. Includes both the Rust inference engine and Python quantization utilities.

import nano_rust_py
print(nano_rust_py.__name__)  # โ†’ "nano_rust_py"

For development (modifying Rust source): see Development Setup below.


๐Ÿš€ Quick Start โ€” 3-Step Example

import nano_rust_py

# Step 1: Create model (input: 4 features, arena: 4KB scratch memory)
model = nano_rust_py.PySequentialModel(input_shape=[4], arena_size=4096)

# Step 2: Add layers with i8 weights
#   Dense layer: 4 inputs โ†’ 3 outputs
#   weights = [4ร—3] matrix flattened, bias = [3] vector
model.add_dense(
    weights=[10, -5, 3, 7, -2, 8, -4, 6, 1, 5, -3, 9],  # 4ร—3 = 12 values
    bias=[1, -1, 2]                                        # 3 values
)
model.add_relu()

# Step 3: Run inference
input_data = [100, -50, 30, 70]  # i8 values: [-128, 127]
output = model.forward(input_data)
print(output)  # โ†’ [15, 0, 22]  (i8 values after ReLU)

# Get predicted class
prediction = model.predict(input_data)
print(prediction)  # โ†’ 2  (argmax index)

๐Ÿ“– Complete Python API Reference

PySequentialModel โ€” The Core Model Class

Constructor

model = nano_rust_py.PySequentialModel(
    input_shape,   # List[int] โ€” shape of input tensor
    arena_size     # int โ€” scratch memory in bytes
)
Parameter Type Description
input_shape List[int] [N] for 1D, [C, H, W] for 3D (e.g., [1, 28, 28] for MNIST)
arena_size int Bytes for intermediate computation. Rule: 2 ร— largest_layer_output ร— sizeof(i8)
# 1D input (e.g., sensor features)
model = nano_rust_py.PySequentialModel([128], 4096)

# 3D input (e.g., MNIST image: 1 channel, 28ร—28)
model = nano_rust_py.PySequentialModel([1, 28, 28], 32768)

Layer Methods

add_dense(weights, bias) โ€” Fully-Connected Layer (Frozen)

Weights stored in Flash (0 bytes RAM). Uses simple requantization.

# 4 inputs โ†’ 2 outputs
model.add_dense(
    weights=[10, -5, 3, 7, -2, 8],  # flat [out ร— in] = [2 ร— 4] = 8 values
    bias=[1, -1]                     # [out] = 2 values
)
Parameter Type Shape Description
weights List[int] [out_features ร— in_features] i8 weight matrix, row-major
bias List[int] [out_features] i8 bias vector

Output: out[j] = clamp(ฮฃ(w[j,i] ร— x[i]) + bias[j]) requantized to i8


add_dense_with_requant(weights, bias, requant_m, requant_shift) โ€” Dense with Calibrated Requantization

For high-accuracy inference. Uses TFLite-style (acc ร— M) >> shift.

model.add_dense_with_requant(
    weights=[10, -5, 3, 7, -2, 8],
    bias=[1, -1],
    requant_m=1234,       # int32 multiplier (from calibration)
    requant_shift=15      # uint32 bit-shift (from calibration)
)
Parameter Type Description
requant_m int Fixed-point multiplier from calibrate_model()
requant_shift int Bit-shift from calibrate_model()

When to use: Always prefer this over add_dense() when you have calibration data. Accuracy improves from ~85% to ~97%.


add_conv2d(kernel, bias, in_ch, out_ch, kh, kw, stride, padding) โ€” 2D Convolution (Frozen)

# 1 input channel โ†’ 8 output channels, 3ร—3 kernel
model.add_conv2d(
    kernel=[...],   # [out_ch ร— in_ch ร— kh ร— kw] = 8ร—1ร—3ร—3 = 72 values
    bias=[...],     # [out_ch] = 8 values
    in_ch=1, out_ch=8, kh=3, kw=3,
    stride=1, padding=1
)
Parameter Type Description
kernel List[int] i8 kernel, shape [out_ch ร— in_ch ร— kh ร— kw], row-major
bias List[int] i8 bias, shape [out_ch]
in_ch int Input channels
out_ch int Output channels (number of filters)
kh, kw int Kernel height and width
stride int Stride (typically 1 or 2)
padding int Zero-padding (use kh // 2 to preserve spatial size)

Output shape: [out_ch, (H + 2*pad - kh) / stride + 1, (W + 2*pad - kw) / stride + 1]


add_conv2d_with_requant(kernel, bias, in_ch, out_ch, kh, kw, stride, padding, requant_m, requant_shift) โ€” Conv2D with Calibrated Requantization

Same as add_conv2d but with calibrated requantization for accuracy.

model.add_conv2d_with_requant(
    kernel=[...], bias=[...],
    in_ch=1, out_ch=8, kh=3, kw=3, stride=1, padding=1,
    requant_m=2048, requant_shift=14
)

add_trainable_dense(in_features, out_features) โ€” Trainable Layer (RAM)

Weights live in RAM (for on-device fine-tuning). Not for frozen inference.

model.add_trainable_dense(128, 10)  # 128 โ†’ 10, weights in RAM
Parameter Type Description
in_features int Input dimension
out_features int Output dimension

RAM cost: in_features ร— out_features + out_features bytes


Activation Layers

Method Formula When to use
add_relu() max(0, x) Default choice, fastest
add_sigmoid() 1 / (1 + e^(-x/16)) Binary classification, fixed-scale
add_sigmoid_scaled(mult, shift) Calibrated sigmoid LUT After calibration
add_tanh() tanh(x/32) Centered output [-1, 1], fixed-scale
add_tanh_scaled(mult, shift) Calibrated tanh LUT After calibration
add_softmax() Pseudo-softmax approximation Multi-class output (last layer)
# Simple (no calibration needed)
model.add_relu()

# Calibrated (from calibrate_model output)
model.add_sigmoid_scaled(scale_mult=42, scale_shift=8)
model.add_tanh_scaled(scale_mult=84, scale_shift=8)

Important: add_sigmoid() and add_tanh() use a fixed scale divisor (16 and 32 respectively). For best accuracy, use the _scaled variants with parameters from calibrate_model().


Structural Layers

Method Parameters Description
add_flatten() โ€” Reshape 3D [C,H,W] โ†’ 1D [Cร—Hร—W]. Use between conv and dense.
add_max_pool2d(kernel, stride, padding) int, int, int Reduce spatial dims by taking max over kernel window
model.add_max_pool2d(kernel=2, stride=2, padding=0)
# Input [8, 28, 28] โ†’ Output [8, 14, 14]

Inference Methods

model.forward(input_data) โ†’ List[int]

Run forward pass, get raw i8 output vector.

output = model.forward([100, -50, 30, 70])
print(output)  # โ†’ [15, -8, 22]  (raw i8 activations)

model.predict(input_data) โ†’ int

Run forward pass, get argmax class index.

class_id = model.predict([100, -50, 30, 70])
print(class_id)  # โ†’ 2

๐Ÿ”ง Python Utilities โ€” nano_rust_py.utils

All utilities are bundled in the PyPI package โ€” no need to clone the repo.

from nano_rust_py.utils import (
    quantize_to_i8,
    quantize_weights,
    calibrate_model,
    compute_requant_params,
    compute_activation_scale_params,
    export_to_rust,
    export_weights_bin,
)

Note: numpy is installed as a dependency. torch is only needed if you use quantize_weights() or calibrate_model() โ€” install with pip install nano-rust-py[train].

These utilities bridge PyTorch training and NANO-RUST inference.

quantize_to_i8(tensor, scale=127.0) โ†’ (np.ndarray, float)

Quantize any float32 tensor to i8 using symmetric linear scaling.

import numpy as np
from nano_rust_py.utils import quantize_to_i8


float_data = np.array([0.5, -0.3, 1.0, -1.0], dtype=np.float32)
q_data, scale = quantize_to_i8(float_data)
print(q_data)   # โ†’ [ 64, -38, 127, -127]
print(scale)    # โ†’ 0.00787  (max_abs / 127)

# To dequantize: float_value โ‰ˆ i8_value ร— scale
print(q_data[0] * scale)  # โ†’ 0.503 โ‰ˆ 0.5 โœ“

quantize_weights(model) โ†’ Dict

Walk a PyTorch model and quantize all weight tensors.

import torch.nn as nn
from nano_rust_py.utils import quantize_weights


model = nn.Sequential(
    nn.Linear(784, 128),
    nn.ReLU(),
    nn.Linear(128, 10),
)

q = quantize_weights(model)
# Returns: {
#   '0': {
#     'type': 'Linear',
#     'weights': np.ndarray (i8, shape [128, 784]),
#     'bias': np.ndarray (i8, shape [128]),
#     'weight_scale': 0.00312,
#     'bias_scale': 0.00156,
#     'params': {'in_features': 784, 'out_features': 128}
#   },
#   '2': {
#     'type': 'Linear',
#     'weights': np.ndarray (i8, shape [10, 128]),
#     ...
#   }
# }
# Note: ReLU (layer '1') has no weights, so it is skipped.

calibrate_model(model, input_tensor, q_weights, input_scale) โ†’ Dict

Run float model and compute per-layer requantization parameters.

from nano_rust_py.utils import calibrate_model, quantize_to_i8, quantize_weights


# 1. Quantize weights
q_weights = quantize_weights(model)

# 2. Prepare a representative input
sample_input = torch.randn(1, 784)
q_input, input_scale = quantize_to_i8(sample_input.numpy().flatten())

# 3. Calibrate
cal = calibrate_model(model, sample_input, q_weights, input_scale)
# Returns: {
#   '0': (requant_m=1234, requant_shift=15, bias_corrected=[...]),
#   '2': (requant_m=5678, requant_shift=14, bias_corrected=[...]),
# }

Why calibrate? Without calibration, the library uses a generic shift = ceil(log2(k)) + 7 which is approximate. Calibration computes the exact scale ratio between input, weights, and output โ€” raising accuracy from ~85% to 95-99%.


compute_requant_params(input_scale, weight_scale, output_scale) โ†’ (int, int)

Compute TFLite-style fixed-point multiplier and shift.

from nano_rust_py.utils import compute_requant_params


M, shift = compute_requant_params(
    input_scale=0.00787,    # from quantize_to_i8(input)
    weight_scale=0.00312,   # from quantize_weights(model)
    output_scale=0.00450    # from quantize_to_i8(expected_output)
)
print(M, shift)  # โ†’ (1407, 15)
# Meaning: output_i8 โ‰ˆ (accumulator ร— 1407) >> 15

export_to_rust(model, model_name, input_shape) โ†’ str

Generate complete Rust source code for the model weights and builder function.

from nano_rust_py.utils import export_to_rust


rust_code = export_to_rust(model, "digit_classifier", input_shape=[1, 28, 28])
with open("generated/digit_classifier.rs", "w") as f:
    f.write(rust_code)

Output file contains:

// Auto-generated by nano_rust_utils
static LAYER_0_W: &[i8] = &[10, -5, 3, ...];
static LAYER_0_B: &[i8] = &[1, -1, ...];

pub fn build_digit_classifier() -> SequentialModel<'static> {
    let mut model = SequentialModel::new();
    model.add(Box::new(FrozenDense::new_with_requant(
        LAYER_0_W, LAYER_0_B, 784, 128, 1234, 15
    ).unwrap()));
    model.add(Box::new(ReLULayer));
    // ...
    model
}

export_weights_bin(q_weights, output_dir) โ†’ List[Path]

Export quantized weights to binary files for include_bytes! in Rust.

from nano_rust_py.utils import export_weights_bin


paths = export_weights_bin(q_weights, "output/")
# Creates:
#   output/0_w.bin  (128 ร— 784 = 100,352 bytes)
#   output/0_b.bin  (128 bytes)
#   output/2_w.bin  (10 ร— 128 = 1,280 bytes)
#   output/2_b.bin  (10 bytes)

๐Ÿ““ Notebooks โ€” Learning Guide

Prerequisites

pip install nano-rust-py numpy torch torchvision ipykernel

Open notebooks in Jupyter/VS Code and select your venv kernel.

Validation Notebooks (notebooks/)

# Notebook What You'll Learn
01 01_pipeline_validation Full pipeline: Convโ†’ReLUโ†’Flattenโ†’Dense. Bit-exact comparison between float32 and i8.
02 02_mlp_classification Denseโ†’ReLUโ†’Dense (MLP). Manual weight quantization and verification.
03 03_deep_cnn Deep CNN with Convโ†’ReLUโ†’MaxPool stacking. Memory estimation for MCU.
04 04_activation_functions Side-by-side comparison: ReLU vs Sigmoid vs Tanh. Fixed vs scaled modes.
05 05_transfer_learning Frozen backbone (Flash) + trainable head (RAM). Hybrid memory pattern.

Real-World Test Scripts (notebooks-for-test/)

Each script follows the full workflow:
Train (GPU) โ†’ Quantize โ†’ Calibrate โ†’ Build NANO Model โ†’ Verify Accuracy

# Script Task Training Data Accuracy
06 run_06_mnist.py Digit classification MNIST (28ร—28) ~97%
07 run_07_fashion.py Fashion item recognition Fashion-MNIST (28ร—28) ~87%
08 run_08_sensor.py Industrial anomaly detection Synthetic sensor data ~98%
09 run_09_keyword_spotting.py Voice keyword detection Synthetic MFCC features ~79%
10 run_10_text_classifier.py Text sentiment analysis Bag-of-words features 100%

Run any script:

python notebooks-for-test/run_06_mnist.py

๐Ÿ—๏ธ The Complete Workflow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  STEP 1: Train in PyTorch (PC/GPU)                              โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                          โ”‚
โ”‚  โ€ข Define nn.Sequential model                                   โ”‚
โ”‚  โ€ข Train on dataset (MNIST, sensor data, audio, etc.)           โ”‚
โ”‚  โ€ข Achieve desired float32 accuracy                             โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  STEP 2: Quantize & Calibrate (Python)                          โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                          โ”‚
โ”‚  โ€ข quantize_weights(model) โ†’ i8 weights + scales               โ”‚
โ”‚  โ€ข calibrate_model() โ†’ requant_m, requant_shift per layer       โ”‚
โ”‚  โ€ข Memory shrinks 4ร— (float32 โ†’ int8)                          โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  STEP 3: Build NANO Model & Verify (Python)                     โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                          โ”‚
โ”‚  โ€ข Create PySequentialModel with i8 weights                     โ”‚
โ”‚  โ€ข Run same test inputs โ†’ compare with PyTorch                  โ”‚
โ”‚  โ€ข Verify accuracy loss < 5% (typically < 2%)                   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  STEP 4: Export to Rust (Python)                                โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                          โ”‚
โ”‚  โ€ข export_to_rust(model, "my_model") โ†’ .rs file                โ”‚
โ”‚  โ€ข Contains: static weight arrays + builder function            โ”‚
โ”‚  โ€ข Or export_weights_bin() โ†’ .bin files for include_bytes!      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  STEP 5: Deploy to MCU (Rust)                                   โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                          โ”‚
โ”‚  โ€ข include!("my_model.rs") in firmware                          โ”‚
โ”‚  โ€ข Allocate arena buffer (stack/static)                         โ”‚
โ”‚  โ€ข Read sensor โ†’ quantize input โ†’ inference โ†’ action            โ”‚
โ”‚  โ€ข See examples/esp32_deploy.rs                                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ—๏ธ Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Python (PyTorch + nano_rust_utils)  โ”‚  โ† Train & Quantize
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  PyO3 Binding (nano_rust_py)         โ”‚  โ† Bridge
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Rust Core (nano-rust-core)          โ”‚  โ† Inference Engine
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚ math.rsโ”‚ โ”‚layers/ โ”‚ โ”‚arena.rs โ”‚  โ”‚
โ”‚  โ”‚ matmul โ”‚ โ”‚dense   โ”‚ โ”‚bump ptr โ”‚  โ”‚
โ”‚  โ”‚ conv2d โ”‚ โ”‚conv    โ”‚ โ”‚ckpt/rst โ”‚  โ”‚
โ”‚  โ”‚ relu   โ”‚ โ”‚pool    โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚  โ”‚sigmoid โ”‚ โ”‚flatten โ”‚              โ”‚
โ”‚  โ”‚ tanh   โ”‚ โ”‚activateโ”‚              โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Memory Layout on MCU

FLASH (read-only)                RAM (read-write)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Frozen weights      โ”‚          โ”‚ Arena Buffer      โ”‚
โ”‚ - Conv2D kernels    โ”‚          โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚ - Dense weights     โ”‚          โ”‚ โ”‚ Intermediate โ”‚  โ”‚
โ”‚ - Bias arrays       โ”‚          โ”‚ โ”‚ activations  โ”‚  โ”‚
โ”‚ (.rs static arrays) โ”‚          โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค  โ”‚
โ”‚                     โ”‚          โ”‚ โ”‚ Trainable    โ”‚  โ”‚
โ”‚ Cost: N bytes       โ”‚          โ”‚ โ”‚ head weights โ”‚  โ”‚
โ”‚ RAM cost: 0 bytes   โ”‚          โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Memory Budget Rules

Component Formula Example (MNIST MLP)
Frozen weights ฮฃ(in ร— out) per dense + ฮฃ(in_ch ร— out_ch ร— kh ร— kw) per conv 100KB Flash
Arena buffer 2 ร— max(layer_output_size) 2 ร— 784 = 1.6KB RAM
Bias arrays ฮฃ(out_features) per layer 138 bytes Flash
Trainable head (if any) in ร— out + out 1.3KB RAM

ESP32 budget: 4MB Flash, 520KB RAM. A typical model uses <100KB Flash + <20KB RAM.


๐Ÿš€ ESP32 Deployment

See the complete examples:

Quick Summary

# Python: export model
from nano_rust_utils import quantize_weights, calibrate_model, export_to_rust

rust_code = export_to_rust(trained_model, "my_model", input_shape=[416])
with open("src/model.rs", "w") as f:
    f.write(rust_code)
// Rust firmware: use exported model
#![no_std]
include!("model.rs");

let mut arena_buf = [0u8; 16384];
let mut arena = Arena::new(&mut arena_buf);
let model = build_my_model();
let (output, _) = model.forward(&input_i8, &[416], &mut arena).unwrap();
let class = nano_rust_core::math::argmax_i8(output);

๐Ÿ”ง Rust Core API (for Firmware Developers)

Layers

use nano_rust_core::layers::*;

// Frozen layers (weights in Flash โ€” 0 bytes RAM)
let dense = FrozenDense::new_with_requant(weights, bias, 784, 128, 1234, 15)?;
let conv = FrozenConv2D::new_with_requant(kernel, bias, 1, 8, 3, 3, 1, 1, 2048, 14)?;

// Trainable layer (weights in RAM โ€” for fine-tuning)
let head = TrainableDense::new(128, 10);

// Activations
let _ = ReLULayer;
let _ = ScaledSigmoidLayer { scale_mult: 42, scale_shift: 8 };
let _ = ScaledTanhLayer { scale_mult: 84, scale_shift: 8 };
let _ = SoftmaxLayer;

// Structural
let _ = FlattenLayer;
let pool = MaxPool2DLayer::new(2, 2, 0)?;

Arena Allocator

use nano_rust_core::Arena;

let mut buf = [0u8; 32768];
let mut arena = Arena::new(&mut buf);

// Checkpoint/restore for scratch memory reuse
let cp = arena.checkpoint();
let scratch = arena.alloc_i8_slice(1024)?;
arena.restore(cp);  // reclaim scratch memory

Sequential Model

use nano_rust_core::model::SequentialModel;

let mut model = SequentialModel::new();
model.add(Box::new(dense));
model.add(Box::new(ReLULayer));
model.add(Box::new(dense2));

let (output, out_shape) = model.forward(input, &[784], &mut arena)?;
let class = nano_rust_core::math::argmax_i8(output);

๐Ÿ—‚๏ธ Project Structure

nano-rust/
โ”œโ”€โ”€ core/                       # Rust no_std core library
โ”‚   โ””โ”€โ”€ src/
โ”‚       โ”œโ”€โ”€ lib.rs              # Crate root & re-exports
โ”‚       โ”œโ”€โ”€ arena.rs            # Bump pointer allocator
โ”‚       โ”œโ”€โ”€ math.rs             # Quantized matmul, conv2d, activations
โ”‚       โ”œโ”€โ”€ error.rs            # NanoError, NanoResult
โ”‚       โ”œโ”€โ”€ model.rs            # SequentialModel (layer pipeline)
โ”‚       โ””โ”€โ”€ layers/
โ”‚           โ”œโ”€โ”€ mod.rs          # Layer trait + Shape struct
โ”‚           โ”œโ”€โ”€ dense.rs        # FrozenDense + TrainableDense
โ”‚           โ”œโ”€โ”€ conv.rs         # FrozenConv2D (im2col+matmul)
โ”‚           โ”œโ”€โ”€ activations.rs  # ReLU, Sigmoid, Tanh, Softmax (LUT)
โ”‚           โ”œโ”€โ”€ flatten.rs      # Flatten 3Dโ†’1D
โ”‚           โ””โ”€โ”€ pooling.rs      # MaxPool2D
โ”œโ”€โ”€ py_binding/                 # PyO3 Python bindings (compiled Rust)
โ”‚   โ””โ”€โ”€ src/lib.rs              # PySequentialModel wrapper
โ”œโ”€โ”€ python/                     # Pure Python modules (bundled in PyPI)
โ”‚   โ””โ”€โ”€ nano_rust_py/
โ”‚       โ”œโ”€โ”€ __init__.py         # Package init โ€” re-exports Rust types
โ”‚       โ””โ”€โ”€ utils.py            # Quantization, calibration, export tools
โ”œโ”€โ”€ scripts/                    # Standalone scripts (not in PyPI)
โ”‚   โ”œโ”€โ”€ nano_rust_utils.py      # Legacy utils (now in nano_rust_py.utils)
โ”‚   โ””โ”€โ”€ export.py               # CLI weight exporter
โ”œโ”€โ”€ notebooks/                  # Validation notebooks (01-05)
โ”œโ”€โ”€ notebooks-for-test/         # Real-world test scripts (06-10)
โ”œโ”€โ”€ examples/                   # ESP32 deployment examples
โ”œโ”€โ”€ generated/                  # Exported Rust weight files
โ”œโ”€โ”€ pyproject.toml              # pip/maturin build config
โ”œโ”€โ”€ Cargo.toml                  # Rust workspace config
โ”œโ”€โ”€ LICENSE                     # MIT
โ””โ”€โ”€ README.md

๐Ÿ› ๏ธ Development Setup

Only needed if you want to modify the Rust source code:

# 1. Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# or: winget install Rustlang.Rust.MSVC

# 2. Clone and setup
git clone https://github.com/LeeNim/nano-rust.git
cd nano-rust
python -m venv .venv
source .venv/bin/activate   # Linux/Mac
# .venv\Scripts\activate    # Windows

# 3. Install deps
pip install maturin numpy torch torchvision ipykernel

# 4. Build from source
# Windows: set CARGO_TARGET_DIR outside OneDrive!
$env:CARGO_TARGET_DIR = "$env:USERPROFILE\.nanorust_target"
maturin develop --release

# 5. Verify
python -c "import nano_rust_py; print('OK')"

๐Ÿ“œ License

MIT ยฉ 2026 Niem Le

๐Ÿ”ฎ Roadmap

  • v0.1.0: Core inference engine with scale-aware requantization
  • v0.2.0: Bundled Python utilities (nano_rust_py.utils) in PyPI package
  • v0.3.0: Const Generics refactor for compile-time optimization
  • v0.4.0: On-device training (backprop for trainable head)
  • v0.5.0: ARM SIMD intrinsics (SMLAD) for Cortex-M

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nano_rust_py-0.2.0.tar.gz (46.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nano_rust_py-0.2.0-cp39-cp39-win_amd64.whl (167.6 kB view details)

Uploaded CPython 3.9Windows x86-64

File details

Details for the file nano_rust_py-0.2.0.tar.gz.

File metadata

  • Download URL: nano_rust_py-0.2.0.tar.gz
  • Upload date:
  • Size: 46.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: maturin/1.12.3

File hashes

Hashes for nano_rust_py-0.2.0.tar.gz
Algorithm Hash digest
SHA256 966c9eabebafb8095994f9eb26b9cfb497900c43db483a66c86e1c3539a7b000
MD5 285ec7c8c0da6f4e8b160efb3b67fa62
BLAKE2b-256 c544738287acf6f56f4458d68ae6f74c2afecec386af4a2e7885ff807b60fa7f

See more details on using hashes here.

File details

Details for the file nano_rust_py-0.2.0-cp39-cp39-win_amd64.whl.

File metadata

File hashes

Hashes for nano_rust_py-0.2.0-cp39-cp39-win_amd64.whl
Algorithm Hash digest
SHA256 fab20839ad9301167bee22f9032a7fe0ef2caef78fd1c9282bc22d07779b6d13
MD5 eb096e4383ecc8058cea55ca31799083
BLAKE2b-256 fa93af23aa5933d4322eeeb246ab4e8fae090bf284b63ff1658ff0c542d1920b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page