大数跨境
0
0

2.0万字长文带你入门硬件加速神经网络性能优化

2.0万字长文带你入门硬件加速神经网络性能优化 极市平台
2023-03-04
0
↑ 点击蓝字 关注极市平台
作者丨董董灿是个攻城狮@知乎(已授权)
来源丨https://zhuanlan.zhihu.com/p/609647676
编辑丨极市平台

极市导读

 

本文尝试对一些常见的利用硬件来进行AI算法加速的原理做一些汇总,也是一些简单的介绍和科普,文中并不涉及很多的硬件知识,总体行文还是以通俗易懂为纲,方便对AI算法加速感兴趣的同学阅读和交流。 >>加入极市CV技术交流群,走在计算机视觉的最前沿

之前写了几篇关于使用硬件进行AI加速运算的科普文章,基本上写了一些经典的常用的方法。

内容主要偏科普性质,不涉及过多硬核的硬件知识。文章中基本是用生活中常见的例子,来对比枯燥乏味的硬件概念,并方便对一些概念进行理解。

今天整理了下,修复一些笔误和表述错误,欢迎收藏本文或分享给周围希望了解AI硬件加速或硬件相关知识的朋友。

目录:


〇、写在前面

近些年来,AI芯片相关的公司越来越多。不论大厂还是创业公司,都希望在这个赛道上分一杯羹。即使不在这个行业工作,或多或少也会看到一些新闻:某家大厂宣布自研芯片了。

为什么呢?

第一,是因为AI算法在各个领域的应用越来越多了。 不论是手机摄像头还是安防摄像头,亦或是送餐机器人、巡检机器人、自动(辅助)驾驶汽车(卡车)等产品,基本都内置了神经网络芯片。

产品不论消费级,还是工业级,都在渐渐的进行智能化转型,采用AI算法来提高图像识别精度、提升对周围事物的感知速度,成为了众多产品的解决方案。

第二,是因为大家熟知的卡脖子问题。 自从华为被美国制裁之后,越来越多的人真切的感受到了国内高科技存在的短板。国内的公司,无论做AI训练还是推理,很大程度上还是依赖国外的芯片,国外芯片优势在于能效不错,生态健全,但价格昂贵。

更重要的一点,谁又能保证,自家公司的产品不会被美国的芯片禁令所影响呢?

当然,不仅仅局限于以上两点原因,但是现象是,国内AI芯片的研究和产业化越来越多,相关公司自然就多起来了。

与传统芯片设计不同的是,AI芯片中很多硬件单元更像是为了AI算法的加速而做的定制化。 比如在AI运算中,最常见、最耗时同时也是最需要优化的计算就是乘累加( multiply-accumulate, MAC)。

这主要是因为不论CNN网络,还是 transformer 类网络,里面都有大量的乘累加运算。如卷积算法,包括卷积的各种变种:可变卷积(deformable convolution net, DCN), 带 dilation 的空洞卷积、反卷积(conv-transpose,有人也称de-convolution),按组卷积(group-convolution)等等。

如矩阵乘算法,包括经常作为分类的全连接层(fully-connected,FC),multi-head attention 中的大量矩阵乘法等等。不论是卷积还是矩阵乘,都是很多数据乘在一起然后做累加

AI芯片要想更加高效快速的完成乘累加运算,必然要为乘累加计算单元坐单独设计:要么将存储器的容量设置的大一些,方便将卷积的权值全部加载进来,减少不必要的数据重复load过程;要么乘累加器运算设计的更加高效,更贴近卷积的运算规则,减少不必要的临时数据生成和缓存,减少临时数据搬移等。

不同厂家硬件的加速设计肯定都不甚相同,但我想目的都是一样的,都是为了更加快速的完成一个乘累加运算。有了AI芯片的硬件支持,与之相关的算法工程化自然也多起来了。

做AI芯片相关的算法工程化,与传统的算法研发有点不同的是,做AI芯片需要更多的关注芯片的底层细节,才能更好的实现某一个AI算法,并且搭建出性能更好的神经网络。

基于此,本文尝试对一些常见的利用硬件来进行AI算法加速的原理做一些汇总,也是一些简单的介绍和科普,文中并不涉及很多的硬件知识,总体行文还是以通俗易懂为纲,方便对AI算法加速感兴趣的同学阅读和交流。

本文仅限于知识学习和交流,禁止进行出于商业目的和个人成就目的的其他任何形式的转载和发表。鉴于个人水平有限,文中难免有些纰漏和错误,如有问题,也欢迎联系我指正。

一、聊下GPU的编程

图形处理器GPU(Graphic Processing Unit),是英伟达推出的专门用来进行图形学的计算,用来显示游戏视频画面等的处理器。

2006年,英伟达推出CUDA,这是一种专门针对GPU的编程模型,或者说软件库,它直接定义了异构编程的软件架构,为英伟达进入AI计算领域埋下了种子。

2012年,图像识别大赛,很多参赛队伍采用GPU完成AI加速,让英伟达乘上了人工智能的东风,从此,一跃成为人工智能硬件领域的绝对霸主,一直到今天。

这期间,国内外很多家公司都试图推出了自己的AI芯片,希望可以在人工智能硬件这一领域上分得一杯羹,但却始终无法撼动英伟达AI芯片老大的位置。

国外强如Google的TPU,AMD以及ARM,国内如华为昇腾、百度昆仑、阿里平头哥等一线互联网企业,以及地平线、寒武纪、比特大陆等自研ASIC芯片的厂家,甚至近几年大火的存算一体芯片,都在一次次的冲击着英伟达,但它却至今屹立不倒。

GPU的神之地位,被英伟达捍卫的死死的。其实,早在GPU被用来做AI计算之前,GPU就已经涉及到了多个领域。其涉及的领域之多,再加上CUDA的软件栈生态之普及,社区活跃度之高,才使得GPU屹立不倒。

GPU涉及到的领域

举三个领域的例子,就足以说明GPU的通用性和重要性。

游戏

游戏领域一直是新科技、新技术的试验场地,同时也是一个赚钱的暴利行业。

为什么这么说?因为新的硬件研发出来,基本上都会在游戏行业找到落地点,比如国内某手机厂商,以王者荣耀运行流畅为卖点,大打广告。

而实际上,软件与硬件的关系,存在着一个著名的安迪比尔定律——硬件提高的性能,很快会软件消耗掉。说这个定律的目的,是方便我们更加通俗的理解软件和硬件的关系。

