在一張 RTX 3090 上比較 vLLM 和 TensorRT-LLM 學到的事 —— 大部分是關於「怎麼量」,而不是哪個引擎比較好。
我的 benchmark 第一次跑出來:4,096 個 token 的 prompt,第一個 token 出來的時間(TTFT)只要 42 毫秒。
看起來很棒。然後我算了一下。
Prefill(在第一個 token 出來之前處理整段 prompt)受限於運算量。一般的 dense 模型大約要做 2 × 參數量 × token 數 次運算。以 Qwen3-8B、4,096 個 token 的 prompt 來算:
2 × 8.19e9 × 4096 / 71e12 FLOP/s ≈ 945 毫秒
71 TFLOPS 是 RTX 3090 的 FP16 tensor core 峰值。任何軟體都不可能讓硬體算得比這更多。所以如果 prefill 至少要 945 毫秒,量到 42 毫秒不代表引擎很快,而是代表量錯了。
原因很簡單:我的測試程式每次都送同一個 prompt,vLLM 的前綴快取就直接從快取拿 prefill 的結果。伺服器的 log 寫得很清楚:Prefix cache hit rate: 75.3%。那一輪所有的 TTFT,量到的都是「查快取」,不是真正的運算。
修法:每個請求開頭都加上一個唯一的 ID,讓 prompt 彼此不共用前綴,伺服器也加上 --no-enable-prefix-caching 關掉前綴快取。修完之後,prefill 量到 962.8 毫秒,是理論上限的 98%。
TensorRT-LLM 也有同樣的陷阱:它的 KV cache 區塊重用預設是開啟的。
Decode(一次生成一個 token)受限於記憶體頻寬,因為每一步都要把全部權重重新讀一次:
16.38 GB / 936 GB/s ≈ 每個 token 17.50 毫秒
| 階段 | 受限於 | 理論值 | vLLM | TensorRT-LLM(pytorch) |
|---|---|---|---|---|
| Decode(batch 1) | 記憶體頻寬 | 每 token 17.50 毫秒 | 20.84 毫秒(84%) | 21.04 毫秒(83%) |
| Prefill(4,096 token) | 運算量 | 945 毫秒 | 962.8 毫秒(98%) | 990.8 毫秒(95%) |
兩個引擎都達到硬體能力的 83–98%,已經沒什麼空間讓其中一個贏過另一個。這解釋了下一個結果。
| 同時請求數 | vLLM | TensorRT-LLM(pytorch) | 差距 |
|---|---|---|---|
| 1 | 45.6 tok/s | 45.2 tok/s | −0.9% |
| 8 | 308.3 tok/s | 310.2 tok/s | +0.6% |
| 32 | 764.9 tok/s | 777.7 tok/s | +1.7% |
差距大約 2%。很容易就寫下「兩個引擎一樣快」然後結案。
但我每個引擎都跑了兩次,而且用 A-B-A-B 交錯執行,讓時間造成的飄移不會偏向任何一方。同一個引擎跑兩次,吞吐量的差異中位數是 5.4%。兩個引擎之間的差距,比同一個引擎跑兩次的差距還小。
所以誠實的結論不是「兩個一樣快」,而是「這個環境分辨不出它們的差別」。先量出雜訊有多大,這句話才有意義。(雜訊最可能的來源是溫度:GeForce 顯示卡沒辦法鎖定時脈。)
我的第二個錯誤,變成了最有意思的發現。
我第一版的文章把 TensorRT-LLM 描述成「預先編譯」的引擎。但我測的版本(1.2.0rc1)有兩種 backend:預設的 pytorch 並不會預先編譯,要預先編譯得用 --backend tensorrt。我在解釋一種機制,量的卻是另一種。
所以我兩種都量了,讓「預先編譯」成為唯一的差別:
| 啟動到可以接第一個請求 | |
|---|---|
| vLLM | 96 秒 |
TensorRT-LLM,pytorch backend | 115 秒 |
TensorRT-LLM,tensorrt backend | 1,225 秒(20 分鐘) |
而且預先編譯的引擎並沒有比較快。同時請求數越多,它反而越慢:
| 同時請求數 | pytorch | tensorrt | 差距 |
|---|---|---|---|
| 1 | 44.6 tok/s | 45.7 tok/s | +2.4% |
| 8 | 302.1 tok/s | 279.7 tok/s | −7.4% |
| 32 | 756.1 tok/s | 616.0 tok/s | −18.5% |
Batch 1 時預先編譯的版本快 2.4%,在雜訊範圍內;batch 32 時慢了 18.5%,遠遠超出雜訊。
我認為原因在排程,不在 kernel。pytorch backend 會把新進來的請求立刻加進正在跑的批次;為固定形狀預先編譯好的引擎,在執行時比較沒有彈性去重新安排工作。負載一高,排程比特化的 kernel 更重要。
後來,新版的 TensorRT-LLM 已經把 trtllm-serve 的 --backend 參數標成淘汰,tensorrt 也不再是它的選項。我的數字剛好說明了這個方向為什麼合理。
在這張卡、這個模型上,引擎改變不了硬體已經定死的數字。選哪個是維運上的考量:啟動時間、部署難易度、改東西的速度。
我預期 TensorRT-LLM 只有在離開這個環境時才會拉開差距:
程式碼、原始數據和圖表,一個指令就能全部重跑: github.com/wu840407/llm-inference-benchmark
測試環境:RTX 3090(24 GB、936 GB/s)、Qwen3-8B(BF16)、vLLM 0.27.1、TensorRT-LLM 1.2.0rc1、WSL2。每種設定先暖機三輪、再正式量五輪;每個引擎交錯跑兩次。