Embedded and Microcontroller Inference
Part 16 of 19
Part 1 of Lesson 10 worked out latency budgets and energy-per-inference in joules, and closed on the observation that those budgets only bind because the hardware underneath them is finite in ways a GPU, or even a phone SoC, never has to reason about. This half of the lesson makes that finiteness literal: kilobytes instead of gigabytes, tens of megahertz instead of gigahertz, and an operating philosophy where a single unplanned memory allocation can be a hard fault with no operating system standing by to catch it.
1. The Constraint Gap, Made Concrete
It is easy to read "memory-constrained" as a synonym for "needs some optimization" and move on. That undersells the regime by two or three orders of magnitude, and the gap is worth pinning down in real numbers before anything else in this chapter makes sense.
1.1 The reference points
A modern phone SoC ships gigabytes of RAM and tens of gigabytes of flash/UFS storage, running at clock speeds in the 2-4 GHz range across multiple cores, usually backed by a dedicated NPU and a full operating system managing virtual memory underneath the ML runtime. A server-class accelerator lives at yet another scale above that — tens to hundreds of gigabytes of HBM, terabytes-per-second of bandwidth. A microcontroller-class target — the class this whole chapter is about — looks nothing like either.
Take the author's own hardware as the anchor, since it is real, shipping, mid-range silicon rather than a cherry-picked worst case: a Nordic nRF52840, a 32-bit Arm Cortex-M4F running at 64 MHz, with 256 KB of RAM and 1 MB (1,024 KB) of flash, no operating system, no MMU, and no virtual memory. Everything the firmware ever touches is a fixed physical address decided at link time — there is no page table translating a "virtual" address into wherever the OS happened to put it, because there is no OS deciding that.
Laid out on a single log-scale axis, the gap looks like this:
Order-of-magnitude memory budget, RAM (rough, log scale):
KB 10 KB 100 KB 1 MB 10 MB 100 MB 1 GB 10 GB
|------|------|------|------|------|------|------|------>
^ ^ ^
Cortex-M0-class nRF52840 phone / server
(8-64 KB RAM) (256 KB RAM) (GB-scale RAM)
nRF52840 sits roughly four to five orders of magnitude
below phone-class RAM, and the entry-level Cortex-M0
tier sits a further order of magnitude below that.The 2020 paper introducing TensorFlow Lite Micro states the gap plainly, in numbers rather than a diagram: embedded processors' nearest mobile counterparts exhibit at least a 100-1,000x difference in compute capability, memory availability, and power consumption, simultaneously. That is not a difference optimization closes; it is a difference that determines which entire classes of model architecture and runtime design are admissible at all. A technique that is "a nice-to-have" at a 2x gap becomes structurally mandatory at a 1,000x gap, because there is no slack left to absorb the cost of not doing it.
1.2 MCU classes, roughly
Not every "microcontroller" is the same shape of constrained, and it is worth having a rough map before going further, because the reasoning in Sections 2 and 4 depends on which tier a given chip sits in:
Cortex-M0 / M0+ entry-level, no hardware FPU, no DSP extensions
typical RAM: 8-64 KB typical flash: 32-256 KB
clock: tens of MHz
Cortex-M3 mid-tier, no hardware FPU, has some DSP-adjacent instructions
typical RAM: 16-128 KB typical flash: 128 KB-1 MB
clock: tens to ~100 MHz
Cortex-M4F mid-range, single-precision hardware FPU, DSP/SIMD extensions
typical RAM: 64-512 KB typical flash: 256 KB-2 MB
clock: 48-180 MHz (nRF52840: 64 MHz, 256 KB RAM, 1 MB flash)
Cortex-M7F high-end MCU, single-precision FPU, deeper pipeline, often has cache
typical RAM: 512 KB-2 MB typical flash: 1-2 MB
clock: 200-480 MHzThese figures are rounded, general knowledge about the Cortex-M family rather than a single datasheet's numbers, and real parts vary considerably within each tier — but the shape of the ladder is what matters for everything downstream in this chapter: even the top of the microcontroller range sits comfortably inside the range that a phone treats as "not enough RAM to boot the operating system," and a meaningful fraction of real embedded products ship on the bottom two tiers, where there is no hardware floating-point support at all.
1.3 What a real TinyML model actually costs
The TensorFlow Lite Micro repository's own reference example is the cleanest illustration available of what "a small model" costs in absolute terms, because its size wasn't an accident — it was a design target. The person-detection example (a binary "is there a person in this frame" classifier, a MobileNetV1 variant with a 0.25 width multiplier and a further depth reduction) was deliberately tuned to satisfy the constraints of the Visual Wake Words Challenge: model size under 250 KB, peak memory usage under 250 KB, and inference cost under 60 million multiply-accumulate operations per inference. Those are not soft targets — the architecture's width and depth multipliers were chosen specifically so the exported int8 model fits inside a 250 KB flash budget, and the entire point of the exercise was proving that a genuinely useful vision model could be squeezed under that ceiling.
Put that model on the nRF52840 and the arithmetic is immediate:
nRF52840 (Cortex-M4F, 64 MHz) — total budget
Flash: 1,024 KB
RAM: 256 KB
TFLite Micro person-detection reference model (int8)
Model (weights + graph, flash-resident): ~250 KB -> ~24% of total flash
Peak tensor arena (RAM, per Lesson 5): ~250 KB -> ~98% of total RAM
What that leaves for everything else that has to run on the same chip:
Flash headroom for firmware, BLE/radio stack, bootloader, OTA slot: ~774 KB
RAM headroom for the call stack, ISR buffers, sensor FIFOs, BLE
connection buffers, and anything else running on the same core: ~6 KBA single small vision model can consume essentially the entire RAM budget of a real shipping chip, leaving single-digit kilobytes of headroom for literally everything else the firmware does. That number is precisely why this class of problem cannot be treated as "the same techniques from Lessons 1-9, just applied to a smaller number" — at this scale the constraint isn't a performance target to hit, it's a hard admission test the model has to pass before it can run at all. A model that is 5% too large for the arena does not run 5% slower; it fails to link, fails to allocate, or never boots. The remaining three sections of this chapter are, in effect, three independent answers to the same question: how does a model get small enough, and predictable enough, to pass that test.
2. Why int8 Is Close to Mandatory, Not an Optimization Choice
Lesson 3 built the affine quantization machinery — scale, zero-point, the int8 grid, per-channel weights — as a technique for shrinking a model and speeding it up on hardware that could, in principle, also run it in float32. On server and mobile targets, quantization is a choice with a real accuracy-versus-efficiency tradeoff to weigh, and plenty of production models still run in float16 or bfloat16 because the tradeoff favors it. On MCU-class hardware, that framing inverts: int8 (or lower) is close to the only mode of operation that is actually available, for two independent reasons that reinforce each other.
2.1 The floating-point question
The Cortex-M0 and M0+ cores — genuinely common in the cheapest, lowest-power tier of embedded products, per the table in Section 1.2 — have no hardware floating-point unit whatsoever. Every float32 multiply or add on those cores is emulated in software, by a soft-float library performing the equivalent operation across many integer instructions. That isn't a modest slowdown; it routinely costs one to two orders of magnitude versus a native float instruction, which for a workload already fighting for every cycle at tens of megahertz effectively rules float32 inference out as a serious option before any accuracy or memory argument even enters the picture.
Move up to Cortex-M4F or M7F — the "F" specifically denotes the floating-point unit — and there is real hardware float support, but two caveats immediately narrow it. First, it is single-precision only: no double-precision path, and no help if any part of a numerically sensitive computation wants more range than 32-bit float provides. Second, and more consequential for inference specifically, the FPU's per-cycle throughput on a single float32 multiply-accumulate is not actually the fast option once the DSP extension is in play. Cortex-M4 and M7 cores also carry SIMD-style integer instructions — packed 8-bit and 16-bit multiply-accumulate instructions like SMLAD, which retires two 16-bit MACs in a single cycle — and this is exactly the instruction class that CMSIS-NN, Arm's own optimized neural network kernel library for Cortex-M, is built around, packing multiple int8 multiply-accumulates into throughput a single-lane FPU cannot match:
Per-cycle MAC throughput, illustrative (exact figures depend on core and pipeline):
Software-emulated float32 (Cortex-M0/M0+, no FPU):
1 float32 MAC ~= tens of integer instructions -> far less than 1 MAC/cycle
Hardware FPU, float32 (Cortex-M4F/M7F):
1 float32 MAC ~= 1 MAC per FPU cycle (roughly, pipeline-dependent)
CMSIS-NN int8 SIMD kernels (Cortex-M4F/M7F, same core):
packed 8-bit/16-bit MAC instructions ~= multiple int8 MACs per cycle
-> the FPU is not the fast path for a quantized model on this hardware;
the integer SIMD pipeline is, and CMSIS-NN targets it directlyCMSIS-NN's own published results make this concrete rather than illustrative: running int8 inference through its SIMD kernels achieves 4.6x higher throughput and 4.9x better energy efficiency than a naive floating-point baseline on the same Cortex-M core. The FPU existing on a chip does not make float32 inference the sensible choice; the SIMD integer pipeline exists specifically because Arm designed it anticipating that NN inference would want it, and CMSIS-NN is the library written to use it.
2.2 The flash question
The second reason is entirely independent of compute throughput and just as binding: int8 shrinks the model's storage footprint, and flash is exactly as scarce as RAM on this class of chip. Every weight stored as a 32-bit float becomes a single byte under int8 quantization — an unconditional 4x reduction in the flash footprint of every weight tensor in the model, before any pruning, distillation, or architecture change is applied on top of it.
Go back to the arithmetic in Section 1.3: the person-detection model's entire architecture was constrained to fit inside a 250 KB flash budget. A float32 version of the identical architecture would need on the order of 1 MB just for weights — which is the entire flash budget of the nRF52840, with nothing left over for the firmware that has to load and run it, let alone the BLE stack, bootloader, or OTA update slot sitting alongside it. int8 is not shaving cost off a model that would otherwise fit; on this class of hardware it is frequently the difference between a model that fits in the flash partition available to it and one that categorically does not.
2.3 Where this leaves float32
Put those two reasons together and "quantize to int8" stops being an item on an optimization checklist and becomes closer to a precondition for the model existing on the target at all — the same way "compile for the target's instruction set" is a precondition rather than an optimization. Some TinyML research pushes further still, into int4, binary, and ternary weight networks for the very tightest RAM/flash budgets, trading additional accuracy for even smaller footprints — but int8 is the practical default this entire ecosystem has standardized on: it's what CMSIS-NN's kernels are written against, what TensorFlow Lite Micro's converter targets by default, and what the affine quantization math from Lesson 3 was built to produce in the first place. Float32 inference on a genuinely MCU-class target is not "the slow option" the way it might be on a server; it is, for a meaningful share of the hardware this ecosystem actually ships on, not an option at all.
3. Depthwise-Separable Convolutions: The Architectural Trick That Makes This Regime Work
Quantization shrinks the numbers a model computes with. It does nothing about how many multiply-accumulates the model's architecture demands in the first place — and for a convolutional vision model, that FLOP count is dominated by one operator: the standard convolution. The single architectural change that made small, MCU-viable vision models practical is replacing standard convolutions with depthwise-separable convolutions, introduced as the core building block of Google's MobileNet family. It's worth deriving precisely why the savings are so large, because the magnitude is easy to state vaguely ("it's more efficient") and much more convincing worked out in actual arithmetic.
3.1 Standard convolution: the FLOP count
A standard convolutional layer takes an input feature map of spatial size Df x Df with M input channels, and produces an output feature map of the same spatial size (assuming "same" padding) with N output channels, using a bank of N filters each of size Dk x Dk x M — one full-depth filter per output channel, since every output channel needs to see every input channel to be computed.
Each output pixel, in each of the N output channels, requires a Dk x Dk x M-element dot product — that's Dk^2 x M multiply-accumulates per output value. There are Df^2 output pixels per channel and N channels, so the total cost is:
Standard convolution — total multiply-accumulates:
cost_standard = Df^2 x Dk^2 x M x N
where:
Df = output spatial size (Df x Df)
Dk = kernel size (Dk x Dk)
M = input channels
N = output channels3.2 Depthwise-separable convolution: the same job, factored into two cheaper steps
A depthwise-separable convolution replaces that single operator with two much cheaper ones run back-to-back:
- Depthwise convolution — apply one
Dk x Dkfilter per input channel, independently, producingMoutput feature maps fromMinput channels with no cross-channel mixing at all. Cost:Df^2 x Dk^2 x M— the same per-pixel filtering cost as before, but with theNfactor gone entirely, because there is no longer a separate full-depth filter per output channel. - Pointwise convolution — a plain
1 x 1convolution across allMchannels, projecting toNoutput channels. This is where the cross-channel mixing that the depthwise step skipped actually happens. Cost:Df^2 x M x N— a standard convolution formula withDkcollapsed to 1.
Depthwise-separable convolution — total multiply-accumulates:
cost_depthwise = Df^2 x Dk^2 x M (per-channel spatial filtering)
cost_pointwise = Df^2 x M x N (1x1 cross-channel mixing)
cost_dsc = cost_depthwise + cost_pointwise
= Df^2 x Dk^2 x M + Df^2 x M x N
= Df^2 x M x (Dk^2 + N)3.3 The ratio: deriving 1/N + 1/k^2
Divide the depthwise-separable cost by the standard convolution cost, and the Df^2 and M factors — which appear in both — cancel completely:
cost_dsc Df^2 x M x (Dk^2 + N) Dk^2 + N
----------- = --------------------------- = -----------
cost_standard Df^2 x Dk^2 x M x N Dk^2 x N
Split the fraction across the two terms of the numerator:
= Dk^2 / (Dk^2 x N) + N / (Dk^2 x N)
= 1/N + 1/Dk^2That is the full derivation of the result stated at the top of this section: a depthwise-separable convolution costs 1/N + 1/k^2 of a standard convolution with the same input/output channel counts and kernel size — not "roughly," but exactly, as a direct algebraic consequence of factoring one full-depth filter bank into a per-channel filter plus a 1x1 mixing step. Notice that the same cancellation applies identically to the parameter count (drop every Df^2 term from both formulas above and the ratio is unchanged) — depthwise-separable convolutions shrink weight count and compute cost by the same factor, which is exactly why they help both the FLOP budget and the flash-resident weight budget from Section 2.2 at once.
Two things fall out of the formula that are worth naming explicitly. First, the 1/N term shrinks as the layer gets wider — deeper stages of a CNN, which typically have more output channels, benefit more from this factoring, not less. Second, the 1/Dk^2 term is fixed by kernel size alone: a 3x3 kernel (Dk^2 = 9) contributes 1/9, about 0.111, to the ratio regardless of how many channels are involved, which is why 3x3 is the kernel size MobileNet-family architectures standardize on — it's small enough that the depthwise step's own cost stays a minor share of the total.
3.4 A concrete worked example
Take a mid-stage layer shape typical of a small vision model: a 28 x 28 feature map (Df = 28), a 3 x 3 kernel (Dk = 3), going from 64 input channels to 128 output channels (M = 64, N = 128):
| Metric | Standard convolution | Depthwise-separable | Ratio |
|---|---|---|---|
| Multiply-accumulates | 57,802,752 (~57.8M) | 6,874,112 (~6.87M) | ~11.9% (~8.4x fewer) |
| Parameters | 73,728 | 8,768 | ~11.9% (~8.4x fewer) |
Checking against the closed-form ratio: 1/N + 1/Dk^2 = 1/128 + 1/9, which works out to approximately 0.00781 + 0.11111 = 0.11892 — matching 6,874,112 / 57,802,752, approximately 0.11892, exactly. An 8.4x reduction in both FLOPs and parameters, for identical input/output shapes and identical receptive field: the depthwise-separable layer sees exactly the same 3x3 neighborhood as the standard layer would, it just factors the computation instead of doing it in one dense step. This lines up closely with the number the original MobileNet paper reports for its own architecture: depthwise-separable convolutions save on the order of 88% of the multiply-adds a comparable standard-convolution network would need, for the 3x3-kernel, many-channel shapes that dominate a real vision backbone — consistent with the roughly 8-9x reduction the 1/N + 1/k^2 formula predicts once N is large enough that the 1/N term becomes small relative to 1/Dk^2.
That factor compounds across an entire network. A vision backbone built almost entirely from 3x3 standard convolutions and one built almost entirely from 3x3 depthwise-separable convolutions are not "somewhat more efficient" relative to each other — they are operating in different orders of magnitude of both compute and weight footprint, for the same accuracy-relevant receptive field. That is precisely why depthwise-separable convolutions, not merely "smaller versions of a standard CNN" or "fewer layers," are the specific architectural move that makes vision-class TinyML models fit inside a 250 KB flash budget at all. Pruning, distillation, and int8 quantization all shrink a model after it has been designed; depthwise-separable convolutions change what the model costs by construction, which is why MobileNet-family and purpose-built tiny architectures lean on this operator as their default rather than their exception.
3.5 Beyond vision: the same factoring over time
Nothing about the derivation in Section 3.3 is specific to two spatial dimensions. A 1x1 pointwise step mixing channels after a per-channel filter is just as valid over a 1D time-series window — a Dk-tap depthwise filter per input channel, followed by a 1x1 convolution across channels — for any sensor stream sampled at a fixed rate: accelerometer or gyroscope traces, PPG waveforms, audio frames feeding a keyword-spotting model. The channel count M in that setting is typically the number of input axes or preprocessed feature channels rather than image color channels, and Dk is a tap count along time rather than a spatial kernel width, but the ratio 1/N + 1/Dk^2 (or, for a 1D kernel, 1/N + 1/Dk, since there is only one spatial dimension being filtered rather than two) is the identical algebraic structure applied to a different axis. Any CNN-style architecture reading fixed-rate sensor windows on an MCU inherits the same lever this section derived for image models, for the same reason: the depthwise step captures per-channel local structure cheaply, and the pointwise step is where the (comparatively expensive) cross-channel mixing is deliberately concentrated into the smallest operator that can still do it.
4. Static and Ahead-of-Time Everything
Lesson 5 established static memory planning as a technique that inference can use and training generally can't: a fixed, forward-only graph with known shapes lets a compiler simulate the whole execution on paper and hand every tensor a fixed (buffer, offset) before a single operator runs, collapsing what would otherwise be scattered malloc/free calls into one arena allocated exactly once. On a server or a phone, that is a genuine optimization — it cuts allocator overhead and fragmentation, and a runtime that skipped it would still function, just slower and less predictably. On MCU-class hardware, static planning stops being an optimization and becomes the only way the system works at all, for a reason that is almost purely about what happens when it fails.
4.1 There is no OS standing behind a failed allocation
On a general-purpose system, a failed malloc returns null, the caller checks for it, and — worst case — the OS's out-of-memory handling kicks in, a process gets killed, something else keeps running. On bare-metal MCU firmware there frequently is no OS at all, no virtual memory to page out to make room, and no process boundary to sacrifice. A heap-exhaustion failure on a device like this is not a caught exception; it is very often a hard fault, a stack corruption, or a silent write into memory that belongs to something else, discovered only much later as a mysterious crash or a watchdog reset in the field. There is no graceful degradation path to fall back to, because graceful degradation is itself a service an OS provides, and the OS isn't there.
Given that failure mode, the actual engineering answer isn't "write more careful allocation-failure handling" — it's "never call the allocator at runtime in the first place, and prove that statically before the firmware ships." Static memory planning, introduced in Lesson 5 as a technique for reducing peak memory, is on this class of hardware the technique that turns "might run out of memory at 3am in the field" into "provably cannot run out of memory, because the entire memory plan was solved and verified at compile time, before a single byte of firmware was flashed."
4.2 TensorFlow Lite Micro's design pillars
TensorFlow Lite Micro is the clearest real-world statement of this philosophy, and it's worth naming its specific design pillars rather than gesturing at "it's embedded-friendly" in the abstract, because each pillar maps to a concrete constraint from earlier in this chapter:
- No dynamic memory allocation. The entire interpreter operates out of the single tensor arena Lesson 5 already covered — one buffer, provided by the application up front, with every tensor's offset resolved by the
MicroAllocatorbefore inference begins. There is nomalloccall anywhere in the inference hot path, by design, not by convention. - No operating system dependencies. The runtime makes no assumption that a filesystem, threads, dynamic library loading, or process isolation exist underneath it, because on the majority of its actual deployment targets none of those things do.
- C++11 with no standard-library dependency. Avoiding the C++ standard library sidesteps exactly the kind of code that tends to allocate quietly and unpredictably underneath an innocuous-looking call — a
std::vectorthat resizes, astd::stringthat heap-allocates past its small-string-optimization threshold — any of which would silently reintroduce the dynamic allocation the rest of the design goes to considerable lengths to eliminate.
That third pillar is worth dwelling on, because it's the one people most often underestimate: a codebase can be scrupulously careful about never calling malloc directly, and still smuggle dynamic allocation back in through a standard-library container that allocates on its owner's behalf. Static-memory-everywhere is not a property of the top-level allocator call sites alone; it has to be a property enforced transitively through every layer the runtime touches, all the way down to the kernel implementations doing the actual arithmetic.
4.3 Operator selection is static too, for the same reason
A general-purpose ML runtime typically resolves which kernel implementation to call for a given operator through some form of dynamic dispatch or a runtime-populated operator registry — flexible, but it means the binary has to be able to represent any op the format supports, whether or not this particular model ever uses one. TFLite Micro instead uses a MicroMutableOpResolver that the application populates, at compile time, with only the specific operators the specific model actually needs.
Dynamic op resolution (general-purpose runtime):
model file -> "this graph uses op #14" -> registry lookup at runtime
-> binary must contain code for every op the format supports
Static op resolution (TFLite Micro's MicroMutableOpResolver):
application code -> resolver.AddDepthwiseConv2D()
-> resolver.AddFullyConnected()
-> resolver.AddSoftmax()
(only the ops this specific model actually uses)
-> linker pulls in only those kernel implementations
-> everything else is simply absent from the binaryThe benefit isn't purely stylistic: it means the linker only pulls in code for operators genuinely present in the graph, directly shrinking the flash footprint of the firmware binary itself — the same flash budget that Section 1.3's arithmetic showed has essentially no slack once the model is loaded.
4.4 Where CMSIS-NN sits in the stack
The leaf level of this whole stack is exactly where CMSIS-NN comes back in. Once the arena has handed every tensor its fixed offset and the op resolver has statically bound every operator to a concrete kernel, the actual arithmetic — the int8 multiply-accumulates from Section 2, run through the depthwise and pointwise convolutions from Section 3 — executes inside CMSIS-NN's hand-optimized Cortex-M kernels, which themselves allocate nothing: they read from and write to the offsets they're handed, using scratch space that was itself sized and planned into the arena ahead of time. Static planning, int8 arithmetic, and the depthwise-separable architecture aren't three independent techniques bolted together — they're three layers of the same governing constraint, each closing off a different way this class of hardware could otherwise run out of room.
5. Putting It Together: A Full Pipeline on Cortex-M4F
It's worth walking the whole stack end to end once, on hardware shaped like the author's own, because the four sections above read as separate arguments until they're seen operating on the same model at the same time.
Consider a representative on-device classifier reading windows from an onboard IMU or PPG sensor at a few hundred to a few thousand samples per second — a tap-detection, gesture, or activity-segmentation model, structurally the same shape of problem as the vision example in Section 1.3, just with a 1D sensor stream instead of a camera frame. The pipeline that gets a trained float32 model onto the chip and running looks like this:
1. Trained model (float32, workstation)
-> weights and activations in 32-bit float, no size constraint yet
2. Architecture built from depthwise-separable convs (Section 3)
-> 1D depthwise filter per sensor-channel/feature, then 1x1 pointwise
mixing across channels; FLOPs and parameters both reduced by
the 1/N + 1/Dk factor derived above, before quantization even starts
3. Post-training (or quantization-aware) int8 conversion (Section 2, Lesson 3)
-> every weight: 4 bytes -> 1 byte, flash footprint divided by ~4
-> every activation: quantized to int8, matching CMSIS-NN's native dtype
4. Offline memory planning (Lesson 5) against a fixed input window shape
-> every tensor gets a compile-time-resolved (buffer, offset) pair
-> peak arena size becomes a known constant, checked against the
256 KB RAM budget before the firmware is ever flashed
5. Static op resolution (Section 4.3)
-> MicroMutableOpResolver registers only the ops this model uses
-> linker drops everything else; flash footprint stays bounded
6. On-device execution (Cortex-M4F, 64 MHz)
-> CMSIS-NN int8 SIMD kernels read/write only the arena's fixed
offsets; zero allocator calls anywhere in the inference path
-> deterministic peak RAM, deterministic flash size, no path to
an unhandled out-of-memory fault at runtimeEvery step in that pipeline is a direct application of one of the four techniques covered above, and every step exists because skipping it reopens exactly the failure mode the others were built to close: skip step 2 and the FLOP count may exceed what 64 MHz can deliver inside the latency budget Part 1 of this lesson established; skip step 3 and the model may not fit in flash at all, or may run an order of magnitude slower on cores without native float32 SIMD; skip step 4 and peak RAM becomes a runtime question instead of a compile-time-verified constant; skip step 5 and the firmware binary carries kernel code for operators the model never calls, eating into flash that has no slack to give. None of these are independent optimizations competing for engineering time — they are four load-bearing walls of the same structure, and on hardware this constrained, removing any one of them tends to bring the other three down with it.
Further Reading
- David, R., Duke, J., Jain, A., et al., "TensorFlow Lite Micro: Embedded Machine Learning on TinyML Systems" — the authoritative paper behind TFLM's design, including the 100-1,000x mobile-versus-embedded resource gap cited in Section 1.
- Lai, L., Suda, N., & Chandra, V., "CMSIS-NN: Efficient Neural Network Kernels for Arm Cortex-M CPUs" — the paper behind Arm's optimized int8/int16 NN kernel library, including the 4.6x throughput / 4.9x energy-efficiency numbers cited in Section 2.
- Arm, "CMSIS-NN Software Library" — the current library documentation, covering the SIMD kernel variants selected per Cortex-M target at compile time.
- Howard, A. G., Zhu, M., Chen, B., et al., "MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications" — the original depthwise-separable convolution architecture and its reported FLOP savings, underlying the derivation in Section 3.
- TensorFlow / tflite-micro, "Person Detection: Training a Model" — the reference example whose 250 KB flash / 60M-MAC constraints anchor the worked memory budget in Section 1.
- GeeksforGeeks, "Depth Wise Separable Convolutional Neural Networks" — an accessible walkthrough of the depthwise/pointwise split for readers who want a second pass at the mechanics before the FLOP algebra above.
Part 3 of Lesson 10 moves from the software discipline of fitting inference into a fixed budget to the hardware built specifically to blow that budget open: NPU architecture, dataflow, and the hardware/software co-design decisions that separate a general-purpose core running optimized kernels from silicon designed around the MAC array from the very first transistor.