Haskell中并行构造树的策略

时间:2018-12-09 11:03:50

标签: haskell parallel-processing tree

我有一个项目,正在Haskell中构建Decision Tree。 生成的树将具有彼此独立的多个分支,因此我认为它们可以并行构造。

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的递归调用和并行策略可能存在一些问题。如果有经验的Haskeller会喜欢上它,那将真的让我开心:)

1 个答案:

答案 0 :(得分:1)

我不是Haskell性能/并行性方面的专家,但是我认为这里发生了一些事情。

首先,确实有这一行:

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

大概,人们可能希望这一行的第一部分建立一个看起来像

的数据结构
                      +-------+
                      | Right |
                      +-------+
                          |
                    +----------+
                    | Question |
                    +----------+
                     |   |    |
   +-----------------+   |    +-----------+
   |                +----+                |
   |                |                     |
+-----+   +-------------------+   +----------------+
| fil |   |       THUNK       |   |     THUNK      |
+-----+   | (affirmativeTree) |   | (negativeTree) |
          +-------------------+   +----------------+

然后,evalTraversable会看到Right并在parEvalTree上运行Question,从而导致两个重击都被触发以进行并行深度评估。

不幸的是,这不是完全会发生的事情,我认为问题是由于额外的Either String造成的。为了评估Question行(甚至只是到WHNF),正如evalTraversable一样,我们必须弄清楚结果是Right decisonTree还是{{1} }。这意味着Left _affirmativeTree必须先获得WHNF的评估,然后才能发挥作用。不幸的是,由于代码的结构,以这种方式对WHNF评估任何一棵树都会迫使几乎所有事情-必须强制选择过滤器,以查看递归negativeTree调用采取的分支,然后它自己对parEvalTree的递归调用也以相同的方式被迫WHNF。

可以通过以下方法避免这种情况:首先分别触发constructTreetrainModel,然后在有足够的时间进行计算之后,才以WHNF形式查看结果:

affirmativeTree

如果您用替换原始代码的这一行运行代码并将其加载到ThreadScope中,您将发现并行度显然有所提高:活动图在几个地方短暂地超过1,并且执行在HEC之间跳转。几个地方。不幸的是,程序的绝大部分时间仍然花在顺序执行上。

我试图对此进行一点研究,我认为您的树构造代码中的某些内容可能有些偏右。我添加了一些negativeTreeuncurry (Question fil) <$> bisequence ((affirmativeTree, negativeTree) `using` parTuple2 rdeepseq rdeepseq) ,看起来过滤器的正反两面经常存在相当大的不平衡,这使得并行执行无法很好地进行:正子树否定子树需要很长时间,往往会非常快地完成操作,从而创建看起来本质上是顺序执行的内容。在某些情况下,正子树是如此之小,以至于引发计算的内核完成了它,然后在另一个内核可以唤醒以窃取工作之前开始负子树。这是ThreadScope在单个内核上长期运行的地方。您可以在图的开头看到带有并行性的较短时间,这是第一个过滤器的负子树正在执行的时间,因为这是带有负子树的主过滤器,其大小足以真正做出贡献并行性。跟踪中还会有一些类似(但小得多)的事件,这些事件会创建合理大小的负树。

我希望,如果您进行了上述更改并尝试找到更均匀地对数据集进行分区的过滤器,您应该会发现此代码的可并行性有了相当大的提高。