軟件企業(yè)網(wǎng)站模板2023全民核酸又開始了
修改內(nèi)容 | 時間 |
---|---|
2.4.1處理請求的流程,引用更好的流程圖 | 2025.02.11 |
首發(fā) | 2025.02.08 |
深入解析 vLLM:高性能 LLM 服務(wù)框架的架構(gòu)之美(一)原理與解析
深入解析 vLLM:高性能 LLM 服務(wù)框架的架構(gòu)之美(二)調(diào)度管理
1. vLLM 整體代碼架構(gòu)
1.1 vLLM 的設(shè)計目標與特點
vLLM 是一個高性能的大語言模型服務(wù)框架。在大語言模型日益普及的今天,如何高效地提供推理服務(wù)成為一個重要挑戰(zhàn)。傳統(tǒng)的服務(wù)框架在處理并發(fā)請求時往往會遇到性能瓶頸、內(nèi)存管理效率低下等問題。vLLM 正是為解決這些關(guān)鍵挑戰(zhàn)而生。
在性能優(yōu)化方面,vLLM 最大的創(chuàng)新在于引入了突破性的 PagedAttention 機制。這項技術(shù)巧妙地將傳統(tǒng)的連續(xù)注意力計算改造為基于頁的形式,使得 GPU 內(nèi)存的利用效率得到極大提升。通過精心設(shè)計的連續(xù)批處理機制,vLLM 能夠充分利用 GPU 算力,顯著提升推理速度。
說到內(nèi)存效率,vLLM 采用了獨特的動態(tài)內(nèi)存管理方案。通過對 KV Cache 的動態(tài)管理和頁式存儲,它能夠有效減少內(nèi)存碎片,提高內(nèi)存利用率。這就像是在有限的空間里實現(xiàn)了"完美俄羅斯方塊",讓每一塊內(nèi)存都物盡其用。
在并發(fā)處理能力上,vLLM 表現(xiàn)同樣出色。它采用靈活的調(diào)度策略,能夠同時處理大量并發(fā)請求,并通過動態(tài)負載均衡確保資源的最優(yōu)分配。這種設(shè)計讓 vLLM 能夠輕松應(yīng)對高并發(fā)場景,就像一個經(jīng)驗豐富的交通指揮官,讓所有請求都能高效有序地通行。
作為一個服務(wù)框架,vLLM 的架構(gòu)設(shè)計非常靈活。它不僅支持單機部署,還能輕松擴展到分布式環(huán)境??蚣芗嫒葜髁鞯?LLM 模型,并提供簡潔的 API 接口,大大降低了開發(fā)者的使用門檻。
在實際應(yīng)用中,vLLM 展現(xiàn)出了令人印象深刻的性能表現(xiàn)。相比傳統(tǒng)方案,它能夠?qū)⑼评矸?wù)的吞吐量提升 2-4 倍,同時顯著降低請求延遲。這種性能提升不僅體現(xiàn)在數(shù)據(jù)上,更為用戶帶來了明顯的體驗改善。
對開發(fā)者而言,vLLM 提供了完善的工具支持。從簡單的集成方式,到豐富的監(jiān)控指標和調(diào)優(yōu)選項,都體現(xiàn)了框架在易用性方面的深思熟慮。這使得開發(fā)者能夠快速上手,并根據(jù)實際需求進行靈活調(diào)整。
1.2 核心組件概覽
vLLM 的系統(tǒng)架構(gòu)由幾個關(guān)鍵組件構(gòu)成,它們各司其職又相互協(xié)作,共同支撐起整個服務(wù)框架。讓我們深入了解每個組件的核心功能:
Engine 是整個系統(tǒng)的中樞,負責(zé)協(xié)調(diào)其他組件的工作。它就像一個指揮家,統(tǒng)籌管理著模型加載、請求分發(fā)、結(jié)果收集等核心流程。Engine 接收用戶請求后,會將其轉(zhuǎn)化為內(nèi)部任務(wù),并交由其他組件處理。
Worker 是實際執(zhí)行推理計算的工作單元。每個 Worker 都可以獨立處理分配給它的計算任務(wù),就像工廠中的熟練工人。在分布式部署時,多個 Worker 可以并行工作,大大提升系統(tǒng)的處理能力。
Scheduler 是 vLLM 的調(diào)度大腦,它的主要職責(zé)是合理分配計算資源和管理請求隊列。通過精心設(shè)計的調(diào)度算法,Scheduler 能夠權(quán)衡不同請求的優(yōu)先級、資源需求和系統(tǒng)負載,確保系統(tǒng)高效運轉(zhuǎn)。
BlockManager 負責(zé)內(nèi)存資源的精細化管理。它采用塊式管理策略,將 GPU 內(nèi)存劃分為多個塊,并通過智能的分配和回收機制,最大化內(nèi)存使用效率。這就像是一個經(jīng)驗豐富的倉庫管理員,讓每一寸空間都得到充分利用。
PagedAttention 是 vLLM 最具創(chuàng)新性的組件。它重新設(shè)計了注意力機制的實現(xiàn)方式,引入頁式管理的概念,使得 KV Cache 的管理更加靈活高效。這項技術(shù)顯著提升了推理性能,是 vLLM 性能優(yōu)勢的關(guān)鍵所在。
CacheEngine 專注于緩存策略的實現(xiàn)。它通過智能的緩存機制,減少重復(fù)計算,提升響應(yīng)速度。就像是系統(tǒng)的記憶庫,能夠快速調(diào)用常用的計算結(jié)果。
這些組件通過精心設(shè)計的接口相互配合,形成了一個高效協(xié)同的整體。每個組件都可以獨立優(yōu)化和擴展,這種模塊化的設(shè)計既保證了系統(tǒng)的靈活性,也便于維護和升級。
1.3 系統(tǒng)架構(gòu)與工作流程
vLLM 的系統(tǒng)架構(gòu)采用了模塊化設(shè)計,各個組件之間通過清晰的接口進行交互。下面這張架構(gòu)圖展示了各個核心組件及其關(guān)系:
當(dāng)用戶發(fā)起一個推理請求時,整個處理流程是這樣的:首先,請求會經(jīng)由 Engine 接收和解析。Engine 會對請求進行初步處理,包括參數(shù)驗證、格式轉(zhuǎn)換等。這就像是前臺接待員,確保所有進入系統(tǒng)的請求都是規(guī)范的。
接下來,Engine 會將請求交給 Scheduler 進行調(diào)度分析。Scheduler 會根據(jù)當(dāng)前系統(tǒng)的負載狀況、可用資源情況,為這個請求制定處理計劃。它會考慮多個因素:是否有空閑的 Worker?內(nèi)存資源是否充足?是否需要進行批處理優(yōu)化?這個過程就像是一個經(jīng)驗豐富的項目經(jīng)理,在有限的資源下做出最優(yōu)的任務(wù)分配。
在執(zhí)行階段,Worker 承擔(dān)著最重要的計算任務(wù)。它們在 Scheduler 的指揮下,有條不紊地進行模型推理計算。這時,BlockManager 會密切配合,確保所需的內(nèi)存資源能夠及時到位。如果發(fā)現(xiàn)內(nèi)存不足,BlockManager 會啟動交換機制,像變魔術(shù)一樣騰出空間來。
PagedAttention 在這個過程中發(fā)揮著關(guān)鍵作用。它創(chuàng)新性地使用頁式管理方式處理注意力計算,使得長序列推理變得更加高效。這就像是給計算過程裝上了"加速器",顯著提升了處理速度。
CacheEngine 則在整個過程中不斷優(yōu)化性能。它會智能地緩存一些計算結(jié)果,在遇到相似請求時直接復(fù)用,避免重復(fù)計算。這就像是系統(tǒng)的"備忘錄",能夠快速提供歷史經(jīng)驗。
最后,處理結(jié)果會再次回到 Engine,由它負責(zé)結(jié)果的整理和返回。整個過程環(huán)環(huán)相扣,每個組件都各司其職又緊密協(xié)作,共同確保了請求處理的高效性。
在分布式部署場景下,這個架構(gòu)還能輕松擴展。多個 Worker 節(jié)點可以并行工作,Scheduler 會自動協(xié)調(diào)它們的任務(wù)分配,就像是一個指揮交響樂團的指揮家,讓所有樂器都能配合得天衣無縫。
2. vLLM 處理請求的流程
一個請求在 vLLM 中的生命周期是一個精心編排的過程,涉及多個組件的協(xié)同工作。主要實現(xiàn)在 vllm/engine/llm_engine.py
和 vllm/engine/ray_worker.py
中。
2.1 初始化并加載模型權(quán)重
如上圖所示,vLLM 的初始化過程包括模型加載、模型參數(shù)初始化、KV Cache 預(yù)分配等關(guān)鍵步驟。
vLLM需要初始化并加載模型權(quán)重,支持從HF Hub加載模型,也支持從本地加載模型。在加載過程中,vLLM將模型權(quán)重加載到GPU中,以便后續(xù)推理在GPU運行。
2.2 估計KV Cache的物理塊數(shù)量
在模型部署的初始化階段,vLLM 會通過一個模擬實驗步驟來決定 GPU 和 CPU 上可以分配的 KV cache 物理塊數(shù)量,確保后續(xù)推理時的內(nèi)存分配不會導(dǎo)致顯存溢出。這個步驟在 vLLM 中被稱為 determine_num_available_blocks。
首先,在啟動 LLMEngine 時,系統(tǒng)會進行一個 “假數(shù)據(jù)模擬” 來測量模型的內(nèi)存使用情況。它通過構(gòu)造假數(shù)據(jù)并執(zhí)行一次模擬前向推理,來觀察 GPU 上模型運行時的峰值內(nèi)存需求。在這次前向推理中,系統(tǒng)不使用 KV cache,而是單純地模擬模型推理所需的基本內(nèi)存。這種方式可以幫助確定整個推理過程會占用多少顯存,從而為后續(xù)的內(nèi)存分配提供依據(jù)。
在完成內(nèi)存需求的測量后,vLLM 會使用測得的內(nèi)存數(shù)據(jù)來計算可分配給 KV cache 的顯存總量。具體來說,分配給 KV cache 的顯存等于 GPU 總顯存減去在不使用 KV cache 時推理所占用的顯存(包括模型本身和推理過程中的中間數(shù)據(jù))。這樣可以確保顯存分配合理,不會因為內(nèi)存不足而導(dǎo)致 OOM(Out Of Memory)錯誤。
接下來,通過計算顯存中可以分配的物理塊數(shù)量,vLLM 會確定 GPU 上可以使用的 KV cache 數(shù)量。物理塊的大小由用戶定義,包括多個參數(shù),例如 block_size、num_heads、head_size、num_layers 以及數(shù)據(jù)類型的大小(如 fp16 對應(yīng)的字節(jié)數(shù)是 2)。計算公式會依據(jù)這些參數(shù)來估算單個物理塊的大小,然后根據(jù)剩余顯存估算出可以分配的物理塊總數(shù)。
2.3 預(yù)分配 KV Cache
在確定好 KV cache 塊的大小之后,vLLM 會進行顯存的預(yù)分配,以確保后續(xù)推理過程中有足夠的內(nèi)存來存儲 KV cache。這一過程的核心是創(chuàng)建空的張量(empty tensor),并將它們直接分配到 GPU 上,從而鎖定一定的顯存空間專門用于 KV cache。這種顯存預(yù)分配的方式能夠避免推理過程中頻繁的動態(tài)內(nèi)存分配,提升系統(tǒng)的穩(wěn)定性和推理效率。
預(yù)分配的顯存專門用于 KV cache,因此在 vLLM 初始化后,你可能會注意到顯存的占用比單純加載模型時要多一些。這是因為這些額外的顯存已經(jīng)被預(yù)先分配給了 KV cache,確保它們在推理時不會受到其他任務(wù)的影響。通過這種顯存的預(yù)先規(guī)劃和鎖定,系統(tǒng)在處理推理請求時能夠更高效地管理內(nèi)存資源,避免了推理階段因顯存分配不足而出現(xiàn)的瓶頸。
2.4 處理請求
2.4.1 請求到達 LLMEngine
這種圖展示了 vLLM 引起國內(nèi)的工作流程,可以把它理解為一些列處理請求的步驟。我們分成兩部分來看:異步(async)和同步(sync)流程。
異步流程
API-Server
接受請求:當(dāng)用戶發(fā)送請求時,API
服務(wù)器首先會接收到這些請求并解析其中的參數(shù)。這些參數(shù)告訴系統(tǒng)后續(xù)進行什么樣的處理。- 生成請求參數(shù)(async_args):
API-Server
會根據(jù)請求參數(shù)生成一個async_args
對象,它包含了請求的詳細信息,比如模型 ID、輸入文本、推理參數(shù)等。 - 請求加入隊列:請求加入隊列后,會等待調(diào)度器調(diào)度。
- 引擎主循環(huán)開始(
run_engine_loop
):在異步流程中,引擎主循環(huán)會不斷從隊列中獲取請求,并進行處理。 - 處理請求(
get_new_and_abort_requests
):在處理過程中,系統(tǒng)會檢查新的請求以及是否有請求被終止,確保每個請求被及時處理。 - 執(zhí)行推理步驟(
engine_step
): engine 開始處理請求,決定哪個請求可以執(zhí)行。 - 異步步驟完成(
add_request_async
): 將請求傳遞到 LLMEngine - 請求加入調(diào)度(
add_seq_group
): 將請求包裝為 seq_group 對象,并加入調(diào)度器 - 返回調(diào)度結(jié)果(
sche_output
): 調(diào)度執(zhí)行,對 waiting,running, swapped 隊列中的請求進行調(diào)度,返回調(diào)度結(jié)果,等待模型推理。 - 單步推理(
step_async
): 模型推理,生成一個step 的輸出結(jié)果。 - 引擎推理(
engine_step
): 引擎推理,處理請求,生成一個step 的輸出結(jié)果。 - 模型推理(
execute_model_req
): model_executor 執(zhí)行推理,生成一個step 的輸出結(jié)果。 - 結(jié)果返回(
return_output
): 將結(jié)果返回給 AsyncLLMEngine,包裝為 request_output 對象。 - 返回結(jié)果(
return_output_async
): 回拋結(jié)果 - 流式輸出(
stream_output
): 流式輸出結(jié)果 - 請求完成(
request_done
): 請求完成,流式將結(jié)果返回API-Server。
同步流程
同步流程相對簡單,主要是在執(zhí)行過程中直接返回結(jié)果:
- 初始化(
init
): 同步流程開始時,系統(tǒng)會初始化所有必要的參數(shù)和資源。 - 請求處理(
add_request
): 此時,系統(tǒng)直接處理請求并開始執(zhí)行。 - 推理計算(
step
): 系統(tǒng)會一步一步地處理推理任務(wù),直到完成。 - 生成并返回結(jié)果(
output
): 處理完成后,系統(tǒng)會直接返回推理結(jié)果。
總體來說,vLLM 的工作流程就像一個工廠,API 服務(wù)器像一個接收原材料的入口,它把請求交給引擎進行處理,處理的方式有兩種:一種是異步的,一種是同步的。每個請求都會經(jīng)過一系列步驟,最終模型給出答案。
2.4.2 調(diào)度器的任務(wù)
在請求進入調(diào)度器后,Scheduler 會根據(jù)當(dāng)前的資源情況(如可用的 KV 緩存塊)來決定如何執(zhí)行任務(wù)。調(diào)度器維護了三個請求隊列:
Waiting Queue
:等待執(zhí)行的請求。Running Queue
:當(dāng)前正在處理的請求。Swapped Queue
:由于顯存不足而被暫時置換出去的請求。
調(diào)度器會判斷是否有足夠的內(nèi)存塊可以分配給新的 tokens。如果有足夠的可用 KV 塊,則請求從等待隊列移動到正在運行的隊列(waiting → running);如果內(nèi)存不足,調(diào)度器會將一些運行中的請求交換到 CPU 內(nèi)存(running → swapped),以騰出空間讓新請求進入運行隊列。
2.4.3 Worker 執(zhí)行推理
當(dāng)請求進入運行隊列后,Scheduler 會將任務(wù)分發(fā)給多個 Worker。每個 Worker 在 GPU 上運行,負責(zé)實際的推理計算。在這一過程中,CacheEngine 會按照調(diào)度器的指示管理緩存塊,包括在 GPU 和 CPU 之間交換內(nèi)存塊,確保內(nèi)存資源得到高效利用。此外,CacheEngine 還會對共享緩存塊執(zhí)行寫時復(fù)制(copy-on-write),以確保數(shù)據(jù)的一致性和高效性。
2.4.4 模型的推理過程
每個 Worker 中的 Worker.model 模塊負責(zé)加載并執(zhí)行模型推理。在這個過程中,它會依賴 PagedAttention 來實現(xiàn)高效的注意力計算。PagedAttention 是優(yōu)化的注意力機制實現(xiàn),適用于大規(guī)模的 Transformer 模型,并使用諸如 xformers 或 FlashAttention 等技術(shù)來加速推理。
此外,模型的其他部分(例如線性層、量化層等)也進行了優(yōu)化,以便在分布式執(zhí)行和張量并行的情況下達到最高性能。在推理階段,Sampler 會負責(zé)選擇下一個生成的 token,使用貪心算法、隨機采樣或者 Beam Search 等策略。
2.4.5 請求的完成和結(jié)果返回
推理完成后,結(jié)果會被發(fā)送回 LLMEngine。LLMEngine 會對生成的 tokens 進行 detokenization,將它們轉(zhuǎn)換回可讀的文本,并最終將生成的結(jié)果流式地返回給用戶。這一流程使得生成的結(jié)果可以盡快交付給用戶,而無需等待整個請求的完全完成。
整個請求的處理流程由 LLMEngine 進行協(xié)調(diào)調(diào)度,通過 Scheduler 管理內(nèi)存和資源的有效利用,Worker 在 GPU 上執(zhí)行具體的推理計算,最終將結(jié)果流式地返回給用戶。
3. vLLM 輸入數(shù)據(jù)的預(yù)處理
在 vLLM 處理用戶請求之前,需要對輸入數(shù)據(jù)進行一系列預(yù)處理操作,以確保數(shù)據(jù)能夠被模型正確處理。這個過程就像是將原始材料加工成標準化的零件,為后續(xù)的推理計算做好準備。
3.1 Tokenization 處理
Tokenization 是輸入預(yù)處理的第一道關(guān)卡。vLLM 使用與原始語言模型相同的分詞器,將輸入文本轉(zhuǎn)換為模型可以理解的 token 序列。主要實現(xiàn)在 vllm/engine/llm_engine.py
和 vllm/engine/tokenizer.py
中。
這個過程包含幾個關(guān)鍵步驟:
-
文本規(guī)范化:首先對輸入文本進行清理和標準化,包括處理特殊字符、統(tǒng)一空白字符等。這就像是將各種形狀的原料整理成統(tǒng)一的形態(tài)。
-
分詞處理:使用模型對應(yīng)的 tokenizer 將文本切分成 token。這個過程會考慮詞頻、語義完整性等因素,就像是將長木材切割成大小合適的木塊。
-
批處理優(yōu)化:vLLM 創(chuàng)新性地實現(xiàn)了動態(tài)批處理機制。它會智能地將多個請求的 token 組合在一起處理,就像是將多個訂單的相似零件放在一起加工,顯著提升處理效率。
3.2 Prompt 模板與格式化
vLLM 支持靈活的 prompt 模板系統(tǒng),幫助用戶更好地構(gòu)造輸入。相關(guān)實現(xiàn)在 vllm/engine/arg_utils.py
和 vllm/engine/sampling_params.py
中:
-
模板定義:用戶可以預(yù)定義不同場景的 prompt 模板,包括系統(tǒng)提示、用戶輸入、歷史對話等部分。
-
變量替換:模板中的變量可以動態(tài)替換,使得同一個模板能夠適應(yīng)不同的輸入場景。
-
格式驗證:系統(tǒng)會自動檢查填充后的 prompt 是否符合預(yù)期格式,確保輸入的規(guī)范性。
3.3 輸入驗證與優(yōu)化
為了確保系統(tǒng)穩(wěn)定性和性能,vLLM 會對輸入進行全面的驗證和優(yōu)化:
-
長度檢查:驗證輸入是否超過模型的最大上下文長度,必要時進行截斷或分段處理。
-
特殊標記處理:自動添加或處理模型所需的特殊標記(如開始符、結(jié)束符等)。
-
資源評估:預(yù)估處理該輸入所需的計算資源和內(nèi)存需求,為后續(xù)調(diào)度做準備。(實現(xiàn)在
vllm/engine/llm_engine.py
中的add_request
方法) -
緩存優(yōu)化:分析輸入是否能夠利用已有的 KV Cache,提前進行優(yōu)化決策。(實現(xiàn)在
vllm/core/block_manager.py
中)
3.4 深入解析 add_request
接下來我們將深入分析 LLMEngine 的請求處理流程。通過 LLMEngine.add_request 方法接收到的請求會經(jīng)過一系列預(yù)處理、調(diào)度、執(zhí)行和輸出處理步驟。
def add_request(self,request_id: str,prompt: Optional[PromptType] = None,params: Optional[Union[SamplingParams, PoolingParams]] = None,arrival_time: Optional[float] = None,lora_request: Optional[LoRARequest] = None,trace_headers: Optional[Mapping[str, str]] = None,prompt_adapter_request: Optional[PromptAdapterRequest] = None,priority: int = 0,*,inputs: Optional[PromptType] = None, # DEPRECATED
) -> None:
"""Add a request to the engine's request pool.The request is added to the request pool and will be processed by the
scheduler as `engine.step()` is called. The exact scheduling policy is
determined by the scheduler.Args:request_id: The unique ID of the request.prompt: The prompt to the LLM. See :class:`~vllm.inputs.PromptType`for more details about the format of each input.params: Parameters for sampling or pooling.:class:`~vllm.SamplingParams` for text generation.:class:`~vllm.PoolingParams` for pooling.arrival_time: The arrival time of the request. If None, we usethe current monotonic time.trace_headers: OpenTelemetry trace headers.priority: The priority of the request.Only applicable with priority scheduling.Details:- Set arrival_time to the current time if it is None.- Set prompt_token_ids to the encoded prompt if it is None.- Create `n` number of :class:`~vllm.Sequence` objects.- Create a :class:`~vllm.SequenceGroup` objectfrom the list of :class:`~vllm.Sequence`.- Add the :class:`~vllm.SequenceGroup` object to the scheduler.Example:>>> # initialize engine>>> engine = LLMEngine.from_engine_args(engine_args)>>> # set request arguments>>> example_prompt = "Who is the president of the United States?">>> sampling_params = SamplingParams(temperature=0.0)>>> request_id = 0>>>>>> # add the request to the engine>>> engine.add_request(>>> str(request_id),>>> example_prompt,>>> SamplingParams(temperature=0.0))>>> # continue the request processing>>> ...
"""
首先,add_request 方法接受了多個參數(shù),其中關(guān)鍵的參數(shù)包括:
- request_id:每個請求的唯一標識符,用于跟蹤和調(diào)度。
- prompt:請求的提示詞,通常是用戶輸入的自然語言文本,定義了生成任務(wù)的起點。
- params:這是生成任務(wù)的參數(shù),可能是 SamplingParams(采樣生成參數(shù))或者 PoolingParams(池化生成參數(shù)),這將影響生成的策略,比如溫度、采樣方法等。
- arrival_time:請求到達的時間,用于統(tǒng)計和分析請求的延遲。
- lora_request:用于處理 LoRA 模型的特定請求,如果模型使用了 LoRA 技術(shù)。
- trace_headers:用于跟蹤請求的元數(shù)據(jù),通常用于日志記錄和調(diào)試。
- prompt_adapter_request:用于處理提示適配器的特定請求,如果模型使用了提示適配器。
- priority:請求的優(yōu)先級,用于調(diào)度器決定請求的執(zhí)行順序。
- inputs:這是一個可選參數(shù),用于兼容舊版本,通??梢院雎?。
3.4.1 preprocess 入口
在 LLMEngine 中,當(dāng)我們使用 add_request 方法添加一個請求時,系統(tǒng)首先會調(diào)用 InputPreprocessor 對輸入進行預(yù)處理,這一過程確保用戶的輸入被模型正確處理。InputPreprocessor 類負責(zé)解析和處理不同類型的輸入(包括文本、tokens 等),并將其轉(zhuǎn)換為模型可以使用的標準化格式。
def preprocess(self,prompt: PromptType,request_id: str,lora_request: Optional[LoRARequest] = None,prompt_adapter_request: Optional[PromptAdapterRequest] = None,
) -> ProcessorInputs:"""Preprocess the input prompt."""if self.model_config.is_encoder_decoder:# Encoder-decoder model requires special mapping of# input prompts to encoder & decoderreturn self._process_encoder_decoder_prompt(prompt,request_id=request_id,)if is_explicit_encoder_decoder_prompt(prompt):raise ValueError("Cannot pass encoder-decoder prompt ""to decoder-only models")# Decoder-only operationreturn self._process_decoder_only_prompt(prompt,request_id=request_id,lora_request=lora_request,prompt_adapter_request=prompt_adapter_request,)
對于 encoder-decoder 模型,輸入需要分為 encoder prompt 和 decoder prompt,每一部分都需要分別進行處理。_process_encoder_decoder_prompt 是專門為 encoder-decoder 模型設(shè)計的,它能夠處理同時包含編碼器和解碼器的 prompt。
現(xiàn)在我們只考慮 decoder-only 模型,對于 decoder-only 模型,輸入處理相對簡單,僅需要處理單一的解碼器 prompt。_process_decoder_only_prompt 的邏輯如下:
def _process_decoder_only_prompt(self,prompt: SingletonPrompt,request_id: str,lora_request: Optional[LoRARequest] = None,prompt_adapter_request: Optional[PromptAdapterRequest] = None,
) -> DecoderOnlyInputs:"""For decoder-only models:Process an input prompt into an :class:`DecoderOnlyInputs` instance.Arguments:* prompt: input prompt* request_id* lora_request* prompt_adapter_requestReturns:* :class:`DecoderOnlyInputs` instance"""prompt_comps = self._prompt_to_llm_inputs(prompt,request_id=request_id,lora_request=lora_request,)return self._build_decoder_only_llm_inputs(prompt_comps,prompt_adapter_request=prompt_adapter_request,)
_prompt_to_llm_inputs
方法負責(zé)將輸入的 prompt
轉(zhuǎn)換為模型可以理解的格式。它根據(jù) prompt
的類型(字符串、tokens 或文本)進行不同的處理。
def _prompt_to_llm_inputs(self,prompt: SingletonPrompt,request_id: str,lora_request: Optional[LoRARequest] = None,) -> SingletonInputs:"""Extract the singleton inputs from a prompt.Arguments:* request_id* prompt: single encoder or decoder input prompt* lora_request: this is only valid for decoder promptsReturns:* :class:`SingletonInputs` instance"""parsed = parse_singleton_prompt(prompt)if parsed["type"] == "str":prompt_text = parsed["content"]prompt_token_ids = self._tokenize_prompt(prompt_text,request_id=request_id,lora_request=lora_request,)return token_inputs(prompt=prompt_text,prompt_token_ids=prompt_token_ids,)if parsed["type"] == "tokens":tokens_content = parsed["content"]prompt_token_ids = tokens_content["prompt_token_ids"]token_type_ids = tokens_content.get("token_type_ids")multi_modal_data = tokens_content.get("multi_modal_data")mm_processor_kwargs = tokens_content.get("mm_processor_kwargs")if multi_modal_data is not None and self._can_process_multimodal():return self._process_multimodal(prompt_token_ids,multi_modal_data,mm_processor_kwargs,lora_request=lora_request,)return token_inputs(prompt_token_ids=prompt_token_ids,prompt_embeds=tokens_content.get("prompt_embeds"),token_type_ids=token_type_ids,multi_modal_data=multi_modal_data,mm_processor_kwargs=mm_processor_kwargs,)if parsed["type"] == "text":text_content = parsed["content"]prompt_text = text_content["prompt"]multi_modal_data = text_content.get("multi_modal_data")mm_processor_kwargs = text_content.get("mm_processor_kwargs")if multi_modal_data is not None and self._can_process_multimodal():return self._process_multimodal(prompt_text,multi_modal_data,mm_processor_kwargs,lora_request=lora_request,)prompt_token_ids = self._tokenize_prompt(prompt_text,request_id=request_id,lora_request=lora_request,)return token_inputs(prompt=prompt_text,prompt_token_ids=prompt_token_ids,prompt_embeds=text_content.get("prompt_embeds"),multi_modal_data=multi_modal_data,mm_processor_kwargs=mm_processor_kwargs,)assert_never(parsed)
_tokenize_prompt
方法負責(zé)將輸入的文本轉(zhuǎn)換為 token 序列。它使用模型對應(yīng)的 tokenizer 將文本切分成 token,并返回對應(yīng)的 token ID 列表。
def _tokenize_prompt(self,prompt: str,request_id: str,lora_request: Optional[LoRARequest],) -> List[int]:"""Apply the model's tokenizer to a text prompt, returning thecorresponding token IDs."""tokenizer = self.get_tokenizer_group()add_special_tokens = Noneif self.model_config.hf_config.model_type == "whisper":# For Whisper, special tokens should be provided by the user based# on the task and language of their request. Also needed to avoid# appending an EOS token to the prompt which disrupts generation.add_special_tokens = Falsereturn tokenizer.encode(request_id=request_id,prompt=prompt,lora_request=lora_request,add_special_tokens=add_special_tokens)
- 獲取 Tokenizer:通過
get_tokenizer_group()
獲取當(dāng)前模型對應(yīng)的分詞器。這個分詞器通常在模型初始化時就已經(jīng)加載,與模型使用相同的詞表和分詞規(guī)則。 - 特殊標記處理:
- 默認情況下,
add_special_tokens
為 None,表示使用模型默認的特殊標記處理方式 - 對于 Whisper 模型等特殊情況,會設(shè)置
add_special_tokens=False
,因為這些模型需要用戶根據(jù)具體任務(wù)和語言來提供特殊標記 - 這樣的設(shè)計確保了不同模型的特殊標記(如開始符、結(jié)束符等)能夠被正確處理
- 默認情況下,
- Token 編碼:調(diào)用 tokenizer 的 encode 方法,將文本轉(zhuǎn)換為 token ID 序列。這個過程包括:
- 將輸入文本分割成子詞(subwords)
- 將每個子詞映射到對應(yīng)的 token ID
- 根據(jù)需要添加特殊標記(如果 add_special_tokens 為 True)
- 處理 LoRA 相關(guān)的特殊需求(如果提供了 lora_request)
- 請求追蹤:通過傳入 request_id,確保能夠追蹤每個請求的 tokenization 過程,這對于調(diào)試和性能分析很有幫助。
3.4 創(chuàng)建 sequence 和 sequence_group
通過這些精心設(shè)計的預(yù)處理步驟,vLLM 能夠?qū)⒏鞣N形式的輸入轉(zhuǎn)換為標準化、高效的形式,為后續(xù)的推理計算打下堅實基礎(chǔ)。這就像是一個細心的廚師,在烹飪之前將所有食材都準備妥當(dāng),確保整個烹飪過程的順暢進行。
在預(yù)處理之后,我們得到了 ProcessorInputs
實例,它包含了處理后的輸入數(shù)據(jù)。接下來,我們調(diào)用 _add_processed_request
方法將處理后的請求添加到引擎的請求池中。
def _add_processed_request(self,request_id: str,processed_inputs: ProcessorInputs,params: Union[SamplingParams, PoolingParams],arrival_time: float,lora_request: Optional[LoRARequest],prompt_adapter_request: Optional[PromptAdapterRequest],trace_headers: Optional[Mapping[str, str]] = None,priority: int = 0,) -> Optional[SequenceGroup]:"""Add a processed request to the engine's request pool.return the created sequence group."""if isinstance(params, SamplingParams) and params.n > 1:ParallelSampleSequenceGroup.add_request(request_id,self,params,processed_inputs=processed_inputs,arrival_time=arrival_time,lora_request=lora_request,trace_headers=trace_headers,prompt_adapter_request=prompt_adapter_request,priority=priority,)return Noneself._validate_model_inputs(processed_inputs, lora_request)# Create the sequences.block_size = self.cache_config.block_sizeseq_id = next(self.seq_counter)eos_token_id = self.input_preprocessor.get_eos_token_id(lora_request)if is_encoder_decoder_inputs(processed_inputs):decoder_inputs = processed_inputs["decoder"]encoder_inputs = processed_inputs["encoder"]else:decoder_inputs = processed_inputsencoder_inputs = Noneseq = Sequence(seq_id, decoder_inputs, block_size, eos_token_id,lora_request, prompt_adapter_request)encoder_seq = (None if encoder_inputs is None else Sequence(seq_id, encoder_inputs, block_size, eos_token_id, lora_request,prompt_adapter_request))# Create a SequenceGroup based on SamplingParams or PoolingParamsif isinstance(params, SamplingParams):seq_group = self._create_sequence_group_with_sampling(request_id,seq,params,arrival_time=arrival_time,lora_request=lora_request,trace_headers=trace_headers,prompt_adapter_request=prompt_adapter_request,encoder_seq=encoder_seq,priority=priority)elif isinstance(params, PoolingParams):seq_group = self._create_sequence_group_with_pooling(request_id,seq,params,arrival_time=arrival_time,lora_request=lora_request,prompt_adapter_request=prompt_adapter_request,encoder_seq=encoder_seq,priority=priority)else:raise ValueError("Either SamplingParams or PoolingParams must be provided.")# Add the sequence group to the scheduler with least unfinished seqs.costs = [scheduler.get_num_unfinished_seq_groups()for scheduler in self.scheduler]min_cost_scheduler = self.scheduler[costs.index(min(costs))]min_cost_scheduler.add_seq_group(seq_group)return seq_group
SequenceGroup
表示的是多個 Sequence
的集合,通常是因為這些 Sequence
共享相同的采樣參數(shù)(如溫度、采樣策略等)以及優(yōu)先級調(diào)度策略(如 priority)。SequenceGroup
的創(chuàng)建是通過 _create_sequence_group_with_sampling
或 _create_sequence_group_with_pooling
方法完成的,具體取決于是否采用采樣策略或者池化策略。
SamplingParams 是用于控制模型生成文本時的行為的參數(shù),比如溫度(temperature)、采樣概率(top_p)等。SamplingParams 會影響生成的策略,比如生成的多樣性、生成的質(zhì)量等。
def _create_sequence_group_with_sampling(self,request_id: str,seq: Sequence,sampling_params: SamplingParams,arrival_time: float,lora_request: Optional[LoRARequest],trace_headers: Optional[Mapping[str, str]] = None,prompt_adapter_request: Optional[PromptAdapterRequest] = None,encoder_seq: Optional[Sequence] = None,priority: int = 0,) -> SequenceGroup:"""Creates a SequenceGroup with SamplingParams."""max_logprobs = self.get_model_config().max_logprobsif (sampling_params.logprobsand sampling_params.logprobs > max_logprobs) or (sampling_params.prompt_logprobsand sampling_params.prompt_logprobs > max_logprobs):raise ValueError(f"Cannot request more than "f"{max_logprobs} logprobs.")sampling_params = self._build_logits_processors(sampling_params, lora_request)# Defensive copy of SamplingParams, which are used by the sampler,# this doesn't deep-copy LogitsProcessor objectssampling_params = sampling_params.clone()sampling_params.update_from_generation_config(self.generation_config_fields, seq.eos_token_id)# Create the sequence group.seq_group = SequenceGroup(request_id=request_id,seqs=[seq],arrival_time=arrival_time,sampling_params=sampling_params,lora_request=lora_request,trace_headers=trace_headers,prompt_adapter_request=prompt_adapter_request,encoder_seq=encoder_seq,priority=priority)return seq_group
3.4.1 深入理解 SequenceGroup
SequenceGroup 是 vLLM 中一個核心概念,它代表了一組共享相同采樣參數(shù)和調(diào)度優(yōu)先級的序列集合。讓我們通過一個具體的生命周期來理解 SequenceGroup:
初始化階段
- 每個 SequenceGroup 初始時只包含一個序列(seq),這個序列對應(yīng)用戶輸入的 prompt
- 初始序列的狀態(tài)被設(shè)置為
waiting
,等待調(diào)度器分配資源
第一次推理階段(Prefill)
- 當(dāng)調(diào)度器選中該 SequenceGroup 后,會首先執(zhí)行 prefill 操作
- 如果采樣參數(shù)中設(shè)置了 n > 1(例如 n = 4),系統(tǒng)會基于初始序列生成 n 個分支
- 所有生成的序列狀態(tài)都變?yōu)?
running
,開始并行生成 tokens
資源競爭階段(Preemption)
當(dāng) GPU 資源不足時,調(diào)度器會觸發(fā)搶占機制。此時根據(jù)序列數(shù)量采取不同策略:
a) Swap 策略 (序列數(shù)量 > 1)
- 適用于多序列場景
- 將 SequenceGroup 下所有序列的 KV Cache 從 GPU 轉(zhuǎn)移到 CPU
- 所有序列狀態(tài)變?yōu)?
swapped
- 保留已計算的 KV Cache,避免重復(fù)計算
b) Recomputation 策略 (序列數(shù)量 = 1)
- 適用于單序列場景
- 釋放該 SequenceGroup 占用的所有 GPU 內(nèi)存塊
- 將序列重新放入 waiting 隊列
- 下次調(diào)度時從 prefill 階段重新開始
- 選擇重計算的原因:單序列重新計算 KV Cache 的成本相對較低
sequence_group 的屬性:
- seqs_dict
self.seqs_dict: Dict[int, Sequence] = {}
-
存儲序列ID到Sequence對象的映射
-
使用字典結(jié)構(gòu)實現(xiàn)快速查找和管理
-
每個 Sequence 對象包含序列的狀態(tài)、token 歷史等信息
-
sampling_params
self.sampling_params: SamplingParams
-
控制文本生成的關(guān)鍵參數(shù)
-
包含溫度(temperature)、top_p、top_k等采樣策略
-
影響生成文本的多樣性和質(zhì)量
-
metrics
self.metrics: Dict[str, Any] = {"arrival_time": float,"first_scheduled_time": Optional[float],"first_token_time": Optional[float],... }
- 記錄序列組的關(guān)鍵時間點和性能指標
- 用于調(diào)度器進行決策和性能分析
- 包括到達時間、首次調(diào)度時間、首個token生成時間等
-
max_running_steps
def get_max_num_running_steps(self) -> int:"""計算剩余生命周期內(nèi)的最大并行序列數(shù)"""
- 預(yù)估序列組在整個生成過程中需要的最大并行步數(shù)
- 幫助調(diào)度器進行資源規(guī)劃和分配
- 考慮了采樣參數(shù)和當(dāng)前生成狀態(tài)
實現(xiàn)細節(jié):
class SequenceGroup:"""A group of sequences that are generated from the same prompt.Args:request_id: The ID of the request.seqs: The list of sequences.sampling_params: The sampling parameters used to generate the outputs.arrival_time: The arrival time of the request.lora_request: LoRA request.pooling_params: The parameters used to generate the poolerfor a pooling model.pooled_data: The extracted hidden states from a pooling model.encoder_seq: Optional, the single encoder sequence. Should be Noneunless you are working with an encoder/decoder model.trace_headers: OpenTelemetry trace headers.prompt_adapter_request: Prompt Adapter request.priority: User-defined priority of the request."""def __init__(self,request_id: str,seqs: List[Sequence],arrival_time: float,sampling_params: Optional[SamplingParams] = None,lora_request: Optional[LoRARequest] = None,pooling_params: Optional[PoolingParams] = None,pooled_data: Optional[torch.Tensor] = None,encoder_seq: Optional[Sequence] = None,trace_headers: Optional[Mapping[str, str]] = None,prompt_adapter_request: Optional[PromptAdapterRequest] = None,priority: int = 0,) -> None:self.request_id = request_idself.seqs = seqsself.first_seq = seqs[0]self.arrival_time = arrival_timeself.is_single_seq = len(seqs) == 1self.seqs_dict = {seq.seq_id: seq for seq in seqs}self.sampling_params = sampling_paramsself.metrics = RequestMetrics(arrival_time=arrival_time,last_token_time=arrival_time,first_scheduled_time=None,first_token_time=None,time_in_queue=None)self.last_token_latency = 0.0self.lora_request = lora_requestself.prompt_logprobs: Optional[PromptLogprobs] = Noneself.state = SequenceGroupState()self.pooling_params = pooling_paramsself.pooled_data = pooled_dataself.prompt_adapter_request = prompt_adapter_requestself.encoder_seq = encoder_seqself.trace_headers = trace_headersself.priority = priorityself.cached_request_output = None
4. 小結(jié)
在這篇文章中,我們踏上了探索 vLLM 架構(gòu)的旅程,就像解構(gòu)一臺精密的機器,我們逐層剖析了它的核心組件和運作機制。從整體架構(gòu)設(shè)計開始,我們看到了 Engine、Worker、Scheduler 等組件如何協(xié)同工作,就像一個精心編排的交響樂團,每個部分都在演奏著自己的樂章,共同譜寫出高效服務(wù)的樂章。
在深入研究請求處理流程時,我們見證了一個請求從誕生到完成的完整生命歷程。就像一顆種子從播種到生長,每個階段都經(jīng)過精心的規(guī)劃和呵護。從最初的模型初始化,到 KV Cache 的預(yù)分配,再到請求的具體處理,vLLM 展現(xiàn)出了令人印象深刻的工程智慧。
特別值得一提的是輸入數(shù)據(jù)的預(yù)處理機制,這就像是一個細心的廚師在烹飪前的準備工作。通過精心設(shè)計的 Tokenization 處理、靈活的 Prompt 模板系統(tǒng),以及嚴謹?shù)恼埱篁炞C流程,vLLM 確保了每個輸入都能被完美處理。而在序列管理方面,Sequence 和 SequenceGroup 的設(shè)計則展現(xiàn)了框架在處理復(fù)雜場景時的優(yōu)雅解決方案。
這次的探索之旅讓我們對 vLLM 的基礎(chǔ)架構(gòu)有了深入的認識,但這僅僅是開始。在下一篇文章中,我們將繼續(xù)深入探討 vLLM 最引人注目的兩大核心機制:調(diào)度器(Scheduler)的智能調(diào)度策略和內(nèi)存管理器(BlockManager)的高效內(nèi)存管理。這兩個機制就像是 vLLM 的雙翼,讓它能夠在高并發(fā)的天空中自由翱翔。我們將看到調(diào)度器如何像一個睿智的指揮官,統(tǒng)籌安排每個請求的處理時機,以及內(nèi)存管理器如何通過創(chuàng)新的 PagedAttention 機制,讓有限的顯存發(fā)揮出最大效能。
5. 參考資料
[1] vLLM Team, “vLLM Documentation,” vLLM Official Documentation, 2024. [Online]. Available: https://docs.vllm.ai/en/latest/
[2] vLLM Project Contributors, “vLLM: Easy, Fast, and Cheap LLM Serving,” GitHub Repository, 2024. [Online]. Available: https://github.com/vllm-project/vllm
[3] vLLM Team, “Serving LLMs at Scale with vLLM,” vLLM Blog, Jun. 2023. [Online]. Available: https://blog.vllm.ai/2023/06/20/vllm.html
[4] vLLM Community, “vLLM Discussions,” GitHub Discussions, 2024. [Online]. Available: https://github.com/vllm-project/vllm/discussions
[5] Y. Feng, “vLLM Diagram Overview,” Personal Blog, Sep. 2024. [Online]. Available: https://fy2462.github.io/2024/09/vllm-diagram-overview/
[6] PaddleJitLab, “vLLM Source Code Analysis: Scheduler,” GitHub Repository, 2024. [Online]. Available: https://github.com/PaddleJitLab/CUDATutorial/blob/develop/docs/16_vllm_source_code/03_scheduler.md