使用 LiteRT 在 CPU 上运行 C++ 推理时,第二个输出 tensor 为何数据异常?
结论
目前还不能确认这是 LiteRT 的 bug,也没有证据表明,获取第二个输出需要配置 scheduling_info 或 run_options。
先修正代码中的两处问题:
- 检查
input_buffers[0].Write<float>(...)的返回状态。 - 在
Run()成功返回后读取输出。如果用Lock()检查内存,应在Unlock()之前完成,解锁后保存的地址不能作为可靠依据。
完成这些检查后,如果 Read<float>() 仍读出错误数据,再用相同输入对比其他推理路径,进一步判断问题是否出在 LiteRT CPU 路径。
为什么 Run() 成功,输出仍可能不正确?
Run() 返回成功,只表示运行时没有报告执行错误。它不会检查 circle 是否在预期范围内,也不会与 Python 的结果比较,因此没有报错并不代表输出数值正确。
output_buffers.size() == 2、元素数量分别为 2 和 3、类型都是 float,也只能说明输出接口的部分元数据符合预期,无法证明两个缓冲区都已正确写入。
这些接近零的数值需要排查,但仅凭数值本身,还无法确定原因是未初始化内存、输出绑定错误,还是其他问题。
保存解锁后的地址,不能证明输出缓冲区没有被写入
当前的调试代码是:
LITERT_ASSIGN_OR_ABORT(
auto host_mem_addr_1,
output_buffers[1].Lock(litert::TensorBuffer::LockMode::kRead));
output_buffers[1].Unlock();
解锁后,代码继续通过 host_mem_addr_1 检查数据。但锁定时取得的主机地址,在解锁后未必还能用于访问当前 tensor。即使某种 CPU 缓冲区实现恰好保持地址不变,也不应依赖这一行为。
如果断点停在 RunCompiledModel(...) 调用之前,输出还没有产生,此时看到旧数据并不异常。应先确认调用成功返回,再重新锁定输出。
原代码通过 Read() 也得到了异常结果,所以修正地址检查方式只能排除调试中的误判,还不能解释全部现象。
先修改输入写入和输出读取代码
现有代码忽略了 Write<float>() 的返回状态,应像检查 Run()、Read() 一样检查它。
可用以下代码替换原来的写入、运行和输出打印部分。代码沿用已有的 continuous_input_data、output_sizes 和缓冲区,使用前需确认输出是形状固定、连续存储的 float32 tensor。
LITERT_ABORT_IF_ERROR(
input_buffers[0].Write<float>(
absl::MakeConstSpan(
continuous_input_data.data(),
continuous_input_data.size())));
LITERT_ABORT_IF_ERROR(
compiled_model.Run(input_buffers, output_buffers));
LITERT_ASSIGN_OR_ABORT(
auto signature,
compiled_model.GetSignature(0));
const auto& output_names = signature.OutputNames();
if (output_names.size() != output_buffers.size() ||
output_sizes.size() != output_buffers.size()) {
std::cerr << "Output metadata mismatch\n";
return 1;
}
for (size_t k = 0; k < output_buffers.size(); ++k) {
std::vector<float> output_data(output_sizes[k]);
LITERT_ABORT_IF_ERROR(
output_buffers[k].Read<float>(
absl::MakeSpan(output_data)));
std::cout << output_names[k] << '\n';
for (size_t i = 0; i < output_data.size(); ++i) {
std::cout << "Element [" << i << "]: "
<< output_data[i] << '\n';
}
}
原来的 absl::MakeSpan(output_span) 可以简化为直接传入 output_span,也可以像示例一样,对 output_data 调用一次 absl::MakeSpan()。仅凭多了一层调用,还不能认定它是第二个输出异常的根因。
要检查锁定后的原始内存,可以在 Run() 成功返回后临时加入以下代码:
LITERT_ASSIGN_OR_ABORT(
auto host_mem,
output_buffers[1].Lock(
litert::TensorBuffer::LockMode::kRead));
const float* values = static_cast<const float*>(host_mem);
for (size_t i = 0; i < output_sizes[1]; ++i) {
std::cout << "Locked element [" << i << "]: "
<< values[i] << '\n';
}
output_buffers[1].Unlock();
在 Unlock() 处设置断点,即可检查仍处于锁定状态的内存。解锁后不要继续解引用 values,也不要为了寻找输出而读取 tensor 有效范围之外的数据。实际工程中,还应按所用 SDK 的接口检查解锁状态,或使用 SDK 提供的作用域锁工具。
核对输出映射,再进行对照推理
dump_model_simple 显示:
Output[0]: ... [StatefulPartitionedCall:1]
Output[1]: ... [StatefulPartitionedCall:0]
名称后缀 :0、:1 不能直接当作 output_buffers 的索引。signature 的输出顺序、输出名称和底层图中的 tensor 名称,需要分别核对。
按以下顺序排查:
- 确认创建输入输出缓冲区、执行推理和查询输出名称时,使用的都是同一个预期的 signature。示例读取的是索引
0,需要核对它与默认运行路径的对应关系。 - 按名称和形状核对 Python 输出,确认比较的是同一份
.tflite文件中的visibility和circle,不要只看两个环境中的列表下标。 - 将预处理后的
float32输入保存下来,供两端读取,确保输入 tensor 完全相同。这样可以排除 OpenCV、Python 图像处理库及归一化步骤的差异。第一个输出一致有参考价值,但不能据此确认输入逐元素相同。 - 条件允许时,用传统 TensorFlow Lite Interpreter 的 C++ 路径运行同一模型和输入,并记录是否启用了 XNNPACK 等 delegate,以便比较不同执行路径。
如果输入相同,Python 和传统 Interpreter 的两个输出都正确,而 CompiledModel 的第二个输出持续错误,就可以将排查范围缩小到 CompiledModel 的输出绑定、缓冲区处理或它使用的执行路径。接下来应检查对应 SDK 源码中 signature 输出到执行器 tensor 的映射,以及输出写入或复制的过程。
需要配置 scheduling_info 或 run_options 吗?
目前没有依据将问题归因于缺少这两个配置。对于代码中采用的同步 Run() 调用,应先检查返回状态、核对输出映射,并在内存仍处于锁定状态时读取数据。
如果实际使用的是异步执行接口,则需要按照该 SDK 版本的要求,等待完成通知或完成事件同步后再访问输出。异步接口的同步要求不能直接套用到当前代码,也没有依据通过添加某个参数来修复问题。
其他注意事项
日志显示 CPU accelerator 已注册,XNNPACK delegate 也已创建。NPU、GPU 的加载警告本身无法解释为什么只有第二个输出异常;Qt 的 Wayland 插件警告涉及图像显示,也不能据此判断 tensor 出错。
输入预处理还有两处需要核对:
convertTo(scaled_image, CV_32F, 1.0f)只改变数据类型,不会将像素除以255。是否需要归一化,应以模型和 Python 代码为准。ResizeWithPaddingResult的字段顺序是top、left,返回值却是{padded_img, delta_x, delta_y},两个偏移量写反了。当前推理没有使用这两个值,因此这处错误不能解释第二个原始输出异常,但会影响后续坐标还原。
如果问题能够稳定复现,应提交 SDK 版本或源码提交号、实际加载的运行库版本、最小复现程序、模型或可公开的等价模型,以及固定输入和两端输出。目前的日志和解锁后保存的地址,还不足以确认这是 LiteRT 的 bug。
备注:内容仅供参考。