这个定律的意思是说,硬件升级带来的性能提升,很快就会被新一代的软件所消耗掉,从而使得人们不得不更换新一代的硬件产品。

说到这,你或许可以想一想,为什么去年才买的手机,今年很多软件就带不起来了?软件更新,正在一步步蚕食掉你的新手机!

举个不太恰当但是很生动的例子。超级玛丽这款游戏,我们玩了很多年,游戏中那么多关卡,那么丰富的剧情,丰富的配图,丰富的配乐,但是你能想象,这款游戏的总大小才64KB么?并且64KB包含了游戏所有的代码、图形和音乐!

这主要是因为,在超级玛丽那个年代,芯片上所连接的硬件资源少的可怜,游戏开发者为了节省内存,进行了大量的代码优化和图片复用优化,那个时代的程序员,对于代码和内存的使用是抠到极限的。

反观现在,一个王者荣耀的更新包,就好几个GB,运行起来占用的内存更是高的离谱。1年前买的手机,今天很可能就带不动最新的王者荣耀了!游戏的升级,迫使人们不断更换新的手机。

其他硬件也是如此,正因为有安迪比尔定律的存在,才使得硬件被迫不断地迭代升级。

虽然英伟达的GPU一开始就是为图形显示服务的,但是随着游戏的不断迭代,游戏商或玩家不光对于GPU的显示技术有了更高的要求,而且对于与显示相关的计算任务也有了更高的要求。

最典型的便是光追技术。所谓光追,就是光线追踪。游戏场景中针对光影的处理,尤其是进行实时的光影转换,如河面的倒影、阳光打在窗户上的朦胧感等,一直是计算图形学的难点。该技术需要大量的实时计算,依据游戏中的实时场景,随时计算光线的折射、反射、漫反射等。自从英伟达在自家芯片上支持了光追技术之后,GPU便成了一些游戏的标配。如一部分游戏发烧友们所说:“玩游戏,必上显卡”。

比特币挖矿

很多人可能还记得英伟达被黑客组织LAPSUS$ 勒索的新闻事件。

原因是英伟达为了限制人们使用显卡挖矿,在显卡的驱动程序中添加了一把软件锁,用来限制挖矿时的带宽,从而降低显卡在挖矿时候的性能。

正常游戏时,显卡可以用到100%的带宽,而挖矿时,显卡却只能发挥50%的带宽性能,这让买了显卡的挖矿矿工们很不爽。之所以这么做,是因为显卡挖矿太有优势了——大量的显卡被买来去挖矿,这不是英伟达希望看到的,英伟达更希望自家的芯片,用来进行科学计算或者做对人类更意义的事。

AI计算

像本章开头说的,自从英伟达的GPU乘上了深度学习的东风,不管是出货量,股价还是公司影响力,都大幅飙升,直接造就了一个硬件王国。

深度学习的训练领域,GPU是当之无愧的王者,至今,染指训练的硬件厂商也寥寥无几,并且训练的性能和精度与GPU相比,还是差一些。

很多ASIC芯片(专用芯片)公司,都拿GPU的计算结果作为精度和性能的标杆,以此来鼓吹自己的芯片性能,大做广告。游戏、挖矿和深度学习这三个领域,就可以让英伟达的GPU立于芯片不败之林了,更别提普通显卡、科学计算甚至数据中心等领域了。

说了这些,那为什么GPU这么牛呢?这要从GPU的硬件架构说起。

多核架构

平常我们电脑上所用的CPU,是一种多核架构,看看你的电脑,可能是4核或者8核的处理器。在执行计算任务时,通过程序的控制,比如多线程,可以让8个核同时工作,此时的计算并行度是8。

而GPU的恐怖之处在于,它远远不止8个核心这么简单,它把计算核心做到了成千上万个甚至更多,通过多线程,可以使得计算并行度成千上万倍的提升。

GPU是众核!举个例子,计算5000个数相加,伪代码大概是下面的样子:

int a[5000];  
int b[5000];  
int c[5000];  
for (int i = 0; i < 5000; i++) {  
   c[i] = a[i] + b[i];  
}  

即使我们使用8核CPU计算,那么每个核还需要计算5000 / 8 = 625个数,而单个核心的计算是串行的,需要排队,也就是算完一个,再算另一个。

假设计算一个数需要1s,那么即使8个核同时运行,也需要625s。这里暂时不考虑使用向量指令。

而由于GPU有成千上万个核,计算5000个数字,使用5000个核同时计算,每个核计算一个数就搞定了!总共需要1s!

这就是GPU!

有个比喻很形象:CPU是指挥部,每个核是一个将军,除了需要指挥军队完成调度这种劳神费心的工作外,如果让它杀敌,它也只能一个一个杀,杀死1w敌军不得把将军累死了?

而GPU是军队,只负责杀敌,1w个士兵杀1w个敌军,一对一,不分分钟的事?正是由于GPU这种独有的硬件架构,加上图形专用硬件单元或者深度学习专用硬件单元(如TensorCore)的加持,再加上多层级的存储架构,使得GPU的硬件,计算性能和访存性能都如王者般,傲视其他ASIC小弟。

总结一下

游戏,挖矿,深度学习,这三个领域足以让英伟达的GPU傲视群雄!安迪比尔定律,软件会吃掉硬件的性能,反过来会迫使硬件更新迭代!GPU也是如此。GPU独有的多核硬件架构以及专用硬件单元和多层级存储,是GPU傲视群雄的王牌。

二、计算和存储的分离

前一章简单务虚,谈了谈GPU的编程和优势。

本章继续沿着硬件说起,谈一谈计算机中的存储和计算。

在这之前,有3个概念需要先明确一下。

第一是计算机。这里说的计算机是广义上的计算机,也就是说具有计算能力的硬件设备(计算的机器)都算。小到某个芯片系统,大到智能手环、手机、电脑甚至服务器,都归到计算机的范畴。

第二是计算。这里要说的计算,指所有的计算,包括科学计算——比如用计算器的算术计算;包括音频编解码——比如手机麦克风对我们说话音频的调制解调处理;包括视频流的计算——比如看电影时一帧帧图像的解码等等。所有芯片需要处理的计算任务,都包含在计算这一概念中。

第三是存储。这里说的存储,泛指计算机中所有能存储数据硬件。包括我们熟知的硬盘、U盘、手机内存、手机运存、GPU显存(显卡),也包括处理器(CPU或其他芯片)内部的片上存储或L1/L2缓存等。

