Whisper медленно распознаёт речь: причины и решение
Часовая планёрка считается сорок минут, а на ноутбуке тот же файл летал. Или один звонок расшифровывается за минуту, а следующий, ровно такой же длины, — за десять. Whisper медленно работает почти всегда по одной из шести понятных причин, и каждая видна за пару команд: ниже — как их отличить друг от друга и что конкретно править.
Содержание
- Сначала измерение: RTF вместо ощущения «долго»
- Реализация и режим вычислений: где теряется кратность
- Параметры декодирования: beam_size, откаты по температуре и петли
- Ядра и потоки: почему больше vCPU не всегда быстрее
- Память и диск: когда «медленно» — это своп и OOM
- Пайплайн вокруг модели: аудио, VAD, очередь и таймауты
- Какой сервер под Whisper брать в MAATRIX
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Сначала измерение: RTF вместо ощущения «долго»
Единственная осмысленная величина здесь — RTF (real-time factor): время обработки, делённое на длительность аудио. RTF 0,25 — «в четыре раза быстрее реального времени», RTF 2,0 — «два часа счёта на час записи». Пока этого числа нет на постоянном эталонном файле, правки конфигурации — гадание.
Длительность берём не на глаз:
ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 call.m4a
Дальше — замер, который разделяет загрузку модели и декодирование: лечатся они по-разному.
import sys, time
from faster_whisper import WhisperModel
t0 = time.perf_counter()
model = WhisperModel("large-v3-turbo", device="cpu", compute_type="int8", cpu_threads=4)
t1 = time.perf_counter()
segments, info = model.transcribe(sys.argv[1], language="ru", beam_size=5)
segs = list(segments) # работа происходит именно здесь
t2 = time.perf_counter()
print(f"загрузка модели : {t1 - t0:6.1f} c")
print(f"расшифровка : {t2 - t1:6.1f} c, сегментов: {len(segs)}")
print(f"длительность : {info.duration:6.1f} c (после VAD {info.duration_after_vad:.1f})")
print(f"RTF : {(t2 - t1) / info.duration:6.2f}")
Ловушка, из-за которой половина замеров в интернете бессмысленна: segments — генератор, и обёрнутый в time вызов transcribe() вернётся за доли секунды — модель ещё ничего не считала. Считает она на итерации, отсюда list(segments) в замере.
Что смотреть в выводе:
- Загрузка занимает минуты при каждом старте. Веса скачиваются заново или читаются с медленного диска. При первом запуске это нормально — репозиторий
large-v3тянет около трёх гигабайт; при каждом — нет. - RTF стабилен от файла к файлу. Узкое место — железо и режим вычислений.
- RTF скачет в разы на одинаковых по длине записях. Откаты по температуре или зацикливание декодера.
duration_after_vadзаметно меньшеduration. Хорошо: до модели дошло меньше аудио. Если числа совпадают, VAD выключен и вы платите временем за тишину.
Запускайте замер дважды: первый прогон ещё тянет веса и греет кэш.
Реализация и режим вычислений: где теряется кратность
Самая частая причина «стало медленно» — не настройка, а то, что именно установлено:
/opt/whisper/venv/bin/pip list | grep -iE 'whisper|ctranslate2|torch'
Есть openai-whisper и torch — это референсная PyTorch-реализация: на процессоре она считает в float32 и предупреждает об этом при каждом запуске:
UserWarning: FP16 is not supported on CPU; using FP32 instead
warnings.warn("FP16 is not supported on CPU; using FP32 instead")
Это не ошибка, но именно здесь теряется основная кратность. faster-whisper — та же модель в формате CTranslate2 с квантованием в int8; на CPU это самый быстрый из массово доступных вариантов, и переход на него без единой правки параметров обычно даёт больший выигрыш, чем все прочие оптимизации вместе.
Вторая половина проблемы — compute_type. Значение по умолчанию auto выбирает лучшее из поддерживаемого, и на процессоре это не всегда int8. Спросите библиотеку:
python3 -c "import ctranslate2; print(ctranslate2.get_supported_compute_types('cpu'))"
Типичный ответ на современном x86-сервере:
{'int8', 'int8_float32', 'float32', 'int16'}
Есть int8 в списке — задавайте его явно, compute_type="int8": разница с float32 почти двукратная и по времени, и по памяти. Скопированный из примеров для видеокарты float16 на процессоре не заработает вовсе:
ValueError: Requested float16 compute type, but the target device or backend
do not support efficient float16 computation.
Честно про цену: int8 — приближение. На чистой речи расхождение с float32 в пределах шума, на записи с сильным фоном формулировки иногда расходятся сильнее — проверяйте на своём материале, а не верьте статье.
Третья ловушка — torch со сборкой CUDA «на всякий случай»: без видеокарты он не ускоряет ничего, а попытка указать устройство даёт RuntimeError: Found no NVIDIA driver on your system. Проверка: python3 -c "import torch; print(torch.cuda.is_available())".
Развернуть за пару минут
Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть WhisperПараметры декодирования: beam_size, откаты по температуре и петли
Здесь причина самого странного симптома: одинаковые по длине файлы, а время отличается в пять-шесть раз.
Откаты по температуре. Whisper режет аудио на окна по 30 секунд и после каждого проверяет результат по двум порогам: compression_ratio_threshold=2.4 и log_prob_threshold=-1.0. Не прошёл — окно декодируется заново со следующей температурой из набора (0.0, 0.2, 0.4, 0.6, 0.8, 1.0), то есть одно неудачное окно считается до шести раз. На чистой речи это редкость, а на музыке, уличном шуме, длинной тишине или плохом телефонном канале — почти на каждом окне. Отсюда и разброс.
Лечится одной строкой, но с честной оговоркой:
segments, info = model.transcribe(
"call.wav",
language="ru",
beam_size=1,
temperature=0, # один проход вместо шести
condition_on_previous_text=False,
vad_filter=True,
)
temperature=0 отключает откаты и делает время предсказуемым, но именно откаты вытаскивают модель из плохих участков — на сложном аудио вы получите быстрее и хуже. Компромисс: оставить два значения, temperature=(0.0, 0.2).
beam_size. По умолчанию 5 — декодер ведёт пять гипотез параллельно. Значение 1 — жадный поиск, на большинстве записей заметно быстрее при почти неотличимом тексте. Снижаете что-то ради скорости — начинайте отсюда, а не с размера модели: качество страдает меньше.
Зацикливание. По умолчанию condition_on_previous_text=True — модель подаёт себе на вход собственный предыдущий вывод. На однообразном аудио это сваливается в петлю, и декодер выдаёт максимум контекста (448 токенов) на каждое окно вместо десятка. Симптом характерный: одна фраза повторяется десятки раз, время взлетает.
word_timestamps=True запускает дополнительное выравнивание по матрицам внимания — нужны субтитры с точностью до слова, платите временем.
Размер модели — последний рычаг, самый грубый. Разработчики публикуют в README относительные скорости — замер на своём железе, но соотношение переносится:
| Модель | Параметров | Скорость (README) | VRAM (документация) |
|---|---|---|---|
| tiny | 39M | ~10x | ~1 ГБ |
| base | 74M | ~7x | ~1 ГБ |
| small | 244M | ~4x | ~2 ГБ |
| medium | 769M | ~2x | ~5 ГБ |
| large-v3 | 1550M | 1x | ~10 ГБ |
| large-v3-turbo | 809M | ~8x | ~6 ГБ |
Вывод: с large-v3 переход на large-v3-turbo — почти бесплатный выигрыш. Turbo — тот же large-v3 с урезанным декодером (четыре слоя вместо тридцати двух); качество близкое, проседает в основном на переводе речи на английский.
Ядра и потоки: почему больше vCPU не всегда быстрее
Самая обидная ошибка — четыре потока на двух ядрах. По умолчанию cpu_threads=0, и CTranslate2 берёт своё значение — четыре потока, независимо от того, сколько ядер у виртуалки. На 2 vCPU это гарантированная толкотня: потоки вытесняют друг друга, время растёт вместо того, чтобы падать.
nproc # сколько ядер реально доступно
lscpu | grep -oE 'avx512[a-z]*|avx2|avx|f16c' | sort -u
Ставьте cpu_threads в коде явно, равным числу ядер, а не через OMP_NUM_THREADS — под systemd переменная может не долететь до процесса; проверка: sudo tr '\0' '\n' < /proc/$(systemctl show -p MainPID --value whisper)/environ | grep -i omp.
Набор инструкций. Вторая команда выше — не украшение: быстрые int8-ядра CTranslate2 на x86 опираются на AVX2, и на старом Xeon без него движок откатывается на общие реализации — int8 перестаёт быть быстрее float32. Видите в выводе только avx — упёрлись в поколение процессора, а не в настройки.
Steal time — тихий убийца на дешёвых виртуалках. Гипервизор не даёт машине ядро, когда его занял сосед:
vmstat 1 5
Последний столбец блока cpu — st: устойчивые значения выше 5–10 % во время расшифровки означают очередь за процессором, и никакой cpu_threads этого не исправит. Тот случай, когда «Whisper медленно работает» — диагноз хостингу, а не софту.
Искусственный лимит. Если сервер вроде мощный, а счёт вялый:
systemctl show whisper -p CPUQuotaPerSecUSec -p AllowedCPUs
docker inspect whisper --format '{{.HostConfig.NanoCpus}} {{.HostConfig.CpusetCpus}}'
CPUQuotaPerSecUSec=infinity — лимита нет; любое конечное значение, оставшееся с отладки, режет скорость пропорционально себе.
Почему масштабирование не линейное. Декодер авторегрессивный — токены генерируются по одному, распараллелить нельзя. Хорошо параллелится только энкодер, разбирающий 30-секундное окно целиком: переход с 2 на 4 ядра даёт заметный выигрыш, с 8 на 16 — куда скромнее. num_workers ускоряет не один файл, а параллельные вызовы transcribe(), и каждый воркер ест свою память.
Память и диск: когда «медленно» — это своп и OOM
Отдельный класс случаев: узкое место не в процессоре.
Своп. Модель в int8, интерпретатор с зависимостями и декодированное аудио — если это не помещается в RAM, ядро выгружает страницы на диск, и производительность падает не на проценты, а на порядок.
free -h
vmstat 1 10 # столбцы si/so — страниц влетает/вылетает в своп
Ненулевые si/so во время расшифровки — приговор: добавляйте память или уменьшайте модель. Крутить параметры декодирования на такой системе бессмысленно.
OOM-killer. Если процесс не тормозит, а «зависает и начинает сначала», смотрите не в лог приложения, а в ядро:
sudo dmesg -T | grep -iE 'killed process|oom'
journalctl -u whisper -n 100 --no-pager
Характерные строки — Out of memory: Killed process 2417 (python3) total-vm:8402112kB, anon-rss:3801024kB и следом от systemd whisper.service: A process of this unit has been killed by the OOM killer. Сервис перезапускается, задача считается заново, снаружи это выглядит как бесконечная обработка.
Перезагрузка модели на каждый запрос. Классика самописных обёрток — WhisperModel(...) внутри обработчика: веса читаются с диска при каждом вызове, и на коротких файлах это дольше самой расшифровки. Модель поднимается один раз при старте процесса.
Диск. large-v3 — около трёх гигабайт, плюс промежуточные файлы и входящее аудио. На переполненном диске всё заканчивается не замедлением, а [Errno 28] No space left on device посреди работы; df -h и du -sh ~/.cache/huggingface держите в мониторинге.
Пакетный режим и память. BatchedInferencePipeline ускоряет длинные файлы кратно, но память растёт вместе с batch_size — на 4 ГБ батчи часто кончаются тем же OOM-killer. Инструмент для сервера от 8 ГБ, а не способ выжать слабую виртуалку.
Пайплайн вокруг модели: аудио, VAD, очередь и таймауты
Часть времени тратится до и после модели.
Декодирование аудио. Whisper внутри приводит звук к 16 кГц моно — трёхчасовой стерео-AAC с видеодорожкой перекодируется заново при каждом прогоне. Для повторных запусков конвертируйте один раз:
ffmpeg -i meeting.mp4 -vn -ac 1 -ar 16000 -c:a pcm_s16le meeting.wav
Флаг -vn тут не формальность: без него ffmpeg разбирает и видеопоток.
VAD. vad_filter=True подключает Silero VAD и выкидывает участки без речи — буквально меньше аудио на входе модели. На планёрках и звонках поддержки, где половина хронометража — паузы, это самый большой выигрыш из всех; насколько именно, покажет разница info.duration и info.duration_after_vad. Порог — через vad_parameters=dict(min_silence_duration_ms=500).
Очередь вместо параллелизма. Два одновременных распознавания на четырёх ядрах не ускорят ничего — поделят те же ядра и удвоят память. Правильная схема — один воркер модели и очередь перед ним, клиенту сразу 202 Accepted с идентификатором задачи.
Таймауты, которые выглядят как «зависло». При синхронной обработке вы упрётесь в дефолты, а не в Whisper: Nginx ждёт ответ 60 секунд и отдаёт 504 Gateway Time-out, оставляя в error.log:
upstream timed out (110: Connection timed out) while reading response header from upstream
Gunicorn при синхронных воркерах убивает обработчик через 30 секунд:
[CRITICAL] WORKER TIMEOUT (pid:2417)
[ERROR] Worker (pid:2417) was sent SIGKILL! Perhaps out of memory?
Минимальный набор правок, пока очередь не сделана:
location /transcribe {
proxy_pass http://127.0.0.1:8000;
proxy_read_timeout 1800s;
proxy_send_timeout 1800s;
proxy_request_buffering off;
client_max_body_size 512m;
}
И --timeout 1800 для gunicorn. Но это подпорка: сорокаминутный HTTP-запрос всё равно оборвётся на слабом мобильном соединении. Асинхронная схема с идентификатором задачи — единственное надёжное решение.
Какой сервер под Whisper брать в MAATRIX
Приложение whisper есть в каталоге apps.maatrix.io и разворачивается автоматически при заказе сервера — работает на Ubuntu и Debian, руками ставить не нужно. Адрес сервиса и ключи доступа — в личном кабинете, в разделе «Доступ».
Честно: у Whisper на процессоре нет параметра, который заменит ядра. Настройки из статьи дают кратный выигрыш один раз — переход с float32 на int8, с large-v3 на turbo, с четырёх потоков на двух ядрах к нормальной конфигурации. Дальше — упор в железо.
Честный минимум: 4 vCPU, 8 ГБ RAM, 60 ГБ NVMe. large-v3-turbo в int8 работает с запасом по памяти, помещаются кэш моделей, очередь и Nginx, cpu_threads=4 соответствует реальным ядрам. Ограничение прямо: это конфигурация под поток файлов — планёрки, звонки, — а не под реальное время. Вариант 2 vCPU / 4 ГБ тоже рабочий, но именно с него чаще всего приходят с жалобой «медленно»: там нет места ни под батчи, ни под второй процесс.
Комфортный вариант: 8 vCPU, 16 ГБ RAM, 100–160 ГБ NVMe. Есть смысл в BatchedInferencePipeline с batch_size=8, в двух воркерах на разные очереди и в паре моделей рядом — turbo для потока, large-v3 для спорных записей. Кэш Hugging Face на пару моделей — 10–20 ГБ, отсюда и диск.
Видеокарта — когда счёт идёт на часы аудио в день. Документация даёт ориентир по видеопамяти: около 6 ГБ для turbo и 10 ГБ для large-v3 — карта на 8–12 ГБ закрывает задачу целиком. Выигрыш относительно CPU кратный, точную цифру снимайте своим же скриптом из первого раздела.
Локация — Великобритания, Лондон. Две причины. Веса моделей тянутся с huggingface.co, и с лондонской площадки — напрямую, без прокси и зависаний на скачивании трёх гигабайт. А загрузка аудио — половина времени в глазах пользователя: пинг из Европы и европейской части России до Лондона — десятки миллисекунд, плюс GDPR-соседство снимает половину вопросов о том, куда уезжают записи разговоров. Если по 152-ФЗ материалы обязаны оставаться в России — берите RU: после скачивания весов Whisper работает офлайн, локация на скорость счёта не влияет.
Оплата — картой российского банка, по СБП, криптовалютой или токеном MAAT: зарубежная карта для сервера в Лондоне не нужна.
Развернуть за пару минут
Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть WhisperОбсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Частые вопросы
Почему один файл считается минуту, а другой такой же длины — десять?
Почти всегда откаты по температуре: окна, не прошедшие пороги compression_ratio_threshold=2.4 и log_prob_threshold=-1.0, декодируются заново до шести раз. Прогоните файл с temperature=0 — время выровнялось, причина найдена. Второй кандидат — зацикливание при condition_on_previous_text=True.
Даст ли ускорение переход с 4 ядер на 16?
Меньше, чем хочется. Энкодер параллелится хорошо, а декодер генерирует токены по одному и от числа ядер почти не зависит. Заметный выигрыш даёт переход с 2 на 4–8 ядер; дальше эффективнее вкладываться в batch_size, VAD и размер модели, а при часах аудио в день — сразу в видеокарту.
Стоит ли уходить с large-v3 на small ради скорости?
Сначала попробуйте large-v3-turbo: по README он примерно в восемь раз быстрее large при близком качестве — четыре слоя декодера вместо тридцати двух. Если мало, снижайте beam_size до 1 и только потом берите small: на русском разница между turbo и small уже заметна на слух.
Нужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.