iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
AI Engineering

128GB 統一記憶體的三十天:DGX Spark 地端 LLM 與生成式 AI 部署實戰系列 第 10

Day 10|TensorRT-LLM 實測:我花了一個下午,跟它的預設值搏感情

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20260823/20141816RGtV89dLJ2.jpg

前情提要:兩個引擎各贏一半

先把前兩天的戰況說明一下,因為要對齊的緣故,因此基準測試我都先用同一款模型 gpt-oss-120b、同一套提示詞,llama.cpp 與 vLLM 交出的成績單有點微妙,誰都沒有全贏。如果是用其他的最新模型,一些碰到的坑和問題可能就會不一樣,但基準測試我還是會想把它做完,才做比較公平的橫向比較和技術選型建議。

先來看單請求 decode,llama.cpp 以 60.6 t/s 大勝 vLLM 的 34.3 t/s,服務機制的固定成本在單人場景無處攤提,昨天的預期被證實,而且差距比想像中大。不過單請求 prefill 卻反過來,vLLM 的 pp2048 約 4,100 t/s,是 llama.cpp(約 1,900,預設批次設定)的兩倍以上。至於同時處理,vLLM 峰值總吞吐 126 t/s 直接噴發,day 8 文章寫到的 llama.cpp 在這一項當然是輸慘,連參賽資格都沒有。

一個贏 decode,一個贏 prefill 與同時處理。那麼號稱把硬體榨到最後一滴的 TensorRT-LLM,照理說應該兩邊都要贏度吧?今天就是來驗證這個照理說的,這邊先劇透一下心情,它是這三個引擎裡唯一一個讓我整個下午都在重開機的,總共七次。值不值得,看到最後你自己評量囉。

部署光是選容器就比別家推論引擎麻煩

21 個 tag,能用的只有一個

TensorRT-LLM 在 Spark 上同樣有 NGC 官方容器,聽起來很安心對吧。點開一看,這個 repo 底下躺著 21 個 tag。我實際比對了三個候選的中繼資料,結果長這樣:

tag TRT-LLM CUDA 需要驅動 結果
spark-single-gpu-dev 1.1.0rc3 12.9.1 575.57 可用
gpt-oss-dev 1.0.0rc5 12.9.0 575.51 可用但較舊
1.2.1(最新穩定) 1.2.1 13.1.0 590.44 出局

我的機器上 NVIDIA 驅動程式版本是 580.173.02,而最新穩定版要求 590 系。也就是說,在這台機器上,「儘量裝最新版」這個多年的習慣是錯的,乖乖用舊版反而是正解,這件事違反直覺到我查了兩次才發現。

好消息是 spark-single-gpu-dev 這個名字本身就在對你眨眼,NVIDIA 有替 DGX Spark 準備專門的建置,arm64、TensorRT 10.11、PyTorch 2.8.0a0,匿名就能拉取下載,解開後 37.1 GB:

docker pull nvcr.io/nvidia/tensorrt-llm/release:spark-single-gpu-dev

支援清單裡沒有 GB10,但還是可以用

下載後第一件事,看它替哪些 GPU 架構編譯過:

CUDA_ARCH_LIST       = 8.0 8.6 9.0 10.0 12.0
TORCH_CUDA_ARCH_LIST = 8.0 8.6 9.0 10.0 12.0+PTX

眼尖的讀者發現了,清單裡沒有 12.1,而 GB10 正是 sm_121。這道牆我們在 Day 8 和 Day 9 都撞過,已經是老朋友了。

不過 TensorRT-LLM 選擇用別的方式來過關,而不是無法啟動,啟動時看訊息,它自己有說:

flashinfer.jit: Prebuilt kernels not found, using JIT backend

預編譯 kernel 沒有 sm_121 的份,好吧,那就現場編,精神可嘉,但代價是可預期的,光一個 RMSNorm 就要編 12 秒。所以這裡送你一個省時撇步,JIT 快取目錄一定要掛成 volume,不然容器一關,下次重啟又要重付一次編譯時間給它,我就是付了好幾次學費才知道:

