8.12.3.14. Generated Artifacts Overview
8.12.3.14.1. Table of Contents
8.12.3.14.2. 1. Overview
When ModelMaker runs with ondevice_training: true and quantization: 2, it generates a set of files that together define everything needed to run integer on-device training on a microcontroller. This page provides a complete map of all generated files, what each contains and where it fits in the system.
8.12.3.14.3. 2. Artifact Summary
File |
Integer ODT-Specific? |
Purpose |
|---|---|---|
|
No (same as inference) |
Compiled frozen model (TVM output, includes PREQUANT for full model) |
|
No |
API header for the frozen model |
|
No |
Feature extraction config |
|
No |
Golden vectors for device validation |
|
Yes |
Defines: INPUT_SIZE, N_CLASSES, BATCH_SIZE, TARGET_MAG arrays, extern layer decls |
|
Yes |
INT8 weights, INT32 offsets, k/mult arrays, layer descriptors, model context |
|
Yes |
Dimension defines, extern declarations for training/val/test arrays |
|
Yes |
Float training samples (TRAIN_INPUTS) and INT8 labels (TRAIN_LABELS) |
Note on mod.a for integer ODT: the frozen model compiled for integer ODT is different from the standard inference mod.a. For full model ODT, this mod.a contains only the PREQUANT block (float→INT8), not the full inference model. For partial model ODT, it contains PREQUANT plus the frozen compute layers. Never mix a standard inference mod.a with integer ODT artifacts.
8.12.3.14.4. 3. Standard Artifacts
8.12.3.14.4.1. mod.a: Compiled Frozen Model
Compiled by TVM from frozen_model/model.onnx. For integer ODT this model is different from standard inference:
Full model ODT: mod.a implements only the PREQUANT operation, float32 input → INT8 output. The entire model computation happens in the integer ODT C library. The TVM model is essentially a per-channel float-to-int8 quantizer.
Partial model ODT: mod.a implements PREQUANT + frozen compute layers. Its output is UINT8 (because the last frozen layer will have ReLU and its output is in [0,255]).
Calling pattern in application code:
struct tvmgen_default_inputs inputs = { .input = (void *)raw_float_input };
struct tvmgen_default_outputs outputs = { .output = (void *)frozen_output_buf };
tvmgen_default_run(&inputs, &outputs);
// frozen_output_buf now has INT8 (full model) or UINT8 (partial model) data
// ready to pass to INTODT_Forward
8.12.3.14.4.2. tvmgen_default.h: Frozen Model API
Same header as standard inference. Declares the input/output structs and the tvmgen_default_run() function.
8.12.3.14.4.3. user_input_config.h: Feature Extraction Config
Same format as standard inference. Defines feature extraction flags (FFT, binning, logging) and parameters (frame size, feature size, channel count). Used by the feature_extract library before calling TVM.
8.12.3.14.4.4. test_vector.c: Golden Vectors
Contains a known input-output pair computed on the PC. Used for device-side validation after flashing.
8.12.3.14.5. 4. Integer ODT-Specific Artifacts
8.12.3.14.5.1. 4.1 integer_model_config.h
This header defines all compile-time constants and declares the model descriptor arrays. It is the first file to look at when understanding a generated model.
Full annotated example:
#ifndef INTEGER_MODEL_CONFIG_H
#define INTEGER_MODEL_CONFIG_H
#include "integer_odl.h" // IntLayerDesc_t, IntModelCtx_t, IntLayerType_t
// ── Model mode ──────────────────────────────────────────────────────────────
// 1 = full model ODT (PREQUANT in TVM, INT8 input to C library)
// 0 = partial model ODT (RELU output of frozen layers, UINT8 input to C library)
#define IS_FULL_MODEL_RETRAIN 1
// ── Model dimensions ────────────────────────────────────────────────────────
// INPUT_SIZE: flat size of raw float input to the model (before feature extraction)
// For arc fault: 128 time samples × 1 channel = 128
#define INPUT_SIZE 128
// FROZEN_OUTPUT_SIZE: the size of the TVM model's output — i.e., the input to INT_LAYERS[0]
// For full model: size of PREQUANT output = same as INPUT_SIZE
// For partial model: size of last frozen layer's output
#define FROZEN_OUTPUT_SIZE 128
// N_CLASSES: number of output logits (last layer's out_size)
#define N_CLASSES 2
// BATCH_SIZE: number of samples processed together per forward+backward call
// Must match the actual batch_size used in your training loop
#define BATCH_SIZE 8
// ── Loss function targets ────────────────────────────────────────────────────
// TARGET_MAG_CORRECT[c]: target INT8 logit for class c when the sample IS class c
// Derived from the model's actual output distribution on training samples.
// ODT pushes logit[c] toward this value when predicting class c correctly.
static const int8_t TARGET_MAG_CORRECT[N_CLASSES] = { 24, 20 };
// TARGET_MAG_WRONG[c]: target INT8 logit for class c when the sample is NOT class c
// Typically negative — ODT pushes logit[c] away from the correct class.
static const int8_t TARGET_MAG_WRONG[N_CLASSES] = { -31, -28 };
// ── Learning rate controls ───────────────────────────────────────────────────
// mu_weight: NITI learning rate for weight updates. Default 1.
// Higher = more aggressive weight updates. Range: 0-4 in practice.
#define MU_WEIGHT_DEFAULT 1
// mu_offset: NITI learning rate for bias/offset updates. Default 1.
#define MU_OFFSET_DEFAULT 1
// ── Layer counts ─────────────────────────────────────────────────────────────
// N_INT_LAYERS: total entries in INT_LAYERS[] (all trainable compute layers)
// For full model with Conv+Linear+Linear: 3
// Does NOT include PREQUANT — that lives in TVM mod.a
#define N_INT_LAYERS 3
// N_TRAINABLE_LAYERS: subset of N_INT_LAYERS that are actually updated during ODT
// Equals N_INT_LAYERS for full model. May be less for partial model.
#define N_TRAINABLE_LAYERS 3
// ── Extern declarations ──────────────────────────────────────────────────────
// INT_LAYERS[]: array of layer descriptors — defined in integer_model_config.c
extern IntLayerDesc_t INT_LAYERS[N_INT_LAYERS];
// INT_MODEL: model context — defined in integer_model_config.c
// Initialized with n_layers, layers pointer, grad_buf pointers, mu values, input_type
extern IntModelCtx_t INT_MODEL;
#endif // INTEGER_MODEL_CONFIG_H
Key points for users:
TARGET_MAG_CORRECTandTARGET_MAG_WRONGare model-specific, they are derived from your actual model’s output distribution and will differ between runs/models.BATCH_SIZE=8is hardcoded. To change it, update it before compilation. The static arrays ininteger_model_config.c(act_buf, grad_buf, relu_mask) are sized at compile time using this define.IS_FULL_MODEL_RETRAINtellsmain.cwhether to pass float or int8 data to TVM. For full model ODT, raw feature-extracted floats go to TVM; for partial, they also go to TVM but the output is uint8.
8.12.3.14.5.2. 4.2 integer_model_config.c
This file defines all layer parameters and static buffers. It is auto-generated and contains:
Layer topology comment:
// Model topology
// layer0: CONV2DRELU [ 128 -> 147] (3 filters, 16x2 kernel)
// layer1: LINEARRELU [ 147 -> 160]
// layer2: LINEAR [ 160 -> 2]
Static buffer declarations (all sized at compile time):
static int8_t layer0_weights[]; // Conv weights [n_filters × in_ch × kH × kW]
static int32_t layer0_offset[]; // Conv biases [n_filters], mutable
static const int8_t layer0_mult[]; // OSS mult [n_filters], const
static const int8_t layer0_k[]; // OSS right-shift [n_filters], const
static uint8_t layer0_act_buf[BATCH_SIZE * 147]; // RELU → uint8
static uint8_t layer0_relu_mask[BATCH_SIZE * 19];
// ... layer1, layer2 similarly ...
static int8_t grad_buf_0[BATCH_SIZE * 160]; // ping-pong delta buffers
static int8_t grad_buf_1[BATCH_SIZE * 160];
INT_LAYERS[] initializer, one struct per layer:
IntLayerDesc_t INT_LAYERS[N_INT_LAYERS] = {
{ // Layer 0: CONV2DRELU
.type = INT_LAYER_CONV2D_RELU,
.in_size = 128,
.out_size = 147,
.clip_lo = 0,
.clip_hi = 255,
.act_buf = layer0_act_buf,
.relu_mask = layer0_relu_mask,
.conv = {
.weights = layer0_weights,
.offset = layer0_offset,
.mult = layer0_mult,
.k = layer0_k,
.n_filters = 3,
.kH = 16,
.kW = 2,
.conv_out_h = 113,
.conv_out_w = 1,
},
},
{ // Layer 1: LINEARRELU
.type = INT_LAYER_LINEAR_RELU,
.in_size = 147,
.out_size = 160,
.clip_lo = 0,
.clip_hi = 255,
.act_buf = layer1_act_buf,
.relu_mask = layer1_relu_mask,
.linear = {
.weights = layer1_weights,
.offset = layer1_offset,
.mult = layer1_mult,
.k = layer1_k,
},
},
{ // Layer 2: LINEAR (output, no ReLU)
.type = INT_LAYER_LINEAR,
.in_size = 160,
.out_size = 2,
.clip_lo = -128,
.clip_hi = 127,
.act_buf = layer2_act_buf,
.relu_mask = NULL, // ← no relu mask for non-RELU layer
.linear = {
.weights = layer2_weights,
.offset = layer2_offset,
.mult = layer2_mult,
.k = layer2_k,
},
},
};
INT_MODEL context:
IntModelCtx_t INT_MODEL = {
.n_layers = N_INT_LAYERS,
.layers = INT_LAYERS,
.grad_buf = { grad_buf_0, grad_buf_1 },
.mu_weight = MU_WEIGHT_DEFAULT, // 1
.mu_offset = MU_OFFSET_DEFAULT, // 1
.input_type = INT_INPUT_INT8, // INT_INPUT_UINT8 for partial model
};
Weight arrays (at the end of the file):
// layer0: CONV2DRELU
static int8_t layer0_weights[] = {
// 3 × 1 × 16 × 2 = 96 values, 16 per row
-12, 8, 23, -41, ...
...
};
static int32_t layer0_offset[] = {
// 3 values (one per filter)
142, -88, 213
};
static const int8_t layer0_mult[] = {
1, 1, 1 // all 1 — OSS multiply is effectively no-op for current models
};
static const int8_t layer0_k[] = {
9, 8, 9 // right-shift amounts derived from ONNX: k = round(-log2(|shift|))
};
layer0_weights, layer0_offset, layer1_weights, etc. are declared as static int8_t, these are the values that INTODT_Backward modifies in-place. The mult and k arrays are const and never modified.
8.12.3.14.5.3. 4.3 train_data.h
Declares the training, validation, and test data arrays:
#ifndef TRAIN_DATA_H
#define TRAIN_DATA_H
#include <stdint.h>
#include "integer_model_config.h"
// Sample counts (total across all classes)
// With export_samples_per_class='[10,5,5]' and 2 classes:
#define N_TRAIN_SAMPLES 20 // 10 per class × 2 classes
#define N_VAL_SAMPLES 10 // 5 per class × 2 classes
#define N_TEST_SAMPLES 10 // 5 per class × 2 classes
// Training inputs: float32, raw feature-extracted data
// For full model ODT: same data that goes through TVM PREQUANT during training
extern const float TRAIN_INPUTS [N_TRAIN_SAMPLES][INPUT_SIZE];
extern const int8_t TRAIN_LABELS [N_TRAIN_SAMPLES];
extern const float VAL_INPUTS [N_VAL_SAMPLES ][INPUT_SIZE];
extern const int8_t VAL_LABELS [N_VAL_SAMPLES ];
extern const float TEST_INPUTS [N_TEST_SAMPLES ][INPUT_SIZE];
extern const int8_t TEST_LABELS [N_TEST_SAMPLES ];
#endif // TRAIN_DATA_H
The training inputs are stored as float32 even though the C library works in INT8. Your main.c passes them through the TVM frozen model first (which does the float→INT8 conversion), then passes the INT8 result to INTODT_Forward. The float values are the same data that TVM was compiled to process.
8.12.3.14.5.4. 4.4 train_data.c
Contains the actual training data. Each row is annotated with its index and label for debugging:
// 40 total samples (train=20, val=10, test=10)
#include "train_data.h"
const float TRAIN_INPUTS[N_TRAIN_SAMPLES][INPUT_SIZE] = {
/* [ 0] label= 0 */ { 0.01234567f, -0.87654321f, 0.45678901f, ... },
/* [ 1] label= 0 */ { 0.23456789f, 0.12345678f, -0.34567890f, ... },
...
/* [ 19] label= 1 */ { -0.98765432f, 0.76543210f, 0.65432198f, ... },
};
const int8_t TRAIN_LABELS[N_TRAIN_SAMPLES] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 10 samples of class 0
1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // 10 samples of class 1
};
const float VAL_INPUTS[N_VAL_SAMPLES][INPUT_SIZE] = { ... };
const int8_t VAL_LABELS[N_VAL_SAMPLES] = { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 };
const float TEST_INPUTS[N_TEST_SAMPLES][INPUT_SIZE] = { ... };
const int8_t TEST_LABELS[N_TEST_SAMPLES] = { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 };
Training data is shuffled: the TRAIN_INPUTS array is randomly shuffled by ModelMaker before writing. Class 0 and class 1 samples are interleaved, not grouped, so each training batch contains a mix of classes.
8.12.3.14.6. 5. How Artifacts Connect at Runtime
┌─────────────────────┐
│ TRAIN_INPUTS[][] │ float[N_TRAIN][INPUT_SIZE] (from train_data.c)
└──────────┬──────────┘
│ float* batch pointer
▼
┌─────────────────────────────────────────────┐
│ TVM Frozen Model (mod.a) │
│ │
│ tvmgen_default_run(&inputs, &outputs) │
│ Full model: PREQUANT → INT8 output │
│ Partial model: PREQUANT + layers → UINT8 │
└──────────┬──────────────────────────────────┘
│ INT8* or UINT8*
▼
┌─────────────────────────────────────────────┐
│ INTODT_Forward(&INT_MODEL, batch, sz) │
│ │
│ Layer 0: conv_forward or linear_forward │
│ reads INT8/UINT8 input │
│ writes INT8/UINT8 to layer0_act_buf │
│ sets layer0_relu_mask bits │
│ │
│ Layer 1: linear_forward │
│ reads layer0_act_buf │
│ writes to layer1_act_buf │
│ sets layer1_relu_mask bits │
│ │
│ Layer 2: linear_forward (output) │
│ reads layer1_act_buf │
│ writes to layer2_act_buf (logits) │
└──────────┬──────────────────────────────────┘
│ logits in layer2_act_buf
▼
┌─────────────────────────────────────────────┐
│ compute_loss(ctx, TRAIN_LABELS+start, sz) │ (user code in main.c)
│ │
│ Reads: layer2_act_buf[b][c] — INT8 logits │
│ Reads: TARGET_MAG_CORRECT[], TARGET_MAG_WRONG[] (from integer_model_config.h)
│ Writes: ctx->grad_buf[0][b][c] = clamp(logit - target, -128, 127)
└──────────┬──────────────────────────────────┘
│ grad_buf[0] filled with dy
▼
┌─────────────────────────────────────────────┐
│ INTODT_Backward(&INT_MODEL, batch, sz) │
│ │
│ Layer 2 backward: │
│ Propagate delta from grad_buf[0] │
│ Update layer2_weights, layer2_offset │
│ Write delta_out to grad_buf[1] │
│ │
│ Apply layer1 relu_mask to grad_buf[1] │
│ │
│ Layer 1 backward: │
│ Propagate delta from grad_buf[1] │
│ Update layer1_weights, layer1_offset │
│ Write delta_out to grad_buf[0] │
│ │
│ Apply layer0 relu_mask to grad_buf[0] │
│ │
│ Layer 0 backward: │
│ Update layer0_weights, layer0_offset │
│ (No delta propagation for first layer) │
└─────────────────────────────────────────────┘
8.12.3.14.7. 6. Next Steps
Deep dive into integer_model_config.h/.c → Integer Model Configuration
Deep dive into train_data.h/.c → Training Data
Understand the C library that uses these artifacts → Integer ODL Library