Skip to main content

espdlx

Train models in PyTorch and export them to .espdl — the native model format for ESP32 boards.

espdlx is the Python training side: build models from espdlx.layers inside an espdlx.Model, train with your own PyTorch loop, then export via the standard path: torch.onnx.export -> esp-dl quantization (espdl_quantize_onnx -> .espdl).

Deployment is paired with the espdlx Arduino library (esp-dl port for ESP32-S3, installable from the Arduino Library Manager) for maximum ease of use: embed the .espdl file as a C array, load it with dl::Model, call run().

Installation

pip install espdlx
# with esp-dl quantization support:
pip install "espdlx[convert]"

Example

Simple CNN trained on MNIST:

import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

import espdlx
import espdlx.layers as L

device = torch.device("mps")

model = espdlx.Model(
    [
        L.Conv2d(1, 16, 3, padding=1), L.ReLU(),
        L.MaxPool2d(2),
        L.Conv2d(16, 32, 3, padding=1), L.ReLU(),
        L.MaxPool2d(2),
        L.Conv2d(32, 10, 1),
        L.Mean(),
        L.Flatten(),
    ],
    name="mnist_cnn",
).to(device)

train_ds = datasets.MNIST(root="./data", train=True, download=True, transform=transforms.ToTensor())
test_ds = datasets.MNIST(root="./data", train=False, download=True, transform=transforms.ToTensor())
train_ld = DataLoader(train_ds, batch_size=128, shuffle=True)
test_ld = DataLoader(test_ds, batch_size=512)

opt = torch.optim.Adam(model.parameters(), lr=1e-3)
model.train()
for epoch in range(3):
    for x, y in train_ld:
        x, y = x.to(device), y.to(device)
        opt.zero_grad()
        loss = F.cross_entropy(model(x), y)
        loss.backward()
        opt.step()

model.eval()
correct = total = 0
with torch.no_grad():
    for x, y in test_ld:
        correct += (model(x.to(device)).argmax(-1) == y.to(device)).sum().item()
        total += len(y)
print(f"test acc: {correct / total:.3f}")

Export to .espdl

Quantize a trained model to .espdl (needs the convert extra):

from torch.utils.data import DataLoader, TensorDataset
from espdlx.convert import convert

calib_loader = DataLoader(TensorDataset(calib_images), batch_size=8)

report = convert(
    model.cpu(),              # any nn.Module; espdlx.Model or plain torch
    example_input,            # dummy input for torch.onnx.export
    calib_loader,             # calibration batches
    "model.espdl",            # also writes model.onnx + espdl_report.json
    calib_steps=16,
)
print(report["espdl_bytes"], report["input_scale"], report["input_zero_point"])

Deploy with the espdlx Arduino library

Install espdlx from the Arduino Library Manager (ESP32-S3), then embed the exported .espdl file as a C header next to your sketch:

# model_data.h — generated from model.espdl (xxd-style, one line per 16 bytes)
xxd -i model.espdl > model_data.h

Run inference with dl::Model (pattern from the espdlx Arduino library examples):

#include <Arduino.h>
#include <espdlx.h>
#include "model_data.h";  // const unsigned char model_espdl[] = { 0x45, 0x44, ... };

dl::Model *model = nullptr;

void setup() {
  Serial.begin(115200);

  // Load the .espdl model straight from flash.
  model = new dl::Model((const char *)model_espdl,
                        fbs::MODEL_LOCATION_IN_FLASH_RODATA);

  auto inputs = model->get_inputs();
  auto outputs = model->get_outputs();
  dl::TensorBase *in = inputs.begin()->second;
  dl::TensorBase *out = outputs.begin()->second;
  Serial.printf("in_elems=%d out_elems=%d\n", in->size, out->size);

  memcpy(in->data, my_input, in->size);  // int8 NHWC, quantized with input_scale
  model->run();                          // or model->run(dl::RUNTIME_MODE_MULTI_CORE)

  float *logits = (float *)out->data;
  int pred = 0;
  for (int i = 1; i < out->size; i++)
    if (logits[i] > logits[pred]) pred = i;
  Serial.printf("pred=%d\n", pred);
}

void loop() { delay(1000); }

The library also ships a SelfTest example (examples/SelfTest/SelfTest.ino) that exercises core + vision + audio through the single espdlx.h header:

#include <Arduino.h>
#include <espdlx.h>

void setup() {
  Serial.begin(115200);
  float data[4] = {1.0f, 2.0f, 3.0f, 4.0f};
  dl::TensorBase t({4}, data, 0, dl::DATA_TYPE_FLOAT, true);
  dl::math::softmax((float *)t.data, 4);  // core check via espdlx.h
}

