改进 VASP 材料模拟性能
利用最新的 Intel® 软件开发工具更有效地利用硬件。
材料建模和设计是科学和工程领域中非常重要的一个分支。合成材料是现代生活的一部分。我们使用的电脑、汽车、飞机、道路,甚至房屋和食品的材料,都得益于材料科学的进步。高性能计算 (HPC) 使得现代材料模拟和设计成为可能。而 Intel® 软件开发工具 可以帮助开发人员和最终用户在模拟中达到最佳性能。
VASP* 是基于第一性原理进行量子材料建模的顶级应用之一。它是一款专为计算性能而设计的 HPC 应用。它使用 OpenMP* 来充分利用系统中的所有核心,并使用 MPI* 将计算分布到大型 HPC 集群上。
让我们看看 Intel 软件开发工具如何帮助对 VASP 工作负载进行性能剖析和调优。
基准测试 VASP
为了获取 VASP 性能的初步信息,我们在配备 Intel® Xeon® 可扩展处理器 的系统上对其进行了测试。(请注意,VASP 是受版权保护的软件,归维也纳大学(奥地利)所有,由物理学院的 Georg Kresse 教授代表。使用 VASP 必须获得适当的许可。)
我们的性能实验使用了 VASP 开发版本,该版本实现了 MPI/OpenMP 混合并行(参见 Porting VASP from MPI to MPI+OpenMP [SIMD]),以模拟硅——具体来说,是使用 HSE 混合泛函理论 来模拟块体硅中的空位。
我们使用此命令启动 VASP
$ mpiexec.hydra –genv OMP_NUM_THREADS ${NUM_OMP} -ppn ${PPN} -n ${NUM_MPI} ./vasp_std
命令中
${NUM_OMP}
是 OpenMP 线程数。${PPN}
是每个节点上的 MPI 进程数。${NUM_MPI}
是当前模拟中的 MPI 进程总数。
对于基准测试,我们将使用带有 LOOP+
时间的输出,该时间衡量主要计算的执行时间,不包括预处理和后处理。
初步结果是针对一个双路 Intel® Xeon® Gold 6148 处理器(2.4 GHz,每路 20 核,每节点 40 核),如 **表 1** 所示。
表 1. Intel Xeon Gold 处理器集群的初始 LOOP+ 和 MPI_Allreduce 时间
配置: 硬件:Intel® Xeon® Gold 6148 处理器 @ 2.40GHz;192 GB RAM。互连:Intel® Omni-Path Host Fabric Interface (Intel® OP HFI) Adapter 100 Series [独立]。软件:Red Hat Enterprise Linux* 7.3;IFS 10.2.0.0.158;Libfabric 1.3.0;Intel® MPI Library 2018 (I_MPI_FABRICS=shm:ofi);Intel® MPI Benchmarks 2018 (使用 Intel® C++ Compiler XE 18.0.0 for Linux* 构建)。基准测试来源:Intel Corporation。
我们将使用这些数据作为基准性能。本文将使用 MPI_Allreduce
代码路径。基于 MPI_Reduce
的默认方法可以使用相同的方法。
现在,让我们看看如何使用 Intel 软件开发工具来优化应用程序性能。
使用 Application Performance Snapshot 进行性能剖析
典型的 HPC 应用是一个由不同编程和执行模型组成的复杂系统。要在现代集群上实现最佳性能,开发人员应考虑系统的许多方面,例如
- MPI 和 OpenMP 并行
- 内存访问
- FPU 利用率
- I/O
任何一个方面的性能不佳都会降低整体应用程序性能。Application Performance Snapshot (APS)(在 Intel® VTune™ Amplifier 中)是一个可以快速总结这些区域性能的强大功能。它也可以作为独立实用程序免费提供。
根据 Getting Started with APS,启动 APS 的 MPI 应用程序的推荐方法是运行此命令来收集 MPI 应用程序的数据
$ <mpi launcher> <mpi parameters> aps <my app> <app parameters>
其中
<mpi launcher>
是 MPI 作业启动器,例如 mpirun、srun 或 aprun。<mpi parameters>
是 MPI 启动器参数。
请注意
apps
必须是最后一个<mpi launcher>
参数。<my app>
是您的应用程序的位置。<app parameters>
是您的应用程序参数。
APS 会启动应用程序并进行数据收集。分析完成后,将创建一个 aps_result_<date>
目录。
Intel® MPI Library 还提供了两个非常方便的选项,用于将外部工具集成到启动过程中
- aps
- gtool
让我们分别看看这两个选项。
-aps 选项
当您使用此选项时,会生成一个新的统计数据文件夹:aps_result_<date>-<time>
。您可以使用 aps 实用程序分析收集到的数据。例如
$ mpirun -aps -n 2 ./myApp $ aps aps_result_20171231_235959
此选项明确针对 aps
,非常符合我们的目标。
-gtool 选项
使用此选项通过 mpiexec.hydra
和 mpirun
命令启动 Intel VTune Amplifier、Intel® Advisor、Valgrind* 和 GNU* Debugger (GDB*) 等工具,为指定的进程服务。此选项的替代方法是使用 I_MPI_GTOOL
环境变量。
–gtool
选项旨在简化特定进程的分析,让您不必在每个参数集(以冒号“:”分隔)中指定分析工具的命令行。虽然允许在参数集内使用 –gtool
,但不要在多个参数集中使用,也不要混合两种分析方法(使用 -gtool
和参数集)。
语法
-gtool "<command line for tool 1>:<ranks set 1>[=launch mode 1][@arch 1]; <command line for tool 2>:<ranks set 2>[=exclusive][@arch 2]; … ;<command line for a tool n>:<ranks set n>[=exclusive][@arch n]" <executable>
或
$ mpirun -n <# of processes> -gtool "<command line for tool 1>:<ranks set 1>[=launch mode 1][@arch 1]" -gtool "<command line for a tool 2>:<ranks set 2>[=launch mode 2][@arch 2]" … -gtool "<command line for a tool n>:<ranks set n>[=launch mode 3][@arch n]" <executable>
表 2 显示了参数。
表 2. 参数
请注意,对于同一个 @arch
参数,进程集不能重叠。缺少 @arch
参数也被视为不同的体系结构。因此,以下语法被认为是有效的
-gtool "gdb:0-3=attach;gdb:0-3=attach@hsw;/usr/bin/gdb:0-3=attach@knl"
另外,请注意,某些工具可能无法协同工作,或者它们的同步使用可能导致不正确的结果。表 3 列出了 [=launch mode]
的参数值。
表 3. [=launch mode] 的参数值
这是一个非常强大且复杂的控制选项。Intel MPI Library Developer Reference 有一个专门介绍 gtool 选项的章节。我们将重点关注我们需要的选项。除了命令行选项外,还可以通过环境变量 (I_MPI_GTOOL)
来控制它们,这样我们就可以保持启动命令不变。这在您拥有复杂的启动脚本来在运行前设置好一切时非常有用。
我们期望的分析很简单,所以我们只需要设置
$ export I_MPI_GTOOL="aps:all"
并收集 VASP 的配置文件。配置文件名为 ,可以通过以下方式处理
$ aps –report=<name>
这将生成一个 HTML 文件,其结果如图 **1** 所示。
APS 提供了可操作的信息,用于查找性能瓶颈。VASP 显示为 MPI 密集型(19.12% 的运行时花费在 MPI 上)。我们可以查看 MPI 时间部分了解更多详情。它显示 MPI 不平衡消耗了 10.66% 的已用时间。MPI 不平衡定义为在 MPI 中花费的无效时间,例如等待其他进程达到 MPI_Allreduce
调用所需的时间。顶级 5 个 MPI 函数部分显示了最耗时的函数。在此次运行中,MPI_Allreduce
是最主要的消耗者,这表明它应该是我们进行性能优化的首要目标。
我们将尝试使用最新的 Intel MPI Library 2019 和 Intel® Performance Scaled Messaging 2 (PSM2) Multi-Endpoint (Multi-EP) 技术来加速 MPI_Allreduce
。让我们开始吧。
Intel® MPI Library 与 Intel® Omni-Path 架构
从前面提供的系统规格可以看出,我们正在使用 Intel® Omni-Path 架构 (Intel® OPA) 硬件。对于 Intel MPI Library,充分利用所有 Intel Omni-Path 架构功能的最佳方式是为 Intel MPI Library 2019 使用 Open Fabrics Interface (OFI)。您可以在以下位置找到与 Intel MPI Library 2018 Update 1 捆绑的 Intel MPI Library 2019 技术预览版:
<install_dir>/compilers_and_libraries_2018.1.163/linux/mpi_2019/
TMI 和 OFI 都支持 PSM2 作为使用 Intel OPA 的最佳选项。
$ export I_MPI_FABRICS=shm:ofi $ export I_MPI_OFI_PROVIDER=psm2
Intel Performance Scaled Messaging 2 (PSM2) 是 PSM 的后继者,它提供了一个 API 和库,针对具有海量 MPI 进程的大型集群。有关概述,请参阅 PSM2 的新特性和功能。
Intel PSM2 Multi-Endpoint (Multi-EP) 和 Intel MPI Library 2019
Intel PSM2(以及 PSM)的一个关键概念是端点。Intel PSM2 遵循端点通信模型,其中端点被定义为一个对象(或句柄),用于支持与其他端点进行消息的发送和接收。(在此处了解更多信息 here。)默认情况下,每个进程只能使用一个端点。但随着 Intel PSM2 的最新版本,这种情况发生了变化,Intel PSM2 Multi-Endpoint (Multi-EP) 功能允许与多线程应用程序一起使用端点。Intel MPI Library 2019 使开发人员能够有效地使用 Intel PSM2 Multi-Endpoint。对于 Multi-Endpoint 支持,MPI 标准线程模型已扩展到 MPI_THREAD_SPLIT
编程模型。它允许程序有效地与 MPI 进行多线程编程,从而消除大部分同步并提高性能。
正如 Intel® MPI Library Developer Reference for Linux* OS 中所述,一个 MPI_THREAD_SPLIT-
兼容程序至少必须是一个线程兼容的 MPI 程序(支持 MPI_THREAD_MULTIPLE
线程级别)。除此之外,还有以下规则适用:
- 进程的不同线程 不得同时使用同一通信器。
- 在线程中创建的任何请求 不得被其他线程访问。也就是说,任何非阻塞操作都必须在同一线程中完成、检查完成或探测。
- 通信完成调用(暗示操作进度,如
MPI_Wait()
或MPI_Test()
)从一个线程调用,并不保证在其他线程中的进度。
此外,所有线程都应有一个标识符 thread_id
,并且 MPI 进程之间的通信只能针对具有相同 thread_id
的线程进行。
Collectives 中的 Multi-EP:MPI_Allreduce
由于 VASP 已经拥有混合 MPI/OpenMP 版本,因此可以轻松地适配 MPI Multi-Endpoint。对于每个 MPI 进程,我们使用 MPI_Comm_dup
例程创建与 OpenMP 线程数相同的额外 MPI 通信器。然后,每个 MPI 缓冲区将在 OpenMP 线程之间进行分割,如图 **2** 所示。
为了跳过所有代码处理,让我们使用 PMPI_
接口并用所需的修改重写 MPI_Allreduce
例程
/* *Copyright 2018 Intel Corporation. *This software and the related documents are Intel copyrighted materials, and your use of them is governed by the express license under which they were provided to you (License). *Unless the License provides otherwise, you may not use, modify, copy, publish, distribute, disclose or transmit this software or the related documents without Intel’s prior written permission. *This software and the related documents are provided as is, with no express or implied warranties, other than those that are expressly stated in the License. */ #include <stdio.h> #include <mpi.h> #include <omp.h> #include <unistd.h> int comm_flag=0; int new_comm=0; int first_comm=0; int init_flag=0; int mep_enable=0; int mep_num_threads=1; int mep_allreduce_threshold=1000; int mep_bcast_threshold=1000; int k_comm=0; void mep_init() { if(init_flag == 0) { mep_enable=atoi(getenv("MEP_ENABLE")); mep_num_threads=atoi(getenv("MEP_NUM_THREADS")); mep_allreduce_threshold=atoi(getenv("MEP_ALLREDUCE_THRESHOLD")); mep_bcast_threshold=atoi(getenv("MEP_BCAST_THRESHOLD")); } init_flag=1; } int MPI_Allreduce(const void *sendbuf, void *recvbuf, int count, MPI_Datatype datatype, MPI_Op op, MPI_Comm comm) { int err; int count_thread; int rest=0; int num_threads=1; int thread_num=0; MPI_Aint lb,extent; size_t datatype_size; if (init_flag == 0) mep_init(); MPI_Type_get_extent(datatype, &lb, &extent); datatype_size=extent;
// if MPI_Allreduce use MPI_IN_PLACE if (&sendbuf[0] == MPI_IN_PLACE) { if(mep_enable == 1) { if(count >= mep_allreduce_threshold) { comm_duplicate(comm,mep_num_threads); count_thread=count/mep_num_threads; rest=count%mep_num_threads; #pragma omp parallel num_threads(mep_num_threads) private (num_threads, thread_num) { num_threads=omp_get_num_threads(); thread_num=omp_get_thread_num(); if(thread_num == num_threads-1) { err = PMPI_Allreduce(MPI_IN_PLACE, recvbuf+thread_num*count_thread*datatype_size, count_thread+rest, datatype, op, comms.dup_comm[k_comm][thread_num]); } else { err = PMPI_Allreduce(MPI_IN_PLACE, recvbuf+thread_num*count_thread*datatype_size, count_thread, datatype, op, comms.dup_comm[k_comm][thread_num]); } } } else { err = PMPI_Allreduce (MPI_IN_PLACE, recvbuf, count, datatype, op, comm); } } else { err = PMPI_Allreduce (MPI_IN_PLACE, recvbuf, count, datatype, op, comm); } } // else MPI_Allreduce not use MPI_IN_PLACE else { if(mep_enable == 1) { if(count >= mep_allreduce_threshold) { comm_duplicate(comm,mep_num_threads); count_thread=count/mep_num_threads; rest=count%mep_num_threads; #pragma omp parallel num_threads(mep_num_threads) private (num_threads,thread_num) { num_threads=omp_get_num_threads(); thread_num=omp_get_thread_num(); if(thread_num == num_threads-1) { err = PMPI_Allreduce(sendbuf+thread_num*count_thread*datatype_size, recvbuf+thread_num*count_thread*datatype_size, count_thread+rest, datatype, op, comms.dup_comm[k_comm][thread_num]); } else { err = PMPI_Allreduce(sendbuf+thread_num*count_thread*datatype_size, recvbuf+thread_num*count_thread*datatype_size, count_thread, datatype, op, comms.dup_comm[k_comm][thread_num]); }
} } } else { err = PMPI_Allreduce (sendbuf, recvbuf, count, datatype, op, comm); } } else { err = PMPI_Allreduce (sendbuf, recvbuf, count, datatype, op, comm); } } return err; }
请注意,comm_duplicate(comm,mep_num_threads)
是一个复制通信器的函数。我们将使用它与 LD_PRELOAD
来替换默认的 MPI_Allreduce
。
使用 Multi-EP 运行
修改后的命令行为:
$ export I_MPI_THREAD_SPLIT=1 $ export I_MPI_THREAD_RUNTIME=openmp $ export MEP_ALLREDUCE_THRESHOLD=1000 $ export MEP_ENABLE=1 $ export OMP_NUM_THREADS=2 $ export MEP_NUM_THREADS=2 $ LD_PRELOAD= lib_mep.so $ mpiexec.hydra -ppn ${PPN} -n ${NUM_MPI} ./vasp_std
性能结果如 **表 4** 所示。
表 4. Intel Xeon Gold 处理器集群的最终 LOOP+ 和 MPI_Allreduce 时间(报告了 OpenMP x MPI 的最佳组合),与初始数据(报告了 OpenMP x MPI 的最佳组合)进行比较
提高性能
使用 APS,我们发现 MPI_Allreduce
是应用程序中的一个性能“热点”。借助新的 Intel MPI Library 2019 技术预览版和 Intel OPA,我们能够更有效地利用系统硬件来加速 MPI_Allreduce
并改进整体应用程序。仅通过软件修改,观察到的最大加速比为 MPI_Allreduce
达到 2.34 倍,LOOP+
达到 1.16 倍。