在说清楚这3个概念之后,那么,计算机的计算和存储,就好比——我们在厨房做饭,厨房里的冰箱就是存储器,冰箱里的菜就是希望处理的数据,而洗菜、切菜、炒菜都属于计算任务,整个厨房就是计算机。

冯诺依曼架构

不论是笔记本,还是手机,还是智能手表智能手环,内部的程序运行机制都是一样的,都绕不开一个著名的计算体系,大家可能听过,叫做冯诺依曼体系。

冯诺依曼是二战时期著名的计算机科学家,他开创性的提出了计算机的冯诺依曼架构,其中最为人津津乐道的,是数据存储和计算的分离。在任何一台计算机中,存储数据的硬件叫做存储器,负责逻辑计算的叫做运算器或计算器。除此之外还有控制器,输入输出(IO)等。

存储和计算分离就是,存储器只负责存储数据,计算单元只负责计算数据,然后将计算出来的结果再存回存储器。

有没有发现,我们在做计算(洗菜)之前,需要将数据(菜)从冰箱里拿出来,放在洗菜池里来洗(计算)。

这个将菜从冰箱里拿出来的过程,叫做数据的搬运。

而在芯片的整个运算过程中,数据的搬运的时间开销是避免不了的,甚至有时会占据绝对的大头。

举个例子——前面说到,卷积运算是一种计算密集型的算法。也就是大量的时间开销都消耗在了卷积的乘法和加法上。但是,如果芯片的片上存储很小,而神经网络中的一张图片又很大,一张图片的数据是无法在一次计算中全部放在片上存储的。

那么这个时候,自然而然会想到将图片拆分成好几份进行计算。

然而,卷积要求的是将所有输入通道进行累加和。如果在通道上进行了数据拆分,那么每次计算的都是不完全的结果(部分和)。

这个时候,这些中间结果都要找个地方放。放哪里?最常见的就是放在片外存储上(对于GPU来说,可能就把这些临时数据放在DDR上,也就是我们常说的显存上,因为显存一般都比较大,大概16GB或者32GB甚至更大,肯定放的下)。

如此一来,存放数据的冰箱可能就不是厨房里的冰箱了,而是客厅里的大冰柜,将数据从厨房搬到客厅的大冰柜临时存起来,这个过程的数据搬运开销是很大的。

也因此,GPU甚至很多ASIC芯片,在进行芯片设计时,都会想办法加大DDR的带宽,通过多路DDR访存甚至使用HBM来提升带宽,以降低数据搬运带来的额外开销。

总结一下

在现有经典的计算机计算架构中,比如冯诺依曼架构中,计算和存储是分离的。这也就导致了计算机在完成运算任务时,需要不断地从存储器中搬运数据到计算单元中,然后完成计算。

这种分离的架构也导致了计算指令和IO指令(数据搬运)的独立,从而衍生出指令流水线。(这个后面会慢慢介绍)除此之外,有些人可能会想,既然计算和存储是分离的,为了防止多余的搬运开销,那我们把计算和存储放在一起不就行了么?我们直接在冰箱里洗菜做饭不就完了?

可以,现在比较前沿的近存芯片或存算一体芯片就是基于这个想法来设计实现的。关于这个内容,后面会介绍。

三、指令——流水线上的工人

刚毕业的时候在青岛某信任职,做嵌入式软件开发,需要调试电路板。

公司有个要求是所有新入职的员工,都要去公司自己的加工厂上两周的班,亲自组装生产电路板,体验产品生产的过程。于是,刚毕业的我,光荣的成为了一名流水线工人,虽然只有两周的时间。

这期间,我和同事一起,完成过一天组装3万片电路板的成就,也完成过一天往电路板上插装不计其数电阻、电容的操作,白班夜班倒,记忆犹新。

当时的我,坐在流水线的椅子上,满脑子就一个想法:“我今天就是个螺丝钉”。也正是这一次流水线的工作体验,让我明白了一个道理——_一个产线或团队的工作效率,取决于整条流水线上动作最慢的那个人,而不是最快的那个人。_一旦动作慢导致产品在你那积压,最常见的就是带小红帽的组长过来噼里啪啦说一顿。不过好在我当时手脚勤快,没有拖后腿。

在流水线上,人就是个机器,机械的执行着每一个动作。直到后来开始做芯片开发,开始给指令排流水,才恍然大悟,原来,每条指令都是流水线工人。

上一章说到,在冯诺依曼架构的计算体系中,计算和存储是分离的。分离的结果就是,我们可以将计算和存储分别看做两个工人,而不是一个,每个工人只负责干自己的事儿。

—— 计算这名工人只负责计算,存储这名工人只负责存储。

计算好比在厨房做菜,存储好比从厨房冰箱里将菜拿出来。这个比喻很重要,能很通俗的理解流水。那么问题来了,一个人从冰箱里拿菜洗菜然后去做菜和两个人,一个专门拿菜洗菜,一个专门做菜,哪种做饭效率高?做过饭的同学肯定知道,当然两个人配合快了。

比如做西红柿鸡蛋,一个人需要先从冰箱拿出鸡蛋,记需要花费 A 的时间,然后炒鸡蛋需要花费B的时间,然后回去拿出西红柿,需要花费C的时间,然后将西红柿和鸡蛋一起炒,需要花费D的时间。总共花费 A + B + C + D的时间。

而两个人的话,第一个人从冰箱里拿出鸡蛋,记A的时间,第二个人炒鸡蛋,花费B的时间,而此时,在第二个人炒鸡蛋的同时,第一个人可以同时去拿出西红柿,需要花费C的时间,然后第二个人抄完鸡蛋后,接过第一个人的西红柿一起炒,需要花费D的时间。总共花费 A + MAX(B,C) + D 的时间。

两个一比较,肯定两个人做饭时间花的少吧。

而两个人做饭时,B,C同时进行,就像流水线上的工人一样,你负责你的事,我负责我的事,我做完就给下一个人,下一个人动作快的话,可以立刻接过去,动作慢的话,就会导致产品积压。

在计算机的体系结构中,计算指令和存储指令,一般都是两条独立的指令,并且在硬件部件上,是两个独立的硬件部件,比如计算是ALU单元,存储是DMA或load/store单元等。

独立的硬件部件和独立的指令设计,天然的保证了两条指令之间可以排流水,就像流水线上的工人一样工作。

