File size: 6,011 Bytes
ca25232
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
---
license: apache-2.0
tags:
- executorch
- xnnpack
- pte
- on-device
- automatic-speech-recognition
base_model:
- jonatasgrosman/wav2vec2-large-xlsr-53-japanese
---
# wav2vec2 XLSR-53 Japanese — ExecuTorch (CTC speech recognition)

Ten seconds of Japanese audio in, a transcript out, in **one forward pass**. No
decoder loop, no beam, no KV cache, nothing to carry between calls — the encoder
is the model and the transcript falls out of an argmax per 20 ms frame.

That is the difference from the Whisper conversions on this shelf, which are
encoder-decoder: one encoder pass per window plus one decoder pass per generated
token. A CTC model trades that machinery — and the ability to punctuate — for a
much simpler thing to run on a device.

```
(waveform (1,160000) fp32 16 kHz mono, attention_mask (1,160000) int64)
    -> logits (1, 499, 2341)
```

| build | file | MB | corr vs fp32 eager | transcripts identical | Mac ms* |
|---|---|---|---|---|---|
| fp32 | `wav2vec2_xlsr53_ja_xnnpack_fp32.pte` | 1271.7 | 1.000000 | **5/5** | 267.8 |
| int8 (dynamic) | `wav2vec2_xlsr53_ja_xnnpack_int8.pte` | **357.9** | 0.997838 | **5/5** | 228.9 |

\*Mac arm64, single process, median of 5, for a 10-second window — a reference
point for relative cost, not a device number. Torch eager fp32 on the same
machine: 215.1 ms. XNNPACK delegate coverage 64.5% (869/1347 ops) for fp32,
75.2% for int8.

**Take the int8 build.** It is 28% of the file, it is faster, and it returns the
same five transcripts character for character, at worst character error rate
0.000. Its correlation is 0.9978 rather than 1.000000, and on this task that
difference did not reach the output — but five sentences of synthetic speech is
what stands behind that, not a benchmark.

**There is no Core ML build.** coremltools rejects the graph on a `stack` whose
operands are a mix of int and float. This one is XNNPACK only, and the same file
runs on Android and on the Mac.

## Running it

**1. The audio.** 16 kHz mono in [-1, 1], exactly 160 000 samples. Zero-pad a
shorter clip; cut a longer one. **Do not normalise it** — the per-utterance
normalisation wav2vec2 expects is inside the graph, and it needs the mask to be
correct (below).

**2. The mask.** `attention_mask` is int64 `(1, 160000)`, 1 on the real samples
and 0 on the padding. It is not optional and it is not cosmetic: padding a
3.7-second clip to 10 seconds without it changed the transcript on **4 of 5** test
sentences.

```
with the mask:     今日はよい天気ですね
without:           今わよい電気で住
```

The zeros drag the mean and shrink the variance the normalisation divides by, so
the speech comes out at the wrong scale, and the encoder attends to the silence.
With the mask — masked mean and variance, and the same mask handed to the encoder
— all five come back identical to the transcript of the unpadded clip.

**3. The decode.** CTC greedy, over `logits[0]`:

```
ids = argmax per frame
collapse runs of the same id
drop the blank (id 0)
map through the repo's vocab.json, turn the word delimiter | into a space, strip
```

That last clause matters. Without the strip this recipe returns
`'今日はよい天気ですね '` where the model's own tokenizer returns
`'今日はよい天気ですね'` — a `|` lands at the end of the utterance. The checker
in the conversion repo runs the hand-written recipe and asserts it against
`processor.batch_decode` on the same ids, which is how that was caught.

There is no punctuation and no casing in the output: the vocabulary has 2341
entries and none of them are `。` or `、`. The model also writes numbers as
digits (`コーヒーを1杯`).

## Verification

Five sentences spoken by macOS `say -v Kyoko`, transcribed through the `.pte` and
through the eager model on the identical padded window and mask:

```
今日はよい天気ですね
駅までの道を教えてください
この電車は東京駅に泊まりますか
コーヒーを1杯お願いします
厚の会議は十時からです
```

**5/5 identical, worst character error rate 0.000.** Synthetic speech is a
control, not a benchmark — it says the conversion transcribes what the fp32 model
transcribes, and nothing about word error rate on real speakers. (The fp32 model
itself mishears 明日 as 厚 on the last one; both builds mishear it the same way,
which is the point.)

```bash
python convert/check_wav2vec2.py fp32     # or int8
```

## The conversion note worth reading

The first build of this model was correct and **13x slower than it should have
been**: 3485 ms against eager's 215 ms, at correlation 1.000000. The cost was one
module.

wav2vec2 puts a positional convolution in front of its encoder,
`Conv1d(1024, 1024, kernel_size=128, groups=16)`, and it carries `weight_norm` —
a live `torch.nn.utils.parametrize` parametrization. Under `torch.export` that
makes the weight a *computed* tensor: the graph gets a `pow` and a `sum` where a
constant should be. XNNPACK's partitioner requires a weight that is a static
parameter, so it declines the convolution, and the convolution runs on the
portable kernels.

Measured on that module alone at sequence 499:

```
as loaded                          delegated  70.0%   3206.7 ms
after remove_parametrizations      delegated 100.0%      3.6 ms
```

with the output identical to the last bit. Materialising the weight before export
is what brought the whole model to 267.8 ms. It is reported upstream as
[pytorch/executorch#22078](https://github.com/pytorch/executorch/issues/22078),
because ExecuTorch already ships the pass that would fix it without touching the
model — it just does not run it during lowering.

- **Source**: [jonatasgrosman/wav2vec2-large-xlsr-53-japanese](https://huggingface.co/jonatasgrosman/wav2vec2-large-xlsr-53-japanese)
- **License**: Apache-2.0

torch.export -> to_edge_transform_and_lower(partitioner) -> .pte
(conversion scripts: [executorch-models](https://github.com/john-rocky/executorch-models))