深入解析 vLLM:高性能 LLM 服务框架的架构之美(一)原理与解析

修改内容时间
2.4.1处理请求的流程,引用更好的流程图2025.02.11
首发2025.02.08

深入解析 vLLM:高性能 LLM 服务框架的架构之美(一)原理与解析
深入解析 vLLM:高性能 LLM 服务框架的架构之美(二)调度管理

1. vLLM 整体代码架构

1.1 vLLM 的设计目标与特点

vLLM 是一个高性能的大语言模型服务框架。在大语言模型日益普及的今天,如何高效地提供推理服务成为一个重要挑战。传统的服务框架在处理并发请求时往往会遇到性能瓶颈、内存管理效率低下等问题。vLLM 正是为解决这些关键挑战而生。

在性能优化方面,vLLM 最大的创新在于引入了突破性的 PagedAttention 机制。这项技术巧妙地将传统的连续注意力计算改造为基于页的形式,使得 GPU 内存的利用效率得到极大提升。通过精心设计的连续批处理机制,vLLM 能够充分利用 GPU 算力,显著提升推理速度。

说到内存效率,vLLM 采用了独特的动态内存管理方案。通过对 KV Cache 的动态管理和页式存储,它能够有效减少内存碎片,提高内存利用率。这就像是在有限的空间里实现了"完美俄罗斯方块",让每一块内存都物尽其用。

在并发处理能力上,vLLM 表现同样出色。它采用灵活的调度策略,能够同时处理大量并发请求,并通过动态负载均衡确保资源的最优分配。这种设计让 vLLM 能够轻松应对高并发场景,就像一个经验丰富的交通指挥官,让所有请求都能高效有序地通行。

作为一个服务框架,vLLM 的架构设计非常灵活。它不仅支持单机部署,还能轻松扩展到分布式环境。框架兼容主流的 LLM 模型,并提供简洁的 API 接口,大大降低了开发者的使用门槛。

在实际应用中,vLLM 展现出了令人印象深刻的性能表现。相比传统方案,它能够将推理服务的吞吐量提升 2-4 倍,同时显著降低请求延迟。这种性能提升不仅体现在数据上,更为用户带来了明显的体验改善。

对开发者而言,vLLM 提供了完善的工具支持。从简单的集成方式,到丰富的监控指标和调优选项,都体现了框架在易用性方面的深思熟虑。这使得开发者能够快速上手,并根据实际需求进行灵活调整。

1.2 核心组件概览

J51BBg

vLLM 的系统架构由几个关键组件构成,它们各司其职又相互协作,共同支撑起整个服务框架。让我们深入了解每个组件的核心功能:

Engine 是整个系统的中枢,负责协调其他组件的工作。它就像一个指挥家,统筹管理着模型加载、请求分发、结果收集等核心流程。Engine 接收用户请求后,会将其转化为内部任务,并交由其他组件处理。

Worker 是实际执行推理计算的工作单元。每个 Worker 都可以独立处理分配给它的计算任务,就像工厂中的熟练工人。在分布式部署时,多个 Worker 可以并行工作,大大提升系统的处理能力。

Scheduler 是 vLLM 的调度大脑,它的主要职责是合理分配计算资源和管理请求队列。通过精心设计的调度算法,Scheduler 能够权衡不同请求的优先级、资源需求和系统负载,确保系统高效运转。

BlockManager 负责内存资源的精细化管理。它采用块式管理策略,将 GPU 内存划分为多个块,并通过智能的分配和回收机制,最大化内存使用效率。这就像是一个经验丰富的仓库管理员,让每一寸空间都得到充分利用。

PagedAttention 是 vLLM 最具创新性的组件。它重新设计了注意力机制的实现方式,引入页式管理的概念,使得 KV Cache 的管理更加灵活高效。这项技术显著提升了推理性能,是 vLLM 性能优势的关键所在。

CacheEngine 专注于缓存策略的实现。它通过智能的缓存机制,减少重复计算,提升响应速度。就像是系统的记忆库,能够快速调用常用的计算结果。

