TensorRT引擎的构建过程可能从几秒到数分钟不等。对于大型强类型模型、深度策略搜索以及全新GPU SKU上冷启动的计时缓存,开发者、终端用户或AI智能体往往只能面对一个卡住的终端,完全不知道该继续等待、重试还是直接终止进程。大多数NVIDIA TensorRT集成在构建过程中既不报告任何进度,也不提供提前中止的方式。在长时运行的智能体工作流中,这会造成GPU算力的浪费和会话的阻塞。

TensorRT提供了IProgressMonitor接口来解决这一问题,该API已在NvInfer.h中存在多个版本。本文将介绍一种适用于Python和C++的最简集成实现,并新增响应Ctrl-C或外部事件循环程序化停止信号的取消路径,同时展示如何将进度流暴露给IDE、服务或智能体运行时使用。

IProgressMonitor接口简介

IProgressMonitor是TensorRT在引擎构建过程中调用的抽象基类。开发者需要继承该类并重写三个方法,其结构在Python和C++中完全一致,只是语法写法略有差异。

当某个阶段的parent_phase不为空时,表示该阶段嵌套在另一个阶段内,因此监控器看到的是一棵进度树,而非一个扁平列表。由于TensorRT可能从多个内部线程调用同一监控器实例,实现时必须保证线程安全。

将监控器与构建器进行绑定,只需在IBuilderConfig上完成一次配置:

config.progress_monitor = MyMonitor() # Python

config->setProgressMonitor(&myMonitor); // C++

构建流程说明

从上至下阅读整个流程:构建器通过phase_start开启"Building Engine"阶段,随后在其内部开启"Tactic Selection"子阶段,其parent_phase指向"Building Engine"。构建过程中,构建器持续调用step_complete,监控器返回布尔值:返回true表示继续构建,返回false则请求取消。在示例流程中,监控器在第47步返回false,即触发取消路径,构建器停止发起新步骤并开始回退,依次对"Tactic Selection"和"Building Engine"调用phase_finish,按相反顺序关闭所有活跃阶段。

环境准备

使用本文所述功能,需满足以下条件:

一块NVIDIA GPU;

TensorRT(当前OSS版本)及其Python绑定,或已编译的C++示例;

Python 3.10及以上版本(Python路径);

TensorRT示例数据:Python使用ResNet-50 ONNX,C++使用MNIST ONNX,二者均包含在示例数据压缩包中,或挂载于NGC官方容器的/usr/src/tensorrt/data目录下;

支持ANSI虚拟终端转义序列的终端,任何现代Linux Shell均可,Windows Terminal需启用VT支持。

Python实现

以下是一个精简的子类实现,仅追踪当前活跃阶段及各阶段的步骤数量:

import tensorrt as trt

from dataclasses import dataclass, field

from threading import Lock

@dataclass

class _PhaseState:

num_steps: int

current_step: int = 0

parent: str | None = None

class RichProgressMonitor(trt.IProgressMonitor):

def __init__(self):

super().__init__()

self._lock = Lock()

self._phases: dict[str, _PhaseState] = {}

self._cancelled = False

self._rendered_lines = 0

def phase_start(self, phase_name, parent_phase, num_steps):

with self._lock:

self._phases[phase_name] = _PhaseState(

num_steps=num_steps, parent=parent_phase

)

self._render()

def step_complete(self, phase_name, step) -> bool:

with self._lock:

if phase_name in self._phases:

self._phases[phase_name].current_step = step

self._render()

return not self._cancelled

def phase_finish(self, phase_name):

with self._lock:

self._phases.pop(phase_name, None)

self._render()

这里有两点值得注意:第一,Lock不是可选项,TensorRT会从多个内部线程调用监控器,若不加锁,渲染会出现数据竞争问题;第二,step_complete是唯一能够停止构建的回调,phase_start返回None,无法在阶段开始前拒绝,最早的取消点是该阶段的第一个step_complete。

进度渲染实现

渲染逻辑因使用环境不同而存在较大差异,其核心模式如下:

def _render(self):

rows = sorted(

self._phases.items(),

key=lambda kv: (kv[1].parent or "", kv[0]),

)

