Getting MiniLM-L6-v2 Onto SiMa's Modalix MLA

July 16, 2026 · by ThinkRobotics

Share

Exporting a HuggingFace BERT model, rewriting its ONNX graph for hardware compatibility, and deploying it to run sentence embeddings in ~7 milliseconds.

sentence-transformers/all-MiniLM-L6-v2
SiMa Modalix DevKit
pyneat 0.2.0
~6.8ms / inference

MiniLM-L6-v2 is a small, fast sentence-embedding model - the kind of thing that powers semantic search, duplicate-question detection, and document similarity. On its own it already runs in tens of milliseconds on a CPU. Point it at SiMa's Modalix Machine Learning Accelerator instead, and that drops to under 7ms - but only after the model gets translated into a dialect the chip actually understands.

The MLA doesn't run arbitrary ONNX. It supports a fixed set of roughly 60 int8 operators, and a stock BERT export from HuggingFace is full of things outside that set - dynamic shapes, Gather-based embedding lookups, Gemm, NaN guards that never actually fire. Getting from a HuggingFace checkpoint to a working MLA binary means exporting to ONNX, surgically rewriting the graph operator-by-operator, quantizing to int8, compiling to a hardware binary, and shipping the result to the DevKit.

6.8ms
Mean Inference Time
20 runs, after 10 warmup
~147/s
Throughput
Single stream, batch=1
11.8×
Faster Than 1-Core CPU
~80ms on a Xeon core

The Six Steps

Two ideas do most of the work: freeze every shape to static values at export time (the MLA compiler requires fixed input shapes), and move anything the chip can't run - the embedding table, the attention mask construction - off-chip onto the CPU.

1 · Export to ONNX
Export the HuggingFace model with static shapes, then simplify with onnxsim.
Host
2 · Graph Surgery
Strip the embedding layer, replace Gemm, remove NaN guards - six surgeries in total.
Container
3 · Quantization
Calibrate on 10 samples and quantize weights + activations to int8.
Container
4 · Compilation
Compile the quantized graph into an MLA .elf binary and MPK archive.
Container
5 · Deploy to DevKit
Copy the MPK archive off the container and onto the DevKit over SSH.
Host
6 · Run Inference
Load the model with pyneat and run sentence embeddings on-device.
DevKit

How the Chip Is Actually Organized

The Modalix DevKit isn't a single accelerator - it's three compute domains working together, and knowing which one does what explains most of the surgery decisions later.

Domain Component Role
APU Cortex-A65 CPU Control logic, tokenization, dequantization
CVU (EV74) Computer Vision Unit Quantization scaling, tessellation (tiling tensors for the MLA)
MLA Machine Learning Accelerator All 6 BERT transformer layers - MatMul, Conv2D, Softmax

The MLA has no random-access memory for large lookup tables. That single hardware fact is why the embedding layer gets moved off-chip in surgery, rather than optimized to fit - a 119,547×384 embedding table simply has nowhere to live on the accelerator.


Prerequisites & Environment

Run on: Host

You'll need an x86_64 Linux dev machine with Docker, the SiMa DevKit reachable over Ethernet, and the SDK container already running with the Model Compiler inside it.

Terminal · Host machine
# Enter the running SDK container
docker exec -it <container_id> bash
Terminal · Inside the container
# Activate the Model Compiler virtual environment
source /sdk-extensions/model-compiler/bin/activate

# Confirm the version
python3 -c "import afe; print('AFE version:', afe.__version__)"
# Expected: AFE version: 2.1.0
Terminal · Host machine
# SSH into the DevKit (password: edgeai)
ssh sima@192.168.0.28
Terminal · On the DevKit
# Confirm pyneat is available
python3 -c "import pyneat; print('pyneat version:', pyneat.__version__)"
# Expected: pyneat version: 0.2.0

Step 1: Export to ONNX

Run on: Host

The Model Compiler doesn't take HuggingFace .bin or PyTorch .pt files directly - it wants ONNX (its most mature import path). Every shape has to be frozen at export time; the MLA compiler has no tolerance for dynamic dimensions.

