C# · 12月 20, 2021

实战:解决C++ AI引擎代码仓库难以维护的问题

0 背景

我们团队目前主要负责研发 AI 引擎(主要用 C++ 实现),简单说来就是将算法 SDK 及模型进行封装,对外提供统一的接口方便后续的应用开发。相信有这类工作的经历的人都会明显感觉到有如下特点:

对外接口基本不变

算法 SDK 版本多:cpu/GPU,X86/ARM…

算法模型更多:1.0,2.0,3.0…还不算麻烦,关键是还有各种组合模型比如 1.0+2.0,1.0+2.0+3.0

本来再复杂的事情只要做的次数少其实都还能接受。所以起初,我们将应用(Application)、算法SDK(SDK)、算法模型(Models)都放在一个仓库中进行管理,需要版本时就整体打包,也安安稳稳的过了些日子。

但是随着业务发展,各个项目需要的引擎版本差异就越来越大:可能A项目需要我们提供 cpu 版本 1.0 模型的安装包,同时B项目又需要我们提供 GPU 版本的 2.0+3.0模型的安装包,同时C项目需要我们定位许久以前 ARM 版本上的一个BUG…于是各种幺蛾子出现:

仓库大小分分钟超过10G

用户常常下错安装包

模型中某个配置项的默认值设置出错

打包时忘了改某个配置参数

只能在某台服务器上可以编译打包,换个环境配置半天都不一定搞得定

我们突然发现,如果不解决工程性问题的话,加人不一定有效,甚至可能会越来越乱。我们需要重新梳理整个工程结构,所以就有了后文的内容。

1 目标

首先,我们要明确痛点和需求,才能更好的进行改进。根据实际需求和现存问题,我们认为新的工程结构应该具备如下特点:

需求 1:尽可能的减少打包操作,让开发有更多时间在开发

需求 2:方便多人协同工作

需求 3:支持本地编译的基础上,支持代码提交后自动化编译出包

2 核心方案:

为了满足上述需求,我们最终采用的是下述两个思路来实现

标准化:用来搞定需求 1 和需求 2

自动化:用来搞定需求 3

2.1 标准化

标准化具体来说有以下两点:

改进方向:通过合理划分工作以及对应代码仓库来实现

原始方案:

代码仓库:Application 仓库(内含指定版本的 SDK 及 Models)

发布仓库:Application 仓库(内含指定版本的 SDK 及 Models)

获取方式:找到 Application 版本后直接下载

维护方式:

(低频)修改业务逻辑(如:新增 rpc 接口):重新构建整个工程并发布

(中频)切换SDK版本(如:将x86版本换为嵌入式arm版本):重新构建整个工程并发布

(高频)切换模型版本(如:将单模型更新为混合(多)模型):重新构建整个工程并发布,或者在已经打包完成的二进制包中手动更新模型文件及配置文件

改进方案:

代码仓库:Application 仓库、SDK 仓库、Models 仓库

发布仓库:Application 仓库(内含指定版本的 SDK)、Models 仓库

获取方式:找到 Application 版本并选定需要的模型,服务器后台经过文件拼接后生成下载链接以供下载

维护方式:

(低频)修改业务逻辑(如:新增 rpc 接口):重新打包 Application 仓库并发布

(中频)切换SDK版本(如:将x86版本换为嵌入式arm版本):重新打包 Application 仓库并发布

(高频)切换模型版本(如:将单模型更新为混合(多)模型):无维护工作

依赖工具:QT(qmake) + Conan

QT(qmake):C++ 工程管理工具,方便按需生成 makefile

Conan:去中心化包管理工具,主要作用是用“包”的形式管理 C++ 或其他二进制文件的依赖关系

Artifactory:Conan 官方提供的支持私有化部署的仓库,用于持久化存储 Conan 中的“包”以及编译出来的最终二进制文件

注:这里选用 QT 进行 C++ 代码管理,主要是因为延续项目历史配置的原因。其实选择任何 Conan 支持的高级工程管理工具都是可行的,例如 CMake

收益:

仓库拆分后,Application 仓库中的内容更清晰简洁,仓库大小从 Gb 级别降至 Mb 级别,方便管理

通过仓库的拆分,将打包过程分为两个阶段并使得 Application (内含指定版本的 SDK)与 Models 解耦,支持按需生成用户实际需要的安装包,降低打包工作量

C++ 中的依赖库被 Conan 整体接管,编译选项被 QT + Conan 整体接管,多人协助基础技术障碍基本扫清

SDK 内部配置、Models内部配置统一使用仓库配置,降低因误操作导致默认配置被改的概率

2.2 自动化

由于代码托管在 Gitlab 上,所以整体方案比较直接:Gitlab-ci + Kubernetes + Docker:

gitlab-ci:Gitlab 内置 CI/CD 工具,在Gitlab 体系中使用非常方便

Kubernetes + Docker:将编译环境制作为容器并支持在私有云动态部署,这里主要作为 gitlab-ci 的 runner 使用

收益:

通过自动化打包,实现统一的出包策略,降低打包过程中的低级错误发生的可能性

无缝对接后续的自动化测试等流程

3. (附录)实现过程中的主要坑点:

3.1 团队协作

在团队中设置专人负责除 Application 以外的所有基础库的维护工作;

3.2 Conan + Artifactory

安装完 Conan 后,可以通过修改 conan.conf 中的 path 字段来改变“包”的实际存储位置;

不允许覆盖、删除所有上传至 Artifactory 的 stable 包;

Conan 中 Settings 系列字段(OS、Architecture、Build Type、Compiler等)无法支持自定义值,因此如果有嵌入式编译需求时,建议在所有项目中增加默认的 Option 字段(类似 shared)用于标识目标机的具体定义(如:Redhat6.4,3559A等);

建议在私有 Artifactory 中维护项目所需的所有基础包,并在使用时移除 conan-center 公共远程仓库,控制外部环境(主要是网络)对编译过程的影响;

conanfile.py 完全覆盖 conanfile.txt 的功能,建议所有 Conan 工程仅维护 conanfile.py 即可;

使用在 Conan 的 Option 扩展“包”的可配置性时要慎之又慎,如无必要勿增实体,要知道每多一个 Option 后续打包的时候都要多一个麻烦;

在 conanfile.py 的 configure()、config_options()、requirements() 中尽可能的将 requirements 与 options 配置明确,降低实际使用时的复杂程度;

使用 Conan 提供的 cpt.packager 工具来管理同时编译多个版本的任务;

3.3 gitlab-ci

打包时版本号的后缀部分根据构建时间自动生成,如:v1.0.0-20181212080808;

打包时将 commit id 的 sha 值随二进制包一同输出;

feature分支仅提供artifacts下载试用,仅主干分支支持上传至 Artifactory 仓库;

若有多个版本,建议在使用 gitlab-ci 实现时以并行任务的方式同时进行构建,以避免频繁修改自动化配置;(.gitlab-ci.yaml文件)

3.4 Kubernetes + Docker

自己制作拿来即用的 Docker 镜像,加快构建速度并控制风险;