vLLM学习整理

学习材料:https://docs.vllm.ai/en/latest/design/arch_overview

一、基础概念

DP

称作Data Parallelism,指同一份权重复制到多组GPU上,每组GPU称作一个DP Rank,各自处理不同的请求批次。

ZMQ

称作ZeroMQ,是一个高性能轻量级的开源异步消息传递库,以消息为中心,而非连接为中心。

AllReduce (全归约)

所有节点各持一份数据,所有节点的数据归约运算(如求和)后分发到所有节点,保证所有节点的数值完全相同。在TP模式下,每个节点经过TP切分的线性层(如attention输出投影、MLP的down投影)后只持有部分结果,通过AllReduce聚合为完整结果。

All-to-All (全交互)

将每个节点不同的数据发送给不同的节点,同时从所有节点接受不同的数据。如下:

GPU 0: [A0, A1, A2, A3]
GPU 1: [B0, B1, B2, B3]
GPU 2: [C0, C1, C2, C3]
GPU 3: [D0, D1, D2, D3]

# 经过All-to-All后
GPU 0: [A0, B0, C0, D0]     0 
GPU 1: [A1, B1, C1, D1]     1 
GPU 2: [A2, B2, C2, D2]     2 
GPU 3: [A3, B3, C3, D3]     3 

所有节点的输入不同,输出也不同。在EP模式下所有的token会被路由到对应专家的GPU上。All-to-All负责将token从当前GPU发送到对应专家的GPU上,专家计算完后再通过All-to-All把结果传回。

asyncio

asyncio是Python标准库中的一个异步I/O框架,基于事件循环(Event Loop)机制,允许等待I/O时执行其他任务,提升程序的并发性能。适合I/O密集型的场景。

代码举例如下:

import asyncio

async def say_hello(delay, name):
    await asyncio.sleep(delay)  # 模拟异步 I/O 操作
    print(f"Hello, {name}!")

async def main():
    # 并发执行多个协程
    await asyncio.gather(
        say_hello(1, "Alice"),
        say_hello(2, "Bob"),
        say_hello(0.5, "Charlie"),
    )

asyncio.run(main())

由于三个任务是并发执行,实际耗时约2s,而非3.5s。

NCCL

全称NVIDIA Collective Communications Library,是NVIDIA专门为多GPU、多节点场景设计的集合通信库。通过自动拓扑探测和通信算法自动选择,让开发者无需关心底层硬件细节。

NCCL提供两类通信操作:

  • 集合通信(Collective Communication),所有GPU同时调用同一个操作协同完成

    原语 语义 典型用途
    AllReduce 所有GPU数据做归约再广播给所有GPU 注意力层TP并行
    Broadcast 一个GPU数据广播给所有GPU rank0广播权重
    Reduce 所有GPU数据归约后存在一个GPU上 梯度归约到单一rank
    AllGather 每个GPU提供部分数据拼成完整数据后广播所有GPU Sequence Parallelism、收集各卡logits
    ReduceScatter 归约后将结果按块分散到各个GPU FSDP
  • 点对点通信(Point-to-Point):一个GPU向另一个GPU发送/接受数据(ncclSend/ncclRecv)。

NCCL设计特点:

  • 将通信和计算融合在同一个GPU内核中,一次kernel launch同时完成数据搬运和归约运算,减少延时
  • 自动拓扑探测,包括节点内(PCIe、NVLink、NVSwitch)/节点间(InfiniBand、RoCE、TCP/IP)/跨数据中心(fabricID、多网络拓扑)等等,NCCL会选择最优通信算法(Ring、Tree等)。

二、vLLM整体架构

Entrypoints

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}")
  • LLM Class用于离线推理
  • 在线推理使用命令:vllm serve <model>

V1 Process Architecture

vLLM V1使用多进程架构,用于功能划分和最大化吞吐量。

场景TP = 4如下:

