CPU中央处理器,负责执行用户和操作系统下发的指令。CPU只能接受01二进制语言,0和1用来控制高低电位。比如,一个加法运算,在x86处理器上的的二进制代码为:
01001000 00000001 11000011
这样一行代码被称为机器码,它执行了加法操作。除了这样的加法,CPU的电路还要实现很多其他指令,如存取内存数据,进行逻辑判断等。不同厂商的电路设计不同,在电路上所能进行的二进制码不同。某类CPU能支持一种指令集(instruction set architecture)。指令集相当于一种设计图纸,规定了一种CPU架构实现哪些指令。
参照指令集,硬件开发人员只需要关心如何设计电路,软件开发人员只关心如何用01机器码实现软件功能。比较常见的指令集有x86、ARM、MIPS、SPARC、Power等。
一个单核CPU架构包括:
随着技术的发展,计算机的速度瓶颈已经变成了超高速的CPU运算速度与落后的数据读取速度之间的矛盾。CPU计算速度在纳秒级别,但是CPU读取主存的速度竟有百纳秒,CPU进行完计算后,要闲置几十倍的时间,实在是巨大的浪费。为了解决这个问题,设计人员为CPU增加了很多中间缓存Cache。
CPU的寄存器存取速度极快,但是造价成本太高,发热量大,不能被大量采用。
当单个CPU主频超过一定范围后,CPU成本和散热成了很大的问题,主频很难突破10GHz。为了获得更快的计算速度和更好的性能,芯片设计者决定绕过主频,采用人海战术,在一块CPU中增加多个核心(Core)。
一个核心是一个可以运行指令的独立单元,它包含了前面所提到的ALU和寄存器,并配备L1和L2 Cache。多个核心共享L3 Cache。
高性能服务器通常可以支持多个处理器(CPU),提供更多计算核心。支持单个CPU的服务器被称为单路服务器,支持两个CPU的服务器被称为双路服务器,支持四个CPU的服务器被称为四路服务器。Intel的四路架构,系统支持四个CPU,假如每块CPU内有8个核心,系统可对外提供32核计算能力。
在多核架构出现之前,CPU在某个特定时刻只能执行某个程序,无法并行。但前文提到,CPU处理速度是纳秒级,速度非常快,所以在单核时代,为了同时处理多项任务,CPU采用不停地在多个线程任务间切换的形式实现多线程。
一个kernel实际上会启动很多线程,这些线程是逻辑上并行的,但是在物理层却并不一定。这其实和CPU的多线程有类似之处,多线程如果没有多核支持,在物理层也是无法实现并行的。逻辑上和单核CPU一样,虽然只有一个核,但是还能实现多线程。
单个CPU每次切换不同的线程任务,会产生一些资源开销。
但是随着多核多线程架构出现,又引出另外一个问题:多线程安全问题。当多个核心都处理相同任务,极有可能使用同一块数据,就有可能出现数据读写的问题:例如,进行i = i + 1操作,如果两个线程短时间内都对变量i加一,变量应该被加了两次。
由于两个线程相隔时间太短,加上前面所说的缓存机制,计算的过程和临时结果还在寄存器和L1缓存,没来得及写到主存上(缓存保存问题)。线程B读到还是较老的数据,这样就出现了数据不一致的情况。这种问题被称为线程安全问题。一般需要使用锁来处理线程安全问题。
前面说到,不同架构的CPU会适配一套指令集,而书写指令集的代码被称为机器语言或机器码。
很多时候我们使用计算机时都接触得到可执行文件(.exe文件),而可执行文件就是二进制机器语言的集合,可以被机器执行,得到我们想要的结果。
正是基于不同厂商有不同的指令集,催生了C语言,建立了一个更为通用的编程范式。
C语言从源代码到执行,要使用编译器来编译(compile)、汇编(assembly)并连接(link)所依赖的库,形成机器可执行文件。执行某个二进制文件时,操作系统会为程序分配内存和CPU资源。“编译”和“汇编”,相当于将C语言翻译成底层语言。另外,代码中使用了库函数printf,当我们使用别人写好的函数时,需要将这些前人写好的库函数连接到我们的可执行文件中,否则会调用函数失败的错误。我们将这种需要编译的语言称为编译型语言。编译型语言有C/C++、Fortran等。
编译、汇编、连接缺一不可,特别是连接的存在,导致不同文件编译的顺序要求严格,继而调试困难。
又由于不同操作系统下,因为架构的不同,调用各种接口的代码也会不同,继而编译过程也不相同,应用软件也就有了不同操作系统下的不同版本。
因为IT圈的一句名言就是:计算机科学任何领域的问题都可以通过增加一个中间层来解决。一些大牛忍受不了C语言这样编写和调试太慢,系统平台之间无法共享移植的问题,于是开始自立门户,创建了新的编程语言,最有名的要数Java和Python,这类语言不需要每次都编译,因此被称为解释型语言。matlab、R、JavaScript也是解释语言。
解释型语言一般是使用C语言等偏底层的语言做一个虚拟机或者解释器,编程人员需要先在自己的计算机上安装这个解释器,接下来就只用关心自己的源代码,其他的事情都交给解释器去做。
如果把编译型语言的编译过程比作将源代码“翻译”成机器语言的话,那么解释性语言就是同声传译。
编译型语言是一篇提前就“翻译”好的稿子,拿过来就能被读出来,这样肯定更快;解释型语言要等翻译边“听”边“翻译”,速度当然慢很多。
如果我比较贪心,又想要高效率,也不想损失可移植性和易用性呢?当前的技术为我们提供了两个方案:
1、使用编译型语言写的模块:
以Python为例,为了保证性能,大部分高性能科学计算库其实都是使用编译型语言编写的。比如numpy,用户安装numpy的包时,其实就是下载了C/C++和Fortran源代码,并在本地编译成了可执行的文件。
2、JIT(Just-In-Time) 即时编译技术
JIT把需要加速的代码编译成了机器语言,不再需要“同声传译”拖累自己了。比如在Python上调用numba库进行过JIT测试,同样的代码会有8倍以上的速度提升。而numba不仅可以编译用于CPU执行的代码,达到和C相比拟的速度,同时还可以调用GPU库(如NVIDIA的CUDA和AMD的ROCs等)来实现GPU加速,这些都可以简单的利用Python中的装饰器来实现。
在进行计算时,都需要用核心(Core)来做算术逻辑运算。核心中有ALU(逻辑运算单元)和寄存器等电路。在进行计算时,一个核心只能顺序执行某项任务。为了同时并行地处理更多任务,芯片公司开发出了多核架构,只要相互之间没有依赖,每个核心做自己的事情,多核之间互不干扰,就可以达到并行计算的效果,极大缩短计算时间。
个人桌面电脑CPU只有2到8个CPU核心,数据中心的服务器(多个CPU)上也只有20到40个左右CPU核心,GPU却有上千个核心。与CPU的核心不同,GPU的核心只能专注于某些特定的任务,但是在执行比较简单而重复的任务时,GPU的超多核心就表现出了巨大的优势。
GPU核心在做计算时,只能直接从显存中读写数据,程序员需要在代码中指明哪些数据需要从内存和显存之间相互拷贝。这些数据传输都是在总线上,因此总线的传输速度和带宽成了部分计算任务的瓶颈。也因为这个瓶颈,很多计算任务并不适合放在GPU上。比如在深度学习中,因为输入是大规模稀疏特征,GPU加速获得的收益小于数据互相拷贝的时间损失。
CPU主要从主存(Main Memory)中读写数据,并通过总线(Bus)与GPU交互。GPU除了有超多计算核心外,也有自己独立的存储,被称之为显存。
显存再往下分为什么register,shared memory,L1,啊啥的,一级一级,同理于CPU的register,L1,L2,Main Memory,Fixed Rigid Disk,Optical Disk,Magnetic Tape
ps:Cache的层次,一般有L1, L2, L3 (L是level的意思)的cache。 通常来说L1,L2是集成 在CPU里面的(可以称之为On-chip cache),而L3是放在CPU外面(可以称之为Off-chip cache)。
一个核心是一个可以运行指令的独立单元,它包含了前面所提到的ALU和寄存器,并配备L1和L2 Cache。多个核心共享L3 Cache。
一台服务器上可以安装多块GPU卡,但GPU卡的发热量极大,普通的空调系统难以给大量GPU卡降温,所以大型数据中心通常使用水冷散热,并且选址在温度较低的地方。
以上结构也被称为异构计算:使用CPU+GPU组合来加速计算。世界上顶尖的数据中心和超级计算机均采用了异构计算架构。例如超越天河2号成为世界第一的超级计算机Summit使用了9216个IBM POWER9 CPU和27648个英伟达Tesla GPU。
英伟达能够在人工智能时代成功,除了他们在长期深耕显卡芯片领域,更重要的是他们率先提供了可编程的软件架构。2007年,英伟达发布了CUDA编程模型,软件开发人员从此可以使用CUDA在英伟达的GPU上进行并行编程。
继CUDA之后,英伟达不断丰富其软件技术栈,提供了科学计算所必须的cuBLAS线性代数库,cuFFT快速傅里叶变换库等,当深度学习大潮到来时,英伟达提供了cuDNN深度神经网络加速库,目前常用的TensorFlow、PyTorch深度学习框架的底层大多基于cuDNN库。
GPU编程可以直接使用CUDA的C/C++版本进行编程,也可以使用其他语言包装好的库,比如Python可使用Numba库调用CUDA。CUDA的编程思想在不同语言上都很相似。