65.9K
CodeProject 正在变化。 阅读更多。
Home

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

2015年7月27日

CPOL

6分钟阅读

viewsIcon

17525

在本文中,我将解释什么是 DNN 以及英特尔® SSSE3 指令集如何帮助加速 DNN 计算过程。

英特尔® 开发人员专区提供用于跨平台应用开发的工具和操作方法信息、平台和技术信息、代码示例以及同行专业知识,以帮助开发人员创新和取得成功。加入我们的Android物联网英特尔® 实感™ 技术Windows社区,下载工具、访问开发工具包、与志同道合的开发人员分享想法,并参与黑客马拉松、竞赛、路演和本地活动。

概述

在过去的三十年里,语音识别技术取得了显著进展,从实验室走向市场。语音识别技术在人们的生活中变得越来越重要,并应用于我们的工作、家庭、汽车、医疗和其他领域。它是全球十大新兴技术之一。

由于今年的发展,语音识别技术的主要算法已从 GMM(高斯混合模型)和 HMM-GMM(隐马尔可夫模型-高斯混合模型)转变为 DNN(深度神经网络)。DNN 的功能类似于人类大脑的工作方式,它是一个非常复杂、计算量大、基于海量数据的模型。得益于互联网,我们只需要一部智能手机,无需关心远程机房中庞大的服务器群如何使其实现。没有互联网,移动设备中的语音识别服务几乎毫无用处,很少能听懂你所说的话并正常工作。

是否可以将 DNN 计算过程从服务器端转移到移动终端设备?手机?平板电脑?答案是肯定的。

借助英特尔 CPU 对 SSSE3 指令集的支持,我们可以轻松运行基于 DNN 的语音识别应用程序,而无需互联网。根据我们的测试,准确率超过 80%,这非常接近在线模式测试的结果。添加直接的 SSSE3 支持可以在移动设备上提供良好的用户体验。在本文中,我将解释什么是 DNN 以及英特尔® SSSE3 指令集如何帮助加速 DNN 计算过程。

引言

DNN 是深度神经网络的缩写,它包含许多隐藏层前馈网络。DNN 是近年来机器学习领域的热点,产生了广泛的应用。DNN 结构很深,需要学习数千万个参数,训练所需时间非常长。

语音识别是 DNN 的典型应用案例。简单来说,语音识别应用程序由声学模型、语言模型和解码过程组成。声学模型用于模拟发音的概率分布。语言模型用于模拟词语之间的关系。解码过程阶段使用上述两个模型将声音转换为文本。神经网络具有模拟任何词语分布的能力。其中,深度神经网络比浅层神经网络具有更强的表达能力,它模拟了大脑的深层结构,可以更准确地“理解”事物的特征。因此,与其他方法相比,深度神经网络可以更准确地模拟声学和语言模型。

图 1. DNN 应用领域

典型 DNN 图

一个典型的 DNN 通常包含线性层和非线性层的多个交替叠加,如下图所示

图 2. 包括 4 个隐藏层的 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 平台的差异化和创新赋能、语音识别技术、性能调优等。

© . All rights reserved.