这些组件通过精心设计的接口相互配合,形成了一个高效协同的整体。每个组件都可以独立优化和扩展,这种模块化的设计既保证了系统的灵活性,也便于维护和升级。

1.3 系统架构与工作流程

vLLM 的系统架构采用了模块化设计,各个组件之间通过清晰的接口进行交互。下面这张架构图展示了各个核心组件及其关系:
B0lniS

当用户发起一个推理请求时,整个处理流程是这样的:首先,请求会经由 Engine 接收和解析。Engine 会对请求进行初步处理,包括参数验证、格式转换等。这就像是前台接待员,确保所有进入系统的请求都是规范的。

接下来,Engine 会将请求交给 Scheduler 进行调度分析。Scheduler 会根据当前系统的负载状况、可用资源情况,为这个请求制定处理计划。它会考虑多个因素:是否有空闲的 Worker?内存资源是否充足?是否需要进行批处理优化?这个过程就像是一个经验丰富的项目经理,在有限的资源下做出最优的任务分配。

在执行阶段,Worker 承担着最重要的计算任务。它们在 Scheduler 的指挥下,有条不紊地进行模型推理计算。这时,BlockManager 会密切配合,确保所需的内存资源能够及时到位。如果发现内存不足,BlockManager 会启动交换机制,像变魔术一样腾出空间来。

PagedAttention 在这个过程中发挥着关键作用。它创新性地使用页式管理方式处理注意力计算,使得长序列推理变得更加高效。这就像是给计算过程装上了"加速器",显著提升了处理速度。

CacheEngine 则在整个过程中不断优化性能。它会智能地缓存一些计算结果,在遇到相似请求时直接复用,避免重复计算。这就像是系统的"备忘录",能够快速提供历史经验。

最后,处理结果会再次回到 Engine,由它负责结果的整理和返回。整个过程环环相扣,每个组件都各司其职又紧密协作,共同确保了请求处理的高效性。

在分布式部署场景下,这个架构还能轻松扩展。多个 Worker 节点可以并行工作,Scheduler 会自动协调它们的任务分配,就像是一个指挥交响乐团的指挥家,让所有乐器都能配合得天衣无缝。

2. vLLM 处理请求的流程

一个请求在 vLLM 中的生命周期是一个精心编排的过程,涉及多个组件的协同工作。主要实现在 vllm/engine/llm_engine.pyvllm/engine/ray_worker.py 中。

2.1 初始化并加载模型权重

xL0fCE

如上图所示,vLLM 的初始化过程包括模型加载、模型参数初始化、KV Cache 预分配等关键步骤。
vLLM需要初始化并加载模型权重,支持从HF Hub加载模型,也支持从本地加载模型。在加载过程中,vLLM将模型权重加载到GPU中,以便后续推理在GPU运行。

2.2 估计KV Cache的物理块数量

qIQ4Mt

在模型部署的初始化阶段,vLLM 会通过一个模拟实验步骤来决定 GPU 和 CPU 上可以分配的 KV cache 物理块数量,确保后续推理时的内存分配不会导致显存溢出。这个步骤在 vLLM 中被称为 determine_num_available_blocks。

首先,在启动 LLMEngine 时,系统会进行一个 “假数据模拟” 来测量模型的内存使用情况。它通过构造假数据并执行一次模拟前向推理,来观察 GPU 上模型运行时的峰值内存需求。在这次前向推理中,系统不使用 KV cache,而是单纯地模拟模型推理所需的基本内存。这种方式可以帮助确定整个推理过程会占用多少显存,从而为后续的内存分配提供依据。

在完成内存需求的测量后,vLLM 会使用测得的内存数据来计算可分配给 KV cache 的显存总量。具体来说,分配给 KV cache 的显存等于 GPU 总显存减去在不使用 KV cache 时推理所占用的显存(包括模型本身和推理过程中的中间数据)。这样可以确保显存分配合理,不会因为内存不足而导致 OOM(Out Of Memory)错误。

