Whisper на GPU: транскрибация дешевле API OpenAI в 20 раз

Считаем стоимость минуты аудио на своём железе против whisper-1, поднимаем Faster-Whisper в Docker с рабочим прокидыванием CUDA и разбираем, что ломается в проде.

YouGPU Team Обновлено: 7 мин

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.0810.098 ₽5.2×
fp16, batch_size=817 с0.0220.027 ₽18.9×
int8, batch_size=816 с0.0210.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)КачествоПрименение
tiny39 M~1 GB< 1 GBНизкоеГолосовые команды
base74 M~1 GB< 1 GBСреднееЧерновики
small244 M~2 GB~1 GBПриемлемоеYouTube субтитры (англ)
medium769 M~5 GB~3 GBХорошееПодкасты, лекции
large-v31550 M~10 GB~6 GBSOTAСложный продакшен

Важно: цифры относятся к загрузке весов. В работе потребление растёт от длины аудио, 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