vllm笔记

vLLM 单进程调用逻辑整理

范围约定:以离线 LLM 入口为例,走同进程模式VLLM_ENABLE_V1_MULTIPROCESSING=0,即 InprocClient),EngineCore 与调用方同进程;executor/worker 层的进程通信细节不做展开,只讲逻辑职责。所有行号以本仓库当前代码为准。


0. 总览:一条主链路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 用户
│ LLM.generate(prompts)

┌──────────────────────────────────────────────────────────┐
│ LLM (vllm/entrypoints/llm.py) │ 入口:参数整理、tqdm、批量循环
└──────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────┐
│ LLMEngine (vllm/v1/engine/llm_engine.py) │ 前端引擎:输入预处理 + 输出后处理
│ ├─ Renderer / InputProcessor │ tokenize、构建 EngineCoreRequest
│ └─ OutputProcessor │ detokenize、构建 RequestOutput
└──────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────┐
│ InprocClient ──► EngineCore(同一进程) │ 引擎内核:调度 + 执行 + 产出输出
│ ├─ StructuredOutputManager │ guided generation 编译
│ ├─ Scheduler │ 排队、每步分配 token 与 KV block
│ └─ UniProcExecutor ──► Worker(同进程) │ 真正执行 forward / sampling
│ └─► GPUModelRunner ──► Model │ (多 worker 时才用 MultiprocExecutor)
└──────────────────────────────────────────────────────────┘

两条主线:

  1. 实例化链(构造)LLMLLMEngineEngineCoreClientEngineCoreUniProcExecutorWorkerModelRunnerModel,再往上把 SchedulerOutputProcessor 等搭好。
  2. 推理链(运行):prompt → 渲染/tokenize → EngineCoreRequestRequest 进调度器 → 每步 schedule()execute_model() → forward → sampling → EngineCoreOutputs → detokenize → RequestOutput

1. 类继承关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
┌─ 入口层 ─────────────────────────────────────────────────────┐
OfflineInferenceMixin (vllm/entrypoints/offline_utils.py)

LLM ────┤ class LLM(BeamSearchOfflineMixin, PoolingOfflineMixin, OfflineInferenceMixin)

┌─ 引擎层 ─────────────────────────────────────────────────────┐
LLMEngine (vllm/v1/engine/llm_engine.py) ← 普通类,无基类

EngineCoreClient (ABC, vllm/v1/engine/core_client.py)
├── InprocClient ← 单进程模式:EngineCore 在本进程直接实例化
├── SyncMPClient ← 多进程同步模式
└── AsyncMPClient ← 多进程异步模式(在线服务)

EngineCore (vllm/v1/engine/core.py)
└── EngineCoreProc (EngineCore) ← 多进程模式的 ZMQ 包装(本整理不展开)

┌─ 调度层 ─────────────────────────────────────────────────────┐
SchedulerInterface (ABC, vllm/v1/core/sched/interface.py)
└── Scheduler (vllm/v1/core/sched/scheduler.py)

┌─ 执行层 ─────────────────────────────────────────────────────┐
Executor (ABC, vllm/v1/executor/abstract.py)
├── MultiprocExecutor ← 多 worker(TP/PP/DP>1)时默认,起 worker 进程
├── UniProcExecutor ← world_size==1 时默认,worker 同进程(最基础的单进程)
└── RayDistributedExecutor

WorkerBase (vllm/v1/worker/worker_base.py)
└── Worker (vllm/v1/worker/gpu_worker.py)

WorkerWrapperBase
└── MultiprocWorkerWrapper (包装 Worker,跑在 worker 进程里)

┌─ 模型运行层 ─────────────────────────────────────────────────┐
GPUModelRunner (V1, vllm/v1/worker/gpu_model_runner.py)
└── 继承 LoRAModelRunnerMixin + KVConnectorModelRunnerMixin
+ ECConnectorModelRunnerMixin
(另有 V2 版本 vllm/v1/worker/gpu/model_runner.py,默认走 V1)

VllmModel (Protocol, vllm/model_executor/models/interfaces_base.py)
├── VllmModelForTextGeneration ← 生成模型接口(compute_logits)
└── VllmModelForPooling ← pooling 模型接口(pooler)

关键点:

  • LLM 的生成逻辑其实在 OfflineInferenceMixin 里(_run_completion / _run_engine),LLM 自己只负责参数和入口。
  • InprocClientSyncMPClient / AsyncMPClient 继承同一个 EngineCoreClient,接口一致,只是”EngineCore 在哪跑、怎么通信”不同。
  • 模型本身是 PyTorch nn.Module 且实现 VllmModel 协议(__init__(vllm_config, prefix)forward(input_ids, positions)),vLLM 通过 is_vllm_model 等运行时协议检查来识别。

2. 关键类速查表

