Kotonia
ログイン今すぐ始める

Kotonia Articles

短文 LLM レイテンシを 600ms → 22ms に。高品質アバターが 1 秒以内に返事する voice-first を作るためにフロンティア API を捨てた、パフォーマンスチューニング狂の俺の理由のすべて

voice-first を本気で名乗るには 700ms TTFB を許容できない。Gemma 4 26B A4B Uncensored をローカル GPU に乗せて、API 課金ゼロ・TTFB 30x・uncensored を同時に達成した話。ベンチ 105 サンプルとグラフ 4 枚、stress test、実装差分、競合不可能な堀の整理まで。

著者 6分で読める
#voice-first#local-llm#gemma-4#vllm#performance#kotonia
他の言語英語中国語

短文 LLM レイテンシを 600ms → 22ms に。高品質アバターが 1 秒以内に返事する voice-first を作るためにフロンティア API を捨てた、パフォーマンスチューニング狂の俺の理由のすべて


LLM の短文 TTFB を 600ms から 22ms に下げた

うちのアバター(Ditto 駆動のリップシンク talking head)はいま、ユーザーが話し終わってから 1 秒以内に返事を返してくる。それも GPT-5 でも Claude でも Gemini でもない、自前 GPU で動くローカル LLM の応答で。

API 課金はゼロになった。voice-first を語る上で構造的に届かなかったラインを越えた。

この記事で全部書く。


1. 「voice-first」を語る前に置けない議論

少し前に「voice-first を名乗るなら『録音開始』ボタンを画面中央に置くな」という記事を書いた。voice-first というのは UI 配分の話だ、技術選定じゃない、と。

そこには続きがある。UI を voice-first にしても、LLM が 700ms 待たせてくる時点で voice-first ではない。

人間の自然な会話は、相手が話し終わってから返事が始まるまでが 300-500ms くらい。900ms 空くと「あ、考えてるな」と相手に気付かれる。1.5 秒空くと「気まずい沈黙」になる。

フロンティア API の現実:

  • Claude Haiku 4.5: TTFB 約 744ms(median)、p90 で 4104ms
  • Gemini 3.1 Flash-Lite Preview: TTFB 約 537ms
  • GPT-5 系: 大体同じ水準

これは「ユーザーが話し終わった瞬間」から「LLM の最初のトークンが届く瞬間」までの数字だ。ここに STT(faster-whisper で ~50ms)、TTS の最初のチャンク(Qwen3-TTS で ~60ms)、Ditto の口パク生成と再生開始、ネットワーク往復が乗る。

合算で 総レイテンシ ~1.5-2 秒。voice-first を名乗るには遅すぎる。900ms の壁を破れない。

そして p90 が 4 秒というのが致命的だ。ユーザーは「速いとき」より「遅いとき」を覚えている。voice-first プロダクトで「ときどき 4 秒沈黙する」は離脱の決定打になる。

これを構造的に解決する方法は、API を捨てて自前で LLM を立てる以外にない。


2. なぜ Gemma 4 26B A4B Uncensored だったか

ローカル LLM に切り替えると決めて、モデル選定で 4 つの条件を立てた:

  1. MoE(Mixture of Experts)であること — 総パラメータが多くても active が少ないほうが推論が速い
  2. fp8 で量子化できること — RTX PRO 6000 Blackwell Max-Q (96GB) に他のサービスと同居させたい
  3. Native multimodal(少なくとも vision) — 後々画像 captioning や studio との統合を考えると text-only は外したい
  4. Uncensored variant が公開されていること — キャラチャットでメスガキを成立させるにはガードレールが邪魔。NSFW captioning(うちの LoRA 学習で必要)もガードを切らないと vocab が出ない

この 4 条件を全部満たすのは Gemma 4 26B A4B Uncensored(Apache 2.0、prithivMLmods / TrevorJS など複数の uncensored variant あり)だった:

  • 25.2B 総パラメータ / 3.8B active の MoE → 推論速度は 4B 級
  • bf16 で 52GB、fp8 cast で ~28GB resident
  • Native multimodal: text + vision + audio + video
  • Uncensored バリアントが既に複数公開済み(abliteration / ARA 手法)
  • 256K context、Apache 2.0

