| --- |
| license: apache-2.0 |
| tags: |
| - executorch |
| - xnnpack |
| - pte |
| - on-device |
| - automatic-speech-recognition |
| base_model: |
| - openai/whisper-small |
| --- |
| # Whisper-small β ExecuTorch (encoder + decoder) |
|
|
| Speech recognition in two `.pte` files: the encoder runs once per 30-second window, the |
| decoder once per generated token. Putting them in one graph would re-encode the audio on |
| every step. |
|
|
| | graph | build | file | size (MB) | corr vs fp32 eager | ms | eager ms | |
| |---|---|---|---|---|---|---| |
| | encoder | XNNPACK fp32 | `whisper_small_encoder_xnnpack_fp32.pte` | 352.8 | 1.000000 | 357.4 | 144.7 | |
| | encoder | XNNPACK fp16 | `whisper_small_encoder_xnnpack_fp16.pte` | 180.5 | 1.000000 | 619.5 | 144.4 | |
| | encoder | XNNPACK int8 | `whisper_small_encoder_xnnpack_int8.pte` | 98.3 | 0.999052 | 339.3 | 151.8 | |
| | encoder | Core ML | `whisper_small_encoder_coreml_all.pte` | 176.8 | 0.999942 | 95.5 | 143.4 | |
| | decoder | XNNPACK fp32 | `whisper_small_decoder_xnnpack_fp32.pte` | 774.0 | 1.000000 | 91.2 | 55.3 | |
| | decoder | XNNPACK fp16 | `whisper_small_decoder_xnnpack_fp16.pte` | 387.3 | 0.999994 | 188.6 | 55.3 | |
| | decoder | XNNPACK int8 | `whisper_small_decoder_xnnpack_int8.pte` | 315.5 | 0.994395 | 85.2 | 57.6 | |
| | decoder | Core ML | `whisper_small_decoder_coreml_all.pte` | 307.6 | 0.999863 | 13.7 | 55.6 | |
|
|
| Every file takes and returns fp32 tensors (token ids stay int64), so any encoder pairs with |
| any decoder. The lightest working pair is 405.9 MB. |
|
|
| - **Source**: [openai/whisper-small](https://huggingface.co/openai/whisper-small) |
| - **License**: Apache-2.0 |
| - **Encoder input**: log-mel spectrogram `[1, 80, 3000]` β 30 s at 16 kHz, 80 mel bins, hop |
| 160, window 400, exactly what `WhisperFeatureExtractor` produces |
| - **Decoder input**: the encoder output plus `decoder_input_ids [1, 128]` int64, |
| left-aligned and padded. Start with `<|startoftranscript|>`, a language token, |
| `<|transcribe|>`, `<|notimestamps|>`. |
|
|
| ## Decoding |
|
|
| No KV cache: the decoder is a static graph over a fixed 128-token window, so a greedy step |
| is take `argmax` of row `len-1`, append it, run again. Stop at `<|endoftext|>` (50257). 128 |
| tokens covers a 30-second window of ordinary speech; past that, start a new window. |
|
|
| That costs a full 128-position forward pass per token, which is the price of a static graph |
| that runs unchanged across runtimes and precisions. |
|
|
| ## Verification (Mac arm64, executorch 1.4.0, torch 2.13.0) |
|
|
| The two wrappers compose back to `WhisperForConditionalGeneration` exactly β max_abs_diff |
| **0.000e+00** β and every graph matches torch fp32 eager at the correlations above. Timings |
| are medians over 5 runs in one process: a relative reference, not a device number. |
|
|
| ## Two things worth knowing about the sizes |
|
|
| **The decoder `.pte` is larger than the decoder's weights.** Whisper ties `proj_out.weight` |
| to `decoder.embed_tokens.weight`, but the two uses need different representations: an |
| embedding table the portable kernels index into, and the same values packed into the XNNPACK |
| delegate's blob for the output matmul. Tying them in PyTorch does not tie them here. |
| Referencing the weight through `F.linear` instead of the `proj_out` module does not either β |
| exported both ways, whisper-tiny's decoder comes out at 198.0 MB exactly. |
|
|
| **The decoder's int8 build is the smallest portable one.** Dynamic int8 quantizes the linear weights and |
| leaves the token embedding table in fp32, and that table is 159.3 MB β 51,865 tokens at |
| 768 dimensions. On this size the rest of the decoder finally outweighs it: int8 comes in at 315.5 MB against fp16's 387.3 MB, so it is the smallest portable build and it ships. |
|
|
| Until recently there was no decoder int8 build at all, and this card said PT2E was observing |
| the int64 `decoder_input_ids`. That was wrong on both halves. |
| `XNNPACKQuantizer.transform_for_annotation` rewrites every scalar argument of |
| `add.Tensor`/`mul.Tensor` as `torch.tensor(float(arg))` whatever the node's dtype β one line |
| in ExecuTorch's `backends/xnnpack/quantizer/xnnpack_quantizer_utils.py`, still present on |
| main. In this decoder the casualty is `position_ids = torch.arange(...) + past_key_values_length` |
| (`modeling_whisper.py:749`, `past_key_values_length` being a python `int`): it comes back |
| float32, and the failure lands on `self.weight[position_ids]` β the **position** embedding |
| lookup, not the token ids, and no observer involved. Measured by running `prepare_pt2e` |
| with an empty quantizer and printing the failing node. |
|
|
| ### Checked in the task's own units |
|
|
| Correlation is a first filter. These are the numbers that decide: |
|
|
| - **encoder int8** β measured end to end β word error rate against the fp32 encoder: mean WER 0.0% (worst clip 0.0%) over 5 spoken sentences, int8 encoder against the fp32 encoder with the same fp32 decoder and the same waveform; the fp32 arm transcribes all five correctly, so the comparison is against a working control rather than against noise. |
| - **decoder int8** β measured end to end β word error rate against the fp32 decoder: mean WER 0.0% (worst clip 0.0%) over 5 spoken sentences, the int8 decoder against the fp32 one with the other half and the waveform held identical; the fp32 arm transcribes all five correctly, so the comparison is against a working control rather than against noise. |
|
|
| The sensitivity of that test, measured by injecting random noise into whisper-tiny's encoder output: rel_l2 0.03 (what int8 actually costs) and 0.10 both give WER 0.000; 0.20 and 0.40 give 0.025. Five clean sentences leave headroom, so a pass means *does not break the transcript*, not *indistinguishable at any error level*. |
| |
| ## Conversion |
| |
| ```bash |
| python convert/export_whisper.py small |
| ``` |
| |
| The ExecuTorch tree ships a single-graph Whisper example under `examples/models/whisper`; |
| this is that model with the halves separated. |
| |
| (conversion scripts: [executorch-models](https://github.com/john-rocky/executorch-models)) |
| |