BMartin1494 commited on
Commit
3e5b984
·
verified ·
1 Parent(s): 5e0f18f

Sync model repo (text/metadata)

Browse files
README.md CHANGED
@@ -2,6 +2,7 @@
2
  library_name: executorch
3
  display_name: Whisper Small INT8 — ExecuTorch + XNNPACK
4
  license: mit
 
5
  base_model: openai/whisper-small
6
  base_model_relation: quantized
7
  tags:
@@ -14,259 +15,205 @@ tags:
14
  - executorch
15
  - edge-ai
16
  - librispeech
17
- pipeline_tag: automatic-speech-recognition
18
  datasets:
19
  - librispeech_asr
20
  metrics:
21
  - wer
 
22
  model-index:
23
  - name: whisper-small-int8-xnnpack-executorch
24
  results:
25
  - task:
26
  type: automatic-speech-recognition
27
- name: Automatic Speech Recognition
28
  dataset:
29
  type: librispeech_asr
30
- name: LibriSpeech test-clean
31
  split: test
32
  args:
33
- evaluation_samples: 2625
34
  metrics:
35
  - type: wer
36
- value: 3.4314
37
- name: Word Error Rate (WER)
38
  - type: cer
39
- value: 1.3030
40
- name: Character Error Rate (CER)
41
  ---
42
 
43
- # Whisper Small INT8 Dynamic (ExecuTorch + XNNPACK)
44
 
