3、Infer Request基础:掌握Infer Request的概念、生命周期、状态机以及如何创建和配置Infer Request

在OpenVINO异步推理的架构中,Infer Request(推理请求)是连接应用代码与底层硬件加速器的核心执行单元。它不仅仅是一个简单的“任务封装”,而是一个具备完整状态机、资源绑定和生命周期管理的实体对象。理解Infer Request的运作机制,是掌握异步流水线、实现吞吐优化的前提。本章将深入剖析Infer Request的概念本质、状态流转、创建配置方法,并通过一个实际案例展示其在不同硬件上的行为差异。

创建与配置 生命周期状态机 执行与资源绑定 CompiledModel create_infer_request() set_input_tensor() set_config() / 回调绑定 就绪(READY) IDLE start_async() BUSY 推理完成 DONE wait()/reset cancel() CANCELLED 异常/释放 输入张量(内存/设备) 输出张量(结果缓冲) 设备队列槽位(硬件) 回调函数(用户态) 吞吐优化核心:多请求复用

上图展示了Infer Request从创建到执行的核心链路。左侧为创建配置阶段,中间为状态机流转,右侧为请求绑定的资源。注意:一个Infer Request在任意时刻只能处于一个状态,且状态转换由OpenVINO运行时内部驱动,用户通过API触发转换。

3.1 Infer Request的概念与设计哲学

Infer Request是ov::InferRequest类的实例,它代表一次可执行的推理任务。从抽象层次看,它封装了三个关键要素:输入数据(Input Tensor)、输出数据(Output Tensor)以及执行配置(如回调函数、设备特定参数)。与直接调用compile_model()后立即执行不同,Infer Request允许开发者将“数据准备”与“计算执行”解耦,从而为异步流水线奠定基础。

设计上,Infer Request遵循“一次创建,多次复用”的原则。在典型的吞吐优化场景中,开发者会创建多个Infer Request(通常等于设备支持的最大并行度),每个请求绑定不同的输入输出缓冲区。当某个请求完成推理后,应用层只需更新其输入张量的数据,再次调用start_async()即可重新提交任务,避免了重复创建请求带来的开销。

特性说明对性能的影响
资源预绑定输入/输出张量在创建时或首次设置时分配内存减少运行时内存分配,降低延迟抖动
状态可查询通过wait()或get_state()获取执行状态支持非阻塞轮询,便于实现流水线
回调机制推理完成时自动触发用户回调避免主动等待,提升CPU利用率
设备亲和性请求与特定设备(如GPU、CPU)绑定减少跨设备数据传输,提高效率

3.2 生命周期与状态机详解

Infer Request的状态机是理解其行为的关键。OpenVINO定义了四个主要状态:IDLE(空闲)、BUSY(执行中)、DONE(完成)和CANCELLED(取消)。状态转换由以下API触发:

  • start_async():将请求从IDLE转换为BUSY,异步开始推理。
  • wait():阻塞等待请求完成,BUSY→DONE。
  • wait_for():带超时等待,返回状态码。
  • cancel():尝试取消正在执行的任务,BUSY→CANCELLED。
  • reset():将DONE或CANCELLED状态重置为IDLE,以便复用。

值得注意的是,DONE状态并不自动回到IDLE。开发者必须显式调用reset()或再次调用start_async()(部分版本支持自动重置)才能复用请求。在实际工程中,忘记重置是导致“请求不可用”异常的常见原因。

风险提示:在GPU设备上,如果请求处于BUSY状态时调用reset(),会抛出ov::Exception。正确做法是先调用wait()确保任务完成,再重置。此外,cancel()并不保证立即终止硬件执行,仅表示应用层不再等待结果。

3.3 创建与配置Infer Request的标准流程

