使用 Intel® SSSE3 指令集加速本地语音识别中的 DNN 算法





0/5 (0投票)
在本文中,我将解释什么是 DNN 以及英特尔® SSSE3 指令集如何帮助加速 DNN 计算过程。
英特尔® 开发人员专区提供用于跨平台应用开发的工具和操作方法信息、平台和技术信息、代码示例以及同行专业知识,以帮助开发人员创新和取得成功。加入我们的Android、物联网、英特尔® 实感™ 技术和Windows社区,下载工具、访问开发工具包、与志同道合的开发人员分享想法,并参与黑客马拉松、竞赛、路演和本地活动。
概述
在过去的三十年里,语音识别技术取得了显著进展,从实验室走向市场。语音识别技术在人们的生活中变得越来越重要,并应用于我们的工作、家庭、汽车、医疗和其他领域。它是全球十大新兴技术之一。
由于今年的发展,语音识别技术的主要算法已从 GMM(高斯混合模型)和 HMM-GMM(隐马尔可夫模型-高斯混合模型)转变为 DNN(深度神经网络)。DNN 的功能类似于人类大脑的工作方式,它是一个非常复杂、计算量大、基于海量数据的模型。得益于互联网,我们只需要一部智能手机,无需关心远程机房中庞大的服务器群如何使其实现。没有互联网,移动设备中的语音识别服务几乎毫无用处,很少能听懂你所说的话并正常工作。
是否可以将 DNN 计算过程从服务器端转移到移动终端设备?手机?平板电脑?答案是肯定的。
借助英特尔 CPU 对 SSSE3 指令集的支持,我们可以轻松运行基于 DNN 的语音识别应用程序,而无需互联网。根据我们的测试,准确率超过 80%,这非常接近在线模式测试的结果。添加直接的 SSSE3 支持可以在移动设备上提供良好的用户体验。在本文中,我将解释什么是 DNN 以及英特尔® SSSE3 指令集如何帮助加速 DNN 计算过程。
引言
DNN 是深度神经网络的缩写,它包含许多隐藏层前馈网络。DNN 是近年来机器学习领域的热点,产生了广泛的应用。DNN 结构很深,需要学习数千万个参数,训练所需时间非常长。
语音识别是 DNN 的典型应用案例。简单来说,语音识别应用程序由声学模型、语言模型和解码过程组成。声学模型用于模拟发音的概率分布。语言模型用于模拟词语之间的关系。解码过程阶段使用上述两个模型将声音转换为文本。神经网络具有模拟任何词语分布的能力。其中,深度神经网络比浅层神经网络具有更强的表达能力,它模拟了大脑的深层结构,可以更准确地“理解”事物的特征。因此,与其他方法相比,深度神经网络可以更准确地模拟声学和语言模型。
典型 DNN 图
一个典型的 DNN 通常包含线性层和非线性层的多个交替叠加,如下图所示
在图 2 中,线性层是完全连接的关系,输入到输出可以用以下公式描述
YT = XTWT + B
XT 是行向量,输入通过神经网络。在语音识别应用中,我们通常将 4 帧数据一起计算,从而创建一个 4xM 的输入矩阵。WT 和 B 是神经网络的线性变换矩阵和偏移向量,通常维度巨大且为方阵。
英特尔® SSSE3 指令集
补充流式 SIMD 扩展 3,简称 SSSE3,由英特尔命名,是 SSSE3 指令集的扩展。SSSE3 指令集是 SIMD 技术的一部分,已集成到英特尔 CPU 中,有助于提高多媒体处理、编码/解码和计算能力。使用 SSSE3 指令集,我们可以在一个时钟周期内通过一条指令处理多个数据输入,从而大大提高程序的效率。它特别适用于矩阵计算。
要使用 SSSE3 指令集,我们应该首先声明并包含 SIMD 头文件
#include //MMX
#include //SSE(include mmintrin.h)
#include //SSE2(include xmmintrin.h)
#include //SSE3(include emmintrin.h)
#include //SSSE3(include pmmintrin.h)
#include //SSE4.1(include tmmintrin.h)
#include //SSE4.2(include smmintrin.h)
#include //AES(include nmmintrin.h)
#include //AVX(include wmmintrin.h)
#include //(include immintrin.h)
头文件 "tmmintrin.h" 用于 SSSE3,此文件中定义的函数如下
/*Add horizonally packed [saturated] words, double words,
{X,}MM2/m{128,64} (b) to {X,}MM1 (a).*/
//a=(a0, a1, a2, a3, a4, a5, a6, a7), b=(b0, b1, b2, b3, b4, b5, b6, b7)
//then r0=a0+a1,r1=a2+a3,r2=a4+a5,r3=a6+a7,r4=b0+b1,r5=b2+b3,r6=b4+b5, r7=b6+b7
extern __m128i _mm_hadd_epi16 (__m128i a, __m128i b);
//a=(a0, a1, a2, a3), b=(b0, b1, b2, b3)
//then r0=a0+a1,r1=a2+a3,r2=b0+b1,r3=b2+b3
extern __m128i _mm_hadd_epi32 (__m128i a, __m128i b);
//SATURATE_16(x) is ((x > 32767) ? 32767 : ((x < -32768) ? -32768 : x))
//a=(a0, a1, a2, a3, a4, a5, a6, a7), b=(b0, b1, b2, b3, b4, b5, b6, b7)
//then r0=SATURATE_16(a0+a1), ..., r3=SATURATE_16(a6+a7),
//r4=SATURATE_16(b0+b1), ..., r7=SATURATE_16(b6+b7)
extern __m128i _mm_hadds_epi16 (__m128i a, __m128i b);
//a=(a0, a1, a2, a3), b=(b0, b1, b2, b3)
//then r0=a0+a1, r1=a2+a3, r2=b0+b1, r3=b2+b3
extern __m64 _mm_hadd_pi16 (__m64 a, __m64 b);
//a=(a0, a1), b=(b0, b1), 则r0=a0+a1, r1=b0+b1
extern __m64 _mm_hadd_pi32 (__m64 a, __m64 b);
//SATURATE_16(x) is ((x > 32767) ? 32767 : ((x < -32768) ? -32768 : x))
//a=(a0, a1, a2, a3), b=(b0, b1, b2, b3)
//then r0=SATURATE_16(a0+a1), r1=SATURATE_16(a2+a3),
//r2=SATURATE_16(b0+b1), r3=SATURATE_16(b2+b3)
extern __m64 _mm_hadds_pi16 (__m64 a, __m64 b);
/*Subtract horizonally packed [saturated] words, double words,
{X,}MM2/m{128,64} (b) from {X,}MM1 (a).*/
//a=(a0, a1, a2, a3, a4, a5, a6, a7), b=(b0, b1, b2, b3, b4, b5, b6, b7)
//then r0=a0-a1, r1=a2-a3, r2=a4-a5, r3=a6-a7, r4=b0-b1, r5=b2-b3, r6=b4-b5, r7=b6-b7
extern __m128i _mm_hsub_epi16 (__m128i a, __m128i b);
//a=(a0, a1, a2, a3), b=(b0, b1, b2, b3)
//then r0=a0-a1, r1=a2-a3, r2=b0-b1, r3=b2-b3
extern __m128i _mm_hsub_epi32 (__m128i a, __m128i b);
//SATURATE_16(x) is ((x > 32767) ? 32767 : ((x < -32768) ? -32768 : x))
//a=(a0, a1, a2, a3, a4, a5, a6, a7), b=(b0, b1, b2, b3, b4, b5, b6, b7)
//then r0=SATURATE_16(a0-a1), ..., r3=SATURATE_16(a6-a7),
//r4=SATURATE_16(b0-b1), ..., r7=SATURATE_16(b6-b7)
extern __m128i _mm_hsubs_epi16 (__m128i a, __m128i b);
//a=(a0, a1, a2, a3), b=(b0, b1, b2, b3)
//then r0=a0-a1, r1=a2-a3, r2=b0-b1, r3=b2-b3
extern __m64 _mm_hsub_pi16 (__m64 a, __m64 b);
//a=(a0, a1), b=(b0, b1), 则r0=a0-a1, r1=b0-b1
extern __m64 _mm_hsub_pi32 (__m64 a, __m64 b);
//SATURATE_16(x) is ((x > 32767) ? 32767 : ((x < -32768) ? -32768 : x))
//a=(a0, a1, a2, a3), b=(b0, b1, b2, b3)
//then r0=SATURATE_16(a0-a1), r1=SATURATE_16(a2-a3),
//r2=SATURATE_16(b0-b1), r3=SATURATE_16(b2-b3)
extern __m64 _mm_hsubs_pi16 (__m64 a, __m64 b);
/*Multiply and add packed words,
{X,}MM2/m{128,64} (b) to {X,}MM1 (a).*/
//SATURATE_16(x) is ((x > 32767) ? 32767 : ((x < -32768) ? -32768 : x))
//a=(a0, a1, a2, ..., a13, a14, a15), b=(b0, b1, b2, ..., b13, b14, b15)
//then r0=SATURATE_16((a0*b0)+(a1*b1)), ..., r7=SATURATE_16((a14*b14)+(a15*b15))
//Parameter a contains unsigned bytes. Parameter b contains signed bytes.
extern __m128i _mm_maddubs_epi16 (__m128i a, __m128i b);
//SATURATE_16(x) is ((x > 32767) ? 32767 : ((x < -32768) ? -32768 : x))
//a=(a0, a1, a2, a3, a4, a5, a6, a7), b=(b0, b1, b2, b3, b4, b5, b6, b7)
//then r0=SATURATE_16((a0*b0)+(a1*b1)), ..., r3=SATURATE_16((a6*b6)+(a7*b7))
//Parameter a contains unsigned bytes. Parameter b contains signed bytes.
extern __m64 _mm_maddubs_pi16 (__m64 a, __m64 b);
/*Packed multiply high integers with round and scaling,
{X,}MM2/m{128,64} (b) to {X,}MM1 (a).*/
//a=(a0, a1, a2, a3, a4, a5, a6, a7), b=(b0, b1, b2, b3, b4, b5, b6, b7)
//then r0=INT16(((a0*b0)+0x4000) >> 15), ..., r7=INT16(((a7*b7)+0x4000) >> 15)
extern __m128i _mm_mulhrs_epi16 (__m128i a, __m128i b);
//a=(a0, a1, a2, a3), b=(b0, b1, b2, b3)
//then r0=INT16(((a0*b0)+0x4000) >> 15), ..., r3=INT16(((a3*b3)+0x4000) >> 15)
extern __m64 _mm_mulhrs_pi16 (__m64 a, __m64 b);
/*Packed shuffle bytes
{X,}MM2/m{128,64} (b) by {X,}MM1 (a).*/
//SELECT(a, n) extracts the nth 8-bit parameter from a. The 0th 8-bit parameter
//is the least significant 8-bits, b=(b0, b1, b2, ..., b13, b14, b15), b is mask
//then r0 = (b0 & 0x80) ? 0 : SELECT(a, b0 & 0x0f), ...,
//r15 = (b15 & 0x80) ? 0 : SELECT(a, b15 & 0x0f)
extern __m128i _mm_shuffle_epi8 (__m128i a, __m128i b);
//SELECT(a, n) extracts the nth 8-bit parameter from a. The 0th 8-bit parameter
//is the least significant 8-bits, b=(b0, b1, ..., b7), b is mask
//then r0= (b0 & 0x80) ? 0 : SELECT(a, b0 & 0x07),...,
//r7=(b7 & 0x80) ? 0 : SELECT(a, b7 & 0x07)
extern __m64 _mm_shuffle_pi8 (__m64 a, __m64 b);
/*Packed byte, word, double word sign, {X,}MM2/m{128,64} (b) to {X,}MM1 (a).*/
//a=(a0, a1, a2, ..., a13, a14, a15), b=(b0, b1, b2, ..., b13, b14, b15)
//then r0=(b0 < 0) ? -a0 : ((b0 == 0) ? 0 : a0), ...,
//r15= (b15 < 0) ? -a15 : ((b15 == 0) ? 0 : a15)
extern __m128i _mm_sign_epi8 (__m128i a, __m128i b);
//a=(a0, a1, a2, a3, a4, a5, a6, a7), b=(b0, b1, b2, b3, b4, b5, b6, b7)
//r0=(b0 < 0) ? -a0 : ((b0 == 0) ? 0 : a0), ...,
//r7= (b7 < 0) ? -a7 : ((b7 == 0) ? 0 : a7)
extern __m128i _mm_sign_epi16 (__m128i a, __m128i b);
//a=(a0, a1, a2, a3), b=(b0, b1, b2, b3)
//then r0=(b0 < 0) ? -a0 : ((b0 == 0) ? 0 : a0), ...,
//r3= (b3 < 0) ? -a3 : ((b3 == 0) ? 0 : a3)
extern __m128i _mm_sign_epi32 (__m128i a, __m128i b);
//a=(a0, a1, a2, a3, a4, a5, a6, a7), b=(b0, b1, b2, b3, b4, b5, b6, b7)
//then r0=(b0 < 0) ? -a0 : ((b0 == 0) ? 0 : a0), ...,
//r7= (b7 < 0) ? -a7 : ((b7 == 0) ? 0 : a7)
extern __m64 _mm_sign_pi8 (__m64 a, __m64 b);
//a=(a0, a1, a2, a3), b=(b0, b1, b2, b3)
//则r0=(b0 < 0) ? -a0 : ((b0 == 0) ? 0 : a0), ...,
//r3= (b3 < 0) ? -a3 : ((b3 == 0) ? 0 : a3)
extern __m64 _mm_sign_pi16 (__m64 a, __m64 b);
//a=(a0, a1), b=(b0, b1), 则r0=(b0 < 0) ? -a0 : ((b0 == 0) ? 0 : a0),
//r1= (b1 < 0) ? -a1 : ((b1 == 0) ? 0 : a1)
extern __m64 _mm_sign_pi32 (__m64 a, __m64 b);
/*Packed align and shift right by n*8 bits,
{X,}MM2/m{128,64} (b) to {X,}MM1 (a).*/
//n: A constant that specifies how many bytes the interim result will be
//shifted to the right, If n > 32, the result value is zero
//CONCAT(a, b) is the 256-bit unsigned intermediate value that is a
//concatenation of parameters a and b.
//The result is this intermediate value shifted right by n bytes.
//then r= (CONCAT(a, b) >> (n * 8)) & 0xffffffffffffffff
extern __m128i _mm_alignr_epi8 (__m128i a, __m128i b, int n);
//n: An integer constant that specifies how many bytes to shift the interim
//result to the right,If n > 16, the result value is zero
//CONCAT(a, b) is the 128-bit unsigned intermediate value that is formed by
//concatenating parameters a and b.
//The result value is the rightmost 64 bits after shifting this intermediate
//result right by n bytes
//then r = (CONCAT(a, b) >> (n * 8)) & 0xffffffff
extern __m64 _mm_alignr_pi8 (__m64 a, __m64 b, int n);
/*Packed byte, word, double word absolute value,
{X,}MM2/m{128,64} (b) to {X,}MM1 (a).*/
//a=(a0, a1, a2, ..., a13, a14, a15)
//then r0 = (a0 < 0) ? -a0 : a0, ..., r15 = (a15 < 0) ? -a15 : a15
extern __m128i _mm_abs_epi8 (__m128i a);
//a=(a0, a1, a2, a3, a4, a5, a6, a7)
//then r0 = (a0 < 0) ? -a0 : a0, ..., r7 = (a7 < 0) ? -a7 : a7
extern __m128i _mm_abs_epi16 (__m128i a);
//a=(a0, a1, a2, a3)
//then r0 = (a0 < 0) ? -a0 : a0, ..., r3 = (a3 < 0) ? -a3 : a3
extern __m128i _mm_abs_epi32 (__m128i a);
//a=(a0, a1, a2, a3, a4, a5, a6, a7)
//then r0 = (a0 < 0) ? -a0 : a0, ..., r7 = (a7 < 0) ? -a7 : a7
extern __m64 _mm_abs_pi8 (__m64 a);
//a=(a0, a1, a2, a3)
//then r0 = (a0 < 0) ? -a0 : a0, ..., r3 = (a3 < 0) ? -a3 : a3
extern __m64 _mm_abs_pi16 (__m64 a);
//a=(a0, a1), then r0 = (a0 < 0) ? -a0 : a0, r1 = (a1 < 0) ? -a1 : a1
extern __m64 _mm_abs_pi32 (__m64 a);
__m64
和 __m128
的数据结构定义在 MMX 的头文件 "mmintrin.h" 和 SSE 的头文件 "xmmintrin.h" 中。
__m64
typedef union __declspec(intrin_type) _CRT_ALIGN(8) __m64
{
unsigned __int64 m64_u64;
float m64_f32[2];
__int8 m64_i8[8];
__int16 m64_i16[4];
__int32 m64_i32[2];
__int64 m64_i64;
unsigned __int8 m64_u8[8];
unsigned __int16 m64_u16[4];
unsigned __int32 m64_u32[2];
} __m64;
__m128
typedef union __declspec(intrin_type) _CRT_ALIGN(16) __m128 {
float m128_f32[4];
unsigned __int64 m128_u64[2];
__int8 m128_i8[16];
__int16 m128_i16[8];
__int32 m128_i32[4];
__int64 m128_i64[2];
unsigned __int8 m128_u8[16];
unsigned __int16 m128_u16[8];
unsigned __int32 m128_u32[4];
} __m128;
案例研究:使用 SSSE3 函数加速 DNN 计算
在本节中,我们以两个函数为例,说明 SSSE3 如何用于加速 DNN 计算过程。
__m128i _mm_maddubs_epi16 (__m128i a, __m128i b) 饱和累加运算
此函数对于 DNN 中的矩阵计算非常关键,参数 a 是一个 128 位寄存器,用于存储 16 个 8 位的无符号整数,参数 b 是 16 个 8 位的有符号整数;返回结果包含 8 个 16 位的有符号整数。此函数非常适合满足矩阵计算的要求。例如
r0 := SATURATE_16((a0*b0) + (a1*b1))
r1 := SATURATE_16((a2*b2) + (a3*b3))
…
r7 := SATURATE_16((a14*b14) + (a15*b15))
__m128i _mm_hadd_epi32 (__m128i a, __m128i b) 相邻元素加法运算
此函数可以称为对对加。参数 a 和 b 都是 128 位寄存器,它们存储 4 个 32 位的有符号整数。根据两个向量中正常对应的元素加法运算,它对输入向量的相邻元素进行加法运算。例如
r0 := a0 + a1
r1 := a2 + a3
r2 := b0 + b1
r3 := b2 + b3
然后,我们假设 DNN 过程中有一个向量计算任务
问:有五个向量 a1, b1, b2, b3, b4。a1 向量是 16 维无符号字符整数,b1, b2, b3, b4 都是 16 维有符号字符整数。我们需要计算 a1*b1, a1*b2, a1*b3, a1*b4 的内积,并将结果存储在 32 位有符号整数中。
如果使用普通设计和 C 语言实现,代码如下所示
unsigned char b1[16],b2[16],b3[16],b4[16];
signed char a1[16];
int c[4],i;
//
Initialize b1,b2,b3,b4 and a1, for c, initialize with zeros
//
for(i=0;i<16;i++){
c[0] += (short)a1[i]*(short)b1[i];
c[1] += (short)a1[i]*(short)b1[i];
c[2] += (short)a1[i]*(short)b1[i];
c[3] += (short)a1[i]*(short)b1[i];
}
假设每个时钟周期进行一次乘法和加法,此代码需要 64 个时钟周期。
然后我们使用 SSSE3 指令集来代替实现
register __m128i a1,b1,b2,b3,b4,c,d1,d2,d3,d4;
// initialize a1 b1 b2 b3 b4 c here, where c is set to zeros//
d1 = _mm_maddubs_epi16(a1,b1);
d1 = _mm_add_epi32(_mm_srai_epi32(_mm_unpacklo_epi16(d1, d1), 16), _mm_srai_epi32(_mm_unpackhi_epi16(d1, d1), 16));
d2 = _mm_maddubs_epi16(a1,b2);
d2 = _mm_add_epi32(_mm_srai_epi32(_mm_unpacklo_epi16(d2, d2), 16), _mm_srai_epi32(_mm_unpackhi_epi16(d2, d2), 16));
d3 = _mm_hadd_epi32(d1, d2);
d1 = _mm_maddubs_epi16(a1,b3);
d1 = _mm_add_epi32(_mm_srai_epi32(_mm_unpacklo_epi16(d1, d1), 16), _mm_srai_epi32(_mm_unpackhi_epi16(d1, d1), 16));
d2 = _mm_maddubs_epi16(a1,b4);
d2 = _mm_add_epi32(_mm_srai_epi32(_mm_unpacklo_epi16(d2, d2), 16), _mm_srai_epi32(_mm_unpackhi_epi16(d2, d2), 16));
d4 = _mm_hadd_epi32(d1, d2);
c = _mm_hadd_epi32(d3, d4);
我们将结果存储在“c”的 128 位寄存器中,该寄存器由 4 个整数连接。考虑到管道,此过程可能需要 12 或 13 个时钟周期。因此,我们可以从该任务中获得的潜在结果是
实现 | CPU 时钟周期 | 升变 |
---|---|---|
普通 C 编码 | 64 | - |
使用 SSSE3 指令集 | 13 | ~ 500% |
众所周知,语音识别的 DNN 过程中有许多矩阵计算,如果我们将代码中的每一个都像这样进行优化,它将在 IA 平台上实现比以往更好的性能。我们与 ISV Unisound 合作,Unisound 在中国提供语音识别服务。Unisound 在 ARM 设备上使用 DNN 过程,性能提升了 10% 以上。
摘要
DNN 正在成为语音识别的主要算法。它已被 Google Now、百度语音、腾讯微信、科大讯飞语音服务、Unisound 语音服务以及许多其他公司采用。同时,我们拥有 SSSE3 指令集,可以帮助优化语音识别过程,如果所有这些应用程序都开始使用它,我相信语音服务将为我们提供更好的体验和 IA 平台更高的使用率。
关于作者
李阿尔文于 2007 年毕业于华中科技大学,主修计算机科学与信息安全。他于 2013 年加入英特尔,担任开发人员关系部门移动赋能团队的高级应用工程师。他专注于 IA 平台的差异化和创新赋能、语音识别技术、性能调优等。