技術

バイトニックソート

この記事に関連するかもしれない記事

GPUでソートを行う必要があり、GPUソートにおける代表格であるバイトニックソートについて調べた。

ググってみると色んなサイトにソースやアルゴリズムの図が載ってるけど、いまいちよく解らない。
図を見ると概念レベルの事はすぐ分かるけど、図というものの宿命としてそのスペースが限られてるためか、どこの図も要素数8個とかで少なすぎて、要素数を2^nに抽象化した際のアルゴリズムをイメージしづらい。
あと、処理の流れの図が、前半と後半で分けてあって、アルゴリズムが大きく分けて2フェーズからなると勘違いしやすい図も多い。
いやまぁこれはあくまでも私の頭の問題ですけどね。

個人的に一番分かりやすかったのは米Wikipediaのバイトニックソートのページ
ここにも図が載ってるけど、分かりやすかったのは図ではなくて、その下に載ってるPythonのソース。

しかしそのソース、純粋なアルゴリズムの理解にはかなり役立つとはいえ、バリバリ再帰つかってるし、リストを連結したりしてるから、GPUだとどんな流れで処理すればいいのかということが分かりにくい。

ということで、まず再帰とリストの連結を取り除いたバージョンを書いてみて、さらにそこからGPUだとこんな感じになるよねというバージョンを書いてみた。
(ソースは本文の最後に載せてます。)

以下ソースの解説。

WikipediaBitonicSorter がWikipediaに載ってたソースをクラス化したもので、 MyFirstBitonicSorter がその名のとおり私が最初に書いたバイトニックソート(再帰とリストの連結なし)で、 GPULikeBitonicSorter がGPUだとこんな処理の流れになるよねバージョンのバイトニックソートである。

もちろん「GPU Like」という事で実際はCPUで処理を行うけど、同じような処理をCUDAやらOpenCLやらOpenGL向けに書けばちゃんと動くと思う。
ただし、共有メモリを使ったり、メモリアクセスをコアレッシングしたり、スレッドの分岐方向をなるべく揃えたりするような工夫はしてないので、このままGPUに持って行っても性能は出し切れないと思う。
そういう最適化は実際にGPUに持っていった後しかできない。

今回のソースはCPUで動作するので速度を測っても仕方ないんだけど、一応速度を測るようにした。
うちの環境では、要素数 1048576 (1024 * 1024) でどれも3分前後かかる中、組み込み関数のsorted(中身はCで書かれたクイックソートかな?)は2秒弱となかなかのタイムだった。
実際にGPUに持って行ってどのくらいのタイムが出るかそのうち試したい。

import random
import timeit


class WikipediaBitonicSorter(object):
    def __init__(self):
        pass
    
    def bitonic_sort(self, data):
        self.comparison_count = 0
        self.swap_count = 0
        return self.__bitonic_sort(True, data)
    
    def __bitonic_sort(self, up, x):
        if len(x)<=1:
            return x
        else: 
            first = self.__bitonic_sort(True,x[:len(x)/2])
            second = self.__bitonic_sort(False,x[len(x)/2:])
            return self.bitonic_merge(up,first+second)
 
    def bitonic_merge(self, up, x): 
        # assume input x is bitonic, and sorted list is returned 
        if len(x) == 1:
            return x
        else:
            self.bitonic_compare(up,x)
            first = self.bitonic_merge(up,x[:len(x)/2])
            second = self.bitonic_merge(up,x[len(x)/2:])
            return first + second
 
    def bitonic_compare(self, up, x):
        dist = len(x)/2
        for i in xrange(dist):
            self.comparison_count += 1
            if (x[i] > x[i+dist]) == up:
                self.swap_count += 1
                x[i], x[i+dist] = x[i+dist], x[i] #swap


