Skip to content

Tiny FireNet CIFAR-10 on RP2350 with `NoodleBuffer

1. Motivation

This experiment evaluates a small CNN classifier for CIFAR-10 on an RP2350-based board using NoodleBuffer abstraction in Noodle. The tested model is a Tiny FireNet-style CNN inspired by SqueezeNet-style fire modules. The input is a CIFAR-10 RGB image of size 32 × 32 × 3, and the output is a 10-class softmax prediction.


2. Model Design

The model follows a compact FireNet structure:

Input RGB              3 × 32 × 32

Stem Conv3×3           24 × 32 × 32
MaxPool                24 × 16 × 16

Fire1 squeeze1×1       12 × 16 × 16
Fire1 expand1×1        24 × 16 × 16
Fire1 expand3×3        24 × 16 × 16
Fire1 concat           48 × 16 × 16

Fire2 squeeze1×1       24 × 16 × 16
Fire2 expand1×1        32 × 16 × 16
Fire2 expand3×3        32 × 16 × 16
Fire2 concat           64 × 16 × 16
MaxPool                64 × 8 × 8

Fire3 squeeze1×1       32 × 8 × 8
Fire3 expand1×1        48 × 8 × 8
Fire3 expand3×3        48 × 8 × 8
Fire3 concat           96 × 8 × 8

Global Average Pool    96
Dense + Softmax        10

The model uses three Fire modules. Each Fire module contains:

input
  └── squeeze 1×1 convolution
          ├── expand 1×1 convolution
          └── expand 3×3 convolution
  └── concatenate expand outputs

This structure is attractive for microcontrollers because the squeeze layer reduces the number of channels before the more expensive expand layers.


3. Input and Tensor Layout

The host sends CIFAR-10 images through serial as HWC RGB bytes:

[y][x][r,g,b]

For example, the payload size is:

32 × 32 × 3 = 3072 bytes

Inside the MCU, the image is converted into CHW float format:

[c][y][x]

Each byte is normalized to [0, 1]:

x[0 * IMG_PIXELS + chw0] = (float)src[hwc + 0] * (1.0f / 255.0f);
x[1 * IMG_PIXELS + chw0] = (float)src[hwc + 1] * (1.0f / 255.0f);
x[2 * IMG_PIXELS + chw0] = (float)src[hwc + 2] * (1.0f / 255.0f);

This is important because the serial protocol uses image-friendly HWC RGB order, while the current Noodle convolution implementation uses CHW tensor order.


4. How NoodleBuffer Is Useful

Earlier Noodle examples often used fixed-size arrays for intermediate tensors. That approach is simple, but it makes memory usage rigid. Every tensor buffer must be manually sized in advance, and overestimating buffer sizes wastes SRAM.

The new NoodleBuffer design changes this behavior. Instead of declaring all tensor arrays manually, the program declares reusable tensor buffers:

static NoodleBuffer X;
static NoodleBuffer A;
static NoodleBuffer B;
static NoodleBuffer S;
static NoodleBuffer E1;
static NoodleBuffer E3;

A buffer grows when a larger tensor is required:

float *x = noodle_buffer_require(dst, required_elements);

Once grown, the buffer keeps its capacity and is reused by later layers. This creates a predictable grow-only memory pattern:

small tensor  -> buffer grows
larger tensor -> buffer grows again
smaller tensor -> buffer reuses existing capacity

This is very useful for embedded inference because it makes peak tensor residency explicit.


5. Buffer Roles

The FireNet implementation uses six NoodleBuffer objects:

Buffer Role
X Input tensor
A Main ping-pong tensor buffer
B Main ping-pong tensor buffer
S Fire-module squeeze output
E1 Fire-module expand 1×1 output
E3 Fire-module expand 3×3 output

The scratch buffers S, E1, and E3 are needed because the Fire module must keep multiple tensors alive at the same time:

S must remain alive while both expand branches are computed.
E1 and E3 must remain alive until concat finishes.

This is different from a simple sequential CNN, where two ping-pong buffers are often enough.


6. Correct Tensor Flow

The correct tensor flow is:

X -> A        Stem convolution
A -> B        Pool1

B -> A        Fire1
A -> B        Fire2
B -> A        Pool2
A -> B        Fire3

B             GAP
B -> A        Dense + Softmax

7. Measured Memory Usage

After the buffers reached their maximum sizes, the measured NoodleBuffer capacities were:

Buffer Size in bytes Corresponding tensor
X 12,288 3 × 32 × 32 × 4
A 98,304 24 × 32 × 32 × 4
B 65,536 64 × 16 × 16 × 4
S 24,576 24 × 16 × 16 × 4
E1 32,768 32 × 16 × 16 × 4
E3 32,768 32 × 16 × 16 × 4
Total 266,240 ≈260 KB

The measured remaining free RAM after buffer growth was approximately:

159,431 bytes ≈ 155 KB

Therefore, the current RP2350 FireNet implementation requires approximately:

Tensor buffer memory: 266,240 bytes ≈ 260 KB
Remaining RAM:        159,431 bytes ≈ 155 KB

8. Inference Result

After fixing the Pool2 tensor-flow bug, a 10-image CIFAR-10 smoke test produced:

Accuracy: 8/10 = 80.00%
Mean inference: 2.085803 s

Example output:

PRED 3 2.086086 0.665115 cat        OK
PRED 8 2.085790 0.842198 ship       OK
PRED 8 2.085797 0.935005 ship       OK
PRED 0 2.085736 0.814907 airplane   OK
PRED 6 2.085793 0.929317 frog       OK
PRED 6 2.085766 0.965863 frog       OK
PRED 1 2.085783 0.557270 automobile OK
PRED 2 2.085754 0.589441 bird       NO
PRED 3 2.085751 0.784107 cat        OK
PRED 9 2.085771 0.574728 truck      NO

The two incorrect predictions were plausible CIFAR-10 confusions:

frog       -> bird
automobile -> truck

9. Discussion

The experiment shows that NoodleBuffer makes memory behavior easier to observe and explain. Instead of manually reasoning about all intermediate arrays, we can inspect the actual capacity reached by each logical buffer.

The final memory profile also explains the hardware boundary:

RP2350: feasible fully in SRAM
Smaller MCUs: likely require tensor externalization

For example, a total tensor-buffer requirement of about 260 KB is acceptable on RP2350, but it is far beyond AVR-class boards and too large for many STM32F103-class boards if kept fully in SRAM.

This supports the broader Noodle idea: the same operator abstraction can be extended from RAM-backed tensors to file-backed or external-memory-backed tensors. NoodleBuffer is therefore a useful stepping stone toward selective tensor residency.


10. Conclusion

The RP2350 Tiny FireNet CIFAR-10 experiment demonstrates that the new grow-only NoodleBuffer abstraction works well for SRAM-backed CNN inference.

The main conclusions are:

  1. NoodleBuffer makes intermediate tensor memory measurable and predictable.
  2. Each buffer grows only to the largest tensor it needs to hold.
  3. The Tiny FireNet CIFAR-10 model requires about 266,240 bytes, or 260 KB, of tensor-buffer memory.
  4. The RP2350 still has about 155 KB free after the buffers reach their peak capacity.
  5. A tensor routing error in Pool2 initially caused poor accuracy, but after correcting the flow from B -> A, the model produced sensible predictions.
  6. The corrected model achieved 8/10 accuracy in a small smoke test, with mean inference time of about 2.086 s per image.

Overall, this experiment confirms that NoodleBuffer is a practical abstraction for managing tensor residency in Noodle. It also gives a clear baseline for deciding when future models can remain fully SRAM-backed and when selective externalization to SD card, flash, or other storage becomes necessary.