接下来,通过计算显存中可以分配的物理块数量,vLLM 会确定 GPU 上可以使用的 KV cache 数量。物理块的大小由用户定义,包括多个参数,例如 block_size、num_heads、head_size、num_layers 以及数据类型的大小(如 fp16 对应的字节数是 2)。计算公式会依据这些参数来估算单个物理块的大小,然后根据剩余显存估算出可以分配的物理块总数。

2.3 预分配 KV Cache

quECx7

在确定好 KV cache 块的大小之后,vLLM 会进行显存的预分配,以确保后续推理过程中有足够的内存来存储 KV cache。这一过程的核心是创建空的张量(empty tensor),并将它们直接分配到 GPU 上,从而锁定一定的显存空间专门用于 KV cache。这种显存预分配的方式能够避免推理过程中频繁的动态内存分配,提升系统的稳定性和推理效率。

预分配的显存专门用于 KV cache,因此在 vLLM 初始化后,你可能会注意到显存的占用比单纯加载模型时要多一些。这是因为这些额外的显存已经被预先分配给了 KV cache,确保它们在推理时不会受到其他任务的影响。通过这种显存的预先规划和锁定,系统在处理推理请求时能够更高效地管理内存资源,避免了推理阶段因显存分配不足而出现的瓶颈。

2.4 处理请求

2.4.1 请求到达 LLMEngine

TXbOGg

这种图展示了 vLLM 引起国内的工作流程,可以把它理解为一些列处理请求的步骤。我们分成两部分来看:异步(async)和同步(sync)流程。

异步流程

  1. API-Server 接受请求:当用户发送请求时,API 服务器首先会接收到这些请求并解析其中的参数。这些参数告诉系统后续进行什么样的处理。
  2. 生成请求参数(async_args):API-Server 会根据请求参数生成一个 async_args 对象,它包含了请求的详细信息,比如模型 ID、输入文本、推理参数等。
  3. 请求加入队列:请求加入队列后,会等待调度器调度。
  4. 引擎主循环开始(run_engine_loop):在异步流程中,引擎主循环会不断从队列中获取请求,并进行处理。
  5. 处理请求(get_new_and_abort_requests):在处理过程中,系统会检查新的请求以及是否有请求被终止,确保每个请求被及时处理。
  6. 执行推理步骤(engine_step): engine 开始处理请求,决定哪个请求可以执行。
  7. 异步步骤完成(add_request_async): 将请求传递到 LLMEngine
  8. 请求加入调度(add_seq_group): 将请求包装为 seq_group 对象,并加入调度器
  9. 返回调度结果(sche_output): 调度执行,对 waiting,running, swapped 队列中的请求进行调度,返回调度结果,等待模型推理。
  10. 单步推理(step_async): 模型推理,生成一个step 的输出结果。
  11. 引擎推理(engine_step): 引擎推理,处理请求,生成一个step 的输出结果。
  12. 模型推理(execute_model_req): model_executor 执行推理,生成一个step 的输出结果。
  13. 结果返回(return_output): 将结果返回给 AsyncLLMEngine,包装为 request_output 对象。
  14. 返回结果(return_output_async): 回抛结果
  15. 流式输出(stream_output): 流式输出结果
  16. 请求完成(request_done): 请求完成,流式将结果返回API-Server。

同步流程
同步流程相对简单,主要是在执行过程中直接返回结果:

  1. 初始化(init): 同步流程开始时,系统会初始化所有必要的参数和资源。
  2. 请求处理(add_request): 此时,系统直接处理请求并开始执行。
  3. 推理计算(step): 系统会一步一步地处理推理任务,直到完成。
  4. 生成并返回结果(output): 处理完成后,系统会直接返回推理结果。

总体来说,vLLM 的工作流程就像一个工厂,API 服务器像一个接收原材料的入口,它把请求交给引擎进行处理,处理的方式有两种:一种是异步的,一种是同步的。每个请求都会经过一系列步骤,最终模型给出答案。

2.4.2 调度器的任务

YziHGR

