4. 常见推理异常类型

推理异常现场千差万别,但绝大多数问题都可以归入有限的几大类。面对一个报错或异常行为,第一步不是急着改代码,而是先回答三个问题:异常发生在哪个阶段?异常的表现形态是什么?异常最可能涉及哪一层组件? 本章给出一个可操作的分类框架,帮助你快速建立“症状 → 类型 → 方向”的映射关系。

4.1 按发生阶段分类:推理生命周期的三个断面

一次完整的 TensorRT 推理可以切分为三个时间断面,每个断面出现的异常,其根因和排查手段截然不同。

  • 构建期(Build Time):从加载 ONNX/PyTorch 模型到生成 Engine 文件。典型异常包括:算子不支持、维度推导失败、显存不足(构建时峰值显存)、精度校准(Calibration)崩溃。
  • 序列化/反序列化期(Serialization):Engine 保存为 .plan 文件,或从 .plan 文件加载。典型异常包括:版本不匹配、平台不匹配(如 GPU 架构不同)、文件损坏、反序列化时显存不足。
  • 执行期(Runtime):Engine 加载完成后执行 enqueueV2/enqueueV3 推理。典型异常包括:输入尺寸/类型不匹配、显存越界、CUDA error、结果数值错误(NaN/Inf)、性能骤降。
判断技巧:报错信息中如果包含 TensorRT: xxx 且发生在 build_serialized_network 或 deserialize_cuda_engine 调用栈中,优先归为构建期/序列化期问题;如果发生在 enqueueV3 或 execute 调用栈中,则归为执行期问题。

4.2 按错误形态分类:五种常见“脸谱”

从错误的外在表现来看,推理异常可以归纳为以下五种形态。每种形态对应不同的排查路径。

错误形态 典型报错关键词 常见根因方向 初步定位手段
显存类 out of memory, CUBLAS_STATUS_ALLOC_FAILED 显存碎片、Batch 过大、工作区(workspace)设置过大、内存泄漏 监控 nvidia-smi 峰值显存,逐步缩小 Batch 或 workspace
形状/维度类 mismatch, invalid dimensions, binding index out of range 输入张量 shape 与 Engine 声明不一致、动态 shape 未设置优化 profile 打印 Engine 的 binding 信息,核对输入输出维度
算子/内核类 unsupported op, no kernel, assertion failed 模型算子 TensorRT 不支持、自定义插件未注册、精度模式不兼容 使用 Polygraphy 或 trtexec 逐层定位失败算子
数值/精度类 NaN, Inf, inconsistent results FP16/INT8 精度溢出、校准数据分布偏差、层归一化参数异常 对比 FP32 与低精度输出,逐层检查中间张量数值范围
同步/并发类 illegal memory access, CUDA error, stream synchronization 多流竞争、异步拷贝未同步、Host 与 Device 生命周期管理错误 启用 compute-sanitizer 检查越界访问,检查 cudaStreamSynchronize 调用点

4.3 症状识别:从现象到类型的快速判断路径

实际现场往往不会直接给出“类型”,而是给出一个模糊的症状。下面是一张判断路径图,帮助你从现象快速收敛到类型。

推理异常现象 程序崩溃/报错 结果错误/性能异常 CUDA error TensorRT 断言失败 输出 NaN/Inf 精度偏差大 显存越界/非法访问 算子不支持/插件缺失 FP16/INT8 溢出 校准数据分布偏差 初步定位工具 compute-sanitizer → 显存类 | Polygraphy → 算子类 | 逐层对比 → 精度类 症状 → 类型 → 工具,三步收敛根因 有报错信息 无显式报错

4.4 初步定位方法:三板斧

无论症状属于哪一类,以下三个方法都能帮助你快速缩小范围,避免在错误的方向上浪费时间。

方法一:启用 TensorRT 详细日志

设置日志级别为 VERBOSE,可以输出每个层(Layer)的构建细节、维度推导过程和内存分配信息。这是定位构建期问题最直接的手段。

// C++ 示例
class MyLogger : public nvinfer1::ILogger {
    void log(Severity severity, const char* msg) noexcept override {
        if (severity == Severity::kVERBOSE) {
            std::cout << "[TRT-VERBOSE] " << msg << std::endl;
        }
    }
};
MyLogger gLogger;
// 创建 builder 时传入
auto builder = nvinfer1::createInferBuilder(gLogger);
关键观察点:在 VERBOSE 日志中搜索 [Mem] 关键字可以查看显存分配记录;搜索 [Layer] 可以查看每个层的名称和维度;搜索 ERROR 或 Assertion 可以快速定位失败位置。

