Pokazywanie postów oznaczonych etykietą GPU. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą GPU. Pokaż wszystkie posty

piątek, 17 sierpnia 2012

Cuda na Javie część 2

W poprzednim wpisie CUDA na Javie opisałem trochę walki z technologią i ze sprzętem, dotyczącym programowania CUDA za pomocą Javy. Bibliotekę jCUDA odrzuciłem ze względu na brak ogólnodostępnej dokumentacji i możliwości rejestracji się, aby się dobrać do tej dokumentacji. Skorzystałem więc z konkurencyjnego rozwiązania: JCuda w wersji 0.3.2a dla CUDA 3.2 (jako że z nowszą wersją nie mogłem się dogadać). Czeka nas jeszcze doinstalowanie czegoś i konfiguracja, ale na początek trochę teorii.

Chcąc pisać w CUDA'ch, tworzymy pliki tekstowe z rozszerzeniem *.cu. Te następnie są on kompilowane z pomocą nvcc (kompilatora dostarczanego przez Nvidię w CUDA Toolkit) do jednego z kilku formatów: (np. PTX, CUBIN, EXE). Pierwszy z nich zawiera instrukcje asemblerowe, które są czytelne dla ludzi (jak ktoś oczywiście lubi / umie). Jest to również uniwersalny format, gdyż przed wykonaniem takiego kodu, jest on kompilowany pod konkretny układ GPU. Natomiast format CUBIN, to już format binarny pod konkretny procesor.

