zoukankan      html  css  js  c++  java
  • 桥接PyTorch和TVM

    桥接PyTorch和TVM

    人工智能最引人入胜的一些应用是自然语言处理。像BERT或GPT-2之类的模型及其变体,可以获住足够多的文本信息。

    这些模型属于称为Transformers的神经网络类体系结构。 HuggingFace transformers library是实现最受欢迎的库之一。

    与已经高度优化的实现的卷积模型或LSTM相比,对于Transformers而言,情况并非如此。本文探索TVM如何填补空白。分两个步骤进行操作:

    • 首先,在TVM上,使用BERT inference推理和调优。
    • 其次,进行一些更基本的探索,以了解如何在PyTorch中使用TVM进行训练。考虑到实验性质,将重点更多地放在可行性上,而不是性能上。
    • 使用TVM优化BERT推理

    如何将BERT从Transformers库传输到TVM?

    有用的是,transformers支持使用PyTorch JIT跟踪其模型,直到建立跟踪模型。

    使用示例输入在AMD Radeon VII上进行100次运行后,PyTorch跟踪模型大约需要0.65-0.7秒,意味着每次运行6.5-7ms。看看是否可以使用TVM更快。将模型转换为TVM轻而易举:

    shape_list = [(i.debugName().split('.')[0], i.type().sizes()) for i in  list(traced_model.graph.inputs())[1:]]

     

    mod_bert, params_bert = tvm.relay.frontend.pytorch.from_pytorch(traced_model,

                            shape_list, default_dtype="float32")

    找不到dtype信息将有一些警告,但是一切顺利!构建并运行,构建遵循标准的TVM配置。将PyTorch(cpu)张量转换为TVM数组。

    target = 'rocm -model=gfx906'  # use what matches your GPU

     

    target_host = 'llvm'

    ctx = tvm.context(target)

     

    tt_a = tvm.nd.array(tokens_tensor.numpy(), ctx)

    st_a = tvm.nd.array(segments_tensors.numpy(), ctx)

    tvm.relay.backend.compile_engine.get().clear() # just to be sure, see https://github.com/apache/incubator-tvm/pull/5724

     

    with tvm.transform.PassContext(opt_level=3):

            graph, lib, params = tvm.relay.build(mod_bert,

                                         target=target,

                                         target_host=target_host,

                                         params=params_bert)

    module = tvm.contrib.graph_runtime.create(graph, lib, ctx)

    这会警告几次:

        WARNING:autotvm:Cannot find config for ... batch_matmul.cuda .... A fallback configuration is used, which may bring great performance regression.

    可能会带来很大的性能下降

    但是首先运行模型,看看输出是否匹配:

        (8.583069e-06, 8.493662e-07)

    看起来不错。记住,在float32中进行计算,因此$ 10 ^ {-6-6 $$ ish是一个很好的结果。

    建立模型并设置参数后,按以下方式对模型进行计时:

    def x():

        for i in range(100):

            module.run()

        ctx.sync()

    x()

    %timeit x()

    该模型每100次运行需要6.65s,或者模型运行67ms。确实很慢,警告说找不到(调整的)配置。调整调度。

    调整确实需要半天左右的时间(在TVM调整,进行有关使用autotvm进行ResNet调整的介绍。)

    此后,可以再次使用新配置来构建模型。这次应该看不到有关缺少配置的任何评论。现在,每次运行的时间约为6.5-7毫秒,类似于PyTorch。从最基本的算子优化中得到的结果。不过,可以将其进一步推进。

    要了解如何操作,深入研究BERT建模和TVM。

    如果不想获取完整的详细信息,请跳过下一部分并向下滚动到Results。补充一点,希望调优部分会在某种意义上过时,因为在不久的将来,可以立即获得改进,或者至少在进行一些初始调优后,速度会更好。因此,如果看不到这里和Results之间的加速,那是因为在提交补丁时做了功课。

    BERT模型

    让仔细看看BERT中的情况。

     像许多深度学习模型一样,BERT带有一些前言(词汇嵌入)和结语(池化),并且大部分被组织成相似的块,这里有12个BertLayer模块。该attention_mask防止BERT在与问题打交道时看答案。

    因此,放大并详细查看BertLayer,这最终是需要快速完成的工作。正如在网络图中看到的那样,BertLayer模块的主要部分是一个子模块BertSelfAttention。

     现在,BertSelfAttention捕捉到了著名的自我关注机制,这是Transformers模型的标志。(不能推荐Sascha Rush的带注释的Transformers来作为详细的演练。)

    BertLayer细节

    如果要详细介绍,应该单独运行一个BertLayer。获取BertLayer的输入,然后BertLayer像对整个模型一样,将单个转换为TVM。

    为了查看TVM模块,定义了一个小的可视化帮助器(基于TVM PR#4370)。

    import graphviz

    def visualize(expr, collapse_small=True, node_attr_dict = {}):

        def collect_ops(node):

            ops = set()

            def visitor(e):

                if isinstance(e, tvm.ir.Op):

                    ops.add(e.name)

            tvm.relay.analysis.post_order_visit(node, visitor)

            return ops

     

        # node_dict maps a Relay node to an index (node ID)

        def _traverse_expr(node, node_dict):

            if node in node_dict:

                return

            node_dict[node] = len(node_dict)

     

        node_dict = {}

        tvm.relay.analysis.post_order_visit(expr, lambda x: _traverse_expr(x, node_dict))

     

        relayviz_nodes = []

     

        dot = graphviz.Digraph(format='svg', )

        dot.attr('node', shape = 'box')

     

        def to_str(node):

            if isinstance(node, tvm.relay.Constant):

                return repr(node).lstrip('Constant(')[:-1]

            else:

                raise NotImplementedError("to_str:" + repr(node))

     

        def is_small_const(c):

            if not (collapse_small and isinstance(c, tvm.relay.Constant)):

                return False

            if isinstance(c.data, tvm.runtime.ndarray.NDArray):

                return numpy.prod(c.data.shape) < 10

            return True

     

        # Sort by node ID

        for node, node_id in sorted(node_dict.items(), key=lambda x: x[1]):

            if isinstance(node, tvm.relay.Function):

                dot.node(str(node_id), 'Function', **node_attr_dict.get(node, {}))

                dot.edge(str(node_dict[node.body]), str(node_id))

            elif isinstance(node, tvm.relay.Var):

                if node.type_annotation is not None:

                    if hasattr(node.type_annotation, 'shape'):

                        shape = tuple([int(x) for x in node.type_annotation.shape])

                        dtype = node.type_annotation.dtype

                        typstr = 'Tensor[{}, {}]'.format(shape, dtype)

                    else:

                        typstr = str(node.type_annotation)

                else:

                    typstr = '?'

                d = dict(shape = 'ellipse')

                d.update(node_attr_dict.get(node, {}))

                dot.node(str(node_id),

                         '{}: {}'.format(

                             node.name_hint, typstr

                         ), **d)

            elif isinstance(node, tvm.relay.Tuple):

                dot.node(str(node_id), 'Tuple[...])', **node_attr_dict.get(node, {}))

                for field in node.fields:

                    dot.edge(str(node_dict[field]), str(node_id))

            elif isinstance(node, tvm.relay.Constant):

     

                if not is_small_const(node): # small consts are shown in ops

                    dot.node(str(node_id), 'Constant({}, {})'.format(node.data.shape, node.data.dtype),

                            **node_attr_dict.get(node, {}))

            elif isinstance(node, tvm.relay.Call):

                args_with_edge = []

                arg_str_list = []

                for arg in node.args:

                    if is_small_const(arg):

                        arg_str_list.append(to_str(arg))

                    else:

                        arg_str_list.append('·')

                        args_with_edge.append(arg)

                arg_str = ', '.join(arg_str_list)

                if isinstance(node.op, tvm.ir.Op):

                    name = node.op.name

                    attrs = {k:getattr(node.attrs, k) for k in node.attrs.keys()} if hasattr(node.attrs, 'keys') else {}

                    #attrs = inspect.getmembers(node.attrs)

                    attr_str_list = [k+'='+(str(v) if len(str(v))<20 else "...") for k, v in attrs.items()]

                    if attr_str_list:

                        attr_str = '| '+ ', '.join(attr_str_list)

                    else:

                        attr_str = ''

                else:

                    ops = collect_ops(node)

                    if ops:

                        name = '_'.join(ops)

                    else:

                        name = '...'

                    attr_str = ''

                s = f'{name}({arg_str}{attr_str})'

                dot.node(str(node_id), s, **node_attr_dict.get(node, {}))

                for arg in args_with_edge:

                    dot.edge(str(node_dict[arg]), str(node_id))

            elif isinstance(node, tvm.ir.Op):

                # dot.node(str(node_id), 'Op {}'.format(node.name))

                pass # covered in call

            elif isinstance(node, tvm.relay.TupleGetItem):

                dot.node(str(node_id), 'TupleGetItem(idx={})'.format(node.index), **node_attr_dict.get(node, {}))

                dot.edge(str(node_dict[node.tuple_value]), str(node_id))

            elif isinstance(node, tvm.relay.Let):

                dot.node(str(node_id), 'Let(XX)', **node_attr_dict.get(node, {}))

                dot.edge(str(node_dict[node.value]), str(node_id))

                dot.edge(str(node_id), str(node_dict[node.var]))

            else:

                raise RuntimeError(

                    'Unknown node type. node_id: {}, node: {}'.format(node_id, type(node)))

     

        return dot

     

    在主要功能上运行。由于某种原因(可能是完全笼统),PyTorchtransformers会将Linear图层转换为batch_matmul,而不是dense。由于TVMbatch_matmul在两个算子上都有scale(与PyTorch不同),也有很多转置算子。

    visualize(mod['main'])

     

     除了命名输入外,还看到许多未命名(编号)的变量。这些是神经网络参数。

    编译模型

    就像完整模型一样,可以在检查子模块计算出相同数量后运行并计时。

    100次运行需要20.2毫秒。包络计算的背后是,BertLayer在PyTorch中,在这一层上花费了约0.2ms,在12层上花费了约2.4ms-不是大多数,而是整个6-7ms runtime中的一部分。与TVM进行比较。(一个好的规则是永远不要进行优化,也不进行测量。)

    TVM的时钟runtime为18.2ms,可运行100次。再次与PyTorch大致相同。

    从图片中看到的一件事是输入被重塑了3次。有一个TVM优化阶段调用,称为“公共子表达式消除”(CSE),结合了三种重塑形式。(前一阵子没有成功,具有不同的形状参数, TVM开发人员在动态到静态转换过程中解决了这个问题。),模型参数被重塑和转置了。也可以摆脱吗?是的。为此,将首先绑定参数,即将其放入模型中。然后,参数已变为常量,而不是输入节点。通过该Foldconstant过程,可以通过transposes和reshapes传播常数,以使更靠近matmuls。

    在这三个之后(当编译中继模型时,TVM将执行此算子),模型如下所示:

    将具有相同输入的三个批处理matmul合并为一个更有效batch_matmul。在TVM PR 5791中实施了此操作。还有另一个恒定折叠的传递。

    new_mod = tvm.relay.transform.CombineParallelBatchMatmul()(new_mod)

    new_mod = tvm.relay.transform.FoldConstant()(new_mod)

    visualize(new_mod["main"])

    经过检查,仍然得到相同的结果。可以再次计时:100次运行需要25.2毫秒。再次有点慢,需要调整新的形状。调整后,在100次运行中处于12.6ms,从大约0.2ms变为大约0.13-0.15ms,这是一个不错的加速。通过手工计算,这应该从总运行时间中减少0.6-0.8ms,或者说介于5%-10%之间,检查。

    优化后对整个BERT模型的结果

    定义一个函数,结合上面的优化过程,然后在整个BERT模型上运行。进行与上述相同的训练。

    可以达到624毫秒进行100次运行。在PyTorch中从6.5-7ms缩短到了TVM中的6.2ms。这是5%-10%的加速。仅采用了特定的形状,而不是很大的形状。更多的分析,将考虑更多的问题形式。

    可能会更进一步-例如,在批处理matmul之后,通过处理重塑来融合添加的内容,现在暂时将其保留。此外,还将受益于TVM的进一步改进,因此,随着时间的推移,基准会如何提高也会很有趣。特别地,即将到来的Ansor调整机制,似乎很有希望。

      比较模型的实现

    一直将PyTorch与TVM输出进行比较,查看是否良好。另外,当研究某个内层时,获取了该内层的输入,转换并输入到TVM模型中。这是一种非常有效的技术。

    有时很难评估结果之间的偏差是由于数值精度,还是由于某处的误差。最初转换模型时,SelfAttentionTVM模型,将子模块输出复制到大约1e-6。但是,BertLayer转换的内容类似于1-e3。不确定是由于累积的数字误差,还是某些地方的材料偏差引起的。(原来是GELU激活,已转换为FastGELU。)

    在这种情况下,想做的一件事,跳转到双精度并检查。数值误差应该变得更小,而其它偏差将保持相同的数量级。使用PyTorch前端,如果传递default_dtype="float64"给转换函数,可以在PyTorch端跟踪转换为float64的模型。

    运行模块并与PyTorch进行比较,应该有1e-14左右的偏差。

     TVM的改进

    必须弥合一些差距(git checkout包括所有差距):

    • TVM PyTorchtransformers不支持fp32以外的输入。实施了改进的转换,也包括在TVM升级中。
    • TVM调度(即main操作的计算组织,batch_matmul)是固定的,非常慢(类似于现在没有经过调整的调度运行)。因此,实施了可调度的调度表
    • PyTorchtransformers生成批量matmul操作(也可以将其更改为生成密集层)。较大的速度优势之一,将查询key和value线性层组合,实现了对批处理matmul操作的融合
    • 当比较计算结果时,注意到GELU函数已转换为其FastGELU变体。(TVM中有一个快速的数学优化过程,可以对误差函数进行一些替换,尽管没有检查,是否对用误差函数表示的GELU产生FastGELU。)
    • TVM最初(并且在某种程度上仍然)专注于静态形式。尝试动态算子,动态重塑(以目标形式作为参数)是这些实验的早期阶段,由于通用子表达式消除过程未检测到可以合并相同的输入重塑,阻止了批处理的融合,情况有所改善。
    • 使用TVM计算训练Pytorch模型

    看看在PyTorch中训练BERT时,是否可以使用TVM。当需要处理自动分化时,这将打开全新的worms。从上面保留 theme,并以BertLayer示例为例,但方法通常代表着 non-trivial 模块。希望将训练期间的计算转移到TVM。

     用户可以采用一个(可跟踪的)模块并执行

    add_tvm_dispatch(module, sample_input)

    如果用与sample_input形状相同的输入来调用模块,将获得TVM计算的输出(当然是PyTorch张量),否则,将使用常规正向。

    如何实现这些任务。仍将无法获得很大的提速。

    通过运行BertLayer从TransformersBert模型到的跟踪,得到了relay模型tvm.relay.frontend.from_pytorch。

    在这之间,要做的一件事是将PyTorch中的模块化接口(带有命名参数)转移到功能接口(TVM可以为做的事情)。按照可以使用的顺序排列函数参数-即首先以直接的方式输入模块,然后与PyTorch使用相同的顺序来传递参数。完成此操作后,BertLayer 在TVM中的性能如下:

     就像在BERT推理中一样,运行一些优化过程。

    进行一些新的转换:

    • Autodifferentiation的一个特殊之处,将使用大量..._like算子来广播或“取消广播”(总和是广播带有autodifferentiation的对偶)。现在有两个张量参数,后者实际上并不需要梯度。ZappLike将这些算子替换为带有shape参数的相应函数。
    • 另一件事是派生类。TVM生成一个张量,所有张量都具有与函数的返回值相同的形状,以此作为链法则的起点。将这些乘以算子的派生产品。相乘并没有多大作用,类似地,TVM将变量(输入)的梯度初始化为相同形式的零。如果未使用,则渐变将为零,但如果使用,则“真实渐变”将添加到零。但是添加零也可以消除。这些由ZeroZapp和OneZapp负责。
    • TVM没有针对LayerNorm(BatchNorm或其它)的训练变体。实现了通过以阐明计算的过程。
    • TVM也没有训练停止。由于TVM当前没有随机数,在某种程度上很难解决。取而代之的是,用一个采用随机bernoulli抽取(值为0/1的值)的构造替换掉落对象,并以此模拟掉落对象。将使用PyTorch为生成版本。这样做还有一个好处,即(如果以与PyTorch相同的顺序生成滤除版本),将获得完全相同的结果。

    正如上面所暗示的,TVM的梯度获取,假定是计算中的最后一个元素(上面讨论的“张量”)。这与PyTorch的模块化视图不太吻合,因为PyTorch希望grad_out每个输出都给出一个。令人高兴的是,这在计算上等效于乘以渐进求和,以此来修改函数。希望具有灵活性,允许两个函数都返回一个张量,而两个函数都返回一个张量元组。

    应用这些修改后,模型如下所示:

    随着let节点数量的增加,使用ToGraphNormalForm传递,将其恢复为正常形式。TVM的渐变获取返回一个函数,该函数具有与原始函数相同的参数(在本例中,使用grad_out和修改),然后返回原始返回值的元组和包含所有输入的梯度的元组。要做的第一件事就是放下所有的渐变色grad_out和dropout不需要。然后,运行简化流程。

    因此,这是向前和向后的图表:

    在PyTorch中,首先计算前向,然后计算后向,必须取出并拆分图形。困难的问题之一,如何处理为正向和反向计算的事物。这是一个与MinCut问题有关的难题。

    极端选择可能是:

    • 只能保留输入并根据需要重新计算所有内容。
    • 如果有一个Salal输出,可以计算梯度并与后面的后一层的导数相乘。(损失函数可能会这样做。)但是,这不适用于非标量张量输出。

    执行以下算子:通常计算正向,但保留所有将在向后使用。这很可能是没有看到端到端加速的原因。下面讨论一些潜在的启发式方法。

    在这里使用一种颜色。首先,将正向计算的所有节点都涂成红色。然后,遍历梯度计算,然后从向后的蓝色为所需的节点着色。在可视化中展示属性支持。

    一点(PyTorch)术语:当有一个功能Layer:x y,然后是一些Loss:y l,向后是BackwardOfLayer:grad _outgrad _ingrad _out = dl / dy和* grad _in = dl / dx`。

    拆分功能,收集蓝色节点,进行捕获-但是常量将被复制,并且输入(Var节点)需要分别处理。现在可以拆分向后,用变量替换所有蓝色节点。

    接下来,进行转发并进行修改,以返回所需的中间体。前向看起来像这样:

    TVM无法返回嵌套元组,将函数中的输出展平。再次,区分张量值函数和元组值函数(那些可能返回多个张量的函数)。

    最后,可以让TVM发挥其魔力并编译功能,对gr_only_compiled_module 和fw_and_cap_compiled_module,定义了便利函数,可以在PyTorch和TVM之间移动张量,并以TVM字典的形式获取模型参数。

    def tensor_to_tvm(t):

        return tvm.nd.from_dlpack(torch.utils.dlpack.to_dlpack(t))

    def tensor_from_tvm(a):

        return(torch.utils.dlpack.from_dlpack(a.to_dlpack()))

     

    model_params_tvm = {k: tensor_to_tvm(v) for k, v in pytorch_model.state_dict().items()}

    同样,在PyTorch和TVM中的GPU上获得输入。

    记录的三个随机抽取的发生顺序与模型中顺序相同。在计算图上进行了深度优先搜索,如果dropout的值连接在图中而不是位于独立的分支上,也是PyTorch绘制矩阵的顺序。

    torch.manual_seed(12345)

    drop_c = {}

    for k in dropout_info.keys(): # we don't know the order

        p, typ = dropout_info[k]

        drop_c[k] = torch.nn.functional.dropout(torch.ones([int(i) for i in typ.shape],

                                                  dtype=getattr(torch, typ.dtype), device="cuda"), p=p)*(1-p)

     

    drop_tvm = {n: tensor_to_tvm(t) for n, t in drop_c.items()}

    现在可以前进了。

    fw_and_cap_compiled_module.set_input('input', inp_tvm[0])

    fw_and_cap_compiled_module.set_input('attention_mask', inp_tvm[1])

    fw_and_cap_compiled_module.set_input(**model_params_tvm)

    fw_and_cap_compiled_module.set_input(**drop_tvm)

    fw_and_cap_compiled_module.run()

    可以将输出与PyTorch的输出进行比较:

    torch.manual_seed(12345)

    pytorch_model.train()

    res = pytorch_model(*inp_c)[0]

    numpy.abs(fw_and_cap_compiled_module.get_output(0).asnumpy()-res.detach().cpu().numpy()).max()

    这给了2.1457672e-06。

    非常好。让也尝试向后。生成一个grad_out,设置所有变量,并运行向后模型

    gr_out_c = torch.randn(res.shape, device="cuda", dtype=res.dtype)

    num_captures = len(capture_vars)

    num_regular_outputs = len(fw_and_cap_fn_flattened.body.fields) - num_captures

    captured_values = {v.name_hint: fw_and_cap_compiled_module.get_output(num_regular_outputs + i) for i, v in enumerate(capture_vars)}

     

    gr_only_compiled_module.set_input(**drop_tvm)

    gr_only_compiled_module.set_input(**model_params_tvm)

    gr_only_compiled_module.set_input(**captured_values)

    gr_only_compiled_module.set_input('gr:out:0', tensor_to_tvm(gr_out_c))

    gr_only_compiled_module.run()

    在PyTorch方面,最简单的方法是重新运行前向(记住要重置随机种子),并获取证书。

    torch.manual_seed(12345)

    pytorch_model.train()

    inp_c_rq = [i.requires_grad_() for i in inp_c]

    for p in pytorch_model.parameters():

        p.requires_grad_()

    res = pytorch_model(*inp_c_rq)[0]

    grads_pt = torch.autograd.grad(res, inp_c_rq + list(pytorch_model.parameters()), gr_out_c, allow_unused=True)

     

    奏效了吗?看来是这样的:

     for i, g_pt in enumerate(grads_pt):

        print(numpy.abs(gr_only_compiled_module.get_output(i).asnumpy() - g_pt.cpu().numpy()).max())

    给列出了1e-5ish范围内的数字。

    但是想在PyTorch中运行一些东西,对吗?

    遵循PyTorch的工作方式,首先定义一个autograd.Function刚刚手动完成的操作:

    在forward:

    • 产生随机值,
    • 向前运行,
    • 记录向后所需的catch,输入和丢失值。

    在中backward,向后运行并返回结果(作为PyTorch张量)。

    这样,得到了一个PyTorch autograd.Function,调用TVM(需要一个小的package)。

    需要做的add_tvm_dispatch(module, sample_inputs)就是跟踪模块,从中创建基于TVM的autograd函数,然后替换调用该函数的正向调用(使用参数)(如果适用)或退回常规方法。向前,Python的无限动力使这种相对容易。由于所有这些都不是真正与TVM相关的,提供了一些保留。

    性能

    就性能而言,并不是一个最终想要达到的目标。调整任务后(以及来自HuggingFace BERT + PyTorch JIT不太实际的推理示例),向前和向后运行了100次启用TVM的BertLayer迭代,类似于为推理所做的方式。一次迭代通过TVM花费6.2毫秒,而在PyTorch上花费1.3毫秒。

    可以通过TVM运行模型。但这还不如通常的方法快。

    更严重的是,有两条直接的途径来提高性能:

    • 查找一组更好的捕获节点。
    • 在TVM图上找到优化。

    就前者的启发式(记住它很可能是NP困难的,没有得出正式的认证),人们会想重新进行廉价的计算,最主要的是逐点计算(或者除了matmul之外,什么都可以?)。

    人工智能芯片与自动驾驶
  • 相关阅读:
    8.用户注销
    7.用户登陆,用户退出,记住用户名和密码
    6.后台验证码-session作用域
    5.验证用户名是否已经被注册:AJAXC请求
    4.前端注册表单验证 && 表单回填
    3.注册后台处理逻辑编写
    HTTP Status 500
    jquery之stop()的用法
    angular.forEach
    jquery如何获取第一个或最后一个子元素?
  • 原文地址:https://www.cnblogs.com/wujianming-110117/p/14800523.html
Copyright © 2011-2022 走看看