Мой вопрос заключается в том, как определить манхэттенскую норму при использовании CUDA, какие изменения необходимо внести в def manhattan
Согласно документации метрика расстояния manhattan
представляет собой сумму двух векторов абсолютных значений разностей элементов.
Одна из проблем, с которой вы, вероятно, столкнетесь, - это пространство памяти.Если мы предположим, что вывод метрики расстояния (т.е. элементов матрицы) выражается как обычная величина питона, это, вероятно, займет 8 байтов памяти.Для указанного размера (60326) это означает, что матрица будет занимать 60326 * 60326 * 8 байт, что составляет почти 30 ГБ.Даже если мы предположим, что вы храните только половину этого, и даже если мы примем 32-битную сумму абсолютных разностей, это все равно будет более 7 ГБ необходимого хранилища.
Когда я попытался запустить такой тест с использованием метода sckit-learn, у меня возникли проблемы даже на машине с 128 ГБ системной памяти:
# cat t5.py
import numpy as np
import random
from scipy.spatial import distance
from sklearn.metrics.pairwise import pairwise_distances
vector_length = 416
num_signatures = 60000
sig_pattern = np.array([0,1,2,3], dtype=np.float32).reshape(4,1)
signatures = np.tile(sig_pattern,(num_signatures, vector_length//sig_pattern.shape[1]))
E = pairwise_distances(signatures, metric = 'manhattan', n_jobs = -1)
print(E[:8,:8])
# time python t5.py
Traceback (most recent call last):
File "t5.py", line 17, in <module>
E = pairwise_distances(signatures, metric = 'manhattan', n_jobs = -1)
File "/root/anaconda2/lib/python2.7/site-packages/sklearn/metrics/pairwise.py", line 1247, in pairwise_distances
return _parallel_pairwise(X, Y, func, n_jobs, **kwds)
File "/root/anaconda2/lib/python2.7/site-packages/sklearn/metrics/pairwise.py", line 1096, in _parallel_pairwise
for s in gen_even_slices(Y.shape[0], n_jobs))
File "/root/anaconda2/lib/python2.7/site-packages/sklearn/externals/joblib/parallel.py", line 789, in __call__
self.retrieve()
File "/root/anaconda2/lib/python2.7/site-packages/sklearn/externals/joblib/parallel.py", line 699, in retrieve
self._output.extend(job.get(timeout=self.timeout))
File "/root/anaconda2/lib/python2.7/multiprocessing/pool.py", line 572, in get
raise self._value
multiprocessing.pool.MaybeEncodingError: Error sending result:
'[array([[ 0., 416., 832., ..., 416., 832., 1248.],
[ 416., 0., 416., ..., 0., 416., 832.],
[ 832., 416., 0., ..., 416., 0., 416.],
...,
[ 416., 0., 416., ..., 0., 416., 832.],
[ 832., 416., 0., ..., 416., 0., 416.],
[1248., 832., 416., ..., 832., 416., 0.]])]'. Reason: 'OverflowError('cannot serialize a string larger than 2 GiB',)'
real 31m47.361s
user 405m28.155s
sys 8m19.851s
В этой ситуации этоказалось, заняло около 30 минут, чтобы вычислить на моей машине.Выходная тестовая матрица выглядела примерно корректно, но Python выдал ошибку, потому что некоторое промежуточное представление было> 2 ГБ.хорошо.
Однако выполняемая операция относительно проста.И, согласно моему тестированию, он может работать значительно быстрее, чем метод numpy / scikit-learn.
Вот рабочий пример:
# cat t4.py
import numpy as np
import numba as nb
from numba import cuda,float32
vector_length = 416
num_vecs_per_block = 8
sm_size = vector_length*num_vecs_per_block
#num_sig must be divisible by num_vecs_per_block
@cuda.jit('void(float32[:,:], float32[:,:], int32, int32)')
def manhattan(signatures, distances, num_sig, vec_len):
sm = cuda.shared.array(sm_size, float32)
temp = cuda.local.array(num_vecs_per_block, dtype = float32)
bid = cuda.blockIdx.x
tid = cuda.threadIdx.x
bdim = cuda.blockDim.x
# load shared memory with vectors for comparison
if tid < num_vecs_per_block:
for i in range(vec_len):
sm[i*num_vecs_per_block+tid] = signatures[i, bid*num_vecs_per_block+tid];
cuda.syncthreads()
#block-stride loop through the vector array
# the addition below to tid results in only elements above the diagonal being computed
# since the output matrix is symmetric
svec = tid +(bid*num_vecs_per_block)
while svec < num_sig:
for i in range(num_vecs_per_block):
temp[i] = 0
for i in range(vec_len):
src = signatures[i,svec]
for j in range(num_vecs_per_block):
#elementwise difference
sdist = src - sm[i*num_vecs_per_block + j]
#absolute value
if sdist < 0:
sdist *= -1
#sum
temp[j] += sdist
for i in range(num_vecs_per_block):
distances[bid*num_vecs_per_block+i, svec] = temp[i]
svec += bdim
num_signatures = 60000
sig_pattern = np.array([0,1,2,3], dtype=np.float32)
signatures = np.tile(sig_pattern,(num_signatures//sig_pattern.shape[0], vector_length))
distances = np.zeros((num_signatures, num_signatures), dtype=np.float32)
threadsperblock = 1024
blockspergrid = num_signatures//num_vecs_per_block
d_signatures = cuda.to_device(signatures)
d_distances = cuda.device_array_like(distances)
manhattan[blockspergrid, threadsperblock](d_signatures, d_distances, num_signatures, vector_length)
d_distances.copy_to_host(distances)
print(distances[:16,:16])
# time python t4.py
[[ 0. 416. 832. 1248. 0. 416. 832. 1248. 0. 416. 832. 1248. 0. 416. 832. 1248.]
[ 416. 0. 416. 832. 416. 0. 416. 832. 416. 0. 416. 832. 416. 0. 416. 832.]
[ 832. 416. 0. 416. 832. 416. 0. 416. 832. 416. 0. 416. 832. 416. 0. 416.]
[1248. 832. 416. 0. 1248. 832. 416. 0. 1248. 832. 416. 0. 1248. 832. 416. 0.]
[ 0. 416. 832. 1248. 0. 416. 832. 1248. 0. 416. 832. 1248. 0. 416. 832. 1248.]
[ 416. 0. 416. 832. 416. 0. 416. 832. 416. 0. 416. 832. 416. 0. 416. 832.]
[ 832. 416. 0. 416. 832. 416. 0. 416. 832. 416. 0. 416. 832. 416. 0. 416.]
[1248. 832. 416. 0. 1248. 832. 416. 0. 1248. 832. 416. 0. 1248. 832. 416. 0.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 416. 832. 1248. 0. 416. 832. 1248.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 416. 0. 416. 832. 416. 0. 416. 832.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 832. 416. 0. 416. 832. 416. 0. 416.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 1248. 832. 416. 0. 1248. 832. 416. 0.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 416. 832. 1248. 0. 416. 832. 1248.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 416. 0. 416. 832. 416. 0. 416. 832.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 832. 416. 0. 416. 832. 416. 0. 416.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 1248. 832. 416. 0. 1248. 832. 416. 0.]]
real 0m11.580s
user 0m5.579s
sys 0m5.922s
#
В этом случае я ограничил *Всего от 1024 * векторов до 60000 вместо 60320, поскольку большее число приводило к тому, что выходная матрица была слишком большой, чтобы поместиться в доступную память моего 16-ГБ графического процессора Tesla P100. Если в вашем графическом процессоре меньше 16 ГБ памяти, этот код не будет работать как есть. Вам нужно будет уменьшить проблему до меньшего числа векторов сигнатур.Было бы относительно просто разделить векторы на две две группы и выполнить вычисления расстояния между двумя группами, чтобы заполнить всю матрицу.
Однако код numba на моей тестовой машине выполняется примерно за 11 секунд,и, кажется, дает эквивалентный результат для очень маленькой 16x16 части выходной матрицы, которую я распечатываю.
Если я профилирую этот код, я обнаруживаю, что ядро GPU работает примерно через 3 секунды,Время передачи данных огромной выходной матрицы от GPU к CPU занимает около 6 секунд, а остаток составляет около 2 секунд при загрузке Python.
Фактический алгоритм GPU ориентирован на блок.Каждый блок отвечает за сравнение 8 векторов со всем массивом векторов.Каждый блок начинается с загрузки 8 векторов в общую память, а затем пересекает весь векторный массив, вычисляя манхэттенское расстояние по каждому из этих 8 векторов.Блок использует цикл блочного шага, чтобы пройти через весь массив.В конце каждой итерации цикла в выходной массив записываются 8 манхэттенских расстояний, соответствующих текущему сравнению.
Кроме того, в коде есть небольшое изменение, так что только выходные значения матрицы вышедиагонали вычисляются.Поскольку вычисления выполняются по блокам, блоки, у которых нет элементов выше диагонали, не вычисляются.Это сокращает время обработки примерно вдвое, но полная матрица вывода все еще находится в памяти.По этой причине нижний левый квадрант моего выходного сигнала 16x16 выше равен нулю, поскольку квадрант 8x8 полностью «ниже» диагонали, поэтому обработка для него пропускается из-за ситуации сходства, уже указанной в вопросе.