netkit builds for ISA-qualified deployment profiles. Select one with NETKIT_TARGET when invoking Make or CMake, or define the matching -D flag in your embedded toolchain.
| Target | Makefile / CMake | Role | Default backends |
|---|---|---|---|
| CPU | NETKIT_TARGET=cpu (default) |
Desktop dev, debug, CI | XNNPACK on (any host ISA); CMSIS-NN off; no MLAS (not needed — see STATUS.md) |
| MCU_ARM | NETKIT_TARGET=mcu_arm |
Arm microcontroller firmware | CMSIS-NN (int8 production); XNNPACK forbidden |
| MPU_ARM | NETKIT_TARGET=mpu_arm |
Arm microprocessor / RTOS | XNNPACK |
| MCU_RISC | NETKIT_TARGET=mcu_risc |
Non-Espressif RISC-V MCU (Nuclei / RV32) | NMSIS-NN on (int8 production); float32 reference; CMSIS + XNNPACK + ESP-NN forbidden |
| MPU_RISC | NETKIT_TARGET=mpu_risc |
RISC-V MPU | XNNPACK on (strong RISC-V MPU support); CMSIS-NN forbidden |
| MCU_ESP | NETKIT_TARGET=mcu_esp |
Espressif MCU — Xtensa and RISC-V (ESP32 / S3 / C3 / C6 / P4) | ESP-NN on (int8 production); float32 reference; XNNPACK forbidden |
Legacy NETKIT_TARGET=mcu / mpu are rejected — use mcu_arm / mpu_arm.
Maturity: float32 + int8 are complete on cpu, Arm MCU/MPU, RISC MCU/MPU, and Espressif MCU. MCU_ARM uses CMSIS-NN; MCU_ESP uses ESP-NN; MCU_RISC uses NMSIS-NN — all CMSIS-style int8; ESP-NN / NMSIS-NN have no float API, so float32 uses reference — STATUS.md.
Target ≠ CPU ISA: NETKIT_TARGET follows vendor stack + NN backend, not the
instruction set alone. ESP32-C3 / C6 / P4 are RISC-V silicon but use mcu_esp +
ESP-NN (same as ESP32-S3). Use mcu_risc + NMSIS-NN only for non-Espressif
RISC-V MCUs. Several Espressif boards share one mcu_esp profile; each sets
NETKIT_ARCH (and its own boards/… tree). Full table:
PLATFORMS.md — Target ≠ CPU ISA.
Derived macros: NETKIT_CLASS_MCU / NETKIT_CLASS_MPU (firmware class), NETKIT_ISA_ARM / NETKIT_ISA_RISC / NETKIT_ISA_ESP (backend family).
Note: NETKIT_ISA_ESP applies to all Espressif chips under mcu_esp, including
RISC-V C3/C6/P4 — it does not mean “use mcu_risc.”
Per-device cookbooks (one section per target): PLATFORMS.md.
| System | Primary? | Notes |
|---|---|---|
| GNU Make | Yes | make, make test (primary; local + manual CI) |
| CMake | Optional | cmake -B cmake-build && cmake --build cmake-build — same flags, auto-detects desktop vs embedded via NETKIT_ARCH |
Both use the same optional backends and NETKIT_ARCH mapping (third_party/netkit_arch.mk, cmake/netkit_arch.cmake) — Arm ARM_MATH_* for CMSIS-NN, Espressif CONFIG_IDF_TARGET_* for ESP-NN, Nuclei/RISC-V tags for NMSIS-NN.
If NETKIT_ARCH is unset (empty), the build is a native desktop / CPU application — no core-specific ARM_MATH_* / ESP / RISC NN defines. Set NETKIT_ARCH when cross-compiling firmware for a specific Arm, Espressif, or RISC-V MCU core.
NETKIT_ARCH |
Flag(s) | Profile | Accelerated NN |
|---|---|---|---|
| (unset) | __GNUC_PYTHON__ (desktop host only) |
CPU | CMSIS / ESP off |
CM0, M0 |
ARM_MATH_CM0 |
MCU Arm | CMSIS-NN when NETKIT_CMSIS_NN=1 |
CM0PLUS, M0PLUS |
ARM_MATH_CM0PLUS |
MCU Arm | CMSIS-NN when NETKIT_CMSIS_NN=1 |
CM3, M3 |
ARM_MATH_CM3 |
MCU Arm | CMSIS-NN when NETKIT_CMSIS_NN=1 |
CM4, M4 |
ARM_MATH_CM4 |
MCU Arm | CMSIS-NN when NETKIT_CMSIS_NN=1 |
CM7, M7 |
ARM_MATH_CM7 |
MCU Arm | CMSIS-NN when NETKIT_CMSIS_NN=1 |
M23, CM23 |
ARM_MATH_ARMV8MBL |
MCU Arm | CMSIS-NN when NETKIT_CMSIS_NN=1 |
M33, CM33 |
ARM_MATH_ARMV8MML, __DSP_PRESENT=1 |
MCU Arm | CMSIS-NN when NETKIT_CMSIS_NN=1 |
M55, CM55 |
ARM_MATH_M55, ARM_MATH_MVEF, ARM_MATH_MVEI |
MCU Arm | CMSIS-NN when NETKIT_CMSIS_NN=1 |
M85, CM85 |
ARM_MATH_M85, ARM_MATH_MVEF, ARM_MATH_MVEI |
MCU Arm | CMSIS-NN when NETKIT_CMSIS_NN=1 |
A32, MPU |
ARM_MATH_A32 |
MPU Arm | CMSIS off |
NEON, A64 |
ARM_MATH_NEON |
MPU Arm | CMSIS off |
N300, N600, N900 |
RISCV_MATH_LOOPUNROLL |
MCU RISC-V (Nuclei) | NMSIS-NN when NETKIT_NMSIS_NN=1 |
NX600, NX900, UX900 |
RISCV_MATH_LOOPUNROLL |
MCU RISC-V (Nuclei) | NMSIS-NN when NETKIT_NMSIS_NN=1 |
RV32IMAC, RV32IMC, RV32IMF, RV32IMAFDC, RV32IMAFC, RV64IMAFDC |
RISCV_MATH_LOOPUNROLL |
MCU RISC-V | NMSIS-NN when NETKIT_NMSIS_NN=1 |
RISC, RISCV |
RISCV_MATH_LOOPUNROLL |
MCU RISC-V (generic tag) | NMSIS-NN when NETKIT_NMSIS_NN=1 |
ESP32 |
CONFIG_IDF_TARGET_ESP32 |
MCU Espressif | ESP-NN when NETKIT_ESP_NN=1 |
ESP32S3 |
CONFIG_IDF_TARGET_ESP32S3 |
MCU Espressif | ESP-NN when NETKIT_ESP_NN=1 |
ESP32C3 |
CONFIG_IDF_TARGET_ESP32C3 |
MCU Espressif | ESP-NN when NETKIT_ESP_NN=1 |
ESP32C6 |
CONFIG_IDF_TARGET_ESP32C6 |
MCU Espressif | ESP-NN when NETKIT_ESP_NN=1 |
ESP32P4 |
CONFIG_IDF_TARGET_ESP32P4 |
MCU Espressif | ESP-NN when NETKIT_ESP_NN=1 |
Aliases like Cortex-M4 normalize to CM4; ESP32-S3 / ESP32_S3 normalize to ESP32S3. CMake also sets NETKIT_TARGET from the arch (cpu / mcu_arm / mpu_arm / mcu_esp / RISC MCU tags → mcu_risc); with Make you pass NETKIT_TARGET explicitly for firmware.
| Flag | When | Purpose |
|---|---|---|
ARM_MATH_LOOPUNROLL |
CMSIS-NN embedded builds | Enables 4× loop unroll in CMSIS-NN kernels |
__DSP_PRESENT=1 |
NETKIT_ARCH=M33 |
Required for Armv8-M DSP extension; avoids scalar fallbacks |
ARM_MATH_MVEF / ARM_MATH_MVEI |
NETKIT_ARCH=M55, M85 |
Helium vector extensions (complements toolchain -mcpu=cortex-m55) |
HOST / __GNUC_PYTHON__ |
Desktop only, or NETKIT_HOST_SMOKE=1 on host MCU/MPU smoke |
CMSIS-NN portable host path (no CMSIS-Core device headers) |
MCU builds also add -Ithird_party/CMSIS-Core/CMSIS/Core/Include when that directory exists.
| Target | Default arena | Override flag |
|---|---|---|
| CPU | Heap (malloc backing via nk_arena_init_heap / Arena::init_heap) |
NETKIT_GLOBAL_ARENA=1 → static/global buffer only |
| MCU | Global/static only (nk_arena_init with your buffer). No heap ever — malloc / new / delete / free are forbidden; weights stay in the flash .nk image |
— |
| MPU | Global/static (same as MCU default) | NETKIT_HEAP_ARENA=1 → also compile heap helpers |
Compile-time macros (from include/netkit_config.h):
| Macro | Meaning |
|---|---|
NETKIT_TARGET_CPU |
Desktop / CPU build |
NETKIT_TARGET_MCU_ARM |
Arm MCU build |
NETKIT_TARGET_MPU_ARM |
Arm MPU build |
NETKIT_TARGET_MCU_RISC |
RISC-V MCU (NMSIS-NN int8 production) |
NETKIT_TARGET_MPU_RISC |
RISC-V MPU (XNNPACK default; CMSIS forbidden) |
NETKIT_TARGET_MCU_ESP |
Espressif MCU (ESP-NN int8 production) |
NETKIT_CLASS_MCU / NETKIT_CLASS_MPU |
Firmware class (arena / lean API) |
NETKIT_ISA_ARM / NETKIT_ISA_RISC / NETKIT_ISA_ESP |
Instruction-set family (backend policy) |
NETKIT_DESKTOP |
CPU only — CLI, regression, debug tooling |
NETKIT_ARENA_HEAP |
Heap arena API compiled in (CPU default; MPU when opted in; never MCU) |
NETKIT_GLOBAL_ARENA |
CPU only — force global/static arena instead of heap default |
NETKIT_MCU_ACCEL_ONLY / NETKIT_MCU_CMSIS_ONLY |
MCU class + REFERENCE_QUANT_LOOPS=0 — QuantOps reference loops omitted (flash); applies to CMSIS-NN, ESP-NN, and NMSIS-NN production |
NETKIT_DISABLE_IOSTREAM |
Default on MCU — keeps iostream out of flash |
NETKIT_USE_CMSIS_NN |
CMSIS-NN backends enabled (see CMSIS section) |
NETKIT_USE_ESP_NN |
ESP-NN backends enabled (see ESP-NN section) |
NETKIT_USE_NMSIS_NN |
NMSIS-NN backends enabled (see NMSIS-NN section) |
NETKIT_IM2COL |
Conv2D strategy for float reference and int8 QuantOps only: 0 = direct loops, 1 = partial im2col, 2 = full im2col + GEMM. Default 0 on cpu / MCU / MPU. CMSIS-NN, ESP-NN, NMSIS-NN, and XNNPACK ignore this knob. Prefer leaving 0; at most try 1 on MCU or on MPU/cpu when XNNPACK is off (small bump possible). Avoid 2 unless profiling shows a clear win. See guidance below. |
NETKIT_LOOP_UNROLL |
1 — experimental 4× manual loop unroll in netkit reference kernels only (default 0). Increases .text size; can exceed flash on small MCUs. Does not affect CMSIS (ARM_MATH_LOOPUNROLL is separate). |
NETKIT_DW_ROW_ACCUM |
Depthwise conv cross-row accumulator strategy (default 1). See src/conv_depthwise_kernel.cpp. |
NETKIT_HOST_SMOKE |
Host MCU/MPU smoke only — __GNUC_PYTHON__ for CMSIS-NN / NMSIS-NN without device Core headers; clears ESP chip asm flags so ESP-NN builds ANSI-only on host |
NETKIT_USE_MMAP |
File mmap for path-based .nk load (NETKIT_MMAP=0|1). POSIX on macOS/Linux; Win32 on Windows. Default 1 on cpu + any MPU; forbidden on MCU |
im2col is primarily an MCU / reference-path Conv2D optimization. It does not affect CMSIS-NN, ESP-NN, NMSIS-NN, or XNNPACK LayerFast convolutions.
| Value | Meaning | Recommendation |
|---|---|---|
0 (default) |
Direct nested loops | Safest — leave this everywhere (cpu, MPU, MCU) |
1 |
Partial im2col | Optional small speedup on MCU, or on MPU/cpu when NETKIT_XNNPACK=0. Host A/B saw modest gains on some float CNN reference runs; not worth it when XNNPACK is on |
2 |
Full im2col + GEMM | Rarely useful; higher scratch / code size. Do not enable without profiling |
# Default (recommended)
make NETKIT_TARGET=mcu_arm NETKIT_ARCH=CM4 lib
# Optional MCU / reference-path experiment
make NETKIT_TARGET=mcu_arm NETKIT_ARCH=CM4 NETKIT_IM2COL=1 lib
make NETKIT_XNNPACK=0 NETKIT_IM2COL=1 lib # MPU/cpu reference onlyDefault arena constant (NK_ARENA_DEFAULT_CAPACITY / Arena::kDefaultCapacity):
| Target | Default | Override |
|---|---|---|
| MCU | 64 KiB | -DNK_ARENA_DEFAULT_CAPACITY=<bytes>, or Make/CMake NETKIT_ARENA_CAPACITY=<bytes> / NETKIT_ARENA_KB=<KiB> |
| CPU / MPU | 64 MiB | same |
make NETKIT_TARGET=mcu_arm NETKIT_ARENA_KB=116 lib
make NETKIT_ARENA_CAPACITY=134217728 lib # 128 MiB on CPU
c++ ... -DNK_ARENA_DEFAULT_CAPACITY=131072 ...Weights always stay in the .nk blob. Preferred on MCU and RTOS/bare-metal MPU: flash/XIP or Load*FromBuffer. File mmap (NETKIT_MMAP / NETKIT_USE_MMAP): default on for cpu and any MPU (POSIX on macOS/Linux; Win32 CreateFileMapping / MapViewOfFile on Windows); forbidden on MCU. Opt out on no-OS MPU with NETKIT_MMAP=0 (falls back to fread into the arena, or prefer buffer/flash). The bump arena holds activations and structs. CLI override: ./netkit --arena <size> run|inspect ….
# Desktop (default) — CLI + tests + libnetkit.a, heap arena default
make
make test
make build-all
# CPU with static/global arena (firmware-style backing on desktop)
make NETKIT_TARGET=cpu NETKIT_GLOBAL_ARENA=1 all
# Lean runtime libraries for firmware
make NETKIT_TARGET=mcu_arm lib
make NETKIT_TARGET=mpu_arm lib
make NETKIT_TARGET=mcu_esp NETKIT_ARCH=ESP32S3 lib
# MCU firmware with CMSIS + core flags
make NETKIT_ARCH=CM4 NETKIT_TARGET=mcu_arm NETKIT_CMSIS_NN=1 lib
make NETKIT_ARCH=M33 NETKIT_TARGET=mcu_arm NETKIT_CMSIS_NN=1 lib
# Espressif MCU with ESP-NN (int8 production; float32 reference)
make esp-nn-init
make NETKIT_ARCH=ESP32S3 NETKIT_TARGET=mcu_esp lib
make NETKIT_ARCH=ESP32C6 NETKIT_TARGET=mcu_esp NETKIT_HOST_SMOKE=1 lib # host ANSI
# MPU with optional heap arena API (MCU forbids heap)
make NETKIT_TARGET=mpu_arm NETKIT_HEAP_ARENA=1 lib
# Convenience aliases
make cpu # NETKIT_TARGET=cpu (heap default)
make cpu-global # NETKIT_TARGET=cpu NETKIT_GLOBAL_ARENA=1
make mcu-arm # NETKIT_TARGET=mcu_arm lib
make mpu-arm # NETKIT_TARGET=mpu_arm lib
make mpu-arm-heap # NETKIT_TARGET=mpu_arm NETKIT_HEAP_ARENA=1 lib
make mcu-risc # NETKIT_TARGET=mcu_risc lib (NMSIS-NN; set NETKIT_ARCH=N300)
make mpu-risc # NETKIT_TARGET=mpu_risc lib (XNNPACK when fetched)
make mcu-esp # NETKIT_TARGET=mcu_esp lib (needs NETKIT_ARCH=ESP32*)
make cmsis-init # fetch CMSIS-Core + CMSIS-NN
make esp-nn-init # fetch ESP-NN (Espressif)
make nmsis-init # fetch NMSIS (RISC-V MCU NMSIS-NN)
make embedded-smoke # lean MCU/MPU smoke binary (test_mlp, cnn_4x4_single)
make test-embedded-smoke-matrix # host smoke matrix (see TESTING.md)# Desktop — XNNPACK on by default (cpu profile), CMSIS-NN off
cmake -B cmake-build -DNETKIT_TARGET=cpu
cmake --build cmake-build
./cmake-build/netkit test
# MCU firmware (use with your toolchain file)
cmake -B build-firmware -DCMAKE_TOOLCHAIN_FILE=... -DNETKIT_TARGET=mcu_arm -DNETKIT_ARCH=CM7 -DNETKIT_CMSIS_NN=ON
cmake --build build-firmware
cmake -B build-m55 -DCMAKE_TOOLCHAIN_FILE=... -DNETKIT_TARGET=mcu_arm -DNETKIT_ARCH=M55 -DNETKIT_CMSIS_NN=ON
# Espressif MCU (ESP-NN)
cmake -B build-esp -DCMAKE_TOOLCHAIN_FILE=... -DNETKIT_TARGET=mcu_esp -DNETKIT_ARCH=ESP32S3 -DNETKIT_ESP_NN=ON
# RISC-V MCU (NMSIS-NN)
cmake -B build-risc -DCMAKE_TOOLCHAIN_FILE=... -DNETKIT_TARGET=mcu_risc -DNETKIT_ARCH=N300 -DNETKIT_NMSIS_NN=ONCMake cache options mirror Make: NETKIT_TARGET, NETKIT_ARCH, NETKIT_CMSIS_NN, NETKIT_ESP_NN, NETKIT_NMSIS_NN, NETKIT_XNNPACK, NETKIT_GLOBAL_ARENA, NETKIT_HEAP_ARENA.
Use for local development and the netkit CLI.
Build outputs:
netkit— CLI (test,run,inspect)libnetkit.a— runtime + desktop extrasexamples/infer_cpp,examples/infer_c,tests/test_c_api
Desktop-only (guarded by NETKIT_DESKTOP):
Cli::Run/nk_cli_runrun_all_tests/nk_run_all_tests- Future tensor analysis / debug tooling
Arena: CLI and regression use heap with NK_ARENA_DEFAULT_CAPACITY (64 MiB) on CPU. Override with ./netkit --arena <size>. Build with NETKIT_GLOBAL_ARENA=1 to use a static buffer instead of heap.
See CLI.md.
Inference-only library for microcontrollers. No CLI, no regression runner.
Default arena: caller-owned static or global buffer sized with NK_ARENA_DEFAULT_CAPACITY (64 KiB):
alignas(max_align_t) static unsigned char arena_mem[65536];
nk_arena_t arena;
nk_arena_init(&arena, arena_mem, sizeof(arena_mem));Optional heap arena when built with NETKIT_HEAP_ARENA=1 — one malloc at startup, never freed:
nk_arena_init_heap(&arena, capacity);
/* ... entire firmware lifetime ... */
/* nk_arena_destroy_heap() is a no-op on MCU/MPU */Build output: libnetkit.a only.
Same lean runtime as MCU. Default static arena constant is NK_ARENA_DEFAULT_CAPACITY (64 MiB) — MPU firmware typically uses a caller-owned buffer sized with nk_inspect_model() rather than the full default.
OS is orthogonal to the MPU target. Many Cortex-A boards run FreeRTOS, Zephyr, or bare metal (no mmap). Defaults assume a VM-capable OS; opt out when you do not have one:
| MPU deployment | Weight load |
|---|---|
| Embedded Linux / POSIX OS / Windows | Default NETKIT_MMAP=1 — file mmap (POSIX or Win32; same as cpu). |
| RTOS / bare metal | make NETKIT_TARGET=mpu_arm NETKIT_MMAP=0 lib (or buffer/flash / Load*FromBuffer). |
MCU builds cannot enable mmap (NETKIT_MMAP=1 is forced off / compile error). Do not assume every MPU build has a virtual-memory OS.
| Component | CPU | MCU / MPU |
|---|---|---|
| Arena, tensor, ops, MLP, CNN | ✓ | ✓ |
.nk loader (nk_loader, nk_format) |
✓ | ✓ |
C API bridge (netkit_api.cpp) |
✓ | ✓ |
CLI (cli.cpp, main.cpp) |
✓ | — |
Regression (test.cpp, nk_regression.cpp) |
✓ | — |
The Makefile uses host clang/clang++ for desktop builds. For firmware:
- Set
NETKIT_ARCHto your core (e.g.CM4,M33,M55,NEON,ESP32S3,ESP32C6). - Build with
NETKIT_TARGET=mcu_arm/mpu_arm/mcu_risc/mpu_risc/mcu_espand linklibnetkit.ainto your board project, or - Use CMake with a
CMAKE_TOOLCHAIN_FILEand-DNETKIT_ARCH=..., or - Add runtime
.cppsources to your board build with-DNETKIT_TARGET_MCU_ARM=1(orMPU_ARM/ RISC /MCU_ESPmacros) and-std=c++26 -Iinclude.
Your firmware toolchain must still pass the appropriate -mcpu / -march flags; netkit's NETKIT_ARCH adds matching CMSIS ARM_MATH_* or Espressif CONFIG_IDF_TARGET_* preprocessor defines.
Optional compile-time kernel backends (Apache-2.0). Fetch once with make cmsis-init (CMSIS-Core headers + CMSIS-NN).
CMSIS-DSP is not used or linked. Portable helpers live in netkit_util (NetkitUtil::).
Backend story: reference + XNNPACK (cpu / MPU) + CMSIS-NN (Arm MCU int8) + ESP-NN (Espressif MCU int8) + NMSIS-NN (RISC-V MCU int8).
ARM CMSIS 6 Core(M) headers are required for on-device MCU firmware when NETKIT_ARCH is set and CMSIS-NN is enabled. Submodule path: third_party/CMSIS-Core → CMSIS/Core/Include. Not needed for host make test or NETKIT_HOST_SMOKE=1 smoke builds.
ARM CMSIS-NN accelerates layer kernels when NETKIT_CMSIS_NN=1, NETKIT_TARGET=mcu_arm, and NETKIT_ARCH is Cortex-M (CM4, M33, …). Includes depthwise conv (linked from arm_depthwise_conv_f32.c and support kernels). On cpu / mpu_arm the flag is ignored (Make warning) and reference / XNNPACK kernels run. On RISC targets CMSIS-NN is forbidden (forced off).
Production MCU path is int8 + CMSIS-NN. Float32 on MCU is still supported via portable/reference kernels, but there is no plan for an optimized float32 MCU build (CMSIS-DSP float helpers are intentionally not built-in). Use NETKIT_TARGET=mcu_arm with reference float kernels when you need float32 on-device.
MCU firmware: stage inputs in SRAM before timed inference. NUCLEO board main.cpp files use g_input_staging + NetkitUtil::CopyInt8 / CopyF32.
Backends are compile-time only — one binary, one backend set; no runtime switching.
CMSIS-NN is not inferred from NETKIT_ARCH alone — use NETKIT_CMSIS_NN=1 or the profile defaults below.
MCU board firmware: boards/nucleo-f446re-*-int8/Makefile overrides NETKIT_CMSIS_NN to 1 (unless reference quant loops). Pass NETKIT_CPPFLAGS on the LTO link line so CMSIS macros match compile units.
NETKIT_TARGET |
Default CMSIS-NN | Default ESP-NN | Default NMSIS-NN | Default XNNPACK |
|---|---|---|---|---|
cpu |
0 | 0 | 0 | 1 (requires ./tools/fetch_xnnpack.sh) |
mcu_arm |
1 (effective with Cortex-M NETKIT_ARCH) |
0 (forbidden) | 0 (forbidden) | 0 (forbidden) |
mpu_arm |
0 | 0 | 0 | 1 (requires ./tools/fetch_xnnpack.sh) |
mcu_risc |
0 (forbidden) | 0 (forbidden) | 1 (effective with NETKIT_ARCH=N300|RV32*…) |
0 (forbidden) |
mpu_risc |
0 (forbidden) | 0 (forbidden) | 0 (forbidden) | 1 (requires ./tools/fetch_xnnpack.sh) |
mcu_esp |
0 (forbidden) | 1 (effective with NETKIT_ARCH=ESP32*) |
0 (forbidden) | 0 (forbidden) |
make cmsis-init
make esp-nn-init # once, for mcu_esp ESP-NN
make nmsis-init # once, for mcu_risc NMSIS-NN
make xnnpack-init # once, for cpu / MPU XNNPACK LayerFast
# Profile defaults (no extra flags needed after fetch)
make test-cpp # cpu: XNNPACK (CMSIS / ESP / NMSIS off)
make NETKIT_TARGET=mcu_arm lib # mcu_arm: CMSIS-NN (set NETKIT_ARCH=CM4 for NN)
make NETKIT_TARGET=mcu_esp NETKIT_ARCH=ESP32S3 lib # mcu_esp: ESP-NN
make NETKIT_TARGET=mcu_risc NETKIT_ARCH=N300 lib # mcu_risc: NMSIS-NN
make NETKIT_TARGET=mpu_arm lib # mpu_arm: XNNPACK
make NETKIT_TARGET=mpu_risc lib # mpu_risc: XNNPACK on (Arm CMSIS-NN off)
# Explicit off (reference kernels)
make NETKIT_CMSIS_NN=0 NETKIT_XNNPACK=0 rebuild test
# Reference-kernel 4× loop unroll — experimental (increases .text)
make NETKIT_LOOP_UNROLL=1 test-cpp| Makefile flag | Macro | Effect |
|---|---|---|
NETKIT_CMSIS_NN=1 |
NETKIT_USE_CMSIS_NN |
mcu_arm + Cortex-M NETKIT_ARCH only |
NETKIT_ESP_NN=1 |
NETKIT_USE_ESP_NN |
mcu_esp + NETKIT_ARCH=ESP32* only |
NETKIT_NMSIS_NN=1 |
NETKIT_USE_NMSIS_NN |
mcu_risc + Nuclei/RV32 NETKIT_ARCH only |
NETKIT_XNNPACK=1 |
NETKIT_USE_XNNPACK |
cpu + any MPU float32/int8 LayerFast; forbidden on MCU |
NETKIT_LOOP_UNROLL=1 |
NETKIT_LOOP_UNROLL=1 |
Experimental. 4× manual unroll in reference kernels only (default 0); see GENERIC_KERNELS.md |
NETKIT_ARCH=<core> |
ARM_MATH_* / CONFIG_IDF_TARGET_* / RISC tags (see table above) |
Core-specific CMSIS-NN / ESP-NN / NMSIS-NN tuning |
Dense weights use CMSIS-NN [out, in] layout via Kernels::FullyConnectedWithBias (same as PyTorch nn.Linear).
Float32 CMSIS-NN support is experimental upstream. Helium (MVE) and Neon targets get optimized kernels when NETKIT_ARCH and the toolchain flags align.
Google XNNPACK (BSD-3) accelerates float32 and int8 (qs8) kernels on cpu and any MPU (mpu_arm, mpu_risc, …) when NETKIT_XNNPACK=1 (the default for those profiles). It is portable across host ISAs (x86, Arm, …); netkit still forbids it on MCU class targets (flash/RAM), where CMSIS-NN / ESP-NN / NMSIS-NN / reference kernels apply instead.
- float32:
XnnpackKernelisLayerFastinactive_kernel.hpp(conv, depthwise, max/avg pool, FC with ReLU/ReLU6 clamp). - int8:
XnnpackQuantis tried first in the quantized plan /QuantOpspath (qs8 conv, depthwise, max pool, FC), then ESP-NN / NMSIS-NN / CMSIS-NN if enabled, then reference.
Espressif ESP-NN (Apache-2.0) accelerates int8 kernels when NETKIT_ESP_NN=1, NETKIT_TARGET=mcu_esp, and NETKIT_ARCH is an Espressif chip (ESP32, ESP32S3, ESP32C3, ESP32C6, ESP32P4). Fetch once with make esp-nn-init (./tools/fetch_esp_nn.sh).
ESP-NN has no float32 API. EspNnKernel occupies the same LayerFast slot as CMSIS float LayerFast, but every float Try* returns false so ComposedKernel falls through to ReferenceKernel. Production Espressif path is int8 + ESP-NN (EspNnQuant), wired like CMSIS-NN (Try* + shared quant plan types).
On cpu / Arm / RISC targets the flag is forced off. Host smoke uses NETKIT_HOST_SMOKE=1 to build ANSI-only ESP-NN (no chip assembly).
make esp-nn-init
make NETKIT_TARGET=mcu_esp NETKIT_ARCH=ESP32S3 lib
make NETKIT_TARGET=mcu_esp NETKIT_ARCH=ESP32C6 NETKIT_HOST_SMOKE=1 libC and C++ firmware both call nk_model_load / nk_model_run_int8 (or C++ Load / forward); backend choice is compile-time only — API_PARITY.md.
Nuclei NMSIS-NN (Apache-2.0; CMSIS-NN API twin for RISC-V) accelerates int8 kernels when NETKIT_NMSIS_NN=1, NETKIT_TARGET=mcu_risc, and NETKIT_ARCH is a Nuclei / RISC-V MCU tag (N300, N600, NX900, RV32IMAC, …). Fetch once with make nmsis-init (./tools/fetch_nmsis.sh).
NMSIS-NN has no float32 API. NmsisNnKernel occupies the same LayerFast slot as CMSIS float LayerFast, but every float Try* returns false so ComposedKernel falls through to ReferenceKernel. Production RISC-V MCU path is int8 + NMSIS-NN (NmsisNnQuant), wired like CMSIS-NN (Try* + shared quant plan types; arm_* → riscv_*).
On cpu / Arm / Espressif / MPU targets the flag is forced off. Host smoke uses NETKIT_HOST_SMOKE=1 plus third_party/nmsis_host_compat.h (__SSAT / __RESTRICT / __CLZ).
make nmsis-init
make NETKIT_TARGET=mcu_risc NETKIT_ARCH=N300 lib
make NETKIT_TARGET=mcu_risc NETKIT_ARCH=RV32IMAC NETKIT_HOST_SMOKE=1 libC and C++ firmware both call nk_model_load / nk_model_run_int8 (or C++ Load / forward); backend choice is compile-time only — API_PARITY.md.
MCU builds force NETKIT_XNNPACK=0; enabling it is a Make override to off / compile error.
./tools/fetch_xnnpack.sh # or: make xnnpack-init
make NETKIT_XNNPACK=1 lib # default on cpu + MPU once fetched
make NETKIT_XNNPACK=0 lib # force reference LayerFast pathIf headers/libs are missing, Make prints a warning and builds without XNNPACK (reference LayerFast). CI forces NETKIT_XNNPACK=0.
Backends are composed at compile time via active_kernel.hpp — there is no runtime backend switch. Layer and ops code call Kernels::Op(...); ComposedKernel tries CMSIS-NN / ESP-NN / NMSIS-NN / XNNPACK Try* methods and falls back to ReferenceKernel.
CNN graph execution uses an MCU-safe op registry (include/ops_resolver.hpp) — the interpreter path: function pointers and fixed static storage, no virtuals, heap, or std::vector. For production speed on a fixed graph, pair AOT embed + packager optimizations with a trimmed NkOpList — PHILOSOPHY.md.
C++26 constinit resolver tables: NkOpList<Ops...> builds the lookup table at compile time into constinit static storage (no dynamic static initialization).
#include "layer_op_registry.hpp"
// Only conv + dense — linker can drop unused operator bodies
cnn.SetOpsResolver(NkOpList<NkConv2DOpDescriptor, NkDenseOpDescriptor>::View());GetDefaultOpsResolver() returns NkAllLayerOps::View() when all src/layer_ops/*.cpp units are linked.
Each layer kind lives in its own translation unit under src/layer_ops/ with a matching descriptor in include/layer_ops/. Link only the .cpp files your graph uses, then point the network at a compile-time resolver:
#include "layer_ops/nk_conv2d_op.hpp"
#include "layer_ops/nk_max_pool2d_op.hpp"
#include "layer_ops/nk_flatten_op.hpp"
#include "layer_ops/nk_dense_op.hpp"
using TrimOps = NkOpList<NkConv2DOpDescriptor, NkMaxPool2DOpDescriptor,
NkFlattenOpDescriptor, NkDenseOpDescriptor>;
cnn.SetOpsResolver(TrimOps::View());Link: nk_op_conv2d.cpp, nk_op_max_pool2d.cpp, nk_op_flatten.cpp, nk_op_dense.cpp, ops_resolver.cpp (omit ops_resolver_default.cpp, avg-pool, and batch-norm TUs).
Verify on desktop:
make trim-lib # libnetkit_trim.a — conv/max-pool/flatten/dense only
make check-trim-lib # asserts avg-pool + batch-norm bodies are absentSee examples/trim_firmware.cpp for a minimal recipe.
Full architecture: KERNELS.md.
Typical forward pass (MCU + CMSIS-NN):
| Op family | Primary backend |
|---|---|
| Conv / depthwise conv / pool / batch norm / FC / NN activations / GELU / softmax | CMSIS-NN (LayerFast) |
| Elementwise mul / matmul / add / scale / ReLU6 clip / LayerNorm2d / GRN | Reference / NetkitUtil |
| Fused blocks (ResNet / MobileNet / ConvNeXt internals) | Same via fused_kernel_ops.hpp |
Any Try* miss |
Reference |
Default regression (make test) and full suite (make test-full) require NETKIT_TARGET=cpu. CMSIS / ESP-NN / NMSIS-NN backends are validated locally via host smoke (make test-embedded-smoke-matrix with NETKIT_HOST_SMOKE=1), which exercises test_mlp and cnn_4x4_single on Arm + RISC + Espressif MCU/MPU profiles.
make cmsis-init
make esp-nn-init
make nmsis-init
make test-cpp
make test-embedded-smoke-matrix # host MCU/MPU + CMSIS + ESP-NN + NMSIS-NN profiles
make NETKIT_HOST_SMOKE=1 NETKIT_TARGET=mcu_arm NETKIT_ARCH=CM4 NETKIT_CMSIS_NN=1 lib
make NETKIT_HOST_SMOKE=1 NETKIT_TARGET=mcu_esp NETKIT_ARCH=ESP32C6 NETKIT_ESP_NN=1 lib
make NETKIT_HOST_SMOKE=1 NETKIT_TARGET=mcu_risc NETKIT_ARCH=N300 NETKIT_NMSIS_NN=1 libSee TESTING.md, ARENA.md, and per-device cookbooks in PLATFORMS.md.
Related: PHILOSOPHY.md · GETTING_STARTED.md · PLATFORMS.md