inoryQwQ commited on
Commit
3c50954
·
1 Parent(s): 007f8dd

First commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. LICENSE +21 -0
  3. README.md +151 -3
  4. ax_common.py +625 -0
  5. config_decoder.json +88 -0
  6. config_encoder_offline.json +48 -0
  7. config_encoder_online.json +88 -0
  8. demo.wav +3 -0
  9. export_onnx.py +1723 -0
  10. generate_data.py +122 -0
  11. ort_common.py +567 -0
  12. requirements_ax.txt +3 -0
  13. requirements_x86.txt +12 -0
  14. run_ax.py +65 -0
  15. run_ort.py +55 -0
  16. wenet/LLM/__init__.py +0 -0
  17. wenet/LLM/causallm_model.py +207 -0
  18. wenet/LLM/decoder.py +161 -0
  19. wenet/LLM/sampler.py +43 -0
  20. wenet/README.md +29 -0
  21. wenet/__init__.py +1 -0
  22. wenet/bin/__init__.py +0 -0
  23. wenet/bin/alignment.py +268 -0
  24. wenet/bin/average_model.py +125 -0
  25. wenet/bin/export_ipex.py +95 -0
  26. wenet/bin/export_jit.py +71 -0
  27. wenet/bin/export_onnx_bpu.py +1065 -0
  28. wenet/bin/export_onnx_cpu.py +470 -0
  29. wenet/bin/export_onnx_gpu.py +1266 -0
  30. wenet/bin/recognize.py +317 -0
  31. wenet/bin/recognize_onnx_gpu.py +297 -0
  32. wenet/bin/train.py +190 -0
  33. wenet/branchformer/__init__.py +0 -0
  34. wenet/branchformer/cgmlp.py +194 -0
  35. wenet/branchformer/encoder.py +177 -0
  36. wenet/branchformer/encoder_layer.py +245 -0
  37. wenet/cli/__init__.py +0 -0
  38. wenet/cli/hub.py +118 -0
  39. wenet/cli/model.py +209 -0
  40. wenet/cli/paraformer_model.py +110 -0
  41. wenet/cli/punc_model.py +115 -0
  42. wenet/cli/transcribe.py +102 -0
  43. wenet/ctl_model/__init__.py +0 -0
  44. wenet/ctl_model/asr_model_ctl.py +277 -0
  45. wenet/ctl_model/encoder.py +172 -0
  46. wenet/dataset/__init__.py +0 -0
  47. wenet/dataset/datapipes.py +470 -0
  48. wenet/dataset/dataset.py +155 -0
  49. wenet/dataset/deprecated/__init__.py +0 -0
  50. wenet/dataset/deprecated/dataset.py +202 -0
.gitattributes CHANGED
@@ -29,6 +29,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
29
  *.tflite filter=lfs diff=lfs merge=lfs -text
30
  *.tgz filter=lfs diff=lfs merge=lfs -text
31
  *.wasm filter=lfs diff=lfs merge=lfs -text
 
32
  *.xz filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
 
29
  *.tflite filter=lfs diff=lfs merge=lfs -text
30
  *.tgz filter=lfs diff=lfs merge=lfs -text
31
  *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.wav filter=lfs diff=lfs merge=lfs -text
33
  *.xz filter=lfs diff=lfs merge=lfs -text
34
  *.zip filter=lfs diff=lfs merge=lfs -text
35
  *.zst filter=lfs diff=lfs merge=lfs -text
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 祈Inory
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,3 +1,151 @@
1
- ---
2
- license: mit
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # wenet.axera
2
+
3
+ WeNet on Axera.
4
+
5
+ 预转换好的模型在[Release](https://github.com/ml-inory/wenet.axera/releases/download/v1.0/axmodel.tar.gz) 基于AIShell数据集的100条语音量化
6
+
7
+ ## 安装依赖
8
+
9
+ x86:
10
+
11
+ ```bash
12
+ pip install -r requirements_x86.txt
13
+ ```
14
+
15
+ 开发板:
16
+
17
+ ```bash
18
+ pip install -r requirements_ax.txt
19
+ ```
20
+
21
+ ## 导出 ONNX
22
+
23
+ ```bash
24
+ python export_onnx.py \
25
+ --pretrained_model_dir pretrained/aishell_u2pp_conformer_exp \
26
+ --output_onnx_dir onnx_model
27
+ ```
28
+
29
+ 如果不指定参数,默认会下载并使用 `pretrained/aishell_u2pp_conformer_exp`。
30
+
31
+ 输出:
32
+
33
+ ```text
34
+ onnx_model/encoder_offline.onnx
35
+ onnx_model/encoder_online.onnx
36
+ onnx_model/decoder.onnx
37
+ ```
38
+
39
+ ## 生成量化数据集
40
+
41
+ ```bash
42
+ wget https://github.com/ml-inory/whisper.axera/releases/download/v1.0/datasets.zip
43
+ unzip datasets.zip -d datasets
44
+ ```
45
+
46
+ ```bash
47
+ python generate_data.py \
48
+ -i datasets \
49
+ --config pretrained/aishell_u2pp_conformer_exp/train.yaml \
50
+ --vocab pretrained/aishell_u2pp_conformer_exp/units.txt \
51
+ --onnx_dir onnx_model \
52
+ --calib_data_path calibration_dataset \
53
+ --max_num 100
54
+ ```
55
+
56
+ ## 转 axmodel
57
+
58
+ 转换:
59
+
60
+ ```bash
61
+ pulsar2 build \
62
+ --input onnx_model/encoder_offline.onnx \
63
+ --config config_encoder_offline.json \
64
+ --output_dir axmodel/encoder_offline \
65
+ --output_name encoder_offline.axmodel \
66
+ --target_hardware AX650 \
67
+ --npu_mode NPU3
68
+
69
+ pulsar2 build \
70
+ --input onnx_model/encoder_online.onnx \
71
+ --config config_encoder_online.json \
72
+ --output_dir axmodel/encoder_online \
73
+ --output_name encoder_online.axmodel \
74
+ --target_hardware AX650 \
75
+ --npu_mode NPU3
76
+
77
+ pulsar2 build \
78
+ --input onnx_model/decoder.onnx \
79
+ --config config_decoder.json \
80
+ --output_dir axmodel/decoder \
81
+ --output_name decoder.axmodel \
82
+ --target_hardware AX650 \
83
+ --npu_mode NPU3
84
+ ```
85
+
86
+ ## 跑 ONNX
87
+
88
+ Offline CTC:
89
+
90
+ ```bash
91
+ python run_ort.py \
92
+ -i demo.wav \
93
+ --config pretrained/aishell_u2pp_conformer_exp/train.yaml \
94
+ --vocab pretrained/aishell_u2pp_conformer_exp/units.txt \
95
+ --onnx_dir onnx_model \
96
+ --mode ctc_prefix_beam_search \
97
+ --calib_data_path ""
98
+ ```
99
+
100
+ Online CTC:
101
+
102
+ ```bash
103
+ python run_ort.py \
104
+ -i demo.wav \
105
+ --online \
106
+ --config pretrained/aishell_u2pp_conformer_exp/train.yaml \
107
+ --vocab pretrained/aishell_u2pp_conformer_exp/units.txt \
108
+ --onnx_dir onnx_model \
109
+ --mode ctc_prefix_beam_search \
110
+ --calib_data_path ""
111
+ ```
112
+
113
+ Attention rescoring:
114
+
115
+ ```bash
116
+ python run_ort.py \
117
+ -i demo.wav \
118
+ --config pretrained/aishell_u2pp_conformer_exp/train.yaml \
119
+ --vocab pretrained/aishell_u2pp_conformer_exp/units.txt \
120
+ --onnx_dir onnx_model \
121
+ --mode attention_rescoring \
122
+ --calib_data_path ""
123
+ ```
124
+
125
+ ## 跑 axmodel
126
+
127
+ 在开发板仓库目录运行。
128
+
129
+ Offline CTC:
130
+
131
+ ```bash
132
+ python3 run_ax.py -i demo.wav --mode ctc_prefix_beam_search
133
+ ```
134
+
135
+ Online CTC:
136
+
137
+ ```bash
138
+ python3 run_ax.py -i demo.wav --online --mode ctc_prefix_beam_search
139
+ ```
140
+
141
+ Offline attention rescoring:
142
+
143
+ ```bash
144
+ python3 run_ax.py -i demo.wav --mode attention_rescoring
145
+ ```
146
+
147
+ Online attention rescoring:
148
+
149
+ ```bash
150
+ python3 run_ax.py -i demo.wav --online --mode attention_rescoring
151
+ ```
ax_common.py ADDED
@@ -0,0 +1,625 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import multiprocessing
2
+ import wave
3
+
4
+ import numpy as np
5
+ import yaml
6
+
7
+
8
+ def _log_add(*values):
9
+ values = [value for value in values if value != -float("inf")]
10
+ if not values:
11
+ return -float("inf")
12
+ max_value = max(values)
13
+ return max_value + np.log(sum(np.exp(value - max_value)
14
+ for value in values))
15
+
16
+
17
+ def _map_sentence(sent, vocabulary, greedy=False, blank_id=0):
18
+ mapped = []
19
+ prev = None
20
+ for token in sent:
21
+ token = int(token)
22
+ if greedy and token == prev:
23
+ prev = token
24
+ continue
25
+ prev = token
26
+ if token == blank_id or token < 0 or token >= len(vocabulary):
27
+ continue
28
+ piece = vocabulary[token]
29
+ if piece.startswith("<") and piece.endswith(">"):
30
+ continue
31
+ mapped.append(piece)
32
+ return "".join(mapped)
33
+
34
+
35
+ def map_batch(batch_sents, vocabulary, num_processes, greedy=False,
36
+ blank_id=0):
37
+ del num_processes
38
+ return [_map_sentence(sent, vocabulary, greedy, blank_id)
39
+ for sent in batch_sents]
40
+
41
+
42
+ def _ctc_prefix_beam_search(log_probs_seq, log_probs_idx, beam_size,
43
+ blank_id):
44
+ beam = {(): (0.0, -float("inf"))}
45
+ for frame_probs, frame_ids in zip(log_probs_seq, log_probs_idx):
46
+ frame_probs = np.asarray(frame_probs, dtype=np.float32)
47
+ frame_probs = frame_probs - _log_add(*frame_probs.tolist())
48
+ next_beam = {}
49
+ for prefix, (prob_blank, prob_non_blank) in beam.items():
50
+ for prob, token in zip(frame_probs, frame_ids):
51
+ token = int(token)
52
+ prob = float(prob)
53
+ next_prob_blank, next_prob_non_blank = next_beam.get(
54
+ prefix, (-float("inf"), -float("inf")))
55
+ if token == blank_id:
56
+ next_beam[prefix] = (
57
+ _log_add(next_prob_blank, prob_blank + prob,
58
+ prob_non_blank + prob),
59
+ next_prob_non_blank,
60
+ )
61
+ continue
62
+
63
+ last = prefix[-1] if prefix else None
64
+ if token == last:
65
+ next_beam[prefix] = (
66
+ next_prob_blank,
67
+ _log_add(next_prob_non_blank, prob_non_blank + prob),
68
+ )
69
+ new_prefix = prefix + (token, )
70
+ nb_blank, nb_non_blank = next_beam.get(
71
+ new_prefix, (-float("inf"), -float("inf")))
72
+ next_beam[new_prefix] = (
73
+ nb_blank,
74
+ _log_add(nb_non_blank, prob_blank + prob),
75
+ )
76
+ else:
77
+ new_prefix = prefix + (token, )
78
+ nb_blank, nb_non_blank = next_beam.get(
79
+ new_prefix, (-float("inf"), -float("inf")))
80
+ next_beam[new_prefix] = (
81
+ nb_blank,
82
+ _log_add(nb_non_blank, prob_blank + prob,
83
+ prob_non_blank + prob),
84
+ )
85
+ beam = dict(sorted(
86
+ next_beam.items(),
87
+ key=lambda item: _log_add(item[1][0], item[1][1]),
88
+ reverse=True)[:beam_size])
89
+ return [(_log_add(prob_blank, prob_non_blank), prefix)
90
+ for prefix, (prob_blank, prob_non_blank) in sorted(
91
+ beam.items(),
92
+ key=lambda item: _log_add(item[1][0], item[1][1]),
93
+ reverse=True)]
94
+
95
+
96
+ def ctc_beam_search_decoder_batch(batch_log_probs_seq,
97
+ batch_log_probs_idx,
98
+ batch_root_trie,
99
+ batch_start,
100
+ beam_size,
101
+ num_processes,
102
+ blank_id=0,
103
+ space_id=-1,
104
+ cutoff_prob=0.999,
105
+ ext_scorer=None):
106
+ del batch_root_trie, batch_start, num_processes, space_id
107
+ del cutoff_prob, ext_scorer
108
+ return [
109
+ _ctc_prefix_beam_search(log_probs_seq, log_probs_idx, beam_size,
110
+ blank_id)
111
+ for log_probs_seq, log_probs_idx in zip(batch_log_probs_seq,
112
+ batch_log_probs_idx)
113
+ ]
114
+
115
+
116
+ def load_config(config_path):
117
+ with open(config_path, "r") as fin:
118
+ return yaml.load(fin, Loader=yaml.FullLoader)
119
+
120
+
121
+ def load_vocab(vocab_path):
122
+ vocabulary = []
123
+ char_dict = {}
124
+ with open(vocab_path, "r") as fin:
125
+ for line in fin:
126
+ arr = line.strip().split()
127
+ assert len(arr) == 2
128
+ char_dict[int(arr[1])] = arr[0]
129
+ vocabulary.append(arr[0])
130
+ return vocabulary, char_dict
131
+
132
+
133
+ def load_wav(audio_file):
134
+ with wave.open(audio_file, "rb") as wav_file:
135
+ sample_rate = wav_file.getframerate()
136
+ num_channels = wav_file.getnchannels()
137
+ sample_width = wav_file.getsampwidth()
138
+ frames = wav_file.readframes(wav_file.getnframes())
139
+
140
+ if sample_width == 1:
141
+ waveform = np.frombuffer(frames, dtype=np.uint8).astype(np.float32)
142
+ waveform -= 128.0
143
+ elif sample_width == 2:
144
+ waveform = np.frombuffer(frames, dtype="<i2").astype(np.float32)
145
+ elif sample_width == 4:
146
+ waveform = np.frombuffer(frames, dtype="<i4").astype(np.float32)
147
+ else:
148
+ raise ValueError(f"Unsupported wav sample width: {sample_width}")
149
+
150
+ if num_channels > 1:
151
+ waveform = waveform.reshape(-1, num_channels).mean(axis=1)
152
+ return waveform, sample_rate
153
+
154
+
155
+ def resample_linear(waveform, orig_sr, target_sr):
156
+ if orig_sr == target_sr:
157
+ return waveform
158
+ duration = waveform.shape[0] / float(orig_sr)
159
+ target_len = int(round(duration * target_sr))
160
+ if target_len <= 1:
161
+ return waveform
162
+ src_pos = np.linspace(0, waveform.shape[0] - 1, target_len)
163
+ return np.interp(src_pos, np.arange(waveform.shape[0]),
164
+ waveform).astype(np.float32)
165
+
166
+
167
+ def hz_to_mel(freq):
168
+ return 1127.0 * np.log1p(freq / 700.0)
169
+
170
+
171
+ def mel_to_hz(mel):
172
+ return 700.0 * np.expm1(mel / 1127.0)
173
+
174
+
175
+ def mel_filterbank(num_mel_bins, n_fft, sample_rate):
176
+ low_mel = hz_to_mel(20.0)
177
+ high_mel = hz_to_mel(sample_rate / 2.0)
178
+ mel_points = np.linspace(low_mel, high_mel, num_mel_bins + 2)
179
+ hz_points = mel_to_hz(mel_points)
180
+ bins = np.floor((n_fft + 1) * hz_points / sample_rate).astype(np.int32)
181
+ fbanks = np.zeros((num_mel_bins, n_fft // 2 + 1), dtype=np.float32)
182
+ for i in range(num_mel_bins):
183
+ left, center, right = bins[i], bins[i + 1], bins[i + 2]
184
+ if center > left:
185
+ fbanks[i, left:center] = (
186
+ np.arange(left, center) - left) / float(center - left)
187
+ if right > center:
188
+ fbanks[i, center:right] = (
189
+ right - np.arange(center, right)) / float(right - center)
190
+ return fbanks
191
+
192
+
193
+ def numpy_fbank(waveform,
194
+ sample_rate=16000,
195
+ num_mel_bins=80,
196
+ frame_length=25,
197
+ frame_shift=10):
198
+ frame_size = int(round(sample_rate * frame_length / 1000.0))
199
+ frame_step = int(round(sample_rate * frame_shift / 1000.0))
200
+ if waveform.shape[0] < frame_size:
201
+ waveform = np.pad(waveform, (0, frame_size - waveform.shape[0]))
202
+ num_frames = 1 + (waveform.shape[0] - frame_size) // frame_step
203
+ frames = np.lib.stride_tricks.as_strided(
204
+ waveform,
205
+ shape=(num_frames, frame_size),
206
+ strides=(waveform.strides[0] * frame_step, waveform.strides[0]),
207
+ ).copy()
208
+ frames *= np.hamming(frame_size).astype(np.float32)
209
+ n_fft = 1
210
+ while n_fft < frame_size:
211
+ n_fft <<= 1
212
+ power = np.abs(np.fft.rfft(frames, n=n_fft))**2
213
+ fbanks = mel_filterbank(num_mel_bins, n_fft, sample_rate)
214
+ mel_energies = np.maximum(np.dot(power, fbanks.T), np.finfo(np.float32).eps)
215
+ return np.log(mel_energies).astype(np.float32)
216
+
217
+
218
+ def compute_feats(audio_file, sr=16000):
219
+ waveform, sample_rate = load_wav(audio_file)
220
+ waveform = resample_linear(waveform.astype(np.float32), sample_rate, sr)
221
+ return numpy_fbank(waveform, sample_rate=sr).reshape(1, -1, 80)
222
+
223
+
224
+ def pad_array_along_axis(array, pad_width, axis, mode="constant", **kwargs):
225
+ if array.shape[axis] >= pad_width:
226
+ return array
227
+ full_pad_width = [(0, 0)] * array.ndim
228
+ full_pad_width[axis] = (0, pad_width - array.shape[axis])
229
+ return np.pad(array, pad_width=full_pad_width, mode=mode, **kwargs)
230
+
231
+
232
+ def numpy_topk(array, k, axis=-1, largest=True):
233
+ if largest:
234
+ partitioned_indices = np.argpartition(array, -k, axis=axis)
235
+ topk_indices = np.take(partitioned_indices, range(-k, 0), axis=axis)
236
+ else:
237
+ partitioned_indices = np.argpartition(array, k, axis=axis)
238
+ topk_indices = np.take(partitioned_indices, range(0, k), axis=axis)
239
+
240
+ topk_values = np.take_along_axis(array, topk_indices, axis=axis)
241
+ sorted_indices_in_topk = np.argsort(topk_values, axis=axis)
242
+ if largest:
243
+ sorted_indices_in_topk = np.flip(sorted_indices_in_topk, axis=axis)
244
+ sorted_topk_values = np.take_along_axis(topk_values,
245
+ sorted_indices_in_topk,
246
+ axis=axis)
247
+ sorted_topk_indices = np.take_along_axis(topk_indices,
248
+ sorted_indices_in_topk,
249
+ axis=axis)
250
+ return sorted_topk_values, sorted_topk_indices
251
+
252
+
253
+ def ctc_decoding(beam_log_probs,
254
+ beam_log_probs_idx,
255
+ encoder_out_lens,
256
+ vocabulary,
257
+ mode="ctc_prefix_beam_search"):
258
+ beam_size = beam_log_probs.shape[-1]
259
+ batch_size = beam_log_probs.shape[0]
260
+ num_processes = min(multiprocessing.cpu_count(), batch_size)
261
+ hyps = []
262
+ score_hyps = []
263
+
264
+ if mode == "ctc_greedy_search":
265
+ if beam_size == 1:
266
+ log_probs_idx = beam_log_probs_idx.squeeze(-1)
267
+ else:
268
+ log_probs_idx = beam_log_probs_idx[:, :, 0]
269
+ batch_sents = []
270
+ for idx, seq in enumerate(log_probs_idx):
271
+ batch_sents.append(seq[0:encoder_out_lens[idx]].tolist())
272
+ hyps = map_batch(batch_sents, vocabulary, num_processes, True, 0)
273
+ elif mode in ("ctc_prefix_beam_search", "attention_rescoring"):
274
+ batch_log_probs_seq_list = beam_log_probs.tolist()
275
+ batch_log_probs_idx_list = beam_log_probs_idx.tolist()
276
+ batch_len_list = encoder_out_lens.tolist()
277
+ batch_log_probs_seq = []
278
+ batch_log_probs_ids = []
279
+ batch_start = []
280
+ batch_root = []
281
+ for i in range(len(batch_len_list)):
282
+ num_sent = batch_len_list[i]
283
+ batch_log_probs_seq.append(batch_log_probs_seq_list[i][0:num_sent])
284
+ batch_log_probs_ids.append(batch_log_probs_idx_list[i][0:num_sent])
285
+ batch_root.append(None)
286
+ batch_start.append(True)
287
+ score_hyps = ctc_beam_search_decoder_batch(batch_log_probs_seq,
288
+ batch_log_probs_ids,
289
+ batch_root,
290
+ batch_start,
291
+ beam_size,
292
+ num_processes,
293
+ 0, -2, 0.99999)
294
+ if mode == "ctc_prefix_beam_search":
295
+ for cand_hyps in score_hyps:
296
+ hyps.append(cand_hyps[0][1])
297
+ hyps = map_batch(hyps, vocabulary, num_processes, False, 0)
298
+ return hyps, score_hyps
299
+
300
+
301
+ def has_higher_scored_collapsed_repeat(hyp, kept_hyps):
302
+ for i in range(1, len(hyp)):
303
+ if hyp[i] != hyp[i - 1]:
304
+ continue
305
+ collapsed = hyp[:i] + hyp[i + 1:]
306
+ if collapsed in kept_hyps:
307
+ return True
308
+ return False
309
+
310
+
311
+ def make_decoder_inputs(encoder_out,
312
+ encoder_out_lens,
313
+ beam_log_probs,
314
+ beam_log_probs_idx,
315
+ vocabulary,
316
+ sos,
317
+ eos,
318
+ decoder_len):
319
+ _, score_hyps = ctc_decoding(beam_log_probs, beam_log_probs_idx,
320
+ encoder_out_lens, vocabulary,
321
+ "attention_rescoring")
322
+ ignore_id = -1
323
+ beam_size = beam_log_probs.shape[-1]
324
+ batch_size = beam_log_probs.shape[0]
325
+ ctc_score, all_hyps = [], []
326
+ for hyps in score_hyps:
327
+ filtered_hyps = []
328
+ kept_hyps = set()
329
+ for score, hyp in hyps:
330
+ hyp = tuple(hyp)
331
+ if has_higher_scored_collapsed_repeat(hyp, kept_hyps):
332
+ continue
333
+ filtered_hyps.append((score, hyp))
334
+ kept_hyps.add(hyp)
335
+ if len(filtered_hyps) == beam_size:
336
+ break
337
+ hyps = filtered_hyps
338
+ cur_len = len(hyps)
339
+ if len(hyps) < beam_size:
340
+ hyps += (beam_size - cur_len) * [(-float("inf"), (0,))]
341
+ cur_ctc_score = []
342
+ for hyp in hyps:
343
+ cur_ctc_score.append(hyp[0])
344
+ all_hyps.append(list(hyp[1]))
345
+ ctc_score.append(cur_ctc_score)
346
+ ctc_score = np.array(ctc_score, dtype=np.float32)
347
+
348
+ max_len = decoder_len - 2
349
+ hyps_pad_sos_eos = np.ones((batch_size, beam_size, max_len + 2),
350
+ dtype=np.int64) * ignore_id
351
+ r_hyps_pad_sos_eos = np.ones((batch_size, beam_size, max_len + 2),
352
+ dtype=np.int64) * ignore_id
353
+ hyps_lens_sos = np.ones((batch_size, beam_size), dtype=np.int32)
354
+ k = 0
355
+ for i in range(batch_size):
356
+ for j in range(beam_size):
357
+ cand = all_hyps[k][:max_len]
358
+ length = len(cand) + 2
359
+ hyps_pad_sos_eos[i][j][0:length] = [sos] + cand + [eos]
360
+ r_hyps_pad_sos_eos[i][j][0:length] = [sos] + cand[::-1] + [eos]
361
+ hyps_lens_sos[i][j] = len(cand) + 1
362
+ k += 1
363
+
364
+ if decoder_len > encoder_out.shape[1]:
365
+ encoder_out = np.pad(encoder_out,
366
+ [(0, 0),
367
+ (0, decoder_len - encoder_out.shape[1]),
368
+ (0, 0)],
369
+ mode="constant",
370
+ constant_values=0)
371
+ elif decoder_len < encoder_out.shape[1]:
372
+ encoder_out = encoder_out[:, :decoder_len, :]
373
+
374
+ return {
375
+ "encoder_out": encoder_out,
376
+ "encoder_out_lens": np.full(batch_size,
377
+ fill_value=decoder_len,
378
+ dtype=np.int32),
379
+ "hyps_pad_sos_eos": hyps_pad_sos_eos.astype(np.int32),
380
+ "hyps_lens_sos": hyps_lens_sos,
381
+ "r_hyps_pad_sos_eos": r_hyps_pad_sos_eos.astype(np.int32),
382
+ "ctc_score": ctc_score,
383
+ }, all_hyps
384
+
385
+
386
+ def make_offline_inputs(feats, seq_len):
387
+ feats = feats[:, :seq_len, :]
388
+ speech_lengths = np.array([feats.shape[1]], dtype=np.int32)
389
+ if feats.shape[1] < seq_len:
390
+ feats = pad_array_along_axis(feats, pad_width=seq_len, axis=1)
391
+ return {"speech": feats, "speech_lengths": speech_lengths}
392
+
393
+
394
+ def make_online_initial_state(configs,
395
+ batch_size=1,
396
+ decoding_chunk_size=16,
397
+ num_decoding_left_chunks=5):
398
+ subsampling = 4
399
+ context = 7
400
+ stride = subsampling * decoding_chunk_size
401
+ decoding_window = (decoding_chunk_size - 1) * subsampling + context
402
+ required_cache_size = decoding_chunk_size * num_decoding_left_chunks
403
+
404
+ output_size = configs["encoder_conf"]["output_size"]
405
+ num_layers = configs["encoder_conf"]["num_blocks"]
406
+ cnn_module_kernel = configs["encoder_conf"].get("cnn_module_kernel", 1) - 1
407
+ head = configs["encoder_conf"]["attention_heads"]
408
+ d_k = configs["encoder_conf"]["output_size"] // head
409
+
410
+ state = {
411
+ "att_cache": np.zeros((batch_size, num_layers, head,
412
+ required_cache_size, d_k * 2),
413
+ dtype=np.float32),
414
+ "cnn_cache": np.zeros((batch_size, num_layers, output_size,
415
+ cnn_module_kernel),
416
+ dtype=np.float32),
417
+ "cache_mask": np.zeros((batch_size, 1, required_cache_size),
418
+ dtype=np.float32),
419
+ "offset": np.zeros((batch_size, 1), dtype=np.int32),
420
+ }
421
+ params = {
422
+ "batch_size": batch_size,
423
+ "context": context,
424
+ "stride": stride,
425
+ "decoding_window": decoding_window,
426
+ }
427
+ return state, params
428
+
429
+
430
+ def make_online_encoder_input(feats, cur, params, state):
431
+ batch_size = params["batch_size"]
432
+ decoding_window = params["decoding_window"]
433
+ end = min(cur + decoding_window, feats.shape[1])
434
+ chunk_xs = feats[:, cur:end, :]
435
+ if chunk_xs.shape[1] < decoding_window:
436
+ chunk_xs = pad_array_along_axis(chunk_xs,
437
+ pad_width=decoding_window,
438
+ axis=1)
439
+ chunk_xs = chunk_xs.astype(np.float32)
440
+ chunk_lens = np.full(batch_size,
441
+ fill_value=chunk_xs.shape[1],
442
+ dtype=np.int32)
443
+ return {
444
+ "chunk_xs": chunk_xs,
445
+ "chunk_lens": chunk_lens,
446
+ "offset": state["offset"],
447
+ "att_cache": state["att_cache"],
448
+ "cnn_cache": state["cnn_cache"],
449
+ "cache_mask": state["cache_mask"],
450
+ }
451
+
452
+
453
+ def output_value(outputs, name):
454
+ if name in outputs:
455
+ return outputs[name]
456
+ r_name = "r_" + name
457
+ if r_name in outputs:
458
+ return outputs[r_name]
459
+ raise KeyError(name)
460
+
461
+
462
+ def update_online_state(state, outputs):
463
+ state["offset"] = output_value(outputs, "offset")
464
+ state["att_cache"] = output_value(outputs, "att_cache")
465
+ state["cnn_cache"] = output_value(outputs, "cnn_cache")
466
+ state["cache_mask"] = output_value(outputs, "cache_mask")
467
+
468
+
469
+ class AxModel:
470
+
471
+ def __init__(self, path):
472
+ from axengine import InferenceSession
473
+
474
+ self.session = InferenceSession(path)
475
+ self.output_names = [item.name for item in self.session.get_outputs()]
476
+
477
+ def run(self, input_feed):
478
+ output_values = self.session.run(self.output_names, input_feed)
479
+ return dict(zip(self.output_names, output_values))
480
+
481
+
482
+ class WenetAXRunner:
483
+
484
+ def __init__(self,
485
+ config_path,
486
+ vocab_path,
487
+ encoder_offline_path="axmodel/encoder_offline/encoder_offline.axmodel",
488
+ encoder_online_path="axmodel/encoder_online/encoder_online.axmodel",
489
+ decoder_path="axmodel/decoder/decoder.axmodel",
490
+ offline_seq_len=1024,
491
+ decoder_len=32,
492
+ decoding_chunk_size=16,
493
+ num_decoding_left_chunks=5,
494
+ batch_size=1):
495
+ self.config_path = config_path
496
+ self.vocab_path = vocab_path
497
+ self.encoder_offline_path = encoder_offline_path
498
+ self.encoder_online_path = encoder_online_path
499
+ self.decoder_path = decoder_path
500
+ self.offline_seq_len = offline_seq_len
501
+ self.decoder_len = decoder_len
502
+ self.decoding_chunk_size = decoding_chunk_size
503
+ self.num_decoding_left_chunks = num_decoding_left_chunks
504
+ self.batch_size = batch_size
505
+
506
+ self.configs = load_config(config_path)
507
+ self.vocabulary, self.char_dict = load_vocab(vocab_path)
508
+ self.eos = self.sos = len(self.char_dict) - 1
509
+
510
+ self._offline_encoder = None
511
+ self._online_encoder = None
512
+ self._decoder = None
513
+
514
+ @property
515
+ def offline_encoder(self):
516
+ if self._offline_encoder is None:
517
+ self._offline_encoder = AxModel(self.encoder_offline_path)
518
+ return self._offline_encoder
519
+
520
+ @property
521
+ def online_encoder(self):
522
+ if self._online_encoder is None:
523
+ self._online_encoder = AxModel(self.encoder_online_path)
524
+ return self._online_encoder
525
+
526
+ @property
527
+ def decoder(self):
528
+ if self._decoder is None:
529
+ self._decoder = AxModel(self.decoder_path)
530
+ return self._decoder
531
+
532
+ def compute_feats(self, audio_file):
533
+ return compute_feats(audio_file)
534
+
535
+ def run_offline_encoder(self, feats):
536
+ encoder_input = make_offline_inputs(feats, self.offline_seq_len)
537
+ speech_lengths = encoder_input["speech_lengths"]
538
+ outputs = self.offline_encoder.run(encoder_input)
539
+ encoder_out_lens = outputs["encoder_out_lens"].astype(np.int32)
540
+ encoder_out_lens[0] = np.ones([speech_lengths[0]],
541
+ dtype=np.int32)[2::2][2::2].sum()
542
+ beam_log_probs, beam_log_probs_idx = numpy_topk(
543
+ outputs["ctc_log_probs"], k=10)
544
+ return {
545
+ "encoder_out": outputs["encoder_out"],
546
+ "encoder_out_lens": encoder_out_lens,
547
+ "ctc_log_probs": outputs["ctc_log_probs"],
548
+ "beam_log_probs": beam_log_probs,
549
+ "beam_log_probs_idx": beam_log_probs_idx,
550
+ }
551
+
552
+ def run_online_encoder(self, feats):
553
+ state, online_params = make_online_initial_state(
554
+ self.configs, self.batch_size, self.decoding_chunk_size,
555
+ self.num_decoding_left_chunks)
556
+ encoder_out = []
557
+ beam_log_probs = []
558
+ beam_log_probs_idx = []
559
+ num_frames = feats.shape[1]
560
+
561
+ for cur in range(0, num_frames - online_params["context"] + 1,
562
+ online_params["stride"]):
563
+ encoder_input = make_online_encoder_input(feats, cur,
564
+ online_params, state)
565
+ outputs = self.online_encoder.run(encoder_input)
566
+ update_online_state(state, outputs)
567
+ encoder_out.append(outputs["chunk_out"])
568
+ beam_log_probs.append(outputs["log_probs"])
569
+ beam_log_probs_idx.append(outputs["log_probs_idx"].astype(np.int32))
570
+
571
+ return {
572
+ "encoder_out": np.concatenate(encoder_out, axis=1),
573
+ "encoder_out_lens": np.full(self.batch_size,
574
+ fill_value=sum(
575
+ out.shape[1]
576
+ for out in encoder_out),
577
+ dtype=np.int32),
578
+ "beam_log_probs": np.concatenate(beam_log_probs, axis=1),
579
+ "beam_log_probs_idx": np.concatenate(beam_log_probs_idx, axis=1),
580
+ }
581
+
582
+ def ctc_decode(self, encoder_outputs, mode):
583
+ return ctc_decoding(encoder_outputs["beam_log_probs"],
584
+ encoder_outputs["beam_log_probs_idx"],
585
+ encoder_outputs["encoder_out_lens"],
586
+ self.vocabulary, mode)
587
+
588
+ def run_decoder(self, encoder_outputs):
589
+ decoder_input, all_hyps = make_decoder_inputs(
590
+ encoder_outputs["encoder_out"],
591
+ encoder_outputs["encoder_out_lens"],
592
+ encoder_outputs["beam_log_probs"],
593
+ encoder_outputs["beam_log_probs_idx"],
594
+ self.vocabulary,
595
+ self.sos,
596
+ self.eos,
597
+ self.decoder_len,
598
+ )
599
+ best_index = self.decoder.run(decoder_input)["best_index"].astype(
600
+ np.int32)
601
+ beam_size = encoder_outputs["beam_log_probs"].shape[-1]
602
+ num_processes = min(multiprocessing.cpu_count(), best_index.shape[0])
603
+ best_sents = []
604
+ k = 0
605
+ for idx in best_index:
606
+ best_sents.append(all_hyps[k:k + beam_size][idx])
607
+ k += beam_size
608
+ hyps = map_batch(best_sents, self.vocabulary, num_processes)
609
+ return "".join(hyps)
610
+
611
+ def transcribe(self,
612
+ audio_file,
613
+ online=False,
614
+ mode="ctc_prefix_beam_search"):
615
+ feats = self.compute_feats(audio_file)
616
+ if online:
617
+ encoder_outputs = self.run_online_encoder(feats)
618
+ else:
619
+ encoder_outputs = self.run_offline_encoder(feats)
620
+
621
+ if mode == "attention_rescoring":
622
+ return self.run_decoder(encoder_outputs)
623
+
624
+ hyps, _ = self.ctc_decode(encoder_outputs, mode)
625
+ return "".join(hyps) if hyps else ""
config_decoder.json ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "ONNX",
3
+ "npu_mode": "NPU1",
4
+ "quant": {
5
+ "input_configs": [
6
+ {
7
+ "tensor_name": "encoder_out",
8
+ "calibration_dataset": "./calibration_dataset/encoder_out.tar.gz",
9
+ "calibration_size": -1,
10
+ "calibration_format": "Numpy"
11
+ },
12
+ {
13
+ "tensor_name": "encoder_out_lens",
14
+ "calibration_dataset": "./calibration_dataset/encoder_out_lens.tar.gz",
15
+ "calibration_size": -1,
16
+ "calibration_format": "Numpy"
17
+ },
18
+ {
19
+ "tensor_name": "hyps_pad_sos_eos",
20
+ "calibration_dataset": "./calibration_dataset/hyps_pad_sos_eos.tar.gz",
21
+ "calibration_size": -1,
22
+ "calibration_format": "Numpy"
23
+ },
24
+ {
25
+ "tensor_name": "hyps_lens_sos",
26
+ "calibration_dataset": "./calibration_dataset/hyps_lens_sos.tar.gz",
27
+ "calibration_size": -1,
28
+ "calibration_format": "Numpy"
29
+ },
30
+ {
31
+ "tensor_name": "r_hyps_pad_sos_eos",
32
+ "calibration_dataset": "./calibration_dataset/r_hyps_pad_sos_eos.tar.gz",
33
+ "calibration_size": -1,
34
+ "calibration_format": "Numpy"
35
+ },
36
+ {
37
+ "tensor_name": "ctc_score",
38
+ "calibration_dataset": "./calibration_dataset/ctc_score.tar.gz",
39
+ "calibration_size": -1,
40
+ "calibration_format": "Numpy"
41
+ }
42
+ ],
43
+ "layer_configs": [
44
+ {
45
+ "start_tensor_names": ["DEFAULT"],
46
+ "end_tensor_names": ["DEFAULT"],
47
+ "data_type": "U8"
48
+ }
49
+ ],
50
+ "calibration_method": "MinMax",
51
+ "conv_bias_data_type": "FP32",
52
+ "disable_auto_refine_scale": true,
53
+ "enable_smooth_quant": true,
54
+ "precision_analysis": true,
55
+ "precision_analysis_method": "EndToEnd",
56
+ "transformer_opt_level": 1
57
+ },
58
+ "input_processors": [
59
+ {
60
+ "tensor_name": "encoder_out",
61
+ "src_dtype": "FP32"
62
+ },
63
+ {
64
+ "tensor_name": "encoder_out_lens",
65
+ "src_dtype": "S32"
66
+ },
67
+ {
68
+ "tensor_name": "hyps_pad_sos_eos",
69
+ "src_dtype": "S32"
70
+ },
71
+ {
72
+ "tensor_name": "hyps_lens_sos",
73
+ "src_dtype": "S32"
74
+ },
75
+ {
76
+ "tensor_name": "r_hyps_pad_sos_eos",
77
+ "src_dtype": "S32"
78
+ },
79
+ {
80
+ "tensor_name": "ctc_score",
81
+ "src_dtype": "FP32"
82
+ }
83
+ ],
84
+ "compiler": {
85
+ "check": 0
86
+ }
87
+ }
88
+
config_encoder_offline.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "ONNX",
3
+ "npu_mode": "NPU3",
4
+ "quant": {
5
+ "input_configs": [
6
+ {
7
+ "tensor_name": "speech",
8
+ "calibration_dataset": "./calibration_dataset/speech.tar.gz",
9
+ "calibration_size": -1,
10
+ "calibration_format": "Numpy"
11
+ },
12
+ {
13
+ "tensor_name": "speech_lengths",
14
+ "calibration_dataset": "./calibration_dataset/speech_lengths.tar.gz",
15
+ "calibration_size": -1,
16
+ "calibration_format": "Numpy"
17
+ }
18
+ ],
19
+ "layer_configs": [
20
+ {
21
+ "start_tensor_names": ["DEFAULT"],
22
+ "end_tensor_names": ["DEFAULT"],
23
+ "data_type": "U16"
24
+ }
25
+ ],
26
+ "calibration_method": "MinMax",
27
+ "conv_bias_data_type": "FP32",
28
+ "disable_auto_refine_scale": true,
29
+ "enable_smooth_quant": true,
30
+ "precision_analysis": true,
31
+ "precision_analysis_method": "EndToEnd",
32
+ "transformer_opt_level": 1
33
+ },
34
+ "input_processors": [
35
+ {
36
+ "tensor_name": "speech",
37
+ "src_dtype": "FP32"
38
+ },
39
+ {
40
+ "tensor_name": "speech_lengths",
41
+ "src_dtype": "S32"
42
+ }
43
+ ],
44
+ "compiler": {
45
+ "check": 0
46
+ }
47
+ }
48
+
config_encoder_online.json ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "ONNX",
3
+ "npu_mode": "NPU3",
4
+ "quant": {
5
+ "input_configs": [
6
+ {
7
+ "tensor_name": "chunk_xs",
8
+ "calibration_dataset": "./calibration_dataset/chunk_xs.tar.gz",
9
+ "calibration_size": -1,
10
+ "calibration_format": "Numpy"
11
+ },
12
+ {
13
+ "tensor_name": "chunk_lens",
14
+ "calibration_dataset": "./calibration_dataset/chunk_lens.tar.gz",
15
+ "calibration_size": -1,
16
+ "calibration_format": "Numpy"
17
+ },
18
+ {
19
+ "tensor_name": "offset",
20
+ "calibration_dataset": "./calibration_dataset/offset.tar.gz",
21
+ "calibration_size": -1,
22
+ "calibration_format": "Numpy"
23
+ },
24
+ {
25
+ "tensor_name": "att_cache",
26
+ "calibration_dataset": "./calibration_dataset/att_cache.tar.gz",
27
+ "calibration_size": -1,
28
+ "calibration_format": "Numpy"
29
+ },
30
+ {
31
+ "tensor_name": "cnn_cache",
32
+ "calibration_dataset": "./calibration_dataset/cnn_cache.tar.gz",
33
+ "calibration_size": -1,
34
+ "calibration_format": "Numpy"
35
+ },
36
+ {
37
+ "tensor_name": "cache_mask",
38
+ "calibration_dataset": "./calibration_dataset/cache_mask.tar.gz",
39
+ "calibration_size": -1,
40
+ "calibration_format": "Numpy"
41
+ }
42
+ ],
43
+ "layer_configs": [
44
+ {
45
+ "start_tensor_names": ["DEFAULT"],
46
+ "end_tensor_names": ["DEFAULT"],
47
+ "data_type": "U16"
48
+ }
49
+ ],
50
+ "calibration_method": "MinMax",
51
+ "conv_bias_data_type": "FP32",
52
+ "disable_auto_refine_scale": true,
53
+ "enable_smooth_quant": true,
54
+ "precision_analysis": true,
55
+ "precision_analysis_method": "EndToEnd",
56
+ "transformer_opt_level": 1
57
+ },
58
+ "input_processors": [
59
+ {
60
+ "tensor_name": "chunk_xs",
61
+ "src_dtype": "FP32"
62
+ },
63
+ {
64
+ "tensor_name": "chunk_lens",
65
+ "src_dtype": "S32"
66
+ },
67
+ {
68
+ "tensor_name": "offset",
69
+ "src_dtype": "S32"
70
+ },
71
+ {
72
+ "tensor_name": "att_cache",
73
+ "src_dtype": "FP32"
74
+ },
75
+ {
76
+ "tensor_name": "cnn_cache",
77
+ "src_dtype": "FP32"
78
+ },
79
+ {
80
+ "tensor_name": "cache_mask",
81
+ "src_dtype": "FP32"
82
+ }
83
+ ],
84
+ "compiler": {
85
+ "check": 0
86
+ }
87
+ }
88
+
demo.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:46dbc998c9d1d48111267c40741dd3200f2e5bcf4075f8c4c97f4451160dce50
3
+ size 134570
export_onnx.py ADDED
@@ -0,0 +1,1723 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import print_function
16
+
17
+ import argparse
18
+ import copy
19
+ import logging
20
+ import os
21
+ import sys
22
+ import tarfile
23
+ import urllib.request
24
+
25
+ import numpy as np
26
+ import torch
27
+ import torch.nn.functional as F
28
+ import yaml
29
+ from wenet.transformer.ctc import CTC
30
+ from wenet.transformer.decoder import TransformerDecoder
31
+ from wenet.transformer.encoder import BaseEncoder
32
+ from wenet.utils.init_model import init_model
33
+ from wenet.utils.mask import make_pad_mask
34
+ from typing import List, Tuple
35
+
36
+ try:
37
+ import onnx
38
+ import onnxruntime
39
+ from onnx import helper, numpy_helper
40
+ from onnxsim import simplify
41
+ except ImportError:
42
+ print("Please install onnxruntime!")
43
+ sys.exit(1)
44
+
45
+ logger = logging.getLogger(__file__)
46
+ logger.setLevel(logging.INFO)
47
+
48
+ DEFAULT_PRETRAINED_MODEL_URL = (
49
+ "https://huggingface.co/openspeech/wenet-models/resolve/main/"
50
+ "aishell_u2pp_conformer_exp.tar.gz")
51
+ DEFAULT_PRETRAINED_MODEL_DIR = "pretrained/aishell_u2pp_conformer_exp"
52
+
53
+
54
+ def safe_extract_tar(tar, output_dir):
55
+ output_dir = os.path.abspath(output_dir)
56
+ for member in tar.getmembers():
57
+ member_path = os.path.abspath(os.path.join(output_dir, member.name))
58
+ if not member_path.startswith(output_dir + os.sep):
59
+ raise RuntimeError(f"Unsafe tar member path: {member.name}")
60
+ tar.extractall(output_dir)
61
+
62
+
63
+ def download_file(url, output_path):
64
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
65
+ print(f"Downloading pretrained model from {url}")
66
+ print(f"Saving to {output_path}")
67
+ urllib.request.urlretrieve(url, output_path)
68
+
69
+
70
+ def prepare_pretrained_model(args):
71
+ model_dir = args.pretrained_model_dir
72
+ archive_dir = os.path.dirname(model_dir.rstrip(os.sep)) or "."
73
+ archive_path = os.path.join(
74
+ archive_dir, os.path.basename(model_dir.rstrip(os.sep)) + ".tar.gz")
75
+
76
+ if not os.path.exists(model_dir):
77
+ if not os.path.exists(archive_path):
78
+ download_file(args.pretrained_model_url, archive_path)
79
+ print(f"Extracting pretrained model to {archive_dir}")
80
+ with tarfile.open(archive_path, "r:gz") as tar:
81
+ safe_extract_tar(tar, archive_dir)
82
+
83
+ args.config = os.path.join(model_dir, "train.yaml")
84
+ args.checkpoint = os.path.join(model_dir, "final.pt")
85
+ args.cmvn_file = os.path.join(model_dir, "global_cmvn")
86
+
87
+ missing = [path for path in (args.config, args.checkpoint)
88
+ if not os.path.exists(path)]
89
+ if missing:
90
+ raise FileNotFoundError(
91
+ "Missing pretrained model files: " + ", ".join(missing))
92
+
93
+ print(f"Using config: {args.config}")
94
+ print(f"Using checkpoint: {args.checkpoint}")
95
+ if os.path.exists(args.cmvn_file):
96
+ print(f"Using CMVN: {args.cmvn_file}")
97
+
98
+
99
+ def _constant_node_value(node):
100
+ if node is None or node.op_type != "Constant":
101
+ return None
102
+ for attr in node.attribute:
103
+ if attr.name == "value":
104
+ return numpy_helper.to_array(attr.t)
105
+ return None
106
+
107
+
108
+ def _attribute_value(attr):
109
+ return helper.get_attribute_value(attr)
110
+
111
+
112
+ def _get_attr(node, name, default=None):
113
+ for attr in node.attribute:
114
+ if attr.name == name:
115
+ return _attribute_value(attr)
116
+ return default
117
+
118
+
119
+ def _cast_to_onnx_dtype(value, to_dtype):
120
+ tensor_type = onnx.TensorProto.DataType.Name(to_dtype).lower()
121
+ dtype_map = {
122
+ "float": np.float32,
123
+ "double": np.float64,
124
+ "float16": np.float16,
125
+ "int64": np.int64,
126
+ "int32": np.int32,
127
+ "int16": np.int16,
128
+ "int8": np.int8,
129
+ "uint64": np.uint64,
130
+ "uint32": np.uint32,
131
+ "uint16": np.uint16,
132
+ "uint8": np.uint8,
133
+ "bool": np.bool_,
134
+ }
135
+ if tensor_type not in dtype_map:
136
+ return None
137
+ return value.astype(dtype_map[tensor_type])
138
+
139
+
140
+ def _shape_from_value_info(value_info):
141
+ if not value_info.type.HasField("tensor_type"):
142
+ return None
143
+ if not value_info.type.tensor_type.shape.dim:
144
+ return None
145
+ shape = []
146
+ for dim in value_info.type.tensor_type.shape.dim:
147
+ if dim.HasField("dim_value") and dim.dim_value > 0:
148
+ shape.append(dim.dim_value)
149
+ else:
150
+ return None
151
+ return tuple(shape)
152
+
153
+
154
+ def _collect_static_shapes(model):
155
+ inferred = onnx.shape_inference.infer_shapes(model)
156
+ shapes = {}
157
+ for value_info in list(inferred.graph.input) + list(
158
+ inferred.graph.value_info) + list(inferred.graph.output):
159
+ shape = _shape_from_value_info(value_info)
160
+ if shape is not None:
161
+ shapes[value_info.name] = shape
162
+ for initializer in inferred.graph.initializer:
163
+ shapes[initializer.name] = tuple(initializer.dims)
164
+ return shapes
165
+
166
+
167
+ def _eval_static_node(node, inputs, static_shapes):
168
+ if node.op_type == "Constant":
169
+ return _constant_node_value(node)
170
+ if node.op_type != "Shape" and any(value is None for value in inputs):
171
+ return None
172
+
173
+ try:
174
+ if node.op_type == "Add":
175
+ return np.add(inputs[0], inputs[1])
176
+ if node.op_type == "Sub":
177
+ return np.subtract(inputs[0], inputs[1])
178
+ if node.op_type == "Mul":
179
+ return np.multiply(inputs[0], inputs[1])
180
+ if node.op_type == "Div":
181
+ return np.divide(inputs[0], inputs[1])
182
+ if node.op_type == "Equal":
183
+ return np.equal(inputs[0], inputs[1])
184
+ if node.op_type == "Greater":
185
+ return np.greater(inputs[0], inputs[1])
186
+ if node.op_type == "GreaterOrEqual":
187
+ return np.greater_equal(inputs[0], inputs[1])
188
+ if node.op_type == "Less":
189
+ return np.less(inputs[0], inputs[1])
190
+ if node.op_type == "LessOrEqual":
191
+ return np.less_equal(inputs[0], inputs[1])
192
+ if node.op_type == "Where":
193
+ return np.where(inputs[0], inputs[1], inputs[2])
194
+ if node.op_type == "Concat":
195
+ axis = _get_attr(node, "axis", 0)
196
+ return np.concatenate(inputs, axis=axis)
197
+ if node.op_type == "Unsqueeze":
198
+ axes = _get_attr(node, "axes")
199
+ if axes is None and len(inputs) > 1:
200
+ axes = inputs[1]
201
+ axes = tuple(int(axis) for axis in np.asarray(axes).reshape(-1))
202
+ return np.expand_dims(inputs[0], axis=axes)
203
+ if node.op_type == "Squeeze":
204
+ axes = _get_attr(node, "axes")
205
+ if axes is None and len(inputs) > 1:
206
+ axes = inputs[1]
207
+ if axes is None:
208
+ return np.squeeze(inputs[0])
209
+ axes = tuple(int(axis) for axis in np.asarray(axes).reshape(-1))
210
+ return np.squeeze(inputs[0], axis=axes)
211
+ if node.op_type == "Cast":
212
+ return _cast_to_onnx_dtype(inputs[0], _get_attr(node, "to"))
213
+ if node.op_type == "Reshape":
214
+ return np.reshape(inputs[0], tuple(int(i) for i in inputs[1]))
215
+ if node.op_type == "Shape":
216
+ if inputs[0] is not None:
217
+ shape = inputs[0].shape
218
+ else:
219
+ shape = static_shapes.get(node.input[0])
220
+ if shape is None:
221
+ return None
222
+ return np.asarray(shape, dtype=np.int64)
223
+ if node.op_type == "Slice":
224
+ data = inputs[0]
225
+ starts = np.asarray(inputs[1]).reshape(-1)
226
+ ends = np.asarray(inputs[2]).reshape(-1)
227
+ axes = (np.asarray(inputs[3]).reshape(-1)
228
+ if len(inputs) > 3 and inputs[3] is not None else
229
+ np.arange(len(starts)))
230
+ steps = (np.asarray(inputs[4]).reshape(-1)
231
+ if len(inputs) > 4 and inputs[4] is not None else
232
+ np.ones(len(starts), dtype=np.int64))
233
+ slices = [slice(None)] * data.ndim
234
+ for start, end, axis, step in zip(starts, ends, axes, steps):
235
+ axis = int(axis)
236
+ start = int(start)
237
+ end = int(end)
238
+ step = int(step)
239
+ if end >= np.iinfo(np.int32).max:
240
+ end = None
241
+ if end <= np.iinfo(np.int32).min:
242
+ end = None
243
+ slices[axis] = slice(start, end, step)
244
+ return data[tuple(slices)]
245
+ if node.op_type == "Gather":
246
+ axis = _get_attr(node, "axis", 0)
247
+ return np.take(inputs[0], inputs[1], axis=axis)
248
+ except Exception:
249
+ return None
250
+ return None
251
+
252
+
253
+ def _constant_node(output_name, value, name):
254
+ const_tensor = numpy_helper.from_array(np.asarray(value),
255
+ name=output_name + "_value")
256
+ return helper.make_node("Constant",
257
+ inputs=[],
258
+ outputs=[output_name],
259
+ name=name,
260
+ value=const_tensor)
261
+
262
+
263
+ def _node_attributes(node):
264
+ return {attr.name: helper.get_attribute_value(attr) for attr in node.attribute}
265
+
266
+
267
+ def _copy_node(node, inputs=None, outputs=None, name=None):
268
+ copied = copy.deepcopy(node)
269
+ if inputs is not None:
270
+ del copied.input[:]
271
+ copied.input.extend(inputs)
272
+ if outputs is not None:
273
+ del copied.output[:]
274
+ copied.output.extend(outputs)
275
+ if name is not None:
276
+ copied.name = name
277
+ return copied
278
+
279
+
280
+ def _producer_map(model):
281
+ return {output: node for node in model.graph.node for output in node.output}
282
+
283
+
284
+ def _unsqueeze_greater_equal_pattern(producer, value_name):
285
+ unsqueeze = producer.get(value_name)
286
+ if unsqueeze is None or unsqueeze.op_type != "Unsqueeze":
287
+ return None, None
288
+ compare = producer.get(unsqueeze.input[0])
289
+ if compare is None or compare.op_type != "GreaterOrEqual":
290
+ return None, None
291
+ return unsqueeze, compare
292
+
293
+
294
+ def rewrite_pulsar2_bool_not(onnx_path):
295
+ """Remove simple Not nodes that Pulsar2 quantization can cast to float.
296
+
297
+ The encoder mask contains Not(Unsqueeze(GreaterOrEqual(...))) and another
298
+ Not over a sliced version of that mask. Pulsar2 can quantize the Not input
299
+ to FP32 and then fail because bitwise Not only accepts bool/integer tensors.
300
+ Rewriting those patterns keeps the graph boolean-equivalent without Not.
301
+ """
302
+ model = onnx.load(onnx_path)
303
+ producer = _producer_map(model)
304
+ rewritten = 0
305
+ new_nodes = []
306
+
307
+ for node in model.graph.node:
308
+ if node.op_type != "Not":
309
+ new_nodes.append(node)
310
+ continue
311
+
312
+ compare = producer.get(node.input[0])
313
+ if compare is not None and compare.op_type == "GreaterOrEqual":
314
+ less = helper.make_node("Less",
315
+ inputs=list(compare.input),
316
+ outputs=list(node.output),
317
+ name=node.name + "_less",
318
+ **_node_attributes(compare))
319
+ new_nodes.append(less)
320
+ rewritten += 1
321
+ continue
322
+
323
+ unsqueeze, compare = _unsqueeze_greater_equal_pattern(
324
+ producer, node.input[0])
325
+ if unsqueeze is not None:
326
+ less_output = node.output[0] + "_less"
327
+ less = helper.make_node("Less",
328
+ inputs=list(compare.input),
329
+ outputs=[less_output],
330
+ name=node.name + "_less",
331
+ **_node_attributes(compare))
332
+ rewritten_unsqueeze = _copy_node(
333
+ unsqueeze,
334
+ inputs=[less_output] + list(unsqueeze.input[1:]),
335
+ outputs=list(node.output),
336
+ name=node.name + "_unsqueeze")
337
+ new_nodes.extend([less, rewritten_unsqueeze])
338
+ rewritten += 1
339
+ continue
340
+
341
+ slice_1 = producer.get(node.input[0])
342
+ slice_0 = producer.get(slice_1.input[0]) if slice_1 else None
343
+ inner_not = producer.get(slice_0.input[0]) if slice_0 else None
344
+ if (slice_1 is not None and slice_1.op_type == "Slice"
345
+ and slice_0 is not None and slice_0.op_type == "Slice"
346
+ and inner_not is not None and inner_not.op_type == "Not"):
347
+ unsqueeze, _ = _unsqueeze_greater_equal_pattern(
348
+ producer, inner_not.input[0])
349
+ if unsqueeze is not None:
350
+ slice_0_output = node.output[0] + "_slice0"
351
+ rewritten_slice_0 = _copy_node(
352
+ slice_0,
353
+ inputs=[unsqueeze.output[0]] + list(slice_0.input[1:]),
354
+ outputs=[slice_0_output],
355
+ name=node.name + "_slice0")
356
+ rewritten_slice_1 = _copy_node(
357
+ slice_1,
358
+ inputs=[slice_0_output] + list(slice_1.input[1:]),
359
+ outputs=list(node.output),
360
+ name=node.name + "_slice1")
361
+ new_nodes.extend([rewritten_slice_0, rewritten_slice_1])
362
+ rewritten += 1
363
+ continue
364
+
365
+ new_nodes.append(node)
366
+
367
+ if rewritten:
368
+ del model.graph.node[:]
369
+ model.graph.node.extend(new_nodes)
370
+ onnx.checker.check_model(model)
371
+ onnx.save(model, onnx_path)
372
+ print(f"Rewrote {rewritten} bool Not node(s) in {onnx_path}")
373
+
374
+
375
+ def rewrite_pulsar2_bool_and(onnx_path):
376
+ """Replace boolean And with arithmetic comparison for Pulsar2 quantization."""
377
+ model = onnx.load(onnx_path)
378
+ rewritten = 0
379
+ new_nodes = []
380
+
381
+ for node in model.graph.node:
382
+ if node.op_type != "And" or len(node.input) != 2 or len(
383
+ node.output) != 1:
384
+ new_nodes.append(node)
385
+ continue
386
+
387
+ left = node.output[0] + "_left_i32"
388
+ right = node.output[0] + "_right_i32"
389
+ added = node.output[0] + "_sum"
390
+ threshold = node.output[0] + "_threshold"
391
+ new_nodes.append(
392
+ helper.make_node("Cast",
393
+ inputs=[node.input[0]],
394
+ outputs=[left],
395
+ name=node.name + "_cast_left",
396
+ to=onnx.TensorProto.INT32))
397
+ new_nodes.append(
398
+ helper.make_node("Cast",
399
+ inputs=[node.input[1]],
400
+ outputs=[right],
401
+ name=node.name + "_cast_right",
402
+ to=onnx.TensorProto.INT32))
403
+ new_nodes.append(
404
+ helper.make_node("Add",
405
+ inputs=[left, right],
406
+ outputs=[added],
407
+ name=node.name + "_add"))
408
+ new_nodes.append(
409
+ _constant_node(threshold, np.asarray(1, dtype=np.int32),
410
+ node.name + "_threshold"))
411
+ new_nodes.append(
412
+ helper.make_node("Greater",
413
+ inputs=[added, threshold],
414
+ outputs=list(node.output),
415
+ name=node.name + "_greater"))
416
+ rewritten += 1
417
+
418
+ if rewritten:
419
+ del model.graph.node[:]
420
+ model.graph.node.extend(new_nodes)
421
+ onnx.checker.check_model(model)
422
+ onnx.save(model, onnx_path)
423
+ print(f"Rewrote {rewritten} bool And node(s) in {onnx_path}")
424
+
425
+
426
+ def simplify_pulsar2_onnx(onnx_path):
427
+ model = onnx.load(onnx_path)
428
+ sim_model, ok = simplify(model)
429
+ if not ok:
430
+ raise RuntimeError(f"onnxsim failed to validate {onnx_path}")
431
+ onnx.checker.check_model(sim_model)
432
+ onnx.save(sim_model, onnx_path)
433
+ print(f"Simplified {onnx_path} for Pulsar2")
434
+
435
+
436
+ def fold_static_pulsar2_subgraphs(onnx_path):
437
+ """Fold static ONNX patterns that Pulsar2 5.0 cannot infer reliably.
438
+
439
+ Pulsar2 5.0 can fail shape inference on ConstantOfShape when its input is
440
+ a constant tensor value instead of an initializer. The legacy exporter emits
441
+ this pattern for masks/padding in the encoder graphs. It can also fail when
442
+ an Expand shape is produced by a constant-only subgraph such as
443
+ Mul/Equal/Where. Fold those static pieces before handing the model to
444
+ Pulsar2.
445
+ """
446
+ model = onnx.load(onnx_path)
447
+ static_shapes = _collect_static_shapes(model)
448
+ constants = {
449
+ initializer.name: numpy_helper.to_array(initializer)
450
+ for initializer in model.graph.initializer
451
+ }
452
+ folded = 0
453
+ new_nodes = []
454
+ for node in model.graph.node:
455
+ inputs = [constants.get(name) for name in node.input]
456
+ if node.op_type == "ConstantOfShape" and node.input:
457
+ shape_value = inputs[0]
458
+ if shape_value is not None:
459
+ fill_value = np.array(0, dtype=np.float32)
460
+ for attr in node.attribute:
461
+ if attr.name == "value":
462
+ fill_value = numpy_helper.to_array(attr.t)
463
+ break
464
+ shape = tuple(int(dim)
465
+ for dim in np.asarray(shape_value).reshape(-1))
466
+ value = np.full(shape,
467
+ fill_value.reshape(-1)[0],
468
+ dtype=fill_value.dtype)
469
+ else:
470
+ value = None
471
+ else:
472
+ value = _eval_static_node(node, inputs, static_shapes)
473
+
474
+ if value is None or len(node.output) != 1:
475
+ new_nodes.append(node)
476
+ continue
477
+
478
+ constants[node.output[0]] = value
479
+ new_nodes.append(_constant_node(node.output[0], value, node.name))
480
+ folded += 1
481
+
482
+ if folded:
483
+ del model.graph.node[:]
484
+ model.graph.node.extend(new_nodes)
485
+ onnx.checker.check_model(model)
486
+ onnx.save(model, onnx_path)
487
+ print(f"Folded {folded} static node(s) in {onnx_path}")
488
+
489
+
490
+ class Encoder(torch.nn.Module):
491
+
492
+ def __init__(self, encoder: BaseEncoder, ctc: CTC, beam_size: int = 10):
493
+ super().__init__()
494
+ self.encoder = encoder
495
+ self.ctc = ctc
496
+ self.beam_size = beam_size
497
+
498
+ def forward(
499
+ self,
500
+ speech: torch.Tensor,
501
+ speech_lengths: torch.Tensor,
502
+ ):
503
+ """Encoder
504
+ Args:
505
+ speech: (Batch, Length, ...)
506
+ speech_lengths: (Batch, )
507
+ Returns:
508
+ encoder_out: B x T x F
509
+ encoder_out_lens: B
510
+ ctc_log_probs: B x T x V
511
+ beam_log_probs: B x T x beam_size
512
+ beam_log_probs_idx: B x T x beam_size
513
+ """
514
+ encoder_out, encoder_mask = self.encoder(speech, speech_lengths, -1,
515
+ -1)
516
+ encoder_out_lens = encoder_mask.squeeze(1).sum(1)
517
+ # ctc_log_probs = self.ctc.log_softmax(encoder_out)
518
+ ctc_log_probs = self.ctc.linear(encoder_out)
519
+ encoder_out_lens = encoder_out_lens.int()
520
+ beam_log_probs, beam_log_probs_idx = torch.topk(ctc_log_probs,
521
+ self.beam_size,
522
+ dim=2)
523
+ return (
524
+ encoder_out,
525
+ encoder_out_lens,
526
+ ctc_log_probs,
527
+ beam_log_probs,
528
+ beam_log_probs_idx,
529
+ )
530
+
531
+
532
+ class StreamingEncoder(torch.nn.Module):
533
+
534
+ def __init__(
535
+ self,
536
+ model,
537
+ required_cache_size,
538
+ beam_size,
539
+ transformer=False,
540
+ return_ctc_logprobs=False,
541
+ ):
542
+ super().__init__()
543
+ self.ctc = model.ctc
544
+ self.subsampling_rate = model.encoder.embed.subsampling_rate
545
+ self.embed = model.encoder.embed
546
+ self.global_cmvn = model.encoder.global_cmvn
547
+ self.required_cache_size = required_cache_size
548
+ self.beam_size = beam_size
549
+ self.encoder = model.encoder
550
+ self.transformer = transformer
551
+ self.return_ctc_logprobs = return_ctc_logprobs
552
+
553
+ def forward(self, chunk_xs, chunk_lens, offset, att_cache, cnn_cache,
554
+ cache_mask):
555
+ """Streaming Encoder
556
+ Args:
557
+ xs (torch.Tensor): chunk input, with shape (b, time, mel-dim),
558
+ where `time == (chunk_size - 1) * subsample_rate + \
559
+ subsample.right_context + 1`
560
+ offset (torch.Tensor): offset with shape (b, 1)
561
+ 1 is retained for triton deployment
562
+ required_cache_size (int): cache size required for next chunk
563
+ compuation
564
+ > 0: actual cache size
565
+ <= 0: not allowed in streaming gpu encoder `
566
+ att_cache (torch.Tensor): cache tensor for KEY & VALUE in
567
+ transformer/conformer attention, with shape
568
+ (b, elayers, head, cache_t1, d_k * 2), where
569
+ `head * d_k == hidden-dim` and
570
+ `cache_t1 == chunk_size * num_decoding_left_chunks`.
571
+ cnn_cache (torch.Tensor): cache tensor for cnn_module in conformer,
572
+ (b, elayers, b, hidden-dim, cache_t2), where
573
+ `cache_t2 == cnn.lorder - 1`
574
+ cache_mask: (torch.Tensor): cache mask with shape (b, required_cache_size)
575
+ in a batch of request, each request may have different
576
+ history cache. Cache mask is used to indidate the effective
577
+ cache for each request
578
+ Returns:
579
+ torch.Tensor: log probabilities of ctc output and cutoff by beam size
580
+ with shape (b, chunk_size, beam)
581
+ torch.Tensor: index of top beam size probabilities for each timestep
582
+ with shape (b, chunk_size, beam)
583
+ torch.Tensor: output of current input xs,
584
+ with shape (b, chunk_size, hidden-dim).
585
+ torch.Tensor: new attention cache required for next chunk, with
586
+ same shape (b, elayers, head, cache_t1, d_k * 2)
587
+ as the original att_cache
588
+ torch.Tensor: new conformer cnn cache required for next chunk, with
589
+ same shape as the original cnn_cache.
590
+ torch.Tensor: new cache mask, with same shape as the original
591
+ cache mask
592
+ """
593
+ offset = offset.squeeze(1)
594
+ T = chunk_xs.size(1)
595
+ chunk_mask = ~make_pad_mask(chunk_lens, T).unsqueeze(1)
596
+ # B X 1 X T
597
+ chunk_mask = chunk_mask.to(chunk_xs.dtype)
598
+ # transpose batch & num_layers dim
599
+ att_cache = torch.transpose(att_cache, 0, 1)
600
+ cnn_cache = torch.transpose(cnn_cache, 0, 1)
601
+
602
+ # rewrite encoder.forward_chunk
603
+ # <---------forward_chunk START--------->
604
+ xs = self.global_cmvn(chunk_xs)
605
+ # chunk mask is important for batch inferencing since
606
+ # different sequence in a batch has different length
607
+ xs, pos_emb, chunk_mask = self.embed(xs, chunk_mask, offset)
608
+ cache_size = att_cache.size(3) # required cache size
609
+ masks = torch.cat((cache_mask, chunk_mask), dim=2)
610
+ index = offset - cache_size
611
+
612
+ pos_emb = self.embed.position_encoding(index, cache_size + xs.size(1))
613
+ pos_emb = pos_emb.to(dtype=xs.dtype)
614
+
615
+ next_cache_start = -self.required_cache_size
616
+ r_cache_mask = masks[:, :, next_cache_start:]
617
+
618
+ r_att_cache = []
619
+ r_cnn_cache = []
620
+ for i, layer in enumerate(self.encoder.encoders):
621
+ i_kv_cache = att_cache[i]
622
+ size = att_cache.size(-1) // 2
623
+ kv_cache = (i_kv_cache[:, :, :, :size], i_kv_cache[:, :, :, size:])
624
+ xs, _, new_kv_cache, new_cnn_cache = layer(
625
+ xs,
626
+ masks,
627
+ pos_emb,
628
+ att_cache=kv_cache,
629
+ cnn_cache=cnn_cache[i],
630
+ )
631
+ # shape(new_att_cache) is (B, head, attention_key_size, d_k * 2),
632
+ # shape(new_cnn_cache) is (B, hidden-dim, cache_t2)
633
+ new_att_cache = torch.cat(new_kv_cache, dim=-1)
634
+ r_att_cache.append(
635
+ new_att_cache[:, :, next_cache_start:, :].unsqueeze(1))
636
+ if not self.transformer:
637
+ r_cnn_cache.append(new_cnn_cache.unsqueeze(1))
638
+ if self.encoder.normalize_before:
639
+ chunk_out = self.encoder.after_norm(xs)
640
+ else:
641
+ chunk_out = xs
642
+
643
+ r_att_cache = torch.cat(r_att_cache, dim=1) # concat on layers idx
644
+ if not self.transformer:
645
+ r_cnn_cache = torch.cat(r_cnn_cache, dim=1) # concat on layers
646
+
647
+ # <---------forward_chunk END--------->
648
+
649
+ # log_ctc_probs = self.ctc.log_softmax(chunk_out)
650
+ log_ctc_probs = self.ctc.linear(chunk_out)
651
+ log_probs, log_probs_idx = torch.topk(log_ctc_probs,
652
+ self.beam_size,
653
+ dim=2)
654
+ log_probs = log_probs.to(chunk_xs.dtype)
655
+
656
+ r_offset = offset + chunk_out.shape[1]
657
+ # the below ops not supported in Tensorrt
658
+ # chunk_out_lens = torch.div(chunk_lens, subsampling_rate,
659
+ # rounding_mode='floor')
660
+ chunk_out_lens = chunk_lens // self.subsampling_rate
661
+ r_offset = r_offset.unsqueeze(1)
662
+ if self.return_ctc_logprobs:
663
+ return (
664
+ log_ctc_probs,
665
+ chunk_out,
666
+ chunk_out_lens,
667
+ r_offset,
668
+ r_att_cache,
669
+ r_cnn_cache,
670
+ r_cache_mask,
671
+ )
672
+ else:
673
+ return (
674
+ log_probs,
675
+ log_probs_idx,
676
+ chunk_out,
677
+ chunk_out_lens,
678
+ r_offset,
679
+ r_att_cache,
680
+ r_cnn_cache,
681
+ r_cache_mask,
682
+ )
683
+
684
+
685
+ class StreamingSqueezeformerEncoder(torch.nn.Module):
686
+
687
+ def __init__(self, model, required_cache_size, beam_size):
688
+ super().__init__()
689
+ self.ctc = model.ctc
690
+ self.subsampling_rate = model.encoder.embed.subsampling_rate
691
+ self.embed = model.encoder.embed
692
+ self.global_cmvn = model.encoder.global_cmvn
693
+ self.required_cache_size = required_cache_size
694
+ self.beam_size = beam_size
695
+ self.encoder = model.encoder
696
+ self.reduce_idx = model.encoder.reduce_idx
697
+ self.recover_idx = model.encoder.recover_idx
698
+ if self.reduce_idx is None:
699
+ self.time_reduce = None
700
+ else:
701
+ if self.recover_idx is None:
702
+ self.time_reduce = "normal" # no recovery at the end
703
+ else:
704
+ self.time_reduce = "recover" # recovery at the end
705
+ assert len(self.reduce_idx) == len(self.recover_idx)
706
+
707
+ def calculate_downsampling_factor(self, i: int) -> int:
708
+ if self.reduce_idx is None:
709
+ return 1
710
+ else:
711
+ reduce_exp, recover_exp = 0, 0
712
+ for exp, rd_idx in enumerate(self.reduce_idx):
713
+ if i >= rd_idx:
714
+ reduce_exp = exp + 1
715
+ if self.recover_idx is not None:
716
+ for exp, rc_idx in enumerate(self.recover_idx):
717
+ if i >= rc_idx:
718
+ recover_exp = exp + 1
719
+ return int(2**(reduce_exp - recover_exp))
720
+
721
+ def forward(self, chunk_xs, chunk_lens, offset, att_cache, cnn_cache,
722
+ cache_mask):
723
+ """Streaming Encoder
724
+ Args:
725
+ xs (torch.Tensor): chunk input, with shape (b, time, mel-dim),
726
+ where `time == (chunk_size - 1) * subsample_rate + \
727
+ subsample.right_context + 1`
728
+ offset (torch.Tensor): offset with shape (b, 1)
729
+ 1 is retained for triton deployment
730
+ required_cache_size (int): cache size required for next chunk
731
+ compuation
732
+ > 0: actual cache size
733
+ <= 0: not allowed in streaming gpu encoder `
734
+ att_cache (torch.Tensor): cache tensor for KEY & VALUE in
735
+ transformer/conformer attention, with shape
736
+ (b, elayers, head, cache_t1, d_k * 2), where
737
+ `head * d_k == hidden-dim` and
738
+ `cache_t1 == chunk_size * num_decoding_left_chunks`.
739
+ cnn_cache (torch.Tensor): cache tensor for cnn_module in conformer,
740
+ (b, elayers, b, hidden-dim, cache_t2), where
741
+ `cache_t2 == cnn.lorder - 1`
742
+ cache_mask: (torch.Tensor): cache mask with shape (b, required_cache_size)
743
+ in a batch of request, each request may have different
744
+ history cache. Cache mask is used to indidate the effective
745
+ cache for each request
746
+ Returns:
747
+ torch.Tensor: log probabilities of ctc output and cutoff by beam size
748
+ with shape (b, chunk_size, beam)
749
+ torch.Tensor: index of top beam size probabilities for each timestep
750
+ with shape (b, chunk_size, beam)
751
+ torch.Tensor: output of current input xs,
752
+ with shape (b, chunk_size, hidden-dim).
753
+ torch.Tensor: new attention cache required for next chunk, with
754
+ same shape (b, elayers, head, cache_t1, d_k * 2)
755
+ as the original att_cache
756
+ torch.Tensor: new conformer cnn cache required for next chunk, with
757
+ same shape as the original cnn_cache.
758
+ torch.Tensor: new cache mask, with same shape as the original
759
+ cache mask
760
+ """
761
+ offset = offset.squeeze(1)
762
+ T = chunk_xs.size(1)
763
+ chunk_mask = ~make_pad_mask(chunk_lens, T).unsqueeze(1)
764
+ # B X 1 X T
765
+ chunk_mask = chunk_mask.to(chunk_xs.dtype)
766
+ # transpose batch & num_layers dim
767
+ att_cache = torch.transpose(att_cache, 0, 1)
768
+ cnn_cache = torch.transpose(cnn_cache, 0, 1)
769
+
770
+ # rewrite encoder.forward_chunk
771
+ # <---------forward_chunk START--------->
772
+ xs = self.global_cmvn(chunk_xs)
773
+ # chunk mask is important for batch inferencing since
774
+ # different sequence in a batch has different length
775
+ xs, pos_emb, chunk_mask = self.embed(xs, chunk_mask, offset)
776
+ elayers, cache_size = att_cache.size(0), att_cache.size(3)
777
+ att_mask = torch.cat((cache_mask, chunk_mask), dim=2)
778
+ index = offset - cache_size
779
+
780
+ pos_emb = self.embed.position_encoding(index, cache_size + xs.size(1))
781
+ pos_emb = pos_emb.to(dtype=xs.dtype)
782
+
783
+ next_cache_start = -self.required_cache_size
784
+ r_cache_mask = att_mask[:, :, next_cache_start:]
785
+
786
+ r_att_cache = []
787
+ r_cnn_cache = []
788
+ mask_pad = torch.ones(1,
789
+ xs.size(1),
790
+ device=xs.device,
791
+ dtype=torch.bool)
792
+ mask_pad = mask_pad.unsqueeze(1)
793
+ max_att_len: int = 0
794
+ recover_activations: List[Tuple[torch.Tensor, torch.Tensor,
795
+ torch.Tensor, torch.Tensor]] = []
796
+ index = 0
797
+ xs_lens = torch.tensor([xs.size(1)], device=xs.device, dtype=torch.int)
798
+ xs = self.encoder.preln(xs)
799
+ for i, layer in enumerate(self.encoder.encoders):
800
+ if self.reduce_idx is not None:
801
+ if self.time_reduce is not None and i in self.reduce_idx:
802
+ recover_activations.append(
803
+ (xs, att_mask, pos_emb, mask_pad))
804
+ (
805
+ xs,
806
+ xs_lens,
807
+ att_mask,
808
+ mask_pad,
809
+ ) = self.encoder.time_reduction_layer(
810
+ xs, xs_lens, att_mask, mask_pad)
811
+ pos_emb = pos_emb[:, ::2, :]
812
+ if self.encoder.pos_enc_layer_type == "rel_pos_repaired":
813
+ pos_emb = pos_emb[:, :xs.size(1) * 2 - 1, :]
814
+ index += 1
815
+
816
+ if self.recover_idx is not None:
817
+ if self.time_reduce == "recover" and i in self.recover_idx:
818
+ index -= 1
819
+ (
820
+ recover_tensor,
821
+ recover_att_mask,
822
+ recover_pos_emb,
823
+ recover_mask_pad,
824
+ ) = recover_activations[index]
825
+ # recover output length for ctc decode
826
+ xs = xs.unsqueeze(2).repeat(1, 1, 2, 1).flatten(1, 2)
827
+ xs = self.encoder.time_recover_layer(xs)
828
+ recoverd_t = recover_tensor.size(1)
829
+ xs = recover_tensor + xs[:, :recoverd_t, :].contiguous()
830
+ att_mask = recover_att_mask
831
+ pos_emb = recover_pos_emb
832
+ mask_pad = recover_mask_pad
833
+
834
+ factor = self.calculate_downsampling_factor(i)
835
+
836
+ xs, _, new_att_cache, new_cnn_cache = layer(
837
+ xs,
838
+ att_mask,
839
+ pos_emb,
840
+ att_cache=att_cache[i][:, :, ::factor, :]
841
+ [:, :, :pos_emb.size(1) - xs.size(1), :]
842
+ if elayers > 0 else att_cache[:, :, ::factor, :],
843
+ cnn_cache=cnn_cache[i] if cnn_cache.size(0) > 0 else cnn_cache,
844
+ )
845
+ cached_att = new_att_cache[:, :, next_cache_start // factor:, :]
846
+ cached_cnn = new_cnn_cache.unsqueeze(1)
847
+ cached_att = (cached_att.unsqueeze(3).repeat(1, 1, 1, factor,
848
+ 1).flatten(2, 3))
849
+ if i == 0:
850
+ # record length for the first block as max length
851
+ max_att_len = cached_att.size(2)
852
+ r_att_cache.append(cached_att[:, :, :max_att_len, :].unsqueeze(1))
853
+ r_cnn_cache.append(cached_cnn)
854
+
855
+ chunk_out = xs
856
+ r_att_cache = torch.cat(r_att_cache, dim=1) # concat on layers idx
857
+ r_cnn_cache = torch.cat(r_cnn_cache, dim=1) # concat on layers
858
+
859
+ # <---------forward_chunk END--------->
860
+
861
+ # log_ctc_probs = self.ctc.log_softmax(chunk_out)
862
+ log_ctc_probs = self.ctc.linear(chunk_out)
863
+ log_probs, log_probs_idx = torch.topk(log_ctc_probs,
864
+ self.beam_size,
865
+ dim=2)
866
+ log_probs = log_probs.to(chunk_xs.dtype)
867
+
868
+ r_offset = offset + chunk_out.shape[1]
869
+ # the below ops not supported in Tensorrt
870
+ # chunk_out_lens = torch.div(chunk_lens, subsampling_rate,
871
+ # rounding_mode='floor')
872
+ chunk_out_lens = chunk_lens // self.subsampling_rate
873
+ r_offset = r_offset.unsqueeze(1)
874
+
875
+ return (
876
+ log_probs,
877
+ log_probs_idx,
878
+ chunk_out,
879
+ chunk_out_lens,
880
+ r_offset,
881
+ r_att_cache,
882
+ r_cnn_cache,
883
+ r_cache_mask,
884
+ )
885
+
886
+
887
+ class StreamingEfficientConformerEncoder(torch.nn.Module):
888
+
889
+ def __init__(self, model, required_cache_size, beam_size):
890
+ super().__init__()
891
+ self.ctc = model.ctc
892
+ self.subsampling_rate = model.encoder.embed.subsampling_rate
893
+ self.embed = model.encoder.embed
894
+ self.global_cmvn = model.encoder.global_cmvn
895
+ self.required_cache_size = required_cache_size
896
+ self.beam_size = beam_size
897
+ self.encoder = model.encoder
898
+
899
+ # Efficient Conformer
900
+ self.stride_layer_idx = model.encoder.stride_layer_idx
901
+ self.stride = model.encoder.stride
902
+ self.num_blocks = model.encoder.num_blocks
903
+ self.cnn_module_kernel = model.encoder.cnn_module_kernel
904
+
905
+ def calculate_downsampling_factor(self, i: int) -> int:
906
+ factor = 1
907
+ for idx, stride_idx in enumerate(self.stride_layer_idx):
908
+ if i > stride_idx:
909
+ factor *= self.stride[idx]
910
+ return factor
911
+
912
+ def forward(self, chunk_xs, chunk_lens, offset, att_cache, cnn_cache,
913
+ cache_mask):
914
+ """Streaming Encoder
915
+ Args:
916
+ chunk_xs (torch.Tensor): chunk input, with shape (b, time, mel-dim),
917
+ where `time == (chunk_size - 1) * subsample_rate + \
918
+ subsample.right_context + 1`
919
+ chunk_lens (torch.Tensor):
920
+ offset (torch.Tensor): offset with shape (b, 1)
921
+ 1 is retained for triton deployment
922
+ att_cache (torch.Tensor): cache tensor for KEY & VALUE in
923
+ transformer/conformer attention, with shape
924
+ (b, elayers, head, cache_t1, d_k * 2), where
925
+ `head * d_k == hidden-dim` and
926
+ `cache_t1 == chunk_size * num_decoding_left_chunks`.
927
+ cnn_cache (torch.Tensor): cache tensor for cnn_module in conformer,
928
+ (b, elayers, hidden-dim, cache_t2), where
929
+ `cache_t2 == cnn.lorder - 1`
930
+ cache_mask: (torch.Tensor): cache mask with shape (b, required_cache_size)
931
+ in a batch of request, each request may have different
932
+ history cache. Cache mask is used to indidate the effective
933
+ cache for each request
934
+ Returns:
935
+ torch.Tensor: log probabilities of ctc output and cutoff by beam size
936
+ with shape (b, chunk_size, beam)
937
+ torch.Tensor: index of top beam size probabilities for each timestep
938
+ with shape (b, chunk_size, beam)
939
+ torch.Tensor: output of current input xs,
940
+ with shape (b, chunk_size, hidden-dim).
941
+ torch.Tensor: new attention cache required for next chunk, with
942
+ same shape (b, elayers, head, cache_t1, d_k * 2)
943
+ as the original att_cache
944
+ torch.Tensor: new conformer cnn cache required for next chunk, with
945
+ same shape as the original cnn_cache.
946
+ torch.Tensor: new cache mask, with same shape as the original
947
+ cache mask
948
+ """
949
+ offset = offset.squeeze(1) # (b, )
950
+ offset *= self.calculate_downsampling_factor(self.num_blocks + 1)
951
+
952
+ T = chunk_xs.size(1)
953
+ chunk_mask = ~make_pad_mask(chunk_lens, T).unsqueeze(1) # (b, 1, T)
954
+ # B X 1 X T
955
+ chunk_mask = chunk_mask.to(chunk_xs.dtype)
956
+ # transpose batch & num_layers dim
957
+ # Shape(att_cache): (elayers, b, head, cache_t1, d_k * 2)
958
+ # Shape(cnn_cache): (elayers, b, outsize, cnn_kernel)
959
+ att_cache = torch.transpose(att_cache, 0, 1)
960
+ cnn_cache = torch.transpose(cnn_cache, 0, 1)
961
+
962
+ # rewrite encoder.forward_chunk
963
+ # <---------forward_chunk START--------->
964
+ xs = self.global_cmvn(chunk_xs)
965
+ # chunk mask is important for batch inferencing since
966
+ # different sequence in a batch has different length
967
+ xs, pos_emb, chunk_mask = self.embed(xs, chunk_mask, offset)
968
+ cache_size = att_cache.size(3) # required cache size
969
+ masks = torch.cat((cache_mask, chunk_mask), dim=2)
970
+ att_mask = torch.cat((cache_mask, chunk_mask), dim=2)
971
+ index = offset - cache_size
972
+
973
+ pos_emb = self.embed.position_encoding(index, cache_size + xs.size(1))
974
+ pos_emb = pos_emb.to(dtype=xs.dtype)
975
+
976
+ next_cache_start = -self.required_cache_size
977
+ r_cache_mask = masks[:, :, next_cache_start:]
978
+
979
+ r_att_cache = []
980
+ r_cnn_cache = []
981
+ mask_pad = chunk_mask.to(torch.bool)
982
+ max_att_len, max_cnn_len = (
983
+ 0,
984
+ 0,
985
+ ) # for repeat_interleave of new_att_cache
986
+ for i, layer in enumerate(self.encoder.encoders):
987
+ factor = self.calculate_downsampling_factor(i)
988
+ # NOTE(xcsong): Before layer.forward
989
+ # shape(att_cache[i:i + 1]) is (b, head, cache_t1, d_k * 2),
990
+ # shape(cnn_cache[i]) is (b=1, hidden-dim, cache_t2)
991
+ # shape(new_att_cache) = [ batch, head, time2, outdim//head * 2 ]
992
+ att_cache_trunc = 0
993
+ if xs.size(1) + att_cache.size(3) / factor > pos_emb.size(1):
994
+ # The time step is not divisible by the downsampling multiple
995
+ # We propose to double the chunk_size.
996
+ att_cache_trunc = (xs.size(1) + att_cache.size(3) // factor -
997
+ pos_emb.size(1) + 1)
998
+ xs, _, new_att_cache, new_cnn_cache = layer(
999
+ xs,
1000
+ att_mask,
1001
+ pos_emb,
1002
+ mask_pad=mask_pad,
1003
+ att_cache=att_cache[i][:, :, ::factor, :][:, :,
1004
+ att_cache_trunc:, :],
1005
+ cnn_cache=cnn_cache[i, :, :, :]
1006
+ if cnn_cache.size(0) > 0 else cnn_cache,
1007
+ )
1008
+
1009
+ if i in self.stride_layer_idx:
1010
+ # compute time dimension for next block
1011
+ efficient_index = self.stride_layer_idx.index(i)
1012
+ att_mask = att_mask[:, ::self.stride[efficient_index], ::self.
1013
+ stride[efficient_index], ]
1014
+ mask_pad = mask_pad[:, ::self.stride[efficient_index], ::self.
1015
+ stride[efficient_index], ]
1016
+ pos_emb = pos_emb[:, ::self.stride[efficient_index], :]
1017
+
1018
+ # shape(new_att_cache) = [batch, head, time2, outdim]
1019
+ new_att_cache = new_att_cache[:, :, next_cache_start // factor:, :]
1020
+ # shape(new_cnn_cache) = [batch, 1, outdim, cache_t2]
1021
+ new_cnn_cache = new_cnn_cache.unsqueeze(1) # shape(1):layerID
1022
+
1023
+ # use repeat_interleave to new_att_cache
1024
+ # new_att_cache = new_att_cache.repeat_interleave(repeats=factor, dim=2)
1025
+ new_att_cache = (new_att_cache.unsqueeze(3).repeat(
1026
+ 1, 1, 1, factor, 1).flatten(2, 3))
1027
+ # padding new_cnn_cache to cnn.lorder for casual convolution
1028
+ new_cnn_cache = F.pad(
1029
+ new_cnn_cache,
1030
+ (self.cnn_module_kernel - 1 - new_cnn_cache.size(3), 0),
1031
+ )
1032
+
1033
+ if i == 0:
1034
+ # record length for the first block as max length
1035
+ max_att_len = new_att_cache.size(2)
1036
+ max_cnn_len = new_cnn_cache.size(3)
1037
+
1038
+ # update real shape of att_cache and cnn_cache
1039
+ r_att_cache.append(new_att_cache[:, :,
1040
+ -max_att_len:, :].unsqueeze(1))
1041
+ r_cnn_cache.append(new_cnn_cache[:, :, :, -max_cnn_len:])
1042
+
1043
+ if self.encoder.normalize_before:
1044
+ chunk_out = self.encoder.after_norm(xs)
1045
+ else:
1046
+ chunk_out = xs
1047
+
1048
+ # shape of r_att_cache: (b, elayers, head, time2, outdim)
1049
+ r_att_cache = torch.cat(r_att_cache, dim=1) # concat on layers idx
1050
+ # shape of r_cnn_cache: (b, elayers, outdim, cache_t2)
1051
+ r_cnn_cache = torch.cat(r_cnn_cache, dim=1) # concat on layers
1052
+
1053
+ # <---------forward_chunk END--------->
1054
+
1055
+ # log_ctc_probs = self.ctc.log_softmax(chunk_out)
1056
+ log_ctc_probs = self.ctc.linear(chunk_out)
1057
+ log_probs, log_probs_idx = torch.topk(log_ctc_probs,
1058
+ self.beam_size,
1059
+ dim=2)
1060
+ log_probs = log_probs.to(chunk_xs.dtype)
1061
+
1062
+ r_offset = offset + chunk_out.shape[1]
1063
+ # the below ops not supported in Tensorrt
1064
+ # chunk_out_lens = torch.div(chunk_lens, subsampling_rate,
1065
+ # rounding_mode='floor')
1066
+ chunk_out_lens = (
1067
+ chunk_lens // self.subsampling_rate //
1068
+ self.calculate_downsampling_factor(self.num_blocks + 1))
1069
+ chunk_out_lens += 1
1070
+ r_offset = r_offset.unsqueeze(1)
1071
+
1072
+ return (
1073
+ log_probs,
1074
+ log_probs_idx,
1075
+ chunk_out,
1076
+ chunk_out_lens,
1077
+ r_offset,
1078
+ r_att_cache,
1079
+ r_cnn_cache,
1080
+ r_cache_mask,
1081
+ )
1082
+
1083
+
1084
+ class Decoder(torch.nn.Module):
1085
+
1086
+ def __init__(
1087
+ self,
1088
+ decoder: TransformerDecoder,
1089
+ ctc_weight: float = 0.5,
1090
+ reverse_weight: float = 0.0,
1091
+ beam_size: int = 10,
1092
+ decoder_fastertransformer: bool = False,
1093
+ ):
1094
+ super().__init__()
1095
+ self.decoder = decoder
1096
+ self.ctc_weight = ctc_weight
1097
+ self.reverse_weight = reverse_weight
1098
+ self.beam_size = beam_size
1099
+ self.decoder_fastertransformer = decoder_fastertransformer
1100
+
1101
+ def forward(
1102
+ self,
1103
+ encoder_out: torch.Tensor,
1104
+ encoder_lens: torch.Tensor,
1105
+ hyps_pad_sos_eos: torch.Tensor,
1106
+ hyps_lens_sos: torch.Tensor,
1107
+ r_hyps_pad_sos_eos: torch.Tensor,
1108
+ ctc_score: torch.Tensor,
1109
+ ):
1110
+ """Encoder
1111
+ Args:
1112
+ encoder_out: B x T x F
1113
+ encoder_lens: B
1114
+ hyps_pad_sos_eos: B x beam x (T2+1),
1115
+ hyps with sos & eos and padded by ignore id
1116
+ hyps_lens_sos: B x beam, length for each hyp with sos
1117
+ r_hyps_pad_sos_eos: B x beam x (T2+1),
1118
+ reversed hyps with sos & eos and padded by ignore id
1119
+ ctc_score: B x beam, ctc score for each hyp
1120
+ Returns:
1121
+ decoder_out: B x beam x T2 x V
1122
+ r_decoder_out: B x beam x T2 x V
1123
+ best_index: B
1124
+ """
1125
+ B, T, F = encoder_out.shape
1126
+ bz = self.beam_size
1127
+ B2 = B * bz
1128
+ encoder_out = encoder_out.repeat(1, bz, 1).view(B2, T, F)
1129
+ encoder_mask = ~make_pad_mask(encoder_lens, T).unsqueeze(1)
1130
+ encoder_mask = encoder_mask.repeat(1, bz, 1).view(B2, 1, T)
1131
+ T2 = hyps_pad_sos_eos.shape[2] - 1
1132
+ hyps_pad = hyps_pad_sos_eos.view(B2, T2 + 1)
1133
+ hyps_lens = hyps_lens_sos.view(B2, )
1134
+ hyps_pad_sos = hyps_pad[:, :-1].contiguous()
1135
+ hyps_pad_eos = hyps_pad[:, 1:].contiguous()
1136
+
1137
+ r_hyps_pad = r_hyps_pad_sos_eos.view(B2, T2 + 1)
1138
+ r_hyps_pad_sos = r_hyps_pad[:, :-1].contiguous()
1139
+ r_hyps_pad_eos = r_hyps_pad[:, 1:].contiguous()
1140
+
1141
+ decoder_out, r_decoder_out, _ = self.decoder(
1142
+ encoder_out,
1143
+ encoder_mask,
1144
+ hyps_pad_sos,
1145
+ hyps_lens,
1146
+ r_hyps_pad_sos,
1147
+ self.reverse_weight,
1148
+ )
1149
+ # decoder_out = torch.nn.functional.log_softmax(decoder_out, dim=-1)
1150
+ V = decoder_out.shape[-1]
1151
+ decoder_out = decoder_out.view(B2, T2, V)
1152
+ mask = ~make_pad_mask(hyps_lens, T2) # B2 x T2
1153
+ # mask index, remove ignore id
1154
+ index = torch.unsqueeze(hyps_pad_eos * mask, 2).to(torch.long)
1155
+ score = decoder_out.gather(2, index).squeeze(2) # B2 X T2
1156
+ # mask padded part
1157
+ score = score * mask
1158
+ decoder_out = decoder_out.view(B, bz, T2, V)
1159
+ if self.reverse_weight > 0:
1160
+ # r_decoder_out = torch.nn.functional.log_softmax(r_decoder_out,
1161
+ # dim=-1)
1162
+ r_decoder_out = r_decoder_out.view(B2, T2, V)
1163
+ index = torch.unsqueeze(r_hyps_pad_eos * mask, 2).to(torch.long)
1164
+ r_score = r_decoder_out.gather(2, index).squeeze(2)
1165
+ r_score = r_score * mask
1166
+ score = (score * (1 - self.reverse_weight) +
1167
+ self.reverse_weight * r_score)
1168
+ r_decoder_out = r_decoder_out.view(B, bz, T2, V)
1169
+ score = torch.sum(score, axis=1) # B2
1170
+ score = torch.reshape(score, (B, bz)) + self.ctc_weight * ctc_score
1171
+ best_index = torch.argmax(score, dim=1)
1172
+ if self.decoder_fastertransformer:
1173
+ return decoder_out, best_index
1174
+ else:
1175
+ return best_index
1176
+
1177
+
1178
+ def to_numpy(tensors):
1179
+ out = []
1180
+ if type(tensors) == torch.tensor:
1181
+ tensors = [tensors]
1182
+ for tensor in tensors:
1183
+ if tensor.requires_grad:
1184
+ tensor = tensor.detach().cpu().numpy()
1185
+ else:
1186
+ tensor = tensor.cpu().numpy()
1187
+ out.append(tensor)
1188
+ return out
1189
+
1190
+
1191
+ def test(xlist, blist, rtol=1e-3, atol=1e-5, tolerate_small_mismatch=True):
1192
+ for a, b in zip(xlist, blist):
1193
+ try:
1194
+ torch.testing.assert_allclose(a, b, rtol=rtol, atol=atol)
1195
+ except AssertionError as error:
1196
+ if tolerate_small_mismatch:
1197
+ print(error)
1198
+ else:
1199
+ raise
1200
+
1201
+
1202
+ def export_offline_encoder(model, configs, args, logger, encoder_onnx_path):
1203
+ bz = 1
1204
+ seq_len = 1024
1205
+ beam_size = args.beam_size
1206
+ feature_size = configs["input_dim"]
1207
+
1208
+ speech = torch.randn(bz, seq_len, feature_size, dtype=torch.float32)
1209
+ speech_lens = torch.randint(low=10,
1210
+ high=seq_len,
1211
+ size=(bz, ),
1212
+ dtype=torch.int32)
1213
+ encoder = Encoder(model.encoder, model.ctc, beam_size)
1214
+ encoder.eval()
1215
+
1216
+ torch.onnx.export(
1217
+ encoder,
1218
+ (speech, speech_lens),
1219
+ encoder_onnx_path,
1220
+ export_params=True,
1221
+ opset_version=13,
1222
+ do_constant_folding=True,
1223
+ input_names=["speech", "speech_lengths"],
1224
+ output_names=[
1225
+ "encoder_out",
1226
+ "encoder_out_lens",
1227
+ "ctc_log_probs",
1228
+ "beam_log_probs",
1229
+ "beam_log_probs_idx",
1230
+ ],
1231
+ dynamic_axes=None,
1232
+ # dynamic_axes={
1233
+ # "speech": {
1234
+ # 0: "B",
1235
+ # 1: "T"
1236
+ # },
1237
+ # "speech_lengths": {
1238
+ # 0: "B"
1239
+ # },
1240
+ # "encoder_out": {
1241
+ # 0: "B",
1242
+ # 1: "T_OUT"
1243
+ # },
1244
+ # "encoder_out_lens": {
1245
+ # 0: "B"
1246
+ # },
1247
+ # "ctc_log_probs": {
1248
+ # 0: "B",
1249
+ # 1: "T_OUT"
1250
+ # },
1251
+ # "beam_log_probs": {
1252
+ # 0: "B",
1253
+ # 1: "T_OUT"
1254
+ # },
1255
+ # "beam_log_probs_idx": {
1256
+ # 0: "B",
1257
+ # 1: "T_OUT"
1258
+ # },
1259
+ # },
1260
+ verbose=False,
1261
+ dynamo=False,
1262
+ )
1263
+ fold_static_pulsar2_subgraphs(encoder_onnx_path)
1264
+ simplify_pulsar2_onnx(encoder_onnx_path)
1265
+ rewrite_pulsar2_bool_not(encoder_onnx_path)
1266
+
1267
+ with torch.no_grad():
1268
+ o0, o1, o2, o3, o4 = encoder(speech, speech_lens)
1269
+
1270
+ providers = ["CPUExecutionProvider"]
1271
+ ort_session = onnxruntime.InferenceSession(encoder_onnx_path,
1272
+ providers=providers)
1273
+ ort_inputs = {
1274
+ "speech": to_numpy(speech),
1275
+ "speech_lengths": to_numpy(speech_lens),
1276
+ }
1277
+ ort_outs = ort_session.run(None, ort_inputs)
1278
+
1279
+ # check encoder output
1280
+ test(to_numpy([o0, o1, o2, o3, o4]), ort_outs)
1281
+ logger.info("export offline onnx encoder succeed!")
1282
+ onnx_config = {
1283
+ "beam_size": args.beam_size,
1284
+ "reverse_weight": configs["model_conf"]["reverse_weight"],
1285
+ "ctc_weight": configs["model_conf"]["ctc_weight"],
1286
+ }
1287
+ return onnx_config
1288
+
1289
+
1290
+ def export_online_encoder(model, configs, args, logger, encoder_onnx_path):
1291
+ decoding_chunk_size = args.decoding_chunk_size
1292
+ subsampling = model.encoder.embed.subsampling_rate
1293
+ context = model.encoder.embed.right_context + 1
1294
+ decoding_window = (decoding_chunk_size - 1) * subsampling + context
1295
+ batch_size = 1
1296
+ audio_len = decoding_window
1297
+ feature_size = configs["input_dim"]
1298
+ output_size = configs["encoder_conf"]["output_size"]
1299
+ num_layers = configs["encoder_conf"]["num_blocks"]
1300
+ # in transformer the cnn module will not be available
1301
+ transformer = False
1302
+ cnn_module_kernel = configs["encoder_conf"].get("cnn_module_kernel", 1) - 1
1303
+ if not cnn_module_kernel:
1304
+ transformer = True
1305
+ num_decoding_left_chunks = args.num_decoding_left_chunks
1306
+ required_cache_size = decoding_chunk_size * num_decoding_left_chunks
1307
+ if configs["encoder"] == "squeezeformer":
1308
+ encoder = StreamingSqueezeformerEncoder(model, required_cache_size,
1309
+ args.beam_size)
1310
+ elif configs["encoder"] == "efficientConformer":
1311
+ encoder = StreamingEfficientConformerEncoder(model,
1312
+ required_cache_size,
1313
+ args.beam_size)
1314
+ else:
1315
+ encoder = StreamingEncoder(
1316
+ model,
1317
+ required_cache_size,
1318
+ args.beam_size,
1319
+ transformer,
1320
+ args.return_ctc_logprobs,
1321
+ )
1322
+ encoder.eval()
1323
+
1324
+ # begin to export encoder
1325
+ chunk_xs = torch.randn(batch_size,
1326
+ audio_len,
1327
+ feature_size,
1328
+ dtype=torch.float32)
1329
+ chunk_lens = torch.ones(batch_size, dtype=torch.int32) * audio_len
1330
+
1331
+ offset = torch.arange(0, batch_size, dtype=torch.int32).unsqueeze(1)
1332
+ # (elayers, b, head, cache_t1, d_k * 2)
1333
+ head = configs["encoder_conf"]["attention_heads"]
1334
+ d_k = configs["encoder_conf"]["output_size"] // head
1335
+ att_cache = torch.randn(
1336
+ batch_size,
1337
+ num_layers,
1338
+ head,
1339
+ required_cache_size,
1340
+ d_k * 2,
1341
+ dtype=torch.float32,
1342
+ )
1343
+ cnn_cache = torch.randn(
1344
+ batch_size,
1345
+ num_layers,
1346
+ output_size,
1347
+ cnn_module_kernel,
1348
+ dtype=torch.float32,
1349
+ )
1350
+
1351
+ cache_mask = torch.ones(batch_size,
1352
+ 1,
1353
+ required_cache_size,
1354
+ dtype=torch.float32)
1355
+ input_names = [
1356
+ "chunk_xs",
1357
+ "chunk_lens",
1358
+ "offset",
1359
+ "att_cache",
1360
+ "cnn_cache",
1361
+ "cache_mask",
1362
+ ]
1363
+ output_names = [
1364
+ "log_probs",
1365
+ "log_probs_idx",
1366
+ "chunk_out",
1367
+ "chunk_out_lens",
1368
+ "r_offset",
1369
+ "r_att_cache",
1370
+ "r_cnn_cache",
1371
+ "r_cache_mask",
1372
+ ]
1373
+ if args.return_ctc_logprobs:
1374
+ output_names = [
1375
+ "ctc_log_probs",
1376
+ "chunk_out",
1377
+ "chunk_out_lens",
1378
+ "r_offset",
1379
+ "r_att_cache",
1380
+ "r_cnn_cache",
1381
+ "r_cache_mask",
1382
+ ]
1383
+ input_tensors = (
1384
+ chunk_xs,
1385
+ chunk_lens,
1386
+ offset,
1387
+ att_cache,
1388
+ cnn_cache,
1389
+ cache_mask,
1390
+ )
1391
+ if transformer:
1392
+ assert (args.return_ctc_logprobs is
1393
+ False), "return_ctc_logprobs is not supported in transformer"
1394
+ output_names.pop(6)
1395
+
1396
+ all_names = input_names + output_names
1397
+ dynamic_axes = {}
1398
+ for name in all_names:
1399
+ # only the first dimension is dynamic
1400
+ # all other dimension is fixed
1401
+ dynamic_axes[name] = {0: "B"}
1402
+
1403
+ torch.onnx.export(
1404
+ encoder,
1405
+ input_tensors,
1406
+ encoder_onnx_path,
1407
+ export_params=True,
1408
+ opset_version=14,
1409
+ do_constant_folding=True,
1410
+ input_names=input_names,
1411
+ output_names=output_names,
1412
+ # dynamic_axes=dynamic_axes,
1413
+ dynamic_axes=None,
1414
+ verbose=False,
1415
+ dynamo=False,
1416
+ )
1417
+ fold_static_pulsar2_subgraphs(encoder_onnx_path)
1418
+ simplify_pulsar2_onnx(encoder_onnx_path)
1419
+ rewrite_pulsar2_bool_not(encoder_onnx_path)
1420
+
1421
+ with torch.no_grad():
1422
+ torch_outs = encoder(chunk_xs, chunk_lens, offset, att_cache,
1423
+ cnn_cache, cache_mask)
1424
+ if transformer:
1425
+ torch_outs = list(torch_outs).pop(6)
1426
+ ort_session = onnxruntime.InferenceSession(
1427
+ encoder_onnx_path, providers=["CPUExecutionProvider"])
1428
+ ort_inputs = {}
1429
+
1430
+ input_tensors = to_numpy(input_tensors)
1431
+ for idx, name in enumerate(input_names):
1432
+ ort_inputs[name] = input_tensors[idx]
1433
+ if transformer:
1434
+ del ort_inputs["cnn_cache"]
1435
+ ort_outs = ort_session.run(None, ort_inputs)
1436
+ test(to_numpy(torch_outs), ort_outs, rtol=1e-03, atol=1e-05)
1437
+ logger.info("export to onnx streaming encoder succeed!")
1438
+ onnx_config = {
1439
+ "subsampling_rate": subsampling,
1440
+ "context": context,
1441
+ "decoding_chunk_size": decoding_chunk_size,
1442
+ "num_decoding_left_chunks": num_decoding_left_chunks,
1443
+ "beam_size": args.beam_size,
1444
+ "feat_size": feature_size,
1445
+ "decoding_window": decoding_window,
1446
+ "cnn_module_kernel_cache": cnn_module_kernel,
1447
+ "return_ctc_logprobs": args.return_ctc_logprobs,
1448
+ }
1449
+ return onnx_config
1450
+
1451
+
1452
+ def export_rescoring_decoder(model, configs, args, logger, decoder_onnx_path,
1453
+ decoder_fastertransformer):
1454
+ bz, seq_len = 1, 32
1455
+ beam_size = args.beam_size
1456
+ decoder = Decoder(
1457
+ model.decoder,
1458
+ model.ctc_weight,
1459
+ model.reverse_weight,
1460
+ beam_size,
1461
+ decoder_fastertransformer,
1462
+ )
1463
+ decoder.eval()
1464
+
1465
+ hyps_pad_sos_eos = torch.randint(low=3,
1466
+ high=1000,
1467
+ size=(bz, beam_size, seq_len),
1468
+ dtype=torch.int32)
1469
+ hyps_lens_sos = torch.randint(low=3,
1470
+ high=seq_len,
1471
+ size=(bz, beam_size),
1472
+ dtype=torch.int32)
1473
+ r_hyps_pad_sos_eos = torch.randint(low=3,
1474
+ high=1000,
1475
+ size=(bz, beam_size, seq_len),
1476
+ dtype=torch.int32)
1477
+
1478
+ output_size = configs["encoder_conf"]["output_size"]
1479
+ encoder_out = torch.randn(bz, seq_len, output_size, dtype=torch.float32)
1480
+ encoder_out_lens = torch.randint(low=3,
1481
+ high=seq_len,
1482
+ size=(bz, ),
1483
+ dtype=torch.int32)
1484
+ ctc_score = torch.randn(bz, beam_size, dtype=torch.float32)
1485
+
1486
+ input_names = [
1487
+ "encoder_out",
1488
+ "encoder_out_lens",
1489
+ "hyps_pad_sos_eos",
1490
+ "hyps_lens_sos",
1491
+ "r_hyps_pad_sos_eos",
1492
+ "ctc_score",
1493
+ ]
1494
+ output_names = ["best_index"]
1495
+ if decoder_fastertransformer:
1496
+ output_names.insert(0, "decoder_out")
1497
+
1498
+ torch.onnx.export(
1499
+ decoder,
1500
+ (
1501
+ encoder_out,
1502
+ encoder_out_lens,
1503
+ hyps_pad_sos_eos,
1504
+ hyps_lens_sos,
1505
+ r_hyps_pad_sos_eos,
1506
+ ctc_score,
1507
+ ),
1508
+ decoder_onnx_path,
1509
+ export_params=True,
1510
+ opset_version=13,
1511
+ do_constant_folding=True,
1512
+ input_names=input_names,
1513
+ output_names=output_names,
1514
+ dynamic_axes=None,
1515
+ # dynamic_axes={
1516
+ # "encoder_out": {
1517
+ # 0: "B",
1518
+ # 1: "T"
1519
+ # },
1520
+ # "encoder_out_lens": {
1521
+ # 0: "B"
1522
+ # },
1523
+ # "hyps_pad_sos_eos": {
1524
+ # 0: "B",
1525
+ # 2: "T2"
1526
+ # },
1527
+ # "hyps_lens_sos": {
1528
+ # 0: "B"
1529
+ # },
1530
+ # "r_hyps_pad_sos_eos": {
1531
+ # 0: "B",
1532
+ # 2: "T2"
1533
+ # },
1534
+ # "ctc_score": {
1535
+ # 0: "B"
1536
+ # },
1537
+ # "best_index": {
1538
+ # 0: "B"
1539
+ # },
1540
+ # },
1541
+ verbose=False,
1542
+ dynamo=False,
1543
+ )
1544
+ fold_static_pulsar2_subgraphs(decoder_onnx_path)
1545
+ simplify_pulsar2_onnx(decoder_onnx_path)
1546
+ rewrite_pulsar2_bool_not(decoder_onnx_path)
1547
+ rewrite_pulsar2_bool_and(decoder_onnx_path)
1548
+ with torch.no_grad():
1549
+ o0 = decoder(
1550
+ encoder_out,
1551
+ encoder_out_lens,
1552
+ hyps_pad_sos_eos,
1553
+ hyps_lens_sos,
1554
+ r_hyps_pad_sos_eos,
1555
+ ctc_score,
1556
+ )
1557
+ providers = ["CPUExecutionProvider"]
1558
+ ort_session = onnxruntime.InferenceSession(decoder_onnx_path,
1559
+ providers=providers)
1560
+
1561
+ input_tensors = [
1562
+ encoder_out,
1563
+ encoder_out_lens,
1564
+ hyps_pad_sos_eos,
1565
+ hyps_lens_sos,
1566
+ r_hyps_pad_sos_eos,
1567
+ ctc_score,
1568
+ ]
1569
+ ort_inputs = {}
1570
+ input_tensors = to_numpy(input_tensors)
1571
+ for idx, name in enumerate(input_names):
1572
+ ort_inputs[name] = input_tensors[idx]
1573
+
1574
+ # if model.reverse weight == 0,
1575
+ # the r_hyps_pad will be removed
1576
+ # from the onnx decoder since it doen't play any role
1577
+ if model.reverse_weight == 0:
1578
+ del ort_inputs["r_hyps_pad_sos_eos"]
1579
+ ort_outs = ort_session.run(None, ort_inputs)
1580
+
1581
+ # check decoder output
1582
+ if decoder_fastertransformer:
1583
+ test(to_numpy(o0), ort_outs, rtol=1e-03, atol=1e-05)
1584
+ else:
1585
+ test(to_numpy([o0]), ort_outs, rtol=1e-03, atol=1e-05)
1586
+ logger.info("export to onnx decoder succeed!")
1587
+
1588
+
1589
+ if __name__ == "__main__":
1590
+ parser = argparse.ArgumentParser(description="export x86_gpu model")
1591
+ parser.add_argument(
1592
+ "--pretrained_model_dir",
1593
+ default=DEFAULT_PRETRAINED_MODEL_DIR,
1594
+ help=("pretrained model directory containing train.yaml, final.pt, "
1595
+ "and global_cmvn"),
1596
+ )
1597
+ parser.add_argument(
1598
+ "--pretrained_model_url",
1599
+ default=DEFAULT_PRETRAINED_MODEL_URL,
1600
+ help="pretrained model tar.gz URL used when pretrained_model_dir is missing",
1601
+ )
1602
+ parser.add_argument(
1603
+ "--reverse_weight",
1604
+ default=-1.0,
1605
+ type=float,
1606
+ required=False,
1607
+ help="reverse weight for bitransformer," +
1608
+ "default value is in config file",
1609
+ )
1610
+ parser.add_argument(
1611
+ "--ctc_weight",
1612
+ default=-1.0,
1613
+ type=float,
1614
+ required=False,
1615
+ help="ctc weight, default value is in config file",
1616
+ )
1617
+ parser.add_argument(
1618
+ "--beam_size",
1619
+ default=10,
1620
+ type=int,
1621
+ required=False,
1622
+ help="beam size would be ctc output size",
1623
+ )
1624
+ parser.add_argument(
1625
+ "--output_onnx_dir",
1626
+ default="onnx_model",
1627
+ help="output onnx encoder and decoder directory",
1628
+ )
1629
+ # arguments for streaming encoder
1630
+ # parser.add_argument(
1631
+ # "--streaming",
1632
+ # action="store_true",
1633
+ # help="whether to export streaming encoder, default false",
1634
+ # )
1635
+ parser.add_argument(
1636
+ "--decoding_chunk_size",
1637
+ default=16,
1638
+ type=int,
1639
+ required=False,
1640
+ help="the decoding chunk size, <=0 is not supported",
1641
+ )
1642
+ parser.add_argument(
1643
+ "--num_decoding_left_chunks",
1644
+ default=5,
1645
+ type=int,
1646
+ required=False,
1647
+ help="number of left chunks, <= 0 is not supported",
1648
+ )
1649
+ parser.add_argument(
1650
+ "--decoder_fastertransformer",
1651
+ action="store_true",
1652
+ help="return decoder_out and best_index for ft",
1653
+ )
1654
+ parser.add_argument(
1655
+ "--return_ctc_logprobs",
1656
+ action="store_true",
1657
+ help="return full ctc_log_probs for TLG streaming encoder",
1658
+ )
1659
+ args = parser.parse_args()
1660
+ prepare_pretrained_model(args)
1661
+
1662
+ torch.manual_seed(0)
1663
+ torch.set_printoptions(precision=10)
1664
+
1665
+ with open(args.config, "r") as fin:
1666
+ configs = yaml.load(fin, Loader=yaml.FullLoader)
1667
+ if os.path.exists(args.cmvn_file):
1668
+ if 'cmvn' not in configs:
1669
+ configs['cmvn'] = "global_cmvn"
1670
+ configs['cmvn_conf'] = {}
1671
+ else:
1672
+ assert configs['cmvn'] == "global_cmvn"
1673
+ assert configs['cmvn_conf'] is not None
1674
+ configs['cmvn_conf']["cmvn_file"] = args.cmvn_file
1675
+ configs['cmvn_conf'].setdefault(
1676
+ "is_json_cmvn", configs.get("is_json_cmvn", True))
1677
+ elif configs.get('cmvn', None) == 'global_cmvn':
1678
+ raise FileNotFoundError(
1679
+ f"Expected global_cmvn in pretrained model dir: {args.cmvn_file}")
1680
+ if (args.reverse_weight != -1.0
1681
+ and "reverse_weight" in configs["model_conf"]):
1682
+ configs["model_conf"]["reverse_weight"] = args.reverse_weight
1683
+ print("Update reverse weight to", args.reverse_weight)
1684
+ if args.ctc_weight != -1:
1685
+ print("Update ctc weight to ", args.ctc_weight)
1686
+ configs["model_conf"]["ctc_weight"] = args.ctc_weight
1687
+ configs["encoder_conf"]["use_dynamic_chunk"] = False
1688
+
1689
+ model, configs = init_model(args, configs)
1690
+ model.eval()
1691
+
1692
+ if not os.path.exists(args.output_onnx_dir):
1693
+ os.mkdir(args.output_onnx_dir)
1694
+
1695
+ export_enc_func = None
1696
+ # if args.streaming:
1697
+ assert args.decoding_chunk_size > 0
1698
+ assert args.num_decoding_left_chunks > 0
1699
+ export_enc_func = export_online_encoder
1700
+ encoder_onnx_path = os.path.join(args.output_onnx_dir, "encoder_online.onnx")
1701
+ onnx_config = export_enc_func(model, configs, args, logger,
1702
+ encoder_onnx_path)
1703
+
1704
+ # else
1705
+ export_enc_func = export_offline_encoder
1706
+ encoder_onnx_path = os.path.join(args.output_onnx_dir, "encoder_offline.onnx")
1707
+ onnx_config = export_enc_func(model, configs, args, logger,
1708
+ encoder_onnx_path)
1709
+
1710
+
1711
+ decoder_onnx_path = os.path.join(args.output_onnx_dir, "decoder.onnx")
1712
+ export_rescoring_decoder(
1713
+ model,
1714
+ configs,
1715
+ args,
1716
+ logger,
1717
+ decoder_onnx_path,
1718
+ args.decoder_fastertransformer,
1719
+ )
1720
+
1721
+ config_dir = os.path.join(args.output_onnx_dir, "config.yaml")
1722
+ with open(config_dir, "w") as out:
1723
+ yaml.dump(onnx_config, out)
generate_data.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+ import shutil
4
+
5
+ from tqdm import tqdm
6
+
7
+ from ort_common import WenetONNXRunner, pack_calibration_dataset
8
+
9
+
10
+ def get_args():
11
+ parser = argparse.ArgumentParser(
12
+ description="Generate calibration_dataset for exported ONNX models")
13
+ parser.add_argument("--input",
14
+ "-i",
15
+ nargs="+",
16
+ required=True,
17
+ help="Input wav file(s) or directory/directories")
18
+ parser.add_argument("--config",
19
+ required=True,
20
+ help="yaml file in checkpoint path")
21
+ parser.add_argument(
22
+ "--vocab",
23
+ required=True,
24
+ help="pretrained units.txt, for example pretrained/<model>/units.txt",
25
+ )
26
+ parser.add_argument("--onnx_dir",
27
+ default="onnx_model",
28
+ help="directory containing exported ONNX models")
29
+ parser.add_argument("--calib_data_path",
30
+ default="calibration_dataset",
31
+ help="output calibration dataset directory")
32
+ parser.add_argument("--parts",
33
+ nargs="+",
34
+ choices=["all", "offline", "online", "decoder"],
35
+ default=["all"],
36
+ help="which model inputs to generate")
37
+ parser.add_argument("--offline_seq_len", type=int, default=1024)
38
+ parser.add_argument("--decoder_len", type=int, default=32)
39
+ parser.add_argument("--decoding_chunk_size", type=int, default=16)
40
+ parser.add_argument("--num_decoding_left_chunks", type=int, default=5)
41
+ parser.add_argument("--max_num",
42
+ type=int,
43
+ default=100,
44
+ help="maximum number of audio files used for calibration; set <= 0 to use all")
45
+ parser.add_argument("--keep_existing",
46
+ action="store_true",
47
+ help="append to an existing calibration directory")
48
+ return parser.parse_args()
49
+
50
+
51
+ def expand_audio_inputs(inputs):
52
+ audio_exts = {".wav", ".flac", ".mp3", ".m4a", ".ogg"}
53
+ audio_files = []
54
+ for path in inputs:
55
+ if os.path.isdir(path):
56
+ for root, _, files in os.walk(path):
57
+ for filename in files:
58
+ if os.path.splitext(filename)[1].lower() in audio_exts:
59
+ audio_files.append(os.path.join(root, filename))
60
+ else:
61
+ audio_files.append(path)
62
+ audio_files = sorted(audio_files)
63
+ if not audio_files:
64
+ raise FileNotFoundError("No audio files found")
65
+ return audio_files
66
+
67
+
68
+ def normalize_parts(parts):
69
+ if "all" in parts:
70
+ return {"offline", "online", "decoder"}
71
+ return set(parts)
72
+
73
+
74
+ def limit_audio_files(audio_files, max_num):
75
+ if max_num is None or max_num <= 0:
76
+ return audio_files
77
+ return audio_files[:max_num]
78
+
79
+
80
+ def main():
81
+ args = get_args()
82
+ parts = normalize_parts(args.parts)
83
+ audio_files = limit_audio_files(expand_audio_inputs(args.input),
84
+ args.max_num)
85
+
86
+ if os.path.exists(args.calib_data_path) and not args.keep_existing:
87
+ shutil.rmtree(args.calib_data_path)
88
+ os.makedirs(args.calib_data_path, exist_ok=True)
89
+
90
+ runner = WenetONNXRunner(
91
+ args.config,
92
+ args.vocab,
93
+ onnx_dir=args.onnx_dir,
94
+ offline_seq_len=args.offline_seq_len,
95
+ decoder_len=args.decoder_len,
96
+ decoding_chunk_size=args.decoding_chunk_size,
97
+ num_decoding_left_chunks=args.num_decoding_left_chunks,
98
+ )
99
+
100
+ counts = {"offline": 0, "online": 0, "decoder": 0}
101
+ progress = tqdm(audio_files,
102
+ desc="Generating calibration data",
103
+ unit="wav")
104
+ for audio_idx, audio_file in enumerate(progress):
105
+ sample_counts = runner.save_calibration_for_audio(
106
+ audio_file, parts, args.calib_data_path, audio_idx)
107
+ for key, value in sample_counts.items():
108
+ counts[key] += value
109
+ progress.set_postfix(offline=counts["offline"],
110
+ online=counts["online"],
111
+ decoder=counts["decoder"])
112
+
113
+ print("Packing calibration dataset...")
114
+ pack_calibration_dataset(args.calib_data_path)
115
+ print(f"Generated calibration data in {args.calib_data_path}")
116
+ print(f"offline samples: {counts['offline']}")
117
+ print(f"online samples: {counts['online']}")
118
+ print(f"decoder samples: {counts['decoder']}")
119
+
120
+
121
+ if __name__ == "__main__":
122
+ main()
ort_common.py ADDED
@@ -0,0 +1,567 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import multiprocessing
2
+ import os
3
+ import tarfile as tf
4
+
5
+ import numpy as np
6
+ import torch
7
+ import torchaudio
8
+ import torchaudio.compliance.kaldi as kaldi
9
+ import yaml
10
+
11
+ try:
12
+ from swig_decoders import (map_batch, ctc_beam_search_decoder_batch,
13
+ TrieVector, PathTrie)
14
+ except ModuleNotFoundError:
15
+ class PathTrie:
16
+ pass
17
+
18
+ class TrieVector(list):
19
+ pass
20
+
21
+ def _log_add(*values):
22
+ values = [value for value in values if value != -float("inf")]
23
+ if not values:
24
+ return -float("inf")
25
+ max_value = max(values)
26
+ return max_value + np.log(sum(np.exp(value - max_value)
27
+ for value in values))
28
+
29
+ def _map_sentence(sent, vocabulary, greedy=False, blank_id=0):
30
+ mapped = []
31
+ prev = None
32
+ for token in sent:
33
+ token = int(token)
34
+ if greedy and token == prev:
35
+ prev = token
36
+ continue
37
+ prev = token
38
+ if token == blank_id or token < 0 or token >= len(vocabulary):
39
+ continue
40
+ piece = vocabulary[token]
41
+ if piece.startswith("<") and piece.endswith(">"):
42
+ continue
43
+ mapped.append(piece)
44
+ return "".join(mapped)
45
+
46
+ def map_batch(batch_sents, vocabulary, num_processes, greedy=False,
47
+ blank_id=0):
48
+ del num_processes
49
+ return [_map_sentence(sent, vocabulary, greedy, blank_id)
50
+ for sent in batch_sents]
51
+
52
+ def _ctc_prefix_beam_search(log_probs_seq, log_probs_idx, beam_size,
53
+ blank_id):
54
+ beam = {(): (0.0, -float("inf"))}
55
+ for frame_probs, frame_ids in zip(log_probs_seq, log_probs_idx):
56
+ next_beam = {}
57
+ for prefix, (prob_blank, prob_non_blank) in beam.items():
58
+ for prob, token in zip(frame_probs, frame_ids):
59
+ token = int(token)
60
+ prob = float(prob)
61
+ next_prob_blank, next_prob_non_blank = next_beam.get(
62
+ prefix, (-float("inf"), -float("inf")))
63
+ if token == blank_id:
64
+ next_beam[prefix] = (
65
+ _log_add(next_prob_blank, prob_blank + prob,
66
+ prob_non_blank + prob),
67
+ next_prob_non_blank,
68
+ )
69
+ continue
70
+
71
+ last = prefix[-1] if prefix else None
72
+ if token == last:
73
+ next_beam[prefix] = (
74
+ next_prob_blank,
75
+ _log_add(next_prob_non_blank,
76
+ prob_non_blank + prob),
77
+ )
78
+ new_prefix = prefix + (token, )
79
+ nb_blank, nb_non_blank = next_beam.get(
80
+ new_prefix, (-float("inf"), -float("inf")))
81
+ next_beam[new_prefix] = (
82
+ nb_blank,
83
+ _log_add(nb_non_blank, prob_blank + prob),
84
+ )
85
+ else:
86
+ new_prefix = prefix + (token, )
87
+ nb_blank, nb_non_blank = next_beam.get(
88
+ new_prefix, (-float("inf"), -float("inf")))
89
+ next_beam[new_prefix] = (
90
+ nb_blank,
91
+ _log_add(nb_non_blank, prob_blank + prob,
92
+ prob_non_blank + prob),
93
+ )
94
+ beam = dict(sorted(
95
+ next_beam.items(),
96
+ key=lambda item: _log_add(item[1][0], item[1][1]),
97
+ reverse=True)[:beam_size])
98
+ return [(_log_add(prob_blank, prob_non_blank), prefix)
99
+ for prefix, (prob_blank, prob_non_blank) in sorted(
100
+ beam.items(),
101
+ key=lambda item: _log_add(item[1][0], item[1][1]),
102
+ reverse=True)]
103
+
104
+ def ctc_beam_search_decoder_batch(batch_log_probs_seq,
105
+ batch_log_probs_idx,
106
+ batch_root_trie,
107
+ batch_start,
108
+ beam_size,
109
+ num_processes,
110
+ blank_id=0,
111
+ space_id=-1,
112
+ cutoff_prob=0.999,
113
+ ext_scorer=None):
114
+ del batch_root_trie, batch_start, num_processes, space_id
115
+ del cutoff_prob, ext_scorer
116
+ return [
117
+ _ctc_prefix_beam_search(log_probs_seq, log_probs_idx, beam_size,
118
+ blank_id)
119
+ for log_probs_seq, log_probs_idx in zip(batch_log_probs_seq,
120
+ batch_log_probs_idx)
121
+ ]
122
+
123
+
124
+ def load_config(config_path):
125
+ with open(config_path, "r") as fin:
126
+ return yaml.load(fin, Loader=yaml.FullLoader)
127
+
128
+
129
+ def load_vocab(vocab_path):
130
+ vocabulary = []
131
+ char_dict = {}
132
+ with open(vocab_path, "r") as fin:
133
+ for line in fin:
134
+ arr = line.strip().split()
135
+ assert len(arr) == 2
136
+ char_dict[int(arr[1])] = arr[0]
137
+ vocabulary.append(arr[0])
138
+ return vocabulary, char_dict
139
+
140
+
141
+ def compute_feats(audio_file: str, sr=16000) -> np.ndarray:
142
+ try:
143
+ import soundfile as sf
144
+ waveform, sample_rate = sf.read(audio_file, dtype="int16",
145
+ always_2d=True)
146
+ waveform = torch.from_numpy(waveform.T).to(torch.float)
147
+ except ModuleNotFoundError:
148
+ waveform, sample_rate = torchaudio.load(audio_file, normalize=True)
149
+ waveform = waveform.to(torch.float)
150
+ if sample_rate != sr:
151
+ waveform = torchaudio.transforms.Resample(
152
+ orig_freq=sample_rate, new_freq=sr)(waveform)
153
+ feats = kaldi.fbank(waveform,
154
+ num_mel_bins=80,
155
+ frame_length=25,
156
+ frame_shift=10,
157
+ energy_floor=0.0,
158
+ sample_frequency=sr)
159
+ return feats.unsqueeze(0).numpy()
160
+
161
+
162
+ def pad_array_along_axis(array, pad_width, axis, mode="constant", **kwargs):
163
+ if array.shape[axis] >= pad_width:
164
+ return array
165
+ full_pad_width = [(0, 0)] * array.ndim
166
+ full_pad_width[axis] = (0, pad_width - array.shape[axis])
167
+ return np.pad(array, pad_width=full_pad_width, mode=mode, **kwargs)
168
+
169
+
170
+ def ctc_decoding(beam_log_probs,
171
+ beam_log_probs_idx,
172
+ encoder_out_lens,
173
+ vocabulary,
174
+ mode="ctc_prefix_beam_search"):
175
+ beam_size = beam_log_probs.shape[-1]
176
+ batch_size = beam_log_probs.shape[0]
177
+ num_processes = min(multiprocessing.cpu_count(), batch_size)
178
+ hyps = []
179
+ score_hyps = []
180
+
181
+ if mode == "ctc_greedy_search":
182
+ log_probs_idx = beam_log_probs_idx[:, :, 0]
183
+ batch_sents = []
184
+ for idx, seq in enumerate(log_probs_idx):
185
+ batch_sents.append(seq[0:encoder_out_lens[idx]].tolist())
186
+ hyps = map_batch(batch_sents, vocabulary, num_processes, True, 0)
187
+ elif mode in ("ctc_prefix_beam_search", "attention_rescoring"):
188
+ batch_log_probs_seq_list = beam_log_probs.tolist()
189
+ batch_log_probs_idx_list = beam_log_probs_idx.tolist()
190
+ batch_len_list = encoder_out_lens.tolist()
191
+ batch_log_probs_seq = []
192
+ batch_log_probs_ids = []
193
+ batch_start = []
194
+ batch_root = TrieVector()
195
+ root_dict = {}
196
+ for i in range(len(batch_len_list)):
197
+ num_sent = batch_len_list[i]
198
+ batch_log_probs_seq.append(batch_log_probs_seq_list[i][0:num_sent])
199
+ batch_log_probs_ids.append(batch_log_probs_idx_list[i][0:num_sent])
200
+ root_dict[i] = PathTrie()
201
+ batch_root.append(root_dict[i])
202
+ batch_start.append(True)
203
+ score_hyps = ctc_beam_search_decoder_batch(batch_log_probs_seq,
204
+ batch_log_probs_ids,
205
+ batch_root,
206
+ batch_start,
207
+ beam_size,
208
+ num_processes,
209
+ 0, -2, 0.99999)
210
+ if mode == "ctc_prefix_beam_search":
211
+ for cand_hyps in score_hyps:
212
+ hyps.append(cand_hyps[0][1])
213
+ hyps = map_batch(hyps, vocabulary, num_processes, False, 0)
214
+ return hyps, score_hyps
215
+
216
+
217
+ def make_decoder_inputs(encoder_out,
218
+ encoder_out_lens,
219
+ beam_log_probs,
220
+ beam_log_probs_idx,
221
+ vocabulary,
222
+ sos,
223
+ eos,
224
+ decoder_len):
225
+ _, score_hyps = ctc_decoding(beam_log_probs, beam_log_probs_idx,
226
+ encoder_out_lens, vocabulary,
227
+ "attention_rescoring")
228
+ ignore_id = -1
229
+ beam_size = beam_log_probs.shape[-1]
230
+ batch_size = beam_log_probs.shape[0]
231
+ ctc_score, all_hyps = [], []
232
+ for hyps in score_hyps:
233
+ cur_len = len(hyps)
234
+ if len(hyps) < beam_size:
235
+ hyps += (beam_size - cur_len) * [(-float("inf"), (0,))]
236
+ cur_ctc_score = []
237
+ for hyp in hyps:
238
+ cur_ctc_score.append(hyp[0])
239
+ all_hyps.append(list(hyp[1]))
240
+ ctc_score.append(cur_ctc_score)
241
+ ctc_score = np.array(ctc_score, dtype=np.float32)
242
+
243
+ max_len = decoder_len - 2
244
+ hyps_pad_sos_eos = np.ones((batch_size, beam_size, max_len + 2),
245
+ dtype=np.int64) * ignore_id
246
+ r_hyps_pad_sos_eos = np.ones((batch_size, beam_size, max_len + 2),
247
+ dtype=np.int64) * ignore_id
248
+ hyps_lens_sos = np.ones((batch_size, beam_size), dtype=np.int32)
249
+ k = 0
250
+ for i in range(batch_size):
251
+ for j in range(beam_size):
252
+ cand = all_hyps[k][:max_len]
253
+ length = len(cand) + 2
254
+ hyps_pad_sos_eos[i][j][0:length] = [sos] + cand + [eos]
255
+ r_hyps_pad_sos_eos[i][j][0:length] = [sos] + cand[::-1] + [eos]
256
+ hyps_lens_sos[i][j] = len(cand) + 1
257
+ k += 1
258
+
259
+ if decoder_len > encoder_out.shape[1]:
260
+ encoder_out = np.pad(encoder_out,
261
+ [(0, 0),
262
+ (0, decoder_len - encoder_out.shape[1]),
263
+ (0, 0)],
264
+ mode="constant",
265
+ constant_values=0)
266
+ elif decoder_len < encoder_out.shape[1]:
267
+ encoder_out = encoder_out[:, :decoder_len, :]
268
+
269
+ return {
270
+ "encoder_out": encoder_out,
271
+ "encoder_out_lens": np.full(batch_size,
272
+ fill_value=decoder_len,
273
+ dtype=np.int32),
274
+ "hyps_pad_sos_eos": hyps_pad_sos_eos.astype(np.int32),
275
+ "hyps_lens_sos": hyps_lens_sos,
276
+ "r_hyps_pad_sos_eos": r_hyps_pad_sos_eos.astype(np.int32),
277
+ "ctc_score": ctc_score,
278
+ }, all_hyps
279
+
280
+
281
+ def make_offline_inputs(feats, seq_len):
282
+ feats = feats[:, :seq_len, :]
283
+ speech_lengths = np.array([feats.shape[1]], dtype=np.int32)
284
+ if feats.shape[1] < seq_len:
285
+ feats = pad_array_along_axis(feats, pad_width=seq_len, axis=1)
286
+ return {"speech": feats, "speech_lengths": speech_lengths}
287
+
288
+
289
+ def make_online_initial_state(configs,
290
+ batch_size=1,
291
+ decoding_chunk_size=16,
292
+ num_decoding_left_chunks=5):
293
+ subsampling = 4
294
+ context = 7
295
+ stride = subsampling * decoding_chunk_size
296
+ decoding_window = (decoding_chunk_size - 1) * subsampling + context
297
+ required_cache_size = decoding_chunk_size * num_decoding_left_chunks
298
+
299
+ output_size = configs["encoder_conf"]["output_size"]
300
+ num_layers = configs["encoder_conf"]["num_blocks"]
301
+ cnn_module_kernel = configs["encoder_conf"].get("cnn_module_kernel", 1) - 1
302
+ head = configs["encoder_conf"]["attention_heads"]
303
+ d_k = configs["encoder_conf"]["output_size"] // head
304
+
305
+ state = {
306
+ "att_cache": np.zeros((batch_size, num_layers, head,
307
+ required_cache_size, d_k * 2),
308
+ dtype=np.float32),
309
+ "cnn_cache": np.zeros((batch_size, num_layers, output_size,
310
+ cnn_module_kernel),
311
+ dtype=np.float32),
312
+ "cache_mask": np.zeros((batch_size, 1, required_cache_size),
313
+ dtype=np.float32),
314
+ "offset": np.zeros((batch_size, 1), dtype=np.int32),
315
+ }
316
+ params = {
317
+ "batch_size": batch_size,
318
+ "context": context,
319
+ "stride": stride,
320
+ "decoding_window": decoding_window,
321
+ }
322
+ return state, params
323
+
324
+
325
+ def make_online_encoder_input(feats, cur, params, state):
326
+ batch_size = params["batch_size"]
327
+ decoding_window = params["decoding_window"]
328
+ end = min(cur + decoding_window, feats.shape[1])
329
+ chunk_xs = feats[:, cur:end, :]
330
+ if chunk_xs.shape[1] < decoding_window:
331
+ chunk_xs = pad_array_along_axis(chunk_xs,
332
+ pad_width=decoding_window,
333
+ axis=1)
334
+ chunk_xs = chunk_xs.astype(np.float32)
335
+ chunk_lens = np.full(batch_size,
336
+ fill_value=chunk_xs.shape[1],
337
+ dtype=np.int32)
338
+ return {
339
+ "chunk_xs": chunk_xs,
340
+ "chunk_lens": chunk_lens,
341
+ "offset": state["offset"],
342
+ "att_cache": state["att_cache"],
343
+ "cnn_cache": state["cnn_cache"],
344
+ "cache_mask": state["cache_mask"],
345
+ }
346
+
347
+
348
+ def update_online_state(state, outputs):
349
+ state["offset"] = outputs[4]
350
+ state["att_cache"] = outputs[5]
351
+ state["cnn_cache"] = outputs[6]
352
+ state["cache_mask"] = outputs[7]
353
+
354
+
355
+ def save_calibration_inputs(calib_data_path, inputs, sample_id):
356
+ for input_name, data in inputs.items():
357
+ data_path = os.path.join(calib_data_path, input_name)
358
+ os.makedirs(data_path, exist_ok=True)
359
+ np.save(os.path.join(data_path, f"{sample_id}.npy"), data)
360
+
361
+
362
+ def pack_calibration_dataset(calib_data_path):
363
+ for input_name in sorted(os.listdir(calib_data_path)):
364
+ data_path = os.path.join(calib_data_path, input_name)
365
+ if not os.path.isdir(data_path):
366
+ continue
367
+ tar_path = os.path.join(calib_data_path, input_name + ".tar.gz")
368
+ with tf.open(tar_path, "w:gz") as tf_file:
369
+ tf_file.add(data_path, arcname=input_name)
370
+
371
+
372
+ class WenetONNXRunner:
373
+
374
+ def __init__(self,
375
+ config_path,
376
+ vocab_path,
377
+ onnx_dir="onnx_model",
378
+ offline_seq_len=1024,
379
+ decoder_len=32,
380
+ decoding_chunk_size=16,
381
+ num_decoding_left_chunks=5,
382
+ batch_size=1,
383
+ providers=None):
384
+ self.config_path = config_path
385
+ self.vocab_path = vocab_path
386
+ self.onnx_dir = onnx_dir
387
+ self.offline_seq_len = offline_seq_len
388
+ self.decoder_len = decoder_len
389
+ self.decoding_chunk_size = decoding_chunk_size
390
+ self.num_decoding_left_chunks = num_decoding_left_chunks
391
+ self.batch_size = batch_size
392
+ self.providers = providers or ["CPUExecutionProvider"]
393
+
394
+ self.configs = load_config(config_path)
395
+ self.vocabulary, self.char_dict = load_vocab(vocab_path)
396
+ self.eos = self.sos = len(self.char_dict) - 1
397
+
398
+ self._offline_encoder = None
399
+ self._online_encoder = None
400
+ self._decoder = None
401
+
402
+ @property
403
+ def offline_encoder(self):
404
+ if self._offline_encoder is None:
405
+ self._offline_encoder = self._new_session("encoder_offline.onnx")
406
+ return self._offline_encoder
407
+
408
+ @property
409
+ def online_encoder(self):
410
+ if self._online_encoder is None:
411
+ self._online_encoder = self._new_session("encoder_online.onnx")
412
+ return self._online_encoder
413
+
414
+ @property
415
+ def decoder(self):
416
+ if self._decoder is None:
417
+ self._decoder = self._new_session("decoder.onnx")
418
+ return self._decoder
419
+
420
+ def _new_session(self, filename):
421
+ import onnxruntime as ort
422
+ return ort.InferenceSession(os.path.join(self.onnx_dir, filename),
423
+ providers=self.providers)
424
+
425
+ def compute_feats(self, audio_file):
426
+ return compute_feats(audio_file)
427
+
428
+ def run_offline_encoder(self,
429
+ feats,
430
+ calib_data_path=None,
431
+ sample_id=0):
432
+ encoder_input = make_offline_inputs(feats, self.offline_seq_len)
433
+ if calib_data_path:
434
+ save_calibration_inputs(calib_data_path, encoder_input, sample_id)
435
+ encoder_out, encoder_out_lens, ctc_log_probs, beam_log_probs, beam_log_probs_idx = (
436
+ self.offline_encoder.run(None, encoder_input))
437
+ return {
438
+ "encoder_out": encoder_out,
439
+ "encoder_out_lens": encoder_out_lens,
440
+ "ctc_log_probs": ctc_log_probs,
441
+ "beam_log_probs": beam_log_probs,
442
+ "beam_log_probs_idx": beam_log_probs_idx,
443
+ }
444
+
445
+ def run_online_encoder(self,
446
+ feats,
447
+ calib_data_path=None,
448
+ sample_prefix=0):
449
+ state, online_params = make_online_initial_state(
450
+ self.configs, self.batch_size, self.decoding_chunk_size,
451
+ self.num_decoding_left_chunks)
452
+ encoder_out = []
453
+ beam_log_probs = []
454
+ beam_log_probs_idx = []
455
+ num_frames = feats.shape[1]
456
+
457
+ for cur in range(0, num_frames - online_params["context"] + 1,
458
+ online_params["stride"]):
459
+ encoder_input = make_online_encoder_input(feats, cur,
460
+ online_params, state)
461
+ if calib_data_path:
462
+ save_calibration_inputs(calib_data_path, encoder_input,
463
+ f"{sample_prefix}_{cur}")
464
+ outputs = self.online_encoder.run(None, encoder_input)
465
+ chunk_log_probs, chunk_log_probs_idx, chunk_out, chunk_out_lens = outputs[:4]
466
+ update_online_state(state, outputs)
467
+ del chunk_out_lens
468
+ encoder_out.append(chunk_out)
469
+ beam_log_probs.append(chunk_log_probs)
470
+ beam_log_probs_idx.append(chunk_log_probs_idx.astype(np.int32))
471
+
472
+ return {
473
+ "encoder_out": np.concatenate(encoder_out, axis=1),
474
+ "encoder_out_lens": np.full(self.batch_size,
475
+ fill_value=sum(
476
+ out.shape[1]
477
+ for out in encoder_out),
478
+ dtype=np.int32),
479
+ "beam_log_probs": np.concatenate(beam_log_probs, axis=1),
480
+ "beam_log_probs_idx": np.concatenate(beam_log_probs_idx, axis=1),
481
+ "num_chunks": len(encoder_out),
482
+ }
483
+
484
+ def ctc_decode(self, encoder_outputs, mode):
485
+ return ctc_decoding(encoder_outputs["beam_log_probs"],
486
+ encoder_outputs["beam_log_probs_idx"],
487
+ encoder_outputs["encoder_out_lens"],
488
+ self.vocabulary, mode)
489
+
490
+ def run_decoder(self,
491
+ encoder_outputs,
492
+ calib_data_path=None,
493
+ sample_id=0):
494
+ decoder_input, all_hyps = make_decoder_inputs(
495
+ encoder_outputs["encoder_out"],
496
+ encoder_outputs["encoder_out_lens"],
497
+ encoder_outputs["beam_log_probs"],
498
+ encoder_outputs["beam_log_probs_idx"],
499
+ self.vocabulary,
500
+ self.sos,
501
+ self.eos,
502
+ self.decoder_len,
503
+ )
504
+ if calib_data_path:
505
+ save_calibration_inputs(calib_data_path, decoder_input, sample_id)
506
+
507
+ best_index = self.decoder.run(None, decoder_input)[0].astype(np.int32)
508
+ beam_size = encoder_outputs["beam_log_probs"].shape[-1]
509
+ num_processes = min(multiprocessing.cpu_count(), best_index.shape[0])
510
+ best_sents = []
511
+ k = 0
512
+ for idx in best_index:
513
+ best_sents.append(all_hyps[k:k + beam_size][idx])
514
+ k += beam_size
515
+ hyps = map_batch(best_sents, self.vocabulary, num_processes)
516
+ return "".join(hyps)
517
+
518
+ def transcribe(self,
519
+ audio_file,
520
+ online=False,
521
+ mode="ctc_prefix_beam_search",
522
+ calib_data_path=None):
523
+ feats = self.compute_feats(audio_file)
524
+ if online:
525
+ encoder_outputs = self.run_online_encoder(
526
+ feats, calib_data_path=calib_data_path, sample_prefix=0)
527
+ else:
528
+ encoder_outputs = self.run_offline_encoder(
529
+ feats, calib_data_path=calib_data_path, sample_id=0)
530
+
531
+ if mode == "attention_rescoring":
532
+ result = self.run_decoder(encoder_outputs, calib_data_path, 0)
533
+ else:
534
+ hyps, _ = self.ctc_decode(encoder_outputs, mode)
535
+ result = "".join(hyps) if hyps else ""
536
+
537
+ if calib_data_path:
538
+ pack_calibration_dataset(calib_data_path)
539
+ return result
540
+
541
+ def save_calibration_for_audio(self,
542
+ audio_file,
543
+ parts,
544
+ calib_data_path,
545
+ sample_id):
546
+ counts = {"offline": 0, "online": 0, "decoder": 0}
547
+ feats = self.compute_feats(audio_file)
548
+ offline_outputs = None
549
+ if "offline" in parts or "decoder" in parts:
550
+ offline_outputs = self.run_offline_encoder(
551
+ feats,
552
+ calib_data_path=calib_data_path if "offline" in parts else None,
553
+ sample_id=sample_id)
554
+ if "offline" in parts:
555
+ counts["offline"] += 1
556
+ if "online" in parts:
557
+ online_outputs = self.run_online_encoder(
558
+ feats,
559
+ calib_data_path=calib_data_path,
560
+ sample_prefix=sample_id)
561
+ counts["online"] += online_outputs["num_chunks"]
562
+ if "decoder" in parts:
563
+ self.run_decoder(offline_outputs,
564
+ calib_data_path=calib_data_path,
565
+ sample_id=sample_id)
566
+ counts["decoder"] += 1
567
+ return counts
requirements_ax.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ https://github.com/AXERA-TECH/pyaxengine/releases/download/0.1.3.rc3/axengine-0.1.3-py3-none-any.whl
2
+ pyyaml
3
+ numpy
requirements_x86.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --extra-index-url https://download.pytorch.org/whl/cpu
2
+
3
+ pyyaml>=5.1
4
+ torch==2.11.0+cpu
5
+ torchaudio==2.11.0+cpu
6
+ tqdm
7
+ soundfile
8
+ openai-whisper==20250625
9
+ onnx
10
+ onnxruntime
11
+ onnxscript
12
+ onnxsim
run_ax.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+
3
+ from ax_common import WenetAXRunner
4
+
5
+
6
+ def get_args():
7
+ parser = argparse.ArgumentParser()
8
+ parser.add_argument("--input", "-i", type=str, required=True, help="Input audio file")
9
+ parser.add_argument("--online", action="store_true")
10
+ parser.add_argument("--config",
11
+ type=str,
12
+ default="pretrained/aishell_u2pp_conformer_exp/train.yaml",
13
+ help="yaml file in checkpoint path")
14
+ parser.add_argument("--vocab",
15
+ type=str,
16
+ default="pretrained/aishell_u2pp_conformer_exp/units.txt",
17
+ help="pretrained units.txt")
18
+ parser.add_argument("--encoder_online",
19
+ type=str,
20
+ default="axmodel/encoder_online/encoder_online.axmodel")
21
+ parser.add_argument("--encoder_offline",
22
+ type=str,
23
+ default="axmodel/encoder_offline/encoder_offline.axmodel")
24
+ parser.add_argument("--decoder",
25
+ type=str,
26
+ default="axmodel/decoder/decoder.axmodel")
27
+ parser.add_argument("--offline_seq_len", type=int, default=1024)
28
+ parser.add_argument("--online_seq_len", type=int, default=67)
29
+ parser.add_argument("--decoder_len", type=int, default=32)
30
+ parser.add_argument("--decoding_chunk_size", type=int, default=16)
31
+ parser.add_argument("--num_decoding_left_chunks", type=int, default=5)
32
+ parser.add_argument("--mode",
33
+ choices=[
34
+ "ctc_greedy_search", "ctc_prefix_beam_search",
35
+ "attention_rescoring"
36
+ ],
37
+ default="ctc_prefix_beam_search",
38
+ help="decoding mode")
39
+ return parser.parse_args()
40
+
41
+
42
+ def main():
43
+ args = get_args()
44
+ print(f"online: {args.online}")
45
+ print(f"mode: {args.mode}")
46
+
47
+ runner = WenetAXRunner(
48
+ args.config,
49
+ args.vocab,
50
+ encoder_offline_path=args.encoder_offline,
51
+ encoder_online_path=args.encoder_online,
52
+ decoder_path=args.decoder,
53
+ offline_seq_len=args.offline_seq_len,
54
+ decoder_len=args.decoder_len,
55
+ decoding_chunk_size=args.decoding_chunk_size,
56
+ num_decoding_left_chunks=args.num_decoding_left_chunks,
57
+ )
58
+ result = runner.transcribe(args.input,
59
+ online=args.online,
60
+ mode=args.mode)
61
+ print(f"ASR Result: {result}")
62
+
63
+
64
+ if __name__ == "__main__":
65
+ main()
run_ort.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+
4
+ from ort_common import WenetONNXRunner
5
+
6
+
7
+ def get_args():
8
+ parser = argparse.ArgumentParser()
9
+ parser.add_argument("--input", "-i", type=str, required=True, help="Input audio file")
10
+ parser.add_argument("--config", type=str, required=True, help="yaml file in checkpoint path")
11
+ parser.add_argument(
12
+ "--vocab",
13
+ type=str,
14
+ required=True,
15
+ help="pretrained units.txt, for example pretrained/<model>/units.txt",
16
+ )
17
+ parser.add_argument("--online", action="store_true")
18
+ parser.add_argument("--offline_seq_len", type=int, default=1024)
19
+ parser.add_argument("--online_seq_len", type=int, default=67)
20
+ parser.add_argument("--decoder_len", type=int, default=32)
21
+ parser.add_argument("--onnx_dir", type=str, default="onnx_model")
22
+ parser.add_argument('--mode',
23
+ choices=[
24
+ 'ctc_greedy_search', 'ctc_prefix_beam_search', 'attention_rescoring'],
25
+ default='ctc_prefix_beam_search',
26
+ help='decoding mode')
27
+ parser.add_argument("--calib_data_path", type=str, default="calibration_dataset", help="Generated calibration data path")
28
+ return parser.parse_args()
29
+
30
+
31
+ def run_ort():
32
+ args = get_args()
33
+ print(f"online: {args.online}")
34
+ print(f"mode: {args.mode}")
35
+ print(f"calib_data_path: {args.calib_data_path}")
36
+ calib_data_path = args.calib_data_path or None
37
+ if calib_data_path:
38
+ os.makedirs(calib_data_path, exist_ok=True)
39
+
40
+ runner = WenetONNXRunner(
41
+ args.config,
42
+ args.vocab,
43
+ onnx_dir=args.onnx_dir,
44
+ offline_seq_len=args.offline_seq_len,
45
+ decoder_len=args.decoder_len,
46
+ )
47
+ result = runner.transcribe(args.input,
48
+ online=args.online,
49
+ mode=args.mode,
50
+ calib_data_path=calib_data_path)
51
+
52
+ print(f"ASR Result: {result}")
53
+
54
+ if __name__ == "__main__":
55
+ run_ort()
wenet/LLM/__init__.py ADDED
File without changes
wenet/LLM/causallm_model.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List, Optional, Union
2
+ import torch
3
+ from wenet.LLM.decoder import DecoderOnly
4
+ from wenet.LLM.sampler import sampler
5
+ from wenet.utils.common import IGNORE_ID, th_accuracy
6
+ from wenet.utils.mask import make_pad_mask, subsequent_mask
7
+
8
+
9
+ class CausalLM(torch.nn.Module):
10
+
11
+ def __init__(
12
+ self,
13
+ vocab_size: int,
14
+ decoder: DecoderOnly,
15
+ special_tokens: dict,
16
+ tie_word_embedding: bool = False,
17
+ linear_bias: bool = False,
18
+ ignore_id: int = IGNORE_ID,
19
+ lsm_weight: float = 0.0,
20
+ reduction: str = 'mean',
21
+ ) -> None:
22
+ super().__init__()
23
+ del special_tokens
24
+
25
+ self.embed = torch.nn.Embedding(vocab_size, decoder.hidden_size)
26
+ self.out = torch.nn.Linear(decoder.hidden_size,
27
+ vocab_size,
28
+ bias=linear_bias)
29
+
30
+ self.decoder = decoder
31
+ self.vocab_size = vocab_size
32
+ self.criterion_att = torch.nn.CrossEntropyLoss(
33
+ ignore_index=ignore_id,
34
+ label_smoothing=lsm_weight,
35
+ reduction=reduction,
36
+ )
37
+ self.tie_word_embedding = tie_word_embedding
38
+ self.ignore_id = ignore_id
39
+
40
+ @torch.jit.unused
41
+ def forward(
42
+ self,
43
+ batch: dict,
44
+ device: torch.device,
45
+ ) -> Dict[str, Optional[torch.Tensor]]:
46
+ """ Forward for training
47
+ """
48
+ text = batch['feats'].to(device)
49
+ target = batch['target'].to(device)
50
+ text_length = batch['feats_lengths'].to(device)
51
+
52
+ mask = ~make_pad_mask(text_length, max_len=text.size(1)).unsqueeze(
53
+ 1) # (B,1,L)
54
+ causal_mask = subsequent_mask(
55
+ mask.size(-1), device=mask.device).unsqueeze(0) # (1,L,L)
56
+ att_mask = causal_mask & mask # (B, L, L)
57
+
58
+ embeding = self.embed(text)
59
+ decoder_out = self.out(self.decoder(embeding,
60
+ att_mask)[0]) # (B, L, vocab_size)
61
+ loss = self.criterion_att(decoder_out.view(-1, self.vocab_size),
62
+ target.view(-1))
63
+ acc = th_accuracy(decoder_out.view(-1, self.vocab_size),
64
+ target,
65
+ ignore_label=self.ignore_id)
66
+
67
+ return {
68
+ "loss": loss,
69
+ "ppl": torch.exp(loss.detach()),
70
+ "th_accuracy": acc
71
+ }
72
+
73
+ def tie_or_clone_weights(self, jit_mode: bool):
74
+ if not self.tie_word_embedding:
75
+ return
76
+ if jit_mode:
77
+ self.out.weight = torch.nn.Parameter(self.embed.weight.clone())
78
+ else:
79
+ self.out.weight = self.embed.weight
80
+ # TODO(Mddct): whether to deal bias for other llm model
81
+
82
+ @torch.jit.unused
83
+ @torch.inference_mode()
84
+ def generate(
85
+ self,
86
+ prompts_tokens: List[List[int]],
87
+ device: torch.device,
88
+ stop_tokens: List[int],
89
+ dtype: torch.dtype = torch.float32,
90
+ output_len: int = 100,
91
+ temperature: Union[float, None] = 0.95,
92
+ top_p: float = 1.0,
93
+ top_k: int = 100,
94
+ ) -> List[List[int]]:
95
+ """Generates responses for given prompts using Gemma model."""
96
+ # If a single prompt is provided, treat it as a batch of 1.
97
+ batch_size = len(prompts_tokens)
98
+ min_prompt_len = min(len(p) for p in prompts_tokens)
99
+ max_prompt_len = max(len(p) for p in prompts_tokens)
100
+ max_seq_len = max_prompt_len + output_len
101
+ assert max_seq_len <= self.decoder.pos_enc.max_len
102
+
103
+ # build KV caches
104
+ kv_caches = []
105
+ for _ in range(len(self.decoder.decoders)):
106
+ size = (batch_size, 0, self.decoder.n_kv_head,
107
+ self.decoder.head_dim)
108
+ k_cache = torch.zeros(size=size, dtype=dtype, device=device)
109
+ v_cache = torch.zeros(size=size, dtype=dtype, device=device)
110
+ kv_caches.append((k_cache, v_cache))
111
+
112
+ # prepare inputs
113
+ token_ids_tensor = torch.full((batch_size, max_seq_len),
114
+ IGNORE_ID,
115
+ dtype=torch.int64,
116
+ device=device)
117
+ input_token_ids_tensor = torch.full((batch_size, min_prompt_len),
118
+ IGNORE_ID,
119
+ dtype=torch.int64,
120
+ device=device)
121
+ # right padding
122
+ for i, p in enumerate(prompts_tokens):
123
+ token_ids_tensor[i, :len(p)] = torch.tensor(p)
124
+ input_token_ids_tensor[i, :min_prompt_len] = torch.tensor(
125
+ p[:min_prompt_len])
126
+
127
+ prompt_mask_tensor = token_ids_tensor != IGNORE_ID
128
+ input_positions_tensor = torch.arange(0,
129
+ min_prompt_len,
130
+ dtype=torch.int64).to(device)
131
+ mask_tensor = torch.ones((1, 1, max_seq_len, max_seq_len),
132
+ dtype=torch.bool)
133
+ mask_tensor = torch.tril(mask_tensor).to(device)
134
+ curr_mask_tensor = mask_tensor.index_select(2, input_positions_tensor)
135
+ att_mask = curr_mask_tensor.squeeze(
136
+ 1)[:, :min_prompt_len, :min_prompt_len]
137
+ output_positions_tensor = torch.LongTensor([min_prompt_len - 1
138
+ ]).to(device)
139
+ temperatures_tensor = None if not temperature else torch.FloatTensor(
140
+ [temperature] * batch_size).to(device)
141
+ top_ps_tensor = torch.FloatTensor([top_p] * batch_size).to(device)
142
+ top_ks_tensor = torch.LongTensor([top_k] * batch_size).to(device)
143
+ output_index = torch.tensor(min_prompt_len,
144
+ dtype=torch.int64).to(device)
145
+
146
+ input_token_embeding = self.embed(input_token_ids_tensor)
147
+ offset = torch.tensor([0] * len(prompts_tokens)).to(device)
148
+ input_offset = offset
149
+
150
+ stop_tokens_tensor = torch.tensor(stop_tokens, device=device)
151
+ # Prefill up to min_prompt_len tokens, then treat other prefill as
152
+ # decode and ignore output.
153
+ for i in range(max_seq_len - min_prompt_len):
154
+ decoder_out, kv_caches, = self.decoder(
155
+ input_token_embeding,
156
+ att_mask,
157
+ input_offset,
158
+ kv_caches,
159
+ )
160
+ decoder_out = self.out(decoder_out)
161
+ decoder_out = decoder_out.index_select(1, output_positions_tensor)
162
+ next_token_ids = sampler(
163
+ decoder_out,
164
+ temperatures_tensor,
165
+ top_ps_tensor,
166
+ top_ks_tensor,
167
+ )
168
+ curr_prompt_mask = prompt_mask_tensor.index_select(
169
+ 1, output_index).squeeze(dim=1)
170
+ curr_token_ids = token_ids_tensor.index_select(
171
+ 1, output_index).squeeze(dim=1)
172
+ output_token_ids = torch.where(curr_prompt_mask, curr_token_ids,
173
+ next_token_ids).unsqueeze(dim=1)
174
+ token_ids_tensor.index_copy_(1, output_index, output_token_ids)
175
+
176
+ input_token_ids_tensor = output_token_ids
177
+ input_token_embeding = self.embed(input_token_ids_tensor)
178
+
179
+ input_positions_tensor = output_index.unsqueeze(dim=-1)
180
+ curr_mask_tensor = mask_tensor.index_select(
181
+ 2, input_positions_tensor)
182
+ att_mask = curr_mask_tensor.squeeze(1)[:, :output_index +
183
+ 1, :output_index + 1]
184
+
185
+ output_positions_tensor = torch.tensor(
186
+ 0, dtype=torch.int64).to(device)
187
+ input_offset = offset + output_index.unsqueeze(-1)
188
+ output_index = output_index + 1
189
+
190
+ if all(torch.isin(next_token_ids, stop_tokens_tensor)):
191
+ break
192
+
193
+ token_ids = token_ids_tensor.tolist()
194
+ results = []
195
+ for i, tokens in enumerate(token_ids):
196
+ trimmed_output = tokens[len(prompts_tokens[i]
197
+ ):len(prompts_tokens[i]) + output_len]
198
+ for stop_token in stop_tokens:
199
+ try:
200
+ eos_index = trimmed_output.index(stop_token)
201
+ trimmed_output = trimmed_output[:eos_index]
202
+ break
203
+ except Exception:
204
+ continue
205
+ results.append(trimmed_output)
206
+
207
+ return results
wenet/LLM/decoder.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import partial
2
+ from typing import List, Optional, Tuple, Union
3
+ import torch
4
+ import torch.utils.checkpoint as ckpt
5
+ from wenet.transformer.attention import T_CACHE
6
+
7
+ from wenet.transformer.encoder_layer import TransformerEncoderLayer
8
+ from wenet.utils.class_utils import (WENET_ACTIVATION_CLASSES,
9
+ WENET_ATTENTION_CLASSES,
10
+ WENET_EMB_CLASSES, WENET_MLP_CLASSES,
11
+ WENET_NORM_CLASSES)
12
+ from wenet.utils.common import mask_to_bias
13
+
14
+
15
+ class DecoderOnly(torch.nn.Module):
16
+
17
+ def __init__(
18
+ self,
19
+ n_kv_head: int,
20
+ head_dim: int,
21
+ hidden_size: int,
22
+ attention_heads: int = 4,
23
+ linear_units: int = 2048,
24
+ num_blocks: int = 6,
25
+ dropout_rate: float = 0.1,
26
+ positional_dropout_rate: float = 0.1,
27
+ attention_dropout_rate: float = 0.0,
28
+ normalize_before: bool = True,
29
+ query_bias: bool = False,
30
+ key_bias: bool = False,
31
+ value_bias: bool = False,
32
+ mlp_bias: bool = False,
33
+ activation_type: str = "gelu",
34
+ gelu_approximate: Union[str, None] = None,
35
+ max_position_embeding: int = 8192,
36
+ mlp_type: str = 'gated',
37
+ layer_norm_type: str = 'rms_norm',
38
+ norm_eps: float = 1e-5,
39
+ rms_norm_offset: bool = True,
40
+ selfattention_layer_type: str = "rope_abs_selfattn",
41
+ use_sdpa: bool = False,
42
+ gradient_checkpointing: bool = False,
43
+ rope_theta: float = 10000.0,
44
+ rope_style: str = 'google',
45
+ scale_embed: bool = True,
46
+ ) -> None:
47
+ super().__init__()
48
+
49
+ assert selfattention_layer_type in ['rope_abs_selfattn']
50
+ self.pos_enc = WENET_EMB_CLASSES["rope_pos"](
51
+ hidden_size,
52
+ head_dim,
53
+ max_len=max_position_embeding,
54
+ dropout_rate=positional_dropout_rate,
55
+ rope_theta=rope_theta,
56
+ scale=scale_embed)
57
+ if activation_type == "gelu" and gelu_approximate is not None:
58
+ activation = WENET_ACTIVATION_CLASSES['gelu'](
59
+ approximate=gelu_approximate)
60
+ else:
61
+ activation = WENET_ACTIVATION_CLASSES[activation_type]()
62
+
63
+ mlp_class = WENET_MLP_CLASSES[mlp_type]
64
+ self.num_blocks = num_blocks
65
+ # TODO: support lora & refactor lora
66
+ self.decoders = torch.nn.ModuleList([
67
+ TransformerEncoderLayer(
68
+ hidden_size,
69
+ WENET_ATTENTION_CLASSES[selfattention_layer_type](
70
+ attention_heads,
71
+ hidden_size,
72
+ attention_dropout_rate,
73
+ query_bias,
74
+ key_bias,
75
+ value_bias,
76
+ use_sdpa,
77
+ n_kv_head,
78
+ head_dim,
79
+ style=rope_style),
80
+ mlp_class(hidden_size, linear_units, dropout_rate, activation,
81
+ mlp_bias),
82
+ dropout_rate,
83
+ normalize_before,
84
+ layer_norm_type=layer_norm_type,
85
+ norm_eps=norm_eps,
86
+ rms_norm_offset=rms_norm_offset,
87
+ ) for _ in range(self.num_blocks)
88
+ ])
89
+ self.pre_norm = normalize_before
90
+ self.final_norm: Optional[torch.nn.Module] = None
91
+ if self.pre_norm:
92
+ norm_class = WENET_NORM_CLASSES[layer_norm_type]
93
+ if layer_norm_type == "rms_norm":
94
+ norm_class = partial(
95
+ norm_class,
96
+ add_unit_offset=rms_norm_offset,
97
+ )
98
+ self.final_norm = norm_class(hidden_size, eps=norm_eps)
99
+
100
+ self.n_kv_head = n_kv_head
101
+ self.head_dim = head_dim
102
+ self._hidden_size = hidden_size
103
+ self.use_sdpa = use_sdpa
104
+ self.gradient_checkpointing = gradient_checkpointing
105
+
106
+ def forward(
107
+ self,
108
+ input: torch.Tensor,
109
+ att_mask: torch.Tensor,
110
+ input_position: Union[int, torch.Tensor] = 0,
111
+ kv_caches: Optional[List[T_CACHE]] = None,
112
+ ) -> Tuple[torch.Tensor, Union[List[T_CACHE], None]]:
113
+ xs, pos_emb = self.pos_enc(input, offset=input_position)
114
+ if self.use_sdpa:
115
+ att_mask = mask_to_bias(att_mask, xs.dtype)
116
+
117
+ if self.gradient_checkpointing and self.training:
118
+ xs = self.forward_layers_checkpointed(xs, att_mask, pos_emb)
119
+ else:
120
+ xs, kv_caches = self.forward_layers(xs, att_mask, pos_emb,
121
+ kv_caches)
122
+ if self.pre_norm and self.final_norm is not None:
123
+ xs = self.final_norm(xs)
124
+ return xs, kv_caches
125
+
126
+ def forward_layers(
127
+ self,
128
+ xs: torch.Tensor,
129
+ att_mask: torch.Tensor,
130
+ pos_emb: torch.Tensor,
131
+ kv_caches: Optional[List[T_CACHE]] = None,
132
+ ) -> Tuple[torch.Tensor, Union[List[T_CACHE], None]]:
133
+ if self.training:
134
+ for (i, layer) in enumerate(self.decoders):
135
+ xs, _, _, _ = layer(xs, att_mask, pos_emb)
136
+ new_kv_caches = kv_caches
137
+ else:
138
+ assert kv_caches is not None
139
+ new_kv_caches = []
140
+ for (i, layer) in enumerate(self.decoders):
141
+ xs, _, new_kv_cache, _ = layer(xs,
142
+ att_mask,
143
+ pos_emb,
144
+ att_cache=(kv_caches[i][0],
145
+ kv_caches[i][1]))
146
+ new_kv_caches.append(new_kv_cache)
147
+
148
+ return xs, new_kv_caches
149
+
150
+ @torch.jit.ignore(drop=True)
151
+ def forward_layers_checkpointed(self, xs: torch.Tensor,
152
+ att_mask: torch.Tensor,
153
+ pos_emb: torch.Tensor) -> torch.Tensor:
154
+ for layer in self.decoders:
155
+ xs, _, _, _ = ckpt.checkpoint(layer.__call__, xs, att_mask,
156
+ pos_emb)
157
+ return xs
158
+
159
+ @property
160
+ def hidden_size(self):
161
+ return self._hidden_size
wenet/LLM/sampler.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Union
2
+ import torch
3
+
4
+
5
+ # modified from https://github.com/google/gemma_pytorch/blob/main/gemma/model.py#L26
6
+ @torch.no_grad()
7
+ def sampler(
8
+ logits: torch.Tensor,
9
+ temperatures: Union[torch.Tensor, None],
10
+ top_ps: torch.Tensor,
11
+ top_ks: torch.Tensor,
12
+ ) -> torch.Tensor:
13
+ assert logits.size(1) == 1
14
+ logits = logits.squeeze(1) # (batch_size, vocab_size)
15
+ if temperatures is None:
16
+ return torch.argmax(logits, dim=-1).squeeze(dim=-1)
17
+
18
+ # Apply temperature scaling.
19
+ logits.div_(temperatures.unsqueeze(dim=1))
20
+
21
+ # Calculate probabilities with softmax.
22
+ probs = torch.softmax(logits, dim=-1, dtype=torch.float)
23
+ probs_sort, probs_idx = torch.sort(probs, dim=-1, descending=True)
24
+
25
+ # Apply top-p, top-k.
26
+ probs_sum = torch.cumsum(probs_sort, dim=-1)
27
+ top_ps_mask = (probs_sum - probs_sort) > top_ps.unsqueeze(dim=1)
28
+ probs_sort = torch.where(top_ps_mask, 0, probs_sort)
29
+
30
+ top_ks_mask = torch.arange(probs_idx.shape[-1], device=probs_idx.device)
31
+ top_ks_mask = top_ks_mask.expand(probs_idx.shape[0], -1)
32
+ top_ks_mask = top_ks_mask >= top_ks.unsqueeze(dim=1)
33
+ probs_sort = torch.where(top_ks_mask, 0, probs_sort)
34
+
35
+ # Re-normalization.
36
+ probs_sort.div_(probs_sort.sum(dim=-1, keepdim=True))
37
+ probs = torch.gather(probs_sort,
38
+ dim=-1,
39
+ index=torch.argsort(probs_idx, dim=-1))
40
+
41
+ next_token_ids = torch.multinomial(probs, num_samples=1,
42
+ replacement=True).squeeze(dim=-1)
43
+ return next_token_ids
wenet/README.md ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Module Introduction
2
+
3
+ Here is a brief introduction of each module(directory).
4
+
5
+ * `bin`: training and recognition binaries
6
+ * `dataset`: IO design
7
+ * `utils`: common utils
8
+ * `transformer`: the core of `WeNet`, in which the standard transformer/conformer is implemented. It contains the common blocks(backbone) of speech transformers.
9
+ * transformer/attention.py: Standard multi head attention
10
+ * transformer/embedding.py: Standard position encoding
11
+ * transformer/positionwise_feed_forward.py: Standard feed forward in transformer
12
+ * transformer/convolution.py: ConvolutionModule in Conformer model
13
+ * transformer/subsampling.py: Subsampling implementation for speech task
14
+ * `transducer`: transducer implementation
15
+ * `squeezeformer`: squeezeformer implementation, please refer [paper](https://arxiv.org/pdf/2206.00888.pdf)
16
+ * `efficient_conformer`: efficient conformer implementation, please refer [paper](https://arxiv.org/pdf/2109.01163.pdf)
17
+ * `paraformer`: paraformer implementation, please refer [paper](https://arxiv.org/pdf/1905.11235.pdf)
18
+ * `paraformer/cif.py`: Continuous Integrate-and-Fire implemented, please refer [paper](https://arxiv.org/pdf/1905.11235.pdf)
19
+ * `branchformer`: branchformer implementation, please refer [paper](https://arxiv.org/abs/2207.02971)
20
+ * `whisper`: whisper implementation, please refer [paper](https://arxiv.org/abs/2212.04356)
21
+ * `ssl`: Self-supervised speech model implementation. e.g. wav2vec2, bestrq, w2vbert.
22
+ * `ctl_model`: Enhancing the Unified Streaming and Non-streaming Model with with Contrastive Learning implementation [paper](https://arxiv.org/abs/2306.00755)
23
+
24
+ `transducer`, `squeezeformer`, `efficient_conformer`, `branchformer` and `cif` are all based on `transformer`,
25
+ they resue a lot of the common blocks of `tranformer`.
26
+
27
+ **If you want to contribute your own x-former, please reuse the current code as much as possible**.
28
+
29
+
wenet/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from wenet.cli.model import load_model, load_model_pt # noqa
wenet/bin/__init__.py ADDED
File without changes
wenet/bin/alignment.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 Mobvoi Inc. (authors: Di Wu)
2
+ # 2022 Tinnove Inc (authors: Wei Ren)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ from __future__ import print_function
17
+
18
+ import argparse
19
+ import copy
20
+ import logging
21
+ import os
22
+ import sys
23
+
24
+ import torch
25
+ import yaml
26
+ from torch.utils.data import DataLoader
27
+ from textgrid import TextGrid, IntervalTier
28
+ import math
29
+
30
+ from wenet.dataset.dataset import Dataset
31
+ from wenet.utils.ctc_utils import force_align
32
+ from wenet.utils.common import get_subsample
33
+ from wenet.utils.init_model import init_model
34
+ from wenet.utils.init_tokenizer import init_tokenizer
35
+
36
+
37
+ def generator_textgrid(maxtime, lines, output):
38
+ # Download Praat: https://www.fon.hum.uva.nl/praat/
39
+ interval = maxtime / (len(lines) + 1)
40
+ margin = 0.0001
41
+
42
+ tg = TextGrid(maxTime=maxtime)
43
+ linetier = IntervalTier(name="line", maxTime=maxtime)
44
+
45
+ i = 0
46
+ for l in lines:
47
+ s, e, w = l.split()
48
+ linetier.add(minTime=float(s) + margin, maxTime=float(e), mark=w)
49
+
50
+ tg.append(linetier)
51
+ print("successfully generator {}".format(output))
52
+ tg.write(output)
53
+
54
+
55
+ def get_frames_timestamp(alignment,
56
+ prob,
57
+ blank_thres=0.999,
58
+ thres=0.0000000001):
59
+ # convert alignment to a praat format, which is a doing phonetics
60
+ # by computer and helps analyzing alignment
61
+ timestamp = []
62
+ # get frames level duration for each token
63
+ start = 0
64
+ end = 0
65
+ local_start = 0
66
+ while end < len(alignment):
67
+ while end < len(alignment) and alignment[end] == 0:
68
+ end += 1
69
+ if end == len(alignment):
70
+ timestamp[-1] += alignment[start:]
71
+ break
72
+ end += 1
73
+ while end < len(alignment) and alignment[end - 1] == alignment[end]:
74
+ end += 1
75
+ local_start = end - 1
76
+ # find the possible front border for current token
77
+ while local_start >= start and (
78
+ prob[local_start][0] < math.log(blank_thres)
79
+ or prob[local_start][alignment[end - 1]] > math.log(thres)):
80
+ alignment[local_start] = alignment[end - 1]
81
+ local_start -= 1
82
+ cur_alignment = alignment[start:end]
83
+ timestamp.append(cur_alignment)
84
+ start = end
85
+ return timestamp
86
+
87
+
88
+ def get_labformat(timestamp, subsample):
89
+ begin = 0
90
+ begin_time = 0
91
+ duration = 0
92
+ labformat = []
93
+ for idx, t in enumerate(timestamp):
94
+ # 25ms frame_length,10ms hop_length, 1/subsample
95
+ subsample = get_subsample(configs)
96
+ # time duration
97
+ i = 0
98
+ while t[i] == 0:
99
+ i += 1
100
+ begin = i
101
+ dur = 0
102
+ while i < len(t) and t[i] != 0:
103
+ i += 1
104
+ dur += 1
105
+ begin = begin_time + begin * 0.01 * subsample
106
+ duration = dur * 0.01 * subsample
107
+ if idx < len(timestamp) - 1:
108
+ print("{:.2f} {:.2f} {}".format(begin, begin + duration,
109
+ char_dict[t[-1]]))
110
+ labformat.append("{:.2f} {:.2f} {}\n".format(
111
+ begin, begin + duration, char_dict[t[-1]]))
112
+ else: # last token
113
+ non_blank = 0
114
+ for i in t:
115
+ if i != 0:
116
+ token = i
117
+ break
118
+ print("{:.2f} {:.2f} {}".format(begin, begin + duration,
119
+ char_dict[token]))
120
+ labformat.append("{:.2f} {:.2f} {}\n".format(
121
+ begin, begin + duration, char_dict[token]))
122
+ begin_time += len(t) * 0.01 * subsample
123
+ return labformat
124
+
125
+
126
+ if __name__ == '__main__':
127
+ parser = argparse.ArgumentParser(
128
+ description='use ctc to generate alignment')
129
+ parser.add_argument('--config', required=True, help='config file')
130
+ parser.add_argument('--input_file', required=True, help='format data file')
131
+ parser.add_argument('--data_type',
132
+ default='raw',
133
+ choices=['raw', 'shard'],
134
+ help='train and cv data type')
135
+ parser.add_argument('--gpu',
136
+ type=int,
137
+ default=-1,
138
+ help='gpu id for this rank, -1 for cpu')
139
+ parser.add_argument('--device',
140
+ type=str,
141
+ default="cpu",
142
+ choices=["cpu", "npu", "cuda"],
143
+ help='accelerator to use')
144
+ parser.add_argument('--blank_thres',
145
+ default=0.999999,
146
+ type=float,
147
+ help='ctc blank thes')
148
+ parser.add_argument('--thres',
149
+ default=0.000001,
150
+ type=float,
151
+ help='ctc non blank thes')
152
+ parser.add_argument('--checkpoint', required=True, help='checkpoint model')
153
+ parser.add_argument('--dict', required=True, help='dict file')
154
+ parser.add_argument(
155
+ '--non_lang_syms',
156
+ help="non-linguistic symbol file. One symbol per line.")
157
+ parser.add_argument('--result_file',
158
+ required=True,
159
+ help='alignment result file')
160
+ parser.add_argument('--batch_size', type=int, default=1, help='batch size')
161
+ parser.add_argument('--gen_praat',
162
+ action='store_true',
163
+ help='convert alignment to a praat format')
164
+ parser.add_argument('--bpe_model',
165
+ default=None,
166
+ type=str,
167
+ help='bpe model for english part')
168
+
169
+ args = parser.parse_args()
170
+ print(args)
171
+ logging.basicConfig(level=logging.DEBUG,
172
+ format='%(asctime)s %(levelname)s %(message)s')
173
+ if args.gpu != -1:
174
+ # remain the original usage of gpu
175
+ args.device = "cuda"
176
+ if "cuda" in args.device:
177
+ os.environ['CUDA_VISIBLE_DEVICES'] = str(args.gpu)
178
+
179
+ if args.batch_size > 1:
180
+ logging.fatal('alignment mode must be running with batch_size == 1')
181
+ sys.exit(1)
182
+
183
+ with open(args.config, 'r') as fin:
184
+ configs = yaml.load(fin, Loader=yaml.FullLoader)
185
+
186
+ # Load dict
187
+ char_dict = {}
188
+ with open(args.dict, 'r') as fin:
189
+ for line in fin:
190
+ arr = line.strip().split()
191
+ assert len(arr) == 2
192
+ char_dict[int(arr[1])] = arr[0]
193
+ eos = len(char_dict) - 1
194
+
195
+ # Init dataset and data loader
196
+ ali_conf = copy.deepcopy(configs['dataset_conf'])
197
+
198
+ ali_conf['filter_conf']['max_length'] = 102400
199
+ ali_conf['filter_conf']['min_length'] = 0
200
+ ali_conf['filter_conf']['token_max_length'] = 102400
201
+ ali_conf['filter_conf']['token_min_length'] = 0
202
+ ali_conf['filter_conf']['max_output_input_ratio'] = 102400
203
+ ali_conf['filter_conf']['min_output_input_ratio'] = 0
204
+ ali_conf['speed_perturb'] = False
205
+ ali_conf['spec_aug'] = False
206
+ ali_conf['spec_trim'] = False
207
+ ali_conf['shuffle'] = False
208
+ ali_conf['sort'] = False
209
+ ali_conf['fbank_conf']['dither'] = 0.0
210
+ ali_conf['batch_conf']['batch_type'] = "static"
211
+ ali_conf['batch_conf']['batch_size'] = args.batch_size
212
+
213
+ tokenizer = init_tokenizer(configs)
214
+ ali_dataset = Dataset(args.data_type,
215
+ args.input_file,
216
+ tokenizer,
217
+ ali_conf,
218
+ partition=False)
219
+
220
+ ali_data_loader = DataLoader(ali_dataset, batch_size=None, num_workers=0)
221
+
222
+ # Init asr model from configs
223
+ model, configs = init_model(args, configs)
224
+
225
+ device = torch.device(args.device)
226
+ model = model.to(device)
227
+
228
+ model.eval()
229
+ with torch.no_grad(), open(args.result_file, 'w',
230
+ encoding='utf-8') as fout:
231
+ for batch_idx, batch in enumerate(ali_data_loader):
232
+ print("#" * 80)
233
+ key, feat, target, feats_length, target_length = batch
234
+
235
+ feat = feat.to(device)
236
+ target = target.to(device)
237
+ feats_length = feats_length.to(device)
238
+ target_length = target_length.to(device)
239
+ # Let's assume B = batch_size and N = beam_size
240
+ # 1. Encoder
241
+ encoder_out, encoder_mask = model._forward_encoder(
242
+ feat, feats_length) # (B, maxlen, encoder_dim)
243
+ maxlen = encoder_out.size(1)
244
+ ctc_probs = model.ctc.log_softmax(
245
+ encoder_out) # (1, maxlen, vocab_size)
246
+ # print(ctc_probs.size(1))
247
+ ctc_probs = ctc_probs.squeeze(0)
248
+ target = target.squeeze(0)
249
+ alignment = force_align(ctc_probs, target)
250
+ fout.write('{} {}\n'.format(key[0], alignment))
251
+
252
+ if args.gen_praat:
253
+ timestamp = get_frames_timestamp(alignment, ctc_probs,
254
+ args.blank_thres, args.thres)
255
+ subsample = get_subsample(configs)
256
+ labformat = get_labformat(timestamp, subsample)
257
+
258
+ lab_path = os.path.join(os.path.dirname(args.result_file),
259
+ key[0] + ".lab")
260
+ with open(lab_path, 'w', encoding='utf-8') as f:
261
+ f.writelines(labformat)
262
+
263
+ textgrid_path = os.path.join(os.path.dirname(args.result_file),
264
+ key[0] + ".TextGrid")
265
+ generator_textgrid(maxtime=(len(alignment) + 1) * 0.01 *
266
+ subsample,
267
+ lines=labformat,
268
+ output=textgrid_path)
wenet/bin/average_model.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2020 Mobvoi Inc (Di Wu)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import argparse
17
+ import glob
18
+ import sys
19
+
20
+ import yaml
21
+ import torch
22
+
23
+
24
+ def get_args():
25
+ parser = argparse.ArgumentParser(description='average model')
26
+ parser.add_argument('--dst_model', required=True, help='averaged model')
27
+ parser.add_argument('--src_path',
28
+ required=True,
29
+ help='src model path for average')
30
+ parser.add_argument('--val_best',
31
+ action="store_true",
32
+ help='averaged model')
33
+ parser.add_argument('--num',
34
+ default=5,
35
+ type=int,
36
+ help='nums for averaged model')
37
+ parser.add_argument('--min_epoch',
38
+ default=0,
39
+ type=int,
40
+ help='min epoch used for averaging model')
41
+ parser.add_argument('--max_epoch',
42
+ default=sys.maxsize,
43
+ type=int,
44
+ help='max epoch used for averaging model')
45
+ parser.add_argument('--min_step',
46
+ default=0,
47
+ type=int,
48
+ help='min step used for averaging model')
49
+ parser.add_argument('--max_step',
50
+ default=sys.maxsize,
51
+ type=int,
52
+ help='max step used for averaging model')
53
+ parser.add_argument('--mode',
54
+ default="hybrid",
55
+ choices=["hybrid", "epoch", "step"],
56
+ type=str,
57
+ help='average mode')
58
+
59
+ args = parser.parse_args()
60
+ print(args)
61
+ return args
62
+
63
+
64
+ def main():
65
+ args = get_args()
66
+ checkpoints = []
67
+ val_scores = []
68
+ if args.val_best:
69
+ if args.mode == "hybrid":
70
+ yamls = glob.glob('{}/*.yaml'.format(args.src_path))
71
+ yamls = [
72
+ f for f in yamls
73
+ if not (os.path.basename(f).startswith('train')
74
+ or os.path.basename(f).startswith('init'))
75
+ ]
76
+ elif args.mode == "step":
77
+ yamls = glob.glob('{}/step_*.yaml'.format(args.src_path))
78
+ else:
79
+ yamls = glob.glob('{}/epoch_*.yaml'.format(args.src_path))
80
+ for y in yamls:
81
+ with open(y, 'r') as f:
82
+ dic_yaml = yaml.load(f, Loader=yaml.FullLoader)
83
+ loss = dic_yaml['loss_dict']['loss']
84
+ epoch = dic_yaml['epoch']
85
+ step = dic_yaml['step']
86
+ tag = dic_yaml['tag']
87
+ if epoch >= args.min_epoch and epoch <= args.max_epoch \
88
+ and step >= args.min_step and step <= args.max_step:
89
+ val_scores += [[epoch, step, loss, tag]]
90
+ sorted_val_scores = sorted(val_scores,
91
+ key=lambda x: x[2],
92
+ reverse=False)
93
+ print("best val (epoch, step, loss, tag) = " +
94
+ str(sorted_val_scores[:args.num]))
95
+ path_list = [
96
+ args.src_path + '/{}.pt'.format(score[-1])
97
+ for score in sorted_val_scores[:args.num]
98
+ ]
99
+ else:
100
+ path_list = glob.glob('{}/[!init]*.pt'.format(args.src_path))
101
+ path_list = sorted(path_list, key=os.path.getmtime)
102
+ path_list = path_list[-args.num:]
103
+ print(path_list)
104
+ avg = {}
105
+ num = args.num
106
+ assert num == len(path_list)
107
+ for path in path_list:
108
+ print('Processing {}'.format(path))
109
+ states = torch.load(path, map_location=torch.device('cpu'))
110
+ for k in states.keys():
111
+ if k not in avg.keys():
112
+ avg[k] = states[k].clone()
113
+ else:
114
+ avg[k] += states[k]
115
+ # average
116
+ for k in avg.keys():
117
+ if avg[k] is not None:
118
+ # pytorch 1.6 use true_divide instead of /=
119
+ avg[k] = torch.true_divide(avg[k], num)
120
+ print('Saving to {}'.format(args.dst_model))
121
+ torch.save(avg, args.dst_model)
122
+
123
+
124
+ if __name__ == '__main__':
125
+ main()
wenet/bin/export_ipex.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2021-2023 Intel Corporation
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import print_function
5
+
6
+ import argparse
7
+ import logging
8
+ import os
9
+
10
+ import torch
11
+ import yaml
12
+
13
+ from wenet.utils.init_model import init_model
14
+ import intel_extension_for_pytorch as ipex
15
+ from intel_extension_for_pytorch.quantization import prepare, convert
16
+
17
+
18
+ def get_args():
19
+ parser = argparse.ArgumentParser(description='export your script model')
20
+ parser.add_argument('--config', required=True, help='config file')
21
+ parser.add_argument('--checkpoint', required=True, help='checkpoint model')
22
+ parser.add_argument('--output_file', default=None, help='output file')
23
+ parser.add_argument('--dtype',
24
+ default="fp32",
25
+ help='choose the dtype to run:[fp32,bf16]')
26
+ parser.add_argument('--output_quant_file',
27
+ default=None,
28
+ help='output quantized model file')
29
+ args = parser.parse_args()
30
+ return args
31
+
32
+
33
+ def scripting(model):
34
+ with torch.inference_mode():
35
+ script_model = torch.jit.script(model)
36
+ script_model = torch.jit.freeze(
37
+ script_model,
38
+ preserved_attrs=[
39
+ "forward_encoder_chunk", "ctc_activation",
40
+ "forward_attention_decoder", "subsampling_rate",
41
+ "right_context", "sos_symbol", "eos_symbol",
42
+ "is_bidirectional_decoder"
43
+ ])
44
+ return script_model
45
+
46
+
47
+ def main():
48
+ args = get_args()
49
+ logging.basicConfig(level=logging.DEBUG,
50
+ format='%(asctime)s %(levelname)s %(message)s')
51
+ # No need gpu for model export
52
+ os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
53
+
54
+ with open(args.config, 'r') as fin:
55
+ configs = yaml.load(fin, Loader=yaml.FullLoader)
56
+ model, configs = init_model(args, configs)
57
+ print(model)
58
+
59
+ # Apply IPEX optimization
60
+ model.eval()
61
+ torch._C._jit_set_texpr_fuser_enabled(False)
62
+ model.to(memory_format=torch.channels_last)
63
+ if args.dtype == "fp32":
64
+ ipex_model = ipex.optimize(model)
65
+ elif args.dtype == "bf16": # For Intel 4th generation Xeon (SPR)
66
+ ipex_model = ipex.optimize(model,
67
+ dtype=torch.bfloat16,
68
+ weights_prepack=False)
69
+
70
+ # Export jit torch script model
71
+ if args.output_file:
72
+ if args.dtype == "fp32":
73
+ script_model = scripting(ipex_model)
74
+ elif args.dtype == "bf16":
75
+ torch._C._jit_set_autocast_mode(True)
76
+ with torch.cpu.amp.autocast():
77
+ script_model = scripting(ipex_model)
78
+ script_model.save(args.output_file)
79
+ print('Export model successfully, see {}'.format(args.output_file))
80
+
81
+ # Export quantized jit torch script model
82
+ if args.output_quant_file:
83
+ dynamic_qconfig = ipex.quantization.default_dynamic_qconfig
84
+ dummy_data = (torch.zeros(1, 67, 80), 16, -16,
85
+ torch.zeros(12, 4, 32, 128), torch.zeros(12, 1, 256, 7))
86
+ model = prepare(model, dynamic_qconfig, dummy_data)
87
+ model = convert(model)
88
+ script_quant_model = scripting(model)
89
+ script_quant_model.save(args.output_quant_file)
90
+ print('Export quantized model successfully, '
91
+ 'see {}'.format(args.output_quant_file))
92
+
93
+
94
+ if __name__ == '__main__':
95
+ main()
wenet/bin/export_jit.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2020 Mobvoi Inc. (authors: Binbin Zhang, Di Wu)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import print_function
16
+
17
+ import argparse
18
+ import logging
19
+ import os
20
+
21
+ import torch
22
+ import yaml
23
+
24
+ from wenet.utils.init_model import init_model
25
+
26
+
27
+ def get_args():
28
+ parser = argparse.ArgumentParser(description='export your script model')
29
+ parser.add_argument('--config', required=True, help='config file')
30
+ parser.add_argument('--checkpoint', required=True, help='checkpoint model')
31
+ parser.add_argument('--output_file', default=None, help='output file')
32
+ parser.add_argument('--output_quant_file',
33
+ default=None,
34
+ help='output quantized model file')
35
+ args = parser.parse_args()
36
+ return args
37
+
38
+
39
+ def main():
40
+ args = get_args()
41
+ args.jit = True
42
+ logging.basicConfig(level=logging.DEBUG,
43
+ format='%(asctime)s %(levelname)s %(message)s')
44
+ # No need gpu for model export
45
+ os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
46
+
47
+ with open(args.config, 'r') as fin:
48
+ configs = yaml.load(fin, Loader=yaml.FullLoader)
49
+ model, configs = init_model(args, configs)
50
+ model.eval()
51
+ print(model)
52
+ # Export jit torch script model
53
+
54
+ if args.output_file:
55
+ script_model = torch.jit.script(model)
56
+ script_model.save(args.output_file)
57
+ print('Export model successfully, see {}'.format(args.output_file))
58
+
59
+ # Export quantized jit torch script model
60
+ if args.output_quant_file:
61
+ quantized_model = torch.quantization.quantize_dynamic(
62
+ model, {torch.nn.Linear}, dtype=torch.qint8)
63
+ print(quantized_model)
64
+ script_quant_model = torch.jit.script(quantized_model)
65
+ script_quant_model.save(args.output_quant_file)
66
+ print('Export quantized model successfully, '
67
+ 'see {}'.format(args.output_quant_file))
68
+
69
+
70
+ if __name__ == '__main__':
71
+ main()
wenet/bin/export_onnx_bpu.py ADDED
@@ -0,0 +1,1065 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022, Horizon Inc. Xingchen Song (sxc19@tsinghua.org.cn)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """NOTE(xcsong): Currently, we only support
15
+ 1. specific conformer encoder architecture, see:
16
+ encoder: conformer
17
+ encoder_conf:
18
+ activation_type: **must be** relu
19
+ attention_heads: 2 or 4 or 8 or any number divisible by output_size
20
+ causal: **must be** true
21
+ cnn_module_kernel: 1 ~ 7
22
+ cnn_module_norm: **must be** batch_norm
23
+ input_layer: **must be** conv2d8
24
+ linear_units: 1 ~ 2048
25
+ normalize_before: **must be** true
26
+ num_blocks: 1 ~ 12
27
+ output_size: 1 ~ 512
28
+ pos_enc_layer_type: **must be** no_pos
29
+ selfattention_layer_type: **must be** selfattn
30
+ use_cnn_module: **must be** true
31
+ use_dynamic_chunk: **must be** true
32
+ use_dynamic_left_chunk: **must be** true
33
+
34
+ 2. specific decoding method: ctc_greedy_search
35
+ """
36
+
37
+ from __future__ import print_function
38
+
39
+ import os
40
+ import sys
41
+ import copy
42
+ import math
43
+ import yaml
44
+ import logging
45
+ from typing import Tuple
46
+
47
+ import torch
48
+ import numpy as np
49
+
50
+ from wenet.transformer.embedding import NoPositionalEncoding
51
+ from wenet.utils.init_model import init_model
52
+ from wenet.bin.export_onnx_cpu import (get_args, to_numpy,
53
+ print_input_output_info)
54
+
55
+ try:
56
+ import onnx
57
+ import onnxruntime
58
+ except ImportError:
59
+ print('Please install onnx and onnxruntime!')
60
+ sys.exit(1)
61
+
62
+ logger = logging.getLogger(__file__)
63
+ logger.setLevel(logging.INFO)
64
+
65
+
66
+ class BPULayerNorm(torch.nn.Module):
67
+ """Refactor torch.nn.LayerNorm to meet 4-D dataflow."""
68
+
69
+ def __init__(self, module, chunk_size=8, run_on_bpu=False):
70
+ super().__init__()
71
+ original = copy.deepcopy(module)
72
+ self.hidden = module.weight.size(0)
73
+ self.chunk_size = chunk_size
74
+ self.run_on_bpu = run_on_bpu
75
+
76
+ if self.run_on_bpu:
77
+ self.weight = torch.nn.Parameter(
78
+ module.weight.reshape(1, self.hidden, 1,
79
+ 1).repeat(1, 1, 1, chunk_size))
80
+ self.bias = torch.nn.Parameter(
81
+ module.bias.reshape(1, self.hidden, 1,
82
+ 1).repeat(1, 1, 1, chunk_size))
83
+ self.negtive = torch.nn.Parameter(
84
+ torch.ones((1, self.hidden, 1, chunk_size)) * -1.0)
85
+ self.eps = torch.nn.Parameter(
86
+ torch.zeros((1, self.hidden, 1, chunk_size)) + module.eps)
87
+ self.mean_conv_1 = torch.nn.Conv2d(self.hidden, 1, 1, bias=False)
88
+ self.mean_conv_1.weight = torch.nn.Parameter(
89
+ torch.ones(self.hidden, self.hidden, 1, 1) /
90
+ (1.0 * self.hidden))
91
+ self.mean_conv_2 = torch.nn.Conv2d(self.hidden, 1, 1, bias=False)
92
+ self.mean_conv_2.weight = torch.nn.Parameter(
93
+ torch.ones(self.hidden, self.hidden, 1, 1) /
94
+ (1.0 * self.hidden))
95
+ else:
96
+ self.norm = module
97
+
98
+ self.check_equal(original)
99
+
100
+ def check_equal(self, module):
101
+ random_data = torch.randn(1, self.chunk_size, self.hidden)
102
+ orig_out = module(random_data)
103
+ new_out = self.forward(random_data.transpose(1, 2).unsqueeze(2))
104
+ np.testing.assert_allclose(to_numpy(orig_out),
105
+ to_numpy(
106
+ new_out.squeeze(2).transpose(1, 2)),
107
+ rtol=1e-02,
108
+ atol=1e-03)
109
+
110
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
111
+ if self.run_on_bpu:
112
+ u = self.mean_conv_1(x) # (1, h, 1, c)
113
+ numerator = x + u * self.negtive # (1, h, 1, c)
114
+ s = torch.pow(numerator, 2) # (1, h, 1, c)
115
+ s = self.mean_conv_2(s) # (1, h, 1, c)
116
+ denominator = torch.sqrt(s + self.eps) # (1, h, 1, c)
117
+ x = torch.div(numerator, denominator) # (1, h, 1, c)
118
+ x = x * self.weight + self.bias
119
+ else:
120
+ x = x.squeeze(2).transpose(1, 2).contiguous()
121
+ x = self.norm(x)
122
+ x = x.transpose(1, 2).contiguous().unsqueeze(2)
123
+ return x
124
+
125
+
126
+ class BPUIdentity(torch.nn.Module):
127
+ """Refactor torch.nn.Identity().
128
+ For inserting BPU node whose input == output.
129
+ """
130
+
131
+ def __init__(self, channels):
132
+ super().__init__()
133
+ self.channels = channels
134
+ self.identity_conv = torch.nn.Conv2d(channels,
135
+ channels,
136
+ 1,
137
+ groups=channels,
138
+ bias=False)
139
+ torch.nn.init.dirac_(self.identity_conv.weight.data, groups=channels)
140
+
141
+ self.check_equal()
142
+
143
+ def check_equal(self):
144
+ random_data = torch.randn(1, self.channels, 1, 10)
145
+ result = self.forward(random_data)
146
+ np.testing.assert_allclose(to_numpy(random_data),
147
+ to_numpy(result),
148
+ rtol=1e-02,
149
+ atol=1e-03)
150
+
151
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
152
+ """Identity with 4-D dataflow, input == output.
153
+ Args:
154
+ x (torch.Tensor): (batch, in_channel, 1, time)
155
+
156
+ Returns:
157
+ (torch.Tensor): (batch, in_channel, 1, time).
158
+ """
159
+ return self.identity_conv(x)
160
+
161
+
162
+ class BPULinear(torch.nn.Module):
163
+ """Refactor torch.nn.Linear or pointwise_conv"""
164
+
165
+ def __init__(self, module, is_pointwise_conv=False):
166
+ super().__init__()
167
+ # Unchanged submodules and attributes
168
+ original = copy.deepcopy(module)
169
+ self.idim = module.weight.size(1)
170
+ self.odim = module.weight.size(0)
171
+ self.is_pointwise_conv = is_pointwise_conv
172
+
173
+ # Modify weight & bias
174
+ self.linear = torch.nn.Conv2d(self.idim, self.odim, 1, 1)
175
+ if is_pointwise_conv:
176
+ # (odim, idim, kernel=1) -> (odim, idim, 1, 1)
177
+ self.linear.weight = torch.nn.Parameter(
178
+ module.weight.unsqueeze(-1))
179
+ else:
180
+ # (odim, idim) -> (odim, idim, 1, 1)
181
+ self.linear.weight = torch.nn.Parameter(
182
+ module.weight.unsqueeze(2).unsqueeze(3))
183
+ self.linear.bias = module.bias
184
+
185
+ self.check_equal(original)
186
+
187
+ def check_equal(self, module):
188
+ random_data = torch.randn(1, 8, self.idim)
189
+ if self.is_pointwise_conv:
190
+ random_data = random_data.transpose(1, 2)
191
+ original_result = module(random_data)
192
+ if self.is_pointwise_conv:
193
+ random_data = random_data.transpose(1, 2)
194
+ original_result = original_result.transpose(1, 2)
195
+ random_data = random_data.transpose(1, 2).unsqueeze(2)
196
+ new_result = self.forward(random_data)
197
+ np.testing.assert_allclose(to_numpy(original_result),
198
+ to_numpy(
199
+ new_result.squeeze(2).transpose(1, 2)),
200
+ rtol=1e-02,
201
+ atol=1e-03)
202
+
203
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
204
+ """Linear with 4-D dataflow.
205
+ Args:
206
+ x (torch.Tensor): (batch, in_channel, 1, time)
207
+ Returns:
208
+ (torch.Tensor): (batch, out_channel, 1, time).
209
+ """
210
+ return self.linear(x)
211
+
212
+
213
+ class BPUGlobalCMVN(torch.nn.Module):
214
+ """Refactor wenet/transformer/cmvn.py::GlobalCMVN"""
215
+
216
+ def __init__(self, module):
217
+ super().__init__()
218
+ # Unchanged submodules and attributes
219
+ self.norm_var = module.norm_var
220
+
221
+ # NOTE(xcsong): Expand to 4-D tensor, (mel_dim) -> (1, 1, mel_dim, 1)
222
+ self.mean = module.mean.unsqueeze(-1).unsqueeze(0).unsqueeze(0)
223
+ self.istd = module.istd.unsqueeze(-1).unsqueeze(0).unsqueeze(0)
224
+
225
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
226
+ """CMVN with 4-D dataflow.
227
+ Args:
228
+ x (torch.Tensor): (batch, 1, mel_dim, time)
229
+ Returns:
230
+ (torch.Tensor): normalized feature with same shape.
231
+ """
232
+ x = x - self.mean
233
+ if self.norm_var:
234
+ x = x * self.istd
235
+ return x
236
+
237
+
238
+ class BPUConv2dSubsampling8(torch.nn.Module):
239
+ """Refactor wenet/transformer/subsampling.py::Conv2dSubsampling8
240
+
241
+ NOTE(xcsong): Only support pos_enc_class == NoPositionalEncoding
242
+ """
243
+
244
+ def __init__(self, module):
245
+ super().__init__()
246
+ # Unchanged submodules and attributes
247
+ original = copy.deepcopy(module)
248
+ self.right_context = module.right_context
249
+ self.subsampling_rate = module.subsampling_rate
250
+ assert isinstance(module.pos_enc, NoPositionalEncoding)
251
+
252
+ # 1. Modify self.conv
253
+ # NOTE(xcsong): We change input shape from (1, 1, frames, mel_dim)
254
+ # to (1, 1, mel_dim, frames) for more efficient computation.
255
+ self.conv = module.conv
256
+ for idx in [0, 2, 4]:
257
+ self.conv[idx].weight = torch.nn.Parameter(
258
+ module.conv[idx].weight.transpose(2, 3))
259
+
260
+ # 2. Modify self.linear
261
+ # NOTE(xcsong): Split final projection to meet the requirment of
262
+ # maximum kernel_size (7 for XJ3)
263
+ self.linear = torch.nn.ModuleList()
264
+ odim = module.linear.weight.size(0) # 512, in this case
265
+ freq = module.linear.weight.size(1) // odim # 4608 // 512 == 9
266
+ self.odim, self.freq = odim, freq
267
+ weight = module.linear.weight.reshape(
268
+ odim, odim, freq,
269
+ 1) # (odim, odim * freq) -> (odim, odim, freq, 1)
270
+ self.split_size = []
271
+ num_split = (freq - 1) // 7 + 1 # XJ3 requires kernel_size <= 7
272
+ slice_begin = 0
273
+ for idx in range(num_split):
274
+ kernel_size = min(freq, (idx + 1) * 7) - idx * 7
275
+ conv_ele = torch.nn.Conv2d(odim, odim, (kernel_size, 1),
276
+ (kernel_size, 1))
277
+ conv_ele.weight = torch.nn.Parameter(
278
+ weight[:, :, slice_begin:slice_begin + kernel_size, :])
279
+ conv_ele.bias = torch.nn.Parameter(torch.zeros_like(conv_ele.bias))
280
+ self.linear.append(conv_ele)
281
+ self.split_size.append(kernel_size)
282
+ slice_begin += kernel_size
283
+ self.linear[0].bias = torch.nn.Parameter(module.linear.bias)
284
+
285
+ self.check_equal(original)
286
+
287
+ def check_equal(self, module):
288
+ random_data = torch.randn(1, 67, 80)
289
+ mask = torch.zeros(1, 1, 67)
290
+ original_result, _, _ = module(random_data, mask) # (1, 8, 512)
291
+ random_data = random_data.transpose(1,
292
+ 2).unsqueeze(0) # (1, 1, 80, 67)
293
+ new_result = self.forward(random_data) # (1, 512, 1, 8)
294
+ np.testing.assert_allclose(to_numpy(original_result),
295
+ to_numpy(
296
+ new_result.squeeze(2).transpose(1, 2)),
297
+ rtol=1e-02,
298
+ atol=1e-03)
299
+
300
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
301
+ """Subsample x with 4-D dataflow.
302
+ Args:
303
+ x (torch.Tensor): Input tensor (#batch, 1, mel_dim, time).
304
+
305
+ Returns:
306
+ torch.Tensor: Subsampled tensor (#batch, odim, 1, time'),
307
+ where time' = time // 8.
308
+ """
309
+ x = self.conv(x) # (1, odim, freq, time')
310
+ x_out = torch.zeros(x.size(0), self.odim, 1, x.size(3))
311
+ x = torch.split(x, self.split_size, dim=2)
312
+ for idx, (x_part, layer) in enumerate(zip(x, self.linear)):
313
+ x_out += layer(x_part)
314
+ return x_out
315
+
316
+
317
+ class BPUMultiHeadedAttention(torch.nn.Module):
318
+ """Refactor wenet/transformer/attention.py::MultiHeadedAttention
319
+
320
+ NOTE(xcsong): Only support attention_class == MultiHeadedAttention,
321
+ we do not consider RelPositionMultiHeadedAttention currently.
322
+ """
323
+
324
+ def __init__(self, module, chunk_size, left_chunks):
325
+ super().__init__()
326
+ # Unchanged submodules and attributes
327
+ original = copy.deepcopy(module)
328
+ self.d_k = module.d_k
329
+ self.h = module.h
330
+ n_feat = self.d_k * self.h
331
+ self.chunk_size = chunk_size
332
+ self.left_chunks = left_chunks
333
+ self.time = chunk_size * (left_chunks + 1)
334
+ self.activation = torch.nn.Softmax(dim=-1)
335
+
336
+ # 1. Modify self.linear_x
337
+ self.linear_q = BPULinear(module.linear_q)
338
+ self.linear_k = BPULinear(module.linear_k)
339
+ self.linear_v = BPULinear(module.linear_v)
340
+ self.linear_out = BPULinear(module.linear_out)
341
+ # 2. denom
342
+ self.register_buffer(
343
+ "denom", torch.full((1, self.h, 1, 1), 1.0 / math.sqrt(self.d_k)))
344
+
345
+ self.check_equal(original)
346
+
347
+ def check_equal(self, module):
348
+ random_data = torch.randn(1, self.chunk_size, self.d_k * self.h)
349
+ mask = torch.ones((1, self.h, self.chunk_size, self.time),
350
+ dtype=torch.bool)
351
+ cache = torch.zeros(1, self.h, self.chunk_size * self.left_chunks,
352
+ self.d_k * 2)
353
+ original_out, original_cache = module(random_data, random_data,
354
+ random_data, mask[:, 0, :, :],
355
+ torch.empty(0), cache)
356
+ random_data = random_data.transpose(1, 2).unsqueeze(2)
357
+ cache = cache.reshape(1, self.h, self.d_k * 2,
358
+ self.chunk_size * self.left_chunks)
359
+ new_out, new_cache = self.forward(random_data, random_data,
360
+ random_data, mask, cache)
361
+ np.testing.assert_allclose(to_numpy(original_out),
362
+ to_numpy(
363
+ new_out.squeeze(2).transpose(1, 2)),
364
+ rtol=1e-02,
365
+ atol=1e-03)
366
+ np.testing.assert_allclose(to_numpy(original_cache),
367
+ to_numpy(new_cache.transpose(2, 3)),
368
+ rtol=1e-02,
369
+ atol=1e-03)
370
+
371
+ def forward(
372
+ self,
373
+ q: torch.Tensor,
374
+ k: torch.Tensor,
375
+ v: torch.Tensor,
376
+ mask: torch.Tensor,
377
+ cache: torch.Tensor,
378
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
379
+ """Compute scaled dot product attention.
380
+
381
+ Args:
382
+ q (torch.Tensor): Query tensor (#batch, size, 1, chunk_size).
383
+ k (torch.Tensor): Key tensor (#batch, size, 1, chunk_size).
384
+ v (torch.Tensor): Value tensor (#batch, size, 1, chunk_size).
385
+ mask (torch.Tensor): Mask tensor,
386
+ (#batch, head, chunk_size, cache_t + chunk_size).
387
+ cache (torch.Tensor): Cache tensor
388
+ (1, head, d_k * 2, cache_t),
389
+ where `cache_t == chunk_size * left_chunks`.
390
+
391
+
392
+ Returns:
393
+ torch.Tensor: Output tensor (#batch, size, 1, chunk_size).
394
+ torch.Tensor: Cache tensor
395
+ (1, head, d_k * 2, cache_t + chunk_size)
396
+ where `cache_t == chunk_size * left_chunks`
397
+ """
398
+ # 1. Forward QKV
399
+ q = self.linear_q(q) # (1, d, 1, c) d == size, c == chunk_size
400
+ k = self.linear_k(k) # (1, d, 1, c)
401
+ v = self.linear_v(v) # (1, d, 1, c)
402
+ q = q.view(1, self.h, self.d_k, self.chunk_size)
403
+ k = k.view(1, self.h, self.d_k, self.chunk_size)
404
+ v = v.view(1, self.h, self.d_k, self.chunk_size)
405
+ q = q.transpose(2, 3) # (batch, head, time1, d_k)
406
+ k_cache, v_cache = torch.split(cache, cache.size(2) // 2, dim=2)
407
+ k = torch.cat((k_cache, k), dim=3)
408
+ v = torch.cat((v_cache, v), dim=3)
409
+ new_cache = torch.cat((k, v), dim=2)
410
+ # 2. (Q^T)K
411
+ scores = torch.matmul(q, k) * self.denom # (#b, n_head, time1, time2)
412
+ # 3. Forward attention
413
+ mask = mask.eq(0)
414
+ scores = scores.masked_fill(mask, -float('inf'))
415
+ attn = self.activation(scores).masked_fill(mask, 0.0)
416
+ attn = attn.transpose(2, 3)
417
+ x = torch.matmul(v, attn)
418
+ x = x.view(1, self.d_k * self.h, 1, self.chunk_size)
419
+ x_out = self.linear_out(x)
420
+ return x_out, new_cache
421
+
422
+
423
+ class BPUConvolution(torch.nn.Module):
424
+ """Refactor wenet/transformer/convolution.py::ConvolutionModule
425
+
426
+ NOTE(xcsong): Only suport use_layer_norm == False
427
+ """
428
+
429
+ def __init__(self, module):
430
+ super().__init__()
431
+ # Unchanged submodules and attributes
432
+ original = copy.deepcopy(module)
433
+ self.lorder = module.lorder
434
+ self.use_layer_norm = False
435
+ self.activation = module.activation
436
+ channels = module.pointwise_conv1.weight.size(1)
437
+ self.channels = channels
438
+ kernel_size = module.depthwise_conv.weight.size(2)
439
+ assert module.use_layer_norm is False
440
+
441
+ # 1. Modify self.pointwise_conv1
442
+ self.pointwise_conv1 = BPULinear(module.pointwise_conv1, True)
443
+
444
+ # 2. Modify self.depthwise_conv
445
+ self.depthwise_conv = torch.nn.Conv2d(channels,
446
+ channels, (1, kernel_size),
447
+ stride=1,
448
+ groups=channels)
449
+ self.depthwise_conv.weight = torch.nn.Parameter(
450
+ module.depthwise_conv.weight.unsqueeze(-2))
451
+ self.depthwise_conv.bias = torch.nn.Parameter(
452
+ module.depthwise_conv.bias)
453
+
454
+ # 3. Modify self.norm, Only support batchnorm2d
455
+ self.norm = torch.nn.BatchNorm2d(channels)
456
+ self.norm.training = False
457
+ self.norm.num_features = module.norm.num_features
458
+ self.norm.eps = module.norm.eps
459
+ self.norm.momentum = module.norm.momentum
460
+ self.norm.weight = torch.nn.Parameter(module.norm.weight)
461
+ self.norm.bias = torch.nn.Parameter(module.norm.bias)
462
+ self.norm.running_mean = module.norm.running_mean
463
+ self.norm.running_var = module.norm.running_var
464
+
465
+ # 4. Modify self.pointwise_conv2
466
+ self.pointwise_conv2 = BPULinear(module.pointwise_conv2, True)
467
+
468
+ # 5. Identity conv, for running `concat` on BPU
469
+ self.identity = BPUIdentity(channels)
470
+
471
+ self.check_equal(original)
472
+
473
+ def check_equal(self, module):
474
+ random_data = torch.randn(1, 8, self.channels)
475
+ cache = torch.zeros((1, self.channels, self.lorder))
476
+ original_out, original_cache = module(random_data, cache=cache)
477
+ random_data = random_data.transpose(1, 2).unsqueeze(2)
478
+ cache = cache.unsqueeze(2)
479
+ new_out, new_cache = self.forward(random_data, cache)
480
+ np.testing.assert_allclose(to_numpy(original_out),
481
+ to_numpy(
482
+ new_out.squeeze(2).transpose(1, 2)),
483
+ rtol=1e-02,
484
+ atol=1e-03)
485
+ np.testing.assert_allclose(to_numpy(original_cache),
486
+ to_numpy(new_cache.squeeze(2)),
487
+ rtol=1e-02,
488
+ atol=1e-03)
489
+
490
+ def forward(self, x: torch.Tensor,
491
+ cache: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
492
+ """Compute convolution module.
493
+ Args:
494
+ x (torch.Tensor): Input tensor (#batch, channels, 1, chunk_size).
495
+ cache (torch.Tensor): left context cache, it is only
496
+ used in causal convolution (#batch, channels, 1, cache_t).
497
+ Returns:
498
+ torch.Tensor: Output tensor (#batch, channels, 1, chunk_size).
499
+ torch.Tensor: Cache tensor (#batch, channels, 1, cache_t).
500
+ """
501
+ # Concat cache
502
+ x = torch.cat((self.identity(cache), self.identity(x)), dim=3)
503
+ new_cache = x[:, :, :, -self.lorder:]
504
+
505
+ # GLU mechanism
506
+ x = self.pointwise_conv1(x) # (batch, 2*channel, 1, dim)
507
+ x = torch.nn.functional.glu(x, dim=1) # (b, channel, 1, dim)
508
+
509
+ # Depthwise Conv
510
+ x = self.depthwise_conv(x)
511
+ x = self.activation(self.norm(x))
512
+ x = self.pointwise_conv2(x)
513
+ return x, new_cache
514
+
515
+
516
+ class BPUFFN(torch.nn.Module):
517
+ """Refactor wenet/transformer/positionwise_feed_forward.py::PositionwiseFeedForward
518
+ """
519
+
520
+ def __init__(self, module):
521
+ super().__init__()
522
+ # Unchanged submodules and attributes
523
+ original = copy.deepcopy(module)
524
+ self.activation = module.activation
525
+
526
+ # 1. Modify self.w_x
527
+ self.w_1 = BPULinear(module.w_1)
528
+ self.w_2 = BPULinear(module.w_2)
529
+
530
+ self.check_equal(original)
531
+
532
+ def check_equal(self, module):
533
+ random_data = torch.randn(1, 8, self.w_1.idim)
534
+ original_out = module(random_data)
535
+ random_data = random_data.transpose(1, 2).unsqueeze(2)
536
+ new_out = self.forward(random_data)
537
+ np.testing.assert_allclose(to_numpy(original_out),
538
+ to_numpy(
539
+ new_out.squeeze(2).transpose(1, 2)),
540
+ rtol=1e-02,
541
+ atol=1e-03)
542
+
543
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
544
+ """Forward function.
545
+
546
+ Args:
547
+ xs: input tensor (B, D, 1, L)
548
+ Returns:
549
+ output tensor, (B, D, 1, L)
550
+ """
551
+ return self.w_2(self.activation(self.w_1(x)))
552
+
553
+
554
+ class BPUConformerEncoderLayer(torch.nn.Module):
555
+ """Refactor wenet/transformer/encoder_layer.py::ConformerEncoderLayer
556
+ """
557
+
558
+ def __init__(self, module, chunk_size, left_chunks, ln_run_on_bpu=False):
559
+ super().__init__()
560
+ # Unchanged submodules and attributes
561
+ original = copy.deepcopy(module)
562
+ self.size = module.size
563
+ assert module.normalize_before is True
564
+ assert module.concat_after is False
565
+
566
+ # 1. Modify submodules
567
+ self.feed_forward_macaron = BPUFFN(module.feed_forward_macaron)
568
+ self.self_attn = BPUMultiHeadedAttention(module.self_attn, chunk_size,
569
+ left_chunks)
570
+ self.conv_module = BPUConvolution(module.conv_module)
571
+ self.feed_forward = BPUFFN(module.feed_forward)
572
+
573
+ # 2. Modify norms
574
+ self.norm_ff = BPULayerNorm(module.norm_ff, chunk_size, ln_run_on_bpu)
575
+ self.norm_mha = BPULayerNorm(module.norm_mha, chunk_size,
576
+ ln_run_on_bpu)
577
+ self.norm_ff_macron = BPULayerNorm(module.norm_ff_macaron, chunk_size,
578
+ ln_run_on_bpu)
579
+ self.norm_conv = BPULayerNorm(module.norm_conv, chunk_size,
580
+ ln_run_on_bpu)
581
+ self.norm_final = BPULayerNorm(module.norm_final, chunk_size,
582
+ ln_run_on_bpu)
583
+
584
+ # 3. 4-D ff_scale
585
+ self.register_buffer("ff_scale",
586
+ torch.full((1, self.size, 1, 1), module.ff_scale))
587
+
588
+ self.check_equal(original)
589
+
590
+ def check_equal(self, module):
591
+ time1 = self.self_attn.chunk_size
592
+ time2 = self.self_attn.time
593
+ h, d_k = self.self_attn.h, self.self_attn.d_k
594
+ random_x = torch.randn(1, time1, self.size)
595
+ att_mask = torch.ones(1, h, time1, time2)
596
+ att_cache = torch.zeros(1, h, time2 - time1, d_k * 2)
597
+ cnn_cache = torch.zeros(1, self.size, self.conv_module.lorder)
598
+ original_x, _, original_att_cache, original_cnn_cache = module(
599
+ random_x,
600
+ att_mask[:, 0, :, :],
601
+ torch.empty(0),
602
+ att_cache=att_cache,
603
+ cnn_cache=cnn_cache)
604
+ random_x = random_x.transpose(1, 2).unsqueeze(2)
605
+ att_cache = att_cache.reshape(1, h, d_k * 2, time2 - time1)
606
+ cnn_cache = cnn_cache.unsqueeze(2)
607
+ new_x, new_att_cache, new_cnn_cache = self.forward(
608
+ random_x, att_mask, att_cache, cnn_cache)
609
+ np.testing.assert_allclose(to_numpy(original_att_cache),
610
+ to_numpy(new_att_cache.transpose(2, 3)),
611
+ rtol=1e-02,
612
+ atol=1e-03)
613
+ np.testing.assert_allclose(to_numpy(original_x),
614
+ to_numpy(new_x.squeeze(2).transpose(1, 2)),
615
+ rtol=1e-02,
616
+ atol=1e-03)
617
+ np.testing.assert_allclose(to_numpy(original_cnn_cache),
618
+ to_numpy(new_cnn_cache.squeeze(2)),
619
+ rtol=1e-02,
620
+ atol=1e-03)
621
+
622
+ def forward(
623
+ self, x: torch.Tensor, att_mask: torch.Tensor, att_cache: torch.Tensor,
624
+ cnn_cache: torch.Tensor
625
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
626
+ """Compute encoded features.
627
+
628
+ Args:
629
+ x (torch.Tensor): (#batch, size, 1, chunk_size)
630
+ att_mask (torch.Tensor): Mask tensor for the input
631
+ (#batch, head, chunk_size, cache_t1 + chunk_size),
632
+ att_cache (torch.Tensor): Cache tensor of the KEY & VALUE
633
+ (#batch=1, head, d_k * 2, cache_t1), head * d_k == size.
634
+ cnn_cache (torch.Tensor): Convolution cache in conformer layer
635
+ (#batch=1, size, 1, cache_t2)
636
+ Returns:
637
+ torch.Tensor: Output tensor (#batch, size, 1, chunk_size).
638
+ torch.Tensor: att_cache tensor,
639
+ (1, head, d_k * 2, cache_t1 + chunk_size).
640
+ torch.Tensor: cnn_cahce tensor (#batch, size, 1, cache_t2).
641
+ """
642
+ # 1. ffn_macaron
643
+ residual = x
644
+ x = self.norm_ff_macron(x)
645
+ x = residual + self.ff_scale * self.feed_forward_macaron(x)
646
+
647
+ # 2. attention
648
+ residual = x
649
+ x = self.norm_mha(x)
650
+ x_att, new_att_cache = self.self_attn(x, x, x, att_mask, att_cache)
651
+ x = residual + x_att
652
+
653
+ # 3. convolution
654
+ residual = x
655
+ x = self.norm_conv(x)
656
+ x, new_cnn_cache = self.conv_module(x, cnn_cache)
657
+ x = residual + x
658
+
659
+ # 4. ffn
660
+ residual = x
661
+ x = self.norm_ff(x)
662
+ x = residual + self.ff_scale * self.feed_forward(x)
663
+
664
+ # 5. final post-norm
665
+ x = self.norm_final(x)
666
+
667
+ return x, new_att_cache, new_cnn_cache
668
+
669
+
670
+ class BPUConformerEncoder(torch.nn.Module):
671
+ """Refactor wenet/transformer/encoder.py::ConformerEncoder
672
+ """
673
+
674
+ def __init__(self, module, chunk_size, left_chunks, ln_run_on_bpu=False):
675
+ super().__init__()
676
+ # Unchanged submodules and attributes
677
+ original = copy.deepcopy(module)
678
+ output_size = module.output_size()
679
+ self._output_size = module.output_size()
680
+ self.after_norm = module.after_norm
681
+ self.chunk_size = chunk_size
682
+ self.left_chunks = left_chunks
683
+ self.head = module.encoders[0].self_attn.h
684
+ self.layers = len(module.encoders)
685
+
686
+ # 1. Modify submodules
687
+ self.global_cmvn = BPUGlobalCMVN(module.global_cmvn)
688
+ self.embed = BPUConv2dSubsampling8(module.embed)
689
+ self.encoders = torch.nn.ModuleList()
690
+ for layer in module.encoders:
691
+ self.encoders.append(
692
+ BPUConformerEncoderLayer(layer, chunk_size, left_chunks,
693
+ ln_run_on_bpu))
694
+
695
+ # 2. Auxiliary conv
696
+ self.identity_cnncache = BPUIdentity(output_size)
697
+
698
+ self.check_equal(original)
699
+
700
+ def check_equal(self, module):
701
+ time1 = self.encoders[0].self_attn.chunk_size
702
+ time2 = self.encoders[0].self_attn.time
703
+ layers = self.layers
704
+ h, d_k = self.head, self.encoders[0].self_attn.d_k
705
+ decoding_window = (self.chunk_size - 1) * \
706
+ module.embed.subsampling_rate + \
707
+ module.embed.right_context + 1
708
+ lorder = self.encoders[0].conv_module.lorder
709
+ random_x = torch.randn(1, decoding_window, 80)
710
+ att_mask = torch.ones(1, h, time1, time2)
711
+ att_cache = torch.zeros(layers, h, time2 - time1, d_k * 2)
712
+ cnn_cache = torch.zeros(layers, 1, self._output_size, lorder)
713
+ orig_x, orig_att_cache, orig_cnn_cache = module.forward_chunk(
714
+ random_x,
715
+ 0,
716
+ time2 - time1,
717
+ att_mask=att_mask[:, 0, :, :],
718
+ att_cache=att_cache,
719
+ cnn_cache=cnn_cache)
720
+ random_x = random_x.unsqueeze(0)
721
+ att_cache = att_cache.reshape(1, h * layers, d_k * 2, time2 - time1)
722
+ cnn_cache = cnn_cache.reshape(1, self._output_size, layers, lorder)
723
+ new_x, new_att_cache, new_cnn_cache = self.forward(
724
+ random_x, att_cache, cnn_cache, att_mask)
725
+ caches = torch.split(new_att_cache, h, dim=1)
726
+ caches = [c.transpose(2, 3) for c in caches]
727
+ np.testing.assert_allclose(to_numpy(orig_att_cache),
728
+ to_numpy(torch.cat(caches, dim=0)),
729
+ rtol=1e-02,
730
+ atol=1e-03)
731
+ np.testing.assert_allclose(to_numpy(orig_x),
732
+ to_numpy(new_x.squeeze(2).transpose(1, 2)),
733
+ rtol=1e-02,
734
+ atol=1e-03)
735
+ np.testing.assert_allclose(
736
+ to_numpy(orig_cnn_cache),
737
+ to_numpy(new_cnn_cache.transpose(0, 2).transpose(1, 2)),
738
+ rtol=1e-02,
739
+ atol=1e-03)
740
+
741
+ def forward(
742
+ self, xs: torch.Tensor, att_cache: torch.Tensor,
743
+ cnn_cache: torch.Tensor, att_mask: torch.Tensor
744
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
745
+ """ Forward just one chunk
746
+
747
+ Args:
748
+ xs (torch.Tensor): chunk input, with shape (b=1, 1, time, mel-dim),
749
+ where `time == (chunk_size - 1) * subsample_rate + \
750
+ subsample.right_context + 1`
751
+ att_cache (torch.Tensor): cache tensor for KEY & VALUE in
752
+ transformer/conformer attention, with shape
753
+ (1, head * elayers, d_k * 2, cache_t1), where
754
+ `head * d_k == hidden-dim` and
755
+ `cache_t1 == chunk_size * left_chunks`.
756
+ cnn_cache (torch.Tensor): cache tensor for cnn_module in conformer,
757
+ (1, hidden-dim, elayers, cache_t2), where
758
+ `cache_t2 == cnn.lorder - 1`
759
+ att_mask (torch.Tensor): Mask tensor for the input
760
+ (#batch, head, chunk_size, cache_t1 + chunk_size),
761
+
762
+ Returns:
763
+ torch.Tensor: output of current input xs,
764
+ with shape (b=1, hidden-dim, 1, chunk_size).
765
+ torch.Tensor: new attention cache required for next chunk, with
766
+ same shape as the original att_cache.
767
+ torch.Tensor: new conformer cnn cache required for next chunk, with
768
+ same shape as the original cnn_cache.
769
+ """
770
+ # xs: (B, 1, time, mel_dim) -> (B, 1, mel_dim, time)
771
+ xs = xs.transpose(2, 3)
772
+ xs = self.global_cmvn(xs)
773
+ # xs: (B, 1, mel_dim, time) -> (B, hidden_dim, 1, chunk_size)
774
+ xs = self.embed(xs)
775
+
776
+ att_cache = torch.split(att_cache, self.head, dim=1)
777
+ cnn_cache = self.identity_cnncache(cnn_cache)
778
+ cnn_cache = torch.split(cnn_cache, 1, dim=2)
779
+ r_att_cache = []
780
+ r_cnn_cache = []
781
+ for i, layer in enumerate(self.encoders):
782
+ xs, new_att_cache, new_cnn_cache = layer(xs,
783
+ att_mask,
784
+ att_cache=att_cache[i],
785
+ cnn_cache=cnn_cache[i])
786
+ r_att_cache.append(new_att_cache[:, :, :, self.chunk_size:])
787
+ r_cnn_cache.append(new_cnn_cache)
788
+ r_att_cache = torch.cat(r_att_cache, dim=1)
789
+ r_cnn_cache = self.identity_cnncache(torch.cat(r_cnn_cache, dim=2))
790
+
791
+ xs = xs.squeeze(2).transpose(1, 2).contiguous()
792
+ xs = self.after_norm(xs)
793
+ # NOTE(xcsong): 4D in, 4D out to meet the requirment of CTC input.
794
+ xs = xs.transpose(1, 2).contiguous().unsqueeze(2) # (B, C, 1, T)
795
+
796
+ return (xs, r_att_cache, r_cnn_cache)
797
+
798
+
799
+ class BPUCTC(torch.nn.Module):
800
+ """Refactor wenet/transformer/ctc.py::CTC
801
+ """
802
+
803
+ def __init__(self, module):
804
+ super().__init__()
805
+ # Unchanged submodules and attributes
806
+ original = copy.deepcopy(module)
807
+ self.idim = module.ctc_lo.weight.size(1)
808
+ num_class = module.ctc_lo.weight.size(0)
809
+
810
+ # 1. Modify self.ctc_lo, Split final projection to meet the
811
+ # requirment of maximum in/out channels (2048 for XJ3)
812
+ self.ctc_lo = torch.nn.ModuleList()
813
+ self.split_size = []
814
+ num_split = (num_class - 1) // 2048 + 1
815
+ for idx in range(num_split):
816
+ out_channel = min(num_class, (idx + 1) * 2048) - idx * 2048
817
+ conv_ele = torch.nn.Conv2d(self.idim, out_channel, 1, 1)
818
+ self.ctc_lo.append(conv_ele)
819
+ self.split_size.append(out_channel)
820
+ orig_weight = torch.split(module.ctc_lo.weight, self.split_size, dim=0)
821
+ orig_bias = torch.split(module.ctc_lo.bias, self.split_size, dim=0)
822
+ for i, (w, b) in enumerate(zip(orig_weight, orig_bias)):
823
+ w = w.unsqueeze(2).unsqueeze(3)
824
+ self.ctc_lo[i].weight = torch.nn.Parameter(w)
825
+ self.ctc_lo[i].bias = torch.nn.Parameter(b)
826
+
827
+ self.check_equal(original)
828
+
829
+ def check_equal(self, module):
830
+ random_data = torch.randn(1, 100, self.idim)
831
+ original_result = module.ctc_lo(random_data)
832
+ random_data = random_data.transpose(1, 2).unsqueeze(2)
833
+ new_result = self.forward(random_data)
834
+ np.testing.assert_allclose(to_numpy(original_result),
835
+ to_numpy(
836
+ new_result.squeeze(2).transpose(1, 2)),
837
+ rtol=1e-02,
838
+ atol=1e-03)
839
+
840
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
841
+ """frame activations, without softmax.
842
+
843
+ Args:
844
+ Tensor x: 4d tensor (B, hidden_dim, 1, chunk_size)
845
+ Returns:
846
+ torch.Tensor: (B, num_class, 1, chunk_size)
847
+ """
848
+ out = []
849
+ for i, layer in enumerate(self.ctc_lo):
850
+ out.append(layer(x))
851
+ out = torch.cat(out, dim=1)
852
+ return out
853
+
854
+
855
+ def export_encoder(asr_model, args):
856
+ logger.info("Stage-1: export encoder")
857
+ decode_window, mel_dim = args.decoding_window, args.feature_size
858
+ encoder = BPUConformerEncoder(asr_model.encoder, args.chunk_size,
859
+ args.num_decoding_left_chunks,
860
+ args.ln_run_on_bpu)
861
+ encoder.eval()
862
+ encoder_outpath = os.path.join(args.output_dir, 'encoder.onnx')
863
+
864
+ logger.info("Stage-1.1: prepare inputs for encoder")
865
+ chunk = torch.randn((1, 1, decode_window, mel_dim))
866
+ required_cache_size = encoder.chunk_size * encoder.left_chunks
867
+ kv_time = required_cache_size + encoder.chunk_size
868
+ hidden, layers = encoder._output_size, len(encoder.encoders)
869
+ head = encoder.encoders[0].self_attn.h
870
+ d_k = hidden // head
871
+ lorder = encoder.encoders[0].conv_module.lorder
872
+ att_cache = torch.zeros(1, layers * head, d_k * 2, required_cache_size)
873
+ att_mask = torch.ones((1, head, encoder.chunk_size, kv_time))
874
+ att_mask[:, :, :, :required_cache_size] = 0
875
+ cnn_cache = torch.zeros((1, hidden, layers, lorder))
876
+ inputs = (chunk, att_cache, cnn_cache, att_mask)
877
+ logger.info("chunk.size(): {} att_cache.size(): {} "
878
+ "cnn_cache.size(): {} att_mask.size(): {}".format(
879
+ list(chunk.size()), list(att_cache.size()),
880
+ list(cnn_cache.size()), list(att_mask.size())))
881
+
882
+ logger.info("Stage-1.2: torch.onnx.export")
883
+ # NOTE(xcsong): Below attributes will be used in
884
+ # onnx2horizonbin.py::generate_config()
885
+ attributes = {}
886
+ attributes['input_name'] = "chunk;att_cache;cnn_cache;att_mask"
887
+ attributes['output_name'] = "output;r_att_cache;r_cnn_cache"
888
+ attributes['input_type'] = "featuremap;featuremap;featuremap;featuremap"
889
+ attributes['norm_type'] = \
890
+ "no_preprocess;no_preprocess;no_preprocess;no_preprocess"
891
+ attributes['input_layout_train'] = "NCHW;NCHW;NCHW;NCHW"
892
+ attributes['input_layout_rt'] = "NCHW;NCHW;NCHW;NCHW"
893
+ attributes['input_shape'] = \
894
+ "{}x{}x{}x{};{}x{}x{}x{};{}x{}x{}x{};{}x{}x{}x{}".format(
895
+ chunk.size(0), chunk.size(1), chunk.size(2), chunk.size(3),
896
+ att_cache.size(0), att_cache.size(1), att_cache.size(2),
897
+ att_cache.size(3), cnn_cache.size(0), cnn_cache.size(1),
898
+ cnn_cache.size(2), cnn_cache.size(3), att_mask.size(0),
899
+ att_mask.size(1), att_mask.size(2), att_mask.size(3)
900
+ )
901
+ torch.onnx.export( # NOTE(xcsong): only support opset==11
902
+ encoder,
903
+ inputs,
904
+ encoder_outpath,
905
+ opset_version=11,
906
+ export_params=True,
907
+ do_constant_folding=True,
908
+ input_names=attributes['input_name'].split(';'),
909
+ output_names=attributes['output_name'].split(';'),
910
+ dynamic_axes=None,
911
+ verbose=False)
912
+ onnx_encoder = onnx.load(encoder_outpath)
913
+ for k in vars(args):
914
+ meta = onnx_encoder.metadata_props.add()
915
+ meta.key, meta.value = str(k), str(getattr(args, k))
916
+ for k in attributes:
917
+ meta = onnx_encoder.metadata_props.add()
918
+ meta.key, meta.value = str(k), str(attributes[k])
919
+ onnx.checker.check_model(onnx_encoder)
920
+ onnx.helper.printable_graph(onnx_encoder.graph)
921
+ onnx.save(onnx_encoder, encoder_outpath)
922
+ print_input_output_info(onnx_encoder, "onnx_encoder")
923
+ logger.info('Export onnx_encoder, done! see {}'.format(encoder_outpath))
924
+
925
+ logger.info("Stage-1.3: check onnx_encoder and torch_encoder")
926
+ torch_output = []
927
+ torch_chunk, torch_att_mask = copy.deepcopy(chunk), copy.deepcopy(att_mask)
928
+ torch_att_cache = copy.deepcopy(att_cache)
929
+ torch_cnn_cache = copy.deepcopy(cnn_cache)
930
+ for i in range(10):
931
+ logger.info("torch chunk-{}: {}, att_cache: {}, cnn_cache: {}"
932
+ ", att_mask: {}".format(i, list(torch_chunk.size()),
933
+ list(torch_att_cache.size()),
934
+ list(torch_cnn_cache.size()),
935
+ list(torch_att_mask.size())))
936
+ torch_att_mask[:, :, :, -(encoder.chunk_size * (i + 1)):] = 1
937
+ out, torch_att_cache, torch_cnn_cache = encoder(
938
+ torch_chunk, torch_att_cache, torch_cnn_cache, torch_att_mask)
939
+ torch_output.append(out)
940
+ torch_output = torch.cat(torch_output, dim=-1)
941
+
942
+ onnx_output = []
943
+ onnx_chunk, onnx_att_mask = to_numpy(chunk), to_numpy(att_mask)
944
+ onnx_att_cache = to_numpy(att_cache)
945
+ onnx_cnn_cache = to_numpy(cnn_cache)
946
+ ort_session = onnxruntime.InferenceSession(encoder_outpath)
947
+ input_names = [node.name for node in onnx_encoder.graph.input]
948
+ for i in range(10):
949
+ logger.info("onnx chunk-{}: {}, att_cache: {}, cnn_cache: {},"
950
+ " att_mask: {}".format(i, onnx_chunk.shape,
951
+ onnx_att_cache.shape,
952
+ onnx_cnn_cache.shape,
953
+ onnx_att_mask.shape))
954
+ onnx_att_mask[:, :, :, -(encoder.chunk_size * (i + 1)):] = 1
955
+ ort_inputs = {
956
+ 'chunk': onnx_chunk,
957
+ 'att_cache': onnx_att_cache,
958
+ 'cnn_cache': onnx_cnn_cache,
959
+ 'att_mask': onnx_att_mask,
960
+ }
961
+ ort_outs = ort_session.run(None, ort_inputs)
962
+ onnx_att_cache, onnx_cnn_cache = ort_outs[1], ort_outs[2]
963
+ onnx_output.append(ort_outs[0])
964
+ onnx_output = np.concatenate(onnx_output, axis=-1)
965
+
966
+ np.testing.assert_allclose(to_numpy(torch_output),
967
+ onnx_output,
968
+ rtol=1e-03,
969
+ atol=1e-04)
970
+ meta = ort_session.get_modelmeta()
971
+ logger.info("custom_metadata_map={}".format(meta.custom_metadata_map))
972
+ logger.info("Check onnx_encoder, pass!")
973
+ return encoder, ort_session
974
+
975
+
976
+ def export_ctc(asr_model, args):
977
+ logger.info("Stage-2: export ctc")
978
+ ctc = BPUCTC(asr_model.ctc).eval()
979
+ ctc_outpath = os.path.join(args.output_dir, 'ctc.onnx')
980
+
981
+ logger.info("Stage-2.1: prepare inputs for ctc")
982
+ hidden = torch.randn((1, args.output_size, 1, args.chunk_size))
983
+
984
+ logger.info("Stage-2.2: torch.onnx.export")
985
+ # NOTE(xcsong): Below attributes will be used in
986
+ # onnx2horizonbin.py::generate_config()
987
+ attributes = {}
988
+ attributes['input_name'], attributes['input_type'] = "hidden", "featuremap"
989
+ attributes['norm_type'] = "no_preprocess"
990
+ attributes['input_layout_train'] = "NCHW"
991
+ attributes['input_layout_rt'] = "NCHW"
992
+ attributes['input_shape'] = "{}x{}x{}x{}".format(
993
+ hidden.size(0),
994
+ hidden.size(1),
995
+ hidden.size(2),
996
+ hidden.size(3),
997
+ )
998
+ torch.onnx.export(ctc,
999
+ hidden,
1000
+ ctc_outpath,
1001
+ opset_version=11,
1002
+ export_params=True,
1003
+ do_constant_folding=True,
1004
+ input_names=['hidden'],
1005
+ output_names=['probs'],
1006
+ dynamic_axes=None,
1007
+ verbose=False)
1008
+ onnx_ctc = onnx.load(ctc_outpath)
1009
+ for k in vars(args):
1010
+ meta = onnx_ctc.metadata_props.add()
1011
+ meta.key, meta.value = str(k), str(getattr(args, k))
1012
+ for k in attributes:
1013
+ meta = onnx_ctc.metadata_props.add()
1014
+ meta.key, meta.value = str(k), str(attributes[k])
1015
+ onnx.checker.check_model(onnx_ctc)
1016
+ onnx.helper.printable_graph(onnx_ctc.graph)
1017
+ onnx.save(onnx_ctc, ctc_outpath)
1018
+ print_input_output_info(onnx_ctc, "onnx_ctc")
1019
+ logger.info('Export onnx_ctc, done! see {}'.format(ctc_outpath))
1020
+
1021
+ logger.info("Stage-2.3: check onnx_ctc and torch_ctc")
1022
+ torch_output = ctc(hidden)
1023
+ ort_session = onnxruntime.InferenceSession(ctc_outpath)
1024
+ onnx_output = ort_session.run(None, {'hidden': to_numpy(hidden)})
1025
+
1026
+ np.testing.assert_allclose(to_numpy(torch_output),
1027
+ onnx_output[0],
1028
+ rtol=1e-03,
1029
+ atol=1e-04)
1030
+ meta = ort_session.get_modelmeta()
1031
+ logger.info("custom_metadata_map={}".format(meta.custom_metadata_map))
1032
+ logger.info("Check onnx_ctc, pass!")
1033
+ return ctc, ort_session
1034
+
1035
+
1036
+ def export_decoder(asr_model, args):
1037
+ logger.info("Currently, Decoder is not supported.")
1038
+
1039
+
1040
+ if __name__ == '__main__':
1041
+ torch.manual_seed(777)
1042
+ args = get_args()
1043
+ args.ln_run_on_bpu = False
1044
+ # NOTE(xcsong): XJ3 BPU only support static shapes
1045
+ assert args.chunk_size > 0
1046
+ assert args.num_decoding_left_chunks > 0
1047
+ os.system("mkdir -p " + args.output_dir)
1048
+ os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
1049
+
1050
+ with open(args.config, 'r') as fin:
1051
+ configs = yaml.load(fin, Loader=yaml.FullLoader)
1052
+
1053
+ model, configs = init_model(args, configs)
1054
+ model.eval()
1055
+ print(model)
1056
+
1057
+ args.feature_size = configs['input_dim']
1058
+ args.output_size = model.encoder.output_size()
1059
+ args.decoding_window = (args.chunk_size - 1) * \
1060
+ model.encoder.embed.subsampling_rate + \
1061
+ model.encoder.embed.right_context + 1
1062
+
1063
+ export_encoder(model, args)
1064
+ export_ctc(model, args)
1065
+ export_decoder(model, args)
wenet/bin/export_onnx_cpu.py ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022, Xingchen Song (sxc19@mails.tsinghua.edu.cn)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import print_function
16
+
17
+ import argparse
18
+ import logging
19
+ import os
20
+ import copy
21
+ import sys
22
+
23
+ import torch
24
+ import yaml
25
+ import numpy as np
26
+
27
+ from wenet.utils.init_model import init_model
28
+
29
+ try:
30
+ import onnx
31
+ import onnxruntime
32
+ from onnxruntime.quantization import quantize_dynamic, QuantType
33
+ except ImportError:
34
+ print('Please install onnx and onnxruntime!')
35
+ sys.exit(1)
36
+
37
+
38
+ def get_args():
39
+ parser = argparse.ArgumentParser(description='export your script model')
40
+ parser.add_argument('--config', required=True, help='config file')
41
+ parser.add_argument('--checkpoint', required=True, help='checkpoint model')
42
+ parser.add_argument('--output_dir', required=True, help='output directory')
43
+ parser.add_argument('--chunk_size',
44
+ required=True,
45
+ type=int,
46
+ help='decoding chunk size')
47
+ parser.add_argument('--num_decoding_left_chunks',
48
+ required=True,
49
+ type=int,
50
+ help='cache chunks')
51
+ parser.add_argument('--reverse_weight',
52
+ default=0.5,
53
+ type=float,
54
+ help='reverse_weight in attention_rescoing')
55
+ args = parser.parse_args()
56
+ return args
57
+
58
+
59
+ def to_numpy(tensor):
60
+ if tensor.requires_grad:
61
+ return tensor.detach().cpu().numpy()
62
+ else:
63
+ return tensor.cpu().numpy()
64
+
65
+
66
+ def print_input_output_info(onnx_model, name, prefix="\t\t"):
67
+ input_names = [node.name for node in onnx_model.graph.input]
68
+ input_shapes = [[d.dim_value for d in node.type.tensor_type.shape.dim]
69
+ for node in onnx_model.graph.input]
70
+ output_names = [node.name for node in onnx_model.graph.output]
71
+ output_shapes = [[d.dim_value for d in node.type.tensor_type.shape.dim]
72
+ for node in onnx_model.graph.output]
73
+ print("{}{} inputs : {}".format(prefix, name, input_names))
74
+ print("{}{} input shapes : {}".format(prefix, name, input_shapes))
75
+ print("{}{} outputs: {}".format(prefix, name, output_names))
76
+ print("{}{} output shapes : {}".format(prefix, name, output_shapes))
77
+
78
+
79
+ def export_encoder(asr_model, args):
80
+ print("Stage-1: export encoder")
81
+ encoder = asr_model.encoder
82
+ encoder.forward = encoder.forward_chunk
83
+ encoder_outpath = os.path.join(args['output_dir'], 'encoder.onnx')
84
+
85
+ print("\tStage-1.1: prepare inputs for encoder")
86
+ chunk = torch.randn(
87
+ (args['batch'], args['decoding_window'], args['feature_size']))
88
+ offset = 0
89
+ # NOTE(xcsong): The uncertainty of `next_cache_start` only appears
90
+ # in the first few chunks, this is caused by dynamic att_cache shape, i,e
91
+ # (0, 0, 0, 0) for 1st chunk and (elayers, head, ?, d_k*2) for subsequent
92
+ # chunks. One way to ease the ONNX export is to keep `next_cache_start`
93
+ # as a fixed value. To do this, for the **first** chunk, if
94
+ # left_chunks > 0, we feed real cache & real mask to the model, otherwise
95
+ # fake cache & fake mask. In this way, we get:
96
+ # 1. 16/-1 mode: next_cache_start == 0 for all chunks
97
+ # 2. 16/4 mode: next_cache_start == chunk_size for all chunks
98
+ # 3. 16/0 mode: next_cache_start == chunk_size for all chunks
99
+ # 4. -1/-1 mode: next_cache_start == 0 for all chunks
100
+ # NO MORE DYNAMIC CHANGES!!
101
+ #
102
+ # NOTE(Mddct): We retain the current design for the convenience of supporting some
103
+ # inference frameworks without dynamic shapes. If you're interested in all-in-one
104
+ # model that supports different chunks please see:
105
+ # https://github.com/wenet-e2e/wenet/pull/1174
106
+
107
+ if args['left_chunks'] > 0: # 16/4
108
+ required_cache_size = args['chunk_size'] * args['left_chunks']
109
+ offset = required_cache_size
110
+ # Real cache
111
+ att_cache = torch.zeros(
112
+ (args['num_blocks'], args['head'], required_cache_size,
113
+ args['output_size'] // args['head'] * 2))
114
+ # Real mask
115
+ att_mask = torch.ones(
116
+ (args['batch'], 1, required_cache_size + args['chunk_size']),
117
+ dtype=torch.bool)
118
+ att_mask[:, :, :required_cache_size] = 0
119
+ elif args['left_chunks'] <= 0: # 16/-1, -1/-1, 16/0
120
+ required_cache_size = -1 if args['left_chunks'] < 0 else 0
121
+ # Fake cache
122
+ att_cache = torch.zeros((args['num_blocks'], args['head'], 0,
123
+ args['output_size'] // args['head'] * 2))
124
+ # Fake mask
125
+ att_mask = torch.ones((0, 0, 0), dtype=torch.bool)
126
+ cnn_cache = torch.zeros(
127
+ (args['num_blocks'], args['batch'], args['output_size'],
128
+ args['cnn_module_kernel'] - 1))
129
+ inputs = (chunk, offset, required_cache_size, att_cache, cnn_cache,
130
+ att_mask)
131
+ print("\t\tchunk.size(): {}\n".format(chunk.size()),
132
+ "\t\toffset: {}\n".format(offset),
133
+ "\t\trequired_cache: {}\n".format(required_cache_size),
134
+ "\t\tatt_cache.size(): {}\n".format(att_cache.size()),
135
+ "\t\tcnn_cache.size(): {}\n".format(cnn_cache.size()),
136
+ "\t\tatt_mask.size(): {}\n".format(att_mask.size()))
137
+
138
+ print("\tStage-1.2: torch.onnx.export")
139
+ dynamic_axes = {
140
+ 'chunk': {
141
+ 1: 'T'
142
+ },
143
+ 'att_cache': {
144
+ 2: 'T_CACHE'
145
+ },
146
+ 'att_mask': {
147
+ 2: 'T_ADD_T_CACHE'
148
+ },
149
+ 'output': {
150
+ 1: 'T'
151
+ },
152
+ 'r_att_cache': {
153
+ 2: 'T_CACHE'
154
+ },
155
+ }
156
+ # NOTE(xcsong): We keep dynamic axes even if in 16/4 mode, this is
157
+ # to avoid padding the last chunk (which usually contains less
158
+ # frames than required). For users who want static axes, just pop
159
+ # out specific axis.
160
+ # if args['chunk_size'] > 0: # 16/4, 16/-1, 16/0
161
+ # dynamic_axes.pop('chunk')
162
+ # dynamic_axes.pop('output')
163
+ # if args['left_chunks'] >= 0: # 16/4, 16/0
164
+ # # NOTE(xsong): since we feed real cache & real mask into the
165
+ # # model when left_chunks > 0, the shape of cache will never
166
+ # # be changed.
167
+ # dynamic_axes.pop('att_cache')
168
+ # dynamic_axes.pop('r_att_cache')
169
+ torch.onnx.export(encoder,
170
+ inputs,
171
+ encoder_outpath,
172
+ opset_version=13,
173
+ export_params=True,
174
+ do_constant_folding=True,
175
+ input_names=[
176
+ 'chunk', 'offset', 'required_cache_size',
177
+ 'att_cache', 'cnn_cache', 'att_mask'
178
+ ],
179
+ output_names=['output', 'r_att_cache', 'r_cnn_cache'],
180
+ dynamic_axes=dynamic_axes,
181
+ verbose=False)
182
+ onnx_encoder = onnx.load(encoder_outpath)
183
+ for (k, v) in args.items():
184
+ meta = onnx_encoder.metadata_props.add()
185
+ meta.key, meta.value = str(k), str(v)
186
+ onnx.checker.check_model(onnx_encoder)
187
+ onnx.helper.printable_graph(onnx_encoder.graph)
188
+ # NOTE(xcsong): to add those metadatas we need to reopen
189
+ # the file and resave it.
190
+ onnx.save(onnx_encoder, encoder_outpath)
191
+ print_input_output_info(onnx_encoder, "onnx_encoder")
192
+ # Dynamic quantization
193
+ model_fp32 = encoder_outpath
194
+ model_quant = os.path.join(args['output_dir'], 'encoder.quant.onnx')
195
+ quantize_dynamic(model_fp32, model_quant, weight_type=QuantType.QUInt8)
196
+ print('\t\tExport onnx_encoder, done! see {}'.format(encoder_outpath))
197
+
198
+ print("\tStage-1.3: check onnx_encoder and torch_encoder")
199
+ torch_output = []
200
+ torch_chunk = copy.deepcopy(chunk)
201
+ torch_offset = copy.deepcopy(offset)
202
+ torch_required_cache_size = copy.deepcopy(required_cache_size)
203
+ torch_att_cache = copy.deepcopy(att_cache)
204
+ torch_cnn_cache = copy.deepcopy(cnn_cache)
205
+ torch_att_mask = copy.deepcopy(att_mask)
206
+ for i in range(10):
207
+ print("\t\ttorch chunk-{}: {}, offset: {}, att_cache: {},"
208
+ " cnn_cache: {}, att_mask: {}".format(
209
+ i, list(torch_chunk.size()), torch_offset,
210
+ list(torch_att_cache.size()), list(torch_cnn_cache.size()),
211
+ list(torch_att_mask.size())))
212
+ # NOTE(xsong): att_mask of the first few batches need changes if
213
+ # we use 16/4 mode.
214
+ if args['left_chunks'] > 0: # 16/4
215
+ torch_att_mask[:, :, -(args['chunk_size'] * (i + 1)):] = 1
216
+ out, torch_att_cache, torch_cnn_cache = encoder(
217
+ torch_chunk, torch_offset, torch_required_cache_size,
218
+ torch_att_cache, torch_cnn_cache, torch_att_mask)
219
+ torch_output.append(out)
220
+ torch_offset += out.size(1)
221
+ torch_output = torch.cat(torch_output, dim=1)
222
+
223
+ onnx_output = []
224
+ onnx_chunk = to_numpy(chunk)
225
+ onnx_offset = np.array((offset)).astype(np.int64)
226
+ onnx_required_cache_size = np.array((required_cache_size)).astype(np.int64)
227
+ onnx_att_cache = to_numpy(att_cache)
228
+ onnx_cnn_cache = to_numpy(cnn_cache)
229
+ onnx_att_mask = to_numpy(att_mask)
230
+ ort_session = onnxruntime.InferenceSession(
231
+ encoder_outpath, providers=['CPUExecutionProvider'])
232
+ input_names = [node.name for node in onnx_encoder.graph.input]
233
+ for i in range(10):
234
+ print("\t\tonnx chunk-{}: {}, offset: {}, att_cache: {},"
235
+ " cnn_cache: {}, att_mask: {}".format(i, onnx_chunk.shape,
236
+ onnx_offset,
237
+ onnx_att_cache.shape,
238
+ onnx_cnn_cache.shape,
239
+ onnx_att_mask.shape))
240
+ # NOTE(xsong): att_mask of the first few batches need changes if
241
+ # we use 16/4 mode.
242
+ if args['left_chunks'] > 0: # 16/4
243
+ onnx_att_mask[:, :, -(args['chunk_size'] * (i + 1)):] = 1
244
+ ort_inputs = {
245
+ 'chunk': onnx_chunk,
246
+ 'offset': onnx_offset,
247
+ 'required_cache_size': onnx_required_cache_size,
248
+ 'att_cache': onnx_att_cache,
249
+ 'cnn_cache': onnx_cnn_cache,
250
+ 'att_mask': onnx_att_mask
251
+ }
252
+ # NOTE(xcsong): If we use 16/-1, -1/-1 or 16/0 mode, `next_cache_start`
253
+ # will be hardcoded to 0 or chunk_size by ONNX, thus
254
+ # required_cache_size and att_mask are no more needed and they will
255
+ # be removed by ONNX automatically.
256
+ for k in list(ort_inputs):
257
+ if k not in input_names:
258
+ ort_inputs.pop(k)
259
+ ort_outs = ort_session.run(None, ort_inputs)
260
+ onnx_att_cache, onnx_cnn_cache = ort_outs[1], ort_outs[2]
261
+ onnx_output.append(ort_outs[0])
262
+ onnx_offset += ort_outs[0].shape[1]
263
+ onnx_output = np.concatenate(onnx_output, axis=1)
264
+
265
+ np.testing.assert_allclose(to_numpy(torch_output),
266
+ onnx_output,
267
+ rtol=1e-03,
268
+ atol=1e-05)
269
+ meta = ort_session.get_modelmeta()
270
+ print("\t\tcustom_metadata_map={}".format(meta.custom_metadata_map))
271
+ print("\t\tCheck onnx_encoder, pass!")
272
+
273
+
274
+ def export_ctc(asr_model, args):
275
+ print("Stage-2: export ctc")
276
+ ctc = asr_model.ctc
277
+ ctc.forward = ctc.log_softmax
278
+ ctc_outpath = os.path.join(args['output_dir'], 'ctc.onnx')
279
+
280
+ print("\tStage-2.1: prepare inputs for ctc")
281
+ hidden = torch.randn(
282
+ (args['batch'], args['chunk_size'] if args['chunk_size'] > 0 else 16,
283
+ args['output_size']))
284
+
285
+ print("\tStage-2.2: torch.onnx.export")
286
+ dynamic_axes = {'hidden': {1: 'T'}, 'probs': {1: 'T'}}
287
+ torch.onnx.export(ctc,
288
+ hidden,
289
+ ctc_outpath,
290
+ opset_version=13,
291
+ export_params=True,
292
+ do_constant_folding=True,
293
+ input_names=['hidden'],
294
+ output_names=['probs'],
295
+ dynamic_axes=dynamic_axes,
296
+ verbose=False)
297
+ onnx_ctc = onnx.load(ctc_outpath)
298
+ for (k, v) in args.items():
299
+ meta = onnx_ctc.metadata_props.add()
300
+ meta.key, meta.value = str(k), str(v)
301
+ onnx.checker.check_model(onnx_ctc)
302
+ onnx.helper.printable_graph(onnx_ctc.graph)
303
+ onnx.save(onnx_ctc, ctc_outpath)
304
+ print_input_output_info(onnx_ctc, "onnx_ctc")
305
+ # Dynamic quantization
306
+ model_fp32 = ctc_outpath
307
+ model_quant = os.path.join(args['output_dir'], 'ctc.quant.onnx')
308
+ quantize_dynamic(model_fp32, model_quant, weight_type=QuantType.QUInt8)
309
+ print('\t\tExport onnx_ctc, done! see {}'.format(ctc_outpath))
310
+
311
+ print("\tStage-2.3: check onnx_ctc and torch_ctc")
312
+ torch_output = ctc(hidden)
313
+ ort_session = onnxruntime.InferenceSession(
314
+ ctc_outpath, providers=['CPUExecutionProvider'])
315
+ onnx_output = ort_session.run(None, {'hidden': to_numpy(hidden)})
316
+
317
+ np.testing.assert_allclose(to_numpy(torch_output),
318
+ onnx_output[0],
319
+ rtol=1e-03,
320
+ atol=1e-05)
321
+ print("\t\tCheck onnx_ctc, pass!")
322
+
323
+
324
+ def export_decoder(asr_model, args):
325
+ print("Stage-3: export decoder")
326
+ decoder = asr_model
327
+ # NOTE(lzhin): parameters of encoder will be automatically removed
328
+ # since they are not used during rescoring.
329
+ decoder.forward = decoder.forward_attention_decoder
330
+ decoder_outpath = os.path.join(args['output_dir'], 'decoder.onnx')
331
+
332
+ print("\tStage-3.1: prepare inputs for decoder")
333
+ # hardcode time->200 nbest->10 len->20, they are dynamic axes.
334
+ encoder_out = torch.randn((1, 200, args['output_size']))
335
+ hyps = torch.randint(low=0, high=args['vocab_size'], size=[10, 20])
336
+ hyps[:, 0] = args['vocab_size'] - 1 # <sos>
337
+ hyps_lens = torch.randint(low=15, high=21, size=[10])
338
+
339
+ print("\tStage-3.2: torch.onnx.export")
340
+ dynamic_axes = {
341
+ 'hyps': {
342
+ 0: 'NBEST',
343
+ 1: 'L'
344
+ },
345
+ 'hyps_lens': {
346
+ 0: 'NBEST'
347
+ },
348
+ 'encoder_out': {
349
+ 1: 'T'
350
+ },
351
+ 'score': {
352
+ 0: 'NBEST',
353
+ 1: 'L'
354
+ },
355
+ 'r_score': {
356
+ 0: 'NBEST',
357
+ 1: 'L'
358
+ }
359
+ }
360
+ inputs = (hyps, hyps_lens, encoder_out, args['reverse_weight'])
361
+ torch.onnx.export(
362
+ decoder,
363
+ inputs,
364
+ decoder_outpath,
365
+ opset_version=13,
366
+ export_params=True,
367
+ do_constant_folding=True,
368
+ input_names=['hyps', 'hyps_lens', 'encoder_out', 'reverse_weight'],
369
+ output_names=['score', 'r_score'],
370
+ dynamic_axes=dynamic_axes,
371
+ verbose=False)
372
+ onnx_decoder = onnx.load(decoder_outpath)
373
+ for (k, v) in args.items():
374
+ meta = onnx_decoder.metadata_props.add()
375
+ meta.key, meta.value = str(k), str(v)
376
+ onnx.checker.check_model(onnx_decoder)
377
+ onnx.helper.printable_graph(onnx_decoder.graph)
378
+ onnx.save(onnx_decoder, decoder_outpath)
379
+ print_input_output_info(onnx_decoder, "onnx_decoder")
380
+ model_fp32 = decoder_outpath
381
+ model_quant = os.path.join(args['output_dir'], 'decoder.quant.onnx')
382
+ quantize_dynamic(model_fp32, model_quant, weight_type=QuantType.QUInt8)
383
+ print('\t\tExport onnx_decoder, done! see {}'.format(decoder_outpath))
384
+
385
+ print("\tStage-3.3: check onnx_decoder and torch_decoder")
386
+ torch_score, torch_r_score = decoder(hyps, hyps_lens, encoder_out,
387
+ args['reverse_weight'])
388
+ ort_session = onnxruntime.InferenceSession(
389
+ decoder_outpath, providers=['CPUExecutionProvider'])
390
+ input_names = [node.name for node in onnx_decoder.graph.input]
391
+ ort_inputs = {
392
+ 'hyps': to_numpy(hyps),
393
+ 'hyps_lens': to_numpy(hyps_lens),
394
+ 'encoder_out': to_numpy(encoder_out),
395
+ 'reverse_weight': np.array((args['reverse_weight'])),
396
+ }
397
+ for k in list(ort_inputs):
398
+ if k not in input_names:
399
+ ort_inputs.pop(k)
400
+ onnx_output = ort_session.run(None, ort_inputs)
401
+
402
+ np.testing.assert_allclose(to_numpy(torch_score),
403
+ onnx_output[0],
404
+ rtol=1e-03,
405
+ atol=1e-05)
406
+ if args['is_bidirectional_decoder'] and args['reverse_weight'] > 0.0:
407
+ np.testing.assert_allclose(to_numpy(torch_r_score),
408
+ onnx_output[1],
409
+ rtol=1e-03,
410
+ atol=1e-05)
411
+ print("\t\tCheck onnx_decoder, pass!")
412
+
413
+
414
+ def main():
415
+ torch.manual_seed(777)
416
+ args = get_args()
417
+ logging.basicConfig(level=logging.DEBUG,
418
+ format='%(asctime)s %(levelname)s %(message)s')
419
+ output_dir = args.output_dir
420
+ os.system("mkdir -p " + output_dir)
421
+ os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
422
+
423
+ with open(args.config, 'r') as fin:
424
+ configs = yaml.load(fin, Loader=yaml.FullLoader)
425
+
426
+ model, configs = init_model(args, configs)
427
+ model.eval()
428
+ print(model)
429
+
430
+ arguments = {}
431
+ arguments['output_dir'] = output_dir
432
+ arguments['batch'] = 1
433
+ arguments['chunk_size'] = args.chunk_size
434
+ arguments['left_chunks'] = args.num_decoding_left_chunks
435
+ arguments['reverse_weight'] = args.reverse_weight
436
+ arguments['output_size'] = configs['encoder_conf']['output_size']
437
+ arguments['num_blocks'] = configs['encoder_conf']['num_blocks']
438
+ arguments['cnn_module_kernel'] = configs['encoder_conf'].get(
439
+ 'cnn_module_kernel', 1)
440
+ arguments['head'] = configs['encoder_conf']['attention_heads']
441
+ arguments['feature_size'] = configs['input_dim']
442
+ arguments['vocab_size'] = configs['output_dim']
443
+ # NOTE(xcsong): if chunk_size == -1, hardcode to 67
444
+ arguments['decoding_window'] = (args.chunk_size - 1) * \
445
+ model.encoder.embed.subsampling_rate + \
446
+ model.encoder.embed.right_context + 1 if args.chunk_size > 0 else 67
447
+ arguments['encoder'] = configs['encoder']
448
+ arguments['decoder'] = configs['decoder']
449
+ arguments['subsampling_rate'] = model.subsampling_rate()
450
+ arguments['right_context'] = model.right_context()
451
+ arguments['sos_symbol'] = model.sos_symbol()
452
+ arguments['eos_symbol'] = model.eos_symbol()
453
+ arguments['is_bidirectional_decoder'] = 1 \
454
+ if model.is_bidirectional_decoder() else 0
455
+
456
+ # NOTE(xcsong): Please note that -1/-1 means non-streaming model! It is
457
+ # not a [16/4 16/-1 16/0] all-in-one model and it should not be used in
458
+ # streaming mode (i.e., setting chunk_size=16 in `decoder_main`). If you
459
+ # want to use 16/-1 or any other streaming mode in `decoder_main`,
460
+ # please export onnx in the same config.
461
+ if arguments['left_chunks'] > 0:
462
+ assert arguments['chunk_size'] > 0 # -1/4 not supported
463
+
464
+ export_encoder(model, arguments)
465
+ export_ctc(model, arguments)
466
+ export_decoder(model, arguments)
467
+
468
+
469
+ if __name__ == '__main__':
470
+ main()
wenet/bin/export_onnx_gpu.py ADDED
@@ -0,0 +1,1266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import print_function
16
+
17
+ import argparse
18
+ import logging
19
+ import os
20
+ import sys
21
+
22
+ import torch
23
+ import torch.nn.functional as F
24
+ import yaml
25
+ from wenet.transformer.ctc import CTC
26
+ from wenet.transformer.decoder import TransformerDecoder
27
+ from wenet.transformer.encoder import BaseEncoder
28
+ from wenet.utils.init_model import init_model
29
+ from wenet.utils.mask import make_pad_mask
30
+
31
+ try:
32
+ import onnxruntime
33
+ except ImportError:
34
+ print("Please install onnxruntime-gpu!")
35
+ sys.exit(1)
36
+
37
+ logger = logging.getLogger(__file__)
38
+ logger.setLevel(logging.INFO)
39
+
40
+
41
+ class Encoder(torch.nn.Module):
42
+
43
+ def __init__(self, encoder: BaseEncoder, ctc: CTC, beam_size: int = 10):
44
+ super().__init__()
45
+ self.encoder = encoder
46
+ self.ctc = ctc
47
+ self.beam_size = beam_size
48
+
49
+ def forward(
50
+ self,
51
+ speech: torch.Tensor,
52
+ speech_lengths: torch.Tensor,
53
+ ):
54
+ """Encoder
55
+ Args:
56
+ speech: (Batch, Length, ...)
57
+ speech_lengths: (Batch, )
58
+ Returns:
59
+ encoder_out: B x T x F
60
+ encoder_out_lens: B
61
+ ctc_log_probs: B x T x V
62
+ beam_log_probs: B x T x beam_size
63
+ beam_log_probs_idx: B x T x beam_size
64
+ """
65
+ encoder_out, encoder_mask = self.encoder(speech, speech_lengths, -1,
66
+ -1)
67
+ encoder_out_lens = encoder_mask.squeeze(1).sum(1)
68
+ ctc_log_probs = self.ctc.log_softmax(encoder_out)
69
+ encoder_out_lens = encoder_out_lens.int()
70
+ beam_log_probs, beam_log_probs_idx = torch.topk(ctc_log_probs,
71
+ self.beam_size,
72
+ dim=2)
73
+ return (
74
+ encoder_out,
75
+ encoder_out_lens,
76
+ ctc_log_probs,
77
+ beam_log_probs,
78
+ beam_log_probs_idx,
79
+ )
80
+
81
+
82
+ class StreamingEncoder(torch.nn.Module):
83
+
84
+ def __init__(
85
+ self,
86
+ model,
87
+ required_cache_size,
88
+ beam_size,
89
+ transformer=False,
90
+ return_ctc_logprobs=False,
91
+ ):
92
+ super().__init__()
93
+ self.ctc = model.ctc
94
+ self.subsampling_rate = model.encoder.embed.subsampling_rate
95
+ self.embed = model.encoder.embed
96
+ self.global_cmvn = model.encoder.global_cmvn
97
+ self.required_cache_size = required_cache_size
98
+ self.beam_size = beam_size
99
+ self.encoder = model.encoder
100
+ self.transformer = transformer
101
+ self.return_ctc_logprobs = return_ctc_logprobs
102
+
103
+ def forward(self, chunk_xs, chunk_lens, offset, att_cache, cnn_cache,
104
+ cache_mask):
105
+ """Streaming Encoder
106
+ Args:
107
+ xs (torch.Tensor): chunk input, with shape (b, time, mel-dim),
108
+ where `time == (chunk_size - 1) * subsample_rate + \
109
+ subsample.right_context + 1`
110
+ offset (torch.Tensor): offset with shape (b, 1)
111
+ 1 is retained for triton deployment
112
+ required_cache_size (int): cache size required for next chunk
113
+ compuation
114
+ > 0: actual cache size
115
+ <= 0: not allowed in streaming gpu encoder `
116
+ att_cache (torch.Tensor): cache tensor for KEY & VALUE in
117
+ transformer/conformer attention, with shape
118
+ (b, elayers, head, cache_t1, d_k * 2), where
119
+ `head * d_k == hidden-dim` and
120
+ `cache_t1 == chunk_size * num_decoding_left_chunks`.
121
+ cnn_cache (torch.Tensor): cache tensor for cnn_module in conformer,
122
+ (b, elayers, b, hidden-dim, cache_t2), where
123
+ `cache_t2 == cnn.lorder - 1`
124
+ cache_mask: (torch.Tensor): cache mask with shape (b, required_cache_size)
125
+ in a batch of request, each request may have different
126
+ history cache. Cache mask is used to indidate the effective
127
+ cache for each request
128
+ Returns:
129
+ torch.Tensor: log probabilities of ctc output and cutoff by beam size
130
+ with shape (b, chunk_size, beam)
131
+ torch.Tensor: index of top beam size probabilities for each timestep
132
+ with shape (b, chunk_size, beam)
133
+ torch.Tensor: output of current input xs,
134
+ with shape (b, chunk_size, hidden-dim).
135
+ torch.Tensor: new attention cache required for next chunk, with
136
+ same shape (b, elayers, head, cache_t1, d_k * 2)
137
+ as the original att_cache
138
+ torch.Tensor: new conformer cnn cache required for next chunk, with
139
+ same shape as the original cnn_cache.
140
+ torch.Tensor: new cache mask, with same shape as the original
141
+ cache mask
142
+ """
143
+ offset = offset.squeeze(1)
144
+ T = chunk_xs.size(1)
145
+ chunk_mask = ~make_pad_mask(chunk_lens, T).unsqueeze(1)
146
+ # B X 1 X T
147
+ chunk_mask = chunk_mask.to(chunk_xs.dtype)
148
+ # transpose batch & num_layers dim
149
+ att_cache = torch.transpose(att_cache, 0, 1)
150
+ cnn_cache = torch.transpose(cnn_cache, 0, 1)
151
+
152
+ # rewrite encoder.forward_chunk
153
+ # <---------forward_chunk START--------->
154
+ xs = self.global_cmvn(chunk_xs)
155
+ # chunk mask is important for batch inferencing since
156
+ # different sequence in a batch has different length
157
+ xs, pos_emb, chunk_mask = self.embed(xs, chunk_mask, offset)
158
+ cache_size = att_cache.size(3) # required cache size
159
+ masks = torch.cat((cache_mask, chunk_mask), dim=2)
160
+ index = offset - cache_size
161
+
162
+ pos_emb = self.embed.position_encoding(index, cache_size + xs.size(1))
163
+ pos_emb = pos_emb.to(dtype=xs.dtype)
164
+
165
+ next_cache_start = -self.required_cache_size
166
+ r_cache_mask = masks[:, :, next_cache_start:]
167
+
168
+ r_att_cache = []
169
+ r_cnn_cache = []
170
+ for i, layer in enumerate(self.encoder.encoders):
171
+ i_kv_cache = att_cache[i]
172
+ size = att_cache.size(-1) // 2
173
+ kv_cache = (i_kv_cache[:, :, :, :size], i_kv_cache[:, :, :, size:])
174
+ xs, _, new_kv_cache, new_cnn_cache = layer(
175
+ xs,
176
+ masks,
177
+ pos_emb,
178
+ att_cache=kv_cache,
179
+ cnn_cache=cnn_cache[i],
180
+ )
181
+ # shape(new_att_cache) is (B, head, attention_key_size, d_k * 2),
182
+ # shape(new_cnn_cache) is (B, hidden-dim, cache_t2)
183
+ new_att_cache = torch.cat(new_kv_cache, dim=-1)
184
+ r_att_cache.append(
185
+ new_att_cache[:, :, next_cache_start:, :].unsqueeze(1))
186
+ if not self.transformer:
187
+ r_cnn_cache.append(new_cnn_cache.unsqueeze(1))
188
+ if self.encoder.normalize_before:
189
+ chunk_out = self.encoder.after_norm(xs)
190
+ else:
191
+ chunk_out = xs
192
+
193
+ r_att_cache = torch.cat(r_att_cache, dim=1) # concat on layers idx
194
+ if not self.transformer:
195
+ r_cnn_cache = torch.cat(r_cnn_cache, dim=1) # concat on layers
196
+
197
+ # <---------forward_chunk END--------->
198
+
199
+ log_ctc_probs = self.ctc.log_softmax(chunk_out)
200
+ log_probs, log_probs_idx = torch.topk(log_ctc_probs,
201
+ self.beam_size,
202
+ dim=2)
203
+ log_probs = log_probs.to(chunk_xs.dtype)
204
+
205
+ r_offset = offset + chunk_out.shape[1]
206
+ # the below ops not supported in Tensorrt
207
+ # chunk_out_lens = torch.div(chunk_lens, subsampling_rate,
208
+ # rounding_mode='floor')
209
+ chunk_out_lens = chunk_lens // self.subsampling_rate
210
+ r_offset = r_offset.unsqueeze(1)
211
+ if self.return_ctc_logprobs:
212
+ return (
213
+ log_ctc_probs,
214
+ chunk_out,
215
+ chunk_out_lens,
216
+ r_offset,
217
+ r_att_cache,
218
+ r_cnn_cache,
219
+ r_cache_mask,
220
+ )
221
+ else:
222
+ return (
223
+ log_probs,
224
+ log_probs_idx,
225
+ chunk_out,
226
+ chunk_out_lens,
227
+ r_offset,
228
+ r_att_cache,
229
+ r_cnn_cache,
230
+ r_cache_mask,
231
+ )
232
+
233
+
234
+ class StreamingSqueezeformerEncoder(torch.nn.Module):
235
+
236
+ def __init__(self, model, required_cache_size, beam_size):
237
+ super().__init__()
238
+ self.ctc = model.ctc
239
+ self.subsampling_rate = model.encoder.embed.subsampling_rate
240
+ self.embed = model.encoder.embed
241
+ self.global_cmvn = model.encoder.global_cmvn
242
+ self.required_cache_size = required_cache_size
243
+ self.beam_size = beam_size
244
+ self.encoder = model.encoder
245
+ self.reduce_idx = model.encoder.reduce_idx
246
+ self.recover_idx = model.encoder.recover_idx
247
+ if self.reduce_idx is None:
248
+ self.time_reduce = None
249
+ else:
250
+ if self.recover_idx is None:
251
+ self.time_reduce = "normal" # no recovery at the end
252
+ else:
253
+ self.time_reduce = "recover" # recovery at the end
254
+ assert len(self.reduce_idx) == len(self.recover_idx)
255
+
256
+ def calculate_downsampling_factor(self, i: int) -> int:
257
+ if self.reduce_idx is None:
258
+ return 1
259
+ else:
260
+ reduce_exp, recover_exp = 0, 0
261
+ for exp, rd_idx in enumerate(self.reduce_idx):
262
+ if i >= rd_idx:
263
+ reduce_exp = exp + 1
264
+ if self.recover_idx is not None:
265
+ for exp, rc_idx in enumerate(self.recover_idx):
266
+ if i >= rc_idx:
267
+ recover_exp = exp + 1
268
+ return int(2**(reduce_exp - recover_exp))
269
+
270
+ def forward(self, chunk_xs, chunk_lens, offset, att_cache, cnn_cache,
271
+ cache_mask):
272
+ """Streaming Encoder
273
+ Args:
274
+ xs (torch.Tensor): chunk input, with shape (b, time, mel-dim),
275
+ where `time == (chunk_size - 1) * subsample_rate + \
276
+ subsample.right_context + 1`
277
+ offset (torch.Tensor): offset with shape (b, 1)
278
+ 1 is retained for triton deployment
279
+ required_cache_size (int): cache size required for next chunk
280
+ compuation
281
+ > 0: actual cache size
282
+ <= 0: not allowed in streaming gpu encoder `
283
+ att_cache (torch.Tensor): cache tensor for KEY & VALUE in
284
+ transformer/conformer attention, with shape
285
+ (b, elayers, head, cache_t1, d_k * 2), where
286
+ `head * d_k == hidden-dim` and
287
+ `cache_t1 == chunk_size * num_decoding_left_chunks`.
288
+ cnn_cache (torch.Tensor): cache tensor for cnn_module in conformer,
289
+ (b, elayers, b, hidden-dim, cache_t2), where
290
+ `cache_t2 == cnn.lorder - 1`
291
+ cache_mask: (torch.Tensor): cache mask with shape (b, required_cache_size)
292
+ in a batch of request, each request may have different
293
+ history cache. Cache mask is used to indidate the effective
294
+ cache for each request
295
+ Returns:
296
+ torch.Tensor: log probabilities of ctc output and cutoff by beam size
297
+ with shape (b, chunk_size, beam)
298
+ torch.Tensor: index of top beam size probabilities for each timestep
299
+ with shape (b, chunk_size, beam)
300
+ torch.Tensor: output of current input xs,
301
+ with shape (b, chunk_size, hidden-dim).
302
+ torch.Tensor: new attention cache required for next chunk, with
303
+ same shape (b, elayers, head, cache_t1, d_k * 2)
304
+ as the original att_cache
305
+ torch.Tensor: new conformer cnn cache required for next chunk, with
306
+ same shape as the original cnn_cache.
307
+ torch.Tensor: new cache mask, with same shape as the original
308
+ cache mask
309
+ """
310
+ offset = offset.squeeze(1)
311
+ T = chunk_xs.size(1)
312
+ chunk_mask = ~make_pad_mask(chunk_lens, T).unsqueeze(1)
313
+ # B X 1 X T
314
+ chunk_mask = chunk_mask.to(chunk_xs.dtype)
315
+ # transpose batch & num_layers dim
316
+ att_cache = torch.transpose(att_cache, 0, 1)
317
+ cnn_cache = torch.transpose(cnn_cache, 0, 1)
318
+
319
+ # rewrite encoder.forward_chunk
320
+ # <---------forward_chunk START--------->
321
+ xs = self.global_cmvn(chunk_xs)
322
+ # chunk mask is important for batch inferencing since
323
+ # different sequence in a batch has different length
324
+ xs, pos_emb, chunk_mask = self.embed(xs, chunk_mask, offset)
325
+ elayers, cache_size = att_cache.size(0), att_cache.size(3)
326
+ att_mask = torch.cat((cache_mask, chunk_mask), dim=2)
327
+ index = offset - cache_size
328
+
329
+ pos_emb = self.embed.position_encoding(index, cache_size + xs.size(1))
330
+ pos_emb = pos_emb.to(dtype=xs.dtype)
331
+
332
+ next_cache_start = -self.required_cache_size
333
+ r_cache_mask = att_mask[:, :, next_cache_start:]
334
+
335
+ r_att_cache = []
336
+ r_cnn_cache = []
337
+ mask_pad = torch.ones(1,
338
+ xs.size(1),
339
+ device=xs.device,
340
+ dtype=torch.bool)
341
+ mask_pad = mask_pad.unsqueeze(1)
342
+ max_att_len: int = 0
343
+ recover_activations: List[Tuple[torch.Tensor, torch.Tensor,
344
+ torch.Tensor, torch.Tensor]] = []
345
+ index = 0
346
+ xs_lens = torch.tensor([xs.size(1)], device=xs.device, dtype=torch.int)
347
+ xs = self.encoder.preln(xs)
348
+ for i, layer in enumerate(self.encoder.encoders):
349
+ if self.reduce_idx is not None:
350
+ if self.time_reduce is not None and i in self.reduce_idx:
351
+ recover_activations.append(
352
+ (xs, att_mask, pos_emb, mask_pad))
353
+ (
354
+ xs,
355
+ xs_lens,
356
+ att_mask,
357
+ mask_pad,
358
+ ) = self.encoder.time_reduction_layer(
359
+ xs, xs_lens, att_mask, mask_pad)
360
+ pos_emb = pos_emb[:, ::2, :]
361
+ if self.encoder.pos_enc_layer_type == "rel_pos_repaired":
362
+ pos_emb = pos_emb[:, :xs.size(1) * 2 - 1, :]
363
+ index += 1
364
+
365
+ if self.recover_idx is not None:
366
+ if self.time_reduce == "recover" and i in self.recover_idx:
367
+ index -= 1
368
+ (
369
+ recover_tensor,
370
+ recover_att_mask,
371
+ recover_pos_emb,
372
+ recover_mask_pad,
373
+ ) = recover_activations[index]
374
+ # recover output length for ctc decode
375
+ xs = xs.unsqueeze(2).repeat(1, 1, 2, 1).flatten(1, 2)
376
+ xs = self.encoder.time_recover_layer(xs)
377
+ recoverd_t = recover_tensor.size(1)
378
+ xs = recover_tensor + xs[:, :recoverd_t, :].contiguous()
379
+ att_mask = recover_att_mask
380
+ pos_emb = recover_pos_emb
381
+ mask_pad = recover_mask_pad
382
+
383
+ factor = self.calculate_downsampling_factor(i)
384
+
385
+ xs, _, new_att_cache, new_cnn_cache = layer(
386
+ xs,
387
+ att_mask,
388
+ pos_emb,
389
+ att_cache=att_cache[i][:, :, ::factor, :]
390
+ [:, :, :pos_emb.size(1) - xs.size(1), :]
391
+ if elayers > 0 else att_cache[:, :, ::factor, :],
392
+ cnn_cache=cnn_cache[i] if cnn_cache.size(0) > 0 else cnn_cache,
393
+ )
394
+ cached_att = new_att_cache[:, :, next_cache_start // factor:, :]
395
+ cached_cnn = new_cnn_cache.unsqueeze(1)
396
+ cached_att = (cached_att.unsqueeze(3).repeat(1, 1, 1, factor,
397
+ 1).flatten(2, 3))
398
+ if i == 0:
399
+ # record length for the first block as max length
400
+ max_att_len = cached_att.size(2)
401
+ r_att_cache.append(cached_att[:, :, :max_att_len, :].unsqueeze(1))
402
+ r_cnn_cache.append(cached_cnn)
403
+
404
+ chunk_out = xs
405
+ r_att_cache = torch.cat(r_att_cache, dim=1) # concat on layers idx
406
+ r_cnn_cache = torch.cat(r_cnn_cache, dim=1) # concat on layers
407
+
408
+ # <---------forward_chunk END--------->
409
+
410
+ log_ctc_probs = self.ctc.log_softmax(chunk_out)
411
+ log_probs, log_probs_idx = torch.topk(log_ctc_probs,
412
+ self.beam_size,
413
+ dim=2)
414
+ log_probs = log_probs.to(chunk_xs.dtype)
415
+
416
+ r_offset = offset + chunk_out.shape[1]
417
+ # the below ops not supported in Tensorrt
418
+ # chunk_out_lens = torch.div(chunk_lens, subsampling_rate,
419
+ # rounding_mode='floor')
420
+ chunk_out_lens = chunk_lens // self.subsampling_rate
421
+ r_offset = r_offset.unsqueeze(1)
422
+
423
+ return (
424
+ log_probs,
425
+ log_probs_idx,
426
+ chunk_out,
427
+ chunk_out_lens,
428
+ r_offset,
429
+ r_att_cache,
430
+ r_cnn_cache,
431
+ r_cache_mask,
432
+ )
433
+
434
+
435
+ class StreamingEfficientConformerEncoder(torch.nn.Module):
436
+
437
+ def __init__(self, model, required_cache_size, beam_size):
438
+ super().__init__()
439
+ self.ctc = model.ctc
440
+ self.subsampling_rate = model.encoder.embed.subsampling_rate
441
+ self.embed = model.encoder.embed
442
+ self.global_cmvn = model.encoder.global_cmvn
443
+ self.required_cache_size = required_cache_size
444
+ self.beam_size = beam_size
445
+ self.encoder = model.encoder
446
+
447
+ # Efficient Conformer
448
+ self.stride_layer_idx = model.encoder.stride_layer_idx
449
+ self.stride = model.encoder.stride
450
+ self.num_blocks = model.encoder.num_blocks
451
+ self.cnn_module_kernel = model.encoder.cnn_module_kernel
452
+
453
+ def calculate_downsampling_factor(self, i: int) -> int:
454
+ factor = 1
455
+ for idx, stride_idx in enumerate(self.stride_layer_idx):
456
+ if i > stride_idx:
457
+ factor *= self.stride[idx]
458
+ return factor
459
+
460
+ def forward(self, chunk_xs, chunk_lens, offset, att_cache, cnn_cache,
461
+ cache_mask):
462
+ """Streaming Encoder
463
+ Args:
464
+ chunk_xs (torch.Tensor): chunk input, with shape (b, time, mel-dim),
465
+ where `time == (chunk_size - 1) * subsample_rate + \
466
+ subsample.right_context + 1`
467
+ chunk_lens (torch.Tensor):
468
+ offset (torch.Tensor): offset with shape (b, 1)
469
+ 1 is retained for triton deployment
470
+ att_cache (torch.Tensor): cache tensor for KEY & VALUE in
471
+ transformer/conformer attention, with shape
472
+ (b, elayers, head, cache_t1, d_k * 2), where
473
+ `head * d_k == hidden-dim` and
474
+ `cache_t1 == chunk_size * num_decoding_left_chunks`.
475
+ cnn_cache (torch.Tensor): cache tensor for cnn_module in conformer,
476
+ (b, elayers, hidden-dim, cache_t2), where
477
+ `cache_t2 == cnn.lorder - 1`
478
+ cache_mask: (torch.Tensor): cache mask with shape (b, required_cache_size)
479
+ in a batch of request, each request may have different
480
+ history cache. Cache mask is used to indidate the effective
481
+ cache for each request
482
+ Returns:
483
+ torch.Tensor: log probabilities of ctc output and cutoff by beam size
484
+ with shape (b, chunk_size, beam)
485
+ torch.Tensor: index of top beam size probabilities for each timestep
486
+ with shape (b, chunk_size, beam)
487
+ torch.Tensor: output of current input xs,
488
+ with shape (b, chunk_size, hidden-dim).
489
+ torch.Tensor: new attention cache required for next chunk, with
490
+ same shape (b, elayers, head, cache_t1, d_k * 2)
491
+ as the original att_cache
492
+ torch.Tensor: new conformer cnn cache required for next chunk, with
493
+ same shape as the original cnn_cache.
494
+ torch.Tensor: new cache mask, with same shape as the original
495
+ cache mask
496
+ """
497
+ offset = offset.squeeze(1) # (b, )
498
+ offset *= self.calculate_downsampling_factor(self.num_blocks + 1)
499
+
500
+ T = chunk_xs.size(1)
501
+ chunk_mask = ~make_pad_mask(chunk_lens, T).unsqueeze(1) # (b, 1, T)
502
+ # B X 1 X T
503
+ chunk_mask = chunk_mask.to(chunk_xs.dtype)
504
+ # transpose batch & num_layers dim
505
+ # Shape(att_cache): (elayers, b, head, cache_t1, d_k * 2)
506
+ # Shape(cnn_cache): (elayers, b, outsize, cnn_kernel)
507
+ att_cache = torch.transpose(att_cache, 0, 1)
508
+ cnn_cache = torch.transpose(cnn_cache, 0, 1)
509
+
510
+ # rewrite encoder.forward_chunk
511
+ # <---------forward_chunk START--------->
512
+ xs = self.global_cmvn(chunk_xs)
513
+ # chunk mask is important for batch inferencing since
514
+ # different sequence in a batch has different length
515
+ xs, pos_emb, chunk_mask = self.embed(xs, chunk_mask, offset)
516
+ cache_size = att_cache.size(3) # required cache size
517
+ masks = torch.cat((cache_mask, chunk_mask), dim=2)
518
+ att_mask = torch.cat((cache_mask, chunk_mask), dim=2)
519
+ index = offset - cache_size
520
+
521
+ pos_emb = self.embed.position_encoding(index, cache_size + xs.size(1))
522
+ pos_emb = pos_emb.to(dtype=xs.dtype)
523
+
524
+ next_cache_start = -self.required_cache_size
525
+ r_cache_mask = masks[:, :, next_cache_start:]
526
+
527
+ r_att_cache = []
528
+ r_cnn_cache = []
529
+ mask_pad = chunk_mask.to(torch.bool)
530
+ max_att_len, max_cnn_len = (
531
+ 0,
532
+ 0,
533
+ ) # for repeat_interleave of new_att_cache
534
+ for i, layer in enumerate(self.encoder.encoders):
535
+ factor = self.calculate_downsampling_factor(i)
536
+ # NOTE(xcsong): Before layer.forward
537
+ # shape(att_cache[i:i + 1]) is (b, head, cache_t1, d_k * 2),
538
+ # shape(cnn_cache[i]) is (b=1, hidden-dim, cache_t2)
539
+ # shape(new_att_cache) = [ batch, head, time2, outdim//head * 2 ]
540
+ att_cache_trunc = 0
541
+ if xs.size(1) + att_cache.size(3) / factor > pos_emb.size(1):
542
+ # The time step is not divisible by the downsampling multiple
543
+ # We propose to double the chunk_size.
544
+ att_cache_trunc = (xs.size(1) + att_cache.size(3) // factor -
545
+ pos_emb.size(1) + 1)
546
+ xs, _, new_att_cache, new_cnn_cache = layer(
547
+ xs,
548
+ att_mask,
549
+ pos_emb,
550
+ mask_pad=mask_pad,
551
+ att_cache=att_cache[i][:, :, ::factor, :][:, :,
552
+ att_cache_trunc:, :],
553
+ cnn_cache=cnn_cache[i, :, :, :]
554
+ if cnn_cache.size(0) > 0 else cnn_cache,
555
+ )
556
+
557
+ if i in self.stride_layer_idx:
558
+ # compute time dimension for next block
559
+ efficient_index = self.stride_layer_idx.index(i)
560
+ att_mask = att_mask[:, ::self.stride[efficient_index], ::self.
561
+ stride[efficient_index], ]
562
+ mask_pad = mask_pad[:, ::self.stride[efficient_index], ::self.
563
+ stride[efficient_index], ]
564
+ pos_emb = pos_emb[:, ::self.stride[efficient_index], :]
565
+
566
+ # shape(new_att_cache) = [batch, head, time2, outdim]
567
+ new_att_cache = new_att_cache[:, :, next_cache_start // factor:, :]
568
+ # shape(new_cnn_cache) = [batch, 1, outdim, cache_t2]
569
+ new_cnn_cache = new_cnn_cache.unsqueeze(1) # shape(1):layerID
570
+
571
+ # use repeat_interleave to new_att_cache
572
+ # new_att_cache = new_att_cache.repeat_interleave(repeats=factor, dim=2)
573
+ new_att_cache = (new_att_cache.unsqueeze(3).repeat(
574
+ 1, 1, 1, factor, 1).flatten(2, 3))
575
+ # padding new_cnn_cache to cnn.lorder for casual convolution
576
+ new_cnn_cache = F.pad(
577
+ new_cnn_cache,
578
+ (self.cnn_module_kernel - 1 - new_cnn_cache.size(3), 0),
579
+ )
580
+
581
+ if i == 0:
582
+ # record length for the first block as max length
583
+ max_att_len = new_att_cache.size(2)
584
+ max_cnn_len = new_cnn_cache.size(3)
585
+
586
+ # update real shape of att_cache and cnn_cache
587
+ r_att_cache.append(new_att_cache[:, :,
588
+ -max_att_len:, :].unsqueeze(1))
589
+ r_cnn_cache.append(new_cnn_cache[:, :, :, -max_cnn_len:])
590
+
591
+ if self.encoder.normalize_before:
592
+ chunk_out = self.encoder.after_norm(xs)
593
+ else:
594
+ chunk_out = xs
595
+
596
+ # shape of r_att_cache: (b, elayers, head, time2, outdim)
597
+ r_att_cache = torch.cat(r_att_cache, dim=1) # concat on layers idx
598
+ # shape of r_cnn_cache: (b, elayers, outdim, cache_t2)
599
+ r_cnn_cache = torch.cat(r_cnn_cache, dim=1) # concat on layers
600
+
601
+ # <---------forward_chunk END--------->
602
+
603
+ log_ctc_probs = self.ctc.log_softmax(chunk_out)
604
+ log_probs, log_probs_idx = torch.topk(log_ctc_probs,
605
+ self.beam_size,
606
+ dim=2)
607
+ log_probs = log_probs.to(chunk_xs.dtype)
608
+
609
+ r_offset = offset + chunk_out.shape[1]
610
+ # the below ops not supported in Tensorrt
611
+ # chunk_out_lens = torch.div(chunk_lens, subsampling_rate,
612
+ # rounding_mode='floor')
613
+ chunk_out_lens = (
614
+ chunk_lens // self.subsampling_rate //
615
+ self.calculate_downsampling_factor(self.num_blocks + 1))
616
+ chunk_out_lens += 1
617
+ r_offset = r_offset.unsqueeze(1)
618
+
619
+ return (
620
+ log_probs,
621
+ log_probs_idx,
622
+ chunk_out,
623
+ chunk_out_lens,
624
+ r_offset,
625
+ r_att_cache,
626
+ r_cnn_cache,
627
+ r_cache_mask,
628
+ )
629
+
630
+
631
+ class Decoder(torch.nn.Module):
632
+
633
+ def __init__(
634
+ self,
635
+ decoder: TransformerDecoder,
636
+ ctc_weight: float = 0.5,
637
+ reverse_weight: float = 0.0,
638
+ beam_size: int = 10,
639
+ decoder_fastertransformer: bool = False,
640
+ ):
641
+ super().__init__()
642
+ self.decoder = decoder
643
+ self.ctc_weight = ctc_weight
644
+ self.reverse_weight = reverse_weight
645
+ self.beam_size = beam_size
646
+ self.decoder_fastertransformer = decoder_fastertransformer
647
+
648
+ def forward(
649
+ self,
650
+ encoder_out: torch.Tensor,
651
+ encoder_lens: torch.Tensor,
652
+ hyps_pad_sos_eos: torch.Tensor,
653
+ hyps_lens_sos: torch.Tensor,
654
+ r_hyps_pad_sos_eos: torch.Tensor,
655
+ ctc_score: torch.Tensor,
656
+ ):
657
+ """Encoder
658
+ Args:
659
+ encoder_out: B x T x F
660
+ encoder_lens: B
661
+ hyps_pad_sos_eos: B x beam x (T2+1),
662
+ hyps with sos & eos and padded by ignore id
663
+ hyps_lens_sos: B x beam, length for each hyp with sos
664
+ r_hyps_pad_sos_eos: B x beam x (T2+1),
665
+ reversed hyps with sos & eos and padded by ignore id
666
+ ctc_score: B x beam, ctc score for each hyp
667
+ Returns:
668
+ decoder_out: B x beam x T2 x V
669
+ r_decoder_out: B x beam x T2 x V
670
+ best_index: B
671
+ """
672
+ B, T, F = encoder_out.shape
673
+ bz = self.beam_size
674
+ B2 = B * bz
675
+ encoder_out = encoder_out.repeat(1, bz, 1).view(B2, T, F)
676
+ encoder_mask = ~make_pad_mask(encoder_lens, T).unsqueeze(1)
677
+ encoder_mask = encoder_mask.repeat(1, bz, 1).view(B2, 1, T)
678
+ T2 = hyps_pad_sos_eos.shape[2] - 1
679
+ hyps_pad = hyps_pad_sos_eos.view(B2, T2 + 1)
680
+ hyps_lens = hyps_lens_sos.view(B2, )
681
+ hyps_pad_sos = hyps_pad[:, :-1].contiguous()
682
+ hyps_pad_eos = hyps_pad[:, 1:].contiguous()
683
+
684
+ r_hyps_pad = r_hyps_pad_sos_eos.view(B2, T2 + 1)
685
+ r_hyps_pad_sos = r_hyps_pad[:, :-1].contiguous()
686
+ r_hyps_pad_eos = r_hyps_pad[:, 1:].contiguous()
687
+
688
+ decoder_out, r_decoder_out, _ = self.decoder(
689
+ encoder_out,
690
+ encoder_mask,
691
+ hyps_pad_sos,
692
+ hyps_lens,
693
+ r_hyps_pad_sos,
694
+ self.reverse_weight,
695
+ )
696
+ decoder_out = torch.nn.functional.log_softmax(decoder_out, dim=-1)
697
+ V = decoder_out.shape[-1]
698
+ decoder_out = decoder_out.view(B2, T2, V)
699
+ mask = ~make_pad_mask(hyps_lens, T2) # B2 x T2
700
+ # mask index, remove ignore id
701
+ index = torch.unsqueeze(hyps_pad_eos * mask, 2)
702
+ score = decoder_out.gather(2, index).squeeze(2) # B2 X T2
703
+ # mask padded part
704
+ score = score * mask
705
+ decoder_out = decoder_out.view(B, bz, T2, V)
706
+ if self.reverse_weight > 0:
707
+ r_decoder_out = torch.nn.functional.log_softmax(r_decoder_out,
708
+ dim=-1)
709
+ r_decoder_out = r_decoder_out.view(B2, T2, V)
710
+ index = torch.unsqueeze(r_hyps_pad_eos * mask, 2)
711
+ r_score = r_decoder_out.gather(2, index).squeeze(2)
712
+ r_score = r_score * mask
713
+ score = (score * (1 - self.reverse_weight) +
714
+ self.reverse_weight * r_score)
715
+ r_decoder_out = r_decoder_out.view(B, bz, T2, V)
716
+ score = torch.sum(score, axis=1) # B2
717
+ score = torch.reshape(score, (B, bz)) + self.ctc_weight * ctc_score
718
+ best_index = torch.argmax(score, dim=1)
719
+ if self.decoder_fastertransformer:
720
+ return decoder_out, best_index
721
+ else:
722
+ return best_index
723
+
724
+
725
+ def to_numpy(tensors):
726
+ out = []
727
+ if type(tensors) == torch.tensor:
728
+ tensors = [tensors]
729
+ for tensor in tensors:
730
+ if tensor.requires_grad:
731
+ tensor = tensor.detach().cpu().numpy()
732
+ else:
733
+ tensor = tensor.cpu().numpy()
734
+ out.append(tensor)
735
+ return out
736
+
737
+
738
+ def test(xlist, blist, rtol=1e-3, atol=1e-5, tolerate_small_mismatch=True):
739
+ for a, b in zip(xlist, blist):
740
+ try:
741
+ torch.testing.assert_allclose(a, b, rtol=rtol, atol=atol)
742
+ except AssertionError as error:
743
+ if tolerate_small_mismatch:
744
+ print(error)
745
+ else:
746
+ raise
747
+
748
+
749
+ def export_offline_encoder(model, configs, args, logger, encoder_onnx_path):
750
+ bz = 32
751
+ seq_len = 100
752
+ beam_size = args.beam_size
753
+ feature_size = configs["input_dim"]
754
+
755
+ speech = torch.randn(bz, seq_len, feature_size, dtype=torch.float32)
756
+ speech_lens = torch.randint(low=10,
757
+ high=seq_len,
758
+ size=(bz, ),
759
+ dtype=torch.int32)
760
+ encoder = Encoder(model.encoder, model.ctc, beam_size)
761
+ encoder.eval()
762
+
763
+ torch.onnx.export(
764
+ encoder,
765
+ (speech, speech_lens),
766
+ encoder_onnx_path,
767
+ export_params=True,
768
+ opset_version=13,
769
+ do_constant_folding=True,
770
+ input_names=["speech", "speech_lengths"],
771
+ output_names=[
772
+ "encoder_out",
773
+ "encoder_out_lens",
774
+ "ctc_log_probs",
775
+ "beam_log_probs",
776
+ "beam_log_probs_idx",
777
+ ],
778
+ dynamic_axes={
779
+ "speech": {
780
+ 0: "B",
781
+ 1: "T"
782
+ },
783
+ "speech_lengths": {
784
+ 0: "B"
785
+ },
786
+ "encoder_out": {
787
+ 0: "B",
788
+ 1: "T_OUT"
789
+ },
790
+ "encoder_out_lens": {
791
+ 0: "B"
792
+ },
793
+ "ctc_log_probs": {
794
+ 0: "B",
795
+ 1: "T_OUT"
796
+ },
797
+ "beam_log_probs": {
798
+ 0: "B",
799
+ 1: "T_OUT"
800
+ },
801
+ "beam_log_probs_idx": {
802
+ 0: "B",
803
+ 1: "T_OUT"
804
+ },
805
+ },
806
+ verbose=False,
807
+ )
808
+
809
+ with torch.no_grad():
810
+ o0, o1, o2, o3, o4 = encoder(speech, speech_lens)
811
+
812
+ providers = ["CUDAExecutionProvider"]
813
+ ort_session = onnxruntime.InferenceSession(encoder_onnx_path,
814
+ providers=providers)
815
+ ort_inputs = {
816
+ "speech": to_numpy(speech),
817
+ "speech_lengths": to_numpy(speech_lens),
818
+ }
819
+ ort_outs = ort_session.run(None, ort_inputs)
820
+
821
+ # check encoder output
822
+ test(to_numpy([o0, o1, o2, o3, o4]), ort_outs)
823
+ logger.info("export offline onnx encoder succeed!")
824
+ onnx_config = {
825
+ "beam_size": args.beam_size,
826
+ "reverse_weight": args.reverse_weight,
827
+ "ctc_weight": args.ctc_weight,
828
+ "fp16": args.fp16,
829
+ }
830
+ return onnx_config
831
+
832
+
833
+ def export_online_encoder(model, configs, args, logger, encoder_onnx_path):
834
+ decoding_chunk_size = args.decoding_chunk_size
835
+ subsampling = model.encoder.embed.subsampling_rate
836
+ context = model.encoder.embed.right_context + 1
837
+ decoding_window = (decoding_chunk_size - 1) * subsampling + context
838
+ batch_size = 32
839
+ audio_len = decoding_window
840
+ feature_size = configs["input_dim"]
841
+ output_size = configs["encoder_conf"]["output_size"]
842
+ num_layers = configs["encoder_conf"]["num_blocks"]
843
+ # in transformer the cnn module will not be available
844
+ transformer = False
845
+ cnn_module_kernel = configs["encoder_conf"].get("cnn_module_kernel", 1) - 1
846
+ if not cnn_module_kernel:
847
+ transformer = True
848
+ num_decoding_left_chunks = args.num_decoding_left_chunks
849
+ required_cache_size = decoding_chunk_size * num_decoding_left_chunks
850
+ if configs["encoder"] == "squeezeformer":
851
+ encoder = StreamingSqueezeformerEncoder(model, required_cache_size,
852
+ args.beam_size)
853
+ elif configs["encoder"] == "efficientConformer":
854
+ encoder = StreamingEfficientConformerEncoder(model,
855
+ required_cache_size,
856
+ args.beam_size)
857
+ else:
858
+ encoder = StreamingEncoder(
859
+ model,
860
+ required_cache_size,
861
+ args.beam_size,
862
+ transformer,
863
+ args.return_ctc_logprobs,
864
+ )
865
+ encoder.eval()
866
+
867
+ # begin to export encoder
868
+ chunk_xs = torch.randn(batch_size,
869
+ audio_len,
870
+ feature_size,
871
+ dtype=torch.float32)
872
+ chunk_lens = torch.ones(batch_size, dtype=torch.int32) * audio_len
873
+
874
+ offset = torch.arange(0, batch_size).unsqueeze(1)
875
+ # (elayers, b, head, cache_t1, d_k * 2)
876
+ head = configs["encoder_conf"]["attention_heads"]
877
+ d_k = configs["encoder_conf"]["output_size"] // head
878
+ att_cache = torch.randn(
879
+ batch_size,
880
+ num_layers,
881
+ head,
882
+ required_cache_size,
883
+ d_k * 2,
884
+ dtype=torch.float32,
885
+ )
886
+ cnn_cache = torch.randn(
887
+ batch_size,
888
+ num_layers,
889
+ output_size,
890
+ cnn_module_kernel,
891
+ dtype=torch.float32,
892
+ )
893
+
894
+ cache_mask = torch.ones(batch_size,
895
+ 1,
896
+ required_cache_size,
897
+ dtype=torch.float32)
898
+ input_names = [
899
+ "chunk_xs",
900
+ "chunk_lens",
901
+ "offset",
902
+ "att_cache",
903
+ "cnn_cache",
904
+ "cache_mask",
905
+ ]
906
+ output_names = [
907
+ "log_probs",
908
+ "log_probs_idx",
909
+ "chunk_out",
910
+ "chunk_out_lens",
911
+ "r_offset",
912
+ "r_att_cache",
913
+ "r_cnn_cache",
914
+ "r_cache_mask",
915
+ ]
916
+ if args.return_ctc_logprobs:
917
+ output_names = [
918
+ "ctc_log_probs",
919
+ "chunk_out",
920
+ "chunk_out_lens",
921
+ "r_offset",
922
+ "r_att_cache",
923
+ "r_cnn_cache",
924
+ "r_cache_mask",
925
+ ]
926
+ input_tensors = (
927
+ chunk_xs,
928
+ chunk_lens,
929
+ offset,
930
+ att_cache,
931
+ cnn_cache,
932
+ cache_mask,
933
+ )
934
+ if transformer:
935
+ assert (args.return_ctc_logprobs is
936
+ False), "return_ctc_logprobs is not supported in transformer"
937
+ output_names.pop(6)
938
+
939
+ all_names = input_names + output_names
940
+ dynamic_axes = {}
941
+ for name in all_names:
942
+ # only the first dimension is dynamic
943
+ # all other dimension is fixed
944
+ dynamic_axes[name] = {0: "B"}
945
+
946
+ torch.onnx.export(
947
+ encoder,
948
+ input_tensors,
949
+ encoder_onnx_path,
950
+ export_params=True,
951
+ opset_version=14,
952
+ do_constant_folding=True,
953
+ input_names=input_names,
954
+ output_names=output_names,
955
+ dynamic_axes=dynamic_axes,
956
+ verbose=False,
957
+ )
958
+
959
+ with torch.no_grad():
960
+ torch_outs = encoder(chunk_xs, chunk_lens, offset, att_cache,
961
+ cnn_cache, cache_mask)
962
+ if transformer:
963
+ torch_outs = list(torch_outs).pop(6)
964
+ ort_session = onnxruntime.InferenceSession(
965
+ encoder_onnx_path, providers=["CUDAExecutionProvider"])
966
+ ort_inputs = {}
967
+
968
+ input_tensors = to_numpy(input_tensors)
969
+ for idx, name in enumerate(input_names):
970
+ ort_inputs[name] = input_tensors[idx]
971
+ if transformer:
972
+ del ort_inputs["cnn_cache"]
973
+ ort_outs = ort_session.run(None, ort_inputs)
974
+ test(to_numpy(torch_outs), ort_outs, rtol=1e-03, atol=1e-05)
975
+ logger.info("export to onnx streaming encoder succeed!")
976
+ onnx_config = {
977
+ "subsampling_rate": subsampling,
978
+ "context": context,
979
+ "decoding_chunk_size": decoding_chunk_size,
980
+ "num_decoding_left_chunks": num_decoding_left_chunks,
981
+ "beam_size": args.beam_size,
982
+ "fp16": args.fp16,
983
+ "feat_size": feature_size,
984
+ "decoding_window": decoding_window,
985
+ "cnn_module_kernel_cache": cnn_module_kernel,
986
+ "return_ctc_logprobs": args.return_ctc_logprobs,
987
+ }
988
+ return onnx_config
989
+
990
+
991
+ def export_rescoring_decoder(model, configs, args, logger, decoder_onnx_path,
992
+ decoder_fastertransformer):
993
+ bz, seq_len = 32, 100
994
+ beam_size = args.beam_size
995
+ decoder = Decoder(
996
+ model.decoder,
997
+ model.ctc_weight,
998
+ model.reverse_weight,
999
+ beam_size,
1000
+ decoder_fastertransformer,
1001
+ )
1002
+ decoder.eval()
1003
+
1004
+ hyps_pad_sos_eos = torch.randint(low=3,
1005
+ high=1000,
1006
+ size=(bz, beam_size, seq_len))
1007
+ hyps_lens_sos = torch.randint(low=3,
1008
+ high=seq_len,
1009
+ size=(bz, beam_size),
1010
+ dtype=torch.int32)
1011
+ r_hyps_pad_sos_eos = torch.randint(low=3,
1012
+ high=1000,
1013
+ size=(bz, beam_size, seq_len))
1014
+
1015
+ output_size = configs["encoder_conf"]["output_size"]
1016
+ encoder_out = torch.randn(bz, seq_len, output_size, dtype=torch.float32)
1017
+ encoder_out_lens = torch.randint(low=3,
1018
+ high=seq_len,
1019
+ size=(bz, ),
1020
+ dtype=torch.int32)
1021
+ ctc_score = torch.randn(bz, beam_size, dtype=torch.float32)
1022
+
1023
+ input_names = [
1024
+ "encoder_out",
1025
+ "encoder_out_lens",
1026
+ "hyps_pad_sos_eos",
1027
+ "hyps_lens_sos",
1028
+ "r_hyps_pad_sos_eos",
1029
+ "ctc_score",
1030
+ ]
1031
+ output_names = ["best_index"]
1032
+ if decoder_fastertransformer:
1033
+ output_names.insert(0, "decoder_out")
1034
+
1035
+ torch.onnx.export(
1036
+ decoder,
1037
+ (
1038
+ encoder_out,
1039
+ encoder_out_lens,
1040
+ hyps_pad_sos_eos,
1041
+ hyps_lens_sos,
1042
+ r_hyps_pad_sos_eos,
1043
+ ctc_score,
1044
+ ),
1045
+ decoder_onnx_path,
1046
+ export_params=True,
1047
+ opset_version=13,
1048
+ do_constant_folding=True,
1049
+ input_names=input_names,
1050
+ output_names=output_names,
1051
+ dynamic_axes={
1052
+ "encoder_out": {
1053
+ 0: "B",
1054
+ 1: "T"
1055
+ },
1056
+ "encoder_out_lens": {
1057
+ 0: "B"
1058
+ },
1059
+ "hyps_pad_sos_eos": {
1060
+ 0: "B",
1061
+ 2: "T2"
1062
+ },
1063
+ "hyps_lens_sos": {
1064
+ 0: "B"
1065
+ },
1066
+ "r_hyps_pad_sos_eos": {
1067
+ 0: "B",
1068
+ 2: "T2"
1069
+ },
1070
+ "ctc_score": {
1071
+ 0: "B"
1072
+ },
1073
+ "best_index": {
1074
+ 0: "B"
1075
+ },
1076
+ },
1077
+ verbose=False,
1078
+ )
1079
+ with torch.no_grad():
1080
+ o0 = decoder(
1081
+ encoder_out,
1082
+ encoder_out_lens,
1083
+ hyps_pad_sos_eos,
1084
+ hyps_lens_sos,
1085
+ r_hyps_pad_sos_eos,
1086
+ ctc_score,
1087
+ )
1088
+ providers = ["CUDAExecutionProvider"]
1089
+ ort_session = onnxruntime.InferenceSession(decoder_onnx_path,
1090
+ providers=providers)
1091
+
1092
+ input_tensors = [
1093
+ encoder_out,
1094
+ encoder_out_lens,
1095
+ hyps_pad_sos_eos,
1096
+ hyps_lens_sos,
1097
+ r_hyps_pad_sos_eos,
1098
+ ctc_score,
1099
+ ]
1100
+ ort_inputs = {}
1101
+ input_tensors = to_numpy(input_tensors)
1102
+ for idx, name in enumerate(input_names):
1103
+ ort_inputs[name] = input_tensors[idx]
1104
+
1105
+ # if model.reverse weight == 0,
1106
+ # the r_hyps_pad will be removed
1107
+ # from the onnx decoder since it doen't play any role
1108
+ if model.reverse_weight == 0:
1109
+ del ort_inputs["r_hyps_pad_sos_eos"]
1110
+ ort_outs = ort_session.run(None, ort_inputs)
1111
+
1112
+ # check decoder output
1113
+ if decoder_fastertransformer:
1114
+ test(to_numpy(o0), ort_outs, rtol=1e-03, atol=1e-05)
1115
+ else:
1116
+ test(to_numpy([o0]), ort_outs, rtol=1e-03, atol=1e-05)
1117
+ logger.info("export to onnx decoder succeed!")
1118
+
1119
+
1120
+ if __name__ == "__main__":
1121
+ parser = argparse.ArgumentParser(description="export x86_gpu model")
1122
+ parser.add_argument("--config", required=True, help="config file")
1123
+ parser.add_argument("--checkpoint", required=True, help="checkpoint model")
1124
+ parser.add_argument(
1125
+ "--cmvn_file",
1126
+ required=False,
1127
+ default="",
1128
+ type=str,
1129
+ help="global_cmvn file, default path is in config file",
1130
+ )
1131
+ parser.add_argument(
1132
+ "--reverse_weight",
1133
+ default=-1.0,
1134
+ type=float,
1135
+ required=False,
1136
+ help="reverse weight for bitransformer," +
1137
+ "default value is in config file",
1138
+ )
1139
+ parser.add_argument(
1140
+ "--ctc_weight",
1141
+ default=-1.0,
1142
+ type=float,
1143
+ required=False,
1144
+ help="ctc weight, default value is in config file",
1145
+ )
1146
+ parser.add_argument(
1147
+ "--beam_size",
1148
+ default=10,
1149
+ type=int,
1150
+ required=False,
1151
+ help="beam size would be ctc output size",
1152
+ )
1153
+ parser.add_argument(
1154
+ "--output_onnx_dir",
1155
+ default="onnx_model",
1156
+ help="output onnx encoder and decoder directory",
1157
+ )
1158
+ parser.add_argument(
1159
+ "--fp16",
1160
+ action="store_true",
1161
+ help="whether to export fp16 model, default false",
1162
+ )
1163
+ # arguments for streaming encoder
1164
+ parser.add_argument(
1165
+ "--streaming",
1166
+ action="store_true",
1167
+ help="whether to export streaming encoder, default false",
1168
+ )
1169
+ parser.add_argument(
1170
+ "--decoding_chunk_size",
1171
+ default=16,
1172
+ type=int,
1173
+ required=False,
1174
+ help="the decoding chunk size, <=0 is not supported",
1175
+ )
1176
+ parser.add_argument(
1177
+ "--num_decoding_left_chunks",
1178
+ default=5,
1179
+ type=int,
1180
+ required=False,
1181
+ help="number of left chunks, <= 0 is not supported",
1182
+ )
1183
+ parser.add_argument(
1184
+ "--decoder_fastertransformer",
1185
+ action="store_true",
1186
+ help="return decoder_out and best_index for ft",
1187
+ )
1188
+ parser.add_argument(
1189
+ "--return_ctc_logprobs",
1190
+ action="store_true",
1191
+ help="return full ctc_log_probs for TLG streaming encoder",
1192
+ )
1193
+ args = parser.parse_args()
1194
+
1195
+ torch.manual_seed(0)
1196
+ torch.set_printoptions(precision=10)
1197
+
1198
+ with open(args.config, "r") as fin:
1199
+ configs = yaml.load(fin, Loader=yaml.FullLoader)
1200
+ if args.cmvn_file and os.path.exists(args.cmvn_file):
1201
+ if 'cmvn' not in configs:
1202
+ configs['cmvn'] = "global_cmvn"
1203
+ configs['cmvn_conf'] = {}
1204
+ else:
1205
+ assert configs['cmvn'] == "global_cmvn"
1206
+ assert configs['cmvn_conf'] is not None
1207
+ configs['cmvn_conf']["cmvn_file"] = args.cmvn_file
1208
+ if (args.reverse_weight != -1.0
1209
+ and "reverse_weight" in configs["model_conf"]):
1210
+ configs["model_conf"]["reverse_weight"] = args.reverse_weight
1211
+ print("Update reverse weight to", args.reverse_weight)
1212
+ if args.ctc_weight != -1:
1213
+ print("Update ctc weight to ", args.ctc_weight)
1214
+ configs["model_conf"]["ctc_weight"] = args.ctc_weight
1215
+ configs["encoder_conf"]["use_dynamic_chunk"] = False
1216
+
1217
+ model, configs = init_model(args, configs)
1218
+ model.eval()
1219
+
1220
+ if not os.path.exists(args.output_onnx_dir):
1221
+ os.mkdir(args.output_onnx_dir)
1222
+ encoder_onnx_path = os.path.join(args.output_onnx_dir, "encoder.onnx")
1223
+ export_enc_func = None
1224
+ if args.streaming:
1225
+ assert args.decoding_chunk_size > 0
1226
+ assert args.num_decoding_left_chunks > 0
1227
+ export_enc_func = export_online_encoder
1228
+ else:
1229
+ export_enc_func = export_offline_encoder
1230
+
1231
+ onnx_config = export_enc_func(model, configs, args, logger,
1232
+ encoder_onnx_path)
1233
+
1234
+ decoder_onnx_path = os.path.join(args.output_onnx_dir, "decoder.onnx")
1235
+ export_rescoring_decoder(
1236
+ model,
1237
+ configs,
1238
+ args,
1239
+ logger,
1240
+ decoder_onnx_path,
1241
+ args.decoder_fastertransformer,
1242
+ )
1243
+
1244
+ if args.fp16:
1245
+ try:
1246
+ import onnxmltools
1247
+ from onnxmltools.utils.float16_converter import \
1248
+ convert_float_to_float16
1249
+ except ImportError:
1250
+ print("Please install onnxmltools!")
1251
+ sys.exit(1)
1252
+ encoder_onnx_model = onnxmltools.utils.load_model(encoder_onnx_path)
1253
+ encoder_onnx_model = convert_float_to_float16(encoder_onnx_model)
1254
+ encoder_onnx_path = os.path.join(args.output_onnx_dir,
1255
+ "encoder_fp16.onnx")
1256
+ onnxmltools.utils.save_model(encoder_onnx_model, encoder_onnx_path)
1257
+ decoder_onnx_model = onnxmltools.utils.load_model(decoder_onnx_path)
1258
+ decoder_onnx_model = convert_float_to_float16(decoder_onnx_model)
1259
+ decoder_onnx_path = os.path.join(args.output_onnx_dir,
1260
+ "decoder_fp16.onnx")
1261
+ onnxmltools.utils.save_model(decoder_onnx_model, decoder_onnx_path)
1262
+ # dump configurations
1263
+
1264
+ config_dir = os.path.join(args.output_onnx_dir, "config.yaml")
1265
+ with open(config_dir, "w") as out:
1266
+ yaml.dump(onnx_config, out)
wenet/bin/recognize.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2020 Mobvoi Inc. (authors: Binbin Zhang, Xiaoyu Chen, Di Wu)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import print_function
16
+
17
+ import argparse
18
+ import copy
19
+ import logging
20
+ import os
21
+
22
+ import torch
23
+ import yaml
24
+ from torch.utils.data import DataLoader
25
+
26
+ from wenet.dataset.dataset import Dataset
27
+ from wenet.utils.config import override_config
28
+ from wenet.utils.init_model import init_model
29
+ from wenet.utils.init_tokenizer import init_tokenizer
30
+ from wenet.utils.context_graph import ContextGraph
31
+ from wenet.utils.ctc_utils import get_blank_id
32
+ from wenet.utils.common import TORCH_NPU_AVAILABLE # noqa just ensure to check torch-npu
33
+
34
+
35
+ def get_args():
36
+ parser = argparse.ArgumentParser(description='recognize with your model')
37
+ parser.add_argument('--config', required=True, help='config file')
38
+ parser.add_argument('--test_data', required=True, help='test data file')
39
+ parser.add_argument('--data_type',
40
+ default='raw',
41
+ choices=['raw', 'shard'],
42
+ help='train and cv data type')
43
+ parser.add_argument('--gpu',
44
+ type=int,
45
+ default=-1,
46
+ help='gpu id for this rank, -1 for cpu')
47
+ parser.add_argument('--device',
48
+ type=str,
49
+ default="cpu",
50
+ choices=["cpu", "npu", "cuda"],
51
+ help='accelerator to use')
52
+ parser.add_argument('--dtype',
53
+ type=str,
54
+ default='fp32',
55
+ choices=['fp16', 'fp32', 'bf16'],
56
+ help='model\'s dtype')
57
+ parser.add_argument('--num_workers',
58
+ default=0,
59
+ type=int,
60
+ help='num of subprocess workers for reading')
61
+ parser.add_argument('--checkpoint', required=True, help='checkpoint model')
62
+ parser.add_argument('--beam_size',
63
+ type=int,
64
+ default=10,
65
+ help='beam size for search')
66
+ parser.add_argument('--length_penalty',
67
+ type=float,
68
+ default=0.0,
69
+ help='length penalty')
70
+ parser.add_argument('--blank_penalty',
71
+ type=float,
72
+ default=0.0,
73
+ help='blank penalty')
74
+ parser.add_argument('--result_dir', required=True, help='asr result file')
75
+ parser.add_argument('--batch_size',
76
+ type=int,
77
+ default=16,
78
+ help='asr result file')
79
+ parser.add_argument('--modes',
80
+ nargs='+',
81
+ help="""decoding mode, support the following:
82
+ attention
83
+ ctc_greedy_search
84
+ ctc_prefix_beam_search
85
+ attention_rescoring
86
+ rnnt_greedy_search
87
+ rnnt_beam_search
88
+ rnnt_beam_attn_rescoring
89
+ ctc_beam_td_attn_rescoring
90
+ hlg_onebest
91
+ hlg_rescore
92
+ paraformer_greedy_search
93
+ paraformer_beam_search""")
94
+ parser.add_argument('--search_ctc_weight',
95
+ type=float,
96
+ default=1.0,
97
+ help='ctc weight for nbest generation')
98
+ parser.add_argument('--search_transducer_weight',
99
+ type=float,
100
+ default=0.0,
101
+ help='transducer weight for nbest generation')
102
+ parser.add_argument('--ctc_weight',
103
+ type=float,
104
+ default=0.0,
105
+ help='ctc weight for rescoring weight in \
106
+ attention rescoring decode mode \
107
+ ctc weight for rescoring weight in \
108
+ transducer attention rescore decode mode')
109
+
110
+ parser.add_argument('--transducer_weight',
111
+ type=float,
112
+ default=0.0,
113
+ help='transducer weight for rescoring weight in '
114
+ 'transducer attention rescore mode')
115
+ parser.add_argument('--attn_weight',
116
+ type=float,
117
+ default=0.0,
118
+ help='attention weight for rescoring weight in '
119
+ 'transducer attention rescore mode')
120
+ parser.add_argument('--decoding_chunk_size',
121
+ type=int,
122
+ default=-1,
123
+ help='''decoding chunk size,
124
+ <0: for decoding, use full chunk.
125
+ >0: for decoding, use fixed chunk size as set.
126
+ 0: used for training, it's prohibited here''')
127
+ parser.add_argument('--num_decoding_left_chunks',
128
+ type=int,
129
+ default=-1,
130
+ help='number of left chunks for decoding')
131
+ parser.add_argument('--simulate_streaming',
132
+ action='store_true',
133
+ help='simulate streaming inference')
134
+ parser.add_argument('--reverse_weight',
135
+ type=float,
136
+ default=0.0,
137
+ help='''right to left weight for attention rescoring
138
+ decode mode''')
139
+ parser.add_argument('--override_config',
140
+ action='append',
141
+ default=[],
142
+ help="override yaml config")
143
+
144
+ parser.add_argument('--word',
145
+ default='',
146
+ type=str,
147
+ help='word file, only used for hlg decode')
148
+ parser.add_argument('--hlg',
149
+ default='',
150
+ type=str,
151
+ help='hlg file, only used for hlg decode')
152
+ parser.add_argument('--lm_scale',
153
+ type=float,
154
+ default=0.0,
155
+ help='lm scale for hlg attention rescore decode')
156
+ parser.add_argument('--decoder_scale',
157
+ type=float,
158
+ default=0.0,
159
+ help='lm scale for hlg attention rescore decode')
160
+ parser.add_argument('--r_decoder_scale',
161
+ type=float,
162
+ default=0.0,
163
+ help='lm scale for hlg attention rescore decode')
164
+
165
+ parser.add_argument(
166
+ '--context_bias_mode',
167
+ type=str,
168
+ default='',
169
+ help='''Context bias mode, selectable from the following
170
+ option: decoding-graph, deep-biasing''')
171
+ parser.add_argument('--context_list_path',
172
+ type=str,
173
+ default='',
174
+ help='Context list path')
175
+ parser.add_argument('--context_graph_score',
176
+ type=float,
177
+ default=0.0,
178
+ help='''The higher the score, the greater the degree of
179
+ bias using decoding-graph for biasing''')
180
+
181
+ parser.add_argument('--use_lora',
182
+ type=bool,
183
+ default=False,
184
+ help='''Whether to use lora for biasing''')
185
+ parser.add_argument("--lora_ckpt_path",
186
+ default=None,
187
+ type=str,
188
+ help="lora checkpoint path.")
189
+ args = parser.parse_args()
190
+ print(args)
191
+ return args
192
+
193
+
194
+ def main():
195
+ args = get_args()
196
+ logging.basicConfig(level=logging.DEBUG,
197
+ format='%(asctime)s %(levelname)s %(message)s')
198
+ if args.gpu != -1:
199
+ # remain the original usage of gpu
200
+ args.device = "cuda"
201
+ if "cuda" in args.device:
202
+ os.environ['CUDA_VISIBLE_DEVICES'] = str(args.gpu)
203
+
204
+ with open(args.config, 'r') as fin:
205
+ configs = yaml.load(fin, Loader=yaml.FullLoader)
206
+ if len(args.override_config) > 0:
207
+ configs = override_config(configs, args.override_config)
208
+
209
+ test_conf = copy.deepcopy(configs['dataset_conf'])
210
+
211
+ test_conf['filter_conf']['max_length'] = 102400
212
+ test_conf['filter_conf']['min_length'] = 0
213
+ test_conf['filter_conf']['token_max_length'] = 102400
214
+ test_conf['filter_conf']['token_min_length'] = 0
215
+ test_conf['filter_conf']['max_output_input_ratio'] = 102400
216
+ test_conf['filter_conf']['min_output_input_ratio'] = 0
217
+ test_conf['speed_perturb'] = False
218
+ test_conf['spec_aug'] = False
219
+ test_conf['spec_sub'] = False
220
+ test_conf['spec_trim'] = False
221
+ test_conf['shuffle'] = False
222
+ test_conf['sort'] = False
223
+ test_conf['cycle'] = 1
224
+ test_conf['list_shuffle'] = False
225
+ if 'fbank_conf' in test_conf:
226
+ test_conf['fbank_conf']['dither'] = 0.0
227
+ elif 'mfcc_conf' in test_conf:
228
+ test_conf['mfcc_conf']['dither'] = 0.0
229
+ test_conf['batch_conf']['batch_type'] = "static"
230
+ test_conf['batch_conf']['batch_size'] = args.batch_size
231
+
232
+ tokenizer = init_tokenizer(configs)
233
+ test_dataset = Dataset(args.data_type,
234
+ args.test_data,
235
+ tokenizer,
236
+ test_conf,
237
+ partition=False)
238
+
239
+ test_data_loader = DataLoader(test_dataset,
240
+ batch_size=None,
241
+ num_workers=args.num_workers)
242
+
243
+ # Init asr model from configs
244
+ args.jit = False
245
+ model, configs = init_model(args, configs)
246
+
247
+ device = torch.device(args.device)
248
+ model = model.to(device)
249
+ model.eval()
250
+ dtype = torch.float32
251
+ if args.dtype == 'fp16':
252
+ dtype = torch.float16
253
+ elif args.dtype == 'bf16':
254
+ dtype = torch.bfloat16
255
+ logging.info("compute dtype is {}".format(dtype))
256
+
257
+ context_graph = None
258
+ if 'decoding-graph' in args.context_bias_mode:
259
+ context_graph = ContextGraph(args.context_list_path,
260
+ tokenizer.symbol_table,
261
+ configs['tokenizer_conf']['bpe_path'],
262
+ args.context_graph_score)
263
+
264
+ _, blank_id = get_blank_id(configs, tokenizer.symbol_table)
265
+ logging.info("blank_id is {}".format(blank_id))
266
+
267
+ # TODO(Dinghao Zhou): Support RNN-T related decoding
268
+ # TODO(Lv Xiang): Support k2 related decoding
269
+ # TODO(Kaixun Huang): Support context graph
270
+ files = {}
271
+ for mode in args.modes:
272
+ dir_name = os.path.join(args.result_dir, mode)
273
+ os.makedirs(dir_name, exist_ok=True)
274
+ file_name = os.path.join(dir_name, 'text')
275
+ files[mode] = open(file_name, 'w')
276
+ max_format_len = max([len(mode) for mode in args.modes])
277
+
278
+ with torch.cuda.amp.autocast(enabled=True,
279
+ dtype=dtype,
280
+ cache_enabled=False):
281
+ with torch.no_grad():
282
+ for batch_idx, batch in enumerate(test_data_loader):
283
+ keys = batch["keys"]
284
+ feats = batch["feats"].to(device)
285
+ target = batch["target"].to(device)
286
+ feats_lengths = batch["feats_lengths"].to(device)
287
+ target_lengths = batch["target_lengths"].to(device)
288
+ infos = {"tasks": batch["tasks"], "langs": batch["langs"]}
289
+ results = model.decode(
290
+ args.modes,
291
+ feats,
292
+ feats_lengths,
293
+ args.beam_size,
294
+ decoding_chunk_size=args.decoding_chunk_size,
295
+ num_decoding_left_chunks=args.num_decoding_left_chunks,
296
+ ctc_weight=args.ctc_weight,
297
+ simulate_streaming=args.simulate_streaming,
298
+ reverse_weight=args.reverse_weight,
299
+ context_graph=context_graph,
300
+ blank_id=blank_id,
301
+ blank_penalty=args.blank_penalty,
302
+ length_penalty=args.length_penalty,
303
+ infos=infos)
304
+ for i, key in enumerate(keys):
305
+ for mode, hyps in results.items():
306
+ tokens = hyps[i].tokens
307
+ line = '{} {}'.format(key,
308
+ tokenizer.detokenize(tokens)[0])
309
+ logging.info('{} {}'.format(mode.ljust(max_format_len),
310
+ line))
311
+ files[mode].write(line + '\n')
312
+ for mode, f in files.items():
313
+ f.close()
314
+
315
+
316
+ if __name__ == '__main__':
317
+ main()
wenet/bin/recognize_onnx_gpu.py ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2020 Mobvoi Inc. (authors: Binbin Zhang, Xiaoyu Chen, Di Wu)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
16
+ #
17
+ # Licensed under the Apache License, Version 2.0 (the "License");
18
+ # you may not use this file except in compliance with the License.
19
+ # You may obtain a copy of the License at
20
+ #
21
+ # http://www.apache.org/licenses/LICENSE-2.0
22
+ #
23
+ # Unless required by applicable law or agreed to in writing, software
24
+ # distributed under the License is distributed on an "AS IS" BASIS,
25
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
26
+ # See the License for the specific language governing permissions and
27
+ # limitations under the License.
28
+ """
29
+ This script is for testing exported onnx encoder and decoder from
30
+ export_onnx_gpu.py. The exported onnx models only support batch offline ASR inference.
31
+ It requires a python wrapped c++ ctc decoder.
32
+ Please install it by following:
33
+ https://github.com/Slyne/ctc_decoder.git
34
+ """
35
+ from __future__ import print_function
36
+
37
+ import argparse
38
+ import copy
39
+ import logging
40
+ import os
41
+ import sys
42
+
43
+ import torch
44
+ import yaml
45
+ from torch.utils.data import DataLoader
46
+
47
+ from wenet.dataset.dataset import Dataset
48
+ from wenet.utils.common import IGNORE_ID
49
+ from wenet.utils.config import override_config
50
+ from wenet.utils.init_tokenizer import init_tokenizer
51
+
52
+ import onnxruntime as rt
53
+ import multiprocessing
54
+ import numpy as np
55
+
56
+ try:
57
+ from swig_decoders import map_batch, \
58
+ ctc_beam_search_decoder_batch, \
59
+ TrieVector, PathTrie
60
+ except ImportError:
61
+ print('Please install ctc decoders first by refering to\n' +
62
+ 'https://github.com/Slyne/ctc_decoder.git')
63
+ sys.exit(1)
64
+
65
+ def get_args():
66
+ parser = argparse.ArgumentParser(description='recognize with your model')
67
+ parser.add_argument('--config', required=True, help='config file')
68
+ parser.add_argument('--test_data', required=True, help='test data file')
69
+ parser.add_argument('--data_type',
70
+ default='raw',
71
+ choices=['raw', 'shard'],
72
+ help='train and cv data type')
73
+ parser.add_argument('--gpu',
74
+ type=int,
75
+ default=-1,
76
+ help='gpu id for this rank, -1 for cpu')
77
+ parser.add_argument('--dict', required=True, help='dict file')
78
+ parser.add_argument('--encoder_onnx',
79
+ required=True,
80
+ help='encoder onnx file')
81
+ parser.add_argument('--decoder_onnx',
82
+ required=True,
83
+ help='decoder onnx file')
84
+ parser.add_argument('--result_file', required=True, help='asr result file')
85
+ parser.add_argument('--batch_size',
86
+ type=int,
87
+ default=32,
88
+ help='asr result file')
89
+ parser.add_argument('--mode',
90
+ choices=[
91
+ 'ctc_greedy_search', 'ctc_prefix_beam_search',
92
+ 'attention_rescoring'
93
+ ],
94
+ default='attention_rescoring',
95
+ help='decoding mode')
96
+ parser.add_argument('--bpe_model',
97
+ default=None,
98
+ type=str,
99
+ help='bpe model for english part')
100
+ parser.add_argument('--override_config',
101
+ action='append',
102
+ default=[],
103
+ help="override yaml config")
104
+ parser.add_argument('--fp16',
105
+ action='store_true',
106
+ help='whether to export fp16 model, default false')
107
+ args = parser.parse_args()
108
+ return args
109
+
110
+ def main():
111
+ args = get_args()
112
+ logging.basicConfig(level=logging.DEBUG,
113
+ format='%(asctime)s %(levelname)s %(message)s')
114
+ os.environ['CUDA_VISIBLE_DEVICES'] = str(args.gpu)
115
+
116
+ with open(args.config, 'r') as fin:
117
+ configs = yaml.load(fin, Loader=yaml.FullLoader)
118
+ if len(args.override_config) > 0:
119
+ configs = override_config(configs, args.override_config)
120
+
121
+ reverse_weight = configs["model_conf"].get("reverse_weight", 0.0)
122
+ special_tokens = configs.get('tokenizer_conf', {}).get('special_tokens', None)
123
+ test_conf = copy.deepcopy(configs['dataset_conf'])
124
+ test_conf['filter_conf']['max_length'] = 102400
125
+ test_conf['filter_conf']['min_length'] = 0
126
+ test_conf['filter_conf']['token_max_length'] = 102400
127
+ test_conf['filter_conf']['token_min_length'] = 0
128
+ test_conf['filter_conf']['max_output_input_ratio'] = 102400
129
+ test_conf['filter_conf']['min_output_input_ratio'] = 0
130
+ test_conf['speed_perturb'] = False
131
+ test_conf['spec_aug'] = False
132
+ test_conf['spec_sub'] = False
133
+ test_conf['spec_trim'] = False
134
+ test_conf['shuffle'] = False
135
+ test_conf['sort'] = False
136
+ test_conf['fbank_conf']['dither'] = 0.0
137
+ test_conf['batch_conf']['batch_type'] = "static"
138
+ test_conf['batch_conf']['batch_size'] = args.batch_size
139
+
140
+ tokenizer = init_tokenizer(configs)
141
+ test_dataset = Dataset(args.data_type,
142
+ args.test_data,
143
+ tokenizer,
144
+ test_conf,
145
+ partition=False)
146
+ test_data_loader = DataLoader(test_dataset, batch_size=None, num_workers=0)
147
+
148
+ # Init asr model from configs
149
+ use_cuda = args.gpu >= 0 and torch.cuda.is_available()
150
+ if use_cuda:
151
+ EP_list = ['CUDAExecutionProvider', 'CPUExecutionProvider']
152
+ else:
153
+ EP_list = ['CPUExecutionProvider']
154
+
155
+ encoder_ort_session = rt.InferenceSession(args.encoder_onnx,
156
+ providers=EP_list)
157
+ decoder_ort_session = None
158
+ if args.mode == "attention_rescoring":
159
+ decoder_ort_session = rt.InferenceSession(args.decoder_onnx,
160
+ providers=EP_list)
161
+
162
+ # Load dict
163
+ vocabulary = []
164
+ char_dict = {}
165
+ with open(args.dict, 'r') as fin:
166
+ for line in fin:
167
+ arr = line.strip().split()
168
+ assert len(arr) == 2
169
+ char_dict[int(arr[1])] = arr[0]
170
+ vocabulary.append(arr[0])
171
+
172
+ vocab_size = len(char_dict)
173
+ sos = (vocab_size - 1 if special_tokens is None else
174
+ special_tokens.get("<sos>", vocab_size - 1))
175
+ eos = (vocab_size - 1 if special_tokens is None else
176
+ special_tokens.get("<eos>", vocab_size - 1))
177
+
178
+ with torch.no_grad(), open(args.result_file, 'w') as fout:
179
+ for _, batch in enumerate(test_data_loader):
180
+ keys = batch['keys']
181
+ feats = batch['feats']
182
+ feats_lengths = batch['feats_lengths']
183
+ feats, feats_lengths = feats.numpy(), feats_lengths.numpy()
184
+ if args.fp16:
185
+ feats = feats.astype(np.float16)
186
+ ort_inputs = {
187
+ encoder_ort_session.get_inputs()[0].name: feats,
188
+ encoder_ort_session.get_inputs()[1].name: feats_lengths
189
+ }
190
+ ort_outs = encoder_ort_session.run(None, ort_inputs)
191
+ encoder_out, encoder_out_lens, ctc_log_probs, \
192
+ beam_log_probs, beam_log_probs_idx = ort_outs
193
+ beam_size = beam_log_probs.shape[-1]
194
+ batch_size = beam_log_probs.shape[0]
195
+ num_processes = min(multiprocessing.cpu_count(), batch_size)
196
+ if args.mode == 'ctc_greedy_search':
197
+ if beam_size != 1:
198
+ log_probs_idx = beam_log_probs_idx[:, :, 0]
199
+ batch_sents = []
200
+ for idx, seq in enumerate(log_probs_idx):
201
+ batch_sents.append(seq[0:encoder_out_lens[idx]].tolist())
202
+ hyps = map_batch(batch_sents, vocabulary, num_processes, True,
203
+ 0)
204
+ elif args.mode in ('ctc_prefix_beam_search',
205
+ "attention_rescoring"):
206
+ batch_log_probs_seq_list = beam_log_probs.tolist()
207
+ batch_log_probs_idx_list = beam_log_probs_idx.tolist()
208
+ batch_len_list = encoder_out_lens.tolist()
209
+ batch_log_probs_seq = []
210
+ batch_log_probs_ids = []
211
+ batch_start = [] # only effective in streaming deployment
212
+ batch_root = TrieVector()
213
+ root_dict = {}
214
+ for i in range(len(batch_len_list)):
215
+ num_sent = batch_len_list[i]
216
+ batch_log_probs_seq.append(
217
+ batch_log_probs_seq_list[i][0:num_sent])
218
+ batch_log_probs_ids.append(
219
+ batch_log_probs_idx_list[i][0:num_sent])
220
+ root_dict[i] = PathTrie()
221
+ batch_root.append(root_dict[i])
222
+ batch_start.append(True)
223
+ score_hyps = ctc_beam_search_decoder_batch(
224
+ batch_log_probs_seq, batch_log_probs_ids, batch_root,
225
+ batch_start, beam_size, num_processes, 0, -2, 0.99999)
226
+ if args.mode == 'ctc_prefix_beam_search':
227
+ hyps = []
228
+ for cand_hyps in score_hyps:
229
+ hyps.append(cand_hyps[0][1])
230
+ hyps = map_batch(hyps, vocabulary, num_processes, False, 0)
231
+ if args.mode == 'attention_rescoring':
232
+ ctc_score, all_hyps = [], []
233
+ max_len = 0
234
+ for hyps in score_hyps:
235
+ cur_len = len(hyps)
236
+ if len(hyps) < beam_size:
237
+ hyps += (beam_size - cur_len) * [(-float("INF"),
238
+ (0, ))]
239
+ cur_ctc_score = []
240
+ for hyp in hyps:
241
+ cur_ctc_score.append(hyp[0])
242
+ all_hyps.append(list(hyp[1]))
243
+ if len(hyp[1]) > max_len:
244
+ max_len = len(hyp[1])
245
+ ctc_score.append(cur_ctc_score)
246
+ if args.fp16:
247
+ ctc_score = np.array(ctc_score, dtype=np.float16)
248
+ else:
249
+ ctc_score = np.array(ctc_score, dtype=np.float32)
250
+ hyps_pad_sos_eos = np.ones(
251
+ (batch_size, beam_size, max_len + 2),
252
+ dtype=np.int64) * IGNORE_ID
253
+ r_hyps_pad_sos_eos = np.ones(
254
+ (batch_size, beam_size, max_len + 2),
255
+ dtype=np.int64) * IGNORE_ID
256
+ hyps_lens_sos = np.ones((batch_size, beam_size),
257
+ dtype=np.int32)
258
+ k = 0
259
+ for i in range(batch_size):
260
+ for j in range(beam_size):
261
+ cand = all_hyps[k]
262
+ l = len(cand) + 2
263
+ hyps_pad_sos_eos[i][j][0:l] = [sos] + cand + [eos]
264
+ r_hyps_pad_sos_eos[i][j][0:l] = [sos] + cand[::-1] + [
265
+ eos
266
+ ]
267
+ hyps_lens_sos[i][j] = len(cand) + 1
268
+ k += 1
269
+ decoder_ort_inputs = {
270
+ decoder_ort_session.get_inputs()[0].name: encoder_out,
271
+ decoder_ort_session.get_inputs()[1].name: encoder_out_lens,
272
+ decoder_ort_session.get_inputs()[2].name: hyps_pad_sos_eos,
273
+ decoder_ort_session.get_inputs()[3].name: hyps_lens_sos,
274
+ decoder_ort_session.get_inputs()[-1].name: ctc_score
275
+ }
276
+ if reverse_weight > 0:
277
+ r_hyps_pad_sos_eos_name = decoder_ort_session.get_inputs(
278
+ )[4].name
279
+ decoder_ort_inputs[
280
+ r_hyps_pad_sos_eos_name] = r_hyps_pad_sos_eos
281
+ best_index = decoder_ort_session.run(None,
282
+ decoder_ort_inputs)[0]
283
+ best_sents = []
284
+ k = 0
285
+ for idx in best_index:
286
+ cur_best_sent = all_hyps[k:k + beam_size][idx]
287
+ best_sents.append(cur_best_sent)
288
+ k += beam_size
289
+ hyps = map_batch(best_sents, vocabulary, num_processes)
290
+
291
+ for i, key in enumerate(keys):
292
+ content = hyps[i]
293
+ logging.info('{} {}'.format(key, content))
294
+ fout.write('{} {}\n'.format(key, content))
295
+
296
+ if __name__ == '__main__':
297
+ main()
wenet/bin/train.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 Mobvoi Inc. (authors: Binbin Zhang)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import print_function
16
+
17
+ import argparse
18
+ import datetime
19
+ import logging
20
+ import os
21
+ import torch
22
+ import yaml
23
+
24
+ import torch.distributed as dist
25
+
26
+ from torch.distributed.elastic.multiprocessing.errors import record
27
+ from wenet.utils.common import lrs_to_str, TORCH_NPU_AVAILABLE # noqa just ensure to check torch-npu
28
+
29
+ from wenet.utils.executor import Executor
30
+ from wenet.utils.config import override_config
31
+ from wenet.utils.init_model import init_model
32
+ from wenet.utils.init_tokenizer import init_tokenizer
33
+ from wenet.utils.train_utils import (
34
+ add_fsdp_args, add_model_args, add_dataset_args, add_ddp_args,
35
+ add_deepspeed_args, add_trace_args, init_distributed,
36
+ init_dataset_and_dataloader, check_modify_and_save_config,
37
+ init_optimizer_and_scheduler, init_scaler, trace_and_print_model,
38
+ wrap_cuda_model, init_summarywriter, save_model, log_per_epoch,
39
+ add_lora_args, reinit_lora)
40
+
41
+
42
+ def get_args():
43
+ parser = argparse.ArgumentParser(description='training your network')
44
+ parser.add_argument('--train_engine',
45
+ default='torch_ddp',
46
+ choices=['torch_ddp', 'torch_fsdp', 'deepspeed'],
47
+ help='Engine for paralleled training')
48
+ # set default value of device to "cuda", avoiding the modify of original scripts
49
+ parser.add_argument('--device',
50
+ type=str,
51
+ default='cuda',
52
+ choices=["cpu", "npu", "cuda"],
53
+ help='accelerator for training')
54
+ parser = add_model_args(parser)
55
+ parser = add_dataset_args(parser)
56
+ parser = add_ddp_args(parser)
57
+ parser = add_lora_args(parser)
58
+ parser = add_deepspeed_args(parser)
59
+ parser = add_fsdp_args(parser)
60
+ parser = add_trace_args(parser)
61
+ args = parser.parse_args()
62
+ if args.train_engine == "deepspeed":
63
+ args.deepspeed = True
64
+ assert args.deepspeed_config is not None
65
+ return args
66
+
67
+
68
+ # NOTE(xcsong): On worker errors, this recod tool will summarize the
69
+ # details of the error (e.g. time, rank, host, pid, traceback, etc).
70
+ @record
71
+ def main():
72
+ args = get_args()
73
+ logging.basicConfig(level=logging.DEBUG,
74
+ format='%(asctime)s %(levelname)s %(message)s')
75
+
76
+ # Set random seed
77
+ torch.manual_seed(777)
78
+
79
+ # Read config
80
+ with open(args.config, 'r') as fin:
81
+ configs = yaml.load(fin, Loader=yaml.FullLoader)
82
+ if len(args.override_config) > 0:
83
+ configs = override_config(configs, args.override_config)
84
+
85
+ # init tokenizer
86
+ tokenizer = init_tokenizer(configs)
87
+
88
+ # Init env for ddp OR deepspeed
89
+ _, _, rank = init_distributed(args)
90
+
91
+ # Get dataset & dataloader
92
+ train_dataset, cv_dataset, train_data_loader, cv_data_loader = \
93
+ init_dataset_and_dataloader(args, configs, tokenizer)
94
+
95
+ # Do some sanity checks and save config to arsg.model_dir
96
+ configs = check_modify_and_save_config(args, configs,
97
+ tokenizer.symbol_table)
98
+
99
+ # Init asr model from configs
100
+ model, configs = init_model(args, configs)
101
+
102
+ if hasattr(args, 'lora_reinit') and args.lora_reinit:
103
+ reinit_lora(model, args, configs, tokenizer)
104
+
105
+ # Check model is jitable & print model archtectures
106
+ trace_and_print_model(args, model)
107
+
108
+ # Tensorboard summary
109
+ writer = init_summarywriter(args)
110
+
111
+ # Dispatch model from cpu to gpu
112
+ model, device = wrap_cuda_model(args, model, configs)
113
+
114
+ # Get optimizer & scheduler
115
+ model, optimizer, scheduler = init_optimizer_and_scheduler(
116
+ args, configs, model)
117
+
118
+ # Save checkpoints
119
+ save_model(model,
120
+ info_dict={
121
+ "save_time":
122
+ datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S'),
123
+ "tag":
124
+ "init",
125
+ **configs
126
+ })
127
+
128
+ # Get executor
129
+ tag = configs["init_infos"].get("tag", "init")
130
+ executor = Executor(global_step=configs["init_infos"].get('step', -1),
131
+ device=device)
132
+
133
+ # Init scaler, used for pytorch amp mixed precision training
134
+ scaler = init_scaler(args)
135
+
136
+ # Start training loop
137
+ start_epoch = configs["init_infos"].get('epoch', 0) + int("epoch_" in tag)
138
+ # if save_interval in configs, steps mode else epoch mode
139
+ end_epoch = configs.get('max_epoch',
140
+ 100) if "save_interval" not in configs else 1
141
+ assert start_epoch <= end_epoch
142
+ configs.pop("init_infos", None)
143
+ final_epoch = None
144
+ for epoch in range(start_epoch, end_epoch):
145
+ configs['epoch'] = epoch
146
+
147
+ lrs = [group['lr'] for group in optimizer.param_groups]
148
+ logging.info('Epoch {} Step {} TRAIN info lr {} rank {}'.format(
149
+ epoch, executor.step, lrs_to_str(lrs), rank))
150
+
151
+ dist.barrier(
152
+ ) # NOTE(xcsong): Ensure all ranks start Train at the same time.
153
+ # NOTE(xcsong): Why we need a new group? see `train_utils.py::wenet_join`
154
+ group_join = dist.new_group(
155
+ backend="gloo", timeout=datetime.timedelta(seconds=args.timeout))
156
+ executor.train(model, optimizer, scheduler, train_data_loader,
157
+ cv_data_loader, writer, configs, scaler, group_join)
158
+ dist.destroy_process_group(group_join)
159
+
160
+ dist.barrier(
161
+ ) # NOTE(xcsong): Ensure all ranks start CV at the same time.
162
+ loss_dict = executor.cv(model, cv_data_loader, configs)
163
+ info_dict = {
164
+ 'epoch': epoch,
165
+ 'lrs': [group['lr'] for group in optimizer.param_groups],
166
+ 'step': executor.step,
167
+ 'save_time': datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S'),
168
+ 'tag': "epoch_{}".format(epoch),
169
+ 'loss_dict': loss_dict,
170
+ **configs
171
+ }
172
+ # epoch cv: tensorboard && log
173
+ log_per_epoch(writer, info_dict=info_dict)
174
+ save_model(model, info_dict=info_dict)
175
+
176
+ final_epoch = epoch
177
+
178
+ if final_epoch is not None and rank == 0:
179
+ final_model_path = os.path.join(args.model_dir, 'final.pt')
180
+ os.remove(final_model_path) if os.path.exists(
181
+ final_model_path) else None
182
+ os.symlink('{}.pt'.format(final_epoch), final_model_path)
183
+ writer.close()
184
+ dist.barrier(
185
+ ) # NOTE(yktian): Ensure all ranks end Train before destroy process group.
186
+ dist.destroy_process_group()
187
+
188
+
189
+ if __name__ == '__main__':
190
+ main()
wenet/branchformer/__init__.py ADDED
File without changes
wenet/branchformer/cgmlp.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 Yifan Peng (Carnegie Mellon University)
2
+ # 2023 Voicecomm Inc (Kai Li)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # Modified from ESPnet(https://github.com/espnet/espnet)
16
+ """MLP with convolutional gating (cgMLP) definition.
17
+
18
+ References:
19
+ https://openreview.net/forum?id=RA-zVvZLYIy
20
+ https://arxiv.org/abs/2105.08050
21
+
22
+ """
23
+
24
+ from typing import Tuple
25
+ import torch
26
+ import torch.nn as nn
27
+ from wenet.utils.class_utils import WENET_ACTIVATION_CLASSES
28
+
29
+
30
+ class ConvolutionalSpatialGatingUnit(torch.nn.Module):
31
+ """Convolutional Spatial Gating Unit (CSGU)."""
32
+
33
+ def __init__(
34
+ self,
35
+ size: int,
36
+ kernel_size: int,
37
+ dropout_rate: float,
38
+ use_linear_after_conv: bool,
39
+ gate_activation: str,
40
+ causal: bool = True,
41
+ ):
42
+ super().__init__()
43
+
44
+ # split input channels
45
+ n_channels = size // 2
46
+ self.norm = nn.LayerNorm(n_channels)
47
+ # self.lorder is used to distinguish if it's a causal convolution,
48
+ # if self.lorder > 0: it's a causal convolution, the input will be
49
+ # padded with self.lorder frames on the left in forward.
50
+ # else: it's a symmetrical convolution
51
+ if causal:
52
+ padding = 0
53
+ self.lorder = kernel_size - 1
54
+ else:
55
+ # kernel_size should be an odd number for none causal convolution
56
+ assert (kernel_size - 1) % 2 == 0
57
+ padding = (kernel_size - 1) // 2
58
+ self.lorder = 0
59
+ self.conv = torch.nn.Conv1d(
60
+ n_channels,
61
+ n_channels,
62
+ kernel_size,
63
+ 1,
64
+ padding,
65
+ groups=n_channels,
66
+ )
67
+ if use_linear_after_conv:
68
+ self.linear = torch.nn.Linear(n_channels, n_channels)
69
+ else:
70
+ self.linear = None
71
+
72
+ if gate_activation == "identity":
73
+ self.act = torch.nn.Identity()
74
+ else:
75
+ self.act = WENET_ACTIVATION_CLASSES[gate_activation]()
76
+
77
+ self.dropout = torch.nn.Dropout(dropout_rate)
78
+
79
+ def espnet_initialization_fn(self):
80
+ torch.nn.init.normal_(self.conv.weight, std=1e-6)
81
+ torch.nn.init.ones_(self.conv.bias)
82
+ if self.linear is not None:
83
+ torch.nn.init.normal_(self.linear.weight, std=1e-6)
84
+ torch.nn.init.ones_(self.linear.bias)
85
+
86
+ def forward(
87
+ self, x: torch.Tensor, cache: torch.Tensor = torch.zeros((0, 0, 0))
88
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
89
+ """Forward method
90
+
91
+ Args:
92
+ x (torch.Tensor): (batch, time, channels)
93
+ cache (torch.Tensor): left context cache, it is only
94
+ used in causal convolution (#batch, channels, cache_t),
95
+ (0, 0, 0) meas fake cache.
96
+
97
+ Returns:
98
+ out (torch.Tensor): (batch, time, channels/2)
99
+ """
100
+
101
+ x_r, x_g = x.chunk(2, dim=-1)
102
+ # exchange the temporal dimension and the feature dimension
103
+ x_g = x_g.transpose(1, 2) # (#batch, channels, time)
104
+
105
+ if self.lorder > 0:
106
+ if cache.size(2) == 0: # cache_t == 0
107
+ x_g = nn.functional.pad(x_g, (self.lorder, 0), 'constant', 0.0)
108
+ else:
109
+ assert cache.size(0) == x_g.size(0) # equal batch
110
+ assert cache.size(1) == x_g.size(1) # equal channel
111
+ x_g = torch.cat((cache, x_g), dim=2)
112
+ assert (x_g.size(2) > self.lorder)
113
+ new_cache = x_g[:, :, -self.lorder:]
114
+ else:
115
+ # It's better we just return None if no cache is required,
116
+ # However, for JIT export, here we just fake one tensor instead of
117
+ # None.
118
+ new_cache = torch.zeros((0, 0, 0),
119
+ dtype=x_g.dtype,
120
+ device=x_g.device)
121
+
122
+ x_g = x_g.transpose(1, 2)
123
+ x_g = self.norm(x_g) # (N, T, D/2)
124
+ x_g = self.conv(x_g.transpose(1, 2)).transpose(1, 2) # (N, T, D/2)
125
+ if self.linear is not None:
126
+ x_g = self.linear(x_g)
127
+
128
+ x_g = self.act(x_g)
129
+ out = x_r * x_g # (N, T, D/2)
130
+ out = self.dropout(out)
131
+ return out, new_cache
132
+
133
+
134
+ class ConvolutionalGatingMLP(torch.nn.Module):
135
+ """Convolutional Gating MLP (cgMLP)."""
136
+
137
+ def __init__(
138
+ self,
139
+ size: int,
140
+ linear_units: int,
141
+ kernel_size: int,
142
+ dropout_rate: float,
143
+ use_linear_after_conv: bool,
144
+ gate_activation: str,
145
+ causal: bool = True,
146
+ ):
147
+ super().__init__()
148
+
149
+ self.channel_proj1 = torch.nn.Sequential(
150
+ torch.nn.Linear(size, linear_units), torch.nn.GELU())
151
+ self.csgu = ConvolutionalSpatialGatingUnit(
152
+ size=linear_units,
153
+ kernel_size=kernel_size,
154
+ dropout_rate=dropout_rate,
155
+ use_linear_after_conv=use_linear_after_conv,
156
+ gate_activation=gate_activation,
157
+ causal=causal,
158
+ )
159
+ self.channel_proj2 = torch.nn.Linear(linear_units // 2, size)
160
+
161
+ def forward(
162
+ self,
163
+ x: torch.Tensor,
164
+ mask: torch.Tensor,
165
+ cache: torch.Tensor = torch.zeros((0, 0, 0))
166
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
167
+ """Forward method
168
+
169
+ Args:
170
+ x (torch.Tensor): (batch, time, channels)
171
+ mask_pad (torch.Tensor): used for batch padding (#batch, 1, time),
172
+ (0, 0, 0) means fake mask. Not used yet
173
+ cache (torch.Tensor): left context cache, it is only
174
+ used in causal convolution (#batch, channels, cache_t),
175
+ (0, 0, 0) meas fake cache.
176
+
177
+ Returns:
178
+ out (torch.Tensor): (batch, time, channels/2)
179
+ """
180
+
181
+ xs_pad = x
182
+
183
+ # size -> linear_units
184
+ xs_pad = self.channel_proj1(xs_pad)
185
+
186
+ # linear_units -> linear_units/2
187
+ xs_pad, new_cnn_cache = self.csgu(xs_pad, cache)
188
+
189
+ # linear_units/2 -> size
190
+ xs_pad = self.channel_proj2(xs_pad)
191
+
192
+ out = xs_pad
193
+
194
+ return out, new_cnn_cache
wenet/branchformer/encoder.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 Yifan Peng (Carnegie Mellon University)
2
+ # 2023 Voicecomm Inc (Kai Li)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # Modified from ESPnet(https://github.com/espnet/espnet)
16
+ """Encoder definition."""
17
+
18
+ import torch
19
+
20
+ from typing import List, Optional, Union
21
+
22
+ from wenet.branchformer.encoder_layer import BranchformerEncoderLayer
23
+ from wenet.branchformer.cgmlp import ConvolutionalGatingMLP
24
+ from wenet.transformer.encoder import BaseEncoder
25
+ from wenet.utils.class_utils import (
26
+ WENET_ATTENTION_CLASSES, )
27
+
28
+
29
+ class BranchformerEncoder(BaseEncoder):
30
+ """Branchformer encoder module."""
31
+
32
+ def __init__(
33
+ self,
34
+ input_size: int,
35
+ output_size: int = 256,
36
+ use_attn: bool = True,
37
+ attention_heads: int = 4,
38
+ selfattention_layer_type: str = "rel_selfattn",
39
+ pos_enc_layer_type: str = "rel_pos",
40
+ use_cgmlp: bool = True,
41
+ cgmlp_linear_units: int = 2048,
42
+ cgmlp_conv_kernel: int = 31,
43
+ use_linear_after_conv: bool = False,
44
+ gate_activation: str = "identity",
45
+ merge_method: str = "concat",
46
+ cgmlp_weight: Union[float, List[float]] = 0.5,
47
+ attn_branch_drop_rate: Union[float, List[float]] = 0.0,
48
+ num_blocks: int = 12,
49
+ dropout_rate: float = 0.1,
50
+ positional_dropout_rate: float = 0.1,
51
+ attention_dropout_rate: float = 0.0,
52
+ input_layer: str = "conv2d",
53
+ stochastic_depth_rate: Union[float, List[float]] = 0.0,
54
+ static_chunk_size: int = 0,
55
+ use_dynamic_chunk: bool = False,
56
+ global_cmvn: torch.nn.Module = None,
57
+ use_dynamic_left_chunk: bool = False,
58
+ causal: bool = False,
59
+ query_bias: bool = True,
60
+ key_bias: bool = True,
61
+ value_bias: bool = True,
62
+ gradient_checkpointing: bool = False,
63
+ use_sdpa: bool = False,
64
+ layer_norm_type: str = 'layer_norm',
65
+ norm_eps: float = 1e-5,
66
+ n_kv_head: Optional[int] = None,
67
+ head_dim: Optional[int] = None,
68
+ ):
69
+ super().__init__(input_size, output_size, attention_heads,
70
+ cgmlp_linear_units, num_blocks, dropout_rate,
71
+ positional_dropout_rate, attention_dropout_rate,
72
+ input_layer, pos_enc_layer_type, True,
73
+ static_chunk_size, use_dynamic_chunk, global_cmvn,
74
+ use_dynamic_left_chunk, gradient_checkpointing,
75
+ use_sdpa, layer_norm_type, norm_eps)
76
+
77
+ encoder_selfattn_layer_args = (
78
+ attention_heads,
79
+ output_size,
80
+ attention_dropout_rate,
81
+ query_bias,
82
+ key_bias,
83
+ value_bias,
84
+ use_sdpa,
85
+ n_kv_head,
86
+ head_dim,
87
+ )
88
+
89
+ cgmlp_layer = ConvolutionalGatingMLP
90
+ cgmlp_layer_args = (
91
+ output_size,
92
+ cgmlp_linear_units,
93
+ cgmlp_conv_kernel,
94
+ dropout_rate,
95
+ use_linear_after_conv,
96
+ gate_activation,
97
+ causal,
98
+ )
99
+
100
+ if isinstance(stochastic_depth_rate, float):
101
+ stochastic_depth_rate = [stochastic_depth_rate] * num_blocks
102
+ if len(stochastic_depth_rate) != num_blocks:
103
+ raise ValueError(
104
+ f"Length of stochastic_depth_rate ({len(stochastic_depth_rate)}) "
105
+ f"should be equal to num_blocks ({num_blocks})")
106
+
107
+ if isinstance(cgmlp_weight, float):
108
+ cgmlp_weight = [cgmlp_weight] * num_blocks
109
+ if len(cgmlp_weight) != num_blocks:
110
+ raise ValueError(
111
+ f"Length of cgmlp_weight ({len(cgmlp_weight)}) should be equal to "
112
+ f"num_blocks ({num_blocks})")
113
+
114
+ if isinstance(attn_branch_drop_rate, float):
115
+ attn_branch_drop_rate = [attn_branch_drop_rate] * num_blocks
116
+ if len(attn_branch_drop_rate) != num_blocks:
117
+ raise ValueError(
118
+ f"Length of attn_branch_drop_rate ({len(attn_branch_drop_rate)}) "
119
+ f"should be equal to num_blocks ({num_blocks})")
120
+
121
+ self.encoders = LayerDropModuleList(
122
+ p=stochastic_depth_rate,
123
+ modules=[
124
+ BranchformerEncoderLayer(
125
+ output_size,
126
+ WENET_ATTENTION_CLASSES[selfattention_layer_type](
127
+ *encoder_selfattn_layer_args) if use_attn else None,
128
+ cgmlp_layer(*cgmlp_layer_args) if use_cgmlp else None,
129
+ dropout_rate,
130
+ merge_method,
131
+ cgmlp_weight[lnum],
132
+ attn_branch_drop_rate[lnum],
133
+ stochastic_depth_rate[lnum],
134
+ ) for lnum in range(num_blocks)
135
+ ])
136
+
137
+
138
+ # modify from : https://github.com/facebookresearch/fairseq/blob/main/fairseq/modules/layer_drop.py # noqa
139
+ class LayerDropModuleList(torch.nn.ModuleList):
140
+ """
141
+ A LayerDrop implementation based on :class:`torch.nn.ModuleList`.
142
+
143
+ We refresh the choice of which layers to drop every time we iterate
144
+ over the LayerDropModuleList instance. During evaluation we always
145
+ iterate over all layers.
146
+
147
+ Usage::
148
+
149
+ layers = LayerDropList(p=0.5, modules=[layer1, layer2, layer3])
150
+ for layer in layers: # this might iterate over layers 1 and 3
151
+ x = layer(x)
152
+ for layer in layers: # this might iterate over all layers
153
+ x = layer(x)
154
+ for layer in layers: # this might not iterate over any layers
155
+ x = layer(x)
156
+
157
+ Args:
158
+ p (float): probability of dropping out each layer
159
+ modules (iterable, optional): an iterable of modules to add
160
+
161
+ Limitations:
162
+ 1 can work with ddp when layer's gradient checkpoint disabled
163
+ 2 can't work with ddp when layer's gradient checkpoint enables
164
+ 3 can work with fsdp
165
+ 4 can work with deepspeed
166
+ """
167
+
168
+ def __init__(self, p: List[float], modules=None):
169
+ super().__init__(modules)
170
+ assert len(p) == len(self)
171
+ self.p = p
172
+
173
+ def __iter__(self):
174
+ dropout_probs = torch.empty(len(self)).uniform_()
175
+ for i, m in enumerate(super().__iter__()):
176
+ if not self.training or (dropout_probs[i] > self.p[i]):
177
+ yield m
wenet/branchformer/encoder_layer.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 Yifan Peng (Carnegie Mellon University)
2
+ # 2023 Voicecomm Inc (Kai Li)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # Modified from ESPnet(https://github.com/espnet/espnet)
16
+ """BranchformerEncoderLayer definition."""
17
+
18
+ import torch
19
+ import torch.nn as nn
20
+ from typing import Optional, Tuple
21
+
22
+ from wenet.transformer.attention import T_CACHE
23
+
24
+
25
+ class BranchformerEncoderLayer(torch.nn.Module):
26
+ """Branchformer encoder layer module.
27
+
28
+ Args:
29
+ size (int): model dimension
30
+ attn: standard self-attention or efficient attention, optional
31
+ cgmlp: ConvolutionalGatingMLP, optional
32
+ dropout_rate (float): dropout probability
33
+ merge_method (str): concat, learned_ave, fixed_ave
34
+ cgmlp_weight (float): weight of the cgmlp branch, between 0 and 1,
35
+ used if merge_method is fixed_ave
36
+ attn_branch_drop_rate (float): probability of dropping the attn branch,
37
+ used if merge_method is learned_ave
38
+ stochastic_depth_rate (float): stochastic depth probability
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ size: int,
44
+ attn: Optional[torch.nn.Module],
45
+ cgmlp: Optional[torch.nn.Module],
46
+ dropout_rate: float,
47
+ merge_method: str,
48
+ cgmlp_weight: float = 0.5,
49
+ attn_branch_drop_rate: float = 0.0,
50
+ stochastic_depth_rate: float = 0.0,
51
+ ):
52
+ super().__init__()
53
+ assert (attn is not None) or (
54
+ cgmlp is not None), "At least one branch should be valid"
55
+
56
+ self.size = size
57
+ self.attn = attn
58
+ self.cgmlp = cgmlp
59
+ self.merge_method = merge_method
60
+ self.cgmlp_weight = cgmlp_weight
61
+ self.attn_branch_drop_rate = attn_branch_drop_rate
62
+ self.stochastic_depth_rate = stochastic_depth_rate
63
+ self.use_two_branches = (attn is not None) and (cgmlp is not None)
64
+
65
+ if attn is not None:
66
+ self.norm_mha = nn.LayerNorm(size) # for the MHA module
67
+ if cgmlp is not None:
68
+ self.norm_mlp = nn.LayerNorm(size) # for the MLP module
69
+ self.norm_final = nn.LayerNorm(
70
+ size) # for the final output of the block
71
+
72
+ self.dropout = torch.nn.Dropout(dropout_rate)
73
+
74
+ # # attention-based pooling for two branches
75
+ self.pooling_proj1 = torch.nn.Linear(size, 1)
76
+ self.pooling_proj2 = torch.nn.Linear(size, 1)
77
+
78
+ # # linear projections for calculating merging weights
79
+ self.weight_proj1 = torch.nn.Linear(size, 1)
80
+ self.weight_proj2 = torch.nn.Linear(size, 1)
81
+
82
+ if self.use_two_branches:
83
+ if self.merge_method == "concat":
84
+ self.merge_proj = torch.nn.Linear(size + size, size)
85
+
86
+ elif self.merge_method == "learned_ave":
87
+ # linear projection after weighted average
88
+ self.merge_proj = torch.nn.Linear(size, size)
89
+
90
+ elif self.merge_method == "fixed_ave":
91
+ assert (0.0 <= cgmlp_weight <=
92
+ 1.0), "cgmlp weight should be between 0.0 and 1.0"
93
+
94
+ # remove the other branch if only one branch is used
95
+ if cgmlp_weight == 0.0:
96
+ self.use_two_branches = False
97
+ self.cgmlp = None
98
+ self.norm_mlp = None
99
+ elif cgmlp_weight == 1.0:
100
+ self.use_two_branches = False
101
+ self.attn = None
102
+ self.norm_mha = None
103
+
104
+ # linear projection after weighted average
105
+ self.merge_proj = torch.nn.Linear(size, size)
106
+ else:
107
+ raise ValueError(f"unknown merge method: {merge_method}")
108
+ else:
109
+ self.merge_proj = torch.nn.Identity()
110
+
111
+ def _forward(
112
+ self,
113
+ x: torch.Tensor,
114
+ mask: torch.Tensor,
115
+ pos_emb: torch.Tensor,
116
+ mask_pad: torch.Tensor = torch.ones((0, 0, 0), dtype=torch.bool),
117
+ att_cache: T_CACHE = (torch.zeros(
118
+ (0, 0, 0, 0)), torch.zeros(0, 0, 0, 0)),
119
+ cnn_cache: torch.Tensor = torch.zeros((0, 0, 0, 0)),
120
+ stoch_layer_coeff: float = 1.0
121
+ ) -> Tuple[torch.Tensor, torch.Tensor, T_CACHE, torch.Tensor]:
122
+ # Two branches
123
+ x1 = x
124
+ x2 = x
125
+
126
+ # Branch 1: multi-headed attention module
127
+ if self.attn is not None:
128
+ x1 = self.norm_mha(x1)
129
+ x_att, new_att_cache = self.attn(x1, x1, x1, mask, pos_emb,
130
+ att_cache)
131
+ x1 = self.dropout(x_att)
132
+
133
+ # Branch 2: convolutional gating mlp
134
+ # Fake new cnn cache here, and then change it in conv_module
135
+ new_cnn_cache = torch.zeros((0, 0, 0), dtype=x.dtype, device=x.device)
136
+ if self.cgmlp is not None:
137
+ x2 = self.norm_mlp(x2)
138
+ x2, new_cnn_cache = self.cgmlp(x2, mask_pad, cnn_cache)
139
+ x2 = self.dropout(x2)
140
+
141
+ # Merge two branches
142
+ if self.use_two_branches:
143
+ if self.merge_method == "concat":
144
+ x = x + stoch_layer_coeff * self.dropout(
145
+ self.merge_proj(torch.cat([x1, x2], dim=-1)))
146
+ elif self.merge_method == "learned_ave":
147
+ if (self.training and self.attn_branch_drop_rate > 0
148
+ and torch.rand(1).item() < self.attn_branch_drop_rate):
149
+ # Drop the attn branch
150
+ w1, w2 = torch.tensor(0.0), torch.tensor(1.0)
151
+ else:
152
+ # branch1
153
+ score1 = (self.pooling_proj1(x1).transpose(1, 2) /
154
+ self.size**0.5)
155
+ score1 = score1.masked_fill(mask_pad.eq(0), -float('inf'))
156
+ score1 = torch.softmax(score1, dim=-1).masked_fill(
157
+ mask_pad.eq(0), 0.0)
158
+
159
+ pooled1 = torch.matmul(score1,
160
+ x1).squeeze(1) # (batch, size)
161
+ weight1 = self.weight_proj1(pooled1) # (batch, 1)
162
+
163
+ # branch2
164
+ score2 = (self.pooling_proj2(x2).transpose(1, 2) /
165
+ self.size**0.5)
166
+ score2 = score2.masked_fill(mask_pad.eq(0), -float('inf'))
167
+ score2 = torch.softmax(score2, dim=-1).masked_fill(
168
+ mask_pad.eq(0), 0.0)
169
+
170
+ pooled2 = torch.matmul(score2,
171
+ x2).squeeze(1) # (batch, size)
172
+ weight2 = self.weight_proj2(pooled2) # (batch, 1)
173
+
174
+ # normalize weights of two branches
175
+ merge_weights = torch.softmax(torch.cat([weight1, weight2],
176
+ dim=-1),
177
+ dim=-1) # (batch, 2)
178
+ merge_weights = merge_weights.unsqueeze(-1).unsqueeze(
179
+ -1) # (batch, 2, 1, 1)
180
+ w1, w2 = merge_weights[:,
181
+ 0], merge_weights[:,
182
+ 1] # (batch, 1, 1)
183
+
184
+ x = x + stoch_layer_coeff * self.dropout(
185
+ self.merge_proj(w1 * x1 + w2 * x2))
186
+ elif self.merge_method == "fixed_ave":
187
+ x = x + stoch_layer_coeff * self.dropout(
188
+ self.merge_proj((1.0 - self.cgmlp_weight) * x1 +
189
+ self.cgmlp_weight * x2))
190
+ else:
191
+ raise RuntimeError(
192
+ f"unknown merge method: {self.merge_method}")
193
+ else:
194
+ if self.attn is None:
195
+ x = x + stoch_layer_coeff * self.dropout(self.merge_proj(x2))
196
+ elif self.cgmlp is None:
197
+ x = x + stoch_layer_coeff * self.dropout(self.merge_proj(x1))
198
+ else:
199
+ # This should not happen
200
+ raise RuntimeError(
201
+ "Both branches are not None, which is unexpected.")
202
+
203
+ x = self.norm_final(x)
204
+
205
+ return x, mask, new_att_cache, new_cnn_cache
206
+
207
+ def forward(
208
+ self,
209
+ x: torch.Tensor,
210
+ mask: torch.Tensor,
211
+ pos_emb: torch.Tensor,
212
+ mask_pad: torch.Tensor = torch.ones((0, 0, 0), dtype=torch.bool),
213
+ att_cache: T_CACHE = (torch.zeros(
214
+ (0, 0, 0, 0)), torch.zeros(0, 0, 0, 0)),
215
+ cnn_cache: torch.Tensor = torch.zeros((0, 0, 0, 0)),
216
+ ) -> Tuple[torch.Tensor, torch.Tensor, T_CACHE, torch.Tensor]:
217
+ """Compute encoded features.
218
+
219
+ Args:
220
+ x (Union[Tuple, torch.Tensor]): Input tensor (#batch, time, size).
221
+ mask (torch.Tensor): Mask tensor for the input (#batch, time, time).
222
+ pos_emb (torch.Tensor): positional encoding, must not be None
223
+ for BranchformerEncoderLayer.
224
+ mask_pad (torch.Tensor): batch padding mask used for conv module.
225
+ (#batch, 1,time), (0, 0, 0) means fake mask.
226
+ att_cache (torch.Tensor): Cache tensor of the KEY & VALUE
227
+ (#batch=1, head, cache_t1, d_k * 2), head * d_k == size.
228
+ cnn_cache (torch.Tensor): Convolution cache in cgmlp layer
229
+ (#batch=1, size, cache_t2)
230
+
231
+ Returns:
232
+ torch.Tensor: Output tensor (#batch, time, size).
233
+ torch.Tensor: Mask tensor (#batch, time, time.
234
+ torch.Tensor: att_cache tensor,
235
+ (#batch=1, head, cache_t1 + time, d_k * 2).
236
+ torch.Tensor: cnn_cahce tensor (#batch, size, cache_t2).
237
+ """
238
+
239
+ stoch_layer_coeff = 1.0
240
+ # with stochastic depth, residual connection `x + f(x)` becomes
241
+ # `x <- x + 1 / (1 - p) * f(x)` at training time.
242
+ if self.training:
243
+ stoch_layer_coeff = 1.0 / (1 - self.stochastic_depth_rate)
244
+ return self._forward(x, mask, pos_emb, mask_pad, att_cache, cnn_cache,
245
+ stoch_layer_coeff)
wenet/cli/__init__.py ADDED
File without changes
wenet/cli/hub.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 Mddct(hamddct@gmail.com)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import sys
17
+ import tarfile
18
+ from pathlib import Path
19
+ from urllib.request import urlretrieve
20
+
21
+ import requests
22
+ import tqdm
23
+
24
+
25
+ def download(url: str, dest: str, only_child=True):
26
+ """ download from url to dest
27
+ """
28
+ assert os.path.exists(dest)
29
+ print('Downloading {} to {}'.format(url, dest))
30
+
31
+ def progress_hook(t):
32
+ last_b = [0]
33
+
34
+ def update_to(b=1, bsize=1, tsize=None):
35
+ if tsize not in (None, -1):
36
+ t.total = tsize
37
+ displayed = t.update((b - last_b[0]) * bsize)
38
+ last_b[0] = b
39
+ return displayed
40
+
41
+ return update_to
42
+
43
+ # *.tar.gz
44
+ name = url.split('?')[0].split('/')[-1]
45
+ tar_path = os.path.join(dest, name)
46
+ with tqdm.tqdm(unit='B',
47
+ unit_scale=True,
48
+ unit_divisor=1024,
49
+ miniters=1,
50
+ desc=(name)) as t:
51
+ urlretrieve(url,
52
+ filename=tar_path,
53
+ reporthook=progress_hook(t),
54
+ data=None)
55
+ t.total = t.n
56
+
57
+ with tarfile.open(tar_path) as f:
58
+ if not only_child:
59
+ f.extractall(dest)
60
+ else:
61
+ for tarinfo in f:
62
+ if "/" not in tarinfo.name:
63
+ continue
64
+ name = os.path.basename(tarinfo.name)
65
+ fileobj = f.extractfile(tarinfo)
66
+ with open(os.path.join(dest, name), "wb") as writer:
67
+ writer.write(fileobj.read())
68
+
69
+
70
+ class Hub(object):
71
+ """Hub for wenet pretrain runtime model
72
+ """
73
+ # TODO(Mddct): make assets class to support other language
74
+ Assets = {
75
+ # wenetspeech
76
+ "chinese": "wenetspeech_u2pp_conformer_libtorch.tar.gz",
77
+ # gigaspeech
78
+ "english": "gigaspeech_u2pp_conformer_libtorch.tar.gz",
79
+ # paraformer
80
+ "paraformer": "paraformer.tar.gz",
81
+ # punc
82
+ "punc": "punc.tar.gz"
83
+ }
84
+
85
+ def __init__(self) -> None:
86
+ pass
87
+
88
+ @staticmethod
89
+ def get_model_by_lang(lang: str) -> str:
90
+ if lang not in Hub.Assets.keys():
91
+ print('ERROR: Unsupported language {} !!!'.format(lang))
92
+ sys.exit(1)
93
+
94
+ # NOTE(Mddct): model_dir structure
95
+ # Path.Home()/.wenet
96
+ # - chs
97
+ # - units.txt
98
+ # - final.zip
99
+ # - en
100
+ # - units.txt
101
+ # - final.zip
102
+ model = Hub.Assets[lang]
103
+ model_dir = os.path.join(Path.home(), ".wenet", lang)
104
+ if not os.path.exists(model_dir):
105
+ os.makedirs(model_dir)
106
+ # TODO(Mddct): model metadata
107
+ if set(["final.zip",
108
+ "units.txt"]).issubset(set(os.listdir(model_dir))):
109
+ return model_dir
110
+ # If not exist, download
111
+ response = requests.get(
112
+ "https://modelscope.cn/api/v1/datasets/wenet/wenet_pretrained_models/oss/tree" # noqa
113
+ )
114
+ model_info = next(data for data in response.json()["Data"]
115
+ if data["Key"] == model)
116
+ model_url = model_info['Url']
117
+ download(model_url, model_dir, only_child=True)
118
+ return model_dir
wenet/cli/model.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2023 Binbin Zhang (binbzha@qq.com)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+
17
+ import torch
18
+ import torchaudio
19
+ import torchaudio.compliance.kaldi as kaldi
20
+ import yaml
21
+
22
+ from wenet.cli.hub import Hub
23
+ from wenet.utils.ctc_utils import (force_align, gen_ctc_peak_time,
24
+ gen_timestamps_from_peak)
25
+ from wenet.utils.file_utils import read_symbol_table
26
+ from wenet.utils.init_model import init_model
27
+ from wenet.transformer.search import (attention_rescoring,
28
+ ctc_prefix_beam_search, DecodeResult)
29
+ from wenet.utils.context_graph import ContextGraph
30
+ from wenet.utils.common import TORCH_NPU_AVAILABLE # noqa just ensure to check torch-npu
31
+
32
+
33
+ class Model:
34
+
35
+ def __init__(self,
36
+ model_dir: str,
37
+ gpu: int = -1,
38
+ beam: int = 5,
39
+ context_path: str = None,
40
+ context_score: float = 6.0,
41
+ resample_rate: int = 16000):
42
+ model_path = os.path.join(model_dir, 'final.zip')
43
+ units_path = os.path.join(model_dir, 'units.txt')
44
+ self.model = torch.jit.load(model_path)
45
+ self.resample_rate = resample_rate
46
+ self.model.eval()
47
+ if gpu >= 0:
48
+ device = 'cuda:{}'.format(gpu)
49
+ else:
50
+ device = 'cpu'
51
+ self.device = torch.device(device)
52
+ self.model.to(device)
53
+ self.symbol_table = read_symbol_table(units_path)
54
+ self.char_dict = {v: k for k, v in self.symbol_table.items()}
55
+ self.beam = beam
56
+ if context_path is not None:
57
+ self.context_graph = ContextGraph(context_path,
58
+ self.symbol_table,
59
+ context_score=context_score)
60
+ else:
61
+ self.context_graph = None
62
+
63
+ def compute_feats(self, audio_file: str) -> torch.Tensor:
64
+ waveform, sample_rate = torchaudio.load(audio_file, normalize=False)
65
+ waveform = waveform.to(torch.float)
66
+ if sample_rate != self.resample_rate:
67
+ waveform = torchaudio.transforms.Resample(
68
+ orig_freq=sample_rate, new_freq=self.resample_rate)(waveform)
69
+ # NOTE (MengqingCao): complex dtype not supported in torch_npu.abs() now,
70
+ # thus, delay placing data on NPU after the calculation of fbank.
71
+ # revert me after complex dtype is supported.
72
+ if "npu" not in self.device.__str__():
73
+ waveform = waveform.to(self.device)
74
+ feats = kaldi.fbank(waveform,
75
+ num_mel_bins=80,
76
+ frame_length=25,
77
+ frame_shift=10,
78
+ energy_floor=0.0,
79
+ sample_frequency=self.resample_rate)
80
+ if "npu" in self.device.__str__():
81
+ feats = feats.to(self.device)
82
+ feats = feats.unsqueeze(0)
83
+ return feats
84
+
85
+ @torch.no_grad()
86
+ def _decode(self,
87
+ audio_file: str,
88
+ tokens_info: bool = False,
89
+ label: str = None) -> dict:
90
+ feats = self.compute_feats(audio_file)
91
+ encoder_out, _, _ = self.model.forward_encoder_chunk(feats, 0, -1)
92
+ encoder_lens = torch.tensor([encoder_out.size(1)],
93
+ dtype=torch.long,
94
+ device=encoder_out.device)
95
+ ctc_probs = self.model.ctc_activation(encoder_out)
96
+ if label is None:
97
+ ctc_prefix_results = ctc_prefix_beam_search(
98
+ ctc_probs,
99
+ encoder_lens,
100
+ self.beam,
101
+ context_graph=self.context_graph)
102
+ else: # force align mode, construct ctc prefix result from alignment
103
+ label_t = self.tokenize(label)
104
+ alignment = force_align(ctc_probs.squeeze(0),
105
+ torch.tensor(label_t, dtype=torch.long))
106
+ peaks = gen_ctc_peak_time(alignment)
107
+ ctc_prefix_results = [
108
+ DecodeResult(tokens=label_t,
109
+ score=0.0,
110
+ times=peaks,
111
+ nbest=[label_t],
112
+ nbest_scores=[0.0],
113
+ nbest_times=[peaks])
114
+ ]
115
+ rescoring_results = attention_rescoring(self.model, ctc_prefix_results,
116
+ encoder_out, encoder_lens, 0.3,
117
+ 0.5)
118
+ res = rescoring_results[0]
119
+ result = {}
120
+ result['text'] = ''.join([self.char_dict[x] for x in res.tokens])
121
+ result['confidence'] = res.confidence
122
+
123
+ if tokens_info:
124
+ frame_rate = self.model.subsampling_rate(
125
+ ) * 0.01 # 0.01 seconds per frame
126
+ max_duration = encoder_out.size(1) * frame_rate
127
+ times = gen_timestamps_from_peak(res.times, max_duration,
128
+ frame_rate, 1.0)
129
+ tokens_info = []
130
+ for i, x in enumerate(res.tokens):
131
+ tokens_info.append({
132
+ 'token': self.char_dict[x],
133
+ 'start': round(times[i][0], 3),
134
+ 'end': round(times[i][1], 3),
135
+ 'confidence': round(res.tokens_confidence[i], 2)
136
+ })
137
+ result['tokens'] = tokens_info
138
+ return result
139
+
140
+ def transcribe(self, audio_file: str, tokens_info: bool = False) -> dict:
141
+ return self._decode(audio_file, tokens_info)
142
+
143
+ def tokenize(self, label: str):
144
+ # TODO(Binbin Zhang): Support BPE
145
+ tokens = []
146
+ for c in label:
147
+ if c == ' ':
148
+ c = "▁"
149
+ tokens.append(c)
150
+ token_list = []
151
+ for c in tokens:
152
+ if c in self.symbol_table:
153
+ token_list.append(self.symbol_table[c])
154
+ elif '<unk>' in self.symbol_table:
155
+ token_list.append(self.symbol_table['<unk>'])
156
+ return token_list
157
+
158
+ def align(self, audio_file: str, label: str) -> dict:
159
+ return self._decode(audio_file, True, label)
160
+
161
+
162
+ def load_model(language: str = None,
163
+ model_dir: str = None,
164
+ gpu: int = -1,
165
+ beam: int = 5,
166
+ context_path: str = None,
167
+ context_score: float = 6.0,
168
+ device: str = "cpu") -> Model:
169
+ if model_dir is None:
170
+ model_dir = Hub.get_model_by_lang(language)
171
+
172
+ if gpu != -1:
173
+ # remain the original usage of gpu
174
+ device = "cuda"
175
+ model = Model(model_dir, gpu, beam, context_path, context_score)
176
+ model.device = torch.device(device)
177
+ model.model.to(device)
178
+ return model
179
+
180
+ # Load the pytorch pt model which contains all the details compared with jit.
181
+ # And we can use the pt model as a third party pytorch nn.Module for training
182
+ def load_model_pt(model_dir):
183
+ """ There are the followi files in in `model_dir`
184
+ * final.pt, required
185
+ * train.yaml, required
186
+ * units.txt, required
187
+ * global_cmvn, optional
188
+ """
189
+ # Check required files
190
+ required_files = ['train.yaml', 'final.pt', 'units.txt']
191
+ for file in required_files:
192
+ file_path = os.path.join(model_dir, file)
193
+ if not os.path.exists(file_path):
194
+ raise FileNotFoundError(
195
+ f"Required file {file} not found in {model_dir}")
196
+ # Read config and override some config
197
+ config_file = os.path.join(model_dir, 'train.yaml')
198
+ with open(config_file, 'r') as fin:
199
+ configs = yaml.load(fin, Loader=yaml.FullLoader)
200
+ token_file = os.path.join(model_dir, 'units.txt')
201
+ configs['tokenizer_conf']['symbol_table_path'] = token_file
202
+ cmvn_file = os.path.join(model_dir, 'global_cmvn')
203
+ if os.path.exists(cmvn_file):
204
+ configs['cmvn_conf']['cmvn_file'] = cmvn_file
205
+ # Read model
206
+ pt_file = os.path.join(model_dir, 'final.pt')
207
+ args = {'checkpoint': pt_file}
208
+ model, configs = init_model(args, configs)
209
+ return model
wenet/cli/paraformer_model.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import os
3
+ from typing import Dict, List, Union
4
+
5
+ import torch
6
+ import torchaudio
7
+ import torchaudio.compliance.kaldi as kaldi
8
+ from wenet.cli.hub import Hub
9
+ from wenet.paraformer.search import (gen_timestamps_from_peak,
10
+ paraformer_greedy_search)
11
+ from wenet.text.paraformer_tokenizer import ParaformerTokenizer
12
+
13
+
14
+ class Paraformer:
15
+
16
+ def __init__(self, model_dir: str, resample_rate: int = 16000) -> None:
17
+
18
+ model_path = os.path.join(model_dir, 'final.zip')
19
+ units_path = os.path.join(model_dir, 'units.txt')
20
+ self.model = torch.jit.load(model_path)
21
+ self.resample_rate = resample_rate
22
+ self.device = torch.device("cpu")
23
+ self.tokenizer = ParaformerTokenizer(symbol_table=units_path)
24
+
25
+ @torch.inference_mode()
26
+ def transcribe_batch(self,
27
+ audio_files: List[Union[str, bytes]],
28
+ tokens_info: bool = False) -> List[Dict]:
29
+ feats_lst = []
30
+ feats_lens_lst = []
31
+ for audio in audio_files:
32
+ if isinstance(audio, bytes):
33
+ with io.BytesIO(audio) as fobj:
34
+ waveform, sample_rate = torchaudio.load(fobj,
35
+ normalize=False)
36
+ else:
37
+ waveform, sample_rate = torchaudio.load(audio, normalize=False)
38
+ if sample_rate != self.resample_rate:
39
+ waveform = torchaudio.transforms.Resample(
40
+ orig_freq=sample_rate,
41
+ new_freq=self.resample_rate)(waveform)
42
+
43
+ waveform = waveform.to(torch.float)
44
+ feats = kaldi.fbank(waveform,
45
+ num_mel_bins=80,
46
+ frame_length=25,
47
+ frame_shift=10,
48
+ energy_floor=0.0,
49
+ sample_frequency=self.resample_rate,
50
+ window_type="hamming")
51
+ feats_lst.append(feats)
52
+ feats_lens_lst.append(
53
+ torch.tensor(feats.shape[0], dtype=torch.int64))
54
+ feats_tensor = torch.nn.utils.rnn.pad_sequence(
55
+ feats_lst, batch_first=True).to(device=self.device)
56
+ feats_lens_tensor = torch.tensor(feats_lens_lst, device=self.device)
57
+
58
+ decoder_out, token_num, tp_alphas, frames = self.model.forward_paraformer(
59
+ feats_tensor, feats_lens_tensor)
60
+ frames = frames.cpu().numpy()
61
+ cif_peaks = self.model.forward_cif_peaks(tp_alphas, token_num)
62
+
63
+ results = paraformer_greedy_search(decoder_out, token_num, cif_peaks)
64
+
65
+ r = []
66
+ for (i, res) in enumerate(results):
67
+ result = {}
68
+ result['confidence'] = res.confidence
69
+ result['text'] = self.tokenizer.detokenize(res.tokens)[0]
70
+ if tokens_info:
71
+ tokens_info_l = []
72
+ times = gen_timestamps_from_peak(res.times,
73
+ num_frames=frames[i],
74
+ frame_rate=0.02)
75
+
76
+ for i, x in enumerate(res.tokens[:len(times)]):
77
+ tokens_info_l.append({
78
+ 'token':
79
+ self.tokenizer.char_dict[x],
80
+ 'start':
81
+ round(times[i][0], 3),
82
+ 'end':
83
+ round(times[i][1], 3),
84
+ 'confidence':
85
+ round(res.tokens_confidence[i], 2)
86
+ })
87
+ result['tokens'] = tokens_info_l
88
+ r.append(result)
89
+ return r
90
+
91
+ def transcribe(self, audio_file: str, tokens_info: bool = False) -> dict:
92
+ result = self.transcribe_batch([audio_file], tokens_info)[0]
93
+ return result
94
+
95
+ def align(self, audio_file: str, label: str) -> dict:
96
+ raise NotImplementedError("Align is currently not supported")
97
+
98
+
99
+ def load_model(model_dir: str = None,
100
+ gpu: int = -1,
101
+ device: str = "cpu") -> Paraformer:
102
+ if model_dir is None:
103
+ model_dir = Hub.get_model_by_lang('paraformer')
104
+ if gpu != -1:
105
+ # remain the original usage of gpu
106
+ device = "cuda"
107
+ paraformer = Paraformer(model_dir)
108
+ paraformer.device = torch.device(device)
109
+ paraformer.model.to(device)
110
+ return paraformer
wenet/cli/punc_model.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import List
3
+
4
+ import jieba
5
+ import torch
6
+ from wenet.cli.hub import Hub
7
+ from wenet.paraformer.search import _isAllAlpha
8
+ from wenet.text.char_tokenizer import CharTokenizer
9
+
10
+
11
+ class PuncModel:
12
+
13
+ def __init__(self, model_dir: str) -> None:
14
+ self.model_dir = model_dir
15
+ model_path = os.path.join(model_dir, 'final.zip')
16
+ units_path = os.path.join(model_dir, 'units.txt')
17
+
18
+ self.model = torch.jit.load(model_path)
19
+ self.tokenizer = CharTokenizer(units_path)
20
+ self.device = torch.device("cpu")
21
+ self.use_jieba = False
22
+
23
+ self.punc_table = ['<unk>', '', ',', '。', '?', '、']
24
+
25
+ def split_words(self, text: str):
26
+ if not self.use_jieba:
27
+ self.use_jieba = True
28
+ import logging
29
+
30
+ # Disable jieba's logger
31
+ logging.getLogger('jieba').disabled = True
32
+ jieba.load_userdict(os.path.join(self.model_dir, 'jieba_usr_dict'))
33
+
34
+ result_list = []
35
+ tokens = text.split()
36
+ current_language = None
37
+ buffer = []
38
+
39
+ for token in tokens:
40
+ is_english = token.isascii()
41
+ if is_english:
42
+ language = "English"
43
+ else:
44
+ language = "Chinese"
45
+
46
+ if current_language and language != current_language:
47
+ if current_language == "Chinese":
48
+ result_list.extend(jieba.cut(''.join(buffer), HMM=False))
49
+ else:
50
+ result_list.extend(buffer)
51
+ buffer = []
52
+
53
+ buffer.append(token)
54
+ current_language = language
55
+
56
+ if buffer:
57
+ if current_language == "Chinese":
58
+ result_list.extend(jieba.cut(''.join(buffer), HMM=False))
59
+ else:
60
+ result_list.extend(buffer)
61
+
62
+ return result_list
63
+
64
+ def add_punc_batch(self, texts: List[str]):
65
+ batch_text_words = []
66
+ batch_text_ids = []
67
+ batch_text_lens = []
68
+
69
+ for text in texts:
70
+ words = self.split_words(text)
71
+ ids = self.tokenizer.tokens2ids(words)
72
+ batch_text_words.append(words)
73
+ batch_text_ids.append(ids)
74
+ batch_text_lens.append(len(ids))
75
+
76
+ texts_tensor = torch.tensor(batch_text_ids,
77
+ device=self.device,
78
+ dtype=torch.int64)
79
+ texts_lens_tensor = torch.tensor(batch_text_lens,
80
+ device=self.device,
81
+ dtype=torch.int64)
82
+
83
+ log_probs, _ = self.model(texts_tensor, texts_lens_tensor)
84
+ result = []
85
+ outs = log_probs.argmax(-1).cpu().numpy()
86
+ for i, out in enumerate(outs):
87
+ punc_id = out[:batch_text_lens[i]]
88
+ sentence = ''
89
+ for j, word in enumerate(batch_text_words[i]):
90
+ if _isAllAlpha(word):
91
+ word = '▁' + word
92
+ word += self.punc_table[punc_id[j]]
93
+ sentence += word
94
+ result.append(sentence.replace('▁', ' '))
95
+ return result
96
+
97
+ def __call__(self, text: str):
98
+ if text != '':
99
+ r = self.add_punc_batch([text])[0]
100
+ return r
101
+ return ''
102
+
103
+
104
+ def load_model(model_dir: str = None,
105
+ gpu: int = -1,
106
+ device: str = "cpu") -> PuncModel:
107
+ if model_dir is None:
108
+ model_dir = Hub.get_model_by_lang('punc')
109
+ if gpu != -1:
110
+ # remain the original usage of gpu
111
+ device = "cuda"
112
+ punc = PuncModel(model_dir)
113
+ punc.device = torch.device(device)
114
+ punc.model.to(device)
115
+ return punc
wenet/cli/transcribe.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2023 Binbin Zhang (binbzha@qq.com)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import argparse
16
+
17
+ from wenet.cli.model import load_model
18
+ from wenet.cli.paraformer_model import load_model as load_paraformer
19
+ from wenet.cli.punc_model import load_model as load_punc_model
20
+
21
+
22
+ def get_args():
23
+ parser = argparse.ArgumentParser(description='')
24
+ parser.add_argument('audio_file', help='audio file to transcribe')
25
+ parser.add_argument('-l',
26
+ '--language',
27
+ choices=[
28
+ 'chinese',
29
+ 'english',
30
+ ],
31
+ default='chinese',
32
+ help='language type')
33
+ parser.add_argument('-m',
34
+ '--model_dir',
35
+ default=None,
36
+ help='specify your own model dir')
37
+ parser.add_argument('-g',
38
+ '--gpu',
39
+ type=int,
40
+ default='-1',
41
+ help='gpu id to decode, default is cpu.')
42
+ parser.add_argument('--device',
43
+ type=str,
44
+ default='cpu',
45
+ choices=["cpu", "npu", "cuda"],
46
+ help='accelerator to use')
47
+ parser.add_argument('-t',
48
+ '--show_tokens_info',
49
+ action='store_true',
50
+ help='whether to output token(word) level information'
51
+ ', such times/confidence')
52
+ parser.add_argument('--align',
53
+ action='store_true',
54
+ help='force align the input audio and transcript')
55
+ parser.add_argument('--label', type=str, help='the input label to align')
56
+ parser.add_argument('--paraformer',
57
+ action='store_true',
58
+ help='whether to use the best chinese model')
59
+ parser.add_argument('--beam', type=int, default=5, help="beam size")
60
+ parser.add_argument('--context_path',
61
+ type=str,
62
+ default=None,
63
+ help='context list file')
64
+ parser.add_argument('--context_score',
65
+ type=float,
66
+ default=6.0,
67
+ help='context score')
68
+ parser.add_argument('--punc', action='store_true', help='context score')
69
+
70
+ parser.add_argument('-pm',
71
+ '--punc_model_dir',
72
+ default=None,
73
+ help='specify your own punc model dir')
74
+
75
+ args = parser.parse_args()
76
+ return args
77
+
78
+
79
+ def main():
80
+ args = get_args()
81
+
82
+ if args.paraformer:
83
+ model = load_paraformer(args.model_dir, args.gpu, args.device)
84
+ else:
85
+ model = load_model(args.language, args.model_dir, args.gpu, args.beam,
86
+ args.context_path, args.context_score, args.device)
87
+ punc_model = None
88
+ if args.punc:
89
+ punc_model = load_punc_model(args.punc_model_dir, args.gpu,
90
+ args.device)
91
+ if args.align:
92
+ result = model.align(args.audio_file, args.label)
93
+ else:
94
+ result = model.transcribe(args.audio_file, args.show_tokens_info)
95
+ if args.punc:
96
+ assert punc_model is not None
97
+ result['text_with_punc'] = punc_model(result['text'])
98
+ print(result)
99
+
100
+
101
+ if __name__ == "__main__":
102
+ main()
wenet/ctl_model/__init__.py ADDED
File without changes
wenet/ctl_model/asr_model_ctl.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2020 Mobvoi Inc. (authors: Binbin Zhang, Di Wu)
2
+ # 2023 NetEase Inc. (authors: Yuting Yang)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # Modified from ESPnet(https://github.com/espnet/espnet) and
16
+ # fairseq(https://github.com/facebookresearch/fairseq)
17
+
18
+ from typing import Dict, Optional
19
+
20
+ import torch
21
+ import torch.nn.functional as F
22
+ from wenet.transformer.ctc import CTC
23
+ from wenet.transformer.decoder import TransformerDecoder
24
+ from wenet.ctl_model.encoder import TransformerEncoder
25
+ from wenet.transformer.asr_model import ASRModel
26
+ from wenet.utils.common import IGNORE_ID
27
+
28
+
29
+ class CTLModel(ASRModel):
30
+ """
31
+ Implementation of Interspeecch 2023 paper:
32
+ 'Enhancing the Unified Streaming and Non-streaming Model
33
+ with Contrastive Learning'
34
+ https://arxiv.org/abs/2306.00755
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ vocab_size: int,
40
+ encoder: TransformerEncoder,
41
+ decoder: TransformerDecoder,
42
+ ctc: CTC,
43
+ ctc_weight: float = 0.5,
44
+ ignore_id: int = IGNORE_ID,
45
+ reverse_weight: float = 0.0,
46
+ lsm_weight: float = 0.0,
47
+ length_normalized_loss: bool = False,
48
+ logit_temp: float = 0.1,
49
+ n_negatives: int = 0,
50
+ ctl_weight: float = 1,
51
+ special_tokens: dict = None,
52
+ ):
53
+ assert 0.0 <= ctc_weight <= 1.0, ctc_weight
54
+ super().__init__(vocab_size,
55
+ encoder,
56
+ decoder,
57
+ ctc,
58
+ ctc_weight,
59
+ ignore_id,
60
+ reverse_weight,
61
+ lsm_weight,
62
+ length_normalized_loss,
63
+ special_tokens=special_tokens)
64
+
65
+ # For CTL Loss
66
+ self.n_negatives = n_negatives
67
+ self.ctl_weight = ctl_weight
68
+ self.logit_temp = logit_temp
69
+
70
+ @torch.jit.unused
71
+ def forward(
72
+ self,
73
+ batch: dict,
74
+ device: torch.device,
75
+ ) -> Dict[str, Optional[torch.Tensor]]:
76
+
77
+ speech = batch['feats'].to(device)
78
+ speech_lengths = batch['feats_lengths'].to(device)
79
+ text = batch['target'].to(device)
80
+ text_lengths = batch['target_lengths'].to(device)
81
+ loss_full, encoder_out_full, _, _ = self.forward_full(
82
+ speech, speech_lengths, text, text_lengths)
83
+ loss_chunk, encoder_out, lens_chunk, encoder_mask = self.forward_chunk(
84
+ speech, speech_lengths, text, text_lengths)
85
+
86
+ ctl_loss = 0.0
87
+ if self.ctl_weight > 0 and self.n_negatives > 0:
88
+ num = encoder_out_full.size(1)
89
+ targets = encoder_out_full
90
+ src = encoder_out
91
+ negs, negs_idxs = self.sample_negatives(targets,
92
+ targets.size(1),
93
+ speech_lengths=lens_chunk)
94
+ ctl_loss = self.CTL(src, targets, negs, encoder_mask)
95
+
96
+ loss = loss_full + loss_chunk + self.ctl_weight * ctl_loss
97
+ return {
98
+ "loss": loss,
99
+ "loss_full": loss_full,
100
+ "loss_chunk": loss_chunk,
101
+ "loss_ctl": ctl_loss
102
+ }
103
+
104
+ def forward_full(
105
+ self,
106
+ speech: torch.Tensor,
107
+ speech_lengths: torch.Tensor,
108
+ text: torch.Tensor,
109
+ text_lengths: torch.Tensor,
110
+ ):
111
+ """Full context mode
112
+ Frontend + Encoder + Decoder + Calc loss
113
+
114
+ Args:
115
+ speech: (Batch, Length, ...)
116
+ speech_lengths: (Batch, )
117
+ text: (Batch, Length)
118
+ text_lengths: (Batch,)
119
+ """
120
+
121
+ assert text_lengths.dim() == 1, text_lengths.shape
122
+ # Check that batch_size is unified
123
+ assert (speech.shape[0] == speech_lengths.shape[0] == text.shape[0] ==
124
+ text_lengths.shape[0]), (speech.shape, speech_lengths.shape,
125
+ text.shape, text_lengths.shape)
126
+ # 1. Encoder
127
+ encoder_out, encoder_mask = self.encoder.forward_full(
128
+ speech, speech_lengths)
129
+ encoder_out_lens = encoder_mask.squeeze(1).sum(1)
130
+
131
+ # 2a. Attention-decoder branch
132
+ if self.ctc_weight != 1.0:
133
+ loss_att, acc_att = self._calc_att_loss(encoder_out, encoder_mask,
134
+ text, text_lengths)
135
+ else:
136
+ loss_att = None
137
+
138
+ # 2b. CTC branch
139
+ if self.ctc_weight != 0.0:
140
+ loss_ctc = self.ctc(encoder_out, encoder_out_lens, text,
141
+ text_lengths)
142
+ else:
143
+ loss_ctc = None
144
+
145
+ if loss_ctc is None:
146
+ loss = loss_att
147
+ elif loss_att is None:
148
+ loss = loss_ctc
149
+ else:
150
+ loss = self.ctc_weight * loss_ctc[0] + (1 -
151
+ self.ctc_weight) * loss_att
152
+ return loss, encoder_out, encoder_out_lens, encoder_mask
153
+
154
+ def forward_chunk(
155
+ self,
156
+ speech: torch.Tensor,
157
+ speech_lengths: torch.Tensor,
158
+ text: torch.Tensor,
159
+ text_lengths: torch.Tensor,
160
+ ):
161
+ """Chunk-based context mode
162
+ Frontend + Encoder + Decoder + Calc loss
163
+
164
+ Args:
165
+ speech: (Batch, Length, ...)
166
+ speech_lengths: (Batch, )
167
+ text: (Batch, Length)
168
+ text_lengths: (Batch,)
169
+ """
170
+
171
+ assert text_lengths.dim() == 1, text_lengths.shape
172
+ # Check that batch_size is unified
173
+ assert (speech.shape[0] == speech_lengths.shape[0] == text.shape[0] ==
174
+ text_lengths.shape[0]), (speech.shape, speech_lengths.shape,
175
+ text.shape, text_lengths.shape)
176
+ # 1. Encoder
177
+ encoder_out, encoder_mask = self.encoder(speech, speech_lengths)
178
+ encoder_out_lens = encoder_mask.squeeze(1).sum(1)
179
+
180
+ # 2a. Attention-decoder branch
181
+ if self.ctc_weight != 1.0:
182
+ loss_att, acc_att = self._calc_att_loss(encoder_out, encoder_mask,
183
+ text, text_lengths)
184
+ else:
185
+ loss_att = None
186
+
187
+ # 2b. CTC branch
188
+ if self.ctc_weight != 0.0:
189
+ loss_ctc = self.ctc(encoder_out, encoder_out_lens, text,
190
+ text_lengths)
191
+ else:
192
+ loss_ctc = None
193
+
194
+ if loss_ctc is None:
195
+ loss = loss_att
196
+ elif loss_att is None:
197
+ loss = loss_ctc
198
+ else:
199
+ loss = self.ctc_weight * loss_ctc[0] + (1 -
200
+ self.ctc_weight) * loss_att
201
+ return loss, encoder_out, encoder_out_lens, encoder_mask
202
+
203
+ def sample_negatives(self, y, num, padding_count=0, speech_lengths=None):
204
+ if self.n_negatives == 0:
205
+ return y.new(0)
206
+ bsz, tsz, fsz = y.shape
207
+ y = y.reshape(-1, fsz) # BTC => (BxT)C
208
+
209
+ # FIXME: what happens if padding_count is specified?
210
+ high = tsz - (padding_count or 0)
211
+ with torch.no_grad():
212
+ assert high > 1, f"{bsz,tsz,fsz}"
213
+
214
+ if self.n_negatives > 0:
215
+ tszs = (torch.arange(num).unsqueeze(-1).expand(
216
+ -1, self.n_negatives).flatten())
217
+ if speech_lengths is not None:
218
+ neg_idxs = [
219
+ torch.randint(low=0,
220
+ high=speech_lengths[i].item() - 1,
221
+ size=(1, self.n_negatives * tsz))
222
+ for i in range(len(speech_lengths))
223
+ ]
224
+ neg_idxs = torch.cat(neg_idxs).reshape(
225
+ bsz, self.n_negatives * tsz)
226
+ else:
227
+ neg_idxs = torch.randint(low=0,
228
+ high=num - 1,
229
+ size=(bsz,
230
+ self.n_negatives * tsz))
231
+ neg_idxs[neg_idxs >= tszs] += 1
232
+
233
+ if self.n_negatives > 0:
234
+ neg_idxs = neg_idxs + (torch.arange(bsz).unsqueeze(1) * high)
235
+
236
+ negs = y[neg_idxs.view(-1)]
237
+ negs = negs.contiguous().view(bsz, num, self.n_negatives,
238
+ fsz).permute(2, 0, 1, 3) # to NxBxTxC
239
+ return negs, neg_idxs
240
+
241
+ def compute_preds(self, x, y, negatives):
242
+ neg_is_pos = (y == negatives).all(-1)
243
+ y = y.unsqueeze(0)
244
+ targets = torch.cat([y, negatives], dim=0)
245
+
246
+ logits = torch.cosine_similarity(x.float(), targets.float(), dim=-1)
247
+ logits = logits / self.logit_temp
248
+ logits = logits.type_as(x)
249
+
250
+ if neg_is_pos.any():
251
+ if not hasattr(self, "_inftensor"):
252
+ self._inftensor = float("-inf")
253
+ # logits[1:] = index_put(logits[1:], neg_is_pos, self._inftensor)
254
+ logits[1:][neg_is_pos] = self._inftensor
255
+ logits = logits.transpose(0, 2)
256
+ logits = logits.transpose(0, 1)
257
+ logits = logits.reshape(-1, logits.size(-1))
258
+ return logits
259
+
260
+ def CTL(self, x, y, negs, mask=None):
261
+ # Step1: compute cosine similarity, shape [B*T, n_negatives+1]
262
+ logits = self.compute_preds(x, y, negs)
263
+
264
+ # Step2: target shape [B*T]
265
+ target = x.new_zeros(x.size(0) * x.size(1), dtype=torch.long)
266
+
267
+ # Step3: compute CTL loss
268
+ if mask is not None:
269
+ normalize_length = mask.sum()
270
+ bz, sz = mask.size(0), mask.size(-1)
271
+ mask = mask.squeeze(1).reshape(bz * sz).eq(0)
272
+ ce = F.cross_entropy(logits, target, reduction='none')
273
+ loss = ce.masked_fill(mask, 0).sum() / normalize_length
274
+ else:
275
+ loss = F.cross_entropy(logits, target)
276
+
277
+ return loss
wenet/ctl_model/encoder.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 Mobvoi Inc (Binbin Zhang, Di Wu)
2
+ # 2022 Xingchen Song (sxc19@mails.tsinghua.edu.cn)
3
+ # 2023 NetEase Inc
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ # Modified from ESPnet(https://github.com/espnet/espnet)
17
+ """Encoder definition."""
18
+ from typing import Optional, Tuple
19
+
20
+ import torch
21
+
22
+ from wenet.utils.mask import make_pad_mask
23
+ from wenet.transformer.encoder import TransformerEncoder, ConformerEncoder
24
+
25
+
26
+ class DualTransformerEncoder(TransformerEncoder):
27
+ """Transformer encoder module."""
28
+
29
+ def __init__(
30
+ self,
31
+ input_size: int,
32
+ output_size: int = 256,
33
+ attention_heads: int = 4,
34
+ linear_units: int = 2048,
35
+ num_blocks: int = 6,
36
+ dropout_rate: float = 0.1,
37
+ positional_dropout_rate: float = 0.1,
38
+ attention_dropout_rate: float = 0.0,
39
+ input_layer: str = "conv2d",
40
+ pos_enc_layer_type: str = "abs_pos",
41
+ normalize_before: bool = True,
42
+ static_chunk_size: int = 0,
43
+ use_dynamic_chunk: bool = False,
44
+ global_cmvn: torch.nn.Module = None,
45
+ use_dynamic_left_chunk: bool = False,
46
+ query_bias: bool = True,
47
+ key_bias: bool = True,
48
+ value_bias: bool = True,
49
+ activation_type: str = "relu",
50
+ gradient_checkpointing: bool = False,
51
+ use_sdpa: bool = False,
52
+ layer_norm_type: str = 'layer_norm',
53
+ norm_eps: float = 1e-5,
54
+ n_kv_head: Optional[int] = None,
55
+ head_dim: Optional[int] = None,
56
+ selfattention_layer_type: str = "selfattn",
57
+ mlp_type: str = 'position_wise_feed_forward',
58
+ mlp_bias: bool = True,
59
+ n_expert: int = 8,
60
+ n_expert_activated: int = 2,
61
+ ):
62
+ """ Construct DualTransformerEncoder
63
+ Support both the full context mode and the streaming mode separately
64
+ """
65
+ super().__init__(input_size, output_size, attention_heads,
66
+ linear_units, num_blocks, dropout_rate,
67
+ positional_dropout_rate, attention_dropout_rate,
68
+ input_layer, pos_enc_layer_type, normalize_before,
69
+ static_chunk_size, use_dynamic_chunk, global_cmvn,
70
+ use_dynamic_left_chunk, query_bias, key_bias,
71
+ value_bias, activation_type, gradient_checkpointing,
72
+ use_sdpa, layer_norm_type, norm_eps, n_kv_head,
73
+ head_dim, selfattention_layer_type, mlp_type,
74
+ mlp_bias, n_expert, n_expert_activated)
75
+
76
+ def forward_full(
77
+ self,
78
+ xs: torch.Tensor,
79
+ xs_lens: torch.Tensor,
80
+ decoding_chunk_size: int = 0,
81
+ num_decoding_left_chunks: int = -1,
82
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
83
+ T = xs.size(1)
84
+ masks = ~make_pad_mask(xs_lens, T).unsqueeze(1) # (B, 1, T)
85
+ if self.global_cmvn is not None:
86
+ xs = self.global_cmvn(xs)
87
+ xs, pos_emb, masks = self.embed(xs, masks)
88
+ mask_pad = masks # (B, 1, T/subsample_rate)
89
+ for layer in self.encoders:
90
+ xs, masks, _, _ = layer(xs, masks, pos_emb, mask_pad)
91
+ if self.normalize_before:
92
+ xs = self.after_norm(xs)
93
+ return xs, masks
94
+
95
+
96
+ class DualConformerEncoder(ConformerEncoder):
97
+ """Conformer encoder module."""
98
+
99
+ def __init__(
100
+ self,
101
+ input_size: int,
102
+ output_size: int = 256,
103
+ attention_heads: int = 4,
104
+ linear_units: int = 2048,
105
+ num_blocks: int = 6,
106
+ dropout_rate: float = 0.1,
107
+ positional_dropout_rate: float = 0.1,
108
+ attention_dropout_rate: float = 0.0,
109
+ input_layer: str = "conv2d",
110
+ pos_enc_layer_type: str = "rel_pos",
111
+ normalize_before: bool = True,
112
+ static_chunk_size: int = 0,
113
+ use_dynamic_chunk: bool = False,
114
+ global_cmvn: torch.nn.Module = None,
115
+ use_dynamic_left_chunk: bool = False,
116
+ positionwise_conv_kernel_size: int = 1,
117
+ macaron_style: bool = True,
118
+ selfattention_layer_type: str = "rel_selfattn",
119
+ activation_type: str = "swish",
120
+ use_cnn_module: bool = True,
121
+ cnn_module_kernel: int = 15,
122
+ causal: bool = False,
123
+ cnn_module_norm: str = "batch_norm",
124
+ query_bias: bool = True,
125
+ key_bias: bool = True,
126
+ value_bias: bool = True,
127
+ conv_bias: bool = True,
128
+ gradient_checkpointing: bool = False,
129
+ use_sdpa: bool = False,
130
+ layer_norm_type: str = 'layer_norm',
131
+ norm_eps: float = 1e-5,
132
+ n_kv_head: Optional[int] = None,
133
+ head_dim: Optional[int] = None,
134
+ mlp_type: str = 'position_wise_feed_forward',
135
+ mlp_bias: bool = True,
136
+ n_expert: int = 8,
137
+ n_expert_activated: int = 2,
138
+ ):
139
+ """ Construct DualConformerEncoder
140
+ Support both the full context mode and the streaming mode separately
141
+ """
142
+ super().__init__(
143
+ input_size, output_size, attention_heads, linear_units, num_blocks,
144
+ dropout_rate, positional_dropout_rate, attention_dropout_rate,
145
+ input_layer, pos_enc_layer_type, normalize_before,
146
+ static_chunk_size, use_dynamic_chunk, global_cmvn,
147
+ use_dynamic_left_chunk, positionwise_conv_kernel_size,
148
+ macaron_style, selfattention_layer_type, activation_type,
149
+ use_cnn_module, cnn_module_kernel, causal, cnn_module_norm,
150
+ query_bias, key_bias, value_bias, conv_bias,
151
+ gradient_checkpointing, use_sdpa, layer_norm_type, norm_eps,
152
+ n_kv_head, head_dim, mlp_type, mlp_bias, n_expert,
153
+ n_expert_activated)
154
+
155
+ def forward_full(
156
+ self,
157
+ xs: torch.Tensor,
158
+ xs_lens: torch.Tensor,
159
+ decoding_chunk_size: int = 0,
160
+ num_decoding_left_chunks: int = -1,
161
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
162
+ T = xs.size(1)
163
+ masks = ~make_pad_mask(xs_lens, T).unsqueeze(1) # (B, 1, T)
164
+ if self.global_cmvn is not None:
165
+ xs = self.global_cmvn(xs)
166
+ xs, pos_emb, masks = self.embed(xs, masks)
167
+ mask_pad = masks # (B, 1, T/subsample_rate)
168
+ for layer in self.encoders:
169
+ xs, masks, _, _ = layer(xs, masks, pos_emb, mask_pad)
170
+ if self.normalize_before:
171
+ xs = self.after_norm(xs)
172
+ return xs, masks
wenet/dataset/__init__.py ADDED
File without changes
wenet/dataset/datapipes.py ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2023 Wenet Community. (authors: Dinghao Zhou)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import collections
16
+ from collections.abc import Callable
17
+ import copy
18
+ import sys
19
+ import tarfile
20
+ import logging
21
+ from typing import List, Optional
22
+ import numpy as np
23
+ import torch
24
+ from torch.utils.data import IterDataPipe, functional_datapipe
25
+ from torch.utils.data import datapipes
26
+ from torch.utils.data.datapipes.iter import Mapper
27
+ from torch.utils.data.datapipes.iter.sharding import (
28
+ SHARDING_PRIORITIES, ShardingFilterIterDataPipe)
29
+ from torch.utils.data.datapipes.utils.common import _check_unpickable_fn
30
+
31
+ from wenet.dataset.processor import parse_url
32
+
33
+
34
+ @functional_datapipe("map_ignore_error")
35
+ class MapperIgnoreErrorDataPipe(Mapper):
36
+
37
+ def __init__(self,
38
+ dataset: IterDataPipe,
39
+ fn: Callable,
40
+ input_col=None,
41
+ output_col=None,
42
+ log_error: bool = True) -> None:
43
+ super().__init__(dataset, fn, input_col, output_col)
44
+ self._iter = None
45
+ self.log_error = log_error
46
+
47
+ def __iter__(self):
48
+ if self._iter is None:
49
+ self._iter = iter(self.datapipe)
50
+
51
+ while True:
52
+ try:
53
+ elem = next(self._iter)
54
+ yield self._apply_fn(elem)
55
+ except StopIteration:
56
+ self._iter = None
57
+ return
58
+ except Exception as ex:
59
+ if self.log_error:
60
+ logging.warning(str(ex))
61
+
62
+
63
+ @functional_datapipe('bucket_by_sequence_length')
64
+ class BucketBySequenceLengthDataPipe(IterDataPipe):
65
+
66
+ def __init__(
67
+ self,
68
+ dataset: IterDataPipe,
69
+ elem_length_func,
70
+ bucket_boundaries: List[int],
71
+ bucket_batch_sizes: List[int],
72
+ wrapper_class=None,
73
+ ) -> None:
74
+ super().__init__()
75
+ _check_unpickable_fn(elem_length_func)
76
+ assert len(bucket_batch_sizes) == len(bucket_boundaries) + 1
77
+ self.bucket_batch_sizes = bucket_batch_sizes
78
+ self.bucket_boundaries = bucket_boundaries + [sys.maxsize]
79
+ self.elem_length_func = elem_length_func
80
+
81
+ self._group_dp = GroupByWindowDataPipe(dataset,
82
+ self._element_to_bucket_id,
83
+ self._window_size_func,
84
+ wrapper_class=wrapper_class)
85
+
86
+ def __iter__(self):
87
+ yield from self._group_dp
88
+
89
+ def _element_to_bucket_id(self, elem):
90
+ seq_len = self.elem_length_func(elem)
91
+ bucket_id = 0
92
+ for (i, b) in enumerate(self.bucket_boundaries):
93
+ if seq_len < b:
94
+ bucket_id = i
95
+ break
96
+ return bucket_id
97
+
98
+ def _window_size_func(self, bucket_id):
99
+ return self.bucket_batch_sizes[bucket_id]
100
+
101
+
102
+ @functional_datapipe("group_by_window")
103
+ class GroupByWindowDataPipe(datapipes.iter.Grouper):
104
+
105
+ def __init__(
106
+ self,
107
+ dataset: IterDataPipe,
108
+ key_func,
109
+ window_size_func,
110
+ wrapper_class=None,
111
+ ):
112
+ super().__init__(dataset,
113
+ key_func,
114
+ keep_key=False,
115
+ group_size=None,
116
+ drop_remaining=False)
117
+ _check_unpickable_fn(window_size_func)
118
+ self.dp = dataset
119
+ self.window_size_func = window_size_func
120
+ if wrapper_class is not None:
121
+ _check_unpickable_fn(wrapper_class)
122
+ del self.wrapper_class
123
+ self.wrapper_class = wrapper_class
124
+
125
+ def __iter__(self):
126
+ for x in self.datapipe:
127
+ key = self.group_key_fn(x)
128
+
129
+ self.buffer_elements[key].append(x)
130
+ self.curr_buffer_size += 1
131
+
132
+ group_size = self.window_size_func(key)
133
+ if group_size == len(self.buffer_elements[key]):
134
+ result = self.wrapper_class(self.buffer_elements[key])
135
+ yield result
136
+ self.curr_buffer_size -= len(self.buffer_elements[key])
137
+ del self.buffer_elements[key]
138
+
139
+ if self.curr_buffer_size == self.max_buffer_size:
140
+ result_to_yield = self._remove_biggest_key()
141
+ if result_to_yield is not None:
142
+ result = self.wrapper_class(result_to_yield)
143
+ yield result
144
+
145
+ for key in tuple(self.buffer_elements.keys()):
146
+ result = self.wrapper_class(self.buffer_elements.pop(key))
147
+ self.curr_buffer_size -= len(result)
148
+ yield result
149
+
150
+
151
+ @functional_datapipe("sort")
152
+ class SortDataPipe(IterDataPipe):
153
+
154
+ def __init__(self,
155
+ dataset: IterDataPipe,
156
+ buffer_size: int = 500,
157
+ key_func=None,
158
+ reverse=False) -> None:
159
+ if key_func is not None:
160
+ _check_unpickable_fn(key_func)
161
+ self.buffer_size = buffer_size
162
+ super().__init__()
163
+ self.dp = dataset
164
+ self._buffer = []
165
+ self.key_func = key_func
166
+ self.reverse = reverse
167
+
168
+ def __iter__(self):
169
+ for elem in self.dp:
170
+ self._buffer.append(elem)
171
+ if len(self._buffer) >= self.buffer_size:
172
+ self._buffer.sort(key=self.key_func, reverse=self.reverse)
173
+ for x in self._buffer:
174
+ yield x
175
+ del self._buffer
176
+ self._buffer = []
177
+ # The sample left over
178
+ self._buffer.sort(key=self.key_func, reverse=self.reverse)
179
+ for x in self._buffer:
180
+ yield x
181
+ del self._buffer
182
+ self._buffer = []
183
+
184
+
185
+ @functional_datapipe("dynamic_batch")
186
+ class DynamicBatchDataPipe(IterDataPipe):
187
+
188
+ def __init__(self, dataset: IterDataPipe, window_class,
189
+ wrapper_class) -> None:
190
+ _check_unpickable_fn(window_class)
191
+ _check_unpickable_fn(wrapper_class)
192
+ super().__init__()
193
+ self.dp = dataset
194
+ assert window_class is not None
195
+ assert wrapper_class is not None
196
+ self.window_class = window_class
197
+ self._buffer = []
198
+ self._wrappr_class = wrapper_class
199
+
200
+ def __iter__(self):
201
+ for elem in self.dp:
202
+ if not self.window_class(elem, len(self._buffer)):
203
+ self._buffer.append(elem)
204
+ else:
205
+ if len(self._buffer) > 0:
206
+ yield self._wrappr_class(self._buffer)
207
+ del self._buffer
208
+ self._buffer = [elem]
209
+ if len(self._buffer) > 0:
210
+ yield self._wrappr_class(self._buffer)
211
+ del self._buffer
212
+ self._buffer = []
213
+
214
+
215
+ @functional_datapipe("prefetch")
216
+ class PrefetchDataPipe(IterDataPipe):
217
+ """Performs prefetching"""
218
+
219
+ def __init__(
220
+ self,
221
+ dataset: IterDataPipe,
222
+ buffer_size: int = 500,
223
+ ):
224
+ # TODO(Mddct): support multiprocessing pool with shared-memory to
225
+ # prefetch
226
+ super().__init__()
227
+ self.dp = dataset
228
+ self._iter = None
229
+ self._prefetch_buffer_size = buffer_size
230
+ self._buffer = None
231
+ if self._prefetch_buffer_size > 0:
232
+ self._buffer = collections.deque(maxlen=self._prefetch_buffer_size)
233
+
234
+ def __iter__(self):
235
+ if self._prefetch_buffer_size > 0:
236
+ if self._iter is None:
237
+ self._iter = iter(self.dp)
238
+ assert self._buffer is not None
239
+
240
+ while True:
241
+ if len(self._buffer) <= self._prefetch_buffer_size // 2:
242
+ while len(self._buffer) < self._prefetch_buffer_size:
243
+ try:
244
+ self._buffer.append(next(self._iter))
245
+ except StopIteration:
246
+ if len(self._buffer) != 0:
247
+ while len(self._buffer) > 0:
248
+ yield self._buffer.popleft()
249
+ self._iter = None
250
+ return
251
+ while len(self._buffer) > self._prefetch_buffer_size // 2:
252
+ elem = self._buffer.popleft()
253
+ yield elem
254
+
255
+ else:
256
+ yield from self.dp
257
+
258
+
259
+ @functional_datapipe("repeat")
260
+ class RepeatDatapipe(IterDataPipe):
261
+
262
+ def __init__(self, dataset: IterDataPipe, count: int = -1):
263
+ super().__init__()
264
+ self.dp = dataset
265
+ self.count = count
266
+
267
+ def __iter__(self):
268
+ if self.count == 1:
269
+ yield from self.dp
270
+ return
271
+ i = 0
272
+ while self.count < 0 or i < self.count:
273
+ for elem in self.dp:
274
+ new_elem = copy.copy(elem)
275
+ yield new_elem
276
+ i += 1
277
+
278
+
279
+ @functional_datapipe("shard")
280
+ class ShardDataPipe(ShardingFilterIterDataPipe):
281
+
282
+ def __init__(self, dataset: IterDataPipe, partition: bool = False):
283
+ super().__init__(dataset, None)
284
+ self.partition = partition
285
+ self.dp = dataset
286
+
287
+ def apply_sharding(self, num_of_instances: int, instance_id: int,
288
+ sharding_group: SHARDING_PRIORITIES):
289
+ if self.partition:
290
+ return super().apply_sharding(num_of_instances, instance_id,
291
+ sharding_group)
292
+ else:
293
+ # We can not handle uneven data for CV on DDP, so we don't
294
+ # sample data by rank, that means every GPU gets the same
295
+ # and all the CV data
296
+ info = torch.utils.data.get_worker_info()
297
+ if info is None:
298
+ self.num_of_instances = 1
299
+ self.instance_id = 0
300
+ else:
301
+ n_workers_per_device = info.num_workers
302
+ self.num_of_instances = n_workers_per_device
303
+ self.instance_id = info.id
304
+
305
+
306
+ @functional_datapipe("interleave")
307
+ class InterlaveDataPipe(IterDataPipe):
308
+
309
+ def __init__(
310
+ self,
311
+ source_datapipes: List[IterDataPipe],
312
+ weights: Optional[List[float]] = None,
313
+ seed=2027,
314
+ ):
315
+ super().__init__()
316
+ self.rng = np.random.default_rng(seed)
317
+ self.source_datapipes = source_datapipes
318
+ self.weights = weights
319
+ if weights is None:
320
+ self.weights = [1 / len(self.source_datapipes)] * len(
321
+ self.source_datapipes)
322
+ else:
323
+ self.weights = [weight / sum(weights) for weight in weights]
324
+ self.iters = None
325
+
326
+ def __iter__(self):
327
+ weights = copy.deepcopy(self.weights)
328
+ exhausted = len(self.source_datapipes) * [False]
329
+ if self.iters is None:
330
+ self.iters = [(i, iter(d))
331
+ for i, d in enumerate(self.source_datapipes)]
332
+ while True:
333
+ # TODO(Mddct): rng
334
+ index_iter = self.rng.choice(self.iters, p=weights)
335
+ i, ite = index_iter
336
+ try:
337
+ elem = next(ite)
338
+ yield elem
339
+ except StopIteration:
340
+ weights[i] = 0.
341
+ exhausted[i] = True
342
+ if all(exhausted):
343
+ return
344
+ weights = [weight / sum(weights) for weight in weights]
345
+
346
+
347
+ class TextLineDataPipe(IterDataPipe):
348
+ """ Streamming Text line
349
+ """
350
+
351
+ def __init__(self, filenames, mode='r'):
352
+ super().__init__()
353
+ _dp = datapipes.iter.FileLister(filenames)
354
+ _dp = datapipes.iter.FileOpener(_dp, mode=mode)
355
+ self.dp = _dp
356
+
357
+ def __iter__(self):
358
+ for fname, stream in self.dp:
359
+ for line in stream:
360
+ line = line.strip('\n')
361
+ yield {"file_name": fname, "line": line}
362
+ stream.close()
363
+
364
+
365
+ @functional_datapipe("tar_file_and_group")
366
+ class TarsDataPipe(IterDataPipe):
367
+ """ Decode wenet's tar , yield {'txt': "...", "raw": "..."}
368
+ """
369
+
370
+ def __init__(self, dataset: IterDataPipe) -> None:
371
+ super().__init__()
372
+ self.dp = dataset
373
+
374
+ def __iter__(self):
375
+ from wenet.dataset.processor import AUDIO_FORMAT_SETS
376
+ for sample in self.dp:
377
+ assert 'file_name' in sample
378
+ assert 'line' in sample
379
+ assert 'stream' in sample
380
+ try:
381
+ with tarfile.open(fileobj=sample['stream'],
382
+ mode="r:*") as stream:
383
+ prev_prefix = None
384
+ example = {
385
+ 'file_name': sample['file_name'],
386
+ 'tar_file_name': sample['line']
387
+ }
388
+ valid = True
389
+ for tarinfo in stream:
390
+ name = tarinfo.name
391
+ pos = name.rfind('.')
392
+ assert pos > 0
393
+ prefix, postfix = name[:pos], name[pos + 1:]
394
+ if prev_prefix is not None and prefix != prev_prefix:
395
+ example['key'] = prev_prefix
396
+ if valid:
397
+ yield example
398
+ example = {
399
+ 'file_name': sample['file_name'],
400
+ 'tar_file_name': sample['line']
401
+ }
402
+ valid = True
403
+ with stream.extractfile(tarinfo) as file_obj:
404
+ try:
405
+ if postfix == 'txt':
406
+ example['txt'] = file_obj.read().decode(
407
+ 'utf8').strip()
408
+ elif postfix in AUDIO_FORMAT_SETS:
409
+ example['wav'] = file_obj.read()
410
+ else:
411
+ example[postfix] = file_obj.read()
412
+ except Exception as ex:
413
+ valid = False
414
+ logging.warning(
415
+ 'error to parse {}'.format(name))
416
+ prev_prefix = prefix
417
+ if prev_prefix is not None:
418
+ example['key'] = prev_prefix
419
+ yield example
420
+ except Exception as ex:
421
+ msg = 'In tar_file_and_group: {} when processing {}'.format(
422
+ ex, sample['line'])
423
+ logging.warning(msg)
424
+ finally:
425
+ if 'process' in sample:
426
+ sample['process'].communicate()
427
+ sample['stream'].close()
428
+
429
+
430
+ class WenetRawDatasetSource(IterDataPipe):
431
+
432
+ def __init__(self,
433
+ filenames: str,
434
+ prefetch: int = 500,
435
+ partition: bool = True,
436
+ shuffle: bool = False,
437
+ shuffle_size: int = 10000,
438
+ cycle: int = 1) -> None:
439
+ super().__init__()
440
+ self.dp = TextLineDataPipe(filenames)
441
+ if shuffle:
442
+ self.dp = self.dp.shuffle(buffer_size=shuffle_size)
443
+ self.dp = self.dp.repeat(cycle).prefetch(prefetch)
444
+ self.dp = self.dp.shard(partition)
445
+
446
+ def __iter__(self):
447
+ for d in self.dp:
448
+ yield d
449
+
450
+
451
+ class WenetTarShardDatasetSource(IterDataPipe):
452
+
453
+ def __init__(self,
454
+ filenames: str,
455
+ prefetch: int = 500,
456
+ partition: bool = True,
457
+ shuffle: bool = False,
458
+ shuffle_size: int = 10000,
459
+ cycle: int = 1) -> None:
460
+ super().__init__()
461
+ self.dp = TextLineDataPipe(filenames)
462
+ if shuffle:
463
+ self.dp = self.dp.shuffle(buffer_size=shuffle_size)
464
+ self.dp = self.dp.repeat(cycle)
465
+ self.dp = self.dp.shard(partition).map_ignore_error(
466
+ parse_url).tar_file_and_group().prefetch(prefetch)
467
+
468
+ def __iter__(self):
469
+ for d in self.dp:
470
+ yield d
wenet/dataset/dataset.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 Wenet Community. (authors: Binbin Zhang)
2
+ # 2023 Wenet Community. (authors: Dinghao Zhou)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ from functools import partial
17
+ import sys
18
+ from typing import Optional
19
+ from wenet.dataset import processor
20
+ from wenet.dataset.datapipes import (WenetRawDatasetSource,
21
+ WenetTarShardDatasetSource)
22
+ from wenet.text.base_tokenizer import BaseTokenizer
23
+ from wenet.utils.file_utils import read_symbol_table
24
+
25
+
26
+ def Dataset(data_type,
27
+ data_list_file,
28
+ tokenizer: Optional[BaseTokenizer] = None,
29
+ conf=None,
30
+ partition=True):
31
+ """ Construct dataset from arguments
32
+
33
+ We have two shuffle stage in the Dataset. The first is global
34
+ shuffle at shards tar/raw file level. The second is global shuffle
35
+ at training samples level.
36
+
37
+ Args:
38
+ data_type(str): raw/shard
39
+ tokenizer (BaseTokenizer or None): tokenizer to tokenize
40
+ partition(bool): whether to do data partition in terms of rank
41
+ """
42
+ assert conf is not None
43
+ assert data_type in ['raw', 'shard']
44
+ # cycle dataset
45
+ cycle = conf.get('cycle', 1)
46
+ # stage1 shuffle: source
47
+ list_shuffle = conf.get('list_shuffle', True)
48
+ list_shuffle_size = sys.maxsize
49
+ if list_shuffle:
50
+ list_shuffle_conf = conf.get('list_shuffle_conf', {})
51
+ list_shuffle_size = list_shuffle_conf.get('shuffle_size',
52
+ list_shuffle_size)
53
+ if data_type == 'raw':
54
+ dataset = WenetRawDatasetSource(data_list_file,
55
+ partition=partition,
56
+ shuffle=list_shuffle,
57
+ shuffle_size=list_shuffle_size,
58
+ cycle=cycle)
59
+ dataset = dataset.map(processor.parse_json)
60
+ else:
61
+ dataset = WenetTarShardDatasetSource(data_list_file,
62
+ partition=partition,
63
+ shuffle=list_shuffle,
64
+ shuffle_size=list_shuffle_size,
65
+ cycle=cycle)
66
+ dataset = dataset.map_ignore_error(processor.decode_wav)
67
+
68
+ singal_channel_conf = conf.get('singal_channel_conf', {})
69
+ dataset = dataset.map(
70
+ partial(processor.singal_channel, **singal_channel_conf))
71
+
72
+ speaker_conf = conf.get('speaker_conf', None)
73
+ if speaker_conf is not None:
74
+ assert 'speaker_table_path' in speaker_conf
75
+ speaker_table = read_symbol_table(speaker_conf['speaker_table_path'])
76
+ dataset = dataset.map(
77
+ partial(processor.parse_speaker, speaker_dict=speaker_table))
78
+
79
+ if tokenizer is not None:
80
+ dataset = dataset.map(partial(processor.tokenize, tokenizer=tokenizer))
81
+
82
+ filter_conf = conf.get('filter_conf', {})
83
+ dataset = dataset.filter(partial(processor.filter, **filter_conf))
84
+
85
+ resample_conf = conf.get('resample_conf', {})
86
+ dataset = dataset.map(partial(processor.resample, **resample_conf))
87
+
88
+ speed_perturb = conf.get('speed_perturb', False)
89
+ if speed_perturb:
90
+ dataset = dataset.map(partial(processor.speed_perturb))
91
+
92
+ feats_type = conf.get('feats_type', 'fbank')
93
+ assert feats_type in ['fbank', 'mfcc', 'log_mel_spectrogram']
94
+ if feats_type == 'fbank':
95
+ fbank_conf = conf.get('fbank_conf', {})
96
+ dataset = dataset.map(partial(processor.compute_fbank, **fbank_conf))
97
+ elif feats_type == 'mfcc':
98
+ mfcc_conf = conf.get('mfcc_conf', {})
99
+ dataset = dataset.map(partial(processor.compute_mfcc, **mfcc_conf))
100
+ elif feats_type == 'log_mel_spectrogram':
101
+ log_mel_spectrogram_conf = conf.get('log_mel_spectrogram_conf', {})
102
+ dataset = dataset.map(
103
+ partial(processor.compute_log_mel_spectrogram,
104
+ **log_mel_spectrogram_conf))
105
+ spec_aug = conf.get('spec_aug', True)
106
+ spec_sub = conf.get('spec_sub', False)
107
+ spec_trim = conf.get('spec_trim', False)
108
+ if spec_aug:
109
+ spec_aug_conf = conf.get('spec_aug_conf', {})
110
+ dataset = dataset.map(partial(processor.spec_aug, **spec_aug_conf))
111
+ if spec_sub:
112
+ spec_sub_conf = conf.get('spec_sub_conf', {})
113
+ dataset = dataset.map(partial(processor.spec_sub, **spec_sub_conf))
114
+ if spec_trim:
115
+ spec_trim_conf = conf.get('spec_trim_conf', {})
116
+ dataset = dataset.map(partial(processor.spec_trim, **spec_trim_conf))
117
+
118
+ language_conf = conf.get('language_conf', {"limited_langs": ['zh', 'en']})
119
+ dataset = dataset.map(partial(processor.detect_language, **language_conf))
120
+ dataset = dataset.map(processor.detect_task)
121
+
122
+ shuffle = conf.get('shuffle', True)
123
+ if shuffle:
124
+ shuffle_conf = conf.get('shuffle_conf', {})
125
+ dataset = dataset.shuffle(buffer_size=shuffle_conf['shuffle_size'])
126
+
127
+ sort = conf.get('sort', True)
128
+ if sort:
129
+ sort_conf = conf.get('sort_conf', {})
130
+ dataset = dataset.sort(buffer_size=sort_conf['sort_size'],
131
+ key_func=processor.sort_by_feats)
132
+
133
+ batch_conf = conf.get('batch_conf', {})
134
+ batch_type = batch_conf.get('batch_type', 'static')
135
+ assert batch_type in ['static', 'bucket', 'dynamic']
136
+ if batch_type == 'static':
137
+ assert 'batch_size' in batch_conf
138
+ batch_size = batch_conf.get('batch_size', 16)
139
+ dataset = dataset.batch(batch_size, wrapper_class=processor.padding)
140
+ elif batch_type == 'bucket':
141
+ assert 'bucket_boundaries' in batch_conf
142
+ assert 'bucket_batch_sizes' in batch_conf
143
+ dataset = dataset.bucket_by_sequence_length(
144
+ processor.feats_length_fn,
145
+ batch_conf['bucket_boundaries'],
146
+ batch_conf['bucket_batch_sizes'],
147
+ wrapper_class=processor.padding)
148
+ else:
149
+ max_frames_in_batch = batch_conf.get('max_frames_in_batch', 12000)
150
+ dataset = dataset.dynamic_batch(
151
+ processor.DynamicBatchWindow(max_frames_in_batch),
152
+ wrapper_class=processor.padding,
153
+ )
154
+
155
+ return dataset
wenet/dataset/deprecated/__init__.py ADDED
File without changes
wenet/dataset/deprecated/dataset.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 Mobvoi Inc. (authors: Binbin Zhang)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import random
16
+
17
+ import torch
18
+ import torch.distributed as dist
19
+ from torch.utils.data import IterableDataset
20
+
21
+ import wenet.dataset.deprecated.processor as processor
22
+ from wenet.text.base_tokenizer import BaseTokenizer
23
+ from wenet.utils.file_utils import read_lists
24
+
25
+
26
+ class Processor(IterableDataset):
27
+
28
+ def __init__(self, source, f, *args, **kw):
29
+ assert callable(f)
30
+ self.source = source
31
+ self.f = f
32
+ self.args = args
33
+ self.kw = kw
34
+
35
+ def set_epoch(self, epoch):
36
+ self.source.set_epoch(epoch)
37
+
38
+ def __iter__(self):
39
+ """ Return an iterator over the source dataset processed by the
40
+ given processor.
41
+ """
42
+ assert self.source is not None
43
+ assert callable(self.f)
44
+ return self.f(iter(self.source), *self.args, **self.kw)
45
+
46
+ def apply(self, f):
47
+ assert callable(f)
48
+ return Processor(self, f, *self.args, **self.kw)
49
+
50
+
51
+ class DistributedSampler:
52
+
53
+ def __init__(self, shuffle=True, partition=True):
54
+ self.epoch = -1
55
+ self.update()
56
+ self.shuffle = shuffle
57
+ self.partition = partition
58
+
59
+ def update(self):
60
+ assert dist.is_available()
61
+ if dist.is_initialized():
62
+ self.rank = dist.get_rank()
63
+ self.world_size = dist.get_world_size()
64
+ else:
65
+ self.rank = 0
66
+ self.world_size = 1
67
+ worker_info = torch.utils.data.get_worker_info()
68
+ if worker_info is None:
69
+ self.worker_id = 0
70
+ self.num_workers = 1
71
+ else:
72
+ self.worker_id = worker_info.id
73
+ self.num_workers = worker_info.num_workers
74
+ return dict(rank=self.rank,
75
+ world_size=self.world_size,
76
+ worker_id=self.worker_id,
77
+ num_workers=self.num_workers)
78
+
79
+ def set_epoch(self, epoch):
80
+ self.epoch = epoch
81
+
82
+ def sample(self, data):
83
+ """ Sample data according to rank/world_size/num_workers
84
+
85
+ Args:
86
+ data(List): input data list
87
+
88
+ Returns:
89
+ List: data list after sample
90
+ """
91
+ data = list(range(len(data)))
92
+ # TODO(Binbin Zhang): fix this
93
+ # We can not handle uneven data for CV on DDP, so we don't
94
+ # sample data by rank, that means every GPU gets the same
95
+ # and all the CV data
96
+ if self.partition:
97
+ if self.shuffle:
98
+ random.Random(self.epoch).shuffle(data)
99
+ data = data[self.rank::self.world_size]
100
+ data = data[self.worker_id::self.num_workers]
101
+ return data
102
+
103
+
104
+ class DataList(IterableDataset):
105
+
106
+ def __init__(self, lists, shuffle=True, partition=True):
107
+ self.lists = lists
108
+ self.sampler = DistributedSampler(shuffle, partition)
109
+
110
+ def set_epoch(self, epoch):
111
+ self.sampler.set_epoch(epoch)
112
+
113
+ def __iter__(self):
114
+ sampler_info = self.sampler.update()
115
+ indexes = self.sampler.sample(self.lists)
116
+ for index in indexes:
117
+ # yield dict(src=src)
118
+ data = dict(src=self.lists[index])
119
+ data.update(sampler_info)
120
+ yield data
121
+
122
+
123
+ def Dataset(data_type,
124
+ data_list_file,
125
+ tokenizer: BaseTokenizer,
126
+ conf,
127
+ partition=True):
128
+ """ Construct dataset from arguments
129
+
130
+ We have two shuffle stage in the Dataset. The first is global
131
+ shuffle at shards tar/raw file level. The second is global shuffle
132
+ at training samples level.
133
+
134
+ Args:
135
+ data_type(str): raw/shard
136
+ bpe_model(str): model for english bpe part
137
+ partition(bool): whether to do data partition in terms of rank
138
+ """
139
+ assert data_type in ['raw', 'shard']
140
+ lists = read_lists(data_list_file)
141
+ shuffle = conf.get('shuffle', True)
142
+ dataset = DataList(lists, shuffle=shuffle, partition=partition)
143
+ if data_type == 'shard':
144
+ dataset = Processor(dataset, processor.url_opener)
145
+ dataset = Processor(dataset, processor.tar_file_and_group)
146
+ else:
147
+ dataset = Processor(dataset, processor.parse_raw)
148
+
149
+ speaker_conf = conf.get('speaker_conf', None)
150
+ if speaker_conf is not None:
151
+ dataset = Processor(dataset, processor.parse_speaker, **speaker_conf)
152
+
153
+ dataset = Processor(dataset, processor.tokenize, tokenizer)
154
+ filter_conf = conf.get('filter_conf', {})
155
+ dataset = Processor(dataset, processor.filter, **filter_conf)
156
+
157
+ resample_conf = conf.get('resample_conf', {})
158
+ dataset = Processor(dataset, processor.resample, **resample_conf)
159
+
160
+ speed_perturb = conf.get('speed_perturb', False)
161
+ if speed_perturb:
162
+ dataset = Processor(dataset, processor.speed_perturb)
163
+
164
+ feats_type = conf.get('feats_type', 'fbank')
165
+ assert feats_type in ['fbank', 'mfcc', 'log_mel_spectrogram']
166
+ if feats_type == 'fbank':
167
+ fbank_conf = conf.get('fbank_conf', {})
168
+ dataset = Processor(dataset, processor.compute_fbank, **fbank_conf)
169
+ elif feats_type == 'mfcc':
170
+ mfcc_conf = conf.get('mfcc_conf', {})
171
+ dataset = Processor(dataset, processor.compute_mfcc, **mfcc_conf)
172
+ elif feats_type == 'log_mel_spectrogram':
173
+ log_mel_spectrogram_conf = conf.get('log_mel_spectrogram_conf', {})
174
+ dataset = Processor(dataset, processor.compute_log_mel_spectrogram,
175
+ **log_mel_spectrogram_conf)
176
+
177
+ spec_aug = conf.get('spec_aug', True)
178
+ spec_sub = conf.get('spec_sub', False)
179
+ spec_trim = conf.get('spec_trim', False)
180
+ if spec_aug:
181
+ spec_aug_conf = conf.get('spec_aug_conf', {})
182
+ dataset = Processor(dataset, processor.spec_aug, **spec_aug_conf)
183
+ if spec_sub:
184
+ spec_sub_conf = conf.get('spec_sub_conf', {})
185
+ dataset = Processor(dataset, processor.spec_sub, **spec_sub_conf)
186
+ if spec_trim:
187
+ spec_trim_conf = conf.get('spec_trim_conf', {})
188
+ dataset = Processor(dataset, processor.spec_trim, **spec_trim_conf)
189
+
190
+ if shuffle:
191
+ shuffle_conf = conf.get('shuffle_conf', {})
192
+ dataset = Processor(dataset, processor.shuffle, **shuffle_conf)
193
+
194
+ sort = conf.get('sort', True)
195
+ if sort:
196
+ sort_conf = conf.get('sort_conf', {})
197
+ dataset = Processor(dataset, processor.sort, **sort_conf)
198
+
199
+ batch_conf = conf.get('batch_conf', {})
200
+ dataset = Processor(dataset, processor.batch, **batch_conf)
201
+ dataset = Processor(dataset, processor.padding)
202
+ return dataset