译:为多核系统并行性能线程化Fortran应用

| Comments

0. 前言

本文翻译自Intel网站上的一篇文章,原题为Threading Fortran applications for parallel performance on multi-core systems。由于本人翻译水平有限及专业术语知道的不多,如有不明白地方可查阅原文,发现错误的话敬请告之,谢谢!

1. 正文

现在大多数处理器都是多核的,预计未来性能增加主要来自于核数的增加。对于那些忽视额外核带来的机遇的性能敏感应用(performance sensitive applications)将很快被淘汰。本文讨论现存串行Fortran应用如何利用多核共享内存系统方法。议题包括数据布局,线程安全,性能及调试。Intel提供了一些软件工具帮助开发健壮、扩展性好的并行应用。

并行层次

  1. SIMD 指令

    -编译器可以自动对循环向量化

  2. 指令级

    -处理器调度(你看不到)

  3. 线程级(通常是共享内存)

    -原生Linux或Windows线程

    -OpenMP

    -Simplest for muliti-core

  4. 分布式内存集群

    -消息传递(一系列MPI)

    -CoArray Fortran(未来的Fortran 2008标准)

  5. embarassingly parallel multiprocessing

引入线程方法

  1. 线程库, 如 Intel® MKL

    -容易且有效,前提是适合你的问题

  2. 编译器自动并行化

    -容易做,但应用范围受限

    -针对编译器认为安全的简单循环

  3. 异步I/O (非常专业,见编译器说明文档)

  4. 原生线程

    -大多数用于任务级并行

    -不是太容易编程和调试的

  5. OpenMP

    -设计用于简化数据级并行

    -(相对的)容易编程和调试

    -一定程度上支持任务级并行,尤其是在OpenMP 3.0中

    -可移植性

Intel® Math Kernel Library

  1. MKL许多部件有线程化的版本

    -基于编译器的OpenMP运行时库

    -1,2及3级的 BLAS , LAPACK

    -稀疏 BLAS

    -离散傅里叶变化

    -向量数学和随机数函数

    -直接稀疏求解器, 如 PARDISO

  2. 连接线程或非线程接口

    -libmklintelthread.a 或 libmkl_sequential.a

    -使用link line 顾问,在 en-us

  3. 设置线程数

    -设置 MKLNUMTHREADS 或 OMPNUMTHREADS 环境变量 -调用 mklsetnumthreads 或 ompsetnumthreads 库函数

实例: PARDISO(Parallel Direct Sparse Solver)

  1. 共享内存系统上大型稀疏对称和非对称系统线性方程组求解器

    -主要求解器使用OpenMP线程化

    -仅仅连接到线程层,libmklintelthread

    -iparm(2)=3将加入线程化用于初始重排序阶段

    -大型问题扩展性很好

    -Fortran 90,Fortran 77 和 C 接口

    -F90 接口能在调用序列时捕捉许多错误

    -支持实数,复数,单双精度

    -iterative refinement

    -使用 MKLNUMTHREADS 或 OMPNUMTHREADS 环境变量控制线程变量 -否则默认处理器数(包括超线程)

  2. 算法见 http://www.pardiso-project.org

自动化并行

  1. 编译器可对简单循环自动线程化

    -Linux下加 -parallel 、Windows下加 /Qparallel 编译

    -至少 -O2 等级优化(比照 OpenMP在 -O0下工作)

    -循环必须满足“简单”条件

    -报告哪个循环并行了,和哪个循环没有,为什么没有并行

    -选项 -par-report2等

    -使用选项 -par-thresholdn 微调并行代价模型

    -默认 n=100, 尝试 n=99

  2. 基于和OpenMP相同的运行时库线程调用

    -调用 kmpcfork_call

    -这些是对低级pthreads 和 Win32线程库的封装

    -识别相同的 OpenMP环境变量

自动化并行条件

  1. 入口处确定循环计数器 (DO WHILE不行)

    -在编译时不必要确定

    -不能跳进或跳出循环(如Fortran中循环不能有goto语句)

  2. 循环迭代独立

    -没有函数调用(或证明没有负作用)

    -除了是内联

    -没有别名(通过不同指针访问相同变量)

    -没有像 X(L+1)=Y(L+1) + X(L) 这样的结构

    -允许规约

    -但部分和可能导致舍入差异

  3. 工作量足以抵消并行开销

  4. OpenMP循环条件类似于自动化并行条件

  5. 指令可用于引导编译器:

    • !DIR$ PARALLEL

    • 断言没有循环数据依赖

    • !DIR$ PARALLEL ALWAYS

    • 重置代价模型,即使编译器认为性能不会改善(像 单个循环的-par-threshold())也线程化循环

    • !DIR$ LOOP COUNT

    • 估计迭代的典型数字(typical number of iterations)