还是拿卷积算法举例子,我先计算上半张图片,同时你就可以搬运下半张图片的数据,整体的AI计算开销,就会少很多,从而起到加速的作用。

不仅如此,在指令集设计或硬件设计时,一般都会考虑流水线的设计,从而完成更高效的计算和访存操作。

而可进行流水操作的指令,也绝不仅仅只有计算指令和存储指令等,不同的计算单元之间,也是可以排流水的。比如卷积层后面接一个池化层,那么在计算第二部分卷积的同时,可以计算第一部分池化。

流水线的发明对于制造业来说是一次技术变革。早期的手工制造业是靠大量人工完成的,这种工作方式要求每个工作者要了解整个成品的完整制造过程,要快速的制造出一个成品需要对整个工序、流程非常熟悉,无形中提高了对工人素质的要求。而流水线的出现,不需要工人了解整个流程,为手工制造业实现了半自动化,加快了加工速度。

四、衣柜般的分层存储

继续介绍一个计算机中的部件——存储器,看完之后,你将了解存储器是什么以及存储器在AI计算中的作用。

在介绍之前,先说一个我今天早晨的事儿。7点起来晨跑,突然发现已经到深秋了,凉飕飕的,感觉是时候换一波秋冬的衣服了。于是开始翻箱倒柜,花了好大一会儿,终于在衣柜的最深处,找到了去年冬季跑步的衣服,口袋里还装着去年的口罩。

找到衣服之后,我突然盯着衣柜看了半天,发现:衣柜的设计确实是很讲究,只可惜我没用好,才导致费了九牛二虎之力才找到了衣服。

衣柜讲究的设计

最经常穿的衣服,应该是要用衣架撑好挂在衣柜横杆上的,方便随时取用;

不经常穿的衣服,比如非当季的衣服,应该是要叠好放在最底层的柜子里,让他过冬。

这种明显的功能区域划分,可以让我在需要某件衣服的时候,方便快捷的找到。

这就和本章的主角——存储器有点像了。

不知道是不是所有带有存储性质的产品都有类似的分层划分。但我知道,存储器这种储存数据和指令的东西,有着很明显的分层或分级划分的。

存储器的分层设计

上一章说到,在计算机系统中,计算单元和存储器是分离的。

而实际上,在计算机中,单看存储器,也是会分成很多层级。存储器最常见的就是内存。在买手机时,我们一定会关注一个参数,那就是内存大小。内存越大,手机可以存储的数据就越多,运行起来也就越流畅,手机性能就越好。

但是在一个计算系统中,除了内存,还有其他的存储。

下图是一个典型的存储器划分示意图。示意图越往上,代表存储器越靠近计算单元,其容量越小,相对造价就越高。这也是为什么,在计算机系统中,单位存储的内存价格很高,而外存(如硬盘)相对较为便宜。

磁盘

磁盘是离CPU最远的存储器。一般作为硬件外设存在。包括我们常见的硬盘、U盘等存储外设。磁盘的读写速度相比其他存储器慢,但是容量大,价格便宜。这个就相当于衣柜的最底层,存放着不经常穿的衣服(数据),像是一个大仓库。

主存

可以理解为电脑的内存条,用来存放程序运行时的指令和数据。程序运行时操作系统需要将程序和数据加载到内存中,它就相当于衣柜中搭衣服的横杆,随取随用。

高速缓存

高速缓存(Cache)是比主存离CPU更近的一级存储,他会把程序需要的指令或者数据预先加载进来,在CPU进行运算时,会首先在缓存中查找数据或指令,如果找不到,就在去主存中寻找,找不到去主存中寻找的过程一般称为CacheMiss。预先加载怎么理解呢?打个比方,我们在冬天肯定有经常要穿的2-3件衣服,但不会每次衣服脏了都放回衣柜中,而是洗完放在阳台晾衣架上,这2-3件衣服轮换穿。CPU也是这样,它会频繁的从高速缓存中存取数据,找不到了,再去内存中找,就好像阳台上没衣服了,再去衣柜里找一样。

寄存器

寄存器(Register)是CPU最近的存储器。用来存放程序运行时需要的指令、地址、立即数等。类比于就是身上正在穿的衣服。

有了这几级存储,在做AI加速时,就可以做很多事情。由于计算和存储是分离的,那么可以将计算和存储指令排流水,实现性能的加速。同样,如果存储也有分层设计,并且开放给程序员的话,那么,单独的存储指令也可以进行流水设计,从而在带宽不变的前提下,提高数据的吞吐和程序的性能。

GPU就是这么做的。熟悉GPU硬件架构的同学可能知道,GPU的编程模型中有DDR(显存,也就是最外层存储,可类比硬盘),Shared Memory(共享内存),当然还有其他的存储。单说 DDR 和 shared memory(SM)这两级存储,就可以排流水。比如——

上表中每一行代表同一时刻,看不懂没关系,只需要知道在同一时刻,程序可以同时将数据从DDR load 到 SM(左侧的一例) 以及在SM上进行计算(右侧的一列)即可。这样就相当于在流水线上有两个工人一起工作,从而提高了性能。

总结一下

存储器的分层设计,一个好处之一就是,程序员可以通过编写存储指令(包括将数据从外存搬运到内存的 load 指令,将数据从内存加载到片上计算的 move 指令等),从而完成流水的排布。

当然,存储器的分层设计肯定不单是这个原因,就不展开了。那么问题来了,这章和AI加速有什么关系呢?其实,存储器作为一个偏计算机底层的部件,是根本不关心上层应用是什么的。

我们可以让计算机进行AI计算,来完成AI加速,也可以让计算机运行一个游戏,完成游戏加速。只要了解了存储器的原理,不论是AI加速还是游戏加速,都能做到性能很好。

经济基础决定上层建筑。——而且只要硬件支持指令流水级,并且编译器做的足够好,甚至都不需要程序员手动去排流水(手写汇编确实太枯燥了)就能自动实现,从而完成对于AI算法的加速计算,但这一点对于编译器的要求很高。

五、一个流水的例子

之前的两章介绍了流水这一技术,它用来进行程序的性能加速。作为入门级的科普文章,当然不会写的太深入太专业,只是从概念上希望大家对流水有个感性的认识。

本章打算再通过一个生活中的小例子,让大家更直观的了解什么是流水。

从生活中的小事开始