Więcej szczegółów na temat procesu kompilowania i linkowania Cudy zamieszczam na poniższym rysunku (schemat ze strony: http://www.think-techie.com/2009/09/gpu-computing-using-jcuda.html):


W plikach *.cu możemy umieszczać typowy kod C jak i tzw. The Kernel Code. Jest to kod, który wykonuje się na urządzeniu (ang. device) obsługującym CUDA. Kompilator nvcc nie jest przez to samowystarczalny. Do jego poprawnego działania wymagany jest jeszcze kompilator C, np. Visual Studio C++. Początkowo zainstalowałem VC++ 2010 Express Edition, ale się okazało, że moja wersja CUDA współpracuje z MCVC 8.0 lub 9.0. Zainstalowałem więc Visual Studio 2008 i przy próbie kompilacji przykładu Creating kernels dostałem następujący błąd:

nvcc fatal   : Cannot find compiler 'cl.exe' in PATH

Ok, szukamy cl.exe I dodajemy do PATH, czyli ***\Microsoft Visual Studio 9.0\VC\bin.

Teraz robi się ciekawie. Uruchamiając przykład, dostaję:
nvcc -m64 -ptx JCudaVectorAddKernel.cu -o JCudaVectorAddKernel.ptx
nvcc process exitValue -1
errorMessage:
nvcc fatal   : Cannot find compiler 'cl.exe' in PATH

ale uruchamiając to samo „z palca” dostaję:

nvcc fatal: Visual Studio configuration file '(null)' could not be found for installation at...

Musiałem doinstalować X64 Compilers and Tools



I zaczęło śmigać.

Czasem może być jeszcze wymagane dodanie do PATH’a: ***\Microsoft Visual Studio 9.0\VC\bin\amd64

Można również się natknąć na poniższy problem:

host_config.h(96) : fatal error C1083: Cannot open include file: 'crtdefs.h': No such file or directory

Aby go rozwiązać, trzeba jeszcze wskazać CUDA, gdzie są pliki nagłówkowe z kompilatora C. Aby to zrobić, należy w pliku nvcc.profile (w katalogu bin instalacji CUDA) w sekcji INCLUDES dodać ścieżkę do odpowiedniego katalogu:

INCLUDES        +=  "-I$(TOP)/include" "-I$(TOP)/include/cudart" "-Id:/Microsoft Visual Studio 9.0/VC/include" $(_SPACE_)

I już działa. Rozwiązanie ostatniego problemu znalazłem tutaj: http://blogs.hoopoe-cloud.com/index.php/2009/09/cudanet-examples-issues

Skoro już wszystko działa (mam nadzieję że niczego nie ominąłem) czas na jakiś kod. Sztandarowym przykładem pokazującym potęgę GPU jest sumowanie wektorów. Poniżej przedstawię przykład ze strony JCuda Code samples. Musiałem jednak trochę go zmodyfikować, gdyż tamtejszy przykład jest przygotowany dla cudów 4.0 i korzysta z metod, których w mojej wersji API (0.3.2a) nie ma. Podrasowałem również kod samego kernela, aby wyjaśnić pewne zjawiska tam zachodzące. Kod biblioteki jest na licencji MIT/X11 License więc mogę modyfikować, z zachowaniem informacji o autorze.

Zacznijmy od kodu Javowego:

// Enable exceptions and omit all subsequent error checks
JCudaDriver.setExceptionsEnabled(true);

// Create the PTX file by calling the NVCC
String ptxFileName = preparePtxFile("JCudaVectorAddKernel.cu");

// Initialize the driver and create a context for the first device.
cuInit(0);
CUdevice device = new CUdevice();
cuDeviceGet(device, 0);
CUcontext context = new CUcontext();
cuCtxCreate(context, 0, device);

// Load the ptx file.
CUmodule module = new CUmodule();
cuModuleLoad(module, ptxFileName);

// Obtain a function pointer to the "add" function.
CUfunction function = new CUfunction();
cuModuleGetFunction(function, module, "add");

Mamy tutaj trochę rzeczy związanych z ustawieniem sterownika Cuda. Początkowo jest włączane rzucanie wyjątków z metod biblioteki, gdy coś pójdzie nie tak. Samo już istnienie takiej metody świadczy według mnie o autorze, któremu jest bliżej do C niż do Javy. Moje obawy zostały potwierdzone przez początkowe formatowanie kodu w źródłowym pliku.

Następnie w linii 5tej ładujemy nasz plik *.cu, a dokładniej, jeżeli nie istnieje dla niego odpowiednik w formacie PTX, to nasz plik źródłowy z kodem kelnera jest kompilowany, w trakcie działania naszej aplikacji. Jest to metoda napisana przez autora przykładu i jak ktoś chce się jej przyjrzeć bliżej to niech zajrzy do kodu.

Wracając do opisanego przykładu dalej następuje inicjalizacja naszego urządzenia i utworzenie dla niego kontekstu. Dalej w linii 15stej nastepuje utworzenie modułu i załadowanie do niego skompilowanego pliku PTX, zwróconego przez metodę preparePtxFile(). W ostatniej linijce w końcu dobieramy się do naszej funkcji, którą napisaliśmy w naszym pliku *.cu.

Dobra teraz czas na przygotowanie danych wejściowych do naszej funkcji:

int numElements = 100000;

// Allocate and fill the host input data
float hostInputA[] = new float[numElements];
float hostInputB[] = new float[numElements];
for (int i = 0; i < numElements; i++) {
    hostInputA[i] = (float) i;
    hostInputB[i] = (float) i;
}

Tworzymy dwa wektory o długości 100k i wypełniamy kolejnymi liczbami. Zabieg ten ma na celu łatwą weryfikację poprawności uzyskanego wyniku. Stworzone dane wejściowe zostaną utworzone na stercie i nie będą dostępne dla GPU. Po za tym odczyt ten mógłby być bardzo długi (z powodu prędkości magistrali, obecności pamięci wirtualnej i stronicowania). Dla tego względu należy dane przekopiować do obszaru urządzenia. Robotę wykonuje poniższy kod:

// Allocate the device input data, and copy the
// host input data to the device
CUdeviceptr deviceInputA = new CUdeviceptr();
cuMemAlloc(deviceInputA, numElements * Sizeof.FLOAT);
cuMemcpyHtoD(deviceInputA, Pointer.to(hostInputA),
        numElements * Sizeof.FLOAT);
CUdeviceptr deviceInputB = new CUdeviceptr();
cuMemAlloc(deviceInputB, numElements * Sizeof.FLOAT);
cuMemcpyHtoD(deviceInputB, Pointer.to(hostInputB),
        numElements * Sizeof.FLOAT);

// Allocate device output memory
CUdeviceptr deviceOutput = new CUdeviceptr();
cuMemAlloc(deviceOutput, numElements * Sizeof.FLOAT);

W linii 3ciej tworzymy wskaźnik, który może wskazywać na pamięć urządzenia. Następnie alokujemy do tego wskaźnika pamięć o wielkości numElements * Sizeof.FLOAT. Pierwsza wartość oznacza długość wektora, a druga wielkość zajmowanej pamięci przez jeden element. Sizeof.FLOAT wynosi 4 (bajty).

Następnie w linii 5tej i 6tej następuje kopiowanie ze „zwykłej” pamięci (host) do urządzenia (device), czyli metoda cuMemcpyHtoD(). Najpierw jako argument podajemy wskaźnik do pamięci urządzenia, później referencję dla naszego wektora z pamięci hosta. Na koniec jeszcze ilość bajtów, jaką należy skopiować. Analogiczne operacje są powtarzane dla drugiego argumentu wejściowego. Na koniec jest jeszcze alokowana pamięć na urządzeniu, gdzie będzie zapisywany wynik.

Dobra czas na przekazanie argumentów do funkcji. W nowszym API można to zrobić bardziej przyjemniej. Ja musiałem się zapytać na forum, co z tym fantem, ale na szczęście autor szybko odpisał. Rozwiązanie dla wersji 0.3.2a poniżej:

// sets parametrs
int offset = 0;

offset = align(offset, Sizeof.INT);
cuParamSetv(function, offset, Pointer.to(new int[]{numElements}), Sizeof.INT);
offset += Sizeof.INT;

offset = align(offset, Sizeof.POINTER);
cuParamSetv(function, offset, Pointer.to(deviceInputA), Sizeof.POINTER);
offset += Sizeof.POINTER;

offset = align(offset, Sizeof.POINTER);
cuParamSetv(function, offset, Pointer.to(deviceInputB), Sizeof.POINTER);
offset += Sizeof.POINTER;

offset = align(offset, Sizeof.POINTER);
cuParamSetv(function, offset, Pointer.to(deviceOutput), Sizeof.POINTER);
offset += Sizeof.POINTER;

Jako pierwszy argument przekazujemy do funkcji długość naszego wektora. Jest to liczba typu int i w linii 5tej właśnie ustawiamy, że numElements będzie przekazywane jako pierwszy argument. Jako że nie możemy utworzyć Pointer’a do pojedynczej zmiennej, trzeba zrobić na szybko jednoelementową tablicę i przekazać jako rozmiar wielkość int’a.

Od razu liczony jest offset, który w ostatniej metodzie danego listingu jest przekazywany do funkcji. Pewnie to jest po to, aby mechanizm wywołujący metodę, wiedział ile danych trzeba odczytać ze stosu (moje podejrzenie).

Dobra, mamy już dane wejściowe, mamy je przyporządkowane jako argument funkcji, najwyższy czas na wywołanie:

// Call the kernel function.
int blockSizeX = 256;
int gridSizeX = (int) Math.ceil((double) numElements / blockSizeX);

cuParamSetSize(function, offset);

// run grid
cuLaunchGrid(function, gridSizeX, blockSizeX);
cuCtxSynchronize();

Ustalamy wielkość bloku i gridu, który posłuży nam do obliczeń, przekazujemy długość parametrów i wywołujemy funkcję. O co chodzi z blokami i gridami to możecie doczytać w książce CUDA w przykładach  lub w CUDA C Programming Guide (obrazek 2-1, strona 9) który jest również dostępny po zainstalowaniu CUDA Toolkit.

Metodę cuCtxSynchronize() wywołujemy po to, aby w razie niepowodzenia jak najszybciej się dowiedzieć, gdzie mniej więcej jest błąd. Pozostaje jeszcze skopiowanie wyniku:

// Allocate host output memory and copy the device output
// to the host.
float hostOutput[] = new float[numElements];
cuMemcpyDtoH(Pointer.to(hostOutput), deviceOutput,
        numElements * Sizeof.FLOAT);

do RAMu, porównanie wyników:

// Verify the result
boolean passed = true;
for (int i = 0; i < numElements; i++) {
    float expected = i + i;
    if (Math.abs(hostOutput[i] - expected) > 1e-5) {
        System.out.println(
                "At index " + i + " found " + hostOutput[i] +
                        " but expected " + expected);
        passed = false;
        break;
    }
}
System.out.println("Test " + (passed ? "PASSED" : "FAILED"));

i sprzątanie:

// Clean up.
cuMemFree(deviceInputA);
cuMemFree(deviceInputB);
cuMemFree(deviceOutput);

No to czas na kod wykonywany na GPU:

extern "C"
__global__ void add(int n, float *a, float *b, float *sum)
{
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    while(i < n) {
      sum[i] = a[i] + b[i];
      i = i + blockDim.x * gridDim.x;
    }
}

Linijkę extern "C", potrzebujemy, aby kompilować naszą funkcję zgodnie z regułami kompilowania dla ANSI C. Następnie __global__ powoduje, że metoda oznaczona takim kwalifikatorem, będzie się wykonywać na GPU i może być wywołana z poziomu hosta (CPU). Inna użyteczna definicja to __device__. Funkcje oznaczone w ten sposób, również działają na GPU, ale mogą jedynie być wywołane z poziomu innych funkcji GPU.

Dalej mamy sygnaturę metody, czyli przekazujemy kolejno długość wektorów i wskaźniki na wektory wejściowe i jeden na wyjściowy. Kod jest podobny do tego przedstawionego w CUDA w przykładach (str. 66). W pierwszej linijce funkcji wyliczamy indeks, od którego będziemy zaczynać sumowanie. blockDim reprezentuje liczbę wątków w każdym wymiarze bloku. My stosujemy tylko jednowymiarowy blok. W blockIdx znajduje się indeks bloku, który aktualnie wykonuje kod danej funkcji, a w threadIdx indeks aktualnego wątku. Są to 3 standardowe zmienne systemu wykonawczego CUDA. Siatki bloków mogą być dwuwymiarowe, a każdy blok może mieć trójwymiarową tablicę wątków. Więc jak się ktoś orientuje w pięciu wymiarach, to ma pole do popisu.

Dalsze obliczenia (pętla while) wykonujemy od póki indeks jest mniejszy od długości zadanego wektora. W linijce 6tej nic się ciekawego nie dzieje (po prostu jest obliczany wynik) i w następnie obliczamy kolejny indeks. Normalnie (w przypadku implementacji jednowątkowej) to byśmy wykonywali inkrementację, ale jako że kod jest wykonywany na GPU, to kolejny indeks musimy trochę inaczej policzyć. Zwiększamy go o iloczyn liczby wątków na blok i liczby bloków w siatce.

Na razie to tyle co przygotowałem o cudach. Kod znajdziecie na githubie (JCudaExample), choć nie jest on nie wiadomo jak odkrywczy. Przy uruchomieniu mogą pojawić się kłopoty, więc warto dopasować ścieżkę (java.library.path) w konfiguracji uruchomieniowej.

Podsumowując, jeśli mamy sporo równoległych obliczeń do wykonania, dobrą kartę graficzną i zależy nam na szybkim czasie działania, to można się pobawić w Cuda. Nie jest to jednak tak przyjemnie jak JVM, gdzie wiadomo, z której linijki leci wyjątek i wszystko staje się szybciej jasne. Mimo wszystko praca ze sprzętem daje dużo satysfakcji.

środa, 15 sierpnia 2012

CUDA na Javie

Poniższy artykuł nie będzie ani o zjawiskach paranormalnych, „interwencji istot nadprzyrodzonych” (patrz: cud na Wikipedii) ani o śnieniu „na jawie” ;) Mianowicie CUDA (ang. Compute Unified Device Architecture) to opracowana przez Nvidię „uniwersalna architektura procesorów wielordzeniowych” występująca głównie na kartach graficznych. Architektura ta ma więc wspomagać rozwiazywanie skomplikowanych problemów numerycznych. Stworzono do tego framework OpenCL, który jest oparty na języku C (a dokładniej na C99). Jak słusznie zauważył Bartek, OpenCL jest osobną, alternatywą dla Cudy i nie będę się nią tutaj zajmował.