-v trtllm-flashinfer-cache:/root/.cache/flashinfer

六個小問題,有兩個會說「平台不支援」

以下是我這個下午實際碰過的問題,按心理受創面積排列。前兩個比較麻煩且事後才會發現,因為它們的錯誤訊息會讓你以為是平台問題,然後心灰意冷地關機。
https://ithelp.ithome.com.tw/upload/images/20260823/20141816eN9XvKxjpg.png

問題一,用 --entrypoint python3 會拿到假的相容性錯誤。 ImportError 說找不到 libnvinfer.so.10,看起來像 TensorRT 根本沒裝好,其實檔案好端端躺在那裡。真相是映像的 LD_LIBRARY_PATH 是 bash 啟動時才補上的,你繞過 bash,它就沒機會補。改用 --entrypoint bash <image> -c 'python3 script.py' 就好。

問題二,Python 腳本忘了加 if __name__ == "__main__" 護欄,直接 MPI_ABORT。 TRT-LLM 用 MPI spawn worker,worker 會重新 import 你的主模組,沒有護欄的話,每個 worker 都熱情地想自己建一份 LLM,然後同歸於盡。

問題三,fp8 KV cache 在這個組合下就是不能用。 這個不是我不會用,是真的被擋:

Assertion failed: KV cache data type e4m3 is not the same as input data type bf16.

gpt-oss 的 checkpoint 把注意力層排除在 MXFP4 之外,注意力跑 bf16,而這版的 FMHA 派送器堅持 KV 型別必須跟輸入一致。重點來了,昨天 vLLM 開 fp8 KV 是成功的,所以這不是模型的錯,是兩個引擎之間真實的能力差距,這件事的後果在記憶體那一節。

問題四,CLI 參數會蓋掉 YAML。 我在 YAML 裡誠心誠意寫的 max_num_tokens,它看都沒看,跑的是 CLI 預設值。這類參數請直接用 CLI 傳。

問題五,cuda_graph_config 的 batch_sizes 與 max_batch_size 不能同時設。 10 秒就噴錯,但兩個欄位看起來實在太像可以互補了,記一筆免得你也順手都填。

問題六,/metrics 開了還是空的。 這部分留到後面稽核那節再說好了。

啟動指令定案版

七次重啟的學費換來的最終版本:

docker run -d --name trt-serve --gpus all --network host --ipc host --shm-size 16g \
  -v /mnt/nas/AIModels/huggingface:/models:ro \
  -v $PWD:/day10:ro \
  -v trtllm-flashinfer-cache:/root/.cache/flashinfer \
  --entrypoint bash nvcr.io/nvidia/tensorrt-llm/release:spark-single-gpu-dev -c "
    trtllm-serve serve /models/gpt-oss-120b \
      --backend pytorch \
      --host 127.0.0.1 --port 8001 \
      --max_seq_len 32768 \
      --max_batch_size 32 \
      --max_num_tokens 2048 \
      --extra_llm_api_options /day10/extra-llm-api-options.yaml \
      --log_level info"

搭配的 YAML:

enable_chunked_prefill: true
enable_iter_perf_stats: true

# 預設會替 [1..32, 64, 128] 都捕捉 CUDA graph,砍掉實際用得到的清單
cuda_graph_config:
  batch_sizes: [1, 2, 4, 8, 16, 32]

kv_cache_config:
  free_gpu_memory_fraction: 0.7
  enable_block_reuse: true

眼尖的話會發現 dtype: fp8 不在裡面,確實在這邊不能用。

載入時間與記憶體水位

項目 調整前(fraction 0.5、CUDA graph 全捕捉) 定案設定
就緒耗時(含 JIT 編譯) 382 s 372 s
記憶體剖析峰值 119.60 GiB 116.40 GiB
分給 KV cache 1.30 GiB 3.76 GiB
KV 容量 18,912 token 54,784 token
實際生效的 max_seq_len 被砍到 18,912 32,769,未被砍

(裝置總量 121.63 GiB。冷快取首次啟動的 Model init 是 400 秒級,掛上 flashinfer 快取 volume 後穩定在 320 到 335 秒。)