Terminal · Host machine
# Run the export script (see export_minilm.py for the full source)
# Loads sentence-transformers/all-MiniLM-L6-v2, exports with static
# shapes (batch=1, seq_len=128), opset 17, and simplifies with onnxsim
python3 export_minilm.py
# -> minilm-l6-v2.onnx (~90 MB)
# -> minilm-l6-v2.sim.onnx (~90 MB, after onnxsim simplification)
Parameter Value Why
dynamic_axes None The MLA compiler requires fixed shapes - dynamic axes pull in unsupported ops
opset_version 17 Newer opsets have better operator coverage
do_constant_folding True Bakes LayerNorm weights/biases in at export time
batch, seq_len 1, 128 Fixed dimensions the compiled binary will expect

Step 2: Graph Surgery

Run on: Host, inside the SDK container

This is where most of the real work happens. The exported ONNX graph has to be audited against the MLA's supported-operator list, and every unsupported op removed, replaced, or moved off-chip.

Terminal · Inside the container
# Audit the model for unsupported operators
sima-neat-sdk_audit_model --model_path minilm-l6-v2.sim.onnx --dtype int8 --json_output

Six surgeries turn the audit results into a compilable graph:

Surgery 1
Strip Embedding Layer

Gather for word embedding lookup isn't supported - no random-access memory on the MLA for a large table. The 44.7 MB embedding table moves to the CPU; the model's new input becomes precomputed hidden_states.

Surgery 2
Replace Mask Construction

Unsqueeze + Gather + Where isn't well-supported on MLA int8 paths. The attention bias tensor [1,12,128,128] is precomputed on the CPU and passed in directly as a model input.

Surgery 3
Strip NaN Guards

IsNan + Where pairs get removed entirely. Static analysis of the data flow shows the pipeline never produces NaN between the embedding layer and the transformer, so the guard is dead weight.

Surgery 4
Replace CLS Gather

The pooler's first-token selection via Gather(0) becomes Slice(0,1) + Reshape - both supported, same result.

Surgery 5
Replace Gemm with MatMul

Gemm isn't in the supported set. It decomposes cleanly into Transpose(B) + MatMul(A, Bᵀ) + Add(bias).

Surgery 6
Remove Orphaned Weights

Once the embedding layer is gone, its ~183 MB of initializers are still sitting in the file, unused. Stripping every initializer with no connected graph node drops the model from ~230 MB to ~41 MB.

Terminal · Inside the container
# Run the surgery script (see surgery_minilm.py for the full source)
python3 surgery_minilm.py
# -> minilm-l6-v2.surg.onnx (~41 MB)

# Validate: compare FP32 outputs of the original vs. surgically modified model
python3 validate_surgery.py
# Expected: max absolute difference < 1e-5
Metric Before Surgery After Surgery
Model file size ~90 MB ~41 MB
Node count ~450+ 233
Unique op types ~16 11 (all int8-supported)
Numerical accuracy - 2.1e-6 max diff vs. baseline

Step 3: Quantization

Run on: Host, inside the SDK container

Quantization converts weights and activations from float32 down to int8 - the MLA's native compute precision. That's a 4× reduction in memory and a meaningful speedup, at a cost of roughly 0.1% accuracy loss on sentence similarity tasks for this model.

Calibration only uses 10 samples here, not the 50-100 you might expect. Transformer activations have fairly stable ranges, so 10 dummy samples get calibration done in ~30 seconds instead of ~5 minutes - a deliberate tradeoff, not an oversight.

Terminal · Inside the container (Python)
# quantize_clean.py — quantization config for this model
from afe.apis.defines import (
    default_quantization, quantization_scheme,
    RequantizationMode, CalibrationMethod
)

# 8-bit activations (symmetric, per-tensor)
act_scheme = quantization_scheme(quantize_activations=True,
                                  quantize_weights=False, num_bits=8)

# 8-bit weights (asymmetric, per-channel)
wt_scheme = quantization_scheme(quantize_activations=False,
                                 quantize_weights=True, num_bits=8)

quant_config = default_quantization \
    .with_activation_quantization(act_scheme) \
    .with_weight_quantization(wt_scheme) \
    .with_requantization_mode(RequantizationMode.sima) \
    .with_calibration(CalibrationMethod.from_str("mse"))