早晨从起床到上班出门,我们一般会做以下几件事:刷牙、烧水、喝水、出门。如果正常按顺序去做,可能就是先刷牙,然后烧水,等水烧开了喝水,然后出门。假设做每件事需要的时间如下表,那么整个出门前需要花费的时间为55分钟。

但是,如果你稍微会一点时间管理的话,我相信你肯定不会先刷牙、然后烧水的,毕竟,烧水和刷牙没有任何关系,而且烧水的时候,也不需要人在边上看着。

于是,就有了下面的做事顺序——起来先烧水,然后在烧水的同时,刷牙,等水烧开了,喝水,出门。这么算下来,总共需要40分钟就能完成。

这两种做事顺序最终的结果都是一样的,而且该做的事都做了。

区别在于,后面比前面节省了15分钟的时间。

这里需要注意2个概念。

依赖——后面的事依赖前面的事情。也就是说喝水肯定依赖烧水完成之后才能出门。

并行——烧水和刷牙没有任何依赖关系,他俩就可以并行去做。

上图中,烧水和刷牙在同一时刻去做了。因此我们可以说,在整个从起床到出门的时间流水线中,烧水和刷牙并行起来了。

单纯的一个并行处理,就可以节省15分钟的时间。在理解了并行的概念之后,流水就好理解了。

流水排布到底是什么样的