方法二:使用 trtexec 快速复现

trtexec 是 TensorRT 自带的命令行工具,可以在不写代码的情况下复现构建和推理过程。它非常适合用来判断问题是否出在模型本身,而非你的业务代码。

# 构建并推理,输出详细日志
trtexec --onnx=model.onnx --saveEngine=model.plan --verbose

# 只做构建,不做推理
trtexec --onnx=model.onnx --buildOnly

# 指定 FP16 精度
trtexec --onnx=model.onnx --fp16 --buildOnly

# 测试动态 shape
trtexec --onnx=model.onnx --minShapes=input:1x3x224x224 \
        --optShapes=input:8x3x224x224 --maxShapes=input:16x3x224x224

如果 trtexec 能成功构建并推理,说明模型本身没有问题,异常大概率出在你的业务代码(如输入预处理、显存管理、Stream 同步等)。如果 trtexec 也失败,则问题在模型或 TensorRT 配置层面。

方法三:Polygraphy 逐层定位

Polygraphy 是 NVIDIA 提供的模型调试工具,可以对比不同精度、不同后端(TensorRT vs ONNX-Runtime)的逐层输出,精确定位第一个产生差异的层。

# 对比 TensorRT FP16 与 ONNX Runtime FP32 的输出差异
polygraphy run model.onnx --trt --fp16 --onnxrt \
    --trt-outputs mark all --onnxrt-outputs mark all \
    --atol 0.01 --rtol 0.01 \
    --validate

当输出中显示 [I] PASSED 表示精度在容忍范围内;如果显示 [E] MISMATCH,会列出第一个不匹配的层名称和数值差异,直接指向问题层。

4.5 案例复盘:一个“假性显存不足”的排查过程

某次线上服务报错 CUDA error: out of memory,但通过 nvidia-smi 观察显存占用并不高(约 3GB / 24GB)。初步判断为显存类问题,但显存监控数据不支持这一结论。

排查过程如下:

  1. 启用 compute-sanitizer:compute-sanitizer --tool memcheck ./your_app,发现报错位置在 cudaMemcpyAsync 调用处,且提示 invalid argument,并非真正的显存耗尽。
  2. 检查输入指针:发现传入 cudaMemcpyAsync 的目的地址是一个 Host 端指针,而非 Device 指针。原因是代码中在某个分支下忘记调用 cudaMalloc,导致指针未初始化。
  3. 修复:在初始化阶段统一分配 Device 内存,并增加指针有效性检查。
误区提醒:看到 out of memory 就立刻调小 Batch 或换大显存显卡,是典型的“症状驱动”而非“根因驱动”。很多显存报错实际上是非法内存访问、指针未初始化或 Stream 不同步导致的“假性 OOM”。务必先用 compute-sanitizer 确认是否存在真正的越界访问。

4.6 常见误区与复盘清单

在推理异常排查中,以下误区会显著拖慢定位速度,需要刻意规避:

  • 只看最后一行报错:TensorRT 的日志往往很长,真正的根因可能在报错前几十行。建议保存完整日志,从下往上逐段阅读。
  • 忽略版本一致性:CUDA、cuDNN、TensorRT 三个版本必须匹配。版本不匹配经常导致“算子不支持”或“反序列化失败”的假象。
  • 动态 shape 未配置优化 profile:使用动态 shape 时,必须调用 setOptimizationProfile 设置 min/opt/max 三个维度,否则推理时输入尺寸超出范围会直接报错。
  • FP16 精度问题直接归咎于 TensorRT:很多 FP16 精度问题源于模型本身(如未做 BatchNorm 融合、激活值范围过大),而非 TensorRT 的精度损失。先用 Polygraphy 对比 FP32 与 FP16 的逐层输出,定位第一个差异层。
  • 忽略 Host 与 Device 生命周期:在异步推理中,如果 Host 端输入数据在 enqueueV3 返回后就被释放,会导致未定义行为,表现为随机性的数值错误或崩溃。

最后,建议每次排查完成后,记录一份简短的复盘清单,包含:异常现象、报错关键词、定位工具、根因、修复措施。积累 10 个以上案例后,你会发现自己对异常类型的敏感度显著提升,大部分问题可以在几分钟内完成初步定位。