Программирование графических процессоров с помощью Haskell

А.С. Лукьянов
Рыбинская государственная авиационная технологическая академия имени П.А. Соловьева,
г. Рыбинск


В данной статье мы рассмотрим возможность использования графических процессоров для организации произвольных вычислений (GPGPU) в программах на функциональном языке программирования Haskell.

Использование современных графических процессоров для осуществления высокопроизводительных математических вычислений становится все более популярным. Их особенности позволяют в некоторых задачах значительно увеличить скорость вычислений [1, 2, 3]. В настоящее время самыми распространенными технологиями программирования графических процессоров являются: CUDA [1, 4, 5], AMD APP [6], DirectCompute [7, 8], OpenCL [4, 5, 9].

Графические процессоры подходят для решения не всех видов задач, только в некоторых можно получить значительный прирост скорости [5]. Например, они используются в следующих областях [1, 10, 11]: обработка звука, обработка фото и видео, моделирование поведения вирусов, получение высококачественных изображений с помощью ультразвука, поиск полезных ископаемых, моделирование физических процессов, моделирование экономических процессов, поиск вредоносного кода в программах.

Haskell является одним из самых распространённых нестрогих функциональных языков программирования [12, 13]. Имеет очень развитую систему типизации. Среди особенностей языка обычно выделяют: недопустимость побочных эффектов, отложенные (ленивые) вычисления, лямбда-выражения, функции высшего порядка, частичное применение функций, сопоставление с образцом (pattern matching), параметрический полиморфизм и полиморфизм классов типов, статическая типизация, автоматическое выведение типов, алгебраические типы данных, возможность писать программы с побочными эффектами без нарушения парадигмы функционального программирования, возможность интеграции с другими программами и библиотеками посредством открытых интерфейсов (стандартное расширение языка Foreign Function Interface).

Функциональные языки программирования применяются для широкого класса задач, от построения компиляторов, организации символьных вычислений, искусственного интеллекта и обработки изображений до разработки компьютерных игр и операционных систем [13, 14].

Сначала, для получения возможности использования из программ на Haskell возможности, например, OpenCL, необходимо описать минимальный набор функций, соответствующих функциям из OpenCL API. Назовем эти функции привязками (bindings). Для их описания будем использовать стандартное расширение FFI (Foreign Function Interface).

Кроме того, функции OpenCL используют специфичные типы данных, которые также должны быть описаны. Опишем такие типы данных:

module OpenCL.Types where

import Foreign

import Foreign.C.Types

-- типы данных OpenCL

type CL_char = CChar

type CL_int = CInt

type CL_uint = CUInt

type CL_long = CLong

type CL_ulong = CULong

type CL_size = CSize

type CL_bool = CL_uint

type CL_bitfield = CL_uint

-- платформа

data CL_platform_id' = CL_platform_id'

type CL_platform_id = Ptr CL_platform_id'

-- устройство

data CL_device_id' = CL_device_id'

type CL_device_id = Ptr CL_device_id'

... -- остальные типы данных, специфичные для OpenCL

В OpenCL идентификаторы платформы, устройства и других объектов представляются в виде указателей на некоторые структуры (неизвестных программисту). Мы могли бы для этого использовать безтиповый указатель Ptr (), однако в таком случае мы потеряли бы контроль типов.

Определив типы данных, можно приступить к определению самих функций привязок. Для этого определим в модуле OpenCL.Bindings основные функции OpenCL API следующим образом:

