跳到内容

如何调试 vLLM-torch.compile 集成

简要说明

  • 使用 tlparse 获取 torch.compile 日志。请在提交错误报告或寻求支持时附上这些日志。
  • vLLM-torch.compile 集成由多个部分组成。vLLM 提供了关闭每个部分的标志:
在线标志 离线标志 结果
--enforce-eager enforce_eager=True 关闭 torch.compile 和 CUDAGraphs
-cc.mode=0 mode=CompilationMode.NONE 仅关闭 torch.compile
-cc.cudagraph_mode=NONE compilation_config=CompilationConfig(cudagraph_mode=CUDAGraphMode.NONE) 仅关闭 CUDAGraphs
-cc.backend=eager compilation_config=CompilationConfig(backend='eager') 关闭 TorchInductor

vLLM-torch.compile 概述

为了提高性能,vLLM 利用 torch.compile 和 CUDAGraphs 进行加速。torch.compile 为 PyTorch 代码生成优化内核,而 CUDAGraphs 则消除了开销。最显著的一点是,vLLM-compile 并不是简单的 torch.compile,它是使用 PyTorch 内部编译 API 构建的自定义编译器。

vLLM-compile diagram

  • 对于给定的模型,我们通过 TorchDynamo 执行全图捕获,该捕获对批处理大小(token 数量)是动态的。
  • 随后,vLLM 可选择性地拆分和/或专门化该图,并使用 TorchInductor 将每个图编译为已编译的制品(artifact)。此步骤可能使用 vLLM 自定义的 Inductor 传递来进一步优化图。
  • 已编译的制品会被保存到 vLLM 的编译缓存中,以便将来加载。
  • vLLM 应用 CUDAGraphs 以减少 CPU 开销。

以上四个步骤中的任何一步都可能出错。当出现问题时,请尝试隔离出故障的子系统——这将允许您在保持可靠性目标的同时,关闭尽可能少的部分,从而最大限度地减少对性能的影响。同时,这也将有助于我们在您提交错误报告时进行排查。

有关设计的更多详细信息,请参阅以下资源:

使用 tlparse

使用 tlparse 查看 torch.compile 日志。这些日志显示了编译过程的所有阶段,包括 torch.compile 生成的融合内核(fused kernels)。

安装 tlparse

pip install tlparse

要启用 torch.compile 日志,可以设置环境变量 TORCH_TRACE=<dir>。在跟踪期间,该目录下会为每个 rank 创建一个文件,每个文件都包含编译期间的制品。如果可以,建议在提交错误报告时附上这些日志文件——它们非常有帮助。

用法(离线推理)

TORCH_TRACE=~/trace_dir python my_script.py
tlparse ~/trace_dir/<rank_0_log_file>

用法(服务)

TORCH_TRACE=~/trace_dir vllm serve
# ctrl-c out of the server
tlparse ~/trace_dir/<rank_0_log_file>

给定其中一个日志文件,tlparse 命令会输出一些 HTML 文件(例如放入 ./tl_out/index.html)。打开它即可查看日志,效果类似于以下内容。

tlparse example

关闭 vLLM-torch.compile 集成

传入 --enforce-eager 以关闭 vLLM-torch.compile 集成,并完全以 eager 模式运行。这包括关闭 CUDAGraphs。

# Online
vllm serve --enforce-eager
# Offline
LLM(model, enforce_eager=True)

若仅需关闭 torch.compile,请在编译配置中传入 mode = NONE。(-cc--compilation_config 的缩写)

# Online
vllm serve -cc.mode=0
# Offline
from vllm.config.compilation import CompilationConfig, CompilationMode
LLM(model, compilation_config=CompilationConfig(mode=CompilationMode.NONE))

若仅需关闭 CUDAGraphs,请传入 cudagraph_mode = NONE

# Online
vllm serve -cc.cudagraph_mode=NONE
# Offline
from vllm.config.compilation import CompilationConfig, CUDAGraphMode
LLM(model, compilation_config=CompilationConfig(cudagraph_mode=CUDAGraphMode.NONE))

调试 TorchDynamo

vLLM 要求模型代码能够通过 TorchDynamo(torch.compile 的前端)捕获为全图。TorchDynamo 并不支持所有的 Python 特性。如果无法支持某个特性(有时称为图中断,即 graph break),它会报错(在 fullgraph 模式下)。

