Headers live in include/. Configuration: include/netkit_config.h. All implementation files use -std=c++26.
New users: GETTING_STARTED.md · Philosophy: PHILOSOPHY.md
Numeric types: float32 and int8 inference today (int8 I/O end-to-end; no C++ float↔int8). float16, int16, and int4 are on the roadmap — DATATYPES.md.
| Makefile | Macro | Outputs | Default backends |
|---|---|---|---|
NETKIT_TARGET=cpu |
NETKIT_TARGET_CPU, NETKIT_DESKTOP |
netkit CLI + full libnetkit.a |
XNNPACK on (any host ISA); CMSIS-NN off |
NETKIT_TARGET=mcu_arm |
NETKIT_TARGET_MCU_ARM |
Lean libnetkit.a only |
CMSIS-NN (int8); float32 reference; XNNPACK forbidden |
NETKIT_TARGET=mpu_arm |
NETKIT_TARGET_MPU_ARM |
Lean libnetkit.a only |
XNNPACK |
NETKIT_TARGET=mcu_risc |
NETKIT_TARGET_MCU_RISC |
Lean libnetkit.a only |
NMSIS-NN (int8); float32 reference; XNNPACK forbidden |
NETKIT_TARGET=mpu_risc |
NETKIT_TARGET_MPU_RISC |
Lean libnetkit.a only |
XNNPACK on; CMSIS-NN forbidden |
NETKIT_TARGET=mcu_esp |
NETKIT_TARGET_MCU_ESP |
Lean libnetkit.a only |
ESP-NN (int8); float32 reference; XNNPACK forbidden |
Derived (from netkit_config.h, shared by C and C++): NETKIT_CLASS_MCU / NETKIT_CLASS_MPU, NETKIT_ISA_ARM / NETKIT_ISA_RISC / NETKIT_ISA_ESP.
Backend selection is compile-time only. C callers via netkit.h share the same kernels — see API_PARITY.md.
| Flag | Macro | Arena / backend |
|---|---|---|
| CPU default | NETKIT_ARENA_HEAP |
Heap API; CLI uses model-sized allocation |
NETKIT_GLOBAL_ARENA=1 |
NETKIT_GLOBAL_ARENA |
Static buffer on CPU |
NETKIT_HEAP_ARENA=1 (MPU only) |
NETKIT_ARENA_HEAP |
Optional heap on MPU; forbidden on MCU |
NETKIT_CMSIS_NN=1 |
NETKIT_USE_CMSIS_NN |
mcu_arm + Cortex-M NETKIT_ARCH only |
NETKIT_XNNPACK=1 |
NETKIT_USE_XNNPACK |
cpu + any MPU LayerFast; forbidden on MCU |
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_MMAP=1 (default cpu/MPU on Apple/Linux/Windows) |
NETKIT_USE_MMAP |
File mmap for .nk loads; forbidden on MCU; opt out with NETKIT_MMAP=0 |
| (MCU default) | NETKIT_DISABLE_IOSTREAM / NETKIT_MCU_ACCEL_ONLY (NETKIT_MCU_CMSIS_ONLY) |
No iostream; QuantOps reference loops omitted when off |
Arena::kDefaultCapacity / NK_ARENA_DEFAULT_CAPACITY: 64 KiB (MCU class), 64 MiB (CPU and MPU class).
See BUILD_TARGETS.md. Same macros apply to the C API — c-api.md. Per-device cookbooks: PLATFORMS.md.
| Header | Purpose |
|---|---|
netkit_config.h |
Compile-time target and arena macros (C and C++) |
arena.hpp |
Bump-pointer arena allocator |
arena_util.hpp |
ArenaUtil::Init, Scoped, model capacity helpers |
tensor.hpp |
Tensor, DataType, kMaxTensorRank |
tensor_factory.hpp |
Tensor creation, fill, print |
tensor_access.hpp |
Typed data accessors + NHWC indexing |
ops.hpp |
Ops:: matrix ops and activations |
conv2d.hpp |
Low-level 2D convolution |
depthwise_conv2d.hpp |
Standalone depthwise convolution |
mlp.hpp |
MLPNetwork, MLPLayer, ActivationType |
cnn.hpp |
CNNNetwork, CnnBlock, ConvActivationType |
nk_loader.hpp / nk_format.hpp |
.nk model loading |
nk_regression.hpp |
Embedded .nk regression test runner |
cli.hpp |
CLI dispatch (Cli::Run) |
test.hpp |
Test suite entry (run_all_tests) |
cmsis_nn_kernel.hpp / cmsis_nn_quant.hpp |
CMSIS-NN LayerFast + int8 Try* (compile-time; mcu_arm only) |
esp_nn_kernel.hpp / esp_nn_quant.hpp |
ESP-NN LayerFast slot + int8 Try* (compile-time; mcu_esp only) |
nmsis_nn_kernel.hpp / nmsis_nn_quant.hpp |
NMSIS-NN LayerFast slot + int8 Try* (compile-time; mcu_risc only) |
xnnpack_kernel.hpp / xnnpack_quant.hpp |
XNNPACK LayerFast + qs8 Try* (compile-time; cpu / MPU) |
For a stable C interface from C++ projects or embedded firmware, use netkit.h. Core runtime symbols are mapped in API_PARITY.md; kernel backends and a few C++ helpers remain C++-only.
See ARENA.md for the full bump-allocator guide.
struct Arena {
// kDefaultCapacity: 64 KiB (MCU), 64 MiB (CPU/MPU) — see netkit_config.h
static constexpr std::size_t kDefaultCapacity = NK_ARENA_DEFAULT_CAPACITY;
std::byte* base{};
std::size_t capacity = 0;
std::size_t offset = 0;
void init(void* memory, std::size_t size);
void* alloc(std::size_t size, std::size_t alignment); // alignment: power of two
void reset();
std::size_t remaining() const;
// When NETKIT_USE_MMAP=1 (cpu/MPU): own a .nk file mapping until reset/destroy
void attach_mapped_file(const void* data, std::size_t size);
void release_mapped_file();
};alloc inserts padding when the current offset is not a multiple of alignment. Use alignof(float) for tensor payloads and weight blobs; use alignof(T) for struct arrays and placement-new targets.
Why alignment matters: weight blobs can have an odd float count, leaving the arena offset at 4 mod 8 on 64-bit platforms. Without padding, a following MLPNetwork or CNNNetwork allocation would be misaligned for placement new. The engine passes the correct alignof at every internal call site.
All network and tensor allocations during load/inference draw from the arena. No free() — call reset() to reuse the buffer. File mmap regions (attach_mapped_file) do not consume bump capacity; they are released on reset(), init(), and destroy_heap().
Target-aware helpers used by the CLI and regression harness. Named capacities are aliases of Arena::kDefaultCapacity (not separate hard-coded sizes):
namespace ArenaUtil {
constexpr std::size_t kHandCapacity = Arena::kDefaultCapacity;
constexpr std::size_t kMnistMlpCapacity = Arena::kDefaultCapacity;
constexpr std::size_t kMnistCnnCapacity = Arena::kDefaultCapacity;
constexpr std::size_t kLargeCnnCapacity = Arena::kDefaultCapacity;
constexpr std::size_t kXLargeCnnCapacity = Arena::kDefaultCapacity;
// Currently return Arena::kDefaultCapacity (arguments reserved for future sizing)
std::size_t CapacityForInputElements(uint32_t input_elements, bool is_cnn);
std::size_t CapacityForModel(uint32_t input_elements, bool is_cnn,
uint32_t weights_bytes, uint32_t biases_bytes);
bool Init(Arena& arena, std::size_t capacity, void* global_buffer = nullptr);
void Release(Arena& arena); // CPU only: frees heap backing; no-op on MPU; MCU never has heap API
class Scoped; // RAII — calls Release on destruction (CPU heap builds)
}On CPU (heap default), Init(capacity, nullptr) calls arena.init_heap() once. Regression reuses one heap buffer for the full suite (NkRegression::BeginRegressionArena / EndRegressionArena). On MCU, pass your static buffer only — heap helpers are not compiled. On MPU, pass a static buffer by default, or enable NETKIT_HEAP_ARENA=1 for optional init_heap(). destroy_heap() frees memory on CPU only.
When NETKIT_ARENA_HEAP is defined, Arena also provides:
bool init_heap(std::size_t size);
void destroy_heap();enum class DataType : uint8_t { Float32, Int8, UInt8, Int16, Int32 };
constexpr uint32_t kMaxTensorRank = 4;
struct Tensor {
void* data;
DataType type;
uint32_t rank;
std::array<uint32_t, kMaxTensorRank> shape;
std::array<uint32_t, kMaxTensorRank> stride;
uint32_t num_elements;
uint32_t bytes;
};Layouts
- MLP tensors: 2D row-major
[rows, cols] - CNN tensors: NHWC
[height, width, channels]
namespace TensorFactory {
Tensor Create2D(Arena& arena, uint32_t rows, uint32_t cols);
Tensor CreateND(Arena& arena, uint32_t rank, std::span<const uint32_t> shape);
Tensor View2D(float* data, uint32_t rows, uint32_t cols);
Tensor View2DInt8(int8_t* data, uint32_t rows, uint32_t cols);
Tensor View3DInt8(int8_t* data, uint32_t depth, uint32_t rows, uint32_t cols);
Tensor View1DInt32(int32_t* data, uint32_t length);
Tensor ViewND(float* data, uint32_t rank, std::span<const uint32_t> shape);
void Fill(Tensor& t, std::span<const float> values);
void Print(const Tensor& t);
void PrintLabeled(const char* label, const Tensor& t, uint32_t max_values = 0);
// max_values == 0 prints every element; otherwise prints the first max_values plus a total count.
}Returns tensors with null data if the arena is full.
float* tensor_data_f32(Tensor& t); // nullptr if dtype ≠ Float32
const float* tensor_data_f32(const Tensor& t);
int8_t* tensor_data_i8(Tensor& t); // nullptr if dtype ≠ Int8
const int8_t* tensor_data_i8(const Tensor& t);
int32_t* tensor_data_i32(Tensor& t); // nullptr if dtype ≠ Int32
const int32_t* tensor_data_i32(const Tensor& t);
uint32_t index_nhwc(const Tensor& t, uint32_t h, uint32_t w, uint32_t c);C equivalents: nk_tensor_data_f32 / _i8 / _i32 (and _const variants), nk_tensor_index_nhwc.
All ops live in namespace Ops. Validation helpers: Ops::IsMatMulValid, Ops::CheckSameShape2D, etc.
Arithmetic
| Function | Description |
|---|---|
Ops::MatMul(A, B, C) |
Matrix multiply |
Ops::MatAdd(A, B, C) |
2D element-wise add |
Ops::MatAddND(A, B, C) |
N-D element-wise add |
Ops::Mul(A, B, C) |
Element-wise multiply |
Ops::MulND(A, B, C) |
N-D element-wise multiply |
Ops::MulScalar(A, scalar, C) |
Scale by scalar |
Activations (in-place when A and C share storage)
| Function | Description |
|---|---|
Ops::ReLU(A, C) |
max(0, x) |
Ops::LeakyReLU(A, C, alpha) |
Leaky ReLU |
Ops::ReLU6(A, C) |
min(max(0, x), 6) |
Ops::Sigmoid(A, C) |
σ(x) |
Ops::Tanh(A, C) |
tanh(x) |
Ops::Softmax(A, C) |
Softmax over elements |
Low-level convolution. pad_h / pad_w are start padding; pad_h_end / pad_w_end are end padding (-1 / NK_PAD_MIRROR mirrors the start for symmetric padding).
struct Conv2D {
int kernel_size = 3;
int stride = 1;
int pad_h = 0, pad_w = 0;
int pad_h_end = 0, pad_w_end = 0;
int in_channels, out_channels;
float* weights; // [out][kh][kw][in] from .nk (OIHW)
float* weights_hwio; // [kh][kw][in][out] repacked at CNN load; null until then
float* bias; // [out]
const int8_t* weights_q = nullptr;
const int32_t* bias_q = nullptr;
bool forward(const Tensor& input, Tensor& output,
NetkitKernelActivation fuse_activation = NetkitKernelActivation::None);
};Output spatial size (per axis): (in + pad_start + pad_end - kernel) / stride + 1.
C: nk_conv2d_t / nk_conv2d_forward (default fusion; no explicit fuse_activation arg).
Standalone depthwise conv (also available as a CNN block via InitDepthwiseConvLayer).
struct DepthwiseConv2D {
int kernel_h = 3, kernel_w = 3;
int stride = 1;
int pad_h = 0, pad_w = 0;
int pad_h_end = -1, pad_w_end = -1; // -1 mirrors start
int channels = 0;
float* weights; // [ch][kh][kw]
float* bias; // [ch]
const int8_t* weights_q = nullptr;
const int32_t* bias_q = nullptr;
bool forward(const Tensor& input, Tensor& output,
NetkitKernelActivation fuse_activation = NetkitKernelActivation::None);
};C: nk_depthwise_conv2d_t / nk_depthwise_conv2d_forward.
enum class ActivationType { None, ReLU, Sigmoid, Tanh, LeakyReLU, ReLU6, Softmax };
class MLPNetwork {
public:
MLPNetwork(uint32_t num_layers, Arena& arena);
bool IsValid() const;
bool IsQuantized() const;
bool HasActivationBuffers() const;
bool InitActivationBuffers(Arena& arena, uint32_t batch_rows); // called by LoadMLP
void InitLayer(uint32_t layer_idx, const Tensor& weights, const Tensor& bias,
ActivationType activation, float leaky_alpha = 0.01f);
/** Skip final Softmax and write logits (`argmax` unchanged for classification). */
void SetOmitFinalSoftmax(bool omit);
bool OmitFinalSoftmax() const;
// Hidden layers use ping-pong buffers allocated at load; final layer writes to output
void forward(const Tensor& input, Tensor& output, Arena& arena);
// Benchmark-only: timing_fn(tag, duration_ns, user_data) per layer
using LayerTimingFn = void (*)(const char* tag, uint64_t duration_ns, void* user_data);
void forward_timed(const Tensor& input, Tensor& output, LayerTimingFn timing_fn, void* user_data);
MLPLayer& GetLayer(uint32_t idx);
};Weight matrix shape per layer: [out_features, in_features] row-major (CMSIS-NN / PyTorch layout).
Most firmware uses NkLoader::LoadMLP / LoadCNN or nk_mlp_load / nk_cnn_load. To wire layers by hand:
Arena::init(orinit_heap) — bind caller-owned memory.MLPNetwork(num_layers, arena)— allocates the layer array from the arena.InitLayer(i, …)for eachifrom0tonum_layers - 1— bind weight/bias tensors (usually views into flash or a prior arena alloc).InitActivationBuffers(arena, batch_rows)— after all layers;batch_rowsis the input batch (typically1). Allocates ping-pong hidden buffers sized to the largest intermediate.forward(input, output, arena)— last layer writes tooutput; hidden layers reuse ping-pong buffers.
alignas(std::max_align_t) static unsigned char memory[65536];
Arena arena;
arena.init(memory, sizeof(memory));
MLPNetwork net(2, arena);
net.InitLayer(0, W0, B0, ActivationType::ReLU);
net.InitLayer(1, W1, B1, ActivationType::None);
if (!net.InitActivationBuffers(arena, /*batch_rows=*/1))
return; // arena overflow
Tensor input = TensorFactory::View2D(in_data, 1, in_features);
Tensor output = TensorFactory::View2D(out_data, 1, out_features);
net.forward(input, output, arena);C equivalent: c-api.md (nk_mlp_create → nk_mlp_init_layer → nk_mlp_init_activation_buffers → nk_mlp_forward).
CNN pipelines support mixed blocks: conv2d, depthwise_conv2d, max_pool2d, avg_pool2d, batch_norm2d, layernorm2d, convnextv2_block, mobilenetv4_uib, resnet_basic_block, yolox_decoupled_head, feature_tap, yolox_pafpn_multiscale, flatten, and dense (classification head). See NK_FORMAT.md, CONVNEXTV2.md, MOBILENETV4.md, RESNET18.md, and YOLOX.md.
enum class CnnBlockType {
Conv2D, DepthwiseConv2D, MaxPool2D, AvgPool2D, BatchNorm2d, LayerNorm2d,
Flatten, Dense, ConvNeXtV2Block, MobilenetV4Uib, ResNetBasicBlock,
YoloxDecoupledHead, FeatureTap, YoloxPafpnMultiscale
};
class CNNNetwork {
public:
CNNNetwork(uint32_t num_layers, Arena& arena);
bool IsValid() const;
bool IsQuantized() const;
bool HasActivationBuffers() const;
std::size_t KernelWorkspaceBytes() const; // CMSIS-NN / NMSIS-NN shared scratch after InitActivationBuffers
bool InitActivationBuffers(Arena& arena, uint32_t in_h, uint32_t in_w, uint32_t in_c); // LoadCNN
void InitConvLayer(uint32_t layer_idx,
int kernel_size,
int stride,
int in_channels,
int out_channels,
float* weights,
float* bias,
ConvActivationType activation,
float leaky_alpha = 0.01f,
int pad_h = 0,
int pad_w = 0,
int pad_h_end = -1,
int pad_w_end = -1);
void InitDepthwiseConvLayer(uint32_t layer_idx,
int kernel_h,
int kernel_w,
int stride,
int channels,
float* weights,
float* bias,
ConvActivationType activation,
float leaky_alpha = 0.01f,
int pad_h = 0,
int pad_w = 0,
int pad_h_end = -1,
int pad_w_end = -1);
void InitPoolLayer(uint32_t layer_idx,
int pool_h,
int pool_w,
int stride,
int pad_h = 0,
int pad_w = 0,
int pad_h_end = -1,
int pad_w_end = -1);
void InitAvgPoolLayer(uint32_t layer_idx,
int pool_h,
int pool_w,
int stride,
int pad_h = 0,
int pad_w = 0,
int pad_h_end = -1,
int pad_w_end = -1);
void InitBatchNormLayer(uint32_t layer_idx, int channels, float* scale, float* bias);
void InitLayerNormLayer(uint32_t layer_idx, int channels, float eps, float* weight, float* bias);
void InitConvNeXtV2BlockLayer(uint32_t layer_idx, Arena& arena, uint32_t spatial_h, uint32_t spatial_w,
int channels, float eps, /* weight pointers */);
void InitMobilenetV4UibLayer(uint32_t layer_idx, Arena& arena, uint32_t spatial_h, uint32_t spatial_w,
int in_channels, int out_channels, /* UIB config + weights */);
void InitResNetBasicBlockLayer(uint32_t layer_idx, Arena& arena, uint32_t spatial_h, uint32_t spatial_w,
int in_channels, int out_channels, int stride, /* conv/BN weights */);
void InitYoloxDecoupledHeadLayer(uint32_t layer_idx, Arena& arena, uint32_t spatial_h, uint32_t spatial_w,
int in_channels, int hidden_dim, int num_classes, int num_convs,
/* stem/branch/pred weight pointers */);
void InitFeatureTapLayer(uint32_t layer_idx, /* tap_id + spatial */);
void InitYoloxPafpnLayer(uint32_t layer_idx, Arena& arena, /* multiscale PAFPN weights */);
void InitFlattenLayer(uint32_t layer_idx);
void InitDenseLayer(uint32_t layer_idx, const Tensor& W, const Tensor& b,
ActivationType activation, float leaky_alpha = 0.01f);
float* GetFeatureTapBuffer(uint8_t tap_id) const;
uint32_t GetFeatureTapElems(uint8_t tap_id) const;
void SetOpsResolver(const NkOpsResolver& resolver); // trim via NkOpList<...>::View()
const NkOpsResolver& GetOpsResolver() const;
// All blocks ping-pong between two load-time buffers; result in GetOutput()
Tensor& forward(const Tensor& input, Arena& arena);
// Benchmark-only: timing_fn(tag, duration_ns, user_data) per block
using LayerTimingFn = void (*)(const char* tag, uint64_t duration_ns, void* user_data);
Tensor& forward_timed(const Tensor& input, Arena& arena, LayerTimingFn timing_fn, void* user_data);
CnnBlock& GetBlock(uint32_t idx);
Tensor& GetOutput();
};Spatial tensors stay NHWC until flatten; dense head output is [1, units]. Returns null data on arena overflow. For quantized CNNs, omit final Softmax via the quant runtime (SetQuantRuntime / Runtime::omit_final_softmax); C exposes that as nk_cnn_set_omit_final_softmax.
NkLoader::LoadCNN builds full pipelines from .nk files (including models/mnist_cnn.nk).
Layer dispatch uses NkOpList<Ops...>::View() for compile-time op tables — see KERNELS.md.
Arena::init— bind caller-owned memory (size withinspect --fullornk_inspect_modelwhen possible).CNNNetwork(num_layers, arena)— allocates the block array from the arena.Init*Layer(layer_idx, …)for each index0 … num_layers - 1in forward order — configure one block type per index. Primitive layers only need weight pointers; composite blocks also takeArena&plusspatial_h/spatial_w(the feature-map height/width at that layer's input) and may allocate fused scratch from the arena during this call.InitActivationBuffers(arena, in_h, in_w, in_c)— after every layer is configured. Uses the network input NHWC shape (same as the tensor passed toforward), not the last layer's output shape. Allocates ping-pong activation buffers and (on CMSIS-NN builds) the shared kernel workspace — see ARENA.md.forward(input, arena)— input must be rank-3 NHWC until aFlattenblock; result is inGetOutput()(also returned by reference). Returns a tensor with nulldataon arena overflow.
Hybrid primitive pipeline (conv → pool → batch norm → flatten → dense):
CNNNetwork net(5, arena);
net.InitConvLayer(0, 3, 1, 3, 16, conv_w, conv_b, ConvActivationType::ReLU, 0.01f, 1, 1);
net.InitPoolLayer(1, 2, 2, 2, 0, 0); // pool_h, pool_w, stride, pad_h, pad_w
net.InitBatchNormLayer(2, 16, bn_scale, bn_bias);
net.InitFlattenLayer(3);
net.InitDenseLayer(4, W, B, ActivationType::None);
if (!net.InitActivationBuffers(arena, in_h, in_w, in_c))
return;
Tensor input = /* NHWC [in_h, in_w, in_c] */;
Tensor& output = net.forward(input, arena);Composite blocks (same index order; pass spatial size at the block input):
| Block | Init function | Notes |
|---|---|---|
| ConvNeXt V2 | InitConvNeXtV2BlockLayer(idx, arena, h, w, …) |
Fused scratch from arena |
| MobileNetV4 UIB | InitMobilenetV4UibLayer(idx, arena, h, w, …) |
|
| ResNet BasicBlock | InitResNetBasicBlockLayer(idx, arena, h, w, …) |
|
| YOLOX decoupled head | InitYoloxDecoupledHeadLayer(idx, arena, h, w, …) |
See YOLOX.md |
| Feature tap | InitFeatureTapLayer(idx, …) |
Read via GetFeatureTapBuffer / GetFeatureTapElems after forward |
| YOLOX PAFPN | InitYoloxPafpnLayer(idx, arena, …) |
Multiscale neck |
YOLOX head only (single fused layer on backbone features):
CNNNetwork net(1, arena);
net.InitYoloxDecoupledHeadLayer(
0, arena,
spatial_h, spatial_w, // e.g. 2, 2 for 2×2 feature map
in_channels, // backbone output depth (e.g. 960)
hidden_dim, num_classes, num_convs,
stem_w, stem_b,
cls_conv_w, cls_conv_b, // arrays of num_convs pointers
reg_conv_w, reg_conv_b,
cls_pred_w, cls_pred_b,
reg_pred_w, reg_pred_b,
obj_pred_w, obj_pred_b);
if (!net.InitActivationBuffers(arena, spatial_h, spatial_w, in_channels))
return;
Tensor input = /* NHWC [spatial_h, spatial_w, in_channels] */;
Tensor& output = net.forward(input, arena); // [H, W, 4+1+num_classes]C equivalent: c-api.md (nk_cnn_create → nk_cnn_init_* → nk_cnn_init_activation_buffers → nk_cnn_forward).
Prefer .nk load when weights live in a file or flash blob: NkLoader::LoadCNN / nk_cnn_load / nk_model_load perform steps 2–4 automatically.
namespace NkLoader {
enum class NetworkKind { Unknown, Mlp, Cnn };
enum class LoadStatus {
Ok, FileOpenFailed, ReadFailed, InvalidMagic, UnsupportedVersion,
TruncatedFile, UnsupportedLayer, SizeMismatch, ArenaOverflow
};
struct LoadResult {
LoadStatus status;
NetworkKind kind;
const char* message;
};
struct ParsedModel { /* header, layers, tensor catalog, optional quant */ };
struct ArchInfo {
uint32_t version;
NetworkKind kind;
std::array<uint32_t, kMaxTensorRank> input_shape;
uint32_t input_rank, num_layers;
uint32_t input_elements, output_elements;
std::size_t weight_floats; // C nk_arch_info_t also exposes weights_bytes / biases_bytes
};
LoadResult ParseFile(const char* nk_path, ParsedModel& out);
LoadResult ParseBuffer(const uint8_t* data, std::size_t size, ParsedModel& out);
void FreeParsedModelExtras(ParsedModel& parsed); // heap scale blob from ParseFile
LoadResult ReadTestSuite(const char* nk_path, TestSuite& out);
std::size_t ModelPayloadBytes(const ParsedModel& model);
void FillArchInfo(const ParsedModel& model, ArchInfo& info);
uint32_t InputElements(const ParsedModel& model);
uint32_t OutputElements(const ParsedModel& model);
void PrintNetworkSummary(const char* nk_path, const ParsedModel& model);
LoadResult LoadMLP(const char* nk_path, Arena& arena, MLPNetwork*& network,
std::array<uint32_t, kMaxTensorRank>& input_shape, uint32_t& input_rank);
// When NETKIT_USE_MMAP=1 (cpu + MPU default on macOS/Linux/Windows; forbidden on MCU):
// private file mmap; arena owns mapping until reset/destroy.
// Otherwise: fread into arena. Prefer Load*FromBuffer / flash on MCU and RTOS MPU.
LoadResult LoadMLPFromBuffer(const uint8_t* data, std::size_t size, Arena& arena, MLPNetwork*& network,
std::array<uint32_t, kMaxTensorRank>& input_shape, uint32_t& input_rank);
LoadResult LoadCNN(const char* nk_path, Arena& arena, CNNNetwork*& network,
std::array<uint32_t, kMaxTensorRank>& input_shape, uint32_t& input_rank);
LoadResult LoadCNNFromBuffer(const uint8_t* data, std::size_t size, Arena& arena, CNNNetwork*& network,
std::array<uint32_t, kMaxTensorRank>& input_shape, uint32_t& input_rank);
LoadResult Load(const char* nk_path, Arena& arena, NetworkKind& kind,
MLPNetwork*& mlp, CNNNetwork*& cnn,
std::array<uint32_t, kMaxTensorRank>& input_shape, uint32_t& input_rank);
}C equivalents: nk_parse_architecture / nk_parse_architecture_memory fill nk_arch_info_t (includes weights_bytes / biases_bytes from the .nk header; C++ ArchInfo exposes weight_floats only). PrintNetworkSummary → nk_arch_print. Embedded firmware uses LoadMLPFromBuffer / LoadCNNFromBuffer (C: nk_mlp_load_memory / nk_cnn_load_memory, or nk_model_load_memory for the combined handle). IsQuantized() → nk_mlp_is_quantized / nk_cnn_is_quantized / nk_model_is_quantized. Int8 run → nk_model_run_int8; int8 / int32 views → nk_tensor_view_2d_int8 / nk_tensor_view_3d_int8 / nk_tensor_view_1d_int32; omit Softmax → nk_mlp_* / nk_cnn_* / nk_model_set_omit_final_softmax; standalone depthwise → nk_depthwise_conv2d_forward. nk_recommended_arena_bytes(path) probes load+forward peaks on CPU heap builds.
High-level C++ usage loads with Load / LoadMLP / LoadCNN (file) or LoadMLPFromBuffer / LoadCNNFromBuffer (embedded .nk bytes) and calls forward directly — the interpreter path via NkOpsResolver. The C API adds nk_model_t + nk_model_run (float32) / nk_model_run_int8 (int8) as convenience wrappers, plus typed nk_mlp_load_memory / nk_cnn_load_memory — see c-api.md.
Compiled firmware: python -m netkit aot generates C++26 or C23 sources. Default C++ lowered output is a static Kernels:: call chain (kLowered = true) with coefs in flash .rodata. C AOT embeds the .nk blob and uses nk_model_load_memory. See GETTING_STARTED.md, API_PARITY.md, and PHILOSOPHY.md.
Format — full binary layout in NK_FILE_SPECIFICATION.md (byte-level) and NK_FORMAT.md (overview). Convert ONNX with python -m netkit convert.
| File | Build | Description |
|---|---|---|
examples/infer_cpp.cpp |
make example-cpp |
Load MLP/CNN and run forward pass via native headers |
examples/infer_c.c |
make example-c |
Same workflow via netkit.h |
namespace NkRegression {
struct RunSummary { uint32_t passed; uint32_t failed; };
RunSummary RunModelTests(const char* nk_path);
}Drives regression tests from embedded cases in .nk files. Format: NK_FORMAT.md.
namespace Cli {
int Run(int argc, char** argv);
}Commands: test, run <model.nk> --input ..., inspect <model.nk> [--full], help / -h / --help. Full reference: CLI.md.
NkRegression::RunSummary run_all_tests();make lib # produces libnetkit.aLink your C++ code:
clang++ -std=c++26 -Iinclude -o my_app my_app.cpp libnetkit.aExclude main.cpp, cli.cpp, and test.cpp from your link if you provide your own entry point.
- Inference only (no training, no autodiff)
- Single-threaded
- No heap allocation in
MLPNetwork/CNNNetworklayer paths - Conv and pool layers support independent start/end padding per axis (
pad_h/pad_w/pad_h_end/pad_w_end;.nkv3+); depthwise conv supports non-squarekernel_h×kernel_w
See DATATYPES.md for float32/int8 I/O rules and the roadmap for remaining types and broader ONNX import.