这可能是创建自定义网络层最简单的方式 - MobulaOP使用说明

大家好,我想在这里给大家介绍我的一个项目:MobulaOP.

MobulaOP是一个简单且灵活的跨框架算子创建工具包。不需要重新编译深度学习框架的源码,就可以创建自定义的C++算子。而且只需要一份C++代码实现和简单的定义,自定义算子就可以在CPU和GPU上运行。

之所以建立这个项目,是因为我发现MXNet创建自定义算子的方法不太方便,其他深度学习框架也同样存在这个问题。

当前,创建自定义算子的方法主要有:

    1. 重新编译深度学习框架的源码
      重新编译源码耗时过长。需要了解对应框架的算子实现形式,编写出的代码不适用于其他框架。
    1. 使用运行时编译(Run-Time Compilation)API
      需要编写对应的CUDA代码,编写过程较复杂,无法在CPU环境下进行调试。
    1. 加载动态文件
      需要了解对应框架的动态加载实现形式,编写较复杂,一份代码不适用于多个框架。

因此,我设计了MobulaOP项目,希望能解决上述问题。

MobulaOP项目当前的特性有:

    1. 项目实现精简,不需要重新编译深度学习框架,就可以实现自定义的C++ operator;
    1. 只需要编写一份代码,就可以让自定义算子运行在不同设备(CPU/GPU),以及不同的深度学习框架(如MXNet, PyTorch)或数值计算库NumPy上;
    1. 在编写自定义层的过程中,用户有更多的注意力关注在运算的实现上;
    1. 对MXNet有更多的支持,使用MobulaOP可以更方便地创建自定义算子(Custom Operator).

MobulaOP暂时只支持Linux系统,之后会加入对Windows等系统的支持。

下面,我想简单地介绍一下MobulaOP的使用方法。

配置MobulaOP

在终端下输入以下命令:

# 将MobulaOP项目拷贝下来
git clone https://github.com/wkcn/MobulaOP
# 进入项目文件夹
cd MobulaOP
# 安装依赖库numpy, pyyaml和easydict
pip install -r requirements.txt
# 进行编译,如果需要在GPU下使用,在选项中输入y
sh build.sh
# 将MobulaOP文件夹加入PYTHONPATH环境变量中
export PYTHONPATH=$PYTHONPATH:$(pwd)

当执行完以上命令后,在项目目录外打开Python交互界面,输入import mobula,如果没有提示,则表示配置成功。

核函数

配置好MobulaOP后,就可以使用C++编写算子(operator)的运算函数了。

这里把并行计算的运算函数称为核函数

以创建一个逐位乘法算子为例,它的实现为:

template <typename T>
MOBULA_KERNEL mul_elemwise_kernel(const int n, const T* a, const T* b, T* out) {
    parfor(n, [&](int i) {
        out[i] = a[i] * b[i];
    });
}

没错,定义一个逐位乘法函数只需要6行代码,并且它支持在CPU和GPU下运行。

其中,MOBULA_KERNEL宏声明了这个函数是一个核函数。核函数不需要定义返回值,同时核函数的函数名后缀为_kernel.

对于参数列表,MobulaOP要求第一个参数为并行计算的线程数。MobulaOP会自动将参数列表中const T*类型的参数识别为输入数组的指针,将T*类型的参数识别为输出数组的指针。

函数块中,调用了并行执行的parfor循环函数。这个函数的第一个参数为循环体的总迭代数,第二个参数为一个接收迭代下标的函数,这里使用了匿名函数。下标i从0开始计数,满足0 <= i < n。MobulaOP会根据运行设备对parfor进行不同的展开。当这段代码在CPU下运行时,MobulaOP会将这段函数展开为:

for (int i = 0; i < n; ++i) {
    out[i] = a[i] * b[i];
}

MobulaOP会自动地使用多线程、OpenMP、CUDA等方法并行地执行这个循环。