Example: matrix multiply

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  subroutine matmul(a,b,c,n)
  real(8) a(n,n),b(n,n),c(n,n)
  c=0.d0
  do i=1,n         ! Outer loop is parallelized.
     do j=1,n      ! inner loops are interchanged
        do k=1,n  ! new inner loop is vectorized 
           c(j,i)=c(j,i)+a(k,i)*b(j,k)
        enddo
     enddo
  enddo
  end

  $ ifort -O3 -parallel -par-report1 -c matmul.f90
  matmul.f90(4) : (col. 0) remark: LOOP WAS AUTO-PARALLELIZED.

OpenMP-优点

  1. 基于编译器指令的标准API

    -最新版本是 3.0

    -C++ 和 Fortran, Linux 和 Windows

    -对不支持OpenMP编译器来说指令相当于注释

    -串并行实现包含在一份源代码中

    -有助于调试

    -允许增量式并行

    -OpenMP规则使检测工具更容易

OpenMP编程模型

Fork-Join 并行:
  1. 主线程生成一个线程组

    -并行被增量式增加

    -串行程序进化成并行程序

    -线程不会被摧毁,但返回到一个线程池中(pool)

    注意Intel的OpenMP实现在使用的线程之外创建了一个单独的监控线程

OpenMP-在哪线程化

  1. 开始于罗列高层机构

  2. 你的程序在哪花最多时间?

    -如果你不知道,做下快速性能分析

    -VTune, PTU, gprof, …

    -如果你的程序只有 x% 并行,加速比总是小于 x%, 无论多少核和线程。

  3. 更喜欢数据并行

    -容易负载均衡

    -容易扩展到更多核

  4. 喜欢粗粒度(高层次)并行

    -例如嵌套的外层循环, 最慢变化的格点坐标, 高层驱动程序

    -减少开销

    -改善每个线程的数据局部性(locality)和重用性

    -不能并行迭代循环,例如时间积分

示例:Square_Charge

  1. 计算一个平面上一系列点的静电势能,由于均方充电分布

    -本质上,对一个平方的2维积分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Square_charge loops over points

 Twod_int integrate over y

  Trap_int integrate over x

   Func calculates 1/r potential

   – Inline func()

   – Vectorize loop over x

   – Thread loop over y

    – Avoid race conditions

    – Could instead thread loop over points, or use MPI

OpenMP: 线程如何交互?

  1. OpenMP 是一个共享内存模型

    -线程之间通过共享变量通信

  2. 不能共享数据共享引起数据竞争:

    -数据竞争:随线程调度不同,程序结果变化

  3. 控制数据竞争

    -使用同步语句阻止数据冲突

  4. 同步代价昂贵,所以…

    -改变数据访问方式尽量减少同步需求

OpenMP-data

  1. 识别哪个数据是线程间共享的,哪个是每个线程单独一个副本

  2. 使模块或common块中共享数据显式global,线程私有数据为局部和自动数据,这种做法有好处,但不是必须

  3. 动态分配 OK (malloc, ALLOCATE)

    -如果共享,分配在串行区域

    -如果每个线程需要各自副本,分配在并行区域

  4. 每个线程有它自己私有堆栈,但所以线程共享堆(heap)

    -所以要使堆对象线程安全需要锁,而这个代价更高

