在上一篇中我們介紹了 MPI-3 中增強(qiáng)的單邊通信方法费薄,下面我們將介紹 MPI-3 中共享內(nèi)存操作。
當(dāng)前硬件的發(fā)展趨勢(shì)是向著多核(眾核)的大(共享)內(nèi)存方向發(fā)展栖雾,單臺(tái)計(jì)算節(jié)點(diǎn)上的核數(shù)越來(lái)越多楞抡,內(nèi)存也越來(lái)越大。在此情況下析藕,共享內(nèi)存并行編程模式的優(yōu)勢(shì)也越來(lái)越顯著召廷。經(jīng)常采用的并行編程模式有 POSIX 線程和 OpenMP。MPI-1 和 MPI-2 標(biāo)準(zhǔn)沒(méi)有提供共享內(nèi)存機(jī)制账胧,各個(gè)進(jìn)程擁有獨(dú)立的內(nèi)存空間竞慢,在 MPI-1 中某個(gè)進(jìn)程只能通過(guò)顯式的消息傳遞來(lái)讀取或更改其它進(jìn)程的數(shù)據(jù),MPI-2 引進(jìn)了單邊通信的方法治泥,允許某個(gè)進(jìn)程以遠(yuǎn)端內(nèi)存訪問(wèn)(RMA)的方式獲取或者更新其它進(jìn)程開(kāi)放窗口內(nèi)的數(shù)據(jù)筹煮,但在本質(zhì)上各個(gè)進(jìn)程之間的內(nèi)存空間并不共享,各個(gè)進(jìn)程之間不能以一種常規(guī)的加載與存儲(chǔ) (load and store) 的方式直接操作其它進(jìn)程的內(nèi)存空間居夹,即使這些內(nèi)存空間可能存在于同一臺(tái)節(jié)點(diǎn)上败潦。各進(jìn)程間顯式的消息傳遞和遠(yuǎn)端內(nèi)存訪問(wèn)操作都可能需要額外的內(nèi)存復(fù)制,從而降低運(yùn)算性能和增大內(nèi)存消耗准脂。為了獲得更高的運(yùn)算性能和降低內(nèi)存消耗劫扒,通常會(huì)在 MPI 程序中混合共享內(nèi)存的機(jī)制,如 POSIX 線程或 OpenMP 等狸膏。這種集成 MPI 和其它外部編程模型的編程方式會(huì)增加編程的復(fù)雜性沟饥,甚至可能會(huì)導(dǎo)致死鎖,數(shù)據(jù)丟失或其它錯(cuò)誤結(jié)果等。
MPI-3 定義了一種共享內(nèi)存機(jī)制贤旷,多個(gè)進(jìn)程可以通過(guò)一種共享內(nèi)存窗口將自己的部分內(nèi)存空間暴露給其它進(jìn)程广料。這是一種可移植的共享內(nèi)存機(jī)制,各進(jìn)程間共享的內(nèi)存可以由 CPU 通過(guò)直接的 load/store 指令進(jìn)行獲取遮晚,就像 POSIX 線程和 OpenMP 等其它共享內(nèi)存機(jī)制一樣性昭。這就允許我們?cè)谝粋€(gè)統(tǒng)一的編程模型下進(jìn)行通常的 MPI 操作和共享內(nèi)存操作,避免混合進(jìn)外部的共享內(nèi)存編程模型及其所帶來(lái)的各種問(wèn)題县遣,降低編程的復(fù)雜度糜颠,提高程序的可移植性,且容易與已經(jīng)存在的 MPI 程序集成萧求。
線程其兴,標(biāo)準(zhǔn) MPI 和 MPI-3 共享內(nèi)存的內(nèi)存共享模型分別如下圖所示:
共享內(nèi)存窗口只能創(chuàng)建在能夠共享內(nèi)存的進(jìn)程(如處在同一節(jié)點(diǎn)上的進(jìn)程)上,不處在同一個(gè)共享內(nèi)存窗口上的進(jìn)程之間只能使用標(biāo)準(zhǔn)的 MPI 通信機(jī)制夸政,如消息傳遞或遠(yuǎn)端內(nèi)存訪問(wèn)等進(jìn)行相互之間的數(shù)據(jù)操作元旬。下圖給出這一編程模型的示意圖:
方法接口
下面給出 MPI-3 共享內(nèi)存相關(guān)方法接口。
MPI.Comm.Split_type(self, int split_type, int key=0, Info info=INFO_NULL)
根據(jù) split_type
將與當(dāng)前 comm 相關(guān)聯(lián)的組分解為不相交的子組守问,并為每個(gè)子組創(chuàng)建一個(gè)相關(guān)聯(lián)的通信子返回匀归。每個(gè)組內(nèi)進(jìn)程的 rank 按照 key
參數(shù)所指定的方式定義,新通信域中各個(gè)進(jìn)程的順序編號(hào)根據(jù) key
的大小決定耗帕,即 key
越小穆端,則相應(yīng)進(jìn)程在原來(lái)通信域中的順序編號(hào)也越小,若兩個(gè)進(jìn)程的 key
相同仿便,則根據(jù)這兩個(gè)進(jìn)程在原來(lái)通信域中的順序號(hào)決定新的編號(hào)体啰。該方法是一個(gè)集合操作,該通信子內(nèi)的所有進(jìn)程的 split_type
參數(shù)必須都相同或者為 MPI.UNDEFINED嗽仪,但是可以提供不同的 key
值荒勇。指定 split_type
值為 MPI.UNDEFINED 的進(jìn)程將返回 MPI.COMM_NULL。MPI 標(biāo)準(zhǔn)定義的 split_type
值只有 MPI.COMM_TYPE_SHARED闻坚,表示將當(dāng)前通信子中的進(jìn)程分解為可以共享內(nèi)存(比如進(jìn)程都在在同一臺(tái)節(jié)點(diǎn)上)的子通信子沽翔。各 MPI 實(shí)現(xiàn)可能會(huì)定義額外的 split_type
值。
MPI.Win.Allocate_shared(type cls, Aint size, int disp_unit=1, Info info=INFO_NULL, Intracomm comm=COMM_SELF)
分配指定大小的共享內(nèi)存并創(chuàng)建和返回用于單邊通信的窗口對(duì)象窿凤。注意調(diào)用該方法的通信子包含的所有進(jìn)程必須能夠操作同一塊共享內(nèi)存(比如說(shuō)所有進(jìn)程都處在同一臺(tái)節(jié)點(diǎn)上)搀擂,否則會(huì)出錯(cuò)。在組內(nèi)通信子 comm
所指定的通信子范圍內(nèi)所有進(jìn)程上執(zhí)行集合操作卷玉。每個(gè)進(jìn)程所返回的窗口對(duì)象會(huì)包含一塊分配好的 size
大小的共享內(nèi)存(可以被該通信子內(nèi)的所有進(jìn)程訪問(wèn))哨颂。每個(gè)進(jìn)程的 size
可以不同,甚至可以為 0相种。disp_unit
指定在遠(yuǎn)端內(nèi)存訪問(wèn)操作中的地址單位威恼,即 origin 所指定的位置在 target 一側(cè)要以 target 進(jìn)程所指定的 diap_unit
為單位計(jì)算品姓。通常如果采用相同類(lèi)型創(chuàng)建窗口,則統(tǒng)一將 disp_unit
設(shè)置成 1 即可箫措。如果有的進(jìn)程需要以組合數(shù)據(jù)類(lèi)型(type)給出緩沖區(qū)腹备,則可能需要指定 disp_unit
為 sizeof(type)。info
對(duì)象用于為 MPI 環(huán)境提供優(yōu)化所需的輔助信息斤蔓。默認(rèn)情況下植酥,各個(gè)進(jìn)程分配的內(nèi)存會(huì)是一塊整體的連續(xù)內(nèi)存區(qū)。但是如果設(shè)置 info
的 key alloc_shared_noncontig 為 "true"弦牡,則會(huì)分配不連續(xù)的內(nèi)存塊友驮。
注意:雖然每個(gè)進(jìn)程可以分配不同大小的內(nèi)存區(qū)域,但是為了提高程序的性能驾锰,如果要分配連續(xù)的共享內(nèi)存卸留,通常讓單個(gè)進(jìn)程分配此完整的連續(xù)內(nèi)存區(qū),其它進(jìn)程設(shè)置 size
為 0椭豫,然后將此連續(xù)內(nèi)存(虛擬地)分配給各個(gè)進(jìn)程耻瑟。
MPI.Win.Shared_query(self, int rank)
該方法查詢 rank 為 rank
的進(jìn)程通過(guò) MPI.Win.Allocate_shared 方法所分配的內(nèi)存區(qū)在當(dāng)前進(jìn)程的地址等信息。對(duì)內(nèi)存中的同一塊內(nèi)存區(qū)赏酥,該方法對(duì)不同的進(jìn)程可能會(huì)返回不同的(虛擬)地址喳整。該方法只能由 flavor 為 MPI.WIN_FLAVOR_SHARED 的窗口對(duì)象調(diào)用,否則會(huì)產(chǎn)生 MPI.ERR_RMA_FLAVOR 錯(cuò)誤裸扶。該方法會(huì)返回由內(nèi)存緩沖區(qū)和偏移單位組成的二元 tuple框都。當(dāng) rank
參數(shù)設(shè)置成 MPI.PROC_NULL 時(shí),會(huì)返回分配內(nèi)存非 0 的 rank 最小的那個(gè)進(jìn)程的內(nèi)存緩沖區(qū)和偏移單位組成的二元 tuple姓言。
例程
下面給出使用例程。
# shm.py
"""
Demonstrates the usage of MPI-3 shared memory operation.
Run this with 4 processes like:
$ mpiexec -n 4 -host node1,node2 python shm.py
"""
import numpy as np
from mpi4py import MPI
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
# split the 4 processes into 2 sub-comms, each can share memory
shm_comm = comm.Split_type(MPI.COMM_TYPE_SHARED)
shm_rank = shm_comm.rank
itemsize = MPI.INT.Get_size()
if shm_rank == 0:
nbytes = 10 * itemsize
else:
nbytes = 0
# on rank 0 of shm_comm, create the contiguous shared block
win = MPI.Win.Allocate_shared(nbytes, itemsize, comm=shm_comm)
# create a numpy array whose data points to the shared mem
buf, itemsize = win.Shared_query(MPI.PROC_NULL)
# create a numpy array from buf
buf = np.array(buf, dtype='B', copy=False)
ary = np.ndarray(buffer=buf, dtype='i', shape=(10,))
# in process rank 1 of shm_comm:
# write the numbers 0, 1, 2, 3, 4 to the first 5 elements of the array
if shm_comm.rank == 1:
ary[:5] = np.arange(5, dtype='i')
# wait in process rank 0 of shm_comm until process 1 has written to the array
shm_comm.Barrier()
# check that the array is actually shared and process 0 can see
# the changes made in the array by process 1 of shm_comm
if shm_comm.rank == 0:
print ary
# show non-contiguous shared memory allocation
if shm_rank == 0:
nbytes = 4 * itemsize
else:
nbytes = 6 * itemsize
info = MPI.Info.Create()
info.Set('alloc_shared_noncontig', 'true')
win = MPI.Win.Allocate_shared(nbytes, itemsize, comm=comm, info=info)
info.Free()
運(yùn)行結(jié)果如下:
$ mpiexec -n 4 -host node1,node2 python shm.py
[0 1 2 3 4 0 0 0 0 0]
[0 1 2 3 4 0 0 0 0 0]
以上介紹了 MPI-3 中共享內(nèi)存操作蔗蹋,在下一篇中我們將介紹 MPI 中多線程的使用何荚。