在请求进入调度器后,Scheduler 会根据当前的资源情况(如可用的 KV 缓存块)来决定如何执行任务。调度器维护了三个请求队列:

  • Waiting Queue:等待执行的请求。
  • Running Queue:当前正在处理的请求。
  • Swapped Queue:由于显存不足而被暂时置换出去的请求。

调度器会判断是否有足够的内存块可以分配给新的 tokens。如果有足够的可用 KV 块,则请求从等待队列移动到正在运行的队列(waiting → running);如果内存不足,调度器会将一些运行中的请求交换到 CPU 内存(running → swapped),以腾出空间让新请求进入运行队列。

2.4.3 Worker 执行推理

0zKzhN

当请求进入运行队列后,Scheduler 会将任务分发给多个 Worker。每个 Worker 在 GPU 上运行,负责实际的推理计算。在这一过程中,CacheEngine 会按照调度器的指示管理缓存块,包括在 GPU 和 CPU 之间交换内存块,确保内存资源得到高效利用。此外,CacheEngine 还会对共享缓存块执行写时复制(copy-on-write),以确保数据的一致性和高效性。

2.4.4 模型的推理过程

0zKzhN

每个 Worker 中的 Worker.model 模块负责加载并执行模型推理。在这个过程中,它会依赖 PagedAttention 来实现高效的注意力计算。PagedAttention 是优化的注意力机制实现,适用于大规模的 Transformer 模型,并使用诸如 xformers 或 FlashAttention 等技术来加速推理。

此外,模型的其他部分(例如线性层、量化层等)也进行了优化,以便在分布式执行和张量并行的情况下达到最高性能。在推理阶段,Sampler 会负责选择下一个生成的 token,使用贪心算法、随机采样或者 Beam Search 等策略。

2.4.5 请求的完成和结果返回

推理完成后,结果会被发送回 LLMEngine。LLMEngine 会对生成的 tokens 进行 detokenization,将它们转换回可读的文本,并最终将生成的结果流式地返回给用户。这一流程使得生成的结果可以尽快交付给用户,而无需等待整个请求的完全完成。

整个请求的处理流程由 LLMEngine 进行协调调度,通过 Scheduler 管理内存和资源的有效利用,Worker 在 GPU 上执行具体的推理计算,最终将结果流式地返回给用户。

3. vLLM 输入数据的预处理

在 vLLM 处理用户请求之前,需要对输入数据进行一系列预处理操作,以确保数据能够被模型正确处理。这个过程就像是将原始材料加工成标准化的零件,为后续的推理计算做好准备。

3.1 Tokenization 处理

Tokenization 是输入预处理的第一道关卡。vLLM 使用与原始语言模型相同的分词器,将输入文本转换为模型可以理解的 token 序列。主要实现在 vllm/engine/llm_engine.pyvllm/engine/tokenizer.py 中。
xVh4L6

这个过程包含几个关键步骤:

  1. 文本规范化:首先对输入文本进行清理和标准化,包括处理特殊字符、统一空白字符等。这就像是将各种形状的原料整理成统一的形态。

  2. 分词处理:使用模型对应的 tokenizer 将文本切分成 token。这个过程会考虑词频、语义完整性等因素,就像是将长木材切割成大小合适的木块。

  3. 批处理优化:vLLM 创新性地实现了动态批处理机制。它会智能地将多个请求的 token 组合在一起处理,就像是将多个订单的相似零件放在一起加工,显著提升处理效率。

3.2 Prompt 模板与格式化

vLLM 支持灵活的 prompt 模板系统,帮助用户更好地构造输入。相关实现在 vllm/engine/arg_utils.pyvllm/engine/sampling_params.py 中:

  1. 模板定义:用户可以预定义不同场景的 prompt 模板,包括系统提示、用户输入、历史对话等部分。

  2. 变量替换:模板中的变量可以动态替换,使得同一个模板能够适应不同的输入场景。

  3. 格式验证:系统会自动检查填充后的 prompt 是否符合预期格式,确保输入的规范性。

3.3 输入验证与优化

