高性能计算(High-performance computing, HPC)是科学研究的前沿领域。科学家们经常在分布式环境中的数百万核心上运行模拟。通常涉及的关键软件栈是静态编译语言,如C/C++或Fortran,结合OpenMP*/MPI*。这种软件栈经受了时间的考验,在HPC世界中几乎无处不在。其关键在于其效率,即利用所有可用计算资源的同时限制内存使用。这种效率水平一直是更“高级”脚本语言如Python*、Octave*或R*所无法企及的。这主要是因为这些语言被设计为高生产力环境,以促进快速原型设计——而不是设计为在大型计算集群上高效运行。然而,技术上没有理由应该是这样,这代表了科学计算的工具问题。
Julia*,一种新的技术计算语言,旨在解决这个问题。它像Python或Octave一样阅读,但性能与C相当。它内置了多线程和分布式计算的原语,允许应用程序扩展到数百万核心。
首先想到的自然问题是:为什么其他高级语言不能表现得这么好?Julia有什么独特之处,让它在保持用户编写可读代码的同时达到这种效率水平?答案是Julia使用类型推断和代码专业化的组合来生成紧凑的机器代码。
为了看看如何做到这一点,让使用Julia宏@code_native,它生成函数调用生成的汇编代码。例如:
julia> @code_native 1 + 1
.section __TEXT,__text,regular,pure_instructions
Filename: int.jl
pushq %rbp
movq %rsp, %rbp
Source line: 32
leaq (%rdi,%rsi), %rax
popq %rbp
retq
Source line: 32
nopw (%rax,%rax)
这个例子显示了当Julia添加两个整数时生成的汇编代码。注意Julia为操作选择了正确的汇编指令,几乎没有开销。
下一个代码片段显示了两个双精度浮点数的加法:
julia> @code_native 1. + 1.
.section __TEXT,__text,regular,pure_instructions
Filename: float.jl
pushq %rbp
movq %rsp, %rbp
Source line: 240
addsd %xmm1, %xmm0
popq %rbp
retq
Source line: 240
nopw (%rax,%rax)
Julia自动选择了不同的指令addsd,因为现在正在添加两个浮点数。这是因为根据编译器推断的输入类型,为“+”函数生成了两个版本。这个想法允许用户编写高级代码,然后让Julia推断输入和输出的类型,并随后为这些输入类型编译该函数的专门版本。这就是Julia快速的原因(图1)。
图1:Julia基准时间相对于C(越小越好)。C性能=1.0。基准代码可以在GitHub仓库中找到。
Julia的独特功能解决了数据科学和数值计算中的“两种语言问题”。这导致了Julia在从航空航天到金融的各种行业中的需求。
这导致了Julia Computing的形成,它与行业协商并促进社区采用Julia。Julia Computing的旗舰产品之一是JuliaPro*,这是一个Julia发行版,包括IDE、调试器和100多个经过测试的包。JuliaPro很快将配备Intel® Math Kernel Library (Intel® MKL),用于加速BLAS操作和针对多核和最新Intel®处理器的优化。
Julia在每个级别都有多个内置的并行计算原语:向量化(SIMD)、多线程和分布式计算。
以一个令人尴尬的并行模拟为例。π的迭代计算涉及生成0和1之间的随机数,并最终计算“落地”在单位圆内的点数与没有落地的点数的比率。这给出了单位正方形和单位圆的面积比,然后可以用来计算π。以下Julia代码使用了@parallel构造。归约器“+”将最终表达式计算的值相加,这些值是在所有Julia进程上并行计算的:
addprocs(2) # 添加2个Julia工作进程
function parallel_π(n)
in_circle = @parallel (+)
for i in 1:n # <-- 分区工作
x = rand()
y = rand()
Int((x^2 + y^2) < 1.0)
end
return (in_circle/n) * 4.0
end
parallel_π(10000)
Julia将其工作卸载到其工作进程,这些进程计算所需的结果并将它们发送回Julia主进程,在那里执行归约。可以通过这种单向通信模型将任意计算片段分配给不同的工作进程。
一个更有趣的用例是让不同的进程解决,例如,线性方程组,然后将它们的输出序列化并发送到主进程。可扩展的机器学习就是这样一个例子,它涉及许多独立的回溯求解。
图2显示了RecSys.jl的性能,它处理数百万电影评分以进行预测。它基于一种名为交替最小二乘(ALS)的算法,这是一种简单的、迭代的协同过滤方法。由于每次求解都是独立的,因此可以轻松并行化和扩展此代码。
图2是一个性能图表,显示了系统的扩展特性。
图2. ALS推荐系统的扩展图表:“nprocs”指的是共享内存和分布式设置中的工作进程数量以及多线程设置中的线程数量。
在图2中,查看了Julia在多线程模式(Julia MT)、分布式模式(Julia Distr)和共享内存模式(Julia Shared)中的扩展性能。共享内存模式允许独立的Julia工作进程(与线程相对)访问共享地址空间。这个图表的一个有趣方面是Julia的分布式能力与Apache Spark*的比较,Apache Spark*是一个流行的可扩展分析框架,在行业中被广泛采用。然而,更有趣的是这个实验的后果:Julia可以很好地扩展到更多的节点。现在让继续进行超级计算中的一个真实实验。
将其全部整合在一起(大规模):Celeste*
Celeste*项目是Julia Computing(Keno Fischer)、Intel Labs(Kiran Pamnany)、JuliaLabs@MIT(Andreas Noack, Jarrett Revels)、劳伦斯伯克利国家实验室(David Schlegel, Rollin Thomas, Prabhat)和加州大学伯克利分校(Jeffrey Regier, Maximilian Lam, Steve Howard, Ryan Giordano, Jon McAuliffe)的合作。Celeste是一个完全生成的层次模型,使用统计推断数学定位和描述天空中的光源。该模型允许天文学家识别有前景的星系进行光谱仪定位,定义星系进行进一步探索,并帮助理解暗能量、暗物质和宇宙的几何形状。实验使用的数据集是斯隆数字巡天(SDSS)(图3),包含超过500万张图像,总计55TB数据。
图3:斯隆数字巡天(SDSS)的样本
使用Julia的原生并行计算能力,研究人员能够将他们的应用程序扩展到劳伦斯伯克利国家实验室国家能源研究科学计算中心(NERSC)Cori*超级计算机上的8,192个Intel® Xeon®处理器核心。这导致图像分析的并行加速比为225倍,处理了超过20,000张图像(或250GB),与以前的迭代相比增加了三个数量级。
请注意,这是通过Julia的原生并行能力实现的,允许扩展到256个节点和每个节点4个线程。这种扩展使Julia牢固地进入了HPC领域,加入了C/C++和Fortran等精英语言的行列。
Intel Labs的Pradeep Dubey很好地总结了这一点:“有了Celeste,更接近于将Julia带入对话,因为展示了使用混合并行性的卓越效率——不仅仅是进程,还有线程——这在Python或R中仍然是不可能的。”
项目的下一步是进一步扩展并处理整个数据集。这将涉及利用最新的Intel® Xeon Phi™处理器和协处理器。
打破新边界
自2012年成立以来,Julia项目已经取得了长足的进步,在科学计算中打破了新的界限。Julia被构想为一种语言,它保留了Python的生产力,同时像C/C++一样快速且可扩展。Celeste项目表明,这种方法允许Julia成为第一个在集群上很好地扩展的高生产力语言。
Julia项目继续每年翻一番其用户基础,并在各种行业中被越来越多的采用。计算科学工具的这种进步不仅对科学研究解决即将到来的时代的挑战有利,而且还将在行业中导致快速的创新周期和周转时间。