Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

*A local LLM and a zero-shot voice-cloning TTS take turns on one 8 GB GPU. Use them as local HTTP services, or as an offline voice server for ESP32 boards running xiaozhi firmware.*

[Results](#results) · [How it works](#how-it-works) · [Requirements](#requirements) · [Quick start](#quick-start) · [Connect a board](#connect-a-board-mode-b) · [Known limitations](#known-limitations) · [Upstream fixes](#fixes-sent-upstream) · [Docs](#documentation) · [32 pitfalls](docs/pitfalls.md)
[Results](#results) · [How it works](#how-it-works) · [Requirements](#requirements) · [Quick start](#quick-start) · [Connect a board](#connect-a-board-mode-b) · [Known limitations](#known-limitations) · [FAQ](#faq) · [Upstream fixes](#fixes-sent-upstream) · [Docs](#documentation) · [32 pitfalls](docs/pitfalls.md)

[![CI](https://github.com/korexil/gpu-duet/actions/workflows/ci.yml/badge.svg)](https://github.com/korexil/gpu-duet/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/korexil/gpu-duet)](https://github.com/korexil/gpu-duet/releases/latest)
Expand Down Expand Up @@ -148,6 +148,46 @@ For mode C, see [server/README.md](server/README.md); to build the engine yourse

Full list: [docs/architecture.md](docs/architecture.md#限制).

## FAQ

All numbers below were measured on an RTX 4060 Laptop (8 GB) with Windows 11.

### My local voice assistant stutters and long answers keep breaking up

When the LLM and the TTS share one GPU at the same time, the TTS slows down and synthesis falls behind playback. This repo makes them take turns on the GPU. On the same server pipeline, total gaps drop from 2.84 s to 0.09 s compared with running both at once (3-run averages, [four-stage measurements](docs/benchmarks.md)); in a controlled test that changes only the scheduling, 6 of 6 runs stalled at once and 0 of 6 when taking turns (simulated player, [pitfall #9](docs/pitfalls.md)). Throttling the LLM and changing the Windows GPU scheduling priority were tried and didn't help.

### It takes seconds after the question before it starts speaking

"Starts speaking" here is the server sending its first audio frame to the board; the board's own buffering and decoding come after that and aren't measured.

- Where the LLM runs and how it is scheduled (native TTS engine in all three): 5.3 s with the LLM on the CPU, 3.9 s with it on the GPU, 2.3 s when taking turns (3-run averages, [four-stage measurements](docs/benchmarks.md)).
- The GPU clocks down when idle: after 8 s idle, the TTS needs 0.64 s for its first block, against 0.17 s when warm (pitfall #11).
- The standalone server holds back 1.2 s of audio to trim each segment's trailing silence. When the first batch of audio is shorter than that, the first frame waits for the next block: 0.19 to 0.20 s longer when taking turns, 0.46 to 0.48 s when running at once ([PCM traces](bench/results/traces-2026-10-16/)).

### VRAM just fits, but everything gets slower with no error

What we ran into: with two TTS instances on the GPU, the Windows driver didn't report an error; it moved most of llama-server's VRAM to shared system memory, and decoding dropped from 66 to 45 tok/s. Check each process's "Dedicated GPU memory" in Task Manager and restart the affected process if it happens; normally start the TTS before the LLM (pitfall #12, [how it works](docs/how-it-works.md)).

### The LLM and the TTS don't fit on an 8 GB GPU

Without the patches it would take about 8.3 GB (estimated), more than the card has. Two patches to the TTS engine save about 1 GB: deduplicating the Q/K/V weights saves 488 MiB and keeping the text embedding table on the CPU saves 594 MiB. With them, the two models take about 6.8 GB and the whole card shows 7.3 GB in use, including the desktop. See [docs/how-it-works.md](docs/how-it-works.md).

### Loudness jumps between sentences, pauses vary, a small speaker clips

Each synthesized sentence is shaped the same way: one gain set from its first 0.6 s, leading silence trimmed, the trailing pause padded to a fixed length (0.9 s by default), and a smooth limiter keeping peaks at -6 dBFS (pitfalls #14 to #16).

### There is a "pop" every time before it speaks

The board we tested turns its amplifier off when idle and back on when it starts playing; other boards may not. After more than 1.5 s of silence, the first sentence starts with 0.3 s of silence (pitfall #17).

### The model won't open from a folder with non-ASCII characters

Upstream qwen3-tts.cpp opens the model and reference audio with ANSI `fopen`, so non-ASCII paths point at the wrong file. The engine patch here converts to wide characters first (`src/common/utf8_path.h`). Tested with Japanese folder names; Chinese ones weren't tested separately (pitfall #32).

### `WinError 206` when installing dependencies, or `Could not open output file ...cu.obj.d` when building

Both hit the 260-character path limit on Windows. For the dependencies, put the venv on a short path or enable long path support in Windows (pitfall #29); for the build, use a short build directory (pitfall #24).

## Fixes sent upstream

Problems found upstream while building this project, fixed and submitted as PRs (as of 2026-10-14):
Expand Down
42 changes: 41 additions & 1 deletion README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

*ローカル LLM とゼロショット音声クローン TTS が、8 GB の GPU 1 枚を交代で使います。ローカル HTTP サービスとして使うことも、xiaozhi ファームウェアを載せた ESP32 ボード向けのオフライン音声サーバーとして使うこともできます。*

[効果](#効果) · [仕組み](#仕組み) · [必要なもの](#必要なもの) · [クイックスタート](#クイックスタート) · [ボードをつなぐ](#ボードをつなぐ使い方-b) · [既知の制限](#既知の制限) · [上流への修正](#上流への修正) · [ドキュメント](#ドキュメント) · [ハマりどころ 32 件](docs/pitfalls.md)
[効果](#効果) · [仕組み](#仕組み) · [必要なもの](#必要なもの) · [クイックスタート](#クイックスタート) · [ボードをつなぐ](#ボードをつなぐ使い方-b) · [既知の制限](#既知の制限) · [よくある問題](#よくある問題) · [上流への修正](#上流への修正) · [ドキュメント](#ドキュメント) · [ハマりどころ 32 件](docs/pitfalls.md)

[![CI](https://github.com/korexil/gpu-duet/actions/workflows/ci.yml/badge.svg)](https://github.com/korexil/gpu-duet/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/korexil/gpu-duet)](https://github.com/korexil/gpu-duet/releases/latest)
Expand Down Expand Up @@ -148,6 +148,46 @@ python standalone\server.py

全一覧:[docs/architecture.md](docs/architecture.md#限制)。

## よくある問題

以下の数値はすべて RTX 4060 Laptop(8 GB)+ Windows 11 で測定したものです。

### ローカル音声アシスタントの声が途切れ途切れになる、長い回答が途中で止まる

LLM と TTS が同時に 1 枚の GPU を使うと TTS が遅くなり、合成が再生に追いつきません。このリポジトリでは 2 つが GPU を交代で使います。同じサーバーパイプラインで、同時に動かした場合と比べて音切れの合計は 2.84 s から 0.09 s に減りました(それぞれ 3 回の平均、[4 段階の測定](docs/benchmarks.md))。スケジューリングだけを変えた対照実験では、同時実行で 6/6 回、交代制で 0/6 回の音切れでした(再生を模擬、[ハマりどころ #9](docs/pitfalls.md))。LLM の速度制限や Windows の GPU スケジューリング優先度の変更は試しましたが、効果はありませんでした。

### 質問してから話し始めるまで数秒かかる

ここでいう「話し始め」は、サーバーがボードへ最初の音声フレームを送った時点です。ボード側のバッファリングとデコードはその後で、測定していません。

- LLM の置き場所とスケジューリング(TTS はいずれもネイティブエンジン):LLM が CPU 上で 5.3 s、GPU 上で 3.9 s、交代制にすると 2.3 s です(それぞれ 3 回の平均、[4 段階の測定](docs/benchmarks.md))。
- GPU はアイドル時にクロックが下がります。8 s アイドルの後、TTS の最初のブロックは 0.64 s、温まった状態なら 0.17 s です(ハマりどころ #11)。
- スタンドアロンサーバーは区切り末尾の無音を削るために 1.2 s 分の音声を保留します。最初に届いた音声が 1.2 s に満たないと、次のブロックが届くまで最初のフレームを送れません。交代制で 0.19~0.20 s、同時実行で 0.46~0.48 s 余計に待ちます([PCM トレース](bench/results/traces-2026-10-16/))。

### VRAM はぎりぎり足りているのに、エラーもなくだんだん遅くなる

実際に起きたケース:TTS のインスタンスが 2 つ GPU に載っていたとき、Windows のドライバーはエラーを出さず、llama-server の VRAM の大部分を共有メモリに移し、デコードが 66 から 45 tok/s に落ちました。タスクマネージャーで各プロセスの「専用 GPU メモリ」を確認し、起きたら影響を受けたプロセスを再起動してください。普段は TTS を先に、LLM を後に起動します(ハマりどころ #12、[仕組み](docs/how-it-works.md))。

### 8 GB の GPU に LLM と TTS が収まらない

パッチなしだと推定で約 8.3 GB 必要で、8 GB を超えます。TTS エンジンへの 2 つのパッチで約 1 GB 節約できます。Q/K/V 重みの重複排除で 488 MiB、テキスト埋め込み表を CPU に置いて 594 MiB です。パッチ適用後は 2 つのモデルで約 6.8 GB、カード全体の使用量はデスクトップ込みで 7.3 GB です。[docs/how-it-works.md](docs/how-it-works.md) を参照。

### 文ごとに音量がばらつく、間の長さがばらつく、小さいスピーカーで音が割れる

合成した文を同じ手順で整えます。冒頭 0.6 s から決めたゲインを文全体に掛け、冒頭の無音を削り、文末の間を一定の長さ(既定 0.9 s)に揃え、なめらかなリミッターでピークを -6 dBFS に抑えます(ハマりどころ #14~#16)。

### 話し始める前に毎回「プツッ」と鳴る

テストしたボードはアイドル時にアンプを切り、再生を始めるときに入れ直します。ほかのボードでも同じとは限りません。1.5 s 以上無音が続いた後の最初の文は、頭に 0.3 s の無音を入れます(ハマりどころ #17)。

### 日本語などを含むフォルダーからモデルを開けない

上流の qwen3-tts.cpp はモデルと参照音声を ANSI の `fopen` で開くため、非 ASCII のパスでは別のファイルを探してしまいます。このリポジトリのエンジンパッチは先にワイド文字に変換してから開きます(`src/common/utf8_path.h`)。日本語のフォルダー名で動作を確認済みで、中国語のフォルダー名は個別にテストしていません(ハマりどころ #32)。

### 依存関係のインストールで `WinError 206`、ビルドで `Could not open output file ...cu.obj.d` が出る

どちらも Windows の 260 文字のパス長制限に当たっています。依存関係は venv を短いパスに置くか、Windows で長いパスのサポートを有効にしてください(ハマりどころ #29)。ビルドはビルド用ディレクトリを短いパスにしてください(ハマりどころ #24)。

## 上流への修正

このプロジェクトの開発中に上流で見つけた問題を修正し、PR を送りました(2026-10-14 時点)。
Expand Down
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

*A local LLM and a zero-shot voice-cloning TTS take turns on one 8 GB GPU. Use them as local HTTP services, or as an offline voice server for ESP32 boards running xiaozhi firmware.*

[效果](#效果) · [原理](#原理) · [需要什么](#需要什么) · [快速开始](#快速开始) · [接板子](#接板子用法-b) · [已知限制](#已知限制) · [上游修复](#提给上游的修复) · [文档](#文档) · [32 条踩坑](docs/pitfalls.md)
[效果](#效果) · [原理](#原理) · [需要什么](#需要什么) · [快速开始](#快速开始) · [接板子](#接板子用法-b) · [已知限制](#已知限制) · [常见问题](#常见问题) · [上游修复](#提给上游的修复) · [文档](#文档) · [32 条踩坑](docs/pitfalls.md)

[![CI](https://github.com/korexil/gpu-duet/actions/workflows/ci.yml/badge.svg)](https://github.com/korexil/gpu-duet/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/korexil/gpu-duet)](https://github.com/korexil/gpu-duet/releases/latest)
Expand Down Expand Up @@ -147,6 +147,46 @@ python standalone\server.py

完整列表:[docs/architecture.md](docs/architecture.md#限制)。

## 常见问题

以下数字都是在 RTX 4060 Laptop(8 GB)+ Windows 11 上测的。

### 本地语音助手说话一顿一顿,长回答断断续续

LLM 和 TTS 同时用一块显卡时,TTS 被拖慢,合成赶不上播放。本仓库让两者轮流用 GPU。同一条服务端管线上,和同时跑相比,断音合计从 2.84 s 降到 0.09 s(各三次平均,[四阶段实测](docs/benchmarks.md));只改调度的对照实验里,同时跑 6/6 次断音,轮流 0/6(模拟播放器,[踩坑 #9](docs/pitfalls.md))。给 LLM 限速、调 Windows 的 GPU 调度优先级都试过,没有用。

### 问完之后要等好几秒才开口

这里的「开口」是服务端向板子发出第一帧音频的时刻;板子自己的缓冲和解码还在后面,没有测。

- LLM 的位置和调度(TTS 都用原生引擎):LLM 在 CPU 上时是 5.3 s,换到 GPU 是 3.9 s,再改成轮流是 2.3 s(各三次平均,[四阶段实测](docs/benchmarks.md))。
- GPU 空闲后会降频:空闲 8 s 后,TTS 第一块音频要 0.64 s,热态 0.17 s(踩坑 #11)。
- 独立服务端为了修剪段尾静音,会先扣住 1.2 s 音频。第一批音频不到 1.2 s 时,要等下一块到了才发第一帧:轮流时多等 0.19~0.20 s,同时跑时 0.46~0.48 s([PCM 跟踪](bench/results/traces-2026-10-16/))。

### 显存刚好够,却越跑越慢,也不报错

我们遇到过:两个 TTS 实例同时占着显卡时,Windows 驱动没有报错,而是把 llama-server 的大部分显存挪到共享内存,解码从 66 降到 45 tok/s。在任务管理器里看每个进程的「专用 GPU 内存」,出现时重启受影响的进程;平时先启动 TTS 再启动 LLM(踩坑 #12,[原理](docs/how-it-works.md))。

### 8 GB 显卡装不下 LLM 和 TTS

不打补丁,推算要 8.3 GB 左右,超出 8 GB。TTS 引擎的两个补丁省约 1 GB:Q/K/V 权重去重省 488 MiB,文本嵌入表放 CPU 省 594 MiB。打补丁后两个模型合计约 6.8 GB,整卡占用 7.3 GB(含桌面)。见 [docs/how-it-works.md](docs/how-it-works.md)。

### 一段回答里忽大忽小、停顿忽长忽短、小喇叭爆音

逐句合成后统一整形:每句按开头 0.6 s 定一个增益,切掉开头静音,句尾停顿补到固定长度(默认 0.9 s),用平滑限幅把峰值留在 -6 dBFS(踩坑 #14~#16)。

### 每次开口前「啪」一声

我们测的板子空闲时会关功放,开口时才上电,其他板子不一定这样。静默 1.5 s 以上之后的第一句,前面留 0.3 s 静音(踩坑 #17)。

### 中文或日文目录下打不开模型

上游 qwen3-tts.cpp 用 ANSI `fopen` 打开模型和参考音,非 ASCII 路径会找错文件。本仓库的引擎补丁改成先转宽字符再打开(`src/common/utf8_path.h`)。已用日文目录实测,中文目录没有单独测(踩坑 #32)。

### 装依赖报 `WinError 206`,或编译报 `Could not open output file ...cu.obj.d`

都是撞上了 Windows 260 字符的路径长度限制。装依赖时,把 venv 放在短路径下,或在系统里开启长路径支持(踩坑 #29);编译时,把编译目录放在短路径下(踩坑 #24)。

## 提给上游的修复

做这个项目时在上游发现的问题,修好后提了 PR(截至 2026-10-14):
Expand Down
2 changes: 1 addition & 1 deletion docs/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ LLM 侧:Gemma 4 E4B 全层上 GPU,逐层 embedding 表留在内存按需读
<img alt="示意图,标注值来自 benchmark 中位数:同时跑时 TTS 实时率 1.1~1.6,首声 3.20 s、断音合计 2.27 s;轮流跑时实时率约 0.6,首声 1.83 s、断音 0" src="img/timeline-light.svg" width="480">
</picture>

TTS 逐帧解码,是大量很小的同步 kernel;LLM 同时解码时,TTS 第一句实时率升到 1.1~1.6,比播放慢。给 LLM 限速(20、14 tok/s)和调 WDDM 调度优先级都无效([踩坑 #9](pitfalls.md))。两者合计只需约 75% 的 GPU,所以串行:LLM 写到句末就停,等 TTS 合成完再续写。
LLM 同时解码时,TTS 第一句实时率升到 1.1~1.6,比播放慢(推测是因为 TTS 逐帧解码、由大量很小的同步 kernel 组成,没有用 profiler 验证)。给 LLM 限速(20、14 tok/s)和调 WDDM 调度优先级都无效([踩坑 #9](pitfalls.md)),所以串行:LLM 写到句末就停,等 TTS 合成完再续写。

一轮回答里 `llamacpp_paced` 和两个服务的交互顺序:

Expand Down
4 changes: 2 additions & 2 deletions docs/pitfalls.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@
## 三、一块 8 GB GPU 上同时跑 LLM 和 TTS

**9. 同时跑,TTS 跟不上实时。** TTS 单独 RTF 约 0.6;LLM 同时解码时,第一句 RTF 升到 1.1~1.6,开头就断。
- 给 LLM 限速(20 或 14 tok/s):无效,RTF 仍约 1.4。TTS 逐帧解码,大量很小的同步 kernel,LLM 只占一点时间片,TTS 每一步都要等。
- 给 LLM 限速(20 或 14 tok/s):无效,RTF 仍约 1.4。推测原因:TTS 逐帧解码,是大量很小的同步 kernel,LLM 只占一点时间片,TTS 每一步也要等(没有用 profiler 验证)。
- 调 WDDM GPU 调度优先级(`D3DKMTSetProcessSchedulingPriorityClass`):对 CUDA 计算无效。
- **轮流用 GPU**:LLM 生成到句末就停,等 TTS `/health` 不再 `busy` 再继续。两者合计只需约 75% 的 GPU,串行够用。[benchmark](../bench/README.md)(每格 6 次,中位数):
- **轮流用 GPU**:LLM 生成到句末就停,等 TTS `/health` 不再 `busy` 再继续。[benchmark](../bench/README.md)(每格 6 次,中位数):

| | 首声 | 断音合计 | 有断音 |
|---|---|---|---|
Expand Down
10 changes: 5 additions & 5 deletions server/core/providers/llm/llamacpp_paced/llamacpp_paced.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""llama.cpp server provider that takes turns with a local TTS on the same GPU.

A streaming TTS decodes frame by frame with many small synchronous kernels. Any
concurrent LLM decoding on the same GPU stretches every step: on an 8 GB laptop
GPU the first sentence ran at RTF 1.1-1.6 while the LLM was still generating,
leaving audible gaps. Rate-limiting the LLM did not help (RTF stayed ~1.4), but
together the two only need ~75% of the GPU, so this provider serializes them:
Concurrent LLM decoding on the same GPU slows a streaming TTS down: on an 8 GB
laptop GPU the first sentence ran at RTF 1.1-1.6 while the LLM was still
generating, leaving gaps. (Probably because the TTS decodes frame by frame with
many small synchronous kernels; not confirmed with a profiler.) Rate-limiting
the LLM did not help (RTF stayed ~1.4), so this provider serializes them:
it generates up to a sentence end, waits while the TTS reports ``busy``, and
continues in the TTS's idle gaps. Measured in bench/ (6 runs per cell, median,
~80 s answers): first audio 3.20 s -> 1.83 s, playback underrun 2.27 s -> 0.
Expand Down
Loading
Loading