Przeglądając angielską wiki na temat CUDA, zauważyłem, że są dostępne 4 biblioteki dla Javy, współpracujące z technologią Nvidi: jCUDA, JCuda, JCublas, JCufft. Jako że moja karta graficzna (GeForce 9600M GT) w laptopie obsługuje CUDA w wersji 1.1 (aktualne dane można sprawdzić na stronie CUDA GPUs), postanowiłem trochę się pobawić tymi klockami i sprawić aby się choć trochę spociła.

Przyglądając się bliżej tym czterem bibliotekom Javowym, okazało się, że tak właściwie są to 2 projekty: jCUDA i JCuda. Pozostałe: JCublas i JCufft są de facto podprojektami JCuda. JCublas zajmuje się operacjami wektorowymi i macierzowymi. Natomiast JCufft zajmuje się szybką transformacją Fouriera. Ten kto kończył studia na Wydziale Elektroniki PWr, lub coś co jest związane z elektroniką, to wie o co chodzi ;) Druciarze rządzą!


Ściągnąłem więc na początek jCUDA wersję na Windowsa 64 bit.

Następnie utworzyłem projekt w IntelliJ Idea, skopiowałem zawartość katalogu examples (ze ściągniętego ZIP’a ) do src, a jcuda.jar, jcuda.dll i jcudafft.dll do lib i dodałem do projektu ze scope’m: "Compile"