文件:行 职责 构造时机
LLM vllm/entrypoints/llm.py:66 离线入口,对外提供 generate() 用户直接创建
OfflineInferenceMixin vllm/entrypoints/offline_utils.py:49 _run_completion / _run_engine,驱动 step 循环 LLM 继承
LLMEngine vllm/v1/engine/llm_engine.py:48 前端引擎:输入输出处理、持有 EngineCoreClient LLM.__init__ 中创建
EngineCoreClient vllm/v1/engine/core_client.py:71 客户端抽象,工厂 make_client 选择实现 工厂方法
InprocClient vllm/v1/engine/core_client.py:276 单进程客户端,内部直接持有 EngineCore make_client(multiprocess_mode=False)
EngineCore vllm/v1/engine/core.py:96 引擎内核:Scheduler + Executor + KV cache + 输出 InprocClient.__init__ 中创建
Scheduler vllm/v1/core/sched/scheduler.py:68 请求排队、每步分配 token/KV block、preemption EngineCore.__init__ 中创建
Executor vllm/v1/executor/abstract.py:37 worker 管理抽象,collective_rpc / execute_model EngineCore.__init__ 中创建
MultiprocExecutor vllm/v1/executor/multiproc_executor.py:103 起 worker 进程、广播调度输出、聚合结果 多 worker 时默认(backend=”mp”)
UniProcExecutor vllm/v1/executor/uniproc_executor.py:45 单个 worker 与 executor 同进程 world_size==1 时默认(backend=”uni”)
Worker vllm/v1/worker/gpu_worker.py:117 每张卡一个:初始化设备、加载模型、执行模型 executor 构造时拉起
GPUModelRunner (V1) vllm/v1/worker/gpu_model_runner.py:421 输入张量准备、forward、sampling、KV cache 初始化 Worker.load_model 时创建
VllmConfig vllm/config/vllm.py:290 全量配置聚合(Model/Cache/Parallel/Scheduler…) EngineArgs.create_engine_config
EngineArgs vllm/engine/arg_utils.py CLI/构造参数 → 配置的转换器 LLM.__init__ 中创建
InputProcessor vllm/v1/engine/input_processor.py:36 原始输入 → EngineCoreRequest,参数校验 LLMEngine.__init__
OutputProcessor vllm/v1/engine/output_processor.py:417 EngineCoreOutputsRequestOutput(detokenize) LLMEngine.__init__
Request vllm/v1/request.py:59 调度器视角的请求状态机(token 数、block hash 等) EngineCore.preprocess_add_request
StructuredOutputManager vllm/v1/structured_output/init.py:36 JSON Schema/正则编译与位掩码管理 EngineCore.__init__
Sampler vllm/v1/worker/gpu/sample/sampler.py:30 根据 logits 采样 token GPUModelRunner.__init__

3. 阶段 A:配置构建(EngineArgs → VllmConfig)

1
2
3
4
5
6
7
8
9
10
11
12
13
用户参数(model、dtype、tp_size...)


EngineArgs(vllm/engine/arg_utils.py)
│ create_engine_config()(arg_utils.py:1784)

VllmConfig(vllm/config/vllm.py:290)
├─ ModelConfig 模型名、dtype、max_model_len、runner_type
├─ CacheConfig KV cache 相关(gpu_memory_utilization、block_size...)
├─ ParallelConfig TP/PP/DP 大小、executor 后端
├─ SchedulerConfig 调度策略、max_num_seqs、max_num_batched_tokens
├─ DeviceConfig / LoadConfig / LoRAConfig / SpeculativeConfig ...
└─ __post_init__() 校验 + 派生配置(如 kv_transfer_config 的自动设置)

说明:

  • VllmConfig 是”配置的总线”,后面所有类的构造函数都只收这一个对象,避免参数满天飞。
  • LLM.__init__ 里先把 kwargs 组装成 EngineArgs(llm.py:176),再由 LLMEngine.from_engine_argscreate_engine_config() 生成 VllmConfig
  • 单进程模式下,LLMEngine.from_engine_args(llm_engine.py:161)会把 multiprocess_mode 置为 envs.VLLM_ENABLE_V1_MULTIPROCESSING(默认 1);要真正同进程需设 VLLM_ENABLE_V1_MULTIPROCESSING=0

4. 阶段 B:LLM / LLMEngine / InprocClient 实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LLM.__init__(llm.py:176)
├─ 整理 kwargs → EngineArgs(llm.py:301 附近)
├─ LLMEngine.from_engine_args(engine_args)(llm.py:349)
│ ├─ vllm_config = engine_args.create_engine_config() ← 阶段 A
│ ├─ executor_class = Executor.get_class(vllm_config) ← world_size==1 时得到 UniProcExecutor
│ └─ LLMEngine(vllm_config, executor_class, ...)

└─ LLMEngine.__init__(llm_engine.py:51)
├─ renderer = renderer_from_config(vllm_config) ← tokenizer 在这里
├─ InputProcessor(vllm_config, renderer) ← 原始输入 → EngineCoreRequest
├─ OutputProcessor(renderer.tokenizer, ...) ← EngineCoreOutputs → RequestOutput
└─ engine_core = EngineCoreClient.make_client(
multiprocess_mode=False, asyncio_mode=False, ← 单进程分支
vllm_config, executor_class, log_stats)
└─ InprocClient(vllm_config, executor_class, log_stats)
└─ self.engine_core = EngineCore(vllm_config, executor_class, log_stats)

注意:InprocClient 的构造(core_client.py:286)就是直接 EngineCore(...),所以 EngineCore 的整个初始化在 LLM(...) 返回前就完成了——这也是为什么 LLM() 很慢:模型加载、KV cache profile 全在这一步。


5. 阶段 C:EngineCore.init 初始化顺序

