Prhub

#38657 [compile] Invoke split FX graph by codegen.

vllm-project/vllm · 作者 zhxchen17 · 合并时间 2026-04-16 12:03

分析状态 已生成
文件变更 3提交数 1 · 评论 12
代码增减 +206 / -9
performance compilation v1 core

执行摘要

通过代码生成替代 FX 图执行,减少推理循环的运行时开销。

关联Issue #177655指出standalone_compile存在显著的运行时开销,特别是在推理循环中。PR body解释,FX图执行存在两个主要开销源:1) getattr()调用获取子模块;2)子模块调用会推送多层CPython栈帧。通过代码生成直接调用子模块,可以避免这些开销。

建议技术管理者精读此PR,重点关注代码生成器的设计决策和潜在漏洞。对于工程师,值得学习如何通过代码生成优化Python执行路径,但需注意review中提到的未解决问题,并在相关工作中避免类似陷阱。

讨论亮点
  • gemini-code-assist[bot] 指出代码生成漏洞_node_ref_format_output 函数不支持嵌套容器(列表、元组、字典),可能导致语法错误;call_module 节点可能忽略关键字参数;operator.getitem 索引未引用可能导致NameError。作者回应将在后续PR中处理。
  • zou3519 建议代码可检查性:理想情况下,应将生成代码保存到文件,像inductor output_code那样,便于调试。作者同意在后续PR中实现。
  • 测试覆盖:zou3519提议添加类似expecttest的测试,用于简单模型的字符串匹配,但本次PR未实现。
  • 结论:尽管存在未解决的robustness问题,但PR已批准,因为核心性能提升明显,且问题计划在后续修复。

实现拆解

  1. 新增代码生成模块 (vllm/compilation/codegen.py): 实现 generate_execution_code 函数,遍历split_gm的图节点,生成直接调用子模块的Python源码;compile_execution_fn 函数编译源码并绑定子模块可调用对象。关键符号包括 generate_execution_code, compile_execution_fn, _node_ref
  2. 集成到后端调用 (vllm/compilation/backends.py): 在 VllmBackend.__call__ 方法中,调用 generate_execution_code 生成代码,使用 compile_execution_fn 创建运行时可调用对象,替换原有的 self.split_gm 调用。影响推理循环的执行路径。
  3. 扩展序列化支持 (vllm/compilation/caching.py): 修改 VllmSerializableFunction 的构造函数,新增 execution_codesubmod_names 参数;在 reconstruct_serializable_fn_from_mega_artifact 函数中,加载并优先使用代码生成的可调用对象,否则回退到split_gm。确保缓存兼容性和性能提升。
  4. 测试与监控: Review中讨论到需要添加测试(如expecttest)和将生成代码保存到文件以供检查,但这些将在后续PR中处理。目前变更包含tlparse日志记录,用于性能追踪。
文件 模块 状态 重要度
vllm/compilation/codegen.py 编译层 added 8.69
vllm/compilation/backends.py 编译层 modified 6.5
vllm/compilation/caching.py 编译层 modified 6.49
vllm/compilation/codegen.py core-logic

新增的代码生成模块,核心实现 generate_execution_code 和 compile_execution_fn,直接减少 FX 执行开销。

import operator
from collections.abc import Callable
from typing import Any
import torch.fx
from torch._dynamo.utils import dynamo_timed
from torch._logging import trace_structured@dynamo_timed("vllm.generate_execution_code")
def generate_execution_code(
    split_gm: torch.fx.GraphModule,
) -> tuple[str, list[str]]:
    """从split_gm的缝合图生成Python源代码。    遍历split_gm.graph.nodes,生成通过__vllm_submods__列表调用子模块的函数,
    避免FX GraphModule开销和字典查找成本。
    """
    lines: list[str] = []
    param_names: list[str] = []
    submod_names: list[str] = []
    submod_index: dict[str, int] = {}
​
    # 构建节点排序用于活跃性分析。
    nodes = list(split_gm.graph.nodes)
    node_order = {node: i for i, node in enumerate(nodes)}