quant_model = loaded_net.quantize(
    calibration_data=calib_data,
    quantization_config=quant_config,
    any_shape_on_mla=True,             # BERT's 3D tensors, not 4D image tensors
    automatic_layout_conversion=True,  # ONNX is NCHW, MLA expects NHWC
    model_name="minilm-l6-v2",
)
MSE calibration over entropy. Minimizes squared error between float and quantized outputs per tensor - generally the better choice for transformers than KL-divergence-based entropy calibration.
any_shape_on_mla=True for non-4D tensors. BERT works in (batch, seq, hidden) - 3D - not the 4D image-style tensors the MLA path defaults to expecting.

Step 4: Compilation

Run on: Host, inside the SDK container

Compilation schedules operators across the three compute domains, tiles tensors to fit the MLA's 128 KB local SRAM, maps memory addresses, and produces .elf binaries the MLA can execute directly.

Pass a directory, not a file path, to compile(). A file path like "model.sima" produces a unified .sima zip that pyneat 0.2.0 can't load. A directory path produces individual .elf binaries inside an MPK archive - what the DevKit runtime actually expects.

Terminal · Inside the container (Python)
# THE KEY INSIGHT: pass a DIRECTORY path, not a file path
quant_model.compile(
    BUILD_DIR,       # Directory path -> produces model_mpk.tar.gz
    batch_size=1,
)

Compile time is roughly 3 minutes (quantize + compile combined). The output directory contains the deployable .tar.gz alongside the raw .elf binary and a set of pipeline-stage config JSONs (quantization/tessellation, buffer concat, MLA processing, post-processing).

22.7MB
Total Weights
Compressed to int8
1
MLA Stage
All 6 BERT layers, one pass
~3min
Compile Time
Quantize + compile combined

Step 5: Deploy to the DevKit

Run on: Host → DevKit (192.168.0.28)

Pull the MPK archive out of the container, then copy it onto the DevKit over SSH.

Terminal · Host machine
# Copy the archive out of the container onto the host
docker cp <container>:/workspace/build_minilm_v5/minilm-l6-v2_mpk.tar.gz \
    /host/path/minilm-l6-v2_mpk_nopooler.tar.gz

# Copy it onto the DevKit (password: edgeai)
scp minilm-l6-v2_mpk_nopooler.tar.gz sima@192.168.0.28:/home/sima/models/
Terminal · On the DevKit
# Verify the archive - it MUST contain at least one .elf file
tar tzf /home/sima/models/minilm-l6-v2_mpk_nopooler.tar.gz

If the archive is missing a .elf file, pyneat will reject it outright with invalid_archive: archive missing model binary artifacts. That almost always traces back to Step 4's file-vs-directory distinction - re-run compile() with a directory path.

Step 6: Run Inference on the DevKit

Run on: DevKit

With the archive in place, load the model and push tensors through it directly.

Terminal · On the DevKit
# Run the inference script (see minilm_inference.py for the full source)
# Loads the MPK with mla_only=True, pushes hidden_states + attention_bias,
# pulls the raw int8 output, and dequantizes it manually on the CPU
python3 minilm_inference.py

inference_terminal.mla_only = True is a deliberate workaround, not an oversight. AFE 2.1.0's combined detessellate+dequantize post-processing stage has a geometry bug for non-4D tensors like BERT's. Setting this flag truncates the pipeline at the raw MLA output; the script then dequantizes manually using the scale (31.62) and zero point (-76) pulled from 0_postproc.json.

Metric Value Notes
Load time ~200 ms One-time model load + runner build
Inference (mean) 6.8 ms 20 runs, after 10 warmup
Min / Max 6.1 ms / 7.6 ms Std dev 0.3 ms - very stable
Throughput ~147 / sec Single stream

For context, the same model takes ~80ms on one Xeon CPU core, ~15ms across 8 cores, and ~5ms on a T4 GPU - the GPU is marginally faster but draws roughly 75W against the MLA's ~5W.


Troubleshooting Common Failure Modes

