• Docs >
  • Torch-TensorRT (FX Frontend) User Guide
Shortcuts

Torch-TensorRT (FX 前端) 用户指南

Torch-TensorRT (FX 前端) 是一个工具,可以通过 torch.fx 将 PyTorch 模型转换为针对在 Nvidia GPU 上运行的 TensorRT 引擎进行优化的工具。TensorRT 是由 NVIDIA 开发的推理引擎,由各种优化组成,包括内核融合、图优化、低精度等。该工具在 Python 环境中开发,使得研究人员和工程师可以非常方便地使用此工作流程。用户使用此工具需要经历几个阶段,我们将在这里介绍它们。

> Torch-TensorRT (FX 前端) 处于 Beta 阶段,目前建议与 PyTorch nightly 版本一起使用。

# Test an example by
$ python py/torch_tensorrt/fx/example/lower_example.py

将PyTorch模型转换为TensorRT引擎

一般来说,欢迎用户使用compile()来完成从模型到tensorRT引擎的转换。这是一个封装API,包含了完成此转换所需的主要步骤。请参考examples/fx下的lower_example.py文件中的示例用法。

def compile(
    module: nn.Module,
    input,
    max_batch_size=2048,
    max_workspace_size=33554432,
    explicit_batch_dimension=False,
    lower_precision=LowerPrecision.FP16,
    verbose_log=False,
    timing_cache_prefix="",
    save_timing_cache=False,
    cuda_graph_batch_size=-1,
    dynamic_batch=True,
) -> nn.Module:

    """
    Takes in original module, input and lowering setting, run lowering workflow to turn module
    into lowered module, or so called TRTModule.

    Args:
        module: Original module for lowering.
        input: Input for module.
        max_batch_size: Maximum batch size (must be >= 1 to be set, 0 means not set)
        max_workspace_size: Maximum size of workspace given to TensorRT.
        explicit_batch_dimension: Use explicit batch dimension in TensorRT if set True, otherwise use implicit batch dimension.
        lower_precision: lower_precision config given to TRTModule.
        verbose_log: Enable verbose log for TensorRT if set True.
        timing_cache_prefix: Timing cache file name for timing cache used by fx2trt.
        save_timing_cache: Update timing cache with current timing cache data if set to True.
        cuda_graph_batch_size: Cuda graph batch size, default to be -1.
        dynamic_batch: batch dimension (dim=0) is dynamic.
    Returns:
        A torch.nn.Module lowered by TensorRT.
    """

在本节中,我们将通过一个示例来说明fx路径使用的主要步骤。 用户可以参考examples/fx中的fx2trt_example.py文件。

  • 步骤1:使用acc_tracer跟踪模型

Acc_tracer 是一个继承自 FX tracer 的追踪器。它带有参数标准化器,可以将所有参数转换为 kwargs 并传递给 TRT 转换器。

import torch_tensorrt.fx.tracer.acc_tracer.acc_tracer as acc_tracer

# Build the model which needs to be a PyTorch nn.Module.
my_pytorch_model = build_model()

# Prepare inputs to the model. Inputs have to be a List of Tensors
inputs = [Tensor, Tensor, ...]

# Trace the model with acc_tracer.
acc_mod = acc_tracer.trace(my_pytorch_model, inputs)

常见错误:

符号跟踪变量不能用作控制流的输入 这意味着模型包含动态控制流。请参阅FX指南中的“动态控制流”部分。

  • 步骤2:构建TensorRT引擎

TensorRT处理批次维度有两种不同的模式,显式批次维度和隐式批次维度。这种模式被早期版本的TensorRT使用,现在已被弃用,但为了向后兼容性仍然支持。在显式批次模式下,所有维度都是显式的,并且可以是动态的,即它们的长度可以在执行时改变。许多新功能,如动态形状和循环,仅在此模式下可用。用户仍然可以选择在compile()中设置explicit_batch_dimension=False时使用隐式批次模式。我们不推荐使用它,因为在未来的TensorRT版本中将缺乏支持。