Pierwsze uruchomienie klasy EnumDevices (widzę że jest tam jakiś main()) i dostajemy stacktrace’a:

Exception in thread "main" java.lang.UnsatisfiedLinkError: no jcuda in java.library.path
 at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1860)
 at java.lang.Runtime.loadLibrary0(Runtime.java:845)
 at java.lang.System.loadLibrary(System.java:1084)
 at jcuda.driver.CUDADriver.(CUDADriver.java:909)
 at jcuda.CUDA.init(CUDA.java:62)
 at jcuda.CUDA.(CUDA.java:42)
 at examples.EnumDevices.main(EnumDevices.java:20)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:601)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

Myślałem początkowo, że to podczas uruchomienia, aplikacja nie widzi dołączanych JAR’ów. Zmiana scope’a na Runtime, powoduje jednak, że IDEA nie widzi biblioteki podczas kompilacji:


Wróciłem wiec do zasięgu compile. Nie chcąc grzebać sobie zbytnio w classpath’ie, ustawiłem właściwość java.library.path w konfiguracji uruchomieniowej wspomnianej klasy na katalog z wymaganymi dll’kami:



Po ponownym uruchomieniu otrzymałem ten oto stos wywołań:

Exception in thread "main" java.lang.UnsatisfiedLinkError: D:\Java_programy\IdeaWorkspace\jCUDAFirstExample\lib\jcuda.dll: Can't load AMD 64-bit .dll on a IA 32-bit platform
 at java.lang.ClassLoader$NativeLibrary.load(Native Method)
 at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1803)
 at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1728)
 at java.lang.Runtime.loadLibrary0(Runtime.java:823)
 at java.lang.System.loadLibrary(System.java:1028)
 at jcuda.driver.CUDADriver.(CUDADriver.java:909)
 at jcuda.CUDA.init(CUDA.java:62)
 at jcuda.CUDA.(CUDA.java:42)
 at examples.EnumDevices.main(EnumDevices.java:20)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
 at java.lang.reflect.Method.invoke(Method.java:597)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