需要注意的是:

  1. MOBULA_KERNEL核函数的第一个参数为调用这个函数进行并行计算的线程数;
  2. 核函数内部语句均为并行执行,编写核函数时要注意线程安全问题。当前,MobulaOP提供了CPU/GPU下单精度浮点数(float32)的atomic_add原子加函数;
  3. 在一个核函数内,允许多次调用parfor函数, 这些parfor的总迭代数可以不同,但实际使用的线程数是相同的;
  4. parfor函数只允许在核函数内部进行调用;
  5. 如果要在核函数中调用其他函数,被调用的函数的声明前需要添加宏MOBULA_DEVICE, 并声明返回值类型。

例子:返回两个数中的最大值

template <typename T>
MOBULA_DEVICE T maximum(const T a, const T b) {
    return a >= b ? a : b;
}

执行核函数

接下来,使用MobulaOP执行上述核函数。

MobulaOP能够自动分析、生成代码,并调用编译器将代码编译为动态链接库。

把上述核函数保存为MulElemWise.cpp文件,放在如下的文件目录结构中:

tutorial
└── MulElemWise
    └─── MulElemWise.cpp

tutorial文件夹下创建test_mul_func.py文件,在这个文件中编写Python代码:

import mobula
mobula.op.load('MulElemWise')

import mxnet as mx
a = mx.nd.array([1,2,3])
b = mx.nd.array([4,5,6])
out = mx.nd.empty(a.shape)
mobula.func.mul_elemwise(a.size, a, b, out)
print (out)  # [4, 10, 18]

在终端中输入python test_mul_func.py即可执行。

这段代码中,与MobulaOP相关的一共有三行(第1、2、8行)

第1行代码导入MobulaOP包。

第2行代码加载MulElemWise模块。MobulaOP会搜索MulElemWise文件夹中是否存在同名的.cpp.py文件,以及__init__.py文件。若找到这些文件,将会对文件进行编译或加载。mobula.op.load也支持指定搜索目录,如mobula.op.load('MulElemWise', os.path.dirname(__file__)).

第8行调用核函数mul_elemwise,与函数声明MOBULA_KERNEL mul_elemwise_kernel(const int n, const T* a, const T* b, T* out)相比,在Python中调用的函数名比C++中的函数名少了后缀_kernel. MobulaOP把加载后的核函数添加到mobula.func中,调用mobula.func.<核函数名>即可调用C++函数。MobulaOP能够自动对参数进行处理, 包括获取数据指针、选择参数模板、处理内存非连续数组、根据参数的输入输出类型自动调用wait_to_readwait_to_write函数等。

创建自定义算子(operator)

如何将核函数封装成一个算子(operator)呢,MobulaOP提供了一个简单的声明方法。
tutorial/MulElemWise文件夹下创建文件MulElemWise.py, 输入以下代码:

import mobula

@mobula.op.register
class MulElemWise:
    def forward(self, a, b):
        mobula.func.mul_elemwise(a.size, a, b, self.y)
    def backward(self, dy):
        self.dX[0][:] = self.F.multiply(dy, self.X[1])
        mobula.func.mul_elemwise(dy.size, dy, self.X[0], self.dX[1])
    def infer_shape(self, in_shape):
        assert in_shape[0] == in_shape[1]
        return in_shape, [in_shape[0]]

第3行的@mobula.op.register为一个Python装饰器,它将其下面的类注册为算子。

一个算子类需要定义forward, backward以及infer_shape函数。

forward函数的参数列表中,ab是算子前向传播的输入;在backward函数的参数列表中,dy为算子后向传播时输入的导数。

MobulaOP会根据forward函数得到算子的输入个数和名称,根据backward得到输出个数。

infer_shape函数传入的是元组(tuple)的列表,分别表示各输入的尺寸(shape). infer_shape的返回值有两个值,第一个值是各个输入的尺寸,第二个值是各个输出的尺寸。infer_shape和MXNet自定义层里的infer_shape是相似的。

在算子的forwardbackward函数中,定义了一些变量:

变量名 描述
self.F 当前环境。假如使用MXNet, self.F = mx.nd
self.X[k] 第k个输入
self.Y[k] 第k个输出
self.dX[k] 第k个输入的导数
self.dY[k] 第k个输出的导数
self.x 第1个输入
self.y 第1个输出
self.dx 第1个输入的导数
self.dy 第1个输出的导数
self.req[k] 第k个输入/输出的处理模式(null/write/add/replace)

值得注意的是,当使用一个数组或数字对另一个数组赋值时,被赋值的变量后面需要加上[:],如self.X[0][:] = data

我们也可以使用内置的assign函数进行赋值,如self.assign(self.X[0], self.req[0], data), 这里的assign函数和MXNet是一致的。

测试自定义算子

编写好MulElemWise算子的定义后,来测试一下吧。

tutorial文件夹下创建文件test_mul_op.py, 输入代码:

import mobula
mobula.op.load('MulElemWise')

import mxnet as mx
a = mx.nd.array([1,2,3])
b = mx.nd.array([4,5,6])

a.attach_grad()
b.attach_grad()
with mx.autograd.record():
    c = mobula.op.MulElemWise(a, b)
    c.backward()
    print (c)  # [4, 10, 18]
    print ('a.grad = {}'.format(a.grad.asnumpy()))  # [4, 5, 6] 
    print ('b.grad = {}'.format(b.grad.asnumpy()))  # [1, 2, 3] 

同样,在终端输入python test_mul_op.py指令执行。

这里与MobulaOP有关的新代码是第11行: c = mobula.op.MulElemWise(a, b)

MobulaOP加载MulElemWise模块后,分析了MulElemWise文件夹下的MulElemWise.cpp文件,把核函数注册到mobula.func中;同时加载同一个文件夹下的MulElemWise.py文件,将算子注册到mobula.op中。这个过程没有发生编译。

mobula.op.MulElemWise(a, b)执行时,MobulaOP会根据变量类型,自动编译所需要的动态链接库,并返回结果。

mobula.op.MulElemWise也可以接受MXNet的符号(Symbol)、Numpy数组或PyTorch Tensor.

例子:
MXNet的符号(Symbol):

a_sym = mx.sym.Variable('a')
b_sym = mx.sym.Variable('b')
c_sym = mobula.op.MulElemWise(a_sym, b_sym)

Numpy数组:

a_np = np.array([1,2,3])
b_np = np.array([4,5,6])
# 由于Numpy不支持记录梯度,因此需要一个实例记录梯度
op = mobula.op.MulElemWise[np.ndarray]()
c_np = op(a_np, b_np)

如何在Gluon内使用MobulaOP定义的算子呢?

我们可以这样写:

class MulElemWiseBlock(mx.gluon.nn.HybridBlock):
    def hybrid_forward(self, F, a, b):
        return mobula.op.MulElemWise(a, b)

这就是MobulaOP的简单使用介绍,上述代码可以在项目的文档部分(docs)查看

希望MobulaOP能够对大家有帮助。

同时,欢迎大家对MobulaOP项目提Issue和PR. 谢谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • 该文章为转载文章,作者简介:汪剑,现在在出门问问负责推荐与个性化。曾在微软雅虎工作,从事过搜索和推荐相关工作。 T...
    名字真的不重要阅读 5,208评论 0 3
  • 妈妈家搬家的时候,整理了满满一大箱的日记、挚友的来信、带祝福语的卡片还有些许照片,在那个年代偶尔留下的照片也...
    剑秋o阅读 282评论 5 6
  • 太宰治的《人间失格》里有这么一段话: 人啊,明明一点儿也不了解对方,错看对方,却视彼此为独一无二的挚友。一生不解对...
    君子喵阅读 572评论 0 0
  • 我要去爱你 爱到你羞愧 把你的过去 爱得面目全非
    梦骚阅读 199评论 0 0
  • 慵懒的午后 嘈杂的车流 心里全是想你的忧愁 不晓得已过了多久 手边的咖啡早已凉透 也不知过了多少个年头 夜夜梦中还...
    刘宜阅读 174评论 0 0