🎙️

Cloud Run の GPU で faster-whisper を動かす

に公開

Cloud Run の GPU で faster-whisper を動かしてみたら思ったより大変だったので残しておきます。ちなみに、GPU なしでは CPU数を8、メモリを16GiBにしても1時間の音声ファイルを文字起こしするのに1時間半〜2時間ほどかかりました。

https://github.com/SYSTRAN/faster-whisper

Cloud Run の設定

Cloud Run での設定方法は公式ドキュメントに詳しく書かれているので割愛します。

https://cloud.google.com/run/docs/configuring/services/gpu?hl=ja

Dockerfile

執筆時点では CUDA 12.2 がプリインストールされているようなのですが、この状態から faster-whisper が動くようにビルドするのに苦労しました。試行錯誤の過程は省略しますが、cuBLAScuDNN を追加でインストールする必要がありました。また、途中でダウンロードしている Debian のパッケージファイルが大きいので最後に削除してイメージサイズを減らします(削除したら 13.8GB -> 2.1GB になりました!)。

なお、大きい音声ファイルでも大丈夫なように Hypercorn を使って HTTP/2 に対応しました(HTTP/1 ではリクエスト・レスポンスともに 32MiB が上限)。

FROM python:3.12-slim-bookworm

# uvをインストール
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv

# パッケージをインストール
COPY app /app
WORKDIR /app
COPY requirements.txt .
RUN uv venv --python=python3.12 && \
  uv pip install --no-cache-dir -r requirements.txt

