fp16-сборка nvidia/parakeet-tdt-0.6b-v3 для ONNX Runtime. Сделана для голосового ввода
в Dispatch: стриминговое распознавание в скользящем
окне на CUDA EP.
Зачем fp16, если есть int8
Мы стартовали на официальном int8-экспорте из релиза sherpa-onnx и заменили его после замеров:
int8 проиграл по обоим параметрам сразу. Причина в способе квантования — граф int8-экспорта
квантован динамически (ConvInteger / DynamicQuantizeLinear), масштабы считаются в
рантайме, и быстрых CUDA-ядер для таких операторов нет. Свёртки fp16 идут по оптимизированному
пути cuDNN.
Замер на RTX 5080, CUDA EP, 13 русских фраз (TTS, 16 кГц моно), WER по словам после
нормализации пунктуации и ё:
int8 (официальный)
fp32 (источник)
fp16 (здесь)
размер
652 МБ
2.4 ГБ
1.2 ГБ
WER
18.9 %
13.5 %
13.5 %
точных совпадений
6/13
6/13
6/13
время на фразу (устойчиво)
~0.22 с
~0.032 с
~0.024 с
fp16 воспроизводит транскрипты fp32 слово в слово при половинном весе и наименьшем времени
прогона. Набор фраз небольшой и синтезирован TTS, поэтому абсолютный WER показателен слабо —
сравнение между сборками корректно, так как аудио и весь путь идентичны.
Состав
файл
что это
encoder.onnx + encoder.onnx.data
FastConformer-энкодер
decoder.onnx + decoder.onnx.data
prediction network (LSTM, 2 слоя, hidden 640)
joiner.onnx + joiner.onnx.data
joint-сеть
tokens.txt
8193 токена, <blk> последним (id 8192)
Веса лежат внешними данными (*.onnx.data) — файл .onnx без своего .data нерабочий,
качать нужно парами.
Форма входов и выходов
Границы графа оставлены в float32 (конвертация с keep_io_types=True), внутри fp16.
Так препроцессор и петля декодирования не требуют правок под fp16:
Два места, где легко ошибиться: feat_dim = 128, а не типовые для NeMo 80 (см. custom
metadata энкодера); канал у энкодера и декодера — вторая ось, не последняя. Последняя
размерность joiner'а 8198 = 8193 класса токенов + 5 длительностей TDT.
Как получено
# 1. fp32-экспорт (encoder/decoder/joiner + tokens.txt)
hf download csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3 --local-dir parakeet-fp32
# 2. fp32 -> fp16
python -c "
import onnx
from onnxruntime.transformers import float16 # именно ORT, см. ниже
onnx.shape_inference.infer_shapes_path('encoder.onnx', 'encoder.shapes.onnx')
m = onnx.load('encoder.shapes.onnx')
fp16 = float16.convert_float_to_float16(m, keep_io_types=True, disable_shape_infer=True)
del fp16.graph.value_info[:]
onnx.save(fp16, 'out/encoder.onnx', save_as_external_data=True,
all_tensors_to_one_file=True, location='encoder.onnx.data', size_threshold=1024)
"
Три грабли, каждая из которых даёт незагружаемый граф:
Конвертер нужен из onnxruntime.transformers, а не onnxconverter_common. Второй не
переписывает атрибут to у узлов Cast: на этой модели он оставлял /pre_encode/Cast с
to=FLOAT, переведя соседнюю константу в float16, и ORT отказывался грузить граф
(«Type parameter (T) of Optype (Add) bound to different types»). В версии ORT этот случай
обработан явно.
Вывод формы обязателен, но только через infer_shapes_path. Без value_info типы не
протягиваются по всем ветвям; в памяти вывод формы недоступен, потому что fp32-энкодер с
внешними данными весит 2.4 ГБ и не влезает в один protobuf. Файл с формами должен лежать
рядом с источником — ссылка на внешние данные внутри графа относительная.
Промежуточные value_info надо снять после конвертации. Вывод формы записал их как
float32, конвертер сменил сами тензоры на float16, и ORT ловит расхождение. На входе и
выходе типы объявлены отдельно, keep_io_types их не менял, поэтому промежуточные
безопасно удалить — ORT выведет заново.
Лицензия и происхождение
Модель — производная от nvidia/parakeet-tdt-0.6b-v3, лицензия CC-BY-4.0 сохраняется.
Веса не переобучались: fp32-экспорт в ONNX сделан
csukuangfj
(проект sherpa-onnx), здесь он только приведён к fp16.