Prhub

#50174 [3/N][Feat][Perf] Add new warmup infrastructure for JITs. Add provider registry and orchestration for JIT warmup

原始 PR 作者 LopezCastroRoberto 合并时间 2026-08-18 01:11 文件变更 12 提交数 21 评论 37 代码增减 +1067 / -168

执行摘要

新增 JIT warmup 注册表与编排,迁移 block-table 内核预热

PR body 明确动机:从实际实例化的 model 与 backend 对象中发现 warmup provider,避免基于模型名清单预热未激活后端的 kernel;将编译纳入 kernel_warmup() 的日志、排序、进度与异常处理框架;尊重 enable_jit_warmup;保持模型构建与运行时执行和启动期编译分离。关联 issue #49349 提出 "Zero JIT compilation during runtime" 总目标,本 PR 被列为基础阻塞项之一。

值得精读。这是 vLLM 启动期 JIT 预热架构的地基 PR,重点看三处设计:VllmJitKernel 的 CompileKey/dispatch/get_warmup_keys 契约如何让预热 key 与运行期特化严格一致;named_parameters**kwargs 转发的支持如何在静态可见性与低样板之间取舍;triton_scalar_specialization_rep 对 Triton 缓存键类的精确建模。合并前建议复核 kernel_warmup() 中非 v2 runner 分支与新增 registry 编排的执行顺序,以及 enable_jit_warmup 默认值对老路径的兼容性。

讨论亮点

核心讨论围绕"如何降低采用成本"与"AST 静态可见性的取舍"展开,结论均已落入代码:

  • LucasWilkinson 问 InputBatch.__init__ 期间会调用哪些 JIT kernel,LopezCastroRoberto 澄清此时只有 ComputeSlotMappingKernel 被注册,activate() 是为后续 50175-50178 预留;Lucas 一度建议暂时跳过,最终保留并写入注释。
  • 样板代码争议:Lucas 担心 dispatch() 大量显式字段降低采用意愿,提出能否用 **kwargs;Roberto 解释 _trace_dispatch() 是静态解析 dispatch 并展开符号化输入,Python 不会替我们做 kwargs 绑定,因此新增 named_parameters 支持,使 **compile_key_fields 变成 1:1 的 CompileKey 字段转发,既精简又保留类型校验。
  • get_warmup_keys 统一接收 vllm_config 的提议被否决:该内核依赖最终化 BlockTable 状态,传 self 会让 registry 保留过期 placeholder buffers,快照标量更安全。
  • triton_scalar_specialization_rep 的魔法值(1、16、2^31±1 等)依据 Triton 的 handle_long_type() 实现,对应 i32/i64/u64 与 constant-1/divisible-16/generic 三类缓存键。
  • 文档术语 "owner" 改为 "kernel wrapper",并澄清 statement 级 if 与条件表达式支持边界及 unused_input 示例语义。

实现拆解

  1. 建立注册契约与运行时缓存:在 vllm/model_executor/warmup/jit_warmup.py 中新增 JitWarmupRegistry(基于 ContextVar 的作用域激活,register_warmup() 只记录元数据不编译,warmup() 统一展开、去重并调用 VllmJitKernel.warmup());VllmJitKernel 新增 _get_or_compile() 运行时缓存,compile() 未写缓存时抛 RuntimeError,把"运行时隐性编译"变成可监控的显式错误。
  2. 扩展 AST 追踪能力_DispatchExprEvaluator 新增 visit_Subscript、Python builtins 解析、in/not in/is/is not 比较;get_function_source_node 支持 lambda;_trace_compile_key_dispatch 支持 dispatch 通过自身 **kwargs 参数一次性转发 CompileKey 字段(named_parameters 机制),重复或未知字段在构造 CompileKey 时抛 TypeError。
  3. 迁移 block-table 内核vllm/v1/worker/block_table.py_compute_slot_mapping_kernel 被封装成 ComputeSlotMappingKernel(VllmJitKernel)BlockTable.__init__SlotMappingMode.TOKEN_TO_KV_SLOT 下调用 register_warmup() 快照实际特化标量;compute_slot_mapping() 改为走统一 kernel 调用;同步删除 vllm/model_executor/warmup/v1_block_table_warmup.py 的旧预热函数。
  4. 接入启动编排GPUModelRunner 与 v1 GPUModelRunner(gpu/model_runner.py) 持有 jit_warmup_registry,两处 InputBatch 构建(占位与真实 KV-cache 几何确定后)都包在 activate() 作用域内;kernel_warmup()enable_jit_warmup 开启时调用 registry.warmup(),带 perf_counter 计时与异常日志,且异常会 re-raise。
  5. 配套jit_warmup_triton_helper.py 新增 triton_scalar_specialization_rep;新增 docs/contributing/jit_kernel_warmup.md(约 400 行契约与搜索空间参考);测试覆盖注册表去重、**kwargs 转发、内置函数解析、下标表达式、标量特化代表值及迁移前后 dispatch 一致性。