EngineCore.__init__(core.py:99),按执行顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1. load_general_plugins()                   插件加载(引擎/调度层也要生效)
2. self.model_executor = executor_class(vllm_config)
└─ Executor.__init__(abstract.py:95)→ UniProcExecutor._init_executor(uniproc_executor.py:46)
├─ 创建 WorkerWrapperBase(driver_worker,与 executor 同一进程,不拉子进程)
└─ 依次直接调用(同进程方法调用,无进程通信):
worker.init_device() 初始化 CUDA
worker.load_model()(→ gpu_worker.py:377)
└─ GPUModelRunner.load_model(gpu_model_runner.py:5155)
└─ 构建模型实例 + 加载权重(ModelLoader)
(注:world_size > 1 时才走 MultiprocExecutor,为每个 rank 拉起 worker 进程;
本整理的单进程主线不涉及。)
3. self._initialize_kv_caches(vllm_config) ← “Prepare model”,最耗时
├─ register_all_kvcache_specs() 注册所有 KV cache 规格
├─ model_executor.get_kv_cache_specs() 收集每层需要的 KV cache 描述
├─ model_executor.determine_available_memory() 显存 profile → 可用 KV 显存
├─ get_kv_cache_configs() 算出每层 num_blocks 等配置
│ └─ 若 auto-fit 调小 max_model_len → collective_rpc("update_max_model_len")
├─ generate_scheduler_kv_cache_config() 回填 cache_config.num_gpu_blocks / block_size
└─ model_executor.initialize_from_config(kv_cache_configs)
├─ collective_rpc("initialize_from_config") 真正分配 KV cache 显存
└─ collective_rpc("compile_or_warm_up_model") torch.compile + 预热 forward
4. self.structured_output_manager = StructuredOutputManager(vllm_config)
5. Scheduler = vllm_config.scheduler_config.get_scheduler_cls()
└─ Scheduler(vllm_config, kv_cache_config, structured_output_manager, ...)
├─ 内部创建 KVCacheManager / BlockPool / Policy
└─ 无 KV cache 的模型 → 关闭 chunked prefill
6. 其它状态:
├─ batch_queue(max_concurrent_batches>1 时启用)→ 选择 step / step_with_batch_queue
├─ request_block_hasher(prefix caching 用)
├─ is_pooling_model / is_ec_consumer 等标志
└─ freeze_gc_heap() + enable_envs_cache()

说明:上面的 collective_rpc 在单进程模式下都是对 driver_worker直接方法调用UniProcExecutor.collective_rpc,uniproc_executor.py:79),只是接口名沿用了多进程语义;多 worker 时才真正走进程间 RPC。

这个阶段产出的关键对象关系:

1
2
3
4
5
6
7
8
9
10
EngineCore
├─ model_executor(UniProcExecutor)
│ └─ driver_worker(单进程模式下 worker 与 EngineCore 同进程)
│ └─ model_runner(GPUModelRunner)
│ ├─ model(VllmModel,nn.Module)
│ └─ sampler(Sampler)
├─ scheduler
│ ├─ kv_cache_manager + block_pool
│ └─ structured_output_manager
└─ (初始化完成后)batch_queue / request_block_hasher ...

5.1 _initialize_kv_caches 深入解析(详细流程图 + 例子)

_initialize_kv_caches(core.py:240,tracing span 为 “Prepare model”)把”模型每层需要什么 KV 规格“和”显存预算“折算成”每个 worker 每层分多少个 block“,然后真正分配显存并编译/预热模型。输入是 VllmConfig,返回给 Scheduler 用的 KVCacheConfig,同时原地更新 cache_config

详细流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
输入: vllm_config(含 model/cache/scheduler 等配置)


┌────────────────────────────────────────────────────────────────┐
│ 1. register_all_kvcache_specs() 注册 spec → 管理器映射 │
│ (single_type_kv_cache_manager.py:1418) │
└────────────────────────────────────────────────────────────────┘


┌────────────────────────────────────────────────────────────────┐
│ 2. get_kv_cache_specs():collective_rpc("get_kv_cache_spec") │
│ worker → model_runner.get_kv_cache_spec(gpu_model_runner │
│ .py:7465):遍历 attention 层收集 {layer_name: KVCacheSpec} │
│ · KV-sharing 层跳过(不占内存) │
│ · 结果: list[dict],每个 worker 一份 │
└────────────────────────────────────────────────────────────────┘


┌────────────────────────────────────────────────────────────────┐
│ 3. non-causal 检查:任何层 non_causal=True → │
│ 关闭 chunked prefill + prefix caching(因果假设失效) │
└────────────────────────────────────────────────────────────────┘


┌────────────────────────────────────────────────────────────────┐
│ 4. determine_available_memory()(gpu_worker.py:400) │
│ · 手动模式: kv_cache_memory_bytes 直接返回 │
│ · 默认: memory_profiling 里跑 profile_run()(dummy forward) │
│ 可用KV = 初始空闲×gpu_memory_utilization − 模型峰值占用 │
│ · 无 KV cache 的模型 → [0] │
└────────────────────────────────────────────────────────────────┘


┌────────────────────────────────────────────────────────────────┐
│ 5. get_kv_cache_configs()(kv_cache_utils.py:2005) │
│ a. 合并所有 worker 的 spec(同名层必须一致) │
│ b. 校验注册表 + get_kv_cache_groups() 分组 │
│ c. _project_kv_cache_groups_to_worker() PP-aware 投影 │
│ d. max_model_len=-1 → _auto_fit_max_model_len() 二分查找 │
│ e. _check_enough_kv_cache_memory() 显存足够性检查 │
│ f. 每个 worker 生成 KVCacheConfig(num_blocks、tensors、组) │
│ g. num_blocks 取全体最小值,tensor 等比收缩 │
└────────────────────────────────────────────────────────────────┘


┌────────────────────────────────────────────────────────────────┐
│ 6. 若 auto-fit 改了 max_model_len → │
│ collective_rpc("update_max_model_len") 同步给所有 worker │
└────────────────────────────────────────────────────────────────┘


┌────────────────────────────────────────────────────────────────┐
│ 7. generate_scheduler_kv_cache_config()(kv_cache_utils.py:1766)│
│ · 断言所有 worker num_blocks 一致 │
│ · 深拷贝 worker0 配置 │
│ · UniformTypeKVCacheSpecs 展开成单个代表 spec │
│ → 回填 cache_config.num_gpu_blocks / block_size / │
│ kv_cache_size_tokens / kv_cache_max_concurrency │
└────────────────────────────────────────────────────────────────┘


