Репа 3 и правильное использование 'now'

Здесь есть основной вопрос монады, не связанный с Репой, плюс несколько конкретных вопросов, связанных с Репой.

Я работаю над библиотекой, использующей Repa3. У меня возникли проблемы с получением эффективного параллельного кода. Если я заставлю свои функции возвращать массивы с задержкой, я получаю мучительно медленный код, который очень хорошо масштабируется до 8 ядер. Этот код занимает более 20 ГБ памяти для профилировщика GHC и работает на несколько порядков медленнее, чем базовые нерасположенные векторы Haskell.

В качестве альтернативы, если я заставлю все мои функции возвращать массивы манифеста Unboxed (все еще пытаясь использовать слияние внутри функций, например, когда я делаю "карту" ), я получаю МНОГО быстрый код (все еще медленнее, чем использование нерасположенных векторов Haskell), который вообще не масштабируется и на самом деле имеет тенденцию становиться немного медленнее с большим количеством ядер.

На основе кода примера FFT в Repa-Algorithms кажется правильным подход всегда возвращать манифестные массивы. Есть ли когда-нибудь случай, когда я должен возвращать отложенные массивы?

Код FFT также позволяет использовать функцию "сейчас" . Тем не менее, я получаю ошибку типа, когда я пытаюсь использовать ее в своем коде:

type Arr t r = Array t DIM1 r
data CycRingRepa m r = CRTBasis (Arr U r)
                     | PowBasis (Arr U r)

fromArray :: forall m r t. (BaseRing m r, Unbox r, Repr t r) => Arr t r -> CycRingRepa m r
fromArray = 
    let mval = reflectNum (Proxy::Proxy m)
    in \x ->
        let sh:.n = extent x
        in assert (mval == 2*n) PowBasis $ now $ computeUnboxedP $ bitrev x

Код компилируется без "now". С "now" я получаю следующую ошибку:

Не удалось совместить тип r' with Array U (Z:. Int) r '    "r" - это жесткая переменная типа, связанная       подпись типа для         fromArray:: (BaseRing m r, Unbox r, Repr t r) = >                      Arr t r → CycRingRepa m r       на C:\Users\crockeea\Documents\Code\LatticeLib\CycRingRepa.hs: 50: 1   Ожидаемый тип: CycRingRepa m r   Фактический тип: CycRingRepa m (массив U DIM1 r)

Я не думаю, что это является моей проблемой. Было бы полезно, если бы кто-то мог объяснить, как работает Монада "сейчас" . По моей наилучшей оценке, монада, похоже, создает "Arr U (Arr U r)". Я ожидаю "Arr U r", который затем будет соответствовать шаблону конструктора данных. Что происходит и как это исправить?

Подписи типов:

computeUnboxedP :: Fill r1 U sh e => Array r1 sh e -> Array U sh e
now :: (Shape sh, Repr r e, Monad m) => Array r sh e -> m (Array r sh e)

Было бы полезно иметь лучшее представление о том, когда целесообразно использовать "сейчас" .

Еще пара вопросов, связанных с Repa: Должен ли я явным образом вызывать computeUnboxedP (как в примере кода FFT), или я должен использовать более общий computeP (потому что часть unbox определяется по моему типу данных)? Должен ли я хранить запаздывающие или манифестные массивы в типе данных CycRingRepa? В конце концов, мне также хотелось бы, чтобы этот код работал с Haskell Integer. Будет ли это требовать от меня написать новый код, который использует что-то другое, чем U-массивы, или я могу написать полиморфный код, который создает U-массивы для типов unbox и другого массива для целых чисел/типов в ящике?

Я понимаю, что здесь много вопросов, и я ценю любые/все ответы!

Ответы

Ответ 1

Репа 3.1 больше не требует эксклюзионного использования now. Параллельные вычислительные функции все монадичны и автоматически применяют deepSeqArray к их результатам. Пакет repa-examples также содержит новую реализацию умножения матрицы, которая демонстрирует их использование.

Ответ 2

Здесь исходный код для now:

now arr = do
  arr `deepSeqArray` return ()
  return arr

Итак, это действительно просто монадическая версия deepSeqArray. Вы можете использовать любой из них для принудительной оценки, а не для зависания. Эта "оценка" отличается от "вычисления", вызванного при вызове computeP.

В вашем коде now не применяется, поскольку вы не в монаде. Но в этом контексте deepSeqArray тоже не помогло бы. Рассмотрим эту ситуацию:

x :: Array U Int Double
x = ...

y :: Array U Int Double
y = computeUnboxedP $ map f x

Так как y относится к x, мы бы хотели убедиться, что x вычисляется до начала вычисления y. Если нет, доступная работа не будет правильно распределена среди банды потоков. Чтобы это получилось, лучше написать y как

y = deepSeqArray x . computeUnboxedP $ map f x

Теперь для отложенного массива имеем

deepSeqArray (ADelayed sh f) y = sh `deepSeq` f `seq` y

Вместо того, чтобы вычислять все элементы, это просто гарантирует, что форма вычисляется, и уменьшает f до нормальной формы с малой головой.

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

multiplyMM arr brr
 = [arr, brr] `deepSeqArrays`
   A.sumP (A.zipWith (*) arrRepl brrRepl)
 where  trr             = computeUnboxedP $ transpose2D brr
        arrRepl         = trr `deepSeqArray` A.extend (Z :. All   :. colsB :. All) arr
        brrRepl         = trr `deepSeqArray` A.extend (Z :. rowsA :. All   :. All) trr
        (Z :. _     :. rowsA) = extent arr
        (Z :. colsB :. _    ) = extent brr

Здесь "extend" генерирует новый массив, копируя значения в каком-то наборе новых измерений. В частности, это означает, что

arrRepl ! (Z :. i :. j :. k) == arrRepl ! (Z :. i :. j' :. k)

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

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

Наконец, computeUnboxedP - это просто computeP со специальным типом. Предоставление computeUnboxedP явно может позволить GHC оптимизировать лучше, и делает код немного яснее.