| name | tinyml-mcu-inference |
| description | Inférence ML optimisée sur microcontrôleurs — CMSIS-NN (ARM Cortex-M), Xtensa NN (ESP32), RV32IMC (RISC-V), kernels optimisés, SIMD vectoriel, DSP intrinsics, mémoire TCM/SRAM, pipeline DMA, benchmark cross-platform et débogage. |
| version | 1.0.0 |
| author | EVA |
| license | Privée EVA St-Étienne |
| platforms | ["linux","macos","windows"] |
| metadata | {"EVA":{"tags":["tinyml","cmsis-nn","arm-cortex-m","xtensa","risc-v","simd","dsp","dma","microcontroller","optimized-kernels"],"related_skills":["tinyml-fundamentals","esp32-s3-deep-learning","tensorflow-lite-deep-dive","model-optimization-edge"]}} |
Inférence ML sur Microcontrôleurs
Vue d'ensemble
L'inférence ML sur microcontrôleurs (µC) nécessite des kernels extrêmement optimisés qui exploitent chaque instruction et byte de mémoire disponibles. Cette skill couvre en profondeur les bibliothèques de kernels ML par architecture (ARM Cortex-M CMSIS-NN, Xtensa LX7 ESP-NN, RISC-V P-ext), les techniques SIMD/DSP, la gestion mémoire TCM/SRAM, et le déploiement cross-platform.
Architectures cibles et optimisations
| Architecture | µC représentatifs | Instructions SIMD/DSP | Bibliothèque kernels | Perf relative |
|---|
| Cortex-M4/M7 | STM32F4/H7, nRF52 | SIMD (16-bit) + DSP (MAC single-cycle) | CMSIS-NN | 1× (réf) |
| Cortex-M33 | STM32U5, NXP i.MX RT | Helium (MVE) 128-bit SIMD | CMSIS-NN v2 | 1.5-3× |
| Cortex-M55 | Alif Ensemble | Helium (MVE) + Matrix | CMSIS-NN v2 | 2-4× |
| Cortex-M85 | NXP i.MX RT700 | Helium (MVE 256-bit) | CMSIS-NN v2 | 3-5× |
| Xtensa LX6/LX7 | ESP32, ESP32-S3 | PIE (instruction vectorielle) | ESP-NN | 1-2× |
| RISC-V (P-ext) | GD32V, Bouffalo | P extension (SIMD/DSP) | NMSIS-NN | 0.8-1× |
1. CMSIS-NN (ARM Cortex-M)
1.1 Architecture CMSIS-NN
CMSIS-NN (Cortex Microcontroller Software Interface Standard - Neural Networks) est la bibliothèque de référence pour l'inférence ML sur ARM Cortex-M. Elle est intégrée à TFLite Micro et utilisée par l'écosystème ARM.
1.2 Convolution INT8 optimisée CMSIS-NN
#include "arm_nnfunctions.h"
#include "arm_nnsupportfunctions.h"
typedef struct {
int16_t *im2col_buffer;
int16_t *scratch_buffer;
int buf_size;
} ConvContext;
arm_cmsis_nn_status conv2d_s8_cmsis(
const int8_t *input,
const int16_t *input_dims,
const int8_t *weights,
const int16_t *weight_dims,
const int32_t *bias,
int8_t *output,
const int16_t *output_dims,
int stride_h, int stride_w,
int pad_h, int pad_w,
int32_t input_offset, int32_t output_offset,
int32_t *output_mult, int32_t *output_shift,
int32_t activation_min, int32_t activation_max,
int8_t *buffer_a,
*buffer_b
) {
cmsis_nn_conv_params conv_params;
conv_params.input_offset = input_offset;
conv_params.output_offset = output_offset;
conv_params.stride.h = stride_h;
conv_params.stride.w = stride_w;
conv_params.padding.h = pad_h;
conv_params.padding.w = pad_w;
conv_params.activation.min = activation_min;
conv_params.activation.max = activation_max;
cmsis_nn_dims input_d;
input_d.n = input_dims[];
input_d.h = input_dims[];
input_d.w = input_dims[];
input_d.c = input_dims[];
cmsis_nn_dims weight_d;
weight_d.n = weight_dims[];
weight_d.h = weight_dims[];
weight_d.w = weight_dims[];
weight_d.c = weight_dims[];
cmsis_nn_dims output_d;
output_d.n = output_dims[];
output_d.h = output_dims[];
output_d.w = output_dims[];
output_d.c = output_dims[];
cmsis_nn_dims filter_d = {weight_d.n, weight_d.h, weight_d.w, weight_d.c};
arm_convolve_wrapper_s8(
&conv_params,
&input_d,
input,
&filter_d,
weights,
&weight_d,
bias,
&output_d,
output,
output_mult,
output_shift,
buffer_a,
buffer_b
);
}
{
start = DWT->CYCCNT;
arm_convolve_wrapper_s8(...);
cycles = DWT->CYCCNT - start;
us = cycles / ()SystemCoreClock * f;
(, cycles, us);
cycles;
}
1.3 CMSIS-NN sur Cortex-M55 (Helium)
__attribute__((always_inline))
static void helium_conv_partial(const int8_t *input,
const int8_t *weights,
int32_t *accum,
int n_elements) {
int8x16_t w_vec = vld1q_s8(weights);
int i;
for (i = 0; i < n_elements - 15; i += 16) {
int8x16_t i_vec = vld1q_s8(&input[i]);
int16x8_t prod_lo = vmull_s8(vget_low_s8(w_vec), vget_low_s8(i_vec));
int16x8_t prod_hi = vmull_s8(vget_high_s8(w_vec), vget_high_s8(i_vec));
accum[0] += vaddlvq_s16(prod_lo);
accum[0] += vaddlvq_s16(prod_hi);
}
for (; i < n_elements; i++) {
accum[0] += weights[i] * input[i];
}
}
1.4 Intégration CMSIS-NN dans TFLite Micro
set(TFLITE_MICRO_CMAKE_DIR "${TFLITE_ROOT}/tensorflow/lite/micro/tools/cmake")
add_subdirectory("${TFLITE_MICRO_CMAKE_DIR}" "${CMAKE_BINARY_DIR}/tflite-micro")
target_compile_definitions(tflite-micro PRIVATE
CMSIS_NN=1
ARM_MATH_DSP=1
ARM_MATH_LOOPUNROLL=1
)
target_link_libraries(tflite-micro PRIVATE
cmsis-nn
cmsis-dsp
)
2. Optimisations SIMD/DSP par Architecture
2.1 ARM Cortex-M4/M7 (SIMD 16-bit)
#include "arm_math.h"
int32_t dot_product_simd(const int16_t *a, const int16_t *b, int n) {
int32_t result = 0;
for (int i = 0; i < n - 1; i += 2) {
__asm volatile(
"SMLAD %0, %1, %2, %0"
: "+r"(result)
: "r"(__PKHBT(a[i], a[i+1], 16)),
"r"(__PKHBT(b[i], b[i+1], 16))
);
}
if (n % 2) {
result += a[n-1] * b[n-1];
}
return result;
}
int32_t dot_product_simd_unrolled {
result = ;
__asm ;
result;
}
2.2 ARM Cortex-M55 (Helium 128-bit)
#include "arm_mve.h"
void conv1d_helium(const int8_t *input, const int8_t *kernel,
int32_t *output, int len, int klen) {
for (int i = 0; i < len; i++) {
int32x4_t acc = vdupq_n_s32(0);
int j;
for (j = 0; j < klen - 3; j += 4) {
int8x16_t in = vld1q_s8(&input[i + j]);
int8x16_t ker = vld1q_s8(&kernel[j]);
acc = vmladavaq_s8(acc, in, ker);
}
for (; j < klen; j++) {
output[i] += input[i + j] * kernel[j];
}
output[i] += vaddvq_s32(acc);
}
}
2.3 Xtensa LX7 (ESP32-S3 — PIE)
#include "esp_nn.h"
void conv_pie_manual(const int8_t *input, const int8_t *kernel,
int8_t *output, int n_elements) {
__asm__ volatile(
"AE_SETQ_AA a0, %0\n"
"AE_SETQ_AA a1, %1\n"
"AE_SETQ_AA a2, %2\n"
:
: "r"(input), "r"(kernel), "r"(output)
);
for (int i = 0; i < n_elements / 8; i++) {
__asm__ volatile(
"AE_LS8X3_IP %0, %1, %2, 24\n"
"AE_MULAAP8S_AAAA %0, %1\n"
:
: "r"(input), (kernel)
:
);
}
}
2.4 RISC-V (P-ext ou V-ext)
#include "riscv_nnfunctions.h"
#include "riscv_nnsupportfunctions.h"
riscv_nn_status riscv_convolve_s8(
const int8_t *input,
const int16_t *dim_input,
const int8_t *weights,
const int16_t *dim_weights,
const int32_t *bias,
int8_t *output,
const int16_t *dim_output,
int32_t input_offset,
int32_t output_offset,
int32_t *output_mult,
int32_t *output_shift,
int32_t activation_min,
int32_t activation_max,
int16_t *buffer_a
) {
return riscv_convolve_wrapper_s8(
input, dim_input, weights, dim_weights,
bias, output, dim_output,
input_offset, output_offset,
output_mult, output_shift,
activation_min, activation_max,
buffer_a
);
}
3. Gestion Mémoire pour Inférence
3.1 Hiérarchie mémoire Cortex-M
3.2 Placement mémoire des modèles
// Linker script — placement optimisé pour TFLite Micro
MEMORY
{
/* Flash : modèle TFLite (lecture seule) */
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
/* SRAM principale : utilisation générale */
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 256K
/* DTCM : Tensor Arena (accès critique 0-wait) */
DTCM (rw) : ORIGIN = 0x10000000, LENGTH = 64K
/* ITCM : code critique (boucles d'inférence) */
ITCM (rx) : ORIGIN = 0x00000000, LENGTH = 16K
}
SECTIONS
{
/* 1. Modèle TFLite en flash */
.tflite_model : ALIGN(4) {
KEEP(*model_data.o(.rodata*))
} > FLASH
/* 2. Code critique d'inférence en ITCM */
.itcm_code : ALIGN(4) {
*cmsis_nn_conv*.o(.text*)
*cmsis_nn_pool*.o(.text*)
*arm_convolve*.o(.text*)
} > ITCM AT > FLASH
/* 3. Tensor Arena en DTCM */
.tensor_arena (NOLOAD) : ALIGN(16) {
. += 60K; /* 60 KB pour Tensor Arena */
} > DTCM
/* 4. Buffers de travail en SRAM */
.work_buffers (NOLOAD) : ALIGN(4) {
. += 32K; /* Buffers im2col + scratch */
} > RAM
}
3.3 DMA pour le chargement des poids
#include "stm32h7xx_hal.h"
DMA_HandleTypeDef hdma;
typedef struct {
int8_t *ping_buffer;
int8_t *pong_buffer;
volatile int active_buffer;
volatile int dma_busy;
} DMADoubleBuffer;
void dma_init_ping_pong(DMADoubleBuffer *db, int buffer_size) {
db->ping_buffer = (int8_t *)0x30000000;
db->pong_buffer = (int8_t *)0x30020000;
db->active_buffer = 0;
db->dma_busy = 0;
hdma.Instance = MDMA_Channel0;
hdma.Init.Request = MDMA_REQUEST_SW;
hdma.Init.Priority = MDMA_PRIORITY_HIGH;
hdma.Init.SourceInc = MDMA_SRC_INC_BYTE;
hdma.Init.DestInc = MDMA_DEST_INC_BYTE;
hdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_BYTE;
hdma.Init.DestDataSize = MDMA_DEST_DATASIZE_BYTE;
hdma.Init.DataAlignment = MDMA_DATAALIGN_PACK;
hdma.Init.BufferTransferLength = buffer_size;
HAL_MDMA_Init(&hdma);
}
void inference_with_dma(TFLiteInterpreter *interp, DMADoubleBuffer *db) {
int current = db->active_buffer;
int8_t *target = current ? db->pong_buffer : db->ping_buffer;
HAL_MDMA_Start_IT(&hdma,
(uint32_t)interp->model_data,
(uint32_t)target,
interp->model_size);
db->dma_busy = ;
(db->dma_busy) {
preprocess_sensor_data();
}
(interp->tensor_arena, target, interp->allocated_bytes);
interp->Invoke();
db->active_buffer = !current;
}
{
DMADoubleBuffer *db = get_dma_context(hmdma);
db->dma_busy = ;
}
4. Techniques Avancées
4.1 Sub-byte quantization (INT4/INT2)
void load_int4_weights(const uint8_t *packed, int8_t *unpacked, int n) {
for (int i = 0; i < n / 2; i++) {
uint8_t byte = packed[i];
unpacked[2*i] = (int8_t)((byte & 0x0F) << 4) >> 4;
unpacked[2*i+1] = (int8_t)((byte & 0xF0)) >> 4;
}
}
void conv_s8_int4_weights(
const int8_t *input,
const uint8_t *weights_int4,
int8_t *output,
int input_c, int output_c,
int kernel_size
) {
int8_t weights_unpacked[output_c * input_c * kernel_size * kernel_size];
load_int4_weights(weights_int4, weights_unpacked,
output_c * input_c * kernel_size * kernel_size);
arm_convolve_wrapper_s8(
...
weights_unpacked,
...
);
}
4.2 Winograd pour convolutions 3×3
4.3 Scratch buffer optimisé
typedef struct {
int8_t *im2col_buffer;
int32_t *accum_buffer;
int size;
} ScratchBuffer;
int taille_scratch_optimale(
int max_kh, int max_kw, int max_c_in,
int max_c_out
) {
int im2col_size = max_kh * max_kw * max_c_in * sizeof(int16_t);
int accum_size = 2 * max_c_out * sizeof(int32_t);
return (im2col_size > accum_size) ? im2col_size : accum_size;
}
5. Benchmark Cross-Platform
5.1 Harness de benchmark
typedef struct {
uint32_t cycles;
float time_us;
int bytes_processed;
float ops_per_cycle;
} BenchResult;
BenchResult bench_conv2d(
int input_h, int input_w, int input_c,
int output_c, int kernel_size, int stride
) {
BenchResult result = {0};
int output_h = (input_h - kernel_size) / stride + 1;
int output_w = (input_w - kernel_size) / stride + 1;
int macs = output_h * output_w * output_c * input_c * kernel_size * kernel_size;
uint32_t start = DWT->CYCCNT;
arm_convolve_wrapper_s8(...);
result.cycles = DWT->CYCCNT - start;
result.time_us = result.cycles / (float)SystemCoreClock * 1e6f;
result.ops_per_cycle = (float)macs / result.cycles;
result.bytes_processed = input_h * input_w * input_c +
output_h * output_w * output_c;
return result;
}
void benchmark_complet() {
printf("=== Benchmark ML µC ===\n");
printf("CPU: %d MHz\n", SystemCoreClock / 1000000);
BenchResult r1 = bench_conv2d(, , , , , );
(,
r1.time_us, r1.ops_per_cycle);
BenchResult r2 = bench_conv2d(, , , , , );
(,
r2.time_us, r2.ops_per_cycle);
BenchResult r3 = bench_depthwise_conv(, , , , );
(,
r3.time_us, r3.ops_per_cycle);
BenchResult r4 = bench_fully_connected(, );
(,
r4.time_us, r4.ops_per_cycle);
}
5.2 Résultats attendus (Cortex-M4 @ 120 MHz)
| Couche | Type | µs | MAC/cycle |
|---|
| Conv 3×3, stride 1, 3→16 (32×32) | Standard | 1250 | 0.85 |
| Conv 3×3, stride 2, 3→16 | Standard | 410 | 0.88 |
| DW Conv 3×3, stride 1, 16 | Depthwise | 310 | 1.62 |
| FC 8→16 | Dense | 0.2 | 0.72 |
| Conv 3×3 + ReLU | Fused | 1270 | 0.83 |
6. Débogage et Profilage
6.1 Utilisation du DWT (Data Watchpoint and Trace)
void init_dwt() {
if (!(CoreDebug->DEMCR & CoreDebug_DEMCR_TRCENA_Msk)) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
}
uint32_t get_cycle_count() {
return DWT->CYCCNT;
}
void profile_layer(const char *name, void (*layer_fn)(void)) {
init_dwt();
uint32_t start = get_cycle_count();
layer_fn();
uint32_t cycles = get_cycle_count() - start;
printf("%-20s : %6u cycles (%.1f µs)\n",
name, cycles, cycles / (float)SystemCoreClock * 1e6f);
}
6.2 Vérification de l'intégrité mémoire
#define CANARY_VALUE 0xDEADBEEF
typedef struct {
uint32_t canary_start;
uint8_t arena[TENSOR_ARENA_SIZE];
uint32_t canary_end;
} SafeTensorArena;
SafeTensorArena safe_arena;
void init_safe_arena() {
safe_arena.canary_start = CANARY_VALUE;
safe_arena.canary_end = CANARY_VALUE;
}
int check_arena_overflow() {
int ok = 1;
if (safe_arena.canary_start != CANARY_VALUE) {
printf("ERREUR: Canary start corrompu (début de l'arena)\n");
ok = 0;
}
if (safe_arena.canary_end != CANARY_VALUE) {
printf("ERREUR: Canary end corrompu (dépassement de l'arena)\n");
ok = 0;
}
return ok;
}
Pièges Courants
-
Alignement mémoire : les opérations SIMD nécessitent un alignement 16 bytes. Utiliser __attribute__((aligned(16))) ou memalign().
-
Cache coherency : sur les Cortex-M7, le cache peut retourner des données périmées après un DMA. Invalider le cache avec SCB_InvalidateDCache_by_Addr().
-
Im2col buffer overflow : im2col réarrange les données d'entrée et peut exploser la RAM. Pour une conv 3×3 stride 1 avec 16 entrées : 3×3×16 = 144 bytes par pixel de sortie. Vérifier la taille.
-
Helium/MVE non disponible : certains Cortex-M55 en configuration basse consommation désactivent Helium. Vérifier __ARM_FEATURE_MVE.
-
PIE sur ESP32-S3 obsolète : PIE est disponible sur S3 mais pas S2. Vérifier le target dans sdkconfig. CONFIG_ESP32S3_PIE=y.
-
RISC-V P-ext immature : la P-extension RISC-V n'est pas encore finalisée (draft v0.5.1). Les implémentations varient entre fabricants.
-
Optimisation -O0 vs -O3 : les kernels CMSIS-NN nécessitent -O2 minimum pour activer les intrinsics SIMD. Sans optimisation, les performances chutent de 10×.
-
Hardware divider : vérifier que __FPU_PRESENT et __DSP_PRESENT sont définis. Sans FPU, les calculs d'échelle (mult/ shift) utilisent des softmath lentes.
Références