Стратегии построения дерева параллельно в Хаскеле - PullRequest
0 голосов
/ 09 декабря 2018

У меня есть проект, в котором я строю Дерево решений в Хаскеле.Сгенерированные деревья будут иметь несколько ветвей, которые не зависят друг от друга, поэтому я подумал, что они могут быть построены параллельно.

Тип данных DecisionTree определяется следующим образом:

data DecisionTree =
    Question Filter DecisionTree DecisionTree |    
    Answer DecisionTreeResult

instance NFData DecisionTree where
    rnf (Answer dtr)            = rnf dtr
    rnf (Question fil dt1 dt2)  = rnf fil `seq` rnf dt1 `seq` rnf dt2

Вот часть алгоритма, которая создает дерево

constructTree :: TrainingParameters -> [Map String Value] -> Filter -> Either String DecisionTree    
constructTree trainingParameters trainingData fil =    
    if informationGain trainingData (parseFilter fil) < entropyLimit trainingParameters    
    then constructAnswer (targetVariable trainingParameters) trainingData    
    else
        Question fil <$> affirmativeTree <*> negativeTree `using` evalTraversable parEvalTree    
        where   affirmativeTree   = trainModel trainingParameters passedTData    
                negativeTree      = trainModel trainingParameters failedTData    
                passedTData       = filter (parseFilter fil) trainingData    
                failedTData       = filter (not . parseFilter fil) trainingData

parEvalTree :: Strategy DecisionTree    
parEvalTree (Question f dt1 dt2) = do    
    dt1' <- rparWith rdeepseq dt1    
    dt2' <- rparWith rdeepseq dt2    
    return $ Question f dt1' dt2'
parEvalTree ans = return ans

trainModel, рекурсивно вызывает constructTree.Соответствующая строка для параллелизма -

Question fil <$> affirmativeTree <*> negativeTree `using` evalTraversable parEvalTree 

Я строю это с флагами GHC -threaded -O2 -rtsopts -eventlog и запускаю его с stack exec -- performance-test +RTS -A200M -N -s -l (я на 2-ядерном компьютере).

Но, похоже, ничего не работает параллельно

SPARKS: 164 (60 converted, 0 overflowed, 0 dud, 0 GC'd, 104 fizzled)

INIT    time    0.000s  (  0.009s elapsed)
MUT     time   29.041s  ( 29.249s elapsed)
GC      time    0.048s  (  0.015s elapsed)
EXIT    time    0.001s  (  0.006s elapsed)
Total   time   29.091s  ( 29.279s elapsed)

threadscope output

Я подозреваю, что могут быть некоторые проблемы с рекурсивными вызовами с rdeepseqи Стратегия параллелизма.Если бы какой-нибудь опытный Хаскеллер зазвонил в нем, это действительно сделало бы мой день :) 1030

1 Ответ

0 голосов
/ 20 декабря 2018

Я не эксперт в производительности / параллелизме Haskell, но я думаю, что здесь происходит несколько вещей.

Во-первых, действительно есть эта строка:

Question fil <$> affirmativeTree <*> negativeTree `using` evalTraversable parEvalTree 

Предположительно, можно ожидать, что первая часть этой строки создает структуру данных, которая выглядит как

                      +-------+
                      | Right |
                      +-------+
                          |
                    +----------+
                    | Question |
                    +----------+
                     |   |    |
   +-----------------+   |    +-----------+
   |                +----+                |
   |                |                     |
+-----+   +-------------------+   +----------------+
| fil |   |       THUNK       |   |     THUNK      |
+-----+   | (affirmativeTree) |   | (negativeTree) |
          +-------------------+   +----------------+

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * evalTraversable * * * * * * * * * * * * * * * 10 * * * * * * * * * * * * * * * * * * * * * *

* * * * * * * * * * * * * * * * *

*1014* *1014* К сожалению, это не то, чтослучается, и я думаю, что проблема связана с дополнительным Either String.Чтобы оценить линию Question (даже просто для WHNF), как и evalTraversable, мы должны выяснить, будет ли результат Right decisonTree или Left _.Это означает, что affirmativeTree и negativeTree должны быть оценены до WHNF, прежде чем parEvalTree сможет вступить в игру.К сожалению, из-за структуры вашего кода, оценка любого дерева в WHNF таким способом заставляет почти все - выбор фильтра должен быть принудительным, чтобы увидеть, какую ветвь принимает рекурсивный вызов constructTree, а затем свой собственный.рекурсивные вызовы trainModel принудительно передаются в WHNF таким же образом.

Этого можно избежать, сначала сначала выделив affirmativeTree и negativeTree отдельно, а затем просматривая результаты в форме WHNF только после того, как ониу вас было время для того, чтобы полностью вычислить, выполнив что-то вроде этого:

uncurry (Question fil) <$> bisequence ((affirmativeTree, negativeTree) `using` parTuple2 rdeepseq rdeepseq)

Если вы запустите свой код с этой строкой, заменив оригинал, и загрузите его в ThreadScope, вы увидите, что явно наблюдается некоторое увеличениев параллелизме: график активности кратковременно поднимается выше 1 в нескольких местах, а выполнение выполняется между HEC в нескольких местах.К сожалению, подавляющее большинство времени программы все еще тратится на последовательное выполнение.

Я попытался немного разобраться в этом, и я думаю, что что-то в вашем коде построения дерева может быть немного смещено вправо.Я добавил несколько traceMarker с и traceEvent с, и похоже, что часто существует довольно большой дисбаланс между положительной и отрицательной сторонами фильтра, что делает параллельное выполнение не очень хорошим: положительное поддерево имеет тенденцию заканчиваться оченьочень быстро, в то время как отрицательное поддерево занимает много времени, создавая то, что выглядит как по существу последовательное выполнение.В некоторых случаях положительное поддерево настолько мало, что ядро, которое инициировало вычисления, завершает его, а затем начинает отрицательное поддерево, прежде чем другое ядро ​​может проснуться, чтобы украсть работу.Вот откуда берутся длинные прогоны на одном ядре в ThreadScope.Короткий промежуток времени с достаточным параллелизмом, который вы можете увидеть в начале графика, - это время, в течение которого выполняется отрицательное поддерево первого фильтра, поскольку это основной фильтр с отрицательным поддеревом, достаточно большим, чтобы реально внести вкладк параллелизму.Есть также несколько похожих (но гораздо меньших) событий позже в трассировке, где создаются отрицательные деревья разумного размера.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...