┌────────────────────────────────────────────────────────────────┐
│ 8. initialize_from_config()(abstract.py:118) │
│ a. collective_rpc("initialize_from_config") │
│ → worker: ensure_kv_transfer_initialized()(有 connector)│
│ → model_runner.initialize_kv_cache(gpu_model_runner │
│ .py:7309):补 encoder-only/KV-sharing 层、初始化 │
│ attention backend、真正分配每层 KV tensor │
│ b. collective_rpc("compile_or_warm_up_model") │
│ → torch.compile + 各 batch 尺寸预热 forward │
│ → 返回 CompilationTimes,主进程取 max 写回 config │
└────────────────────────────────────────────────────────────────┘


输出: scheduler_kv_cache_config → Scheduler 构造(core.py:133→146→155)
(resolve_kv_cache_block_sizes 再算出调度器 block/hash block size)

具体数字例子

假设单进程加载一个 8 层 full attention 模型(TP=PP=DP=1),每层 num_kv_heads=8head_size=128fp16

1
2
3
每 token 每层 K+V 字节 = 2(K/V) × 8 heads × 128 dims × 2 B = 4096 B = 4 KiB
每 block(16 token) 每层 = 16 × 4 KiB = 64 KiB
8 层 → 每 block 总共 = 64 KiB × 8 = 512 KiB

第 2 步:每个 worker 返回 8 条 spec:

1
2
3
4
{
"model.layers.0.self_attn": FullAttentionSpec(block_size=16, num_kv_heads=8, head_size=128, dtype=fp16),
...共 8 层...
}

第 4 步:profile 结果——初始空闲 16 GiB、gpu_memory_utilization=0.9,dummy forward 测出模型峰值占用 10 GiB:

1
可用 KV 显存 = 16 GiB × 0.9 − 10 GiB = 4 GiB

第 5 步:合并后 8 层同类型 → 归为一个组,计算 block 数:

1
2
3
4
5
6
7
8
num_blocks = 4 GiB ÷ 512 KiB = 8192

生成的 kv_cache_configs[0]:
num_blocks = 8192
kv_cache_tensors = 8 个 KVCacheTensor(size=512 MiB, shared_by=["model.layers.i.self_attn"])
kv_cache_groups = [ KVCacheGroupSpec(
layer_names=[8 个层名],
kv_cache_spec=FullAttentionSpec(block_size=16, ...)) ]

(若 max_model_len=-1,第 5 步 d 会二分找到一个能放进 4 GiB 的最大长度,例如 8192×16÷32=4096 量级;若调小了,第 6 步会把新值广播给 worker。)

第 7 步:调度器配置 = 深拷贝上述配置,UniformTypeKVCacheSpecs(如果有)展开成单个 FullAttentionSpec(block_size=16);回填:

1
2
3
4
cache_config.num_gpu_blocks       = 8192
cache_config.block_size = 16
cache_config.kv_cache_max_concurrency ≈ 8192 × 16 ÷ 4096 = 32 (单组场景)
cache_config.kv_cache_size_tokens ≈ 32 × 4096 = 131072

第 8 步:worker 为每层分配 512 MiB 的 KV tensor(8 层共 4 GiB),然后按编译配置跑预热 forward,返回编译耗时。

结果消费方

产物 消费方
scheduler_kv_cache_config Scheduler 构造(core.py:133→155),驱动 KVCacheManager/BlockPool
cache_config.num_gpu_blocks / block_size 调度器的 block 分配、watermark、前缀缓存
kv_cache_size_tokens / max_concurrency 显存容量估算、日志
max_model_len(可能被 auto-fit 修改) 全局生效,限制请求上下文长度

5.2 GPUModelRunner.initialize_kv_cache 详解(流程图)

这是 _initialize_kv_caches 第 8 步里 worker 侧的”真正分配显存”环节,由 Worker.initialize_from_config(gpu_worker.py:591)调用。输入是每层该分多少 block 的 KVCacheConfig,输出是每层绑定好的 KV tensor

主流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Worker.initialize_from_config(gpu_worker.py:591)
├─ cache_config.num_gpu_blocks = kv_cache_config.num_blocks
├─ ensure_kv_transfer_initialized(...) 仅配置了 KV connector 时
└─ model_runner.initialize_kv_cache(kv_cache_config) ← 本函数(gpu_model_runner.py:7309)


GPUModelRunner.initialize_kv_cache
1. kv_cache_config = deepcopy(...) 并保存到 self.kv_cache_config;_mamba_bufs = None
2. may_add_encoder_only_layers_to_kv_cache_config() 补 encoder-only 层(gpu_model_runner.py:7439)
3. maybe_add_kv_sharing_layers_to_kv_cache_groups() 把 KV-sharing 层并进目标层组
4. initialize_attn_backend(kv_cache_config, is_profiling) ← 关键步骤(:6748)
└─ 每个 KV cache group 里,为每层选 attention backend(FA/FlashInfer/Triton...),
按 (backend, kv_cache_spec, num_heads_q) 去重,建出 self.attn_groups
5. initialize_mamba_ssu_backend(mamba_config, kv_cache_config) Mamba 层专用
6. kernel_block_sizes = prepare_kernel_block_sizes(kv_cache_config, attn_groups)
(vllm/v1/worker/utils.py:331)
└─ 调度器 block_size 与 kernel 支持的 block_size 可能不同:
如调度器用 256、kernel 只支持 64 → kernel_block_sizes=[64],1 个调度块拆 4 个 kernel 块
7. initialize_metadata_builders(kv_cache_config, kernel_block_sizes)(:6855)
└─ 每个 attn group 建 AttentionMetadataBuilder(后续 forward 造 metadata 用)
8. may_reinitialize_input_batch(kv_cache_config, kernel_block_sizes)(:6965)
└─ 若最终 block size 与构造时占位不同 / 多组 → 重建 InputBatch(含 BlockTables)
9. kv_caches = initialize_kv_cache_tensors(kv_cache_config, kernel_block_sizes)(:7226)
└─ 真正分配显存(见下方子流程)
10. spec decode 校验:uses_extract_hidden_states 时 validate_same_kv_cache_group()
11. KV connector 注册(has_kv_transfer_group() 且非 profiling):
├─ cross_layers_kv_cache 非空 → register_cross_layers_kv_cache(uniform 布局)
└─ 否则 → register_kv_caches(kv_caches) + set_host_xfer_buffer_ops()

