自定義運算元高效能開發
自定義運算元高效能開發
在計圖中,一共有三種方法來開發自定義的運算元:
- 使用元運算元進行組合。
- 使用Code運算元開發自定義運算元。
- 使用計圖編譯器編譯自定義的模組和custom op。
其中,元運算元開發是最為簡單的, 但不免有些情況存在元運算元表達能力不足。可以使用Code運算元進行開發,Code運算元在保持了開發的便捷性,還具有很高的可定製性和效能。和方法3相比,Code運算元的開發更加簡單,非常適合使用者構建模型中的創新運算元。
本文主要介紹Code運算元,關於元運算元和自定義模組,參考文件:
• 使用元運算元開發卷積
• 使用計圖編譯器編譯自定義的模組和custom op
Code 運算元的輸入引數
使用Python的help命令(help(jt.code)),可以看到文件如下:
@param[in] shape 輸出的形狀, a integer array
@param[in] dtype 輸出的資料型別
@param[in] inputs 一個計圖變數陣列
@param[in] cpu_src CPU前向程式碼字串,內建變數包括:- in{x}, in{x}_shape{y}, in{x}_stride{y}, in{x}_type, in{x}_p, @in0(…)
- out{x}, out{x}_shape{y}, out{x}_stride{y}, out{x}_type, out{x}_p, @out0(…)
- out, out_shape{y}, out_stride{y}, out_type, out_p, @out(…)
@param[in] cpu_header CPU標頭檔案字串
@param[in] cuda_src CUDA 前向程式碼字串,和上述引數具有同樣的內建變數。
@param[in] cuda_header CUDA標頭檔案字串。
可以看到,使用者需要提供Code運算元的輸入,輸出的形狀和型別,以及對應的程式碼。計圖會通過編譯快取器,讓相同的程式碼只編譯一次。如果希望最大化Code運算元的效能,儘量保證Code運算元的程式碼不會出現過多變種。在Code運算元的程式碼中,使用者可以使用內建變數,訪問計圖的變數。下面將用若干個例項,來介紹Code運算元的使用。
下面的例項中,首先生成了一個隨機的長度為10的變數a,然後計算了 2 a 2 2a^2 2a2 和對應的導數 4 a 4a 4a,在這個例子中使用了@out, @in0,這種C++中沒有的語法,這種語法目的是給使用者提供方便的訪問計圖變數的介面。這種語法在後端會被翻譯成C++可以識別的語法。
from jittor import Function
import jittor as jt
class Func(Function):
def execute(self, x):
self.save_vars = x
return jt.code(x.shape, x.dtype, [x],
cpu_src=’’’
for (int i=0; i<in0_shape0; i++)
@out(i) = @in0(i)*@in0(i)*2;
‘’’)
def grad(self, grad_x):
x = self.save_vars
return jt.code(x.shape, x.dtype, [x, grad_x],
cpu_src='''
for (int i=0; i<in0_shape0; i++)
@out(i) = @in1(i)*@in0(i)*4;
''')
a = jt.random([10])
func = Func()
b = func(a)
print(b)
print(jt.grad(b,a))
例項2:使用stl和alias
下面的例項中,實現了一個簡單的排序演算法,演示瞭如何使用C++演算法庫中排序演算法,以及使用別名alias來增加程式碼的可讀性。
a = jt.array([3,2,1])
b = jt.code(a.shape, a.dtype, [a],
cpu_header="""
#include
@alias(a, in0)
@alias(b, out)
“”",
cpu_src="""
for (int i=0; i<a_shape0; i++)
@b(i) = @a(i);
std::sort(&@b(0), &@b(in0_shape0));
“”"
)
assert (b.data==[1,2,3]).all()
例項3:多輸出的Code運算元
在某些情況下,運算元可能有多個輸出,在這個例項中,演示瞭如何設定多輸出。該運算元輸入為一維向量,輸出為兩個長度為1的向量,分別是最小值和最大值。
同之前例項不同的地方是,原來傳入單個shape和dtype,這裡傳入的是一個shape陣列和dtype陣列。同時還在這個例項中演示瞭如何使用cout。
a = jt.array([3,2,1])
b,c = jt.code([(1,), (1,)], [a.dtype, a.dtype], [a],
cpu_header="""
#include
using namespace std;
“”",
cpu_src="""
@alias(a, in0)
@alias(b, out0)
@alias(c, out1)
@b(0) = @c(0) = @a(0);
for (int i=0; i<a_shape0; i++) {
@b(0) = std::min(@b(0), @a(i));
@c(0) = std::max(@c(0), @a(i));
}
cout << “min:” << @b(0) << " max:" << @c(0) << endl;
“”"
)
assert b.data == 1, b
assert c.data == 3, c
例項4:動態大小的輸出
在某些情況下,運算元的輸出的大小可能是會變化的,比如把輸入中大於0和小於等於0的數,分別緊密排列在兩個向量中。下面的例項就實現了這樣一個運算元。
可以發現下面的陣列的輸出形狀被設定成了負數,這是計圖的特殊機制,傳入負數代表這個陣列的大小是不確定的,負數的絕對值,代表了這個維度最大上限。需要注意的是,動態大小隻能在第一維度出現,而且在演算法最後結束的時候,需要使用set_shape來設定確定的形狀。
a = jt.array([5,-4,3,-2,1])
negtive shape for max size of vary dimension
b,c = jt.code([(-5,), (-5,)], [a.dtype, a.dtype], [a],
cpu_src="""
@alias(a, in0)
@alias(b, out0)
@alias(c, out1)
int num_b=0, num_c=0;
for (int i=0; i<a_shape0; i++) {
if (@a(i)>0)
@b(num_b++) = @a(i);
else
@c(num_c++) = @a(i);
}
b->set_shape({num_b});
c->set_shape({num_c});
“”"
)
assert (b.data == [5,3,1]).all()
assert (c.data == [-4,-2]).all()
綜合例項5:使用Code運算元實現三維點雲K近鄰查詢
下面的例項展示瞭如何使用code運算元,使用數行程式碼實現三維點雲中十分常用的K近鄰查詢。Code運算元的設計和實現,讓使用者既可以享受到Python語言的便捷與易用性,又可以獲得高效能語言的效能。
可以留意到,在計圖的Code運算元中,可以使用openmp實現自動並行化的,關於openmp的使用,可以參考openmp文件。
a = jt.random((n,3))
b = jt.code([n, k], “int32”, [a],
cpu_header="#include “,
cpu_src=”""
using namespace std;
auto n=out_shape0, k=out_shape1;
// 使用openmp實現自動並行化
#pragma omp parallel for
for (int i=0; i<n; i++) {
// 儲存k近鄰的距離和下標
vector<pair<float,int>> id(n);
for (int j=0; j<n; j++) {
auto dx = @in0(i,0)[email protected](j,0);
auto dy = @in0(i,1)[email protected](j,1);
auto dz = @in0(i,2)[email protected](j,2);
id[j] = {dxdx+dydy+dz*dz, j};
}
// 使用c++演算法庫的nth_element排序
nth_element(id.begin(),
id.begin()+k, id.end());
// 將下標輸出到計圖的變數中
for (int j=0; j<k; j++)
@out(i,j) = id[j].second;
}"""
)
將計圖使用code運算元實現的K近鄰查詢,和PyTorch的運算元用時進行比較,速度對比如下(k=10,點雲數量n=[100,1000,10000]):
引數 n=100 n=1000 n=10000
PyTorch 433 µs 7.6 ms 623 ms
Jittor 68 µs 5.9 ms 484 ms
速度對比 6.4X 1.29X 1.29X
注:此處使用的K近鄰演算法為暴力演算法,還存在更優的演算法實現,由於文章篇幅有限,此處僅用於展示Code運算元的使用。
例項6:使用CUDA進行加速
在這個例項中,使用CUDA實現了簡單的兩個2維向量相乘。並且反向傳播對應的導數。
這個例項與之前的區別,定義了CUDA kernel,這需要使用者有一定的CUDA基礎。這裡面的@ARGS_DEF,@ARGS分別是CUDA kernel函式的引數宣告和引數傳遞,而@PRECALC包含了計圖預處理核心的程式碼。除此之外,其他語法和CUDA保持高度一致。
import jittor as jt
from jittor import Function
jt.flags.use_cuda = 1
class Func(Function):
def execute(self, a, b):
self.save_vars = a, b
return jt.code(a.shape, a.dtype, [a,b],
cuda_src=’’’
global static void kernel1(@ARGS_DEF) {
@PRECALC
for (int i=blockIdx.x; i<in0_shape0; i+=gridDim.x)
for (int j=threadIdx.x; j<in0_shape1; j+=blockDim.x)
@out(i,j) = @in0(i,j)*@in1(i,j);
}
kernel1<<<32, 32>>>(@ARGS);
‘’’)
def grad(self, grad):
a, b = self.save_vars
return jt.code([a.shape, b.shape], [a.dtype, b.dtype], [a, b, grad],
cuda_src='''
__global__ static void kernel2(@ARGS_DEF) {
@PRECALC
for (int i=blockIdx.x; i<in0_shape0; i+=gridDim.x)
for (int j=threadIdx.x; j<in0_shape1; j+=blockDim.x) {
@out0(i,j) = @in2(i,j)*@in1(i,j);
@out1(i,j) = @in2(i,j)*@in0(i,j);
}
}
kernel2<<<32, 32>>>(@ARGS);
''')
a = jt.random((100,100))
b = jt.random((100,100))
func = Func()
c = func(a,b)
print©
print(jt.grad(c, [a, b]))
綜合例項7:實現可以同時在GPU和CPU上執行的Pool演算法
注:計圖內部已經實現了Pool,使用者不需要自己實現
import jittor as jt
from jittor import Function
jt.flags.use_cuda = 1
class Func(Function):
def execute(self, x):
out = jt.code([N,C,h,w], x.dtype, [x],
cuda_src=f’’’
global static void kernel1(@ARGS_DEF) {{
@PRECALC
int p3 = threadIdx.x;
int s3 = blockDim.x;
int p2 = threadIdx.y + blockIdx.x * blockDim.y;
int s2 = blockDim.y * gridDim.x;
int i1 = blockIdx.y;
int i0 = blockIdx.z;
for (int i3 = p3; i3 < out_shape3; i3 += s3)
for (int i2 = p2; i2 < out_shape2; i2 += s2) {{
int k3 = i3*{stride}-{padding};
int k2 = i2*{stride}-{padding};
int k3_ = min(k3 + {kernel_size}, in0_shape3);
int k2_ = min(k2 + {kernel_size}, in0_shape2);
k3 = max(0, k3);
k2 = max(0, k2);
@out(i0, i1, i2, i3) = @in0(i0, i1, k2, k3);
for (int p = k2; p < k2_; ++p)
for (int q = k3; q < k3_; ++q)
@out(i0, i1, i2, i3) = {op}(@out(i0, i1, i2, i3), @in0(i0, i1, p, q));
}}
}}
int tx = min(1024, out_shape3);
int ty = min(1024 / tx, out_shape2);
int bx = (out_shape2 - 1) / ty + 1;
int by = out_shape1;
int bz = out_shape0;
dim3 s1(bx, by, bz);
dim3 s2(tx, ty);
kernel1<<<s1, s2>>>(@ARGS);
‘’’,
cpu_src=f’’’
for (int i0=0; i0<out_shape0; i0++)
for (int i1=0; i1<out_shape1; i1++)
for (int i2=0; i2<out_shape2; i2++)
for (int i3=0; i3<out_shape3; i3++) {{
int k2 = i2*{stride}-{padding};
int k3 = i3*{stride}-{padding};
int k2_ = std::min(k2 + {kernel_size}, in0_shape2);
int k3_ = std::min(k3 + {kernel_size}, in0_shape3);
k2 = std::max(0, k2);
k3 = std::max(0, k3);
@out(i0, i1, i2, i3) = @in0(i0, i1, k2, k3);
for (int p = k2; p < k2_; ++p)
for (int q = k3; q < k3_; ++q)
@out(i0, i1, i2, i3) = std::{op}(@out(i0, i1, i2, i3), @in0(i0, i1, p, q));
}}
‘’’)
self.save_vars = x, out
return out
def grad(self, grad_x):
x, pout = self.save_vars
return jt.code(x.shape, x.dtype, [x, pout, grad_x],
cuda_header=f'''
@alias(pout, in1);
''',
cuda_src=f'''
__global__ static void kernel3(@ARGS_DEF) {{
@PRECALC
int p3 = threadIdx.x;
int s3 = blockDim.x;
int p2 = threadIdx.y + blockIdx.x * blockDim.y;
int s2 = blockDim.y * gridDim.x;
int i1 = blockIdx.y;
int i0 = blockIdx.z;
for (int i3 = p3; i3 < pout_shape3; i3 += s3)
for (int i2 = p2; i2 < pout_shape2; i2 += s2) {{
int k3 = i3*{stride}-{padding};
int k2 = i2*{stride}-{padding};
int k3_ = min(k3 + {kernel_size}, in0_shape3);
int k2_ = min(k2 + {kernel_size}, in0_shape2);
k3 = max(0, k3);
k2 = max(0, k2);
int bo=1;
for (int p = k2; p < k2_ && bo; ++p)
for (int q = k3; q < k3_ && bo; ++q) {{
if (@pout(i0,i1,i2,i3) == @in0(i0,i1,p,q)) {{
atomicAdd(&@out(i0,i1,p,q), @in2(i0,i1,i2,i3));
bo=0;
}}
}}
}}
}}
cudaMemsetAsync(out_p, 0, out->size);
int tx = min(1024, pout_shape3);
int ty = min(1024 / tx, pout_shape2);
int bx = (pout_shape2 - 1) / ty + 1;
int by = pout_shape1;
int bz = pout_shape0;
dim3 s1_(bx, by, bz);
dim3 s2_(tx, ty);
kernel3<<<s1_, s2_>>>(@ARGS);
''',
cpu_src=f'''
@alias(pout, in1);
for (int i=0; i<out_shape0; i++)
for (int j=0; j<out_shape1; j++)
for (int k=0; k<out_shape2; k++)
for (int l=0; l<out_shape3; l++) @out(i,j,k,l) = 0;
for (int i0=0; i0<pout_shape0; i0++)
for (int i1=0; i1<pout_shape1; i1++)
for (int i2=0; i2<pout_shape2; i2++)
for (int i3=0; i3<pout_shape3; i3++) {{
int k3 = i3*{stride}-{padding};
int k2 = i2*{stride}-{padding};
int k3_ = std::min(k3 + {kernel_size}, in0_shape3);
int k2_ = std::min(k2 + {kernel_size}, in0_shape2);
k3 = std::max(0, k3);
k2 = std::max(0, k2);
int bo=1;
for (int p = k2; p < k2_ && bo; ++p)
for (int q = k3; q < k3_ && bo; ++q) {{
if (@pout(i0,i1,i2,i3) == @in0(i0,i1,p,q)) {{
@out(i0,i1,p,q) += @in2(i0,i1,i2,i3);
bo=0;
}}
}}
}}
''')
N,C,H,W = [2,10,100,100]
stride = 2
padding = 0
kernel_size = 3
op = “max”
x = jt.random((N,C,H,W))
h = (H+padding2-kernel_size)//stride+1
w = (W+padding2-kernel_size)//stride+1
func = Func()
out = func(x)
print(out)
print(jt.grad(out, x))