https://ithelp.ithome.com.tw/upload/images/20260823/20141816zHDG2co7Y7.png

對照 Day 9 的 vLLM,就緒 681 秒、KV 16.48 GiB 相當於 849,942 token。看出詭異之處了嗎,它啟動比 vLLM 快將近一倍,KV 卻只有人家的十幾分之一。這不是筆誤,這是今天覺得最奇怪的謎題,本文下方記憶體那邊會說明。

先劇透一個數字讓你感受預設值的殺傷力:光是把 CUDA graph 捕捉清單從預設砍到實際用得到的六個,KV 就多了 2.9 倍,max_seq_len 也不再被默默調降。在這台機器上,預設值需要多確認與查找資料。

單請求對照的三引擎同框

方法論同 Day 8。附註一下,llama.cpp 的 prefill 是預設批次設定(-ub 512)的成績,開 -ub 2048 後 pp8192 可推到約 2,390 t/s,完整的參數對齊版明天 day11 的總表會有處理:

測項 llama.cpp vLLM TensorRT-LLM vs vLLM
prefill pp2048(t/s) 1,907 4,093 4,912 1.20 倍
prefill pp8192(t/s) 1,852 3,548 6,278 1.77 倍
decode(t/s) 60.6 34.3 32.3 0.94 倍

(量測方法與 Day 9 逐字相同。pp8192 那格因 KV 容量觸發逐出、無法完全排除快取重用,數字標註這邊需要先說明只能保留喔,詳見本文最下方的附錄。)

https://ithelp.ithome.com.tw/upload/images/20260823/20141816roHHkScUql.png
今天文章開頭說那個照理說兩邊都要贏的預測,答案揭曉:錯了。XD

prefill 那半確實贏了,贏的原因值得探究,TRT-LLM 的特點是提示詞越長贏越多,pp2048 快 20%,pp8192 快 77%。更漂亮的是它自己的走勢,vLLM 從 pp2048 到 pp8192 是往下掉的(4,093 到 3,548),TRT-LLM 居然是往上飛的(4,912 到 6,278)。長提示詞把它的 kernel 餵得更飽,這才是編譯最佳化該有的樣子。

然後是 decode。32.3 t/s,比 vLLM 還低 6%,只有 llama.cpp 的一半。 輸就算了,還輸給它理論上應該輾壓的對手。原因說穿了很簡單,TRT-LLM 背的服務機制固定成本比 vLLM 更重,而 decode 恰好是最沒空間能攤提這種成本的場景。

所以如果把 Day 8 的驗算習慣拿來再檢視一遍。decode 理論天花板估算 76.0 t/s,llama.cpp 樸實無華地拿到 79.8%,TRT-LLM 帶著全套編譯最佳化,拿到 42.5%。這個百分比的差距,其實就是花時間弄它值不值得的誠實豆沙包,而它給的答案很殘酷,在 decode 這段路上,那些最佳化沒有把頻寬用起來。

同時處理對照看看踢館 vLLM 的主場會如何 ?

同一套同時處理量測打向 trtllm-serve,結果又如何呢?

同時處理 vLLM 總吞吐 TRT-LLM 總吞吐 比值 TRT-LLM 每人 TRT-LLM P99 TTFT
1 30.00 29.25 0.97 29.25 0.42 s
4 64.44 58.75 0.91 14.69 1.59 s
8 81.40 80.01 0.98 10.00 3.17 s
16 101.48 103.73 1.02 6.48 6.30 s
32 125.98 115.13 0.91 3.60 27.74 s

結果這一場是平手。五級裡四級落在 0.91 到 1.02 之間,連一格值得寫分析的差距都沒有。單請求 prefill 快 77% 的那位猛將,一進這個世界,就跟 vLLM 手牽手走在同一條線上惹。想通了其實不意外,這種場景的應用,其瓶頸從算力換成頻寬,而頻寬是硬體給的,編譯最佳化再厲害也搬不動它。