Hmmm, wersje systemu mam 64 bitową, ale nie na procku AMD tylko Intel Core 2 Duo T6400 2.00 GHz.

Dobra ściągamy wersję 32 bitową na Winde. Podmiana plików, przebudowa projektu i Voila:

Total number of devices: 1
Name: GeForce 9600M GT
Version: 1.1
Clock rate: 1250000 MHz
Threads per block: 512

W przykładowym kodzie znajduje się jeszcze klasa LoadModule, uruchamiam więc:

Exception in thread "main" CUDA Driver error: 301
 at jcuda.CUDA.setError(CUDA.java:1874)
 at jcuda.CUDA.loadModule(CUDA.java:375)
 at examples.LoadModule.main(LoadModule.java:36)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
 at java.lang.reflect.Method.invoke(Method.java:597)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

I idę do linii gdzie poleciał wyjątek:

File cubinFile = new File("resources", "test_module.cubin");
cuda.loadModule(cubinFile.getAbsolutePath());

W archiwum z biblioteką jest jeszcze katalog resource, wiec kopiuję go do projektu i dołączam jako zasób:



Od razu nowe skróty klawiaturowe do zapamiętania, dostępne w oknie Project Structure - Modules - dodaj jako: Alt + S Sources, Alt + T Test Sources, Alt + E Excluded.