文件 模块 状态 重要度
vllm/model_executor/warmup/jit_warmup.py 预热框架 modified 9.02
vllm/v1/worker/block_table.py 块表预热 modified 8.71
vllm/model_executor/warmup/kernel_warmup.py 启动编排 modified 6.99
vllm/v1/worker/gpu_model_runner.py 模型加载 modified 7.18
vllm/model_executor/warmup/jit_warmup_triton_helper.py 特化辅助 modified 7.24
vllm/model_executor/warmup/v1_block_table_warmup.py 旧预热路径 removed 7.39
docs/contributing/jit_kernel_warmup.md 贡献文档 added 6.52
tests/model_executor/test_jit_warmup.py 预热测试 modified 7.85
tests/v1/worker/test_jit_warmup_migration.py 迁移验证 added 6.61
vllm/v1/worker/gpu/model_runner.py 模型加载 modified 5.27
vllm/v1/worker/cpu_model_runner.py 模型加载 modified 4.9
docs/contributing/README.md 贡献文档 modified 1.58

关键符号

JitWarmupRegistry.activate JitWarmupRegistry.register_warmup JitWarmupRegistry.warmup VllmJitKernel._get_or_compile VllmJitKernel.compile_key _CompileKeyDispatchTrace.input_names_for _CompileKeyDispatchTrace.compile_key _DispatchExprEvaluator.visit_Subscript get_function_source_node triton_scalar_specialization_rep ComputeSlotMappingKernel.dispatch ComputeSlotMappingKernel.get_warmup_keys ComputeSlotMappingKernel.compile kernel_warmup

关键源码片段

vllm/model_executor/warmup/jit_warmup.py data-contract

核心契约文件:新增 JitWarmupRegistry、扩展 AST 追踪(builtins/Subscript/in 比较 / 命名参数转发)、VllmJitKernel 增加 _get_or_compile 运行时缓存,是所有预热接入方必须遵守的地基。

# vllm/model_executor/warmup/jit_warmup.py
# _CompileKeyDispatchTrace.compile_key:在静态追踪 dispatch 时,
# 允许把 dispatch 自身 **kwargs 里未被声明的命名参数 1:1 转发给 CompileKey 字段。
def compile_key(
    self,
    compile_key_type: type[CompileKeyT],
    kwargs: Mapping[str, Any],
) -> CompileKeyT:
    dispatch_values = _eval_local_exprs(
        self.local_exprs, {**self.defaults, **kwargs}, self.globals
    )
    named_parameters = self.named_parameters
    # 先物化直接转发的字段(不在命名参数集合里的 kwargs 直通 CompileKey)。
    fields: dict[str, Any] = {}
    if named_parameters is not None:
        fields = {
            name: value
            for name, value in kwargs.items()
            if name not in named_parameters
        }
    # 命名参数则按 AST 表达式求值(可参与 range 展开、特化映射等变换)。
    for field, expr in self.field_exprs:
        if field in fields:
            raise TypeError(f"CompileKey field '{field}' is specified twice")
        fields[field] = _eval_dispatch_expr(expr, dispatch_values, self.globals)
    return compile_key_type(**fields)
​
​
# VllmJitKernel 运行时入口:cache miss 时先编译,再检查是否真的写入了缓存,
# 否则抛 RuntimeError,把 " 运行时隐性 JIT 编译 " 变成可被 jit-monitor 捕获的显式错误。
def _get_or_compile(
    self,
    compile_key: CompileKeyT,
    *,
    runtime_context: Mapping[str, Any] | None = None,
) -> Any:
    if compile_key not in self._compiled_cache:
        self.compile(compile_key)
    try:
        return self._compiled_cache[compile_key]
    except KeyError as exc:
        details = [f"compile_key={compile_key!r}"]
        if runtime_context:
            details.append(f"runtime_context={dict(runtime_context)!r}")
        raise RuntimeError(
            f"{type(self).__name__}.compile(...) did not cache its JIT "
            f"executor ({', '.join(details)})"
        ) from exc
vllm/v1/worker/block_table.py core-logic

迁移主体:裸 Triton 内核 _compute_slot_mapping_kernel 封装为 ComputeSlotMappingKernel,BlockTable 构造时注册实际特化参数,是本次第一个真正接入新契约的运行时内核。