class MyFirstBitonicSorter(object):
    def __init__(self):
        pass
    
    def bitonic_sort(self, data):
        self.comparison_count = 0
        self.swap_count = 0
        
        self.data = data
        chunk_size = 2
        while chunk_size <= len(self.data):
            for chunk_index in xrange(len(self.data) / chunk_size):
                _from = chunk_size * chunk_index
                _to = _from + chunk_size - 1
                self.sort(chunk_index % 2 == 0, _from, _to)
            chunk_size *= 2
        return self.data
    
    def sort(self, up, _from, _to):
        chunk_size = _to - _from + 1
        if chunk_size == 2:
            self.compare(up, _from, _to)
        else:
            split = 1
            while split < chunk_size:
                sub_chunk_size = chunk_size / split
                for sub_chunk_index in xrange(split):
                    for i in xrange(sub_chunk_size / 2):
                        a = sub_chunk_size * sub_chunk_index + i + _from
                        b = a + sub_chunk_size / 2
                        self.compare(up, a, b)
                split *= 2
            
    def compare(self, up, a, b):
        self.comparison_count += 1
        if (self.data[a] > self.data[b]) == up:
            self.swap_count += 1
            self.data[a], self.data[b] = self.data[b], self.data[a]
        

class GPULikeBitonicSorter(object):
    def __init__(self):
        pass
    
    def bitonic_sort(self, data):
        self.comparison_count = 0
        self.swap_count = 0
        self.launch_kernel_count = 0
        
        chunk_size = 2
        while chunk_size <= len(data):
            sub_chunk_size = chunk_size
            while sub_chunk_size > 1:
                self.launch_kernel(data, chunk_size, sub_chunk_size)
                sub_chunk_size /= 2
            chunk_size *= 2
        return data
    
    def launch_kernel(self, data, chunk_size, sub_chunk_size):
        self.launch_kernel_count += 1
        for i in xrange(len(data) / 2):
            self.kernel(data, len(data), chunk_size, sub_chunk_size, i)
        
    def kernel(self, data, data_size, chunk_size, sub_chunk_size, thread_index):
        half_chunk_size = chunk_size / 2
        chunk_index = thread_index / half_chunk_size
        
        half_sub_chunk_size = sub_chunk_size / 2
        sub_chunk_index = thread_index / half_sub_chunk_size
        
        up = chunk_index % 2 == 0
        a = sub_chunk_size * sub_chunk_index + thread_index % half_sub_chunk_size
        b = a + half_sub_chunk_size
        
        self.comparison_count += 1
        if (data[a] > data[b]) == up:
            self.swap_count += 1
            data[a], data[b] = data[b], data[a]
            
        
if __name__ == "__main__":
    #random.seed(12345)
    
    data_size = 128 * 128
    trial_count = 3
    
    data = [random.randint(0, data_size) for i in xrange(data_size)]
    results = []
    times = []
    
    wbs = WikipediaBitonicSorter()
    times.append(
        timeit.timeit(
            setup = 'from __main__ import results, data, wbs',
            stmt = 'results.append(wbs.bitonic_sort(data[:]))',
            number = trial_count
        )
    )
    print 'wbs.comparison_count = %d' % wbs.comparison_count
    print 'wbs.swap_count = %d' % wbs.swap_count
    print 'wbs time: %f sec' % times[-1]
    print

    mfbs = MyFirstBitonicSorter()
    times.append(
        timeit.timeit(
            setup = 'from __main__ import results, data, mfbs',
            stmt = 'results.append(mfbs.bitonic_sort(data[:]))',
            number = trial_count
        )
    )
    print 'mfbs.comparison_count = %d' % mfbs.comparison_count
    print 'mfbs.swap_count = %d' % mfbs.swap_count
    print 'mfbs time: %f sec' % times[-1]
    print
    
    glbs = GPULikeBitonicSorter()
    times.append(
        timeit.timeit(
            setup = 'from __main__ import results, data, glbs',
            stmt = 'results.append(glbs.bitonic_sort(data[:]))',
            number = trial_count
        )
    )
    print 'glbs.comparison_count = %d' % glbs.comparison_count
    print 'glbs.swap_count = %d' % glbs.swap_count
    print 'glbs.launch_kernel_count = %d' % glbs.launch_kernel_count
    print 'glbs time: %f sec' % times[-1]
    print
    
    times.append(
        timeit.timeit(
            setup = 'from __main__ import results, data',
            stmt = 'results.append(sorted(data))',
            number = trial_count
        )
    )
    print 'builtin function sorted time: %f sec' % times[-1]
    print
    
    for i in xrange(len(results) - 1):
        if results[i] != results[i+1]:
            print 'Result has a difference.'

コメント

コメントを残す

メールアドレスが公開されることはありません。



※画像をクリックして別の画像を表示