video: https://www.bilibili.com/video/BV1YT411j7c2
notes_zh: https://mlc.ai/zh/chapter_auto_program_optimization/index.html
notes_en: https://mlc.ai/chapter_auto_program_optimization/index.html
ipynb: https://github.com/mlc-ai/notebooks/blob/main/6_Integration_with_Machine_Learning_Frameworks.ipynb
与机器学习框架的整合
前言
在过去的章节中,我们学习了机器学习编译的抽象和张量函数之间的变换。
本章将讨论如何将机器学习模型从现有的机器学习框架引入 MLC 流程。
准备工作
首先,我们导入必要的依赖项。
1 2 3 4 5 6 7 8 import tvmfrom tvm.ir.module import IRModulefrom tvm.script import tir as T, relax as Rfrom tvm import relaximport numpy as npfrom __future__ import annotations
1 2 3 4 import torchimport torch.nn as nnfrom torch import fxfrom torch.nn import functional as F
通过 Builder 构造 IRModule
在过去的章节中,我们一直在通过直接编写 TVMScript 来构建 IRModule。 随着模型变得越来越大,我们需要一种编程方式来构建 IRModule。在本节中,我们回顾一些支持该过程的工具。
从张量表达式构造 TensorIR
首先,我们回顾张量表达式 (tensor expression, TE) 这一领域特定语言来构建 TensorIR 函数。
我们首先创建一个 placeholder,它表示 TensorIR 函数的输入。
1 2 A = te.placeholder((128 , 128 ), name="A" , dtype="float32" ) B = te.placeholder((128 , 128 ), name="B" , dtype="float32" )
这里的每个输入和中间结果都表示为一个 te.Tensor
对象。
每个 te.Tensor
都有一个 shape 字段和 dtype 字段,用于记录计算的 shape 和数据类型。
我们可以通过一系列张量表达式来描述计算。这里的 te.compute
使用 te.compute(output_shape, fcompute)
这样的接口。fcompute 函数描述了我们要如何计算给定索引的每个元素 [i, j]
的值。
te_matmul
函数接受一个 te.Tensor
类型的对象,并返回矩阵乘法结果。请注意我们是如何根据 A 和 B 的输入 shape 构造计算的。te_matmul
适用于具有不同输入 shape 的 A 和 B。
1 2 3 4 5 6 def te_matmul (A: te.Tensor, B: te.Tensor ) -> te.Tensor: assert A.shape[1 ] == B.shape[0 ] n = A.shape[0 ] m = B.shape[1 ] k = te.reduce_axis((0 , A.shape[1 ]), name="k" ) return te.compute((n, m), lambda i, j: te.sum (A[i, k] * B[k, j], axis=k), name="matmul" )
我们可以使用 A 和 B 获得调用 te_matmul
的结果。
要创建 TensorIR 函数,我们可以调用 te.create_prim_func
并传入输入和输出值。
1 te.create_prim_func([A, B, C]).show()
我们可以用类似的方式为 ReLU 计算创建张量表达式。在这里,我们写一个可以适用于具有任何维度数量和 shape 的 te.Tensor
的 te_relu
函数。
1 2 def te_relu (A: te.Tensor ) -> te.Tensor: return te.compute(A.shape, lambda *i: te.max (A(*i), 0 ), name="relu" )
让我们在两种不同的输入维度数量和 shape 上尝试 te_relu
。 第一个 X1
的尺寸为 (10,)
。
1 2 3 X1 = te.placeholder((10 ,), name="X1" , dtype="float32" ) Y1 = te_relu(X1) te.create_prim_func([X1, Y1]).show()
然后是形状为 (10, 20)
的 X2
。
1 2 3 X2 = te.placeholder((10 , 20 ), name="X1" , dtype="float32" ) Y2 = te_relu(X2) te.create_prim_func([X2, Y2]).show()
te
API 允许我们做的另一件事是组合操作并创建“融合 (fused)”算子。例如,我们可以将 matmul 的结果再次应用 relu。
1 2 C = te_matmul(A, B) D = te_relu(C)
我们可以通过只传递感兴趣的输入和输出值,跳过中间值来创建一个 TensorIR 函数。 这将导致 matmul 的结果被分配为 TensorIR 函数中的临时空间。
1 te.create_prim_func([A, B, D]).show()
我们还可以将中间结果 C 传递到参数列表中。在这种情况下,TensorIR 函数希望我们也从调用方传入 C。通常我们建议只传入输入和输出,这样我们就可以在里面进行更高级的融合。
1 te.create_prim_func([A, B, C, D]).show()
使用 BlockBuilder 构造 IRModule
到目前为止,我们已经创建了一个 TensorIR 函数。 为了构建端到端的模型执行,我们还需要能够通过计算图连接多个 TensorIR 函数。
让我们首先创建一个 block builder,它可以帮助我们逐步构建一个 relax.Function
。
1 2 A = relax.Var("A" , (128 , 128 ), relax.DynTensorType(2 , "float32" )) B = relax.Var("B" , (128 , 128 ), relax.DynTensorType(2 , "float32" ))
我们通过创建 block builder 和一系列元张量函数来构造 Relax 函数。
1 2 3 4 5 6 7 8 9 10 11 bb = relax.BlockBuilder() with bb.function("main" ): with bb.dataflow(): C = bb.emit_te(te_matmul, A, B) D = bb.emit_te(te_relu, C) R = bb.emit_output(D) bb.emit_func_output(R, params=[A, B]) MyModule = bb.get() MyModule.show()
深入理解 BlockBuilder API
现在让我们深入了解 BlockBuilder 的 API。将 BlockBuilder 代码和生成的 IRModule 并排放置会很有帮助。
BlockBuilder 带有与 Relax 函数中相应的作用域。例如,bb.dataflow()
创建一个 dataflow block,其中所有对 BlockBuilder 的调用都处在 dataflow block 的作用域中。
1 2 3 with bb.function("main" ): with bb.dataflow():
每个中间结果都是一个 relax.Var
,对应一个存储计算结果的变量。 DataflowVar
表示该变量是 dataflow block(和计算图)内的中间步骤。
1 isinstance (C, relax.Var)
Relax 函数中的每一行都是由 emit_te
调用生成的。 例如,
1 lv = R.call_tir(te_matmul, (A, B), (128 , 128 ), dtype="float32" )
是由如下代码所生成。
1 C = bb.emit_te(te_matmul, A, B).
在幕后,bb.emit_te
做了以下事情:
为 A 和 B 创建一个输入 te.placeholder
,
通过 te_matmul
函数运行它们。
调用 te.create_prim_func
来创建一个 TensorIR 函数。
通过 call_tir
生成对函数的调用。
我们可以发现,上面 BlockBuilder 构造后的结果是一个有两个中间值的计算图,一个节点对应 te_matmul
操作,另一个节点对应 te_relu
。
我们可以通过 bb.emit_output
创建每个 dataflow block 的输出变量。
1 2 3 with bb.dataflow(): ... R = bb.emit_output(D)
上面的代码标志着 D
是一个可以在 dataflow block 之外引用的变量。
最后,函数输出由 bb.emit_func_output
标记。 我们只能在每个函数作用域内调用一次 emit_func_output
。
值得注意的是,我们可以在输出阶段指定函数的参数列表。 这样做在我们动态收集参数列表的情况下会有帮助。
1 2 3 4 with bb.function("main" ): ... bb.emit_func_output(R, params=[A, B])
或者,我们也可以在函数范围的开头指定参数列表。
1 2 3 4 with bb.function("main" , params=[A, B]): ... bb.emit_func_output(R)
从 PyTorch 导入模型
现在我们已经学习了以编程方式构建 IRModule 的工具。 让我们使用它们将机器学习模型从 PyTorch 导入成为 IRModule。
大多数机器学习框架都带有计算图抽象,其中每个节点对应一个操作,边对应它们之间的依赖关系。 我们将采用 PyTorch 模型,获取 PyTorch 原生格式的计算图,并将其转换为 IRModule。
让我们从在 PyTorch 中定义一个模型开始。 为了使示例保持一致,我们将使用 matmul + ReLU 示例。
1 2 3 4 5 6 7 8 9 class MyModel (nn.Module): def __init__ (self ): super (MyModel, self).__init__() self.weight = nn.Parameter(torch.randn(128 , 128 )) def forward (self, x ): x = torch.matmul(x, self.weight) x = torch.relu(x) return x
创建 TorchFX GraphModule
我们使用 TorchFX 来表示来自 PyTorch 的模型的计算图。
1 2 3 model = MyModel() fx_module = fx.symbolic_trace(model) type (fx_module)
fx_module
包含一个简单的计算图,可以打印成表格便于查看。我们的目标是将此图转换为 IRModule。
1 fx_module.graph.print_tabular()
构造映射函数
让我们定义整体的翻译逻辑。 主要流程如下:
创建一个 node_map
,将 fx.Node
映射到相应的 relax.Var
,该 relax.Var
代表 IRModule 中的已翻译节点。
以拓扑顺序迭代 FX 图中的节点。
给定映射输入,获取节点的映射输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 def map_param (param: nn.Parameter ): ndim = len (param.data.shape) return relax.const( param.data.cpu().numpy(), relax.DynTensorType(ndim, "float32" ) ) def fetch_attr (fx_mod, target: str ): """Helper function to fetch an attr""" target_atoms = target.split('.' ) attr_itr = fx_mod for i, atom in enumerate (target_atoms): if not hasattr (attr_itr, atom): raise RuntimeError(f"Node referenced nonexistant target {'.' .join(target_atoms[:i])} " ) attr_itr = getattr (attr_itr, atom) return attr_itr def from_fx (fx_mod, input_shapes, call_function_map, call_module_map ): input_index = 0 node_map = {} named_modules = dict (fx_mod.named_modules()) bb = relax.BlockBuilder() fn_inputs = [] fn_output = None with bb.function("main" ): with bb.dataflow(): for node in fx_mod.graph.nodes: if node.op == "placeholder" : shape = input_shapes[input_index] input_index += 1 input_var = relax.Var( node.target, shape, relax.DynTensorType(len (shape), "float32" ) ) fn_inputs.append(input_var) node_map[node] = input_var elif node.op == "get_attr" : node_map[node] = map_param(fetch_attr(fx_mod, node.target)) elif node.op == "call_function" : node_map[node] = call_function_map[node.target](bb, node_map, node) elif node.op == "call_module" : named_module = named_modules[node.target] node_map[node] = call_module_map[type (named_module)](bb, node_map, node, named_module) elif node.op == "output" : output = node_map[node.args[0 ]] assert fn_output is None fn_output = bb.emit_output(output) bb.emit_func_output(output, fn_inputs) return bb.get()
我们没有在 from_fx
函数中定义函数映射。 我们将通过映射提供每个 torch function 的翻译规则。 具体来说,以下代码块显示了我们如何通过 emit_te
API 做到这一点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def map_matmul (bb, node_map, node: fx.Node ): A = node_map[node.args[0 ]] B = node_map[node.args[1 ]] return bb.emit_te(te_matmul, A, B) def map_relu (bb, node_map, node: fx.Node ): A = node_map[node.args[0 ]] return bb.emit_te(te_relu, A) MyModule = from_fx( fx_module, input_shapes = [(1 , 128 )], call_function_map = { torch.matmul: map_matmul, torch.relu: map_relu, }, call_module_map={}, ) MyModule.show()
回到 FashionMNIST 的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import torchimport torchvisiontest_data = torchvision.datasets.FashionMNIST( root="data" , train=False , download=True , transform=torchvision.transforms.ToTensor() ) test_loader = torch.utils.data.DataLoader(test_data, batch_size=1 , shuffle=True ) class_names = ['T-shirt/top' , 'Trouser' , 'Pullover' , 'Dress' , 'Coat' , 'Sandal' , 'Shirt' , 'Sneaker' , 'Bag' , 'Ankle boot' ] img, label = next (iter (test_loader)) img = img.reshape(1 , 28 , 28 ).numpy()
1 2 3 4 5 6 7 8 9 import matplotlib.pyplot as pltplt.figure() plt.imshow(img[0 ]) plt.colorbar() plt.grid(False ) plt.show() print ("Class:" , class_names[label[0 ]])
1 2 !wget -nc https://github.com/mlc-ai/web-data/raw/main/models/fasionmnist_mlp_params.pkl
以上是我们关心的模型,我们可以按如下的方式构建其 PyTorch 模型。
1 2 3 4 5 6 7 8 9 10 11 12 class MLP (nn.Module): def __init__ (self ): super (MLP, self).__init__() self.linear0 = nn.Linear(784 , 128 , bias=True ) self.relu = nn.ReLU() self.linear1 = nn.Linear(128 , 10 , bias=True ) def forward (self, x ): x = self.linear0(x) x = self.relu(x) x = self.linear1(x) return x
1 2 3 4 5 6 7 8 import pickle as pklmlp_model = MLP() mlp_params = pkl.load(open ("fasionmnist_mlp_params.pkl" , "rb" )) mlp_model.linear0.weight.data = torch.from_numpy(mlp_params["w0" ]) mlp_model.linear0.bias.data = torch.from_numpy(mlp_params["b0" ]) mlp_model.linear1.weight.data = torch.from_numpy(mlp_params["w1" ]) mlp_model.linear1.bias.data = torch.from_numpy(mlp_params["b1" ])
1 2 3 4 torch_res = mlp_model(torch.from_numpy(img.reshape(1 , 784 ))) pred_kind = np.argmax(torch_res.detach().numpy(), axis=1 ) print ("Torch Prediction:" , class_names[pred_kind[0 ]])
让我们尝试通过为相应的 nn.Module
定义映射函数来从 FX 转换。 在这里,我们重用了来自 TVM TOPI (TVM operator inventory) 的预定义 TE 库,而不是定义我们自己的张量表达式。
topi.nn.dense(x, w)
执行转置矩阵乘法 x @ w.T
topi.add
执行广播加法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from tvm import topidef map_nn_linear (bb, node_map, node, nn_mod ): x = node_map[node.args[0 ]] w = map_param(nn_mod.weight) if nn_mod.bias is not None : b = map_param(nn_mod.bias) y = bb.emit_te(topi.nn.dense, x, w) return bb.emit_te(topi.add, y, b) def map_nn_relu (bb, node_map, node, nn_mod ): return map_relu(bb, node_map, node) MLPModule = from_fx( fx.symbolic_trace(mlp_model), input_shapes = [(1 , 784 )], call_function_map={ }, call_module_map={ torch.nn.Linear: map_nn_linear, torch.nn.ReLU: map_nn_relu, }, ) MLPModule.show()
1 2 3 4 5 6 7 8 ex = relax.vm.build(MLPModule, target="llvm" ) vm = relax.VirtualMachine(ex, tvm.cpu()) data_nd = tvm.nd.array(img.reshape(1 , 784 )) nd_res = vm["main" ](data_nd) pred_kind = np.argmax(nd_res.numpy(), axis=1 ) print ("MLPModule Prediction:" , class_names[pred_kind[0 ]])
备注:翻译成高层算子
在大多数机器学习框架中,有时先转换为更高一级的内置的原始算子会更有帮助。下面的代码块给出了一个例子来做到这一点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 def map_nn_relu_op (bb, node_map, node, nn_mod ): A = node_map[node.args[0 ]] return bb.emit(relax.op.relu(A)) def map_nn_linear_op (bb, node_map, node, nn_mod ): x = node_map[node.args[0 ]] w = map_param(nn_mod.weight) if nn_mod.bias is not None : b = map_param(nn_mod.bias) y = bb.emit(relax.op.dense(x, w)) return bb.emit(relax.op.add(y, b)) MLPModuleHighLevel = from_fx( fx.symbolic_trace(mlp_model), input_shapes = [(1 , 784 )], call_function_map={ }, call_module_map={ torch.nn.Linear: map_nn_linear_op, torch.nn.ReLU: map_nn_relu_op, }, ) MLPModuleHighLevel.show()
上面展示了我们使用那些内置的算子将模型导入为 IRModule 后的结果。这些内置算子是 比 TensorIR 函数更高级别的抽象 。我们可以有不同的机会将这些原始算子进一步转换为库函数或 TensorIR 函数。
在大多数情况下,在有高级算子支持的情况下,转换为高级内置函数会很有帮助。但是,有很多情况下我们找不到对应的高级内置算子或者想直接指定 TensorIR 函数。 在这些情况下,我们可以自定义翻译逻辑或变换从而生成 call_tir
或调用库函数。 通常,我们可以结合高级操作、TensorIR 和库抽象来获得最佳结果。 我们将在后续章节中讨论权衡取舍。
讨论
在本章中,我们重点关注了 MLC 流程的 开发 部分。 我们研究了从机器学习框架中获取模型到 IRModule 的不同方法。 我们还简要介绍了高级原始运算符。
一旦我们将模型放入 IRModule 中,我们就可以在原始函数和计算图函数上引入更多种类的变换。一个好的 MLC 流程将这些转换组合在一起,形成最终部署形式。
总结
张量表达式 API 允许我们创建原始的 TensorIR 函数。
BlockBuilder API 通过 emit_te
和其他函数创建 IRModule。
通过将模型转换为 IRModule,实现与现有的机器学习框架的整合。