LabHub
はじめる
学ぶ 学習パス コース

音声 AI エージェント — 聞いて、調べて、話すパイプライン

遅延予算 — 話し終えた瞬間から最初の音まで

LabHubで続きを見る

一言でいうと

ユーザーが感じる遅延は、話をやめた瞬間から最初の音が出るまでです。これは、エンドポイント判定(オーディオ時間) + 確定 + 検索 + LLMの最初の断片 + TTSの最初の音(ウォールクロック時間)の合計です。直列につなぐとLLM全体とTTS全体が加算され、重ねて動かすと、最初の断片の時間だけが加算されます。段階ごとにバジェットを決め、分布(p50・p95)で突き合わせてはじめて、どこを減らすべきかが見えます。

なぜ必要なのか

前のモジュールで部品ごとに測った数字は、個別に見ればどれも小さいです。確定は数十ms、最初のトークンは100ms余り、TTSのRTFは0.04。ところが、つなげると秒単位になります。LabHubの会話練習の3.3秒も、3つの部品(ASR 0.4 + モデル0.6 + TTS 2.3)の直列の合計です(backend/app/lang_talk.py)。どの部品が遅いかよりも、どうつないだかのほうが大きな差を生みます。

どう動くのか

2つの時計。ユーザーが話している間、ストリーミングASRはすでに書き起こしています。だから、その時間は遅延ではありません。遅延は、話が終わったあとからです。ところが、「話が終わった」とわかる瞬間(エンドポイント)は、実際の話の終わりより遅くなります。VADが終わりを見たあと、gapの分だけさらに静かにならないと、確信できないからです(モジュール2)。この分はオーディオ時間で測ります: エンドポイント − 実際の話の終わり。このコースのクエリでは、gap 0.5秒で中央値が約0.57秒でした。エンドポイント以降の処理(確定・検索・LLM・TTS)はウォールクロックで、エンドポイントの瞬間を0として測ります。2つの時計を混ぜず、最後に足します。

直列と重ね合わせ。直列は、LLMが答えをすべて書き終えたあと、TTSが全体を合成します。最初の音 = 確定 + 検索 + LLM全体 + TTS全体。重ね合わせは、LLMが書いている間、断片ができるたびにTTSに渡します。最初の音 = 確定 + 検索 + (最初の断片ができるまで) + (最初の断片の合成)。同じ5つのクエリで、最初の音の中央値は、直列が1,814 ms、重ね合わせが1,593 msでした(クラスターのnuc1ノードのCPU 2個のPodで測った値。ノードごとに違うので、ラボで自分で測った数字を見てください)。利益が期待より小さいのは、モジュール7で見たように、LLMとTTSが同じ2コアを分け合うためです。

トレースで描く。数字の表より、バーの図のほうが速く読めます。Chromeのトレース形式(Trace Event Format)は、{"name", "ph": "X", "ts", "dur", "tid"}のイベントのリストで、ts・durはマイクロ秒です。chrome://tracingやPerfettoで開くと、LLMのバーとTTSのバーが重なった区間が見えます。この形式は、モジュール9のスパン記録(span)につながります。

平均ではなく分布。会話の不快感は、ときどき来る長い沈黙から生まれます。そのため、中央値(p50)とともにp95を見ます。サンプルが10個なら、p95は最も遅いものです(最近傍順位)。サンプルが少ないときに補間したパーセンタイルは、存在しない値を作り出してしまうので、このコースは最近傍順位を使います。

バジェット。人同士の会話のターンの間隔がおおむね数百msだという観察(モジュール2)から逆算して、段階ごとにバジェットを決めます。例: エンドポイント700・確定150・検索100・最初のトークン400・最初の音600 ms。測定した中央値がバジェットを超えた段階が、減らす場所です。ある分はモデルでは減りません(エンドポイント)。ある分はプロンプトで減ります(最初のトークン: キャッシュと短いコンテキスト)。ある分はつなぎ目で減ります(最初の音: 重ね合わせと短い最初の断片)。

メモリもバジェットです。このPodの上限は2 GiBです。Pythonプロセス(ストリーミングASR・TTS・エンベディング)が最大で約370 MB、LLMサーバーが約730 MBを使いました(nuc1で実測、リクエストが集中するとLLMサーバーは830 MBまで上がりました)。合計で半分をわずかに超えます。モデルをもう1つ載せたり、LLMを1.5Bに大きくしたりした途端に上限に達します。超えるとカーネルがプロセスを終了させ(OOM)、ユーザーは説明のない沈黙を聞くことになります。

現場での姿

LabHubの会話練習は、1ターンを「録音全体 → ASR → モデル → TTS」でつなぎ、GPUを奪い合わないように、席を一度に1人しか受け付けません(lang_talk.py)。その構造で3.3秒を減らすには、部品を速くするよりも、ストリーミング入力(エンドポイントを機械が判定する)、モデルストリームとTTSの重ね合わせ、短い最初の文が先です。このモジュールで、その効果を小さなモデルで自分で測ります。

測るときに注意することが1つあります。このPodはCPU 2コアの上限ですが、コンテナ内のnprocはノードのコア数(24)を返します。numpyが使うBLASはその数だけスレッドを立てて、2コアの中でお互いを押しのけます。エコー遅延を探す短いnp.dot 3,200回が、0.11秒から52秒になりました(このイメージを作る際に実測)。そのため、イメージがOPENBLAS_NUM_THREADS=2を固定しています。遅延を測る実験は、こうした環境の違い1つで、数百倍ずれます。

次のラボですること

12個のクエリのエンドポイント遅延を、オーディオ時間で測ります。確定 → 検索 → LLM → TTSを直列につないで測り、同じクエリで重ね合わせて測って比較します。重ねた実行1つをChromeのトレース形式で描き、10個のクエリのユーザー体感遅延のp50/p95を出します。段階別のバジェットと測定を突き合わせ、2つのプロセスのメモリを測って、Podの上限と比較します。