|
: [; L; x4 u) w
# O! F" |: x) }7 A 撰文 | 袁进辉
3 C; G( t8 i% x 上周写了一篇《浅谈GPU虚拟化和分布式深度学习框架的异同》,想不到引起很多关注和讨论。和朋友们讨论之后,觉得这个话题值得再发散一下: 5 R7 P& t3 `' m0 {
首先,文章只讨论了GPU“一分多”这种“狭义”的虚拟化,还存在另外的虚拟化,“多虚一”也是存在的。在此,我想特别强调的是:上一篇文章后半部分对深度学习框架“多合一”的讨论恰恰是想说明“多虚一”是不可能靠仅仅一层包打天下的API就能实现,反而要靠”去虚拟化“的思路。 # W3 B' U! x; g/ V: |% w( v
其次,“去虚拟化”这种理念在追求性能极致的场景不是新鲜事,在这篇文章里再举几个相关的例子。
, R! M: J0 w% `7 Y. V" ] 在这篇小品文里再讨论一下:虚拟化的本质和局限性,为什么在追求性能极致的场景要“去虚拟化”。 " Q5 L0 e; t8 H* _' N: x" O' N( A
1
, s4 j$ h$ s6 k: W3 k4 v “虚拟化”溯源 ) x8 p1 A% T, }0 c- S6 r9 \; K
“虚拟化”可能是计算机科学历史上最伟大的思想之一,对此,计算机先驱David Wheeler有一句名言: ) f0 [. T2 f7 ]
All problems in computer science can be solved by another level of indirection, except for the problem of too many layers of indirection. 3 j1 `5 f: x1 x
这句话被尊称为“软件工程的基本定理”,并被C++之父Bjarne Stroustrup在专著《The C++ Programming Language》的序言处引用。不过,大部分人记住了前半句,却忽略了后半句,而后半句正是本文想展开讨论的。 ) p* d' a' J& t# |3 Q" E
"another level of indirection"刻画了“虚拟化”的精髓:通过引入一层新的抽象,把与上层应用无关的细节隐藏掉,有选择地给上层用户暴露一些功能供其使用,也称底层细节对用户透明,既不损害上层应用的功能,又能享受“关注点分离”(separation of concerns)的好处,增加易用性。
! ?: l' q! r. O1 g7 p2 _' y; Z7 n5 { 可以说:虚拟化的案例无处不在,无往而不利。 2 d$ e6 q) ^" v; H7 O
操作系统是对硬件资源的虚拟化:计算核心被虚拟化成进程;硬件内存变成虚拟内存;存储介质被虚拟化成文件系统;网络传输通过多层协议栈被虚拟化成文件描述符,使得数据传输就像读写普通的文件一样。
& I& C5 v; ~4 b- F9 J+ k 分布式存储把通过网络互联多个单机文件系统虚拟化成的一个网络文件系统,使得用户不用再关心网络数据传输的细节,可以像访问本地文件一样访问其它节点上的数据。 # r C. {) Z- ^2 T1 x" l
当然,虚拟化造就了今天伟大的云计算技术和市场,云原生如火如荼,开发者只需要基于云服务的API编程,而不需要关于API 之下的物理细节,成本低,可靠性又高。 ; n4 L+ g3 a( E- l3 m
虚拟化的成功不可否认。但是,成也萧何,败也萧何,虚拟化也有其代价。 , N% M% ?! _# P
2 * V; G4 U( _ p
“虚拟化”和极致性能的矛盾
3 y! Z" Q, z3 U7 M5 U' d 一层层的抽象层次,一步步降低编程的复杂度,每一层都向上隐藏了一些东西,上层就相应地丢失一些操控能力。每经过一层抽象,就引入一些对上层而言的“不确定性”,最终的结果是,性能的天花板一步一步下降。 & o( `4 J9 q0 J% B
以Hadoop的分布式存储为例,虚拟化的结果是掩盖了数据真实的存储位置,不管在哪个节点上,访问接口都是一样的。不过,当向这个集群调度一些MapReduce的数据处理任务时,如果Task被调度到数据所在的节点上,那么就不需要网络传输,如果调度到另外的节点,就需要把数据读到内存并通过网络发送到计算所在的节点。也就是,被隐藏掉的数据位置信息有可能被调度器用来提升系统效率。
8 B+ b0 t, z5 j( @; Z& _; U 随着摩尔定律放缓,不止在深度学习领域,在任何追求极致性能的场景就出现了一种击穿、击碎中间抽象层次,一竿子插到底进行协同优化的强烈需求。
. y+ J5 p2 v4 Z) o+ c4 I; V' B 让我们看六个例子。 9 y) ?8 P( t: ~& D! q( T
1、操作系统把CPU资源抽象成被操作系统调度和管理的内核线程,避免了用户调度计算资源的麻烦,但是,在高并发场景,用户级线程(如coroutine)越来越多,计算资源更多的在用户态(user mode)来调度,而不是完全依靠内核的调度。
; T7 _/ U. A9 _3 X; P% c 2、操作系统通过页表机制实现虚拟内存,神不知鬼不觉实现数据的换入换出,但在高性能场景,用户程序会通过编程接口禁止这一行为,譬如在RDMA和GPU异步数据传输都依赖于锁页内存,确保操作系统不会帮倒忙。 - `0 X s# T2 A
3、传统的网络传输协议栈TCP/IP都在操作系统内核,为了避免内核态和用户态的数据拷贝和上下文切换,人们先是使用用户态协议栈(如Intel dpdk),仍无法满足需求的话,就使用支持内核旁路(bypass)的RDMA技术,完全跳过了操作系统这一层。
8 x. B6 n+ W( ?, B9 d$ Z+ { 4、缓存(Cache)技术也可以视为一种虚拟化,命中就直接使用,没命中就花时间从更慢的存储取过来,在局部性较好的负载下工作得很好。但在特定领域,譬如深度学习领域,工作负载有特别的规律,如果用软件来控制缓存数据的弹出以及读取甚至可以做到100%的命中率,这种办法被称为Scratchpad,现在几乎所有的AI芯片都没有用Cache,而是使用Scratchpad技术。
* \5 W( W0 o. o/ K 5、操作系统的文件系统或者网络文件系统,把底层硬件的细节隐藏了,编程时不需要考虑数据在哪台机器的哪块磁盘上,编程更简单了,但访问近处的数据速度快,访问远处的数据速度慢,为了提高效率,有人就提出了locality aware(局部性感知)的调度,把计算任务尽可能调度到存储数据的节点上去。
4 V$ [7 ^: d" j {/ C 6、工业级的大型软件系统里,通常不会使用库函数,恨不得把底层代码库重新打造一遍,譬如Chromium, OceanBase等大型C++项目,我想,这也可以算作反虚拟化的例子。 ! ]- B9 _ b& \( X7 r" h3 D0 L
3 ; E! X1 Y! _( X7 w+ Y2 R) k
“虚拟化”的舒适区 5 s4 p, ?9 I M6 I
虚拟化试图向上层提供一剂一劳永逸的灵丹妙药,既解决易用性,又不损害上层应用对性能的需求。但是,“虚拟化”到底好不好应该具体问题具体分析,那该如何判断虚拟化到底合不合适呢?
% T0 ~- [/ X! z: r+ P 这里尝试抛出一个用来判断是否有必要引入一层抽象(虚拟化)的量化指标,供参考。每引入一层虚拟化,就向上层隐藏了一些东西,向上提供的服务的延迟就因此引入了不确定性(以分布式存储为例,有的数据近,有的数据远;以分布式GPU资源池为例,有的近,有的远),服务的响应延迟有一个波动范围,也就是[min, max],延迟越小越好。 5 T# b6 {& n) \- c) ?( J
+ E2 T% T: f- t* r/ [
如果上层某一个应用需要保证的最低延迟仍大于max,那么这层虚拟化对这个应用就没有损害,可以大胆的引入这个虚拟化技术(也就是上图的C区域)。
( W3 z! d7 t+ D$ ? 如果上层某一个应用需要保证的最低延迟介于min和max之间,那么引入这层虚拟化就对这个应用是有损害的,把这层虚拟化敲掉,就可以确保以min值满足这个应用对延迟的需求(也就是上图的B区域)。 ; u* s3 M1 O, W& U7 |8 ]* D* y' j
如果上层某一个应用需要保证的最低延迟小于min,那么即使敲掉当前层的虚拟化,仍不能满足这个应用,就需要继续向下敲,进行联合优化,直到延迟得到满足(也就是上图的A区域)。 $ }& \! @% ]% ~: x7 Z$ ~
4 : ^- w, u/ [( \
鱼与熊掌兼得? ! i% l, A! D( g: @- o! E
如上所述,虚拟化的基本思路是通过“隐藏细节”给上层应用提供一种假象,降低上层应用使用底层资源的复杂度。不过,有时候,“隐藏”掉的信息会阻碍上层应用挖掘极致的性能。有没有两全其美的办法呢? / e: V f6 g$ j. L: b; x
David Patterson在《计算机体系结构的黄金时代》一文中开出的药方是从算法到硬件直接打通,结合算法和硬件的特点全部搞定制。定制表现在DSL和DSA,一方面是设计领域特定语言,方便编程,另一方面为面向领域应用设计领域特定架构,挖掘极致效率。
9 R$ ?1 U4 y" v5 ]+ J David Patterson的药方既不是在已有方案中插入新的抽象层次,也不是完全不要中间抽象,而是把原有的抽象层次全部敲碎重建,这种重建是基于对算法和硬件特点的充分挖掘。我理解这里有两个关键:软硬件协同设计(分工),以及编译器技术。
% ~9 G7 O; h4 C! C0 J% b1 | 一方面,这里的思路和虚拟化不同之处是:不是向上层应用隐藏什么,而是强调要向上层暴露什么,或者说向上层让渡什么职责。 1 Q' A* _8 n" o7 U6 h
另一方面,这里没有期待只要提供一层API的抽象就包打天下,而是对从算法到硬件的映射复杂性有充足的认识。这种映射既包含任务相关但硬件无关的问题,也包含硬件相关的问题,本质等同于人们熟知的编译器技术。 ' O6 O" k2 _. z! q" ^. d# \* x4 }
说到这里,想到另一个“去虚拟化”的绝佳例子,就是软件定义网络(Software defined network,SDN)。对于同一套硬件基础设施,承接不同的工作负载,交换机的最优转发规则是不同的。SDN的思路是,令交换机的转发规则是可编程的,对每一个不同的业务负载,都用静态分析得到最优的转发规则或策略(control plane),并按照这个规则对交换机编程,交换机在转发数据时只需要按照已编程的规则执行即可(data plane)。 2 {) P4 ?( S0 I: i) M( N7 N
在SDN的例子里,交换机让渡了一部分职责给软件,软件根据不同的业务负载都生成不一样的路由规则(策略),这是SDN平衡灵活性和极致性能的关键。
% r1 l; R c0 [) Q) L' s' k$ R. A 在OneFlow解决分布式深度学习的难题时,也是类似的思路,它不是靠额外引入的一个抽象层次实现,而是分成了控制平面和数据平面。在控制平面,编译器根据特定深度学习模型的任务负载和底层硬件拓扑生成对这个配置最优的执行计划(execution plan), 这个plan不是一劳永逸的,它需要上层模型的知识,也需要底层硬件的知识,模型或硬件拓扑一旦变化,plan就会变化。但是,编译器生成plan的机制可以认为是不变的,也是整套系统的精髓。 ) y; \3 ~# o% @( ?1 Y
5
- Q. f3 n2 F& n* F* e! K* Q" o5 b 结语 . ~/ ]" a6 j/ C- w( O; ^4 G4 j; W
这篇文章补充解释了我对“通过向上层算法隐藏硬件信息”的虚拟化思路,以及“通过向上层算法暴露硬件信息和让渡职责”的去虚拟化的思路的理解。
. [* X4 y; m* S 简单来说,虚拟化的思想强调的是隐藏(底层细节)和限制(上层的功能范围),潜台词是:我认为对上层应用的需求足够了解,告诉上层应用太多细节也没有用,我把底层的东西一揽子处理好了,你只管调用我为你提供的API就可以了。 7 G- S# ] A* v( n) c
软件定义的思想强调的是暴露(底层细节)和让渡(硬件的策略可被软件分析和配置),潜台词是:我对上层应用的需求了解不足,没有办法为上层应用提供一个足够令人满意的一揽子的策略,于是干脆把底层策略暴露给上层,上层应用对自己的需求和任务负载最清楚,上层自己来负责生成策略并来配置底层。
% F u: B* o1 W( w 致力于提升深度学习系统性能的朋友对“去虚拟化”的思路应该是习以为常的,这应该是整个社区经过探索和碰壁逐渐收敛出来的主流思路,不是我个人的发明,我只是把观察和理解写下来而已。
& q# |! t7 A: w9 j8 U7 O" L. P" S 顺着这个思路,算法-软件-硬件协同优化领域正在发生一些令人兴奋的进展。毫无疑问,广义上的编译器技术(无论是单设备代码生成,还是分布式执行计划生成)是里面最核心的部分。 $ d; L5 h# n" N, D' s; [+ ^$ ~
注:题图源自Pixabay
* r2 l$ K1 n7 q! @ 欢迎下载体验OneFlow新一代开源深度学习框架:https://github.com/Oneflow-Inc/oneflow ' |+ l( U9 Y5 H- O( u4 j2 f5 Z
4 ^1 ?, g9 v, E* H- n& T
8 T9 r$ P6 p- D
|