はじめてのParaisoプログラミング

Paraisoは、一様メッシュ上での偏微分方程式の陽解法という分野に特化し、その分野の計算を、直胞機械(OrthotopeMachineの訳語、今決めた)と呼ばれる仮想機械のプログラムとして表します。直胞機械は、配列変数とその操作を表現する仮想機械であり、解きたい問題の内包する並列性をなるべく生かすように設計されています。たとえば、配列変数の要素を格納する順序や、アクセスする順序は自由。命令どうしの依存関係もはじめから明確で、依存性のない命令を実行する順序も、同時に実行することも自由です。Paraisoは、この直胞機械のプログラムを記述し、それを既存の言語のプログラムに翻訳し、さらに自動チューニングを行うための様々なメカニズムを備えたフレームワークです。より詳しい用語の解説などはParaisoの論文を見てください。

しかし、Paraisoにコードを生成させるためにユーザーがすべきことを、具体的に一言でいうなら、「OM型の値を作り、コードジェネレータに渡すこと」となります。

ハローワールドの中身

前回掲載したサンプルexamples/HelloWorld/Generator.hsには、Paraisoにコードを生成させるための全てが記されています。順にみていきましょう。

最初の十数行は#include みたいなもんで、必要なモジュールをインクルードしています。

#!/usr/bin/env runhaskell
{-# LANGUAGE NoImplicitPrelude, OverloadedStrings #-}
{-# OPTIONS -Wall #-}

import           Data.Tensor.TypeLevel
import qualified Data.Text.IO as T
import           Language.Paraiso.Annotation (Annotation)
import           Language.Paraiso.Generator (generateIO)
import qualified Language.Paraiso.Generator.Native as Native
import           Language.Paraiso.Name
import           Language.Paraiso.OM
import           Language.Paraiso.OM.Builder
import           Language.Paraiso.OM.DynValue (f2d, DynValue)
import           Language.Paraiso.OM.Value (StaticValue(..))
import           Language.Paraiso.OM.PrettyPrint (prettyPrintA1)
import           Language.Paraiso.OM.Realm 
import qualified Language.Paraiso.OM.Reduce as Reduce
import           Language.Paraiso.Optimization
import           NumericPrelude
import           System.Process (system)

まずはmainから見ていきましょう。一行目は"output"というフォルダを作っています。二行目では、myOMという名の直胞機械のデータフロー・グラフを出力しています。これは参考のために出力しているのであって、コード生成に必須の過程ではありません。三行目では、myOMの定義とmySetupというコード生成時の設定を渡してC++のコードを生成しています。

-- the main program
main :: IO ()
main = do
  _ <- system "mkdir -p output"
  T.writeFile "output/OM.txt" $ prettyPrintA1 $ myOM
  _ <- generateIO mySetup myOM
  return ()

直胞機械を定義する時には使わないが、コード生成時にはじめて必要になる情報はLanguage.Paraiso.Generator.NativeモジュールにあるSetup型でまとめて指定します。ここでは具体的な計算のサイズや、ライブラリを生成するフォルダを指定しています。コード生成対象の言語はデフォルトではC++です。

-- the code generation setup
mySetup :: Native.Setup Vec2 Int
mySetup = 
  (Native.defaultSetup $ Vec :~ 10 :~ 20)
  { Native.directory = "./dist/" 
  }

次はいよいよmyOMの定義です。直胞機械を定義するには、名前(tableMaker)、マシン全体の注釈、静的変数のリスト、カーネルのリストの4つをmakeOM関数に渡します。

-- the orthotope machine to be generated
myOM :: OM Vec2 Int Annotation
myOM = optimize O3 $
  makeOM (mkName "TableMaker") [] myVars myKernels

myOMには、九九の表を保持するためのtableと、その合計を計算するためのtotalという2つの変数があります。2つの変数のRealmは、それぞれArrayScalarです。Array変数は解きたい問題の解像度とおなじサイズをもつ配列変数で、Scalar変数は普通に1つしかない値です。それぞれの変数はNameも持っています。

-- the variables we use
table :: Named (StaticValue TArray Int)
table = "table" `isNameOf` StaticValue TArray  undefined

total :: Named (StaticValue TScalar Int)
total = "total" `isNameOf` StaticValue TScalar undefined

myVars :: [Named DynValue]
myVars = [f2d table, f2d total]

Paraisoでは、Language.Paraiso.Nameモジュールで定義されているName型を識別子の名前として使います。これは、ただの文字列と区別するために、Data.Textモジュールの提供するマルチバイト文字列型Textnewtypeしたものです。

次にカーネルの定義です。OMのカーネルとは、一度に実行される計算のかたまりです。myOMにはcreateという名前のカーネルがひとつだけあります。

-- the only kernel our OM has
myKernels :: [Named (Builder Vec2 Int Annotation ())]
myKernels = ["create" `isNameOf` createBuilder]

カーネルを作るにはBuilder Monadを使います。ここでは、x軸とy軸の添字を読み込んでその積を計算するカーネルを作っています。ついでに九九の表の総計も求めてみましょう。

-- the kernel builder monad
createBuilder :: Builder Vec2 Int Annotation ()
createBuilder = do 
  x <- bind $ loadIndex (Axis 0) 
  y <- bind $ loadIndex (Axis 1) 
  z <- bind $ x*y
  store table z
  store total $ reduce Reduce.Sum z