如果您遇到图中断,请在 pytorch/pytorch 中提交议题,以便 PyTorch 开发人员可以优先处理。然后,请尽最大努力重写代码以避免图中断。有关详细信息,请参阅此 Dynamo 指南

调试动态形状全图捕获

vLLM 要求模型的向前传播能够捕获为对批处理大小(即 token 数量)动态的全图。默认情况下,它会将此图编译为一个制品,并将其用于所有批处理大小。

如果您的代码无法通过动态形状捕获,您可能会看到静默的不正确结果、明显的错误或 CUDA 非法内存访问。例如,以下代码无法捕获为单个图:

if data.size[0] % 128 == 0:
    foo(...)
else:
    bar(...)

这个问题很容易诊断。使用 tlparse 并点击 compilation_metrics:它会告诉您关于批处理大小的符号约束。如果存在任何限制批处理大小的约束,那么就说明有问题。

Bad tlparse example

要避免这种情况,请采取以下任一方式:

  1. 避免根据 token 数量进行分支判断
  2. 将分支逻辑包装在自定义算子中。TorchDynamo 不会追踪进入自定义算子。

调试约束冲突与动态形状守卫问题

动态形状守卫(guards)是 Dynamo 守卫的一个特定类别。它们是 torch.compile 附加到动态维度(如 seq_len)上的约束,以确保已编译的制品保持有效。这些守卫通常出现在框架代码、自定义传递或用户代码根据动态形状值进行分支时。

示例

if x > 10:
    # path A
else:
    # path B

这会创建一个守卫 x > 10x <= 10,具体取决于所追踪的路径。

vLLM 的假设:vLLM 假设 torch.compile 添加的所有守卫都是可以安全删除的,且不会将编译后的图限制为特定的输入形状。当此假设被违背时,会导致用户需要调试的问题。表明此假设被违背的副作用包括运行时错误或 ConstraintViolationErrors

如果动态形状被限制为单个值,则会抛出 ConstraintViolationErrors。如果您遇到约束冲突错误或怀疑动态形状守卫被错误添加,可以使用更严格的动态形状模式来帮助调试:

# Online - using unbacked mode
vllm serve meta-llama/Llama-3.2-1B -cc.dynamic_shapes_config.type=unbacked

# Online - using backed_size_oblivious mode
vllm serve meta-llama/Llama-3.2-1B -cc.dynamic_shapes_config.type=backed_size_oblivious
# Offline - using unbacked mode
from vllm.config.compilation import CompilationConfig, DynamicShapesConfig, DynamicShapesType
LLM(model, compilation_config=CompilationConfig(
    dynamic_shapes_config=DynamicShapesConfig(type=DynamicShapesType.UNBACKED)
))

# Offline - using backed_size_oblivious mode
from vllm.config.compilation import CompilationConfig, DynamicShapesConfig, DynamicShapesType
LLM(model, compilation_config=CompilationConfig(
    dynamic_shapes_config=DynamicShapesConfig(type=DynamicShapesType.BACKED_SIZE_OBLIVIOUS)
))

这些模式更严格,减少或消除了对动态形状守卫的需求,从而有助于隔离问题。

  • unbacked:使用不支持守卫的 unbacked symints,更容易识别守卫被错误添加的位置。
  • backed_size_oblivious:使用一种对守卫更加严格的模式。

有关动态形状模式的更多详细信息,请参阅 动态形状与 vLLM 守卫删除

打印守卫

要查看编译期间添加的所有守卫,可以使用 TORCH_LOGS=+dynamic

TORCH_LOGS=+dynamic vllm serve meta-llama/Llama-3.2-1B

在日志中查找 [guard added] 以了解守卫被添加的位置。这可以帮助您识别哪些操作导致了守卫被错误地添加。

调试 TorchInductor

TorchInductor 获取捕获的图并将其编译为可能调用一个或多个 Triton 内核的 Python 代码。在极少数(但令人遗憾的)情况下,它可能会产生不正确的 Triton 内核。这可能表现为静默的不正确结果、CUDA 非法内存访问或明显的错误。

Inductor 运行时断言

