~/blog/gtx-970-gemma4-e2b-voice-assistant

LLM Deep Dive · part 5

[趣味競賽] 把 GTX 970 變語音助手:Gemma 4 E2B 多模態 + Piper TTS,端到端 2.8 秒

cat --toc

TL;DR

Part 4 我量了一張 2014 年的 GTX 970 跑 Gemma 4 E2B 有多快;這篇看它能做到什麼程度。同一張垃圾卡跑出一個完整的離線語音助手:會看圖、會把 30 秒語音轉成文字、會回答、會寫 code——再接上 Piper TTS(純 CPU、不吃 VRAM),它還會開口講話。模型(3.2GB)加它 942MB 的視覺/音訊投影層擠進約 3.5GB 的 VRAM,只剩 535MB。語音進 → 理解 → 語音出,端到端約 2.8 秒——而且是在一台我早就退役的舊電腦上,那張 GTX 970 二手大概 $15 美金。先說清楚:多模態「看起來」比文字快,但那是 prefill/量測的效果,不是模型 decode 真的變快。

來聽聽一張 10 年前的顯卡開口講話 👇 —— 這是助手實際合成的回覆,全程都在一張 2014 年的 GTX 970 上跑:

白話版:老卡變成會看會聽會講話的助手

Part 4 問的是「多快」,這篇問的是「能做到什麼程度」。結果是——上一篇那張垃圾桶等級的 2014 GTX 970,可以變成一個完整的離線語音助手。給它一張照片,它說得出那是一隻貓;對它講話,它能寫下你說了什麼;它會回答、會寫 Python,然後一個純 CPU 的小語音合成引擎再把答案唸出來。沒有雲端、沒有 API key、沒有訂閱。而且以我來說,硬體一毛都沒花——這本來就是一台我退役收進櫃子的舊電腦。(真要買的話,二手 970 差不多兩杯咖啡。)


從「多快」到「做到什麼程度」

Part 4 已經量過速度:Gemma 4 E2B 在 GTX 970 上跑約 47.6 tok/s,沒有 tensor core 的卡卡在解量化。但 Gemma 4 E2B 不是一個外掛視覺的文字模型——它天生就是多模態的,吃圖片也吃語音。對一張 3.5GB 的垃圾卡來說,接下來當然要問:這些它到底用不用得上?而且如果它聽得到,能不能讓它講話?

這是 LLM Deep Dive 第五篇。同樣的硬體、同樣的模型、沒再多花錢——就只是看看在 VRAM 用完之前,能組出多完整的一個助手。

多模態硬塞 4GB:942MB 投影層,只剩 535MB

多模態輸入靠一個獨立檔案:gemma-4-E2B-it-mmproj.gguf,這個投影層負責把圖片和音訊編碼成語言模型看得懂的 token。它放在 Google 的 QAT Q4_0 repo 裡,942MB。帳面數字不太妙:

模型(QAT Q4_0):    3,194 MB
投影層(mmproj):      942 MB
合計:              4,136 MB
GTX 970 可用 VRAM: ~4,096 MB(3.5GB 快 + 0.5GB 慢)

爆了。但 llama.cpp 不會把每個權重同時 pin 在 VRAM 裡,所以還是載得起來——啟動指令加上 --mmproj

./build/bin/llama-server \
  -m ~/models/gemma-4-E2B_q4_0-it.gguf \
  --mmproj ~/models/gemma-4-E2B-it-mmproj.gguf \
  -ngl 99 -c 2048 --port 8080 \
  --jinja --chat-template-kwargs '{"enable_thinking":false}'

實測 VRAM:閒置 3,405MB、推理中 3,496MB——剩 535MB。很緊,但每一項測試都穩、沒 OOM。不過要先提醒一下:llama.cpp 把音訊輸入標成「highly experimental」。我這邊端到端跑得通,但它比視覺那條路新、也比較毛。

它看得到,也聽得到

看圖。 我丟給它的就是這張,配「What is in this image?」:

測試用的虎斑貓照片——趴著、直直看著鏡頭、室內背景模糊

Q4_0 答得準:一隻虎斑貓、趴著、看著鏡頭、室內背景模糊。換繁體中文,它一樣用三句乾淨的話講完。另一張沙拉照片則把食材一個個列出來——生菜、紅洋蔥、小黃瓜、胡蘿蔔、胡椒粒:

測試用的沙拉照片——木板上的生菜、紅洋蔥、小黃瓜、胡蘿蔔,還有散落的胡椒粒

我踩到的一個坑:把 image_url 那塊放在 text 後面,模型就咬定它沒看到圖。Google 建議圖片放在文字前面(音訊放在文字後面);照這個順序排就好了。

聽聲音。 同一個 mmproj 也處理語音。Gemma 4 E2B 吃最長 30 秒的片段。這是我餵進去的英文片段:

轉寫近乎完美——「Hello, this is a test of speech recognition on a GTX 970 graphics card」只漏了一個「a」。再來是繁體中文這段:

前半段完美,然後把「語音辨識」聽成「與音變式」。不過這個我不能怪模型:我餵進去的是合成的 TTS 語音,這個測試分不出來是模型聽錯、還是合成語音本身的瑕疵。

「多模態比較快」——小心,那多半是量測

這裡有個我差點踩進去的陷阱。圖片和語音那幾項量出來比文字高:約 51–53 tok/s,純文字是 47.6。很容易就寫成「多模態比較快,因為投影層把媒體壓成比較少的 token」。

這個解釋站不太住——至少不能照字面講。少一點 input token,省的是 prefill(處理 prompt 那一趟),而且 context 短一點,KV cache 和 attention 的負擔也可能輕一點,所以 decode 要說完全不受影響也不對。但這離「多模態比較快」差得遠。比較可能的是:那幾個高一點的數字是 prefill 混進了平均、或單純 run 比較短,而不是生成階段真的變快。不把 prompt 處理和生成的計時分開,就不能說那是真的 decode 變快。它也不會因為在看貓而不是讀句子就生成得比較快——判斷時看背後的機制,別被剛好漂亮的數字騙了。

給它一個聲音:Piper TTS,跑在 CPU,不用錢

模型聽得到了。要讓它講話,我需要一個不會去碰那塊已經爆滿的 VRAM 的語音合成。Piper(現在改在 OHF-Voice/piper1-gpl 維護)正好是這個:一個獨立的神經網路 TTS 執行檔,純 CPU 跑、不吃 GPU 記憶體,而且合成速度遠快於即時。en_US-amy-medium 這個聲音是一個 61MB 的 ONNX 檔。

echo "Hello! I am running on a GTX 970." | \
  ~/piper/piper/piper --model ~/piper/voices/en_US-amy-medium.onnx \
  --output_file /tmp/test.wav

在 Ryzen 5 3500X 上,它把一段 11 秒的語音用 0.63 秒合成完——real-time factor 約 0.05,差不多是播放速度的 18 倍。出來是這樣:

GPU 負責想,CPU 負責講,兩邊從不為了記憶體打架。

完整一圈:語音進、語音出,2.8 秒

串起來就是一個真的助手:一個 WAV 進去,Gemma 在 GPU 上轉寫加回答,Piper 在 CPU 上把回覆唸出來,一個 WAV 出來。

[麥克風 WAV] → Gemma 4 E2B(GPU,~3.5GB)→ 轉寫 + 理解 + 回答
                                              │
                                              ▼
                                        Piper TTS(CPU,0 VRAM)
                                              │
                                              ▼
                                         [喇叭 WAV]

我講了「this is a test of speech recognition on a GTX 970 graphics card」。助手聽到、理解、然後回答——就是這篇最上面那段你聽到的。

階段耗時在哪
轉寫 + 理解 + 回答1.9sGPU(GTX 970)
語音合成(TTS)0.9sCPU(R5 3500X)
端到端2.8s

2.8 秒,全程離線,在 2014 年的硬體上。如果改成串流輸出(一邊生成一邊合成),還能再壓短。

花了多少錢

我自己是 $0——這張 GTX 970 在一台我早就退役的舊電腦裡,本來就有,整套沒花半毛。真要從零重現,唯一要買的就是那張卡:

項目我的成本
GTX 970(退役舊電腦,本來就有)$0
Ryzen 5 3500X + B450M + 16GB DDR4(本來就有)$0
llama.cpp + Piper$0
Gemma 4 + Piper 聲音$0
總計$0

