Создание массивов на GPU с помощью numba в python с использованием Cuda - PullRequest
0 голосов
/ 04 марта 2019

Я хочу оценить функцию в каждой точке меша.Беда в том, что если я создаю сетку на стороне процессора, процесс ее передачи на GPU занимает больше времени, чем фактические вычисления.Могу ли я создать сетку на стороне графического процессора?

В приведенном ниже коде показано создание меша на стороне процессора и оценка большей части выражения на стороне GPU (я не был уверен, как заставить atan2 работать на GPU, поэтому я оставил егона стороне процессора).Я должен заранее извиниться и сказать, что я все еще изучаю этот материал, так что я уверен, что есть много возможностей для улучшения кода ниже!

Спасибо!

import math
from numba import vectorize, float64
import numpy as np
from time import time

@vectorize([float64(float64,float64,float64,float64)],target='cuda')
def a_cuda(lat1, lon1, lat2, lon2):
    return  (math.sin(0.008726645 * (lat2 - lat1))**2) + \
             math.cos(0.01745329*(lat1)) * math.cos(0.01745329*(lat2)) * (math.sin(0.008726645 * (lon2 - lon1))**2)

def LLA_distance_numba_cuda(lat1, lon1, lat2, lon2):
    a = a_cuda(np.ascontiguousarray(lat1), np.ascontiguousarray(lon1), 
               np.ascontiguousarray(lat2), np.ascontiguousarray(lon2))
    return earthdiam_nm * np.arctan2(a,1-a)

# generate a mesh of one million evaluation points
nx, ny = 1000,1000
xv, yv = np.meshgrid(np.linspace(29, 31, nx), np.linspace(99, 101, ny))
X, Y = np.float64(xv.reshape(1,nx*ny).flatten()), np.float64(yv.reshape(1,nx*ny).flatten())
X2,Y2 = np.float64(np.array([30]*nx*ny)),np.float64(np.array([101]*nx*ny))

start = time()
LLA_distance_numba_cuda(X,Y,X2,Y2)
print('{:d} total evaluations in {:.3f} seconds'.format(nx*ny,time()-start))

1 Ответ

0 голосов
/ 09 марта 2019

Давайте установим базовый уровень производительности.Добавив определение (1.0) для earthdiam_nm и запустив ваш код под nvprof, мы получим:

$ nvprof python t38.py
1000000 total evaluations in 0.581 seconds
(...)
==1973== Profiling result:
            Type  Time(%)      Time     Calls       Avg       Min       Max  Name
 GPU activities:   55.58%  11.418ms         4  2.8544ms  2.6974ms  3.3044ms  [CUDA memcpy HtoD]
                   28.59%  5.8727ms         1  5.8727ms  5.8727ms  5.8727ms  cudapy::__main__::__vectorized_a_cuda$242(Array<double, int=1, A, mutable, aligned>, Array<double, int=1, A, mutable, aligned>, Array<double, int=1, A, mutable, aligned>, Array<double, int=1, A, mutable, aligned>, Array<double, int=1, A, mutable, aligned>)
                   15.83%  3.2521ms         1  3.2521ms  3.2521ms  3.2521ms  [CUDA memcpy DtoH]
(...)

Так что на моей конкретной установке «ядро» само работает за ~ 5,8 мс (маленький, медленный) QuadroK2000 GPU, и время копирования данных составляет в общей сложности 11,4 мс для 4 копий с хоста на устройство и 3,2 мс для передачи результатов обратно на хост.Основное внимание уделяется 4 копиям с хоста на устройство.

Давайте сначала пойдем за низко висящими фруктами.Эта строка кода:

X2,Y2 = np.float64(np.array([30]*nx*ny)),np.float64(np.array([101]*nx*ny))

на самом деле ничего не делает, кроме передачи значений 30 и 101 каждому «работнику».Я использую «работник» здесь, чтобы обратиться к идее конкретного скалярного вычисления в процессе numba «трансляции» функции vectorize по большому набору данных.Процесс векторизации / широковещания numba не требует, чтобы каждый вход был набором данных одинакового размера, просто чтобы передаваемые данные были «широковещательными».Таким образом, можно создать vectorize ufunc, который работает, например, с массивом и скаляром.Это означает, что каждый работник будет использовать свой элемент массива плюс скаляр для выполнения своих вычислений.

Поэтому низко висящий плод состоит в том, чтобы просто удалить эти два массива и передать значения (30, 101) какскаляры к уфунку a_cuda.В то время как мы собираемся использовать «низко висящий фрукт», давайте включим ваше вычисление arctan2 (вместо math.atan2) и ваше окончательное масштабирование на earthdiam_nm в код векторизации, поэтому нам не нужно делать это на хосте.в python / numpy:

$ cat t39.py
import math
from numba import vectorize, float64
import numpy as np
from time import time
earthdiam_nm = 1.0
@vectorize([float64(float64,float64,float64,float64,float64)],target='cuda')
def a_cuda(lat1, lon1, lat2, lon2, s):
    a = (math.sin(0.008726645 * (lat2 - lat1))**2) + \
             math.cos(0.01745329*(lat1)) * math.cos(0.01745329*(lat2)) * (math.sin(0.008726645 * (lon2 - lon1))**2)
    return math.atan2(a, 1-a)*s