显式批处理是默认模式,必须为动态形状设置。对于大多数视觉任务,如果用户希望获得与隐式模式类似的效果,其中只有批处理维度发生变化,他们可以选择在compile()中启用dynamic_batch。它有一些要求: 1. 输入、输出和激活的形状除了批处理维度外是固定的。 2. 输入、输出和激活的批处理维度是主要维度。 3. 模型中的所有操作符不修改批处理维度(排列、转置、分割等)或在批处理维度上进行计算(求和、softmax等)。

对于最后一个路径的示例,如果我们有一个形状为 (batch, sequence, dimension) 的3D张量 t,操作如 torch.transpose(0, 2)。如果这三个条件中的任何一个不满足,我们需要将 InputTensorSpec 指定为具有动态范围的输入。

import deeplearning.trt.fx2trt.converter.converters
from torch.fx.experimental.fx2trt.fx2trt import InputTensorSpec, TRTInterpreter

# InputTensorSpec is a dataclass we use to store input information.
# There're two ways we can build input_specs.
# Option 1, build it manually.
input_specs = [
  InputTensorSpec(shape=(1, 2, 3), dtype=torch.float32),
  InputTensorSpec(shape=(1, 4, 5), dtype=torch.float32),
]
# Option 2, build it using sample_inputs where user provide a sample
inputs = [
torch.rand((1,2,3), dtype=torch.float32),
torch.rand((1,4,5), dtype=torch.float32),
]
input_specs = InputTensorSpec.from_tensors(inputs)

# IMPORTANT: If dynamic shape is needed, we need to build it slightly differently.
input_specs = [
    InputTensorSpec(
        shape=(-1, 2, 3),
        dtype=torch.float32,
        # Currently we only support one set of dynamic range. User may set other dimensions but it is not promised to work for any models
        # (min_shape, optimize_target_shape, max_shape)
        # For more information refer to fx/input_tensor_spec.py
        shape_ranges = [
            ((1, 2, 3), (4, 2, 3), (100, 2, 3)),
        ],
    ),
    InputTensorSpec(shape=(1, 4, 5), dtype=torch.float32),
]

# Build a TRT interpreter. Set explicit_batch_dimension accordingly.
interpreter = TRTInterpreter(
    acc_mod, input_specs, explicit_batch_dimension=True/False
)

# The output of TRTInterpreter run() is wrapped as TRTInterpreterResult.
# The TRTInterpreterResult contains required parameter to build TRTModule,
# and other informational output from TRTInterpreter run.
class TRTInterpreterResult(NamedTuple):
    engine: Any
    input_names: Sequence[str]
    output_names: Sequence[str]
    serialized_cache: bytearray

#max_batch_size: set accordingly for maximum batch size you will use.
#max_workspace_size: set to the maximum size we can afford for temporary buffer
#lower_precision: the precision model layers are running on (TensorRT will choose the best perforamnce precision).
#sparse_weights: allow the builder to examine weights and use optimized functions when weights have suitable sparsity
#force_fp32_output: force output to be fp32
#strict_type_constraints: Usually we should set it to False unless we want to control the precision of certain layer for numeric #reasons.
#algorithm_selector: set up algorithm selection for certain layer
#timing_cache: enable timing cache for TensorRT
#profiling_verbosity: TensorRT logging level
trt_interpreter_result = interpreter.run(
    max_batch_size=64,
    max_workspace_size=1 << 25,
    sparse_weights=False,
    force_fp32_output=False,
    strict_type_constraints=False,
    algorithm_selector=None,
    timing_cache=None,
    profiling_verbosity=None,
)

常见错误:

运行时错误:当前不支持函数xxx的转换! - 这意味着我们不支持这个xxx操作符。请参考下面的“如何添加缺失的操作”部分以获取进一步的说明。

  • 步骤3:运行模型

一种方法是使用TRTModule,它基本上是一个PyTorch的nn.Module。

from torch_tensorrt.fx import TRTModule
mod = TRTModule(
    trt_interpreter_result.engine,
    trt_interpreter_result.input_names,
    trt_interpreter_result.output_names)
# Just like all other PyTorch modules
outputs = mod(*inputs)
torch.save(mod, "trt.pt")
reload_trt_mod = torch.load("trt.pt")
reload_model_output = reload_trt_mod(*inputs)

