Szukaj…


Wprowadzenie

Przed użyciem OpenCL należy skonfigurować kod, aby z niego korzystać. W tym temacie skupiono się na tym, jak uruchomić i uruchomić opencl w swoim projekcie i wykonać podstawowe jądro. Przykłady oparte są na pakiecie OpenCL.NET C #, ale ponieważ opakowanie nie dodaje abstrakcji do OpenCL, kod prawdopodobnie będzie działał z bardzo niewielką liczbą zmian w C / C ++.

Połączenia w języku C # mogą wyglądać następująco: „Cl.GetPlatformIDs”. W przypadku interfejsu OpenCL w stylu C można nazwać „clGetPlatformID”, a w stylu C ++ „cl :: GetPlatformIDs”

Uwagi

  • NVidia, AMD i Intel mają nieco inne implementacje OpenCL, ale znane różnice są (z mojego doświadczenia) ograniczone do nawiasów i ukrytych rzutów. Czasami NVidia powoduje awarię jądra podczas próby znalezienia prawidłowego przeciążenia dla metody. W takim przypadku pomaga zaoferować wyraźną obsadę, aby wspomóc GPU. Problem zaobserwowano w przypadku jąder skompilowanych w czasie wykonywania.

  • Aby uzyskać więcej informacji na temat używanych wywołań w tym temacie, wystarczy google „OpenCL”, a następnie nazwę funkcji. Grupa Khronos ma pełną dokumentację dotyczącą wszystkich parametrów i typów danych dostępnych na ich stronie internetowej.

Inicjalizacja urządzenia docelowego

Jądra OpenCL można uruchamiać na GPU lub na CPU. Pozwala to na awaryjne rozwiązania, w których klient może mieć bardzo przestarzały system. Programista może również ograniczyć swoją funkcjonalność do procesora lub karty graficznej.

Aby rozpocząć korzystanie z OpenCL, potrzebujesz „Kontekstu” i „Urządzenia”. Obie są strukturami zdefiniowanymi przez OpenCL API (znany również jako cl :: Context lub clContext & ~ Device) i definiują używany procesor docelowy.

Aby uzyskać informacje o urządzeniu i kontekście, należy przeszukać listę dostępnych platform, z których każda może obsługiwać wiele urządzeń. Platforma reprezentuje fizyczny procesor graficzny i procesor, podczas gdy urządzenie może dodatkowo odróżnić zawarte w nich jednostki obliczeniowe. W przypadku układów GPU większość platform będzie mieć tylko jedno urządzenie. Ale procesor może oferować dodatkową zintegrowaną kartę graficzną oprócz swoich możliwości procesora.

Kontekst zarządza pamięcią, kolejkami poleceń, różnymi jądrami i programami. Kontekst może być ograniczony do jednego urządzenia, ale może również odnosić się do wielu urządzeń.

Krótka uwaga API zanim zaczniemy kodować: Niemal każde wywołanie OpenCL podaje wartość błędu, albo jako wartość zwracaną, albo poprzez wartość ref (wskaźnik w C). Teraz zacznijmy.

ErrorCode err;
var platforms = Cl.GetPlatformIDs(out err);
if(!CheckError(err, "Cl.GetPlatformIDs")) return;
foreach (var platform in platforms) {
    foreach (var device in Cl.GetDeviceIDs(platform, DeviceType.Gpu, out err)) {
        if(!CheckError(err, "Cl.GetDeviceIDs")) continue;
        [...]
    }
}

Ten fragment kodu odpytuje wszystkie dostępne urządzenia GPU w systemie. Możesz teraz dodać je do listy lub rozpocząć kontekst bezpośrednio od pierwszego dopasowania. Funkcja „CheckError (...)” jest prostym narzędziem, które sprawdza, czy kod błędu ma wartość sukcesu czy inną, i może zaoferować ci pewne logowanie. Zaleca się stosowanie oddzielnej funkcji lub makra, ponieważ często będziesz to nazywać.

ErrorCode jest tylko wyliczeniem dla typu danych cl_int dla C #, C / C ++ może porównać wartość int ze wstępnie zdefiniowanymi stałymi błędów, wymienionymi tutaj: https://www.khronos.org/registry/OpenCL/sdk/1.0/docs/man/ xhtml / error.html

Możesz także sprawdzić, czy urządzenie obsługuje wszystkie potrzebne funkcje, w przeciwnym razie jądra mogą ulec awarii w czasie wykonywania. Możesz zapytać o możliwości urządzenia za pomocą

Cl.GetDeviceInfo(_device, DeviceInfo.ImageSupport, out err)

Ten przykład pyta urządzenie, czy może wykonywać funkcje obrazu. W następnym i ostatnim kroku musimy zbudować nasz kontekst z zebranych urządzeń.

_context = Cl.CreateContext(null, 1, new[] { _device }, ContextNotify, IntPtr.Zero, out err);

Tu się dzieje. Dla ludzi C / C ++ IntPtr jest adresem wskaźnika w C #. Skoncentruję się tutaj na ważnych częściach.

  • Drugi parametr określa liczbę urządzeń, których chcesz użyć
  • Trzeci parametr to tablica tych urządzeń (lub wskaźnik w C / C ++)
  • Trzeci parametr to wskaźnik funkcji dla funkcji zwrotnej. Ta funkcja będzie używana za każdym razem, gdy wystąpią błędy w kontekście.

W celu późniejszego użycia musisz zachować używane urządzenia i kontekst.