表格中有一格比較可以看的。同時處理 32 的 P99 TTFT 是 27.74 秒,vLLM 是 16.15 秒,這格 TRT-LLM 慘敗。它的原因並不在引擎快慢,而是 KV 裝不下。32 筆請求需要約 69,632 個 token 的 KV,它口袋裡只裝得下 22 筆,剩下 10 筆在後面排排隊,所以尾端延遲就是這樣被拉出來的。引擎的能力差距,最後以排隊的形式呈現,下一節講記憶體的部分也會對應到喔。
https://ithelp.ithome.com.tw/upload/images/20260823/20141816ZZdWit1EvT.png

Day 9 的務實判準是每人生成不低於閱讀速度、尾端等待不超過四秒。vLLM 那天的結論是多人場景建議 8 人就好,TRT-LLM 用同一把尺量出來,也是同時處理 8(每人 10.00 t/s、P99 3.17 秒)。同時處理 16 每人剩 6.48 t/s、P99 逼近 6.3 秒,其實就不太能這麼多人用,如果硬是要用 16 人,請還記得加上用戶可能會問一堆更常上下文的問題,尖峰時間可能會更慢。兩個引擎、同一個答案,這台機器的服務甜蜜點看來是個物理常數,記憶體的空間和算力的限制,所以有的小公司會買二台,平行算就很好用了。

記憶體用法差異,不同推論引擎呈現完全不同的性格

昨天說 vLLM 是自我保護型人格,看到權重逼近可用記憶體就自動放棄預取,乖巧謹慎。TRT-LLM 是另一種人,而且性格差異大到要分三段小小說明一下。
https://ithelp.ithome.com.tw/upload/images/20260823/201418161nACQqL8Zq.png

第一,它的旋鈕跟 vLLM 的旋鈕,字面像、意思完全不同。 vLLM 的 gpu-memory-utilization 分母是 GPU 總記憶體,TRT-LLM 的 kv_cache_free_gpu_memory_fraction 分母是「載入權重與 buffer 之後剩下的」。把 0.70 照抄過去,得到的是完全不對等的設定。我原本的如意算盤是直接釘住 Day 9 的 KV token 數,讓兩邊排程空間一致,這個計畫在第一次啟動就陣亡了,原因往下看。

第二,它不吃 gpt-oss 的滑動窗分流。 Day 8 發現 gpt-oss 宣告 sliding_window 128,36 層裡交替一半只看前 128 個 token,等於只有 18 層需要吃滿上下文,llama.cpp 有乖乖照做,vLLM 看起來也有。TRT-LLM 1.1.0rc3 就沒有,沒設 max_attention_window 時 36 層通通給滿 32768 的視窗,啟動日誌印出的每 token KV 成本 73,728 B,正好就是 36 層全算的數字。我們來看看這二款地端 AI 推論引擎平台的比較:

每 token KV 相對 vLLM
vLLM(fp8、有分流) 20,819 B 1.00 倍
TRT-LLM(fp8、36 層) 36,864 B 1.77 倍
TRT-LLM(bf16、36 層) 73,728 B 3.54 倍

而坑三讓 fp8 那列用不了,所以實際付的是最貴的 bf16 成本。要裝下 Day 9 的 849,942 token 得花 58.3 GiB,加上權重,這台機器裝不下。對齊 KV 容量在這裡不是設定問題,是物理問題,我的如意算盤就是這樣沒的。

第三,真正的壓力在啟動時的記憶體剖析。 剖析峰值吃掉 119.60 GiB,全機只剩 2 GiB 喘息空間。權重才佔 59 GiB,代表剖析過程另外堆了 60 GiB 的暫時配置,其中一大塊是 CUDA graph 捕捉,而它預設連 64、128 這些根本用不到的批次都要捕一輪。後果是連鎖的,KV 只分到 1.30 GiB,然後:

Attention window size 32769 exceeds upper bound 18912 for available blocks. Reducing to 18912.
Adjusted max_seq_len to 18912

這兩行的意義是,我要求 32768 的上下文,它默默改成 18912,然後繼續跑,都不回報的。 這是今天最想筆記下來的重點,不看啟動日誌的話,你會一直以為自己跑在 32K 上下文。

