智能体/生成式 AI

让长时间运行的 NVIDIA TensorRT 引擎在 Python 或 C++ 中构建可观察和可取消

构建 TensorRT 引擎可能需要几秒钟到几分钟的时间。大型强类型模型、深度策略搜索和全新 GPU SKU 上的冷定时缓存可能会让开发者、最终用户或 AI 智能体盯着冻结的终端,而不知道是否要等待、重试或终止该进程。大多数 NVIDIA TensorRT 集成在构建过程中不会报告任何内容,或者无法提前终止。在长时间运行的智能体工作流中,这会浪费 GPU 小时并导致会话卡顿。

TensorRT 提供 IProgressMonitor (用于修复此问题的 API) ,并且多个版本都使用 NvInfer.h。本教程将介绍 Python 和 C++ 的最小插入式实现,添加响应 Ctrl-C 的取消路径或来自外部事件循环的编程停止信号,并展示在何处显示生成的进度流,以便 IDE、服务或代理运行时使用。

本文中的每个代码块均基于两个由 NVIDIA 维护的 OSS 示例进行提升或建模:

  • Python:samples/python/simple_progress_monitor/ ( ResNet-50,强类型网络)
  • C++:samples/sampleProgressMonitor/ ( MNIST)

IProgressMonitor 为您提供的功能

IProgressMonitor 是 TensorRT 在引擎构建期间调用的抽象基类。您可以对其进行子类并覆盖三种方法。在 Python 和 C++ 中,形状相同;只有拼写不同。

概念 Python 方法 C++ 方法 您要做什么
阶段进入 phase_start(phase_name, parent_phase, num_steps) phaseStart(phaseName, parentPhase, nbSteps) 保留进度行并录制 num_steps
阶段内步骤完成 step_complete(phase_name, step) -> bool stepComplete(phaseName, step) -> bool 前进吧。返回 False/ false 以取消构建。
阶段结束 phase_finish(phase_name) phaseFinish(phaseName) 拆掉这一行。
表 1. IProgressMonitor 接口镜像在 Python 和 C++ 上。这三种方法具有相同的语义,并且 step_complete 是唯一返回值会改变构建器行为的回调函数

parent_phase 为非 null 的阶段会嵌套在另一个阶段内,因此显示器会看到进度树,而非平面列表。实现必须是线程安全的,因为 TensorRT 可以从多个内部线程调用同一监视器实例。

通过在 IBuilderConfig 上进行设置,将显示器连接到构建器。这两种语言的单次调用:

config.progress_monitor = MyMonitor()      # Python

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

从上到下阅读图表。构建器使用 phase_start 打开构建引擎阶段,然后打开内嵌的战术选择,其中 parent_phase 指向构建引擎。随着构建过程的进行,构建器调用 step_complete (实体箭头) ,您的显示器将返回一个布尔值 (虚线箭头) :true 允许构建继续,false 则会请求取消。在此处显示的运行中,显示器在步骤 47 (即红色取消路径) 返回 false,构建器停止发出新步骤并展开。它在战术选择构建引擎中提前调用 phase_finish,以反向顺序关闭每个活动阶段。

本教程构建的内容

本教程展示了如何在 Python 和 C++ 中实现 IProgressMonitor,通过 step_complete 添加取消,以及将进度更新路由到终端、IDE、服务或代理运行时。

预备知识

  • 一个 NVIDIA GPU。
  • TensorRT (当前 OSS 版本) 及其 Python 绑定,或 C++ 示例的构建。
  • Python 3.10 或更高版本 ( Python 路径) 。
  • TensorRT 样本数据:ResNet-50 ONNX (适用于 Python) 和 MNIST ONNX (适用于 C++) 。两者都随附样本数据存档,或安装在官方 NGC 容器中的 /usr/src/tensorrt/data 下。
  • 支持 ANSI 虚拟终端转义的终端。任何现代 Linux shell 都符合要求;如果启用了 VT,Windows 终端可以正常运行。

1. Python 中的子类 IProgressMonitor

子类很小。它仅追踪哪些阶段处于活动状态,以及每个阶段包含多少步长。

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

2. 使用虚拟终端转义渲染嵌套进度条

渲染器是因环境而异的部分,因此本节给出形状并指向用于生产级实现的上游示例。模式为:

def _render(self):
    # Order phases by nesting depth so children draw under parents.
    rows = sorted(
        self._phases.items(),
        key=lambda kv: (kv[1].parent or "", kv[0]),
    )
    # Move the cursor up by the number of lines the PREVIOUS render printed,
    # not the current row count — phases are added on nesting and removed on
    # phase_finish, so the two differ exactly when the tree changes shape.
    if self._rendered_lines:
        print(f"\x1b[{self._rendered_lines}A", end="")
    for name, st in rows:
        # step is a 0-based index in [0, num_steps); +1 turns it into a
        # completed count so the bar can actually reach 100%.
        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}")
    # Clear rows left behind when a phase finishes and the count shrinks.
    for _ in range(self._rendered_lines - len(rows)):
        print("\x1b[2K")
    self._rendered_lines = len(rows)

