28

有限差分法と repa を使用してコール オプションの価格を設定したい場合、次のようにします。

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. レパでこれを行うより慣用的な方法はありますか?
  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.External 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

そのため、結果を見るにはあまりにも早く完了します。mパラメータを 1kとp3k に増やします

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

そうです、それは並行して実行されます。最初の試行で、2 コア マシンで 1.6 倍。それが効率的かどうかは別の問題です。+RTS -s を使用すると、実行時の統計を確認できます。

タスク: 4 (1 バウンド、3 ピーク ワーカー (合計 3)、-N2 を使用)

そのため、3 つのスレッドが並行して実行されていました (おそらくアルゴ用に 2 つ、IO マネージャー用に 1 つ)。

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 が示唆するように、プログラムの実行時の動作を確認するには、(Hackage から) threadscope をコンパイルし、次のように実行します。

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

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

そして、次のように 2 つのコアのイベント トレースを取得します。

ここに画像の説明を入力

それで、ここで何が起こっているのですか?GC と実行のシングル スレッド インターリーブからわかるように、最初の期間 (0.8 秒) のセットアップ時間 (大きなリストを割り当てて repa 配列にエンコードする) があります。次に、最後の 300 ミリ秒で実際の並列作業が発生する前に、シングル コアで別の 0.8 秒の処理が行われます。

したがって、実際のアルゴリズムは適切に並列化されている可能性がありますが、周囲のすべてのテスト セットアップは基本的に結果を圧倒します。データセットをシリアル化し、ディスクからロードするだけで、より良い動作が得られます。

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

これであなたのプロフィールはより健全に見えます:

ここに画像の説明を入力

これは素晴らしいですね!GC はほとんど使用せず (98.9% の生産性)、私の 2 つのコアは問題なく並列実行されています。

したがって、最終的に、適切な並列処理が得られることがわかります。

1コアで1.855秒

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

および 2 コアの場合、1.014 秒

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

さて、あなたの質問に具体的に答えてください:

  1. レパでこれを行うより慣用的な方法はありますか?

一般に、repa コードは、並列走査、コンシューマーとプロデュース、およびインライン化可能なカーネル関数で構成する必要があります。あなたがそれをしている限り、コードはおそらく慣用的です。疑問がある場合は、チュートリアルを参照してください。f一般に、ワーカー カーネル (のような) をインラインとしてマークします。

  1. 上記の方法は実際に並行して価格設定されますか?

computePまたはさまざまなマップやフォールドなどの並列コンビネータを使用すると、コードは並列で実行されます。はい、並行して実行する必要があります。

  1. コードが実際に並列実行されるものを生成しているかどうかを判断するにはどうすればよいですか?

一般に、並列操作を使用するため、アプリオリにわかります。疑問がある場合は、コードを実行してスピードアップを観察してください。その後、コードを最適化する必要がある場合があります。

于 2012-12-29T16:22:47.510 に答える