28

假设我想使用有限差分法为看涨期权定价并重新计算,那么下面的工作就完成了:

import Data.Array.Repa as Repa

r, sigma, k, t, xMax, deltaX, deltaT :: Double
m, n, p :: Int
r = 0.05
sigma = 0.2
k = 50.0
t = 3.0
m = 3
p = 1
xMax = 150
deltaX = xMax / (fromIntegral m)
n = 800
deltaT = t / (fromIntegral n)

singleUpdater a = traverse a id f
  where
    Z :. m = extent a
    f _get (Z :. ix) | ix == 0   = 0.0
    f _get (Z :. ix) | ix == m-1 = xMax - k
    f  get (Z :. ix)             = a * get (Z :. ix-1) +
                                   b * get (Z :. ix) +
                                   c * get (Z :. ix+1)
      where
        a = deltaT * (sigma^2 * (fromIntegral ix)^2 - r * (fromIntegral ix)) / 2
        b = 1 - deltaT * (r  + sigma^2 * (fromIntegral ix)^2)
        c = deltaT * (sigma^2 * (fromIntegral ix)^2 + r * (fromIntegral ix)) / 2

priceAtT :: Array U DIM1 Double
priceAtT = fromListUnboxed (Z :. m+1) [max 0 (deltaX * (fromIntegral j) - k) | j <- [0..m]]

testSingle :: IO (Array U DIM1 Double)
testSingle = computeP $ singleUpdater priceAtT 

但现在假设我想并行定价期权(比如做一个现货阶梯),那么我可以这样做:

multiUpdater a = fromFunction (extent a) f
     where
       f :: DIM2 -> Double
       f (Z :. ix :. jx) = (singleUpdater x)!(Z :. ix)
         where
           x :: Array D DIM1 Double
           x = slice a (Any :. jx)

priceAtTMulti :: Array U DIM2 Double
priceAtTMulti = fromListUnboxed (Z :. m+1 :. p+1)
                [max 0 (deltaX * (fromIntegral j) - k) | j <- [0..m], _l <- [0..p]]

testMulti :: IO (Array U DIM2 Double)
testMulti = computeP $ multiUpdater priceAtTMulti

问题:

  1. 在 repa 中是否有更惯用的方式来做到这一点?
  2. 上述方法实际上会并行定价吗?
  3. 如何确定我的代码是否真的在生成将并行执行的东西?

我尝试了 3 次,但遗憾的是在 ghc 中遇到了一个错误:

bash-3.2$ ghc -fext-core --make Test.hs
[1 of 1] Compiling Main             ( Test.hs, Test.o )
ghc: panic! (the 'impossible' happened)
 (GHC version 7.4.1 for x86_64-apple-darwin):
    MkExternalCore died: make_lit
4

1 回答 1

61

您的错误与您的代码无关——它是您用来-fext-core以外部核心格式打印编译输出的。只是不要这样做(要查看核心,我使用ghc-core)。

-O2用和编译-threaded

$ ghc -O2 -rtsopts --make A.hs -threaded 
[1 of 1] Compiling Main             ( A.hs, A.o )
Linking A ...

然后运行+RTS -N4,例如,使用 4 个线程:

$ time ./A +RTS -N4
[0.0,0.0,8.4375e-3,8.4375e-3,50.009375,50.009375,100.0,100.0]
./A  0.00s user 0.00s system 85% cpu 0.008 total

所以它完成得太快而看不到结果。我会把你的mp参数增加到 1k 和 3k

$ time ./A +RTS -N2
./A +RTS -N2  3.03s user 1.33s system 159% cpu 2.735 total

所以是的,它确实并行运行。1.6x 在 2 核机器上,第一次尝试。它是否有效是另一个问题。使用 +RTS -s 可以查看运行时统计信息:

任务:4(1 个绑定,3 个高峰工人(总共 3 个),使用 -N2)

所以我们有 3 个线程并行运行(2 个可能用于算法,1 个用于 IO 管理器)。

您可以通过调整 GC 设置来减少运行时间。例如,通过使用-A我们可以减少 GC 开销,并产生真正的并行加速。