好消息是這功能還是救得回來。只要把 CUDA graph 清單砍到剩下我們實際會用得到的六個,峰值只降 3.2 GiB,KV 卻多了 2.9 倍。為什麼槓桿這麼大,因為 KV 拿的是剩下那一點的比例,在只剩 2 GiB 的地方,每省 1 GiB 都是等比放大的。這也是統一記憶體機器的調參手感跟獨立顯卡完全不同的原因,你不是在分配記憶體,你是在跟剖析階段的峰值搶最後幾 GiB

有沒有真的快取到呢 ? Day 9 的陷阱在我眼前重演

Day 9 留下一條鐵律,同一組 seed 重跑會讓 prefix cache 命中,把 prefill 數字變好看而不是變錯,結果今天自己又踩了一次。

Phase 0 的三次握手測試都用寫死的 seed,同樣 2048 個輸入 token,TTFT 從 686 ms 一路快到 64.75 ms,換算 prefill 是 31,600 t/s。這數字要是真的,NVIDIA 可以直接發新聞稿了。 XD
它當然是假的,只是把前兩趟算好的東西再讀一次。唯一的安慰是,這順便證明了 enable_block_reuse 確實在運作。

正式量測時,因此每一級都換 seed。但麻煩的是這版 TRT-LLM 的命中率計數器打不通,/metrics 設了開關仍回空(也就是上面提到的問題六)。最後打通一半的方法是啟動時開 kv_cache_events 事件緩衝,數每一級 stored 的 block 數,跟零重用時理論上該有幾個去對比。同時處理 1、4、8 三級分毫不差,零重用確認,pp2048 也乾淨,pp8192 與同時處理 16、32 無法完全證實(詳見附錄)。

這種代用稽核的原則,它證明得了乾淨,證明不了髒。 數字對得上就是零重用,對不上就得另外判斷。

值不值得用 TensorRT-LLM ?

理論上 TensorRT-LLM 是強大的,但實際上它的部署耗時,llama.cpp 約幾分鐘、vLLM 十幾分鐘、TRT-LLM 花了我一整個下午,光啟動就重來七次(映像選錯、fp8、KV 設定兩次、CUDA graph、稽核開關、正式量測),每次 6 到 7 分鐘的權重載入都要重付一次,那個下午有很嚴重的作業感。

換到的效能:

對 vLLM 對 llama.cpp
prefill pp2048 +20% +157%
prefill pp8192 +77% +231%
單請求 decode -6% -47%
同時處理峰值吞吐 -9% 不適用

我的判斷,這台機器上 TensorRT-LLM 只有一個場景值得。

那個場景是長提示詞、單請求、prefill 佔成本大宗的工作。RAG 的檢索段、整份文件的摘要、程式碼庫的一次性分析,這類工作的錢幾乎全花在 prefill 上,而它 pp8192 快 77% 是實打實的,也是三個引擎裡唯一提示詞越長跑越快的。

除此之外我不會選它。要服務一群人?TensorRT-LLM 成績和 vLLM 類似,而 vLLM 部署只要一行指令就緒、KV 大十幾倍、尾端延遲又好上一截。一個人自己用?那就選 llama.cpp ,其 decode 是 TensorRT-LLM 的 1.9 倍,十分鐘裝好,連下午茶都來得及。
https://ithelp.ithome.com.tw/upload/images/20260823/20141816agSE9T35Hw.png

最誠實的一句話還是那個 42.5%。 llama.cpp 用最樸素的方式拿到 79.8% 的理論達成率,TensorRT-LLM 帶著整套編譯最佳化只拿到 42.5%。在這台機器、這款模型、這個版本上,那些最佳化沒有花在 decode 上,而 decode 偏偏是單機使用者感受最深的那段路。