# CUDA まわりで足りないものをインストール
RUN apt update && \
  apt install -y --no-install-recommends  wget && \

  ## cuBLAS
  apt update && \
  apt install -y --no-install-recommends  wget && \
  wget -q https://developer.download.nvidia.com/compute/cuda/12.9.0/local_installers/cuda-repo-debian12-12-9-local_12.9.0-575.51.03-1_amd64.deb && \
  dpkg -i cuda-repo-debian12-12-9-local_12.9.0-575.51.03-1_amd64.deb && \
  cp /var/cuda-repo-debian12-12-9-local/cuda-*-keyring.gpg /usr/share/keyrings/ && \
  apt update && \
  apt install -y --no-install-recommends libcublas-12-9 && \

  ## cuDNN
  wget -q https://developer.download.nvidia.com/compute/cudnn/9.10.1/local_installers/cudnn-local-repo-debian12-9.10.1_1.0-1_amd64.deb && \
  dpkg -i cudnn-local-repo-debian12-9.10.1_1.0-1_amd64.deb && \
  cp /var/cudnn-local-repo-debian12-9.10.1/cudnn-*-keyring.gpg /usr/share/keyrings/ && \
  apt update && \
  apt install -y --no-install-recommends cudnn-cuda-12 && \

  ## 不要なファイルを削除
  rm -f /usr/share/keyrings/*.gpg  && \
  rm -f /var/cuda-repo-debian12-12-9-local/cuda-*-keyring.gpg && \
  rm -f /var/cudnn-local-repo-debian12-9.10.1/cudnn-*-keyring.gpg && \
  rm -f cuda-repo-debian12-12-9-local_12.9.0-575.51.03-1_amd64.deb && \
  rm -f cudnn-local-repo-debian12-9.10.1_1.0-1_amd64.deb && \
  apt purge -y --auto-remove cuda-repo-debian12-12-9-local cudnn-local-repo-debian12-9.10.1 && \
  apt purge -y --auto-remove wget && \
  apt clean

EXPOSE 8080

# 起動
CMD ["uv", "run", "hypercorn", "main:app", "--bind", "0.0.0.0:8080", "--access-logfile", "-", "--error-logfile","-"]

なお、 CUDA 12.2 以外の環境では NVIDIA の公式サイトから対応するバージョンのパッケージファイルを探せばよさそうです。

https://developer.nvidia.com/cudnn-downloads
https://developer.nvidia.com/cuda-downloads

main.py

API 本体はお好きなように実装してください。

app/main.py
from dataclasses import asdict
from fastapi import FastAPI, UploadFile, HTTPException
from faster_whisper import WhisperModel
import json
import logging
import os
from shutil import copyfileobj
from tempfile import NamedTemporaryFile

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
faster_whisper_logger = logging.getLogger("faster_whisper").setLevel(logging.DEBUG)

app = FastAPI()

# モデルを初期化
model_name = "large-v3-turbo"
device = "cuda"
logger.info(f"モデルをロード中: {model_name} ({device})")
model = WhisperModel(model_name, device=device)
logger.info("ロード完了")

@app.post("/transcribe")
def transcribe_audio(upload_file: UploadFile):
    temp_file_path = None
    try:
        logger.info("一時ファイルに保存")
        with NamedTemporaryFile(delete=False, suffix=f"-{upload_file.filename}") as temp_file:
            temp_file_path = temp_file.name
            copyfileobj(upload_file.file, temp_file)

        logger.info(f"文字起こし開始: {temp_file_path}")
        segments_generator, info_obj = model.transcribe(temp_file_path)
        segments = list(segments_generator)
        logger.info(f"文字起こし完了")
        return {
            "info": asdict(info_obj),
            "segments": segments,
        }
        logger.info("処理完了")
    except Exception as e:
        logger.error(f"エラー発生: {e}")
        raise HTTPException(status_code=500, detail=str(e))
    finally:
        if temp_file_path and os.path.exists(temp_file_path):
            os.remove(temp_file_path)
            logger.info(f"一時ファイルを削除完了: {temp_file_path}")

Build & Run

ローカルでのコマンドですが、Cloud Run でもちゃんとビルドできました。

docker build -t transcription_api .
docker run -p 8080:8080 --rm transcription_api

サンプルリクエスト

こちらから拝借したサンプル音声を投げてみます。

api_endpoint="http://localhost:8080/transcribe"
curl -X POST \
    --form "upload_file=@data/sample_audio.mp3" \
    $api_endpoint | jq > result.json

ちゃんと動いてそうです。35 秒の音声ファイルを 1 秒くらいで文字起こししてくれました。90 分のファイルでも 3 分ほどでできました。

result.json(一部省略)
{
  "info": {
    "language": "ja",
    "language_probability": 0.9990234375,
    "duration": 35.172,
    "duration_after_vad": 35.172,
    "all_language_probs": [...],
    "transcription_options": {
      "beam_size": 5,
      ...
      "hotwords": null
    },
    "vad_options": null
  },
  "segments": [
    {
      "id": 1,
      "seek": 0,
      "start": 0.3,
      "end": 6.04,
      "text": "パラ言語情報ということなんですが、簡単に最初に復習をしておきたいと思います。",
      "tokens": [...],
      "avg_logprob": -0.056525735622819734,
      "compression_ratio": 1.7802197802197801,
      "no_speech_prob": 0.055755615234375,
      "words": null,
      "temperature": 0.0
    },
    {
      "id": 2,
      "seek": 0,
      "start": 6.92,
      "end": 13.88,
      "text": "こうやって話しておりますと、それはもちろん言語的情報を伝えるということが一つの重要な目的なんでありますが、",
      "tokens": [...],
      "avg_logprob": -0.056525735622819734,
      "compression_ratio": 1.7802197802197801,
      "no_speech_prob": 0.055755615234375,
      "words": null,
      "temperature": 0.0
    },
    {
      "id": 3,
      "seek": 0,
      "start": 14.040000000000001,
      "end": 18.12,
      "text": "同時にパラ言語情報、そして非言語情報が伝わっております。",
      "tokens": [...],
      "avg_logprob": -0.056525735622819734,
      "compression_ratio": 1.7802197802197801,
      "no_speech_prob": 0.055755615234375,
      "words": null,
      "temperature": 0.0
    },
    {
      "id": 4,
      "seek": 0,
      "start": 18.240000000000002,
      "end": 25.46,
      "text": "この三文法は藤崎先生によるものでして、パラ言語情報というのは、要は意図的に制御できる。",
      "tokens": [...],
      "avg_logprob": -0.056525735622819734,
      "compression_ratio": 1.7802197802197801,
      "no_speech_prob": 0.055755615234375,
      "words": null,
      "temperature": 0.0
    },
    {
      "id": 5,
      "seek": 2546,
      "start": 25.46,
      "end": 30.92,
      "text": "和社がちゃんとコントロールして出しているんだけども、言語情報と違って連続的に変化する。",
      "tokens": [...],
      "avg_logprob": -0.09018049581811347,
      "compression_ratio": 1.3518518518518519,
      "no_speech_prob": 0.000060617923736572266,
      "words": null,
      "temperature": 0.0
    },
    {
      "id": 6,
      "seek": 2546,
      "start": 31.3,
      "end": 34.64,
      "text": "カテゴライズすることがやや難しい、そういった情報であります。",
      "tokens": [...],
      "avg_logprob": -0.09018049581811347,
      "compression_ratio": 1.3518518518518519,
      "no_speech_prob": 0.000060617923736572266,
      "words": null,
      "temperature": 0.0
    }
  ]
}

Discussion