使用MPI並行化遺傳算法框架GAFT

前言

本文中做者使用MPI的Python接口mpi4py來將本身的遺傳算法框架GAFT進行多進程並行加速。並對加速效果進行了簡單測試。python

項目連接:git

正文

咱們在用遺傳算法優化目標函數的時候,函數一般都是高維函數,其導數通常比較難求取。這樣咱們的適應度函數計算一般都是比較費時的計算。github

例如在使用遺傳算法尋找最優結構時候一般須要調用量化軟件進行第一性原理計算結構的total energy,這是很是費時的過程; 例如咱們優化力場參數的時候,以力場計算出的能量同基準能量以前的偏差做爲適應度,也須要調用相應的力場程序獲取總能量來求取,一樣這個過程也是相對耗時的。算法

這就會致使一個問題,當咱們的種羣比較大的時候,咱們須要利用適應度信息來產生下一代種羣,這時候每一代繁殖的過程將會很耗時。但有幸的是,種羣的選擇交叉變異過程對於種羣中的個體都是相互獨立的過程,咱們能夠將這一部分進行並行處理來加速遺傳算法的迭代。編程

使用mpi4py

因爲實驗室的集羣都是MPI環境,我仍是選擇使用MPI接口來將代碼並行化,這裏我仍是用了MPI接口的Python版本mpi4py來將代碼並行化。關於mpi4py的使用,我以前寫過一篇博客專門作了介紹,能夠參見《Python多進程並行編程實踐-mpi4py的使用》app

將mpi4py的接口進一步封裝

爲了能讓mpi的接口在GAFT中更方便的調用,我決定將mpi4py針對遺傳算法中須要用的地方進行進一步封裝,爲此我單獨寫了個MPIUtil類, 詳細代碼參見gaft/mpiutil.py框架

封裝通訊子經常使用的接口

例如進程同步, 獲取rank,進程數,判斷是否爲主進程等。函數

class MPIUtil(object):
    def __init__(self):
        logger_name = 'gaft.{}'.format(self.__class__.__name__)
        self._logger = logging.getLogger(logger_name)

    # Wrapper for common MPI interfaces.
    def barrier(self):
        if MPI_INSTALLED:
            mpi_comm = MPI.COMM_WORLD
            mpi_comm.barrier()

 @property
    def rank(self):
        if MPI_INSTALLED:
            mpi_comm = MPI.COMM_WORLD
            return mpi_comm.Get_rank()
        else:
            return 0

 @property
    def size(self):
        if MPI_INSTALLED:
            mpi_comm = MPI.COMM_WORLD
            return mpi_comm.Get_size()
        else:
            return 1

 @property
    def is_master(self):
        return self.rank == 0複製代碼

組內集合通訊接口

因爲本次並行化的任務是在種羣繁衍時候進行的,所以我須要將上一代種羣進行劃分,劃分紅多個子部分,而後在每一個進程中對劃分好的子部分進行選擇交叉變異等遺傳操做。在最後將每一個字部分獲得的子種羣進行收集合並。爲此寫了幾個劃分和收集的接口:測試

def split_seq(self, sequence):
        ''' Split the sequence according to rank and processor number. '''
        starts = [i for i in range(0, len(sequence), len(sequence)//self.size)]
        ends = starts[1: ] + [len(sequence)]
        start, end = list(zip(starts, ends))[self.rank]

        return sequence[start: end]

    def split_size(self, size):
        ''' Split a size number(int) to sub-size number. '''
        if size < self.size:
            warn_msg = ('Splitting size({}) is smaller than process ' +
                        'number({}), more processor would be ' +
                        'superflous').format(size, self.size)
            self._logger.warning(warn_msg)
            splited_sizes = [1]*size + [0]*(self.size - size)
        elif size % self.size != 0:
            residual = size % self.size
            splited_sizes = [size // self.size]*self.size
            for i in range(residual):
                splited_sizes[i] += 1
        else:
            splited_sizes = [size // self.size]*self.size

        return splited_sizes[self.rank]

    def merge_seq(self, seq):
        ''' Gather data in sub-process to root process. '''
        if self.size == 1:
            return seq

        mpi_comm = MPI.COMM_WORLD
        merged_seq= mpi_comm.allgather(seq)
        return list(chain(*merged_seq))複製代碼

用於限制程序在主進程執行的裝飾器

有些函數例如日誌輸出,數據收集的函數,我只但願在主進程執行,爲了方便,寫了個裝飾器來限制函數在主進程中執行:優化

def master_only(func):
    ''' Decorator to limit a function to be called only in master process in MPI env. '''
 @wraps(func)
    def _call_in_master_proc(*args, **kwargs):
        if mpi.is_master:
            return func(*args, **kwargs)

    return _call_in_master_proc複製代碼

在遺傳算法主循環中添加並行

主要在種羣繁衍中對種羣針對進程數進行劃分而後並行進行遺傳操做併合並子種羣完成並行,代碼改動不多。詳見:github.com/PytLab/gaft…

# Enter evolution iteration.
for g in range(ng):
    # Scatter jobs to all processes.
    local_indvs = []
    local_size = mpi.split_size(self.population.size // 2)

    # Fill the new population.
    for _ in range(local_size):
        # Select father and mother.
        parents = self.selection.select(self.population, fitness=self.fitness)
        # Crossover.
        children = self.crossover.cross(*parents)
        # Mutation.
        children = [self.mutation.mutate(child) for child in children]
        # Collect children.
        local_indvs.extend(children)

    # Gather individuals from all processes.
    indvs = mpi.merge_seq(local_indvs)
    # The next generation.
    self.population.individuals = indvs複製代碼

測試加速效果

測試一維搜索

下面我針對項目中的一維優化的例子進行並行加速測試來看看加速的效果。例子代碼在/examples/ex01/

因爲本身本子核心數量有限,我把gaft安裝在實驗室集羣上使用MPI利用多核心進行並行計算覺得優化,種羣大小爲50,代數爲100,針對不一樣核心數能夠獲得不一樣的優化時間和加速比。可視化以下圖:

核心數 優化時間(s) 加速比
1 1.473 1.0
2 0.877 1.68
3 0.657 2.24
4 0.533 2.76
5 0.467 3.15
6 0.540 2.73
7 0.431 3.42
8 0.382 3.86
9 0.355 4.15
10 0.317 4.65

核心數與優化時間的關係:

核心數與加速比:

測試力場優化

這裏我對本身要研究的對象進行加速測試,這部分代碼並未開源,針對每一個個體的適應度計算都須要調用其餘的計算程序,所以此過程相比直接有函數表達式的目標函數計算要耗時不少。

一樣,我針對不一樣核心數看看使用MPI在集羣上加速的效果:

核心數 優化時間(s) 優化時間 加速比
1 2.29e04 6 h 21 min 1.0
2 1.94e04 5 h 23 min 1.18
4 1.62e04 4 h 30 min 1.41
6 1.35e04 3 h 45 min 1.69
12 8.73e03 2 h 25 min 2.62

核心數與優化時間的關係:

核心數與加速比:

可見針對上述兩個案例,MPI對遺傳算法的加速仍是比較理想的,程序能夠扔到集羣上飛起啦~~~

總結

本文主要總結了使用mpi4py對遺傳算法進行並行化的方法和過程,並對加速效果進行了測試,可見MPI對於遺傳算法框架GAFT的加速效果仍是較爲理想的。帶有MPI並行的遺傳算法框架目前也已更新並上傳至GitHub(github.com/PytLab/gaft) 歡迎圍觀[]~( ̄▽ ̄)~*

相關文章
相關標籤/搜索