Przejdź do treści

CuPy

logo 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


Ostatnia aktualizacja: 5 grudnia 2023