Streaming Speech Recognition on a Microcontroller: a 65.7M-Parameter Transducer in 2 and 4 Bits on the ESP32-P4
Organizations: Faculty of Computer Science, University of Science and Technology Houari Boumediene (USTHB), Algiers, Algeria
Abstract
We run Kroko, a 65.7M-parameter streaming Zipformer2 transducer for English, on the ESP32-P4, a microcontroller with two 400 MHz RISC-V cores, a 128-bit SIMD extension (PIE) and 32 MB of PSRAM, but no neural accelerator. Without any training, the encoder's 290 matrices are stored with 2 or 4 bits per weight in Hadamard-rotated groups: the width of each matrix comes from its measured sensitivity, and each matrix is first corrected toward the original model's output, then rounded with GPTQ and a search over the group norms. The released 71 MB of int8 weights become a 26.65 MB file. Activations are int16 between the matrix products and int8 into them. A C engine with hand-written PIE kernels processes each 1.28 s chunk of audio in 0.90 s on both cores, beam search included, and its PC build computes the same bits as the chip, so accuracy is measured on whole test sets. LibriSpeech WER is 4.34% on test-clean and 10.26% on test-other, against 3.94% and 9.17% for the released model; on FLEURS and Common Voice the gap grows to 2.3 and 1.1 points. A GPU simulator shows that the integer arithmetic costs nothing and that the whole gap comes from the weights, and an ablation on all of test-clean shows that GPTQ does most of the work. We also report the port's speed history, from 51.3 s to 0.62 s of encoder time per chunk, each design step measured again in one build, an attempt to reach 16 MB that stopped at 19.63 MB, and what did not work. We did not measure energy. The inference engine, the firmware and the 2/4-bit weights are available at https://github.com/BoumedineBillal/esp32-pie-asr.
Figures & tables
| Stack | Layers | Width | Rate (Hz) | Heads | Largest FF | Conv kernel | Matrix params | At 4 bits |
|---|---|---|---|---|---|---|---|---|
| 0 | 2 | 192 | 50 | 4 | 512 | 31 | 2.02M | 69.5 % |
| 1 | 2 | 256 | 25 | 4 | 768 | 31 | 3.78M | 40.8 % |
| 2 | 3 | 384 | 12.5 | 4 | 1024 | 15 | 11.60M | 18.4 % |
| 3 | 4 | 512 | 6.25 | 8 | 1536 | 15 | 30.22M | 13.8 % |
| 4 | 3 | 384 | 12.5 | 4 | 1024 | 15 | 11.60M | 100 % |
| 5 | 2 | 256 | 25 | 4 | 768 | 31 | 3.78M | 100 % |
| Part | Parameters |
|---|---|
| 4 bits: 193 encoder-path matrices | 25.35M |
| 2 bits: the other 97 | 38.37M |
| float16: biases, norms, depthwise convolutions, gates | 1.1M |
| int8 (as released): decoder and joiner output | 0.9M |
| File on the microSD card | 26.65 MB |
| Variant | 4-bit matrices | Size (MB) | Error | WER |
|---|---|---|---|---|
| Released weights, unquantized, same simulator | – | – | 0 | 4.02 % |
| Round to nearest, all at 4 bits | 290 | 36.67 | 0.0136 | 4.58 % |
| Round to nearest, all at 2 bits (small ones at 4) | 42 | 20.84 | 0.2194 | 21.76 % |
| Round to nearest, the 25 MB plan | 193 | 27.07 | 0.0445 | 8.83 % |
| + GPTQ, 128 clips | 193 | 27.07 | 0.0163 | 4.75 % |
| + correction toward the original | 193 | 27.07 | 0.0128 | 4.68 % |
| Clips | Activations | WER |
| 73 (dev-clean) | float | 7.27 % |
| int16 everywhere, static scales | 7.36 % | |
| int8 everywhere, static scales | 12.75 % | |
| test-clean, 500 | float | 3.33 % |
| the chosen activations, float products | 3.33 % | |
| the chosen activations, integer products | 3.39 % |
| Kernel | What it does |
|---|---|
| pq_decode2 , pq_decode4 | weight codes to int8 levels through a gather table |
| pq_tile16 , pq_tile16_n | 16 rows one group frames; the second also applies the norms and shifts |
| pq_fwht | the integer Hadamard transform of an input group (radix 8) |
| pq_q8_shift | int32 to int8 by a power-of-two shift |
| pq_out_tab16 , pq_out_add16 , pq_out_act8 | read-out: activation table, residual add, int8 output |
| pq_sumsq16 , pq_scale_bypass16 | BiasNorm’s sum of squares; its scale fused with the bypass |
| First port (float glue) | Integer port | ||
|---|---|---|---|
| Step | ms | Step | ms |
| plain float C | 51,300 | integer products | 1,257 |
| 2/4-bit products on PIE | 21,400 | PIE int8 rounding | 1,127 |
| int8 front end and joiner, fast math | 5,720 | int8 caches | 1,064 |
| two cores | 4,600 | softmax in int8, once | 983 |
| key-major attention | 2,750 | int16 FF hidden layer on PIE | 888 |
| Design | Encoder | Chunk | real time | |
| a | int8 products on PIE, float in between (the first port) | 1043.8 | 1176.9 | 0.92 |
| b | integer products, int8 attention caches and softmax rows | 776.5 | 909.7 | 0.71 |
| c | + int16 feed-forward hidden layer | 721.4 | 854.2 | 0.67 |
| d | + int16 residual stream | 700.7 | 834.4 | 0.65 |
| e | + integer convolution modules | 645.9 | 778.8 | 0.61 |
| f | + integer non-linear attention | 631.2 | 763.8 | 0.60 |
| Words | Sentences | False alarms | WER | |
|---|---|---|---|---|
| Phrases off | 0 / 60 | 62 / 72 | 0 (0) | 19.5 % |
| Phrases on (the chip) | 46 / 60 | 70 / 72 | 3 (0) | 3.4 % |
| Ours (the chip) | Released | |||||
|---|---|---|---|---|---|---|
| Set | Clips | greedy | beam 4 | greedy | beam 4 | Gap, beam 4 |
| LibriSpeech test-clean | 2620 | 4.52 | 4.34 | 4.03 | 3.94 | +0.40 |
| LibriSpeech test-other | 2939 | 10.89 | 10.26 | – | 9.17 | +1.09 |
| FLEURS English (US) test | 647 | 17.75 | 15.95 | 14.92 | 13.68 | +2.26 [1.63, 2.95] |
| Common Voice 17.0 English test | 1000 | 20.72 | 19.56 | 18.85 | 18.47 | +1.09 [0.50, 1.69] |
| FLEURS, scaled to peak 0.5 | 647 | 11.42 | 10.97 | 10.28 | 9.99 | +0.97 [0.68, 1.28] |
| Part | Size |
|---|---|
| weights, used in place | 26.65 MB |
| engine state and workspace | about 4 MB |
| screen frame buffer ( , RGB565) | 1.2 MB |
| free, at the lowest | about 0.5 MB |
| System | Params | Weights | Runs on | Streaming | clean | other | RT |
|---|---|---|---|---|---|---|---|
| This work, beam 4 | 65.7M | 2/4-bit, 26.65 MB | ESP32-P4, 2 cores | yes | 4.34 | 10.26 | 0.70 |
| This work, greedy | 65.7M | 2/4-bit, 26.65 MB | ESP32-P4, 2 cores | yes | 4.52 | 10.89 | |
| Kroko as released, beam 4 | 65.7M | int8, 71 MB | PC | yes | 3.94 | 9.17 | |
| Oído CTC, greedy [ 29 ] | 13M | int8, 14.0 MB | ESP32-S3, 1 core | no | 3.70 | 8.23 | 1.97 |
| Oído streaming, greedy [ 29 ] | 13M | int8 | ESP32-S3, 1 core | yes | 6.27 | 13.06 | |
| Oído streaming, with LM [ 29 ] | 14.3M | int8 | ESP32-S3, 1 core | yes | 4.86 | 11.03 | 1.96 |
Automatic Speech Recognition
Automatic speech recognition converts spoken language into text across speakers, languages, domains, and recording conditions.
See all 484 papers