Репа 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 оптимизировать лучше, и делает код немного яснее.