默认情况下(在 torch < 2.12 时),vLLM 会禁用 Inductor 的运行时断言(assert_size_stride, assert_alignment),以避免在大型模型上每次前向传播约 2ms 的开销。设置 VLLM_LOGGING_LEVEL=DEBUG 会自动重新启用它们,以便调试会话获得完整的形状/步长(shape/stride)验证。

VLLM_LOGGING_LEVEL=DEBUG vllm serve <model>

您也可以通过 --compilation-config 显式覆盖它们。

vllm serve <model> -cc.inductor_compile_config='{"size_asserts": true, "alignment_asserts": true, "scalar_asserts": true}'

在 torch >= 2.12 版本中,PyTorch 使用了高效的“仅断言一次”策略,这些标志不再被 vLLM 抑制。

要调试是否是 TorchInductor 导致的问题,可以通过在编译配置中传入 backend='eager' 来禁用它。

# online
vllm serve -cc.backend=eager
# offline
LLM(compilation_config=CompilationConfig(backend='eager'))

如果是 Inductor 的问题,请向 PyTorch 提交错误报告。如果您喜欢探索,可以调试 Inductor 输出代码中的 Triton 内核(可以通过 tlparse 定位这些代码)。

tlparse example

您也可以使用 TORCH_LOGS=output_code <command> 来打印 Inductor 输出代码。

可编辑的 TorchInductor 代码

通过设置 VLLM_COMPILE_CACHE_SAVE_FORMAT=unpacked 或传入 -cc.compile_cache_save_format=unpacked,可以编辑运行的 TorchInductor 代码。默认值为 binary,这意味着它不可编辑。

这是一个非常有用的技巧:您可以在输出代码中设置断点(例如 torch.distributed.breakpoint())和添加打印语句。

调试 vLLM-compile 缓存

vLLM 为 torch.compile 制品构建了自己的缓存。其思想是制品可以编译一次,然后在后续重用。这是在 torch.compile 的编译器缓存 之上的一个层级。

虽然 torch.compile 的编译器缓存非常稳定,但 vLLM 的编译器缓存有时并不准确。您可以通过设置 VLLM_DISABLE_COMPILE_CACHE=1 来禁用它。

您也可以手动删除此缓存。

  • 使用 rm -rf ~/.cache/vllm 删除 vLLM 的编译缓存(请查看日志确认位置是否更改)。
  • 使用 rm -rf /tmp/torchinductor_$(whoami) 删除 torch.compile 的内置缓存。

vLLM 的缓存是缓存键到编译制品的映射。vLLM 通过组合多个因素(例如配置标志和模型名称)来计算缓存键。如果 vLLM 的编译缓存出错,通常意味着缺少了某个因素。请参阅此示例,了解 vLLM 如何计算缓存键的一部分。

vLLM 的编译缓存要求被编译的代码最终是可序列化的。如果不是,则会在保存时报错。通常解决方法是:

  • 重写不可序列化的部分(这可能比较困难,因为目前很难判断哪些是可序列化的,哪些不是)。
  • 提交错误报告。
  • 通过设置 VLLM_DISABLE_COMPILE_CACHE=1 来忽略错误(注意,这会使服务器冷启动变慢)。

调试 CUDAGraphs

CUDAGraphs 是一项功能,允许:

  • 将启动一个或多个 CUDA 内核的可调用对象捕获到 CUDAGraph 中。
  • 重放 CUDAGraph。

被捕获的 CUDAGraph 包含了捕获过程中使用的所有内存。重放 CUDAGraph 时,会读写完全相同的内存区域。

这导致了一些限制:

  1. 为了在新数据上使用 CUDAGraphs,您需要将数据复制到 CUDAGraph 正在读取的缓冲区中。
  2. CUDAGraphs 仅捕获 CUDA 内核,不捕获 CPU 上执行的工作。

vLLM 使用原始 CUDAGraphs API,如果使用不当,是不安全的。

若仅需关闭 CUDAGraphs,请传入 cudagraph_mode = NONE

# Online
vllm serve -cc.cudagraph_mode=NONE
# Offline
from vllm.config.compilation import CompilationConfig, CUDAGraphMode
LLM(model, compilation_config=CompilationConfig(cudagraph_mode=CUDAGraphMode.NONE))