1.1 · Data contracts

Why Some Edge AI Bugs Never Crash

Edge AI · Machine Learning · Embedded · Architecture

Most bugs are easy to notice. The program crashes, a test fails, or a value comes back as NaN (short for “not a number”). You get an error message, you follow it to the cause, and you fix it.

Machine learning on small devices has a kind of bug that behaves differently. The model is trained on data prepared one way, and then on the device it receives data prepared a slightly different way. Nothing crashes and every test still passes. The model keeps producing answers that look normal but are wrong. Accuracy is lower than it should be, and the usual reaction is to blame the model: you collect more data, retrain, and tune it for weeks, when the model was fine all along and the problem was the input.

This post explains where this kind of bug comes from and why being careful does not prevent it. It also introduces the three checks that the rest of Part 1 builds, one post at a time.

Before you start

This series is written for people who build machine learning systems for small devices, but you do not need to be an expert to follow it. A few terms appear throughout, so here is what they mean.

Edge AI means running a machine learning model on the device that collects the data, such as a sensor board, instead of sending the data to a server. The device is often a microcontroller: a small, low-power chip that runs one program, called firmware, usually written in C or C++.

A window is a fixed-length slice of sensor readings, for example two seconds of accelerometer data. Models for sensor data usually look at one window at a time.

A feature is a number calculated from a window, such as the average or the highest value of one sensor channel. Many models are trained on features instead of raw readings.

Everything in this series applies whatever you are building: a fall detector, a presence sensor, a machine that predicts equipment failure, or a voice command detector. If you train a model in one language and run it in another, this problem applies to you. The companion code runs on an ordinary laptop, with no sensor board and no downloaded dataset: edge-ai-toolkit/01-contract. This first post needs no code. The next four use it.

Two programs that must agree

Nearly every edge AI system I have worked on has the same shape.

Python training code and C device firmware both feed one definition of a window: 100 readings at 50 per second, the channel order ax ay az gx gy gz, the units, the missing-reading marker -32768, and a 24-value feature order.
Both programs must agree on every line in the green box, and nothing crashes if they do not.

On the left, a Python program reads recorded sensor data, cuts it into windows, calculates features and trains a model. On the right, C firmware reads a live sensor, fills a buffer with readings, calculates the same features and runs the model.

The two programs must agree exactly on what a window of data is. They must agree on how many readings it holds, the order of the sensor channels, the units of each value, how a missing reading is marked, and where each feature sits in the list the model receives. I call the point where they meet the seam, the way a seam in clothing is where two separately made pieces are joined.

Each of those details is a place where the two sides can disagree without any error. The two programs are written in different languages and kept in different folders, often by different people, and sometimes months apart.

There is also a difference neither side chose. Python usually calculates with 64-bit decimal numbers (float64), while a microcontroller usually uses 32-bit ones (float32). The same calculation therefore gives very slightly different results on each side, so any check that compares them has to allow for that. Post 1.3 deals with this properly.

This is an old problem

Disagreements like this are not new, and they are not specific to machine learning. In 1999, NASA lost the Mars Climate Orbiter because one team’s software reported thrust data in imperial units (pound-force seconds) and another team’s software expected metric units (newton-seconds). Neither program crashed. Both did exactly what they were written to do. The spacecraft’s path was calculated wrongly, it came far too close to Mars, and it was lost.

Machine learning makes this kind of mistake harder to notice, because a wrong answer looks just like a right one. The model still produces a number, the number is in a sensible range, and it has the right type. Nothing about it looks wrong.

An example from a real project

Here is one case from a presence detector I am working on, a device that detects whether a room is occupied.

An earlier version of the model reported 99.4% accuracy. That figure came from a random split: the recorded data was shuffled, and some windows were set aside for testing. The problem is that windows from the same recording session end up on both sides. The model is then tested on data that is almost identical to what it trained on, so the score is far too high.

When I evaluated it with an honest split, keeping entire recording sessions out of training and using them only for testing, the accuracy fell to 81.9%. For comparison, a model that always answers “the room is empty” scores 72.6% on the same data, because the room is empty most of the time.

The more revealing number was recall on occupied rooms, meaning the share of occupied moments the model correctly detected. It was 42.5%, so the model missed more than half of the moments that mattered. On two individual recordings, recall was 16.6% and 0.4%. A model guessing at random would have caught about half.

Nothing crashed and no test failed. The dashboard showed 99.4%.

This particular mistake was in how the data was split, not a disagreement between Python and C. It belongs to the same family, though: a quiet error in the method that produces a confident, normal-looking number that nobody has a reason to question. Once you have been caught by one of these, you start looking for the others.

Why being careful is not enough

The obvious fix is discipline. Document the details, add a note to the README, and ask everyone to keep the Python and the C matching.

This fails, and it fails for a predictable reason: it depends on people remembering, usually under deadline pressure. Someone will change the window length six months from now, and they will not remember that a C header file needs the same change. Very often that someone is you.

A fix that works takes a different approach. Instead of relying on people to keep two things matching, set up the project so that the two things cannot drift apart without something failing loudly, such as the build stopping or a test failing.

That idea is simple to state, but applying it properly takes three separate checks. There are three different kinds of thing that can disagree, and each check can only catch one kind.

The three checks this series builds

LevelWhat can disagreeHow it is caughtCovered in
1A constant, such as the window lengthOne source file generates the values for both languages, and a fingerprint of that file detects any changePost 1.2
2A function, such as the code that calculates featuresSaved example inputs with their correct outputs, which both languages must reproducePosts 1.3 and 1.4
3A value nobody has decided yet, such as a detection thresholdThe firmware refuses to compile until someone sets itPost 1.5

Here is what each level means in plain terms.

Level 1 handles fixed numbers that both sides use, such as the sample rate or the order of the sensor channels. Instead of typing these numbers in two places, you keep them in one file and generate the Python and C versions from it.

Level 2 handles calculations. A function cannot be compared the way a number can, so you compare what it produces instead: you save a set of example inputs together with the exact outputs the Python version gives, and both versions have to reproduce those outputs. These saved pairs are called golden vectors. Post 1.4 then breaks the C code on purpose in ten different ways to confirm that the test catches every one.

Level 3 handles values that are still undecided. A placeholder that looks reasonable can easily be mistaken for a real decision months later, so the build refuses to continue until someone makes the decision.

Each level catches a kind of mistake that the level before it cannot see. The next post starts with Level 1: keeping shared constants in one file and generating both the Python and the C versions from it.

Comments

get new posts

About one email a week, and only when there is something new.

Subscribe →