📅  最后修改于: 2020-12-10 04:46:08             🧑  作者: Mango
在此,详细解释Apache MXNet中的系统组件。首先,我们将研究MXNet中的执行引擎。
Apache MXNet的执行引擎非常通用。我们可以将其用于深度学习以及任何特定于领域的问题:根据它们的依赖关系执行一堆函数。它的设计方式是对具有依赖性的函数进行序列化,而无依赖性的函数可以并行执行。
下面给出的API是Apache MXNet执行引擎的核心接口-
virtual void PushSync(Fn exec_fun, Context exec_ctx,
std::vector const& const_vars,
std::vector const& mutate_vars) = 0;
上面的API具有以下内容-
exec_fun – MXNet的核心接口API允许我们将名为exec_fun的函数及其上下文信息和依赖项推送到执行引擎。
exec_ctx-应该在其中执行上述函数exec_fun的上下文信息。
const_vars-这些是函数读取的变量。
mutate_vars-这些是要修改的变量。
执行引擎向其用户保证,修改公共变量的任何两个函数的执行将按其推入顺序进行序列化。
以下是Apache MXNet执行引擎的函数类型-
using Fn = std::function;
在上面的函数, RunContext包含运行时信息。运行时信息应由执行引擎确定。 RunContext的语法如下-
struct RunContext {
// stream pointer which could be safely cast to
// cudaStream_t* type
void *stream;
};
以下是有关执行引擎功能的一些要点-
所有功能均由MXNet执行引擎的内部线程执行。
将阻止函数推送到执行引擎是不好的,因为这样,该函数将占用执行线程,并且还会减少总吞吐量。
为此,MXNet提供了另一个异步函数,如下所示:
using Callback = std::function;
using AsyncFn = std::function;
在此AsyncFn函数,我们可以传递线程的大部分内容,但是执行引擎在调用回调函数之前不会认为函数完成。
在Context中,我们可以指定要在其中执行的函数的上下文。这通常包括以下内容-
该函数应在CPU还是GPU上运行。
如果我们在上下文中指定GPU,则使用哪个GPU。
Context和RunContext之间存在巨大差异。上下文具有设备类型和设备ID,而RunContext具有只能在运行时确定的信息。
VarHandle,用于指定函数的依赖关系,就像一个令牌(特别是由执行引擎提供),我们可以用来表示函数可以修改或使用的外部资源。
但是问题来了,为什么我们需要使用VarHandle?这是因为,Apache MXNet引擎旨在与其他MXNet模块分离。
以下是有关VarHandle的一些要点-
它是轻量级的,因此创建,删除或复制变量的操作成本很少。
我们需要指定不可变的变量,即将在const_vars中使用的变量。
我们需要指定可变变量,即将在mutate_vars中修改的变量。
执行引擎用来解析功能之间的依赖性的规则是,当两个功能之一修改至少一个公共变量时,将按其推入顺序对任何两个功能的执行进行序列化。
为了创建新变量,我们可以使用NewVar() API。
要删除变量,我们可以使用PushDelete API。
让我们通过一个简单的例子了解它的工作-
假设我们有两个函数,即F1和F2,并且它们都使变量V2突变。在这种情况下,如果在F1之后按F2,则可以保证在F1之后执行F2。另一方面,如果F1和F2都使用V2,则它们的实际执行顺序可能是随机的。
推送和等待是执行引擎的两个更有用的API。
以下是Push API的两个重要功能:
所有的Push API都是异步的,这意味着无论Push函数是否完成,API调用都会立即返回。
Push API不是线程安全的,这意味着一次只能有一个线程可以进行引擎API调用。
现在,如果我们谈论Wait API,以下几点代表它-
如果用户希望等待特定函数完成,则他/她应在闭包中包含回调函数。一旦包含,请在函数末尾调用该函数。
另一方面,如果用户要等待涉及某个变量的所有功能完成,则他/她应使用WaitForVar(var) API。
如果有人要等待所有推送的函数完成,请使用WaitForAll() API。
用于指定功能的依赖关系,就像一个令牌。
Apache MXNet中的运算符是一类,其中包含实际的计算逻辑以及辅助信息,并有助于系统执行优化。
Forward是核心运算符界面,其语法如下:
virtual void Forward(const OpContext &ctx,
const std::vector &in_data,
const std::vector &req,
const std::vector &out_data,
const std::vector &aux_states) = 0;
Forward()中定义的OpContext的结构如下:
struct OpContext {
int is_train;
RunContext run_ctx;
std::vector requested;
}
OpContext描述了运算符的状态(无论是在培训阶段还是测试阶段),应该在哪个设备上运行该运算符以及所请求的资源。两个更有用的执行引擎API。
从上面的Forward核心接口,我们可以了解所需的资源,如下所示:
in_data和out_data表示输入和输出张量。
req表示如何将计算结果写入out_data 。
OpReqType可以定义为-
enum OpReqType {
kNullOp,
kWriteTo,
kWriteInplace,
kAddTo
};
像Forward运算符,我们可以选择实现Backward接口,如下所示:
virtual void Backward(const OpContext &ctx,
const std::vector &out_grad,
const std::vector &in_data,
const std::vector &out_data,
const std::vector &req,
const std::vector &in_grad,
const std::vector &aux_states);
操作员界面允许用户执行以下任务-
用户可以指定就地更新,并可以减少内存分配成本
为了使它更整洁,用户可以从Python隐藏一些内部参数。
用户可以定义张量和输出张量之间的关系。
为了执行计算,用户可以从系统中获取其他临时空间。
众所周知,在卷积神经网络(CNN)中,一种卷积具有多种实现方式。为了从中获得最佳性能,我们可能需要在这几个卷积之间切换。
因此,Apache MXNet将运算符语义接口与实现接口分开。这种分离以OperatorProperty类的形式完成,该类包括以下内容-
InferShape -InferShape接口具有以下两个目的:
第一个目的是告诉系统每个输入和输出张量的大小,以便可以在Forward和Backward调用之前分配空间。
第二个目的是执行大小检查,以确保在运行之前没有错误。
语法如下-
virtual bool InferShape(mxnet::ShapeVector *in_shape,
mxnet::ShapeVector *out_shape,
mxnet::ShapeVector *aux_shape) const = 0;
请求资源-如果您的系统可以管理诸如cudnnConvolutionForward之类的操作的计算工作区怎么办?您的系统可以执行优化,例如重复使用空间等等。在这里,MXNet可以通过以下两个接口轻松实现此目标-
virtual std::vector ForwardResource(
const mxnet::ShapeVector &in_shape) const;
virtual std::vector BackwardResource(
const mxnet::ShapeVector &in_shape) const;
但是,如果ForwardResource和BackwardResource返回非空数组怎么办?在这种情况下,系统通过Operator的Forward和Backward界面中的ctx参数提供相应的资源。
向后依赖-Apache MXNet具有以下两个不同的运算符签名来处理向后依赖-
void FullyConnectedForward(TBlob weight, TBlob in_data, TBlob out_data);
void FullyConnectedBackward(TBlob weight, TBlob in_data, TBlob out_grad, TBlob in_grad);
void PoolingForward(TBlob in_data, TBlob out_data);
void PoolingBackward(TBlob in_data, TBlob out_data, TBlob out_grad, TBlob in_grad);
这里,需要注意的两个要点-
FullyConnectedBackward不使用FullyConnectedForward中的out_data,并且
PoolingBackward需要PoolingForward的所有参数。
这就是为什么对于FullyConnectedForward可以安全地释放一次消耗的out_data张量,因为向后函数将不需要它。借助该系统,可以尽早收集一些张量作为垃圾。
到位选项-Apache MXNet为用户提供了另一个接口,以节省内存分配成本。该接口适用于元素张量输入和输出张量具有相同形状的元素操作。
以下是用于指定就地更新的语法-
借助OperatorProperty,我们可以创建一个运算符。为此,请遵循以下步骤-
virtual std::vector<:pair void="">> ElewiseOpProperty::ForwardInplaceOption(
const std::vector &in_data,
const std::vector &out_data)
const {
return { {in_data[0], out_data[0]} };
}
virtual std::vector<:pair void="">> ElewiseOpProperty::BackwardInplaceOption(
const std::vector &out_grad,
const std::vector &in_data,
const std::vector &out_data,
const std::vector &in_grad)
const {
return { {out_grad[0], in_grad[0]} }
}
创建运算符
首先在OperatorProperty中实现以下接口:
virtual Operator* CreateOperator(Context ctx) const = 0;
该示例如下-
class ConvolutionOp {
public:
void Forward( ... ) { ... }
void Backward( ... ) { ... }
};
class ConvolutionOpProperty : public OperatorProperty {
public:
Operator* CreateOperator(Context ctx) const {
return new ConvolutionOp;
}
};
参数化运算符
如果要实现卷积运算符,则必须知道内核大小,步幅大小,填充大小等。为什么,因为在调用任何前向或后向接口之前,应将这些参数传递给运算符。
为此,我们需要定义一个ConvolutionParam结构,如下所示:
#include
struct ConvolutionParam : public dmlc::Parameter {
mxnet::TShape kernel, stride, pad;
uint32_t num_filter, num_group, workspace;
bool no_bias;
};
现在,我们需要将其放入ConvolutionOpProperty并将其传递给运算符,如下所示:
class ConvolutionOp {
public:
ConvolutionOp(ConvolutionParam p): param_(p) {}
void Forward( ... ) { ... }
void Backward( ... ) { ... }
private:
ConvolutionParam param_;
};
class ConvolutionOpProperty : public OperatorProperty {
public:
void Init(const vector& kwargs) {
// initialize param_ using kwargs
}
Operator* CreateOperator(Context ctx) const {
return new ConvolutionOp(param_);
}
private:
ConvolutionParam param_;
};
将操作员属性类和参数类注册到Apache MXNet
最后,我们需要将操作员属性类和参数类注册到MXNet。可以在以下宏的帮助下完成-
DMLC_REGISTER_PARAMETER(ConvolutionParam);
MXNET_REGISTER_OP_PROPERTY(Convolution, ConvolutionOpProperty);
在上面的宏中,第一个参数是名称字符串,第二个参数是属性类名称。