メインコンテンツへスキップ

Voice TypeがCore MLエンコーダーをオフにした理由

whisper.cppにはApple Silicon向けのCore MLエンコーダー経路が用意されています。Voice Typeはこれを強制的に無効化しています。Core MLエンコーダーは固定の入力シェイプを必要とする一方、ライブ音声入力は長さの異なるバッチを生成するためです。

要点

結論から
  • コンパイル済みのCore MLエンコーダーバンドルは、固定の入力シェイプを前提としています。
  • ライブ音声入力は、長さの異なるバッチを生成します。バッチ化処理が、フレーズを途中で切ることを拒否するためです。
  • そのためVoice Typeは、コンテキスト作成時にCore ML経路を無効化し、可変長のオーディオを受け付けるMetalで動作します。
  • この経路を削除したことで、モデルごとに163 MB〜1.17 GBあったダウンロードもなくなりました。

whisper.cppには、Apple Silicon向けのオプションのCore MLエンコーダー経路があります。Macの音声入力アプリなら当然オンにするはずの機能です。Voice Typeはこれを強制的に無効化し、代わりにMetalバックエンドを使っています。

理由はシェイプの不一致であり、ライブ音声入力がオーディオをバッチ化する方法に由来します。

TL;DR

  • コンパイル済みのCore MLエンコーダーバンドルは、固定の入力シェイプを前提としています。
  • ライブ音声入力は、長さの異なるバッチを生成します。バッチ化処理が、フレーズを途中で切ることを拒否するためです。
  • そのためVoice Typeは、コンテキスト作成時にCore ML経路を無効化し、可変長のオーディオを受け付けるMetalで動作します。
  • この経路を削除したことで、モデルごとに163 MB〜1.17 GBあったダウンロードもなくなりました。

バッチの長さが一定でない理由

Voice Typeは、固定のタイマー間隔で区切ったオーディオを認識器に送るわけではありません。ストリーミング式の音声区間検出(VAD)が発話区間を判定し、バッチ化処理は発話区間をまるごと蓄積します。タイマーが作動したからといって、1つの区間を分割することはありません。

バッチをモデルに送るタイミングを決めるルールは、すべて時計ではなく発話に基づく条件です。

  • ライブバッチの目標は29秒で、15〜29秒の範囲に制限されます。
  • 5秒未満のバッチは、ポーズ(間)によってフラッシュされることはありません。待機し、次のフレーズがそこに加わります。
  • ポーズによるフラッシュには、閉じた発話区間のあとに少なくとも400 msの無音が必要です。
  • 次の区間をまるごと追加すると目標を超える場合は、まず現在のバッチをフラッシュし、その区間をまるごと次のバッチの先頭にします。
  • ホットキーを離したときは、残っているものが長さにかかわらずフラッシュされます。

その結果、バッチの長さは定数ではなく分布になります。バッチは5秒のこともあれば31秒のこともあります。音声入力としては、これが正しい挙動です。そうしなければ、ウィンドウの境界で単語を真っ二つにしてしまうからです。

Core MLだと何がコストになるか

Core MLモデルは、入力の次元があらかじめ組み込まれた形で、.mlmodelcバンドルとして事前にコンパイルされます。コンパイル時と異なる長さのオーディオを渡すと、固定シェイプに合わせてパディングする(無音に対してもエンコーダーの処理コストを払うことになる)か、コンパイル済みのバリアントを複数用意しておく(ディスク上のバンドルが複数になる)か、のどちらかになります。

Voice Typeはどちらも選びません。Core MLエンコーダー経路は、whisperコンテキストを作成する箇所で強制的に無効化されており、コメントにはまさにこの理由、つまり固定シェイプの入力という制約が明記されています。そのうえでバックエンドのポリシーは、デフォルトでMetalを選択し、CPUにフォールバックするのは、シミュレーター上、ユーザーデフォルトでGPUが無効にされている場合、またはクラッシュ後にセーフモードがラッチされた場合に限ります。

Metalは、バッチ化処理が生成するどんな長さでも受け付けます。

削除によって解放されたもの

Core MLのサポートは、単に無効化されただけでなく、削除されました。以前のビルドでは、各モデルの重みの隣にエンコーダーバンドルggml-<name>-encoder.mlmodelcをインストールしており、モデルによって約163 MBから1.17 GBに及びました。これらのバンドルは、モデルごとにダウンロード、解凍、配置されていました。

もはやそれらを削除するコードパスは存在しないため、アプリにはワンタイムのマイグレーションが組み込まれており、アップグレード時に、孤立したエンコーダーバンドルをモデルディレクトリから一掃します。その際、隣にある.binの重みには決して触れないよう注意しています。

つまり、Core MLをオフにしたことでユーザーに見える結果は、ダウンロードサイズとモデルディレクトリが小さくなったことです。そのうえ、認識器は文の長さを気にしません。

Voice Typeが各ステージをどこで実行するかを示した図。オーディオキャプチャとSilero音声区間検出器はCPUで動作します。whisper.cppの認識器はMetal GPUで動作し、メインの経路として描かれています。3つ目の分岐であるApple Neural Engine上のCore MLは破線で描かれ、無効化されています。理由として、コンパイル済みのCore MLエンコーダーは固定の入力シェイプを必要とするが、ライブ音声入力は長さの異なるバッチを生成する、と記されています。セーフモードでは、認識器をCPUに戻すフォールバックの矢印が示されています。

破線の分岐は、アプリが意図的に通らない経路です。出典:whisper.cpp.swift/LibWhisper.swift。

Metalが実際に実行していること

MetalはAppleの低レベルGPU APIで、このアプリでは2つの別々の役割を担っています。

whisperコンテキストはMetalバックエンドで動作するため、エンコーダーとデコーダーはGPU上で実行されます。これとは別に、バッチごとのオーディオ調整が、モデルが何かを受け取る前の独立した信号処理ステージとして実行されます。DCオフセットの除去、フィルターとラウドネス制御、そしてモデルが想定する16 kHzへのリサンプリングです。

どちらもユニファイドメモリから読み込むため、キャプチャ経路とモデルのあいだで、オーディオバッファがバスを越えてコピーされることはありません。

主張していないこと

ここでは、確定処理(ファイナライズ)にかかる時間は公表していません。その数値と一緒にお渡しできるベンチマークスクリプトが今のところなく、再現する手段のないレイテンシの数字はマーケティングにすぎないからです。

ベンチマークがなくても構造的に言えて、真実であるのは、ホットキーを離した時点で残っている作業量は、あなたが話した長さではなく、バッチの目標によって上限が決まるということです。2分の音声入力と30分の音声入力は、仕上げに残る作業量が同じ状態で終わります。

ご自分のマシンで計測するには、同じ文を10回音声入力し、ホットキーを離してからテキストが入力されるまでの間隔を数えてください。重要なのはその数値であり、それはあなた自身のものです。

トレードオフ

モデルサイズと精度のトレードオフがあります。小さいモデルほどファイナライズは速くなりますが、聞き逃す単語は増えます。Voice Typeは27 MBから550 MBまで複数のサイズを提供しているので、このトレードオフはご自身で決められます。

大きいモデルは、ユニファイドメモリに余裕のあるApple Silicon Macに向いています。小さいモデルは8 GBのマシンに向いています。8 GBのマシンでは、大きいモデルはデコードよりもページングに時間を費やしてしまうからです。

関連記事:テクノロジーの概要 · 音声区間検出 · レイテンシのデモ