子流程:initialize_kv_cache_tensors(真正分配 tensor)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
initialize_kv_cache_tensors(kv_cache_config, kernel_block_sizes)(gpu_model_runner.py:7226)
├─ if use_uniform_kv_cache(attn_groups): 仅当"配置了 KV connector 且 connector 要求
│ └─ allocate_uniform_kv_caches(...) 跨层块布局"时走(kv_connector_model_runner_mixin.py:115)
│ 所有层共享一个大 tensor,块内按层连续排布
├─ else:默认 per-layer 布局
│ ├─ _allocate_kv_cache_tensors(kv_cache_config)(:7029)
│ │ └─ 遍历 kv_cache_tensors:
│ │ block_stride>0 → 共享 packed backing;否则 torch.zeros(size)
│ │ 按 shared_by 建立 {layer_name: tensor}
│ │ 并断言 所有层都有 tensor(漏配直接报错)
│ └─ _reshape_kv_cache_tensors(raw_tensors, kernel_block_sizes)(:7081)
│ └─ 每层按自己的 spec(num_kv_heads/head_size/dtype)reshape 成
│ (num_blocks, 2, block_size, num_kv_heads, head_size)
│ packed 布局按 offset/block_stride 切出每层 view
├─ 处理 KV-sharing:kv_caches[layer] = kv_caches[target_layer]
└─ bind_kv_cache(kv_caches, static_forward_context, ...)(vllm/v1/worker/utils.py:462)
├─ 按层序填充 runner 的 kv_caches 列表
└─ forward_context[layer_name].kv_cache = kv_cache ← 每层 Attention 挂上自己的 tensor

返回后(Worker.initialize_from_config 收尾)

1
2
3
4
5
6
initialize_from_config 收尾
├─ enable_return_routed_experts → model_runner.init_routed_experts_capturer()(:7382)
└─ needs_kv_cache_zeroing(如 Mamba 层)→ model_runner._init_kv_zero_meta()(:1090)
└─ 构造 KVBlockZeroer,供运行时新分配 block 清零使用

之后 EngineCore 再调 collective_rpc("compile_or_warm_up_model") 做编译/预热

具体例子(沿用 5.1 的 8 层 full attention)

输入 kv_cache_config

1
2
3
4
5
num_blocks        = 8192
kv_cache_tensors = 8 × KVCacheTensor(size=512 MiB, shared_by=["model.layers.i.self_attn"])
kv_cache_groups = [ KVCacheGroupSpec(layer_names=[8 层],
kv_cache_spec=FullAttentionSpec(block_size=16, num_kv_heads=8,
head_size=128, dtype=fp16)) ]

各步骤结果:

1
2
3
4
5
6
4. attn_groups       = 1 组,backend 按平台选(CUDA 默认 FlashAttention)
6. kernel_block_sizes = [16] (调度器 block 16 == kernel block 16,无需拆分)
7. metadata builders = 该组建 1 个 FlashAttentionMetadataBuilder
8. InputBatch 重建 block_sizes=[16],max_num_blocks_per_req = cdiv(4096, 16)
9. 分配 每层 torch.zeros(512 MiB) → reshape 成 (8192, 2, 16, 8, 128)
→ bind:8 个 Attention 层各拿到自己的 512 MiB tensor