先講清楚,重點不是叫你去買電子垃圾。二手 970 你真要買大概 NT$500(≈$15 美金),但真正的點是:你家退役電腦裡、或抽屜深處那張卡,搞不好還有一份工可以做。這就是那份「第二春」長什麼樣:一個會看、會聽、會講、會寫 code 的離線助手。

收穫

最花時間的地方

是 VRAM 餘量,不是速度。把一個 3.2GB 的模型跟一個 942MB 的投影層塞進 3.5GB、還留 535MB,這才是整場的重點——再多一個大零件就載不起來了。圖片順序那個怪癖也吃掉不少時間才找到:圖放在文字後面,模型就咬定它沒收到圖。

下次也用得上的判斷

當多模態 benchmark 比文字快,別馬上信——先看你量的是 prefill 還是 decode。少一點 input token,穩穩會加快的是 prompt 處理,不是生成。任何「這個模態比較快」、卻沒把這兩個階段分開的說法,都先存疑。

通用原則

這個 build 裡,GPU 從來不是瓶頸——VRAM 餘量、還有這些零件兜不兜得起來才是。一個小小的多模態模型加上一個純 CPU 的 TTS 引擎,就把一張垃圾卡變成一個完整的助手。難的是把全部塞進記憶體,不是買硬體。

GTX 970 到底做得到什麼

能力狀態速度
英文 / 中文對話47.6 tok/s
Tool use(含平行呼叫)47.6 tok/s
JSON / 程式碼生成47.6 tok/s
圖片理解~51 tok/s(被 prefill 灌水)
語音辨識(中 / 英)✅ / ⚠️ 大致正確~52 tok/s(被 prefill 灌水)
語音合成(TTS)✅ CPU,~18x 即時0.9s
完整語音助手一圈端到端 2.8s

一張 2014 的卡、一個 2026 的模型、一個免費的 CPU TTS 引擎——一個完整的離線多模態語音助手——而且對我來說是 $0,因為那張卡本來就在一台退役的舊電腦裡。不用 5090 才能入門,甚至可能根本不用買:你家櫃子或抽屜裡那張舊卡,說不定就能拿來做這套離線助手,給它一個第二春。


附錄:音訊 API

音訊用 OpenAI 風格的 input_audio content block 傳進去,base64 編碼的 16kHz 單聲道 WAV:

payload = {
    "messages": [{
        "role": "user",
        "content": [
            {"type": "input_audio", "input_audio": {"data": audio_b64, "format": "wav"}},
            {"type": "text", "text": "Transcribe exactly what is said."},
        ],
    }],
}
  • 模態順序: Google 建議圖片放在文字前面、音訊放在文字後面,效果最好。這是建議、不是硬性規定——但我這次把圖片放在文字後面,模型就說它什麼都沒收到。(我的音訊放在文字前面,照樣轉寫成功。)
  • 音訊上限: 最長 30 秒、16kHz 單聲道 WAV。
  • base64 大小: 一段音訊的 base64 很快就會超過 shell 的參數長度上限——用 Python 發請求,不要 curl。
  • 幫 970 編 llama.cpp 加 -DCMAKE_CUDA_ARCHITECTURES=52commit 42a0afd59,2026 年 6 月)。

常見問題

GTX 970 跑得動多模態模型嗎?
跑得動。Gemma 4 E2B 的 QAT Q4_0 權重(3.2GB)加上它 942MB 的多模態投影層,一起塞進 970 約 3.5GB 的可用 VRAM,還剩約 535MB,能吃圖片和語音輸入。
Gemma 4 E2B 看圖真的比純文字快嗎?
不太算。圖片和語音的 tok/s 量出來比較高,但這不代表模型 decode 得比較快。投影出來的 token 比較少,會讓 prompt 處理(prefill)變便宜、也可能讓 KV/attention 的負擔稍微輕一點;但不把 prefill 和生成的計時分開,就不能說那是真的 decode 變快。
怎麼便宜地幫本地 LLM 加上講話能力?
Piper TTS 純 CPU 跑、不吃任何 VRAM,合成速度約是即時的 16–33 倍。把模型的回覆文字 pipe 進去就好。完整的「語音進 → LLM → 語音出」這一圈在 970 上跑大約 2.8 秒。