数据截至 (上游 commit f25c580af159)
06 · 巧妙之处、边界与代码地图
这一章讲什么: 前五章读完后,把「值得带走的设计」「该小心的坑」「和 兄弟项目的分工」收拢在一处,最后给一张按主题跳源码的全局地图。
1. 巧妙之处(可借鉴的技术)
每条先白话点出「妙在哪」,再给出处。
1.1 一份块对象,服务两套语义
KVCacheBlock 同时是「显存页框」和「前缀缓存条目」:ref_cnt 管生命周期,_block_hash 管可查找性,空闲双向链表的顺序天然就是 LRU 驱逐顺序。驱逐不需要单独的缓存管理器——分配新块时顺手把旧哈希作废(BlockPool._maybe_evict_cached_block,vllm/v1/core/block_pool.py:679)。想给任何「可复用的昂贵中间结果」做缓存,这套「池 + 引用计数 + 懒惰驱逐」结构都能照搬。
1.2 链式哈希让前缀命中是 O(前缀块数)
块哈希 = hash(父块哈希, 本块 token, 额外 key)(hash_block_tokens,vllm/v1/core/kv_cache_utils.py:621)。因为内容被递归卷入,第 i 块命中蕴含前面全命中,查找从前往后遇到第一个 miss 就停(FullAttentionManager.find_longest_cache_hit,vllm/v1/core/single_type_kv_cache_manager.py:686)。而且哈希在请求创建/追加 token 时增量算好(Request.block_hashes,vllm/v1/request.py:219),调度热路径零重算。
1.3 「没有 prefill/decode 阶段」的统一进度条
调度器只维护 num_computed_tokens 追赶 num_tokens_with_spec(注释见 vllm/v1/core/sched/scheduler.py:503-511)。一个抽象同时长出 chunked prefill、continuous batching、投机解码调度——特性数量没有变成分支数量,这是 V1 能持续加特性而不散架的根本原因。
1.4 SchedulerOutput 的 diff 协议
老请求每步只发增量(新块号、新 token 数),全量信息只在新请求时发一次(SchedulerOutput,vllm/v1/core/sched/output.py:219;worker 侧持久化 batch 增量更新,GPUModelRunner._update_states,vllm/v1/worker/gpu_model_runner.py:1191)。把进程间通信当成「状态同步协议」设计,而不是每步传整个 batch。
1.5 CUDA graph 的「单一事实来源」派发
决策集中在 CudagraphDispatcher.dispatch(vllm/v1/cudagraph_dispatcher.py:235):模式 + padded 尺寸算好写进 forward context;CUDAGraphWrapper(vllm/compilation/cuda_graph.py:145)盲目执行,不自行判断。尺寸向上取整用预计算的完整映射数组 O(1) 查(_compute_bs_to_padded_graph_size,vllm/v1/cudagraph_dispatcher.py:72)——连二分都省了。
1.6 KV cache 容量是量出来的,不是配出来的
启动时真跑一次 dummy forward 测显存峰值,扣掉权重、激活、CUDA graph 缓冲,剩下全给 KV cache(GPUWorker.determine_available_memory,vllm/v1/worker/gpu_worker.py:512;编排入口 EngineCore._initialize_kv_caches,vllm/v1/engine/core.py:254)。用户只给一个比例(gpu_memory_utilization 默认 0.92,vllm/config/cache.py:111),永远拿满可用显存,永不理论超卖。