为了确保系统稳定性和性能,vLLM 会对输入进行全面的验证和优化:

  1. 长度检查:验证输入是否超过模型的最大上下文长度,必要时进行截断或分段处理。

  2. 特殊标记处理:自动添加或处理模型所需的特殊标记(如开始符、结束符等)。

  3. 资源评估:预估处理该输入所需的计算资源和内存需求,为后续调度做准备。(实现在 vllm/engine/llm_engine.py 中的 add_request 方法)

  4. 缓存优化:分析输入是否能够利用已有的 KV Cache,提前进行优化决策。(实现在 vllm/core/block_manager.py 中)

3.4 深入解析 add_request

接下来我们将深入分析 LLMEngine 的请求处理流程。通过 LLMEngine.add_request 方法接收到的请求会经过一系列预处理、调度、执行和输出处理步骤。

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 方法接受了多个参数,其中关键的参数包括:

  • request_id:每个请求的唯一标识符,用于跟踪和调度。
  • prompt:请求的提示词,通常是用户输入的自然语言文本,定义了生成任务的起点。
  • params:这是生成任务的参数,可能是 SamplingParams(采样生成参数)或者 PoolingParams(池化生成参数),这将影响生成的策略,比如温度、采样方法等。
  • arrival_time:请求到达的时间,用于统计和分析请求的延迟。
  • lora_request:用于处理 LoRA 模型的特定请求,如果模型使用了 LoRA 技术。
  • trace_headers:用于跟踪请求的元数据,通常用于日志记录和调试。
  • prompt_adapter_request:用于处理提示适配器的特定请求,如果模型使用了提示适配器。
  • priority:请求的优先级,用于调度器决定请求的执行顺序。
  • inputs:这是一个可选参数,用于兼容旧版本,通常可以忽略。
3.4.1 preprocess 入口

在 LLMEngine 中,当我们使用 add_request 方法添加一个请求时,系统首先会调用 InputPreprocessor 对输入进行预处理,这一过程确保用户的输入被模型正确处理。InputPreprocessor 类负责解析和处理不同类型的输入(包括文本、tokens 等),并将其转换为模型可以使用的标准化格式。

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 模型设计的,它能够处理同时包含编码器和解码器的 prompt。

现在我们只考虑 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 方法负责将输入的 prompt 转换为模型可以理解的格式。它根据 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 方法负责将输入的文本转换为 token 序列。它使用模型对应的 tokenizer 将文本切分成 token,并返回对应的 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() 获取当前模型对应的分词器。这个分词器通常在模型初始化时就已经加载,与模型使用相同的词表和分词规则。
  • 特殊标记处理
    • 默认情况下,add_special_tokens 为 None,表示使用模型默认的特殊标记处理方式
    • 对于 Whisper 模型等特殊情况,会设置 add_special_tokens=False,因为这些模型需要用户根据具体任务和语言来提供特殊标记
    • 这样的设计确保了不同模型的特殊标记(如开始符、结束符等)能够被正确处理
  • Token 编码:调用 tokenizer 的 encode 方法,将文本转换为 token ID 序列。这个过程包括:
    • 将输入文本分割成子词(subwords)
    • 将每个子词映射到对应的 token ID
    • 根据需要添加特殊标记(如果 add_special_tokens 为 True)
    • 处理 LoRA 相关的特殊需求(如果提供了 lora_request)
  • 请求追踪:通过传入 request_id,确保能够追踪每个请求的 tokenization 过程,这对于调试和性能分析很有帮助。

3.4 创建 sequence 和 sequence_group

通过这些精心设计的预处理步骤,vLLM 能够将各种形式的输入转换为标准化、高效的形式,为后续的推理计算打下坚实基础。这就像是一个细心的厨师,在烹饪之前将所有食材都准备妥当,确保整个烹饪过程的顺畅进行。

在预处理之后,我们得到了 ProcessorInputs 实例,它包含了处理后的输入数据。接下来,我们调用 _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 共享相同的采样参数(如温度、采样策略等)以及优先级调度策略(如 priority)。SequenceGroup 的创建是通过 _create_sequence_group_with_sampling_create_sequence_group_with_pooling 方法完成的,具体取决于是否采用采样策略或者池化策略。

