この記事の概要を簡単まとめ!
- “YukariWhisper”はv0.0.5にアップデートされた
- 音声認識時の異常な結果はタイムアウト実装で暫定対応した
- 詳細な解析で原因はWhisper自体にあり、正確なタイムアウト実装は不可能と判断
- 試したいこと:YukariWhisperをサブGPUに対応させる
- x4モードでの動作は結果にどれくらい影響するかを確認
- 結果:同時処理数が多い場合明らかに影響する
- 考察:同時処理数を絞る、X299(Skylake-X)を使う
- x4接続にやらせるなら用途は限定する必要がある
私は凝り性というものであろう。ある難問にぶち当たったとき、その問題を絶対に解決したいと考え、そのためにあらゆる資料や先駆者の情報を探して参考にし、自分で試していく。それが失敗するのなら別の方法を次々に試し、最終的な答えが出ない時は一旦間をおいて解析を続ける。そのために他のことを放置することが多々あるので直さないといけない部分でもあるが。
その1つであり、現在最もやりたいことでもあるYukariWhisper。これの異常な音声認識対応は、残念ながら仕様の壁にぶち当たったので解決できそうになかった。そこで次に考えたのが、サブGPUを使用した負荷分散。しかし試していると、ここにも仕様の壁で問題が発生した。今回はそのレポートを書き、注意喚起も兼ねることにした。
ひとっ飛びできる目次
本気で使うなら石油王になるしかない
YukariWhisperを快適に使うために
v0.0.5へアップデート
TYAPA氏の力作とも言えるYukariWhisperは、公開後はゆかコネ開発者であるNao氏からポート追従のプルリクがあり、まずこれが実装された。その後一部GPUで動作しない問題を、デフォルトの量子化をint8_float16
とし、GPUのCUDA CCのバージョンを参照することで適切に動作可能になったことで解消され、これがv0.0.3となった。v0.0.4は新規インストールの失敗について修正を行ったものであり、これは既に導入済みのユーザはアップデートしなくていいものになっている。
この時点で既に十分な完成度を誇っているYukariWhisperだが、以前から発生しているある問題に、ユーザはもちろんTYAPA氏も悩まされていた。ブレスや環境音のノイズ等を音声として認識してしまい、それによって音声認識が詰まること、異常な結果を出力することが以前から発生している。このためリアルタイム性と正確性が失われるタイミングが多々あり、これで使用を躊躇している人もいる。
この問題の解決のため、Pythonスクリプトの中身を確認し、その改造を行うことにしたのが前回記事である。以前からも改造はしていたが、その改造は全体的な利便性向上を狙ったものである。デバッグの基本戦法であるprintを使った調査の結果、最も時間がかかっていた箇所はNGワード判定あたりであると確認できた。実際はfor文の箇所ではあったのだが、異常な音声認識結果は時間がかかるという点に注目して、指定時間以上はタイムアウトで結果を破棄するようにした。これにより異常な音声認識の「結果」は破棄できるようにはなった。また、これに似た機能がYukariWhisperにプルリクされていたようで、これがマージされて現在v0.0.5となっている。そのため現在は私が書いたのと似たものが既に公式化していることになる。
詳細解析で分かったこと
しかしこの実装は小手先の騙しに過ぎなかった。というのも、確かに「結果」だけならタイムアウト実装によって破棄できるのだが、音声認識が詰まることについては解決には至らなかった。だが調査当初はWhisper本体は中身が分からず、弄るのも危険なのでYukariWhisper部分だけ注目して調査しており、そのせいで実際に時間がかかっている部分にタイムアウトを実装することができなかったのである。その後の調査では、以下のコードをまず確認した。
1 2 3 4 5 6 7 8 9 10 |
# 上のコードから if self.vad.is_speech(speech_array): #whisperで認識 segments = self.model_wrapper.transcribe(speech_array) for segment in segments: # 時間が指定秒以上かかる場合は結果を破棄して次の音声認識結果に移行する if round((time.time()-start_t), 1) >= self.recognizers.recognition_timeout: print("音声認識がタイムアウトしました。") continue # 下に続く |
segments = self.model_wrapper.transcribe(speech_array)
の後すぐにfor segment in segments:
を実行しており、ここでまず疑問が浮かんだ。何故リストでもなさそうなところでfor文を実行できるのかと思い、faster-whisperについて調査した。するとこのサイトでsegmentはジェネレータ1)簡潔に説明すると「通常の関数の、return文をyieldに置き換えたもの」であるという。詳しい説明はここやここが詳しい。であるという解説を得ることができた。そのためfor文での実行やリストに集約することで書き起こしされるということになる。そのためsegmentsに対してprintを実行するとよくわからない結果を表示したがこれがジェネレータであり、そしてこれの実行時の処理に時間がかかるということになるようだ。
segmentsは元を辿るとwhisper_utils.pyのtranscribeを呼び出しているが、その大元はfaster-whisperのWhisperModel.transcribeを呼び出して実行していることになる。その中身を調査してみたものの、それは完成品のスクリプトであり、手を加えることはできないものであった。おそらくオーバーライドを用いればうまく実装できるのかもしれないが、そこまでできるほどの技術はないため手を出すことはなかった。
スポンサーリンク
スポンサーリンク
何度か試すも全て失敗
私の推測では、このジェネレータでノイズを含んだ音声認識結果の処理を行うときに異常に時間がかかるということで、この処理に対してタイムアウトを実装すればいけるのではないかと考えた。その際に使われるのがスレッドで、threading.Threadやcomcurrent.futureでタイムアウト時間を指定できるということで試した。しかしどういうわけか、スレッドで実行した場合、必ず強制終了させられてしまいエラー内容も出ないため、修正が難航を極めた。どんなに修正しても必ず強制終了し、回避方法も全く無いため修正および実装は不可能と判断、諦めることにした。
停止する理由について、transcribeからジェネレータを受け取り、それを実行している最中に強制終了することによって何らかの問題が発生しているとみているが、スレッドで発生したエラーは親スレッド(mainなど)で受け取れないので、どうして止まるのかが分からないのである。エラーの詳細が分かれば対象方法があるのだが、そのエラーをどうやって表示すればいいかが分からない以上先に進むことができない。解決しないことに時間を使うよりは、他のことに時間を使うべきだ。
サブGPUとx4モード
YukariWhisperを使っている時に気になるのがGPUへの負荷である。GPUの性能と世代によって負荷が変わるこれは、Turing(所謂RTX2000番台)の最上位である2080Tiをもってしても、ゲームをはじめとする様々な「GPUを使う処理」の負荷を下げなければならないほど使うことが難しいものになっている。そこで私が安直に考えたのが、YukariWhisperやエンコードのみを担当させるための「サブGPU」である。このことは2GPU構成に関する注意を書いた記事で解説している。
しかしこの際に仕様の壁に当たった。PCIeの速度である。通常GPUはCPUのPCIeを使用し、その場合はCPU側のスロットに接続する。これによってGPUはx16で動作し、最大速度で利用できる。それ以外のPCIeスロットはチップセット側のPCIeリビジョンと速度に従うものとなる。そして大抵のチップセットは利用できる速度はx1, x2, x4のみである。したがってサブGPUはx4で動作することになる。つまりx16幅は必然的にx4モードで動作させられることになってしまうのである。
なお、私の調べではIntelはZ系チップセット、AMDはX系やB系チップセットでPCIeレーン数を2×8に分割できるようになっている。ただし、マザーボードによっては分割対応していないことがあるため、マザーボードの仕様は必ず確認する必要がある。
用途を限定することによる遅延の減少
PCIe 3.0でx16前提のGPUがx4で動作した場合についての影響を調べたPC Watchの記事では、ゲーム用途の場合タイトルによって1割強の差が出るとの結果である。そのためサブGPUとして使用する際は用途を限定しないと、レーン数が少ないことによるボトルネックが発生することになる。いくらGPUが並列処理に強くとも、データ通信の経路が少なければ処理したデータをCPUにすぐ送れない。
ならば、実際に用途を限定して運用すればボトルネックを防ぐことができるはず、という結論に至る。そもそも1650Sを導入した目的はYukariWhisperを担当させて、2080Tiの負荷を軽減させることを目的としている。一方で1650SはHEVCとBフレームサポートであるので、エンコードも担当することができる。それも狙って導入しているので、それらと同時に実行したときの負荷も計測し、「最も最適な負荷分散」を探ることとする。
スポンサーリンク
スポンサーリンク
YukariWhisperもx4モードの影響を受けるか
実験環境の設定
実験を行うにあたって、結果に影響を与えるパーツと設定について確認する。
- Mainboard: Asrock H370 Pro4(PCIe2: 3.0×16→RTX 2080 Ti(Turing, 11GB), PCIe4: 3.0×4→GTX 1650 Super(Turing, 4GB))
- CPU: i7-9700(8C/8T, Base: 3.00GHz, Turbo: 4.70GHz ※常時ターボブースト切で使用)
- RAM: CFD/PANRAM DDR4-2666 2x8GB + Corsair DDR4-2666 2x6GB (総計32GB)
- 音声認識経路:マイク(USB接続)→DAWでノイズフィルタ適用→仮想OIF(VB-CABLE OUTPUT)→YukariWhisper
- 同時使用設定:YukariWhisperのみ担当・映像出力と同時に行う・エンコードと同時に行う・全て同時に行うといった、同時処理の数で負荷が変動するかを確認する。ただし、実際に配信することはせず、録画の負荷から配信も同時に行った場合を推定する。
エンコード形式は両方ともHEVC・Bフレームサポートであるため、同じ条件で可能である。その場合GPUのモデル差についてはここでは無視できるものとして扱う。また、実際の環境に近付けるため、配信で使用しているソフトウェアは全て起動している状態で検証する。
なお、目的はx4接続でYukariWhisperに対する影響を調べることであるため、YukariWhisperは常にサブGPUの1650Sを対象に実行する。検証するゲームは基本負荷が非常に高いACVIをベースに、次の4パターンの組み合わせで実施する。
- YukariWhisperのみ実行(映像出力端子は一切使用しない)
- HEVCエンコード+YukariWhisper(映像出力端子は一切使用しない)
- 映像出力(DisplayPortをサブモニタに接続)+YukariWhisper
- HEVCエンコード+映像出力(DisplayPortをサブモニタに接続)+YukariWhisper
取得するデータについて、今回はYukariWhisperへの影響を調べるものであるので、OBS側のデータは不要である。したがって、GPU-Zのログのみ記録し、その結果のうち影響が目に見えて分かるものを選択してグラフ化する。
実験結果(動画)
動画は以下の通りである。なお、HEVCはそのままアップロードできないため、容量削減を兼ねて再エンコードしている。この時に動画のサイズとFPS値は無視できるものとする。
パターン1
パターン2
パターン3
パターン4
各実験データのグラフ化
各パターンについてグラフ化したものは、1つの画像としてまとめて掲載する。
結果考察
ACVIでも最も楽に終わるミッションが「テスターAC撃破」であり、これを配信でやっているという想定での実行である。1650SにYukariWhisperだけを実行させた場合を基準としてそれぞれを比較すると、同時に処理するものが多い程VRAM消費量が多くなっており、YukariWhisperと同時に他の何か1つ以上行わせるとクロックがアフターバーナーでセットした最大値まで張り付くことが分かる。これは接続がx4であることによってデータ転送のボトルネックが生じ、それを解消するために高速で処理を行っているためであると推測される。不思議なことにYukariWhisper単体のみを実行させた場合は最大まで張り付いていなかった。本来ならこれもアフターバーナーで設定した最大値まで張り付くはずだが、突発的な何らかの遅延要素があったためにこのような結果になったと思われる。
スポンサーリンク
スポンサーリンク
動画での確認としては、パターン1がパターン2・パターン3と比較して遅いがこれは先のクロックの低さが影響している。本来であれば失敗データなのだが、シンプルに録り直し忘れた偶発的に「クロックも音声認識速度に影響する」ことを証明できたため採用している。フルクロックならパターン1が最も早く音声認識が確定する。最も音声認識に時間がかかったのは当然パターン4で、平均4秒の遅延を確認できる。パターン2は平均3.3秒、パターン3は平均3秒である。この関係から見るに、映像出力よりエンコードの方が重いということが分かる。ただしこれは元々のゲームが重いことも要因であり、別のゲームで同じ実験をすれば結果が変わるはずだ。つまり、ゲームによって確定時間が変わる可能性があるので、あらかじめそのゲームをプレイした状態でテストし、影響度を確認する必要がある。
使用時に発生する可能性のある異常な音声認識の処理による遅延の発生時は、その処理が確定するまでクロックが張り付き、VRAMも異常なまでに使用量が膨れ上がっている。その間も音声認識は行われるが、この異常な音声認識が確定し、タイムアウトとして処理されるまで処理待ちが発生することになる。このキューの分が蓄積され、VRAMの異常な消費になって表れているものと考えられる。そしてタイムアウト後は一気にキューが実行されてその分のVRAMが解放され、クロックも音声認識を行っていない場合は待機状態に戻る。この余計な負荷はできれば避けたいものであるが、現状は仕様の関係でタイムアウトの実装が後置処理にならざるを得ない。そのため、極力ノイズが発生しないようにユーザ側で対処する程度しか回避方法がないのである。それがなければ動作モード自体はx4でも、多少遅れる程度で使えるということになる。
なお、Turingまでの世代のGPUのPCIeリビジョンは3.0であるため、高速化したい場合の理想的方法はX299チップセットのSkylake-Xを使用するほかない。CPUの世代は落ちるがCPU直結のPCIeレーンが最大44になるため、2×16を容易に実現することができる。ただし入手は今となっては困難であり、付加価値が異常に高くなってしまっている。したがって、もしやるなら素直に最新世代一式を買った方が安上がりである。ただしその場合はGPUのPCIeリビジョンが4.0でなければならず、しかしその場合は大抵、3080Ti以上のハイエンド1本で済ませる方が様々な面で楽であるが。
x4接続にやらせるなら用途は限定する必要がある
YukariWhisperを使うなら、それをサブGPUにやらせればいい。そんな安直な考えのもとで導入したサブGPU、1650S。到底今のゲームをやるにはスペックが全く足りないが、世代的にはHEVCとBフレームサポートであるためエンコード用途にも使用できる。2080Tiで行っているもののうちゲーム用途以外のことは全てこれに代行させようと考えたのだが、マザーボード側の仕様がそれを邪魔した。x4までしか速度が出ないため、YukariWhisperを使ったうえでエンコードもさせた場合、音声認識の確定時間にも影響を及ぼし、リアルタイム性を失わせることが分かったためである。
様々なパターンを想定して実験を行ってみると、想定していた通り、YukariWhisper単体で実行したときと他にも処理を代行させたときとで遅延がはっきり異なることが分かった。エンコードは映像出力よりも明らかに負荷のレベルが高く、同時にゲームによっても遅延が変動することが発覚した。元が重いゲームほど、YukariWhisperに与える影響は大きくなるので、もしあまりにも遅延が激しすぎると思ったときはゲームの設定も見直す必要があるだろう。もっとも、競技シーンレベルでやっている人はそうも言ってられないとは思うが。
だが実験結果から用途を限定することによって、x4でも一応実用範囲内の遅延に収まることを確認できた。また、映像出力程度であれば影響度は小さく、エンコードも軽量なものを1個程度なら影響度は小さい。ただし元々のゲームが重い場合は遅延を避けることができないため、YukariWhisper単体にしておくべきである。本来なら影響するはずがないのだが、不思議な話である。PCIe帯域がゲーム側のGPUにほぼ全て使われてしまっているのが原因かもしれないが、詳細は不明だ。しかし工夫すれば何とか使えるあたり、まだまだ速度向上の方法はあるのではないだろうか。
以上、ゆかコネ検証報告:GPUのx4モードは音声認識に影響するか?であった。サブGPUを使うなら、本体もGPUも最新世代のものが一番である。
KIBEKIN at 44:44 Apr. 1st, 2024
スポンサーリンク