{-# LANGUAGE ForeignFunctionInterface #-}

module OpenCL.Bindings where

import Foreign

import OpenCL.Types

-- получить идентификаторы платформ

foreign import ccall "clGetPlatformIDs" clGetPlatformIDs ::

CL_uint ->

Ptr CL_platform_id ->

Ptr CL_uint ->

IO CL_int

-- получить идентификаторы устройств в платформе

foreign import ccall "clGetDeviceIDs" clGetDeviceIDs ::

CL_platform_id ->

CL_bitfield ->

CL_uint ->

Ptr CL_device_id ->

Ptr CL_uint ->

IO CL_int

... -- остальные функции OpenCL

Приведенные функции соответствуют следующим функциям из стандарта OpenCL:

cl_int clGetPlatformIDs(

cl_uint num_entries,

cl_platform_id *platforms,

cl_uint *num_platforms)

cl_int clGetDeviceIDs(

cl_platform_id platform,

cl_device_type device_type,

cl_uint num_entries,

cl_device_id *devices,

cl_uint *num_devices)

Преобразовав подобным образом описания остальных функций, мы уже получаем возможность использовать все возможности OpenCL из программ на Haskell, но в довольно низкоуровневом виде.

Для использования этих функций привязок можно воспользоваться модулем Foreign [15], предназначенным для работы с внешними функциями и для низкоуровневой работы с памятью. Например, получить список идентификаторов платформ, используя функцию clGetPlatformIDs, можно следующим образом:

getPlatforms :: IO [CL_platform_id]

getPlatforms =

-- заводим переменную - количество платформ

alloca $ \p_num_platforms ->

do

-- узнаем количество платформ

clGetPlatformIDs 0 nullPtr p_num_platforms

num_platforms <- peek p_num_platforms

-- резервируем память для нужного количества платформ

allocaArray (fromIntegral num_platforms) $ \p_platforms ->

do

-- получаем их идентификаторы (адреса)

err <- clGetPlatformIDs (fromIntegral num_platforms) p_platforms nullPtr

-- если все хорошо

-- загружаем список идентификаторов и возвращаем их

case err of

0 -> peekArray (fromIntegral num_platforms) p_platforms

_ -> return []

Для компиляции (или интерпретации) такой программы необходимо передать компилятору ghc (или интерпретатору ghci) параметр «-lOpenCL», сообщающий ему о необходимости связывания с соответствующей библиотекой.

Работать с функциями привязками приходится в императивном стиле и на достаточно низком уровне. Для упрощения работы мы опишем обертки, позволяющие использовать возможности OpenCL без низкоуровневой работы с памятью — с использованием типов Haskell. В самом простом случае обертки дублируют функции привязки, скрывая низкоуровневую работу с памятью. Например, функции получения списка идентификаторов платформ и устройств могут иметь следующие типы:

-- получить список идентификаторов OpenCL платформ

hclGetPlatformIDs ::

IO (Either CL_error_code [CL_platform_id])

-- получить список устройств (указанных типов) в платформе

hclGetDeviceIDs ::

[CL_device_type] ->

CL_platform_id ->

IO (Either CL_error_code [CL_device_id])

Такими функциями уже значительно проще пользоваться. Они возвращают результат работы либо код произошедшей ошибки.

Коды ошибок описываются следующим образом:

-- коды ошибок OpenCL

data CL_error_code =

CL_SUCCESS |

CL_DEVICE_NOT_FOUND |

CL_DEVICE_NOT_AVAILABLE |

...

CL_INVALID_MIP_LEVEL |

CL_INVALID_GLOBAL_WORK_SIZE |

CL_UNKNOWN_ERROR CL_int

deriving(Eq,Show)

А также, определяется соответствие интерпретаций ошибок их кодам:

-- соответствие ошибок их кодам

clErrorCode 0 = CL_SUCCESS

clErrorCode (-1) = CL_DEVICE_NOT_FOUND

clErrorCode (-2) = CL_DEVICE_NOT_AVAILABLE

...

clErrorCode (-62) = CL_INVALID_MIP_LEVEL

clErrorCode (-63) = CL_INVALID_GLOBAL_WORK_SIZE

clErrorCode e = CL_UNKNOWN_ERROR e

Далее, подобным образом описываются обертки над остальными привязками.

С помощью описанных функций мы можем использовать возможности OpenCL таким же образом, как и в родном для него C или C++. То есть получать информацию о устройствах, создавать контексты, компилировать и исполнять функции OpenCL kernel, а также загружать данные в память и из памяти устройства.

И, как уже было сказано, для компиляции (или интерпретации) программ, использующих OpenCL, необходимо передать компилятору ghc (или интерпретатору ghci) параметр «-lOpenCL».

Простейшие обертки затем могут быть использованы для описания более высокоуровневых функций, позволяющих задавать необходимые действия в декларативной форме.

И, так как графические процессоры можно эффективно использовать только для различных численных расчетов, мы можем описать некоторый DSL язык, описывающий необходимые преобразования данных, по которому уже будем генерировать код для OpenCL kernel. Развитая система типов Haskell позволяет легко описать такой DSL как тип данных.

Списоклитературы

1. NVIDIA Corporation, NVIDIA CUDA Guide, 2010

2. John D. Owens, David Luebke, Naga Govindaraju, Mark Harris, Jens Krüger, Aaron E. Lefohn, and Timothy J. Purcell, A Survey of General-Purpose Computation on GraphicsHardware, 2007

3. Hillis, W. Daniel, Steele, Guy L., Data Parallel Algorithms, 1986

4. http://www.nvidia.ru/page/gpu_computing.html

5. Matt Pharr, Randima Fernando GPU Gems 2: Programming Techniques for High-Performance Graphics and General-Purpose Computation – Addison-Wesley Professional, 2005 – 880 с.

6. http://developer.amd.com/gpu/AMDAPPSDK/documentation/Pages/default.aspx

7. http://www.nvidia.com/object/cuda_directcompute.html

8. http://msdn.microsoft.com/en-ca/library/ff476331(v=VS.85).aspx

9. http://www.khronos.org/opencl/

10. http://www.nvidia.com/object/cuda_home_new.html

11. http://www.kaspersky.ru/news?id=207733144

12. http://www.rsdn.ru/article/funcprog/fp.xml

13. Душкин Р. В. Функциональное программирование на языке Haskell – М.: ДМК пресс, 2007 – 608 с.

14. http://homepages.inf.ed.ac.uk/wadler/realworld/index.html

15.http://www.haskell.org/ghc/docs/latest/html/libraries/base-4.3.1.0/Foreign.html 


Назад к списку