继续上面的例子,比如我们起床需要刷两次牙,烧两次水,喝两次水。(当然现实中不会有人这么做,但是在AI神经网络中,重复某个计算是常有的事。如果刷两次牙,烧两次水,喝两次水,然后出门,我们该怎么管理时间呢?

刷牙1和刷牙2肯定是顺序来的,同理烧水1和烧水2,喝水1和喝水2都是需要有顺序的,也就是前面说的依赖。

但是刷牙与烧水之间、烧水与喝水之间是有可能并行起来的。

比如烧第二次水的时候,我们可以喝第一次的水。上图中,整个左上角的的排布,像一个瀑布一样由上而下,每一行都有两件事同时在做,同一时刻两件事互不影响,但整个系统又井然有序。这种排布,就叫做流水。

  • 在指令序列中,将刷牙、烧水、喝水替换成指令,就完成了指令流水;
  • 在神经网络中,将刷牙、烧水、喝水替换成AI算法,就完成了算法流水。

但是能排流水总是需要满足前面说的两个前提:同一时刻的两件事、或两条指令、或两个算法是解除依赖的,并且可以并行处理的。

说到这,有同学可能会问,既然这样,我们弄两个烧水壶同时烧水不就行了么?

当然可以,这就是升级硬件。双核CPU肯定要比单核CPU性能好,就是这个原因了。排流水是在硬件资源有限的前提下,最大限度的减少程序运行时间,提升整个AI软件栈的性能!

Resnet50 中的算法并行

在Resnet50的网络结构中,存在很多可并行的算法。

上图是截取的Resnet50网络中的一部分,可以看到中间有个加法节点,加法节点有两个输入,分别为左边的卷积1和右边的卷积2(Conv为Convolution的缩写,中文名为卷积)。

左边的卷积1依赖于它前面的Relu的输出,而右边的卷积2依赖于很靠前的某个节点的输出,两者并没有实际上的数据依赖,因此,在深度学习编译器对两个节点进行编译调度时,可以将两者进行并行化处理(Parallelization),从而减少一个卷积运算的耗时。

总结一下

之所以又花了一章来介绍流水和并行技术,是因为并行和流水技术在AI软件的性能优化中占据了很重要的位置。在硬件资源有限的前提下,我们只能通过软件手段来持续进行AI的加速优化。这里面,更深刻的理解硬件的架构,利用好硬件的优势,编写更加硬件友好的软件代码,才能更加有效的实现AI加速。知己知彼,百战不殆。

六、异构编程

上一章用一个生活中的小例子,介绍了流水这一概念。在计算资源有限的情况下,我们可以通过软件的流水技术来提升程序性能。

但如果你是土豪,不想耗费太多精力去做软件优化,就想砸钱来提升程序性能,有办法么?

当然有,性能不够,芯片来凑。

正所谓“众人拾柴火焰高”,只要芯片足够多,性能就能飙到顶。异构芯片编程就是这样的一种方式。

所谓异构编程,就是将不同厂家、不同架构的芯片放在一个统一的计算机系统中,通过软件的调度,来实现AI计算的一种方式。

比如将x86的CPU和英伟达的GPU放在一起进行编程。人工智能的发展催生了异构编程的火热。主要是因为神经网络中有大量的稠密计算,如果用传统的CPU从头到尾训练一个神经网络,需要耗费大量的算力才能计算出来,就算把CPU累死,估计都算不出来。

在神经网络中,矩阵和卷积运算这两种稠密运算,几乎占据了大部分神经网络90%的耗时。因此,有很多公司开发专用的AI芯片(也称ASIC 芯片,Application Specific Integrated Circuit,专用集成电路),专门围绕着卷积运算或者矩阵运算设计硬件单元,来完成运算加速。这就像,给CPU搞了个外挂。英伟达的GPU,谷歌的NPU,寒武纪的MLU等都是类似的ASIC芯片。

它们通过PCIe总线和主机相连,从而作为协处理单元完成AI的加速。

这和我们扩展电脑内存一个道理。

  • 内存不够了,买个内存条插上,内存就够了。
  • 算力不够了,买个显卡插上去,算力就够了。

正是因为人工智能对于算力的需求,才出现了越来越多的AI芯片公司,并且使得异构编程这一技术越来越为人所知。其中,英伟达的 CUDA 编程就是最广为人知的一种异构编程方式。

异构编程初探

不熟悉的同学这时候可能会有些疑问,异构编程难么?首先说,不难。因为大部分做ASIC芯片的厂家都会提供异构编程所需要的驱动或者计算库。如英伟达有 cuDriver 库用来驱动显卡,有 cuDNN 库用来加速常见的深度学习算法,cuFFT 库用来就是FFT相关运算(里面都把算法写好了,我们自己调用即可),还有 TensorRT 专门用来做神经网络推理的优化和网络融合等等。

再不济,如果你的神经网络中有个不常见的算法,像英伟达还提供了CUDA编程,这是一种类C语言的编程语言,只需要学会某些简单的标识符和核函数(kernel)的编写,就能写出远超CPU性能的代码。关于CUDA编程的资料网上有很多,感兴趣的同学可以自行去查找。除了英伟达,其他厂家也基本都是这个套路。对外提供提供加速库和驱动来辅助完成异构编程,从而实现AI的计算加速。

一次与异构编程相关的面试

记得有一次去某公司面试,面试官告诉我,他们公司是基于视觉做智能交通的解决方案的。所谓的解决方案,就是给他们的客户提供一整套的软件+硬件的产品,打包卖给他们。

我当时比较好奇,你们也自己做芯片么?面试官说,我们自己不做,但我们会买。国内做AI芯片的企业开发的芯片产品我们都会买,当然GPU也会买,然后做二次开发,在这些硬件上部署我们自己的算法。那你们的解决方案中,卖给用户的硬件,是只有一家的产品,还是会多家混用?面试官:有可能多家的都有,看谁家的性能好用谁家的。

这家做解决方案的公司,会用到不同厂家的芯片,但核心的AI算法是自己的。这是一种典型的异构编程场景:在服务器主机上通过PCIe总线连接多张AI加速卡,实现AI算法计算加速,在云端实现交通场景下路人和车辆的识别。

总结一下

异构编程可以认为是一种使用专用芯片对神经网络进行加速的外挂方式。

通过这种专用的加速卡,来完成神经网络中相关算法的加速运算。其实,异构编程并不是一个很新的概念。据一个从事手机开发的朋友讲,他们很早之前做手机,手机系统中会有很多不同的芯片,主处理器和协处理之间都会有通信,某些算法在主处理器上跑,某些算法在协处理器上跑,最终完成一个整体运算。

这就是一种异构编程,只不过当时他们认为这是理所当然的。而随着人工智能的热潮,异构编程这一概念才越来越多的被人所熟知。从而也成为了AI加速中一个不可或缺的编程方式。

七、存算一体

计算机冯诺依曼架构下一个特点,就是存储和计算分离,这会带来一个问题,那就是计算机有时会遇到存储墙,也就是存储带宽不够导致的性能下降。如果说流水技术可以为此破局,那么其实还有一个大杀器——存算一体技术。存算一体,简而言之,就是把存储和计算放在一起,直接打破冯诺依曼架构的桎梏。

它是怎么做的呢

还是先从一个例子说起。假设我要做个炒鸡蛋。正常的话我有以下步骤:

  1. 1. 把鸡蛋从冰箱里拿出来
  2. 2. 拿着鸡蛋从冰箱走到灶台
  3. 3. 在灶台开火,开始炒鸡蛋

这个过程是经典的冯诺依曼架构中的流程。

这里面有一个弊端就是:数据(鸡蛋)需要从存储器(冰箱)中 load(拿)到计算单元(锅)中,然后进行计算(炒鸡蛋)。

我们知道,load数据的过程是耗时的,尤其是数据量比较大的情况下。比方需要炒1000个鸡蛋,一个锅肯定炒不下,需要多次拿鸡蛋,多次炒。并且拿鸡蛋的速度取决于从冰箱走到灶台的速度,这里就是存储器到计算单元之间的总线带宽,带宽越大,速度越快。

但无论带宽多大,只要是这种架构,总是会有耗时,并且带宽是不可能很大的。

在这个时候,会出现一种极端情况,厨师炒鸡蛋的速度很快,可能1秒钟就炒完了,而从冰箱里拿鸡蛋到灶台,遇到个手脚不灵活的人,可能需要3秒钟。不论多长时间,只要大于炒鸡蛋的时间,厨师就得等着鸡蛋过来。

这个时候就是说,计算单元在空闲,性能瓶颈在带宽,程序打到了存储墙。

所谓存储墙,就是由于存储的数据需要load,但是load的时间很长,像是有一面墙在那里,导致计算单元空闲的情况。

为了解决这个问题,就有人提出,既然这样,那为什么不能把计算单元和存储单元放在一起呢?反正芯片都是人设计的嘛,放在一起就不需要来回搬运数据了啊。比如,直接把锅做的特别大,大到可以放下1000个鸡蛋,或者说直接在冰箱里面炒鸡蛋,不用来回拿鸡蛋,这样不香么?别说,还真香。这就是存算一体技术。

  • 存——指的是存储器。
  • 算——指的是计算单元。

两者合为一体,将计算单元和存储单元设计到一起,减少甚至消除数据的搬运。就这一点技术,就能使AI计算的性能得到飞一般的提升。

应用场景

存算一体技术,在AI领域,最有效的场景便是,存储器的内存足够大,可以放得下整个神经网络的权值参数。举个例子,resnet50的权值参数大概为24MB,AlexNet的权值参数大概为59M,而VGG-16大概有130M的参数量大小。

假设一个存算芯片的容量为40M,那么很明显resnet50的所有参数都可以全部放进存储器,这样在做模型推理的时候,只需要把 feature map(也就是图片,比如人脸识别时,拍摄的人脸照片)加载进内存就能推理出结果了。相反,VGG-16由于权值参数太多,无法一次全部加载完成,仍然需要分多次加载。并且每次加载都是需要耗时的,此时,存算技术对VGG-16带来的性能提升肯定没有Resnet50高。因此,存算一体在模型参数小于存储容量的场景下,其性能优势十分明显,可以说,这个时候,没有任何多余的数据搬运操作。

总结一下

前两天一个同事给我提了个需求,需要开放几个接口给他,我跟他说,这些接口我早就都准备好了,是因为一直没有需求所以没有开放。需求,会推动产品技术的不断迭代。

冯诺依曼架构,从二战时期被提出来开始影响世界。但随着近年AI的不断发展,对于计算机性能的要求逐步提高,使得人们不得不尝试打破传统,开始创新,并且从学界开始大规模走向工业界。

在如今美国动不动就禁止中国先进芯片工艺的大背景下,存算一体技术,或许也是一条出路,正如中纪委网站上一个关于存算一体公开课上说的那样——28nm的存算一体芯片,他的能效跟7nm主流的GPU是相当的。

或许随着国内存算一体技术的不断发展,我们真能实现弯道超车,期待那一天早点到来。

八、循环展开

前面几章,说了一些经典的利用硬件进行程序加速的方法,本章说一个很简单的软件加速方法,并讲解下其原理。如果要我说一个最简单,最有效的,并且人人都能学会的程序优化方法,我估计会投票给Unrooling(译为:循环展开)。循环展开,从名字就能看出来是什么意思:就是把一段循环代码展开来写。

听着简单,但具体怎么做呢?先举个简单的例子。

一个例子

高斯年轻的时候,老师曾问他:从1加到100,结果是多少?高斯思考片刻后,给出了5050的答案,让老师大吃一惊。这里也用这个例子,计算1到100的所有数的和。用C语言很容易写出下面的代码:

#include "stdio.h"  
int main() {  
int sum = 0;  
for (int i = 1; i <= 100; ++i) {  
   sum = sum + i;  
}  
printf("sum = %d\n", sum);  
return 0;  

这个写法很容易想出来:100个数相加,循环100次,每次累加一个数字,最终输出结果 sum = 5050。

那么,这段代码,如果用循环展开会怎么写呢?

#include "stdio.h"  

int main() {
int sum = 0;
for (int i = 1; i <= 100; i+=4) {
sum = sum + i;
sum = sum + (i + 1);
sum = sum + (i + 2);
sum = sum + (i + 3);
}
printf("sum = %d\n", sum);
return 0;
}

如上是循环展开的写法,我们把原来循环100次,每次循环加1个数的写法,写成了循环25次,每次循环加4个数。

从结果上看肯定是一致的,最终结果都是5050。但是两者的性能会有较大的差别。

性能验证

下面实际验证一下两种写法的性能。为了更准确的获取到程序中数字累加的耗时,把100个数字的相加改为10000个数字相加。因为100个数字对于计算机来说太少了,两者的性能差别不大,不容易看出差别。在我的笔记本上进行如下的测试,有以下代码,其中 gettimeofday函数用来获取时间戳。

#include "stdio.h"  
#include <sys/time.h>  
int main() {  
   int sum = 0;  
   struct timeval tv0tv1;  
   #if 0  
   printf("do not use unrooling\n");  
   gettimeofday(&tv0, NULL);   
   for (int i = 1; i <= 10000; ++i) {  
     sum = sum + i;  
   }  
 #else  
   printf("use unrooling\n");  
   gettimeofday(&tv0, NULL);  
   for (int i = 1; i <= 10000; i+=4) {  
     sum = sum + i;  
     sum = sum + (i + 1);  
     sum = sum + (i + 2);  
     sum = sum + (i + 3);  
   }  
 #endif  
   gettimeofday(&tv1, NULL);  
   printf("time = %ld\n", (tv1.tv_usec) - (tv0.tv_usec));  
   printf("sum = %d\n", sum);  
   return 0;  
 } 

测试结果如下:

可以看到,使用循环展开后,10000个数的累加耗时为14微秒;不使用循环展开,10000个数的累加耗时为24微秒。两者相差10us的耗时,大约相差40%的耗时!这是很夸张的,要知道,在做程序性能优化时,如果能一次优化掉40%的耗时,几乎就算是一个很棒的优化手段了。(感兴趣的同学可以复制上面的代码实际测试一下)为什么循环展开会有效呢这和计算机的取指令以及缓存机制有关。其性能加速的思想是:减少CPU读取指令的失败次数,也就是降低指令的Cache Miss。可能不太好理解,没关系,先看一个实际例子就懂了。

理解Cache Miss

双11刚刚过去,你肯定买了不少的快递。假设你买了100件快递,并且这些快递已经放在了快递柜中一个个的小格子里了。

现在要去取快递,100件快递至少要开100次的快递柜门,才能把所有的快递取出来。为什么这里说至少100次呢?因为很有可能因为某些原因一次不能成功开启快递柜的门,这些原因可能包括:

  • 走错快递柜
  • 输错取件码
  • 脑袋发蒙,快递没取出来又给关上了

连着取100个快递,谁能说得准会发生什么事情呢?但不管怎样,在这个场景下,你需要一件一件地将快递拿出来,但由于上面几个原因,如果运气不好,就会出现好几次打不开快递柜门的情况。那有没有办法优化一下这个问题呢?当然有,那就是让快递员把多个快递放在同一个快递格子里。

如上图,比如每4个快递放在一个快递格子里(相同颜色代表一个格子),这样打开一个格子,就一定能取出4个快递。那至少开25次的快递柜门,就能把所有的快递取出来。这很显然,比100次的效率要高不少。

说回循环展开,100个快递就需要做100次循环,每个快递的取出就是循环里的一条加法指令。如果不做循环展开,那就需要一个快递一个快递的取(相当于从指令内存iCache中一条指令一条指令的取),并且很有可能取完这一条指令后,并不能成功的取到下一条指令,从而发生Cache Miss现象。

但如果我们做了循环展开,相当于把相邻的4条指令绑定了,CPU做一次循环,看到里面一定有四条相邻的指令,就像我们打开快递柜门,里面一定有四个快递一样。CPU取完第一条指令后,能很快地取到第二条指令,从而很快地取到第三条第四条指令。在取完4条指令后,才会有可能发生Cache Miss 现象。

连着取四个快递之后Miss一次,和每取一个快递,就Miss一次,浪费的时间肯定是不一样的。循环展开能有效地降低指令的Cache Miss 。这个方法对于程序加速很有效,并且实施起来也很简单,如果你有兴趣,不防在自己的项目中试一试。

但是需要说明的是:现在的编译器可能会对你的代码自动做循环展开优化,因此,如果你想试试效果,建议编译的时候不要打开任何优化选项。

九、深度理解吞吐量和延时

写到这,基本上想写的与硬件相关的科普写的差不多了,如果有其他想了解的,欢迎联系我,我们一起学习。

在最后,本章介绍两个在性能优化方面非常非常重要的概念——吞吐和延时。其实不光在做神经网络性能优化时会用到,在计算机网络的性能调优时,这两个概念也会被反复提到,可见其重要性。很多同学对这两个概念的最大误区,大概都集中在:高吞吐就等于低延时,低吞吐就等于高延时。这样理解是有问题的。

吞吐

吞吐或吞吐量(Throughput):完成一个特定任务的速率(The Rate of completing a specific action),也可以理解为在单位时间内完成的任务量。对于计算机网络而言,吞吐量的衡量单位一般是 bits / second 或者 Bytes / second。

举个例子,如果说一条数据通路的吞吐量是 40Gbps,那么意味着,如果你往这个数据通路中注入40Gb 的数据量,那么它可以在1秒内流过这条数据通路。对于神经网络而言,我们可以把吞吐量的衡量设置为每秒处理的图片数量(如果是图像任务)或语音数量(如果是NLP任务)。

延时

延时(Latency):完成一个任务所花费的时间(The time taken to complete an action)。举个例子,如果我用我的电脑去 ping 一个网站,从我发送这条 ping 指令(数据包)开始,一直到这条 ping 指令到达对方服务器的时间,就可以理解为延时。

ping 百度的延时。一般在打游戏时,都会关注延时,如果延时太高,玩游戏就会很卡,同样,ping 百度也是很多开发人员喜欢的用来测试网络环境的手段。你是不是也喜欢在测试网络的时候,试试能否打开百度呢?

那么,吞吐量和延时这两者有什么关系呢?是不是意味着,高的吞吐量就会有低的延时?延时增加总是会导致吞吐量减少?

我们看一个ATM(Automatic Teller Machine)机取款的例子。

假设银行里有一台ATM机,平均下来它基本会花费1分钟将钱吐出来送给客户(包括插卡、输密码等步骤,这里不考虑个人差异等因素)。这就意味着,如果我排着队轮到我使用这台ATM机取钱,我可以预见的是,1分钟的时间,我就可以拿到钱并且离开ATM机。

换句话说,这台ATM机的延时是1分钟(或者60秒,或者60000毫秒)。

那么吞吐量呢?吞吐量是1/60个人每秒。也就是说,如果存在 1/60个人去取钱的话,那么ATM机每秒能接待的客户是1/60个。

这是很简单的数学计算。所以,吞吐量 = 1 / 延时 ?对么?

如果ATM机突然进行了升级,从之前平均1分钟可以接待一个客户,到升级后平均30秒就可以接待一个客户。那么此刻的ATM机的延时是多少?没错,是30秒。那么吞吐量呢?30秒可以接待1个客户,一分钟可以接待两个,吞吐量翻倍了。

延时减半,吞吐量翻倍。

看起来很符合上面的公式。

我们继续。银行为了应对更多的客户取钱需求,在原来仅有的ATM机旁又安装了一台新的ATM机,我们假设这两天ATM机都是未升级前的。

也就是一台机器平均1分钟可以接待一名客户。

那么我去取钱,从我占据一台ATM机,到取出钱来,还是会花费1分钟,也就是延时是1分钟。

那么此时的吞吐量呢?

两天ATM机可以同时一起工作,也就是1分钟可以处理两名客户。吞吐量为2个人/分钟,或者2/60个人每秒。

和只有一台ATM机的时候相比,延时没有变,一个客户取一次钱,都是需要花费1分钟,但是整个ATM机的吞吐量却增加了一倍。

吞吐量的增加,和延时没有关系!

这个例子很清晰的可以说明这个问题。

所以,对于吞吐量,我们可以理解为,一个系统可以并行处理的任务量。而延时,指一个系统串行处理一个任务时所花费的时间。

对应到神经网络性能优化这个场景下。神经网络的吞吐量,就是每秒中可以处理的图片数量,或者语音数量。这与模型本身的性能有关,也与实际完成计算的硬件资源有关。比如两个GPU可以并行独立完成,其吐吞量一般要比单个 GPU 高。

搞神经网络训练的人,都喜欢堆显卡,就是为了提高整体系统吐吞量,毕竟训练一个模型,需要处理海量的数据。搞神经网络推理的人,都喜欢做性能优化,为了提高整个模型在有限硬件资源下的速度。

毕竟,2秒完成一张图片的识别会让人忍受不了,而1ms的时间,大部分人会感受不到卡顿。

免责声明

本文旨在进行软硬件相关知识的科普和技术交流,非商业目的。本文仅限于知识学习和交流,禁止进行出于商业目的和个人成就目的的其他任何形式的转载和发表。作者撰写本文的目的是技术交流和学习总结。

本文文字和表达以及相关算法引申基本都为原创,作者已经尽量减少对于他人资料的直接引用,但在整理文章的过程中,不可避免的会查阅一些资料,也会不可避免的出现一些引用链接的丢失问题。在查阅资料的过程中,发现了很多国内外优秀的技术博客和科普文章,他们将自己对于算法的理解,对于行业的洞见,甚至实验代码都放在了网上,免费供人阅读学习,正是由于这些免费的学习资料,才使得学习不再是难事。如果文中某些内容是您原创,并且本文下方并未给您署名引用,或您希望署名引用,或您希望删除,或其他需求,请联系我。同时,非常希望并感谢您将自己原创文章、图片、代码等用于学术交流或科普传播,一起为深度学习贡献一份力量。最后,限于作者白天要上班,文章基本都是晚上下班后和周末抽时间写的,时间精力有限。文中一些表述难免有疏漏,同时,作者为了尽可能用通俗易懂的语言说清楚一些算法原理,会导致某些算法上的表述并不严谨,但不妨碍我们对一些算法有一个感性认识。如果您有更好的想法,也欢迎联系我,一起学习交流。


参考资料

  1. http://v.ccdi.gov.cn/2022/08/24/VIDEbPKeBjuogdZH2Lu8jpOK220824.shtml中央纪委国家监委网站(存算一体)

  2. https://zhuanlan.zhihu.com/p/32649047 部分图片


附之前的9篇文章链接:

AI加速(一)| 闲聊GPU

AI加速(二)| 计算和存储的分离

AI加速(三)| 每条指令都是流水线的工人

AI加速(四)| 衣柜般的分层存储设计

AI加速(五)| 一个例子看懂流水

AI加速(六)| 异构编程

AI加速(七)| 存算一体

AI加速(八)| Unrooling

AI加速(九)| 深度理解吞吐量和延时

公众号后台回复“极市直播”获取100+期极市技术直播回放+PPT

极市干货
技术干货损失函数技术总结及Pytorch使用示例深度学习有哪些trick?目标检测正负样本区分策略和平衡策略总结
实操教程GPU多卡并行训练总结(以pytorch为例)CUDA WarpReduce 学习笔记卷积神经网络压缩方法总结

点击阅读原文进入CV社区
收获更多技术干货

【声明】内容源于网络
0
0
极市平台
为计算机视觉开发者提供全流程算法开发训练平台,以及大咖技术分享、社区交流、竞赛实践等丰富的内容与服务。
内容 8155
粉丝 0
极市平台 为计算机视觉开发者提供全流程算法开发训练平台,以及大咖技术分享、社区交流、竞赛实践等丰富的内容与服务。
总阅读3.2k
粉丝0
内容8.2k