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,构建器在下一个步骤边界处响应并停止。