最後,說一件跟效能無關但我覺得很值得思考的事情吧。前兩個引擎我是在調參數,這個引擎我是在跟它的預設值搏感情。 21 個 tag 只有一個能用、最新穩定版因驅動出局、fp8 KV 被 FMHA 擋下、滑動窗分流沒被實作、YAML 被 CLI 蓋掉、CUDA graph 替用不到的批次捕捉還默默把上下文砍到 18,912、/metrics 開了還是空的。
這些都不是效能問題,是「你得先知道它會這樣」的問題。而它有一個具體的量化,砍掉 CUDA graph 用不到的批次,KV 多 2.9 倍、上下文從 18,912 回到 32,769。同一台機器、同一個模型、同一款推論引擎,差別只在有沒有回頭看那行啟動日誌。

明天預告

三個引擎的測試資料都到齊了,Day 11 會整理成完整的綜合測試總表,讓設定值對齊、同表比較、各場景的選型結論陸續列出,還會加碼一個神秘嘉賓給大家參考看看囉。

我們 Day 11 見吧。


附錄:Day 10 的測試紀律與已知偏離

與 Day 9 完全對齊的部分:客戶端沿用 vllm bench serve(不換工具,避免比到量測方法而不是引擎)、單請求與同時處理的請求數與 warmup 設定相同、每級 seed 為 4200 加同時處理數、級間冷卻 120 秒、max_seq_len 同為 32768、max_batch_size 同為 32、chunked prefill 皆開啟且切塊同為 2048。

已知偏離,Day 11 我會加註在總表

  1. KV dtype 不同。 vLLM 用 fp8_e4m3,TRT-LLM 只能用 bf16,因為 FMHA 派送器拒絕 KV 與輸入型別不一致。
  2. KV 容量差 17.5 倍。 vLLM 849,942 token,TRT-LLM 48,480 token,來源是每 token 成本 20,819 B 對 73,728 B,加上剖析峰值吃掉的記憶體。
  3. 同時處理 32 那一格性質不同。 vLLM 全部併批,TRT-LLM 只裝得下 22 筆,其餘在排隊,這一格不能當成純粹的引擎吞吐比較。
  4. 同時處理 16 與 32 的快取稽核缺失。 事件緩衝在該級內被沖掉,乾淨程度只有每級換 seed 這個結構性保證。pp8192 那格同樣標註保留。

Day 1|為什麼 2026 年是地端 AI 部署元年:系列規劃與硬體總覽
Day 2|DGX Spark GB10 深度解析:128GB 統一記憶體到底解決了什麼問題
Day 3|網路與儲存規劃:10GbE 骨幹、雙 Spark 直連與 NFS 集中模型庫
Day 4|開箱之後:DGX OS 初始環境建置與 CUDA、Docker 生態確認
Day 5|NFS 模型庫實戰:下載工具、權限設計與版本管理
Day 6|llama.cpp、vLLM、TensorRT-LLM 、Ollama、DS4 與 Unsloth 的定位與取捨
Day 7|第一個模型上線:gpt-oss-120b 從模型庫到 API 的完整流程
Day 8|llama.cpp 實測:先定量尺與 SOP
Day 9|vLLM 實測:官方容器、同時處理吞吐曲線,與剛出爐的新模型 Ornith 1.5
Day 10|TensorRT-LLM 實測:我花了一個下午,跟它的預設值搏感情
Day 11|地端 AI 三大推論引擎綜合評測,同場加映神秘嘉賓
Day 12|量化格式解析:「4-bit」兩個字,古今多少事 ? 都付笑談中
Day 13|Perplexity 把整套 Agent 搬上 DGX Spark:地端元年的官方背書
Day 14|Qwen3.8-27B 與 Flash-Next 加碼實測:地端模型世代對決與落地評估
Day 15|Ornith 1.5 實測:為 AI agent 而生的 35B-A3B
Day 16|模型選型方法論:五個問題幫你跳脫排行榜迷失,找到適合自己任務用的 AI 模型


上一篇
Day 9|vLLM 實測:官方容器、同時處理吞吐曲線,與剛出爐的新模型 Ornith 1.5
下一篇
Day 11|地端 AI 三大推論引擎綜合評測,同場加映神秘嘉賓
系列文
128GB 統一記憶體的三十天:DGX Spark 地端 LLM 與生成式 AI 部署實戰16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言