Po zakończeniu wszystkich interakcji OpenCL musisz ponownie uwolnić kontekst za pomocą

Cl.ReleaseContext(_context);

Kompilowanie jądra

Jądra można kompilować w czasie wykonywania na urządzeniu docelowym. Aby to zrobić, potrzebujesz

  • kod źródłowy jądra
  • urządzenie docelowe, na którym ma zostać wykonana kompilacja
  • kontekst zbudowany na urządzeniu docelowym

Szybka aktualizacja terminologii: program zawiera kolekcję jąder. Możesz myśleć o programie jako o kompletnym pliku źródłowym C / C ++ / C #, podczas gdy jądra są różnymi elementami funkcji tego pliku.

Najpierw musisz utworzyć program z kodu źródłowego.

var program = Cl.CreateProgramWithSource(_context, 1, new[] { source }, null, out err);

Możesz łączyć wiele plików źródłowych w jeden program i kompilować je razem, co pozwala mieć jądra w różnych plikach i kompilować je za jednym razem.

W następnym kroku musisz skompilować program na urządzeniu docelowym.

err = Cl.BuildProgram(program, 1, new[] { _device }, string.Empty, null, IntPtr.Zero);

Teraz pojawia się małe zastrzeżenie: kod błędu mówi tylko, czy samo wywołanie funkcji zakończyło się powodzeniem, ale nie oznacza, że kod faktycznie się skompilował. Aby to sprawdzić, musimy zapytać o dodatkowe informacje

BuildStatus status;
status = Cl.GetProgramBuildInfo(program, _device, ProgramBuildInfo.Status, out err).CastTo<BuildStatus>();
if (status != BuildStatus.Success) {
    var log = Cl.GetProgramBuildInfo(program, _device, ProgramBuildInfo.Log, out err);
}

Ludzie w C / C ++ mogą zignorować rzutowanie na końcu i po prostu porównać zwróconą liczbę całkowitą z odpowiednią stałą.

Pierwsze połączenie sprawdza, czy nasza kompilacja zakończyła się pomyślnie. Jeśli nie, możemy pobrać dziennik i zobaczyć dokładnie, co poszło nie tak. Zobacz uwagi niektórych typowych problemów dotyczących różnych platform.

Po zbudowaniu programu musisz wyodrębnić różne jądra ze skompilowanego programu. W tym celu tworzysz jądra

_kernel = Cl.CreateKernel(_program, kernel, out err);

gdzie „jądro” jest ciągiem nazwy jądra. Kiedy skończysz z jądrem, musisz go zwolnić

Cl.ReleaseKernel(_kernel);

Tworzenie kolejki poleceń

Aby zainicjować dowolną operację na urządzeniach, potrzebujesz kolejki poleceń dla każdego urządzenia. Kolejka śledzi różne połączenia wykonane z urządzeniem docelowym i utrzymuje je w porządku. Większość poleceń można również wykonać w trybie blokującym lub nieblokującym.

Tworzenie kolejki jest dość proste:

_queue = Cl.CreateCommandQueue(_context, _device, CommandQueueProperties.None, out err);

Podstawową interakcją z kolejką poleceń jest kolejkowanie różnych operacji, które chcesz wykonać, np. Kopiowanie danych do i z urządzenia i uruchamianie jądra.

Po zakończeniu korzystania z kolejki poleceń musisz zwolnić kolejkę z wywołaniem

Cl.ReleaseCommandQueue(_queue);

Wykonywanie jądra

Teraz sprowadzamy się do prawdziwych rzeczy, wykonując wasze jądra na urządzeniu równoległym. Przeczytaj o podstawach sprzętowych, aby w pełni zrozumieć wysyłanie jądra.

Najpierw musisz ustawić argumenty jądra, zanim faktycznie wywołasz jądro. Odbywa się to za pośrednictwem

err = Cl.SetKernelArg(_kernel, $argumentIndex, $argument);

Jeśli nie ustawisz wszystkich argumentów przed uruchomieniem jądra, jądro ulegnie awarii.

Zanim faktycznie uruchomimy nasze jądro, musimy obliczyć „globalny rozmiar pracy” i „lokalny rozmiar pracy”.

globalny rozmiar pracy to całkowita liczba wątków, które zostaną uruchomione na twoim GPU. Lokalny rozmiar roboczy to liczba wątków w każdym bloku wątku. Lokalny rozmiar pracy można pominąć, jeśli jądro nie potrzebuje żadnych specjalnych wymagań. Ale jeśli podany jest lokalny rozmiar pracy, globalny rozmiar pracy musi być wielokrotnością lokalnego rozmiaru pracy.

Rozmiary robocze mogą być jednowymiarowe, dwuwymiarowe lub trójwymiarowe. Wybór liczby wymiarów zależy wyłącznie od Ciebie i możesz wybrać to, co najbardziej odpowiada Twojemu algorytmowi.

Teraz, gdy zdecydowaliśmy się na nasze rozmiary pracy, możemy nazwać jądro.

Event clevent;
err = Cl.EnqueueNDRangeKernel(_queue, _kernel, $dimensions, null, $globalWorkSize, $localWorkSize, 0, null, out clevent);

Wymiary $ określa naszą pożądaną liczbę wymiarów, $ globalWorkSize to tablica rozmiarów $ wymiary z globalnym rozmiarem pracy i takie same dla $ localWorkSize. Ostatni argument daje obiekt reprezentujący aktualnie wykonywane jądro.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow