RAII+接口模式的生产者消费者封装,以及多batch的体现
前言:RAII(Resource Acquisition Is Initialization),也称直译为“资源获取就是初始化”。这里的生产者消费者指的是模型推理的过程,送一个图片进来,送的这个线程认为是生产者的线程;把这个图片变成推理后的结果,那就称为是消费者。多batch指的是单批次多图处理,就是消费者一次拿batch个图片出来,而不是只pop一个图片出来进行推理。然后为什么要提到多batch呢?是因为GPU有个叫计算密集型的特性,也就是说计算越密集越友好。比如说给一张图推理假设需要5ms,两张图可能就只需要6ms,四张图可能只需要8ms。所以在生产者消费者封装的过程中就要考虑到GPU的特性,并且充分利用到它的特性,实现高性能,高性能的关键就是多batch的体现
这篇博文是承接上文的内容继续挖掘:使用RAII+接口模式对模型加载和推理进行封装
infer.cpp
对于生产者消费者模式,forward函数的过程就是往队列里抛任务,这里就是抛图片进来。还要注意一点就是尽量保证资源哪里分配,就在哪里释放,就在哪里使用,这样能使得程序足够的简单而不会太乱。所以worker内实现了模型的加载,使用,释放,好处就是资源全部在一个线程上。
// infer.cpp
#include "infer.hpp"
#include <mutex>
#include <thread>
#include <future>
#include <queue>
#include <string>
#include <memory>
#include <chrono>
#include <condition_variable>
using namespace std;
struct Job {
shared_ptr<promise<string>> pro;
string input;
};
class InferImpl : public InferInterface {
public:
// 析构函数
virtual ~InferImpl() {
worker_running_ = false;
cv_.notify_one();
if (worker_thread_.joinable()) worker_thread_.join();
}
bool load_model(const string& file) {
// 尽量保证资源在哪里分配,就在哪里使用,就在哪里释放,这样不会太乱。比如这里我们就都在 worker 函数内完成。
// 这里的pro表示是否启动成功
promise<bool> pro;
worker_running_ = true;
worker_thread_ = thread(&InferImpl::worker, this, file, std::ref(pro));
return pro.get_future().get();就是说只有在模型加载完成后才会为promise赋值,这时通过get一直等到结果为止
}
virtual shared_future<string> forward(string pic) override {
// printf("正在使用 %s 进行推理.\n", context_.c_str());
Job job;
job.pro.reset(new promise<string>());
job.input = pic;
lock_guard<mutex> l(job_lock_);
qjobs_.push(job);
//detection push
//face push
//feature push
//一次等待结果,实际上就是让detection+face+feature 并发执行
// 一旦有任务需要推理,发送通知
cv_.notify_one();
// return job.pro->get_future().get(); // 不能这样直接返回模型推理的结果,因为这样会等待这个模型推理结束后才往下走,相当于还是串行,如果我们有多个模型的话就实现不了多个模型并发了。
return job.pro->get_future(); // 这里是直接返回future对象,让外部按需要再.get()获取结果,所以这时forward的返回值类型就变成了shared_future<string>
}
void worker(string file, promise<bool>& pro) {
// //启动worker函数这个线程,worker是实际执行推理的函数
// context的加载、使用和释放都在worker内
string context = file;
if (context.empty()) { // 未初始化,返回false
pro.set_value(false);
return;
}
else { // 已初始化,返回true,之后正式开始进行推理
pro.set_value(true);
}
int max_batch_size = 5;
vector<Job> jobs; // 拿多张图片 batch
int batch_id = 0;//batch_id 的id,表示用不同的batch来做推理
//作为消费者,都要加while(true)来捕获内容
//因为我们知道创建线程就必须要join,所以如果这里不用while(worker_running_),而是while(true)的话,线程就停不下来,那join就会一直等待线程结束。
while (worker_running_) {
//对于生产者消费者模式,这里是充当消费者,在队列取任务并执行的过程
// 被动等待接收通知
unique_lock<mutex> l(job_lock_);
cv_.wait(l, [&](){
// true:停止等待
return !qjobs_.empty() || !worker_running_;
});
// 是因为程序发送终止信号而退出wait的,所以break掉使得线程正常结束掉。
if (!worker_running_) break;
// 可以一次拿一批出来, 最大拿maxBatchSize个
while (jobs.size() < max_batch_size && !qjobs_.empty()) {
jobs.emplace_back(qjobs_.front());//当小于max_batch_size时继续从qjobs里取图片放进自定义的vector jobs里
qjobs_.pop();
}
// 执行batch推理
for (int i=0; i<jobs.size(); ++i) {
auto& job = jobs[i];
char name[100];
sprintf(name, "%s : batch->%d[%d]", job.input.c_str(), batch_id, (int)jobs.size());
//通过job.pro的方式把结果回调(反馈)回去
job.pro->set_value(name);
}
batch_id++;
//推理结束,把jobs清空,保证下次能继续地去拿图片数据
jobs.clear();
this_thread::sleep_for(chrono::milliseconds(infer_time_));
}
printf("释放模型: %s\n", context.c_str());
context.clear(); // 释放模型
printf("线程终止\n");
}
private:
atomic<bool> worker_running_{false}; // 表示thread是否正在运行,实现退出机制。目的是使得类对象被析构时,类函数线程正常的自动退出。因为我们知道创建线程就必须要join,否则core dumped。所以如果这里不用while(worker_running_),而是while(true)的话,线程就停不下来,那join就会一直等待线程结束。
thread worker_thread_;
queue<Job> qjobs_;//队列
mutex job_lock_;//加锁,防止资源访问冲突
condition_variable cv_;因为消费者始终是while(true),所以condition_variable 用来防止内容为空时还一直做while循环浪费资源
int infer_time_ = 1000;
};
shared_ptr<InferInterface> create_infer(const string& file) {
shared_ptr<InferImpl> instance(new InferImpl());
if (!instance->load_model(file)) instance.reset();
return instance;
}
infer.hpp
// infer.hpp
#ifndef INFER_HPP
#define INFER_HPP
#include <memory>
#include <string>
#include <future>
class InferInterface {
public:
virtual std::shared_future<std::string> forward(std::string pic) = 0;
};
std::shared_ptr<InferInterface> create_infer(const std::string& file);
#endif
main.cpp
// main.cpp
#include "infer.hpp"
using namespace std;
int main() {
string file = "model a";
auto infer = create_infer(file);
if (infer == nullptr) {
printf("模型加载失败\n");
return -1;
}
//这里就代表这个有三个任务(模型),forward函数就代表把图片push进队列,这里push了三次。执行完这三行代码后会等待所有模型的推理结果一起返回,实现多个模型的推理并发执行。
//这里的三个forward也可以理解成单个模型同时进行三张图的推理,即我们题目所指的多batch实现,单批次多图处理。
auto fa = infer->forward("A");
auto fb = infer->forward("B");
auto fc = infer->forward("C");
//最后一起回收结果
printf("%s\n", fa.get().c_str());
printf("%s\n", fb.get().c_str());
printf("%s\n", fc.get().c_str());
//如果按照下面注释掉的部分的方式来进行推理的话,会每次都等待结果,无法进行单批次多图处理。
// auto fa = infer->forward("A").get();
// auto fb = infer->forward("B").get();
// auto fc = infer->forward("C").get();
// printf("%s\n", fa.c_str());
// printf("%s\n", fb.c_str());
// printf("%s\n", fc.c_str());
printf("程序终止\n");
return 0;
}
Refer: RAII 资源获取即初始化