SamplingParams 是用于控制模型生成文本时的行为的参数,比如温度(temperature)、采样概率(top_p)等。SamplingParams 会影响生成的策略,比如生成的多样性、生成的质量等。

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 中一个核心概念,它代表了一组共享相同采样参数和调度优先级的序列集合。让我们通过一个具体的生命周期来理解 SequenceGroup:

SequenceGroup
初始化阶段

  • 每个 SequenceGroup 初始时只包含一个序列(seq),这个序列对应用户输入的 prompt
  • 初始序列的状态被设置为 waiting,等待调度器分配资源

第一次推理阶段(Prefill)

  • 当调度器选中该 SequenceGroup 后,会首先执行 prefill 操作
  • 如果采样参数中设置了 n > 1(例如 n = 4),系统会基于初始序列生成 n 个分支
  • 所有生成的序列状态都变为 running,开始并行生成 tokens

资源竞争阶段(Preemption)
当 GPU 资源不足时,调度器会触发抢占机制。此时根据序列数量采取不同策略:

a) Swap 策略 (序列数量 > 1)

  • 适用于多序列场景
  • 将 SequenceGroup 下所有序列的 KV Cache 从 GPU 转移到 CPU
  • 所有序列状态变为 swapped
  • 保留已计算的 KV Cache,避免重复计算

b) Recomputation 策略 (序列数量 = 1)

  • 适用于单序列场景
  • 释放该 SequenceGroup 占用的所有 GPU 内存块
  • 将序列重新放入 waiting 队列
  • 下次调度时从 prefill 阶段重新开始
  • 选择重计算的原因:单序列重新计算 KV Cache 的成本相对较低

sequence_group 的属性:

  • seqs_dict
self.seqs_dict: Dict[int, Sequence] = {}
  • 存储序列ID到Sequence对象的映射

  • 使用字典结构实现快速查找和管理

  • 每个 Sequence 对象包含序列的状态、token 历史等信息

  • sampling_params

self.sampling_params: SamplingParams
  • 控制文本生成的关键参数

  • 包含温度(temperature)、top_p、top_k等采样策略

  • 影响生成文本的多样性和质量

  • metrics

    self.metrics: Dict[str, Any] = {"arrival_time": float,"first_scheduled_time": Optional[float],"first_token_time": Optional[float],...
    }
    
    • 记录序列组的关键时间点和性能指标
    • 用于调度器进行决策和性能分析
    • 包括到达时间、首次调度时间、首个token生成时间等
  • max_running_steps

    def get_max_num_running_steps(self) -> int:"""计算剩余生命周期内的最大并行序列数"""
    
    • 预估序列组在整个生成过程中需要的最大并行步数
    • 帮助调度器进行资源规划和分配
    • 考虑了采样参数和当前生成状态

实现细节:

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. 小结

在这篇文章中,我们踏上了探索 vLLM 架构的旅程,就像解构一台精密的机器,我们逐层剖析了它的核心组件和运作机制。从整体架构设计开始,我们看到了 Engine、Worker、Scheduler 等组件如何协同工作,就像一个精心编排的交响乐团,每个部分都在演奏着自己的乐章,共同谱写出高效服务的乐章。

在深入研究请求处理流程时,我们见证了一个请求从诞生到完成的完整生命历程。就像一颗种子从播种到生长,每个阶段都经过精心的规划和呵护。从最初的模型初始化,到 KV Cache 的预分配,再到请求的具体处理,vLLM 展现出了令人印象深刻的工程智慧。

特别值得一提的是输入数据的预处理机制,这就像是一个细心的厨师在烹饪前的准备工作。通过精心设计的 Tokenization 处理、灵活的 Prompt 模板系统,以及严谨的请求验证流程,vLLM 确保了每个输入都能被完美处理。而在序列管理方面,Sequence 和 SequenceGroup 的设计则展现了框架在处理复杂场景时的优雅解决方案。