45
- This is an INT8-quantized version of [openai/whisper-small](https://huggingface.co/openai/whisper-small) optimized for edge deployment on ARM devices using [ExecuTorch](https://github.com/pytorch/executorch) with the XNNPACK backend. The model uses 8da8w dynamic quantization for the 192 encoder/decoder Transformer-stack Linear weights via Optimum-ExecuTorch, plus a manual weight-only INT8 quantization pass on `decoder.embed_tokens`, and is exported to the `.pte` format for efficient inference on ARM Cortex-A processors (AWS Graviton, mobile ARM, embedded). The final output projection (`proj_out` / `lm_head`), positional embeddings, and encoder convolutional stem remain FP32.
 
 
 
46
 
47
  ## Key Highlights
48
 
49
- Compared to the FP32 baseline:
50
 
51
- - **2.72x smaller** — 1074.6592 MB to 395.0460 MB (63.2% reduction)
52
- - **1.2% lower peak memory** — 4038.56 MB to 3991.57 MB
53
- - **No accuracy loss** — WER 3.4596% 3.4314% (−0.0282 pp; the INT8 model is marginally better)
54
- - **Faster on-device (Vivo X300)** — total inference 25.4 s → 4.83 s (RTF 0.48266), prefill throughput +34.7%, decode throughput +588.7%
55
 
56
  ## Model Details
57
 
58
- ### Model Description
59
-
60
- Quantized version of Whisper Small, a Seq2Seq transformer for automatic speech recognition from OpenAI. The model is composed of an audio encoder that processes 80-channel log-mel spectrograms and an autoregressive decoder that generates text token-by-token. This export uses separate `encoder` and `text_decoder` ExecuTorch methods with the Optimum-ExecuTorch static-cache Seq2Seq export format. The selected bundle also patches decoder KV-cache writes so cross-attention cache updates use contiguous full-buffer `copy_()` writes and self-attention updates use `narrow(...).copy_()` instead of the slower indexed update path. The static cache resets decoder state between samples, but the exported CPU `.pte` does not reuse cross-attention K/V projections across decode steps.
61
-
62
- - **Developed by:** OpenAI
63
- - **Model type:** Automatic Speech Recognition
64
- - **License:** MIT
65
- - **Base model:** [openai/whisper-small](https://huggingface.co/openai/whisper-small) quantized, not finetuned
66
-
67
- ### Model Sources
68
-
69
- - **Repository:** https://huggingface.co/openai/whisper-small
70
- - **Upstream code:** https://github.com/openai/whisper
71
-
72
- ## How to Get Started with the Model
73
 
74
  ### Install dependencies
75
 
76
  ```bash
77
- pip install executorch torch transformers soundfile numpy
78
  ```
79
 
80
- ### Download the model
81
-
82
- ```python
83
- from huggingface_hub import hf_hub_download
84
-
85
- model_path = hf_hub_download(
86
- repo_id="Arm/whisper-small-int8-xnnpack-executorch",
87
- filename="whisper-small-int8-executorch.pte",
88
- )
89
- ```
90
-
91
- ### Run the example script
92
 
93
  ```bash
94
  python example.py
95
  ```
96
 
97
- The script expects `whisper-small-int8-executorch.pte`, `whisper-small-preprocessor-int8-executorch.pte`, and `sample_input.flac` next to `example.py`. Pass `--variant fp32` to run the FP32 baseline files instead. It transcribes the audio and saves the transcript to `transcription.json`:
98
-
99
- ```json
100
- {
101
- "transcription": "I thank all who have loved me in their hearts, with thanks and love from mine.",
102
- "generated_tokens": 19,
103
- "stop_reason": "eos",
104
- "elapsed_s": 1.104
105
- }
106
- ```
107
-
108
  ### Core inference loop
109
 
110
  ```python
111
- from executorch.runtime import Runtime
112
- from transformers import AutoTokenizer
113
  import torch
114
 
115
- tokenizer = AutoTokenizer.from_pretrained(".", local_files_only=True, use_fast=True)
116
- runtime = Runtime.get()
117
- program = runtime.load_program("whisper-small-int8-executorch.pte")
118
-
119
- encoder_method = program.load_method("encoder")
120
- decoder_method = program.load_method("text_decoder")
121
-
122
- # Preprocess: load audio → 16 kHz mono → log-mel features [1, 80, 3000]
123
- # (use the local whisper-small-preprocessor-int8-executorch.pte, or WhisperAudioProcessor as fallback)
124
-
125
- # Encoder pass (stateless, run once per utterance)
126
- encoder_hidden = encoder_method.execute([features.float().contiguous()])[0]
127
-
128
- # Autoregressive decoder: step through token-by-token
129
- tokens = [50258, 50259, 50359, 50363] # decoder_start + en + transcribe + notimestamps
130
- for step in range(128):
131
- input_tensor = torch.tensor([[tokens[-1]]], dtype=torch.long)
132
- pos_tensor = torch.tensor([step + len(tokens) - 1], dtype=torch.long)
133
- logits = decoder_method.execute([input_tensor, encoder_hidden, pos_tensor])[0]
134
- next_token = int(torch.as_tensor(logits).flatten().argmax())
135
- tokens.append(next_token)
136
- if next_token == 50257: # EOS
137
- break
138
-
139
- text = tokenizer.decode(
140
- tokens,
141
- skip_special_tokens=True,
142
- clean_up_tokenization_spaces=False,
143
- )
 
144
  ```
145
 
146
- ## Evaluation
147
-
148
- ### Testing Data, Factors & Metrics
 
 
149
 
150
- #### Testing Data
151
-
152
- Evaluated on the full LibriSpeech test-clean split (2,625 utterances).
153
 
154
- #### Metrics
155
 
156
- - **WER (Word Error Rate)** — primary ASR metric; lower is better. Measures word-level edit distance between hypothesis and reference.
157
- - **CER (Character Error Rate)** character-level edit distance; lower is better.
 
158
 
159
- Both metrics use Whisper tokenizer normalization and corpus-level Levenshtein distance.
160
 
161
- ### Results
 
 
 
162
 
163
- **Accuracy**
164
 
165
- | Metric | FP32 (Original) | INT8 Dynamic (Optimized) | Delta |
166
- |---|---|---|---|
167
- | WER | 3.4596% | 3.4314% | −0.0282 pp |
168
- | CER | 1.3368% | 1.3030% | −0.0338 pp |
169
 
170
- **Efficiency**
171
 
172
- | Metric | FP32 (Original) | INT8 Dynamic (Optimized) | Improvement |
173
  |---|---|---|---|
174
- | Model Size (.pte) | 1074.6592 MB | 395.0460 MB | 2.72x smaller |
175
- | Peak RSS Memory† | 4038.56 MB | 3991.57 MB | 46.99 MB lower |
176
- | Prefill Throughput† | 940.14 tok/s | 1266.64800 tok/s | 1.35x faster |
177
- | Decode Throughput† | 1.31163 tok/s | 9.03314 tok/s | 6.89x faster |
178
- | Prompt Tokens† | 1500.00 | 1500.00 | |
179
- | Generated Tokens† | 31.20 | 32.90 | |
180
- | Aggregate Sampling Time† | 0.00 ms | 0.00 ms | |
181
- | Model Load Time† | 590.50 ms | 485.40 ms | 105.1 ms lower |
182
- | Prompt Eval Time† | 1,595.60 ms | 1,184.30 ms | 411.3 ms lower |
183
- | Time to First Token† | 2,359.70 ms | 1,290.90 ms | 1,068.8 ms lower |
184
- | Decode Time† | 23,022.40 ms | 3,535.70 ms | 19,486.7 ms lower |
185
- | Total Inference Time† | 25,382.10 ms | 4,826.60 ms | 20,555.5 ms lower |
186
- | Audio Length† | 10.000 s | 10.000 s | — |
187
- | Batch Size† | 1 | 1 | — |
188
- | Real-Time Factor (RTF)† | 2.538 | 0.48266 | 5.26x faster |
189
-
190
- †On-device latency and peak RSS memory were measured on a **Vivo X300** (Android, ARM) over ADB on a fixed 10-second clip, batch size 1, greedy decoding (`--temperature 0`).
191
- Accuracy (WER/CER) was measured on a MacBook Pro M4 (CPU, ExecuTorch XNNPACK) over the full LibriSpeech test-clean split (2,625 utterances). Model size is platform-independent.
192
-
193
- On the 10-second smartphone clip, end-to-end transcription is ~4.83 s for the selected INT8 export vs ~25.4 s for FP32, giving RTF 0.48266 and faster-than-real-time execution. INT8 accelerates the memory-bound prefill path substantially, while the `StaticLayer.update()` cache-write patch is the decisive decode improvement: it avoids the indexed `index_copy_` / `index_put`-style cache-update path by using contiguous copy/slice writes for K/V cache updates. Cross-attention K/V recompute is still present, but the exported decoder no longer pays the old indexed cache-write overhead.
194
 
195
  ## Technical Specifications
196
 
197
  ### Objective
198
 
199
- English automatic speech recognition from 16 kHz mono audio using a Seq2Seq encoder-decoder transformer.
 
 
200
 
201
  ### Quantization
202
 
203
- - **Method:** TorchAO 8da8w (dynamic INT8) on the 192 encoder/decoder Transformer-stack Linear weights via `int8_dynamic_activation_int8_weight`, plus weight-only INT8 on `decoder.embed_tokens`
204
- - **Precision:** INT8 dynamic per-token activations, INT8 per-channel Linear weights, INT8 per-axis embedding weights for `decoder.embed_tokens`
205
- - **Backend:** XNNPACK
206
- - **Calibration:** None required — activations are quantized dynamically at runtime, so this recipe needs no calibration data
207
- - **Layers kept in FP32:**
208
- - Encoder conv stem (`conv1`, `conv2`) — Conv1d layers not covered by Linear quantization; kept FP32 in this latency-oriented profile to avoid adding dequantization work to the prefill path
209
- - `encoder.embed_positions` (sinusoidal positional embeddings) — read-only at runtime; its indexing path is not compatible with `IntxUnpackedToInt8Tensor` during `torch.export`
210
- - `decoder.embed_positions` — same `torch.export` indexing constraint
211
- - Final output projection (`proj_out` / `lm_head`) — diagnostics report `output_projection_best_match_is_quantized=false`
212
- - **Additional quantized embedding:**
213
- - `decoder.embed_tokens` — weight-only INT8 via `IntxWeightOnlyConfig(int8, PerAxis(0))`
214
- - **Coverage:** 192 Transformer-stack Linear weights are quantized (encoder 12×6 = 72, decoder 12×10 = 120). The missing 193rd Linear is the final output projection, which remains FP32. Because the decoder stack count is 120, the decoder cross-attention `encoder_attn.k_proj` / `encoder_attn.v_proj` projections are included in the 8da8w INT8 path; they are still recomputed every generated token in the exported CPU graph.
215
-
216
- ### Export Pipeline
217
-
218
- 1. Load pretrained `openai/whisper-small` via `optimum.exporters.executorch.tasks.asr.load_seq2seq_speech_model`
219
- 2. Apply 8da8w dynamic quantization to the encoder/decoder Transformer-stack Linear weights via TorchAO
220
- 3. Disable Optimum `qembedding` and manually quantize only `decoder.embed_tokens` to weight-only INT8, leaving `encoder.embed_positions` and `decoder.embed_positions` in FP32
221
- 4. Patch the Optimum decoder cache (`Seq2SeqLMDecoderExportableModuleWithStaticCache`) to register `cumulative_length` tensors as buffers for `torch.export` compatibility, and reset self-/cross-attention cache positions on each decoder step
222
- 5. Replace `decoder.embed_tokens` with an exportable INT8 wrapper that performs gather + dequantization with standard tensor ops, avoiding the unsupported runtime kernel `quantized_decomposed::embedding_byte.out`
223
- 6. Patch `transformers.cache_utils.StaticLayer.update()` so cross-attention K/V cache updates use full-buffer `copy_()` and self-attention updates use `narrow(...).copy_()` instead of indexed writes
224
- 7. Export via `optimum.exporters.executorch.convert.export_to_executorch` with the XNNPACK recipe
225
- 8. Produces a Seq2Seq `.pte` with two ExecuTorch methods: `encoder` and `text_decoder`, plus the static-cache decoder export. On CPU/ExecuTorch this does not provide reusable cached cross-attention K/V projections.
226
-
227
- ### Preprocessing
228
 
229
- | Property | Value |
230
- |---|---|
231
- | Audio sample rate | 16,000 Hz (mono) |
232
- | Feature type | Log-mel spectrogram |
233
- | Input shape | `[1, 80, 3000]` (batch × mel bins × time frames) |
234
- | Data type | float32 |
235
- | Time coverage | 30 seconds (padded or trimmed) |
236
- | Preferred preprocessor | Local `whisper-small-preprocessor-int8-executorch.pte` next to `whisper-small-int8-executorch.pte` |
237
 
238
- **Steps:**
239
- 1. Load audio file (`.flac` or `.wav`) with `soundfile`, convert to mono float32
240
- 2. Resample to 16,000 Hz if needed
241
- 3. Prefer the exported ExecuTorch preprocessor `whisper-small-preprocessor-int8-executorch.pte`; if unavailable, fall back to `WhisperAudioProcessor(feature_size=80, max_audio_len=300, stack_output=True)`
242
- 4. Produce log-mel spectrogram features with shape `[1, 80, 3000]`
243
 
244
- No additional normalization is required before model execution.
245
 
246
- ### Postprocessing
247
-
248
- | Property | Value |
249
  |---|---|
250
- | Encoder output | `[1, 1500, 768]` (hidden states) |
251
- | Decoder output per step | `[vocab_size]` (logit vector, 51865 tokens) |
252
- | Decoding strategy | Greedy argmax |
253
- | Tokenizer source | `openai/whisper-small` tokenizer |
254
-
255
- **Steps:**
256
- 1. Run `encoder` method once per utterance to obtain hidden states `[1, 1500, 768]`
257
- 2. Start with `decoder_start_token_id = 50258` and force prefix tokens `[50259, 50359, 50363]` (`<|en|>`, `<|transcribe|>`, `<|notimestamps|>`)
258
- 3. At each free decoding step: run `text_decoder(last_token, encoder_hidden, cache_position)` → logits `[vocab_size]`
259
- 4. Apply token suppression (non-speech / special tokens set to `-inf`) and apply `begin_suppress_tokens = [220, 50257]` only at the first free step
260
- 5. Argmax over vocabulary → next token ID
261
- 6. Stop on EOS token (50257), `max_generation_tokens=128`, timeout (`120` seconds), or repetition guard
262
- 7. Decode token IDs to text with `tokenizer.decode(..., skip_special_tokens=True, clean_up_tokenization_spaces=False)`
263
-
264
- Decoder cache must be reloaded between samples (`program.load_method("text_decoder")`) to reset decoder cache state. This reset does not change the decode bottleneck: cross-attention K/V projections over the encoder frames are still recomputed per generated token in the exported CPU graph.
265
 
266
  ## Known Limitations
267
 
268
- - Accuracy metrics come from the full LibriSpeech test-clean split (2,625 utterances); they should be compared against results produced with the same decoding and normalization setup.
269
- - The static decoder cache has a fixed maximum sequence length; utterances requiring more than 128 generated tokens will be truncated. This is separate from cross-attention K/V reuse, which is not available in the exported CPU `.pte` graph.
270
- - Cross-attention K/V recompute over the 1500 encoder frames is still present. The K/V projection weights are INT8 8da8w in the selected bundle, and the selected export removes the previous indexed cache-write overhead with contiguous copy/slice writes; reusable cached cross-attention K/V projections are still not available in the exported CPU `.pte` graph.
271
- - The decoder processes audio as a fixed 30-second window; audio shorter than 30 seconds is zero-padded.
272
- - Peak RSS memory and latency figures were collected on a Vivo X300 (Android, ARM); accuracy was measured separately on a MacBook Pro M4. On-device numbers are device-specific and may differ on other ARM hardware, such as AWS Graviton or other phones.
 
 
 
 
 
 
2
  library_name: executorch
3
  display_name: Whisper Small INT8 — ExecuTorch + XNNPACK
4
  license: mit
5
+ pipeline_tag: automatic-speech-recognition
6
  base_model: openai/whisper-small
7
  base_model_relation: quantized
8
  tags:
 
15
  - executorch
16
  - edge-ai
17
  - librispeech
 
18
  datasets:
19
  - librispeech_asr
20
  metrics:
21
  - wer
22
+ - cer
23
  model-index:
24
  - name: whisper-small-int8-xnnpack-executorch
25
  results:
26
  - task:
27
  type: automatic-speech-recognition
 
28
  dataset:
29
  type: librispeech_asr
30
+ name: LibriSpeech ASR (test-clean)
31
  split: test
32
  args:
33
+ evaluation_samples: 2620
34
  metrics:
35
  - type: wer
36
+ value: 3.41
37
+ name: WER (ExecuTorch)
38
  - type: cer
39
+ value: 1.29
40
+ name: CER (ExecuTorch)
41
  ---
42
 
43
+ # Whisper Small INT8 (ExecuTorch + XNNPACK + KleidiAI)
44
 
45
+ INT8 8da8w quantized version of [openai/whisper-small](https://huggingface.co/openai/whisper-small),
46
+ optimized for ARM deployment using ExecuTorch, XNNPACK, and KleidiAI. The model targets
47
+ on-device English speech-to-text transcription on Android and ARM-based edge hardware,
48
+ delivering near-identical accuracy at substantially lower latency and memory footprint.
49
 
50
  ## Key Highlights
51
 
52
+ Compared to the FP32 baseline on ARM hardware (Vivo X300, Arm C1):
53
 
54
+ - **2.72x smaller** — .pte artifact shrinks from 1074.76 MB to 395.05 MB
55
+ - **1.40x faster inference** — end-to-end latency drops from 10927.0 ms to 7802.5 ms (p50)
56
+ - **WER preserved** — 3.45% to 3.41% on LibriSpeech test-clean (2620 utterances)
 
57
 
58
  ## Model Details
59
 
60
+ | Property | Value |
61
+ |---|---|
62
+ | Developed by | OpenAI |
63
+ | Model type | Automatic Speech Recognition (encoder-decoder Transformer) |
64
+ | Language | English |
65
+ | License | MIT |
66
+ | Base model | [openai/whisper-small](https://huggingface.co/openai/whisper-small) |
67
+ | Modification | Post-training quantization (8da8w), not finetuned |
68
+ | Parameter count | 241.73M |
69
+ | PyTorch state dict size | 922.31 MB |
70
+ | Optimized .pte size | 395.05 MB |
71
+
72
+ ## How to Get Started
 
 
73
 
74
  ### Install dependencies
75
 
76
  ```bash
77
+ pip install transformers torch soundfile numpy
78
  ```
79
 
80
+ ### Run inference
 
 
 
 
 
 
 
 
 
 
 
81
 
82
  ```bash
83
  python example.py
84
  ```
85
 
 
 
 
 
 
 
 
 
 
 
 
86
  ### Core inference loop
87
 
88
  ```python
89
+ from transformers import WhisperForConditionalGeneration, WhisperProcessor
 
90
  import torch
91
 
92
+ MODEL_NAME = "openai/whisper-small"
93
+ LANGUAGE = "en"
94
+ TASK = "transcribe"
95
+ MAX_NEW_TOKENS = 128
96
+
97
+ processor = WhisperProcessor.from_pretrained(MODEL_NAME)
98
+ model = WhisperForConditionalGeneration.from_pretrained(MODEL_NAME)
99
+ model.eval()
100
+
101
+ def transcribe(audio_path: str) -> str:
102
+ """Load audio and return transcribed text."""
103
+ import soundfile as sf
104
+ import numpy as np
105
+
106
+ audio_np, sr = sf.read(audio_path, dtype="float32")
107
+ if audio_np.ndim == 2:
108
+ audio_np = audio_np.mean(axis=1)
109
+
110
+ features = processor.feature_extractor(
111
+ audio_np, sampling_rate=sr, return_tensors="pt"
112
+ ).input_features # [1, 80, 3000]
113
+
114
+ with torch.no_grad():
115
+ output_ids = model.generate(
116
+ features,
117
+ language=LANGUAGE,
118
+ task=TASK,
119
+ max_new_tokens=MAX_NEW_TOKENS,
120
+ )
121
+ return processor.tokenizer.batch_decode(output_ids, skip_special_tokens=True)[0].strip()
122
  ```
123
 
124
+ > **Note on .pte artifacts:** The `pte_optimized/` and `pte_original/` directories contain
125
+ > ExecuTorch-serialized models for on-device ARM inference. Running them requires the
126
+ > ExecuTorch C++ seq2seq runner — they are not suitable for a simple Python
127
+ > `method.execute()` call. The `example.py` script above uses the HuggingFace Transformers
128
+ > library for Python-based evaluation and prototyping.
129
 
130
+ ## Evaluation
 
 
131
 
132
+ ### Testing data
133
 
134
+ 2620 utterances from the LibriSpeech ASR `test-clean` split. Calibration: none required
135
+ (dynamic activation quantization). Dynamic weight-only 8da8w weights INT8 offline,
136
+ activations INT8 quantized per-token at runtime.
137
 
138
+ ### Metrics
139
 
140
+ | Metric | Description |
141
+ |---|---|
142
+ | WER | Word Error Rate (lower is better) |
143
+ | CER | Character Error Rate (lower is better) |
144
 
145
+ ### Accuracy results
146
 
147
+ | Model | WER | CER |
148
+ |---|---|---|
149
+ | openai/whisper-small (FP32) | 3.45% | 1.33% |
150
+ | whisper-small INT8 8da8w (ExecuTorch) | **3.41%** | **1.29%** |
151
 
152
+ ### Efficiency results (Vivo X300, Arm C1)
153
 
154
+ | Metric | FP32 Original | INT8 Optimized | Improvement |
155
  |---|---|---|---|
156
+ | .pte file size | 1074.76 MB | 395.05 MB | **2.72x smaller** |
157
+ | End-to-end latency p50 | 10927.0 ms | 7802.5 ms | **1.40x faster** |
158
+ | End-to-end latency p90 | 11742.0 ms | 8027.0 ms | 1.46x faster |
159
+ | RTFx | 0.641 | 0.897 | **+40%** |
160
+ | Decode throughput | 3.62 tok/s | 5.69 tok/s | **+57%** |
161
+ | Prefill throughput | 620.0 tok/s | 745.6 tok/s | +20% |
162
+ | Time to first token (TTFT) | 2683.9 ms | 2196.2 ms | -18% |
163
+ | Peak memory (USS) | 5226.97 MB | 4662.29 MB | **1.12x less** |
164
+ | Model load time | 1327.3 ms | 1015.0 ms | 1.31x faster |
165
+
166
+ Benchmark conditions: batch size 1, ~7 s audio clips, 50 runs (10 warmup), offline mode,
167
+ 16 kHz input on Android 16 / OriginOS 6.
 
 
 
 
 
 
 
 
168
 
169
  ## Technical Specifications
170
 
171
  ### Objective
172
 
173
+ English speech-to-text transcription using Whisper's encoder-decoder Transformer
174
+ architecture. The encoder processes 80-bin log-mel spectrograms; the decoder
175
+ autoregressively generates token IDs which are decoded to text with the Whisper tokenizer.
176
 
177
  ### Quantization
178
 
179
+ - **Method:** TorchAO 8da8w 8-bit dynamic activation quantization + 8-bit weight quantization
180
+ - **Calibration:** none required (dynamic activation quantization). Dynamic weight-only 8da8w weights INT8 offline, activations INT8 quantized per-token at runtime.
181
+ - **Weight granularity:** per-channel
182
+ - **Symmetry:** symmetric INT8
183
+ - **Skipped layers (kept FP32):** `proj_out`/`lm_head`, encoder + decoder positional embeddings, `encoder.conv1` / `encoder.conv2`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
+ ### Export pipeline
 
 
 
 
 
 
 
186
 
187
+ 1. Load pretrained FP32 `openai/whisper-small` from HuggingFace
188
+ 2. Export encoder and decoder to ExecuTorch Seq2Seq format via Optimum ExecuTorch
189
+ 3. Apply 8da8w (8-bit dynamic activation, 8-bit weight) quantization with TorchAO
190
+ 4. Export tokenizer files (`tokenizer.json`, `tokenizer_config.json`, `special_tokens_map.json`)
191
+ 5. Export audio preprocessor to `whisper_preprocessor.pte`
192
 
193
+ ### Input preprocessing
194
 
195
+ | Step | Parameters |
 
 
196
  |---|---|
197
+ | Load audio as waveform | 16 kHz mono |
198
+ | Log-mel spectrogram | n_mels=80, hop_length=160, n_fft=400, sample_rate=16000, duration=30s |
199
+ | Pad or trim | 3000 time frames |
200
+
201
+ **Input tensor:** `[1, 80, 3000]`, dtype `float32` — log-mel spectrogram (batch=1, mel bins, time frames)
202
+
203
+ ### Output postprocessing
204
+
205
+ 1. Autoregressive greedy decoding with the Whisper tokenizer
206
+ 2. Skip special tokens
 
 
 
 
 
207
 
208
  ## Known Limitations
209
 
210
+ - Evaluated on English speech (LibriSpeech test-clean); performance on other languages or
211
+ accents has not been measured for this INT8 variant.
212
+ - The `.pte` runtime on Python requires the ExecuTorch C++ seq2seq runner not a simple
213
+ `method.execute()` call. Use `example.py` (HuggingFace Transformers) for Python prototyping.
214
+ - Maximum audio duration: 30 seconds per chunk (3000 mel time frames at 16 kHz).
215
+ Longer audio must be chunked externally.
216
+ - Android latency measured on Vivo X300 with Arm C1 processor (1x C1-Ultra, 3x C1-Premium,
217
+ 4x C1-Pro cores at 4.21 / 3.5 / 2.7 GHz). Results may differ on other ARM devices.
218
+ - RTFx > 1.0 indicates real-time capable transcription on this hardware; values below 1.0
219
+ on lower-tier devices are expected.
benchmarks/whisper-small-vivo-x300-fp32.yaml CHANGED
@@ -51,11 +51,13 @@ context:
51
  num_runs: 10
52
  performance:
53
  end_to_end_latency_ms:
54
- p50: 25382.10
55
- rtfx: 0.39
56
- peak_memory_mb: 4038.56
57
- model_load_time_ms: 590.50
 
 
58
  accuracy:
59
- normalised_wer: 3.4596
60
  normalisation: whisper_tokenizer.normalize (Whisper-style)
61
- cer: 1.3368
 
51
  num_runs: 10
52
  performance:
53
  end_to_end_latency_ms:
54
+ p50: 10927.0
55
+ p90: 11742.0
56
+ p99: 11742.0
57
+ rtfx: 0.6406
58
+ peak_memory_mb: 5226.97
59
+ model_load_time_ms: 1327.0
60
  accuracy:
61
+ normalised_wer: 3.4077
62
  normalisation: whisper_tokenizer.normalize (Whisper-style)
63
+ cer: 1.2888
benchmarks/whisper-small-vivo-x300-int8.yaml CHANGED
@@ -64,11 +64,13 @@ context:
64
  num_runs: 10
65
  performance:
66
  end_to_end_latency_ms:
67
- p50: 4826.60
68
- rtfx: 2.07
69
- peak_memory_mb: 3991.57
70
- model_load_time_ms: 485.40
 
 
71
  accuracy:
72
- normalised_wer: 3.4314
73
  normalisation: whisper_tokenizer.normalize (Whisper-style)
74
- cer: 1.3030
 
64
  num_runs: 10
65
  performance:
66
  end_to_end_latency_ms:
67
+ p50: 7802.5
68
+ p90: 8027.0
69
+ p99: 8027.0
70
+ rtfx: 0.897
71
+ peak_memory_mb: 4662.29
72
+ model_load_time_ms: 1015.0
73
  accuracy:
74
+ normalised_wer: 3.407
75
  normalisation: whisper_tokenizer.normalize (Whisper-style)
76
+ cer: 1.288
config.yaml CHANGED
@@ -1,17 +1,19 @@
1
  input:
2
  shape: [1, 80, 3000]
3
  dtype: float32
4
- description: "Log-mel spectrogram features produced by local whisper-small-preprocessor-int8-executorch.pte when available, otherwise by WhisperAudioProcessor"
5
  preprocessing:
6
- - load_audio: "Load .flac/.wav file with soundfile; convert to mono float32"
7
- - resample: "Linear interpolation to 16 kHz if needed"
8
- - feature_extraction: "Prefer local whisper-small-preprocessor-int8-executorch.pte; fallback to WhisperAudioProcessor(feature_size=80, max_audio_len=300, stack_output=True) → [1, 80, 3000]"
 
 
 
 
 
9
 
10
  output:
11
- shape: "[1, 51865] per decode step"
12
- format: "Decoder logits over the 51865-token vocabulary at each autoregressive step; greedy argmax decoding produces token IDs"
13
  postprocessing:
14
- decoding: "Greedy argmax over vocab dimension at each autoregressive step"
15
- prompt: "[50258, 50259, 50359, 50363] # decoder_start + <|en|> + <|transcribe|> + <|notimestamps|>"
16
- stop_condition: "EOS token (50257), max_generation_tokens (128), timeout (120 s), or repetition_guard"
17
- output_text: "tokenizer.decode(token_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)"
 
1
  input:
2
  shape: [1, 80, 3000]
3
  dtype: float32
4
+ description: "Log-Mel spectrogram (batch=1, mel_bins=80, time_frames=3000 = 30s at 16kHz)"
5
  preprocessing:
6
+ - load_audio_as_waveform_16khz
7
+ - apply_log_mel_spectrogram:
8
+ n_mels: 80
9
+ hop_length: 160
10
+ n_fft: 400
11
+ sample_rate: 16000
12
+ duration_s: 30
13
+ - pad_or_trim_to_3000_frames
14
 
15
  output:
16
+ format: "Token ID sequence (autoregressive generation)"
 
17
  postprocessing:
18
+ - greedy_decode_with_whisper_tokenizer
19
+ - skip_special_tokens: true
 
 
example.py CHANGED
@@ -1,381 +1,128 @@
1
- """Minimal inference example for Whisper Small INT8 using ExecuTorch.
2
 
3
- Loads a quantized .pte model and transcribes a single audio file.
4
- The INT8 model was exported via Optimum-ExecuTorch with 8da8w dynamic
5
- quantization on the 192 encoder/decoder Transformer-stack Linear weights plus
6
- a manual weight-only INT8 pass on `decoder.embed_tokens`. It uses separate
7
- 'encoder' and 'text_decoder' ExecuTorch methods. Decoder cache state is reset
8
- between samples, but the exported CPU graph does not reuse cross-attention K/V
9
- projections across decode steps.
10
 
11
- This example loads the tokenizer from the base Hugging Face Whisper repo and
12
- uses the root-level ExecuTorch model/preprocessor files in this directory.
13
 
14
  Requirements:
15
- pip install executorch torch transformers soundfile numpy
16
  """
17
 
18
- import argparse
19
  import json
20
- import time
21
  from pathlib import Path
22
 
23
- import numpy as np
24
  import torch
25
- from executorch.runtime import Runtime
26
- from transformers import AutoTokenizer
27
 
28
- # -- Configuration -------------------------------------------------------------
29
  AUDIO_PATH = "sample_input.flac"
30
- TOKENIZER_REPO = "openai/whisper-small"
31
- MODEL_FILENAMES = {
32
- "int8": "whisper-small-int8-executorch.pte",
33
- "fp32": "whisper-small-fp32-executorch.pte",
34
- }
35
- PREPROCESSOR_FILENAMES = {
36
- "int8": "whisper-small-preprocessor-int8-executorch.pte",
37
- "fp32": "whisper-small-preprocessor-fp32-executorch.pte",
38
- }
39
-
40
- DECODER_START_TOKEN_ID = 50258
41
- FORCED_PREFIX_IDS = [50259, 50359, 50363] # <|en|>, <|transcribe|>, <|notimestamps|>
42
- EOS_TOKEN_ID = 50257
43
-
44
- MAX_GENERATION_TOKENS = 128
45
- MAX_SECONDS_PER_SAMPLE = 120.0
46
- REPETITION_GUARD_REPEATS = 3
47
- REPETITION_GUARD_MIN_PATTERN_LEN = 2
48
- REPETITION_GUARD_MAX_PATTERN_LEN = 64
49
-
50
- # Tokens forced to -inf during decoding (Whisper non-speech / special tokens).
51
- SUPPRESS_TOKENS = (
52
- 1, 2, 7, 8, 9, 10, 14, 25, 26, 27, 28, 29, 31, 58, 59, 60, 61, 62, 63,
53
- 90, 91, 92, 93, 359, 503, 522, 542, 873, 893, 902, 918, 922, 931, 1350,
54
- 1853, 1982, 2460, 2627, 3246, 3253, 3268, 3536, 3846, 3961, 4183, 4667,
55
- 6585, 6647, 7273, 9061, 9383, 10428, 10929, 11938, 12033, 12331, 12562,
56
- 13793, 14157, 14635, 15265, 15618, 16553, 16604, 18362, 18956, 20075,
57
- 21675, 22520, 26130, 26161, 26435, 28279, 29464, 31650, 32302, 32470,
58
- 36865, 42863, 47425, 49870, 50254, 50257, 50258, 50358, 50359, 50360,
59
- 50361, 50362,
60
- )
61
- BEGIN_SUPPRESS_TOKENS = (220, 50257)
62
-
63
-
64
- def parse_args() -> argparse.Namespace:
65
- parser = argparse.ArgumentParser(
66
- description="Run Whisper Small ExecuTorch inference from an exported model bundle."
67
- )
68
- parser.add_argument(
69
- "--variant",
70
- choices=sorted(MODEL_FILENAMES),
71
- default="int8",
72
- help="Model variant to run. Defaults to the INT8 optimized export.",
73
- )
74
- parser.add_argument(
75
- "--audio",
76
- default=AUDIO_PATH,
77
- help="Path to the input audio file (.flac/.wav).",
78
- )
79
- return parser.parse_args()
80
-
81
-
82
- def load_audio(audio_path: str) -> tuple[np.ndarray, int]:
83
- import soundfile as sf
84
-
85
- waveform, sample_rate = sf.read(str(audio_path), dtype="float32")
86
- if waveform.ndim == 2:
87
- waveform = waveform.mean(axis=1)
88
- return waveform, int(sample_rate)
89
-
90
-
91
- def resample_to_16k(waveform: np.ndarray, sample_rate: int) -> np.ndarray:
92
- if sample_rate == 16000:
93
- return waveform
94
- target_len = int(round(len(waveform) * 16000 / sample_rate))
95
- resampled = np.interp(
96
- np.linspace(0, len(waveform) - 1, target_len),
97
- np.arange(len(waveform)),
98
- waveform,
99
- )
100
- return resampled.astype(np.float32)
101
-
102
-
103
- def load_preprocessor(preprocessor_path: Path):
104
- if not preprocessor_path.exists():
105
- return None, None
106
-
107
- runtime = Runtime.get()
108
- program = runtime.load_program(str(preprocessor_path))
109
- method_names = sorted(program.method_names)
110
- method_name = "forward" if "forward" in method_names else method_names[0]
111
- return program, program.load_method(method_name)
112
-
113
-
114
- def preprocess(audio_path: str, preprocessor_method) -> torch.Tensor:
115
- """Load audio, resample to 16 kHz mono, and produce [1, 80, 3000] log-mel features."""
116
- waveform, sample_rate = load_audio(audio_path)
117
- waveform_16k = resample_to_16k(waveform, sample_rate)
118
- waveform_tensor = torch.from_numpy(waveform_16k).float().contiguous()
119
-
120
- if preprocessor_method is not None:
121
- outputs = preprocessor_method.execute([waveform_tensor])
122
- features = outputs[0]
123
- if isinstance(features, (list, tuple)):
124
- features = features[0]
125
- return torch.as_tensor(features).float().contiguous()
126
-
127
- from executorch.extension.audio.mel_spectrogram import WhisperAudioProcessor
128
-
129
- fallback_preprocessor = WhisperAudioProcessor(
130
- feature_size=80,
131
- max_audio_len=300,
132
- stack_output=True,
133
- )
134
- with torch.no_grad():
135
- features = fallback_preprocessor(waveform_tensor)
136
- return features.float().contiguous()
137
-
138
-
139
- def load_model(pte_path: str) -> tuple:
140
- """Load the .pte program and its inference methods (encoder + text_decoder)."""
141
- runtime = Runtime.get()
142
- program = runtime.load_program(pte_path)
143
- available = sorted(program.method_names)
144
- print(f" Available methods: {available}")
145
-
146
- if "encoder" in available and "text_decoder" in available:
147
- return program, {
148
- "format": "seq2seq",
149
- "encoder": program.load_method("encoder"),
150
- "decoder": program.load_method("text_decoder"),
151
- }
152
- if "forward" in available:
153
- return program, {
154
- "format": "forward",
155
- "forward": program.load_method("forward"),
156
- }
157
- raise RuntimeError(f"Unknown export format. Methods found: {available}")
158
-
159
-
160
- def apply_suppression(scores: torch.Tensor, first_free_step: bool) -> torch.Tensor:
161
- vocab_size = scores.shape[-1]
162
- out = scores.clone()
163
- # Do not suppress EOS globally; otherwise the decode loop can never finish
164
- # naturally and will fall through to max token / repetition guards.
165
- valid_suppress = [t for t in SUPPRESS_TOKENS if 0 <= t < vocab_size and t != EOS_TOKEN_ID]
166
- if valid_suppress:
167
- out[0, valid_suppress] = float("-inf")
168
- if first_free_step:
169
- valid_begin = [t for t in BEGIN_SUPPRESS_TOKENS if 0 <= t < vocab_size]
170
- if valid_begin:
171
- out[0, valid_begin] = float("-inf")
172
- return out
173
-
174
-
175
- def decode_tokens(tokenizer, token_ids: list[int]) -> str:
176
- return tokenizer.decode(
177
- token_ids,
178
- skip_special_tokens=True,
179
- clean_up_tokenization_spaces=False,
180
- ).strip()
181
-
182
-
183
- def find_repeated_suffix_pattern(
184
- token_ids: list[int],
185
- *,
186
- repeats: int = REPETITION_GUARD_REPEATS,
187
- min_pattern_len: int = REPETITION_GUARD_MIN_PATTERN_LEN,
188
- max_pattern_len: int = REPETITION_GUARD_MAX_PATTERN_LEN,
189
- ) -> int | None:
190
- total = len(token_ids)
191
- upper = min(max_pattern_len, total // repeats)
192
- for pattern_len in range(min_pattern_len, upper + 1):
193
- pattern = token_ids[-pattern_len:]
194
- if all(
195
- token_ids[-pattern_len * (idx + 1) : -pattern_len * idx or None] == pattern
196
- for idx in range(repeats)
197
- ):
198
- return pattern_len
199
- return None
200
-
201
-
202
- def transcribe_seq2seq(
203
- program,
204
- encoder_method,
205
- decoder_method,
206
- features: torch.Tensor,
207
- tokenizer,
208
- ) -> dict:
209
- """Run encoder once, then autoregressively decode token-by-token."""
210
- # Reload the decoder method to reset decoder cache state for this utterance.
211
- decoder_method = program.load_method("text_decoder")
212
-
213
- encoder_outputs = encoder_method.execute([features])
214
- encoder_hidden = encoder_outputs[0]
215
- if isinstance(encoder_hidden, (list, tuple)):
216
- encoder_hidden = encoder_hidden[0]
217
- encoder_hidden = torch.as_tensor(encoder_hidden).float().contiguous()
218
-
219
- forced_prefix = list(FORCED_PREFIX_IDS)
220
- tokens = [DECODER_START_TOKEN_ID]
221
- cache_position = 0
222
- forced_prefix_idx = 0
223
- generated_token_count = 0
224
- stop_reason = "max_tokens"
225
- started = time.perf_counter()
226
- generated_free_tokens: list[int] = []
227
-
228
- for _step in range(MAX_GENERATION_TOKENS + len(forced_prefix)):
229
- input_tensor = torch.tensor([[tokens[-1]]], dtype=torch.long).contiguous()
230
- pos_tensor = torch.tensor([cache_position], dtype=torch.long).contiguous()
231
-
232
- decoder_outputs = decoder_method.execute([input_tensor, encoder_hidden, pos_tensor])
233
- flat_logits = decoder_outputs[0]
234
- if isinstance(flat_logits, (list, tuple)):
235
- flat_logits = flat_logits[0]
236
- flat_logits = torch.as_tensor(flat_logits).float().flatten()
237
-
238
- if forced_prefix_idx < len(forced_prefix):
239
- # Force the language / task / no-timestamps prefix before free decoding.
240
- next_token = forced_prefix[forced_prefix_idx]
241
- forced_prefix_idx += 1
242
  else:
243
- scores = flat_logits.unsqueeze(0)
244
- first_free = generated_token_count == 0
245
- scores = apply_suppression(scores, first_free_step=first_free)
246
- next_token = int(scores[0].argmax().item())
247
- generated_token_count += 1
248
- generated_free_tokens.append(next_token)
249
-
250
- if next_token == EOS_TOKEN_ID:
251
- stop_reason = "eos"
252
- tokens.append(next_token)
253
- cache_position += 1
254
- break
255
- repeated_suffix_len = find_repeated_suffix_pattern(generated_free_tokens)
256
- if repeated_suffix_len is not None:
257
- trim_count = repeated_suffix_len * REPETITION_GUARD_REPEATS
258
- del generated_free_tokens[-trim_count:]
259
- del tokens[-(trim_count - 1) :]
260
- generated_token_count = max(0, generated_token_count - trim_count)
261
- stop_reason = "repetition_guard"
262
- break
263
- if time.perf_counter() - started >= MAX_SECONDS_PER_SAMPLE:
264
- stop_reason = "timeout"
265
- break
266
-
267
- tokens.append(next_token)
268
- cache_position += 1
269
-
270
- elapsed = time.perf_counter() - started
271
- text = decode_tokens(tokenizer, tokens)
272
- return {
273
- "transcription": text,
274
- "generated_tokens": generated_token_count,
275
- "stop_reason": stop_reason,
276
- "elapsed_s": round(elapsed, 3),
277
- }
278
 
279
 
280
- def transcribe_forward(forward_method, features: torch.Tensor, tokenizer) -> dict:
281
- """Fallback path for a monolithic 'forward' export (re-runs the full graph per step)."""
282
- prompt = [DECODER_START_TOKEN_ID] + list(FORCED_PREFIX_IDS)
283
- decoder_ids = torch.tensor([prompt], dtype=torch.long).contiguous()
284
- generated_token_count = 0
285
- stop_reason = "max_tokens"
286
- started = time.perf_counter()
287
- generated_free_tokens: list[int] = []
288
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  with torch.no_grad():
290
- for _step in range(MAX_GENERATION_TOKENS):
291
- outputs = forward_method.execute([features, decoder_ids])
292
- logits = outputs[0]
293
- if isinstance(logits, (list, tuple)):
294
- logits = logits[0]
295
- logits = torch.as_tensor(logits).float()
296
- next_token_scores = logits[:, -1, :]
297
- first_free = generated_token_count == 0
298
- next_token_scores = apply_suppression(next_token_scores, first_free_step=first_free)
299
- next_token = next_token_scores.argmax(dim=-1, keepdim=True).long()
300
- decoder_ids = torch.cat([decoder_ids, next_token], dim=1)
301
- generated_token_count += 1
302
- generated_free_tokens.append(int(next_token.item()))
303
 
304
- if EOS_TOKEN_ID >= 0 and bool(torch.all(next_token == EOS_TOKEN_ID)):
305
- stop_reason = "eos"
306
- break
307
- repeated_suffix_len = find_repeated_suffix_pattern(generated_free_tokens)
308
- if repeated_suffix_len is not None:
309
- trim_count = repeated_suffix_len * REPETITION_GUARD_REPEATS
310
- generated_free_tokens = generated_free_tokens[:-trim_count]
311
- decoder_ids = decoder_ids[:, :-trim_count]
312
- generated_token_count = max(0, generated_token_count - trim_count)
313
- stop_reason = "repetition_guard"
314
- break
315
- if time.perf_counter() - started >= MAX_SECONDS_PER_SAMPLE:
316
- stop_reason = "timeout"
317
- break
318
 
319
- elapsed = time.perf_counter() - started
320
- text = decode_tokens(tokenizer, decoder_ids[0].tolist())
321
- return {
 
 
 
322
  "transcription": text,
323
- "generated_tokens": generated_token_count,
324
- "stop_reason": stop_reason,
325
- "elapsed_s": round(elapsed, 3),
326
  }
 
 
 
 
327
 
328
 
329
- def save_results(result: dict, script_dir: Path) -> None:
330
- output_path = script_dir / "transcription.json"
331
- with open(output_path, "w", encoding="utf-8") as f:
332
- json.dump(result, f, indent=2, ensure_ascii=False)
333
- print(f"Saved transcription to {output_path}")
334
-
335
-
336
  def main() -> None:
337
- args = parse_args()
338
- script_dir = Path(__file__).parent
339
- model_path = script_dir / MODEL_FILENAMES[args.variant]
340
- audio_arg = Path(args.audio)
341
- audio_path = audio_arg if audio_arg.is_absolute() else (script_dir / audio_arg).resolve()
342
- preprocessor_path = script_dir / PREPROCESSOR_FILENAMES[args.variant]
343
-
344
- print(f"Loading tokenizer from {TOKENIZER_REPO} ...")
345
- tokenizer = AutoTokenizer.from_pretrained(
346
- TOKENIZER_REPO,
347
- use_fast=True,
348
- )
349
-
350
- _preprocessor_program = None
351
- preprocessor_method = None
352
- if preprocessor_path.exists():
353
- print(f"Loading preprocessor from {preprocessor_path} ...")
354
- _preprocessor_program, preprocessor_method = load_preprocessor(preprocessor_path)
355
- else:
356
- print("Local preprocessor .pte not found; falling back to WhisperAudioProcessor.")
357
-
358
- print(f"Loading model from {model_path} ...")
359
- program, methods = load_model(str(model_path))
360
-
361
- print(f"Preprocessing audio: {audio_path}")
362
- features = preprocess(str(audio_path), preprocessor_method)
363
- print(f" Input features shape: {tuple(features.shape)}")
364
-
365
- print("Running transcription ...")
366
- if methods["format"] == "seq2seq":
367
- result = transcribe_seq2seq(
368
- program, methods["encoder"], methods["decoder"], features, tokenizer
369
- )
370
- else:
371
- result = transcribe_forward(methods["forward"], features, tokenizer)
372
 
373
- print(f"\nTranscription: {result['transcription']!r}")
374
- print(f"Generated tokens: {result['generated_tokens']}")
375
- print(f"Stop reason: {result['stop_reason']}")
376
- print(f"Elapsed: {result['elapsed_s']:.3f} s")
377
 
378
- save_results(result, script_dir)
 
379
 
380
 
381
  if __name__ == "__main__":
 
1
+ """Whisper Small ASR inference example using HuggingFace Transformers.
2
 
3
+ The optimized.pte and original.pte artifacts in the sibling pte_optimized/ and
4
+ pte_original/ directories are ExecuTorch-optimized models (8da8w INT8 dynamic
5
+ quantization) intended for on-device ARM inference (Android / Graviton).
6
+ Running those artifacts directly requires the ExecuTorch C++ runtime and a
7
+ specialized seq2seq runner they are not suitable for a simple Python
8
+ ``method.execute()`` call.
 
9
 
10
+ This script demonstrates equivalent inference using the HuggingFace Transformers
11
+ library, which is the recommended path for Python-based evaluation and prototyping.
12
 
13
  Requirements:
14
+ pip install transformers torch soundfile numpy
15
  """
16
 
 
17
  import json
 
18
  from pathlib import Path
19
 
 
20
  import torch
21
+ import torch.nn.functional as F
22
+ from transformers import WhisperForConditionalGeneration, WhisperProcessor
23
 
24
+ # ── Configuration ──────────────────────────────────────────────────────────────
25
  AUDIO_PATH = "sample_input.flac"
26
+ MODEL_NAME = "openai/whisper-small"
27
+ LANGUAGE = "en"
28
+ TASK = "transcribe"
29
+ MAX_NEW_TOKENS = 128
30
+ SAMPLE_RATE = 16000
31
+
32
+
33
+ # ── Audio Loading ─��────────────────────────────────────────────────────────────
34
+ def load_audio(audio_path: str) -> tuple[torch.Tensor, int]:
35
+ """Load audio file and return (waveform_1d_float32, sample_rate)."""
36
+ try:
37
+ import soundfile as sf
38
+
39
+ audio_np, sr = sf.read(audio_path, dtype="float32")
40
+ if audio_np.ndim == 2:
41
+ audio_np = audio_np.mean(axis=1)
42
+ import numpy as np
43
+
44
+ return torch.from_numpy(audio_np.astype(np.float32)), int(sr)
45
+ except ImportError:
46
+ import torchaudio
47
+
48
+ waveform, sr = torchaudio.load(audio_path)
49
+ if waveform.shape[0] > 1:
50
+ waveform = waveform.mean(dim=0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  else:
52
+ waveform = waveform.squeeze(0)
53
+ return waveform.float(), int(sr)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
 
56
+ # ── Resampling ─────────────────────────────────────────────────────────────────
57
+ def resample_to_16k(waveform: torch.Tensor, sample_rate: int) -> torch.Tensor:
58
+ """Resample waveform to 16 kHz using linear interpolation."""
59
+ if sample_rate == SAMPLE_RATE:
60
+ return waveform
61
+ new_len = int(round(len(waveform) * SAMPLE_RATE / sample_rate))
62
+ return F.interpolate(
63
+ waveform.view(1, 1, -1), size=new_len, mode="linear", align_corners=False
64
+ ).view(-1)
65
+
66
+
67
+ # ── Preprocessing ──────────────────────────────────────────────────────────────
68
+ def preprocess(audio_path: str, processor: WhisperProcessor) -> torch.Tensor:
69
+ """Load audio and extract 80-bin log-mel spectrogram features [1, 80, 3000]."""
70
+ waveform, sr = load_audio(audio_path)
71
+ waveform_16k = resample_to_16k(waveform, sr)
72
+ features = processor.feature_extractor(
73
+ waveform_16k.numpy(), sampling_rate=SAMPLE_RATE, return_tensors="pt"
74
+ ).input_features
75
+ return features # [1, 80, 3000]
76
+
77
+
78
+ # ── Inference ──────────────────────────────────────────────────────────────────
79
+ def transcribe(
80
+ audio_path: str,
81
+ model: WhisperForConditionalGeneration,
82
+ processor: WhisperProcessor,
83
+ ) -> str:
84
+ """Run Whisper inference and return transcribed text."""
85
+ features = preprocess(audio_path, processor)
86
  with torch.no_grad():
87
+ output_ids = model.generate(
88
+ features,
89
+ language=LANGUAGE,
90
+ task=TASK,
91
+ max_new_tokens=MAX_NEW_TOKENS,
92
+ )
93
+ return processor.tokenizer.batch_decode(output_ids, skip_special_tokens=True)[0].strip()
 
 
 
 
 
 
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
+ # ── Save Results ───────────────────────────────────────────────────────────────
97
+ def save_results(audio_path: str, text: str) -> None:
98
+ """Save transcription result to JSON in the same directory as this script."""
99
+ out_dir = Path(__file__).parent
100
+ result = {
101
+ "audio_file": str(Path(audio_path).name),
102
  "transcription": text,
103
+ "model": MODEL_NAME,
104
+ "language": LANGUAGE,
105
+ "task": TASK,
106
  }
107
+ out_path = out_dir / "transcription.json"
108
+ with open(out_path, "w") as f:
109
+ json.dump(result, f, indent=2)
110
+ print(f"Transcription saved to: {out_path}")
111
 
112
 
113
+ # ── Main ───────────────────────────────────────────────────────────────────────
 
 
 
 
 
 
114
  def main() -> None:
115
+ print(f"Loading model: {MODEL_NAME}")
116
+ processor = WhisperProcessor.from_pretrained(MODEL_NAME)
117
+ model = WhisperForConditionalGeneration.from_pretrained(MODEL_NAME)
118
+ model.eval()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
+ audio_path = str(Path(__file__).parent / AUDIO_PATH)
121
+ print(f"Transcribing: {audio_path}")
122
+ text = transcribe(audio_path, model, processor)
 
123
 
124
+ print(f"\nTranscription: {text}")
125
+ save_results(audio_path, text)
126
 
127
 
128
  if __name__ == "__main__":
metadata.yaml CHANGED
@@ -1,16 +1,59 @@
1
- model_name: openai/whisper-small
2
- model_type: asr
3
- organization: OpenAI
4
- license: MIT
5
- source_url: https://github.com/openai/whisper
6
- precision: INT8 Dynamic + W8 Embedding
7
- optimization_type: 8da8w + embed_tokens weight-only int8
8
- backend: executorch
9
- dataset: LibriSpeech
10
- evaluation_split: test-clean
11
- evaluation_samples: 2625
12
- artifacts:
13
- optimized_model: whisper-small-int8-executorch.pte
14
- optimized_preprocessor: whisper-small-preprocessor-int8-executorch.pte
15
- original_model: whisper-small-fp32-executorch.pte
16
- original_preprocessor: whisper-small-preprocessor-fp32-executorch.pte
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: 1.0.0
2
+ task: speech-asr
3
+ created_at: '2026-06-25T13:41:20Z'
4
+ context:
5
+ model:
6
+ id: Arm/whisper-small-int8-xnnpack-executorch
7
+ base_model_id: openai/whisper-small
8
+ profile: Arm-Optimized
9
+ weight_dtype: int8
10
+ quantization:
11
+ method: PTQ-dynamic
12
+ weight_bits: 8
13
+ activation_bits: 8
14
+ symmetric: true
15
+ mode: dynamic
16
+ weight_granularity: per-channel
17
+ model_size_mb: 395.046
18
+ parameter_count: 241734912
19
+ filename: whisper-small-int8-executorch.pte
20
+ format: pte
21
+ target:
22
+ name: Vivo X300
23
+ hardware_class: Premium smartphone
24
+ cpu_architecture: aarch64
25
+ cpu_model: C1-Ultra, C1-Premium, C1-Pro
26
+ cpu_core_count: 8
27
+ system_memory_gb: 16
28
+ os: android
29
+ os_version: Android 16 / OriginOS 6
30
+ runtime:
31
+ name: executorch
32
+ execution_backend: cpu
33
+ config:
34
+ optimisations:
35
+ - XNNPACK
36
+ - KleidiAI
37
+ version: 1.1.0
38
+ dataset:
39
+ name: librispeech_asr
40
+ sample_count: 2625
41
+ benchmark:
42
+ batch_size: 1
43
+ audio_length_s: 7.0
44
+ num_runs: 50
45
+ warmup_runs: 10
46
+ mode: offline
47
+ sample_rate_hz: 16000
48
+ performance:
49
+ end_to_end_latency_ms:
50
+ p50: 7802.5
51
+ p90: 8027.0
52
+ p99: 8027.0
53
+ peak_memory_mb: 4662.29
54
+ rtfx: 0.897
55
+ model_load_time_ms: 1015.0
56
+ accuracy:
57
+ normalised_wer: 0.03407
58
+ normalisation: Whisper-style
59
+ cer: 0.01288
sample_input.flac CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:7eb558a5f4a8f347b6b48a7e50c159fdba6c5d1ff873c4fdefa7437dd98ab484
3
- size 108864
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e01d537af7b04625d66242aa153005230c5790dc92d5ad80a8ee510684409fb4
3
+ size 228675