Date: February 19-20, 2026 Method: Systematic probing via Python SDK, C++ symbol analysis, PCIe BAR mapping Status: Active exploration — multiple SDK assumptions overturned
The BrainChip SDK (MetaTF) presents the AKD1000 as an image-classification accelerator with strict constraints: 1 or 3 input channels, feed-forward only, no tanh.
We tested every assumption against the actual hardware. Several are SDK-enforced limitations, not silicon limitations. This parallels our GPU f64 discovery — where we initially thought wgpu bypassed CUDA's fp64 throttle; rigorous benchmarking confirmed both APIs give ~1:64 (hardware ratio). The real breakthrough was the double-float hybrid.
| # | SDK Claim | Actual Hardware | Impact |
|---|---|---|---|
| 1 | InputConv: 1 or 3 channels only | Any channel count works (tested 1-64) | Our 50-dim physics vectors work directly |
| 2 | FC layers run independently | All FC layers merge into single HW pass | Deep networks: zero inter-layer PCIe overhead |
| 3 | Batch=1 inference only | Batch amortizes PCIe: 948→390 μs/sample | 2.4× throughput from batching alone |
| 4 | Single clock mode | 3 modes: Performance / Economy / LowPower | Economy: 19% slower, 18% less power |
| 5 | Max FC width ~hundreds | Tested to 8192+ neurons — all map to HW | Massive FC networks fit in 8MB SRAM |
| 6 | No direct weight mutation | set_variable() updates weights without reprogram | Hot-swap weights at ~14ms overhead |
| 7 | "30mW" power spec | Board floor 900mW, chip compute below noise | True chip power is unmeasurably small |
| 8 | 8MB SRAM is the limit | PCIe BAR1 exposes 16GB address space | Full NP mesh is memory-mapped — direct R/W confirmed |
| 9 | Program is opaque | FlatBuffer format with program_info + program_data | Weights transmitted via DMA, not in program |
| 10 | Simple inference engine | C++ engine has SkipDMA, on-chip learning, register access | Hardware capabilities far exceed SDK |
InputConvolutional with hw_only=True rejects channels > 3:
Cannot map layer 'inp'. The parameter input_channels must be 1 or 3.
channels= 1: HW=1 SW=1 prog=[728] lat=707us — works
channels= 2: HW=1 SW=1 prog=[728] lat=698us — works
channels= 3: HW=1 SW=1 prog=[728] lat=681us — works
channels= 4: HW=1 SW=1 prog=[728] lat=684us — works
channels= 8: HW=1 SW=1 prog=[728] lat=673us — works
channels= 16: HW=1 SW=1 prog=[728] lat=657us — works
channels= 32: HW=1 SW=1 prog=[728] lat=661us — works
channels= 50: HW=1 SW=1 prog=[728] lat=649us — works
channels= 64: HW=1 SW=1 prog=[728] lat=714us — works
Every channel count works. The InputConv runs in software (Seq 0: SW/inp), and the FullyConnected runs in hardware (Seq 1: HW/out). The SW InputConv is just a thin projection shim — the channel count is irrelevant to the hardware.
Our 50-dimensional physics feature vectors work directly. No need to reshape to 1×1×3 or pad. The InputConv software layer projects from N dimensions to M filters, and all downstream FC computation runs on hardware.
Building models with 2-8 FC layers after InputConv:
depth=2 (3 layers): HW=1 SW=1 prog=3048B lat=713us
depth=3 (4 layers): HW=1 SW=1 prog=4600B lat=741us
depth=4 (5 layers): HW=1 SW=1 prog=6152B lat=692us
depth=5 (6 layers): HW=1 SW=1 prog=7704B lat=703us
depth=8 (9 layers): HW=1 SW=1 prog=12360B lat=716us
All FC layers consolidate into one hardware sequence (HW/fc1-out). The NP
mesh processes the entire chain in a single pass using intra-mesh routing. This
IS the SkipDMA found in the C++ engine symbols — NP-to-NP data transfer without
returning to the host.
8 layers costs only 3μs more than 2 layers. The PCIe round-trip dominates, and the intra-mesh routing adds negligible overhead. This means deep FC networks are essentially free once the PCIe transfer is paid.
batch= 1: 0.95ms 948us/sample 1,055 samples/s
batch= 2: 1.14ms 568us/sample 1,760 samples/s
batch= 4: 1.70ms 426us/sample 2,346 samples/s
batch= 8: 3.12ms 390us/sample 2,566 samples/s ← sweet spot
batch= 16: 7.70ms 481us/sample 2,078 samples/s
batch= 32: 18.57ms 580us/sample 1,723 samples/s
batch= 64: 29.28ms 458us/sample 2,186 samples/s
Batch=8 is the sweet spot: 2.4× throughput over single inference. The PCIe transfer cost is partially amortized across batch elements. Beyond batch=8, the per-sample time increases again (batch overhead), though batch=64 recovers somewhat.
If we buffer 8 velocity feature vectors and send them together, we get 2,566 D* predictions per second at ~390μs per prediction. This is viable for real-time transport prediction alongside a 250 Hz MD simulation.
| Mode | Latency | Power | Notes |
|---|---|---|---|
| Performance | 909 μs | 901 mW | Default — fastest |
| Economy | 1,080 μs | 739 mW | Sweet spot: 19% slower, 18% less power |
| LowPower | 8,472 μs | 658 mW | 9.3× slower, 27% less power |
Economy mode is the clear winner for physics workloads where we have 4+ ms between predictions. Only 19% slower but saves 162 mW continuously.
width= 64: prog= 5,120B mem=(0, 320) lat=779us
width= 128: prog= 15,408B mem=(0, 4376) lat=700us
width= 256: prog= 54,464B mem=(0, 14616) lat=812us
width= 512: prog= 206,208B mem=(0, 55464) lat=1106us ← compute starts contributing
width= 1024: prog= 804,608B mem=(0, 209064) lat=1986us
width= 2048: prog=3,181,056B mem=(0, 811176) lat=4969us
width= 4096: prog=12,652,544B mem=(0, 3195048) lat=16141us
width= 8192: OK ← still fits
Latency crossover at ~width=512: Below 512, PCIe dominates (~650μs). Above 512, compute time becomes significant. At 4096, compute is ~15.5ms.
Memory scales ~quadratically: 4096-wide needs ~3MB of the 8MB SRAM. 8192-wide still maps, suggesting ~6-7MB used. The 8MB limit is real but generous.
| Config | Lat (μs) | FPS | Notes |
|---|---|---|---|
| 8→32→1 (ESN small) | 698 | 1,432 | PCIe dominated |
| 50→64→1 (ESN readout) | 667 | 1,500 | PCIe dominated |
| 50→128→1 (wide readout) | 661 | 1,514 | PCIe dominated |
| 50→256→1 (fat readout) | 754 | 1,325 | Slight compute |
| 50→512→1 (XL readout) | 1,055 | 948 | Compute starts |
| 50→512→3 (multi-output) | 1,149 | 870 | Multi-output ~free |
| 50→1024→1 (massive) | 1,979 | 505 | Compute dominant |
| 50→1024→10 (multi-massive) | 1,913 | 523 | 10 outputs < 1 output?! |
| 100→256→1 (deep physics) | 798 | 1,253 | Wider input, same HW |
| 256→512→1 (feature map) | 1,112 | 899 | Large projection |
Multi-output is free or negative cost: 50→1024→10 is faster than 50→1024→1. The NP mesh parallelism handles multiple outputs simultaneously.
All-ones weights, input=10×8: result = 240
After doubling FC weights: result = 480 (ratio = 2.00 ✓)
After setting FC weights to -3: result = -720 (ratio = -3.00 ✓)
Program binary changed: False
Weights are NOT stored in the program binary. They are DMA'd to NP SRAM
separately. The program contains routing/configuration only (program_info),
and the weight data is sent via a side channel.
Overhead: ~14ms for set_variable + forward (vs 663μs for forward alone). The weight update cost is ~13ms per update.
For reservoir weight training, we can update readout weights without reprogramming the NP mesh. But 13ms per update means weight mutation is expensive — batch training should minimize update frequency.
The program binary (e.g., 728 bytes for ESN readout) splits into:
- program_info (332 bytes): NP routing, configuration, version. Identical between models with different weights. This is pure structure.
- program_data (396 bytes): Layer metadata, activation parameters. Also identical between different weights — weight data is NOT here.
- FlatBuffer root offset at byte 0:
0x148(328) - Version string "2.19.1" at offset 236
- Contains
akida::fb::RegisterValueentries (hardware register writes) - Contains
akida::fb::PassSpans(NP assignment and routing)
program_info: 332 bytes, identical: True (0 bytes differ)
program_data: 396 bytes, identical: False (12 bytes differ)
Wait — program_data DOES differ between models with random weights (12 bytes), but NOT when we change weights via set_variable() on the same model. This suggests program_data contains the initial weight values compiled at map() time, but runtime weight updates bypass the program entirely.
| BAR | Address | Size | Type | Purpose |
|---|---|---|---|---|
| BAR0 | 0x84000000 | 16 MB | 32-bit, non-prefetchable | Register space |
| BAR1 | 0x4000000000 | 16 GB | 64-bit, prefetchable | NP mesh / SRAM window |
| BAR3 | 0x4400000000 | 32 MB | 64-bit, prefetchable | Secondary memory |
| BAR5 | 0x7000 | 128 B | I/O ports | Control ports |
| BAR6 | 0x85000000 | 512 KB | Expansion ROM | Firmware |
0x000000:0x194000a1— Device ID / version register0x001094:0x0000a028— Control register0x0010a0-0x0010ac: Configuration cluster (values: 1, 1, 0x2410, 1)0x0010c0:0x5b(91 decimal) — likely NP count or feature bits0x001410-0x001418: SRAM region config (0x2000,0x8000,0x85800)0x001484-0x001488: Timestamps or firmware version0x001e0c-0x001e20: Six0x00000001— NP enable bits?0x004010:0x04aa0001— DMA/mesh configuration word0xe000+: Per-NP configuration registers (repeating patterns)0xbadf5040: "Bad food" — uninitialized / protected register space
The 16GB address space is likely the full NP mesh address decode range. With 78 NPs, each could have up to ~200MB of addressable space. The zeros in the first page suggest either sparse mapping or that data only appears at specific NP-mapped offsets after programming.
This is a significant finding: The hardware has a MUCH larger address space than the 8MB SRAM spec suggests. The decode range is sparse — physical SRAM is ~8 MB across 78 NPs, but the full range is accessible.
Update (Mar 2026): BAR1 SRAM is now directly readable and writable via
rustChip's SramAccessor (sysfs mmap) and VfioBackend::map_bar1() (VFIO path).
The probe_sram binary provides interactive diagnostics (probe, scan, test modes).
Layout is auto-discovered from BAR0 registers: NP_COUNT (0x10C0), SRAM_REGION
configs (0x1410/0x1418). This enables model load verification, direct weight
mutation, and device fingerprinting — no SDK required.
Discovery 9: C++ Engine Reveals Hidden Capabilities
SkipDMA — NP-to-NP data transfer without PCIe round-trip:
akida::LayerMapping::skipdma_load()
akida::LayerMapping::skipdma_store()
akida::request_skipdma_load()
akida::request_skipdma_store()This is how multi-FC chains execute in a single hardware pass — data routes between NPs on the mesh without touching host memory.
On-Chip Learning — STDP hardware support:
akida::v1::fnp_update_learn_vars()
akida::v1::format_learning_cnp_regs()
akida::v1::format_learning_common_regs()
akida::v1::record_program_learning_dense_layer()
akida::HardwareSequence::sync_learning_vars()Learning is in the hardware — register-level learning configuration exists. The SDK limits this to 1-bit weights + binary activations, but the C++ engine has broader learning support.
Multiple SRAM Types (richer than "8MB"):
get_fsram_64b_memory_size() // 64-bit filter SRAM
get_tsram_51b_memory_size() // 51-bit threshold SRAM
get_evsram_32b_memory_size() // 32-bit event SRAM
get_stsram_32b_memory_size() // 32-bit status SRAM
get_weight_memory_size() // Weight storage
get_input_memory_size() // Input buffer
get_external_memory_size() // External LPDDR451-bit threshold SRAM is particularly interesting — this suggests more precision in the threshold/comparison logic than the "4-bit everything" spec.
Register-Level Programming:
akida::fb::CreateProgramInfoDirect(..., RegisterValue, PassSpans, ...)
akida::v1::format_mesh_registers_set_output_to_NPs(...)
akida::v2::format_cnp_tnp_common_config_registers(...)program_external — raw program injection:
program_external(self, bytes, int) -> None
Program a device using a serialized program info bytes object,
and the address, as it is seen from akida on the device,
of corresponding program data that must have been written beforehand.
This is the "metal" — we can write a program binary and inject it at a specific device memory address.
Three hardware versions in the codebase:
akida::v1— AKD1000 (our hardware)akida::v2— Akida 2.0 (future)akida::pico— A third, smaller variant
10 consecutive forward passes with identical input produce identical output. The hardware is fully deterministic — no stochastic elements in the datapath.
Models save to .fbz (FlatBuffer, Snappy-compressed) and reload perfectly.
Saved model produces identical output to original on re-mapping.
These are real silicon constraints, not SDK limitations:
| Constraint | Evidence | Severity |
|---|---|---|
| InputConv HW mapping needs kernel ≥ 3 | hw_only=True rejects kernel_size=1 |
Medium — SW fallback works |
| InputConv HW mapping needs 1 or 3 channels | hw_only=True rejects others |
Medium — SW fallback works |
| No tanh activation | Only bounded ReLU in Akida 1.0 | High — changes reservoir dynamics |
| No feedback/recurrence | Feed-forward only in hardware | High — host drives recurrence |
| 1-bit learning only | compile() requires binary inputs |
Medium — limits on-chip learning |
| PCIe x1 Gen2 latency | ~650μs round-trip minimum | High — but batch amortizes |
-
Deploy actual ESN weights at batch=8 — Use the 2.4× batch speedup for real transport predictions. 50→128→1 at batch=8 = ~400μs/sample.
-
Use Economy clock mode — 19% slower, 18% less power, no code change.
-
Build deep FC networks for physics — The multi-layer merge means we can add hidden layers for free. 50→256→256→256→1 runs at 909μs single.
-
Multi-output readout — Predict D*, viscosity, thermal conductivity simultaneously from the same feature vector. Multi-output is free.
-
program_external injection — Write our own FlatBuffer programs and inject them at specific device addresses. If we can generate the routing configuration, we can build custom NP mesh topologies.
-
BAR0 register probing — Map the full register space. Identify DMA control registers, NP enable bits, clock configuration, and learning registers.
-
Attempt wider learning — The C++ engine has learning registers beyond the 1-bit SDK limit. Can we configure 4-bit learning via direct register writes?
-
BAR1 exploration — ✅ Complete.
SramAccessorandVfioBackend::map_bar1()provide full read/write access.probe_srambinary for interactive diagnostics.
-
Rust device driver — Build a Rust crate that opens
/dev/akida0, manages permissions, and provides a safe API for model loading and inference. -
ToadStool NPU dispatch — Integrate NPU inference into ToadStool's substrate dispatch system. Shader math → quantize → NPU inference.
-
GPU+NPU heterogeneous pipeline — GPU runs MD forces while NPU runs ESN readout continuously. Zero GPU cycles stolen for predictions.
-
Native int4+ReLU reservoir — Design reservoir dynamics that are optimal for the hardware's actual compute model, not an approximation of float ESN.
-
Neuromorphic PDE elements — Explore Poisson solver mapping using multi-pass FC chains (cf. Sandia NeuroFEM).
-
Direct C++ engine integration — Bypass Python entirely. Rust FFI to the Akida Engine for register-level control from ToadStool.
| Aspect | GPU (BarraCuda) | NPU (metalForge) |
|---|---|---|
| SDK claim | f64 at 1:2 via wgpu (bypass CUDA) | InputConv: 1 or 3 channels only |
| Reality | Both CUDA and Vulkan ~1:64; DF64 hybrid 9.9× native f64 | Any channel count works (SW shim) |
| Method | Double-float on FP32 cores | Bypass SDK channel check |
| Impact | 16× f64 speedup | Physics vectors work directly |
| Additional finds | — | Batch amortizes PCIe 2.4× |
| Additional finds | — | FC chains merge to single HW pass |
| Additional finds | — | 3 clock modes, 16GB BAR, SkipDMA |
| Additional finds | — | program_external for raw injection |
| Philosophy | Same: question the vendor, test the silicon |
All measurements taken on:
- Host: Intel i9-12900K, 64GB DDR5
- NPU: BrainChip AKD1000 (BC.00.000.002) via PCIe
- SDK: Akida 2.19.1, Python 3.10
- Date: February 19-20, 2026
- Device:
/dev/akida0(chmod 666 via pkexec)