​
    # 为每个产生值的节点,找到其最后一个消费者的位置。
    # 如果最后一个消费者是输出节点,则跳过(返回句柄清理)。
    # 否则,在该消费者后安排del以早期释放内存。
    del_after: dict[int, list[str]] = {} # 位置 -> 要删除的名称
    for node in nodes:
        if node.op == "output":
            continue
        users = list(node.users.keys())
        if not users:
            continue
        last_user = max(users, key=lambda u: node_order[u]) # 找到最后使用的节点
        if last_user.op == "output":
            continue
        del_after.setdefault(node_order[last_user], []).append(node.name)
​
    for i, node in enumerate(nodes):
        if node.op == "placeholder":
            param_names.append(node.name) # 收集参数名
        elif node.op == "call_module":
            target = node.target
            if target not in submod_index:
                submod_index[target] = len(submod_names)
                submod_names.append(target) # 记录子模块名
            idx = submod_index[target]
            args_str = ", ".join(_node_ref(a) for a in node.args) # 处理位置参数
            kwargs_str = ", ".join(
                f"{k}={_node_ref(v)}" for k, v in node.kwargs.items()
            ) # 处理关键字参数
            all_args = ", ".join(filter(None, [args_str, kwargs_str]))
            lines.append(f"    {node.name} = __vllm_submods__[{idx}]({all_args})")
        elif node.op == "call_function" and node.target is operator.getitem:
            source = _node_ref(node.args[0]) # 获取源节点引用
            index = node.args[1]
            assert isinstance(index, int) # 假设索引为整数
            lines.append(f"    {node.name} = {source}[{index}]")
        elif node.op == "output":
            assert len(node.args) == 1
            ret = _node_ref(node.args[0]) # 处理返回值
            lines.append(f"    return {ret}")
        else:
            raise RuntimeError(f"Unsupported node from codegen: {node.format_node()}")
​
        # 在变量的最后使用后发出del语句。
        if i in del_after:
            names = sorted(del_after[i])
            lines.append(f"    del {', '.join(names)}")
​
    assert len(param_names) > 0
    params = ", ".join(param_names)
    header = f"def execution_fn({params}, *, __vllm_submods__):"
    return "import torch\n" + "\n".join([header] + lines) + "\n", submod_names

关键符号

generate_execution_code compile_execution_fn _node_ref

评论区精华

代码生成器的 robustness 问题 正确性

gemini-code-assist[bot] 指出 _node_ref 不支持嵌套容器,call_module 忽略 kwargs,可能导致语法错误和运行时失败。

结论:作者承认问题,计划在后续 PR 中修复。 · unresolved

生成代码的可检查性 设计

zou3519 建议将生成代码保存到文件,像 inductor output_code 那样,便于调试和检查。

结论:作者同意在后续 PR 中实现。 · pending

测试覆盖 测试

zou3519 提议添加类似 expecttest 的测试,用于验证代码生成正确性。

结论:未在本次 PR 中实现,可能作为后续工作。 · unresolved

风险与影响

  • 正确性风险:代码生成器当前不支持嵌套容器和字典返回值,可能导致运行时错误或数据损坏。
  • 兼容性风险:新增的 execution_codesubmod_names 序列化字段可能影响现有缓存加载,需确保向后兼容。
  • 维护风险:引入自定义代码生成逻辑增加了编译层的复杂性,未来维护和调试可能更困难。
  • 安全风险:使用 exec() 执行生成代码,虽然上下文受控,但仍需注意代码注入风险。
  • 性能影响:基准测试显示子模块调用间隙从20微秒降至3微秒,对高吞吐推理场景有显著提升。
  • 用户影响:透明优化,无需用户干预,但缓存磁盘使用略有增加。
  • 系统影响:降低了推理循环的CPU开销,可能提升整体系统效率;所有编译路径均受益。
  • 团队影响:工程师需熟悉新的代码生成机制,并在后续迭代中完善测试和robustness。
代码生成漏洞 缺少测试覆盖 序列化兼容性

关联 Issue

