Architecture

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.

Distillation pipelineStrands Decider 4B teaches nano, micro, tiny and small through distillation on ~430k decisions; they run on Python numpy or the C99 runtime. Strands Decider 2B labels sampled device states for the sparrow-pico compiler, which writes a C header. TEACHERSDISTILLATIONTIERSRUNTIMES soft labels Strands Decider 4Bteacher Strands Decider 2Bpublic Distillation~430k decisions Label sampled statesfrom your device spec nano1.11 MB to 2.2 MB · static micro7.6 MB · static tiny · small17.9 MB / 47 MB · encoder sparrow-pico compilerone MLP per question Pythonnumpy only C99 runtimeno malloc · model in flash C header~2 KB per question
Distillation pipeline, stacked TEXT TIERS Strands Decider 4Bteacher Distillation~430k decisions nano1.11 MB to 2.2 MBmicro7.6 MBtiny · small17.9 MB / 47 MB Pythonnumpy onlyC99no malloc PICO Strands Decider 2Bpublic Label sampled statesfrom your device spec sparrow-pico compilerone MLP per question C header~2 KB per question
A full Strands Decider (4B) teaches the text tiers through distillation on ~430k decisions. For pico, the public Strands Decider 2B labels sampled states from your device spec, and the compiler writes C.

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.

Inside nano Text in becomes tokens and static embeddings of 128 dimensions. Number pooling builds one vector per key: number pair, so battery: 6% is read as a value. A decision head turns both into probabilities. statebattery: 6%front_obstacle_m: 7.0path: clear tokensstatic embeddings number poolingkey: number pairs mean pool128-d battery × f(6%) decision head+ question, options probabilities
Inside nano, stacked statebattery: 6%front_obstacle_m: 7.0path: clear tokensstatic embeddingsnumber poolingkey: number mean pool128-dbattery × f(6%)a value, not a token decision head+ question, options probabilities
Mean-pooled words lose which number belongs to which key. nano also builds one vector per 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. 1

    Write a spec

    sensors, rules, questions

  2. 2

    Sample states

    from your sensor ranges

  3. 3

    Label

    public Strands Decider 2B

  4. 4

    Train

    one tiny MLP per question

  5. 5

    Emit C

    ~2 KB per question (501 parameters)

9.3 µs per decision on ESP32-S30.18 µs on a laptop
greenhouse.json
{
  "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"]}
  }
}
firmware/main.c
#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 */
terminal
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/main

94.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.

  1. Your firmware

    app_main() · sparrow_choice(...)
  2. sparrow C99 runtime

    no malloc · model read from flash
  3. int8 SIMD kernels

    ESP-NN on the vector unit
  4. Memory placement

    weights in PSRAM / internal RAM
  5. ESP32-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.

Flash it yourself

Time per decision on the ESP32-S3: sparrow-pico 42.9 µs to 9.3 µs; sparrow-nano-general 9.6 ms to 3.8 ms; sparrow-nano-robot 73.5 ms to 3.5 ms.

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
ModelTime per decision on ESP32-S3, first build to today
sparrow-pico-rover-v1.142.9 µs → 9.3 µs
sparrow-nano-general-v1.19.6 ms → 3.8 ms
sparrow-nano-robot-v1.273.5 ms → 3.5 ms

Three runtimes, one contract

Runtimes.
RuntimeWhat you get
Pythonnumpy only
C99no malloc, model read from flash, ESP-IDF component
sparrow-pico compilerdevice spec in, C header out
Question types and answer shapes, from the Python guide.
typecriteriaanswer
nouloptional {"false": ..., "true": ...}{"noul": P(true)}
choice{name: description}, 2 or more{"choice", "probabilities", "confidence"}
scorelist 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.

Research report