架构概述¶
本文档提供了 vLLM 架构的概述。
入口点¶
vLLM 提供了多种与系统交互的入口点。下图展示了它们之间的关系。
LLM 类¶
LLM 类提供了主要的 Python 接口,用于进行离线推理,即在不使用单独的模型推理服务器的情况下与模型进行交互。
以下是 LLM 类使用示例
代码
from vllm import LLM, SamplingParams
# Define a list of input prompts
prompts = [
"Hello, my name is",
"The capital of France is",
"The largest ocean is",
]
# Define sampling parameters
sampling_params = SamplingParams(temperature=0.8, top_p=0.95)
# Initialize the LLM engine with the OPT-125M model
llm = LLM(model="facebook/opt-125m")
# Generate outputs for the input prompts
outputs = llm.generate(prompts, sampling_params)
# Print the generated outputs
for output in outputs:
prompt = output.prompt
generated_text = output.outputs[0].text
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
更多 API 细节可以在 API 文档的 离线推理 部分找到。
LLM 类的代码可以在 vllm/entrypoints/llm.py 中找到。
OpenAI 兼容的 API 服务器¶
vLLM 的第二个主要接口是通过其 OpenAI 兼容的 API 服务器。该服务器可以使用 vllm serve 命令启动。
vllm CLI 的代码可以在 vllm/entrypoints/cli/main.py 中找到。
有时,您可能会直接看到 API 服务器入口点被使用,而不是通过 vllm CLI 命令。例如:
警告
python -m vllm.entrypoints.openai.api_server 已弃用,未来版本可能会不再支持。
该代码可以在 vllm/entrypoints/openai/api_server.py 中找到。
关于 API 服务器的更多详细信息可以在 OpenAI 兼容服务器 文档中找到。
LLM 引擎¶
LLMEngine 和 AsyncLLMEngine 类是 vLLM 系统运行的核心,负责模型推理和异步请求处理。
LLMEngine¶
LLMEngine 类是 vLLM 引擎的核心组件。它负责接收来自客户端的请求并从模型生成输出。LLMEngine 包括输入处理、模型执行(可能分布在多个主机和/或 GPU 上)、调度和输出处理。
- 输入处理:使用指定的 tokenizer 处理输入文本的 token 化。
- 调度:决定在每个步骤中处理哪些请求。
- 模型执行:管理语言模型的执行,包括在多个 GPU 上的分布式执行。
- 输出处理:处理模型生成的输出,将语言模型的 token ID 解码成人类可读的文本。
LLMEngine 的代码可以在 vllm/engine/llm_engine.py 中找到。
AsyncLLMEngine¶
AsyncLLMEngine 类是 LLMEngine 类的一个异步包装器。它使用 asyncio 创建一个后台循环,持续处理传入的请求。AsyncLLMEngine 专为在线服务设计,能够处理多个并发请求并将输出流式传输给客户端。
OpenAI 兼容的 API 服务器使用 AsyncLLMEngine。还有一个演示 API 服务器,在 vllm/entrypoints/api_server.py 中作为一个更简单的例子。
AsyncLLMEngine 的代码可以在 vllm/engine/async_llm_engine.py 中找到。
Worker¶
Worker 是一个运行模型推理的进程。vLLM 遵循一个常见的实践,即使用一个进程控制一个加速器设备,例如 GPU。例如,如果我们使用大小为 2 的张量并行和大小为 2 的流水线并行,那么我们总共有 4 个 worker。Worker 通过它们的 rank 和 local_rank 进行标识。rank 用于全局协调,而 local_rank 主要用于分配加速器设备和访问本地资源,如文件系统和共享内存。
Model Runner¶
每个 worker 都有一个 Model Runner 对象,负责加载和运行模型。大部分模型执行逻辑都 resides 在这里,例如准备输入张量和捕获 cudagraphs。
Model¶
每个 Model Runner 对象都有一个 Model 对象,这是实际的 torch.nn.Module 实例。有关各种配置如何影响我们最终获得的类的详细信息,请参阅 huggingface_integration。
类层级结构¶
下图展示了 vLLM 的类层级结构
此类层级结构背后有几个重要的设计选择
1. **可扩展性**:层级结构中的所有类都接受一个包含所有必要信息的配置对象。VllmConfig 类是传递的 मुख्य 配置对象。类层级结构非常深,每个类都需要读取它感兴趣的配置。通过将所有配置封装在一个对象中,我们可以轻松地传递配置对象并访问所需的配置。假设我们想添加一个新功能(鉴于 LLM 推理领域发展迅速,这很常见),该功能仅涉及 Model Runner。我们将在 VllmConfig 类中添加一个新的配置选项。由于我们传递整个配置对象,因此只需将配置选项添加到 VllmConfig 类,Model Runner 就可以直接访问它。我们无需更改 Engine、Worker 或 Model 类的构造函数来传递新的配置选项。
2. **统一性**:Model Runner 需要一个统一的接口来创建和初始化模型。vLLM 支持 50 多种流行的开源模型。每种模型都有自己的初始化逻辑。如果构造函数签名因模型而异,Model Runner 将不知道如何调用构造函数,而没有复杂且容易出错的检查逻辑。通过使 Model 类的构造函数统一,Model Runner 可以轻松创建和初始化模型,而无需了解特定模型类型。这对于组合模型也很有用。视觉-语言模型通常由视觉模型和语言模型组成。通过使构造函数统一,我们可以轻松创建视觉模型和语言模型,并将它们组合成一个视觉-语言模型。
注意
为了支持此更改,所有 vLLM 模型的签名都已更新为
为了避免意外传递不正确的参数,构造函数现在是仅关键字参数。这确保了如果传递了旧配置,构造函数将引发错误。vLLM 开发人员已经为 vLLM 中的所有模型进行了此更改。对于 out-of-tree 注册模型,开发人员需要更新他们的模型,例如通过添加 shim 代码来将旧的构造函数签名适配到新的签名。
代码
class MyOldModel(nn.Module):
def __init__(
self,
config,
cache_config: Optional[CacheConfig] = None,
quant_config: Optional[QuantizationConfig] = None,
lora_config: Optional[LoRAConfig] = None,
prefix: str = "",
) -> None:
...
from vllm.config import VllmConfig
class MyNewModel(MyOldModel):
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
config = vllm_config.model_config.hf_config
cache_config = vllm_config.cache_config
quant_config = vllm_config.quant_config
lora_config = vllm_config.lora_config
super().__init__(config, cache_config, quant_config, lora_config, prefix)
from packaging import version
if version.parse(__version__) >= version.parse("0.6.4"):
MyModel = MyNewModel
else:
MyModel = MyOldModel
这样,模型就可以与 vLLM 的新旧版本一起工作。
3. **初始化时的分片和量化**:某些功能需要更改模型权重。例如,张量并行需要分片模型权重,量化需要量化模型权重。有两种方法可以实现此功能。一种方法是在模型初始化后更改模型权重。另一种方法是在模型初始化期间更改模型权重。vLLM 选择后者。第一种方法对于大型模型来说不是可扩展的。假设我们想使用 16 个 H100 80GB GPU 运行一个 405B 模型(约 810GB 权重)。理想情况下,每个 GPU 应只加载 50GB 权重。如果我们模型初始化后更改模型权重,我们需要将完整的 810GB 权重加载到每个 GPU,然后分片权重,这会导致巨大的内存开销。相反,如果在模型初始化期间分片权重,每一层将只创建它所需的权重分片,从而大大减小内存开销。同样的概念也适用于量化。请注意,我们还在模型构造函数中添加了一个额外的参数 prefix,以便模型可以根据前缀以不同的方式进行初始化。这对于非均匀量化很有用,其中模型的不同部分被量化为不同的精度。prefix 通常是顶层模型的空字符串,而对于子模型,它是像 "vision" 或 "language" 这样的字符串。通常,它与 checkpoint 文件中模块的 state dict 的名称匹配。
此设计的一个缺点是很难为 vLLM 中的单个组件编写单元测试,因为每个组件都需要由完整的配置对象进行初始化。我们通过提供一个默认初始化函数来解决这个问题,该函数创建一个具有所有字段设置为 None 的默认配置对象。如果我们想测试的组件只关心配置对象中的少数几个字段,我们可以创建一个默认配置对象并设置我们关心的字段。这样,我们就可以单独测试该组件。请注意,vLLM 中的许多测试都是端到端测试,用于测试整个系统,因此这并不是一个大问题。
总而言之,完整的配置对象 VllmConfig 可以被视为一个引擎级别的全局状态,该状态在所有 vLLM 类之间共享。