到目前为止,我们详细解释了将PyTorch模型转换为TensorRT引擎的主要步骤。欢迎用户参考源代码以获取一些参数的解释。在转换方案中,有两个重要的操作。一个是acc tracer,它帮助我们将PyTorch模型转换为acc图。另一个是FX路径转换器,它帮助将acc图的操作转换为相应的TensorRT操作,并为其构建TensorRT引擎。

Acc Tracer

Acc tracer 是一个自定义的 FX 符号追踪器。与普通的 FX 符号追踪器相比,它做了一些额外的事情。我们主要依赖它将 PyTorch 操作或内置操作转换为 acc 操作。fx2trt 使用 acc 操作有两个主要目的:

  1. 在PyTorch操作和内置操作中有许多执行类似操作的操作,例如torch.add、builtin.add和torch.Tensor.add。使用acc tracer,我们将这三个操作规范化为单个acc_ops.add。这有助于减少我们需要编写的转换器数量。

  2. acc ops 只有 kwargs,这使得编写转换器更容易,因为我们不需要添加额外的逻辑来在 args 和 kwargs 中查找参数。

FX2TRT

在符号追踪之后,我们得到了PyTorch模型的图表示。fx2trt利用了fx.Interpreter的强大功能。fx.Interpreter逐个节点遍历整个图,并调用该节点所代表的函数。fx2trt通过为每个节点调用相应的转换器来覆盖原始的函数调用行为。每个转换器函数都会添加相应的TensorRT层。

下面是一个转换器函数的示例。装饰器用于将此转换器函数注册到相应的节点。在此示例中,我们将此转换器注册到目标为 acc_ops.sigmoid 的 fx 节点。

@tensorrt_converter(acc_ops.sigmoid)
def acc_ops_sigmoid(network, target, args, kwargs, name):
    """
    network: TensorRT network. We'll be adding layers to it.

    The rest arguments are attributes of fx node.
    """
    input_val = kwargs['input']

    if not isinstance(input_val, trt.tensorrt.ITensor):
        raise RuntimeError(f'Sigmoid received input {input_val} that is not part '
                        'of the TensorRT region!')

    layer = network.add_activation(input=input_val, type=trt.ActivationType.SIGMOID)
    layer.name = name
    return layer.get_output(0)

如何添加缺失的操作

你实际上可以将其添加到任何你想要的地方,只需要记住导入文件,以便在使用acc_tracer进行跟踪之前,所有的acc操作和映射器都会被注册。

  • 步骤 1. 添加一个新的 acc op

待办事项:需要更多地解释关于acc操作的逻辑,比如我们何时想要分解一个操作以及何时想要重用其他操作。

acc tracer中,如果图中节点有注册到acc操作的映射,我们将图中的节点转换为acc操作。

为了使转换到acc ops发生,需要两件事。一是应该定义一个acc op函数,二是应该注册一个映射。

定义一个acc操作很简单,我们首先只需要一个函数,并通过这个装饰器acc_normalizer.py将函数注册为acc操作。例如,以下代码添加了一个名为foo()的acc操作,它将两个给定的输入相加。

# NOTE: all acc ops should only take kwargs as inputs, therefore we need the "*"
# at the beginning.
@register_acc_op
def foo(*, input, other, alpha):
    return input + alpha * other

有两种方法可以注册映射。一种是register_acc_op_mapping()。让我们注册一个从torch.add到我们刚刚创建的foo()的映射。我们需要为其添加装饰器register_acc_op_mapping。

this_arg_is_optional = True

@register_acc_op_mapping(
    op_and_target=("call_function", torch.add),
    arg_replacement_tuples=[
        ("input", "input"),
        ("other", "other"),
        ("alpha", "alpha", this_arg_is_optional),
    ],
)
@register_acc_op
def foo(*, input, other, alpha=1.0):
    return input + alpha * other

