Caffe2针对移动集成进行了优化,灵活,易于更新,并且能够运行在低功耗设备上。 本文将介绍如何在移动项目中实现Caffe2。
如果您希望在移动端看到可行的Caffe2实施(仅限目前的Android),请查看此演示项目。
Caffe2由以下组成:
它是纯C++的,唯一的非可选依赖关系是:
对于某些用例,您还可以塞入NNPACK,这特别优化了ARM上的卷积。它是可选的(但推荐)。
错误处理是通过抛出异常,通常是caffe2::EnforceNotMet,它继承自std::exception。
模型由两部分组成:代表学习参数(训练期间更新)的一组权重(通常为浮点数),以及一组构成计算图的“操作”,表示如何将输入数据(随每个图通过而变化)与学习参数(不随每个图通过变化)组合起来。参数(和计算图中的中间状态存在于Workspace中,它基本上是一个std::unordered_map<string,Blob>
,其中Blob
表示任意类型的指针,通常是TensorCPU
,它是一个n维数组(一个Python的numpy ndarray
,Torch的Tensor
等)。
核心类是caffe2::Predictor,它展示了构造函数:
Predictor(const NetDef& init_net, const NetDef& predict_net)
其中两个NetDef输入是表示上述两个计算图的Google协议缓冲区对象—— init_net
通常运行一组将权重反序列化到Workspace中的操作,而predict_net
指定如何为每个输入执行计算图。
Predictor是一个有状态的类——通常,流程将实例化该类一次,并在多次请求中重用。 根据用例,安装开销可能是微不足道的或不容忽视的。构造函数执行以下操作:
init_net
,分配内存并设置参数的值。predict_net
(将caffe2::NetDef 映射到caffe2::NetBase实例(通常为caffe2::SimpleNet)。一个关键点是,所有的初始化在某种意义上是“静态”可验证的——如果构造函数在一台机器上失败(通过抛出异常),那么它在每台机器上总是会失败。在导出NetDef实例之前,请验证Net构造是否可以正确执行。
目前,Caffe2针对具有NEON的ARM CPU(自2012年起基本为任何ARM CPU)进行了优化。 也许令人惊讶的是,ARM CPU的性能优于板载GPU(在老于iPhone 6的设备上我们的NNPACK ARM CPU实现优于苹果的MPSCNNConvolution)。将计算卸载到GPU/DSP上还有其他优势,并且我们正在积极开展Caffe2中的这些工作。
对于卷积实现,建议使用NNPACK,因为它比大多数框架中使用的标准im2col/sgemm
实现要快得多(约2x3x)。 建议将OperatorDef::engine
设置为NNPACK。 例:
def pick_engines(net):
net = copy.deepcopy(net)
for op in net.op:
if op.type == "Conv":
op.engine = "NNPACK"
if op.type == "ConvTranspose":
op.engine = "BLOCK"
return net
对于非卷积(例如排序)工作负载,关键计算基元通常是全连接层(例如Caffe2中的FullyConnectedOp,Caffe中的InnerProductLayer,Torch中的nn.Linear)。对于这些用例,您可以切回到BLAS库,特别是加速iOS上的Accelerate和Android上的Eigen。
实例化和运行Predictor模型的内存使用量是它的权重和激活的总和。没有分配“静态”内存,所有分配都与Predictor拥有的Workspace实例相关联,所以在删除所有Predictor实例后,应该不会再占用内存。
在导出运行之前,推荐使用以下命令:
def optimize_net(net):
optimization = memonger.optimize_interference(
net,
[b for b in net.external_input] +
[b for b in net.external_output])
try:
# This can fail if the blobs aren't in the workspace.'
stats = memonger.compute_statistics(optimization.assignments)
print("Memory saving: {:.2f}%".format(
float(stats.optimized_nbytes) / stats.baseline_nbytes * 100))
except Exception as e:
print(e)
return pick_engines(share_conv_buffers(rename_blobs(optimization.net)))
这将自动共享在图的拓扑顺序中有效的激活(有关更详细的讨论,请参阅Predictor)。
Caffe2使用注册表模式来注册运算符类。宏位于核心运算符operator.h的运算符注册表部分:
// The operator registry. Since we are not expecting a great number of devices,
// we will simply have an if-then type command and allocate the actual
// generation to device-specific registerers.
// Note that although we have CUDA and CUDNN here, the registerers themselves do
// not depend on specific cuda or cudnn libraries. This means that we will be
// able to compile it even when there is no cuda available - we simply do not
// link any cuda or cudnn operators.
CAFFE_DECLARE_REGISTRY(
CPUOperatorRegistry,
OperatorBase,
const OperatorDef&,
Workspace*);
#define REGISTER_CPU_OPERATOR_CREATOR(key, ...) \
CAFFE_REGISTER_CREATOR(CPUOperatorRegistry, key, __VA_ARGS__)
#define REGISTER_CPU_OPERATOR(name, ...) \
CAFFE_REGISTER_CLASS(CPUOperatorRegistry, name, __VA_ARGS__)
#define REGISTER_CPU_OPERATOR_STR(str_name, ...) \
CAFFE_REGISTER_TYPED_CLASS(CPUOperatorRegistry, str_name, __VA_ARGS__)
#define REGISTER_CPU_OPERATOR_WITH_ENGINE(name, engine, ...) \
CAFFE_REGISTER_CLASS(CPUOperatorRegistry, name##_ENGINE_##engine, __VA_ARGS__)
并使用,例如conv_op.cc:
REGISTER_CPU_OPERATOR(Conv, ConvOp<float, CPUContext>);
REGISTER_CPU_OPERATOR(ConvGradient, ConvGradientOp<float, CPUContext>);