OpenMP-数据作用域

  1. 区分字面显式并行区域和”动态区域“(函数或子程序在一个显式并行区域内调用。这些函数或子程序 可能不包含OpenMP指令或只包含”orphaned“ OpenMP指令

  2. 字面显式:!$OMP PARALLEL 到 !$OMP END PARALLEL

    -数据默认共享(除了循环索引)

    -局部数据:可以用 PRIVATE子句改变

    -全局数据: 可以用 !$OMP THREADPRIVATE 申明 common块,模块变量

    -并行区域内初始值未定义

    -除非用 FIRSTPRIVATE(局部变量)

    -或 COPYIN(全局变量)

    -出了并行区域后的值未定义

    -除非使用 LASTPRIVATE(局部变量)

    -调用的函数(动态区域)必须线程安全,即使他们本身不包含显式并行区域

线程安全

  1. 一个线程安全函数可以同时被多个线程调用,并仍可得出正确结果

    -潜在的数据冲突必须被阻止(同步)或避免(私有化)

    -静态局部数据:默认情况下,每个线程可访问相同数据地址!潜在不安全

    -自动数据:每个线程拥有各自独立的副本,放在各自堆栈中

  2. ifort 串行默认:

    -局部标量是自动变量

    -局部数据是静态变量

  3. 当用 -openmp 编译时,默认改变

    -局部数组是自动

    -和 -auto 编译相同

    -这可能增加需要的堆栈大小

    -小心段错误

使函数线程安全

  1. 使用编译选项

    -选项 -openmp 或

    -选项 -auto

    -可能对串行优化有轻微影响

  2. 在源代码中

    -在声明中使用 AUTOMATIC关键字

    -但不覆盖编译器产生的临时文件

    -声明函数为 RECURSIVE

    -覆盖整个函数,包括编译器产生代码

    -如果你不想依赖编译选项,这是最好办法使代码线程安全

  3. 下面两种情况,任意一种:

    -不用 -save 或 SAVE 关键字

    -避免全局变量

    -或不要写他们,除了同步

  4. OpenMP有许多同步结构保护潜在不安全的操作

    • REDUCTION 子句

    • !$OMP CRITICAL

    • !$OMP SINGLE

    • 等等

线程安全库

  1. Intel® MKL库是线程安全的

    -串行版本和线程版本一样

  2. Intel Fortran运行时库有两个版本

    -默认是线程不安全的(libifcore)

    -加 -threads 编译连接线程安全版本 (libifcoremt)

    -如果你用 -openmp 编译, 默认连接的是线程安全版本

性能考虑

  1. 首先开始优化串行代码,向量化内循环等(例如 -O3 -ipo …)

  2. 确保足够的并行工作量

  3. 最小化线程间数据共享

    -除非是只读变量

  4. 避免cache行错误共享 (false sharing of cache lines)

1
2
3
4
!$OMP parallel do
  do i=1,nthreads
    do j=1,1000
      A(i,j) = A(i,j) + ..

-每个线程认为它的 A(i,j)副本可能无效

-转换 A 下标 改善每个线程数据 locality

-连续内存访问也允许内循环向量化

-有助于提高性能

  1. 调度选项

    -如果工作在循环迭代数不是均匀分布的,考虑 DYNAMIC 或 GUIDED

线程级应用计时

  1. Fortran标准计时器 CPU_TIME 返回 ”处理器时间“

    -所有线程/核 的时间总和

    -很像 Linux 下 “time” 命令的 “user time”

    -所以它可能表现为线程级应用跑的没有串行版本快

  2. Fortran内置子程序 SYSTEM_CLOCK 从真实时钟返回数据

    -不相加每个核时间

    -很像 Linux 下 “time” 命令的 “real time”

    -可用来测试由于线程化的加速比

1
2
3
4
   Call system_clock(count1, count_rate)
              ……
              Call system_clock(count2, count_rate)
              Time = (count2 - count1) / count_rate
  1. dclock (Intel专门函数)也可以用来计时

线程亲和力(Affinity)接口

  1. 允许OpenMP线程绑定物理或逻辑核

    -export 环境变量 KMP_AFFINITY=

    -在分配逻辑核前物理上使用使用所有物理核(超线程)

    -紧密分配线程给相同socket连续核(例如受益于共享cache)

    -分散分配线程给交替sockets(例如使内存通道最大化)

    -有助于优化内存或cache访问

    -如果超线程支持的话,尤其重要

    -否则一些物理核闲置而另外一些跑多个线程

    -详情见编译器文档

NUMA考虑

  1. 想要内存分配更接近它将要使用的地方

    • “first touch” 决定分配位置

    -所以初始化一个OpenMP循环内数据方式与你计划之后要用到的方式一样

1
2
3
4
5
6
  !$OMP parallel do                      !$OMP parallel do
        do i=1,n                                   do i=1,n
          do j=1,m                                  do j=1,m
            A(j,i) = 0.0                               dowork(A(j,i)) 
          enddo                                      enddo
        enddo                                      enddo
  1. 记住设置 KMP_AFFINITY

常见问题

  1. 不足的堆栈大小

    -OpenMP中最经常遇到的问题

    -典型症状: 初始化是段错误

  2. 对于整个程序(共享+局部数据):

    -增加shell limits值

    -(地址空间,内存分配)

    • Bash : ulimit -s [value in KB] or [unlimited]

    -只增加一次

    • C shell: limit stacksize 1000000 (1 GB)

    • Windows : /F:100000000 (以字节为单位)

    • 典型 OS 默认: ~ 10 MB

  3. 对于单个线程(只针对线程局部数据)

    • export OMP_STACKSIZE=[size],默认4 MB

    -实际分配内存,不要设置太大

调试 OpenMP 应用提示

  1. 设置 OMPNUMTHREADS=1 运行

    -产生线程级代码,但用一个线程运行

    -如果工作,产生对线程代码Thread Checker

    -如果失败,排除数据竞争或其他同步问题原因

  2. 用 -openmp-stubs-auto 编译

    -RTL调用被解决,但没有线程级代码产生

    -分配局部数组在堆栈上,和OpenMP一样

    -如果工作,检查缺失的 FIRSTPRIVATE ,LASTPRIVATE

    -如果失败, 消除了线程级代码产生原因

  3. 如果没有 -auto 编译,隐含改变内存模型

    -可能不足的堆栈大小

    -可能连续调用之间值未保存

  4. 如果用 PRINT 语句调试

    -内部I/O缓冲线程安全(加上-openmp),但不同线程打印语句顺序不确定

    -输出线程号用 ompgetthread_num()

    -记住调用模块 USE OMP_LIB (申明这个是整数)

  5. 用 -O0 -openmp 调试

    -不行其它优化,OpenMP线程在 -O0 等级时不关闭

浮点重复性

  1. 用不同数量线程运行同样程序可能得出稍微不同结果

    -由于不同工作分解导致微小的舍入差异

    -大多数情况可通过 -fp-model precise 解决

    -查看 “Consistency of Floating-Point Results using the Intel® Compiler

  2. 在OpenMP中浮点规约仍不能严格再现,即使线程数相同

    -不同线程贡献顺序每次运行可能都不同

    • -fp-model precise 这儿没什么帮助

    -如果关心这个,需要些显式代码

Intel 专门环境变量

  1. KMP_SETTINGS = 0 | 1

    -在运行时打印环境变量或默认

  2. KMP_VERSION = off | on

    -打印运行时库版本

  3. KMP_LIBRARY = turnaround | throughput | serial

    • turnaround idle threads do not yield to other processes

    • throughput idle threads sleep and yield after KMP_BLOCKTIME msec

  4. KMP_BLOCKTIME

    • 线程在睡眠前等待时间(默认200毫秒)
  5. KMP_AFFINITY

  6. KMPMONITORSTACKSIZE

    -设置分配给监控线程的堆栈

  7. KMPCPUINFOFILE

    -使用机器拓扑文件(例如代替Linux的 /proc/cpuinfo)

调试 OpenMP 应用工具

  1. 编译器源代码checker (’parallel lint’)

    • ifort -openmp -diag-enable sc-parallel3

    • 产生一些可能API违规操作的错误和警告诊断,包括

    -依赖性或数据竞争

    -例如:由于缺少 PRIVATE 或 REDUCTION 申明

    -并行区域闭一只,包括动态区域

    -可以跨源文件分析

  2. Updated Intel Parallel Debugger, idb (Linux) and Intel Parallel Debugger Extension (on Windows)

    – Thread groups; lock-stepping; thread-specific break points

    – On-the-fly serialization; shared data access detection

    – OpenMP windows for threads, tasks, barriers, locks, …

    – New for Fortran in version 11.1 update 2

Intel® Thread Checker

  1. Unified set of tools that pinpoint hard-to-find errors in multi-threaded applications – data races

    – deadlocks

    – memory defects

    – security issues

  2. Display data at the Linux command line or via a Windows GUI

Intel® Thread Profiler

  1. Features & Benefits

    – View application concurrency level to ensure core utilization

    – Identify where thread and synchronization related overhead impacts performance

    – Identify objects impacting performance

    – Visualize the distribution of work to threads

    – Visualize when threads are active and inactive

    – Supports native threads , Intel® Threading Building Blocks or OpenMP* applications on Windows* for IA-32/Intel® 64 architecture

    – Data collector for Linux, but must copy to Windows for display.

总结

Intel软件工具提供对利用多核架构线程级应用扩展支持。

提供线程化一个Fortran应用时引起的一系列问题的建议和背景信息。

引用

[1] Intel Software Products information, evaluations, active user forums

[2] Parallelism tips

[3] Developing Multithreaded Applications: A Platform Consistent Approach

[4] The Intel® Fortran Compiler User and Reference Guides, available online

Comments