void loop() { delay(5000); }

Model zoo

Prebuilt architectures (espdlx/zoo.py, all residual-free variants — no runtime tensor Add on-device, so skip connections are omitted):

from espdlx.zoo import MobileNetV2, MobileNetV1, VGG, DSCNN

model = MobileNetV2.Slim(num_classes=5)  # flowers, 128px RGB
model = MobileNetV2.Base(num_classes=32) # CIFAR-100 pretrain backbone, 32px
model = MobileNetV2.Fomo()               # EXPERIMENTAL centroid detector, 96px
Model Input Params MMACs .espdl Sketch flash PSRAM ctx S3 latency single / multi
MobileNetV2.Slim(5) 128px RGB 13,880 14.2 43.6 KB 1.20 MB 324 KB 87 ms / 73 ms
MobileNetV2.Base(5) 128px RGB 132,280 100.2 169 KB 1.33 MB 1.09 MB 243 ms / 256 ms
MobileNetV2.Base(32) 32px RGB 134,224 6.3 170 KB 1.28 MB 218 KB 15.0 ms / 14.9 ms
MobileNetV2.Fomo (experimental) 96px gray 6,498 2.3 34.5 KB 1.15 MB 159 KB 37.3 ms / 30.9 ms
MobileNetV1.Slim(10) 96px RGB 352,330 52.0 367 KB 1.51 MB 612 KB 446 ms / 256 ms
ResNet.BottleneckTiny(10) 96px RGB 1,305,258 104.6 1.27 MB 2.46 MB 1.53 MB 1045 ms / 811 ms
VGG.Small(10) 96px RGB 295,770 120.8 303 KB 1.44 MB 624 KB 716 ms / 638 ms
VGG.Stride(10) 96px RGB 1,219,450 174.2 1.18 MB 2.37 MB 1.53 MB 1570 ms / 1296 ms
VGG.Wide(10) 96px RGB 1,560,522 212.7 1.50 MB 2.70 MB 1.72 MB 3102 ms / 2501 ms
VGG.Base(10) 96px RGB 663,106 233.0 662 KB 1.81 MB 973 KB 2297 ms / 1780 ms
VGG.Large(10) 96px RGB 1,176,746 475.2 1.14 MB 2.32 MB 1.80 MB 4804 ms / 3560 ms
DSCNN.Tiny(2) 40x32 mel 968 0.8 6.8 KB 1.11 MB 66 KB 26.3 ms / 17.0 ms
DSCNN.Small(2) 40x98 mel 3,160 1.7 16.2 KB 1.12 MB 93 KB 16.0 ms / 14.2 ms
  • All device numbers measured 2026-09-12 on ESP32-S3 @ 240 MHz via esp-dl (.espdl w8a8, OPI PSRAM, huge_app, N=20 runs, micros() around model->run()).
  • .espdl from espdlx.convert after 1 epoch of synthetic-noise training per model; sizes and latency are weights-independent (accuracy on noise weights is meaningless, so no accuracy column).
  • PSRAM ctx = model context + working set (free_psram drop after load). Sketch flash is ~1.1 MB framework floor + model + test vector.
  • MobileNetV2.Fomo / espdlx.fomo are experimental: synthetic unit tests only, no on-device accuracy verification yet (latency above just proves it runs). Best real-data run so far: P=0.03/R=0.28 — not a working detector yet.

License

MIT — see LICENSE.

Metadata

Release files for espdlx 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for espdlx 0.1.0
File Size Uploaded Provenance Actions
espdlx-0.1.0.tar.gz 24.1 kB Without provenance Details for espdlx-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for espdlx 0.1.0
File Interpreter ABI Platform Actions
espdlx-0.1.0-py3-none-any.whl Python 3 none any Details for espdlx-0.1.0-py3-none-any.whl

Total release size: 44.4 kB

Release files / espdlx-0.1.0.tar.gz

Download URL espdlx-0.1.0.tar.gz
Size 24.1 kB
Tags Source
SHA-256 checksum
How to use checksums
389d9c993b50a342897130ae565110d537f1482898b64299d0cf82e590c60967
BLAKE2b-256 checksum
How to use checksums
b949a5776715389e979cd6deb581501126a6a1a79415e5de768d5ae836be483e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.5.9

Release files / espdlx-0.1.0-py3-none-any.whl

Download URL espdlx-0.1.0-py3-none-any.whl
Size 20.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9d10596d7ef2044b0953641715b28a2cd3af068a3ffa00a234bf6bacec843c42
BLAKE2b-256 checksum
How to use checksums
dd5e3376da60d132dc88671272e53cdcd93bbe1beeb25892993a50d5a2e3e1ac
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.5.9

Security for espdlx 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page