if self._rendered_lines:

print(f"\x1b[{self._rendered_lines}A", end="")

for name, st in rows:

done = min(st.current_step + 1, st.num_steps)

pct = done / max(st.num_steps, 1)

bar = "█" * int(40 * pct) + "·" * (40 - int(40 * pct))

indent = " " if st.parent else ""

print(f"\x1b[2K{indent}{name:<28} [{bar}] {done}/{st.num_steps}")

for _ in range(self._rendered_lines - len(rows)):

print("\x1b[2K")

self._rendered_lines = len(rows)

注意:当此监控器处于附加状态时,请勿将stdout重定向至文件或管道,否则转义码会被直接写入日志,导致日志不可读。对于非终端输出目标,应将_render()替换为结构化发送器。

添加取消功能

取消功能的添加只需三行代码,安装SIGINT处理器将标志位置为True,再由step_complete响应该标志:

import signal

def install_cancel(monitor: RichProgressMonitor):

def handler(signum, frame):

monitor._cancelled = True

print("\nCancelling TensorRT build at next step boundary...")

signal.signal(signal.SIGINT, handler)

绑定监控器并运行构建器:

builder = trt.Builder(TRT_LOGGER)

network = builder.create_network(

1 << int(trt.NetworkDefinitionCreationFlag.STRONGLY_TYPED)

)

parser = trt.OnnxParser(network, TRT_LOGGER)

with open(onnx_path, "rb") as f:

parser.parse(f.read())

config = builder.create_builder_config()

monitor = RichProgressMonitor()

config.progress_monitor = monitor

install_cancel(monitor)

serialized = builder.build_serialized_network(network, config)

if serialized is None:

if monitor._cancelled:

print("Build cancelled cleanly.")

else:

print("Build failed.")

build_serialized_network()在取消时返回None。构建器会在下一个步骤边界处回退,通常较快,但不是即时的,在较长的策略搜索步骤内可能会有延迟。建议应用程序将取消延迟情况告知用户,在回退窗口期显示简单的"正在取消..."提示即可。

同一标志位也可从非信号路径设置,例如IDE停止按钮、智能体超时或CI取消Webhook,只需将monitor._cancelled设置为True,构建即在下一个步骤边界处中止。

C++实现

#include <NvInfer.h>

#include <atomic>

#include <mutex>

#include <unordered_map>

class RichProgressMonitor : public nvinfer1::IProgressMonitor {

public:

void phaseStart(char const* phaseName,

char const* parentPhase,

int32_t nbSteps) noexcept override {

std::lock_guard<std::mutex> g(mu_);

phases_[phaseName] = {nbSteps, 0, parentPhase ? parentPhase : ""};

render();

}

bool stepComplete(char const* phaseName,

int32_t step) noexcept override {

std::lock_guard<std::mutex> g(mu_);

auto it = phases_.find(phaseName);

if (it != phases_.end())

it->second.current = step;

render();

return !cancelled_.load();

}

void phaseFinish(char const* phaseName) noexcept override {

std::lock_guard<std::mutex> g(mu_);

phases_.erase(phaseName);

render();

}

void requestCancel() noexcept {

cancelled_.store(true);

}

private:

struct Phase {

int32_t nbSteps;

int32_t current;

std::string parent;

};

std::mutex mu_;

std::unordered_map<std::string, Phase> phases_;

std::atomic<bool> cancelled_{false};

void render() noexcept;

};

绑定方式与Python一致:

auto config =

std::unique_ptr<nvinfer1::IBuilderConfig>(

builder->createBuilderConfig());

RichProgressMonitor monitor;

config->setProgressMonitor(&monitor);

取消标志使用std::atomic<bool>至关重要,因为requestCancel()可能从另一个线程或信号处理器中调用。其余部分与Python版本完全对应。

真实系统中的集成方式

IProgressMonitor是构建器与应用层之间唯一的集成点,取消操作的触发来源可以是终端Ctrl-C、IDE停止按钮、HTTP取消Webhook或智能体超时,所有路径都通过同一个monitor._cancelled标志生效。

以下是几种常见的集成场景:

IDE扩展:重写_render()以发送语言服务器协议中的$/progress通知,每个阶段对应一个进度Token,step_complete()对应report消息,phase_finish()对应end消息。

FastAPI/HTTP服务:在后台线程运行构建,_render()将条目推入asyncio.Queue,请求处理器通过Server-Sent Events消费,客户端获得实时流,取消通过POST /builds/{id}/cancel调用monitor.requestCancel()实现。

智能体工具调用:每次阶段切换时向工具调用流发送一条结构化数据块,格式为{"phase": ..., "step": ..., "total": ...},智能体运行时在用户可见的追踪中渲染,同一个requestCancel()钩子供智能体超时时调用。该模式对智能体运行时尤为重要,长时间运行的构建需要可观测、可取消,使智能体能够汇报进度、执行时间预算并干净地停止。

在所有上述场景中,IProgressMonitor都是正确的边界:其上(渲染、流式传输、传输协议)属于应用层,其下(策略计时、内核选择)属于构建器内部。

常见集成问题

以下是容易引发集成Bug的典型情形:

不要在终端渲染器处于附加状态时重定向stdout,转义码会污染日志。对于非交互式输出目标,应换用结构化发送器。

phase_start()无法取消,它返回None,最早的取消点是该阶段的第一个step_complete(),若用户在较长的phase_start()期间发出取消请求,构建会在第一个步骤边界前持续运行。

phase_finish()可能在所有num_steps均报告前触发,这在错误恢复、构建器内部短路或step_complete()返回false时均会发生,应将其视为阶段结束的权威信号,不要假设current_step等于num_steps。

取消存在延迟,但有上限。构建器会在检查返回值前完成当前步骤,较长的策略搜索步骤可能使延迟达到数秒甚至数十秒。

线程安全是必要条件,同一监控器实例会被多个构建器线程调用,在_render()中对dict或unordered_map进行无保护访问最终会导致崩溃或数据撕裂。

快速上手

端到端运行最快的方式如下:

git clone --depth 1 https://github.com/NVIDIA/TensorRT.git

cd TensorRT/samples/python/simple_progress_monitor

python3 simple_progress_monitor.py

这将启动一个带动态进度显示的ResNet-50引擎构建过程。可以将simple_progress_monitor.py中的监控器类替换为上文版本,或在现有类基础上添加取消处理器。C++等效实现位于samples/sampleProgressMonitor/。

对于更大规模的系统,下一步应将终端渲染器替换为应用程序已有的传输方式,例如语言服务器协议通知、服务器推送事件或结构化工具调用数据块。IProgressMonitor将成为TensorRT构建进度转化为应用程序进度模型的接入点。

Q&A

Q1:IProgressMonitor接口有哪些核心方法,各自的作用是什么?

A:IProgressMonitor有三个核心方法:phase_start在一个构建阶段开始时被调用,传入阶段名称、父阶段和步骤总数,返回None;step_complete在每个步骤完成时被调用,返回布尔值,返回true表示继续构建,返回false则请求取消;phase_finish在阶段结束时被调用。由于TensorRT会从多个内部线程调用监控器,三个方法的实现必须保证线程安全。

Q2:TensorRT引擎构建的取消操作为什么会有延迟?

A:取消操作的延迟来自构建器的内部机制。构建器不会立即响应取消信号,而是在完成当前步骤后才检查step_complete的返回值。如果正处于耗时较长的策略搜索步骤中,这个延迟可能达到数秒甚至数十秒。因此取消不是即时生效的,建议应用程序在触发取消后向用户显示"正在取消..."等提示,说明系统正处于回退窗口期。

Q3:如何在FastAPI服务中集成TensorRT构建进度监控?

A:在FastAPI服务中,可以在后台线程运行TensorRT引擎构建,同时重写监控器的_render()方法,将每次进度更新推入asyncio.Queue。请求处理器通过Server-Sent Events持续消费队列中的数据,客户端即可获得实时进度流。取消功能通过新增一个POST /builds/{id}/cancel接口实现,该接口调用monitor.requestCancel()将取消标志置为True,构建器在下一个步骤边界处响应并停止。

NVIDIA