From 20e7f36ee7b59b2155ff83356b9cab399672d891 Mon Sep 17 00:00:00 2001 From: Lihan Date: Sat, 23 Sep 2023 23:40:26 -0500 Subject: [PATCH 1/8] fusion rules --- batch/opt/fusion_rules.py | 203 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 batch/opt/fusion_rules.py diff --git a/batch/opt/fusion_rules.py b/batch/opt/fusion_rules.py new file mode 100644 index 0000000..c90c424 --- /dev/null +++ b/batch/opt/fusion_rules.py @@ -0,0 +1,203 @@ +from core.ir import * +from batch.ast import * +from batch.ast2ir import * +import codegen + +# def loop_idx_change(expr, ori_iter_list, iter_list): +# # count = 0 +# if isinstance(expr, Loop): +# for i in expr.body: +# loop_idx_change(i, ori_iter_list, iter_list) +# if isinstance(expr, Assignment): +# loop_idx_change(expr.lhs, ori_iter_list, iter_list) +# loop_idx_change(expr.rhs, ori_iter_list, iter_list) +# if isinstance(expr, Expr): +# loop_idx_change(expr.left, ori_iter_list, iter_list) +# loop_idx_change(expr.right, ori_iter_list, iter_list) +# while isinstance(expr, Index): +# try: +# print("index:::", expr.index.__name__, ori_iter_list) +# for idx, item in enumerate(ori_iter_list): +# if expr.index.__name__ == item.__name__: +# expr.index.__name__ = iter_list[idx].__name__ +# expr = expr.dobject +# except: +# expr = expr.dobject +# break + +# def fuse_elementwise(node): + + +# eval_len = len(node.eval.size) +# temp = node.operators[0].compute[0] +# print("operator0:",codegen.cpu.to_string(temp)) +# print("operator1:",codegen.cpu.to_string(node.operators[1].compute[0])) +# loop_count = 0 +# while isinstance(temp, Loop): +# if loop_count == eval_len: +# break +# temp = temp.body[0] +# loop_count += 1 +# lhs = node.operators[0].eval +# rhs = node.operators[1].eval +# res = node.eval + +# t = node.operators[0].compute[0] +# pre_loop = None +# iter_list = [] +# ori_iter_list = [] +# for i in range(eval_len): +# i_loop = Loop(0, node.eval.size[i], 1, []) +# lhs = bind(lhs, i_loop.iterate) +# rhs = bind(rhs, i_loop.iterate) +# res = bind(res, i_loop.iterate) +# iter_list.append(i_loop.iterate) +# ori_iter_list.append(t.iterate) +# t = t.body[0] +# if i == 0: +# pre_loop = i_loop +# t_loop = i_loop +# else: +# t_loop.body.append(i_loop) +# t_loop = i_loop +# assign = Assignment(res, Expr(lhs, rhs, '+')) +# loop_idx_change(temp, ori_iter_list, iter_list) + +# i_loop.body.append(temp) +# i_loop.body.append(assign) +# node.operators[0].compute.clear() +# node.compute = [pre_loop] +# # ir = [] +# # codegen.cpu.gen_cpp(node, ir) +# # code = '' +# # for d in ir: +# # if d: +# # code += codegen.cpu.to_string(d) +# # print(code) + +# def fuse_innerprod(node): +# temp = fused_loop = node.operators[0].compute[0] +# ori_iter_list = [] +# while isinstance(temp, Loop): +# ori_iter_list.append(temp.iterate) +# temp = temp.body[0] + +# pre_loop = Loop(0, node.eval.size[0], 1, []) +# node.compute = [pre_loop] +# lhs = bind(node.operators[0].eval, pre_loop.iterate) +# rhs = bind(node.operators[1].eval, pre_loop.iterate) +# res = bind(node.eval, pre_loop.iterate) +# inner_loop = Loop(0, node.operators[0].eval.size[1], 1, []) +# pre_loop.body.append(inner_loop) +# lhs = bind(lhs, inner_loop.iterate) +# rhs = bind(rhs, inner_loop.iterate) +# assign = Assignment(res, Expr(lhs, rhs, '*'), '+') +# loop_idx_change(temp.lhs,ori_iter_list, [pre_loop.iterate, inner_loop.iterate]) +# inner_loop.body.append(temp) +# inner_loop.body.append(assign) +# node.operators[0].compute.clear() + +def loop_idx_change(oloop, fusedloop): + # if isinstance(oloop, Loop): + # print('oloop:', codegen.cpu.to_string(oloop), oloop.iterate) + # if isinstance(fusedloop[0], Loop) : + # print('iloop 0 :', codegen.cpu.to_string(fusedloop[0]), fusedloop[0].iterate) + # if len(fusedloop) > 1: + # print('iloop 1 :', codegen.cpu.to_string(fusedloop[1])) + # print(fusedloop, codegen.cpu.to_string(fusedloop[0])) + for iloop in fusedloop: + while (isinstance(oloop, Loop) and isinstance(iloop, Loop)) and oloop.start == iloop.start and oloop.end.__name__ == iloop.end.__name__ and oloop.step == iloop.step: + # iloop.iterate = oloop.iterate + iloop.iterate.__name__ = oloop.iterate.__name__ + iloop.iterate = oloop.iterate + oloop = oloop.body[0] + iloop = iloop.body + + loop_idx_change(oloop, iloop) + +def get_same_loop(outloop, fusedloop): + pre_o = outloop + pre_i = fusedloop + oloop = outloop.body[0] + iloop = fusedloop.body[0] + + loop_idx_change(outloop, [fusedloop]) + # print(fusedloop.body[0].body, fusedloop.body[0].iterate) + # print("loop body:::",codegen.cpu.to_string(fusedloop.body[0])) + + # for i in fusedloop.body[0].body: + # print(codegen.cpu.to_string(i)) + # pre_i.iterate.__name__ = pre_o.iterate.__name__ + while (isinstance(oloop, Loop) and isinstance(iloop, Loop)) and oloop.start == iloop.start and oloop.end.__name__ == iloop.end.__name__ and oloop.step == iloop.step: + + pre_o = oloop + pre_i = iloop + # print(codegen.cpu.to_string(pre_i)) + # pre_i.iterate.__name__ = pre_o.iterate.__name__ + # print(codegen.cpu.to_string(iloop)) + oloop = oloop.body[0] + iloop = iloop.body[0] + + return pre_o, pre_i + +def fuse_elementwise(ast): + if type(ast.operators[0]) == BatchOp: + fuse_elementwise(ast.operators[0]) + if type(ast.operators[1]) == BatchOp: + fuse_elementwise(ast.operators[1]) + + if type(ast) == Batch or not ast.op_type in core.ast.op_mapping: + return + + if ast.operators[1].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[1].compute[0] + + oloop, iloop = get_same_loop(outer_loop, loop) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + iloop.body.clear() + ast.operators[1].compute.clear() + + if ast.operators[0].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[0].compute[0] + # print("outer loop body:: ",codegen.cpu.to_string(loop)) + oloop, iloop = get_same_loop(outer_loop, loop) + + # print("loop body:::",codegen.cpu.to_string(iloop), iloop.body) + # print("assign: ", codegen.cpu.to_string(iloop.body[0]), codegen.cpu.to_string(iloop.body[0])) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + iloop.body.clear() + ast.operators[0].compute.clear() + + +def fuse_innerprod(ast): + if type(ast.operators[0]) == BatchOp: + fuse_innerprod(ast.operators[0]) + if type(ast.operators[1]) == BatchOp: + fuse_innerprod(ast.operators[1]) + + if type(ast) == Batch or not ast.op_type == 'vec_mul_vec': + return + + if ast.operators[1].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[1].compute[0] + + oloop, iloop = get_same_loop(outer_loop, loop) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + iloop.body.clear() + ast.operators[1].compute.clear() + + if ast.operators[0].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[0].compute[0] + + oloop, iloop = get_same_loop(outer_loop, loop) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + iloop.body.clear() + ast.operators[0].compute.clear() \ No newline at end of file From 8e59a613e54e4c72b35adff7f6aaa66c1d7c53ec Mon Sep 17 00:00:00 2001 From: Lihan Date: Tue, 26 Sep 2023 19:50:58 -0500 Subject: [PATCH 2/8] fusion rules added --- batch/opt/fusion_rules.py | 141 ++++++++++++++++++++++++++++++++------ 1 file changed, 121 insertions(+), 20 deletions(-) diff --git a/batch/opt/fusion_rules.py b/batch/opt/fusion_rules.py index c90c424..ed184fa 100644 --- a/batch/opt/fusion_rules.py +++ b/batch/opt/fusion_rules.py @@ -121,31 +121,52 @@ def get_same_loop(outloop, fusedloop): oloop = outloop.body[0] iloop = fusedloop.body[0] - loop_idx_change(outloop, [fusedloop]) - # print(fusedloop.body[0].body, fusedloop.body[0].iterate) - # print("loop body:::",codegen.cpu.to_string(fusedloop.body[0])) - - # for i in fusedloop.body[0].body: - # print(codegen.cpu.to_string(i)) - # pre_i.iterate.__name__ = pre_o.iterate.__name__ - while (isinstance(oloop, Loop) and isinstance(iloop, Loop)) and oloop.start == iloop.start and oloop.end.__name__ == iloop.end.__name__ and oloop.step == iloop.step: + pre_iter_i = [] + pre_iter_o = [] + pre_iter_i.append(pre_i.iterate) + pre_iter_o.append(pre_o.iterate) + while (isinstance(oloop, Loop) and isinstance(iloop, Loop)) and oloop.start == iloop.start and oloop.end.__name__ == iloop.end.__name__ and oloop.step == iloop.step: pre_o = oloop pre_i = iloop - # print(codegen.cpu.to_string(pre_i)) - # pre_i.iterate.__name__ = pre_o.iterate.__name__ - # print(codegen.cpu.to_string(iloop)) + pre_iter_i.append(pre_i.iterate) + pre_iter_o.append(pre_o.iterate) + oloop = oloop.body[0] iloop = iloop.body[0] - return pre_o, pre_i + return pre_o, pre_i, pre_iter_o, pre_iter_i + +def change_index(iassign, iter_o, iter_i): + if isinstance(iassign, Index): + for idx, item in enumerate(iter_i): + if iassign.index == item: + iassign.index = iter_o[idx] + if isinstance(iassign.dobject, Index): + change_index(iassign.dobject, iter_o, iter_i) + + elif isinstance(iassign, Expr): + # both item.left and item.right + change_index(iassign.left, iter_o, iter_i) + change_index(iassign.right, iter_o, iter_i) + elif isinstance(iassign, Assignment): + # both item.lhs and item.rhs + change_index(iassign.lhs, iter_o, iter_i) + change_index(iassign.rhs, iter_o, iter_i) + elif isinstance(iassign, Loop): + # oloop.body[0] and item.body + for i in iassign.body: + change_index(i, iter_o, iter_i) + + def fuse_elementwise(ast): + if type(ast.operators[0]) == BatchOp: fuse_elementwise(ast.operators[0]) if type(ast.operators[1]) == BatchOp: fuse_elementwise(ast.operators[1]) - + if type(ast) == Batch or not ast.op_type in core.ast.op_mapping: return @@ -153,7 +174,9 @@ def fuse_elementwise(ast): outer_loop = ast.compute[0] loop = ast.operators[1].compute[0] - oloop, iloop = get_same_loop(outer_loop, loop) + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) for i in range(len(iloop.body)): oloop.body.insert(i, iloop.body[i]) iloop.body.clear() @@ -162,13 +185,13 @@ def fuse_elementwise(ast): if ast.operators[0].compute and ast.compute: outer_loop = ast.compute[0] loop = ast.operators[0].compute[0] - # print("outer loop body:: ",codegen.cpu.to_string(loop)) - oloop, iloop = get_same_loop(outer_loop, loop) - # print("loop body:::",codegen.cpu.to_string(iloop), iloop.body) - # print("assign: ", codegen.cpu.to_string(iloop.body[0]), codegen.cpu.to_string(iloop.body[0])) + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) for i in range(len(iloop.body)): oloop.body.insert(i, iloop.body[i]) + iloop.body.clear() ast.operators[0].compute.clear() @@ -185,19 +208,97 @@ def fuse_innerprod(ast): if ast.operators[1].compute and ast.compute: outer_loop = ast.compute[0] loop = ast.operators[1].compute[0] + + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + + iloop.body.clear() + ast.operators[1].compute.clear() + + if ast.operators[0].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[0].compute[0] + + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + + iloop.body.clear() + ast.operators[0].compute.clear() + + +def fuse_bsv(ast): + print(type(ast.operators[0]), type(ast.operators[1]), type(ast), ast.op_type) + if type(ast.operators[0]) == BatchOp: + fuse_bsv(ast.operators[0]) + if type(ast.operators[1]) == BatchOp: + fuse_bsv(ast.operators[1]) - oloop, iloop = get_same_loop(outer_loop, loop) + if type(ast) == Batch or not ast.op_type == 'scal_mul_vec': + return + print(ast.op_type, ast.operators[0].compute , ast.compute) + if ast.operators[1].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[1].compute[0] + + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) for i in range(len(iloop.body)): oloop.body.insert(i, iloop.body[i]) + iloop.body.clear() ast.operators[1].compute.clear() if ast.operators[0].compute and ast.compute: outer_loop = ast.compute[0] loop = ast.operators[0].compute[0] + + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + + iloop.body.clear() + ast.operators[0].compute.clear() + + +def fuse_bvm(ast): + if type(ast.operators[0]) == BatchOp: + fuse_bvm(ast.operators[0]) + if type(ast.operators[1]) == BatchOp: + fuse_bvm(ast.operators[1]) - oloop, iloop = get_same_loop(outer_loop, loop) + if type(ast) == Batch or not ast.op_type == 'vec_mul_mat': + return + if ast.operators[1].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[1].compute[0] + + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + + iloop.body.clear() + ast.operators[1].compute.clear() + + if ast.operators[0].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[0].compute[0] + + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) for i in range(len(iloop.body)): oloop.body.insert(i, iloop.body[i]) + iloop.body.clear() ast.operators[0].compute.clear() \ No newline at end of file From c2004f7d47a91fcd1ac1554ccec37b165320244b Mon Sep 17 00:00:00 2001 From: Lihan Date: Thu, 5 Oct 2023 15:23:50 -0500 Subject: [PATCH 3/8] gpu template --- batch/ast.py | 19 +- batch/ast2ir.py | 28 +- .../__pycache__/fusion_rules.cpython-310.pyc | Bin 0 -> 13676 bytes batch/opt/__pycache__/ir.cpython-310.pyc | Bin 0 -> 1297 bytes .../__pycache__/parallelism.cpython-310.pyc | Bin 0 -> 911 bytes batch/opt/fusion_rules.py | 855 +++++++++++++----- batch/opt/ir.py | 24 + batch/opt/parallelism.py | 34 + batch/test/kge.py | 85 +- codegen/gpu.py | 133 +++ codegen/gpu_template.cu | 16 + 11 files changed, 945 insertions(+), 249 deletions(-) create mode 100644 batch/opt/__pycache__/fusion_rules.cpython-310.pyc create mode 100644 batch/opt/__pycache__/ir.cpython-310.pyc create mode 100644 batch/opt/__pycache__/parallelism.cpython-310.pyc create mode 100644 batch/opt/ir.py create mode 100644 batch/opt/parallelism.py create mode 100644 codegen/gpu.py create mode 100644 codegen/gpu_template.cu diff --git a/batch/ast.py b/batch/ast.py index c02a76d..dd5fcc9 100644 --- a/batch/ast.py +++ b/batch/ast.py @@ -17,16 +17,22 @@ def __init__(self, base): self.base = base self.base.ref_count += 1 - self.batch_size = size[0] + if (len(size) == 2): self.item_type = 'vec' self.dim = size[1] + self.batch_size = size[0] elif len(size) == 3: self.item_type = 'mat' self.dim1 = size[1] self.dim2 = size[2] + self.batch_size = size[0] elif len(size) == 1: self.item_type = 'scal' + self.batch_size = size[0] + elif len(size) == 0: + self.item_type = 'const' + self.batch_size = 0 else: raise TypeError('Batch item type not supported') @@ -69,8 +75,12 @@ def bvm(v1: Batch, v2: Batch): assert v1.dim == v2.dim1 return BatchOp('vec_mul_mat', v1, v2) +def bov(v1: Batch, v2: Batch): + assert v1.item_type == 'vec' and v2.item_type == 'vec' + return BatchOp('vec_outer_vec', v1, v2) + class BatchOp(Batch): - Types = ['scal_mul_vec', 'vec_mul_vec', 'vec_mul_mat'] + list(core.ast.op_mapping.keys()) + Types = ['scal_mul_vec', 'vec_mul_vec', 'vec_mul_mat', 'vec_outer_vec'] + list(core.ast.op_mapping.keys()) def __init__(self, op_type, *operators): assert op_type in BatchOp.Types @@ -128,6 +138,11 @@ def __init__(self, op_type, *operators): res = Tensor(name, (bsize, dim), dtype) super().__init__(res) + elif op_type == 'vec_outer_vec': + bsize = self.operators[0].batch_size + res = Tensor(name, (bsize, self.operators[0].dim, self.operators[1].dim ), dtype) + super().__init__(res) + else: # TODO: complete other ops pass diff --git a/batch/ast2ir.py b/batch/ast2ir.py index 89ca85e..a6ae683 100644 --- a/batch/ast2ir.py +++ b/batch/ast2ir.py @@ -1,3 +1,5 @@ +import sys +sys.path.append('/data/backed_up/lihhu/CUKE/cuke') from batch.ast import * from core.ast2ir import * @@ -23,6 +25,7 @@ def gen_ir(node): node.operators[0]._gen_ir() node.operators[1]._gen_ir() node.base._gen_ir() + # print(node.operators[1].eval, node.op_type, node.base.compute) node.eval = node.base.eval node.decl = node.base.decl[:] node.compute = node.base.compute[:] @@ -45,7 +48,6 @@ def gen_ir(node): pre_loop.body.append(inner_loop) lhs = bind(lhs, inner_loop.iterate) rhs = bind(rhs, inner_loop.iterate) - assign = Assignment(res, Expr(lhs, rhs, '*'), '+') inner_loop.body.append(assign) @@ -92,7 +94,31 @@ def gen_ir(node): assign = Assignment(res, Expr(lhs, rhs, '*'), '+') loop2.body.append(assign) + + elif node.op_type == 'vec_outer_vec': + assert is_bvec(node.operators[0]) and is_bvec(node.operators[1]) + node.operators[0]._gen_ir() + node.operators[1]._gen_ir() + size = helpers.get_ir_of_size(node._size()) + node.base.eval = node.eval = Ndarray(node.dtype, size) + node.decl = [Decl(node.eval)] + pre_loop = Loop(0, node.eval.size[0], 1, []) + node.compute = [pre_loop] + lhs = bind(node.operators[0].eval, pre_loop.iterate) + rhs = bind(node.operators[1].eval, pre_loop.iterate) + res = bind(node.eval, pre_loop.iterate) + loop1 = Loop(0, node.eval.size[1], 1, []) + pre_loop.body.append(loop1) + lhs = bind(lhs, loop1.iterate) + res = bind(res, loop1.iterate) + loop2 = Loop(0, node.eval.size[2], 1, []) + loop1.body.append(loop2) + + rhs = bind(rhs, loop2.iterate) + res = bind(res, loop2.iterate) + assign = Assignment(res, Expr(lhs, rhs, '*')) + loop2.body.append(assign) return node diff --git a/batch/opt/__pycache__/fusion_rules.cpython-310.pyc b/batch/opt/__pycache__/fusion_rules.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f58c1d3a3c8b7ee92de4d1fffdbef584ce50ed8 GIT binary patch literal 13676 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%0DPb|Z&ozI>1XFxWv0y9^s0HQ?TgRsi#Vlxq#5on zd*^<~ARAgIRNid>$#?a2oZ1d+V9^gKUulbz>Uk#d)Ql?J4_CZQf^79!$Oa8@|TtRG)nSm z1|nS1$vmqpbkOxG7vL7fs3l}o4IyHZceszGNUscmY!mI`4#NECwK!vV`~fxGHKuys z;t5mhF9-Q#TWA;5F*=_K?|Cf~4mU9Lr!Epq@eA5l@{`VN>3JI_5^mE}QUrGit}SyF zv(#X}eZ-bIP~_So;T~oe3p%>CAh?g8!UF=j1@Lg@dFq=?vb@qq=*&wwnwJ7=SEtSA zu4BTNMNbZh{Z+NxJ|3q3EtpyWUT}-n`xmWGaLNiz^ID1Z@Zr?c0?bAR{$BaM`!ay? c3&%by*+tK%7y1QVnu=ar#M+|WY|D=P1xelIwEzGB literal 0 HcmV?d00001 diff --git a/batch/opt/__pycache__/parallelism.cpython-310.pyc b/batch/opt/__pycache__/parallelism.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d7fd6d5a6998a950a2ffbea1dde29c1e7c85ad48 GIT binary patch literal 911 zcmYjOO=}ZD7@nD(Y_{7Z)hY$SKu$_3WU+{N5K&tXqSQm{r66IlGc?=m$2c>ACL~gf zKcqd1f5TrfS5G~9_2fI#w9PE<=kq?#zO!33n=yj({dZgZq6qzQ#p)qYyavoNd)-HjGy4d3F{S>==dEj-(*)EtyfEYAG3vBR+ zsK+ipAsj!r?rfRhwuAjF;N%8>w8VV}qmpoRfD8prZGo{<3)R$l$C+bOpJ5X%h?>C+ zK3O}DFA&6a7Z(KPmpvZ9Hs7FX%uqjc{5|xgIVZp;p3gX)k$#icz_-30nhmGI!v&eU zeZ%uRoXmm?r-)AOo^P6a4qxC4*vW0UYTf$&rTak2IU5g{Nr!!%O~rKG43%K~JwNT8 zPUAsdrK7#9fV_PROgc|G9`RG1?atHZ-TRiBi4?Z})@gl`cCC^^F;gjRBgquYbCKJc zR9R_ky^{T_>9i{3*a#bEIxDqdWhyNFSXI&nqU4sAtN_i-I4M8|6`$A|%qe53HbUC? zjn>(5SqP8~K|(l$i!`?(lhSMKLDi);VA>o3sW292fO_6fq#nX2`1c~=%&=s@(vjf( zu}t!8I2o)8H^PcbTu;SXV Zij7_t6(8r~mHWrK0WLxj?BSk9@E?a->=Xb1 literal 0 HcmV?d00001 diff --git a/batch/opt/fusion_rules.py b/batch/opt/fusion_rules.py index ed184fa..c327683 100644 --- a/batch/opt/fusion_rules.py +++ b/batch/opt/fusion_rules.py @@ -3,117 +3,6 @@ from batch.ast2ir import * import codegen -# def loop_idx_change(expr, ori_iter_list, iter_list): -# # count = 0 -# if isinstance(expr, Loop): -# for i in expr.body: -# loop_idx_change(i, ori_iter_list, iter_list) -# if isinstance(expr, Assignment): -# loop_idx_change(expr.lhs, ori_iter_list, iter_list) -# loop_idx_change(expr.rhs, ori_iter_list, iter_list) -# if isinstance(expr, Expr): -# loop_idx_change(expr.left, ori_iter_list, iter_list) -# loop_idx_change(expr.right, ori_iter_list, iter_list) -# while isinstance(expr, Index): -# try: -# print("index:::", expr.index.__name__, ori_iter_list) -# for idx, item in enumerate(ori_iter_list): -# if expr.index.__name__ == item.__name__: -# expr.index.__name__ = iter_list[idx].__name__ -# expr = expr.dobject -# except: -# expr = expr.dobject -# break - -# def fuse_elementwise(node): - - -# eval_len = len(node.eval.size) -# temp = node.operators[0].compute[0] -# print("operator0:",codegen.cpu.to_string(temp)) -# print("operator1:",codegen.cpu.to_string(node.operators[1].compute[0])) -# loop_count = 0 -# while isinstance(temp, Loop): -# if loop_count == eval_len: -# break -# temp = temp.body[0] -# loop_count += 1 -# lhs = node.operators[0].eval -# rhs = node.operators[1].eval -# res = node.eval - -# t = node.operators[0].compute[0] -# pre_loop = None -# iter_list = [] -# ori_iter_list = [] -# for i in range(eval_len): -# i_loop = Loop(0, node.eval.size[i], 1, []) -# lhs = bind(lhs, i_loop.iterate) -# rhs = bind(rhs, i_loop.iterate) -# res = bind(res, i_loop.iterate) -# iter_list.append(i_loop.iterate) -# ori_iter_list.append(t.iterate) -# t = t.body[0] -# if i == 0: -# pre_loop = i_loop -# t_loop = i_loop -# else: -# t_loop.body.append(i_loop) -# t_loop = i_loop -# assign = Assignment(res, Expr(lhs, rhs, '+')) -# loop_idx_change(temp, ori_iter_list, iter_list) - -# i_loop.body.append(temp) -# i_loop.body.append(assign) -# node.operators[0].compute.clear() -# node.compute = [pre_loop] -# # ir = [] -# # codegen.cpu.gen_cpp(node, ir) -# # code = '' -# # for d in ir: -# # if d: -# # code += codegen.cpu.to_string(d) -# # print(code) - -# def fuse_innerprod(node): -# temp = fused_loop = node.operators[0].compute[0] -# ori_iter_list = [] -# while isinstance(temp, Loop): -# ori_iter_list.append(temp.iterate) -# temp = temp.body[0] - -# pre_loop = Loop(0, node.eval.size[0], 1, []) -# node.compute = [pre_loop] -# lhs = bind(node.operators[0].eval, pre_loop.iterate) -# rhs = bind(node.operators[1].eval, pre_loop.iterate) -# res = bind(node.eval, pre_loop.iterate) -# inner_loop = Loop(0, node.operators[0].eval.size[1], 1, []) -# pre_loop.body.append(inner_loop) -# lhs = bind(lhs, inner_loop.iterate) -# rhs = bind(rhs, inner_loop.iterate) -# assign = Assignment(res, Expr(lhs, rhs, '*'), '+') -# loop_idx_change(temp.lhs,ori_iter_list, [pre_loop.iterate, inner_loop.iterate]) -# inner_loop.body.append(temp) -# inner_loop.body.append(assign) -# node.operators[0].compute.clear() - -def loop_idx_change(oloop, fusedloop): - # if isinstance(oloop, Loop): - # print('oloop:', codegen.cpu.to_string(oloop), oloop.iterate) - # if isinstance(fusedloop[0], Loop) : - # print('iloop 0 :', codegen.cpu.to_string(fusedloop[0]), fusedloop[0].iterate) - # if len(fusedloop) > 1: - # print('iloop 1 :', codegen.cpu.to_string(fusedloop[1])) - # print(fusedloop, codegen.cpu.to_string(fusedloop[0])) - for iloop in fusedloop: - while (isinstance(oloop, Loop) and isinstance(iloop, Loop)) and oloop.start == iloop.start and oloop.end.__name__ == iloop.end.__name__ and oloop.step == iloop.step: - # iloop.iterate = oloop.iterate - iloop.iterate.__name__ = oloop.iterate.__name__ - iloop.iterate = oloop.iterate - oloop = oloop.body[0] - iloop = iloop.body - - loop_idx_change(oloop, iloop) def get_same_loop(outloop, fusedloop): pre_o = outloop @@ -127,16 +16,46 @@ def get_same_loop(outloop, fusedloop): pre_iter_i.append(pre_i.iterate) pre_iter_o.append(pre_o.iterate) while (isinstance(oloop, Loop) and isinstance(iloop, Loop)) and oloop.start == iloop.start and oloop.end.__name__ == iloop.end.__name__ and oloop.step == iloop.step: + if len(pre_i.body)>1: + break pre_o = oloop pre_i = iloop pre_iter_i.append(pre_i.iterate) pre_iter_o.append(pre_o.iterate) - oloop = oloop.body[0] + oloop = oloop.body[-1] iloop = iloop.body[0] return pre_o, pre_i, pre_iter_o, pre_iter_i +def loop_merge(o_loop, i_loop): + # print(codegen.cpu.to_string(o_loop), codegen.cpu.to_string(i_loop)) + if (isinstance(o_loop, Loop) and isinstance(i_loop, Loop)) and o_loop.start == i_loop.start and o_loop.end.__name__ == i_loop.end.__name__ and o_loop.step == i_loop.step: + + for ii in range(len(i_loop.body)-1): + + change_index(i_loop.body[ii], [o_loop.iterate], [i_loop.iterate]) + o_loop.body.insert(ii, i_loop.body[ii]) + # print('last loop i', count, codegen.cpu.to_string(i_loop.body[-1])) + change_index(i_loop.body[-1], [o_loop.iterate], [i_loop.iterate]) + # print("outer::",codegen.cpu.to_string(o_loop)) + # print("inner::",codegen.cpu.to_string(i_loop)) + if not isinstance(i_loop.body[-1], Loop): + o_loop.body.insert(-1, i_loop.body[-1]) + elif not isinstance(o_loop.body[-1], Loop): + o_loop.body.insert(0, i_loop.body[-1]) + else: + loop_merge(o_loop.body[-1], i_loop.body[-1]) + + # o_loop.body.insert(-1, i_loop.body[-1]) + # else: + # if isinstance(i_loop, Loop): + # for ii in range(len(i_loop.body)): + # o_loop.body.insert(ii, i_loop.body[ii]) + # else: + # o_loop.body.insert(0, i_loop) + + def change_index(iassign, iter_o, iter_i): if isinstance(iassign, Index): for idx, item in enumerate(iter_i): @@ -154,151 +73,611 @@ def change_index(iassign, iter_o, iter_i): change_index(iassign.lhs, iter_o, iter_i) change_index(iassign.rhs, iter_o, iter_i) elif isinstance(iassign, Loop): - # oloop.body[0] and item.body for i in iassign.body: change_index(i, iter_o, iter_i) - +def swap_arr_to_reg(ir, pre, cur): + # print(ir, codegen.cpu.to_string(ir), codegen.cpu.to_string(pre), codegen.cpu.to_string(cur)) + if isinstance(ir, Index): + # print(ir.dobject, pre) + temp = ir + while isinstance(temp, Index): + # print(temp, codegen.cpu.to_string(temp)) + temp = temp.dobject + # print('*************', temp, codegen.cpu.to_string(temp), pre) + if temp == pre: + return cur + else: + return ir + + elif isinstance(ir, Expr): + ir.left = swap_arr_to_reg(ir.left, pre, cur) + ir.right = swap_arr_to_reg(ir.right, pre, cur) + elif isinstance(ir, Assignment): + ir.lhs = swap_arr_to_reg(ir.lhs, pre, cur) + ir.rhs = swap_arr_to_reg(ir.rhs, pre, cur) + elif isinstance(ir, Loop): + for i in range(len(ir.body)): + ir.body[i] = swap_arr_to_reg(ir.body[i], pre, cur) + return ir def fuse_elementwise(ast): - if type(ast.operators[0]) == BatchOp: - fuse_elementwise(ast.operators[0]) - if type(ast.operators[1]) == BatchOp: - fuse_elementwise(ast.operators[1]) + if type(ast) == BatchOp: + if type(ast.operators[0]) == BatchOp: + fuse_elementwise(ast.operators[0]) + if type(ast.operators[1]) == BatchOp: + fuse_elementwise(ast.operators[1]) + else: + return - if type(ast) == Batch or not ast.op_type in core.ast.op_mapping: - return + if type(ast.operators[1]) == BatchOp and ast.op_type in core.ast.op_mapping.keys(): + # fuse operators1 into elementwise + if ast.item_type == 'vec' and ast.operators[1].item_type == ast.item_type: + # check if type of operator1 is vector + if ast.operators[1].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[1].compute[0] - if ast.operators[1].compute and ast.compute: - outer_loop = ast.compute[0] - loop = ast.operators[1].compute[0] + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + iloop.body.clear() + ast.operators[1].compute.clear() + elif ast.item_type == 'scal' and ast.operators[1].item_type == ast.item_type: + # check if type of operator1 is scalar + if ast.operators[1].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[1].compute[0] - oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) - for i in iloop.body: - change_index(i, iter_o, iter_i) - for i in range(len(iloop.body)): - oloop.body.insert(i, iloop.body[i]) - iloop.body.clear() - ast.operators[1].compute.clear() - - if ast.operators[0].compute and ast.compute: - outer_loop = ast.compute[0] - loop = ast.operators[0].compute[0] - - oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) - for i in iloop.body: - change_index(i, iter_o, iter_i) - for i in range(len(iloop.body)): - oloop.body.insert(i, iloop.body[i]) - - iloop.body.clear() - ast.operators[0].compute.clear() + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + iloop.body.clear() + ast.operators[1].compute.clear() + elif ast.item_type not in ['vec', 'scal']: + raise ValueError(f"Tensor type is wrong. Expect ast as 'vec' or 'scal' but found '{ast.item_type}'.") + elif ast.operators[1].item_type not in ['vec', 'scal']: + raise ValueError(f"Tensor type is wrong. Expect operators[1] as 'vec' or 'scal' but found '{ast.operators[1].item_type}'.") + elif ast.operators[1].item_type != ast.item_type: + raise ValueError(f"Tensor shape are not the same. Expect 'vec' or 'scal' but found ast: '{ast.item_type}' and operators[1]: '{ast.operators[1].item_type}'.") + + if type(ast.operators[0]) == BatchOp and ast.op_type in core.ast.op_mapping.keys(): + # fuse operators0 into elementwise + if ast.item_type == 'vec' and ast.operators[0].item_type == ast.item_type: + # check if type of operator1 is vector + if ast.operators[0].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[0].compute[0] + + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + + iloop.body.clear() + ast.operators[0].compute.clear() + if ast.item_type == 'scal' and ast.operators[0].item_type == ast.item_type: + # check if type of operator1 is scalar + if ast.operators[0].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[0].compute[0] + + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + + iloop.body.clear() + ast.operators[0].compute.clear() + elif ast.item_type not in ['vec','scal']: + raise ValueError(f"Tensor shape are not the same. Expect ast as 'vec' or 'scal' but found '{ast.item_type}'.") + elif ast.operators[0].item_type not in ['vec','scal']: + raise ValueError(f"Tensor shape are not the same. Expect operators[0] as 'vec' or 'scal' but found '{ast.operators[0].item_type}'.") + elif ast.operators[0].item_type != ast.item_type: + raise ValueError(f"Tensor shape are not the same. Expect 'vec' or 'scal' but found ast: '{ast.item_type}' and operators[0]: '{ast.operators[0].item_type}'.") -def fuse_innerprod(ast): - if type(ast.operators[0]) == BatchOp: - fuse_innerprod(ast.operators[0]) - if type(ast.operators[1]) == BatchOp: - fuse_innerprod(ast.operators[1]) +def fuse_bvv(ast): + if type(ast) == BatchOp: + if type(ast.operators[1]) == BatchOp: + fuse_bvv(ast.operators[1]) + if type(ast.operators[0]) == BatchOp: + fuse_bvv(ast.operators[0]) + else: + return + + if type(ast.operators[1]) == BatchOp and ast.op_type == 'vec_mul_vec': + # fuse operators1 into bvv + if ast.item_type == 'scal' and ast.operators[1].item_type == 'vec': + # check if type of operator1 is vector + if ast.operators[1].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[1].compute[0] - if type(ast) == Batch or not ast.op_type == 'vec_mul_vec': - return + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + iloop.body.clear() + ast.operators[1].compute.clear() + elif ast.operators[1].item_type != 'vec': + raise ValueError(f"Tensor type is wrong. Expect operators[1] as 'vec' but found '{ast.operators[1].item_type}'.") + + if type(ast.operators[0]) == BatchOp and ast.op_type == 'vec_mul_vec': + # fuse operators1 into bvv + if ast.item_type == 'scal' and ast.operators[0].item_type == 'vec': + # check if type of operator0 is vector + if ast.operators[0].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[0].compute[0] - if ast.operators[1].compute and ast.compute: - outer_loop = ast.compute[0] - loop = ast.operators[1].compute[0] - - oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) - for i in iloop.body: - change_index(i, iter_o, iter_i) - for i in range(len(iloop.body)): - oloop.body.insert(i, iloop.body[i]) - - iloop.body.clear() - ast.operators[1].compute.clear() + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + iloop.body.clear() + ast.operators[0].compute.clear() + elif ast.operators[0].item_type != 'vec': + raise ValueError(f"Tensor type is wrong. Expect operators[0] as 'vec' but found '{ast.operators[0].item_type}'.") + +def fuse_bsv(ast): + if type(ast) == BatchOp: + if type(ast.operators[1]) == BatchOp: + fuse_bsv(ast.operators[1]) + if type(ast.operators[0]) == BatchOp: + fuse_bsv(ast.operators[0]) + else: + return + + if type(ast) == BatchOp and ast.op_type == 'scal_mul_vec': + # fuse operators into bsv + if ast.item_type == 'vec' and ast.operators[1].item_type == 'vec' and ast.operators[0].item_type == 'scal': + # check if type of operators are scal and vector + if ast.operators[1].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[1].compute[0] - if ast.operators[0].compute and ast.compute: - outer_loop = ast.compute[0] - loop = ast.operators[0].compute[0] - - oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) - for i in iloop.body: - change_index(i, iter_o, iter_i) - for i in range(len(iloop.body)): - oloop.body.insert(i, iloop.body[i]) - - iloop.body.clear() - ast.operators[0].compute.clear() + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + iloop.body.clear() + ast.operators[1].compute.clear() + if ast.operators[0].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[0].compute[0] + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + iloop.body.clear() + ast.operators[0].compute.clear() + elif ast.operators[0].item_type != 'scal': + raise ValueError(f"Tensor type is wrong. Expect operators[0] as 'scal' but found '{ast.operators[0].item_type}'.") + elif ast.operators[1].item_type != 'vec': + raise ValueError(f"Tensor type is wrong. Expect operators[1] as 'vec' but found '{ast.operators[0].item_type}'.") + elif ast.item_type != 'vec': + raise ValueError(f"Tensor type is wrong. Expect ast node as 'vec' but found '{ast.item_type}'.") -def fuse_bsv(ast): - print(type(ast.operators[0]), type(ast.operators[1]), type(ast), ast.op_type) - if type(ast.operators[0]) == BatchOp: - fuse_bsv(ast.operators[0]) - if type(ast.operators[1]) == BatchOp: - fuse_bsv(ast.operators[1]) - - if type(ast) == Batch or not ast.op_type == 'scal_mul_vec': - return - print(ast.op_type, ast.operators[0].compute , ast.compute) - if ast.operators[1].compute and ast.compute: - outer_loop = ast.compute[0] - loop = ast.operators[1].compute[0] - - oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) - for i in iloop.body: - change_index(i, iter_o, iter_i) - for i in range(len(iloop.body)): - oloop.body.insert(i, iloop.body[i]) - - iloop.body.clear() - ast.operators[1].compute.clear() +def fuse_bvm(ast): + if type(ast) == BatchOp: + if type(ast.operators[1]) == BatchOp: + fuse_bvm(ast.operators[1]) + if type(ast.operators[0]) == BatchOp: + fuse_bvm(ast.operators[0]) + else: + return + + if type(ast) == BatchOp and ast.op_type == 'vec_mul_mat': + # fuse operators1 into bov + if ast.item_type == 'vec' and ast.operators[0].item_type == 'vec' and ast.operators[1].item_type == 'mat': + # check if type of operator1 is vector + if ast.operators[1].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[1].compute[0] - if ast.operators[0].compute and ast.compute: - outer_loop = ast.compute[0] - loop = ast.operators[0].compute[0] - - oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) - for i in iloop.body: - change_index(i, iter_o, iter_i) - for i in range(len(iloop.body)): - oloop.body.insert(i, iloop.body[i]) - - iloop.body.clear() - ast.operators[0].compute.clear() + oloop = outer_loop + iloop = loop + iter_o = [] + iter_i = [] + if (isinstance(oloop, Loop) and isinstance(iloop, Loop)) and oloop.start == iloop.start and oloop.end.__name__ == iloop.end.__name__ and oloop.step == iloop.step: + iter_i.append(iloop.iterate) + iter_o.append(oloop.iterate) + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + iloop.body.clear() + ast.operators[1].compute.clear() + + if ast.operators[0].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[0].compute[0] + + oloop = outer_loop + iloop = loop + iter_o = [] + iter_i = [] + if (isinstance(oloop, Loop) and isinstance(iloop, Loop)) and oloop.start == iloop.start and oloop.end.__name__ == iloop.end.__name__ and oloop.step == iloop.step: + iter_i.append(iloop.iterate) + iter_o.append(oloop.iterate) + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + iloop.body.clear() + ast.operators[0].compute.clear() + elif ast.item_type != 'vec': + raise ValueError(f"Tensor shape are not the same. Expect ast node type 'mat' but found '{ast.item_type}'.") + elif ast.operators[1].item_type != 'mat': + raise ValueError(f"Tensor shape are not the same. Expect ast.operators[1] node type 'mat' but found '{ast.operators[1].item_type}'.") + elif ast.operators[0].item_type != 'vec': + raise ValueError(f"Tensor shape are not the same. Expect ast.operators[0] node type 'vec' but found '{ast.operators[1].item_type}'.") + +def fuse_bov(ast): + if type(ast) == BatchOp: + if type(ast.operators[1]) == BatchOp: + fuse_bov(ast.operators[1]) + if type(ast.operators[0]) == BatchOp: + fuse_bov(ast.operators[0]) + else: + return + if type(ast.operators[1]) == BatchOp and ast.op_type == 'vec_outer_vec': + # fuse operators1 into bov + if ast.item_type == 'mat' and ast.operators[1].item_type == 'vec': + # check if type of operator1 is vector + if ast.operators[1].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[1].compute[0] -def fuse_bvm(ast): - if type(ast.operators[0]) == BatchOp: - fuse_bvm(ast.operators[0]) - if type(ast.operators[1]) == BatchOp: - fuse_bvm(ast.operators[1]) - - if type(ast) == Batch or not ast.op_type == 'vec_mul_mat': - return - if ast.operators[1].compute and ast.compute: - outer_loop = ast.compute[0] - loop = ast.operators[1].compute[0] - - oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) - for i in iloop.body: - change_index(i, iter_o, iter_i) - for i in range(len(iloop.body)): - oloop.body.insert(i, iloop.body[i]) - - iloop.body.clear() - ast.operators[1].compute.clear() + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + iloop.body.clear() + ast.operators[1].compute.clear() + elif ast.item_type != 'mat': + raise ValueError(f"Tensor shape are not the same. Expect ast node type 'mat' but found '{ast.item_type}'.") + elif ast.operators[0].item_type != 'vec': + raise ValueError(f"Tensor shape are not the same. Expect ast.operators[1] node type 'vec' but found '{ast.operators[1].item_type}'.") + + + if type(ast.operators[0]) == BatchOp and ast.op_type == 'vec_outer_vec': + # fuse operators0 into bov + if ast.item_type == 'mat' and ast.operators[0].item_type == 'vec': + # check if type of operator0 is vector + if ast.operators[0].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[0].compute[0] - if ast.operators[0].compute and ast.compute: - outer_loop = ast.compute[0] - loop = ast.operators[0].compute[0] + oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + iloop.body.clear() + ast.operators[0].compute.clear() + elif ast.item_type != 'mat': + raise ValueError(f"Tensor shape are not the same. Expect ast node type 'mat' but found '{ast.item_type}'.") + elif ast.operators[0].item_type != 'vec': + raise ValueError(f"Tensor shape are not the same. Expect ast.operators[0] node type 'vec' but found '{ast.operators[0].item_type}'.") - oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) - for i in iloop.body: - change_index(i, iter_o, iter_i) - for i in range(len(iloop.body)): - oloop.body.insert(i, iloop.body[i]) + + +def fuse_operators(ast): + + if type(ast) == BatchOp: + if type(ast.operators[1]) == BatchOp: + # print('op1:', ast.operators[1].op_type) + fuse_operators(ast.operators[1]) + if type(ast.operators[0]) == BatchOp: + # print('op0:', ast.operators[0].op_type) + fuse_operators(ast.operators[0]) + else: + return + + if type(ast.operators[1]) == BatchOp and ast.op_type in core.ast.op_mapping.keys(): + # fuse operators1 into elementwise + if ast.item_type == 'vec' and ast.operators[1].item_type == ast.item_type: + # check if type of operator1 is vector + if ast.operators[1].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[1].compute[0] + + # oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + # for i in iloop.body: + # change_index(i, iter_o, iter_i) + # for i in range(len(iloop.body)): + # oloop.body.insert(i, iloop.body[i]) + + loop_merge(outer_loop, loop) + if ast.operators[1].op_type in core.ast.op_mapping.keys() or ast.operators[1].op_type == "scal_mul_vec": + a = Scalar(ast.operators[1].eval.dtype) + pre_arr = ast.operators[1].eval + ast.operators[1].decl = [Decl(a)] + ast.operators[1].eval = a + outer_loop = swap_arr_to_reg(outer_loop, pre_arr, a) + ast.operators[1].compute.clear() + elif ast.item_type == 'scal' and ast.operators[1].item_type == ast.item_type: + # check if type of operator1 is scalar + if ast.operators[1].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[1].compute[0] + + loop_merge(outer_loop, loop) + if ast.operators[1].op_type in core.ast.op_mapping.keys() or ast.operators[1].op_type == "scal_mul_vec": + a = Scalar(ast.operators[1].eval.dtype) + pre_arr = ast.operators[1].eval + ast.operators[1].decl = [Decl(a)] + ast.operators[1].eval = a + outer_loop = swap_arr_to_reg(outer_loop, pre_arr, a) + ast.operators[1].compute.clear() + elif ast.item_type not in ['vec', 'scal']: + raise ValueError(f"Tensor type is wrong. Expect ast as 'vec' or 'scal' but found '{ast.item_type}'.") + elif ast.operators[1].item_type not in ['vec', 'scal']: + raise ValueError(f"Tensor type is wrong. Expect operators[1] as 'vec' or 'scal' but found '{ast.operators[1].item_type}'.") + elif ast.operators[1].item_type != ast.item_type: + raise ValueError(f"Tensor shape are not the same. Expect 'vec' or 'scal' but found ast: '{ast.item_type}' and operators[1]: '{ast.operators[1].item_type}'.") + + + if type(ast.operators[0]) == BatchOp and ast.op_type in core.ast.op_mapping.keys(): + # fuse operators0 into elementwise + if ast.item_type == 'vec' and ast.operators[0].item_type == ast.item_type: + # check if type of operator0 is vector + if ast.operators[0].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[0].compute[0] + loop_merge(outer_loop, loop) + if ast.operators[0].op_type in core.ast.op_mapping.keys() or ast.operators[0].op_type == "scal_mul_vec": + a = Scalar(ast.operators[0].eval.dtype) + pre_arr = ast.operators[0].eval + ast.operators[0].decl = [Decl(a)] + ast.operators[0].eval = a + outer_loop = swap_arr_to_reg(outer_loop, pre_arr, a) + ast.operators[0].compute.clear() + if ast.item_type == 'scal' and ast.operators[0].item_type == ast.item_type: + # check if type of operator1 is scalar + if ast.operators[0].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[0].compute[0] + + loop_merge(outer_loop, loop) + if ast.operators[0].op_type in core.ast.op_mapping.keys() or ast.operators[0].op_type == "scal_mul_vec": + a = Scalar(ast.operators[0].eval.dtype) + pre_arr = ast.operators[0].eval + ast.operators[0].decl = [Decl(a)] + ast.operators[0].eval = a + outer_loop = swap_arr_to_reg(outer_loop, pre_arr, a) + ast.operators[0].compute.clear() + elif ast.item_type not in ['vec','scal']: + raise ValueError(f"Tensor shape are not the same. Expect ast as 'vec' or 'scal' but found '{ast.item_type}'.") + elif ast.operators[0].item_type not in ['vec','scal']: + raise ValueError(f"Tensor shape are not the same. Expect operators[0] as 'vec' or 'scal' but found '{ast.operators[0].item_type}'.") + elif ast.operators[0].item_type != ast.item_type: + raise ValueError(f"Tensor shape are not the same. Expect 'vec' or 'scal' but found ast: '{ast.item_type}' and operators[0]: '{ast.operators[0].item_type}'.") + + if type(ast.operators[1]) == BatchOp and ast.op_type == 'vec_mul_vec': + # fuse operators1 into bvv + if ast.item_type == 'scal' and ast.operators[1].item_type == 'vec': + # check if type of operator1 is vector + print(ast.operators[1].compute , ast.compute) + if ast.operators[1].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[1].compute[0] + + # oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + # for i in iloop.body: + # change_index(i, iter_o, iter_i) + # for i in range(len(iloop.body)): + # oloop.body.insert(i, iloop.body[i]) + # iloop.body.clear() + loop_merge(outer_loop, loop) + if ast.operators[1].op_type in core.ast.op_mapping.keys() or ast.operators[1].op_type == "scal_mul_vec": + a = Scalar(ast.operators[1].eval.dtype) + pre_arr = ast.operators[1].eval + ast.operators[1].decl = [Decl(a)] + ast.operators[1].eval = a + outer_loop = swap_arr_to_reg(outer_loop, pre_arr, a) + ast.operators[1].compute.clear() + elif ast.operators[1].item_type != 'vec': + raise ValueError(f"Tensor type is wrong. Expect operators[1] as 'vec' but found '{ast.operators[1].item_type}'.") + + + if type(ast.operators[0]) == BatchOp and ast.op_type == 'vec_mul_vec': + # fuse operators1 into bvv + if ast.item_type == 'scal' and ast.operators[0].item_type == 'vec': + # check if type of operator0 is vector + if ast.operators[0].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[0].compute[0] + + # oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + # for i in iloop.body: + # change_index(i, iter_o, iter_i) + # for i in range(len(iloop.body)): + # oloop.body.insert(i, iloop.body[i]) + # iloop.body.clear() + + loop_merge(outer_loop, loop) + if ast.operators[0].op_type in core.ast.op_mapping.keys() or ast.operators[0].op_type == "scal_mul_vec": + a = Scalar(ast.operators[0].eval.dtype) + pre_arr = ast.operators[0].eval + ast.operators[0].decl = [Decl(a)] + ast.operators[0].eval = a + outer_loop = swap_arr_to_reg(outer_loop, pre_arr, a) + ast.operators[0].compute.clear() + elif ast.operators[0].item_type != 'vec': + raise ValueError(f"Tensor type is wrong. Expect operators[0] as 'vec' but found '{ast.operators[0].item_type}'.") + + + if type(ast) == BatchOp and ast.op_type == 'scal_mul_vec': + # fuse operators into bsv + if ast.item_type == 'vec' and ast.operators[1].item_type == 'vec' and ast.operators[0].item_type == 'scal': + # check if type of operators are scal and vector + if ast.operators[1].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[1].compute[0] + + # oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + # for i in iloop.body: + # change_index(i, iter_o, iter_i) + # for i in range(len(iloop.body)): + # oloop.body.insert(i, iloop.body[i]) + # iloop.body.clear() + loop_merge(outer_loop, loop) + if ast.operators[1].op_type in core.ast.op_mapping.keys() or ast.operators[1].op_type == "scal_mul_vec": + a = Scalar(ast.operators[1].eval.dtype) + pre_arr = ast.operators[1].eval + ast.operators[1].decl = [Decl(a)] + ast.operators[1].eval = a + outer_loop = swap_arr_to_reg(outer_loop, pre_arr, a) + ast.operators[1].compute.clear() + if ast.operators[0].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[0].compute[0] + + oloop = outer_loop + iloop = loop + iter_o = [] + iter_i = [] + if (isinstance(oloop, Loop) and isinstance(iloop, Loop)) and oloop.start == iloop.start and oloop.end.__name__ == iloop.end.__name__ and oloop.step == iloop.step: + iter_i.append(iloop.iterate) + iter_o.append(oloop.iterate) + if isinstance(iloop, Loop): + for i in iloop.body: + change_index(i, iter_o, iter_i) + + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + if ast.operators[0].op_type in core.ast.op_mapping.keys() or ast.operators[0].op_type == "scal_mul_vec": + a = Scalar(ast.operators[0].eval.dtype) + pre_arr = ast.operators[0].eval + ast.operators[0].decl = [Decl(a)] + ast.operators[0].eval = a + oloop = swap_arr_to_reg(oloop, pre_arr, a) + iloop.body.clear() + ast.operators[0].compute.clear() + elif ast.operators[0].item_type != 'scal': + raise ValueError(f"Tensor type is wrong. Expect operators[0] as 'scal' but found '{ast.operators[0].item_type}'.") + elif ast.operators[1].item_type != 'vec': + raise ValueError(f"Tensor type is wrong. Expect operators[1] as 'vec' but found '{ast.operators[0].item_type}'.") + elif ast.item_type != 'vec': + raise ValueError(f"Tensor type is wrong. Expect ast node as 'vec' but found '{ast.item_type}'.") - iloop.body.clear() - ast.operators[0].compute.clear() \ No newline at end of file + + if type(ast) == BatchOp and ast.op_type == 'vec_mul_mat': + # fuse operators1 into bov + if ast.item_type == 'vec' and ast.operators[0].item_type == 'vec' and ast.operators[1].item_type == 'mat': + # check if type of operator1 is vector + if ast.operators[1].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[1].compute[0] + + # oloop = outer_loop + # iloop = loop + # iter_o = [] + # iter_i = [] + # if (isinstance(oloop, Loop) and isinstance(iloop, Loop)) and oloop.start == iloop.start and oloop.end.__name__ == iloop.end.__name__ and oloop.step == iloop.step: + # iter_i.append(iloop.iterate) + # iter_o.append(oloop.iterate) + # for i in iloop.body: + # change_index(i, iter_o, iter_i) + # for i in range(len(iloop.body)): + # oloop.body.insert(i, iloop.body[i]) + # iloop.body.clear() + loop_merge(outer_loop, loop) + ast.operators[1].compute.clear() + + if ast.operators[0].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[0].compute[0] + oloop = outer_loop + iloop = loop + iter_o = [] + iter_i = [] + + while (isinstance(oloop, Loop) and isinstance(iloop, Loop)) and oloop.start == iloop.start and oloop.end.__name__ == iloop.end.__name__ and oloop.step == iloop.step: + iter_i.append(iloop.iterate) + iter_o.append(oloop.iterate) + if isinstance(oloop.body[-1], Loop) and isinstance(iloop, Loop): + oloop = oloop.body[-1].body[-1] + iloop = iloop.body[-1] + else: + break + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + ast.operators[0].compute.clear() + elif ast.item_type != 'vec': + raise ValueError(f"Tensor shape are not the same. Expect ast node type 'mat' but found '{ast.item_type}'.") + elif ast.operators[1].item_type != 'mat': + raise ValueError(f"Tensor shape are not the same. Expect ast.operators[1] node type 'mat' but found '{ast.operators[1].item_type}'.") + elif ast.operators[0].item_type != 'vec': + raise ValueError(f"Tensor shape are not the same. Expect ast.operators[0] node type 'vec' but found '{ast.operators[1].item_type}'.") + + + if type(ast.operators[1]) == BatchOp and ast.op_type == 'vec_outer_vec': + # fuse operators1 into bov + if ast.item_type == 'mat' and ast.operators[1].item_type == 'vec': + # check if type of operator1 is vector + if ast.operators[1].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[1].compute[0] + oloop = outer_loop + iloop = loop + iter_i = [] + iter_o = [] + while (isinstance(oloop, Loop) and isinstance(iloop, Loop)) and oloop.start == iloop.start and oloop.end.__name__ == iloop.end.__name__ and oloop.step == iloop.step: + iter_i.append(iloop.iterate) + iter_o.append(oloop.iterate) + if isinstance(oloop.body[-1], Loop) and isinstance(iloop, Loop): + oloop = oloop.body[-1].body[-1] + iloop = iloop.body[-1] + else: + break + for i in iloop.body: + change_index(i, iter_o, iter_i) + for i in range(len(iloop.body)): + oloop.body.insert(i, iloop.body[i]) + ast.operators[1].compute.clear() + elif ast.item_type != 'mat': + raise ValueError(f"Tensor shape are not the same. Expect ast node type 'mat' but found '{ast.item_type}'.") + elif ast.operators[0].item_type != 'vec': + raise ValueError(f"Tensor shape are not the same. Expect ast.operators[1] node type 'vec' but found '{ast.operators[1].item_type}'.") + + + if type(ast.operators[0]) == BatchOp and ast.op_type == 'vec_outer_vec': + # fuse operators0 into bov + if ast.item_type == 'mat' and ast.operators[0].item_type == 'vec': + # check if type of operator0 is vector + if ast.operators[0].compute and ast.compute: + outer_loop = ast.compute[0] + loop = ast.operators[0].compute[0] + + # oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) + # for i in iloop.body: + # change_index(i, iter_o, iter_i) + # for i in range(len(iloop.body)): + # oloop.body.insert(i, iloop.body[i]) + # iloop.body.clear() + loop_merge(outer_loop, loop) + ast.operators[0].compute.clear() + elif ast.item_type != 'mat': + raise ValueError(f"Tensor shape are not the same. Expect ast node type 'mat' but found '{ast.item_type}'.") + elif ast.operators[0].item_type != 'vec': + raise ValueError(f"Tensor shape are not the same. Expect ast.operators[0] node type 'vec' but found '{ast.operators[0].item_type}'.") \ No newline at end of file diff --git a/batch/opt/ir.py b/batch/opt/ir.py new file mode 100644 index 0000000..022bf6f --- /dev/null +++ b/batch/opt/ir.py @@ -0,0 +1,24 @@ +from core.ir import * + +class ThreadMapping: + loop_id = 0 + def __init__(self, start, end, step, body: list): + self.lid = ThreadMapping.loop_id + ThreadMapping.loop_id += 1 + self.start = start + self.end = end + self.step = step + self.body = body + self.iterate = Scalar('int', f'_l{self.lid}') + +class ThreadIdy: + def __init__(self): + pass + +class ThreadIdx: + def __init__(self): + pass + +class Sync: + def __init__(self): + pass \ No newline at end of file diff --git a/batch/opt/parallelism.py b/batch/opt/parallelism.py new file mode 100644 index 0000000..7aae015 --- /dev/null +++ b/batch/opt/parallelism.py @@ -0,0 +1,34 @@ +from core.ir import * +from batch.ast import * +from batch.ast2ir import * +import codegen +from batch.opt.ir import * +# for better optimization on GPU + +def parallel(ast): + if type(ast) == BatchOp: + if type(ast.operators[1]) == BatchOp: + parallel(ast.operators[1]) + if type(ast.operators[0]) == BatchOp: + parallel(ast.operators[0]) + else: + return + print(ast.op_type) + if ast.compute: + stmt = ast.compute[0] + if isinstance(stmt, Loop): + print(stmt.end.name(), stmt.iterate.name()) + if stmt.end.name() == 'batch_size': + ast.compute = stmt.body + for i in ast.compute: + if isinstance(i, Loop): + i.start = 'threadIdx.x' + i.step = 'blockDim.x' + # a = ThreadMapping(stmt.start, stmt.end, stmt.step, stmt.body) + # 16 ty * 32 tx each thread block + assign = Assignment(stmt.iterate, 'threadIdx.y + blockIdx.x * 16') + # a.iterate = stmt.iterate + ast.compute.insert(0, assign) + ast.decl.append(Decl(stmt.iterate)) + + \ No newline at end of file diff --git a/batch/test/kge.py b/batch/test/kge.py index 148a283..b3f9c58 100644 --- a/batch/test/kge.py +++ b/batch/test/kge.py @@ -1,5 +1,9 @@ -import codegen +import sys +sys.path.append('/data/backed_up/lihhu/CUKE/cuke') +from codegen import * from batch.ast import * +from batch.opt.fusion_rules import * +from batch.opt.parallelism import * def transE(): nnodes = Var('nnodes') @@ -17,8 +21,13 @@ def transE(): res = vh - vt + vr - code = codegen.cpu.print_cpp(res._gen_ir()) + # code = codegen.cpu.print_cpp(res._gen_ir()) + ast = res._gen_ir() + fuse_operators(ast) + parallel(ast) + # code = codegen.cpu.print_cpp(ast) + code = codegen.gpu.print_cpp(ast) print(code) def transH(): @@ -39,6 +48,14 @@ def transH(): code = codegen.cpu.print_cpp(res._gen_ir()) + # print(code) + ast = res._gen_ir() + + + fuse_operators(ast) + parallel(ast) + # code = codegen.cpu.print_cpp(ast) + code = codegen.gpu.print_cpp(ast) print(code) def transR(): @@ -60,6 +77,11 @@ def transR(): res = bvm(vh -vt, mr) + vr code = codegen.cpu.print_cpp(res._gen_ir()) + ast = res._gen_ir() + fuse_operators(ast) + + + code = codegen.cpu.print_cpp(ast) print(code) @@ -76,10 +98,19 @@ def transF(): vh = Batch(Eemb[h]) vt = Batch(Eemb[t]) vr = Batch(Remb[r]) + + alpha = Const(val=2, dtype='float') + alpha = Batch(alpha) + # alpha = 2 - res = bvv(vh, vt) - bvv(vh - vt, vr) - + res = bvv(vh, vt) - bvv(vh - vt, vr) + code = codegen.cpu.print_cpp(res._gen_ir()) + ast = res._gen_ir() + fuse_operators(ast) + + + code = codegen.cpu.print_cpp(ast) print(code) @@ -100,14 +131,52 @@ def RESCAL(): res = bvv(bvm(vh, mr), vt) code = codegen.cpu.print_cpp(res._gen_ir()) + + ast = res._gen_ir() + + fuse_operators(ast) + + code = codegen.cpu.print_cpp(ast) print(code) +def test(): + nnodes = Var('nnodes') + nedges = Var('nedges') + dim = Var('dim') + batch_size = Var('batch_size') + Eemb = Tensor('Eemb', (nnodes, dim)) + Remb = Tensor('Remb', (nedges, dim)) + Proj = Tensor('Proj', (nedges, dim, dim)) + h = Tensor('h', (batch_size, ), dtype='int') + t = Tensor('t', (batch_size, ), dtype='int') + r = Tensor('r', (batch_size, ), dtype='int') + vh = Batch(Eemb[h]) + vt = Batch(Eemb[t]) + vr = Batch(Remb[r]) + vrr = Batch(Remb[r]) + proj_m = Batch(Proj[r]) + proj_h = Batch(Proj[h]) + + # res = vh - vt + vr - vrr + # res = bov(vh+vr, vt-vr) + res = vh+vt - (proj_m+ proj_h) + + # code = codegen.cpu.print_cpp(res._gen_ir()) + # print(code) + ast = res._gen_ir() + + fuse_operators(ast) + code = codegen.cpu.print_cpp(ast) + print(code) + # t = codegen.cpu.print_cpp(ast) + # print(t) if __name__ == "__main__": - transE() + # test() + # transE() transH() - transR() - transF() - RESCAL() + # transR() + # transF() + # RESCAL() diff --git a/codegen/gpu.py b/codegen/gpu.py new file mode 100644 index 0000000..9db4763 --- /dev/null +++ b/codegen/gpu.py @@ -0,0 +1,133 @@ +from core.ast2ir import * +import helpers +import batch +from batch.opt.ir import * + +def to_string(ir): + match ir.__class__.__name__: + case 'Expr': + return f"({to_string(ir.left)}" + f" {ir.op} " + f"{to_string(ir.right)})" + case 'Assignment': + if ir.op is None: + return f"{to_string(ir.lhs)} = {to_string(ir.rhs)};\n" + else: + return f"{to_string(ir.lhs)} {ir.op}= {to_string(ir.rhs)};\n" + case 'Loop': + code = f"for (int {to_string(ir.iterate)} = {to_string(ir.start)}; {to_string(ir.iterate)} < {to_string(ir.end)}; {to_string(ir.iterate)} += {to_string(ir.step)}) {{\n" + for e in ir.body: + if e: + code += to_string(e) + code += "} \n" + return code + case 'CUDAThread': + pass + case 'Scalar' | 'Ndarray' | 'Ref': + return ir.name() + case 'Index': + # print(ir.ind_arr, ir.index, ir.dobject, ir.index.addr()) + if ir.ind_arr != None: + if type(ir.ind_arr) == Slice: + return f'{to_string(ir.dobject)}[(({to_string(ir.ind_arr.start)})+({to_string(ir.ind_arr.step)})*({to_string(ir.index)}))]' + else: # idx is a Tensor + if ir.index == None: + return f'{to_string(ir.dobject)}[{to_string(ir.ind_arr)}]' + else: + return f'{to_string(ir.dobject)}[{to_string(ir.ind_arr)}[{to_string(ir.index)}]]' + else: + return f'{to_string(ir.dobject)}[{to_string(ir.index)}]' + case 'Decl': + # return '' + # variables are passed in as pytorch arguments + if type(ir.dobject) == Scalar: + if not ir.dobject.is_arg: + # it is a zero or one + if ir.dobject.val != None: + return f"{ir.dobject.dtype} {ir.dobject.name()} = {to_string(ir.dobject.val)};\n" + else: + return f"{ir.dobject.dtype} {ir.dobject.name()};\n" + else: + return '' + elif type(ir.dobject) == Ndarray: + code = '' + if not ir.dobject.is_arg: + if ir.dobject.val != None: + code = f'torch::Tensor obj_{ir.dobject.name()} = torch::{"ones" if ir.dobject.val == 1 else "zeros"}({{{",".join([to_string(s) for s in ir.dobject.size])}}}, at::k{"Int" if ir.dobject.dtype=="int" else "Float"});\n' + else: + code = f'torch::Tensor obj_{ir.dobject.name()} = torch::empty({{{",".join([to_string(s) for s in ir.dobject.size])}}}, at::k{"Int" if ir.dobject.dtype=="int" else "Float"});\n' + + # code += f'auto {ir.dobject.name()} = obj_{ir.dobject.name()}.accessor<{ir.dobject.dtype}, {len(ir.dobject.size)}>();\n' + return code + elif type(ir.dobject) == Ref: + code = f'{ir.dobject.dobject.dtype}* {ir.dobject.name()} = ({ir.dobject.dobject.dtype}*)&{ir.dobject.dobject.addr()}' + return code + case _: + return str(ir) + +def gen_cuda(ast, cpu_ir, gpu_ir): + # 2 ir list for cpu and gpu + def action_cpu(node, res): + if type(node) == Var or type(node) == One or type(node) == Zero or type(node) == Ones or type(node) == Zeros or type(node) == Tensor: + res.extend(node.decl) + elif type(node) == TensorOp: + res.extend(node.decl) + # res.extend(node.compute) + elif type(node) == batch.ast.BatchOp: + res.extend(node.decl) + # res.extend(node.compute) + + def action_cuda(node, res): + if type(node) == TensorOp: + # res.extend(node.decl) + res.extend(node.compute) + elif type(node) == batch.ast.BatchOp: + res.extend(node.decl) + res.extend(node.compute) + t = helpers.Traversal(action_cpu) + cpu_ir.extend(t(ast)) + + t = helpers.Traversal(action_cuda) + gpu_ir.extend(t(ast)) + +def print_cpp(ast): + cpu_ir = [] + gpu_ir = [] + gen_cuda(ast, cpu_ir, gpu_ir) + print(cpu_ir, "CUDA IR:::" , gpu_ir) + + args = helpers.get_input_nodes(ast) + argscpu = ', '.join([f'torch::Tensor obj_{a}' if type(args[a]) == Tensor else f'{args[a].dtype} {a}' for a in args]) + # argsptr = ', '.join([f'obj_{a}.data_ptr<{args[a].dtype}>()' if type(args[a]) == Tensor else f'{a}' for a in args]) + # ptrs = ', '.join([f'{args[a].dtype}* {a}' if type(args[a]) == Tensor else f'{args[a].dtype} {a}' for a in args]) + + print(args) + + argsptr = ', '.join([f'obj_{a}.packed_accessor32<{args[a].dtype}, {len(args[a].ref_size)}, torch::RestrictPtrTraits>()' if type(args[a]) == Tensor else f'{a}' for a in args]) + ptrs = ', '.join([f'torch::PackedTensorAccessor32<{args[a].dtype}, {len(args[a].ref_size)}, torch::RestrictPtrTraits> {a}' if type(args[a]) == Tensor else f'{args[a].dtype} {a}' for a in args]) + # in cuda kernel: const torch::PackedTensorAccessor32 + # host call cuda: .packed_accessor32() + + code = '' + declare = '' + for d in gpu_ir: + if d: + if type(d) == Decl and type(d.dobject) == Ndarray: + declare += to_string(d) + argsptr += f', obj_{d.dobject.name()}.packed_accessor32<{d.dobject.dtype}, {len(d.dobject.size)}, torch::RestrictPtrTraits>()' + ptrs += f', torch::PackedTensorAccessor32<{d.dobject.dtype}, {len(d.dobject.size)}, torch::RestrictPtrTraits> {d.dobject.name()}' + else: + code += to_string(d) + + Return = '' + if type(ast.eval) == Scalar: + rtype = ast.dtype + Return = f'return {ast.eval.name()};\n' + elif type(ast.eval) == Ndarray: + rtype = 'torch::Tensor' + Return = f'return obj_{ast.eval.name()};\n' + else: + raise TypeError('wrong output type', ast.eval) + + with open('codegen/gpu_template.cu', 'r') as f: + c_code = f.read() + c_code = c_code.replace('RTYPE', rtype).replace('FNAME', ast.name).replace('ARGS', argscpu).replace('CODE', code).replace('PTR_VARS', argsptr).replace('PTRS', ptrs).replace('DECL', declare).replace('RETURN', Return) + return c_code \ No newline at end of file diff --git a/codegen/gpu_template.cu b/codegen/gpu_template.cu new file mode 100644 index 0000000..63e39b1 --- /dev/null +++ b/codegen/gpu_template.cu @@ -0,0 +1,16 @@ +#include + +__global__ void FNAME_kernel(PTRS){ + CODE +} + +RTYPE FNAME(ARGS) +{ + DECL + FNAME_kernel<<< >>>(PTR_VARS); + RETURN +} + +PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { + m.def("run", &FNAME); +} \ No newline at end of file From 99b569fb66277225152814d90c02160aa3ff89a9 Mon Sep 17 00:00:00 2001 From: Lihan Date: Sun, 8 Oct 2023 10:09:12 -0500 Subject: [PATCH 4/8] add ast_ref for tracking ir in ast --- core/ir.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/ir.py b/core/ir.py index 4963406..76fbed5 100644 --- a/core/ir.py +++ b/core/ir.py @@ -1,10 +1,12 @@ class IR: - pass + def __init__(self) -> None: + self.ast_ref = None class DOject(IR): nobjects = 0 def __init__(self, dtype: str): + super(IR, self).__init__() self.dobject_id = DOject.nobjects DOject.nobjects += 1 self.dtype = dtype @@ -12,6 +14,7 @@ def __init__(self, dtype: str): class Expr(IR): def __init__(self, left, right, op: str): + super(IR, self).__init__() self.left = left self.right = right self.op = op @@ -20,6 +23,7 @@ def __init__(self, left, right, op: str): class Assignment(IR): def __init__(self, lhs, rhs, op=None): + super(IR, self).__init__() self.lhs = lhs self.rhs = rhs self.op = op @@ -30,6 +34,7 @@ class Loop(IR): loop_id = 0 def __init__(self, start, end, step, body: list): + super(IR, self).__init__() self.lid = Loop.loop_id Loop.loop_id += 1 self.start = start @@ -83,6 +88,7 @@ def addr(self): class Index(IR): nindices = 0 def __init__(self, dobject, index=None, ind_arr=None): + super(IR, self).__init__() self.dobject = dobject self.index = index self.ind_arr = ind_arr @@ -112,4 +118,5 @@ def addr(self): class Decl(IR): def __init__(self, dobject): + super(IR, self).__init__() self.dobject = dobject \ No newline at end of file From b63d3b4744ffba81d8fdad9ab1cf21388a84ec85 Mon Sep 17 00:00:00 2001 From: Lihan Date: Sat, 14 Oct 2023 23:26:42 -0500 Subject: [PATCH 5/8] update for backup --- batch/ast2ir.py | 7 + .../__pycache__/fusion_rules.cpython-310.pyc | Bin 13676 -> 14395 bytes batch/opt/__pycache__/ir.cpython-310.pyc | Bin 1297 -> 3087 bytes .../__pycache__/parallelism.cpython-310.pyc | Bin 911 -> 2905 bytes batch/opt/fusion_rules.py | 305 +++++++++++------- batch/opt/ir.py | 79 ++++- batch/opt/parallelism.py | 142 ++++++-- batch/opt/tiling.py | 12 + batch/test/kge.py | 135 ++++++-- codegen/__init__.py | 3 +- codegen/cpu.py | 36 +-- codegen/gpu.py | 84 ++--- codegen/gpu_instructionsets.py | 21 ++ codegen/gpu_template.cu | 2 +- 14 files changed, 573 insertions(+), 253 deletions(-) create mode 100644 batch/opt/tiling.py create mode 100644 codegen/gpu_instructionsets.py diff --git a/batch/ast2ir.py b/batch/ast2ir.py index a6ae683..a2c0be5 100644 --- a/batch/ast2ir.py +++ b/batch/ast2ir.py @@ -29,6 +29,8 @@ def gen_ir(node): node.eval = node.base.eval node.decl = node.base.decl[:] node.compute = node.base.compute[:] + for i in node.compute: + i.ast_ref = node node.base.decl.clear() node.base.compute.clear() @@ -46,6 +48,7 @@ def gen_ir(node): res = bind(node.eval, pre_loop.iterate) inner_loop = Loop(0, node.operators[0].eval.size[1], 1, []) pre_loop.body.append(inner_loop) + pre_loop.ast_ref = node lhs = bind(lhs, inner_loop.iterate) rhs = bind(rhs, inner_loop.iterate) assign = Assignment(res, Expr(lhs, rhs, '*'), '+') @@ -65,6 +68,7 @@ def gen_ir(node): res = bind(node.eval, pre_loop.iterate) inner_loop = Loop(0, node.eval.size[1], 1, []) pre_loop.body.append(inner_loop) + pre_loop.ast_ref = node rhs = bind(rhs, inner_loop.iterate) res = bind(res, inner_loop.iterate) @@ -77,6 +81,7 @@ def gen_ir(node): node.operators[1]._gen_ir() size = helpers.get_ir_of_size(node._size()) node.base.eval = node.eval = Ndarray(node.dtype, size) + node.eval.val = 0 node.decl = [Decl(node.eval)] pre_loop = Loop(0, node.eval.size[0], 1, []) node.compute = [pre_loop] @@ -85,6 +90,7 @@ def gen_ir(node): res = bind(node.eval, pre_loop.iterate) loop1 = Loop(0, node.eval.size[1], 1, []) pre_loop.body.append(loop1) + pre_loop.ast_ref = node res = bind(res, loop1.iterate) loop2 = Loop(0, node.operators[0].eval.size[1], 1, []) loop1.body.append(loop2) @@ -109,6 +115,7 @@ def gen_ir(node): res = bind(node.eval, pre_loop.iterate) loop1 = Loop(0, node.eval.size[1], 1, []) pre_loop.body.append(loop1) + pre_loop.ast_ref = node lhs = bind(lhs, loop1.iterate) res = bind(res, loop1.iterate) loop2 = Loop(0, node.eval.size[2], 1, []) diff --git a/batch/opt/__pycache__/fusion_rules.cpython-310.pyc b/batch/opt/__pycache__/fusion_rules.cpython-310.pyc index 2f58c1d3a3c8b7ee92de4d1fffdbef584ce50ed8..00be7fca84eebaab49cf7e99c919d09ab2ab616f 100644 GIT binary patch literal 14395 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%0DPb|Z&ozI>1XFxWv0y9^s0HQ?TgRsi#Vlxq#5on zd*^<~ARAgIRNid>$#?a2oZ1d+V9^gKUulbz>Uk#d)Ql?J4_CZQf^79!$Oa8@|TtRG)nSm z1|nS1$vmqpbkOxG7vL7fs3l}o4IyHZceszGNUscmY!mI`4#NECwK!vV`~fxGHKuys z;t5mhF9-Q#TWA;5F*=_K?|Cf~4mU9Lr!Epq@eA5l@{`VN>3JI_5^mE}QUrGit}SyF zv(#X}eZ-bIP~_So;T~oe3p%>CAh?g8!UF=j1@Lg@dFq=?vb@qq=*&wwnwJ7=SEtSA zu4BTNMNbZh{Z+NxJ|3q3EtpyWUT}-n`xmWGaLNiz^ID1Z@Zr?c0?bAR{$BaM`!ay? c3&%by*+tK%7y1QVnu=ar#M+|WY|D=P1xelIwEzGB diff --git a/batch/opt/__pycache__/parallelism.cpython-310.pyc b/batch/opt/__pycache__/parallelism.cpython-310.pyc index d7fd6d5a6998a950a2ffbea1dde29c1e7c85ad48..4f6b1e4099e06f57d0cbc4eb47f48e82327550b5 100644 GIT binary patch literal 2905 zcmbtW&yVCr6?T=&ZnxX>V|ypNnGgj?LZB1069s`#h!Dxlf_8yPqDdlUSZH}&wKLt@ zV|&WgyEBqoTFEHDKfrl%T+W<1BMuQKgz5qkryRMi6ov1VyL)C9al%%;s`u*EFMscS ztO=_oeu?w;#wr zhNwf?krC>S?8+E*BzrPJ-IXhH6?H86o;O(g94mMOX4$K|nNRKc6QHKDjZ%FEVZ7Qi ztl|3k+MD^%e5Y;H{+OTZRZ{ykyU+D=HKwn^_Bao;hb_QpZYYTo#@Hw>H}F7Kd>EH+#RdTSaI{D)OM|Un3ls->!B%zS{+z1QTL1u^yuKw z*!b{J z`Z~VxlkacI)TCQ?)9k*I!)duS866%@x3)k0>CLTddS9W(WQSWtX|~E#r;~}AjH;s> z<*AM^oJ#dAh?of$vyjDKMqDt#W44Z0bmAwj7V#H-_8ffNsEnd*@mzxa53pW>okQbu z*g0Td>VTckVaLan7h!+h_znsH-*DFgvh24R_TD7E5QGH*gWkMcjh`U^lIL7`W$i19DQ7x?NlDq|3f zhqgfAxn%nzdg%NOGn7cLPz-L{yiYmyb#7S$uIwq>`oea6=lJKx+is6;zxwY7 z-mK%&J#uTl*{)f4&aTwqrwm_YvHD+4pUynK+OS#VP7m4`Z=78*&(s*}e6L}LILjM5WAlfsG5ZBJy6;3XgV399&94f zPfKMx?@;=_TiR|>DxI1_S9T?p60{Qxi&SpI9rnZNy?Yb2Q#{OV zn3ko=r49G8bdqWt$aZg;M<(B4K^D>PpN*{<4o$ z0-eP?;_I;X*#_S5Q39IlyEIEcd+TT=oJR3SF%$m~8=B_4lybxk!~-tn{)7es!_yx_ zGW`n#7$Y6w{{D24{>7|H_3jN?nI{} z3f(oTze_v291pgeO=UW)N|o)Sw|WsGaTgq2a((`y!+itv8ac8>{cpkb4%{vwPH9P3 zghTvSXj~xKZ~ng|n{z^guCN9uJ38=pHC+uY9D#m;+FvE{HHbFtmj`pZeazgKdg}Q4 zCfp||)jGs^I`Bt=C*1Y^*D)y)+%Y%MXpae2MA$4g*WQ)wXqOwBa_y;=ZM68uI6-Gc z_iEHlnEnYG$bFEJRyXiF({(>3H;7r1W%CVmz(cA7{QS(#_-%o|W0ICrC@bC3-7(z6 n7xv)7fUa2^y>(Q`=|ugIFsz7bB9Gv9uODqhSEJXWevACL~gf zKcqd1f5TrfS5G~9_2fI#w9PE<=kq?#zO!33n=yj({dZgZq6qzQ#p)qYyavoNd)-HjGy4d3F{S>==dEj-(*)EtyfEYAG3vBR+ zsK+ipAsj!r?rfRhwuAjF;N%8>w8VV}qmpoRfD8prZGo{<3)R$l$C+bOpJ5X%h?>C+ zK3O}DFA&6a7Z(KPmpvZ9Hs7FX%uqjc{5|xgIVZp;p3gX)k$#icz_-30nhmGI!v&eU zeZ%uRoXmm?r-)AOo^P6a4qxC4*vW0UYTf$&rTak2IU5g{Nr!!%O~rKG43%K~JwNT8 zPUAsdrK7#9fV_PROgc|G9`RG1?atHZ-TRiBi4?Z})@gl`cCC^^F;gjRBgquYbCKJc zR9R_ky^{T_>9i{3*a#bEIxDqdWhyNFSXI&nqU4sAtN_i-I4M8|6`$A|%qe53HbUC? zjn>(5SqP8~K|(l$i!`?(lhSMKLDi);VA>o3sW292fO_6fq#nX2`1c~=%&=s@(vjf( zu}t!8I2o)8H^PcbTu;SXV Zij7_t6(8r~mHWrK0WLxj?BSk9@E?a->=Xb1 diff --git a/batch/opt/fusion_rules.py b/batch/opt/fusion_rules.py index c327683..c1ace26 100644 --- a/batch/opt/fusion_rules.py +++ b/batch/opt/fusion_rules.py @@ -47,22 +47,17 @@ def loop_merge(o_loop, i_loop): else: loop_merge(o_loop.body[-1], i_loop.body[-1]) - # o_loop.body.insert(-1, i_loop.body[-1]) - # else: - # if isinstance(i_loop, Loop): - # for ii in range(len(i_loop.body)): - # o_loop.body.insert(ii, i_loop.body[ii]) - # else: - # o_loop.body.insert(0, i_loop) - def change_index(iassign, iter_o, iter_i): - if isinstance(iassign, Index): + if isinstance(iassign, Indexing): + for idx, item in enumerate(iter_i): - if iassign.index == item: - iassign.index = iter_o[idx] - if isinstance(iassign.dobject, Index): + if iassign.idx == item: + iassign.idx = iter_o[idx] + if isinstance(iassign.dobject, Indexing): change_index(iassign.dobject, iter_o, iter_i) + if isinstance(iassign.idx, Indexing): + change_index(iassign.idx, iter_o, iter_i) elif isinstance(iassign, Expr): # both item.left and item.right @@ -75,21 +70,35 @@ def change_index(iassign, iter_o, iter_i): elif isinstance(iassign, Loop): for i in iassign.body: change_index(i, iter_o, iter_i) - + +def change_ref(ir, ast): + ir.astnode = ast + if isinstance(ir, Indexing): + if isinstance(ir.dobject, Indexing): + change_ref(ir.dobject, ast) + + elif isinstance(ir, Expr): + # both item.left and item.right + change_ref(ir.left, ast) + change_ref(ir.right, ast) + elif isinstance(ir, Assignment): + # both item.lhs and item.rhs + change_ref(ir.lhs, ast) + change_ref(ir.rhs, ast) + elif isinstance(ir, Loop): + for i in ir.body: + change_ref(i, ast) + # print('after::', i, i.ast_ref.op_type) + def swap_arr_to_reg(ir, pre, cur): - # print(ir, codegen.cpu.to_string(ir), codegen.cpu.to_string(pre), codegen.cpu.to_string(cur)) - if isinstance(ir, Index): - # print(ir.dobject, pre) + if isinstance(ir, Indexing): temp = ir - while isinstance(temp, Index): - # print(temp, codegen.cpu.to_string(temp)) + while isinstance(temp, Indexing): temp = temp.dobject - # print('*************', temp, codegen.cpu.to_string(temp), pre) if temp == pre: return cur else: return ir - elif isinstance(ir, Expr): ir.left = swap_arr_to_reg(ir.left, pre, cur) ir.right = swap_arr_to_reg(ir.right, pre, cur) @@ -389,43 +398,57 @@ def fuse_operators(ast): fuse_operators(ast.operators[0]) else: return - + + # if ast.op_type in core.ast.op_mapping.keys() and type(ast.operators[1]) == Batch and type(ast.operators[0]) == Batch: + # outer_loop = ast.compute[0] + # a = Scalar(ast.eval.dtype) + # pre_arr = ast.eval + # ast.decl.pop(0) + # ast.decl.append(Decl(a)) + # ast.eval = a + # outer_loop = swap_arr_to_reg(outer_loop, pre_arr, a) + if type(ast.operators[1]) == BatchOp and ast.op_type in core.ast.op_mapping.keys(): # fuse operators1 into elementwise if ast.item_type == 'vec' and ast.operators[1].item_type == ast.item_type: # check if type of operator1 is vector if ast.operators[1].compute and ast.compute: + for i in ast.operators[1].compute: + change_ref(i, ast) outer_loop = ast.compute[0] loop = ast.operators[1].compute[0] - # oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) - # for i in iloop.body: - # change_index(i, iter_o, iter_i) - # for i in range(len(iloop.body)): - # oloop.body.insert(i, iloop.body[i]) - loop_merge(outer_loop, loop) - if ast.operators[1].op_type in core.ast.op_mapping.keys() or ast.operators[1].op_type == "scal_mul_vec": - a = Scalar(ast.operators[1].eval.dtype) + if ast.operators[1].op_type in core.ast.op_mapping.keys() or ast.operators[1].op_type in ["scal_mul_vec", "vec_mul_vec"]: + a = Scalar(ast.operators[1].eval.dtype, val=0) pre_arr = ast.operators[1].eval - ast.operators[1].decl = [Decl(a)] + ast.operators[1].decl.pop(0) + ast.operators[1].decl.append(Decl(a)) ast.operators[1].eval = a outer_loop = swap_arr_to_reg(outer_loop, pre_arr, a) - ast.operators[1].compute.clear() + # ast.operators[1].compute.clear() + ast.operators[1].valid = False + ast.decl.extend(ast.operators[1].decl) + elif ast.item_type == 'scal' and ast.operators[1].item_type == ast.item_type: # check if type of operator1 is scalar if ast.operators[1].compute and ast.compute: + for i in ast.operators[1].compute: + change_ref(i, ast) outer_loop = ast.compute[0] loop = ast.operators[1].compute[0] loop_merge(outer_loop, loop) - if ast.operators[1].op_type in core.ast.op_mapping.keys() or ast.operators[1].op_type == "scal_mul_vec": - a = Scalar(ast.operators[1].eval.dtype) + if ast.operators[1].op_type in core.ast.op_mapping.keys() or ast.operators[1].op_type in ["scal_mul_vec", "vec_mul_vec"]: + a = Scalar(ast.operators[1].eval.dtype, val=0) pre_arr = ast.operators[1].eval - ast.operators[1].decl = [Decl(a)] + ast.operators[1].decl.pop(0) + ast.operators[1].decl.append(Decl(a)) ast.operators[1].eval = a outer_loop = swap_arr_to_reg(outer_loop, pre_arr, a) - ast.operators[1].compute.clear() + # ast.operators[1].compute.clear() + ast.operators[1].valid = False + ast.decl.extend(ast.operators[1].decl) elif ast.item_type not in ['vec', 'scal']: raise ValueError(f"Tensor type is wrong. Expect ast as 'vec' or 'scal' but found '{ast.item_type}'.") elif ast.operators[1].item_type not in ['vec', 'scal']: @@ -439,30 +462,42 @@ def fuse_operators(ast): if ast.item_type == 'vec' and ast.operators[0].item_type == ast.item_type: # check if type of operator0 is vector if ast.operators[0].compute and ast.compute: + for i in ast.operators[0].compute: + change_ref(i, ast) outer_loop = ast.compute[0] loop = ast.operators[0].compute[0] loop_merge(outer_loop, loop) - if ast.operators[0].op_type in core.ast.op_mapping.keys() or ast.operators[0].op_type == "scal_mul_vec": - a = Scalar(ast.operators[0].eval.dtype) + if ast.operators[0].op_type in core.ast.op_mapping.keys() or ast.operators[0].op_type in ["scal_mul_vec", "vec_mul_vec"]: + a = Scalar(ast.operators[0].eval.dtype, val=0) pre_arr = ast.operators[0].eval - ast.operators[0].decl = [Decl(a)] + ast.operators[0].decl.pop(0) + ast.operators[0].decl.append(Decl(a)) ast.operators[0].eval = a outer_loop = swap_arr_to_reg(outer_loop, pre_arr, a) - ast.operators[0].compute.clear() + # ast.operators[0].compute.clear() + ast.operators[0].valid = False + ast.decl.extend(ast.operators[0].decl) + if ast.item_type == 'scal' and ast.operators[0].item_type == ast.item_type: # check if type of operator1 is scalar if ast.operators[0].compute and ast.compute: + for i in ast.operators[0].compute: + change_ref(i, ast) outer_loop = ast.compute[0] loop = ast.operators[0].compute[0] loop_merge(outer_loop, loop) - if ast.operators[0].op_type in core.ast.op_mapping.keys() or ast.operators[0].op_type == "scal_mul_vec": - a = Scalar(ast.operators[0].eval.dtype) + if ast.operators[0].op_type in core.ast.op_mapping.keys() or ast.operators[0].op_type in ["scal_mul_vec", "vec_mul_vec"]: + a = Scalar(ast.operators[0].eval.dtype, val=0) pre_arr = ast.operators[0].eval - ast.operators[0].decl = [Decl(a)] + ast.operators[0].decl.pop(0) + ast.operators[0].decl.append(Decl(a)) ast.operators[0].eval = a outer_loop = swap_arr_to_reg(outer_loop, pre_arr, a) - ast.operators[0].compute.clear() + # ast.operators[0].compute.clear() + ast.operators[0].valid = False + ast.decl.extend(ast.operators[0].decl) + elif ast.item_type not in ['vec','scal']: raise ValueError(f"Tensor shape are not the same. Expect ast as 'vec' or 'scal' but found '{ast.item_type}'.") elif ast.operators[0].item_type not in ['vec','scal']: @@ -474,61 +509,60 @@ def fuse_operators(ast): # fuse operators1 into bvv if ast.item_type == 'scal' and ast.operators[1].item_type == 'vec': # check if type of operator1 is vector - print(ast.operators[1].compute , ast.compute) if ast.operators[1].compute and ast.compute: + for i in ast.operators[1].compute: + change_ref(i, ast) outer_loop = ast.compute[0] loop = ast.operators[1].compute[0] - - # oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) - # for i in iloop.body: - # change_index(i, iter_o, iter_i) - # for i in range(len(iloop.body)): - # oloop.body.insert(i, iloop.body[i]) - # iloop.body.clear() + loop_merge(outer_loop, loop) - if ast.operators[1].op_type in core.ast.op_mapping.keys() or ast.operators[1].op_type == "scal_mul_vec": + if ast.operators[1].op_type in core.ast.op_mapping.keys() or ast.operators[1].op_type in ["scal_mul_vec", "vec_mul_vec"]: a = Scalar(ast.operators[1].eval.dtype) pre_arr = ast.operators[1].eval - ast.operators[1].decl = [Decl(a)] + ast.operators[1].decl.pop(0) + ast.operators[1].decl.append(Decl(a)) ast.operators[1].eval = a outer_loop = swap_arr_to_reg(outer_loop, pre_arr, a) - ast.operators[1].compute.clear() + # ast.operators[1].compute.clear() + ast.operators[1].valid = False + ast.decl.extend(ast.operators[1].decl) + elif ast.operators[1].item_type != 'vec': raise ValueError(f"Tensor type is wrong. Expect operators[1] as 'vec' but found '{ast.operators[1].item_type}'.") if type(ast.operators[0]) == BatchOp and ast.op_type == 'vec_mul_vec': - # fuse operators1 into bvv + # fuse operators0 into bvv if ast.item_type == 'scal' and ast.operators[0].item_type == 'vec': # check if type of operator0 is vector if ast.operators[0].compute and ast.compute: + for i in ast.operators[0].compute: + change_ref(i, ast) outer_loop = ast.compute[0] loop = ast.operators[0].compute[0] - - # oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) - # for i in iloop.body: - # change_index(i, iter_o, iter_i) - # for i in range(len(iloop.body)): - # oloop.body.insert(i, iloop.body[i]) - # iloop.body.clear() loop_merge(outer_loop, loop) - if ast.operators[0].op_type in core.ast.op_mapping.keys() or ast.operators[0].op_type == "scal_mul_vec": + if ast.operators[0].op_type in core.ast.op_mapping.keys() or ast.operators[0].op_type in ["scal_mul_vec", "vec_mul_vec"]: a = Scalar(ast.operators[0].eval.dtype) pre_arr = ast.operators[0].eval - ast.operators[0].decl = [Decl(a)] + ast.operators[0].decl.pop(0) + ast.operators[0].decl.append(Decl(a)) ast.operators[0].eval = a outer_loop = swap_arr_to_reg(outer_loop, pre_arr, a) - ast.operators[0].compute.clear() + # ast.operators[0].compute.clear() + ast.operators[0].valid = False + ast.decl.extend(ast.operators[0].decl) + elif ast.operators[0].item_type != 'vec': raise ValueError(f"Tensor type is wrong. Expect operators[0] as 'vec' but found '{ast.operators[0].item_type}'.") - if type(ast) == BatchOp and ast.op_type == 'scal_mul_vec': # fuse operators into bsv if ast.item_type == 'vec' and ast.operators[1].item_type == 'vec' and ast.operators[0].item_type == 'scal': # check if type of operators are scal and vector if ast.operators[1].compute and ast.compute: + for i in ast.operators[1].compute: + change_ref(i, ast) outer_loop = ast.compute[0] loop = ast.operators[1].compute[0] @@ -539,14 +573,20 @@ def fuse_operators(ast): # oloop.body.insert(i, iloop.body[i]) # iloop.body.clear() loop_merge(outer_loop, loop) - if ast.operators[1].op_type in core.ast.op_mapping.keys() or ast.operators[1].op_type == "scal_mul_vec": + if ast.operators[1].op_type in core.ast.op_mapping.keys() or ast.operators[1].op_type in ["scal_mul_vec", "vec_mul_vec"]: a = Scalar(ast.operators[1].eval.dtype) pre_arr = ast.operators[1].eval - ast.operators[1].decl = [Decl(a)] + ast.operators[1].decl.pop(0) + ast.operators[1].decl.append(Decl(a)) ast.operators[1].eval = a outer_loop = swap_arr_to_reg(outer_loop, pre_arr, a) - ast.operators[1].compute.clear() + # ast.operators[1].compute.clear() + ast.operators[1].eval = None + ast.decl.extend(ast.operators[1].decl) + if ast.operators[0].compute and ast.compute: + for i in ast.operators[0].compute: + change_ref(i, ast) outer_loop = ast.compute[0] loop = ast.operators[0].compute[0] @@ -563,14 +603,18 @@ def fuse_operators(ast): for i in range(len(iloop.body)): oloop.body.insert(i, iloop.body[i]) - if ast.operators[0].op_type in core.ast.op_mapping.keys() or ast.operators[0].op_type == "scal_mul_vec": + if ast.operators[0].op_type in core.ast.op_mapping.keys() or ast.operators[0].op_type in ["scal_mul_vec", "vec_mul_vec"]: a = Scalar(ast.operators[0].eval.dtype) pre_arr = ast.operators[0].eval - ast.operators[0].decl = [Decl(a)] + ast.operators[0].decl.pop(0) + ast.operators[0].decl.append(Decl(a)) ast.operators[0].eval = a oloop = swap_arr_to_reg(oloop, pre_arr, a) - iloop.body.clear() - ast.operators[0].compute.clear() + # iloop.body.clear() + # ast.operators[0].compute.clear() + ast.operators[0].valid = False + ast.decl.extend(ast.operators[0].decl) + elif ast.operators[0].item_type != 'scal': raise ValueError(f"Tensor type is wrong. Expect operators[0] as 'scal' but found '{ast.operators[0].item_type}'.") elif ast.operators[1].item_type != 'vec': @@ -579,56 +623,59 @@ def fuse_operators(ast): raise ValueError(f"Tensor type is wrong. Expect ast node as 'vec' but found '{ast.item_type}'.") - if type(ast) == BatchOp and ast.op_type == 'vec_mul_mat': - # fuse operators1 into bov - if ast.item_type == 'vec' and ast.operators[0].item_type == 'vec' and ast.operators[1].item_type == 'mat': - # check if type of operator1 is vector - if ast.operators[1].compute and ast.compute: - outer_loop = ast.compute[0] - loop = ast.operators[1].compute[0] - + # if type(ast) == BatchOp and ast.op_type == 'vec_mul_mat': + # # fuse operators1 into bvm + # if ast.item_type == 'vec' and ast.operators[0].item_type == 'vec' and ast.operators[1].item_type == 'mat': + # # check if type of operator1 is vector + # if ast.operators[1].compute and ast.compute: + # for i in ast.operators[1].compute: + # change_ref(i, ast) + # outer_loop = ast.compute[0] + # loop = ast.operators[1].compute[0] + # loop_merge(outer_loop, loop) + # # ast.operators[1].compute.clear() + # ast.operators[1].valid = False + # ast.decl.extend(ast.operators[1].decl) + + # if ast.operators[0].compute and ast.compute: + # print(ast.compute) + # for i in ast.compute: + # print('before:::', codegen.gpu.to_string(i)) + # ast.compute.extend(ast.operators[0].compute) + # for i in ast.compute: + # print('after:::', codegen.gpu.to_string(i)) + # ast.operators[0].valid = False + # for i in ast.operators[0].compute: + # change_ref(i, ast) + # outer_loop = ast.compute[0] + # loop = ast.operators[0].compute[0] # oloop = outer_loop # iloop = loop # iter_o = [] # iter_i = [] - # if (isinstance(oloop, Loop) and isinstance(iloop, Loop)) and oloop.start == iloop.start and oloop.end.__name__ == iloop.end.__name__ and oloop.step == iloop.step: + + # while (isinstance(oloop, Loop) and isinstance(iloop, Loop)) and oloop.start == iloop.start and oloop.end.__name__ == iloop.end.__name__ and oloop.step == iloop.step: # iter_i.append(iloop.iterate) # iter_o.append(oloop.iterate) + # if isinstance(oloop.body[-1], Loop) and isinstance(iloop, Loop): + # oloop = oloop.body[-1].body[-1] + # iloop = iloop.body[-1] + # else: + # break # for i in iloop.body: # change_index(i, iter_o, iter_i) # for i in range(len(iloop.body)): # oloop.body.insert(i, iloop.body[i]) - # iloop.body.clear() - loop_merge(outer_loop, loop) - ast.operators[1].compute.clear() + # # ast.operators[0].compute.clear() + # ast.operators[0].valid = False + # ast.decl.extend(ast.operators[0].decl) - if ast.operators[0].compute and ast.compute: - outer_loop = ast.compute[0] - loop = ast.operators[0].compute[0] - oloop = outer_loop - iloop = loop - iter_o = [] - iter_i = [] - - while (isinstance(oloop, Loop) and isinstance(iloop, Loop)) and oloop.start == iloop.start and oloop.end.__name__ == iloop.end.__name__ and oloop.step == iloop.step: - iter_i.append(iloop.iterate) - iter_o.append(oloop.iterate) - if isinstance(oloop.body[-1], Loop) and isinstance(iloop, Loop): - oloop = oloop.body[-1].body[-1] - iloop = iloop.body[-1] - else: - break - for i in iloop.body: - change_index(i, iter_o, iter_i) - for i in range(len(iloop.body)): - oloop.body.insert(i, iloop.body[i]) - ast.operators[0].compute.clear() - elif ast.item_type != 'vec': - raise ValueError(f"Tensor shape are not the same. Expect ast node type 'mat' but found '{ast.item_type}'.") - elif ast.operators[1].item_type != 'mat': - raise ValueError(f"Tensor shape are not the same. Expect ast.operators[1] node type 'mat' but found '{ast.operators[1].item_type}'.") - elif ast.operators[0].item_type != 'vec': - raise ValueError(f"Tensor shape are not the same. Expect ast.operators[0] node type 'vec' but found '{ast.operators[1].item_type}'.") + # elif ast.item_type != 'vec': + # raise ValueError(f"Tensor shape are not the same. Expect ast node type 'mat' but found '{ast.item_type}'.") + # elif ast.operators[1].item_type != 'mat': + # raise ValueError(f"Tensor shape are not the same. Expect ast.operators[1] node type 'mat' but found '{ast.operators[1].item_type}'.") + # elif ast.operators[0].item_type != 'vec': + # raise ValueError(f"Tensor shape are not the same. Expect ast.operators[0] node type 'vec' but found '{ast.operators[1].item_type}'.") if type(ast.operators[1]) == BatchOp and ast.op_type == 'vec_outer_vec': @@ -636,6 +683,8 @@ def fuse_operators(ast): if ast.item_type == 'mat' and ast.operators[1].item_type == 'vec': # check if type of operator1 is vector if ast.operators[1].compute and ast.compute: + for i in ast.operators[1].compute: + change_ref(i, ast) outer_loop = ast.compute[0] loop = ast.operators[1].compute[0] oloop = outer_loop @@ -654,7 +703,17 @@ def fuse_operators(ast): change_index(i, iter_o, iter_i) for i in range(len(iloop.body)): oloop.body.insert(i, iloop.body[i]) - ast.operators[1].compute.clear() + if ast.operators[1].op_type in core.ast.op_mapping.keys() or ast.operators[1].op_type in ["scal_mul_vec", "vec_mul_vec"]: + a = Scalar(ast.operators[1].eval.dtype) + pre_arr = ast.operators[1].eval + ast.operators[1].decl.pop(0) + ast.operators[1].decl.append(Decl(a)) + ast.operators[1].eval = a + outer_loop = swap_arr_to_reg(outer_loop, pre_arr, a) + # ast.operators[1].compute.clear() + ast.operators[1].valid = False + ast.decl.extend(ast.operators[1].decl) + elif ast.item_type != 'mat': raise ValueError(f"Tensor shape are not the same. Expect ast node type 'mat' but found '{ast.item_type}'.") elif ast.operators[0].item_type != 'vec': @@ -666,17 +725,23 @@ def fuse_operators(ast): if ast.item_type == 'mat' and ast.operators[0].item_type == 'vec': # check if type of operator0 is vector if ast.operators[0].compute and ast.compute: + for i in ast.operators[0].compute: + change_ref(i, ast) outer_loop = ast.compute[0] loop = ast.operators[0].compute[0] - # oloop, iloop, iter_o, iter_i = get_same_loop(outer_loop, loop) - # for i in iloop.body: - # change_index(i, iter_o, iter_i) - # for i in range(len(iloop.body)): - # oloop.body.insert(i, iloop.body[i]) - # iloop.body.clear() loop_merge(outer_loop, loop) - ast.operators[0].compute.clear() + if ast.operators[0].op_type in core.ast.op_mapping.keys() or ast.operators[0].op_type in ["scal_mul_vec", "vec_mul_vec"]: + a = Scalar(ast.operators[0].eval.dtype) + pre_arr = ast.operators[0].eval + ast.operators[0].decl.pop(0) + ast.operators[0].decl.append(Decl(a)) + ast.operators[0].eval = a + outer_loop = swap_arr_to_reg(outer_loop, pre_arr, a) + # ast.operators[0].compute.clear() + ast.operators[0].valid = False + ast.decl.extend(ast.operators[0].decl) + elif ast.item_type != 'mat': raise ValueError(f"Tensor shape are not the same. Expect ast node type 'mat' but found '{ast.item_type}'.") elif ast.operators[0].item_type != 'vec': diff --git a/batch/opt/ir.py b/batch/opt/ir.py index 022bf6f..17925a0 100644 --- a/batch/opt/ir.py +++ b/batch/opt/ir.py @@ -1,24 +1,71 @@ from core.ir import * -class ThreadMapping: - loop_id = 0 - def __init__(self, start, end, step, body: list): - self.lid = ThreadMapping.loop_id - ThreadMapping.loop_id += 1 - self.start = start - self.end = end - self.step = step - self.body = body - self.iterate = Scalar('int', f'_l{self.lid}') +# class ThreadMapping(): +# loop_id = 0 +# def __init__(self, start, end, step, body: list): +# self.lid = ThreadMapping.loop_id +# ThreadMapping.loop_id += 1 +# self.start = start +# self.end = end +# self.step = step +# self.body = body +# self.iterate = Scalar('int', f'_l{self.lid}') -class ThreadIdy: +class BlockIdy(IR): def __init__(self): - pass + super().__init__() -class ThreadIdx: +class BlockIdx(IR): def __init__(self): - pass + super().__init__() -class Sync: +class BlockDimy(IR): def __init__(self): - pass \ No newline at end of file + super().__init__() + +class BlockDimx(IR): + def __init__(self): + super().__init__() + +class ThreadIdy(IR): + def __init__(self): + super().__init__() + +class ThreadIdx(IR): + def __init__(self): + super().__init__() + +class SyncThreads(IR): + def __init__(self): + super().__init__() + +class SyncWarps(IR): + def __init__(self): + super().__init__() + +class ShuffleDown(IR): + def __init__(self, dobject): + super().__init__() + self.dobject = dobject + +class ShuffleUp(IR): + def __init__(self, dobject): + super().__init__() + self.dobject = dobject + +class ShuffleXor(IR): + def __init__(self, dobject): + super().__init__() + self.dobject = dobject + +class SaveAtThread(IR): + def __init__(self, src, dst, threadid): + super().__init__() + self.src = src + self.dst = dst + self.threadid = threadid + +class BroadCast(IR): + def __init__(self, dobject): + super().__init__() + self.dobject = dobject \ No newline at end of file diff --git a/batch/opt/parallelism.py b/batch/opt/parallelism.py index 7aae015..c69cc36 100644 --- a/batch/opt/parallelism.py +++ b/batch/opt/parallelism.py @@ -5,30 +5,128 @@ from batch.opt.ir import * # for better optimization on GPU -def parallel(ast): +def swap_arr_to_reg(ir, pre, cur): + if isinstance(ir, Indexing): + temp = ir + while isinstance(temp, Indexing): + temp = temp.dobject + if temp == pre: + return cur + else: + return ir + elif isinstance(ir, Expr): + ir.left = swap_arr_to_reg(ir.left, pre, cur) + ir.right = swap_arr_to_reg(ir.right, pre, cur) + elif isinstance(ir, Assignment): + ir.lhs = swap_arr_to_reg(ir.lhs, pre, cur) + ir.rhs = swap_arr_to_reg(ir.rhs, pre, cur) + elif isinstance(ir, Loop): + for i in range(len(ir.body)): + ir.body[i] = swap_arr_to_reg(ir.body[i], pre, cur) + return ir + +def find_arr_ind(ir, pre): + if isinstance(ir, Indexing): + temp = ir + while isinstance(temp, Indexing): + temp = temp.dobject + if temp == pre: + return ir + else: + return None + elif isinstance(ir, Expr): + return Expr(find_arr_ind(ir.left, pre), find_arr_ind(ir.right, pre), ir.op) + elif isinstance(ir, Assignment): + return find_arr_ind(ir.lhs, pre) + elif isinstance(ir, Loop): + for i in ir.body: + t = find_arr_ind(i, pre) + if t: + return t + +def add_reduction(ast): + if type(ast) == BatchOp: + if type(ast.operators[1]) == BatchOp: + add_reduction(ast.operators[1]) + if type(ast.operators[0]) == BatchOp: + add_reduction(ast.operators[0]) + # todo: add traverse action to add reduction + if ast.op_type == 'vec_mul_vec': + # this inner_prod node is fused with upper layer + eval = ast.eval + # print(codegen.cpu.to_string(eval), codegen.cpu.to_string(ast.operators[0].eval), codegen.cpu.to_string(ast.operators[1].eval)) + # iff eval is scalar, we need to add shfl_sync + if not (isinstance(ast.operators[0].eval, Ndarray) or isinstance(ast.operators[1].eval, Ndarray)): + for i in ast.compute: + # search all compute stmts + if isinstance(i, Loop): + for j in i.body: + # search loop body + if isinstance(j, Loop): + # find the stmt of ast node + main_loop = j.astnode.compute + for idx, item in enumerate(main_loop): + if isinstance(item, Loop) and item == j: + main_loop.insert(idx+1, SyncThreads()) + main_loop.insert(idx+1, BroadCast(eval)) + main_loop.insert(idx+1, ShuffleDown(eval)) + + if isinstance(ast.eval, Ndarray): + new_compute = [] + for idx, item in enumerate(ast.compute): + new_compute.append(item) + if isinstance(item, Loop): + a = Scalar(ast.eval.dtype) + pre_arr = ast.eval + ast.decl.append(Decl(a)) + t = find_arr_ind(item, pre_arr) + + swap_arr_to_reg(item, pre_arr, a) + new_compute.append(ShuffleDown(a)) + new_compute.append(SaveAtThread(a, t, 0)) + ast.compute = new_compute + + +def cuda_spec(ast): + if ast.compute and ast.valid: + compute_list = [] + for body in ast.compute: + body_list = [] + if isinstance(body, Loop): + # print(body.iterate, body.iterate.dobject) + ast.decl.append(Decl(body.iterate)) + assign = Assignment(body.iterate, Expr(ThreadIdy(), Expr(BlockDimy(), BlockIdx(), '*'), '+')) + body_list.append(assign) + for item in body.body: + if isinstance(item, Loop) and item.start == 0: + item.start = ThreadIdx() + item.step = BlockDimx() + body_list.append(item) + compute_list.extend(body_list) + ast.compute = compute_list + +def add_cuda_spec(ast): if type(ast) == BatchOp: if type(ast.operators[1]) == BatchOp: - parallel(ast.operators[1]) + add_cuda_spec(ast.operators[1]) if type(ast.operators[0]) == BatchOp: - parallel(ast.operators[0]) + add_cuda_spec(ast.operators[0]) else: return - print(ast.op_type) - if ast.compute: - stmt = ast.compute[0] - if isinstance(stmt, Loop): - print(stmt.end.name(), stmt.iterate.name()) - if stmt.end.name() == 'batch_size': - ast.compute = stmt.body - for i in ast.compute: - if isinstance(i, Loop): - i.start = 'threadIdx.x' - i.step = 'blockDim.x' - # a = ThreadMapping(stmt.start, stmt.end, stmt.step, stmt.body) - # 16 ty * 32 tx each thread block - assign = Assignment(stmt.iterate, 'threadIdx.y + blockIdx.x * 16') - # a.iterate = stmt.iterate - ast.compute.insert(0, assign) - ast.decl.append(Decl(stmt.iterate)) - - \ No newline at end of file + + cuda_spec(ast) + + +def parallel(ast): + + # print(ast, ast.op_type, ast.compute) + # for i in ast.compute: + # print(i, i.ast_ref, i.ast_ref.compute) + # print(ast.compute) + # for i in ast.compute: + # print(codegen.gpu.to_string(i)) + + add_cuda_spec(ast) + add_reduction(ast) + + \ No newline at end of file diff --git a/batch/opt/tiling.py b/batch/opt/tiling.py new file mode 100644 index 0000000..9e881f6 --- /dev/null +++ b/batch/opt/tiling.py @@ -0,0 +1,12 @@ +from core.ir import * +from batch.ast import * +from batch.ast2ir import * +import codegen +from batch.opt.ir import * + + +def tile_loop(ast): + + + if ast.compute and ast.valid: + pass \ No newline at end of file diff --git a/batch/test/kge.py b/batch/test/kge.py index b3f9c58..cb581c5 100644 --- a/batch/test/kge.py +++ b/batch/test/kge.py @@ -4,6 +4,8 @@ from batch.ast import * from batch.opt.fusion_rules import * from batch.opt.parallelism import * +import run +import torch def transE(): nnodes = Var('nnodes') @@ -25,10 +27,23 @@ def transE(): ast = res._gen_ir() fuse_operators(ast) - parallel(ast) + # code = codegen.cpu.print_cpp(ast) - code = codegen.gpu.print_cpp(ast) + parallel(ast) + code = codegen.gpu.print_cuda(ast) print(code) + # h = torch.randint(0, 9999, (4096, )).cuda(0) + # r = torch.randint(0, 100, (4096, )).cuda(0) + # t = torch.randint(0, 9999, (4096, )).cuda(0) + # eemb = torch.rand((9999, 512)).cuda(0) + # remb = torch.rand((100, 512)).cuda(0) + + # y = eemb[h] - eemb[t] + remb[r] + # print(y) + + # x = run.gpu.compile_and_run(code, 4096, 512, 0, eemb, h,t, 0, remb, r) + + # print(x) def transH(): nnodes = Var('nnodes') @@ -37,26 +52,37 @@ def transH(): batch_size = Var('batch_size') Eemb = Tensor('Eemb', (nnodes, dim)) Remb = Tensor('Remb', (nedges, dim)) + Pemb = Tensor('Pemb', (nedges, dim)) h = Tensor('h', (batch_size, ), dtype='int') t = Tensor('t', (batch_size, ), dtype='int') r = Tensor('r', (batch_size, ), dtype='int') vh = Batch(Eemb[h]) vt = Batch(Eemb[t]) vr = Batch(Remb[r]) + vp = Batch(Pemb[r]) - res = vh - vt + vr - bsv(bvv(vr, vh - vt), vr) + res = vh - vt + vr - bsv(bvv(vp, vh - vt), vp) - code = codegen.cpu.print_cpp(res._gen_ir()) - - # print(code) + # code = codegen.cpu.print_cpp(res._gen_ir()) ast = res._gen_ir() - - fuse_operators(ast) parallel(ast) # code = codegen.cpu.print_cpp(ast) - code = codegen.gpu.print_cpp(ast) + code = codegen.gpu.print_cuda(ast) print(code) + h = torch.randint(0, 9999, (4096, )).cuda(0) + r = torch.randint(0, 100, (4096, )).cuda(0) + t = torch.randint(0, 9999, (4096, )).cuda(0) + eemb = torch.rand((9999, 512)).cuda(0) + remb = torch.rand((100, 512)).cuda(0) + pemb = torch.rand((100, 512)).cuda(0) + + y = eemb[h] + remb[r] - eemb[t] - torch.einsum('a,ab->ab', torch.einsum('ab,ab->a', pemb[r], eemb[h]-eemb[t]), pemb[r]) + print(y) + + x = run.gpu.compile_and_run(code, 4096, 512, 0, eemb, h,t, 0, remb, r, pemb) + print(x) + def transR(): nnodes = Var('nnodes') @@ -77,13 +103,28 @@ def transR(): res = bvm(vh -vt, mr) + vr code = codegen.cpu.print_cpp(res._gen_ir()) + ast = res._gen_ir() fuse_operators(ast) + # print(codegen.cpu.print_cpp(ast)) + parallel(ast) - - code = codegen.cpu.print_cpp(ast) - + # code = codegen.cpu.print_cpp(ast) + code = codegen.gpu.print_cuda(ast) print(code) + # h = torch.randint(0, 9999, (4096, )).cuda(0) + # r = torch.randint(0, 100, (4096, )).cuda(0) + # t = torch.randint(0, 9999, (4096, )).cuda(0) + # eemb = torch.rand((9999, 512)).cuda(0) + # remb = torch.rand((100, 512)).cuda(0) + # pemb = torch.rand((100, 512, 512)).cuda(0) + + # y = torch.einsum('ab,abc->ac', eemb[h] - eemb[t], pemb[r]) + remb[r] + # print(y) + + # x = run.gpu.compile_and_run(code, 4096, 512, 0, eemb, h,t, 0, pemb, r, remb) + # print(x) + def transF(): nnodes = Var('nnodes') @@ -108,11 +149,20 @@ def transF(): code = codegen.cpu.print_cpp(res._gen_ir()) ast = res._gen_ir() fuse_operators(ast) - - - code = codegen.cpu.print_cpp(ast) - + parallel(ast) + code = codegen.gpu.print_cuda(ast) print(code) + h = torch.randint(0, 9999, (4096, )).cuda(0) + r = torch.randint(0, 100, (4096, )).cuda(0) + t = torch.randint(0, 9999, (4096, )).cuda(0) + eemb = torch.rand((9999, 512)).cuda(0) + remb = torch.rand((100, 512)).cuda(0) + + y = torch.einsum('ab,ab->a', eemb[h], eemb[t]) - torch.einsum('ab,ab->a',(eemb[h] - eemb[t]), remb[r]) + print(y) + + x = run.gpu.compile_and_run(code, 4096, 512, 0, eemb, h,t, 0, remb, r) + print(x) def RESCAL(): nnodes = Var('nnodes') @@ -135,11 +185,23 @@ def RESCAL(): ast = res._gen_ir() fuse_operators(ast) - - code = codegen.cpu.print_cpp(ast) - + parallel(ast) + # code = codegen.cpu.print_cpp(ast) + code = codegen.gpu.print_cuda(ast) print(code) + h = torch.randint(0, 9999, (4096, )).cuda(0) + r = torch.randint(0, 100, (4096, )).cuda(0) + t = torch.randint(0, 9999, (4096, )).cuda(0) + eemb = torch.rand((9999, 512)).cuda(0) + remb = torch.rand((100, 512, 512)).cuda(0) + + y = torch.einsum('ab,ab->a', torch.einsum('ab,abc->ac', eemb[h], remb[r]), eemb[t]) + print(y, y.shape) + + x = run.gpu.compile_and_run(code, 4096, 512, 0, eemb, h, 0, remb, r, t) + print(x) + def test(): nnodes = Var('nnodes') nedges = Var('nedges') @@ -159,24 +221,33 @@ def test(): proj_h = Batch(Proj[h]) # res = vh - vt + vr - vrr - # res = bov(vh+vr, vt-vr) - res = vh+vt - (proj_m+ proj_h) + res = bov(vh+vr, vt-vr) # code = codegen.cpu.print_cpp(res._gen_ir()) # print(code) ast = res._gen_ir() - fuse_operators(ast) - code = codegen.cpu.print_cpp(ast) - print(code) + parallel(ast) - # t = codegen.cpu.print_cpp(ast) - # print(t) + code = codegen.gpu.print_cuda(ast) + print(code) + h = torch.randint(0, 9999, (4096, )).cuda(0) + r = torch.randint(0, 100, (4096, )).cuda(0) + t = torch.randint(0, 9999, (4096, )).cuda(0) + eemb = torch.rand((9999, 512)).cuda(0) + remb = torch.rand((100, 512)).cuda(0) + + y = torch.einsum('ab,ac->abc', eemb[h] + remb[r], eemb[t] - remb[r]) + print(y) + + x = run.gpu.compile_and_run(code, 4096, 512, 0, eemb, h, 0, remb, r, t) + print(x) if __name__ == "__main__": - # test() - # transE() - transH() - # transR() - # transF() - # RESCAL() + # test() # bov success + transE() # success + # transH() # success + # transR() # success + # transF() # success + # RESCAL() # success + \ No newline at end of file diff --git a/codegen/__init__.py b/codegen/__init__.py index 0674dd6..8ef5280 100644 --- a/codegen/__init__.py +++ b/codegen/__init__.py @@ -1 +1,2 @@ -import codegen.cpu \ No newline at end of file +import codegen.cpu +import codegen.gpu \ No newline at end of file diff --git a/codegen/cpu.py b/codegen/cpu.py index f9be9cf..39b31dd 100644 --- a/codegen/cpu.py +++ b/codegen/cpu.py @@ -21,17 +21,13 @@ def to_string(ir): return code case 'Scalar' | 'Ndarray' | 'Ref': return ir.name() - case 'Index': - if ir.ind_arr != None: - if type(ir.ind_arr) == Slice: - return f'{to_string(ir.dobject)}[(({to_string(ir.ind_arr.start)})+({to_string(ir.ind_arr.step)})*({to_string(ir.index)}))]' - else: # idx is a Tensor - if ir.index == None: - return f'{to_string(ir.dobject)}[{to_string(ir.ind_arr)}]' - else: - return f'{to_string(ir.dobject)}[{to_string(ir.ind_arr)}[{to_string(ir.index)}]]' + case 'Literal': + return str(ir.val) + case 'Indexing': + if type(ir.dobject) == Slice: + return f'(({to_string(ir.dobject.start)})+({to_string(ir.dobject.step)})*({to_string(ir.idx)}))' else: - return f'{to_string(ir.dobject)}[{to_string(ir.index)}]' + return f'{to_string(ir.dobject)}[{to_string(ir.idx)}]' case 'Decl': # variables are passed in as pytorch arguments if type(ir.dobject) == Scalar: @@ -53,9 +49,6 @@ def to_string(ir): code += f'auto {ir.dobject.name()} = obj_{ir.dobject.name()}.accessor<{ir.dobject.dtype}, {len(ir.dobject.size)}>();\n' return code - # elif type(ir.dobject) == Ref: - # code = f'{ir.dobject.dobject.dtype}* {ir.dobject.name()} = ({ir.dobject.dobject.dtype}*)&{ir.dobject.dobject.addr()}' - # return code case _: return str(ir) @@ -64,14 +57,15 @@ def to_string(ir): def gen_cpp(ast, ir): def action(node, res): - if type(node) == Var or type(node) == One or type(node) == Zero or type(node) == Ones or type(node) == Zeros or type(node) == Tensor: - res.extend(node.decl) - elif type(node) == TensorOp: - res.extend(node.decl) - res.extend(node.compute) - elif type(node) == batch.ast.BatchOp: - res.extend(node.decl) - res.extend(node.compute) + if node.valid == True: + if type(node) == Var or type(node) == One or type(node) == Zero or type(node) == Ones or type(node) == Zeros or type(node) == Tensor: + res.extend(node.decl) + elif type(node) == TensorOp: + res.extend(node.decl) + res.extend(node.compute) + elif type(node) == batch.ast.BatchOp: + res.extend(node.decl) + res.extend(node.compute) t = helpers.Traversal(action) ir.extend(t(ast)) diff --git a/codegen/gpu.py b/codegen/gpu.py index 9db4763..7b58a8b 100644 --- a/codegen/gpu.py +++ b/codegen/gpu.py @@ -2,6 +2,7 @@ import helpers import batch from batch.opt.ir import * +from codegen.gpu_instructionsets import * def to_string(ir): match ir.__class__.__name__: @@ -19,24 +20,14 @@ def to_string(ir): code += to_string(e) code += "} \n" return code - case 'CUDAThread': - pass case 'Scalar' | 'Ndarray' | 'Ref': return ir.name() - case 'Index': - # print(ir.ind_arr, ir.index, ir.dobject, ir.index.addr()) - if ir.ind_arr != None: - if type(ir.ind_arr) == Slice: - return f'{to_string(ir.dobject)}[(({to_string(ir.ind_arr.start)})+({to_string(ir.ind_arr.step)})*({to_string(ir.index)}))]' - else: # idx is a Tensor - if ir.index == None: - return f'{to_string(ir.dobject)}[{to_string(ir.ind_arr)}]' - else: - return f'{to_string(ir.dobject)}[{to_string(ir.ind_arr)}[{to_string(ir.index)}]]' + case 'Indexing': + if type(ir.dobject) == Slice: + return f'(({to_string(ir.dobject.start)})+({to_string(ir.dobject.step)})*({to_string(ir.idx)}))' else: - return f'{to_string(ir.dobject)}[{to_string(ir.index)}]' + return f'{to_string(ir.dobject)}[{to_string(ir.idx)}]' case 'Decl': - # return '' # variables are passed in as pytorch arguments if type(ir.dobject) == Scalar: if not ir.dobject.is_arg: @@ -51,58 +42,71 @@ def to_string(ir): code = '' if not ir.dobject.is_arg: if ir.dobject.val != None: - code = f'torch::Tensor obj_{ir.dobject.name()} = torch::{"ones" if ir.dobject.val == 1 else "zeros"}({{{",".join([to_string(s) for s in ir.dobject.size])}}}, at::k{"Int" if ir.dobject.dtype=="int" else "Float"});\n' + code = f'torch::Tensor obj_{ir.dobject.name()} = torch::{"ones" if ir.dobject.val == 1 else "zeros"}({{{",".join([to_string(s) for s in ir.dobject.size])}}}, torch::TensorOptions(torch::k{"Int" if ir.dobject.dtype=="int" else "Float"}).device(torch::kCUDA));\n' else: - code = f'torch::Tensor obj_{ir.dobject.name()} = torch::empty({{{",".join([to_string(s) for s in ir.dobject.size])}}}, at::k{"Int" if ir.dobject.dtype=="int" else "Float"});\n' + code = f'torch::Tensor obj_{ir.dobject.name()} = torch::empty({{{",".join([to_string(s) for s in ir.dobject.size])}}}, torch::TensorOptions(torch::k{"Int" if ir.dobject.dtype=="int" else "Float"}).device(torch::kCUDA));\n' # code += f'auto {ir.dobject.name()} = obj_{ir.dobject.name()}.accessor<{ir.dobject.dtype}, {len(ir.dobject.size)}>();\n' return code - elif type(ir.dobject) == Ref: - code = f'{ir.dobject.dobject.dtype}* {ir.dobject.name()} = ({ir.dobject.dobject.dtype}*)&{ir.dobject.dobject.addr()}' - return code + # elif type(ir.dobject) == Ref: + # code = f'{ir.dobject.dobject.dtype}* {ir.dobject.name()} = ({ir.dobject.dobject.dtype}*)&{ir.dobject.dobject.addr()}' + # return code + case 'ThreadIdy' | 'ThreadIdx' | 'BlockIdy' | 'BlockIdx' | 'BlockDimy' | 'BlockDimx' | 'SyncThreads' | 'SyncWarps': + return ir2gpu(ir) + case 'ShuffleDown': + return f'for (int off = blockDim.x/2; off > 0; off >>= 1) {{\n {to_string(ir.dobject)} += __shfl_down_sync(0xffffffff, {to_string(ir.dobject)}, off); \n}}\n' + case 'ShuffleUp': + return f'for (int off = blockDim.x/2; off > 0; off >>= 1) {{\n {to_string(ir.dobject)} += __shfl_up_sync(0xffffffff, {to_string(ir.dobject)}, off); \n}}\n' + case 'ShuffleXor': + return f'for (int off = blockDim.x/2; off > 0; off >>= 1) {{\n {to_string(ir.dobject)} += __shfl_xor_sync(0xffffffff, {to_string(ir.dobject)}, off); \n}}\n' + case 'BroadCast': + return f'{to_string(ir.dobject)} = __shfl_sync(0xffffffff, {to_string(ir.dobject)}, 0);\n' + case 'SaveAtThread': + return f'if (threadIdx.x == {ir.threadid}) {{\n {to_string(Assignment(ir.dst, ir.src))} }}\n' case _: return str(ir) def gen_cuda(ast, cpu_ir, gpu_ir): # 2 ir list for cpu and gpu def action_cpu(node, res): - if type(node) == Var or type(node) == One or type(node) == Zero or type(node) == Ones or type(node) == Zeros or type(node) == Tensor: - res.extend(node.decl) - elif type(node) == TensorOp: - res.extend(node.decl) - # res.extend(node.compute) - elif type(node) == batch.ast.BatchOp: - res.extend(node.decl) - # res.extend(node.compute) + if node.valid: + if type(node) == Var or type(node) == One or type(node) == Zero or type(node) == Ones or type(node) == Zeros or type(node) == Tensor: + res.extend(node.decl) + elif type(node) == TensorOp: + res.extend(node.decl) + # res.extend(node.compute) + elif type(node) == batch.ast.BatchOp: + res.extend(node.decl) + # res.extend(node.compute) def action_cuda(node, res): - if type(node) == TensorOp: - # res.extend(node.decl) - res.extend(node.compute) - elif type(node) == batch.ast.BatchOp: - res.extend(node.decl) - res.extend(node.compute) + if node.valid: + if type(node) == TensorOp: + # res.extend(node.decl) + res.extend(node.compute) + elif type(node) == batch.ast.BatchOp: + res.extend(node.decl) + res.extend(node.compute) t = helpers.Traversal(action_cpu) cpu_ir.extend(t(ast)) t = helpers.Traversal(action_cuda) gpu_ir.extend(t(ast)) -def print_cpp(ast): +def print_cuda(ast): cpu_ir = [] gpu_ir = [] gen_cuda(ast, cpu_ir, gpu_ir) - print(cpu_ir, "CUDA IR:::" , gpu_ir) + # print(cpu_ir, "CUDA IR:::" , gpu_ir) args = helpers.get_input_nodes(ast) + argscpu = ', '.join([f'torch::Tensor obj_{a}' if type(args[a]) == Tensor else f'{args[a].dtype} {a}' for a in args]) # argsptr = ', '.join([f'obj_{a}.data_ptr<{args[a].dtype}>()' if type(args[a]) == Tensor else f'{a}' for a in args]) # ptrs = ', '.join([f'{args[a].dtype}* {a}' if type(args[a]) == Tensor else f'{args[a].dtype} {a}' for a in args]) - - print(args) - argsptr = ', '.join([f'obj_{a}.packed_accessor32<{args[a].dtype}, {len(args[a].ref_size)}, torch::RestrictPtrTraits>()' if type(args[a]) == Tensor else f'{a}' for a in args]) - ptrs = ', '.join([f'torch::PackedTensorAccessor32<{args[a].dtype}, {len(args[a].ref_size)}, torch::RestrictPtrTraits> {a}' if type(args[a]) == Tensor else f'{args[a].dtype} {a}' for a in args]) + argsptr = ', '.join([f'obj_{a}.packed_accessor32<{args[a].dtype if args[a].dtype!="int" else "int64_t"}, {len(args[a].ref_size)}, torch::RestrictPtrTraits>()' if type(args[a]) == Tensor else f'{a}' for a in args]) + ptrs = ', '.join([f'torch::PackedTensorAccessor32<{args[a].dtype if args[a].dtype!="int" else "int64_t"}, {len(args[a].ref_size)}, torch::RestrictPtrTraits> {a}' if type(args[a]) == Tensor else f'{args[a].dtype} {a}' for a in args]) # in cuda kernel: const torch::PackedTensorAccessor32 # host call cuda: .packed_accessor32() @@ -116,7 +120,7 @@ def print_cpp(ast): ptrs += f', torch::PackedTensorAccessor32<{d.dobject.dtype}, {len(d.dobject.size)}, torch::RestrictPtrTraits> {d.dobject.name()}' else: code += to_string(d) - + # print(declare) Return = '' if type(ast.eval) == Scalar: rtype = ast.dtype diff --git a/codegen/gpu_instructionsets.py b/codegen/gpu_instructionsets.py new file mode 100644 index 0000000..e6bd2d3 --- /dev/null +++ b/codegen/gpu_instructionsets.py @@ -0,0 +1,21 @@ +from batch.opt.ir import * +from codegen import * + +def ir2gpu(ir): + match ir.__class__.__name__: + case 'BlockIdx': + return 'blockIdx.x' + case 'BlockIdy': + return 'blockIdx.y' + case 'BlockDimx': + return 'blockDim.x' + case 'BlockDimy': + return 'blockDim.y' + case 'ThreadIdy': + return 'threadIdx.y' + case 'ThreadIdx': + return 'threadIdx.x' + case 'SyncThreads': + return '__syncthreads();\n' + case 'SyncWarps': + return '__syncwarps();\n' \ No newline at end of file diff --git a/codegen/gpu_template.cu b/codegen/gpu_template.cu index 63e39b1..5219403 100644 --- a/codegen/gpu_template.cu +++ b/codegen/gpu_template.cu @@ -7,7 +7,7 @@ __global__ void FNAME_kernel(PTRS){ RTYPE FNAME(ARGS) { DECL - FNAME_kernel<<< >>>(PTR_VARS); + FNAME_kernel<<< batch_size/16, dim3(32,16) >>>(PTR_VARS); RETURN } From db0eb3016700aaacc944791cc99e6fe30c4db177 Mon Sep 17 00:00:00 2001 From: Lihan Date: Thu, 26 Oct 2023 15:29:40 -0500 Subject: [PATCH 6/8] update in my own repo --- __init__.py | 0 __pycache__/helpers.cpython-310.pyc | Bin 0 -> 2147 bytes batch/opt/__init__.py | 5 + .../opt/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 246 bytes batch/opt/__pycache__/ir.cpython-310.pyc | Bin 3087 -> 4834 bytes .../__pycache__/parallelism.cpython-310.pyc | Bin 2905 -> 3065 bytes batch/opt/__pycache__/smem.cpython-310.pyc | Bin 0 -> 7560 bytes batch/opt/__pycache__/tiling.cpython-310.pyc | Bin 0 -> 3101 bytes batch/opt/ir.py | 51 ++- batch/opt/parallelism.py | 16 +- batch/opt/smem.py | 365 ++++++++++++++++++ batch/opt/sort.cu | 81 ++++ batch/opt/tiling.py | 157 +++++++- batch/test/kge.py | 81 ++-- codegen/gpu.py | 51 +++ core/ast.py | 7 +- core/ast2ir.py | 141 ++++--- core/ir.py | 85 ++-- run/__init__.py | 2 +- run/gpu.py | 8 + test/examples.py | 1 - transh.cpp | 115 ++++++ 22 files changed, 995 insertions(+), 171 deletions(-) create mode 100644 __init__.py create mode 100644 __pycache__/helpers.cpython-310.pyc create mode 100644 batch/opt/__init__.py create mode 100644 batch/opt/__pycache__/__init__.cpython-310.pyc create mode 100644 batch/opt/__pycache__/smem.cpython-310.pyc create mode 100644 batch/opt/__pycache__/tiling.cpython-310.pyc create mode 100644 batch/opt/smem.py create mode 100644 batch/opt/sort.cu create mode 100644 run/gpu.py create mode 100644 transh.cpp diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/__pycache__/helpers.cpython-310.pyc b/__pycache__/helpers.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20096fcfc8f4d07636add6018cf99884356d8afd GIT binary patch literal 2147 zcmZ`)OK%)S5bo~jnVsFN9mjc)2NFUGBoib%LOB46qJ;2}KynUt5VAm{@pQ+Y^<-z= z-L{A|zTlj>a&;swIr1C$4P5$!#05BVM2f;!y|S+Ebysy&b#>LZ&D2znVf^#m zHGR9w*q^jGJR&UaV%iTu6jQurOXDwJVqY>PRP>aoXjd!?B~|>CEk`O*9ekxq7g^T* z8|PTYJ-<;BcX$Xa?qb@jAet?6#bAXi5v)W?1}m{j@Qqam)>7YHGR2lQwx~-wJ{iQ* zypL(=MlEkyjzAaLbG`ugNm0708D_#utLsN5hR$WVfB5z1^X2Bco?p}T2tMaVPkfr^)v$7To{%SU+2h-@2SLqE#HEC2GRO<*hv@#| zNF=UWvmNPp%bnO2_7p{mn8g+|X96$wqLx47oH^O@UGCy_VXyD=$sxY8F3ioA?WH&! zJrg|Kd0(#!*>cMVJte?>3$0UF^#@a}fNsRu!Orv>JEtc*6b;RNn=|lT@AGF4@w4#o z6Re;8jD6qTiG$^MVq>F$7IL1j80HP+>B+z&XiRn+j~ z3gb}Yt{D;4BcCkkp>2$pux5Qs=B!U2RX^mm+SWcb`q4f&R(vq>-3H}exW-t-L>@m4 zLCclGBHXZeFv>cHYTC;o#opP%jht3KGTNF`B;eNbGIZFOq}Vj)L4Y&NegTs59+y1D z?1`&lhWD|a#qR=_FRscSpTT+tzkWR7$Lkq>?m#AAY+pRoen)b01R|&*sp~@%z758) zUA7}u*-nI~LnBU}y^R)`A#H#~WVH)1XENN@yo-JFK9OrgsIy<)j?kej>N?K}EiGL* z!R4kgk7C4FX(3DK#ISg*^W69H{Bff;YvTKPzOh-a(@V%kqYa0sf70|ol~ zJmJihmJK3vspWeT9FSmD5ROf7gudhSCgwD(m>D9ML3~o#yfCW(>(NLL(F>G-Oqlmb zF z9G<(^-}Llrwmu?%RQV}S<32aHz`l`W%2P-(Nl{|>Hv%S|MS@w2Bm?oMzG6Qj*?Zy{ z0}gYby%1i<~pe#tqngN zE@1Lz+~@b8V#zFZ-gKCf8@km(7W=epjGm*W?*tD1s9?e^gVG`E!4DO=Pj8P)_YM_X Ocns7apBCw@e)oT#BHyY2 literal 0 HcmV?d00001 diff --git a/batch/opt/__init__.py b/batch/opt/__init__.py new file mode 100644 index 0000000..9c5cebe --- /dev/null +++ b/batch/opt/__init__.py @@ -0,0 +1,5 @@ +from .fusion_rules import * +from .parallelism import * +from .ir import * +from .smem import * +from .tiling import * \ No newline at end of file diff --git a/batch/opt/__pycache__/__init__.cpython-310.pyc b/batch/opt/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c401017ed44dba74da816a742c585b3f4ca9680 GIT binary patch literal 246 zcmd1j<>g`kf*BihQ;UG~V-N=!FabFZKwPW?BvKes7;_jxAT%SCW`fepP?`luGX*nf zGQVU5YSCo8#i-?{$$E6oWj;!N9=?f;@}>_V+y! literal 0 HcmV?d00001 diff --git a/batch/opt/__pycache__/ir.cpython-310.pyc b/batch/opt/__pycache__/ir.cpython-310.pyc index 0210bb46d939512c462e0eebed76efcdf53a0be8..cef551ad71ae2a48abab301f8480ae0895ce4d3f 100644 GIT binary patch delta 1502 zcmZWp&u5WUJzBk581GAdv@4cCCX5Kf?^WD^+ z^SRE*h{Lc|md=Ow?MJy8!-q$eSbWIDj5xl}#PJaJ%vv7(H^eOZv)cF2KOqX}7qou_ z{W(!Yzo`9D^h;tM{rPYV`Qzv>h?D4_+%swu;?yQ9msJ*i2d@5xZ7|hmMjvby8_RAQ zk0XzgnN+vaCADf^gRJ`2^ubln%pQ1Zjr)*S4|os8)pPE<^Qt!htb&%@833V*(!>qcM{uxT@-}D72sujBjW%b1N-BZbGCB0f9a~-Ox zop}!y)#J=>a8^amWjLdrIz8~!S=V=$6ULjwNT)WxsLN2Of1Ha)KA^E8nw|WM=z&?x zpT@vS!a7YXi{EUwz9LTkQo0Z+H4v<32nfxl@sKq^7{c7o*7!rlStR9z;0rX7QT08H~YNL0!y$>CQ}$Jr=*QPGJ_`Ocfw{@zD3aP%DZ9kbzR1pX8lg2 zVcC){&3&5;&5}!G&Zs~5Qvpao4X@gP_%Jb%Ovm8l~B~7RC|9{PMkR zsW+Ars?Mjzsu=wUdqfi0Y#L-06`3)jl3IevU118o&;FOW6{kP$wA$TJHYmSgBTOsk zoatJ+Y>)i}edrqp1~37Awq^Xlel$=B6QeBPeZpFlvawM!x4ZI?E{^DYzO@iGxe7uZEQ!Iw%j%iCP@$K}W}e~kS3b+5FRd*h Hd(`<4VksaG delta 273 zcmaE)+AqPE&&$ij00h~EnyK1s6ZvGA?l4T$xy6QQ9d5aA4G%Jcpx_(PXk1rxl~|Zjk;|3QcCt8k sC8O))iQHC#E^w1TX8LLJPnH+*pFBlKi9-g+=U@=voxD#-j7fwY0L5=U(*OVf diff --git a/batch/opt/__pycache__/parallelism.cpython-310.pyc b/batch/opt/__pycache__/parallelism.cpython-310.pyc index 4f6b1e4099e06f57d0cbc4eb47f48e82327550b5..efa394681dc1c1638e2f89ed7b1e281cd5786dbe 100644 GIT binary patch delta 691 zcmY*W&ubGw6yBNL*_qjH)>NW3qS%6$fC#n_YJ(`+#2-}=S`UIHhVE)iVw=p)A;>OP z(iZBEgA5}62_8I%H$m{=$*ZTXcduT&c=OG;M7qoS-h1DB-7}H=DY^xklMe++5i|kQu+&+gR%^K72IH+-NAH^{~R!FLs8`!Hs07t4{Ev?yA0>d3BzbPkKndCB%&X z7ct~)`q%@G{dBOXtNZ-@LLD^kkeCH0rp9nYM-70-U~v&(|8McJXs8?BxvK6FGa4>y q*gqj-{g!C;dQoo&im?O0xR1mY^~Jk6p-(nvKwh4xk<;wB@Banc%b=71 delta 573 zcmZ8d&ui2`7@co2lTET|Oc!zOu{|h@BIu#eE`qeXD;5<5J=7o6unFomw!4!N_K=9M zQ0;lZ|3Soq=*5dyrKg@ma}w_!JnG4p2nuHS-Z$^f@V=M%+5U1Y*qxj67`}tAo#bQd zbr6d1EitB9&Ss=(o2wJ%jJV+CY$Rd}^pklk+TDF^L*X$ku#wb_8PE2?ogTZ>FF4M) zc4u5Y%lXKIwpv)wtXg|&4#lFomxIG?&-@bI`P-8#ll9uM_EMkCKz8WRjO8Xhe{&#X z3J1vOQ;UMSJZ{z+msf0vE@~Z>y$2uzQ=33ix9+imDvtTr3Jzvzj$9UU-7p^-{gVrY zgd%HYBbTF~{=W@`j*^4r zV~74jaMd%m_iD9=%P&ZsL;K_S*H-nRrKl_f{Cgz+L~x(0zWMH*U#{+`#R{sI{Li6u l#*vEBG)bp8%ml!3eNq?A-R9M1HMyPv{;t`Tr<|7W{{}r_fo1>z diff --git a/batch/opt/__pycache__/smem.cpython-310.pyc b/batch/opt/__pycache__/smem.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a23ea4c8576960d5b1ea6f4b3a3449120fe8319a GIT binary patch literal 7560 zcmcIp&66BQRHM$$+c*=`v$A;n^|UIlB*vNjxe1!2j=Zi8rAoi(lQ z>FJNG99bi>13{Y$5lhT{A!5W{)+d74|A0S$1IIc55u6lU4Fm!#e(zOvPxn}cy)YA1 zmHF~z=F9h$@4fC9i-Co|zlwf6{P_o#_0QCq{jwfYT>DT73-F0dX zkGtqQ(*44!Wn@NX@y$w4`uKV>Cj)$anU@88bFwH)_y)2pEBNN+f?UM6AeZDazC~%@ zv#MuaU}vi;VsZ6ulgOF;d1z$te26F76qcneV`(wA)qP`i?2#?S$U)0#iIIyjo5wyi zj9rS z&R~^JVU>aPXIQCn+j@{2Wr3efFou6J*|X~2RbY|zf?lyu)rB}{N9|su8@*;2`ycmY z_@v!y#h&ax?1W7nJ8k(Sc6*KeFwVC^UGJ-Q{c%IZ?p}Bp#o5MS5cXsod=N$LR&PJ- z={VSJ_hem#2T>S1-A7UED7@T{o(wRn8}8~jquQ-UI(9$l_Xn~2urCj*UTn8joQw9u z{d%_@VNi$rgIW+hY7D}9)P5Q&pGbCPLpNg4{?bxs(SLb;hX6Yd8_m5?)(-|d-S(qL z2Rk2r_M0E=G!OPdjOga0o&G@Y(Aw7qhpL2mq%_fY@CroMc7-njJO#T#Z?PgOc0pvt z;=cyPf~a6DdDGYZ+7-M)lQ=l}qqc*Gb?^-|raBmCn{;n1QhB?O(Sk`e;7f70no*>@ zDoZc2K}Dp+-d)*Hs&QBqYK4Gw*5tKGEkfMJ6Hy}ofq`&^`-BVl&~dZ zK`CugQc@CQ*B!Lo5y(wR&4AP^ARbDz=WxrD0%maqDsI)Ygjcoiz7%g){gX4qrRJkX zuNBrqh#RWIQsXnJ1#GXD@QUsJpz83!Vh6gU)@bC^5w`%liYHpcD}anjkcjIB|Gi9K zp=f;Ws4f8XunhhjjpR`gnqo1;!3TJ;u1k zZGQ$M2aJ!fLI8^^f}LO?jl$nJ>b7xgOMpqkcWKWroxAt%RP!gCQbo`h`|Vw@3JR~v z=&B06m{D|W=9XHf))~C2Zpt&TOl<%V3w@exq!qu0CpwQ8DK+%M4#XNXgq}oGd||(G zZAG|E(vHa=v2W^1{{o$Fve4itQw<>&ZZlKBLSv7?%(}~curck!g{BNAc7BgX!OnEl z<33oL_TflVY+~&vDQkVWX|OcG8acxc2}_*=f!}0kG1b3Dvl&LwFVEGQ1J4#a&4Yxs zr}_FOHoJo-+QKX4YY_Z<1@6K1f|vY-at2USC?{dharO$$!o$k>7Cc!CSC6ret>@N} zs}|`}N_t}<#k#d_L7_HrUD?K!_ByVk_2eRQjvejdTFQ*YSn%bNK(Z$gTp^xxacyM{ zkC|+OsePT3E{#c8Hh&?J7G$Ucx~a!#jl5)SJqLI4`aSAn=H>mPx@nL zluLIC(w&0IP9`(6W`1s74qB4OsudF$x}fr7cT`B%DW>Zb({(DQXm}49q!i~tr8_Da z>v%y5Q+kdIGi?`U+J4f$Z5=re9*oL*(UkSl*aoK;^s+t!6U9G|{&3{Z^4_?_`%#5( zsNGqCJnTLD2$!@ej}}b1vkJ+4ceIE#7kN#gR;k@tCAm!?=&zBlJi;j2>&F`iDe_7G z1iQ1Q&w{E1hv%o5!DQxWL0Cs(xHkL;gG>F$e(=?3NpG43vqZZ=Cd+0Sw1s*}m+=pY z>2qUC7RC-%wqz0CGI(2J{$(*M)um1z=OU~!;p_9soR$UNrY9e*Xy2^NNO1*HJkzpxvxPd|r~40C&%(8orTgcw(uWDRgtH_nRgM{VsPdV9q&x z*=#^I_UODh-zj3>fcGU$W==wu%WGI4nz=l>U^Ww|xv*~4x6>JnM-ZBMWDnogS4=sP zi8+rG(b-qorp~eLOnPUz2d$?u{l?gu?VB^ygMMzz*~=Mdud6CE_VV70z1ZrnPb`4# z1(2z|B<&M>nZr))CBZpuFAQaSv4?+y6Fz2(5f6U%H5TKll7}5{3 zzg6QCOI(Jh+a_A4x_)XttRlgL9s?66nPSdb#U4|Xmc`K2CuuM}sB2Rk?0}#UaRvX%$v!T~nRBB&S zi<;Qm7F(1({KfEZhrcimBJ7nkVWX7y#VhMb2wDG1|HQ&ePylx8K~m-QK@; z7>f^IikmORug2nb?8x@MqN(ZvUU6o(+i&QXV&|o}Qp?mo>$Qim_0(hN)>TEZ;0topoV|6hyBi{zi)7&7-s!4dkcb5C`YA0@C2NLJmy>in795 z_Tj-UlA>BB+SmKnYx(^~yH{`b!r?)q=3#&`>spQ)yNxdRLMd-809ExQoUM5gQgHNZ zAZK6KJcO&XdM!uUa-CptrrkwaUCSeLR!E6){{5P}+ikRvOzXZ1>om-#togh@&36w- z6|xP^;$w$*80k5oQ9zy*p9mkhJ@R@yl;v-u<#Vrs(TdpOHI|(XTGgiY13=c$TSG0N zL=Zggv-#ECWOa+mz#1C;}tcG%#Da8?|mI@u*T3++W~(39+z?B_p2Dsdk? znM)-oK_gq^RAL*{B=D520ct~fYexL!)2ghWWoVL}zDg)q+CIL$?6 zOJm0R|)!)4A(C}dy+g-+M-T}P!8W1+ms@S7I*fpM}{ zRG5}lsB-iJjS5jG&GZL%xR)x!Oom@$cq;b6kGYo$J;fq%8)VC%qf_KEO9oh}8X?DZ zL1C6|M|HmEzJm+*uyuRqHFD>5<}it{W3KJFDc7WT3<_w zma^T{EZSP`VZYzyA|~Gh?D1^?kL2VZRhN)L^* ze@VO=6>n&Txf&$hTXbKrU-&oN`6TkO&{X+)gW4UNlg0$H!iAR0(Ihrp*Zs4@os zL6BrT9r*%qSAEg8Mgn4?>Sl)7?m#SDN`xrhF_etF1Lg3N(gN4sN&F~ea1shH#hxgD zxZGn(@*OHmupmE0vIfCQ4=R}dsl_Q^7UdC?3pg;dIt9$&!o;KuGJGE)Dk$Oln=>L` zP&lp_kHfSnACPKCME#M74m7H;okfjQ7SPXWF45Il8u>U`j!)+E$==2(mT@v*8x$|p z-w}0_b4}=O*`O8}ucg<24ZS}zfPzOQsugz5&|Jvrp9p%qV)%E1;oqZVv#Qrj?s)w< zxRv_4pMahzjQBYUKy4oBX24~mACC&?7foSQ!Wm0?gHO0(ir`YSM!jXYDt zk2aR{|CH7%l9A(Eru_|yF)XP!Q5M-iA!Sqg>77!>`bAU0U4hgWT>L&26HK{&7AfJI z)3NhB_QM3Tb73-W@}Q^8^8QbpnsjI#g&^7y#R=XK1sA<#3dzaL0z7-3-bhyq*x?WG z{Al+jJ5%XH~f0e;lg!X_0s6Me%qNW0R39zM^hY>*R`} zptZUfJ6NvfBa%#f&?+JEb@KsQv4|1|G?+mAvV)Bgd#XA)u-L(`I<+D*h@w#bB}Q~= z{-l$5IIkCePB`jQTAS%PRlWZ`3|uAu_5_WBy$IjNkL}_jBVXow6kjbO;t3Fwl@I}K z2p8Rk7=lran_ZR%K^+*1ef3|S;e*hK&o6P_K literal 0 HcmV?d00001 diff --git a/batch/opt/__pycache__/tiling.cpython-310.pyc b/batch/opt/__pycache__/tiling.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fef06f17f226724daceddfc23c9ab5933412288e GIT binary patch literal 3101 zcmZuzO^+M5874WL(P%W1HXCETa+}0)oH*kkR_nkBg4#jbIESFcCcw5Ru!$ka;zTR0 zzLvwa*8mj+P=A1Lxvq~n^$+Av^vY{b27(@X31B2>o|hwOKav^n>*Y)G{o?a-TCXPz z-}lFN^4=?q{f7o;KY_s?QHn26DW>>{{jF^IvK77+qhK%a^Ah7g$$zrFP=zW&8>xzl z(Nv;9|*l|0?VyXd=c27BS4p@d_`*6Zn;N*Jw7m@Wy6U0!Oc36A6M4n%9=X{ z_h){#x}L52ak3sKYwjAj4@_$6*uAmf3x1zHXw0SOZ&=x4t2IN6?y|!#Xk{jLUhsFa zrb`CJU{aXwq?fz+lZncY2a~=F2I|228k#d~elHQn~ZZtoyhougTMG}zxiYJc?izkJ;89UbJDF}?lvbY|LS zFoMLlW>0h#D=Xx>w^1cR@`NX%!6TmhC>zNS8+?PyO7gvhGIml zV#M+js78MG(ZYt+rP6iWV%+m#wYyQ%ol*QX5cE~5UZ?6BDi?KU zGr0JoJreIba&58lezhbG3Zvhom0|E;6O} z_V+O0%1lqWHUL&`1Ynd6WvTX4{-8G(#507ES|tFtSEFHg68Q@l=(Yi5mOMTpzzx$h z&#%N8#fodaRuHuhNMFrBh8WiumzlExgV{-cz z)vttSJ|)lSfV_niFc1=+Xe zb(4DO>qsEdu1{*QVcn*~I)ICGtHa{|w&}d#*L%KY@ZN(D&+5^%jiH3(O%HGg{>!-I z<<1L!z#vikf()%+%0QRqG9!(>9L=+GJmrTk+vdD+_`eJLymW4#2=ZTC_rBMq*^%B_ zX=yF!Ujw}k4?){`jXVYUSGHXG4RVi9J!h+BoOI;jKMtE7n<_h+@KOJkDm;9c2XOr= zde8rlFKe8+z^6D(przOMd6|6ub@lk=Z&`WEGjKY2kUuFrxa&7C&NfPTBTz1{@H^=3 z)yGF8Gw6&Sc}(AvBTDo9%yr+ZQX=kn?B@qgB2SE6IHhy<#Bk_B>ir(sf!Yh{K);J4 z?xGYAQKdl4OW}2~CEgZNAQN*bH-V*;ktLua0a%pzIThD=1F52cf0N(D>?QP4VD2XW z9Y*5oO@0-LB;^|c(GwK$wcPgSodcQx=Th-Nv&!5HZYW#d@US41PamGLj(4+ncvit9=gBO0 zl|R$d<> 1; j > 0; j >>= 1) { + __syncthreads(); + int ixj = tid ^ j; + if (ixj > tid) { + if ((tid & k) == 0 && shared_arr[tid] > shared_arr[ixj]) + swap(shared_arr[tid], shared_arr[ixj], shared_ord[tid], shared_ord[ixj]); + if ((tid & k) != 0 && shared_arr[tid] < shared_arr[ixj]) + swap(shared_arr[tid], shared_arr[ixj], shared_ord[tid], shared_ord[ixj]); + } + } + } + + __syncthreads(); + arr[tid] = shared_arr[tid]; + ord[shared_ord[tid]] = tid; +} + +__global__ void build_index(long * indices, long * uniq_idx, long * buf_idx, int * uniq_cnt){ + __shared__ long idx[C], ord[C], ibuf[C], iuniq[C], count[C], ord_uniq[C]; + int tid = threadIdx.x; + idx[tid] = indices[blockIdx.x * C + tid]; + ord[tid] = tid; + ord_uniq[tid] = tid; + count[tid] = 0; + __syncthreads(); + + bitonic_sort(idx, ord); + ibuf[tid] = (tid > 0 && idx[tid] > idx[tid-1]) ? 1:0; + __syncthreads(); + + for (int offset = 1; offset < C; offset *= 2) { + __syncthreads(); + if (tid >= offset) { + ibuf[tid] += ibuf[tid - offset]; + } + } + + if (tid == 0) { count[ibuf[C-1]+1] = C; } + else if (idx[tid] > idx[tid-1]) { + count[ibuf[tid]] = tid; } + iuniq[tid] = _REL_ID_; + __syncthreads(); + + // exceed threshold + if (tid > 0 && count[tid]-count[tid-1]>T) { + iuniq[tid-1] = idx[count[tid]-1]; } + __syncthreads(); + + bitonic_sort(iuniq, ord_uniq); + + int temp = ord_uniq[ibuf[tid]]; + if (iuniq[temp] < _REL_ID_){ + ibuf[tid] = temp; + }else{ + ibuf[tid] = idx[tid] + C; + } + if (iuniq[tid] < _REL_ID_ && iuniq[tid+1] == _REL_ID_){ + uniq_cnt[blockIdx.x] = tid+1; + } + buf_idx[blockIdx.x * C + tid] = ibuf[ord[tid]]; + uniq_idx[blockIdx.x * C + tid] = iuniq[tid]; +} \ No newline at end of file diff --git a/batch/opt/tiling.py b/batch/opt/tiling.py index 9e881f6..688de65 100644 --- a/batch/opt/tiling.py +++ b/batch/opt/tiling.py @@ -4,9 +4,162 @@ import codegen from batch.opt.ir import * +def swap_arr_to_reg(ir, pre, cur): + # print(codegen.gpu.to_string(ir), codegen.gpu.to_string(pre), codegen.gpu.to_string(cur)) + if isinstance(ir, Indexing): + temp = ir + while isinstance(temp, Indexing): + # print(codegen.gpu.to_string(temp), codegen.gpu.to_string(temp.idx)) + if temp.idx == pre: + temp.idx = Expr(pre, cur, '+') + temp = temp.dobject + elif isinstance(ir, Expr): + ir.left = swap_arr_to_reg(ir.left, pre, cur) + ir.right = swap_arr_to_reg(ir.right, pre, cur) + elif isinstance(ir, Assignment): + ir.lhs = swap_arr_to_reg(ir.lhs, pre, cur) + ir.rhs = swap_arr_to_reg(ir.rhs, pre, cur) + elif isinstance(ir, Loop): + for i in range(len(ir.body)): + ir.body[i] = swap_arr_to_reg(ir.body[i], pre, cur) + return ir -def tile_loop(ast): +def tile_wD(ir): + if isinstance(ir, Loop): + if ir.end.name() == 'dim': + scalar_D = Scalar('int', 'D') + tbody = ir.body + ir.step = scalar_D + new_loop = Loop(0, scalar_D, 1,[]) + for i in range(len(tbody)): + # tile_wD(tbody[i]) + tbody[i] = swap_arr_to_reg(tbody[i], ir.iterate, new_loop.iterate) + new_loop.body.extend(tbody) + # iloops.append(new_loop) + ir.body = [new_loop] + return ir + +def recursive_tile(ir): + if isinstance(ir, Loop): + ir = tile_wD(ir) + for i in range(len(ir.body)): + ir.body[i] = recursive_tile(ir.body[i]) + + return ir + +def tile_loops(ir, tile_list): + if isinstance(ir, Loop) and ir.end.name() == 'dim': + scalar_D = Scalar('int', 'D') + ir.step = scalar_D + new_loop = Loop(0, scalar_D, 1,[]) + tile_list.append(ir) + for i in range(len(ir.body)): + ir.body[i] = tile_loops(ir.body[i], tile_list) + return ir +def swap_loops_tile(ir): + if isinstance(ir, Loop): + # print('oloop and iloop:', codegen.gpu.to_string(oloop), codegen.gpu.to_string(iloop)) + temp = ir + + # while isinstance(temp, Loop) and temp.end.name() != 'dim': + + # temp = temp.body[0] + for i in range(len(ir.body)): + # print(ir.body[i], codegen.gpu.to_string(ir.body[i])) + if isinstance(ir.body[i], Loop) and ir.body[i].end.name() != 'dim': + # print(ir.body[i], codegen.gpu.to_string(ir.body[i])) + ir.body[i] = swap_loops_tile(ir.body[i]) + + elif isinstance(ir.body[i], Loop) and ir.body[i].end.name() == 'dim': + # print(ir.body[i], codegen.gpu.to_string(ir.body[i])) + tile_list = [] + temp = ir.body[i] + tile_loops(temp, tile_list) + # print(temp, tile_list, codegen.gpu.to_string(temp), codegen.gpu.to_string(tile_list[-1])) + # print(codegen.gpu.to_string(tile_list[0])) + # print(tile_list) + multi_lv_loop = {} + for lidx in range(len(tile_list)): + t = tile_list[lidx] + # add tiled loop here + scalar_D = Scalar('int', 'D') + new_loop = Loop(0, scalar_D, 1,[]) + temp_body = [] + for k in range(len(t.body)): + item = t.body[k] + if isinstance(item, Loop) and item.end.name() == 'dim': + if new_loop.body != []: + # add new_tiled loop and create a new one + temp_body.append(new_loop) + new_loop = Loop(0, scalar_D, 1,[]) + + if item in multi_lv_loop.keys(): + multi_lv_loop[item].append([multi_lv_loop[item][-1][0]+1, t]) + else: + multi_lv_loop[item] = [[1, t]] + temp_body.append(item) + elif t in multi_lv_loop.keys(): + oloop = Loop(0, scalar_D, 1,[]) + for jj in range(len(t.body)): + t.body[jj] = swap_arr_to_reg(t.body[jj], multi_lv_loop[t][0][1].iterate, oloop.iterate) + loop1 = oloop + for i in range(len(multi_lv_loop[t])): + tloop = Loop(0, scalar_D, 1,[]) + loop1.body.append(tloop) + if i+1 < len(multi_lv_loop[t]): + for jj in range(len(t.body)): + t.body[jj] = swap_arr_to_reg(t.body[jj], multi_lv_loop[t][i+1][1].iterate, tloop.iterate) + loop1 = tloop + for jj in range(len(t.body)): + t.body[jj] = swap_arr_to_reg(t.body[jj], t.iterate, tloop.iterate) + # print('...............', oloop, codegen.gpu.to_string(tloop), codegen.gpu.to_string(t)) + tloop.body = t.body + temp_body.append(oloop) + else: + # add tiled loop here + item = swap_arr_to_reg(item, t.iterate, new_loop.iterate) + new_loop.body.append(item) + if new_loop.body != []: + temp_body.append(new_loop) + tile_list[lidx].body = temp_body + # while isinstance(temp, Loop) and temp.end.name() == 'dim': + # scalar_D = Scalar('int', 'D') + # temp.step = scalar_D + # new_loop = Loop(0, scalar_D, 1,[]) + # tile_list.append([new_loop, temp.iterate]) + # if not isinstance(temp.body[0], Loop): + # oloop = temp + + # new_loop2 = Loop(0, scalar_D, 1,[]) + # if len(temp.body) > 1: + # for oidx in range(1, len(temp.body)): + # swap_arr_to_reg(temp.body[oidx], temp.iterate, new_loop2.iterate) + # new_loop2.body.append(temp.body[oidx]) + # tbody = [temp.body[0], new_loop2] + # temp.body = tbody + # temp = temp.body[0] + # # temp is assign, oloop is outloop of it + # print('tile list len:', tile_list) + # for i in range(len(tile_list)): + # oloop.body = [tile_list[i][0]] + # temp = swap_arr_to_reg(temp, tile_list[i][1], tile_list[i][0].iterate) + # oloop = oloop.body[0] + # oloop.body = [temp] + return ir + +def tile_loop(ast): + if type(ast) == BatchOp: + if type(ast.operators[1]) == BatchOp: + tile_loop(ast.operators[1]) + if type(ast.operators[0]) == BatchOp: + tile_loop(ast.operators[0]) + else: + return if ast.compute and ast.valid: - pass \ No newline at end of file + # print(ast.compute[0], codegen.gpu.to_string(ast.compute[0])) + for i in ast.compute: + # recursive_tile(i) + t = swap_loops_tile(i) + # print(codegen.gpu.to_string(t)) \ No newline at end of file diff --git a/batch/test/kge.py b/batch/test/kge.py index cb581c5..561715b 100644 --- a/batch/test/kge.py +++ b/batch/test/kge.py @@ -2,8 +2,10 @@ sys.path.append('/data/backed_up/lihhu/CUKE/cuke') from codegen import * from batch.ast import * -from batch.opt.fusion_rules import * -from batch.opt.parallelism import * +# from batch.opt.fusion_rules import * +# from batch.opt.parallelism import * +# from batch.opt.tiling import * +from batch.opt import * import run import torch @@ -28,8 +30,9 @@ def transE(): ast = res._gen_ir() fuse_operators(ast) - # code = codegen.cpu.print_cpp(ast) + tile_loop(ast) parallel(ast) + add_smem(ast) code = codegen.gpu.print_cuda(ast) print(code) # h = torch.randint(0, 9999, (4096, )).cuda(0) @@ -66,22 +69,24 @@ def transH(): # code = codegen.cpu.print_cpp(res._gen_ir()) ast = res._gen_ir() fuse_operators(ast) + tile_loop(ast) parallel(ast) + add_smem(ast) # code = codegen.cpu.print_cpp(ast) code = codegen.gpu.print_cuda(ast) print(code) - h = torch.randint(0, 9999, (4096, )).cuda(0) - r = torch.randint(0, 100, (4096, )).cuda(0) - t = torch.randint(0, 9999, (4096, )).cuda(0) - eemb = torch.rand((9999, 512)).cuda(0) - remb = torch.rand((100, 512)).cuda(0) - pemb = torch.rand((100, 512)).cuda(0) + # h = torch.randint(0, 9999, (4096, )).cuda(0) + # r = torch.randint(0, 100, (4096, )).cuda(0) + # t = torch.randint(0, 9999, (4096, )).cuda(0) + # eemb = torch.rand((9999, 512)).cuda(0) + # remb = torch.rand((100, 512)).cuda(0) + # pemb = torch.rand((100, 512)).cuda(0) - y = eemb[h] + remb[r] - eemb[t] - torch.einsum('a,ab->ab', torch.einsum('ab,ab->a', pemb[r], eemb[h]-eemb[t]), pemb[r]) - print(y) + # y = eemb[h] + remb[r] - eemb[t] - torch.einsum('a,ab->ab', torch.einsum('ab,ab->a', pemb[r], eemb[h]-eemb[t]), pemb[r]) + # print(y) - x = run.gpu.compile_and_run(code, 4096, 512, 0, eemb, h,t, 0, remb, r, pemb) - print(x) + # x = run.gpu.compile_and_run(code, 4096, 512, 0, eemb, h,t, 0, remb, r, pemb) + # print(x) def transR(): @@ -106,9 +111,10 @@ def transR(): ast = res._gen_ir() fuse_operators(ast) - # print(codegen.cpu.print_cpp(ast)) + # todo decouple operators + tile_loop(ast) parallel(ast) - + # add_smem(ast) # code = codegen.cpu.print_cpp(ast) code = codegen.gpu.print_cuda(ast) print(code) @@ -149,20 +155,22 @@ def transF(): code = codegen.cpu.print_cpp(res._gen_ir()) ast = res._gen_ir() fuse_operators(ast) + tile_loop(ast) parallel(ast) + add_smem(ast) code = codegen.gpu.print_cuda(ast) print(code) - h = torch.randint(0, 9999, (4096, )).cuda(0) - r = torch.randint(0, 100, (4096, )).cuda(0) - t = torch.randint(0, 9999, (4096, )).cuda(0) - eemb = torch.rand((9999, 512)).cuda(0) - remb = torch.rand((100, 512)).cuda(0) + # h = torch.randint(0, 9999, (4096, )).cuda(0) + # r = torch.randint(0, 100, (4096, )).cuda(0) + # t = torch.randint(0, 9999, (4096, )).cuda(0) + # eemb = torch.rand((9999, 512)).cuda(0) + # remb = torch.rand((100, 512)).cuda(0) - y = torch.einsum('ab,ab->a', eemb[h], eemb[t]) - torch.einsum('ab,ab->a',(eemb[h] - eemb[t]), remb[r]) - print(y) + # y = torch.einsum('ab,ab->a', eemb[h], eemb[t]) - torch.einsum('ab,ab->a',(eemb[h] - eemb[t]), remb[r]) + # print(y) - x = run.gpu.compile_and_run(code, 4096, 512, 0, eemb, h,t, 0, remb, r) - print(x) + # x = run.gpu.compile_and_run(code, 4096, 512, 0, eemb, h,t, 0, remb, r) + # print(x) def RESCAL(): nnodes = Var('nnodes') @@ -185,22 +193,25 @@ def RESCAL(): ast = res._gen_ir() fuse_operators(ast) + tile_loop(ast) parallel(ast) + add_smem(ast) + # traversal call funcs to opt ir # code = codegen.cpu.print_cpp(ast) code = codegen.gpu.print_cuda(ast) print(code) - h = torch.randint(0, 9999, (4096, )).cuda(0) - r = torch.randint(0, 100, (4096, )).cuda(0) - t = torch.randint(0, 9999, (4096, )).cuda(0) - eemb = torch.rand((9999, 512)).cuda(0) - remb = torch.rand((100, 512, 512)).cuda(0) + # h = torch.randint(0, 9999, (4096, )).cuda(0) + # r = torch.randint(0, 100, (4096, )).cuda(0) + # t = torch.randint(0, 9999, (4096, )).cuda(0) + # eemb = torch.rand((9999, 512)).cuda(0) + # remb = torch.rand((100, 512, 512)).cuda(0) - y = torch.einsum('ab,ab->a', torch.einsum('ab,abc->ac', eemb[h], remb[r]), eemb[t]) - print(y, y.shape) + # y = torch.einsum('ab,ab->a', torch.einsum('ab,abc->ac', eemb[h], remb[r]), eemb[t]) + # print(y, y.shape) - x = run.gpu.compile_and_run(code, 4096, 512, 0, eemb, h, 0, remb, r, t) - print(x) + # x = run.gpu.compile_and_run(code, 4096, 512, 0, eemb, h, 0, remb, r, t) + # print(x) def test(): nnodes = Var('nnodes') @@ -245,9 +256,9 @@ def test(): if __name__ == "__main__": # test() # bov success - transE() # success + # transE() # success # transH() # success - # transR() # success + transR() # success # transF() # success # RESCAL() # success \ No newline at end of file diff --git a/codegen/gpu.py b/codegen/gpu.py index 7b58a8b..46ce585 100644 --- a/codegen/gpu.py +++ b/codegen/gpu.py @@ -15,6 +15,7 @@ def to_string(ir): return f"{to_string(ir.lhs)} {ir.op}= {to_string(ir.rhs)};\n" case 'Loop': code = f"for (int {to_string(ir.iterate)} = {to_string(ir.start)}; {to_string(ir.iterate)} < {to_string(ir.end)}; {to_string(ir.iterate)} += {to_string(ir.step)}) {{\n" + # print(ir, ir.body, to_string(ir.body)) for e in ir.body: if e: code += to_string(e) @@ -22,13 +23,27 @@ def to_string(ir): return code case 'Scalar' | 'Ndarray' | 'Ref': return ir.name() + case 'Literal': + return str(ir.val) case 'Indexing': if type(ir.dobject) == Slice: return f'(({to_string(ir.dobject.start)})+({to_string(ir.dobject.step)})*({to_string(ir.idx)}))' + # elif type(ir.dobject) == Pointer: + # code = f'{to_string(ir.dobject)}[' + # for i in range(len(ir.dobject.dims)): + # code += f'{to_string(ir.idx)}*{to_string(ir.dobject.dims[i])}' + # if i();\n' return code + elif type(ir.dobject) == Shared: + shape = '' + for i in range(len(ir.dobject.dobject.size)): + shape += f'[{to_string(ir.dobject.dobject.size[i])}]' + return f'__shared__ {ir.dobject.dobject.dtype} {ir.dobject.dobject.name()}{shape};\n' + elif type(ir.dobject) == Pointer: + return f'{ir.dobject.dtype} {ir.dobject.name()};\n' + + elif type(ir.dobject) == Buffer or Uniq: + return f'torch::Tensor {ir.dobject.dobject.__name__}_{ir.dobject.__class__.__name__} = torch::empty({{{to_string(ir.dobject.dobject.size[0])}/16, 16}}, torch::TensorOptions(torch::k{"Int" if ir.dobject.dobject.dtype=="int" else "Float"}).device(torch::kCUDA));\n' + else: + return f'{to_string(ir.dobject)}' # elif type(ir.dobject) == Ref: # code = f'{ir.dobject.dobject.dtype}* {ir.dobject.name()} = ({ir.dobject.dobject.dtype}*)&{ir.dobject.dobject.addr()}' # return code @@ -63,6 +90,26 @@ def to_string(ir): return f'{to_string(ir.dobject)} = __shfl_sync(0xffffffff, {to_string(ir.dobject)}, 0);\n' case 'SaveAtThread': return f'if (threadIdx.x == {ir.threadid}) {{\n {to_string(Assignment(ir.dst, ir.src))} }}\n' + case 'Uniq' | 'Buffer': + return f'{ir.dobject.__name__}_{ir.__class__.__name__}[{to_string(BlockIdx())}]' + # return f'[{to_string(BlockIdx())}]' + case 'IF': + return f"{to_string(ir.left)} = {to_string(ir.condition)} ? {to_string(ir.true_var)} : {to_string(ir.false_var)};\n" + case 'Pointer': + return f'{ir.name()}' + case 'Access_ptr': + code = f'{to_string(ir.dobject)}[' + for i in range(len(ir.idx)-1): + code += '(' + for i in range(len(ir.idx)): + if i()' ptrs += f', torch::PackedTensorAccessor32<{d.dobject.dtype}, {len(d.dobject.size)}, torch::RestrictPtrTraits> {d.dobject.name()}' + elif type(d) == Decl and type(d.dobject) in [Buffer, Uniq]: + declare += to_string(d) + argsptr += f', {d.dobject.dobject.__name__}_{d.dobject.__class__.__name__}.packed_accessor32<{d.dobject.dobject.dtype}, 2, torch::RestrictPtrTraits>()' + ptrs += f', torch::PackedTensorAccessor32<{d.dobject.dobject.dtype}, 2, torch::RestrictPtrTraits> {d.dobject.dobject.__name__}_{d.dobject.__class__.__name__}' else: code += to_string(d) # print(declare) diff --git a/core/ast.py b/core/ast.py index ded7e22..d20fef3 100644 --- a/core/ast.py +++ b/core/ast.py @@ -89,6 +89,7 @@ def __init__(self): self.ref_count = 0 self.id = ASTNode.nuniq ASTNode.nuniq += 1 + self.valid = True class Tensor(ASTNode): @@ -238,6 +239,8 @@ class Const(Var): def __init__(self, val, dtype): super().__init__(f'c{Const.nconsts}', dtype) Const.nconsts += 1 + # slice is considered constant because once the slice is created its start, stop, step cannot be reassigned + # however, start, stop, step themselves can be variables if dtype == 'slice': assert type(val.start) == int or is_int_var(val.start) assert type(val.stop) == int or is_int_var(val.stop) @@ -258,6 +261,7 @@ def __init__(self, op_type, *operators): dtype = operators[0].dtype self.operators = [] for opr in operators: + # an index can be referenced multiple times in the ast, we should create duplicate copies so that they can bind with different loop iterates if type(opr) == TensorOp and opr.op_type == 'index' and opr.ref_count >= 1: new_opr = copy.copy(opr) new_opr.ref_count = 1 @@ -325,7 +329,7 @@ def __init__(self, op_type, *operators): elif is_int_var(self.operators[1]): self.operators[1] = self.operators[1] elif is_1dint_tensor(self.operators[1]): - fix_size.append(self.operators[1].ref_size[0]) + fix_size.append(self.operators[1]._size()[0]) else: raise TypeError('index must be int, Var of int, or 1d int Tensor') @@ -400,4 +404,3 @@ def __init__(self, op_type, *operators): - diff --git a/core/ast2ir.py b/core/ast2ir.py index 6062bf6..8b329c6 100755 --- a/core/ast2ir.py +++ b/core/ast2ir.py @@ -5,23 +5,31 @@ -def bind(arr: (Ndarray, Index), index, fix_ref=False): - # fix_ref == True means index at current position instead of the first unbind - if type(arr) == Ndarray or index == None or fix_ref: - return Index(arr, index=index) - else: - ref_chain = [arr] - while (type(ref_chain[-1].dobject) != Ndarray): - ref_chain.append(ref_chain[-1].dobject) - for ref in ref_chain[::-1]: - if ref.index == None: - ref.index = index - return arr - return Index(arr, index=index) +def get_first_unbind(index: (Indexing, Ndarray, Slice)): + if type(index) == Indexing: + x = get_first_unbind(index.dobject) + if x != None: + return x + else: + if type(index.idx) == Literal and index.idx.val == -1: + return index + else: + y = get_first_unbind(index.idx) + return y + return None + + +def bind(index: (Indexing, Ndarray, Slice), idx): + x = get_first_unbind(index) + if x == None: + return Indexing(index, idx) + else: + x.idx = idx + return index def gen_ir(node): @@ -30,13 +38,15 @@ def gen_ir(node): return node if type(node) == Const: if node.dtype != 'slice': - node.eval = node.val + assert type(node.val) == int or type(node.val) == float + node.eval = Literal(node.val, node.dtype) else: node.val.start._gen_ir() node.val.stop._gen_ir() node.val.step._gen_ir() node.eval = Slice(node.val.start.eval, node.val.stop.eval, node.val.step.eval) + elif type(node) == Var or (type(node) == Tensor and len(node._size()) == 0): node.eval = Scalar(node.dtype, node.name, node.is_arg) node.decl = [Decl(node.eval)] @@ -140,39 +150,41 @@ def gen_ir(node): elif node.op_type == 'index': node.operators[0]._gen_ir() node.operators[1]._gen_ir() - if type(node.operators[1]) == Var or (type(node.operators[1]) == Const and node.operators[1].dtype == 'int'): - node.eval = Index(node.operators[0].eval, index=node.operators[1].eval) - else: # ind_arr can be a slice or Tensor - # TODO: asssert tensor is 1d int? - node.eval = Index(node.operators[0].eval, ind_arr=node.operators[1].eval) + if type(node.operators[1].eval) in (Scalar, Literal, Indexing): + node.eval = Indexing(node.operators[0].eval, node.operators[1].eval) + elif type(node.operators[1].eval) in (Ndarray, Slice): + node.eval = Indexing(node.operators[0].eval, Indexing(node.operators[1].eval, Literal(-1, 'int'))) + else: + raise TypeError('incorrect index type!') elif node.op_type == 'apply': - node.operators[0]._gen_ir() - node.operators[2]._gen_ir() + node.operators[0]._gen_ir() # input tensor + node.operators[2]._gen_ir() # axis - axis = node.operators[2].eval + axis = node.operators[2].eval.val outer_loop = Loop(0, node.operators[0].eval.size[axis], 1, []) + + # item is an indexing to the input tensor in axis dimension item = node.operators[3] item.eval = node.operators[0].eval for i in range(axis): - item.eval = bind(item.eval, None) - item.eval = bind(item.eval, outer_loop.iterate, True) + item.eval = Indexing(item.eval, Literal(-1, 'int')) + item.eval = Indexing(item.eval, outer_loop.iterate) - item.decl = [] ret = node.operators[1](item) ret._gen_ir() def action(node, res): - if type(node) == Var or type(node) == One or type(node) == Zero or type(node) == Ones or type(node) == Zeros or type(node) == Tensor: - res.extend(node.decl) - node.decl.clear() - elif type(node) == TensorOp: - res.extend(node.decl) - res.extend(node.compute) - node.decl.clear() - node.compute.clear() + if node.valid == True: + if type(node) == Var or type(node) == One or type(node) == Zero or type(node) == Ones or type(node) == Zeros or type(node) == Tensor: + res.extend(node.decl) + node.valid = False + elif type(node) == TensorOp: + res.extend(node.decl) + res.extend(node.compute) + node.valid = False t = helpers.Traversal(action) ret_ir = t(ret) @@ -213,6 +225,9 @@ def action(node, res): assign = Assignment(res, ret.eval) outer_loop.body.append(assign) + # the above code generates two loops: the first loop calculates the results, the second loop stores the results to an output tensor + # TODO: the two loops might be generated together and save the fusion step + # TODO: the output is still stored in a temporary tensor after fusion. the temp tensor can be removed scope = outer_loop.body while len(scope) == 2: fuse(scope, scope[0], scope[1]) @@ -222,6 +237,7 @@ def action(node, res): node.compute = [outer_loop] + elif node.op_type == 'reduce': node.operators[0]._gen_ir() node.operators[2]._gen_ir() # init @@ -261,7 +277,7 @@ def action(node, res): item2.eval = node.operators[0].eval for i in range(axis): # TODO: is this correct? item2.eval = bind(item2.eval, None) - if type(item2.eval) == Index and type(item2.eval.ind_arr) == Slice: + if type(item2.eval) == Indexing and type(item2.eval.ind_arr) == Slice: item2.eval = bind(item2.eval, outer_loop.iterate) else: item2.eval = bind(item2.eval, outer_loop.iterate, True) @@ -272,30 +288,16 @@ def action(node, res): ret._gen_ir() def action(node, res): - if type(node) == Var or type(node) == One or type(node) == Zero or type(node) == Ones or type(node) == Zeros or type(node) == Tensor: - res.extend(node.decl) - node.decl.clear() - elif type(node) == TensorOp: - res.extend(node.decl) + if type(node) == TensorOp: res.extend(node.compute) - node.decl.clear() - node.compute.clear() - t = helpers.Traversal(action) - ret_ir = t(ret) - ret_decl = [] - ret_compute = [] - - for ir in ret_ir: - if type(ir) == Decl: - ret_decl.append(ir) - else: - ret_compute.append(ir) + ret_compute = t(ret) node.operators.append(ret) outer_loop.body.extend(ret_compute) - node.decl.extend(ret_decl) + node.decl.extend(ret.decl) + ret.valid = False if (len(ret.eval.size) > 0): pre_loop = Loop(0, ret.eval.size[0], 1, []) @@ -344,7 +346,7 @@ def action(node, res): item1 = node.operators[6] item2 = node.operators[7] - item1.eval = Index(node.eval, ind_arr=node.operators[3].eval) + item1.eval = Indexing(node.eval, ind_arr=node.operators[3].eval) item1.eval = bind(item1.eval, outer_loop.iterate) item2.eval = node.operators[0].eval for i in range(axis): @@ -357,35 +359,21 @@ def action(node, res): ret._gen_ir() def action(node, res): - if type(node) == Var or type(node) == One or type(node) == Zero or type(node) == Ones or type(node) == Zeros or type(node) == Tensor: - res.extend(node.decl) - node.decl.clear() - elif type(node) == TensorOp: - res.extend(node.decl) + if type(node) == TensorOp: res.extend(node.compute) - node.decl.clear() - node.compute.clear() - t = helpers.Traversal(action) - ret_ir = t(ret) - ret_decl = [] - ret_compute = [] - - for ir in ret_ir: - if type(ir) == Decl: - ret_decl.append(ir) - else: - ret_compute.append(ir) + ret_compute = t(ret) node.operators.append(ret) outer_loop.body.extend(ret_compute) - node.decl.extend(ret_decl) + node.decl.extend(ret.decl) + ret.valid = False if (len(ret.eval.size) > 0): pre_loop = Loop(0, ret.eval.size[0], 1, []) outer_loop.body.append(pre_loop) - res = bind(Index(node.eval, ind_arr=node.operators[3].eval), outer_loop.iterate) + res = bind(Indexing(node.eval, ind_arr=node.operators[3].eval), outer_loop.iterate) res = bind(res, pre_loop.iterate) rhs = bind(ret.eval, pre_loop.iterate) for i in range(1, len(ret.eval.size)): @@ -396,11 +384,16 @@ def action(node, res): rhs = bind(rhs, pre_loop.iterate) pre_loop.body.append(Assignment(res, rhs)) else: - res = bind(Index(node.eval, ind_arr=node.operators[3].eval), outer_loop.iterate) + res = bind(Indexing(node.eval, ind_arr=node.operators[3].eval), outer_loop.iterate) assign = Assignment(res, ret.eval) outer_loop.body.append(assign) node.compute.append(outer_loop) - return node + # points from IR back to ASTNode + for d in node.decl: + d.astnode = node + for s in node.compute: + s.astnode = node + return node diff --git a/core/ir.py b/core/ir.py index 76fbed5..274089a 100644 --- a/core/ir.py +++ b/core/ir.py @@ -1,20 +1,22 @@ class IR: - def __init__(self) -> None: - self.ast_ref = None + def __init__(self): + # astnode tracks the location of this IR in the AST + self.astnode = None class DOject(IR): nobjects = 0 - def __init__(self, dtype: str): - super(IR, self).__init__() + def __init__(self, dtype: str, size: (list, tuple)): + super().__init__() self.dobject_id = DOject.nobjects DOject.nobjects += 1 self.dtype = dtype + self.size = size class Expr(IR): def __init__(self, left, right, op: str): - super(IR, self).__init__() + super().__init__() self.left = left self.right = right self.op = op @@ -23,7 +25,7 @@ def __init__(self, left, right, op: str): class Assignment(IR): def __init__(self, lhs, rhs, op=None): - super(IR, self).__init__() + super().__init__() self.lhs = lhs self.rhs = rhs self.op = op @@ -34,7 +36,7 @@ class Loop(IR): loop_id = 0 def __init__(self, start, end, step, body: list): - super(IR, self).__init__() + super().__init__() self.lid = Loop.loop_id Loop.loop_id += 1 self.start = start @@ -46,31 +48,33 @@ def __init__(self, start, end, step, body: list): class Scalar(DOject): def __init__(self, dtype: str, name: str = None, is_arg = False, val = None): - super().__init__(dtype) + super().__init__(dtype, []) self.__name__ = name if name else f's{self.dobject_id}' - self.size = [] self.val = val self.is_arg = is_arg - - def name(self): return self.__name__ - def addr(self): - return self.name() + +class Literal(DOject): + def __init__(self, val: (int, float), dtype: str): + super().__init__(dtype, []) + self.val = val class Slice(IR): def __init__(self, start, stop, step): + super().__init__() self.start = start self.stop = stop self.step = step + self.dtype = 'int' + self.size = [Expr(Expr(self.stop, self.start, '-'), self.step, '/')] class Ndarray(DOject): def __init__(self, dtype: str, size: tuple, name: str = None, is_arg = False, val = None): - super().__init__(dtype) - self.size = size + super().__init__(dtype, size) self.__name__ = name if name else f'arr{self.dobject_id}' self.val = val # val is None, 0, or 1 self.is_arg = is_arg @@ -81,42 +85,37 @@ def __getitem__(self, item): def name(self): return self.__name__ - def addr(self): - return self.name() - -class Index(IR): - nindices = 0 - def __init__(self, dobject, index=None, ind_arr=None): - super(IR, self).__init__() +class Indexing(DOject): + def __init__(self, dobject, idx): + assert dobject != None and type(dobject) in (Slice, Ndarray, Indexing) + assert idx != None and type(idx) in (Scalar, Literal, Indexing) self.dobject = dobject - self.index = index - self.ind_arr = ind_arr - self.dtype = self.dobject.dtype - if ind_arr == None: - self.size = dobject.size[1:] - elif type(ind_arr) == Ndarray: - self.size = ind_arr.size + dobject.size[1:] - elif type(ind_arr) == Slice: - s = Expr(Expr(ind_arr.stop, ind_arr.start, '-'), ind_arr.step, '/') - self.size = [s] + dobject.size[1:] - self.index_id = Index.nindices - Index.nindices += 1 + self.idx = idx + + if type(self.dobject) in (Ndarray, Slice): + if type(idx) == Literal and idx.val == -1: + # idx is unspecified, which means the Indexing is a range of indice stored in dobject, so the size of Indexing should the same as the dobject + size = dobject.size[:] + self.ref_point = 1 + else: + # idx is a specific Scalar, Literal, or Indexing, in any case, the size of the Indexing operation should be as follows + # ref_point should be the next dimension if the node is further Indexed + size = idx.size + dobject.size[1:] + self.ref_point = len(idx.size) + else: + # dobject is an Indexing + size = dobject.size[:dobject.ref_point] + idx.size + dobject.size[dobject.ref_point+1:] + self.ref_point = dobject.ref_point + len(idx.size) + super().__init__(dobject.dtype, size) - def name(self): - return f'ref{self.index_id}_{self.dobject.name()}' - def addr(self): - if self.ind_arr: - return f'{self.dobject}[{self.ind_arr[0]}]' - else: - return f'{self.dobject}[0]' class Decl(IR): - def __init__(self, dobject): - super(IR, self).__init__() + def __init__(self, dobject: (Scalar, Ndarray)): + super().__init__() self.dobject = dobject \ No newline at end of file diff --git a/run/__init__.py b/run/__init__.py index 2e42cc2..b07f703 100644 --- a/run/__init__.py +++ b/run/__init__.py @@ -1 +1 @@ -from run import cpu \ No newline at end of file +from run import cpu, gpu \ No newline at end of file diff --git a/run/gpu.py b/run/gpu.py new file mode 100644 index 0000000..eeb7e44 --- /dev/null +++ b/run/gpu.py @@ -0,0 +1,8 @@ +from torch.utils.cpp_extension import load + +def compile_and_run(code, *args): + f = open('run/.tmp/cuda_code.cu', 'w') + f.write(code) + f.close() + module = load(name='module', sources=['run/.tmp/cuda_code.cu']) + return module.run(*args) \ No newline at end of file diff --git a/test/examples.py b/test/examples.py index 08dc911..fda5a32 100644 --- a/test/examples.py +++ b/test/examples.py @@ -150,4 +150,3 @@ def conv1d_v2(width): print(code) - diff --git a/transh.cpp b/transh.cpp new file mode 100644 index 0000000..6af2922 --- /dev/null +++ b/transh.cpp @@ -0,0 +1,115 @@ +#include + +torch::Tensor sub_add_sub_index_Eemb_h_index_Eemb_t_index_Remb_r_scal_mul_vec_vec_mul_vec_index_Remb_r_sub_index_Eemb_h_index_Eemb_t_index_Remb_r(int batch_size, int dim, int nnodes, torch::Tensor obj_Eemb, torch::Tensor obj_h, torch::Tensor obj_t, int nedges, torch::Tensor obj_Remb, torch::Tensor obj_r) +{ + auto Eemb = obj_Eemb.accessor(); +auto h = obj_h.accessor(); +auto t = obj_t.accessor(); +torch::Tensor obj_arr6 = torch::empty({batch_size,dim}, at::kFloat); +auto arr6 = obj_arr6.accessor(); +for (int _l0 = 0; _l0 < batch_size; _l0 += 1) { +for (int _l1 = 0; _l1 < dim; _l1 += 1) { +arr6[_l0][_l1] = (Eemb[h[_l0]][_l1] - Eemb[t[_l0]][_l1]); +} +} +auto Remb = obj_Remb.accessor(); +auto r = obj_r.accessor(); +torch::Tensor obj_arr12 = torch::empty({batch_size,dim}, at::kFloat); +auto arr12 = obj_arr12.accessor(); +for (int _l2 = 0; _l2 < batch_size; _l2 += 1) { +for (int _l3 = 0; _l3 < dim; _l3 += 1) { +arr12[_l2][_l3] = (arr6[_l2][_l3] + Remb[r[_l2]][_l3]); +} +} +torch::Tensor obj_arr15 = torch::empty({batch_size,dim}, at::kFloat); +auto arr15 = obj_arr15.accessor(); +for (int _l4 = 0; _l4 < batch_size; _l4 += 1) { +for (int _l5 = 0; _l5 < dim; _l5 += 1) { +arr15[_l4][_l5] = (Eemb[h[_l4]][_l5] - Eemb[t[_l4]][_l5]); +} +} +torch::Tensor obj_arr18 = torch::empty({batch_size}, at::kFloat); +auto arr18 = obj_arr18.accessor(); +for (int _l6 = 0; _l6 < batch_size; _l6 += 1) { +for (int _l7 = 0; _l7 < dim; _l7 += 1) { +arr18[_l6] += (Remb[r[_l6]][_l7] * arr15[_l6][_l7]); +} +} +torch::Tensor obj_arr21 = torch::empty({batch_size,dim}, at::kFloat); +auto arr21 = obj_arr21.accessor(); +for (int _l8 = 0; _l8 < batch_size; _l8 += 1) { +for (int _l9 = 0; _l9 < dim; _l9 += 1) { +arr21[_l8][_l9] = (arr18[_l8] * Remb[r[_l8]][_l9]); +} +} +torch::Tensor obj_arr24 = torch::empty({batch_size,dim}, at::kFloat); +auto arr24 = obj_arr24.accessor(); +for (int _l10 = 0; _l10 < batch_size; _l10 += 1) { +for (int _l11 = 0; _l11 < dim; _l11 += 1) { +arr24[_l10][_l11] = (arr12[_l10][_l11] - arr21[_l10][_l11]); +} +} +return obj_arr24; + +} + +PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { + m.def("run", &sub_add_sub_index_Eemb_h_index_Eemb_t_index_Remb_r_scal_mul_vec_vec_mul_vec_index_Remb_r_sub_index_Eemb_h_index_Eemb_t_index_Remb_r); +} +[] +arr6[_l0][_l1] = (Eemb[h[_l0]][_l1] - Eemb[t[_l0]][_l1]); + +[] +arr21[_l8][_l9] = (arr18[_l8] * Remb[r[_l8]][_l9]); + +[, ] +arr6[_l2][_l3] = (Eemb[h[_l2]][_l3] - Eemb[t[_l2]][_l3]); + +arr12[_l2][_l3] = (arr6[_l2][_l3] + Remb[r[_l2]][_l3]); + +#include + +torch::Tensor sub_add_sub_index_Eemb_h_index_Eemb_t_index_Remb_r_scal_mul_vec_vec_mul_vec_index_Remb_r_sub_index_Eemb_h_index_Eemb_t_index_Remb_r(int batch_size, int dim, int nnodes, torch::Tensor obj_Eemb, torch::Tensor obj_h, torch::Tensor obj_t, int nedges, torch::Tensor obj_Remb, torch::Tensor obj_r) +{ + auto Eemb = obj_Eemb.accessor(); +auto h = obj_h.accessor(); +auto t = obj_t.accessor(); +torch::Tensor obj_arr6 = torch::empty({batch_size,dim}, at::kFloat); +auto arr6 = obj_arr6.accessor(); +auto Remb = obj_Remb.accessor(); +auto r = obj_r.accessor(); +torch::Tensor obj_arr12 = torch::empty({batch_size,dim}, at::kFloat); +auto arr12 = obj_arr12.accessor(); +torch::Tensor obj_arr15 = torch::empty({batch_size,dim}, at::kFloat); +auto arr15 = obj_arr15.accessor(); +for (int _l4 = 0; _l4 < batch_size; _l4 += 1) { +for (int _l5 = 0; _l5 < dim; _l5 += 1) { +arr15[_l4][_l5] = (Eemb[h[_l4]][_l5] - Eemb[t[_l4]][_l5]); +} +} +torch::Tensor obj_arr18 = torch::empty({batch_size}, at::kFloat); +auto arr18 = obj_arr18.accessor(); +for (int _l6 = 0; _l6 < batch_size; _l6 += 1) { +for (int _l7 = 0; _l7 < dim; _l7 += 1) { +arr18[_l6] += (Remb[r[_l6]][_l7] * arr15[_l6][_l7]); +} +} +torch::Tensor obj_arr21 = torch::empty({batch_size,dim}, at::kFloat); +auto arr21 = obj_arr21.accessor(); +torch::Tensor obj_arr24 = torch::empty({batch_size,dim}, at::kFloat); +auto arr24 = obj_arr24.accessor(); +for (int _l10 = 0; _l10 < batch_size; _l10 += 1) { +for (int _l11 = 0; _l11 < dim; _l11 += 1) { +arr6[_l2][_l3] = (Eemb[h[_l2]][_l3] - Eemb[t[_l2]][_l3]); +arr12[_l10][_l11] = (arr6[_l10][_l11] + Remb[r[_l10]][_l11]); +arr21[_l10][_l11] = (arr18[_l10] * Remb[r[_l10]][_l11]); +arr24[_l10][_l11] = (arr12[_l10][_l11] - arr21[_l10][_l11]); +} +} +return obj_arr24; + +} + +PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { + m.def("run", &sub_add_sub_index_Eemb_h_index_Eemb_t_index_Remb_r_scal_mul_vec_vec_mul_vec_index_Remb_r_sub_index_Eemb_h_index_Eemb_t_index_Remb_r); +} From 04f628630e3ea7a454be42883eeb8a63cb9ae87c Mon Sep 17 00:00:00 2001 From: Lihan Date: Thu, 26 Oct 2023 20:43:09 -0500 Subject: [PATCH 7/8] sync to github/dev --- batch/opt/__init__.py | 10 +- .../__pycache__/fusion_rules.cpython-310.pyc | Bin 14395 -> 9172 bytes batch/opt/fusion_rules.py | 357 +++--------------- batch/test/kge.py | 51 ++- core/ast.py | 62 ++- core/ast2ir.py | 223 ++++++----- core/ir.py | 5 + core/test/examples.py | 116 +++++- opt/loop.py | 15 +- test/examples.py | 230 +++++++++++ 10 files changed, 605 insertions(+), 464 deletions(-) diff --git a/batch/opt/__init__.py b/batch/opt/__init__.py index 9c5cebe..0c77ff1 100644 --- a/batch/opt/__init__.py +++ b/batch/opt/__init__.py @@ -1,5 +1,5 @@ -from .fusion_rules import * -from .parallelism import * -from .ir import * -from .smem import * -from .tiling import * \ No newline at end of file +import batch.opt.fusion_rules +import batch.opt.ir +import batch.opt.parallelism +import batch.opt.smem +import batch.opt.tiling \ No newline at end of file diff --git a/batch/opt/__pycache__/fusion_rules.cpython-310.pyc b/batch/opt/__pycache__/fusion_rules.cpython-310.pyc index 00be7fca84eebaab49cf7e99c919d09ab2ab616f..9722f62d1d6f5c3db6bf0bb698c50660f9152a79 100644 GIT binary patch delta 3271 zcmbtWPjC}e7~i+M$!@l7lD65VB@GRwq^6|MTAKVX;2L-YVg84vj9Bp&?Ud%K&oz*vWN_U-rm z-uHd)`@Y|MZ}ZvQjXl9)ON%DJ-?dxY^Kayqg5BuA(&hs`aleoXUA?k55Nd)+KRn~` zn7_3W)Mo5nMEE;fD9Dzt;4di4>LuZf;9vnmIwj&;Fe@6UODF_D3amghd=oGj$xja!WZ3Xh70(mhc?`8S0ko57VJ$H2V;_063ETulkLNvAMcfj%VenEVU~tt z%z}G)O}zq-72MJYj9Zo}Wmdu&Z%H@RMoa^XQO+y~POhJ_Tz~N#d9D)PbiZ5yez;s` z>%2;M5{L=W>{s#1TFrTt@Dvz@z3iAN8ETHkfujaqTUS}^OGU?J{1l+F(FU#IGCu$^ z9%R<+EYhjwJPQAEr&<#c$jH5n9CGFwfqMaJhtm@Ey8re&qSN z#oQ*{BZv9LL&Len++I^L;NceI1?E?e?&ItC|fJd`pwqkWx>5>tURadr^Lflphah6O3wDpA^ z&|&nD1n3Dz)0W#n+d!{sI{>o6<%Wn$FyAq;OPCH+kkxA=J78;72?>k-Qry~T_0j1n zs-g*Dx}}030DK+53Q!xmK)(@SxrN+$G^+c}7g*h#WLmc$zXsgDxS-fRTdqnK#rd8* znAB}v1iB9;i0}!J8m*&sQW@s5MJ+$~b~$6LZb?5~e2)P6V8nDfO)hSYj;lBc^Rm$e zJ>zn~SjS{>#S(P3j%c9K&jouk`u(N5K>rFVHA<&PrGn3SjR(1bO0G3h$q|=Isy&!W z;KYb^32cifcp=}gNS+c#C;13_42Yw8MV%EFl@d*e?=x7U!S8=1XFkF9kh6szZ`eah z>B1WKkW#uxuBW5AVt(Z{ou3^A{|;v-yY0VziS&UVB~AijT7l$lsxy+m7>VjhK;mBH z#v4$@cK~o>kZt7^ zH0*tHex#?JXZ-W(j2Hd_WW5*3$(a5|F@*hs`=olF!xH>l#0Z9(bS8f+CDoeEW4W2^ z>G_!~=(Q#o?p03btP}!=+zj3duDfe8bxMgiO`{D9f+Uo8k=9V0{2J`_mBs!oWfBfO zgUVzow5O}A_a}0*Mxr>E$2qHr%|pY}+Y|ler^%sElH3gSisk2-I1t{v{$ArDYK*tQ zO68rf#z@Q2c`I?eI1dr~8PZSgMxyO_5G)`i`r_6!qkcjB|B*ys-PnFFR7hsM2)KL&5?frwG>1Y literal 14395 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 0): + init = Zeros(size, dtype=self.dtype) + else: + init = Const(0, dtype=self.dtype) + return self.reduce(func, init, axis) + def aggr(self, func, init, indices, axis=0, size=None): if callable(func): from core.ast2ir import gen_ir @@ -202,6 +217,13 @@ def size(self): def _gen_ir(self): return core.ast2ir.gen_ir(self) + def round(self): + return TensorOp('round', self) + + def abs(self): + return TensorOp('abs', self) + + class Ones(Tensor): nones = 0 def __init__(self, size, dtype='float'): @@ -252,7 +274,7 @@ def einsum(exp: str, tensor1, tensor2): return TensorOp('einsum', tensor1, tensor2, exp) class TensorOp(Tensor): - Types = ['index', 'apply', 'reduce', 'aggr', 'einsum'] + list(op_mapping.keys()) + Types = ['index', 'apply', 'reduce', 'aggr', 'einsum'] + list(op_mapping.keys()) + math_op + cmp_op def __init__(self, op_type, *operators): assert op_type in TensorOp.Types @@ -274,15 +296,23 @@ def __init__(self, op_type, *operators): if isinstance(opr, ASTNode): opr.ref_count += 1 - # TODO: implement scalar +/-/*/div tensor - if op_type in op_mapping: - ref_size = self.operators[0].fix_size + self.operators[0].ref_size - fix_size = [] + if op_type in op_mapping or op_type in cmp_op: + + if type(self.operators[0]) == int: + self.operators[0] = Const(self.operators[0], 'int') + elif type(operators[0]) == float: + self.operators[0] = Const(self.operators[0], 'float') if type(self.operators[1]) == int: self.operators[1] = Const(self.operators[1], 'int') elif type(operators[1]) == float: self.operators[1] = Const(self.operators[1], 'float') - + assert is_same_size(self.operators[0]._size(), self.operators[1]._size()) or len(self.operators[0]._size()) == 0 or len(self.operators[1]._size()) == 0 + if len(self.operators[0]._size()) < len(self.operators[1]._size()): + self.operators[0], self.operators[1] = self.operators[1], self.operators[0] + + ref_size = self.operators[0].fix_size + self.operators[0].ref_size + fix_size = [] + elif op_type == 'einsum': exp = self.operators[2] inputs, output = exp.split('->') @@ -325,7 +355,14 @@ def __init__(self, op_type, *operators): step = Const(step, 'int') self.operators[1] = Const(slice(start, stop, step), 'slice') - fix_size.append((stop - start)//step) + csize = eval_const_expr((stop - start)//step) + if csize != None: + fix_size.append(csize) + else: + if step.val == 1: + fix_size.append(stop-start) + else: + fix_size.append((stop - start)//step) elif is_int_var(self.operators[1]): self.operators[1] = self.operators[1] elif is_1dint_tensor(self.operators[1]): @@ -395,6 +432,15 @@ def __init__(self, op_type, *operators): self.operators.append(item1) self.operators.append(item2) + elif op_type in math_op: + ref_size = self.operators[0]._size() + fix_size = [] + if op_type == 'round': + dtype = 'int' + elif op_type == 'abs': + dtype = self.operators[0].dtype + + name = f'{op_type}_' + '_'.join([op.name if hasattr(op, 'name') else '' for op in self.operators]) super().__init__(name, ref_size, dtype, fix_size) diff --git a/core/ast2ir.py b/core/ast2ir.py index 8b329c6..5c23fed 100755 --- a/core/ast2ir.py +++ b/core/ast2ir.py @@ -19,7 +19,22 @@ def get_first_unbind(index: (Indexing, Ndarray, Slice)): return None - +def replace_output(ir, old, new): + if type(ir) == list or type(ir) == tuple: + for l in ir: + replace_output(l, old, new) + elif type(ir) == Loop: + replace_output(ir.body, old, new) + elif type(ir) == Assignment: + if ir.lhs == old: + ir.lhs = new + else: + replace_output(ir.lhs, old, new) + elif type(ir) == Indexing: + if ir.dobject == old: + ir.dobject = new + else: + replace_output(ir.dobject, old, new) @@ -68,10 +83,9 @@ def gen_ir(node): node.decl = [Decl(node.eval)] elif type(node) == TensorOp: - if node.op_type in op_mapping: + if node.op_type in op_mapping or node.op_type in cmp_op: node.operators[0]._gen_ir() node.operators[1]._gen_ir() - # TODO: add support for scalar + tensor assert isinstance(node.operators[0], Tensor) and isinstance(node.operators[1], Tensor) if is_same_size(node.operators[0]._size(), node.operators[1]._size()): if len(node._size()) > 0: @@ -91,14 +105,70 @@ def gen_ir(node): rhs = bind(rhs, pre_loop.iterate) res = bind(res, pre_loop.iterate) - op = op_mapping[node.op_type] + if node.op_type in op_mapping: + op = op_mapping[node.op_type] + else: + op = node.op_type assign = Assignment(res, Expr(lhs, rhs, op)) pre_loop.body.append(assign) else: node.eval = Scalar(node.dtype) node.decl = [Decl(node.eval)] - node.compute = [Assignment(node.eval, Expr(node.operators[0].eval, node.operators[1].eval, op_mapping[node.op_type]))] + if node.op_type in op_mapping: + op = op_mapping[node.op_type] + else: + op = node.op_type + node.compute = [Assignment(node.eval, Expr(node.operators[0].eval, node.operators[1].eval, op))] + else: + size = helpers.get_ir_of_size(node._size()) + node.eval = Ndarray(node.dtype, size) + node.decl = [Decl(node.eval)] + pre_loop = Loop(0, node.eval.size[0], 1, []) + node.compute = [pre_loop] + lhs = bind(node.operators[0].eval, pre_loop.iterate) + rhs = node.operators[1].eval + res = bind(node.eval, pre_loop.iterate) + for i in range(1, len(node.eval.size)): + loop = Loop(0, node.eval.size[i], 1, []) + pre_loop.body.append(loop) + pre_loop = loop + lhs = bind(lhs, pre_loop.iterate) + res = bind(res, pre_loop.iterate) + + if node.op_type in op_mapping: + op = op_mapping[node.op_type] + else: + op = node.op_type + assign = Assignment(res, Expr(lhs, rhs, op)) + pre_loop.body.append(assign) + + elif node.op_type in math_op: + node.operators[0]._gen_ir() + if len(node._size()) > 0: + size = helpers.get_ir_of_size(node._size()) + node.eval = Ndarray(node.dtype, size) + node.decl = [Decl(node.eval)] + pre_loop = Loop(0, node.eval.size[0], 1, []) + node.compute = [pre_loop] + val = bind(node.operators[0].eval, pre_loop.iterate) + res = bind(node.eval, pre_loop.iterate) + for i in range(1, len(node.eval.size)): + loop = Loop(0, node.eval.size[i], 1, []) + pre_loop.body.append(loop) + pre_loop = loop + val = bind(val, pre_loop.iterate) + res = bind(res, pre_loop.iterate) + + assign = Assignment(res, Math(val, node.op_type)) + pre_loop.body.append(assign) + + else: + node.eval = Scalar(node.dtype) + node.decl = [Decl(node.eval)] + node.compute = [Assignment(node.eval, Math(node.operators[0].eval, node.op_type))] + + elif node.op_type == 'einsum': node.operators[0]._gen_ir() @@ -206,43 +276,17 @@ def action(node, res): node.eval = Ndarray(ret.eval.dtype, size) node.decl.append(Decl(node.eval)) node.decl.extend(ret_decl) - - # node.eval <= ret.eval - res = bind(node.eval, outer_loop.iterate) - if (len(ret.eval.size) > 0): - pre_loop = Loop(0, ret.eval.size[0], 1, []) - outer_loop.body.append(pre_loop) - res = bind(res, pre_loop.iterate) - rhs = bind(ret.eval, pre_loop.iterate) - for i in range(1, len(ret.eval.size)): - loop = Loop(0, ret.eval.size[i], 1, []) - pre_loop.body.append(loop) - pre_loop = loop - res = bind(res, pre_loop.iterate) - rhs = bind(rhs, pre_loop.iterate) - pre_loop.body.append(Assignment(res, rhs)) - else: - assign = Assignment(res, ret.eval) - outer_loop.body.append(assign) - - # the above code generates two loops: the first loop calculates the results, the second loop stores the results to an output tensor - # TODO: the two loops might be generated together and save the fusion step - # TODO: the output is still stored in a temporary tensor after fusion. the temp tensor can be removed - scope = outer_loop.body - while len(scope) == 2: - fuse(scope, scope[0], scope[1]) - if type(scope[0]) is not Loop: - break - scope = scope[0].body - node.compute = [outer_loop] + res = bind(node.eval, outer_loop.iterate) + replace_output(node.compute, ret.eval, res) + node.decl = [d for d in node.decl if d.dobject != ret.eval] elif node.op_type == 'reduce': node.operators[0]._gen_ir() node.operators[2]._gen_ir() # init node.operators[3]._gen_ir() - axis = node.operators[3].eval + axis = node.operators[3].eval.val size = helpers.get_ir_of_size(node._size()) if len(size) > 0: @@ -252,6 +296,7 @@ def action(node, res): node.decl.append(Decl(node.eval)) node.compute = [] + # initialize output if len(node.eval.size) > 0: pre_loop = Loop(0, node.eval.size[0], 1, []) node.compute.append(pre_loop) @@ -268,19 +313,17 @@ def action(node, res): assign = Assignment(node.eval, node.operators[2].eval) node.compute.append(assign) - + # TODO: iterating over the reduction dimension in the outer loop may not give best performance + # TODO: it might be better to make it the innermost loop outer_loop = Loop(0, node.operators[0].eval.size[axis], 1, []) item1 = node.operators[4] item2 = node.operators[5] item1.eval = node.eval item2.eval = node.operators[0].eval - for i in range(axis): # TODO: is this correct? - item2.eval = bind(item2.eval, None) - if type(item2.eval) == Indexing and type(item2.eval.ind_arr) == Slice: - item2.eval = bind(item2.eval, outer_loop.iterate) - else: - item2.eval = bind(item2.eval, outer_loop.iterate, True) + for i in range(axis): + item2.eval = Indexing(item2.eval, Literal(-1, 'int')) + item2.eval = Indexing(item2.eval, outer_loop.iterate) item2.decl = [] item1.decl = [] @@ -288,41 +331,41 @@ def action(node, res): ret._gen_ir() def action(node, res): - if type(node) == TensorOp: - res.extend(node.compute) + if node.valid == True: + if type(node) == Var or type(node) == One or type(node) == Zero or type(node) == Ones or type(node) == Zeros or type(node) == Tensor: + res.extend(node.decl) + node.valid = False + elif type(node) == TensorOp: + res.extend(node.decl) + res.extend(node.compute) + node.valid = False t = helpers.Traversal(action) - ret_compute = t(ret) + ret_ir = t(ret) + ret_decl = [] + ret_compute = [] + + for ir in ret_ir: + if type(ir) == Decl: + ret_decl.append(ir) + else: + ret_compute.append(ir) node.operators.append(ret) outer_loop.body.extend(ret_compute) - node.decl.extend(ret.decl) - ret.valid = False + node.decl.extend(ret_decl) + node.compute.append(outer_loop) - if (len(ret.eval.size) > 0): - pre_loop = Loop(0, ret.eval.size[0], 1, []) - outer_loop.body.append(pre_loop) - res = bind(node.eval, pre_loop.iterate) - rhs = bind(ret.eval, pre_loop.iterate) - for i in range(1, len(ret.eval.size)): - loop = Loop(0, ret.eval.size[i], 1, []) - pre_loop.body.append(loop) - pre_loop = loop - res = bind(res, pre_loop.iterate) - rhs = bind(rhs, pre_loop.iterate) - pre_loop.body.append(Assignment(res, rhs)) - else: - assign = Assignment(node.eval, ret.eval) - outer_loop.body.append(assign) + replace_output(node.compute, ret.eval, node.eval) + node.decl = [d for d in node.decl if d.dobject != ret.eval] - node.compute.append(outer_loop) elif node.op_type == 'aggr': node.operators[0]._gen_ir() # input tensor node.operators[2]._gen_ir() # init node.operators[3]._gen_ir() # indices node.operators[4]._gen_ir() # axis - axis = node.operators[4].eval + axis = node.operators[4].eval.val size = helpers.get_ir_of_size(node._size()) node.eval = Ndarray(node.dtype, size) node.decl.append(Decl(node.eval)) @@ -346,12 +389,11 @@ def action(node, res): item1 = node.operators[6] item2 = node.operators[7] - item1.eval = Indexing(node.eval, ind_arr=node.operators[3].eval) - item1.eval = bind(item1.eval, outer_loop.iterate) + item1.eval = Indexing(node.eval, Indexing(node.operators[3].eval, outer_loop.iterate)) item2.eval = node.operators[0].eval for i in range(axis): - item2.eval = bind(item2.eval, None) - item2.eval = bind(item2.eval, outer_loop.iterate, True) + item2.eval = Indexing(item2.eval, Literal(-1, 'int')) + item2.eval = Indexing(item2.eval, outer_loop.iterate) item2.decl = [] item1.decl = [] @@ -359,37 +401,34 @@ def action(node, res): ret._gen_ir() def action(node, res): - if type(node) == TensorOp: - res.extend(node.compute) + if node.valid == True: + if type(node) == Var or type(node) == One or type(node) == Zero or type(node) == Ones or type(node) == Zeros or type(node) == Tensor: + res.extend(node.decl) + node.valid = False + elif type(node) == TensorOp: + res.extend(node.decl) + res.extend(node.compute) + node.valid = False t = helpers.Traversal(action) - ret_compute = t(ret) + ret_ir = t(ret) + ret_decl = [] + ret_compute = [] + + for ir in ret_ir: + if type(ir) == Decl: + ret_decl.append(ir) + else: + ret_compute.append(ir) node.operators.append(ret) outer_loop.body.extend(ret_compute) - node.decl.extend(ret.decl) - ret.valid = False - - if (len(ret.eval.size) > 0): - pre_loop = Loop(0, ret.eval.size[0], 1, []) - outer_loop.body.append(pre_loop) - res = bind(Indexing(node.eval, ind_arr=node.operators[3].eval), outer_loop.iterate) - res = bind(res, pre_loop.iterate) - rhs = bind(ret.eval, pre_loop.iterate) - for i in range(1, len(ret.eval.size)): - loop = Loop(0, ret.eval.size[i], 1, []) - pre_loop.body.append(loop) - pre_loop = loop - res = bind(res, pre_loop.iterate) - rhs = bind(rhs, pre_loop.iterate) - pre_loop.body.append(Assignment(res, rhs)) - else: - res = bind(Indexing(node.eval, ind_arr=node.operators[3].eval), outer_loop.iterate) - assign = Assignment(res, ret.eval) - outer_loop.body.append(assign) - + node.decl.extend(ret_decl) node.compute.append(outer_loop) + replace_output(node.compute, ret.eval, item1.eval) + node.decl = [d for d in node.decl if d.dobject != ret.eval] + # points from IR back to ASTNode for d in node.decl: d.astnode = node diff --git a/core/ir.py b/core/ir.py index 274089a..ee9999a 100644 --- a/core/ir.py +++ b/core/ir.py @@ -86,6 +86,11 @@ def name(self): return self.__name__ +class Math(IR): + def __init__(self, val, type): + self.val = val + self.type = type + class Indexing(DOject): def __init__(self, dobject, idx): assert dobject != None and type(dobject) in (Slice, Ndarray, Indexing) diff --git a/core/test/examples.py b/core/test/examples.py index 2302794..b4577f7 100644 --- a/core/test/examples.py +++ b/core/test/examples.py @@ -13,6 +13,7 @@ def func(): ast = func() code = codegen.cpu.print_cpp(ast._gen_ir()) + print(code) A = torch.rand(10, 10) B = torch.rand(10, 10) @@ -78,11 +79,6 @@ def test4(): d = run.cpu.compile_and_run(code, A, i, t) print(d, A[i] + t) -def f5(): - A = Tensor('a', (10, )) - t = Var('t', A.dtype) - - return A[0] + t def test6(): A = Tensor('a', (10, )) @@ -348,20 +344,65 @@ def test17(): d = run.cpu.compile_and_run(code, A, B) print(A[1:10][:, 2:4] + B[1:10][:, 2:4]) + print(d) print(torch.equal(A[1:10][:, 2:4] + B[1:10][:, 2:4], d)) +def test18(): + A = Tensor('A', (100, 20)) + B = Tensor('B', (100, ), dtype='int') + + ast = A[1:10][B[2:4]] + A[1:10][B[1:3]] + print(helpers.get_input_nodes(ast)) + ir = gen_ir(ast) + + code = codegen.cpu.print_cpp(ir) + A = torch.rand(100, 20) + B = torch.randint(0, 20, (100, )).to(torch.int32) + d = run.cpu.compile_and_run(code, A, B) + + print(A[1:10][:, B[2:4]] + A[1:10][:, B[1:3]]) + print(d) + print(torch.equal(A[1:10][:, B[2:4]] + A[1:10][:, B[1:3]], d)) + +def test19(): + nnodes = 100 + nedges = 300 + rowptr = Tensor('rowptr', (nnodes + 1, ), dtype='int') + colidx = Tensor('colidx', (nedges, ), dtype='int') + edge_list = Tensor('edge_list', (10, 2), dtype='int') + ast = colidx[rowptr[edge_list[0][0]]:rowptr[edge_list[0][1]]] + colidx[rowptr[edge_list[0][0]]:rowptr[edge_list[0][1]]] + + print(helpers.get_input_nodes(ast)) + code = codegen.cpu.print_cpp(gen_ir(ast)) + print(code) + + +def test20(): + A = Tensor('A', (100, ), dtype='float') + x = Var('x', dtype='float') + res = A + 10 + code = codegen.cpu.print_cpp(gen_ir(res)) + print(code) + + +def compression(): + input = Tensor('input', (50, 32), dtype='float') + res = (input * 1000).round() + res = res.apply(lambda x:x[1:32]-x[0:31], axis=0) + res = res.abs().max(axis=1) + code = codegen.cpu.print_cpp(gen_ir(res)) + print(code) + + def apply_test1(): - num_node = 10 num_edges = 20 - max_degree = 20 - rowptr = Tensor('rowptr', (num_node+1,), dtype='int') + length = 50 rowidx = Tensor('rowidx', (num_edges,), dtype='int') colidx = Tensor('colidx', (num_edges,), dtype='int') - edge_idx = Tensor('edge_idx', (num_edges,), dtype='int') + edge_idx = Tensor('edge_idx', (length,), dtype='int') - v0 = rowidx[0] def apply_func(edge_id): v0 = rowidx[edge_id] @@ -370,7 +411,28 @@ def apply_func(edge_id): res = edge_idx.apply(apply_func) code = codegen.cpu.print_cpp(gen_ir(res)) - print(code) + print(helpers.get_input_nodes(res)) + + edge_idx = torch.randint(0, num_edges, (length,)).to(torch.int32) + rowidx = torch.randint(0, 1000, (num_edges,)).to(torch.int32) + colidx = torch.randint(0, 1000, (num_edges,)).to(torch.int32) + + d = run.cpu.compile_and_run(code, edge_idx, rowidx, colidx) + + res = torch.zeros_like(edge_idx) + for i in range(len(edge_idx)): + e = edge_idx[i] + v0 = rowidx[e] + v1 = colidx[e] + 1 + res[i] = v0 + v1 + + print(d) + print(res) + print(torch.equal(d, res)) + + + + def apply_test2(): @@ -396,7 +458,6 @@ def apply_test3(): def apply_func(item): def apply_func2(item2): - print(type(item2)) return C[item2] + B[item2] return item.apply(apply_func2) @@ -419,8 +480,7 @@ def apply_test4(): def apply_func(item): def apply_func2(item2): - print(is_int_var(item2)) - return B[item2] + return B[item2] + 1 return item.apply(apply_func2) @@ -692,8 +752,34 @@ def test29(): if __name__ == "__main__": # test1() + # test2() + # test3() + # test4() + # test6() + # test7() + # test8() + # test9() + # test10() + # test11() + # test12() + # test13() + # test14() + # test15() + # test16() # test17() + # test18() + # test19() + # test20() + compression() + # apply_test1() + # apply_test2() + # apply_test3() + # apply_test4() + # test_aggr1() + # test27() + # test28() # spmv() # test_einsum1() - test_apply5() + # apply_test2() + # test_apply5() # test27() \ No newline at end of file diff --git a/opt/loop.py b/opt/loop.py index 5eaf746..5baf1f8 100644 --- a/opt/loop.py +++ b/opt/loop.py @@ -17,16 +17,15 @@ def rebind_iterate(ir, old, new): rebind_iterate(ir.rhs, old, new) elif type(ir) == Ndarray: rebind_iterate(ir.size, old, new) - elif type(ir) == Ref: + # elif type(ir) == Ref: + # rebind_iterate(ir.dobject, old, new) + elif type(ir) == Indexing: rebind_iterate(ir.dobject, old, new) - elif type(ir) == Index: - rebind_iterate(ir.dobject, old, new) - rebind_iterate(ir.ind_arr, old, new) - if type(ir.index) == Scalar: - if ir.index == old: - ir.index = new + if type(ir.idx) in (Scalar, Literal): + if ir.idx == old: + ir.idx = new else: - rebind_iterate(ir.index, old, new) + rebind_iterate(ir.idx, old, new) elif type(ir) == Slice: rebind_iterate(ir.start, old, new) rebind_iterate(ir.stop, old, new) diff --git a/test/examples.py b/test/examples.py index fda5a32..d5718fb 100644 --- a/test/examples.py +++ b/test/examples.py @@ -128,7 +128,17 @@ def f36(): return A.num_elem() + Set(T[40:]).num_elem() + Set(T[1:d]).num_elem() +def compression(): + input = Tensor('input', (1024, )) + input = input.reshape((32, 33)) + x = (input * 1000).round() + y = x.apply(lambda y:y[1:31]-y[0:30], axis=0) + res = y.encoding() + ir = gen_ir(res) + code = codegen.cpu.print_cpp(ir) + code = codegen.cerebra.print_code(ir) + print(code) def conv1d_v1(): @@ -149,4 +159,224 @@ def conv1d_v2(width): code = codegen.cpu.print_cpp(ir) print(code) +import numpy as np +from cset.ast2ir import * +def subgraph_matching_test_code(): + + pattern_size = 4 + + num_node = 10 + num_edges = 20 + + rowptr = Tensor('rowptr', (num_node+1,), dtype='int') + colidx = Tensor('colidx', (num_edges,), dtype='int') + edge_list = Set(Tensor('edge_list', (num_edges, 2), dtype='int')) + + count = Zero(dtype='int') + + class inner_subgraph_matching: + def __init__(self, level, *path): + self.level = level + self.path = list(*path) + + def __call__(self, item): + + if self.level == pattern_size-1: + return count+1 + + if self.level==1: + v0_nb = Set(colidx[rowptr[item[0]]:rowptr[item[0]+1]]) + v1_nb = Set(colidx[rowptr[item[1]]:rowptr[item[1]+1]]) + candidate_set = v0_nb.intersect(v1_nb) + return candidate_set.applyfunc(inner_subgraph_matching(self.level+1, [item[0], item[1]])) + else: + candidate_set = Set(colidx[rowptr[item]:rowptr[item+1]]) + candidate_set = candidate_set.filter(SmallerThan(self.path[-1])) + for v in self.path: + v_nb = Set(colidx[rowptr[v]:rowptr[v+1]]) + candidate_set = candidate_set.intersect(v_nb) + + return candidate_set.applyfunc(inner_subgraph_matching(self.level+1, self.path + [item])) + + res = edge_list.applyfunc(inner_subgraph_matching(1)) + code = codegen.cpu.print_cpp(res._gen_ir()) + print(code) + +def triangle_counting(): + np_rowptr = np.fromfile("../MiCo/snap.txt.vertex.bin", dtype=np.int64) + np_colidx = np.fromfile("../MiCo/snap.txt.edge.bin", dtype=np.int32) + + torch_rowptr = torch.from_numpy(np_rowptr, ).to(torch.int32) + torch_colidx =torch.from_numpy(np_colidx) + torch_edge_list = torch.zeros([torch_colidx.shape[0], 2], dtype=torch.int32) + + edge_idx = 0 + for i in range(0, torch_rowptr.shape[0]-1): + for j in range(torch_rowptr[i].item() , torch_rowptr[i+1].item()): + if(torch_colidx[j] Date: Sat, 4 Nov 2023 09:34:12 -0500 Subject: [PATCH 8/8] sync with dev --- __pycache__/helpers.cpython-310.pyc | Bin 2147 -> 2302 bytes batch/ast.py | 5 +- batch/ast2ir.py | 3 +- batch/opt/__init__.py | 6 +- .../opt/__pycache__/__init__.cpython-310.pyc | Bin 246 -> 328 bytes .../__pycache__/fusion_rules.cpython-310.pyc | Bin 9172 -> 14393 bytes .../__pycache__/parallelism.cpython-310.pyc | Bin 3065 -> 3075 bytes batch/opt/__pycache__/tiling.cpython-310.pyc | Bin 3101 -> 3090 bytes batch/opt/fusion_rules.py | 369 +++++++++++++++--- .../__pycache__/parallelism.cpython-310.pyc | Bin 0 -> 4131 bytes .../__pycache__/smem.cpython-310.pyc | Bin 0 -> 8189 bytes .../__pycache__/tiling.cpython-310.pyc | Bin 0 -> 4381 bytes batch/opt/{ => node_wise}/parallelism.py | 91 ++++- batch/opt/{ => node_wise}/smem.py | 80 +++- batch/opt/{ => node_wise}/tiling.py | 127 +++--- batch/opt/sort.cu | 81 ---- .../sort/__pycache__/mysort.cpython-310.pyc | Bin 0 -> 876 bytes .../opt/sort/__pycache__/sort.cpython-310.pyc | Bin 0 -> 860 bytes batch/opt/sort/build_indexing.py | 31 ++ batch/opt/sort/mysort.py | 18 + batch/opt/sort/sort.cu | 121 ++++++ batch/opt/sort/test | Bin 0 -> 734504 bytes batch/opt/sort/test.cu | 187 +++++++++ batch/test/kge.py | 31 +- codegen/cpu.py | 56 ++- codegen/gpu.py | 15 +- core/ast.py | 143 +++---- core/ast2ir.py | 242 +++++++----- core/ir.py | 10 +- core/test/examples.py | 100 ++++- helpers.py | 20 +- run/.tmp/cpu_code.cpp | 21 +- 32 files changed, 1289 insertions(+), 468 deletions(-) create mode 100644 batch/opt/node_wise/__pycache__/parallelism.cpython-310.pyc create mode 100644 batch/opt/node_wise/__pycache__/smem.cpython-310.pyc create mode 100644 batch/opt/node_wise/__pycache__/tiling.cpython-310.pyc rename batch/opt/{ => node_wise}/parallelism.py (67%) rename batch/opt/{ => node_wise}/smem.py (83%) rename batch/opt/{ => node_wise}/tiling.py (59%) delete mode 100644 batch/opt/sort.cu create mode 100644 batch/opt/sort/__pycache__/mysort.cpython-310.pyc create mode 100644 batch/opt/sort/__pycache__/sort.cpython-310.pyc create mode 100644 batch/opt/sort/build_indexing.py create mode 100644 batch/opt/sort/mysort.py create mode 100644 batch/opt/sort/sort.cu create mode 100755 batch/opt/sort/test create mode 100644 batch/opt/sort/test.cu diff --git a/__pycache__/helpers.cpython-310.pyc b/__pycache__/helpers.cpython-310.pyc index 20096fcfc8f4d07636add6018cf99884356d8afd..7b7d077fc187c464d2c27c5e95e4b7ee132417ca 100644 GIT binary patch delta 966 zcmZ{iL2DC16vy|?&g{;vo6RO^lUhs#jY3*Mr3XbSC<%ff7U{tnu(gmyx~*+9i9M7J zd+;V63Ul(NCl7k4AHbV}SFa0#;L(#e5h}jfbknqDcX*F^|M%wi-YoOe`&V!`b2(eX z??+?KuiGEpMaXju4YUSYw@u`8xE_gGsJ$^l2=%uFw6?aO^=Jq$pl7ZTaX*BBw+Zg< zVU>lE9h?dY*6N@Q73YMKOBKZ%VY*1#MoPY^J+}G`36T}Dw7BySQ{FH&nrUYYSwP_X zBf!y9Q6}#dW{Rp)xo;%*For|CR;}c3G9HzJF~omL@(_%t6^Hi;X;2EvSQDqX6SS`x zgIDm>{sDO+B_ri@a(z=}qR>neonIN4$u+VI;z|a{?kAu_&MtKF2cz1@N5x$ANEf-N zH|0CDKYC*-)))L<%k1_x#mZB^#WwtP|5?mXoZqT*d6U+n7&e57FKgh`TN5R8mU?VU z=s4Iwfuq^PAs#pw7jcz=?mN^54`UCP(g*uN?7`SbF0FDCt(Y)8pnrGd7y6{48rG>$ zk6cuht0{LueltsQo>_21K4K3HRVAk79=ilr<#+Z0Cge-w%+&d$R=H!>V5r@epN$4o zB{ydZw=nLi9vnDxK+MXfxmCT4LQMVbmG!zF)9&hy-!fOlvf7A?IKjEfU3Jbur+$N* Z7zJ@n+Wf3Bi!yHaSVi99llq)v{RdDu&r1LR delta 780 zcmZ{izi-n(6vzF}cjvR~q)s7i2?Ht$v{4ue$`Aw#YE*;}0>n}kkYy1I;f5rfi^O2m zfgOf7u!4Y%rGEeeVuP8*3rH-85wRBW&TY~nh)?h7{qpDgOOTG0B z60gfBTBE5c%KW61F`Bi(8du8yRHjE2!z-9uyW#d=kR|!*@We*#QSiNw@TaZ1D{U>zNXG>Atj3?x3Bi&oUg}vfb+cLb4t@(m^{Usyu%s}zT@?a+T7}{ zQzojCbIk>>l2hOT2Sw(wIt#$3e1^^}@SQsMAfP;;;|pwx8sj?{il}7}4X(|vq8E%i zY22c5e~aZR4DUp}UKpOiU*ftnFX|>^_plU&lvlu!nuz;_eW~EJeYPM0Jgb%ufUz{N<<27{@mawOe!v%b%s?f$a>X>X9 NY#TqS({{^u{{X=%urmMv diff --git a/batch/ast.py b/batch/ast.py index dd5fcc9..c358a68 100644 --- a/batch/ast.py +++ b/batch/ast.py @@ -80,7 +80,7 @@ def bov(v1: Batch, v2: Batch): return BatchOp('vec_outer_vec', v1, v2) class BatchOp(Batch): - Types = ['scal_mul_vec', 'vec_mul_vec', 'vec_mul_mat', 'vec_outer_vec'] + list(core.ast.op_mapping.keys()) + Types = ['scal_mul_vec', 'vec_mul_vec', 'vec_mul_mat', 'vec_outer_vec'] + list(core.ast.arith_op.keys()) def __init__(self, op_type, *operators): assert op_type in BatchOp.Types @@ -107,7 +107,7 @@ def __init__(self, op_type, *operators): name = f'{op_type}_' + '_'.join([op.name if hasattr(op, 'name') else '' for op in self.operators]) - if op_type in core.ast.op_mapping: + if op_type in core.ast.arith_op: match op_type: case 'add': res = self.operators[0].base + self.operators[1].base @@ -147,4 +147,3 @@ def __init__(self, op_type, *operators): pass self.op_type = op_type - diff --git a/batch/ast2ir.py b/batch/ast2ir.py index a2c0be5..dde619d 100644 --- a/batch/ast2ir.py +++ b/batch/ast2ir.py @@ -21,7 +21,7 @@ def gen_ir(node): node.base._gen_ir() node.eval = node.base.eval elif type(node) == BatchOp: - if node.op_type in core.ast.op_mapping: + if node.op_type in core.ast.arith_op: node.operators[0]._gen_ir() node.operators[1]._gen_ir() node.base._gen_ir() @@ -128,4 +128,3 @@ def gen_ir(node): loop2.body.append(assign) return node - diff --git a/batch/opt/__init__.py b/batch/opt/__init__.py index 0c77ff1..5be731f 100644 --- a/batch/opt/__init__.py +++ b/batch/opt/__init__.py @@ -1,5 +1,5 @@ import batch.opt.fusion_rules import batch.opt.ir -import batch.opt.parallelism -import batch.opt.smem -import batch.opt.tiling \ No newline at end of file +import batch.opt.node_wise.parallelism +import batch.opt.node_wise.smem +import batch.opt.node_wise.tiling \ No newline at end of file diff --git a/batch/opt/__pycache__/__init__.cpython-310.pyc b/batch/opt/__pycache__/__init__.cpython-310.pyc index 7c401017ed44dba74da816a742c585b3f4ca9680..bc0ac54d876d6be7fd45e602363e5f47caf73246 100644 GIT binary patch literal 328 zcmd1j<>g`k0>wMlsk4CeV-N=!FabFZKwPW?BvKes7;_k+7{N3XlxBv~EKr&iNHYdA zXfnM7>R|BGWQ!6@N-Rmv(916<(Mu~W&dkq?FDlJRExyGH7Qe-Vkj^ZMl1K3J@>5dd z%QK5p^$HS;5_57=b25u_qa?5?D9%mIjgrD9Q<9mJnU}tjp@;?O8Zhz8R6ixLBvC&p zF*!RmCBC#kKPNLIqg3BH)Z0}*ximW!CpO=@50SIPn&`m9x$g7?m!jQs{!kEJdrJ0~KGn8fl(oDe&n#?a5fx0vq zZ!v25X|mqpNh>YR%+HH2D$PkPzQtXTSd^HPlbVxRoO_EYvnYzCI5#!-7F$VXPG(;E PN`@kKpxz?(i66rOzIY}3 diff --git a/batch/opt/__pycache__/fusion_rules.cpython-310.pyc b/batch/opt/__pycache__/fusion_rules.cpython-310.pyc index 9722f62d1d6f5c3db6bf0bb698c50660f9152a79..a6cea75d8ea81c3454045d192459085113276750 100644 GIT binary patch literal 14393 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 GJp8{CHB24= delta 3375 zcmbtWU2Gdg5Wc-T`_31~aS}VW>$rCRoH|X?rln35S}KtyP%2dgN(H4Mn4F2?CjMDp z3boEki=`q}MI_xvev|@&2Y8?_m5@NHcmV-I0Tl@ugy^tcp5{2ssZ>o)r=e{|+)3 z$&D>jXqoKa`qI3kF6(YkUN(ZJtcx|V>Ndq(mo9>%uBDJJn(iwi5_HAZp9llxUy9g# z&VL0VA%6>akFK5;A*eT;H7k@AG7Y#(rULO$f?Vp0N~k1~J6ndk7EpPQV=L=2OVPYf zxL|2MV8D;CfIVjr_hTsZJ2#tDxj20kG1{!-gv_M#~e#~P1)iLC;N_gA- zVg>Po#hP#6D&bK8rj16w3RhNYj;n-Ef|B2fj;WGe-cSrMYQnXBmB!IjXhg=3f>hQ! zpfsH3CqTyiOq(4>dN^UVA<=;AgLO8u_LoW@@b;I=KRJfHOfv2!3xhAvefZ(#kb-z! zWr7H7NXdoX_&h`jIdwYTnhIkGObVr`Ap=3oE=;S!v*Wa#-91~=>R=f+Bs8mkVBMZY zc9Up_gmJZs$a1npl1msm;^ClNe&6w9Cwq&0I`9lB?D6pHiQ;4@d~00^gN2>2%Gj-!dcBovXub_k*=1sAxm{L`p z5~?2fG$U#%Mt6sROu8^j2eL#5WwDAMc@FQ63_yv3B{lf3TO_H zKX-?M?($PCdyX~D-i^=GHi#;w%am(URk0qQCTvM!XLM2p6y5kEKr!TaXliJurW|iK z{@iJO22lfV(6n{vRZbr`%MUr03XZ5a2@+Y4KzTUr(Kj$!T(JjTZAdiW=qtiz2DrO) z4**|*nnsaxP*d^efYibvz^u{{dMJc^YzV8bKmGYNP(fMIf@LhUX z+SmL-xx70#zX5tg#qEGeuRr@0h*q z75X8J8zR~WUxNajzhTE%EuCODp;~_3G2}U^_?mSSYF1v&&3|!E`~XkvmvtoeH|T71 z{IkT!V7T*C{3w>-MriUzs!(wHa1@>b&7Ol6^NDIr<=O~M!Kh{l-#txC*&3>5EGiM_5AiWA1!Vzw?%7H}5#f{(feve`_rIGvxW%crwv%XQDRk)7pw z%4A`-2pf~-hlYD`g2w5hLl?DlL0zZyx;y)-k zgAayO3)>SXfrgD)Ml*#%u`aU_WlkzZoSn#Ed{SdXxja3UC+6O*u^})i!B-YVLGX$l z$j3hMoGfk^$rpROy?&&j0P>0fB%UKX)7_p9TR`4y50cJw*u^BIH@(TCo=6W`R3)9V ssQ1!EG(tLu&bsNADUDnn8bw3o?ocvJ^H2p9v42Sos{u8nc6tN<0%qK48UO$Q diff --git a/batch/opt/__pycache__/parallelism.cpython-310.pyc b/batch/opt/__pycache__/parallelism.cpython-310.pyc index efa394681dc1c1638e2f89ed7b1e281cd5786dbe..3823759ec27173dd386dfad26ccca7717370a3d5 100644 GIT binary patch delta 698 zcmZ8eO=}ZD7~Ywk{g};%P5O~oQ5%#7+e0pbpdg67=|$tIXv5l>+H8~E&1BLRoNa|Y z_yg9HAf%qXNdJNO0|Y^3MeuKU^PSZwy2CvC&NI)v^E}LtM%t`Im5OJ;w|x5sUw>Pv z5%ey*NP=TaIw=-MPmBahR~Z3K(T4Hp`a_(U2~l|JDRP34A#Fuu36zJj3@R(Aq$sg; zmDqsQsVC1UD@BENP7!6NIB{f6SHPRd(7j}RrYTnQ$~smK!>Or{jczKej2>l1S6^oY znBlWrflIol|K4;-nG7`y*+`8O46gq&O@$!ezg&IBjoyr2jWTUL`(Q;Yl!ucc7rx}V zvmFiN2^6*;$2?@B3a^m8v{&1u?(Sq~XTUe2gD@|5{e6B*KG+r9XQBq0-0{aUj9A9( z*5fOs+=QjL1RI;BkRSABZOuMA%t>GJVUGOVi3RWZLWpx96id1d3O4D`?}xoX6vd*U z`2w1{Y5{a$j_+n2XAzgpY|B}`a2a{X!+N@S4%J~Sz_6c@@9sQ7r^*?i)+(IJt$lyc zXM*TfI+s|}(AKb?QTn2NRhQQQa4Y4*`OLeI1NrPo%+ IJzp;W1d~;=VgLXD delta 657 zcmZ8d&ui2`6waGWekbehcH3>0VoR-{h!%>M-h?XXL4@k1w1i-$?Z(|?cRJgOOl&1k zsCtVRZ(Hvkq&@YI@Zv=x;@{!Tm(;2_!#uwE-uK@3X0pkjZ7=aWTfz0Jwa8{Zc?}Au z`DGgIk&sL&ktU3cY`+z{R3Z|>Uk2~yva)_RC72_^$_RfWNn|6pl&Z+7757@IkrSCI z6|STiVD|bNNwri*g|Tf>cwE?Z7^<3bPmeHMrLrhfH_yAtfMJL2&cXF+()DO7C0 z=gNK)Wj}~ktzOWH{ce)<3$+tIFK9=w-l`L`UH`X#0YPE(Ir9U~`6VpNr;VwF2MAnf zLajPvpAuWu$raTGo0P{Sb<8xOg&Kt18o-PEyZsUl@-^p!cGH}8-CsN@r-}do diff --git a/batch/opt/__pycache__/tiling.cpython-310.pyc b/batch/opt/__pycache__/tiling.cpython-310.pyc index fef06f17f226724daceddfc23c9ab5933412288e..528ffce475a8d0860e40d5290c9d4c8147d1a2b1 100644 GIT binary patch delta 604 zcmY*WJ&P1U5UuL&ndzO69To9*1VKCl<=8+(HaIu)>p~GVI!>6}8XAZOCf3Z}9N5rBb-jA8Usu2G?e>yKNfH9DFRQDYuiho! zrNmgeKk@^vynm}sBA$1jy}{ET0-ywf5-YKTMNx`U66C7F3NOJ9uHUm*ydCD{j1p?MF$o{u}3;rV_-8c-Kk(1-?%HmFg4_pnniPX%#SIqyWL=I?Gwee+o&q? zGgv6E5BZ@uQWT85F;vQ?)Xmi_q;!r5gd=YWcr^QrNv+m*r}Z+TyQWVBhbgsP^V4nV zbGI*$&*Mrr6m^&%*L-8jm?Ewx>@umOjcT`yMhmp{&Z?7!rJGwjB*@EPLz$ZqHvqx6CR!W7T&mghLN-4tGqici-y|cLeYe* l&ITMD?`d6%@d$RCEuJY6i%^BA|0?CSqZh#;t#ugR`UkSVjN$+Q delta 575 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 AUH||9 literal 0 HcmV?d00001 diff --git a/batch/opt/node_wise/__pycache__/smem.cpython-310.pyc b/batch/opt/node_wise/__pycache__/smem.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26ad3f9483cee4bddf5567b5374ca4a2140ef110 GIT binary patch literal 8189 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 BMm+!k literal 0 HcmV?d00001 diff --git a/batch/opt/node_wise/__pycache__/tiling.cpython-310.pyc b/batch/opt/node_wise/__pycache__/tiling.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6193db44986855cdea76d8dc1bf1f10a8e29116 GIT binary patch literal 4381 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 literal 0 HcmV?d00001 diff --git a/batch/opt/parallelism.py b/batch/opt/node_wise/parallelism.py similarity index 67% rename from batch/opt/parallelism.py rename to batch/opt/node_wise/parallelism.py index b932d34..3df97a4 100644 --- a/batch/opt/parallelism.py +++ b/batch/opt/node_wise/parallelism.py @@ -44,6 +44,49 @@ def find_arr_ind(ir, pre): if t: return t +def if_contain(item, ir): + if isinstance(item, Loop): + flag = False + for i in item.body: + if isinstance(i, Loop): + flag = if_contain(i, ir) + elif i == ir: + flag = True + if flag: + return True + + return False + +def add_thready(ir, arr): + if isinstance(ir, Indexing): + temp = ir + idx_list = [] + while isinstance(temp, Indexing): + idx_list.append(temp.idx) + temp = temp.dobject + if temp == arr: + idx_list = idx_list[::-1] + temp = Indexing(temp, Literal(-1, 'int')) + temp.idx = ThreadIdy() + for i in idx_list: + if isinstance(i, (Scalar, Literal, Indexing)): + temp = Indexing(temp, i) + else: + temp = Indexing(temp, Literal(-1, 'int')) + temp.idx = i + ir = temp + # print('yes', codegen.gpu.to_string(temp)) + elif isinstance(ir, Assignment): + ir.lhs = add_thready(ir.lhs, arr) + ir.rhs = add_thready(ir.rhs, arr) + elif isinstance(ir, Expr): + ir.left = add_thready(ir.left, arr) + ir.right = add_thready(ir.right, arr) + elif isinstance(ir, Loop): + for i in range(len(ir.body)): + ir.body[i] = add_thready(ir.body[i], arr) + return ir + def add_reduction(ast): if type(ast) == BatchOp: if type(ast.operators[1]) == BatchOp: @@ -58,22 +101,8 @@ def add_reduction(ast): # this inner_prod node is fused with upper layer eval = ast.eval # print(codegen.cpu.to_string(eval), codegen.cpu.to_string(ast.operators[0].eval), codegen.cpu.to_string(ast.operators[1].eval)) + # print(codegen.gpu.to_string(eval), ast.eval, ast.operators[0].eval, ast.operators[1].eval) # iff eval is scalar, we need to add shfl_sync - if not (isinstance(ast.operators[0].eval, Ndarray) or isinstance(ast.operators[1].eval, Ndarray)): - for i in ast.compute: - # search all compute stmts - if isinstance(i, Loop): - for j in i.body: - # search loop body - if isinstance(j, Loop): - # find the stmt of ast node - main_loop = j.astnode.compute - for idx, item in enumerate(main_loop): - if isinstance(item, Loop) and item == j: - main_loop.insert(idx+1, SyncThreads()) - main_loop.insert(idx+1, BroadCast(eval)) - main_loop.insert(idx+1, ShuffleDown(eval)) - if isinstance(ast.eval, Ndarray): new_compute = [] for idx, item in enumerate(ast.compute): @@ -88,6 +117,38 @@ def add_reduction(ast): new_compute.append(ShuffleDown(a)) new_compute.append(SaveAtThread(a, t, 0)) ast.compute = new_compute + elif not ((isinstance(ast.operators[0].eval, Ndarray) or isinstance(ast.operators[1].eval, Ndarray))): + for i in ast.compute: + # search all compute stmts + if isinstance(i, Loop): + for j in i.body: + # search loop body + if isinstance(j, Loop): + # find the stmt of ast node + main_loop = j.astnode.compute + for idx, item in enumerate(main_loop): + + if isinstance(item, Loop) and item == j: + # fused operators + main_loop.insert(idx+1, SyncThreads()) + main_loop.insert(idx+1, BroadCast(eval)) + main_loop.insert(idx+1, ShuffleDown(eval)) + elif isinstance(item, Loop): + # before fuse operators + if if_contain(item, j.body[0]): + main_loop.insert(idx+1, SyncThreads()) + main_loop.insert(idx+1, BroadCast(eval)) + main_loop.insert(idx+1, ShuffleDown(eval)) + if ast.op_type == 'vec_mul_mat': + # print(ast.eval, codegen.gpu.to_string(ast.eval), ast.eval.size) + ast.eval.size.insert(0, Scalar('int', 'C')) + # print(ast.compute[0].astnode.compute) + for i in ast.compute[0].astnode.compute: + # print(codegen.gpu.to_string(i)) + t = add_thready(i, ast.eval) + # print(t, codegen.gpu.to_string(t)) + + def cuda_spec(ast): diff --git a/batch/opt/smem.py b/batch/opt/node_wise/smem.py similarity index 83% rename from batch/opt/smem.py rename to batch/opt/node_wise/smem.py index b3c30fd..dd2a659 100644 --- a/batch/opt/smem.py +++ b/batch/opt/node_wise/smem.py @@ -39,16 +39,6 @@ def get_ori_var(ir): elif isinstance(ir, Ndarray): return ir -def change_expridx(ir, var): - # if is expr, change right of expr as loop.iterate - if isinstance(ir, Indexing): - temp = ir - temp.idx = change_expridx(temp.idx, var) - temp = temp.dobject - elif isinstance(ir, Expr): - if ir.right != var and ir.op == '+': - ir.right = var - return ir def if_exist(ir, arr): if isinstance(ir, Indexing): @@ -174,17 +164,18 @@ def data_loading(compute_ir, smem_arr, ori_arr): stmt.body.insert(0, store_loop) elif len(smem_arr[i].size) == 3: temp_stmt = stmt - + # print(codegen.gpu.to_string(temp_stmt)) for iloop in stmt.body: if isinstance(iloop, Loop) and isinstance(iloop.body[0], Loop): - if iloop.start == 0 and iloop.end.name() == 'dim' and isinstance(iloop.body[0].start, ThreadIdx) and iloop.body[0].end.name() == 'D': + if iloop.start == 0 and iloop.end.name() == 'dim' and iloop.body[0].end.name() == 'D': temp_stmt = iloop break + # print(codegen.gpu.to_string(temp_stmt)) for shared_item in cur_arr: flag = if_exist(temp_stmt, shared_item[0]) # print(codegen.gpu.to_string(temp_stmt), codegen.gpu.to_string(shared_item[0])) if flag: - oloop = Loop(0, 2, 1, []) + oloop = Loop(Literal(0, 'int'), Literal(2, 'int'), Literal(1, 'int'), []) store_loop1 = Loop(ThreadIdy(), stmt.step, BlockDimy(), []) store_loop2 = Loop(ThreadIdx(), stmt.step, BlockDimx(), []) oloop.body.append(store_loop1) @@ -322,11 +313,12 @@ def add_smem(ast): smema = Ndarray('float', size, f'smem_{i.dobject_id}') smem_list.append(smema) ast.decl.append(Decl(Shared(smema))) - + decl = data_loading(ast.compute, smem_list, var_list) ast.decl.extend(decl) if type(ast) == BatchOp and not ast.valid and isinstance(ast.eval, Ndarray): + # change shared memory access node = ast.compute[0].astnode ir_dict = {} smem_list = [] @@ -352,14 +344,68 @@ def add_smem(ast): temp = temp.dobject idx_list = idx_list[::-1] newsmem = smem_list[i] + for kk in range(len(idx_list)): newsmem = Indexing(newsmem, Literal(-1, 'int')) - if isinstance(idx_list[kk], Scalar): - newsmem.idx = ThreadIdy() + if isinstance(idx_list[kk], (Scalar, ThreadIdy)): + newsmem.idx = idx_list[kk] elif isinstance(idx_list[kk], Expr): newsmem.idx = idx_list[kk].right smem_idx_list.append(newsmem) for j in node.compute: for idx in range(len(smem_idx_list)): j = change_access(j, ir_dict[key][idx], smem_idx_list[idx]) - \ No newline at end of file + + if type(ast) == BatchOp and not ast.valid and ast.op_type=='vec_mul_mat': + # print(ast.operators[0], ast.operators[1]) + # print(ast.operators[0].eval, codegen.gpu.to_string(ast.operators[0].eval)) + # print(ast.operators[0].op_type, ast.operators[1].op_type) + # print(ast.compute[0].astnode) + main_loop = ast.compute[0].astnode.compute + smem_dict = {} + for i in main_loop: + # print(codegen.gpu.to_string(i)) + if if_in_ir(i, ast.operators[0].eval, smem_dict): + temp_stmt = i + # find target loop + for iloop in i.body: + if isinstance(iloop, Loop) and isinstance(iloop.body[0], Loop): + if iloop.start == 0 and iloop.end.name() == 'dim' and iloop.body[0].end.name() == 'D': + temp_stmt = iloop + break + # print('eval in this stmt:', temp_stmt, codegen.gpu.to_string(temp_stmt)) + newsmem = ast.operators[0].eval + arr = list(smem_dict.keys())[0] + idx = smem_dict[arr][0] + iter_list = [] + tidx = idx + while isinstance(tidx,Indexing): + iter_list.append(tidx.idx) + tidx = tidx.dobject + # print(arr, codegen.gpu.to_string(arr), idx, codegen.gpu.to_string(idx)) + store_loop = Loop(ThreadIdx(), Scalar('int', 'D'), BlockDimx(), []) + + temp_stmt.body.insert(0, SyncThreads()) + temp_stmt.body.insert(0, store_loop) + smema = Ndarray('float', [Scalar('int', 'C'), Scalar('int', 'D')], f'smem_{ast.operators[0].eval.dobject_id}') + node.decl.append(Decl(Shared(smema))) + + new_access = Indexing(smema, Literal(-1, 'int')) + new_access.idx = ThreadIdy() + new_access = Indexing(new_access, iter_list[0].right) + change_access(temp_stmt, idx, new_access) + + smema = Indexing(smema, Literal(-1, 'int')) + smema.idx = ThreadIdy() + smema = Indexing(smema, store_loop.iterate) + + global_arr = arr + for i in iter_list[::-1]: + global_arr = Indexing(global_arr, Literal(-1, 'int')) + if isinstance(i, Expr): + global_arr.idx = Expr(i.left, i.right, i.op) + else: + global_arr.idx = i + global_arr.idx.right = store_loop.iterate + assign = Assignment(smema, global_arr) + store_loop.body.append(assign) diff --git a/batch/opt/tiling.py b/batch/opt/node_wise/tiling.py similarity index 59% rename from batch/opt/tiling.py rename to batch/opt/node_wise/tiling.py index 688de65..841290d 100644 --- a/batch/opt/tiling.py +++ b/batch/opt/node_wise/tiling.py @@ -24,6 +24,26 @@ def swap_arr_to_reg(ir, pre, cur): ir.body[i] = swap_arr_to_reg(ir.body[i], pre, cur) return ir +def swap_reg_to_arr(ir, pre, cur, iloop): + if isinstance(ir, Scalar): + if ir == pre and iloop.end.name()=='D': + ir = Indexing(cur, iloop.iterate) + elif isinstance(ir, Expr): + ir.left = swap_reg_to_arr(ir.left, pre, cur, iloop) + ir.right = swap_reg_to_arr(ir.right, pre, cur, iloop) + elif isinstance(ir, Assignment): + ir.lhs = swap_reg_to_arr(ir.lhs, pre, cur, iloop) + ir.rhs = swap_reg_to_arr(ir.rhs, pre, cur, iloop) + elif isinstance(ir, Loop): + + for i in range(len(ir.body)): + if ir.end.name() == 'D' and isinstance(ir.body[i], Loop) and ir.body[i].end.name() == 'D': + for j in range(len(ir.body[i].body)): + ir.body[i].body[j] = swap_reg_to_arr(ir.body[i].body[j], pre, cur, ir) + else: + ir.body[i] = swap_reg_to_arr(ir.body[i], pre, cur, ir) + return ir + def tile_wD(ir): if isinstance(ir, Loop): if ir.end.name() == 'dim': @@ -32,8 +52,9 @@ def tile_wD(ir): ir.step = scalar_D new_loop = Loop(0, scalar_D, 1,[]) for i in range(len(tbody)): - # tile_wD(tbody[i]) tbody[i] = swap_arr_to_reg(tbody[i], ir.iterate, new_loop.iterate) + # if isinstance(tbody[i], Assignment) and isinstance(tbody[i].rhs, Literal): + # print(codegen.gpu.to_string(tbody[i])) new_loop.body.extend(tbody) # iloops.append(new_loop) ir.body = [new_loop] @@ -58,36 +79,37 @@ def tile_loops(ir, tile_list): return ir def swap_loops_tile(ir): + decl = [] + del_decl = [] + replace_pair = [] if isinstance(ir, Loop): - # print('oloop and iloop:', codegen.gpu.to_string(oloop), codegen.gpu.to_string(iloop)) - temp = ir - - # while isinstance(temp, Loop) and temp.end.name() != 'dim': - - # temp = temp.body[0] + # print(ir, codegen.gpu.to_string(ir)) for i in range(len(ir.body)): # print(ir.body[i], codegen.gpu.to_string(ir.body[i])) if isinstance(ir.body[i], Loop) and ir.body[i].end.name() != 'dim': # print(ir.body[i], codegen.gpu.to_string(ir.body[i])) - ir.body[i] = swap_loops_tile(ir.body[i]) + ir.body[i], temp_decl, temp_del = swap_loops_tile(ir.body[i]) + decl.extend(temp_decl) + del_decl.extend(temp_del) elif isinstance(ir.body[i], Loop) and ir.body[i].end.name() == 'dim': # print(ir.body[i], codegen.gpu.to_string(ir.body[i])) tile_list = [] - temp = ir.body[i] - tile_loops(temp, tile_list) - # print(temp, tile_list, codegen.gpu.to_string(temp), codegen.gpu.to_string(tile_list[-1])) - # print(codegen.gpu.to_string(tile_list[0])) - # print(tile_list) + tile_loops(ir.body[i], tile_list) multi_lv_loop = {} + + + # print(tile_list) for lidx in range(len(tile_list)): t = tile_list[lidx] + # print(t, codegen.gpu.to_string(t)) # add tiled loop here scalar_D = Scalar('int', 'D') new_loop = Loop(0, scalar_D, 1,[]) temp_body = [] for k in range(len(t.body)): item = t.body[k] + # print(item, codegen.gpu.to_string(item)) if isinstance(item, Loop) and item.end.name() == 'dim': if new_loop.body != []: # add new_tiled loop and create a new one @@ -113,53 +135,64 @@ def swap_loops_tile(ir): loop1 = tloop for jj in range(len(t.body)): t.body[jj] = swap_arr_to_reg(t.body[jj], t.iterate, tloop.iterate) - # print('...............', oloop, codegen.gpu.to_string(tloop), codegen.gpu.to_string(t)) tloop.body = t.body temp_body.append(oloop) else: # add tiled loop here + # print(codegen.gpu.to_string(item), item.lhs, item.rhs) + if isinstance(item, Assignment) and isinstance(item.rhs, Literal): + new_lhs = Ndarray(item.lhs.dtype, [scalar_D]) + decl.append(Decl(new_lhs)) + del_decl.append(item.lhs) + replace_pair.append([new_lhs, item.lhs]) + item = swap_arr_to_reg(item, t.iterate, new_loop.iterate) new_loop.body.append(item) if new_loop.body != []: temp_body.append(new_loop) tile_list[lidx].body = temp_body - # while isinstance(temp, Loop) and temp.end.name() == 'dim': - # scalar_D = Scalar('int', 'D') - # temp.step = scalar_D - # new_loop = Loop(0, scalar_D, 1,[]) - # tile_list.append([new_loop, temp.iterate]) - # if not isinstance(temp.body[0], Loop): - # oloop = temp - - # new_loop2 = Loop(0, scalar_D, 1,[]) - # if len(temp.body) > 1: - # for oidx in range(1, len(temp.body)): - # swap_arr_to_reg(temp.body[oidx], temp.iterate, new_loop2.iterate) - # new_loop2.body.append(temp.body[oidx]) - # tbody = [temp.body[0], new_loop2] - # temp.body = tbody - # temp = temp.body[0] - # # temp is assign, oloop is outloop of it - # print('tile list len:', tile_list) - # for i in range(len(tile_list)): - # oloop.body = [tile_list[i][0]] - # temp = swap_arr_to_reg(temp, tile_list[i][1], tile_list[i][0].iterate) - # oloop = oloop.body[0] - # oloop.body = [temp] - return ir + for i in range(len(ir.body)): + if replace_pair: + for jj in replace_pair: + # print(codegen.gpu.to_string(jj[0]), codegen.gpu.to_string(jj[1]), codegen.gpu.to_string(ir.body[i])) + if isinstance(ir.body[i], Loop): + for temp in ir.body[i].body: + temp = swap_reg_to_arr(temp, jj[1], jj[0], ir.body[i]) + else: + ir.body[i] = swap_reg_to_arr(ir.body[i], jj[1], jj[0], ir) + # print(ir.body[i], codegen.gpu.to_string(ir.body[i])) + return ir, decl, del_decl + +def tile_loop(ast, eval_list=[]): + + + if ast.compute and ast.valid: + # if ast.compute: + # print(ast.compute[0], codegen.gpu.to_string(ast.compute[0])) + # print(codegen.gpu.to_string(ast.eval), codegen.gpu.to_string(ast.operators[0].eval), codegen.gpu.to_string(ast.operators[1].eval), ast.operators[0].eval) + # print(ast.eval, codegen.gpu.to_string(ast.eval)) + for i in ast.compute: + # recursive_tile(i) + body, decl, del_decl = swap_loops_tile(i) + for i in range(len(decl)): + # print(decl[i], del_decl[i], codegen.gpu.to_string(decl[i]), codegen.gpu.to_string(del_decl[i])) + eval_list.append([del_decl[i], decl[i].dobject]) + ast.decl.extend(decl) + for dd in ast.decl: + if dd.dobject in del_decl: + ast.decl.remove(dd) -def tile_loop(ast): if type(ast) == BatchOp: if type(ast.operators[1]) == BatchOp: - tile_loop(ast.operators[1]) + tile_loop(ast.operators[1], eval_list) if type(ast.operators[0]) == BatchOp: - tile_loop(ast.operators[0]) + tile_loop(ast.operators[0], eval_list) else: return - if ast.compute and ast.valid: - # print(ast.compute[0], codegen.gpu.to_string(ast.compute[0])) - for i in ast.compute: - # recursive_tile(i) - t = swap_loops_tile(i) - # print(codegen.gpu.to_string(t)) \ No newline at end of file + if not ast.valid: + # print(ast.op_type, eval_list, ast.eval, ast.decl) + for id, item in enumerate(eval_list): + if item[0] == ast.eval: + ast.eval = item[1] + eval_list.pop(id) \ No newline at end of file diff --git a/batch/opt/sort.cu b/batch/opt/sort.cu deleted file mode 100644 index bc064f0..0000000 --- a/batch/opt/sort.cu +++ /dev/null @@ -1,81 +0,0 @@ -__device__ void swap(long& a, long& b, long& a_idx, long& b_idx) { - int tmp = a; - a = b; - b = tmp; - tmp = a_idx; - a_idx = b_idx; - b_idx = tmp; -} - -__device__ void bitonic_sort(long* arr, long* ord) { - __shared__ long shared_arr[C]; - __shared__ long shared_ord[C]; - - int tid = threadIdx.x; - shared_arr[tid] = arr[tid]; - shared_ord[tid] = tid; - __syncthreads(); - - for (int k = 2; k <= C; k <<= 1) { - for (int j = k >> 1; j > 0; j >>= 1) { - __syncthreads(); - int ixj = tid ^ j; - if (ixj > tid) { - if ((tid & k) == 0 && shared_arr[tid] > shared_arr[ixj]) - swap(shared_arr[tid], shared_arr[ixj], shared_ord[tid], shared_ord[ixj]); - if ((tid & k) != 0 && shared_arr[tid] < shared_arr[ixj]) - swap(shared_arr[tid], shared_arr[ixj], shared_ord[tid], shared_ord[ixj]); - } - } - } - - __syncthreads(); - arr[tid] = shared_arr[tid]; - ord[shared_ord[tid]] = tid; -} - -__global__ void build_index(long * indices, long * uniq_idx, long * buf_idx, int * uniq_cnt){ - __shared__ long idx[C], ord[C], ibuf[C], iuniq[C], count[C], ord_uniq[C]; - int tid = threadIdx.x; - idx[tid] = indices[blockIdx.x * C + tid]; - ord[tid] = tid; - ord_uniq[tid] = tid; - count[tid] = 0; - __syncthreads(); - - bitonic_sort(idx, ord); - ibuf[tid] = (tid > 0 && idx[tid] > idx[tid-1]) ? 1:0; - __syncthreads(); - - for (int offset = 1; offset < C; offset *= 2) { - __syncthreads(); - if (tid >= offset) { - ibuf[tid] += ibuf[tid - offset]; - } - } - - if (tid == 0) { count[ibuf[C-1]+1] = C; } - else if (idx[tid] > idx[tid-1]) { - count[ibuf[tid]] = tid; } - iuniq[tid] = _REL_ID_; - __syncthreads(); - - // exceed threshold - if (tid > 0 && count[tid]-count[tid-1]>T) { - iuniq[tid-1] = idx[count[tid]-1]; } - __syncthreads(); - - bitonic_sort(iuniq, ord_uniq); - - int temp = ord_uniq[ibuf[tid]]; - if (iuniq[temp] < _REL_ID_){ - ibuf[tid] = temp; - }else{ - ibuf[tid] = idx[tid] + C; - } - if (iuniq[tid] < _REL_ID_ && iuniq[tid+1] == _REL_ID_){ - uniq_cnt[blockIdx.x] = tid+1; - } - buf_idx[blockIdx.x * C + tid] = ibuf[ord[tid]]; - uniq_idx[blockIdx.x * C + tid] = iuniq[tid]; -} \ No newline at end of file diff --git a/batch/opt/sort/__pycache__/mysort.cpython-310.pyc b/batch/opt/sort/__pycache__/mysort.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02773e9853b5212756c36eea3536d1db2dc7b22d GIT binary patch literal 876 zcmYjPy^a$x5VpO)`2~q)2(Ca|G>Rx%5{@e?>7H>$ ziDoKL;fAW{E91dOqGFXi5|wDO3wO~&z+)Zn5}qo$B(wAvR3VEQ^@h(* zmu|*nfY=Q|w+u|k zju+>>{v|AcjZ_GvSO`B9VqYs)qCXbm&gJFv$+)#JG`ZJyTPyPdd%i@24bBX9HxZZw zv_&>k!sw8)a7z3A*}zA(Hgd~f_siVbqHO1~X@ovlU9~`rLBIwp@HBU}-k4nR^aC)9 twDo0pls8Rz@2CA20{Y-P71ibpNe}EajCZFuy~jD8FwrrcvI{mG{RQt%)ms1n literal 0 HcmV?d00001 diff --git a/batch/opt/sort/__pycache__/sort.cpython-310.pyc b/batch/opt/sort/__pycache__/sort.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e7c4063211f3e987e4c85f423e644710be0036da GIT binary patch literal 860 zcmYjPy>8nu5GJWV#&O)C0ono`yLhp-PDK$QNIMkpQgq=$C?stwktKy9QOD4htbK=e z>@)ODxOVE+xl`}t7|;Rt@s2-t-`%k}JDU)Yzb~%LFG|R7bjA{ca{<~RXrgIF1g@;2 zTP7GKhUrMhYpRoPOhg}vPIdN3bY{p+e3LvxA~oqvCb*`b$(;XzC}cUOA+DN2hq!C3 z+h>Ri<>xHKb+I)9HoR4)Q+RtU5jYp1-5UTy1l2?^6KSg1BYB`A1{>H6cA^u=n1<79 zUshU{wKh9-0`HE3HWq&g+PwyF+%kE-p>BvBvxz@ohm*{ZQ$3#&QSf-`%` zV!D4mDt+;ELtzz1K^t{w0Y1|NN~Ozz%$(W`wg&qEXW#~o&CZ#+gIQQ4Y*PTi3-6k>EwtbtV3;_mA&f|hwyo~N jZ1^%TG59`Zy&f?ch#i{o?oiWv?BanFozfXQXOq+a3OCNQ literal 0 HcmV?d00001 diff --git a/batch/opt/sort/build_indexing.py b/batch/opt/sort/build_indexing.py new file mode 100644 index 0000000..2132098 --- /dev/null +++ b/batch/opt/sort/build_indexing.py @@ -0,0 +1,31 @@ +import mysort +import torch + +batch_size = 128 + +th = h = torch.randint(0, 9999, (batch_size, )).int().cuda(0) +tr = r = torch.randint(0, 100, (batch_size, )).int().cuda(0) +tt = t = torch.randint(0, 9999, (batch_size, )).int().cuda(0) + +# h = torch.tensor([1, 2, 3, 4]) +# t = torch.tensor([10, 20, 30, 40]) +# r = torch.tensor([3, 1, 4, 2]) + +print(h) +print(t) +print(r) +print('after sorting:::') +sorted_indices = torch.argsort(r) + +sorted_h = h[sorted_indices] +sorted_t = t[sorted_indices] +sorted_r = r[sorted_indices] +print("Sorted h:", sorted_h) +print("Sorted t:", sorted_t) +print(sorted_r) + +runiq = torch.zeros((batch_size//16, 16)).int().cuda(0) +rbuffer = torch.zeros((batch_size//16, 16)).int().cuda(0) +uniq_cnt = torch.zeros((batch_size//16,)).int().cuda(0) + +th, tt, tr = mysort.index_building(th, tt, tr, runiq, rbuffer, uniq_cnt, batch_size, 16, 100) diff --git a/batch/opt/sort/mysort.py b/batch/opt/sort/mysort.py new file mode 100644 index 0000000..0784afa --- /dev/null +++ b/batch/opt/sort/mysort.py @@ -0,0 +1,18 @@ + +import torch +from torch.utils.cpp_extension import load + +sort_func = load(name='sort', sources=['sort.cu']) + +class build_index(torch.autograd.Function): + @staticmethod + def forward(ctx, head, tail, relation, r_uniq, r_buffer, uniq_cnt, n, gs, rel_num): + sort_func.gpu_sort(head, tail, relation, r_uniq, r_buffer, uniq_cnt, n, gs, rel_num) + return head, tail, relation + + @staticmethod + def backward(ctx): + pass + + +index_building = build_index.apply \ No newline at end of file diff --git a/batch/opt/sort/sort.cu b/batch/opt/sort/sort.cu new file mode 100644 index 0000000..6932e6d --- /dev/null +++ b/batch/opt/sort/sort.cu @@ -0,0 +1,121 @@ +#include +#include +#include +#include +#include +#include +using namespace std; + +#define BLOCK_SIZE 256 +#define C 16 +#define T 3 +#define DIV(x, ts) ((x) % (ts) != 0 ? (x) / (ts) + 1 : (x) / (ts)) + +__device__ void swap(int& a, int& b, int& a_idx, int& b_idx) { + int tmp = a; + a = b; + b = tmp; + tmp = a_idx; + a_idx = b_idx; + b_idx = tmp; +} + +__device__ void bitonic_sort(int* arr, int* ord) { + __shared__ int shared_arr[C]; + __shared__ int shared_ord[C]; + + int tid = threadIdx.x; + shared_arr[tid] = arr[tid]; + shared_ord[tid] = tid; + __syncthreads(); + + for (int k = 2; k <= C; k <<= 1) { + for (int j = k >> 1; j > 0; j >>= 1) { + __syncthreads(); + int ixj = tid ^ j; + if (ixj > tid) { + if ((tid & k) == 0 && shared_arr[tid] > shared_arr[ixj]) + swap(shared_arr[tid], shared_arr[ixj], shared_ord[tid], shared_ord[ixj]); + if ((tid & k) != 0 && shared_arr[tid] < shared_arr[ixj]) + swap(shared_arr[tid], shared_arr[ixj], shared_ord[tid], shared_ord[ixj]); + } + } + } + + __syncthreads(); + arr[tid] = shared_arr[tid]; + ord[shared_ord[tid]] = tid; +} + +__global__ void build_index(torch::PackedTensorAccessor32 indices, torch::PackedTensorAccessor32 r_Uniq, torch::PackedTensorAccessor32 r_Buffer, torch::PackedTensorAccessor32 uniq_cnt, int rel_num){ + __shared__ int idx[C], ord[C], ibuf[C], iuniq[C], pcount[C], ord_uniq[C]; + int tid = threadIdx.x; + idx[tid] = indices[blockIdx.x * C + tid]; + ord[tid] = tid; + ord_uniq[tid] = tid; + pcount[tid] = 0; + __syncthreads(); + + bitonic_sort(idx, ord); + ibuf[tid] = (tid > 0 && idx[tid] > idx[tid-1]) ? 1:0; + __syncthreads(); + + for (int offset = 1; offset < C; offset *= 2) { + __syncthreads(); + if (tid >= offset) { + ibuf[tid] += ibuf[tid - offset]; + } + } + + if (tid == 0) { pcount[ibuf[C-1]+1] = C; } + else if (idx[tid] > idx[tid-1]) { + pcount[ibuf[tid]] = tid; } + iuniq[tid] = rel_num; + __syncthreads(); + + // exceed threshold + if (tid > 0 && pcount[tid]-pcount[tid-1]>T) { + iuniq[tid-1] = idx[pcount[tid]-1]; } + __syncthreads(); + + bitonic_sort(iuniq, ord_uniq); + + int temp = ord_uniq[ibuf[tid]]; + if (iuniq[temp] < rel_num){ + ibuf[tid] = temp; + }else{ + ibuf[tid] = idx[tid] + C; + } + if (iuniq[tid] < rel_num && iuniq[tid+1] == rel_num){ + uniq_cnt[blockIdx.x] = tid+1; + } + r_Buffer[blockIdx.x][tid] = ibuf[ord[tid]]; + r_Uniq[blockIdx.x][tid] = iuniq[tid]; +} + + +void gpu_sort(torch::Tensor head, torch::Tensor tail, torch::Tensor relation, torch::Tensor r_Uniq, torch::Tensor r_Buffer, torch::Tensor uniq_cnt, int batch, int group_size, int rel_num) { + // int batch=4096; + dim3 nblocks(DIV(batch, C)); + dim3 nthreads(32, C); + + torch::Tensor sorted_indices = torch::argsort(relation); + std::cout << "Original Head: " << relation << std::endl; + + head = torch::index_select(head, 0, sorted_indices); + tail = torch::index_select(tail, 0, sorted_indices); + relation = torch::index_select(relation, 0, sorted_indices); + + + std::cout << "Sorted Head: " << relation << std::endl; + + build_index<<< batch/C, C>>>(relation.packed_accessor32(), r_Uniq.packed_accessor32(), r_Buffer.packed_accessor32(), uniq_cnt.packed_accessor32(), rel_num); + + std::cout << "uniq: " << r_Uniq << std::endl; + std::cout << "buffer: " << r_Buffer << std::endl; + std::cout << "uniq_cnt: " << uniq_cnt << std::endl; +} + +PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { + m.def("gpu_sort", &gpu_sort, "ss"); +} \ No newline at end of file diff --git a/batch/opt/sort/test b/batch/opt/sort/test new file mode 100755 index 0000000000000000000000000000000000000000..bd901b9976dfc84afb2d3c138bff1ac8c4959818 GIT binary patch literal 734504 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 oJzl!^6_3cjUh-+Cn!aMn +#include +#include +#include +using namespace std; + +#define BLOCK_SIZE 256 +#define C 16 +#define T 2 + + +__device__ void swap(long& a, long& b, long& a_idx, long& b_idx) { + int tmp = a; + a = b; + b = tmp; + tmp = a_idx; + a_idx = b_idx; + b_idx = tmp; +} + +__device__ void bitonic_sort(long* arr, long* ord) { + __shared__ long shared_arr[C]; + __shared__ long shared_ord[C]; + + int tid = threadIdx.x; + shared_arr[tid] = arr[tid]; + shared_ord[tid] = tid; + __syncthreads(); + + for (int k = 2; k <= C; k <<= 1) { + for (int j = k >> 1; j > 0; j >>= 1) { + __syncthreads(); + int ixj = tid ^ j; + if (ixj > tid) { + if ((tid & k) == 0 && shared_arr[tid] > shared_arr[ixj]) + swap(shared_arr[tid], shared_arr[ixj], shared_ord[tid], shared_ord[ixj]); + if ((tid & k) != 0 && shared_arr[tid] < shared_arr[ixj]) + swap(shared_arr[tid], shared_arr[ixj], shared_ord[tid], shared_ord[ixj]); + } + } + } + + __syncthreads(); + arr[tid] = shared_arr[tid]; + ord[shared_ord[tid]] = tid; +} + +__global__ void build_index(long * indices, long * uniq_idx, long * buf_idx, long * uniq_cnt){ + __shared__ long idx[C], ord[C], ibuf[C], iuniq[C], count[C], ord_uniq[C]; + int tid = threadIdx.x; + idx[tid] = indices[blockIdx.x * C + tid]; + ord[tid] = tid; + ord_uniq[tid] = tid; + count[tid] = 0; + __syncthreads(); + + bitonic_sort(idx, ord); + ibuf[tid] = (tid > 0 && idx[tid] > idx[tid-1]) ? 1:0; + __syncthreads(); + + for (int offset = 1; offset < C; offset *= 2) { + __syncthreads(); + if (tid >= offset) { + ibuf[tid] += ibuf[tid - offset]; + } + } + + // buf_idx[blockIdx.x * C + tid] = ibuf[ord[tid]]; + if (tid == 0) { count[ibuf[C-1]+1] = C; } + else if (idx[tid] > idx[tid-1]) { + count[ibuf[tid]] = tid; } + iuniq[tid] = 999; + __syncthreads(); + + // exceed threshold + if (tid > 0 && count[tid]-count[tid-1]>T) { + iuniq[tid-1] = idx[count[tid]-1]; } + __syncthreads(); + + bitonic_sort(iuniq, ord_uniq); + + int temp = ord_uniq[ibuf[tid]]; + // if(threadIdx.x == 0 && blockIdx.x == 0){ + // for(int i=0;i>>(d_data, d_uniq_idx, d_buf_idx, d_uniq_cnt); + + // print result + // for (int i = 0; i < n; i++) { + // printf("%d ", data[i]); + // } + // printf("\n%d\n", n); + + cudaMemcpy(uniq_idx, d_uniq_idx, n*sizeof(long), cudaMemcpyDeviceToHost); + cudaMemcpy(buf_idx, d_buf_idx, n*sizeof(long), cudaMemcpyDeviceToHost); + cudaMemcpy(uniq_cnt, d_uniq_cnt, n/C*sizeof(long), cudaMemcpyDeviceToHost); + printf("unique idx:\n"); + for(int i=0;i {to_string(ir.right)} ? ({to_string(ir.left)}) : ({to_string(ir.right)}))" + elif ir.op == 'smaller': + return f"({to_string(ir.left)} < {to_string(ir.right)} ? ({to_string(ir.left)}) : ({to_string(ir.right)}))" case 'Assignment': if ir.op is None: return f"{to_string(ir.lhs)} = {to_string(ir.rhs)};\n" @@ -19,46 +27,59 @@ def to_string(ir): code += to_string(e) code += "} \n" return code + case 'FilterLoop': + code = f"for (int {to_string(ir.iterate)} = {to_string(ir.start)}; {to_string(ir.iterate)} < {to_string(ir.end)}; {to_string(ir.iterate)} += {to_string(ir.step)}) {{\n" + for e in ir.body: + if e: + code += to_string(e) + if ir.cond: + code += f"if({to_string(ir.cond)}){{\n" + for e in ir.cond_body: + if e: + code += to_string(e) + code += "} \n" + code += "} \n" + return code + case 'Not': + return f"!{to_string(ir.dobject)}" case 'Scalar' | 'Ndarray' | 'Ref': return ir.name() case 'Literal': return str(ir.val) case 'Indexing': if type(ir.dobject) == Slice: - return f'(({to_string(ir.dobject.start)})+({to_string(ir.dobject.step)})*({to_string(ir.idx)}))' + if ir.dobject.step == 1 or (type(ir.dobject.step) == Literal and ir.dobject.step.val == 1): + return f'(({to_string(ir.dobject.start)})+({to_string(ir.idx)}))' + else: + return f'(({to_string(ir.dobject.start)})+({to_string(ir.dobject.step)})*({to_string(ir.idx)}))' else: return f'{to_string(ir.dobject)}[{to_string(ir.idx)}]' + case 'Search': + code = f"BinarySearch({to_string(ir.dobject)}, {to_string(ir.start)}, {to_string(ir.end)}, {to_string(ir.item)})" + return code case 'Decl': # variables are passed in as pytorch arguments if type(ir.dobject) == Scalar: if not ir.dobject.is_arg: - # it is a zero or one - if ir.dobject.val != None: - return f"{ir.dobject.dtype} {ir.dobject.name()} = {to_string(ir.dobject.val)};\n" - else: - return f"{ir.dobject.dtype} {ir.dobject.name()};\n" + return f"{ir.dobject.dtype} {ir.dobject.name()};\n" else: return '' elif type(ir.dobject) == Ndarray: code = '' if not ir.dobject.is_arg: - if ir.dobject.val != None: - code = f'torch::Tensor obj_{ir.dobject.name()} = torch::{"ones" if ir.dobject.val == 1 else "zeros"}({{{",".join([to_string(s) for s in ir.dobject.size])}}}, at::k{"Int" if ir.dobject.dtype=="int" else "Float"});\n' - else: - code = f'torch::Tensor obj_{ir.dobject.name()} = torch::empty({{{",".join([to_string(s) for s in ir.dobject.size])}}}, at::k{"Int" if ir.dobject.dtype=="int" else "Float"});\n' - + code = f'torch::Tensor obj_{ir.dobject.name()} = torch::empty({{{",".join([to_string(s) for s in ir.dobject.size])}}}, at::k{"Int" if ir.dobject.dtype=="int" else "Float"});\n' code += f'auto {ir.dobject.name()} = obj_{ir.dobject.name()}.accessor<{ir.dobject.dtype}, {len(ir.dobject.size)}>();\n' return code + case 'Math': + return f"{ir.type}({to_string(ir.val)})" case _: return str(ir) - - def gen_cpp(ast, ir): def action(node, res): if node.valid == True: - if type(node) == Var or type(node) == One or type(node) == Zero or type(node) == Ones or type(node) == Zeros or type(node) == Tensor: + if type(node) == Var or type(node) == Tensor: res.extend(node.decl) elif type(node) == TensorOp: res.extend(node.decl) @@ -66,6 +87,11 @@ def action(node, res): elif type(node) == batch.ast.BatchOp: res.extend(node.decl) res.extend(node.compute) + elif type(node) == cset.ast.Set: + res.extend(node.decl) + elif type(node) == cset.ast.SetOp: + res.extend(node.decl) + res.extend(node.compute) t = helpers.Traversal(action) ir.extend(t(ast)) diff --git a/codegen/gpu.py b/codegen/gpu.py index 46ce585..0d70f46 100644 --- a/codegen/gpu.py +++ b/codegen/gpu.py @@ -46,22 +46,13 @@ def to_string(ir): if type(ir.dobject) == Scalar: if not ir.dobject.is_arg: - # it is a zero or one - if ir.dobject.val != None: - return f"{ir.dobject.dtype} {ir.dobject.name()} = {to_string(ir.dobject.val)};\n" - else: - return f"{ir.dobject.dtype} {ir.dobject.name()};\n" + return f"{ir.dobject.dtype} {ir.dobject.name()};\n" else: return '' elif type(ir.dobject) == Ndarray: code = '' if not ir.dobject.is_arg: - if ir.dobject.val != None: - code = f'torch::Tensor obj_{ir.dobject.name()} = torch::{"ones" if ir.dobject.val == 1 else "zeros"}({{{",".join([to_string(s) for s in ir.dobject.size])}}}, torch::TensorOptions(torch::k{"Int" if ir.dobject.dtype=="int" else "Float"}).device(torch::kCUDA));\n' - else: - code = f'torch::Tensor obj_{ir.dobject.name()} = torch::empty({{{",".join([to_string(s) for s in ir.dobject.size])}}}, torch::TensorOptions(torch::k{"Int" if ir.dobject.dtype=="int" else "Float"}).device(torch::kCUDA));\n' - - # code += f'auto {ir.dobject.name()} = obj_{ir.dobject.name()}.accessor<{ir.dobject.dtype}, {len(ir.dobject.size)}>();\n' + code = f'torch::Tensor obj_{ir.dobject.name()} = torch::empty({{{",".join([to_string(s) for s in ir.dobject.size])}}}, torch::TensorOptions(torch::k{"Int" if ir.dobject.dtype=="int" else "Float"}).device(torch::kCUDA));\n' return code elif type(ir.dobject) == Shared: shape = '' @@ -117,7 +108,7 @@ def gen_cuda(ast, cpu_ir, gpu_ir): # 2 ir list for cpu and gpu def action_cpu(node, res): if node.valid: - if type(node) == Var or type(node) == One or type(node) == Zero or type(node) == Ones or type(node) == Zeros or type(node) == Tensor: + if type(node) == Var or type(node) == Tensor: res.extend(node.decl) elif type(node) == TensorOp: res.extend(node.decl) diff --git a/core/ast.py b/core/ast.py index 415846f..0c19699 100644 --- a/core/ast.py +++ b/core/ast.py @@ -1,18 +1,27 @@ import copy import core +MIN_INT = -2147483648 +MAX_INT = 2147483647 + +arith_op = {'add': '+', 'sub': '-', 'mul': '*', 'floordiv': '/', 'truediv': '/'} +math_op = ['round', 'abs'] +cmp_op = ['bigger', 'smaller'] +func_op = ['index', 'apply', 'reduce', 'aggr', 'einsum', 'setval'] + + def is_int_var(v): - return isinstance(v, Tensor) and v.dtype == 'int' and len(v._size()) == 0 + return isinstance(v, Tensor) and v.dtype == 'int' and len(v.ref_size) == 0 def is_scalar(v): - return isinstance(v, int|float) or (isinstance(v, Tensor) and len(v._size()) == 0) + return isinstance(v, int|float) or (isinstance(v, Tensor) and len(v.ref_size) == 0) def is_1dint_tensor(v): - return isinstance(v, Tensor) and v.dtype == 'int' and len(v._size()) == 1 + return isinstance(v, Tensor) and v.dtype == 'int' and len(v.ref_size) == 1 def eval_const_expr(e): - if type(e) == TensorOp and (e.op_type in op_mapping): + if type(e) == TensorOp and (e.op_type in arith_op): lhs = eval_const_expr(e.operators[0]) if lhs != None: rhs = eval_const_expr(e.operators[1]) @@ -54,7 +63,7 @@ def has_same_value(e1, e2): elif type(e1) == TensorOp: if e1.op_type != e2.op_type: return False - elif e1.op_type in op_mapping: + elif e1.op_type in arith_op: return has_same_value(e1.operators[0], e2.operators[0]) and has_same_value(e2.operators[1], e2.operators[1]) else: if len(e1.operators) != len(e2.operators): @@ -81,16 +90,14 @@ def is_same_size(s1, s2): def bigger(x, y): return TensorOp('bigger', x, y) -op_mapping = {'add':'+', 'sub':'-', 'mul':'*', 'floordiv':'/', 'truediv':'/'} -math_op = ['round', 'abs'] -cmp_op = ['bigger', 'smaller'] +def smaller(x, y): + return TensorOp('smaller', x, y) class ASTNode: nuniq = 0 def __init__(self): self.decl = [] - self.compute = [] self.eval = None self.ref_count = 0 self.id = ASTNode.nuniq @@ -98,6 +105,7 @@ def __init__(self): self.valid = True + class Tensor(ASTNode): def __init__(self, name, size:list|tuple, dtype='float', fix_size=[], is_arg=True): super().__init__() @@ -147,41 +155,37 @@ def apply(self, func, axis=0): if callable(func): from core.ast2ir import gen_ir op = TensorOp('apply', self, func, axis) - gen_ir(op) return op else: raise TypeError('must apply a callable function') def reduce(self, func, init, axis=0): - if callable(func): + if callable(func) and callable(init): from core.ast2ir import gen_ir op = TensorOp('reduce', self, func, init, axis) - gen_ir(op) return op else: raise TypeError('reduce must use a callable function') def sum(self, axis=0): func = lambda x, y: x + y - size = self._size()[:axis] + self._size()[axis+1:] - if (len(size) > 0): - init = Zeros(size, dtype=self.dtype) - else: - init = Const(0, dtype=self.dtype) + init = lambda x: x.setval(0) return self.reduce(func, init, axis) def max(self, axis=0): func = lambda x, y: bigger(x, y) - size = self._size()[:axis] + self._size()[axis+1:] - if (len(size) > 0): - init = Zeros(size, dtype=self.dtype) - else: - init = Const(0, dtype=self.dtype) + init = lambda x: x.setval(MIN_INT) + return self.reduce(func, init, axis) + + def min(self, axis=0): + func = lambda x, y: smaller(x, y) + init = lambda x: x.setval(MAX_INT) return self.reduce(func, init, axis) + def aggr(self, func, init, indices, axis=0, size=None): - if callable(func): - from core.ast2ir import gen_ir + if callable(func) and callable(init): + from core.ast2ir import gen_ir op = TensorOp('aggr', self, func, init, indices, axis, size) gen_ir(op) return op @@ -190,14 +194,23 @@ def aggr(self, func, init, indices, axis=0, size=None): def aggr_sum(self, indices, axis=0, size=None): func = lambda x, y: x + y - s = self._size()[:axis] + self._size()[axis+1:] - if (len(s) > 0): - init = Zeros(s, dtype=self.dtype) - else: - init = Const(0, dtype=self.dtype) + init = lambda x: x.setval(0) return self.aggr(func, init, indices, axis, size) + def aggr_max(self, indices, axis=0, size=None): + func = lambda x, y: bigger(x, y) + init = lambda x: x.setval(MIN_INT) + return self.aggr(func, init, indices, axis, size) + def aggr_min(self, indices, axis=0, size=None): + func = lambda x, y: smaller(x, y) + init = lambda x: x.setval(MAX_INT) + return self.aggr(func, init, indices, axis, size) + + + + def setval(self, val): + return TensorOp('setval', self, val) def _size(self): return self.fix_size + self.ref_size @@ -214,8 +227,7 @@ def size(self): else: return Const(0, dtype='int') - def _gen_ir(self): - return core.ast2ir.gen_ir(self) + def round(self): return TensorOp('round', self) @@ -223,18 +235,8 @@ def round(self): def abs(self): return TensorOp('abs', self) - -class Ones(Tensor): - nones = 0 - def __init__(self, size, dtype='float'): - super.__init__(f'ones_{Ones.nones}', size, dtype, [], False) - Ones.nones += 1 - -class Zeros(Tensor): - nzeros = 0 - def __init__(self, size, dtype='float'): - super().__init__(f'zeros_{Zeros.nzeros}', size, dtype, [], False) - Zeros.nzeros += 1 + def _gen_ir(self): + return core.ast2ir.gen_ir(self) class Var(Tensor): @@ -242,18 +244,6 @@ def __init__(self, name, dtype='int', is_arg=True): super().__init__(name, [], dtype, [], is_arg) -class One(Var): - none = 0 - def __init__(self, dtype='float'): - super().__init__(f'one_{One.none}', dtype, False) - One.none += 1 - -class Zero(Var): - nzero = 0 - def __init__(self, dtype='float'): - super().__init__(f'zero_{Zero.nzero}', dtype, False) - Zero.nzero += 1 - # const is var without name class Const(Var): @@ -274,10 +264,12 @@ def einsum(exp: str, tensor1, tensor2): return TensorOp('einsum', tensor1, tensor2, exp) class TensorOp(Tensor): - Types = ['index', 'apply', 'reduce', 'aggr', 'einsum'] + list(op_mapping.keys()) + math_op + cmp_op + Types = func_op + list(arith_op.keys()) + math_op + cmp_op def __init__(self, op_type, *operators): assert op_type in TensorOp.Types + self.compute = [] + self.output_order = [] # TODO: infer result data type dtype = operators[0].dtype @@ -296,7 +288,7 @@ def __init__(self, op_type, *operators): if isinstance(opr, ASTNode): opr.ref_count += 1 - if op_type in op_mapping or op_type in cmp_op: + if op_type in arith_op or op_type in cmp_op: if type(self.operators[0]) == int: self.operators[0] = Const(self.operators[0], 'int') @@ -317,8 +309,8 @@ def __init__(self, op_type, *operators): exp = self.operators[2] inputs, output = exp.split('->') input1, input2 = inputs.split(',') - op1_size = self.operators[0].fix_size + self.operators[0].ref_size - op2_size = self.operators[1].fix_size + self.operators[1].ref_size + op1_size = self.operators[0]._size() + op2_size = self.operators[1]._size() ref_size = [] fix_size = [] for i in output: @@ -374,11 +366,6 @@ def __init__(self, op_type, *operators): assert type(self.operators[2]) == int axis = self.operators[2] self.operators[2] = Const(axis, 'int') - # size cannot be determined except for axis dimension, so set -1 - ref_size = [self.operators[0]._size()[axis], -1] - fix_size = [] - # data type also cannot be determined - dtype = None data_size = self.operators[0]._size() item_size = data_size[:axis] + data_size[axis + 1:] @@ -387,7 +374,13 @@ def __init__(self, op_type, *operators): self.operators[0].dtype, [], False) else: item = Var(f'item_of_{self.operators[0].name}', self.operators[0].dtype, False) + + ret = self.operators[1](item) + dtype = ret.dtype + ref_size = [self.operators[0]._size()[axis]] + ret._size() + fix_size = [] self.operators.append(item) + self.operators.append(ret) elif op_type == 'reduce': assert type(self.operators[3]) == int @@ -404,8 +397,10 @@ def __init__(self, op_type, *operators): else: item1 = Var(f'item1_of_{self.operators[0].name}', self.operators[0].dtype, False) item2 = Var(f'item2_of_{self.operators[0].name}', self.operators[0].dtype, False) + self.operators.append(item1) self.operators.append(item2) + self.operators.append(self.operators[1](item1, item2)) elif op_type == 'aggr': assert is_1dint_tensor(self.operators[3]) @@ -413,7 +408,7 @@ def __init__(self, op_type, *operators): axis = self.operators[4] self.operators[4] = Const(axis, 'int') if self.operators[5] == None: - self.operators[5] = self.operators[0]._size()[axis] + self.operators[5] = self.operators[3].ref_size[0] else: assert is_int_var(self.operators[5]) if type(self.operators[5]) == int: @@ -431,15 +426,25 @@ def __init__(self, op_type, *operators): item2 = Var(f'item2_of_{self.operators[0].name}', self.operators[0].dtype, False) self.operators.append(item1) self.operators.append(item2) + self.operators.append(self.operators[1](item1, item2)) elif op_type in math_op: - ref_size = self.operators[0]._size() + ref_size = self.operators[0].ref_size fix_size = [] if op_type == 'round': dtype = 'int' elif op_type == 'abs': dtype = self.operators[0].dtype + elif op_type == 'setval': + ref_size = self.operators[0].ref_size + fix_size = self.operators[0].fix_size + assert is_scalar(self.operators[1]) + if type(self.operators[1]) == int: + self.operators[1] = Const(self.operators[1], 'int') + elif type(self.operators[1]) == float: + self.operators[1] = Const(self.operators[1], 'float') + name = f'{op_type}_' + '_'.join([op.name if hasattr(op, 'name') else '' for op in self.operators]) @@ -447,6 +452,8 @@ def __init__(self, op_type, *operators): self.op_type = op_type + # call the init function for reduce and aggr + if self.op_type in ('reduce', 'aggr'): + self.operators[2] = self.operators[2](self) - - + self.input_orders = [None for o in self.operators] \ No newline at end of file diff --git a/core/ast2ir.py b/core/ast2ir.py index 5c23fed..c7eb451 100755 --- a/core/ast2ir.py +++ b/core/ast2ir.py @@ -19,6 +19,15 @@ def get_first_unbind(index: (Indexing, Ndarray, Slice)): return None +def bind(index: (Indexing, Ndarray, Slice), idx): + x = get_first_unbind(index) + if x == None: + return Indexing(index, idx) + else: + x.idx = idx + return index + + def replace_output(ir, old, new): if type(ir) == list or type(ir) == tuple: for l in ir: @@ -36,20 +45,9 @@ def replace_output(ir, old, new): else: replace_output(ir.dobject, old, new) - - -def bind(index: (Indexing, Ndarray, Slice), idx): - x = get_first_unbind(index) - if x == None: - return Indexing(index, idx) - else: - x.idx = idx - return index - - def gen_ir(node): assert isinstance(node, ASTNode) - if len(node.compute) > 0 or len(node.decl) > 0 or node.eval: + if node.eval or len(node.decl) > 0 or (type(node) == TensorOp and len(node.compute) > 0): return node if type(node) == Const: if node.dtype != 'slice': @@ -72,20 +70,12 @@ def gen_ir(node): node.eval = Ndarray(node.dtype, size, node.name, node.is_arg) node.decl = [Decl(node.eval)] - # here we define two special tensors to simply programming for sum/prod operations - elif (type(node) == Ones or type(node) == Zeros) and len(node._size()) > 0: - size = helpers.get_ir_of_size(node._size()) - node.eval = Ndarray(node.dtype, size, node.name, False, 1 if (type(node) == Ones) else 0) - node.decl = [Decl(node.eval)] - - elif ((type(node) == Ones or type(node) == Zeros) and len(node._size()) == 0) or ((type(node) == One or type(node) == Zero)): - node.eval = Scalar(node.dtype, node.name, False, 1 if (type(node) == Ones or type(node) == One) else 0) - node.decl = [Decl(node.eval)] - elif type(node) == TensorOp: - if node.op_type in op_mapping or node.op_type in cmp_op: + if node.op_type in arith_op or node.op_type in cmp_op: node.operators[0]._gen_ir() node.operators[1]._gen_ir() + node.input_orders[0] = [] + node.input_orders[1] = [] assert isinstance(node.operators[0], Tensor) and isinstance(node.operators[1], Tensor) if is_same_size(node.operators[0]._size(), node.operators[1]._size()): if len(node._size()) > 0: @@ -105,8 +95,8 @@ def gen_ir(node): rhs = bind(rhs, pre_loop.iterate) res = bind(res, pre_loop.iterate) - if node.op_type in op_mapping: - op = op_mapping[node.op_type] + if node.op_type in arith_op: + op = arith_op[node.op_type] else: op = node.op_type assign = Assignment(res, Expr(lhs, rhs, op)) @@ -115,8 +105,8 @@ def gen_ir(node): else: node.eval = Scalar(node.dtype) node.decl = [Decl(node.eval)] - if node.op_type in op_mapping: - op = op_mapping[node.op_type] + if node.op_type in arith_op: + op = arith_op[node.op_type] else: op = node.op_type node.compute = [Assignment(node.eval, Expr(node.operators[0].eval, node.operators[1].eval, op))] @@ -136,15 +126,25 @@ def gen_ir(node): lhs = bind(lhs, pre_loop.iterate) res = bind(res, pre_loop.iterate) - if node.op_type in op_mapping: - op = op_mapping[node.op_type] + if node.op_type in arith_op: + op = arith_op[node.op_type] else: op = node.op_type assign = Assignment(res, Expr(lhs, rhs, op)) pre_loop.body.append(assign) + l = node.compute[0] + for i in range(len(node.eval.size)): + node.output_order.append((i, l)) + node.input_orders[0].append((i, l)) + node.input_orders[1].append((i, l)) + l = l.body[0] + + + elif node.op_type in math_op: node.operators[0]._gen_ir() + node.input_orders[0] = [] if len(node._size()) > 0: size = helpers.get_ir_of_size(node._size()) node.eval = Ndarray(node.dtype, size) @@ -168,54 +168,124 @@ def gen_ir(node): node.decl = [Decl(node.eval)] node.compute = [Assignment(node.eval, Math(node.operators[0].eval, node.op_type))] + l = node.compute[0] + for i in range(len(node.eval.size)): + node.output_order.append((i, l)) + node.input_orders[0].append((i, l)) + l = l.body[0] + + elif node.op_type == 'setval': + node.operators[0]._gen_ir() + node.operators[1]._gen_ir() + node.input_orders[0] = [] + # node.operators[1] must be a Scalar, so no input_order is needed + + node.eval = node.operators[0].eval + node.decl = node.operators[0].decl[:] + node.operators[0].decl.clear() + val = node.operators[1].eval + + if len(node.ref_size) > 0: + size = helpers.get_ir_of_size(node.ref_size) + pre_loop = Loop(0, size[0], 1, []) + node.compute = [pre_loop] + res = bind(node.eval, pre_loop.iterate) + for i in range(1, len(size)): + loop = Loop(0, size[i], 1, []) + pre_loop.body.append(loop) + pre_loop = loop + res = bind(res, pre_loop.iterate) + + assign = Assignment(res, val) + pre_loop.body.append(assign) + else: + node.compute = [Assignment(node.eval, val)] + + l = node.compute[0] + for i in range(len(node.eval.size)): + node.output_order.append((i, l)) + node.input_orders[0].append((i, l)) + l = l.body[0] elif node.op_type == 'einsum': node.operators[0]._gen_ir() node.operators[1]._gen_ir() + node.input_orders[0] = [] + node.input_orders[1] = [] + exp = node.operators[2] inputs, output = exp.split('->') input1, input2 = inputs.split(',') all_indices = ''.join(sorted(set(input1 + input2))) all_loops = [] + mapping = {} + for i in range(len(all_indices)): + pos1 = input1.find(all_indices[i]) + pos2 = input2.find(all_indices[i]) + if (pos1 >= 0 and pos2 < 0): + mapping[all_indices[i]] = len(all_loops) + l = Loop(0, node.operators[0].eval.size[pos1], 1, []) + all_loops.append(l) + node.input_orders[0].append((len(node.input_orders[0]), l)) + elif (pos1 < 0 and pos2 >= 0): + mapping[all_indices[i]] = len(all_loops) + l = Loop(0, node.operators[1].eval.size[pos2], 1, []) + all_loops.append(l) + node.input_orders[1].append((len(node.input_orders[1]), l)) + + reduce_begins = len(all_loops) + + for i in range(len(all_indices)): + pos1 = input1.find(all_indices[i]) + pos2 = input2.find(all_indices[i]) + if pos1 >= 0 and pos2 >= 0: + mapping[all_indices[i]] = len(all_loops) + l = Loop(0, node.operators[0].eval.size[pos1], 1, []) + all_loops.append(l) + node.input_orders[0].append((len(node.input_orders[0]), l)) + node.input_orders[1].append((len(node.input_orders[1]), l)) + for i in all_indices: pos1 = input1.find(i) - if pos1 >= 0: - all_loops.append(Loop(0, node.operators[0].eval.size[pos1], 1, [])) - else: - pos2 = input2.find(i) - if pos2 >= 0: - all_loops.append(Loop(0, node.operators[1].eval.size[pos2], 1, [])) - else: - raise IndexError('index not found!') + pos2 = input2.find(i) + if pos1 < 0 and pos2 < 0: + raise IndexError('index not found!') op1 = node.operators[0].eval for i in input1: - idx = all_indices.find(i) - op1 = bind(op1, all_loops[idx].iterate, ) + op1 = bind(op1, all_loops[mapping[i]].iterate) op2 = node.operators[1].eval for i in input2: - idx = all_indices.find(i) - op2 = bind(op2, all_loops[idx].iterate) + op2 = bind(op2, all_loops[mapping[i]].iterate) size = helpers.get_ir_of_size(node._size()) - node.eval = Ndarray(node.dtype, size, val=0) + node.eval = Ndarray(node.dtype, size) node.decl = [Decl(node.eval)] res = node.eval for i in output: - idx = all_indices.find(i) - res = bind(res, all_loops[idx].iterate) + res = bind(res, all_loops[mapping[i]].iterate) body = Assignment(res, Expr(op1, op2, '*'), '+') + init = Assignment(res, 0) + if reduce_begins == 0: + node.compute.append(init) pre_loop = all_loops[0] - node.compute = [pre_loop] + node.compute.append(pre_loop) for i in range(1, len(all_loops)): + if reduce_begins == i: + pre_loop.body.append(init) loop = all_loops[i] pre_loop.body.append(loop) pre_loop = loop pre_loop.body.append(body) + l = node.compute[0] + for i in range(len(node.eval.size)): + node.output_order.append((i, l)) + l = l.body[0] + elif node.op_type == 'index': node.operators[0]._gen_ir() @@ -228,6 +298,7 @@ def gen_ir(node): raise TypeError('incorrect index type!') elif node.op_type == 'apply': + #TODO: add input_orders for apply, reduce, and aggr node.operators[0]._gen_ir() # input tensor node.operators[2]._gen_ir() # axis @@ -243,12 +314,12 @@ def gen_ir(node): item.eval = Indexing(item.eval, Literal(-1, 'int')) item.eval = Indexing(item.eval, outer_loop.iterate) - ret = node.operators[1](item) - ret._gen_ir() + ret = node.operators[-1] + ret._gen_ir() # generate IR for applied func def action(node, res): if node.valid == True: - if type(node) == Var or type(node) == One or type(node) == Zero or type(node) == Ones or type(node) == Zeros or type(node) == Tensor: + if type(node) == Var or type(node) == Tensor: res.extend(node.decl) node.valid = False elif type(node) == TensorOp: @@ -267,10 +338,6 @@ def action(node, res): else: ret_compute.append(ir) - node.operators.append(ret) - node.dtype = ret.dtype - node.ref_size = [node._size()[0]] + ret._size() - node.fix_size = [] outer_loop.body.extend(ret_compute) size = helpers.get_ir_of_size(node._size()) node.eval = Ndarray(ret.eval.dtype, size) @@ -282,9 +349,14 @@ def action(node, res): replace_output(node.compute, ret.eval, res) node.decl = [d for d in node.decl if d.dobject != ret.eval] + # TODO: need test for this + node.output_order = [(0, outer_loop)] + for i in range(len(ret.output_order)): + node.output_order.append((i+1, ret.output_order[i][1])) + + elif node.op_type == 'reduce': node.operators[0]._gen_ir() - node.operators[2]._gen_ir() # init node.operators[3]._gen_ir() axis = node.operators[3].eval.val @@ -295,23 +367,11 @@ def action(node, res): node.eval = Scalar(node.dtype) node.decl.append(Decl(node.eval)) - node.compute = [] - # initialize output - if len(node.eval.size) > 0: - pre_loop = Loop(0, node.eval.size[0], 1, []) - node.compute.append(pre_loop) - res = bind(node.eval, pre_loop.iterate) - rhs = bind(node.operators[2].eval, pre_loop.iterate) - for i in range(1, len(node.eval.size)): - loop = Loop(0, node.eval.size[i], 1, []) - pre_loop.body.append(loop) - pre_loop = loop - res = bind(res, pre_loop.iterate) - rhs = bind(rhs, pre_loop.iterate) - pre_loop.body.append(Assignment(res, rhs)) - else: - assign = Assignment(node.eval, node.operators[2].eval) - node.compute.append(assign) + node.operators[2]._gen_ir() # init + + # node.compute.extend(node.operators[2].compute) + # node.decl.extend(node.operators[2].decl) + # node.operators[2].valid = False # TODO: iterating over the reduction dimension in the outer loop may not give best performance # TODO: it might be better to make it the innermost loop @@ -327,12 +387,12 @@ def action(node, res): item2.decl = [] item1.decl = [] - ret = node.operators[1](item1, item2) + ret = node.operators[-1] ret._gen_ir() def action(node, res): if node.valid == True: - if type(node) == Var or type(node) == One or type(node) == Zero or type(node) == Ones or type(node) == Zeros or type(node) == Tensor: + if type(node) == Var or type(node) == Tensor: res.extend(node.decl) node.valid = False elif type(node) == TensorOp: @@ -351,7 +411,6 @@ def action(node, res): else: ret_compute.append(ir) - node.operators.append(ret) outer_loop.body.extend(ret_compute) node.decl.extend(ret_decl) node.compute.append(outer_loop) @@ -359,30 +418,23 @@ def action(node, res): replace_output(node.compute, ret.eval, node.eval) node.decl = [d for d in node.decl if d.dobject != ret.eval] + node.output_order = ret.output_order + elif node.op_type == 'aggr': node.operators[0]._gen_ir() # input tensor - node.operators[2]._gen_ir() # init node.operators[3]._gen_ir() # indices node.operators[4]._gen_ir() # axis axis = node.operators[4].eval.val size = helpers.get_ir_of_size(node._size()) node.eval = Ndarray(node.dtype, size) node.decl.append(Decl(node.eval)) + # this must be called after node.eval is constructed + node.operators[2]._gen_ir() # init - node.compute = [] - # initialize output - pre_loop = Loop(0, node.eval.size[0], 1, []) - node.compute.append(pre_loop) - res = bind(node.eval, pre_loop.iterate) - rhs = node.operators[2].eval - for i in range(1, len(node.eval.size)): - loop = Loop(0, node.eval.size[i], 1, []) - pre_loop.body.append(loop) - pre_loop = loop - res = bind(res, pre_loop.iterate) - rhs = bind(rhs, pre_loop.iterate) - pre_loop.body.append(Assignment(res, rhs)) + # node.compute.extend(node.operators[2].compute) + # node.decl.extend(node.operators[2].decl) + # node.operators[2].valid = False # compute outer_loop = Loop(0, node.operators[0].eval.size[axis], 1, []) @@ -397,12 +449,12 @@ def action(node, res): item2.decl = [] item1.decl = [] - ret = node.operators[1](item1, item2) + ret = node.operators[-1] ret._gen_ir() def action(node, res): if node.valid == True: - if type(node) == Var or type(node) == One or type(node) == Zero or type(node) == Ones or type(node) == Zeros or type(node) == Tensor: + if type(node) == Var or type(node) == Tensor: res.extend(node.decl) node.valid = False elif type(node) == TensorOp: @@ -421,7 +473,6 @@ def action(node, res): else: ret_compute.append(ir) - node.operators.append(ret) outer_loop.body.extend(ret_compute) node.decl.extend(ret_decl) node.compute.append(outer_loop) @@ -429,10 +480,17 @@ def action(node, res): replace_output(node.compute, ret.eval, item1.eval) node.decl = [d for d in node.decl if d.dobject != ret.eval] + + node.output_order = [(0, outer_loop)] + for i in range(len(ret.output_order)): + node.output_order.append((i+1, ret.output_order[i][1])) + # points from IR back to ASTNode for d in node.decl: d.astnode = node - for s in node.compute: - s.astnode = node + + if type(node) == TensorOp: + for s in node.compute: + s.astnode = node return node diff --git a/core/ir.py b/core/ir.py index ee9999a..387ba4a 100644 --- a/core/ir.py +++ b/core/ir.py @@ -47,10 +47,9 @@ def __init__(self, start, end, step, body: list): class Scalar(DOject): - def __init__(self, dtype: str, name: str = None, is_arg = False, val = None): + def __init__(self, dtype: str, name: str = None, is_arg = False): super().__init__(dtype, []) self.__name__ = name if name else f's{self.dobject_id}' - self.val = val self.is_arg = is_arg def name(self): return self.__name__ @@ -73,10 +72,9 @@ def __init__(self, start, stop, step): class Ndarray(DOject): - def __init__(self, dtype: str, size: tuple, name: str = None, is_arg = False, val = None): + def __init__(self, dtype: str, size: tuple, name: str = None, is_arg = False): super().__init__(dtype, size) self.__name__ = name if name else f'arr{self.dobject_id}' - self.val = val # val is None, 0, or 1 self.is_arg = is_arg def __getitem__(self, item): @@ -116,10 +114,6 @@ def __init__(self, dobject, idx): super().__init__(dobject.dtype, size) - - - - class Decl(IR): def __init__(self, dobject: (Scalar, Ndarray)): super().__init__() diff --git a/core/test/examples.py b/core/test/examples.py index b4577f7..75687e3 100644 --- a/core/test/examples.py +++ b/core/test/examples.py @@ -389,12 +389,27 @@ def test20(): def compression(): input = Tensor('input', (50, 32), dtype='float') res = (input * 1000).round() - res = res.apply(lambda x:x[1:32]-x[0:31], axis=0) + res = res.apply(lambda x:x[0:32]-x[-1:31], axis=0) res = res.abs().max(axis=1) code = codegen.cpu.print_cpp(gen_ir(res)) print(code) +def test_math1(): + input = Tensor('input', (50, 32), dtype='float') + res = input[0].abs() + code = codegen.cpu.print_cpp(gen_ir(res)) + print(code) + +def test_math2(): + input = Tensor('input', (50, 32), dtype='float') + input = input.setval(0) + res = input[0].abs() + code = codegen.cpu.print_cpp(gen_ir(res)) + print(code) + + + def apply_test1(): num_edges = 20 @@ -492,11 +507,10 @@ def apply_func2(item2): - def test_aggr1(): A = Tensor('A', (10, 20)) - indices = Tensor('idx', (30, ), dtype='int') - res = A.aggr_sum(indices) + indices = Tensor('idx', (A._size()[0], ), dtype='int') + res = A.aggr_max(indices) code = codegen.cpu.print_cpp(res._gen_ir()) print(code) @@ -716,10 +730,9 @@ def test26(): res = run.cpu.compile_and_run(code, A) print(res, torch.sum(A)) -def test27(): +def reduce_test1(): A = Tensor('a', (10, 20)) - init = Zeros(A[1]._size()) - ast = A.reduce(lambda a,b: a+b, init, axis=1) + ast = A.reduce(lambda a,b: a+b, lambda res: res.setval(0), axis=1) ir = gen_ir(ast) print(helpers.get_input_nodes(ir)) code = codegen.cpu.print_cpp(ir) @@ -729,7 +742,7 @@ def test27(): res = run.cpu.compile_and_run(code, A) print(res - torch.sum(A, dim=1)) -def test28(): +def reduce_test2(): A = Tensor('a', (10, 20, 5)) ast = A.sum(axis=1) ir = gen_ir(ast) @@ -740,6 +753,30 @@ def test28(): res = run.cpu.compile_and_run(code, A) print(res - torch.sum(A, dim=1)) + +def reduce_test3(): + A = Tensor('a', (10, 20, 5)) + ast = A.max(axis=1) + ir = gen_ir(ast) + print(helpers.get_input_nodes(ir)) + code = codegen.cpu.print_cpp(ir) + + A = torch.rand(10, 20, 5) + res = run.cpu.compile_and_run(code, A) + print(res - torch.max(A, dim=1).values) + + +def reduce_test4(): + A = Tensor('a', (10,)) + ast = A.max() + ir = gen_ir(ast) + print(helpers.get_input_nodes(ir)) + code = codegen.cpu.print_cpp(ir) + + A = torch.rand(10, ) + res = run.cpu.compile_and_run(code, A) + print(res - torch.max(A)) + def test29(): A = Tensor('A', (10, )) B = Tensor('B', (10, ), dtype='int') @@ -749,8 +786,37 @@ def test29(): print(code) +def conv1d_v1(): + A = Tensor('a', (100, )) + ast = A[0:97] + A[1:98] + A[2:99] + ir = gen_ir(ast) + print(helpers.get_input_nodes(ir)) + code = codegen.cpu.print_cpp(ir) + print(code) + +def conv1d_v2(width): + A = Tensor('a', (100, )) + res = Tensor('t', A[width:]._size()).setval(0) + for i in range(width): + res = res + A[i:i+97] + ir = gen_ir(res) + print(helpers.get_input_nodes(ir)) + code = codegen.cpu.print_cpp(ir) + print(code) + +def cmp_test(): + A = Tensor('a', (10, 20)) + B = Tensor('b', (10, 20)) + res = bigger(A, B) + + code = codegen.cpu.print_cpp(res._gen_ir()) + print(code) + + if __name__ == "__main__": + # conv1d_v1() + # conv1d_v2(3) # test1() # test2() # test3() @@ -770,16 +836,22 @@ def test29(): # test18() # test19() # test20() - compression() - # apply_test1() - # apply_test2() + # compression() + # test_math1() + # test_math2() + apply_test1() + apply_test2() # apply_test3() # apply_test4() + # reduce_test1() + # reduce_test2() + # reduce_test3() + # reduce_test4() # test_aggr1() - # test27() - # test28() # spmv() # test_einsum1() # apply_test2() # test_apply5() - # test27() \ No newline at end of file + # test27() + + # cmp_test() \ No newline at end of file diff --git a/helpers.py b/helpers.py index 40d9fce..6079b42 100644 --- a/helpers.py +++ b/helpers.py @@ -15,23 +15,19 @@ def _post_traverse(self, node, visited, res): else: visited.add(node) - if type(node) == Var or type(node) == One or type(node) == Zero: + if type(node) == Var: self.action(node, res) elif type(node) == Const: if node.dtype == 'slice': self._post_traverse(node.val.start, visited, res) self._post_traverse(node.val.stop, visited, res) self._post_traverse(node.val.step, visited, res) - elif type(node) == Tensor or type(node) == Ones or type(node) == Zeros: + elif type(node) == Tensor: for s in node.fix_size: self._post_traverse(s, visited, res) for s in node.ref_size: self._post_traverse(s, visited, res) self.action(node, res) - # elif type(node) == Set: - # self._post_traverse(node.storage, visited, res) - # self._post_traverse(node.nelem, visited, res) - # self.action(node, res) elif type(node) == TensorOp: for s in node.fix_size: self._post_traverse(s, visited, res) @@ -46,6 +42,18 @@ def _post_traverse(self, node, visited, res): for c in node.operators: self._post_traverse(c, visited, res) self.action(node, res) + elif type(node) == Set: + self._post_traverse(node.storage, visited, res) + for n in node.nelem: + self._post_traverse(n, visited, res) + self.action(node, res) + elif type(node) == SetOp: + self._post_traverse(node.storage, visited, res) + for n in node.nelem: + self._post_traverse(n, visited, res) + for c in node.operators: + self._post_traverse(c, visited, res) + self.action(node, res) def __call__(self, ast): visited = set() diff --git a/run/.tmp/cpu_code.cpp b/run/.tmp/cpu_code.cpp index c649bf4..3512403 100644 --- a/run/.tmp/cpu_code.cpp +++ b/run/.tmp/cpu_code.cpp @@ -1,29 +1,22 @@ #include -torch::Tensor reduce_a__zeros_0_c3_item1_of_a_item2_of_a(torch::Tensor obj_a) +torch::Tensor reduce_a___c2_item1_of_a_item2_of_a_add_item1_of_a_item2_of_a(torch::Tensor obj_a) { auto a = obj_a.accessor(); -torch::Tensor obj_zeros_0 = torch::zeros({20}, at::kFloat); -auto zeros_0 = obj_zeros_0.accessor(); -torch::Tensor obj_arr2 = torch::empty({10}, at::kFloat); -auto arr2 = obj_arr2.accessor(); -torch::Tensor obj_arr5 = torch::empty({10}, at::kFloat); -auto arr5 = obj_arr5.accessor(); +torch::Tensor obj_arr4 = torch::empty({10}, at::kFloat); +auto arr4 = obj_arr4.accessor(); for (int _l0 = 0; _l0 < 10; _l0 += 1) { -arr2[_l0] = zeros_0[_l0]; +arr4[_l0] = 0; } for (int _l1 = 0; _l1 < 20; _l1 += 1) { for (int _l2 = 0; _l2 < 10; _l2 += 1) { -arr5[_l2] = (arr2[_l2] + a[_l2][_l1]); +arr4[_l2] = (arr4[_l2] + a[_l2][_l1]); } -for (int _l3 = 0; _l3 < 10; _l3 += 1) { -arr2[_l3] = arr5[_l3]; } -} -return obj_arr2; +return obj_arr4; } PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { - m.def("run", &reduce_a__zeros_0_c3_item1_of_a_item2_of_a); + m.def("run", &reduce_a___c2_item1_of_a_item2_of_a_add_item1_of_a_item2_of_a); } \ No newline at end of file