Одновременно напишите выражение Haskell параллельного массива, запустите на процессорах и графических процессорах с repa и ускорьте

Репа и ускорение сходства API

Библиотека репозитория Haskell предназначена для автоматического вычисления параллельных массивов на процессорах. Библиотека ускорения - это автоматические данные parallelism на графических процессорах. API-интерфейсы очень похожи, с идентичными представлениями N-мерных массивов. Можно даже переключаться между массивами ускорения и репо с fromRepa и toRepa в Data.Array.Accelerate.IO:

fromRepa :: (Shapes sh sh', Elt e) => Array A sh e -> Array sh' e
toRepa   :: Shapes sh sh'          => Array sh' e  -> Array A sh e

Существует несколько бэкендов для ускорения, включая LLVM, CUDA и FPGA (см. рис. 2 из http://www.cse.unsw.edu.au/~keller/Papers/acc-cuda.pdf). Я обнаружил repa backend для ускорения, хотя библиотека, похоже, не поддерживается. Учитывая, что репа и ускорение моделей программирования схожи, я надеюсь, что есть элегантный способ переключения между ними, то есть функции, написанные один раз, могут быть выполнены с помощью repa R.computeP или с одним из ускоренных бэкендов, например. с функцией CUDA .

Две очень похожие функции: Repa и Accelerate на тыкве

Возьмите простую функцию порога обработки изображений. Если значение пикселя в градациях серого меньше 50, то оно равно 0, в противном случае оно сохраняет свое значение. Вот что он делает с тыквой:

Xkbqn.pngGPiucSr.png

В следующем коде представлены repa и ускоряются реализации:

module Main where

import qualified Data.Array.Repa as R
import qualified Data.Array.Repa.IO.BMP as R
import qualified Data.Array.Accelerate as A
import qualified Data.Array.Accelerate.IO as A
import qualified Data.Array.Accelerate.Interpreter as A

import Data.Word

-- Apply threshold over image using accelerate (interpreter)
thresholdAccelerate :: IO ()
thresholdAccelerate = do
  img <- either (error . show) id `fmap` A.readImageFromBMP "pumpkin-in.bmp"
  let newImg = A.run $ A.map evalPixel (A.use img)
  A.writeImageToBMP "pumpkin-out.bmp" newImg
    where
      -- *** Exception: Prelude.Ord.compare applied to EDSL types
      evalPixel :: A.Exp A.Word32 -> A.Exp A.Word32
      evalPixel p = if p > 50 then p else 0

-- Apply threshold over image using repa
thresholdRepa :: IO ()
thresholdRepa = do
  let arr :: IO (R.Array R.U R.DIM2 (Word8,Word8,Word8))
      arr = either (error . show) id `fmap` R.readImageFromBMP "pumpkin-in.bmp" 
  img <- arr
  newImg <- R.computeP (R.map applyAtPoint img)
  R.writeImageToBMP "pumpkin-out.bmp" newImg
  where
    applyAtPoint :: (Word8,Word8,Word8) -> (Word8,Word8,Word8)
    applyAtPoint (r,g,b) =
        let [r',g',b'] = map applyThresholdOnPixel [r,g,b]
        in (r',g',b')
    applyThresholdOnPixel x = if x > 50 then x else 0

data BackendChoice = Repa | Accelerate

main :: IO ()
main = do
  let userChoice = Repa -- pretend this command line flag
  case userChoice of
    Repa       -> thresholdRepa
    Accelerate -> thresholdAccelerate

Вопрос: могу ли я написать это только один раз?

Реализации thresholdAccelerate и thresholdRepa очень похожи. Есть ли элегантный способ написать функции обработки массивов один раз, затем выбрать многоядерные процессоры (repa) или графические процессоры (ускорить) в коммутаторе программно? Я могу думать о выборе моего импорта в соответствии с тем, хочу ли я, чтобы CPU или GPU импортировал либо Data.Array.Accelerate.CUDA, либо Data.Array.Repa для выполнения действия типа Acc a с помощью:

run :: Arrays a => Acc a -> a

Или, чтобы использовать класс типа, например. что-то примерно такое:

main :: IO ()
main = do
  let userChoice = Repa -- pretend this is a command line flag
  action <- case userChoice of
    Repa       -> applyThreshold :: RepaBackend ()
    Accelerate -> applyThreshold :: CudaBackend ()
  action

Или это так, что для каждой функции параллельного массива, которую я хочу выразить как для процессоров, так и для графических процессоров, я должен реализовать ее дважды - один раз с библиотекой repa и снова с библиотекой ускорения?

Ответ 1

Короткий ответ заключается в том, что на данный момент вам, к сожалению, нужно написать обе версии.

Тем не менее, мы работаем над поддержкой процессора для Accelerate, что избавит вас от необходимости в версии кода Repa. В частности, Accelerate совсем недавно приобрел новый бэкенд на основе LLVM, который предназначен для обоих графических процессоров и процессоров: https://github.com/AccelerateHS/accelerate-llvm

Этот новый бэкэнд по-прежнему неполный, ошибочный и экспериментальный, но мы планируем сделать его жизнеспособной альтернативой существующим бэкэндам CUDA.

Ответ 2

Я думал об этом год и несколько месяцев назад при разработке yarr. В то время возникали серьезные проблемы с выводами семейств типов или что-то вроде этого (я точно не помню), что помешало реализовать такую ​​унифицирующую оболочку vector, repa, yarr, accelerate и т.д. Оба эффективно и позволять не писать слишком много явных сигнатур типа или реализовывать его в принципе (я не помню).

Это был GHC 7.6. Я не знаю, есть ли существенные улучшения в GHC 7.8 в этой области. Теоретически у меня не было никаких проблем, поэтому мы можем ожидать такого рода когда-нибудь, коротко или долго, когда GHC будет готов.