场景TP=2 DP=4如下:

  • API Server Process
    • 处理HTTP请求;输入预处理;流式输出。
    • 用ZMQ方式与Engine Core Process交互。
    • 当使用DP(数据并行)时,会自动启动多个API Server。也可以通过--api-server-count配置。
  • Engine Core Process
    • Scheduler:负责调度,决定谁先算,算多少,以及Continuous BatchingChunked Prefill
    • KV Cache管理:用KVCacheManager管理KVCache,方法参见PagedAttention
    • 协调GPU Worker:通过Executor->Workers->ModelRunners层级,调度GPU工作,每个GPU对应一个独立的Worker进程
    • 它的数量与DP的数量是一一对应的
  • GPU Worker Process
    • 加载权重和推理以及管理GPU内存。
    • GPU WorkerEngine Core之间通信使用消息队列。
  • DP Coordinator Process
    • 用于负载均衡。决定每个新的请求应该分配给哪个DP Rank。每个Engine Core会定期向DP Coordinator上报负载状态(waiting队列长度/running队列长度等);当有新的请求时,会按score = len(waiting)x4 + len(running)打分,优先分配给分数低的;
    • 协调MoE前向推理。对Dense模型DP Rank都是独立工作,但MoE模型各DP Rank之间会根据EP或者TP进行分片。注意是注意力层只在DP Rank内做TP;只有MoE层,会跨所有DP Rank做EP和TP。在DP>1的情况下一般还是用EP并行;DP=1的情况下一般用TP并行。

LLMEngine

LLMEngine和AsyncLLMEngine是vLLM的核心功能模块,用于处理推理和异步请求。

LLMEngine包含了如下处理过程:

  • Input Processing:使用tokenizer将输入文本转成tokens
  • Scheduling:调度(选择每一步处理了哪些请求)
  • Model Execution:模型推理,包括多GPU分布式推理
  • Output Processing:输出从tokens转换成文本

AsyncLLMEngine对LLMEngine外包一层,用asyncio使LLMEngine变成持续运行的后台服务。工作流程如下:

客户端 A ──generate()──┐
客户端 B ──generate()──┼──► 请求队列 ──► AsyncLLMEngine ──► LLMEngine.step()
客户端 C ──generate()──┘                    │
                                            │ 每生成一个 token
                                            ▼
                                      流式推送给对应客户端

Worker

一个Worker就是一个独立进程,绑定一张GPU,负责执行模型推理。每个Worker对应一个rank编号(全局),和local rank编号(本地)。

Model Runner

每个Worker有个Model Runner,负责加载和运行模型

Model

每个Model Runner有个Model对象,对应torch.nn.Module实例。

类继承关系

这样设计基于如下一些考量:

  • 可扩展性,所有配置都被封装在vllmconfig中,并在类之间传递
  • 统一性,所有模型的构造函数统一为def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""),并仅接受关键参数
  • 分片与量化,在模型初始化时进行量化和分片

三、PagedAttention

原理

问题:传统KV Cache为了保证连续存储,需要预留较大连续地址空间,导致显存利用率不高。

解决思路:借鉴虚拟内存分页管理的思想:每个进程都在虚拟地址中运行,虚拟地址通过MMU映射对应具体的物理地址。

方法

将KVCache按block单位(vLLM中默认16),拆分成多份,存储在逻辑block块中;维护一个虚拟block到物理block的映射表,该表主要内容包括:逻辑块id到物理块id的映射关系 和 每个块已经使用的数量。

  • 在Prefill阶段,根据prompt长度申请连续的逻辑块,并将其映射到不要求连续的物理块中。然后计算得到kv值存入其中。(逻辑连续,物理不连续)
  • 在Decode阶段,读取KV Cache (看起来是连续的逻辑块,背后映射到了不连续的物理块中)进行attention运算。新生成的KV Cache填入到连续的逻辑块中;如果当前的逻辑块已满,则vLLM会开辟新的逻辑块,并更新映射表。

Prefix Caching

这样KV Cache的存放就非常灵活,引出Prefix Caching(前缀缓存)机制,每个逻辑块存满后会计算一个Hash值,该值由自身token和前一个块的hash值产生(实际还包含model、LoRA、cache salt等extra keys);当hash值相同时则共用一组物理块,并引用计数+1。当释放时则-1,只有到0时才释放物理块。

Preemption (抢占)

当多个请求导致物理块全部用完时,vLLM会抢占部分请求以释放其KV block。默认FCFS策略下,被抢占的是最晚到达的请求(除非显式配置了优先级调度)。释放方式在两个版本间有差异:

  • V1:仅支持Recompute方式,直接释放抢占请求的所有KV block,请求回到waiting队列,等资源充足时重新Prefill(已缓存的前缀可命中Prefix Caching)。注意V1已移除CPU swap机制(--swap-space参数已删除)。
  • V0:除Recompute外还支持Swapping,将被抢占请求的KV block换出到CPU内存(--swap-space配置),等资源充足时再换回GPU继续推理。

分布式TP并行

在TP场景下每张卡输入的token是一样的,只是按head切分后每张卡计算得到对应head的KV Cache。所以每张卡维护各自的逻辑块-物理块的映射关系即可。

评论