I otrzymujemy miły komunikat po uruchomieniu:

Test passed

Z ciekawości jeszcze zajrzałem co się znajduje w podpakiecie driver przykładowego kodu. Po uruchomieniu okazuje się, że aplikacje wypisują to samo na ekran. Porównałem kdiff3’em, co się zmienia w kodzie. Jak można przeczytać w release note w pliku readme.txt od wersji 1.1 biblioteki wprowadzono bardziej obiektowy sposób korzystania z biblioteki, aby nam Javowcom było łatwiej pracować :)

Added object oriented support for jCUDA and CUDA interface.

Niestety aby dowiedzieć się coś więcej o tej bibliotece, trzeba się zarejestrować na stronie producenta. Próbowałem z dwóch kont (gmail’owe i studenckie), ale zero odzewu. Później się doszukałem, że rejestracja jest tymczasowo nieaktywna i ze wyślą e-mail’a aktywacyjnego, gdy rejestracja będzie znów dostępna…

Jako, ze nie chcę pracować z biblioteką do której nie ma ogólnodostępnej dokumentacji, a rejestracja nic nie daje, wziąłem  się za drugą dostępną bibliotekę JCuda. Nauczony powyższym doświadczeniem od razu ściągnąłem wersję 4.0 32 bitową, dokumentację (są javadoc’i) i kod źródłowy. Początkowo postanowiłem przejrzeć przykłady: JCuda Code samples.

Zacząłem od pierwszego JCublasSample. Ściągnąłem, skopiowałem do src i to co było w ZIPie z JCudą skopiowałem do lib w moim projekcie. Następnie dodałem je jako zależności z zakresem Compile.


Pierwsze uruchomienie i oczywiście stacktrace:

Creating input data...
Performing Sgemm with Java...
Performing Sgemm with JCublas...
Error while loading native library "JCublas-windows-x86_64" with base name "JCublas"

Operating system name: Windows 7
Architecture         : amd64
Architecture bit size: 64
Stack trace from the attempt to load the library as a resource:
java.lang.NullPointerException: No resource found with name '/lib/JCublas-windows-x86_64.dll'
 at jcuda.LibUtils.loadLibraryResource(LibUtils.java:144)
 at jcuda.LibUtils.loadLibrary(LibUtils.java:80)
 at jcuda.jcublas.JCublas.initialize(JCublas.java:94)
 at jcuda.jcublas.JCublas.(JCublas.java:82)
 at JCublasSample.sgemmJCublas(JCublasSample.java:64)
 at JCublasSample.testSgemm(JCublasSample.java:49)
 at JCublasSample.main(JCublasSample.java:25)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:601)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
Stack trace from the attempt to load the library as a file:
java.lang.UnsatisfiedLinkError: no JCublas-windows-x86_64 in java.library.path
 at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1860)
 at java.lang.Runtime.loadLibrary0(Runtime.java:845)
 at java.lang.System.loadLibrary(System.java:1084)
 at jcuda.LibUtils.loadLibrary(LibUtils.java:90)
 at jcuda.jcublas.JCublas.initialize(JCublas.java:94)
 at jcuda.jcublas.JCublas.(JCublas.java:82)
 at JCublasSample.sgemmJCublas(JCublasSample.java:64)
 at JCublasSample.testSgemm(JCublasSample.java:49)
 at JCublasSample.main(JCublasSample.java:25)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:601)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

Exception in thread "main" java.lang.UnsatisfiedLinkError: Could not load the native library
 at jcuda.LibUtils.loadLibrary(LibUtils.java:122)
 at jcuda.jcublas.JCublas.initialize(JCublas.java:94)
 at jcuda.jcublas.JCublas.(JCublas.java:82)
 at JCublasSample.sgemmJCublas(JCublasSample.java:64)
 at JCublasSample.testSgemm(JCublasSample.java:49)
 at JCublasSample.main(JCublasSample.java:25)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:601)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

