請求不等齊才出發:推論服務裡的連續批次處理

上週有讀者問:為什麼同樣一張 GPU,別人跑的請求量是你們的三倍?模型一樣、量化方式一樣、顯示卡也一樣。差距往往不在模型,而在排程。

專屬插圖
請求不等齊才出發:推論服務裡的連續批次處理

請求不等齊才出發:推論服務裡的連續批次處理

上週有讀者問:為什麼同樣一張 GPU,別人跑的請求量是你們的三倍?模型一樣、量化方式一樣、顯示卡也一樣。差距往往不在模型,而在排程。

先說清楚為什麼要批次處理

推論每執行一次 forward,都要把整個模型的權重從 VRAM 讀取一遍。一張 70B 的模型權重大約 140GB(fp16),單一請求計算 1 個 token,就是把這 140GB 搬動一遍只換回 1 個 token,算力全在等待記憶體頻寬。兩條請求一起算,權重還是只搬動一次,產出翻倍。批次處理的第一性原理就在這裡:權重讀取是固定成本,token 是邊際產出,批次越大,分攤成本越薄。

代價是以延遲換取吞吐量:一條請求的回應時間會隨著同批次請求數慢慢爬升。所以批次處理策略的核心問題不是「批次多大」,而是「誰跟誰同時計算」。

靜態批次處理的死穴

早期推論服務常用靜態批次處理:湊夠 N 條請求(或等到超時)一起送進 GPU,算完這一批,再湊下一批。

問題出在「一起送進」就意味著「一起結束」。假設一個批次裡有 4 條請求,最短的生成 50 個 token,最長的要 800 個 token。最短的那條在 50 步之後其實早就能回傳了,但它必須陪著整批等 800 步跑完。GPU 在替三條較快的請求繼續算第 800 步,產出的 token 全是它們真正需要的(每條只需 1 step per token),算力被白白浪費。

而且「湊夠 N 條」在新請求到來時是僵化的:一條新請求哪怕只等 20 毫秒就能上車,也得站在路邊等當前這班車開完,往往是幾百毫秒到幾秒。

這兩筆帳加起來,就是靜態批次處理典型的 GPU 利用率 10%~30%。

連續批次處理:按步換人

連續批次處理(continuous batching,也叫 in-flight batching)把「批次」從請求佇列降級到 token 步級別。每一步 forward 之前,排程器看一眼:誰上次已經生成完 EOS 了?把它踢出當前批次,騰出的 KV cache 顯存立刻回收;佇列裡有新請求?把它塞進這一批,共享同一個 forward 的算子啟動開銷。

效果有三層:

1. **短請求不再陪跑**。50 token 的請求 50 步就回傳,剩下 750 步的顯存和算力歸新請求用。

2. **新請求不等班**。起步延遲從「整批空檔」壓到「當前步結束」,通常幾十毫秒量級,體感上服務變快了,排隊少了,P99 延遲顯著下降。

3. **批次大小天然彈性**。GPU 同一時刻在算的其實是「當前這批」,可能 64 個請求,每步都在增減,利用率跟著負載走,而不是跟著定時輪詢的運氣走。

代價是記憶體管理變複雜:KV cache 要按請求粒度分配和釋放,排程器要維護每步的「上車名單」。這正是 vLLM 的 PagedAttention 要解決的事——把 KV cache 切成固定大小的塊,像作業系統管理虛擬記憶體頁一樣使用,碎片和預配置問題一起消除。

實作建議

- 查一下你在跑的引擎和排程模式。TF-Server 預設靜態批次;vLLM、SGLang、TensorRT-LLM 預設連續批次。如果你的 P99 尾延遲高、GPU 利用率卻始終上不去,先懷疑排程而不是模型。

- 批次大小別設成信仰參數。連續批次下更值得盯的是 `max_num_seqs` 和 KV cache 總量:前者限制併發請求數,後者限制能同時塞進顯存的 KV 總量。併發上不去多半是 KV 顯存先到頂,增加請求數沒用,要麼減小上下文上限,要麼用量化 KV。

- 長短請求混跑時,給長請求設 `max_tokens` 上限。一條失控的 4K token 長回覆能拖住一批人的整體周轉,設上限是保護,不是限制品質。

一句話:單次 forward 再快,也救不了排在下一班車上的請求。讓 GPU 每 20 毫秒都滿載幹的是有用的活,比讓它每小時跑幾次滿批,划算得多。

留言區

歡迎分享你的想法!

發表留言

0/500

載入留言中…