8.12.3.17. Integer ODL Library: API Reference
8.12.3.17.1. Table of Contents
8.12.3.17.2. 1. Overview
The integer ODL library (integer_odl.h / integer_odl.c) provides three public functions that implement integer neural network forward pass, backward pass, and inference. Everything else (loss computation, training loop structure, epoch management) is your responsibility in main.c.
Files:
Header:
integer_ondevice_training/integer_odl.hImplementation:
integer_ondevice_training/integer_odl.cBoth files are linked into your CCS project from the SDK
common/directory
Dependencies: integer_model_config.h, <stdio.h>, <string.h>, <math.h>, <inttypes.h>
No initialization required. INT_MODEL in integer_model_config.c is statically initialized. Call any of the three functions immediately after flash.
8.12.3.17.2.1. Input Type: Which Mode Are You In?
The input_type field in IntModelCtx_t controls how the library interprets the first input to the layer stack. This is set automatically by ModelMaker in integer_model_config.c; you do not set it manually. But you need to understand it to write correct main.c code.
|
Value |
When set |
What you pass to INTODT_Forward / Infer |
Where PREQUANT lives |
|---|---|---|---|---|
|
1 |
Full model ODT |
|
In TVM |
|
2 |
Partial model ODT |
|
In TVM |
For ModelMaker-generated projects on device: you are always in INT_INPUT_INT8 (full model) or INT_INPUT_UINT8 (partial model) mode. You always run TVM first, then pass TVM’s output buffer to the library.
8.12.3.17.3. 2. Public API
8.12.3.17.3.1. 2.1 INTODT_Infer
void INTODT_Infer(IntModelCtx_t *ctx, const void *input, int8_t *logit_output);
Runs a single-sample inference pass through all layers. Does not write to act_buf or relu_mask. Does not modify weights. Safe to call at any time, including between batches during training.
Parameters:
Parameter |
Type |
Description |
|---|---|---|
|
|
Model context. Pass |
|
|
Pointer to one sample. |
|
|
Caller-allocated buffer of |
Internally, it uses ctx->grad_buf[0/1] as ping-pong temporary storage for layer activations (since act_buf is not written during inference).
Typical usage:
int8_t logits[N_CLASSES];
INTODT_Infer(&INT_MODEL, frozen_model_output, logits);
8.12.3.17.3.2. 2.2 INTODT_Forward
void INTODT_Forward(IntModelCtx_t *ctx, const void *batch_input, int32_t batch_size);
Runs a batch forward pass through all layers. Writes each layer’s output to layer->act_buf and sets layer->relu_mask bits. Must be called before INTODT_Backward.
Parameters:
Parameter |
Type |
Description |
|---|---|---|
|
|
Model context. Pass |
|
|
Pointer to batch data. For full model ODT: |
|
|
Number of samples in this batch. Must be > 0 and ≤ |
For each sample b from 0 to batch_size-1:
Iterates through all layers from first to last
Calls
conv_forwardorlinear_forwardfor eachWrites output to
layer->act_buf + b * layer->out_sizeWrites ReLU mask bits for RELU layers
// Step 1: Prepare batch (float → TVM → INT8 or UINT8)
for (int b = 0; b < batch_size; b++) {
struct tvmgen_default_inputs tvm_in = { .input = (void *)TRAIN_INPUTS[start + b] };
struct tvmgen_default_outputs tvm_out = { .output = (void *)(frozen_out + b * FROZEN_OUTPUT_SIZE) };
tvmgen_default_run(&tvm_in, &tvm_out);
}
// Step 2: Forward pass on the batch
INTODT_Forward(&INT_MODEL, frozen_out, batch_size);
Where frozen_out is a static buffer of BATCH_SIZE * FROZEN_OUTPUT_SIZE bytes.
8.12.3.17.3.3. 2.3 INTODT_Backward
void INTODT_Backward(IntModelCtx_t *ctx, const void *batch_input, int32_t batch_size);
Runs a batch backward pass. Reads loss gradient dy from ctx->grad_buf[0], propagates deltas backward through all layers, and updates weights and offsets in-place.
Parameters:
Parameter |
Type |
Description |
|---|---|---|
|
|
Model context. |
|
|
Same pointer as passed to |
|
|
Same value as passed to |
ctx->grad_buf[0] must contain the loss gradient before calling. Each element grad_buf[0][b * N_CLASSES + c] is the gradient of the loss with respect to logit[b][c], saturated to INT8 range. Your compute_loss function is responsible for writing this.
The function:
Iterates layers from last to first
For each layer: a. Computes delta propagation to the lower layer (NITI-shifted, written to the other grad_buf) b. If the layer below has ReLU: applies relu_mask to the propagated delta c. Updates offsets using NITI-shifted sum of deltas d. Updates weights using NITI-shifted outer product of input activations and deltas
Flips between grad_buf[0] and grad_buf[1] for each layer (ping-pong)
After this call: All weights and offsets in INT_LAYERS[] have been updated. The next INTODT_Forward call uses the updated weights automatically.
8.12.3.17.4. 4. The User Loss Function
The loss function is the bridge between INTODT_Forward and INTODT_Backward. You write it in main.c. It must:
Read logits from the last layer’s
act_bufCompute gradients per sample per class
Write gradients (clamped to INT8) into
ctx->grad_buf[0]Return total loss
8.12.3.17.4.1. Reference Implementation: Per-Class MSE Loss
#define CLAMP(x, lo, hi) ((x) < (lo) ? (lo) : (x) > (hi) ? (hi) : (x))
static int32_t compute_loss(IntModelCtx_t *ctx, const int8_t *labels, int32_t batch_size)
{
// Get pointers to last layer's output and grad_buf[0]
IntLayerDesc_t *out_layer = &ctx->layers[ctx->n_layers - 1];
int32_t n_classes = (int32_t)out_layer->out_size; // == N_CLASSES
int8_t *output_buf = (int8_t *)out_layer->act_buf; // logits: int8
int8_t *dy = ctx->grad_buf[0]; // write gradients here
int32_t total_loss = 0;
for (int32_t b = 0; b < batch_size; b++) {
for (int32_t c = 0; c < n_classes; c++) {
int8_t logit = output_buf[b * n_classes + c];
// Target: what this logit should ideally be
int32_t target = (labels[b] == (int8_t)c)
? (int32_t)TARGET_MAG_CORRECT[c]
: (int32_t)TARGET_MAG_WRONG[c];
int32_t grad = (int32_t)logit - target;
// Accumulate squared error for logging
total_loss += grad * grad;
// Write INT8 gradient, clamp to prevent overflow in backward
dy[b * n_classes + c] = (int8_t)CLAMP(grad, -128, 127);
}
}
return total_loss;
}
This is an MSE loss where the target is not a one-hot label but a class-specific logit magnitude. The model is pushed to output logits near TARGET_MAG_CORRECT for the correct class and near TARGET_MAG_WRONG for other classes. These targets are calibrated to the model’s actual distribution, making convergence fast even on small datasets.
The clamp to INT8 is required because INTODT_Backward reads from grad_buf[0] as int8_t*. The gradient (logit - target) could be up to ±255 (e.g., logit=127, target=-128 → grad=255). Clamping to [-128, 127] before writing prevents silent truncation.
8.12.3.17.5. 5. Writing a Complete Training Loop
Here is the complete standard pattern for a training loop in main.c:
#include <stdio.h>
#include <stdint.h>
#include "integer_odl.h"
#include "integer_model_config.h"
#include "train_data.h"
#define NUM_EPOCHS 30
#define CLAMP(x,lo,hi) ((x)<(lo)?(lo):(x)>(hi)?(hi):(x))
// Static buffer for TVM frozen model output (INT8 for full model ODT)
// Size: BATCH_SIZE × FROZEN_OUTPUT_SIZE
static int8_t frozen_out[BATCH_SIZE * FROZEN_OUTPUT_SIZE];
static int32_t compute_loss(IntModelCtx_t *ctx, const int8_t *labels, int32_t batch_size)
{
IntLayerDesc_t *out_layer = &ctx->layers[ctx->n_layers - 1];
int8_t *logit_buf = (int8_t *)out_layer->act_buf;
int8_t *dy = ctx->grad_buf[0];
int32_t total_loss = 0;
for (int32_t b = 0; b < batch_size; b++) {
for (int32_t c = 0; c < N_CLASSES; c++) {
int32_t grad = (int32_t)logit_buf[b * N_CLASSES + c]
- (int32_t)((labels[b] == c) ? TARGET_MAG_CORRECT[c] : TARGET_MAG_WRONG[c]);
total_loss += grad * grad;
dy[b * N_CLASSES + c] = (int8_t)CLAMP(grad, -128, 127);
}
}
return total_loss;
}
static int32_t eval_accuracy(void)
{
int8_t logits[N_CLASSES];
int32_t correct = 0;
for (int32_t i = 0; i < N_TRAIN_SAMPLES; i++) {
// Run TVM frozen model on one sample
struct tvmgen_default_inputs tvm_in = { .input = (void *)TRAIN_INPUTS[i] };
struct tvmgen_default_outputs tvm_out = { .output = (void *)frozen_out };
tvmgen_default_run(&tvm_in, &tvm_out);
// Run integer ODT inference
INTODT_Infer(&INT_MODEL, frozen_out, logits);
// Argmax
int32_t pred = 0;
for (int32_t c = 1; c < N_CLASSES; c++)
if (logits[c] > logits[pred]) pred = c;
if (pred == (int32_t)TRAIN_LABELS[i]) correct++;
}
return correct;
}
int main(void)
{
// ── Board and peripheral initialization (device-specific) ────────────────
// Device_init(); SYSCFG_init(); UART_init(); ...
// ── Print config ──────────────────────────────────────────────────────────
printf("INPUT_SIZE=%d N_CLASSES=%d BATCH_SIZE=%d EPOCHS=%d SAMPLES=%d\n",
INPUT_SIZE, N_CLASSES, BATCH_SIZE, NUM_EPOCHS, N_TRAIN_SAMPLES);
// ── Pre-training accuracy ─────────────────────────────────────────────────
int32_t acc = eval_accuracy();
printf("Accuracy BEFORE: %d / %d (%.1f%%)\n", acc, N_TRAIN_SAMPLES, 100.0f * acc / N_TRAIN_SAMPLES);
// ── Compute batch structure ───────────────────────────────────────────────
int32_t num_full_batches = N_TRAIN_SAMPLES / BATCH_SIZE;
int32_t remainder = N_TRAIN_SAMPLES % BATCH_SIZE;
int32_t num_batches = num_full_batches + (remainder > 0 ? 1 : 0);
// ── Training loop ─────────────────────────────────────────────────────────
for (int32_t epoch = 0; epoch < NUM_EPOCHS; epoch++) {
int32_t epoch_loss = 0;
for (int32_t batch_idx = 0; batch_idx < num_batches; batch_idx++) {
int32_t batch_size = (batch_idx < num_full_batches) ? BATCH_SIZE : remainder;
int32_t sample_start = batch_idx * BATCH_SIZE;
// Step 1: Run TVM on all samples in this batch
for (int32_t b = 0; b < batch_size; b++) {
struct tvmgen_default_inputs tvm_in = { .input = (void *)TRAIN_INPUTS[sample_start + b] };
struct tvmgen_default_outputs tvm_out = { .output = (void *)(frozen_out + b * FROZEN_OUTPUT_SIZE) };
tvmgen_default_run(&tvm_in, &tvm_out);
}
// Step 2: Integer ODT forward pass
INTODT_Forward(&INT_MODEL, frozen_out, batch_size);
// Step 3: Compute loss — writes dy to grad_buf[0]
epoch_loss += compute_loss(&INT_MODEL, TRAIN_LABELS + sample_start, batch_size);
// Step 4: Integer ODT backward pass — reads dy from grad_buf[0], updates weights
INTODT_Backward(&INT_MODEL, frozen_out, batch_size);
}
printf("Epoch [%d/%d] Loss: %.1f\n", epoch + 1, NUM_EPOCHS, (float)epoch_loss / num_batches);
}
// ── Post-training accuracy ────────────────────────────────────────────────
acc = eval_accuracy();
printf("Accuracy AFTER: %d / %d (%.1f%%)\n", acc, N_TRAIN_SAMPLES, 100.0f * acc / N_TRAIN_SAMPLES);
return 0;
}
8.12.3.17.6. 5b. Partial Model ODT: Training Loop Variant
For partial model ODT (IS_FULL_MODEL_RETRAIN == 0), the TVM frozen model runs the PREQUANT block plus frozen compute layers and outputs UINT8. The C library (INT_INPUT_UINT8) receives that UINT8 directly.
The training loop pattern is nearly identical to full model ODT. The only difference is TVM’s output is UINT8 instead of INT8. The INT_MODEL.input_type is set to INT_INPUT_UINT8 in integer_model_config.c automatically; you do not need to change any library call.
// Partial model ODT — training loop in main.c
// FROZEN_OUTPUT_SIZE = size of TVM output (last frozen layer's RELU output, UINT8)
static uint8_t frozen_out[BATCH_SIZE * FROZEN_OUTPUT_SIZE]; // uint8 for partial model
for (int32_t epoch = 0; epoch < NUM_EPOCHS; epoch++) {
int32_t epoch_loss = 0;
for (int32_t batch_idx = 0; batch_idx < num_batches; batch_idx++) {
int32_t batch_size = (batch_idx < num_full_batches) ? BATCH_SIZE : remainder;
if (batch_size == 0) break;
int32_t sample_start = batch_idx * BATCH_SIZE;
// Step 1: Run TVM on each sample — outputs UINT8 (RELU output of last frozen layer)
for (int32_t b = 0; b < batch_size; b++) {
struct tvmgen_default_inputs tvm_in = { .input = (void *)TRAIN_INPUTS[sample_start + b] };
struct tvmgen_default_outputs tvm_out = { .output = (void *)(frozen_out + b * FROZEN_OUTPUT_SIZE) };
tvmgen_default_run(&tvm_in, &tvm_out);
// frozen_out[b * FROZEN_OUTPUT_SIZE] now holds UINT8 activations for sample b
}
// Step 2–4: same as full model — library uses INT_INPUT_UINT8 automatically
INTODT_Forward (&INT_MODEL, frozen_out, batch_size);
epoch_loss += compute_loss(&INT_MODEL, TRAIN_LABELS + sample_start, batch_size);
INTODT_Backward(&INT_MODEL, frozen_out, batch_size);
}
printf("Epoch [%d/%d] Loss: %.1f\n", epoch + 1, NUM_EPOCHS, (float)epoch_loss / num_batches);
}
Key difference from full model loop: frozen_out is uint8_t* not int8_t*. That is the only change in your main.c.
8.12.3.17.7. 6. Writing an Inference-Only Loop
If you only need inference
// Allocate per-call output buffer
static int8_t frozen_out[FROZEN_OUTPUT_SIZE]; // single sample, not batched
int8_t logits[N_CLASSES];
// Run inference on new data from sensor
float sensor_data[INPUT_SIZE];
// ... fill sensor_data from ADC/DMA ...
// Feature extraction
float feature_buf[INPUT_SIZE];
// run featature extraction
// TVM frozen model
struct tvmgen_default_inputs tvm_in = { .input = (void *)feature_buf };
struct tvmgen_default_outputs tvm_out = { .output = (void *)frozen_out };
tvmgen_default_run(&tvm_in, &tvm_out);
// Integer ODT inference
INTODT_Infer(&INT_MODEL, frozen_out, logits);
8.12.3.17.8. 7. Tuning mu_weight and mu_offset
8.12.3.17.8.1. Changing mu at Runtime
You can change mu between epochs to implement a learning rate schedule:
INT_MODEL.mu_weight = 4;
INT_MODEL.mu_offset = 4;