$ time ./A +RTS -N1 -A100M   
./A +RTS -N1 -A100M  1.99s user 0.29s system 99% cpu 2.287 total

$ time ./A +RTS -N2 -A100M   
./A +RTS -N2 -A100M  2.30s user 0.86s system 147% cpu 2.145 total

您有时可以通过使用 LLVM 后端来提高数值性能。这似乎也是这种情况:

$ ghc -O2 -rtsopts --make A.hs -threaded -fforce-recomp -fllvm
[1 of 1] Compiling Main             ( A.hs, A.o )
Linking A ...

$ time ./A +RTS -N2 -A100M                                    
./A +RTS -N2 -A100M  2.09s user 0.95s system 147% cpu 2.065 total

没什么了不起的,但是您正在改善单线程版本的运行时间,而且我没有以任何方式修改您的原始代码。要真正改善事物,您需要进行分析和优化。

重新访问 -A 标志,我们可以在初始线程分配区域上使用更严格的界限来进一步缩短时间。

$ ghc -Odph -rtsopts --make A.hs -threaded -fforce-recomp -fllvm

$ time ./A +RTS -N2 -A60M -s
./A +RTS -N2 -A60M 1.99s user 0.73s system 144% cpu 1.880 total

因此,通过使用并行运行时、LLVM 后端并小心使用 GC 标志,将其从 2.7 降低到 1.8 秒(提高了 30%)。您可以查看 GC 标志表面以找到最佳值:

在此处输入图像描述

周围的波谷-A64 -N2是数据集大小的理想选择。

我也强烈考虑在你的内核中使用手动公共子表达式消除,以避免过度重新计算。

正如 Alp 所建议的,要查看程序的运行时行为,请编译 threadscope(来自 Hackage)并运行如下:

$ ghc -O2 -fllvm -rtsopts -threaded -eventlog --make A.hs

$ ./A +RTS -ls -N2 -A60M

你会得到两个核心的事件跟踪,如下所示:

在此处输入图像描述

那么这里发生了什么?您有一个初始阶段(0.8 秒)的设置时间 - 分配您的大列表并将其编码到一个 repa 数组中 - 正如您通过 GC 和执行的单线程交错所看到的那样。然后在最后 300 毫秒发生实际并行工作之前,单核上还有 0.8 秒的时间。

因此,虽然您的实际算法可能很好地并行化,但所有周围的测试设置基本上都会淹没结果。如果我们序列化您的 dataset,然后从磁盘加载它,我们可以获得更好的行为:

$ time ./A +RTS -N2 -A60M
./A +RTS -N2 -A60M  1.76s user 0.25s system 186% cpu 1.073 total

现在您的个人资料看起来更健康了:

在此处输入图像描述

这看起来很棒!很少的 GC(98.9% 的生产力),我的两个内核愉快地并行运行。

所以,最后,我们可以看到你得到了很好的并行性:

1核1.855s

$ time ./A +RTS -N1 -A25M
./A +RTS -N1 -A25M  1.75s user 0.11s system 100% cpu 1.855 total

2核1.014s

$ time ./A +RTS -N2 -A25M   
./A +RTS -N2 -A25M  1.78s user 0.13s system 188% cpu 1.014 total

现在,具体回答您的问题:

  1. 在 repa 中是否有更惯用的方式来做到这一点?

一般来说,repa 代码应该由并行遍历、消费者和生产者以及可内联的内核函数组成。所以只要你这样做,那么代码可能是惯用的。如有疑问,请查看教程。我通常会将您的工作内核(如f)标记为内联。

  1. 上述方法实际上会并行定价吗?

如果您使用并行组合器(如computeP各种映射和折叠),代码将并行执行。所以是的,它应该并且确实并行运行。

  1. 如何确定我的代码是否真的在生成将并行执行的东西?

通常,您会先验地知道,因为您使用并行操作。如果有疑问,请运行代码并观察加速。然后您可能需要优化代码。

于 2012-12-29T16:22:47.510 回答