自 2022 年起,我在工作中負責把 Kaldi、Whisper 這類 speech recognition 技術與 GPU runtime 接進實際的系統,範圍包含 model inference、system deployment 與後續的 performance maintenance。

這篇不是某個專案的完整案例,不會寫內部架構、資料內容或效能數據。我想整理的是自己面對這類工作的方式:怎麼看待 Kaldi 與 Whisper 的差異、為什麼我會先把 GPU runtime 弄穩,以及部署之後會固定檢查哪些東西。文中的技術說明以官方文件為準,踩坑經驗則來自我過去公開在 blog.clarence.tw 的文章。

Speech AI 在產品中的位置

我在 我的技術工作方式 提過,LLM 與 Speech AI 不只是呼叫一個 model API,而是要和 batch processing、local model、GPU runtime 與既有 backend workflow 串在一起。

語音辨識在產品裡通常只是其中一段。一般來說,前面會有音訊取得、格式轉換與切段,後面會有文字後處理、儲存、搜尋,或交給其他服務繼續使用。所以我評估一個 Speech AI 元件時,除了辨識結果本身,也會一起看:

  • 輸入音訊的格式與品質是否一致,前處理放在哪一層。
  • 辨識是即時串流還是批次處理,失敗時怎麼 retry。
  • 模型輸出要怎麼驗證,哪些情況需要人工確認。
  • 這個 runtime 能不能在另一台機器上被重複建出來。

這種「同一個工具在不同內容上表現差很多」的感覺,我很早就有。2019 年 Amazon Transcribe 剛支援簡體中文時,我試用並寫了心得,當時拿歌曲測試的效果不太好,換成以對話為主的影片就好很多。不論是 managed service 還是自己部署的模型,都要用接近實際情境的音訊來判斷。

Kaldi 與 Whisper 的特性與取捨

Kaldi 與 Whisper 在我的工作中都會用到。我不把它們看成誰取代誰,而是兩種設計思路很不一樣的工具。

Kaldi

依照 Kaldi 官方說明,Kaldi 是以 C++ 撰寫、採 Apache License v2.0 的 speech recognition toolkit,定位是給 speech recognition 研究者使用。它的特色包括:

  • 以 finite state transducer(FST)為核心,使用 OpenFst。傳統的 decoding graph 是 HCLG,由 HMM、context dependency、lexicon 與 grammar / language model 組合而成。
  • 提供各種資料集的 recipes,從資料準備、訓練到解碼都有完整流程可以參考。
  • 提供 online decoding 的函式庫,也在 src/cudadecoder 提供 batched CUDA decoding 的實作。

對維運來說,Kaldi 的好處是元件拆得很細,lexicon、language model 與 acoustic model 各自可以調整;代價是建置流程長、需要的語音專業知識多,環境也比較容易因為編譯參數或相依套件不同而出問題。

Whisper

依照 openai/whisper 的說明,Whisper 是一個 Transformer sequence-to-sequence 的通用語音辨識模型,以大量且多樣的音訊訓練,可以做多語言轉寫、翻譯成英文與語言辨識,程式碼與模型權重採 MIT License。

部署時比較需要注意的幾點:

  • 模型有 tiny、base、small、medium、large 與 turbo 等尺寸。官方列出的 VRAM 需求大約從 1 GB 到 10 GB,速度也差很多,選模型時要和 GPU 資源一起考慮。
  • 需要 ffmpeg;transcribe() 會以 30 秒的 sliding window 處理整段音訊。
  • turbo 是 large-v3 的最佳化版本,速度快但沒有針對翻譯任務訓練,需要翻譯時要改用多語言模型。
  • 官方 model card 提醒,模型可能產生音訊裡沒有說的內容(hallucination)、可能重複輸出,在不同語言、口音與方言上的表現也不一致,並且不建議用在高風險的決策情境。

怎麼選

面向KaldiWhisper
設計方式多個元件組成的 pipeline(acoustic model、lexicon、language model、FST decoding)端到端的 Transformer 模型
上手成本高,需要理解 recipe 與建置流程低,安裝套件後就能轉寫
可調整性可以分別調整詞彙、language model 等元件主要透過模型尺寸、解碼參數與前後處理調整
多語言依訓練資料與 recipe 而定內建多語言轉寫、翻譯成英文與語言辨識
需要特別注意編譯環境、相依套件、模型與 graph 的版本管理VRAM、長音訊處理、hallucination 與重複輸出

我的原則很簡單:先用接近實際情境的音訊比較,再看部署與維護成本,而不是只看公開 benchmark。

我對 Kaldi 的公開貢獻

我在 Kaldi 的公開貢獻是一個小修正:kaldi-asr/kaldi#4657,於 2021 年 12 月合併。當時從 Gitpod 開啟 Kaldi 會出現找不到 .gitpod.dockerfile 的錯誤,原因是設定檔裡的檔名大小寫和實際的 .gitpod.Dockerfile 不一致。這個 PR 修正了檔名,移除 Dockerfile 裡不存在的使用者與用不到的路徑,並固定 Intel MKL 的安裝版本。

修改的範圍不大,但它也是我對這類工具一直很在意的事:開發與執行環境要能被別人重複建起來。

GPU runtime 要先穩:driver、CUDA、container

不論用 Kaldi 還是 Whisper,只要用到 GPU,模型之下就有一整層 runtime。我會把它拆成幾層來看,出問題時也按照這個順序往下查:

  1. 硬體與韌體設定:GPU 是否被系統認到、Secure Boot 是否啟用。
  2. Kernel 與 NVIDIA kernel module:module 是否針對目前的 kernel 建好、是否被允許載入。
  3. NVIDIA driver:nvidia-smi 能不能正常回應。
  4. CUDA 與相關函式庫:CUDA runtime、cuDNN 等版本是否和 driver、framework 相容。
  5. Container runtime:container 裡能不能看到 GPU。
  6. 應用與模型:PyTorch、Kaldi build 與模型檔本身。

