Skip to content

Latest commit

 

History

History
450 lines (334 loc) · 18.5 KB

BEYOND_SDK.md

File metadata and controls

450 lines (334 loc) · 18.5 KB

AKD1000: Beyond the SDK — What the Hardware Actually Does

Date: February 19-20, 2026 Method: Systematic probing via Python SDK, C++ symbol analysis, PCIe BAR mapping Status: Active exploration — multiple SDK assumptions overturned


Executive Summary

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.

Key Discoveries

# 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

Discovery 1: InputConv Channel Limit is SDK-Enforced

What the SDK says

InputConvolutional with hw_only=True rejects channels > 3:

Cannot map layer 'inp'. The parameter input_channels must be 1 or 3.

What we tested

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.

What this means

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.


Discovery 2: FC Chains Merge Into Single Hardware Pass

What we tested

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.

Latency barely changes with depth

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.


Discovery 3: Batch Inference Amortizes PCIe

Measured on 50→256→256→256→1 model (108KB program)

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.

Implication for MD

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.


Discovery 4: Three Clock Modes with Real Power Differences

Measured on 50→256→256→256→1 model

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.


Discovery 5: FC Width Scales to 8192+

Tested: InputConv(8→N) → FC(N→N) → FC(N→1)

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.

Physics-Scale Benchmarks

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.


Discovery 6: Weight Mutation Without Reprogramming

Tested: set_variable() on mapped hardware model

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.

Implication

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.


Discovery 7: Program Binary is FlatBuffer Format

Structure

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.

Binary analysis

  • FlatBuffer root offset at byte 0: 0x148 (328)
  • Version string "2.19.1" at offset 236
  • Contains akida::fb::RegisterValue entries (hardware register writes)
  • Contains akida::fb::PassSpans (NP assignment and routing)

Between two models with different weights

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.


Discovery 8: PCIe BAR Layout — 16GB Address Space

Measured via sysfs resource files

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

BAR0 Register Map (first 64KB probed)

  • 0x000000: 0x194000a1 — Device ID / version register
  • 0x001094: 0x0000a028 — Control register
  • 0x0010a0-0x0010ac: Configuration cluster (values: 1, 1, 0x2410, 1)
  • 0x0010c0: 0x5b (91 decimal) — likely NP count or feature bits
  • 0x001410-0x001418: SRAM region config (0x2000, 0x8000, 0x85800)
  • 0x001484-0x001488: Timestamps or firmware version
  • 0x001e0c-0x001e20: Six 0x00000001 — NP enable bits?
  • 0x004010: 0x04aa0001 — DMA/mesh configuration word
  • 0xe000+: Per-NP configuration registers (repeating patterns)
  • 0xbadf5040: "Bad food" — uninitialized / protected register space

BAR1 (16GB) — First 64KB is all zeros

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

Symbol analysis of core.so (1,048 exported symbols)

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 LPDDR4

51-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

Discovery 10: Hardware Determinism and Model Persistence

Determinism

10 consecutive forward passes with identical input produce identical output. The hardware is fully deterministic — no stochastic elements in the datapath.

Model persistence

Models save to .fbz (FlatBuffer, Snappy-compressed) and reload perfectly. Saved model produces identical output to original on re-mapping.


What Doesn't Work (Confirmed Hardware Limits)

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

Evolution Plan

Phase 1: Exploit What We Found (This Week)

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

  2. Use Economy clock mode — 19% slower, 18% less power, no code change.

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

  4. Multi-output readout — Predict D*, viscosity, thermal conductivity simultaneously from the same feature vector. Multi-output is free.

Phase 2: Push the Boundaries (Next 2 Weeks)

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

  2. BAR0 register probing — Map the full register space. Identify DMA control registers, NP enable bits, clock configuration, and learning registers.

  3. 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?

  4. BAR1 exploration — ✅ Complete. SramAccessor and VfioBackend::map_bar1() provide full read/write access. probe_sram binary for interactive diagnostics.

Phase 3: Cross-Substrate Integration (Month 2)

  1. Rust device driver — Build a Rust crate that opens /dev/akida0, manages permissions, and provides a safe API for model loading and inference.

  2. ToadStool NPU dispatch — Integrate NPU inference into ToadStool's substrate dispatch system. Shader math → quantize → NPU inference.

  3. GPU+NPU heterogeneous pipeline — GPU runs MD forces while NPU runs ESN readout continuously. Zero GPU cycles stolen for predictions.

Phase 4: Hardware-Native Design (Month 3+)

  1. Native int4+ReLU reservoir — Design reservoir dynamics that are optimal for the hardware's actual compute model, not an approximation of float ESN.

  2. Neuromorphic PDE elements — Explore Poisson solver mapping using multi-pass FC chains (cf. Sandia NeuroFEM).

  3. Direct C++ engine integration — Bypass Python entirely. Rust FFI to the Akida Engine for register-level control from ToadStool.


Comparison: GPU f64 Discovery vs NPU Exploration

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

Raw Data Archive

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)