这次的探索之旅让我们对 vLLM 的基础架构有了深入的认识,但这仅仅是开始。在下一篇文章中,我们将继续深入探讨 vLLM 最引人注目的两大核心机制:调度器(Scheduler)的智能调度策略和内存管理器(BlockManager)的高效内存管理。这两个机制就像是 vLLM 的双翼,让它能够在高并发的天空中自由翱翔。我们将看到调度器如何像一个睿智的指挥官,统筹安排每个请求的处理时机,以及内存管理器如何通过创新的 PagedAttention 机制,让有限的显存发挥出最大效能。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/19540.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Web安全|渗透测试|网络安全

基础入门(P1-P5) p1概念名词 1.1域名 什么是域名? 域名:是由一串用点分隔的名字组成的Internet上某一台计算机或计算机组的名称,用于在数据传输时对计算机的定位标识(有时也指地理位置)。 什么是二级域名多级域名&am…

JavaSE的基础语法(5)

一.Java中的方法 函数:把完成某一特定功能的代码进行抽取,把他们卸写在一组大括号中,为其命名通过函数名调用即可 Java中的方法:类似于其他语言中的函数(在面向对象的语言中习惯称之为方法,且不能独立存在,需要定义在类中) 将完成某个特定功能的某一段代码封装到一个有名称的代…

springcloud集成gateway

本篇文章只介绍gateway模块的搭建步骤,并无gateway详细介绍 gateway详解请查看:SpringCloudGateway官方文档详解 前置处理 父模块中已指定版本 不知道如何选择版本看这篇: 手把手教你梳理springcloud与springboot与springcloudalibaba的版本…

【Linux】VS code编写cpp文件调用CPLEX求解,Makefile撰写方式

1、先给出Makefile # 目标文件 TARGET my_program# 源文件 SRC blend.cpp# 头文件目录(如果有) INCLUDES -I./# CPLEX 路径设置(根据你的 CPLEX 安装路径) CPLEX_INCLUDE -I/mnt/c/users/daniel/Ubuntu/cplex/cplex/include …

DeepSeek24小时写作机器人,持续创作高质量文案

内容创作已成为企业、自媒体和创作者的核心竞争力。面对海量的内容需求,人工创作效率低、成本高、质量参差不齐等问题日益凸显。如何在有限时间内产出高质量内容?DeepSeek写作机器人,一款24小时持续创作的智能工具,为企业和个人提…

开源协议深度解析:理解MIT、GPL、Apache等常见许可证

目录 前言1. MIT协议:自由而宽松的开源许可1.1 MIT协议的主要特点1.2 MIT协议的适用场景 2. GPL协议:自由软件的捍卫者2.1 GPL协议的核心理念2.2 GPL协议的适用场景 3. Apache License 2.0:开源与专利保护的平衡3.1 Apache License 2.0的主要…

第四十四篇--Tesla P40+Janus-Pro-7B部署与测试

环境 系统:CentOS-7 CPU: 14C28T 显卡:Tesla P40 24G 驱动: 515 CUDA: 11.7 cuDNN: 8.9.2.26创建环境 conda create --name trans python3.10torch 2.6.0 transformers 4.48.3克隆项目 git clone https:/…

【前端】自己从头实现一个gpt聊天页面

预览 最小化功能点 主界面:侧边栏会话历史、聊天窗口发送和断开。侧边栏:展示会话列表,每个会话包含多条聊天记录, 通过localstorage本地储存和恢复,会话需要重命名和删除。聊天框:区分一下发送者和回答者…

【第13章:自监督学习与少样本学习—13.1 自监督学习最新进展与实现方法】

凌晨三点的实验室,博士生小王盯着屏幕里正在"自娱自乐"的神经网络——这个没有吃过一张标注图片的模型,正在通过旋转、拼图、填色等游戏任务,悄悄掌握着理解世界的秘诀。这种魔法般的修炼方式,正是当今AI领域最炙手可热的技术:自监督学习。 一、打破数据枷锁:自…

案例-06.部门管理-根据ID查询

