CuPy
Wprowadzenie¶
CuPy to biblioteka stanowiąca rozszerzenie popularnej biblioteki NumPy o możliwość łatwego wykonywania obliczeń na GPU. API zostało zaprojektowane tak, aby zapewnić kompatybilność z bibliotekami NumPy i SciPy, umożliwiając tym samym bezpośrednie i szybkie wykorzystanie ich w istniejącym już kodzie. Dużą zaletą jest też niezależność od stosowanych urządzeń – CuPy nie jest ograniczona pod względem danego typu GPU czy CPU. Wspiera platformę NVIDIA CUDA, a od pewnego czasu (wersji 9.0) także AMD HIP/ROCm (wsparcie eksperymentalne).
Akceleracja uzyskiwana przez przeniesienie operacji z CPU na GPU jest zależna od ich specyfiki, jednak notowane przyspieszenia są z reguły znaczne, sięgające często nawet 300%.
Dostępność¶
CuPy dostępna jest tylko dla języka Python. Można ją zainstalować przy użyciu standardowych menedżerów oprogramowania (pip, conda) lub zbudować ręcznie ze źródeł.
Użycie z GPU NVIDIA
Do korzystania z CuPy dla NVIDIA GPU, wymagane jest GPU z Compute Capability 3.0 lub wyżej, CUDA Toolkit z wersją co najmniej 10.2 oraz Python w wersji od 3.8 do 3.11. Dodatkowo, niezbędna jest także biblioteka NumPy. Do wykorzystania wszystkich potencjalnych funkcjonalności mogą być także potrzebne inne biblioteki, opisane szczegółowo w dokumentacji instalacji CuPy.
Aby zainstalować CuPy, budując bezpośrednio ze źródła, potrzebny jest także GNU C++ compiler (g++) w wersji co najmniej 6.
Użycie z GPU AMD
Do korzystania z CuPy dla AMD GPU (wsparcie eksperymentalne) wymagana jest karta kompatybilna z ROCm w wersji 4.3 lub 5.0. Zarówno przy instalacji ze standardowymi menedżerami oprogramowania jak i przy budowaniu ze źródła wymagane są dodatkowe biblioteki i konfiguracja zmiennych środowiskowych, które opisane zostały w dokumentacji opisującej korzystanie z CuPy na AMD GPU.
Szczegóły¶
Ze względu na różnice występujące między akceleratorami GPU a standardowym CPU, CuPy posiada zestaw elementów nieobecnych w NumPy. Podobnie jak w NumPy, podstawową strukturą macierzy wielowymiarowej jest obiekt ndarray
, jednakże w tym wypadku skojarzony jest on z urządzeniem, na którym się znajduje. Alokacja, zmiany oraz obliczenia zachodzą na urządzeniu, na którym znajduje się dana macierz. Jest to szczególnie istotne, gdy pracujemy na systemach z więcej niż jednym GPU.
W przypadku, gdy macierze, na których ma zostać wykonana operacja nie znajdują się na tym samym akceleratorze, CuPy spróbuje nawiązać połączenie między pamięciami urządzeń na zasadzie peer-to-peer. Taki transfer danych może skutkować spadkiem wydajności obliczeń, a w przypadku gdy topologia sieci łączącej urządzenia nie zezwala na takie połączenie – błędem krytycznym.
Tworzenie kerneli¶
Niewątpliwą zaletą CuPy jest łatwość definiowania kerneli CUDA. Biblioteka umożliwia tworzenie trzech typów kerneli:
- elementwise,
- reduction,
- raw.
Pierwszy rodzaj – kernel elementwise – dotyczy operacji realizowanych na poszczególnych elementach wejściowych macierzy.
Przykładowy kernel elementwise
kernel = cp.ElementwiseKernel(
'float32 x, float32 y', 'float32 z',
'''
if (x - 2 > y) {
z = x * y;
} else {
z = x + y;
}
''', 'my_kernel')
Kernel reduction dotyczy realizacji operacji map-reduce. Składnia konstruktora tej klasy różni się od poprzedniej.
Przykładowy kernel reduction
l2norm_kernel = cp.ReductionKernel(
'T x', # input params
'T y', # output params
'x * x', # map
'a + b', # reduce
'y = sqrt(a)', # post-reduction map
'0', # identity value
'l2norm' # kernel name
)
Ostatnią klasą są kernele typu raw, umożliwiające wykorzystanie wprost kodu we frameworku CUDA. Jest to abstrakcja oferująca najszersze możliwości i definiowanie dowolnych funkcji do wykonania na GPU.
Przykład tworzenia klasy RawKernel
add_kernel = cp.RawKernel(r'''
extern "C" __global__
void my_add(const float* x1, const float* x2, float* y) {
int tid = blockDim.x * blockIdx.x + threadIdx.x;
y[tid] = x1[tid] + x2[tid];
}
''', 'my_add')
CuPy domyślnie kompiluje kernele w trakcie działania programu. Kernele są optymalizowane na podstawie rozmiarów i typów przekazanych argumentów, co może skutkować kilkukrotną kompilacją tego samego kernela. Aby zmniejszyć ten narzut, skompilowane kernele są przechowywane w folderze wskazanym przez zmienną środowiskową CUPY_CACHE_DIR
. Umożliwia to szybsze wykonanie danej operacji – pod warunkiem, że wywołujemy ją z podobnymi argumentami – podczas jej kolejnego wywołania (gdyż nie ma wtedy potrzeby ponownego kompilowania).
Kompatybilność z NumPy¶
Przy korzystaniu z CuPy należy mieć świadomość pewnych wyjątków w naśladowaniu zachowania NumPy. Przykładem może być rzutowanie typu float
na zmienne typu uint
czy zachowanie w przypadku zaindeksowania pola poza zakresem tablicy. Więcej informacji w dokumentacji przedstawiającej różnice między CuPy a NumPy.
Informacje o wydaniu¶
Obecna wersja 12.2.0 została opublikowana we wrześniu 2023. Kod CuPy jest dostępny open source. Biblioteka ma stabilne API i jest stale rozwijana. Najpopularniejsze narzędzia do zarządzania bibliotekami w języku Python umożliwiają łatwe zainstalowanie biblioteki bez potrzeby budowania ze źródeł i wymaganej do tego konfiguracji.
Linki¶
- strona CuPy
- dokumentacja
- repozytorium
- materiały