#177655 Significant runtime overhead for standalone_compile

完整报告

执行摘要

  • 一句话:通过代码生成替代FX图执行,减少推理循环的运行时开销。
  • 推荐动作:建议技术管理者精读此PR,重点关注代码生成器的设计决策和潜在漏洞。对于工程师,值得学习如何通过代码生成优化Python执行路径,但需注意review中提到的未解决问题,并在相关工作中避免类似陷阱。

功能与动机

关联Issue #177655指出standalone_compile存在显著的运行时开销,特别是在推理循环中。PR body解释,FX图执行存在两个主要开销源:1) getattr()调用获取子模块;2)子模块调用会推送多层CPython栈帧。通过代码生成直接调用子模块,可以避免这些开销。

实现拆解

  1. 新增代码生成模块 (vllm/compilation/codegen.py): 实现 generate_execution_code 函数,遍历split_gm的图节点,生成直接调用子模块的Python源码;compile_execution_fn 函数编译源码并绑定子模块可调用对象。关键符号包括 generate_execution_code, compile_execution_fn, _node_ref
  2. 集成到后端调用 (vllm/compilation/backends.py): 在 VllmBackend.__call__ 方法中,调用 generate_execution_code 生成代码,使用 compile_execution_fn 创建运行时可调用对象,替换原有的 self.split_gm 调用。影响推理循环的执行路径。
  3. 扩展序列化支持 (vllm/compilation/caching.py): 修改 VllmSerializableFunction 的构造函数,新增 execution_codesubmod_names 参数;在 reconstruct_serializable_fn_from_mega_artifact 函数中,加载并优先使用代码生成的可调用对象,否则回退到split_gm。确保缓存兼容性和性能提升。
  4. 测试与监控: Review中讨论到需要添加测试(如expecttest)和将生成代码保存到文件以供检查,但这些将在后续PR中处理。目前变更包含tlparse日志记录,用于性能追踪。

关键文件:

  • vllm/compilation/codegen.py(模块 编译层;类别 source;类型 core-logic;符号 generate_execution_code, compile_execution_fn, _node_ref): 新增的代码生成模块,核心实现generate_execution_code和compile_execution_fn,直接减少FX执行开销。
  • vllm/compilation/backends.py(模块 编译层;类别 source;类型 integration): 修改VllmBackend的__call__方法,集成代码生成,替换原有split_gm调用,直接影响推理执行路径。
  • vllm/compilation/caching.py(模块 编译层;类别 source;类型 serialization): 扩展VllmSerializableFunction以支持execution_code和submod_names的序列化,确保缓存兼容性和性能提升。

关键符号:generate_execution_code, compile_execution_fn, _node_ref

关键源码片段

vllm/compilation/codegen.py

新增的代码生成模块,核心实现generate_execution_code和compile_execution_fn,直接减少FX执行开销。

import operator
from collections.abc import Callable
from typing import Any
import torch.fx
from torch._dynamo.utils import dynamo_timed
from torch._logging import trace_structured@dynamo_timed("vllm.generate_execution_code")
def generate_execution_code(
    split_gm: torch.fx.GraphModule,
) -> tuple[str, list[str]]:
    """从split_gm的缝合图生成Python源代码。    遍历split_gm.graph.nodes,生成通过__vllm_submods__列表调用子模块的函数,
    避免FX GraphModule开销和字典查找成本。
    """
    lines: list[str] = []
    param_names: list[str] = []
    submod_names: list[str] = []
    submod_index: dict[str, int] = {}
​
    # 构建节点排序用于活跃性分析。
    nodes = list(split_gm.graph.nodes)
    node_order = {node: i for i, node in enumerate(nodes)}
​
    # 为每个产生值的节点,找到其最后一个消费者的位置。
    # 如果最后一个消费者是输出节点,则跳过(返回句柄清理)。
    # 否则,在该消费者后安排del以早期释放内存。
    del_after: dict[int, list[str]] = {} # 位置 -> 要删除的名称
    for node in nodes:
        if node.op == "output":
            continue
        users = list(node.users.keys())
        if not users:
            continue
        last_user = max(users, key=lambda u: node_order[u]) # 找到最后使用的节点
        if last_user.op == "output":
            continue
        del_after.setdefault(node_order[last_user], []).append(node.name)