一.根据ID查询-接口文档 二.根据ID查询-Controller层 package com.gjw.controller;/*** 部门管理Controller*/import com.gjw.anno.Log; import com.gjw.pojo.Dept; import com.gjw.pojo.Result; import com.gjw.service.DeptService; import com.gjw.service.impl.DeptServi…

【MySQL】使用 JDBC 连接数据库

文章目录 前言1. 认识 JDBC 1.1 概念1.2 好处 2. 使用 JDBC 2.1 安装数据驱动包2.2 把 jar 包导入到项目中2.3 代码编写2.4 测试结果 3. 代码优化4. 源码展示结语 前言 在 MySQL 系列中,我们介绍了很多内容,包括但不限于建库建表,增删查改等…

matlab模拟风场的随机脉动风

1、内容简介 matlab137-模拟风场的随机脉动风 可以交流、咨询、答疑 2、内容说明 略 模拟风场的随机脉动风,并进行相关的统计分析和计算,包括风速谱、空间相关性、自谱、互谱、以及POD(Proper Orthogonal Decomposition)分解等…

API 接口自动化

HTTP协议 - 白月黑羽 HTTP协议简介 如果客户端是浏览器,如何在chrome浏览器中查看 请求和响应的HTTP消息?按f12-》network 清除当前信息 响应的消息体在Response里看 点preview,可以看响应的消息体展开的格式 HTTP请求消息 请求头 reques…

(四)Axure学习图文教程

Axure中文学习网: Axure中文网 – 交互原型设计软件Axure RP中文正版支持 – 北京口耳相传科技有限公司 一、界面介绍 工具栏:主要操作功能。 站点地图:类似大纲界面,方便理清原型框架及逻辑关系。 元件库:调用所需…

2025年 Java 面试八股文

第一章-Java基础篇 1. Java中的基本数据类型有哪些?⭐ Java中有8种基本数据类型(Primitive Types),分别是: byte:8位,-128 ~ 127short:16位,-32,768 ~ 32,767int&…

基于Docker-compose的禅道部署实践:自建MySQL与Redis集成及故障排查指南

基于Docker-compose的禅道部署实践:自建MySQL与Redis集成及故障排查指南 禅道镜像版本:easysoft/zentao:21.4 Redis版本:redis:6.2.0 Mysql版本:mysql:8.0.35 文章目录 **基于Docker-compose的禅道部署实践:自建MySQL与…

【Java八股文】01-Java基础面试篇

【Java八股文】01-Java基础面试篇 概念Java特点Java为什么跨平台JVM、JDK、JRE关系 面向对象什么是面向对象,什么是封装继承多态?多态体现的方面面向对象设计原则重载重写的区别抽象类和实体类区别Java抽象类和接口的区别抽象类可以被实例化吗 深拷贝浅拷…

基于Qt 和微信小程序的用户管理系统:WebSocket + SQLite 实现注册与登录

目录 一. 概要 二. 技术栈 三. 系统功能设计 3.1 功能模块 3.2 数据表设计 四. 具体实现 4.1 Qt 服务端 4.1.1 初始化 WebSocket 服务器 4.1.2 用户管理界面 4.2 微信小程序端 4.2.1 注册功能 4.2.2 登录功能 五. 运行效果 六. 源码下载 一. 概要 在物联网和智能设备…

【STM32】舵机SG90

1.舵机原理 舵机内部有一个电位器,当转轴随电机旋转,电位器的电压会发生改变,电压会带动转一定的角度,舵机中的控制板就会电位器输出的电压所代表的角度,与输入的PWM所代表的角度进行比较,从而得出一个旋转…

PostgreSQL:备库的延迟问题处理步骤

目录标题 1. 查看主备状态计算方式:实际情况:举个例子: 2. 查看历史状态3. 分析日志文件4. 查看数据库层面的复制状态5. 检查活动事务6. 检查系统资源7. 检查网络状况8. 检查复制槽状态9. 检查未提交的两阶段事务 要排查 PostgreSQL 备库的延…