op_and_target 决定了哪个节点将触发此映射。op 和 target 是 FX 节点的属性。在 acc_normalization 中,当我们看到一个节点的 op 和 target 与 op_and_target 中设置的相同时,我们将触发映射。由于我们希望从 torch.add 映射,那么 op 将是 call_function,target 将是 torch.addarg_replacement_tuples 决定了我们如何使用原始节点的 args 和 kwargs 来构造新的 acc op 节点的 kwargs。arg_replacement_tuples 中的每个元组代表一个参数映射规则。它包含两个或三个元素。第三个元素是一个布尔变量,用于确定此 kwarg 在原始节点中是否是可选的。只有当它为 True 时,我们才需要指定第三个元素。第一个元素是原始节点中的参数名称,它将用作 acc op 节点的参数,其名称是元组中的第二个元素。元组的顺序很重要,因为元组的位置决定了参数在原始节点的 args 中的位置。我们使用此信息将原始节点的 args 映射到 acc op 节点的 kwargs。 如果以下情况都不成立,我们不需要指定 arg_replacement_tuples。

  1. 原始节点和累加操作节点的kwargs具有不同的名称。

  2. 有可选参数。

另一种注册映射的方法是通过register_custom_acc_mapper_fn()。这种方法旨在减少冗余的操作注册,因为它允许你使用一个函数通过一些组合映射到一个或多个现有的acc操作。在函数中,你基本上可以做任何你想做的事情。让我们用一个例子来解释它是如何工作的。

@register_acc_op
def foo(*, input, other, alpha=1.0):
    return input + alpha * other

@register_custom_acc_mapper_fn(
    op_and_target=("call_function", torch.add),
    arg_replacement_tuples=[
        ("input", "input"),
        ("other", "other"),
        ("alpha", "alpha", this_arg_is_optional),
    ],
)
def custom_mapper(node: torch.fx.Node, _: nn.Module) -> torch.fx.Node:
    """
    `node` is original node, which is a call_function node with target
    being torch.add.
    """
    alpha = 1
    if "alpha" in node.kwargs:
        alpha = node.kwargs["alpha"]
    foo_kwargs = {"input": node["input"], "other": node["other"], "alpha": alpha}
    with node.graph.inserting_before(node):
        foo_node = node.graph.call_function(foo, kwargs=foo_kwargs)
        foo_node.meta = node.meta.copy()
        return foo_node

在自定义映射器函数中,我们构建了一个acc操作节点并返回它。我们在这里返回的节点将接管原始节点的所有子节点acc_normalizer.py

最后一步将是为我们添加的新acc操作或映射函数添加单元测试。添加单元测试的位置在这里test_acc_tracer.py

  • 步骤2. 添加一个新的转换器

所有为acc ops开发的转换器都在acc_op_converter.py中。它可以为你提供一个如何添加转换器的好例子。

本质上,转换器是将acc操作映射到TensorRT层的机制。如果我们能够找到所有需要的TensorRT层,我们就可以开始使用TensorRT APIs为节点添加转换器。

@tensorrt_converter(acc_ops.sigmoid)
def acc_ops_sigmoid(network, target, args, kwargs, name):
    """
    network: TensorRT network. We'll be adding layers to it.

    The rest arguments are attributes of fx node.
    """
    input_val = kwargs['input']

    if not isinstance(input_val, trt.tensorrt.ITensor):
        raise RuntimeError(f'Sigmoid received input {input_val} that is not part '
                        'of the TensorRT region!')

    layer = network.add_activation(input=input_val, type=trt.ActivationType.SIGMOID)
    layer.name = name
    return layer.get_output(0)

我们需要使用tensorrt_converter装饰器来注册转换器。装饰器的参数是我们需要转换的fx节点的目标。在转换器中,我们可以在kwargs中找到fx节点的输入。如示例所示,原始节点是acc_ops.sigmoid,在acc_ops.py中只有一个参数“input”。我们获取输入并检查它是否是TensorRT张量。之后,我们向TensorRT网络添加一个sigmoid层并返回该层的输出。我们返回的输出将由fx.Interpreter传递给acc_ops.sigmoid的子节点。

如果我们在TensorRT中找不到与节点执行相同操作的对应层怎么办。

在这种情况下,我们需要做更多的工作。TensorRT 提供了作为自定义层的插件。我们尚未实现此功能。一旦启用,我们将进行更新

最后一步是为我们添加的新转换器添加单元测试。用户可以在此文件夹中添加相应的单元测试。