​
    for i, node in enumerate(nodes):
        if node.op == "placeholder":
            param_names.append(node.name) # 收集参数名
        elif node.op == "call_module":
            target = node.target
            if target not in submod_index:
                submod_index[target] = len(submod_names)
                submod_names.append(target) # 记录子模块名
            idx = submod_index[target]
            args_str = ", ".join(_node_ref(a) for a in node.args) # 处理位置参数
            kwargs_str = ", ".join(
                f"{k}={_node_ref(v)}" for k, v in node.kwargs.items()
            ) # 处理关键字参数
            all_args = ", ".join(filter(None, [args_str, kwargs_str]))
            lines.append(f"    {node.name} = __vllm_submods__[{idx}]({all_args})")
        elif node.op == "call_function" and node.target is operator.getitem:
            source = _node_ref(node.args[0]) # 获取源节点引用
            index = node.args[1]
            assert isinstance(index, int) # 假设索引为整数
            lines.append(f"    {node.name} = {source}[{index}]")
        elif node.op == "output":
            assert len(node.args) == 1
            ret = _node_ref(node.args[0]) # 处理返回值
            lines.append(f"    return {ret}")
        else:
            raise RuntimeError(f"Unsupported node from codegen: {node.format_node()}")
​
        # 在变量的最后使用后发出del语句。
        if i in del_after:
            names = sorted(del_after[i])
            lines.append(f"    del {', '.join(names)}")
​
    assert len(param_names) > 0
    params = ", ".join(param_names)
    header = f"def execution_fn({params}, *, __vllm_submods__):"
    return "import torch\n" + "\n".join([header] + lines) + "\n", submod_names

评论区精华

  • gemini-code-assist[bot] 指出代码生成漏洞_node_ref_format_output 函数不支持嵌套容器(列表、元组、字典),可能导致语法错误;call_module 节点可能忽略关键字参数;operator.getitem 索引未引用可能导致NameError。作者回应将在后续PR中处理。
  • zou3519 建议代码可检查性:理想情况下,应将生成代码保存到文件,像inductor output_code那样,便于调试。作者同意在后续PR中实现。
  • 测试覆盖:zou3519提议添加类似expecttest的测试,用于简单模型的字符串匹配,但本次PR未实现。
  • 结论:尽管存在未解决的robustness问题,但PR已批准,因为核心性能提升明显,且问题计划在后续修复。

    • 代码生成器的robustness问题 (correctness): 作者承认问题,计划在后续PR中修复。
    • 生成代码的可检查性 (design): 作者同意在后续PR中实现。
    • 测试覆盖 (testing): 未在本次PR中实现,可能作为后续工作。

风险与影响

  • 风险:- 正确性风险:代码生成器当前不支持嵌套容器和字典返回值,可能导致运行时错误或数据损坏。
  • 兼容性风险:新增的 execution_codesubmod_names 序列化字段可能影响现有缓存加载,需确保向后兼容。
  • 维护风险:引入自定义代码生成逻辑增加了编译层的复杂性,未来维护和调试可能更困难。
  • 安全风险:使用 exec() 执行生成代码,虽然上下文受控,但仍需注意代码注入风险。
  • 影响:- 性能影响:基准测试显示子模块调用间隙从20微秒降至3微秒,对高吞吐推理场景有显著提升。
  • 用户影响:透明优化,无需用户干预,但缓存磁盘使用略有增加。
  • 系统影响:降低了推理循环的CPU开销,可能提升整体系统效率;所有编译路径均受益。
  • 团队影响:工程师需熟悉新的代码生成机制,并在后续迭代中完善测试和robustness。
  • 风险标记:代码生成漏洞, 缺少测试覆盖, 序列化兼容性

关联脉络

  • 暂无明显关联 PR

参与讨论