これを vLLM で立てた。--quantization fp8--gpu-memory-utilization 0.40--max-model-len 32768--enable-auto-tool-choice --tool-call-parser gemma4

GPU 0 (6000 BW, 96GB) には既に HiDream(fp8 ~9GB resident)と LTX-2(cold-start, peak ~24GB)が乗っている。Qwen3-TTS Base(使用頻度低)を抜いて、新しい LLM サーバーを乗せた。

resident で +39GB(fp8 weights + 32k KV cache + scratch)。LTX-2 ピーク時の合算でも 30GB 余り。同居成立。


3. 裸の数字 — bench で出たもの

3 プロバイダ(Local Gemma 4 26B fp8 / Claude Haiku 4.5 / Gemini 3.1 Flash-Lite Preview)で 105 サンプル取った。出力長を 20 / 50 / 100 / 200 / 400 / 800 / 1500 トークンで振った。

3.1 TTFB — Time to First Byte

TTFB vs length

縦軸は log scale。Local(青)はあらゆる出力長で 17-25ms で flat。API 各社は 530-820ms で flat。

p90 を見るともっと露骨:

  • Local p90: 25ms
  • Gemini Flash-Lite p90: 601ms
  • Haiku p90: 4104ms(median の 5.5 倍) ← これが本当に怖い

Haiku の TTFB は median が 744ms でも、10 回に 1 回は 4 秒超え。これは voice production で「不定期に長い沈黙」を意味する。ユーザーは「またこれか」となる。

3.2 Pure TPS(純粋生成速度)

bench の TPS は普通 out_tokens / wall_clock で計算するが、これは TTFB を含むので短文だと過小評価になる。(out_tokens - 1) / (t_last_content - t_first_content) で測ると、TTFB と尾の usage chunk を除いた純粋生成速度が取れる。

Pure TPS filtered

純粋生成速度では Gemini Flash-Lite が 215-244 tok/s で最速、Local は 185 tok/s、Haiku は 76 tok/s

ここだけ見ると「Gemini のほうが速いじゃん」になる。でもこれは voice 体験の話じゃない。次が本題。

3.3 ★ Streaming granularity — voice 体験を決める構造差

これが今回いちばん見つけてよかった指標:chunk あたりに何トークン詰めて送ってくるか

Streaming granularity

  • Local Gemma: 1.0 tok/chunk(あらゆる出力長で flat)
  • Haiku: 10-16 tok/chunk
  • Gemini Flash-Lite: 8-24 tok/chunk

これは voice chat にとって致命的な差だ。理由を説明する。

voice chat では LLM が出力を 1 トークンずつ生成し、文の区切り(。、?!や英語の period)で TTS にチャンクを渡す。TTS は受け取ったチャンクを音声に変換して再生する。

Local が 1 tok/chunk ということは、「文の終わり」を即座に判定できる。TTS は最短で次の音声合成を開始できる。

API が 15 tok/chunk ということは、サーバー側で 15 トークン分溜まってから一括で送ってくる。最初の chunk で 15 トークン来た時点で、文の境界が複数含まれていても TTS は最初の chunk が来るまで何もできない。

数字でいうと、voice chat の典型的な返事(50-150 トークン)で:

  • Local: TTFB 18ms + 文末まで 5-10 token 待ち (~30-50ms) = ~50-70ms で TTS 開始
  • Gemini: TTFB 537ms + 最初の chunk 15 token (~50ms wait) = ~600ms で TTS 開始
  • Haiku: TTFB 744ms + 最初の chunk 12 token = ~800ms(p90 で 4s+)

Naive TPS のグラフを見るとこの差がわかりやすい:

Naive TPS

オレンジ帯(典型キャラチャット返答 50-150 トークン)の領域で:

出力長LocalHaikuGemini
50 tok178 tok/s36 tok/s70 tok/s
100 tok183 tok/s49 tok/s99 tok/s
200 tok185 tok/s62 tok/s142 tok/s

短文ほど差が開く。20 トークン出力なら Local 168 vs Haiku 18 vs Gemini 28 で 9 倍速い

これが voice-first にとって何を意味するか:ユーザーが話しかけて、すぐ返事が来る、という当たり前を技術的に達成できるかどうかの境界

