1、VLLM
vLLM是一個建立在【PagedAttention】之上的高吞吐的【分布式服務引擎】,目標是【提高吞吐量】、【提高內存利用率】(kv-cache內存利用率高達96%),它的內存管理分配方式從【固定分配】改進為【分頁管理】,類似操作系統中的分頁管理,vLLM本體是一個Python編寫的開源大語言模型推理框架 https://github.com/vllm-project/vllm。
常規大模型推
理主要存在【顯存】與【吞吐】方面的挑戰:
【顯存】:
- 【請求的輸入輸出長度會不同】:固定長度的預分配內存會造成較多內存浪費,同時kv-cache無法共享內存,單純以填充的方式 會導致顯存浪費。
- 【大型kv-cache】:對llm來說,一個請求的kv-cache需求可以高達1.6G,顯存本身一般只有數十G,即便不計weights占用,所有顯存都給kv-cache,也不過幾十個請求;
低效內存管理會產生越來越多的內存碎片,占用更多顯存,進一步減少批處理大小。
【吞吐】:
- 【請求不會同時到達】:一般的批處理策略 會等待請求全部到達,滿足一個批次的要求,再進行推理,導致較大的推理延遲;
- 【復雜的解碼算法】:greedy search、beam search、sampling等解碼算法對內存管理的復雜性要求不同,需要高效兼容它們,不使解碼成為推理瓶頸。
2、原理篇
KV cache
在自回歸生成過程中,模型需要逐步生成每個token,而每次生成新token時,傳統的Transformer架構會重新計算所有歷史token的Key和Value矩陣。
這種重復計算導致計算復雜度呈二次增長(O(n 2 ?d)),KV Cache通過緩存已計算的Key和Value向量,僅需計算當前token的Query向量,并與緩存的K/V進行注意力計算,將復雜度降至線性(O(n?d)),大幅加快推理速度。
【paged attention關鍵一】【block table的設計】:paged attention脫胎于操作系統的【分頁】設計。
- 【分頁】:程序【內存】按4k分頁,每一頁是一個邏輯地址,映射表為【邏輯地址,物理地址】的映射;
- 【paged attention】:【prompt】按【block】分塊,【每個block中有4個token】,每個block相當于一個4k分頁,為邏輯block,
映射表為【該映射表 邏輯block的index 在 物理block中的『index』,已填充的token的數量】。 - paged attention 塊(block)中有詞元(token),并且后續需要持續增加token,導致映射表需要把index這個因素利用進來,
同時增加【已填充的token的數量】這個信息,標記是否還能在這個block中增加token。
【pagedattention關鍵二】【并行采樣的過程】:block內存的【共享】與【分裂】(copy on write)。
每個【物理塊】會帶著一個ref_count計數,表示有多少個【虛擬塊】指向它,這個ref_count是共享內存的關鍵。
- 【共享】:可能是不同的請求,可能是相同請求中不同的位置,它們在邏輯block中的4個token完全一致,此時它們共享一個物理block。
- 【分裂】:兩個請求的邏輯block,前1個token計算結果相同時,它們會指向一個新的物理block,
存放第一個相同的token,如 “one”,該【物理blockA】的ref_count_A被設置為2,
及至第二第三個token的計算結果也相同,如"two",“three”,ref_count_A仍為2;
直到第4個token,兩個請求計算出了不同的結果"ten"與"hundred",
此時copy on write被觸發,【物理blockA】被復制到【物理blockB】,
【物理blockA】的第4個token被寫入"ten",【物理blockB】的第4個token被寫入"hundred",
分別為【“one”,“two”,“three”,『“ten”』】,【“one”,“two”,“three”,『“hundred”』】,
ref_count_A被【修改】為1,ref_count_B被【設置】為1,一輪分裂結束。
其他
-
離散顯存
對比非paged attention的顯存管理方式(分配固定大小),paged attention顯存與token【在邏輯上是連續的】,【在物理地址上是離散的】,
【多個】【不同長度】的請求可以【同時】做推理,還能【共享內存】——>【內存利用率】、【吞吐量】都得到了大量提升。 -
調度與搶占:
當區塊大小【較小】時,【重新計算】效率更高,因為小塊會導致GPU CPU之間有大量小塊傳輸,占用帶寬;
當區塊大小【較大】時,【交換】(將逐出的頁面復制到磁盤上的交換空間)效率更高。
3、實現篇
api──engine
│ ├──scheduler──blockmanager
│ ├──executor──worker──model runner──model loader(lora)
│ │ ├──models──layer(attention)──ops(pytorch)──kernels
│ │ ├──sampleout(spec_decode)
- API: vLLM的一般有兩種用法,異步的 API Server,同步的LLMEngine。
- API Server: 異步服務端,它會為每一個request關聯一個output stream,用來返回推理生成的結果。
- LLMEngine API: 多用于測試、調試代碼,它是對下層引擎的封裝,并將引用層傳入的參數組成engine_arges, 提供給引擎。
- Engine: Engine層的LLMEngine具體由Scheduler與Executor兩個部分組成,Scheduler負責邏輯上的虛擬調度,Executor負責實際分配顯存與運行推理。
- Scheduler:
-
SequenceGroup: SequenceGroup是為了llm推理時存在的【一個prompt - 多個output】設計的,Scheduler首先會把prompt(request)轉換為SequenceGroup,這是一個list,里面的元素是若干個Sequence,它們共享相同的prompt。
-
Queue status: Scheduler維護了3個隊列,其中waiting隊列主要是用戶剛剛輸入到scheduler中的token序列;running隊列是正在進行推理的token序列;swapped隊列是當顯存不足或者推理優先級降低時,從GPU中換出的token序列
-
BlockManager: BlockManager下屬兩個KVCache分配器UncachedAllocator與CachedAllocator。UncachedAllocator是正常token的KVCache分配器, 通過block_table維護分配狀態;CachedAllocator主要添加了token前綴hash計算,會將相同前綴的KVCache直接復用起來,并引入了evictor等概念,被清除的KVCache會先暫時根據LRU原則轉移到evictor中,如果短時間內又出現相同前綴的token,可以恢復它的前綴KVCache使用。
-
Block Table: 記錄邏輯塊號 到 物理塊號的映射,類似頁表的功能,是由虛到實的連接。
-
Evictor: 驅逐器,顯存不足或新請求分配時,主動淘汰低優先級數據,如最近最少使用,提高顯存利用效率,提升吞吐量。
-
- Executor: 統籌模型實際運行配置config,有單卡單worker的使用,也有ray分布式框架組織的worker集群,核心類是Worker。
- Worker:
- ModelRunner: Worker的核心類,主要執行load_model()、capture_model(self.gpu_cache)、prepare_model_input(seq_group)、execute_model(…)的操作,其中的ModelLoader加載實際具體的llm模型結構如llama、qwen、deepseek等。
- Attention Backend: 因為會直接和GPU或者其他計算平臺打交道,這里封裝了一層Attention Backend, 屏蔽這些不同平臺的差異細節,如CUDA的Graph與Cache Engine,或者是別的gpu平臺如Biren。
- Backend: Backend負責從模型layers-ops-gpu kernel的調用,涉及到具體硬件算子的調用,在大語言模型transformer架構中,最核心的是attention類算子。
(以下的文件路徑參考vllm 0.6.0)
3.1、api
prompts = ["Hello, my name is","The president of the United States is","The capital of France is","The future of AI is",]
sampling_params = SamplingParams(temperature=0.8, top_p=0.95)
llm = LLM(model="facebook/opt-125m") # 假數據的模擬推理在這里進行,得到kvcache塊大小
outputs = llm.generate(prompts, sampling_params) # 用戶視角:1個批處理請求 # 系統視角:4個獨立請求
for output in outputs:prompt = output.promptgenerated_text = output.outputs[0].textprint(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
vllm的推理引擎內核(LLMEngine),實際上是【異步】工作的,batch size是根據當前【輸入情況】與【顯存情況】動態變化的。
【同步】的離線推理,只是把結果全部收集完畢后,再一起返回給我們,看起來是同步,實際上也是【異步】工作的。
vllm/vllm/engine/
arg_utils.py的class EngineArgs中,有所有定義的參數,部分參數說明如下:
參數名 | 類型 | 默認值 | 說明 |
---|---|---|---|
model | str | 必填 | HuggingFace模型名稱或本地路徑(如"meta-llama/Llama-3-8B-Instruct" ) |
tokenizer | str | None | 自定義分詞器路徑,未指定時使用與模型匹配的分詞器 |
tokenizer_mode | str | "auto" | 分詞器模式:"auto" (自動選擇快速分詞器)、"slow" (強制慢速分詞器) |
trust_remote_code | bool | False | 是否信任遠程代碼(如HuggingFace自定義模型代碼),存在安全風險 |
tensor_parallel_size | int | 1 | 張量并行GPU數量,需與模型規模匹配(如70B模型需8 GPU) |
dtype | str | "auto" | 計算數據類型:"float16" 、"bfloat16" 、"float32" ,"auto" 根據模型配置自動選擇 |
device | str | "auto" | 硬件設備類型:"cuda" 、"cpu" |
quantization | str | None | 量化方法:"awq" 、"gptq" 、"bitsandbytes" 等,用于減少顯存占用 |
gpu_memory_utilization | float | 0.9 | GPU顯存利用率(0~1),值越高KV緩存越大,但可能引發OOM |
swap_space | int | 4 | 每個GPU的CPU交換空間大小(GiB),用于臨時存儲不活躍請求的KV緩存 |
block_size | int | 16 | KV緩存塊大小(token數),較小值減少內存碎片,較大值提升長文本性能 |
max_num_batched_tokens | int | 2048 | 單次批處理的最大token數,影響吞吐量,高并發場景建議增大 |
max_num_seqs | int | 256 | 最大并發處理序列數,受GPU顯存限制 |
disable_async_output_proc | bool | False | 禁用異步輸出處理(默認啟用),關閉后可能降低性能 |
enable_prefix_caching | bool | False | 啟用前綴緩存,避免重復計算共享提示詞,提升聊天場景性能 |
enable_chunked_prefill | bool | False | 啟用分塊預填充,優化長輸入的內存使用,性能提升15-25% |
scheduling_policy | str | "fcfs" | 調度策略:"fcfs" (先到先服務)或"priority" (優先級調度) |
num_scheduler_steps | int | 1 | 每次調度的前向步驟數,多步調度(如設為10)可提升吞吐量 |
kv_cache_dtype | str | "auto" | KV緩存數據類型(fp8 /fp8_e4m3 等),支持FP8顯存優化 |
speculative_model | str | None | 推測解碼的草稿模型名稱(如facebook/opt-125m ) |
num_speculative_tokens | int | 0 | 推測解碼時采樣的token數量 |
enable_lora | bool | False | 啟用LoRA適配器支持 |
max_loras | int | 1 | 單批次中LoRA的最大數量 |
guided_decoding_backend | str | "xgrammar" | 引導解碼引擎(如outlines-dev/outlines ) |
preemption_mode | str | "recompute" | 搶占模式(recompute 重計算/swap 交換) |
spec_decoding_acceptance_method | str | "rejection_sampler" | 推測解碼的接受方法(rejection_sampler 或typical_acceptance_sampler ) |
disable_logprobs_during_spec_decoding | bool | False | 推測解碼期間禁用log概率返回 |
speculative_model_quantization | str | None | 草稿模型的量化方法(如awq ) |
speculative_draft_tensor_parallel_size | int | 1 | 草稿模型的張量并行GPU數量 |
ngram_prompt_lookup_min/max | int | None | 推測解碼中N-gram提示查找的窗口大小范圍 |
multi_step_stream_outputs | bool | True | 多步調度時是否流式輸出所有步驟結果 |
3.2、engine
engine中包含最關鍵的 【決策層Scheduler】 與 【執行層Executor】。
vllm/vllm/engine/llm_engine.py的class LLMEngine是vllm的核心類,有關鍵成員scheduler、executor,它們的進一步說明在下面的章節。
- scheduler的創建
386 self.scheduler = [387 Scheduler(388 scheduler_config, cache_config, lora_config,389 parallel_config.pipeline_parallel_size,390 self.async_callbacks[v_id]391 if model_config.use_async_output_proc else None)392 for v_id in range(parallel_config.pipeline_parallel_size)393 ]
- executor的創建
305 self.model_executor = executor_class(306 model_config=model_config,307 cache_config=cache_config,308 parallel_config=parallel_config,309 scheduler_config=scheduler_config,310 device_config=device_config,311 lora_config=lora_config,312 speculative_config=speculative_config,313 load_config=load_config,314 prompt_adapter_config=prompt_adapter_config,315 observability_config=self.observability_config,316 )
class LLMEngine中關鍵函數def add_request、def step:
- add_request():把每一個請求包裝成SequenceGroup,并加入waiting隊列
- step():執行一次推理過程,1個【prefill】算一個推理,每個【decode】各算一個推理
加載模型與預分配顯存
【第一條】請求過來的時候,要【加載模型】與【預分配顯存】(加載模型會加載已經搭建好的模型結構,這里注意看預分配顯存)
【預分配顯存】
-
1、杜撰假數據
max_num_seqs、max_num+batched_token:
在【一個推理階段】中,LLMEngine【最多能處理的【seq條數】與【token數】】,
平均一個seq要處理max_num_batched_tokens // max_num_seqs個token,余數部分我們默認放在第一個seq中,
假設max_num_batched_tokens=10,max_num_seqs = 3,那么我們就能杜撰出3條seq,每個seq的長度分別為4,3,3。 -
2、用假數據做一次不使用kv-cache的前向推理
這是為了【精確測量】所有顯存占用的情況,建立【最壞】顯存占用的邊界:
實際模型運行過程中,除了模型本身帶來的【固定靜態顯存】大小之外,還會存在:
①動態顯存:與輸入形狀(batch_size, seq_len)強相關的【中間activation】與【temporary tensors】
②系統顯存占用:pytorch相關的啟動顯存、內存碎片等
所以:【可以分配給KV cache的顯存】 = gpu總顯存 - 不使用kv-cache做一次推理所占用的顯存(運行時記錄) -
3、計算【可以分配給KV cache的【物理塊數量】】
【物理塊數量】=【可以分配給KV cache的顯存】/【物理塊大小】
【物理塊大小】=【物理塊尺寸block_size】(一個物理塊可以容納的token數量,vllm默認值是16)x【每個token在kv_cache中占用的大小】
【每個token在kv_cache中占用的大小】:
首先,token的本質可以是【英文子詞拆分】或者【中文字符映射】:
“transformer” → [“trans”, “former”] # 子詞拆分
“你好” → [37955] # 中文字符直接映射
其次,需要區分兩種token的存儲需求【嵌入存儲】與【kvcache存儲】(此處以【bert large bf16】為例(1024),bert base則d_model為768):
【嵌入存儲】大小:d_model(模型核心維度) x dtype_size = 1024 x 2 = 2048
【kvcache存儲】大小:k向量+v向量,它倆均為【num_heads x head_size x num_layers 個元素】的大小,
num_heads:注意力頭數,large為16,
head_size:單頭維度,= d_model / num_heads = 1024/16 = 64,
num_layers:transformer層數,bert large中為encoder層數24,
故【kvcache存儲】大小 = num_heads x head_size x num_layers x dtype_size x 2
= 16x64x24x2x2 = 98, 304 Bytes = 96 K
= 【每個token在kv_cache中占用的大小】
由此可得 【物理塊大小】=【物理塊尺寸block_size】x【每個token在kv_cache中占用的大小】
= 16 x 98, 304 = 1, 572, 864 Bytes = 1.5M,
【物理塊數量】=【可以分配給KV cache的顯存】/【物理塊大小】=【可以分配給KV cache的顯存】/1.5M
(上述計算是基于bert large的,如果以gpt 175B為例,物理塊大小 ≈ 4.7M x 16 ≈ 72M)
- 4、將預分配的塊加載到gpu上
按照計算好的塊大小與數量,創建empty tensor,加載到gpu中,實現顯存預分配,這些內存專門用于做kvcache
3.3、scheduler
./vllm/vllm/core/scheduler.py與./vllm/vllm/sequence.py比較關鍵的兩個文件。
SequenceGroup
存在【一個prompt -> 多個output】的情況,SequenceGroup是為了這種【一對多】的情況設計的,
如decode,parallel sampling(n=3) 或者 beam search(n=3, best_of=3)(強制要求n=best_of,輸出topK個output) 時,
一個SequenceGroup中,有【3個seq,且它們都共享同一個prompt】,
每個seq都有若干狀態:
waiting:沒做過prefill的,
running:做完prefill,已經開始做推理的,
swapped:gpu顯存不夠暫時被換到cpu,等待換回來繼續推理的,被搶占的(最新vllm已經移除這個狀態)
scheduler中,prompt變為SequenceGroup的過程:
- 1、在LLMEngine.encode_request()中,每個prompt字符串通過tokenizer轉換為對應的prompt_token_ids,比如:
prompt_token_ids = tokenizer.encode(“Hello, my name is”) # 可能得到 [1, 150, 50, 10] - 2、Sequence對象??:每個prompt_token_ids會實例化為Sequence,核心屬性包括:
prompt_token_ids: 編碼后的輸入token IDs。
output_token_ids: 初始為空,用于存儲生成的token。
status: 初始為SequenceStatus.WAITING,表示待調度。
logical_token_blocks: 根據block_size(默認為16)劃分邏輯塊,記錄token的內存占用。 - 3、??SequenceGroup封裝??:所有Sequence被封裝為SequenceGroup,關鍵屬性包括:
request_id: 請求的唯一標識。
seqs_dict: 以seq_id為鍵的Sequence字典。
sampling_params: 采樣策略(如temperature、top_k等)。
arrival_time: 請求到達時間戳。
scheduler調度
add_request()->每個prompt包裝成sequenceGroup實例->放到waiting(waiting隊列中的seq_group只有一個seq,即是原始的prompt)
step()->決定哪些seq_group被送去推理->model_executor執行推理->放到running
(做prefill):【prompt->SequenceGroup->list[Sequence],Sequence->list[LogicalTokenBlock]+data+status,LogicalTokenBlock->block_number(logic)+block_size(16) 】
- ??調度器處理??:Scheduler將SequenceGroup加入隊列,根據優先級和資源(如GPU block可用性)決定何時執行。調度時會生成SequenceGroupMetadata,包含:
- seq_data: 各Sequence的token IDs和狀態。
- block_tables: 記錄每個Sequence的物理內存塊映射。
- ??執行階段??:當SequenceGroup被選中執行時:
1、is_prompt標記為True(首次推理)。
2、模型根據prompt_token_ids生成logits,采樣得到新token(如"John")。
3、新token追加到output_token_ids,并更新logical_token_blocks。 - 關鍵中間類
- SequenceData??:存儲序列的token IDs和累計log概率,是Sequence的核心數據成員。
- ??SequenceStatus??:枚舉類,管理序列的生命周期狀態(如RUNNING、FINISHED)。
- ??LogicalTokenBlock??:管理邏輯內存塊,記錄token分布和空閑槽位,與物理塊通過【block_tables】映射,這是虛與實連接的紐帶,類似【頁表】。
3.4、block_manager
./vllm/vllm/core/block_manager_v1.py與vllm/vllm/block.py中,
- 主要類嵌套關系:
Scheduler->BlockSpaceManagerV1->UncachedBlockAllocator/CachedBlockAllocator
CachedBlockAllocator通過前綴復用提升性能與顯存效率??,而??UncachedBlockAllocator以簡單性換取通用性??。
實際應用中可根據請求特征(如前綴重復率、顯存限制)靈活選擇兩者,同時這兩者都可以設置為self.gpu_allocator 或者self.cpu_allocator。 - 主要函數調用堆棧
BlockSpaceManagerV1.allocate()->BlockSpaceManagerV1.allocate_sequence->CachedBlockAllocator.allocate()->BlockSpaceManagerV1.block_tables: Dict[str, BlockTable] - 虛擬塊號與物理塊號的關聯過程
- 0、Sequence創建時會自帶Counter()全局遞增的seq_id
- 1、BlockSpaceManagerV1.allocate()為SequenceGroup 分配物理塊,會把waiting狀態的seqs拿出來分配顯存,其中的BlockSpaceManagerV1._allocate_sequence()調用CachedBlockAllocator.allocate()實際分配物理塊,
又因為一個prompt對應一個SequenceGroup,seqs們的prompt是相同的,所以只需要為一個seq分配物理塊,其他seq共享此物理塊。 - 2、CachedBlockAllocator.allocate()中的allocate_block()實際申請顯存,返回一個物理塊PhysicalTokenBlock,其中有物理塊號PhysicalTokenBlock.block_number
- 3、BlockTable的成員是 一個【PhysicalTokenBlock的list】 + 一個【PhysicalTokenBlock對應的物理塊號】的list,
在BlockSpaceManagerV1._allocate_sequence()中,通過BlockTable.append()來增加兩個list中的元素。 - 4、prompt->SequenceGroup(Sequence自帶seq_id)->BlockTable->list[PhysicalTokenBlock]:虛擬塊號是list[PhysicalTokenBlock]的index,物理塊號是PhysicalTokenBlock.block_number
通過這個鏈路,可以找到prompt的物理塊。
class Counter:def __init__(self, start: int = 0) -> None:self.counter = startdef __next__(self) -> int:i = self.counterself.counter += 1return idef reset(self) -> None:self.counter = 0self.seq_counter = Counter()
seq_id = next(self.seq_counter)
seq = Sequence(seq_id, processed_inputs, block_size, eos_token_id, lora_request, prompt_adapter_request)class BlockSpaceManagerV1(BlockSpaceManager):def allocate(self, seq_group: SequenceGroup) -> None:wait_seqs = seq_group.get_seqs(status=SequenceStatus.WAITING)seq = wait_seqs[0] # 一個prompt->SequenceGroup->若干Sequence,因此Sequence們的KV緩存物理塊分配是完全相同的,# 只需要為第一個序列(wait_seqs[0])分配物理塊,其他序列通過引用共享這些塊即可。block_table: BlockTable = \self._allocate_sequence(seq,seq_group.num_seqs(),is_encoder_decoder)# Assign the self-attention block tables for each sequence.if len(wait_seqs) == 1:self.block_tables[seq.seq_id] = block_tableelse:for seq in wait_seqs:self.block_tables[seq.seq_id] = block_table.copy()def _allocate_sequence(self, \seq: Optional[Sequence], \ref_count: int, \is_encoder_decoder: bool = True) -> BlockTable:num_prompt_blocks = self._get_seq_num_required_blocks(seq) # seq需要的block數量block_table: BlockTable = BlockTable()assert seq is not Nonefor logical_idx in range(num_prompt_blocks):if (self.block_sliding_window is not Noneand logical_idx >= self.block_sliding_window):block = block_table[logical_idx % self.block_sliding_window]# Set the reference counts of the token blocks.block.ref_count = ref_countelse:block = self.gpu_allocator.allocate() # self.gpu_allocator: BlockAllocatorBase = CachedBlockAllocator()block.ref_count = ref_countblock_table.append(block) # 見BlockTable.append,往BlockTable中增加一個block與其物理塊號return block_table # block_table中是 一個sequence所需的 所有物理塊class CachedBlockAllocator(BlockAllocatorBase):def allocate(self,block_hash: Optional[int] = None,num_hashed_tokens: int = 0) -> PhysicalTokenBlock:if block_hash in self.cached_blocks:self.cache_metric_data.query(hit=True)else:self.cache_metric_data.query(hit=False)self.cached_blocks[block_hash] = self.allocate_block(block_hash, num_hashed_tokens)block = self.cached_blocks[block_hash]assert block.block_hash == block_hashblock.ref_count += 1return block # 返回新建的PhysicalTokenBlockdef allocate_block(self, block_hash: int,num_hashed_tokens: int) -> PhysicalTokenBlock:if self.current_num_blocks == self.num_blocks:block = self.evictor.evict()block.block_hash = block_hashblock.num_hashed_tokens = num_hashed_tokensreturn blockblock = PhysicalTokenBlock(device=self.device,block_number=self.current_num_blocks, # 物理塊號block_size=self.block_size,block_hash=block_hash,num_hashed_tokens=num_hashed_tokens)self.current_num_blocks += 1 #物理塊號 自加1return block # 返回一個新建的PhysicalTokenBlockclass PhysicalTokenBlock:def __init__(self,device: Device,block_number: int,block_size: int,block_hash: int,num_hashed_tokens: int,) -> None:self.device = deviceself.block_number = block_numberself.block_size = block_sizeself.block_hash = block_hashself.num_hashed_tokens = num_hashed_tokensself.ref_count = 0self.last_accessed = DEFAULT_LAST_ACCESSED_TIMEself.computed = Falseclass BlockTable:def __init__(self, blocks: Optional[List[PhysicalTokenBlock]] = None):self._blocks: List[PhysicalTokenBlock] = [] # PhysicalTokenBlock的listself._block_ids: List[int] = [] # PhysicalTokenBlock.block_number的list,物理塊號if blocks is not None:for block in blocks:self.append(block)def append(self, block: PhysicalTokenBlock):self._blocks.append(block)self._block_ids.append(block.block_number)
- 滑動窗口
- 滑動窗口模型的核心思想是:每個序列只能使用固定數量的KV緩存塊(窗口大小),當序列長度超過窗口大小時,舊的KV緩存塊會被循環重用,這樣可以限制每個序列的內存使用量,而不會隨著序列長度無限增長。
- BlockSpaceManagerV1.block_sliding_window實際窗口大小會向上取整為block_size的整數倍。
- 當邏輯索引超過窗口大小時,會循環重用之前的block,確保每個序列使用的block數量不超過窗口大小。
- 由于block是循環重用的,釋放時只需要釋放窗口內的block,使用set去重避免多次釋放同一個block。
- 滑動窗口模型用于優化長序列處理的一種技術,它通過限制每個序列可以使用的KV緩存塊數量來減少內存占用。
3.5、executor
在vllm/executor,有若干相關聯的文件與類:
./executor_base.py:class ExecutorBase(ABC)
./gpu_executor.py:class GPUExecutor(ExecutorBase)
./distributed_gpu_executor.py:class DistributedGPUExecutor(GPUExecutor)
./multiproc_gpu_executor.py:class MultiprocessingGPUExecutor(DistributedGPUExecutor)
./ray_gpu_executor.py:class RayGPUExecutor(DistributedGPUExecutor)
它們的繼承關系是:
ExecutorBase(ABC)
├── GPUExecutor
│ ?????????├── DistributedGPUExecutor
│ ??????│ ??????├── MultiprocessingGPUExecutor
│ ??????│ ??????├── RayGPUExecutor
這里有兩個抽象類(ExecutorBase、DistributedGPUExecutor),三個可實例化的類(GPUExecutor、MultiprocessingGPUExecutor、RayGPUExecutor),它們都包含了如下功能:模型初始化(_init_executor)、KV緩存管理(determine_num_available_blocks, initialize_cache)、模型執行(execute_model)、LoRA和Prompt Adapter管理、健康檢查(check_health)。
它們的區別與聯系在于:
-
ExecutorBase: 抽象基類,定義了Executor的接口,包含基本配置信息(ModelConfig, CacheConfig等),所有方法都是抽象方法,需要子類實現
-
GPUExecutor: 基礎GPU執行器,單設備版本,直接與GPU設備交互,使用GPUWorker作為底層工作器,處理特定的GPU硬件設置,單進程執行模型
-
DistributedGPUExecutor: 分布式GPU執行器的抽象基類,增加了對多設備并行執行的支持,引入了parallel_worker_tasks管理并行任務,提供了_run_workers抽象方法用于在多個worker上執行操作
-
MultiprocessingGPUExecutor: 基于Python多進程的分布式GPU執行器,使用ProcessWorkerWrapper封裝worker進程,通過WorkerMonitor監控worker進程狀態,實現了_run_workers方法,使用多進程通信
-
RayGPUExecutor: 基于Ray的分布式GPU執行器,使用Ray進行分布式執行,支持Ray的placement group進行資源調度,可選使用Ray的編譯DAG優化執行,實現了_compiled_ray_dag方法構建優化執行圖,實現更復雜的worker管理和通信機制
Ray版本提供了最強大的分布式能力,而MultiprocessingGPUExecutor版本適合單機多卡場景,基礎GPUExecutor則用于最簡單的單卡情況。
3.6、worker
在vllm源代碼vllm/vllm/worker/中 有worker.py(默認GPU)、openvino_worker.py、tpu_worker.py、xpu_worker.py等worker。
這些worker均繼承自 LoraNotSupportedWorkerBase 和 LocalOrDistributedWorkerBase,提供統一的 Worker 接口(如 execute_worker、prepare_worker_input),通過 model_runner封裝模型執行邏輯, 支持張量并行(Tensor Parallelism)和流水線并行(Pipeline Parallelism),通過 parallel_config 配置。
3.7、model runner
在vllm源代碼vllm/vllm/worker/中 有model_runner.py、multi_step_model_runner.py、openvino_model_runner.py、tpu_model_runner.py、xpu_model_runner.py等不同硬件的model_runner。
這些model_runner的基礎功能:都實現了 ModelRunnerBase 抽象基類,提供了模型運行的基本功能,包括模型加載、輸入準備和執行模型推理,都使用了AttentionMetadata 和 get_attn_backend 來處理注意力機制,都實現了對輸入序列的批處理邏輯,包括填充(padding)和對齊,都支持KV緩存的維護和使用,都區分了prompt(預填充)和decode(解碼)兩種不同的處理模式。
3.8、model loader與lora
model loader:
vllm/vllm/model_executor/model_loader/loader.py 中
主要是class BaseModelLoader(ABC)與class DefaultModelLoader(BaseModelLoader),以及若干繼承了BaseModelLoader的model loader,在抽象基類BaseModelLoader中,只需要實現def load_model即可。
這些model loader的功能目標相同,均用于加載神經網絡模型(返回nn.Module),支持配置模型、設備、并行策略等參數,均調_initialize_model初始化模型,并通過load_weights加載權重,輸入參數也完全一致(model_config、device_config等)。
lora:
Low-Rank Adaptation低秩適配器,在model loader加載權重時參與,核心是用兩個低秩矩陣相乘得到不同的高秩矩陣,在實際調用時判斷使用何種不同的矩陣去修改基礎權重,得到不同的效果,可支持不同的業務。
lora通過API層(如/v1/load_lora_adapter)或LLM.generate()傳入LoRARequest。
Engine層??接收LoRA請求并傳遞給Scheduler,Scheduler通過BlockManager請求分配顯存塊,封裝成WorkerInput,Worker在forward時,將LoRA參數與基礎模型權重結合:
W′ =W+BA,其中,B∈R^{d×r} ,A∈R^{r×k} (r?d,k為低秩維度)。
這種做法僅需加載基礎模型一次,多個輕量級適配器(<2%參數量)共享顯存,資源高效,而且?支持業務場景多樣化(如客服、翻譯等),每個任務獨立適配器,無需全量微調。
vllm/vllm/lora/ops/lora_ops.py中主要有兩類算子:
bgmv (batch-gather-matrix-vector) ,批聚合矩陣向量乘,適用 多請求并行處理。
sgmv (sequence-gather-matrix-vector),序列聚合矩陣向量乘,適用 單請求的長序列優化。
實現的算子是bgmv_expand、sgmv_expand、bgmv_shrink、sgmv_shrink等.
LoRA通過??動態參數適配??,在vLLM架構中顯著提升多任務效率,LoRA依賴Executor和Worker的協同,側重資源復用,提升了大模型的生產力上限。
3.9、models
在vllm/vllm/model_executor/models/中添加各自大模型的組裝文件,如llama.py、qwen2.py、deepseek.py等文件,使用下述attention與相關layers,使用pytorch,調用cuda后端與算子,組裝完成各種目標模型,最后運行在gpu硬件上。這一步,可以視為一個正常地使用pytorch搭建模型的過程。
3.10、layers與attention
layer層的關鍵經典算子:
vllm/vllm/model_executor/layers/fused_moe/fused_moe.py
vllm/vllm/model_executor/layers/rotary_embedding.py
vllm/vllm/model_executor/layers/layernorm.py
vllm/vllm/model_executor/layers/quantization/gptq.py
vllm/vllm/model_executor/layers/linear.py
vllm/vllm/attention/backends/flash_attn.py
其它是一些流程化的代碼,如forward、create_weights、apply、weight_loader等過程。
Attention要單獨拿出來說明,原始代碼中Attention相關有如下幾個文件:
vllm/vllm/attention/backends/abstract.py
vllm/vllm/attention/ops/paged_attn.py
vllm/vllm/attention/selector.py
vllm/vllm/attention/layer.py
- 抽象層 (abstract.py): 規定所有Attention實現必須提供的接口,定義AttentionBackend(后端實現的工廠接口)、AttentionImpl(定義forward與其接口)、AttentionMetadata(管理注意力計算的元數據)等抽象基類。
- ? ?適配器層 (layer.py): ?承上啟下??,屏蔽底層硬件的差異,同時為上層提供統一的Attention接口,將abstract.py定義的接口轉換為具體后端(如pagedattn.py)可調用的形式。
- 具體實現 (pagedattn.py): ?實現PagedAttention算法,包括KV緩存分塊管理??,維護邏輯塊到物理塊的映射表,以及優化非連續內存的矩陣乘。
- 后端選擇 (selector.py): ?根據硬件(優先選擇硬件原生支持的后端)和模型參數(如head_size、dtype)動態選擇最優后端。
3.11、ops與kernels
使用pytorch中已實現的ops,可以嘗試自己寫更高性能的cuda算子。
3.12、sampleout(spec_decode)
-
Speculative decoding推測解碼(也有稱投機采樣),讓小型草稿模型推理出較多的推測性回答,提升解碼速度,再讓大模型驗證是否正確,在保證準確性的情況下提升效率。
-
通過LLM初始化時指定speculative_model和num_speculative_tokens,Executor層??的SpecDecodeWorker協調草案模型(Proposer)與主模型(Scorer)的執行,SpecDecodeWorker調用輕量級草案模型(如OPT-125M)快速生成候選token序列(如5個token),??候選序列通過RejectionSampler比較概率分布,接受連續匹配的token,若首個token被拒絕,回退到原始自回歸解碼;否則繼續驗證后續token,通過并行生成-驗證減少解碼步數,吞吐量提升30%-50%(尤其短序列)。
-
vllm/vllm/spec_decode/spec_decode_worker.py:class SpecDecodeWorker(LoraNotSupportedWorkerBase)
vllm/vllm/spec_decode/multi_step_worker.py:class MultiStepWorker(Worker, ProposerWorkerBase)
vllm/vllm/spec_decode/draft_model_runner.py:class TP1DraftModelRunner(ModelRunner)
vllm/vllm/spec_decode/target_model_runner.py:class TargetModelRunner(ModelRunner)
流程:使用draft模型生成候選token->使用target模型驗證這些token->接受或拒絕候選token。 -
SpecDecode通過并行解碼優化??,在vLLM架構中顯著提升推理速度,SpecDecode依賴Executor和Worker的協同,側重計算加速,擴展了大模型的生產力邊界,長文本生成延遲降低20%-30%。