算法Graph框架使用介绍

很在之前就提到了,我独立开发主要有两个大方向的项目想做。一个是影像特效类app的开发,另外则是一个算法框架。算法框架这个在很早之前就有想法做了,但是受限于能力和时间问题迟迟没有开始动手。在过去一段时间已经慢慢地实现了一个基本的基础框架,也有一些 demo开始跑起来了。今天就介绍一下这个框架如何使用。

在介绍具体使用方法之前,先简单说一下做这个框架的一些初衷和想要达到的目标。最早在开始工作的就有想法开始做了,想着工作上很多程序实现大多数上就应该是一个graph,一个直观的流程图。这样对于方案设计、实现、调试和性能优化都是一个非常好的模式。而且随着工作经验的增加,学到和实现的程序越来越多。基于graph的方式,把学过的内容都积累成一个一个graph中的node。后续要重复使用的时候,调用起来非常方便,因为借口都是统一的。这对于学习对于工作,都是有好处的。所以,这活其实应该早点就做。

对于这个框架,以我目前的认知水平来说,主要想作成拥有以下能力的框架

  • graph-node形式框架,node高度统一可扩展
  • node之间支持数据输入输出、数据依赖,支持统一参数设置
  • 支持任意数据类型
  • 任务节点node支持动态化插件,运行时加载
  • 多线程并行调度
  • 支持性能profile
  • 支持脚本节点

目前想到的功能特效就这些,做完这些就差不多算得上一个比较可用的graph框架了。下面介绍目前的演示例子,基本上算是刚刚跑通了demo。先看一下封装一个自定义的节点任务要做什么

先看一下派生类的定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class TestTask: public Task
{
    //定义当前节点输入数据名称、类型
    const std::vector<std::pair<std::string, std::string>> input_defines_ =
            {
                    {"input_0", typeid(int).name()},
                    {"input_1", typeid(float).name()},
            };
    //定义当前节点输出数据名称、类型
    const std::vector<std::pair<std::string, std::string>> output_defines_ =
            {
                    {"output_0", typeid(int).name()},
                    {"output_1", typeid(float).name()},
            };

public:
    //构造函数,节点名称是必须,可扩展其他参数
    TestTask(const std::string& name);
    
    //初始化节点
    int InitTask() final;
    
    //更新节点参数
    int UpdateParams(const std::string& name, const std::any value) final;
    
    //执行节点任务
    int RunTask() final;

    //返回节点输入输出的定义,父类会调用
    std::vector<std::pair<std::string, std::string>> GetInputDefines() const final;
    std::vector<std::pair<std::string, std::string>> GetOutputDefines() const final;
};

需要重写的函数,非常少,基本上都是必要的初始化、参数设置和运行任务。再看一下具体实现过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
TestTask::TestTask(const std::string& name) : Task(name)
{

}

int TestTask::InitTask()
{
    //初始化节点,参数(如果有)的定义可以放在此处实现
    param_.insert(std::make_pair("int_param", std::make_any<int>(0)));
    param_.insert(std::make_pair("float_param", std::make_any<float>(0.0f)));
    param_.insert(std::make_pair("string_param", std::make_any<std::string>("defaults")));

    return 0;
}

int TestTask::UpdateParams(const std::string& name, const std::any value)
{
    //更新参数
    if(param_.find(name) != param_.end())
    {
        param_[name] = value;
    }

    return  0;
}

//返回输入和输出的数据定义
std::vector<std::pair<std::string, std::string>> TestTask::GetInputDefines() const
{
    return input_defines_;
}

std::vector<std::pair<std::string, std::string>> TestTask::GetOutputDefines() const
{
    return output_defines_;
}

//执行任务
int TestTask::RunTask()
{
    //1. 获取输入数据、参数
    auto input_data = GetInputData();
    for(auto& data: input_data)
    {
        if(!data.has_value()) {return  -1;}
    }

    //注意根据数据定义转换为正确的类型
    int input0 = std::any_cast<int>(input_data[0]);
    float input1 = std::any_cast<float>(input_data[1]);
    int param0 =   std::any_cast<int>(param_["int_param"]);
    float param1 =   std::any_cast<float>(param_["float_param"]);

    //2. 具体的处理过程
    auto result0 = input0 + param0;
    auto result1 = input1 + param1;

    //3. 设置结果到输出
    output_data_[output_defines_[0].first] = result0;
    output_data_[output_defines_[1].first] = result1;

    return  0;
}

以上就是封装一个自定义的任务需要实现的简单介绍了,需要注意的就是输入和输出数据的类型,以上例子虽然是数字和字符串等基础类型,实际上支持任意的数据类型。接着往下看,有了节点任务后,怎么构建一个多任务的流水线方案,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
void SimpleDataTest()
{
    //创建任务
    std::shared_ptr<Task> t1, t2, t3;
    GraphManager::Instance().CreateGraph("test");
    GraphManager::Instance().CreateTask<TestTask>("test", "t1", t1);
    GraphManager::Instance().CreateTask<TestTask>("test", "t2", t2);
    GraphManager::Instance().CreateTask<TestTask>("test", "t3", t3);


    //创建数据
    std::vector<std::any> data = {1, 1.0f};
    GraphManager::Instance().CreateDataSource("test", "test_data", data);
    TaskDataConnect t1_data_source;
    t1_data_source.back_task_name = "t1";
    t1_data_source.front_info.emplace_back(std::make_pair("test_data", 0));
    t1_data_source.front_info.emplace_back(std::make_pair("test_data", 1));
    GraphManager::Instance().AddEdge("test", t1_data_source);

    //构建链接关系,构建运行的graph
    TaskDataConnect t1_t2_connect;
    t1_t2_connect.back_task_name = "t2";
    t1_t2_connect.front_info.emplace_back(std::make_pair("t1", 0));
    t1_t2_connect.front_info.emplace_back(std::make_pair("t1", 1));
    GraphManager::Instance().AddEdge("test", t1_t2_connect);

    TaskDataConnect t3_connect;
    t3_connect.back_task_name = "t3";
    t3_connect.front_info.emplace_back(std::make_pair("t1", 0));
    t3_connect.front_info.emplace_back(std::make_pair("t2", 1));
    GraphManager::Instance().AddEdge("test", t3_connect);


    //执行前更新每个节点的参数
    t1->UpdateParams("int_param", 1);
    t2->UpdateParams("int_param", 1);
    t3->UpdateParams("int_param", 1);
    t1->UpdateParams("float_param", 1.0f);
    t2->UpdateParams("float_param", 1.0f);
    t3->UpdateParams("float_param", 1.0f);

    //执行graph
    GraphManager::Instance().RunAllGraph();


    //获取执行的结果
    auto output3 = t3->GetOutputData();
    for(auto [name, value] : output3)
    {
        if(typeid(int).name() == value.type().name())
        {
            std::cout << "name: "<<t3->GetName() + name << " value:" << std::any_cast<int>(value) << std::endl;
        }else if(typeid(float).name() == value.type().name())
        {
            std::cout << "name: " <<t3->GetName() + name << " value:" << std::any_cast<float>(value) << std::endl;
        }
    }
}

可以看到,链接了多个测试任务,而且直接数据是有所依赖的。确定好链接关系后,直接运行,就可以得到预想的执行结果,无需关系具体的调度和执行。所以目前的主线开发任务,这个框架的完成度大概就是以上展示的情况,基本的架子算是有了。后续就是在此框架上不断使用和体验,完善。稍晚一点会开源到 github,感兴趣的朋友可以关注我的公众号,开源的时候会第一时间通知。


微信公众号

Licensed under CC BY-NC-SA 4.0