def LLA_distance_numba_cuda(lat1, lon1, lat2, lon2):
    return a_cuda(np.ascontiguousarray(lat1), np.ascontiguousarray(lon1),
               np.ascontiguousarray(lat2), np.ascontiguousarray(lon2), earthdiam_nm)

# generate a mesh of one million evaluation points
nx, ny = 1000,1000
xv, yv = np.meshgrid(np.linspace(29, 31, nx), np.linspace(99, 101, ny))
X, Y = np.float64(xv.reshape(1,nx*ny).flatten()), np.float64(yv.reshape(1,nx*ny).flatten())
# X2,Y2 = np.float64(np.array([30]*nx*ny)),np.float64(np.array([101]*nx*ny))
start = time()
Z=LLA_distance_numba_cuda(X,Y,30.0,101.0)
print('{:d} total evaluations in {:.3f} seconds'.format(nx*ny,time()-start))
#print(Z)
$ nvprof python t39.py
==2387== NVPROF is profiling process 2387, command: python t39.py
1000000 total evaluations in 0.401 seconds
==2387== Profiling application: python t39.py
==2387== Profiling result:
            Type  Time(%)      Time     Calls       Avg       Min       Max  Name
 GPU activities:   48.12%  8.4679ms         1  8.4679ms  8.4679ms  8.4679ms  cudapy::__main__::__vectorized_a_cuda$242(Array<double, int=1, A, mutable, aligned>, Array<double, int=1, A, mutable, aligned>, Array<double, int=1, A, mutable, aligned>, Array<double, int=1, A, mutable, aligned>, Array<double, int=1, A, mutable, aligned>, Array<double, int=1, A, mutable, aligned>)
                   33.97%  5.9774ms         5  1.1955ms     864ns  3.2535ms  [CUDA memcpy HtoD]
                   17.91%  3.1511ms         4  787.77us  1.1840us  3.1459ms  [CUDA memcpy DtoH]
(snip)

Теперь мы видим, что операции копирования HtoD были сокращены с итого 11,4 мс до 5,6 мс.Ядро выросло с ~ 5,8 мс до ~ 8,5 мс, потому что мы выполняем больше работы в ядре, но сообщаемое Python время выполнения функции сократилось с ~ 0,58 с до ~ 0,4 с.

Можем ли мысделать лучше?

Мы можем, но для того, чтобы сделать это (я считаю), нам нужно будет использовать другой метод numba cuda.Метод vectorize удобен для скалярных поэлементных операций, но он не может знать, где в общем наборе данных выполняется операция.Нам нужна эта информация, и мы можем получить ее в коде CUDA, но для этого нам потребуется переключиться на декоратор @cuda.jit.

Следующий код преобразует предыдущую функцию vectorize a_cuda вфункция устройства @cuda.jit (по существу без других изменений), а затем мы создаем ядро ​​CUDA, которое выполняет генерацию сетки в соответствии с предоставленными скалярными параметрами и вычисляет результат:

$ cat t40.py
import math
from numba import vectorize, float64, cuda
import numpy as np
from time import time

earthdiam_nm = 1.0

@cuda.jit(device='true')
def a_cuda(lat1, lon1, lat2, lon2, s):
    a = (math.sin(0.008726645 * (lat2 - lat1))**2) + \
             math.cos(0.01745329*(lat1)) * math.cos(0.01745329*(lat2)) * (math.sin(0.008726645 * (lon2 - lon1))**2)
    return math.atan2(a, 1-a)*s

@cuda.jit
def LLA_distance_numba_cuda(lat2, lon2, xb, xe, yb, ye, s, nx, ny, out):
    x,y = cuda.grid(2)
    if x < nx and y < ny:
        lat1 = (((xe-xb) * x)/(nx-1)) + xb # mesh generation
        lon1 = (((ye-yb) * y)/(ny-1)) + yb # mesh generation
        out[y][x] = a_cuda(lat1, lon1, lat2, lon2, s)

nx, ny = 1000,1000
Z = cuda.device_array((nx,ny), dtype=np.float64)
threads = (32,32)
blocks = (32,32)
start = time()
LLA_distance_numba_cuda[blocks,threads](30.0,101.0, 29.0, 31.0, 99.0, 101.0, earthdiam_nm, nx, ny, Z)
Zh = Z.copy_to_host()
print('{:d} total evaluations in {:.3f} seconds'.format(nx*ny,time()-start))
#print(Zh)
$ nvprof python t40.py
==2855== NVPROF is profiling process 2855, command: python t40.py
1000000 total evaluations in 0.294 seconds
==2855== Profiling application: python t40.py
==2855== Profiling result:
            Type  Time(%)      Time     Calls       Avg       Min       Max  Name
 GPU activities:   75.60%  10.364ms         1  10.364ms  10.364ms  10.364ms  cudapy::__main__::LLA_distance_numba_cuda$241(double, double, double, double, double, double, double, __int64, __int64, Array<double, int=2, A, mutable, aligned>)
                   24.40%  3.3446ms         1  3.3446ms  3.3446ms  3.3446ms  [CUDA memcpy DtoH]
(...)

Теперь мы видимчто:

  1. Время выполнения ядра еще больше - около 10 мс (потому что мы делаем генерацию сетки)
  2. Нет явного копирования данных с хоста на устройство
  3. Общее время выполнения функции уменьшено с ~ 0,4 с до ~ 0,3 с
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...