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) └──────────────────────────────────────────────────────────┘
|
两条主线:
- 实例化链(构造):
LLM → LLMEngine → EngineCoreClient → EngineCore → UniProcExecutor → Worker → ModelRunner → Model,再往上把 Scheduler、OutputProcessor 等搭好。
- 推理链(运行):prompt → 渲染/tokenize →
EngineCoreRequest → Request 进调度器 → 每步 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 自己只负责参数和入口。
InprocClient 与 SyncMPClient / 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 |
EngineCoreOutputs → RequestOutput(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_args 调 create_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=8、head_size=128、fp16:
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) # 块表追加物理块号
|
三个关键点:
- cache miss 是”粘滞”的:一旦某个块未命中,
cache_miss=True 后后续所有块都走重新分配——这模拟了前缀缓存只能”从头连续命中”的特性(中间断了一块,后面的都作废)。
- 命中时按块加 256 个 cached token,后面
num_batched_tokens 会减去这部分,表示”这次不用算”。
- 释放过但 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 → 循环结束
|