创建Infer Request的前提是拥有一个已编译的模型(ov::CompiledModel)。以下是标准操作流程:

  1. 获取编译模型:通过core.compile_model(model, device)获得。
  2. 创建请求:调用compiled_model.create_infer_request(),返回一个ov::InferRequest对象。
  3. 设置输入张量:使用request.set_input_tensor(tensor)或request.set_tensor(input_name, tensor)。推荐使用张量名称,避免索引混淆。
  4. 配置可选参数:通过request.set_callback()绑定完成回调,或通过request.set_config()设置设备特定参数(如GPU的GPU_THROUGHPUT_STREAMS)。
  5. 提交任务:调用request.start_async()开始异步推理。
// C++示例:创建并配置Infer Request
ov::Core core;
auto model = core.read_model("model.xml");
auto compiled = core.compile_model(model, "GPU");
auto infer_request = compiled.create_infer_request();

// 设置输入(假设模型只有一个输入)
auto input_tensor = ov::Tensor(compiled.input().get_element_type(), {1, 3, 224, 224});
infer_request.set_input_tensor(input_tensor);

// 绑定回调(可选)
infer_request.set_callback([&](std::exception_ptr ex) {
    if (ex) {
        std::cerr << "推理失败" << std::endl;
    } else {
        // 处理输出
        auto output = infer_request.get_output_tensor();
    }
});

// 启动异步推理
infer_request.start_async();

在Python中,流程类似,但回调使用lambda表达式,且start_async()不阻塞。Python的InferRequest还支持wait_for()返回StatusCode枚举,便于精细控制。

3.4 实战案例:多请求队列在CPU上的吞吐优化

为了展示Infer Request的实际价值,我们设计一个视频帧批处理场景。假设有一个实时视频分析应用,需要处理30FPS的1080p视频流,模型为轻量级目标检测网络(输入尺寸640×640)。

初始方案与瓶颈:最初采用同步推理,每帧调用compiled_model(input)。实测CPU利用率仅45%,帧率仅18FPS,无法满足实时性。分析发现,同步调用导致CPU在等待推理完成时空闲,且每次调用都有张量拷贝开销。

优化方案:采用4个Infer Request组成请求池,每个请求绑定独立的输入输出缓冲区。主线程循环执行以下操作:

  1. 从视频流读取一帧,写入当前空闲请求的输入张量(通过request.get_input_tensor()获取可写指针)。
  2. 调用start_async()提交任务。
  3. 检查下一个请求是否完成(wait_for(0)),若完成则取出输出并标记为空闲。

关键参数与结果:使用set_config({"NUM_STREAMS": "4"})(CPU设备)允许运行时创建多个内部流。优化后CPU利用率提升至82%,帧率稳定在28FPS。进一步增加请求数到6个,帧率提升至31FPS,但内存占用增加约200MB(每个请求的输入输出张量约3MB)。

复盘与经验:请求池大小并非越大越好。当请求数超过设备并行度时,额外的请求只会增加调度开销。对于CPU,建议请求数等于物理核心数或超线程数;对于GPU,则参考GPU_THROUGHPUT_STREAMS的推荐值。此外,输入张量的内存复用至关重要——避免在每次循环中重新分配张量,而是直接修改已有张量的数据内容。

性能调优要点:在创建Infer Request时,使用compiled_model.input(0)获取输入端口信息,并预先分配ov::Tensor。对于动态形状模型,务必在set_input_tensor()前设置好张量形状,否则会触发隐式重新分配,导致性能下降。

3.5 常见误区与总结

在实际开发中,以下误区需要特别警惕:

  • 误区一:认为Infer Request是线程安全的。实际上,同一个Infer Request对象不能同时被多个线程调用。如果需要多线程提交任务,应为每个线程创建独立的请求。
  • 误区二:忽略wait()的必要性。即使使用回调,在程序退出前也应调用wait()确保所有请求完成,否则可能导致设备资源未释放。
  • 误区三:在回调中执行耗时操作。回调运行在OpenVINO内部线程中,若执行复杂逻辑会阻塞其他请求的完成通知。建议回调仅做轻量级标记(如设置原子标志),实际处理放在主线程。
  • 误区四:混淆set_input_tensor()与set_tensor()。前者设置整个输入,后者可指定部分张量(用于多输入模型)。错误使用会导致数据错位。