• 再改RNNs,试用While循环不展开特性


    RNNs的循环问题

    上次改RNNs虽然保证了正确性,但是还是遗留了一个问题。这里直接截取核心的一段:

    1. ......
    2. def recurrent(self, x, h_0, w_ih, w_hh, b_ih, b_hh):
    3. time_step = x.shape[0]
    4. outputs = []
    5. t = 0
    6. h = h_0
    7. while t < time_step:
    8. h = self.cell(x[t], h, w_ih, w_hh, b_ih, b_hh)
    9. if self.is_lstm:
    10. outputs.append(h[0])
    11. else:
    12. outputs.append(h)
    13. t += 1
    14. outputs = P.Stack()(outputs)
    15. return outputs, h
    16. ......

    核心问题在于RNN的循环特性,也就是每个time step的结果都要依赖上一个,一般RNN层都是以一个大算子的形式直接实现双向多层,然后Python层做个binding调用。用小算子堆叠+While循环,表现在计算图里就会是一个很深的调用栈。假设一个循环中执行的算子数为N,输入序列长度为T ,则引入一个单层单向的RNN,在While循环展开后需要N*T个算子,同样也是调用栈深度。

    MindSpore默认设置的最大调用深度为1000,一旦序列长度过长,会直接报错:

    1. RuntimeError: mindspore/ccsrc/pipeline/jit/static_analysis/evaluator.cc:201 Eval]
    2. Exceed function call depth limit 1000, (function call depth: 1001, simulate call depth: 998).
    3. It's always happened with complex construction of code or infinite recursion or loop.
    4. Please check the code if it's has the infinite recursion or call 'context.set_context(max_call_depth=value)' to adjust this value.
    5. If max_call_depth is set larger, the system max stack depth should be set larger too to avoid stack overflow.

    此时可以适当调大max_call_depth参数,但是会引入几个问题:

    1. 代码修改,哪怕只有一行
    2. 编译时间过长
    3. max_call_depth的设置需要多次尝试

    总体来说,一般序列长度在200以下不会触发max_call_depth的限制,但是通常NLP长文本任务轻易就会到达500以上的长度,且编译时间随着文本长度的增加而线性增长,导致RNNs在长文本场景下几乎不可用。

    While循环不展开

    所幸1.5到1.6这段前端编译团队时间攻关了这个问题,提供了While循环不展开的写法,这里先放个链接。

    使用流程控制语句 - MindSpore master documentation

    昨天试用了一下,把RNNs的实现改成了While循环不展开的形态:

    1. def construct(self, x, h, seq_length, w_ih, w_hh, b_ih, b_hh):
    2. time_step = x.shape[0]
    3. outputs = P.Zeros()((time_step, h.shape[0], h.shape[1]), x.dtype)
    4. t = P.ScalarToTensor()(0, mstype.int64)
    5. while t < time_step:
    6. x_t = x[t]
    7. h = self.cell(x_t, h, w_ih, w_hh, b_ih, b_hh)
    8. outputs[t] = h
    9. t += 1
    10. if seq_length is not None:
    11. h = get_hidden(outputs, seq_length)
    12. mask = sequence_mask(seq_length, time_step)
    13. outputs = select_by_mask(outputs, mask)
    14. return outputs, h
    '
    运行

    将while循环条件变量由scalar改成Tensor,即可直接使用不展开编译,但是有以下的限制:

    while的条件为变量时, while循环体不能展开, while循环体内的表达式都是在各个step运行时计算,因此循环体内部不能出现标量、List、Tuple等非Tensor类型的计算操作,这些类型计算操作需要在图编译时期完成,与 while在执行期进行计算的机制是矛盾的。

    所以实现上不再用List保存然后Stack的方式,而是先实例化一个空的Tensor,然后逐个step的填充结果。简单测试了一下GPU环境下的编译时间和执行时间(batch size = 3, input size = 16, hidden size = 32):

    序列长度编译时间-展开(s)执行时间-展开(s)编译时间-不展开(s)执行时间-不展开(s)
    1002.2580.0490.3750.048
    50016.2210.1300.3800.149
    100054.5040.2070.3500.302
    10000--0.4202.686

    可以看到循环不展开的情况下,编译时间基本不受序列长度的影响,而执行时间上相对循环展开会有一定的损耗。但是在序列长度为500以上均需要调整max_call_depth, 当长度达到10000时,编译时长已经到了完全不可接受的程度(编译了30分钟还没有结束,手动结束进程)。

    小结

    作为一个NLPer,循环不展开特性是刚需中的刚需,一些有时序依赖的模型都可以用这个方式进行加速,在静态图下可以基本无视文本长度进行训练了。MindSpore想要从静态图出发逐步去支持Python语法集合来达到动静统一的目标也逐步开始有达成的痕迹,相较于Tensorflowtf.while_loop接口设计,确实基本实现了Python语法一致。写此文之前咨询过开发的同事,对于List和Tuple等非Tensor的操作应该后续也会支持,在常用场景都覆盖的情况下,将While循环默认置为不展开,又是对易用性的一个显著提升。

    最后RNNs的坑基本上算是填完了,后面会把修改后的代码上到master分支,后续有好玩/有用的特性应该还会写写,顺带预告一下下一篇,讲讲CRF和怎么用MindSpore实现。

  • 相关阅读:
    青翼科技-国产化ARM系列TES720D-KIT
    网络——多区域OSPF配置(OSPF系列第1篇)
    C++特性——auto关键字、范围for、指针空值nullptr
    文本变成文本路径图 保存txt
    前端知识案例101-javascript基础语法-捕获介绍
    RoHS认证测试的3种方法,RoHS测试,RoHS检测,RoHS认证是什么
    【Java网络原理】 四
    linux du 查看文件夹大小
    备战23秋招,c/c++Linux后端开发岗(简历/技术面)分享
    Python基础分享之一 函数
  • 原文地址:https://blog.csdn.net/Kenji_Shinji/article/details/127089537