Whisper на GPU: транскрибация дешевле API OpenAI в 20 раз
Считаем стоимость минуты аудио на своём железе против whisper-1, поднимаем Faster-Whisper в Docker с рабочим прокидыванием CUDA и разбираем, что ломается в проде.
TL;DR — Кратко:
- Минута аудио на арендованной A6000 обходится примерно в 0.025 ₽ против 0.51 ₽ у whisper-1 — разница около 20 раз, расчёт в первом разделе.
- Faster-Whisper на CTranslate2 даёт до 4 раз меньше времени инференса, чем ванильный openai/whisper, при том же beam_size.
- Главная ошибка сборки: образ python:slim не содержит cuBLAS и cuDNN, поэтому device=cuda падает при загрузке модели.
Минута аудио через API OpenAI стоит $0.006. Пока это десяток тестовых файлов, сумма незаметна. На архиве колл-центра за год или на постоянном потоке видео это уже статья расходов, которую приходится планировать.
Вторая причина уйти с API — данные. Записи разговоров с клиентами, внутренние встречи, медицинские приёмы уезжают на сторонний сервер, и этот вопрос обычно решается не инженерами.
Альтернатива — поднять Whisper у себя. Ниже: сколько это стоит в рублях, почему ванильная версия от OpenAI медленнее и как собрать сервис, который действительно запустится на видеокарте.
Считаем деньги: 0.025 ₽ против 0.51 ₽ за минуту
Начнём с цифры, ради которой всё затевается.
Сторона OpenAI. Модель whisper-1 в прайсе OpenAI стоит $0.006 за минуту аудио. По курсу ЦБ РФ на 18 августа 2026 года (85.01 ₽ за доллар) это 0.51 ₽ за минуту.
Сторона своего сервера. Здесь нужны две величины: цена часа аренды и RTF — Real Time Factor, доля реального времени, которую занимает обработка. RTF 0.02 означает, что минута аудио обрабатывается за 1.2 секунды.
Цену берём из нашего каталога: RTX A6000 на 48 ГБ начинается от 73 ₽/час, то есть 0.0203 ₽ за секунду GPU. RTF берём из бенчмарка в README faster-whisper — там 13 минут аудио прогнали на разных режимах.
| Режим (данные README faster-whisper) | Время на 13 мин | RTF | ₽ за минуту аудио | Против whisper-1 |
|---|---|---|---|---|
| fp16, beam_size=5, без батчинга | 1 мин 03 с | 0.081 | 0.098 ₽ | 5.2× |
fp16, batch_size=8 | 17 с | 0.022 | 0.027 ₽ | 18.9× |
int8, batch_size=8 | 16 с | 0.021 | 0.025 ₽ | 20.4× |
Отсюда и цифра в заголовке. Батчинг здесь не украшение, а половина экономии: без него разница схлопывается с 20 раз до 5.
Две оговорки, без которых таблица врёт. Первая: бенчмарк README снят на модели large-v2 и потребительской RTX 3070 Ti 8GB под CUDA 12.4, а не на A6000 с large-v3. Модели сопоставимы по размеру (1550M параметров обе), A6000 в целом быстрее 3070 Ti, так что 20 раз — это консервативная оценка снизу, а не потолок. Вторая: расчёт считает GPU занятой полезной работой на 100%. В реальности между запросами карта простаивает, и если поток аудио неровный, экономика ухудшается — на маленьких объёмах API остаётся выгоднее, потому что вы не платите за простой.
Железо: сколько VRAM нужно Whisper
CPU для инференса не подходит. На процессоре, даже мощном, large-v3 даёт RTF больше единицы: часовое аудио обрабатывается дольше часа.
Объём VRAM диктует, какую версию модели вы вообще загрузите.
| Модель | Параметры | VRAM (FP16) | VRAM (INT8) | Качество | Применение |
|---|---|---|---|---|---|
| tiny | 39 M | ~1 GB | < 1 GB | Низкое | Голосовые команды |
| base | 74 M | ~1 GB | < 1 GB | Среднее | Черновики |
| small | 244 M | ~2 GB | ~1 GB | Приемлемое | YouTube субтитры (англ) |
| medium | 769 M | ~5 GB | ~3 GB | Хорошее | Подкасты, лекции |
| large-v3 | 1550 M | ~10 GB | ~6 GB | SOTA | Сложный продакшен |
Важно: цифры относятся к загрузке весов. В работе потребление растёт от длины аудио,
beam_sizeи размера батча.
Для русского языка со сленгом и шумами берите только large-v3. RTX 3060 на 12 ГБ проходит впритык. Для параллельной обработки нужны карты уровня A5000/A6000, где 48 ГБ позволяют держать несколько воркеров одновременно. Если хотите разобраться, куда именно уходит видеопамять и почему её всегда мало, у нас есть отдельный разбор по VRAM, а сравнение конкретных карт — в статье про A100 против RTX 4090.
Почему faster-whisper, а не openai/whisper
Официальный репозиторий openai/whisper написан на PyTorch. Он точный, но не оптимизирован под нагрузку.
Стандартом де-факто для продакшена стал Faster-Whisper. Под капотом — CTranslate2, движок инференса для Transformer-моделей на C++. README проекта заявляет до 4 раз меньше времени при той же точности и меньшем расходе памяти; таблица выше это подтверждает: 1 мин 03 с против 2 мин 23 с у ванильной версии на одинаковом beam_size=5.
Плюс квантование: int8 режет память примерно вдвое (2926 МБ против 4525 МБ в том же бенчмарке) и слегка ускоряет обработку.
Готовим окружение: Docker и NVIDIA Container Toolkit
Ставить зависимости Python прямо на хост неудобно: версии CUDA и Python начинают конфликтовать с остальными задачами на машине. Пакуем всё в Docker.
Понадобятся Linux (Ubuntu 20.04/22.04), драйверы NVIDIA (проверяются через nvidia-smi) и NVIDIA Container Toolkit — без него Docker не увидит видеокарту. Установка по официальной инструкции NVIDIA:
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
&& curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list |
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' |
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker Пишем сервис на FastAPI
Микросервис принимает файл и отдаёт JSON с текстом.
Структура проекта
whisper-service/
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── app/
└── main.py requirements.txt
fastapi
uvicorn[standard]
python-multipart
faster-whisper main.py
Модель инициализируется один раз при старте, а не на каждый запрос — это критично для производительности.
import os
import tempfile
from fastapi import FastAPI, File, HTTPException, UploadFile
from fastapi.concurrency import run_in_threadpool
from faster_whisper import WhisperModel
MODEL_SIZE = os.getenv("MODEL_SIZE", "large-v3")
DEVICE = os.getenv("DEVICE", "cuda")
COMPUTE_TYPE = os.getenv("COMPUTE_TYPE", "float16") # int8_float16 — экономия VRAM
app = FastAPI(title="Whisper GPU Service")
model = WhisperModel(MODEL_SIZE, device=DEVICE, compute_type=COMPUTE_TYPE)
def transcribe_sync(path: str, language: str | None):
segments, info = model.transcribe(
path,
beam_size=5,
language=language,
vad_filter=True,
)
# segments — генератор: транскрибация идёт в момент итерации, а не при вызове
return info, [
{"start": s.start, "end": s.end, "text": s.text} for s in segments
]
@app.post("/transcribe")
async def transcribe(file: UploadFile = File(...), language: str | None = None):
# mkstemp даёт случайное имя в tmpdir: имя из запроса в путь не попадает
fd, tmp_path = tempfile.mkstemp()
try:
with os.fdopen(fd, "wb") as buffer:
while chunk := await file.read(1024 * 1024):
buffer.write(chunk)
# transcribe блокирующий, поэтому уводим его из event loop
info, segments = await run_in_threadpool(transcribe_sync, tmp_path, language)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
finally:
os.unlink(tmp_path)
return {
"language": info.language,
"language_probability": info.language_probability,
"duration": info.duration,
"text": " ".join(s["text"].strip() for s in segments),
"segments": segments,
} Три вещи здесь неочевидны и стоят отдельного слова.
tempfile.mkstemp вместо имени из запроса. Если писать файл как temp_{file.filename}, имя приходит от клиента: и путь можно увести за пределы каталога, и два параллельных запроса с одинаковым именем затрут друг друга. Расширение не нужно — PyAV внутри faster-whisper определяет контейнер по содержимому.
run_in_threadpool вокруг model.transcribe. Метод синхронный и держит поток целиком. Вызванный напрямую внутри async def, он блокирует event loop, и сервис перестаёт принимать запросы, пока считает текущий. Никакой параллельности при этом не будет, сколько бы воркеров вы ни подняли.
language=None по умолчанию, но передавать язык явно стоит почти всегда — почему, в разделе про отказы в проде.
Dockerfile
Здесь прячется самая частая ошибка. CTranslate2 не тащит CUDA-библиотеки за собой: в зависимостях faster-whisper есть ctranslate2, onnxruntime, av, tokenizers, но нет ни nvidia-cublas-cu12, ни nvidia-cudnn-cu12. README прямо требует cuBLAS для CUDA 12 и cuDNN 9. На голом python:3.10-slim конструктор WhisperModel(device="cuda") упадёт на ненайденной libcublas.
Поэтому база — официальный образ NVIDIA, который README и рекомендует:
FROM nvidia/cuda:12.3.2-cudnn9-runtime-ubuntu22.04
RUN apt-get update && apt-get install -y --no-install-recommends
python3 python3-pip ffmpeg
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt
COPY app /app
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] Альтернатива, если базовый образ менять не хочется: поставить библиотеки через pip (pip install nvidia-cublas-cu12 nvidia-cudnn-cu12==9.*) и выставить LD_LIBRARY_PATH на их каталоги до запуска Python. Способ рабочий, но образ NVIDIA надёжнее и не требует возни с путями.
docker-compose.yml
Ключевая часть — проброс GPU.
services:
whisper:
build: .
ports:
- "8000:8000"
environment:
MODEL_SIZE: large-v3
COMPUTE_TYPE: float16
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
volumes:
- ./cache:/root/.cache/huggingface Том с кешем обязателен: без него large-v3 будет качаться с Hugging Face при каждом пересоздании контейнера. Ключ version из файла убран намеренно — Compose V2 его игнорирует и предупреждает об устаревании.
Батчинг: где берётся оставшаяся экономия
Код выше обрабатывает файлы по одному. Именно этот режим в расчёте стоимости дал только 5 раз против API — почти вчетверо хуже, чем мог бы.
За пропускную способность в faster-whisper отвечает BatchedInferencePipeline. Это отдельная обёртка, а не аргумент transcribe:
from faster_whisper import BatchedInferencePipeline, WhisperModel
model = WhisperModel("large-v3", device="cuda", compute_type="int8_float16")
batched = BatchedInferencePipeline(model=model)
segments, info = batched.transcribe("audio.mp3", batch_size=8, language="ru") BatchedInferencePipeline.transcribe — drop-in замена WhisperModel.transcribe, так что в сервисе меняется одна строка. VAD в батчевом режиме включён по умолчанию.
Платить за это придётся памятью: в бенчмарке README fp16 без батчинга занимал 4525 МБ, с batch_size=8 — уже 6090 МБ. На 48 ГБ A6000 запас есть, на 12 ГБ RTX 3060 с large-v3 батч придётся держать маленьким.
Запуск и проверка
Сборка и старт:
docker compose up --build -d Первый запуск долгий: тянется образ NVIDIA почти на 2 ГБ, потом веса large-v3. Смотрим логи и убеждаемся, что модель загрузилась и CUDA видна:
docker compose logs -f Пробный запрос:
curl -X POST -F "file=@test_audio.mp3" "http://localhost:8000/transcribe?language=ru" Что ломается в проде
Галлюцинации в тишине
На длинных паузах и фоновом шуме Whisper начинает выдумывать: «Спасибо за просмотр», «Субтитры создавал…» или зацикливает одну фразу.
Лечится VAD — отсечением тишины до подачи в модель. В коде выше vad_filter=True уже стоит, по умолчанию он режет паузы длиннее двух секунд. Для плотной речи порог стоит опустить:
model.transcribe(
path,
vad_filter=True,
vad_parameters=dict(min_silence_duration_ms=500),
) CUDA Out Of Memory
Вылезает на длинных файлах, большом beam_size или слишком крупном батче.
Первым делом переключите compute_type на int8_float16 или int8: по бенчмарку README это экономит около 35% VRAM при сопоставимом качестве. Дальше — уменьшайте batch_size, если используете BatchedInferencePipeline. Подробный разбор того, как читать OOM и куда смотреть в первую очередь, есть в гайде по запуску DeepSeek-R1.
Неверное определение языка
Автодетект иногда решает, что русский — это болгарский, и выдаёт абракадабру. Если язык исходника известен заранее, передавайте language="ru" явно. Проверяйте language_probability в ответе: значения ниже 0.5 — повод отправить файл на ручную проверку.
Что в итоге
Свой Whisper поднимается за пару часов вместе с настройкой Docker. Взамен — контроль над данными, отсутствие лимитов по запросам и экономика, которая на объёмах отличается от API на порядок.
Но экономика работает только при загруженной карте. Пока поток аудио редкий и неровный, API дешевле: вы не платите за простой GPU. Точка перелома считается просто — возьмите свой месячный объём в минутах, умножьте на 0.51 ₽ и сравните со стоимостью аренды за месяц. Если своё железо выигрывает, следующий вопрос — покупать или арендовать, и на него мы отвечали отдельным расчётом TCO.
Для экспериментов хватит домашней игровой карты. Для SaaS с мгновенным ответом или архивов на терабайты смотрите на профессиональные карты с большой памятью — там, где помещаются несколько воркеров сразу.
Не хватает VRAM для Whisper Large-v3?
Арендуйте RTX A6000 на 48 ГБ от 73 ₽/час и обрабатывайте аудио в десятки раз быстрее реального времени. Драйверы и Docker уже на борту.
Запустить Whisper на GPU