⚠️
"archive missing model binary artifacts (.elf or .so)". The compiler produced a unified .sima file instead of individual .elf binaries. Pass a directory path to compile(), not a file path.
⚠️
"detessdequant validation found mismatched detess frame/slice geometry". AFE 2.1.0's combined post-process stage has a geometry bug for 3D tensors. Set opts.inference_terminal.mla_only = True and dequantize manually using the scale/zp from 0_postproc.json.
⚠️
"CPU-backed Tensor pushed into a device-visible EV74/DMS route". The pipeline expects EV74 (CVU) memory but you're pushing CPU memory. Quick fix: export SIMA_ALLOW_INPUTSTREAM_CPU_TO_EV74_COPY=1. Proper fix: allocate tensors with memory=pyneat.TensorMemory.EV74.
⚠️
"expects shape=128x1x384. Received shape=384x128x1". The auto-layout detector guessed CHW instead of HWC for a [1,128,384] array. Explicitly pass layout=pyneat.TensorLayout.HWC when building the tensor.
⚠️
Model loads, but inference gives wrong results. Check the dequantization scale and zero point pulled from 0_postproc.json against what's hardcoded in your inference script, and compare against an FP32 reference - the int8 output should land within ~2% of it.

Command Quick Reference

All six steps, one command block per machine, in the order you'd actually run them.

Full sequence, machine by machine
# ── HOST, ENTER CONTAINER ────────────────────────────
docker exec -it <container_id> bash

# ── INSIDE THE CONTAINER ─────────────────────────────
source /sdk-extensions/model-compiler/bin/activate
python3 export_minilm.py
sima-neat-sdk_audit_model --model_path minilm-l6-v2.sim.onnx --dtype int8 --json_output
python3 surgery_minilm.py
python3 validate_surgery.py
python3 compile_v2.py
exit

# ── HOST -> DEVKIT ───────────────────────────────────
docker cp <container>:/workspace/build_minilm_v5/minilm-l6-v2_mpk.tar.gz .
scp minilm-l6-v2_mpk_nopooler.tar.gz sima@192.168.0.28:/home/sima/models/
ssh sima@192.168.0.28

# ── ON THE DEVKIT ────────────────────────────────────
tar tzf /home/sima/models/minilm-l6-v2_mpk_nopooler.tar.gz
python3 minilm_inference.py
Step Runs On Key Output
1. Export Host minilm-l6-v2.sim.onnx (~90 MB)
2. Graph Surgery Container minilm-l6-v2.surg.onnx (~41 MB, 233 nodes)
3. Quantization Container int8 model, calibrated on 10 samples
4. Compilation Container minilm-l6-v2_mpk.tar.gz
5. Deploy Host → DevKit MPK archive on /home/sima/models/
6. Inference DevKit Sentence embedding, ~6.8ms

Quick Glossary

  • MLA - Machine Learning Accelerator, the neural network compute engine on Modalix.
  • CVU / EV74 - Computer Vision Unit, handles tensor pre- and post-processing.
  • Tessellation - slicing large tensors into tiles that fit the MLA's local memory.
  • MPK - Model Pack, the deployable archive containing .elf binaries and pipeline config JSONs.
  • Scale / Zero Point - the two numbers defining int8↔float32 mapping: float = (int8 - zp) × scale.
  • Orphaned initializer - a weight tensor left in the graph file after surgery disconnected it from any node.

KEY TAKEAWAY

The Model Barely Changes - the Graph Around It Does

MiniLM-L6-v2 itself is untouched mathematically - it's still the same 6-layer transformer producing the same embeddings. Nearly every line of engineering effort here goes into the graph surrounding it: freezing shapes, moving the embedding table off-chip, decomposing Gemm, and working around a genuine compiler bug in the post-processing stage. That's the recurring shape of accelerator deployment work - the model is rarely the hard part, the translation to hardware-native operations is.

HuggingFace ONNX SiMa Model Compiler pyneat Modalix MLA

Want to Run This on Real Hardware?

Everything in this walkthrough targets the SiMa Modalix DevKit - the same board used throughout this guide.

Get the Modalix DevKit 3.0