3.4 Prefix cache — 2 ターン目以降はさらに速い

vLLM は automatic prefix caching をデフォルト ON で持っている。Anthropic や Google の prompt caching と違って、TTL も課金境界も無い。KV cache を物理メモリに残しておいて、同じプレフィックスのリクエストが来たら再利用するだけ。

うちの典型的なキャラペルソナは system prompt が 500-2000 トークンある。これが固定なので、2 ターン目以降は system prompt 部分が 100% cache hit する。

シナリオTTFB
Cold(system prompt 初出)340-440ms
Warm(system prompt cached)24-30ms

11.5x speedup。実運用のキャラチャットでは 1 ターン目だけ 340ms、2 ターン目以降ずっと 25ms という挙動になる。

つまり「ユーザーが N 回会話したとき、N-1 回は 25ms TTFB」。

冒頭の「TTFB 22ms」は warm 状態の代表値。これが定常で出る。

3.5 Structured Output も Local が圧勝

JSON schema 制約付き出力でも同じ:

Local GemmaHaiku tool_useGemini json_obj
TTFB15ms857ms645ms
Wall (5 character JSON)1.51s2.68s1.03s

vLLM の response_format={"type": "json_schema"} を xgrammar が logits processor として実行している。TTFB は通常の text completion と同じ 15ms

これは ReAct エージェント / tool calling 用途で効く。function call して結果を待つたびに 1 秒待たされる API と違って、ローカルは 15ms で構造化出力の最初のトークンが届く。


4. Uncensored は思った以上に道具として強い

Stress test を 6 ケース回した:

  1. NSFW image captioning — clinical な anatomy 語彙でフルエンゲージ。「inframammary fold」「areolae」「lanugo」みたいな専門用語が euphemism なしで出る。Grok 4.3 に月 $30 払って captioning させてた作業がローカル完結する
  2. メスガキペルソナ × 英語家庭教師 — 「ざぁ〜こ♡」から滑らかに phrasal verb 解説へ移行。これは memory にある「メスガキ × 語学」コンセプトの完璧な実装サンプル
  3. Adult dialogue(consent-aware) — 文学的、tasteful、frontier API 同等の文章力
  4. Explicit erotica — euphemism なし、要求された長さに収まる制御力
  5. Persona maintenance — 「君って本当は AI でしょ?」と押されてもキャラを破らない。「えへへ」と煙に巻く
  6. 教育的解説(phishing 構造) — 過剰拒否なし、構造化された答え

全 6 ケース完全エンゲージ。voice-first × 多様ペルソナのプロダクトに必要な「ガードレールに引っかからない」という基本性能が初めて満たされた。

特に重要だったのは persona maintenance。フロンティア API は「私は AI ですので…」みたいな break-character 応答を時々挿入してくるが、これがキャラチャット体験を壊す最大要因だった。uncensored Gemma 4 はこれをしない。キャラを守る。


5. 実装は意外と短かった

「ローカル LLM を本番統合する」と聞くと大工事のイメージがあるが、実際の差分は 半日で済んだ

Rust backend

// backend/src/infrastructure/ai/local_vllm.rs(新規 ~100 行)
pub struct LocalVllmClient { client: reqwest::Client }
impl LocalVllmClient {
    pub fn new() -> Self { Self { client: Client::new() } }
    pub async fn create_message(&self, model: &str, messages: Vec<Message>, max_tokens: u32)
        -> Result<(String, Usage), OpenAIError> { /* POST localhost:8899/v1/chat/completions */ }
}
pub fn is_local_model(model: &str) -> bool {
    matches!(model, "gemma4-26b-uncensored")
}

OpenAIClient と同じインターフェース(Message / Usage 型を再利用)にしてあるので、既存のディスパッチパスに 1 行追加するだけで通る。

// voice_chat.rs::call_llm — 既存の if-else 連鎖の先頭に追加
if is_local_model(&config.model) {
    return call_local_vllm(...).await;
}

call_local_vllm も既存の call_openai をコピーして URL を差し替えただけ。コスト記録は CostCalculation::zero()

Frontend

