【VLLM篇】:原理-实现
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)
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算子。