Driver 與 CUDA 的版本關係

依照 NVIDIA 的 CUDA Compatibility 文件,driver 與 CUDA toolkit 可以分開更新。從 CUDA 11 開始有 minor version compatibility:同一個 major 版本內編譯的應用,只要 driver 達到最低版本(例如 CUDA 12.x 需要 525 以上、CUDA 11.x 需要 450 以上)就可以執行,但部分需要較新 driver 的功能會受限,把 device code 編成 PTX 再 JIT 的應用也不適用。

實務上的意思是:先決定 driver 版本,再決定上面能跑哪些 CUDA 版本,而不是每次升級 framework 都跟著動 driver。

用 container 隔開 CUDA 與 framework

NVIDIA Container Toolkit 的安裝前提是 host 上已經裝好 NVIDIA driver。以 Docker 為例,安裝套件後執行:

BASH
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

nvidia-ctk 會修改 host 上的 /etc/docker/daemon.json,所以這一步應該納入設定管理。接著用官方的 sample workload 確認 container 看得到 GPU:

BASH
sudo docker run --rm --runtime=nvidia --gpus all ubuntu nvidia-smi

這樣分層之後,host 主要負責 driver 與 container toolkit,CUDA、cuDNN 與 framework 則跟著 image 走。不同服務可以使用不同的 CUDA 版本,升級與 rollback 也變成切換 image,而不是在 host 上反覆安裝與移除套件。

我在部落格記錄過的問題

以下是我過去公開記錄的 GPU 環境問題。它們不一定發生在同一個環境,但很能說明為什麼 runtime 要分層管理:

現象原因與處理方向學到的事
nvidia-smi 無法和 driver 溝通Kernel 更新後 driver module 沒有重建,用 DKMS 為新 kernel 重新建立Kernel 更新也是 GPU 環境的變更
Secure Boot 下 driver 無法載入DKMS 在本機編譯的 module 沒有簽章而被拒絕,改用 Ubuntu 預先簽署的 linux-modules-nvidia-* 套件不必為了 driver 關掉 Secure Boot
nvidia-smi 顯示 No devices were found套件安裝的 driver 無法初始化 GPU,改用 NVIDIA 官方 installer 重裝,並處理 kernel 與 GCC 版本不一致安裝失敗時先看 kernel log 與 installer log
CUDA 套件相依無法安裝圖形介面占用 driver,需要切到 multi-user.target 再移除與重裝更新 driver 要排停機時間
dpkg 覆寫檔案錯誤不同版本的 NVIDIA 套件互相覆寫同一個檔案同一台機器的 driver 套件來源要一致
DGX 出現 NO_PUBKEYNVIDIA CUDA repository 更換簽章金鑰,更新 key 後恢復外部 repository 也會改變,更新失敗要先看訊息
cuDNN 驗證範例編譯失敗範例程式需要額外的 FreeImage 函式庫驗證步驟本身也有相依套件

另外,CUDA Toolkit 11.7 的安裝方式(deb local、deb network、runfile)與 darknet 指定 GPU 編號(CUDA_VISIBLE_DEVICES 與 framework 自己的設定)這兩篇,也是我在規劃 GPU 環境時常回頭看的基本功。

部署與效能維運的檢查清單

以下是一般性的檢查項目,實際內容會依環境調整。

部署前

  • 記錄完整的版本組合:OS、kernel、NVIDIA driver、CUDA、cuDNN、framework、模型版本與 image tag。
  • 確認 Secure Boot 狀態(mokutil --sb-state),決定 kernel module 要用預先簽署的套件還是自行簽署。
  • 決定 kernel 與 driver 的更新策略,避免自動更新在沒人注意時改變 GPU 環境。
  • 依模型尺寸與預期的併發量估算 VRAM,決定每張 GPU 跑幾個 process,以及用 CUDA_VISIBLE_DEVICES 怎麼分配。

部署後驗證

  • Host:lsmod | grep nvidia 確認 module 已載入,nvidia-smi 正常回應。
  • Container:用 sample workload 確認 container 內看得到 GPU。
  • 應用:用一小段固定的測試音訊跑完整流程,確認輸出格式與內容合理。
  • 重開機後再驗證一次,確認 driver 與服務都會自動恢復。

日常觀察

  • 用 nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv 這類指令,或接進 monitoring,持續觀察 GPU 使用率與記憶體。
  • 觀察處理時間與佇列長度,區分是 GPU 不夠、前處理太慢,還是 I/O 卡住。
  • 留意辨識結果的異常,例如大量重複文字或明顯不屬於音訊的內容。

有風險的操作

  • apt purge nvidia*、切換 multi-user.target、移除或替換 kernel module 都會讓 GPU 服務中斷,應該排在維護時段,並先確認有回復方式。
  • nvidia-ctk runtime configure 會改寫 Docker daemon 設定,要和既有設定一起 review。
  • 不要為了省事直接關閉 Secure Boot;如果真的需要,要先評估安全影響。

學到的事

  • 模型只是其中一層。 Speech AI 能不能長期穩定運作,很大一部分取決於 driver、kernel、CUDA 與 container 這些底層有沒有被管好。
  • Driver 留在 host,其他交給 image。 把 CUDA 與 framework 放進 container,升級與 rollback 都會單純很多。
  • 用自己的情境驗證。 Kaldi 與 Whisper 各有取捨,公開 benchmark 只能參考,最後還是要用接近實際情境的音訊來比較。
  • 把踩過的坑寫下來。 我在部落格記錄的每個 GPU 問題,後來都變成部署前後的檢查項目。