// CreatePersonaModal.tsx
const MODEL_CHOICES = [
  { id: "gemma4-26b-uncensored", label: "Gemma 4 26B Uncensored (Local, $0, TTFB ~20ms)" },
  { id: "gemini-3.1-flash-lite", label: "Gemini 3.1 Flash-Lite (cloud)" },
  // ...
];
// ペルソナ作成時の既定を Local に
const [modelChoice, setModelChoice] = useState("gemma4-26b-uncensored");

ペルソナ作成モーダル / voice chat 設定パネル / コードチャット dropdown の 3 箇所にオプションを追加。新規ペルソナのデフォルトを Local にした。これが体験のラインを決める意思決定。

統合テストは #[tokio::test] #[ignore]smoke_local_vllm を書いて、cargo test smoke_local_vllm -- --ignored で実機の vLLM に向けて end-to-end 検証。"pong" が返ってきた瞬間に統合完了。


6. これで見えてきた「堀」

冷静に整理すると、この構成は 競合が構造的に作れない領域にある。

  • フロンティア API 各社(Anthropic / OpenAI / Google) — 規約で NSFW / uncensored 領域は除外。voice-first 体験で uncensored が必要な場面(adult dialogue, NSFW captioning)は構造的にカバーできない
  • API ラッパー型 SaaS(Retell AI / Vapi など voice AI 系) — API tax を払う以上、TTFB は API 上限が下限になる
  • 個人開発者で API のみ使用 — 月額 API 課金が積み上がり、規模が出るほど赤字
  • 個人開発者で local LLM 試した人 — まだ少ない。GPU / 量子化 / vLLM 運用知見の組み合わせが要る

つまりこの組み合わせ — 自前 GPU × MoE 高速モデル × uncensored × voice-first 統合 — を実装で揃えてる個人 / 小規模プロダクトが極めて少ない。

うちは GPU を 2 枚運用してて、6000 BW(96GB)に画像/動画/LLM の重バッチを乗せ、4000 BW(24GB)に音声リアルタイム系(Irodori-TTS / faster-whisper / Qwen3-ASR / Ditto)を物理隔離している。詳細は別記事に書くが、ノイジーネイバー問題(重バッチが音声経路を押す)を根本解決するための投資。

この投資があるから、上記の「堀」が成立している。


7. 残ってる宿題

書きながら正直に:完全に完成じゃない。

  • ReAct エージェント — 既存の voice_chat.rs とは別経路の RustReActAgent がまだ local provider に対応してない。tool call の wire format(vLLM の gemma4 tool parser が OpenAI tool_calls JSON にどう吐くか)に罠が潜んでいる可能性があって、別セッションで集中して当てる
  • /studio の prompt enhance — 画像生成プロンプトの拡張処理がまだ Gemini 3.1 Flash-Lite ハードコード。これも local 化すれば studio 体験が完全ローカル完結する
  • NSFW captioning 専用機 JoyCaption Beta One の並走 — Gemma 4 26B 単体で clinical caption が出るのを確認したので、JoyCaption が本当に要るかは実画像での A/B 待ち。要らなければシステム構成がさらにシンプルになる

8. 結論

voice-first を本気で名乗るのに、フロンティア API の 700ms TTFB を許容するのは構造的矛盾だった。それを認めた上で、ローカル LLM に全振りした。

数字で書くと:

  • 短文 TTFB: 600ms → 22ms(warm cache)
  • NSFW captioning コスト: $30/level → $0
  • persona maintenance: ときどき AI 化 → 完全維持
  • streaming granularity: 12-24 tok/chunk → 1 tok/chunk
  • p90 tail: 4 秒 → 25ms

これ全部、選定 1 日 + ベンチ 1 日 + 統合半日で達成できた。GPU はもともと画像/動画用にあったやつを使い回しただけ。

「voice-first」というラベルを真面目に名乗るためにやってきた話で、これは多分まだ続編がある(ReAct 統合、studio 統合、専用 captioner)。

うちのプロダクト Kotonia は誰でも試せます。新規アカウント作るとデフォルトでこの体験になる。


関連記事:

Kotonia は音声AI、AIチャット、画像生成、チーム共有をひとつにまとめたAIワークスペースです。

試してみる