OpenCL
Wprowadzenie¶
OpenCL (Open Computing Language) to otwarty standard określający model programowania dla platform akcelerowanych, który powstał jako alternatywa dla CUDA. Jest rozwiązaniem nieograniczającym się tylko do kart GPU, tylko jednego producenta. Udostępnia jednolity interfejs pozwalający programować różne typy akceleratorów, zaliczając do tego grona również wielordzeniowe CPU.
Początkowo stanowił najpoważniejszą alternatywę dla CUDA do programowania akceleratorów GPU. Przez pewien czas był podstawową metodą programowania akceleratorów GPU AMD. Wraz z rozwojem nowszych modeli programowania wspierających architektury GPU, takich jak HIP czy wysokopoziomowy SYCL, rola OpenCL na tym polu zmalała. Obecnie stosowany jest coraz częściej do programowania mniej rozpowszechnionych, wąsko wyspecjalizowanych układów obliczeniowych.
Dostępność¶
Podstawowym językiem programowania do pracy z OpenCL jest język C. Interakcja z akceleratorem jest realizowana poprzez funkcje biblioteczne zdefiniowane przez standard OpenCL (API w języku C) oraz funkcje akcelerowane (wyrażane w nieznacznie zmodyfikowanym języku C). Dostępne są również oficjalne rozwiązania ułatwiające pracę z językiem C++.
Program OpenCL kompiluje się przy użyciu standardowego kompilatora C/C++, jednakże niezbędne jest posiadanie zainstalowanej tzw. implementacji OpenCL. Rolą implementacji jest dostarczenie biblioteki OpenCL oraz sterownika potrafiącego kompilować kod akcelerowany pod wspierane przez daną implementację rodzaje akceleratorów (więcej w sekcji implementacje).
Szczegóły¶
OpenCL realizuje klasyczny model z podziałem na kod hosta (wykonywany na CPU) i kod akceleratora w postaci
odrębnych funkcji (tzw. kerneli). W odróżnieniu od innych modeli programowania akceleratorów (CUDA, HIP, SYCL) wymaga
aby kod kerneli był wyodrębniony do osobnych plików. Ewentualnie kernele mogą być deklarowane w kodzie hosta jako
literał tekstowy (ciąg znaków char *
).
Kod hosta poprzez funkcje biblioteczne kontroluje zachowanie akceleratora i zleca mu do wykonania kernele. Kernele wyraża się w tzw. języku OpenCL C, czyli języku C z drobnymi modyfikacjami składniowymi. Oficjalny standard wspiera również pracę w języku C++, poprzez wrappery C++ ułatwiające korzystanie z biblioteki OpenCL oraz język C++ for OpenCL do pisania kerneli. C++ for OpenCL rozszerza OpenCL C o możliwość korzystania z większości funkcjonalności języka C++17 w kodzie akcelerowanym.
OpenCL a inne języki programowania
Dzięki separacji kodu akcelerowanego od kodu hosta (i ich osobnej kompilacji), możliwe jest stosunkowo łatwe użycie OpenCL z poziomu innych języków programowania. W tym celu wystarczy wołać funkcje z biblioteki OpenCL (API C) z poziomu innego języka. W przypadku języków dobrze współpracujących z C (jak np. Fortran) można to zrobić bezpośrednio, Istnieją również (nieoficjalne) biblioteki pośredniczące dla innych języków.
Należy jednak mieć na uwadze, że w każdym przypadku część akcelerowana musi zostać dostarczona jako kod OpenCL C.
W porównaniu do innych modeli programowania akceleratorów, OpenCL może być postrzegany jako **niskopoziomowe rozwiązanie ** bazujące na języku C. Przejawia się to w tym, że wiele podstawowych czynności, które w wyżej poziomowych modelach programowania mogą zostać wykonane automatycznie, w OpenCL trzeba dokonywać manualnie. Zwiększa to poziom kontroli nad zachowaniem sprzętu, jednakże powoduje, że nawet prosty program staje się stosunkowo złożony.
Schemat programu¶
Typowy program OpenCL (w języku C) ma mniej więcej następującą strukturę, a każdej akcji odpowiada wywołanie jednej lub kilku funkcji.
Czynności początkowe:
- wybór akceleratora,
- utworzenie powiązanego z nim tzw. obiektu kontekstu (
cl_context
) –clCreateContext
- utworzenie kolejki do zlecania obliczeń –
clCreateCommandQueue
Kolejnym krokiem jest przygotowanie kernela do wykonania na akceleratorze, co wykonuje się według schematu:
- wczytanie tzw. programu, czyli kodu źródłowego z kernelami,
- utworzenie obiektu programu i zlecenie kompilacji kerneli – funkcje z grupy
clCreateProgramWith...
orazclBuildProgram
- wybranie odpowiedniego kernela z programu i finalnie utworzenie obiektu kernela gotowego do wykonywania
obliczeń –
clCreateKernel
Dopiero tak przygotowany kernel można uruchomić na akceleratorze. Wywołanie kernela, wymaga podania argumentów
kernela (clSetKernelArg
) oraz konfiguracji poziomu równoległości, co dokonuje się w równie niskopoziomowy sposób.
Oczywiście wcześniej trzeba przygotować pamięć dla akceleratora. Alokacja pamięci w przestrzeni adresowej
akceleratora odbywa się poprzez utworzenie powiązanych z odpowiednim kontekstem buforów (clCreateBuffer
). Transfery
danych oraz wywołania kernela zleca się korzystając z obiektu kolejki (funkcje clEnqueueWriteBuffer
, clEnqueueReadBuffer
oraz clEnqueueNDRangeKernel
).
Po zakończeniu pracy wszystkie używane wcześniej obiekty należy ręcznie zdealokować odpowiednimi
funkcjami (clRelease...
).
Kompilacja kerneli¶
Patrząc na opisany schemat użycia, widać, że kompilacja kodu akcelerowanego odbywa się dopiero w czasie wykonania programu (runtime). Faktyczna kompilacja wykonywana jest przez tzw. driver (sterownik) OpenCL obsługujący docelowe urządzenie/akcelerator. Dzięki temu, że obiekt programu jest skojarzony z kontekstem, który to z kolei jest przypisany do konkretnego urządzenia, biblioteka OpenCL jest w stanie przekazać zadanie kompilacji do odpowiedniego sterownika. Takie podejście pozwala na kompilację tego samego kodu akcelerowanego pod różne akceleratory i stwarza możliwość wykorzystywania przez jeden program akceleratorów różnego typu.
Na wielu platformach możliwa jest także prekompilacja kerneli do odpowiedniej postaci pośredniej lub binarnej – potrzeba jednak do tego odpowiedniego kompilatora. W takiej sytuacji, w programie użytkownika wystarczy wczytać plik ze skompilowaną wersją kernela. Jest to rozwiązanie szczególnie często stosowane w sytuacjach gdy kompilacja kerneli zajmuje dużo czasu (na przykład przy pracy z układami FPGA). Ogranicza jednak ono przenośność kerneli tylko do platform o kompatybilnym formacie binarnym lub pośrednim.
OpenCL a CUDA¶
Jako rozwiązanie niżej poziomowe, OpenCL jest w użyciu pod wieloma względami bardziej zbliżony do wykorzystania CUDA za pośrednictwem driver API niż standardowego runtime API (patrz CUDA > Biblioteka CUDA). Przykładowo, w CUDA driver API wymaga kernele także są wczytywane z zewnętrznych plików – w tym wypadku jednak zawsze w postaci prekompilowanej.
Pomimo innego podejścia i własnej terminologii, większość konceptów stosowanych w OpenCL znajduje swoje odpowiedniki w CUDA (patrz spis najistotniejszych analogii oraz CUDA > Terminologia).
OpenCL a SYCL¶
Modele programistyczne OpenCL oraz SYCL na poziomie podstawowych koncepcji i terminologii są do siebie zbliżone. Podobnie stosuje się kolejki do delegowania obliczeń, zarządzanie pamięcią odbywa się poprzez obiekty buforów, a siatkę obliczeń definiuję się za pomocą zakresów (NDRange). W OpenCL nie występują jednak pewne koncepty rozwinięte w SYCL. Przykładem mogą być akcesory, przez co zarządzanie transferami pamięci w OpenCL musi odbywać się ręcznie.
Podobieństwo modeli wynika z tego, że SYCL powstał w ramach prac nad standardem OpenCL jako jego wysokopoziomowy interfejs. Początkowo był on nakładką na OpenCL, której rolą było ułatwienie programowania kerneli – m.in. poprzez możliwość mieszania kodu hosta z kodem akcelerowanym oraz dostarczenie nowoczesnych funkcjonalności języka C++. Obecnie SYCL jest rozwijany jako osobny standard i w znacznej mierze przejmuje rolę uniwersalnego sposobu programowania akceleratorów różnego typu (przede wszystkim GPU oraz wielordzeniowe CPU).
Informacje o standardzie¶
Pierwsza wersja specyfikacji została udostępniona w 2009 roku. Standard jest systematycznie rozwijany przez Khronos Group (konsorcjum koordynujące rozwój wielu otwartych standardów). Obecna główna wersja to OpenCL 3.0, opublikowana we wrześniu 2020 roku, z późniejszymi drobnymi poprawkami. Aktualna wersja to 3.0.14, wydana w kwietniu 2023 roku.
Wersja 3.0 uporządkowała standard, dokonując unifikacji wersji 1.2 oraz 2.x. Dzieli ona funkcjonalności OpenCL na wymagane oraz opcjonalne. Część wymagana do zgodności ze standardem pokrywa się z OpenCL 1.2, z kolei funkcjonalności wprowadzone do standardu w wersjach 2.x uznano w wersji 3.0 za opcjonalne. Takie rozwiązanie z jednej strony ogranicza konkurencyjność OpenCL względem innych modeli programowania akceleratorów, gdyż zgodność ze standardem nie gwarantuje obsługi nowych funkcjonalności. Jednocześnie ułatwia to implementację standardu dla wąsko wyspecjalizowanych układów obliczeniowych, na których zaimplementowanie wysokopoziomowych rozwiązań jest często niemożliwe lub nieefektywne. Przykładem takiej funkcjonalności może być pamięć zunifikowana (unified memory). Duża opcjonalność na poziomie standardu jest krokiem w kierunku ugruntowania OpenCL jako niskopoziomowego modelu do programowania wąsko wyspecjalizowanych platform akcelerowanych.
Język programowania kerneli "C++ for OpenCL" jest oparty o standard OpenCL 3.0 oraz C++17. Posiada on swoją własną numerację, która odpowiada unifikacji standardu OpenCL.
- wersja 1.0, kompatybilna wstecznie z OpenCL 2.0
- wersja 2021, kompatybilna wstecznie z OpenCL 3.0
Implementacje¶
Kod OpenCL może być uruchamiany na wielu platformach sprzętowych. Pewne rozeznanie, które sprzęty posiadają wsparcie dla OpenCL, dostarcza lista produktów zgodnych ze standardem OpenCL, utrzymywana przez grupę Khronos. Lista ta obejmuje tylko te implementacje i urządzenia, które przeszły w całości oficjalne testy zgodności z pełną wersją danego standardu; szukając implementacji pod kątem konkretnego sprzętu nie należy ograniczać się tylko do tej listy.
Najważniejsi producenci układów CPU oraz GPU (AMD, Intel oraz NVIDIA) dostarczają implementacje OpenCL dla swoich architektur. Najczęściej wchodzą one w skład podstawowych pakietów narzędzi do tworzenia oprogramowania na te platformy (takich jak Intel oneAPI Base Toolkit, CUDA Toolkit, ROCm). W niektórych przypadkach możliwe jest również osobne pobranie środowiska do uruchamiania OpenCL (np. runtime dla Intel). Istnieją także alternatywne, otwartoźródłowe implementacje, takie jak PoCL (Portable Computing Language)
Wsparcie OpenCL jest powszechne w segmencie mobilnych procesorów o architekturze Arm oraz towarzyszących im układów GPU (Samsung, Qualcomm, Google). Dodatkowo, OpenCL jest często stosowany jako jedna z metod programowania wyspecjalizowanych oraz rozwijających się architektur takich jak układy FPGA (AMD/Xilinx, Intel), DSP (Texas Instruments) czy RISC-V.
Linki¶
- strona OpenCL
- dokumentacja
- materiały
- zbiór materiałów ze strony OpenCL
- oficjalny tutorial
- tutorial wideo – wprowadzenie do OpenCL (2016) – ⚠️ niektóre informacje dotyczące innych technologii mogą być przestarzałe
- przykładowe kody w OpenCL
- materiały do kursu OpenCL (2014) – ⚠️ niektóre informacje o platformach sprzętowych oraz implementacjach OpenCL mogą być nieaktualne