上游 simple_progress_monitor.py 渲染相同的形状,并改进了颜色和宽度处理。转义序列 \x1b[NA 将光标向上移动 N 行,然后 \x1b[2K 清除一行。第一次渲染调用会写入空行;后续调用会将其覆盖到位。

连接此监视器时,请勿将标准输出重定向到文件或管道。转义码将逐字写入日志,使其不可读。对于非终端汇,将 _render() 替换为结构化发射器。

3. 添加取消路径

显示器存在后,取消是三行附加内容。安装可翻转标志的 SIGINT 处理程序,然后让 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。构建器在下一步边界时展开,通常是快速的,但不是瞬间的,尤其是在漫长的战术搜索步骤中。

应用应向用户公布取消延迟。在放松窗口中显示一条简单的“Cancelling …” (取消 …) 消息会大有益。

您可以从任何非信号路径 (例如 IDE Stop 按钮、智能体超时或 CI cancel webhook) 设置相同的标志。设置 monitor._cancelled = True,构建将在下一步边界处中止。

4。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;
};

以相同的方式连接:

auto config =
    std::unique_ptr<nvinfer1::IBuilderConfig>(
        builder->createBuilderConfig());

RichProgressMonitor monitor;

config->setProgressMonitor(&monitor);

std::atomic<bool> 对于“Cancel” (取消) 标志很重要,因为 requestCancel() 可能会从其他线程或信号处理器调用。其他所有内容都与 Python 版本一致。

在实际系统中连接到何处

图 3. IProgressMonitor 是构建器表面和应用表面之间的单一集成点

agent runtime 中提取取消箭头以确定具体程度,但相同的机制适用于每个汇点。终端的 Ctrl-C、IDE Stop 按钮、HTTP cancel webhook 或智能体 timeout 都会翻转相同的 monitor._cancelled 标志,并且取消将在下一个 step_complete 返回时生效。在真实系统中的连接位置

终端是最简单的例子。有趣的集成会在其他地方取得进展:

  • IDE 扩展:覆盖 _render() 以在语言服务器协议中发出 $/progress 通知,或在协议中发出同等的 window/showProgress 通知。每个阶段成为一个进度令牌;step_complete() 成为报告消息;phase_finish() 成为结束。
  • FastAPI/ HTTP 服务:在后台线程上运行构建,并让 _render() 将条目推送到请求处理程序通过服务器发送事件排空的 asyncio.Queue 中。客户端将获得一个实时串流;cancel hook 只是一个调用 monitor.requestCancel()POST /builds/{id}/cancel
  • 代理工具调用:在每个阶段转换 ({"phase": ..., "step": ..., "total": ...}) 中,向工具调用流发送一个结构化数据块。智能体运行时在用户可见追踪中进行渲染,而相同的 requestCancel() hook 是智能体超时在构建超出预算时调用的数据。这种模式对于智能体运行时也很重要。长时间运行的构建需要是可观察和可取消的,以便智能体能够报告进度、执行时间预算,并彻底停止。

在这三种情况下,IProgressMonitor 都是正确的边界。高于此级别的内容 (渲染、流式传输、传输) 是应用级的;低于此级别的内容 (策略计时、内核选择) 是构建者的业务。

要处理的边缘案例

这些行为是集成错误的常见来源:

  • 连接终端渲染器时,请勿重定向 stdout。转义序列会污染日志。对于非交互式汇点,将渲染器替换为结构化发射器。
  • phase_start() 无法取消。返回 None。最早的取消点是该阶段的第一个 step_complete()。如果用户在很长的 phase_start() 期间取消,构建将继续进行,直到第一步边界。
  • phase_finish() 可能会在所有 num_steps 报告之前发射。这可能会在错误恢复、构建器内部短路或 step_complete() 返回 False 期间发生。将其视为权威的相位结束信号;不要假设 current_step == num_steps
  • 取消延迟是有限的,但不是零。构建器在检查返回值之前完成当前步骤。长时间的战术搜索步骤可以将其推向数秒到几十秒的范围。
  • 线程安全性是必需的。从多个构建器线程调用同一监视器实例;从 _render() 进行的未被分析的 dictunordered_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 的 monitor 类替换为上述版本,或在现有类周围附加取消处理程序。samples/sampleProgressMonitor/ 中提供了 C++ 等效函数。

对于较大的系统,下一步是将终端渲染器替换为应用已使用的传输,例如语言服务器协议通知、服务器发送事件或结构化工具调用块。IProgressMonitor 将成为 TensorRT 构建进度转换为应用进度模型的点。

了解详情

有关更多信息,请参阅以下资源:

标签