# vllm/v1/worker/block_table.py
# 迁移后的 slot-mapping 内核:从裸 Triton 函数变成 VllmJitKernel 子类,
# 统一暴露 CompileKey / dispatch / compile 契约,供注册表在启动期预热。
class ComputeSlotMappingKernel(VllmJitKernel["ComputeSlotMappingKernel.CompileKey"]):
    triton_block_size = 1024
​
    @dataclass(frozen=True)
    class CompileKey:
        # 只有真正参与 Triton 特化的标量才进 key;
        # num_tokens / max_num_tokens 已在 do_not_specialize 中排除。
        kv_cache_block_size: int
        blocks_per_kv_block: int
        total_cp_world_size: int
        total_cp_rank: int
        cp_kv_cache_interleave_size: int
        block_table_stride: int
        block_size: int
​
    def dispatch( # type: ignore[override]
        self,
        *,
        block_table_stride: int,
        block_size: int,
        **compile_key_fields: int,
    ) -> CompileKey:
        # 直接字段走 **compile_key_fields 转发;stride / block_size 则映射成
        # Triton 特化类的代表值(1、16 的倍数或 generic 代表)。
        return self.CompileKey(
            **compile_key_fields,
            block_table_stride=triton_scalar_specialization_rep(block_table_stride),
            block_size=triton_scalar_specialization_rep(block_size),
        )
​
    def get_warmup_keys(self, **dispatch_kwargs: int) -> list[CompileKey]:
        # 复用 dispatch 的 AST 追踪,保证预热 key 与运行期 dispatch 完全一致。
        return self._trace_dispatch(self.dispatch)(**dispatch_kwargs)
​
    def compile(self, compile_key: CompileKey) -> None:
        # compile-only 预热:用伪张量描述符触发 Triton 编译,不分配真实 buffer。
        warmup = getattr(self.kernel, "warmup", None)
        assert warmup is not None
        int32_ptr = TritonWarmupTensor(torch.int32)
        int64_ptr = TritonWarmupTensor(torch.int64)
        warmup(
            2, # 任意值即可,num_tokens / max_num_tokens 在 do_not_specialize 中
            2,
            int32_ptr, int64_ptr, int32_ptr,
            compile_key.block_table_stride,
            compile_key.block_size,
            int64_ptr,
            KV_CACHE_BLOCK_SIZE=compile_key.kv_cache_block_size,
            BLOCKS_PER_KV_BLOCK=compile_key.blocks_per_kv_block,
            TOTAL_CP_WORLD_SIZE=compile_key.total_cp_world_size,
            TOTAL_CP_RANK=compile_key.total_cp_rank,
            CP_KV_CACHE_INTERLEAVE_SIZE=compile_key.cp_kv_cache_interleave_size,
            PAD_ID=PAD_SLOT_ID,
            BLOCK_SIZE=self.triton_block_size,
            grid=(2,),
        )
vllm/v1/worker/gpu_model_runner.py entrypoint

接线点:GPUModelRunner 持有 JitWarmupRegistry,并在两处 InputBatch 构建(占位与真实 KV-cache 几何)外包裹 activate() 作用域收集注册。

# vllm/v1/worker/gpu_model_runner.py
# 两处 InputBatch 构建都包在 registry 作用域内:模型 / KV-cache 组件
# 在激活期间调用 register_warmup() 只记录元数据,不触发编译。
self.jit_warmup_registry = JitWarmupRegistry(vllm_config)# 占位 InputBatch(模型加载前、KV-cache 几何未知时)
# Capture warmup providers registered by the initial placeholder InputBatch
with self.jit_warmup_registry.activate():
    self.input_batch = InputBatch(
        max_num_reqs=self.max_num_reqs,
        max_model_len=max(self.max_model_len, self.max_encoder_len),
        max_num_batched_tokens=self.max_num_tokens,
        device=self.device,
        vocab_size=self.model_config.get_vocab_size(),
        block_sizes=[placeholder_block_size],
        kernel_block_sizes=[placeholder_block_size],
        max_num_blocks_per_req=[placeholder_max_num_blocks],
        # ... 其余参数与迁移前保持一致
    )# may_reinitialize_input_batch() 中真实 KV-cache 几何确定后再次捕获注册。
with self.jit_warmup_registry.activate():
    self.input_batch = InputBatch(
        # ... 携带最终 block_sizes / slot_mapping_modes 等真实参数
    )

评论区精华

InputBatch.__init__ 期间有哪些 JIT kernel 被调用 question

LucasWilkinson 质疑为何 GPUModelRunner 构造 InputBatch 时要包 activate();LopezCastroRoberto 澄清当前只有 ComputeSlotMappingKernel 被注册,activate() 是为后续 50175-50178 的模型级注册预留。

结论:Lucas 曾建议先跳过,最终保留;代码注释明确标注两处 activate() 的作用与归属。 · 已解决

