接上篇深入探索WeNet智能语音识别源码之掩码篇,继续此专题,探索WeNet框架的底层解码引擎。
为什么要使用缓存?什么是缓存?
● 在流式推理中,输入为一个个陆续到来的 chunk。就第 i 个 chunk 而言,在计算第 k 层网络的输出时,因网络结构对左侧上下文存在依赖,所以需要依赖第 k - 1 层网络中在 i 之前的一些 chunks 的输出。如果针对当前到来的 chunk,把它与所依赖的 chunk 序列(例如有 10 层自注意力层,每层依赖左侧 4 个 chunk,那么累积起来就需要依赖左侧 40 个 chunk)拼接起来作为网络输入进行前向传播,其计算量会很大。对于那些已经计算过的 chunk,可以把在计算下一个 chunk 的输出时所需的中间量保存下来,以此减少重复计算。这种方式被称为 cache。
此外,wenet 的网络在设计时,对因果卷积和自注意力的左侧上下文均采用有限长度,所以无论序列多长,每次缓存(cache)的大小都是固定不变的(不会增长)。
仅仅编码器部分在涉及 chunk 计算时才会用到缓存机制。对于连接时序分类解码器(CTC decoder)来说,由于它是线性层,所以不需要 cache。而对于集束搜索解码器(AED decoder),是在计算完整个序列的编码器输出后进行重新评分,并不涉及 chunk。
流式引擎缓存代码分析
● BaseEncoder.forward_chunk()分析:
forward_chunk()是对单个chunk进行前向计算的核心函数。下面从该函数的内容来了解cache的实现。
图片中源码来自于github开源项目Wenet wenet/transformer/encoder.py
● xs是当前的chunk输入,由于对于单个chunk的前向计算,需要之前的chunk的计算得到的信息,因此这里需要传入相关的三个cache信息。
●subsampling_cache:torch.Tensor subsampling输出的cache。即第一个conformer block的输入。
● elayers_output_cache:List[torch.Tensor] 第1个到最后1个conformer block的输出的cache。也就是第2个conformer block的输入和CTC层的输入。
● conformer_cnn_cache:List[torch.Tensor] conformer block里的conv层的左侧依赖的输入cache。
(1)cache的大小:
● subsampling_cache和elayers_output_cache的大小由self-attention是对左侧的依赖长度required_cache_size决定。
● decoding_chunk_size是解码帧级别的chunk大小, num_decoding_left_chunks是self-attention依赖的左侧chunk数。
图片中源码来自于github开源项目Wenet wenet/transformer/encoder.py
● conformer_cnn_cache值的大小与函数内参数required_cache_size无关,由casual网络的左侧上下文lorder决定。
(2)offset:
● 当按chunk进行输入时,不能直接得到chunk在序列中的位置,需要传入offset给出该chunk在整个序列里的偏移,用于计算positional encoding。
图片中源码来自于github开源项目Wenet wenet/transformer/encoder.py
● subsampling内部的计算虽然存在冗余,但是不进行cache。一个是其实现比较复杂,另一个原因是subsampling的计算量占比不大。
(3)subsampling_cache:
● subsampling的输出的cache。即第一个conformer block的输入。
图片中源码来自于github开源项目Wenet wenet/transformer/encoder.py
(4)elayers_output_cache:
● 第1个到最后1个conformer block的输出的cache。也就是第2个conformer block的输入和CTC层的输入。
图片中源码来自于github开源项目Wenet wenet/transformer/encoder.py
注意,此处的xs不是当前的chunk,而是当前chunk+cache输入,所以其长度不是chunk_size, 而是
chunk_size + required_cache_size。
图片中源码来自于github开源项目Wenet wenet/transformer/encoder.py
● layer()对应着
wenet/transformer/encoder_layer.py中的
ConformerEncoderLayer.forward()。
下面是其具体过程。
图片中源码来自于github开源项目Wenet wenet/transformer/encoder_layer.py
注意,self-attention之前的一些前向计算其实仍然存在冗余,如果对attention层的输入进行cache,而不是对conformer block层的输入cache,可以进一步降低计算量。
(5)conformer_cnn_cache:
● conformer block里的conv层的左侧依赖的输入cache。
● conformer_cnn_cache大小为lorder,即因果卷积左侧依赖。
图片中源码来自于github开源项目Wenet wenet/transformer/convolution.py
● 缓存工作流程如下图。
●以上便是使用WeNet框架中模型实现时使用的缓存技巧代码解读的全部内容了。希望通过本文的学习,读者能够更深入地了解WeNet框架。同时,也鼓励大家继续探索更多深度学习框架和模型优化技术,以推动人工智能领域的不断发展和进步。
下一期我们会从模型推理方面继续探索WeNet源码,“我们下期见!”
注:文章图片中源码均来自于github开源项目Wenet
图文 | 智慧客服项目组 张禹豪
编辑 | 智慧客服项目组 张禹豪
审核 | 王涛 于向丽 刘小菁
校对 | 党委办公室(办公室)