Process finished with exit code 1

Z czego interesujące jest to:

no JCublas-windows-x86_64 in java.library.path

problem rozwiązuję jak w przypadku pierwszej biblioteki, czyli ustawiam właściwość java.library.path dla wirtualnej maszyny Javy w konfiguracji uruchomieniowej przykładu. Uruchamiam i ten sam błąd. Przyglądam się bliżej nazwie brakującego pliku: JCublas-windows-x86_64. Okazuje się, że w wskazanym katalogu mam taki plik ale bez przyrostka _64, czyli pewnie framework, na podstawie wyświetlanej wartości „Architecture: amd64” poszukuje automatycznie bibliotek 64bitowych. No dobra, ściągnąłem, podmieniłem, uruchamiam i znów myślałem, że dostałem to samo.

Jednak gdy się bliżej przyjrzałem, to się okazało ze jest trochę odmienny:

java.lang.UnsatisfiedLinkError: D:\Java_programy\IdeaWorkspace\JCudaExample01\lib\JCublas-windows-x86_64.dll: Can't find dependent libraries

Zajrzałem więc do JCuda Tutorial’a i tamta strona zaprowadziła mnie do JCuda FAQ na jakimś formu:

Type 2: UnsatisfiedLinkError: Can't find dependent libraries

Czyli jednak czegoś nie doinstalowałem związanego bezpośrednio z CUDĄ. Ściągnąłem więc CUDA Toolkit (64 bit) zainstalowałem i uruchomiłem ponownie komputer. Po odpaleniu aplikacji dostałem:

Creating input data...
Performing Sgemm with Java...
Performing Sgemm with JCublas...
testSgemm FAILED

Już jest lepiej, ale chyba coś dalej jest nie tak. Zajrzałem w kod i się okazało, że przykładowa aplikacja korzysta z biblioteki JCublas, a ta wykorzystuje cuBLAS (ang. CUDA Basic Linear Algebra Subroutines), czyli bibliotekę wspomagającą podstawowe obliczenia algebry liniowej. Nie chciało mi się zgłębiać co jest nie tak i uruchomiłem inny przykład.

Klasa JCufftSample wymaga do uruchomienia dodatkowo javovej biblioteki JTransforms, która to liczy m.in. transformatę Fouriera. Uruchomienie tego przykładu wykrywało w moim przypadku architekturę 32 bitową:

Operating system name: Windows 7
Architecture         : x86
Architecture bit size: 32

No i oczywiście sypało wyjątkami. Postanowiłem więc w katalogu lib trzymać zarówno 64 bitowe jaki 32 bitowe dll’ki. Prawdopodobnie dzięki temu udało mi się wywalić wirtualną maszynę:

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x0ad2de60, pid=4376, tid=6112
#
# JRE version: 6.0_22-b04
# Java VM: Java HotSpot(TM) Client VM (17.1-b03 mixed mode, sharing windows-x86 )

Trochę zniechęcająca jest praca z tymi bibliotekami. Ale tak to zazwyczaj jest, jak się bawimy ze sprzętem, że na początku bardzo ciężko się z nim dogadać, a później to już jakoś lepiej idzie.

Jako że początkowo korzystałem z bibliotek w wersji 0.4.0-beta1 dla CUDA 4.0 postanowiłem zrobić downgrade do JCuda 0.3.2a. dla CUDA 3.2. Taka konfiguracja sprawia mi najmniej problemów. JCufftSample zaczął działać, i najprostszy, podstawowy test z tutorial’a też.
Proponuję wszystkim zaczynać od tego przykładu, gdyż ten test alokuje pewną pamięć w przestrzeni GPU i ją zwalnia – nic więcej.

Teraz to by było na tyle odnośnie cudów. Kolejne porady, co jeszcze należy doinstalować i jak przygotować środowisko do pracy z tym wynalazkiem w kolejnej części.

No i przepraszam za rozjechane listingi kodu pod Chrome'm, ale coś SyntaxHighlighter się pod tą przeglądarką rozjeżdża.