vLLM深度解析:高吞吐量大語言模型推理系統的內部架構
博客文章(Inside vLLM: Anatomy of a High-Throughput LLM Inference System)深度解析了vLLM的內部架構,我簡單整理了一下
LLM引擎和引擎核心
LLM引擎是vLLM的基礎構建塊。單獨而言,它已經能夠實現高吞吐量推理——但僅限于離線設置。
使用以下離線推理代碼片段作為示例:
from vllm import LLM, SamplingParams
prompts = [
"Hello, my name is",
"The president of the United States is",
]
sampling_params = SamplingParams(temperature=0.8, top_p=0.95)
def main():
llm = LLM(model="TinyLlama/TinyLlama-1.1B-Chat-v1.0")
outputs = llm.generate(prompts, sampling_params)
if __name__ == "__main__":
main()LLM引擎構造函數
引擎的主要組件包括:
?vLLM配置:包含所有用于配置模型、緩存、并行性等的參數
?處理器:通過驗證、分詞和處理將原始輸入轉換為EngineCoreRequests
?引擎核心客戶端:在我們的示例中使用InprocClient
?輸出處理器:將原始EngineCoreOutputs轉換為用戶看到的RequestOutput
引擎核心本身由幾個子組件組成:
?模型執行器:驅動模型的前向傳播
?結構化輸出管理器:用于引導解碼
?調度器:決定哪些請求進入下一個引擎步驟,包含:
–策略設置(FCFS或優先級)
–等待和運行隊列
–KV緩存管理器——分頁注意力的核心
圖片
KV緩存管理器維護一個free_block_queue——可用KV緩存塊的池(通常有數十萬個,取決于VRAM大小和塊大小)。
標準transformer層的塊大小計算如下:
2 * block_size (默認=16) * num_kv_heads * head_size * dtype_num_bytes (bf16為2)在模型執行器構建期間,創建Worker對象,并執行三個關鍵程序:
1.初始化設備:
–分配CUDA設備并檢查模型數據類型支持
–驗證是否有足夠的VRAM可用
–設置分布式設置
2.加載模型:
–實例化模型架構
–加載模型權重
–調用model.eval()
–可選:對模型調用torch.compile()
3.初始化KV緩存:
–獲取每層KV緩存規范
–運行虛擬/分析前向傳播并獲取GPU內存快照
–分配、重塑并綁定KV緩存張量到注意力層
–準備注意力元數據
–捕獲CUDA圖以提高延遲
Generate函數
第一步是驗證并向引擎提供請求。對于每個提示:
1.創建唯一的請求ID并記錄到達時間
2.調用輸入預處理器對提示進行分詞
3.將此信息打包到EngineCoreRequest中
4.將請求傳遞到引擎核心,設置狀態為WAITING
圖片
接下來,只要有請求需要處理,引擎就會重復調用其step()函數。每個步驟有三個階段:
1.調度:選擇在此步驟中運行哪些請求(解碼和/或預填充)
2.前向傳播:運行模型并采樣令牌
3.后處理:將采樣的令牌ID附加到每個請求,去分詞,并檢查停止條件
停止條件包括:
?請求超過其長度限制
?采樣的令牌是EOS ID
?采樣的令牌匹配任何stop_token_ids
?輸出中存在停止字符串
調度器
推理引擎處理兩種主要的工作負載類型:
?預填充請求——對所有提示令牌的前向傳播。這些通常是計算密集型的
?解碼請求——僅對最近令牌的前向傳播。所有較早的KV向量已經被緩存。這些是內存帶寬受限的
V1調度器可以在同一步驟中混合兩種類型的請求,這是更智能的設計選擇。
調度器優先處理解碼請求——即已經在運行隊列中的請求。對于每個這樣的請求:
1.計算要生成的新令牌數量
2.調用KV緩存管理器的allocate_slots函數
3.通過減去令牌數量來更新令牌預算
之后,它處理來自等待隊列的預填充請求。
allocate_slots的功能:
1.計算塊數量——確定必須分配多少個新的KV緩存塊
2.檢查可用性——如果管理器池中沒有足夠的塊,則提前退出
3.分配塊——通過KV緩存管理器的協調器從塊池中獲取前n個塊
圖片
運行前向傳播
調用模型執行器的execute_model,主要步驟:
1.更新狀態——從input_batch中修剪已完成的請求
2.準備輸入——從CPU→GPU復制緩沖區;計算位置;構建slot_mapping
3.前向傳播——使用自定義分頁注意力內核運行模型。所有序列被扁平化并連接成一個長的"超級序列"
4.收集最后令牌狀態——提取每個序列最終位置的隱藏狀態并計算logits
5.采樣——根據采樣配置從計算的logits中采樣令牌
前向傳播步驟有兩種執行模式:
?急切模式——啟用急切執行時運行標準PyTorch前向傳播
?"捕獲"模式——當不強制急切時執行/重播預捕獲的CUDA圖
高級特性——擴展核心引擎邏輯
接下來我們深入了解:
分塊預填充
分塊預填充是通過將預填充步驟分成更小的塊來處理長提示的技術。如果沒有它,我們可能會遇到單個很長的請求獨占一個引擎步驟的情況,從而阻止其他預填充請求運行。
例如,讓每個塊包含n(=8)個令牌。執行P的完整預填充將需要≥3個引擎步驟,只有在最后的分塊預填充步驟中我們才會采樣一個新令牌。
在vLLM V1中,通過將long_prefill_token_threshold設置為正整數來啟用分塊預填充。
圖片
前綴緩存
前綴緩存避免重新計算多個提示在開頭共享的令牌。
關鍵在于long_prefix:定義為任何長于KV緩存塊(默認16個令牌)的前綴。
工作原理:
1.在第一次generate調用期間,引擎調用hash_request_tokens:
–將long_prefix + prompts[0]分成16令牌塊
–對于每個完整塊,計算哈希
–每個結果存儲為包含哈希和其令牌ID的BlockHash對象
2.引擎調用find_longest_cache_hit檢查這些哈希是否已存在于cached_block_hash_to_block中
3.在第二次具有相同前綴的generate調用中,find_longest_cache_hit找到所有n個塊的匹配。引擎可以直接重用這些KV塊
前綴緩存默認啟用。要禁用它:enable_prefix_caching = False。
引導解碼(FSM)
引導解碼是一種技術,在每個解碼步驟中,logits被基于語法的有限狀態機約束。這確保只能采樣語法允許的令牌。
示例代碼:
from vllm.sampling_params import GuidedDecodingParams
guided_decoding_params = GuidedDecodingParams(choice=["Positive", "Negative"])
sampling_params = SamplingParams(guided_decoding=guided_decoding_params)
圖片
vLLM中的工作原理:
1.在LLM引擎構建時,創建StructuredOutputManager
2.添加請求時,狀態設置為WAITING_FOR_FSM,語法異步編譯
3.在調度期間,如果異步編譯完成,狀態切換到WAITING
4.在前向傳播產生logits后,使用位掩碼將不允許的logits設置為-∞
5.采樣下一個令牌后,通過accept_tokens推進請求的FSM
推測解碼
在自回歸生成中,每個新令牌都需要大型LM的前向傳播。推測解碼通過引入較小的草稿LM來加速這一過程。
步驟:
1.草稿:在小模型上運行并便宜地提議k個令牌
2.驗證:在大模型上對上下文+k個草稿令牌運行一次
3.接受/拒絕
:從左到右遍歷k個草稿令牌:
–如果大模型的概率≥草稿概率,接受它
–否則,以p_large(token)/p_draft(token)的概率接受它
–在第一次拒絕時停止,或接受所有k個草稿令牌
vLLM V1支持更快但不太準確的提議方案:n-gram、EAGLE和Medusa。
示例配置:
speculative_cnotallow={
"method": "ngram",
"prompt_lookup_max": 5,
"prompt_lookup_min": 3,
"num_speculative_tokens": 3,
}
圖片
分離式P/D(預填充/解碼)
預填充和解碼具有非常不同的性能配置文件(計算密集型vs內存帶寬受限),因此分離它們的執行是明智的設計。
在實踐中,我們運行N個vLLM預填充實例和M個vLLM解碼實例,根據實時請求混合自動縮放它們。預填充工作器將KV寫入專用KV緩存服務;解碼工作器從中讀取。
圖片
從UniprocExecutor到MultiProcExecutor
當模型權重不再適合單個GPU的VRAM時,第一個選擇是使用張量并行性(例如TP=8)在同一節點的多個GPU上分片模型。如果模型仍然不適合,下一步是跨節點的流水線并行性。
MultiProcExecutor的工作原理:
1.初始化rpc_broadcast_mq消息隊列
2.構造函數循環遍歷world_size并為每個rank生成守護進程
3.每個工作器設置兩個隊列:
–rpc_broadcast_mq(與父進程共享)用于接收工作
–worker_response_mq用于發送響應
4.工作器進入忙循環,阻塞在rpc_broadcast_mq.dequeue上
5.運行時,MultiProcExecutor將請求入隊到所有子工作器的rpc_broadcast_mq中
分布式系統服務vLLM
假設我們有兩個H100節點,想要在它們上運行四個vLLM引擎。如果模型需要TP=4,我們可以這樣配置節點。
在無頭服務器節點上:
vllm serve <model-name>
--tensor-parallel-size 4
--data-parallel-size 4
--data-parallel-size-local 2
--data-parallel-start-rank 0
--data-parallel-address <master-ip>
--data-parallel-rpc-port 13345
--headlessvLLM中的工作原理:
在無頭節點上,CoreEngineProcManager啟動2個進程,每個運行EngineCoreProc.run_engine_core。每個函數創建一個DPEngineCoreProc,然后進入其忙循環。
在API服務器節點上,我們實例化一個AsyncLLM對象。內部創建一個DPLBAsyncMPClient。
完整的請求生命周期:
1.請求命中API服務器上的create_completion路由
2.函數異步分詞提示,并準備元數據
3.調用AsyncLLM.generate,最終調用DPAsyncMPClient.add_request_async
4.根據DP協調器的狀態進行負載均衡
5.ADD請求發送到選擇的引擎的input_socket
6.在該引擎處:
–輸入線程解除阻塞,從輸入socket解碼數據
–主線程在input_queue上解除阻塞,將請求添加到引擎
–輸出線程在output_queue上解除阻塞并通過輸出socket發送結果
7.這些結果觸發AsyncLLM輸出異步任務,將令牌傳播回FastAPI的create_completion路由
基準測試和自動調優——延遲vs吞吐量
在最高級別上有兩個競爭的指標:
?延遲——從提交請求到返回令牌的時間
?吞吐量——系統每秒可以生成/處理的令牌/請求數量
延遲和吞吐量競爭的本質變得清晰:隨著批大小B↓趨近于1,ITL下降;隨著B↑趨近于無窮大,ITL上升,但吞吐量提高。
屋頂線模型有助于理解:在飽和批B_sat以下,步驟時間由HBM帶寬主導;超出B_sat,內核變為計算受限,步驟時間大致與B成正比。
如何在vLLM中進行基準測試
vLLM提供vllm bench {serve,latency,throughput} CLI:
?latency:使用短輸入(默認32令牌)和小批次(默認8)采樣128個輸出令牌
?throughput:一次性提交固定的提示集(默認:1000個ShareGPT樣本)
?serve:啟動vLLM服務器并通過從泊松分布采樣請求到達間隔時間來模擬真實世界工作負載
本文轉載自??AI帝國??,作者:無影寺

















