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.'
コメント