dispatch 样板代码可否用 **kwargs 消减 设计

Lucas 担心显式字段太多降低采用意愿;Roberto 解释 _trace_dispatch 是静态解析,kwargs 绑定需由借口的 named_parameters 机制模拟,并新增 **compile_key_fields 1:1 转发支持,重复 / 未知字段仍会报错。

结论:通过 named_parameters 实现直接转发,既保持静态可见性又减少样板;附 ForwardingKernel 测试验证。 · 已解决

get_warmup_keys 是否统一接收 vllm_config 设计

Lucas 建议以 vllm_config 为标准签名减少样板;Roberto 指出 block-table 内核依赖最终化状态(stride 等),传 BlockTable 会让 registry 保留过期 placeholder buffers,快照标量更安全。

结论:维持注册时快照标量的方案;多数内核后续只需 KERNEL.register_warmup() 无参调用。 · 已解决

triton_scalar_specialization_rep 的魔法值来源 documentation

Lucas 请求澄清 1/16/2^31±1 等魔法值;Roberto 引用 Triton specialize.cc handle_long_type() 实现,说明 i32/i64/u64 类型边界与 constant-1、divisible-16、generic 三类缓存键的对应关系。

结论:docstring 补充 Triton 缓存键语义说明,测试覆盖全部边界。 · 已解决

文档中 "owner" 术语语义不清 documentation

Lucas 询问 owner 指什么;Roberto 将其统一改名为 kernel wrapper,并新增定义句。

结论:术语改为 kernel wrapper,文档已更新。 · 已解决

traced dispatch 中 statement 级 if 的限制 documentation

Lucas 要求说明 statement-level if 为何不支持;Roberto 补充条件表达式(IfExp)受支持、helper 函数内可自由使用 if,并给出 FA4 MLA 与 Inkling 代码示例。

结论:文档澄清 AST 追踪边界,并说明可借助 helper 函数绕过。 · 已解决

文档 AI 换行与 debug_probe 示例名 style

Lucas 提出文档存在 AI 风格换行问题,并质疑 debug_probe 是否特殊约定名称;Roberto 修复换行,将该示例改名为 unused_input 并注明无特殊含义,最终直接删除该段落。

结论:文档清理完成,整体篇幅也按 Lucas 建议精简。 · 已解决

风险与影响

  1. 启动路径行为变化:kernel_warmup()enable_jit_warmup 开启时执行 registry.warmup(),任一带注册内核 compile() 抛错会直接中断启动(fail-fast 是设计意图),但可能把过去"预热失败仍可运行"的场景变成启动失败,需在 CI 覆盖无对应后端库的环境。
  2. 块表内核迁移风险:compute_slot_mapping() 的调用方式从裸 Triton launch(显式传 PAD_ID、BLOCK_SIZE 等 constexpr)改为 wrapper 调用,常量固化在 ComputeSlotMappingKernel 内部;任何依赖旧 kwargs 形态的外部调用方会被破坏。block_table_stride 依赖实际分配,占位注册与真实注册会产生不同 key,靠 registry 去重保障。
  3. AST 追踪契约收紧:_trace_dispatch 拒绝语句级 if、非自身 **kwargs 展开;存量 dispatch 写法若不合规会在启动期抛 ValueError,需在迁移 PR 中逐一适配。
  4. _get_or_compile 缓存契约:compile() 若不写入 _compiled_cache,运行时 cache miss 直接抛 RuntimeError,所有后续 VllmJitKernel 子类都必须遵守。
  5. 平台覆盖:test_jit_warmup_migration.py 明确 skip 非 CUDA 环境,CPU/ROCm 的标量特化边界未在本 PR 验证;cpu_model_runner.py 只挂 registry 未接 warmup,存在遗漏风险。

对运行时/用户:启动阶段新增统一的 "JIT kernel warmup starting/finished" 日志与计时,为消除首请求 JIT 编译卡顿铺路;除 block-table 内核外,现有模型运行路径对用户基本透明。对系统:所有 v1 worker(GPU/CPU runner)构造时新增 registry 实例,kernel_warmup() 成为唯一启动编译编排入口,旧的手工预热路径被删除。对团队:确立 VllmJitKernel 贡献者契约,后续 DSv4 de-JITification(#50175-50178)与 Triton/TileLang/CuTeDSL 覆盖工作都依赖此基座,是 "Zero JIT compilation during runtime" 里程碑的分水岭。

核心启动路径变更 新建数据契约 涉及多 worker 路径 后续 PR 依赖此基础 AST 追踪契约约束 启动失败语义收紧

关联 Issue

#49349 Zero JIT compilation during runtime

完整报告

参与讨论