若调度器 block_size=256 而 kernel 只支持 64(多组场景),则 kernel_block_sizes=[64],block table 里 1 个逻辑块会拆成 4 个 kernel 块(blocks_per_kv_block = block_size // kernel_block_size)。


6. 阶段 D:generate 完整调用链

6.1 请求入队(单条 prompt 视角)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
LLM.generate(prompts, sampling_params)(llm.py:422)
└─ OfflineInferenceMixin._run_completion(offline_utils.py:326)
└─ _add_completion_requests(offline_utils.py:290)
├─ _preprocess_cmpl_one(offline_utils.py:145)
│ └─ renderer.render_cmpl(renderers/base.py:928)
│ ├─ tokenize_prompts() 文本 → token ids
│ └─ process_for_engine() 组装成 EngineInput(含 prompt_token_ids)
└─ _render_and_add_requests(offline_utils.py:523)
└─ _add_request(offline_utils.py:552)
├─ request_id = 自增计数器
└─ LLMEngine.add_request(request_id, prompt, params, ...)(llm_engine.py:218)
├─ InputProcessor.process_inputs(input_processor.py:242)
│ ├─ 参数校验(SamplingParams.verify)
│ ├─ 补全 max_tokens(默认到 max_model_len)
│ └─ 产出 EngineCoreRequest(prompt_token_ids、sampling_params、lora...)
├─ OutputProcessor.add_request(request) 登记请求状态、detokenizer
└─ InprocClient.add_request(request)(core_client.py:297)
├─ EngineCore.preprocess_add_request(core.py:855)
│ ├─ Request.from_engine_core_request(request.py:198)
│ └─ structured_output_manager.grammar_init(req) ← 结构化输出编译
└─ EngineCore.add_request(core.py:372)
└─ Scheduler.add_request(scheduler.py:1964)
└─ 进入 waiting 队列(+ prefix cache hash 计算)

6.2 运行循环(step 循环)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
_run_engine(offline_utils.py:573)
└─ while llm_engine.has_unfinished_requests():
└─ LLMEngine.step()(llm_engine.py:296)
├─ outputs = engine_core.get_output()
│ └─ InprocClient.get_output(core_client.py:290)
│ └─ EngineCore.step_fn() → EngineCore.step(core.py:479)
│ ├─ scheduler.schedule()(scheduler.py:388)
│ │ ├─ 按策略从 running/waiting 选请求
│ │ ├─ 分配 num_scheduled_tokens(prefill 一次性/分块,decode 每步 1 token)
│ │ ├─ 分配/复用 KV blocks(prefix caching 命中则不重算)
│ │ └─ 产出 SchedulerOutput(每个请求调度多少 token、元数据)
│ ├─ model_executor.execute_model(scheduler_output)(uniproc_executor.py:108)
│ │ └─ collective_rpc("execute_model") → 同进程直接调用
│ │ → Worker.execute_model(gpu_worker.py:836)
│ │ └─ GPUModelRunner.execute_model(gpu_model_runner.py:4056)
│ │ ├─ _update_states() 更新 batch 状态
│ │ ├─ _prepare_inputs() 组装 input_ids/positions/attn_metadata
│ │ ├─ 选择 cudagraph/compile 路径(_determine_batch_execution_and_padding)
│ │ ├─ _model_forward()(gpu_model_runner.py:3770)
│ │ │ └─ self.model(input_ids, positions, ...) → hidden_states → logits
│ │ └─ 返回待采样状态(execute_model_state)
│ ├─ 若需要:model_executor.sample_tokens(grammar_output)(uniproc_executor.py:123,直接调用)
│ │ └─ Worker.sample_tokens(gpu_worker.py:830)
│ │ └─ GPUModelRunner.sample_tokens(gpu_model_runner.py:4435)
│ │ ├─ apply_grammar_bitmask() 结构化输出约束
│ │ └─ _sample()(gpu_model_runner.py:3582)
│ │ └─ Sampler → sampled_token_ids
│ └─ scheduler.update_from_output(...)
│ ├─ 更新 num_computed_tokens、KV block 状态
│ ├─ 检测 finish(max_tokens/stop/eos)
│ └─ 产出 EngineCoreOutputs(new_token_ids、finish_reason、logprobs...)
├─ output_processor.process_outputs(outputs)(output_processor.py:576)
│ ├─ Detokenizer.update() token ids → 文本(增量式)
│ ├─ stop string 检测
│ └─ 组装 RequestOutput
└─ 返回本次 step 的 RequestOutput 列表

收集 finished=True 的输出 → 按 request_id 排序返回

6.3 一次 step 的数据流(简化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SchedulerOutput(每个请求:调度多少 token、KV block 分配、采样参数)


ModelRunner 输入准备(input_ids / positions / attention metadata 全部批量化、搬 GPU)


model.forward() ──► hidden_states ──► compute_logits() ──► logits


grammar bitmask(结构化输出)──► Sampler ──► sampled_token_ids


EngineCoreOutputs(new_token_ids、finish_reason、logprobs、scheduler_stats)


OutputProcessor(Detokenizer 增量解码 + stop 检测)──► RequestOutput

7. 构造依赖顺序(谁先谁后)

1
2
3
4
5
6
7
8
9
10
11
12
EngineArgs
└─► VllmConfig(所有子 Config)
└─► LLMEngine
├─► Renderer(tokenizer)
├─► InputProcessor
├─► OutputProcessor
└─► InprocClient
└─► EngineCore
├─► UniProcExecutor ──► Worker(同进程)──► GPUModelRunner ──► Model(先加载权重)
├─► KV cache:specs ─► memory profile ─► configs ─► 分配/预热
└─► StructuredOutputManager
└─► Scheduler(依赖 kv_cache_config、structured_output_manager)

依赖要点:

  • Scheduler 必须等 KV cache 配置算完才能建,因为 block 数/block size 是它的输入。
  • ModelRunner 必须先加载模型、profile 显存,才能决定 KV cache 大小——所以初始化顺序是”模型 → KV cache → 调度器”。
  • OutputProcessor 依赖 renderer 的 tokenizer(detokenize 需要)。

8. 想快速定位时的源码索引

想找什么 位置
离线入口 LLM / generate vllm/entrypoints/llm.py:66 / :422
离线 step 循环 vllm/entrypoints/offline_utils.py:573
前端引擎 LLMEngine vllm/v1/engine/llm_engine.py:48
客户端工厂 / 单进程客户端 vllm/v1/engine/core_client.py:83 / :276
引擎内核 EngineCore.__init__ vllm/v1/engine/core.py:99
KV cache 初始化 vllm/v1/engine/core.py:249
调度器 schedule() vllm/v1/core/sched/scheduler.py:388
执行器抽象 vllm/v1/executor/abstract.py:37
单进程执行器(world_size==1 默认) vllm/v1/executor/uniproc_executor.py:45
执行器直接调用 worker(collective_rpc) vllm/v1/executor/uniproc_executor.py:79
多进程执行器(多 worker 时默认) vllm/v1/executor/multiproc_executor.py:103
worker 加载与执行 vllm/v1/worker/gpu_worker.py:250 / :836
model runner 执行 vllm/v1/worker/gpu_model_runner.py:4056
模型 forward 调用点 vllm/v1/worker/gpu_model_runner.py:3770
sampling vllm/v1/worker/gpu_model_runner.py:3582
输入处理 vllm/v1/engine/input_processor.py:242
输出处理 / detokenize vllm/v1/engine/output_processor.py:576
全量配置 vllm/config/vllm.py:290
模型协议接口 vllm/model_executor/models/interfaces_base.py:47

9. 附录:教学版 LLM 调度器(vllm_basic_scheduler.ipynb)调用逻辑详解

来源:D:\code\InfraTech\llm_infer\vllm_basic_scheduler.ipynb。这是一个从零模拟 vLLM 调度器的教学 notebook:用假模型 + 随机 token + 迷你 KV 块管理器,复刻 V0 风格 continuous batching(prefill/decode 分阶段 + 前缀缓存 + 抢占)。下文按”请求到达 → allocate → token 计数 → schedule → 主循环”逐步拆解。(其实就是nano-vllm)

9.1 组件与真实 vLLM 的对应

教学版 对应真实 vLLM 职责
Config SchedulerConfig / CacheConfig 块大小、最大并发、token 预算
Sequence Request 单请求:token 列表、状态机、block_table
Block / BlockManager KVCacheManager + BlockPool 块的分配/释放、前缀缓存(xxhash)
Scheduler Scheduler(V0 风格) prefill/decode 调度、抢占
run_fake_model execute_model + Sampler 每个序列”生成”一个随机 token
Engine LLMEngine 请求队列 + 主循环 + 可视化

9.2 初始化链路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Config(num_kvcache_blocks, kvcache_block_size=256, max_num_seqs, max_num_batched_tokens)
└─ BlockManager(num_blocks, block_size)
├─ blocks[] = [Block(i) for i in range(num_blocks)] 每个 Block: block_id/ref_count/hash/token_ids
├─ free_block_ids = deque(0..N-1) 空闲块队列(FIFO)
├─ used_block_ids = set()
└─ hash_to_block_id = {} 前缀缓存表
└─ Scheduler(config)
├─ waiting: deque / running: deque
├─ num_seqs / num_batched_tokens
└─ block_manager
└─ Engine(...)
├─ reqs_q = queue.Queue() 外部请求队列
└─ 前 init_reqs 个请求:
prompt → Sequence(token_ids) → scheduler.add(seq) → waiting 队尾

9.3 请求到达:allocate 的调用与 block cache 命中处理

一个序列进入调度器后,只有在 schedule() 的 prefill 阶段被选中时才会真正分配块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
用户/Engine:  Sequence(token_ids) ──► scheduler.add(seq) ──► waiting.append(seq)
(此时只排队,不分配任何块)

schedule() → prefill() 选中该 seq:
├─ 预算检查: num_batched_tokens + len(seq) ≤ max_num_batched_tokens
├─ 块检查: block_manager.can_allocate(seq) ← len(free_block_ids) ≥ seq.num_blocks
└─ block_manager.allocate(seq) ← 核心:逐块分配/复用
for i in range(seq.num_blocks): # 每 256 个 token 一块
token_ids = seq.block(i)
h = compute_hash(token_ids, prefix=上一个块的hash) # xxhash 链式哈希
block_id = hash_to_block_id.get(h, -1)

if block_id == -1 或 blocks[block_id].token_ids != token_ids:
cache_miss = True # 命中失败
if cache_miss:
# 未命中:从空闲队列头拿新块(FIFO)
block = _allocate_block(free_block_ids[0])
# → block.reset()(ref_count=1)、从 free 移除、加入 used
else:
# ★ 前缀缓存命中:
seq.num_cached_tokens += block_size # 这 256 个 token 不用重算
if block_id in used_block_ids:
block.ref_count += 1 # 别的请求还在用 → 共享
else:
block = _allocate_block(block_id)# 块已被释放但 hash 还在 → 重新占用
if h != -1:
block.update(h, token_ids) # 记录内容+hash
hash_to_block_id[h] = block_id # 写入缓存表
seq.block_table.append(block_id) # 块表追加物理块号

三个关键点:

  1. cache miss 是”粘滞”的:一旦某个块未命中,cache_miss=True 后后续所有块都走重新分配——这模拟了前缀缓存只能”从头连续命中”的特性(中间断了一块,后面的都作废)。
  2. 命中时按块加 256 个 cached token,后面 num_batched_tokens 会减去这部分,表示”这次不用算”。
  3. 释放过但 hash 保留的块(类似真实 vLLM 的 cached 块)命中时会被重新占用,而不是新分配——这是前缀缓存”复用”的本质。

9.4 token 计数:num_tokens 是怎么增加的

字段 含义 在哪里增长
seq.num_tokens 序列总 token 数(prompt + 生成) 初始 = len(prompt);decode 每步 append_token +1
seq.num_prompt_tokens 初始 prompt 长度 构造时固定
seq.num_cached_tokens 前缀缓存命中的 token 数 allocate() 命中一个块 +256;deallocate() 归零
seq.num_completion_tokens 已生成 token 数 = num_tokens − num_prompt_tokens 随 append_token 增长
scheduler.num_batched_tokens 本轮实际要算的 token 预算 每步 schedule() 重置为 0;prefill 时 += len(seq) − num_cached_tokens
scheduler.num_seqs 本轮并发请求数 每步重置为 0;选中一个请求 +1

一个具体例子(假设 seq 有 600 个 prompt token、块大小 256):

1
2
3
4
5
6
初始:   num_tokens=600, num_prompt_tokens=600, num_blocks=ceil(600/256)=3
prefill: 前 256 token 命中缓存 → num_cached_tokens=256
num_batched_tokens += 600 − 256 = 344 ← 这轮实际要算的量
(注意 prefill 只算 prompt,num_tokens 不变)
decode: 每步 run_fake_model 生成 1 token → append_token → num_tokens=601,602,...
累计生成 64 个后 num_completion_tokens==max_tokens → FINISHED

9.5 schedule 的完整调用逻辑(prefill / decode / preempt / postprocess)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
scheduler.schedule(prefill_first=True)
├─ num_seqs = 0; num_batched_tokens = 0 # 每步重置预算
├─ first_call, second_call = (prefill, decode) # prefill_first=False 则反过来
├─ scheduled_seqs, is_prefill = first_call()
│ └─ 有结果就直接返回(一轮只做一种)
├─ scheduled_seqs, is_prefill = second_call()
└─ assert scheduled_seqs # 教学版假定两轮必有一轮有结果

prefill():处理新请求(FCFS,从 waiting 队头)
while waiting 且 num_seqs < max_num_seqs:
seq = waiting[0]
if token 预算超限 或 not can_allocate(seq): break # 不够就停,等下一轮
num_seqs += 1
block_manager.allocate(seq) # 见 9.3
num_batched_tokens += len(seq) - seq.num_cached_tokens
seq.status = RUNNING; waiting.popleft → running.append

decode():推进正在生成的请求
while running 且 num_seqs < max_num_seqs:
seq = running.popleft()
while not block_manager.can_append(seq): # 没块装新 token
if running: preempt(running.pop()) # ★ 抢占队尾请求
else: preempt(seq); break # 只剩自己 → 放弃本轮
else:
num_seqs += 1
block_manager.may_append(seq) # 见下
scheduled_seqs.append(seq)
running.extendleft(reversed(scheduled_seqs)) # 保持原顺序

may_append(seq):decode 时的块维护
if len(seq) % block_size == 1: # 新 token 是下一个块的首个 → 分配新块
从 free_block_ids[0] 分配,追加进 block_table
elif len(seq) % block_size == 0: # 刚填满一个块 → 算 hash 写入缓存表
last_block.update(hash, token_ids); hash_to_block_id[h] = block_id
else: # 块中间 → 什么都不做
pass

preempt(seq):资源不足时把请求踢回等待队列
seq.status = WAITING
block_manager.deallocate(seq) # 逆序释放块:ref_count--,为 0 回 free 队列
waiting.appendleft(seq) # 插到队首,优先重新调度
(被抢占的请求 num_cached_tokens=0,重新 prefill 时从头算)

postprocess(seqs, token_ids):每步"前向"之后的收尾
for seq, token_id in zip(seqs, token_ids):
seq.append_token(token_id) # num_tokens += 1
if (token_id == eos 且 not ignore_eos) 或 num_completion_tokens == max_tokens:
seq.status = FINISHED
block_manager.deallocate(seq) # 释放所有块(hash 保留供复用)
running.remove(seq)

9.6 continuous batching 与三种演示场景

教学版的 continuous batching 核心:主循环里不断”补新请求 + 调度”,新请求不等旧请求跑完就能进:

1
2
3
4
5
6
Engine.run_engine(steps)
├─ scheduler.schedule(prefill_first) ← 每步调度
├─ token_ids = run_fake_model(seqs) ← 假前向,每序列出 1 token
├─ scheduler.postprocess(seqs, token_ids)
└─ 若 steps > add_reqs_step: ← 按节奏从外部队列补新请求
reqs_q.get() → Sequence → scheduler.add(seq)

三种演示:

场景 配置 观察点
cell 27 prefill_first=True、块充足 新请求随到随 prefill,旧请求持续 decode(连续批处理)
cell 29 num_kvcache_blocks=3, block_size=10(总容量 30 token) 资源不足 → can_append 失败 → 抢占 running 队尾请求回 waiting
cell 31 prefill_first=False decode 优先:先推进 running 里的生成,再收新请求

注意教学版与真实 V1 的差异:一轮 schedule() 只做 prefill 或 decode 一种(更接近 V0 的分阶段 continuous batching);真实 V1 是 prefill chunk 与 decode 混在同一 step 的一个 batch 里。另外教学版空闲块队列是 FIFO,真实 vLLM 是 LRU + watermark。

9.7 完整调用链路(一次请求从入队到结束)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
初始化: Config → BlockManager → Scheduler → Engine → Sequence(prompt) → scheduler.add → waiting

▼ 主循环(while not scheduler.is_finished())
schedule(prefill_first)
├─ prefill: 预算/块检查 → allocate(逐块 hash,命中复用/未命中新分配)→ running
│ 块不够 → 不调度,等下一轮
└─ decode: can_append 失败 → preempt(队尾)(deallocate + 回 waiting 队首)
may_append(跨块边界分配新块/填满块写 hash)


run_fake_model(seqs) ──► 每序列 1 个随机 token(或达到 max_len 返回 eos)


postprocess: append_token(num_tokens+1)→ 判定 eos/max_tokens
│ ├─ 未结束 → 留在 running,下一轮继续 decode
│ └─ 结束 → FINISHED → deallocate(块回池,hash 保留)→ running 移除

├─ 中途补请求: steps > add_reqs_step → reqs_q.get → scheduler.add(下一轮 prefill 进)


waiting 和 running 都空 → is_finished()=True → 循环结束