Big teacher. Small student. Plain C.
How a 4B decider's judgement ends up in a few KB of firmware.
Taught by a full Strands Decider
A full Strands Decider (4B) teaches the text tiers through distillation on ~430k decisions. The students keep the same question shapes, so a schema written for Strands Decider works here.
Static embeddings that can read a number
nano uses static embeddings over a pruned 8-16k vocabulary at 128 dimensions. Key-value number pooling reads battery: 6% as a value, so sensor readings in text are not just tokens.
key: number pair, so battery: 6% is read as a value, not just a token.A compiler, not a model download
You write a spec: sensors, rules and questions. The public Strands Decider 2B labels sampled states. The compiler writes one tiny MLP per question as C. Your firmware includes the header and calls a function.
- 1
Write a spec
sensors, rules, questions
- 2
Sample states
from your sensor ranges
- 3
Label
public Strands Decider 2B
- 4
Train
one tiny MLP per question
- 5
Emit C
~2 KB per question (501 parameters)
{
"name": "greenhouse",
"context": "open vents when it is above 30 C; water when soil moisture is below 25%; never water and vent at the same time in frost.",
"features": [
{"name": "temp_c", "type": "number", "min": -10, "max": 50},
{"name": "soil_moisture", "type": "number", "min": 0, "max": 100, "unit": "%"},
{"name": "raining", "type": "bool"},
{"name": "time_of_day", "type": "enum", "values": ["night", "morning", "afternoon", "evening"]}
],
"questions": {
"action": {"type": "choice", "instructions": "What should the controller do now?",
"criteria": {"nothing": "no change", "water": "start watering",
"vent": "open the vents", "heat": "turn on the heater"}},
"alert": {"type": "noul", "instructions": "Should the grower be alerted?"},
"stress": {"type": "score", "instructions": "How stressed are the plants?",
"criteria": ["fine", "mild", "severe"]}
}
}#include "sparrow_pico_greenhouse.h"
greenhouse_state s = {.temp_c = 34.0f, .soil_moisture = 40.0f, .raining = 0,
.time_of_day = GREENHOUSE_TIME_OF_DAY_AFTERNOON};
float p[GREENHOUSE_ACTION_N];
greenhouse_action(&s, p); /* probabilities over the options */python training/pico_compile.py sample greenhouse.json --n 3000 --work runs/pico-greenhouse
python training/label_decider.py <checkpoint dir> runs/pico-greenhouse/rows.jsonl runs/pico-greenhouse/teacher.jsonl
python training/pico_compile.py build greenhouse.json --work runs/pico-greenhouse --out firmware/main94.0% vs the spec's rules (its 2B teacher: 94.2%). A pico model is as good as its teacher on your spec, and no better.
Built for the ESP32-S3
int8 SIMD on the vector unit through the ESP-NN kernel, with weights in PSRAM or internal RAM. The C99 runtime has no malloc and reads the model from flash.
Your firmware
app_main() · sparrow_choice(...)sparrow C99 runtime
no malloc · model read from flashint8 SIMD kernels
ESP-NN on the vector unitMemory placement
weights in PSRAM / internal RAMESP32-S3
240 MHz · 16 MB flash · 8 MB PSRAM
ESP32-S3 (QFN56 rev0.2), 240 MHz, 16 MB flash, 8 MB PSRAM
The C runtime matches the Python runtime to within 1e-5 on the host; on the ESP32-S3, int8 builds agree with the host on 150-155 of 155 decisions.
First on-chip build vs today, same board. Today's nano models also use a smaller hidden layer. Each row has its own scale. Board: ESP32-S3 (QFN56 rev0.2), 240 MHz, 16 MB flash, 8 MB PSRAM.
Show as table
| Model | Time per decision on ESP32-S3, first build to today |
|---|---|
| sparrow-pico-rover-v1.1 | 42.9 µs → 9.3 µs |
| sparrow-nano-general-v1.1 | 9.6 ms → 3.8 ms |
| sparrow-nano-robot-v1.2 | 73.5 ms → 3.5 ms |
Three runtimes, one contract
| Runtime | What you get |
|---|---|
| Python | numpy only |
| C99 | no malloc, model read from flash, ESP-IDF component |
| sparrow-pico compiler | device spec in, C header out |
| type | criteria | answer |
|---|---|---|
noul | optional {"false": ..., "true": ...} | {"noul": P(true)} |
choice | {name: description}, 2 or more | {"choice", "probabilities", "confidence"} |
score | list of 2-10 levels, lowest first | {"score": expected level, "legend", "probabilities", "confidence"} |
sparrow-decider serve exposes POST /v1/systemone, following the Strands Decider /v1/systemone contract.