fortran66のブログ

fortran について書きます。

【メモ帳】Fortran から Swift を呼ぶ その3

Fortran の配列を Swift 経由で GPU に送り計算する

Swift の文法もよくわからないまま、GPU へ突撃。GPU 用の機械語を吐き出すコンパイラの言語は C++14 になっています。

プログラム

以下では GPU 用のプログラム(拡張子 .metal)、 Swift 用のプログラム(拡張子 .swift)、Fortran 用のプログラム(拡張子 .90)をその順にコンパイル&リンクして実行します。

なおエラーチェックをしないことで手抜きで書きました。また M1 Mac を前提として比較的新しい API や共有メモリーを無チェックで使っています。

xcode はエラーチェックの不備などがあると修正候補を示してくれるので、言われるままにクリックしてみました。びっくりマークやはてなマークが意味もよくわからぬままプイプイ挿入されてエラーが消えましたw ちんねりむっつりマニュアル類を眺めて沈潜して黙考せねばならない古典プログラミング環境に対して、なんとなく浮草の如く漂ってゆけるモダンなプログラミング環境の片鱗を味わう事ができました。

手順メモ

metal 絡みのプログラムを Xcode を使わずにコマンドラインコンパイルする方法がネット上で調べてもよく分かりませんでしたが、適当にやったら出来ました。

xcrun -sdk macosx metal mymetal.metal 

swiftc mymetal.swift -emit-library

gfortran mymetal.f90 libmymetal.dylib 

GPU

GPU での計算を行うプログラム。1次元の配列構造を用います。

#include <metal_stdlib>

kernel void mykernel(device float* arr [[buffer(0)]],
                     constant int32_t &nsize [[buffer(1)]],
                     uint pos [[thread_position_in_grid]])
{
    if (pos < (uint)nsize) {
        arr[pos] = metal::sqrt(arr[pos]);
    }
}

Swift 用

Fortran から配列を受け取り、計算に用いるバイト列としてパラメータとともに GPU に送りこみます。また GPU 用のコンパイル済み機械語ルーチンも送りこんで計算させます。

import Metal

@_cdecl("mymetal")
public func mymetaln(n: Int32, px: UnsafeMutablePointer<Float32>) {
    let device = MTLCreateSystemDefaultDevice()
    let cmdQueue = device?.makeCommandQueue()
    let cmdBuff = cmdQueue?.makeCommandBuffer()
    let encoder = cmdBuff?.makeComputeCommandEncoder()
    
    // make pipeline for GPU binary
    let lib = device?.makeDefaultLibrary()
    let kernelSub: MTLFunction! = lib?.makeFunction(name: "mykernel")   
    let pipeline = try! device?.makeComputePipelineState(function: kernelSub)
    encoder?.setComputePipelineState(pipeline!)
   
    // make memory buffers
    var nsize = Int(n)
    let nlen = MemoryLayout<Float32>.size * nsize
    let buff0 = device?.makeBuffer(bytes: px, length: nlen, options: [.storageModeShared, .cpuCacheModeWriteCombined])
    encoder?.setBuffer(buff0, offset: 0, index: 0)
    let buff1 = device?.makeBuffer(bytes: &nsize, length: MemoryLayout<Int>.size, options: .storageModeShared)
    encoder?.setBuffer(buff1, offset: 0, index: 1)
       
    // group GPU threads
    //    non-uniform thread group assumed
    let w = pipeline?.threadExecutionWidth
    let perGroup = MTLSize(width: w!   , height: 1, depth: 1)
    let perGrid  = MTLSize(width: nsize, height: 1, depth: 1)
    encoder?.dispatchThreads(perGrid, threadsPerThreadgroup: perGroup)
    encoder?.endEncoding()
    
    // run GPU kernel
    cmdBuff?.commit()
    cmdBuff?.waitUntilCompleted()
    
    // to array
    let pbuff = buff0?.contents().bindMemory(to: Float32.self, capacity: nsize)
    for i in 0 ..< n {
        px[Int(i)] = pbuff![Int(i)]
    }

    print("GPU calculation finished")
}

Fortran

module test_m
    implicit none
    interface
        subroutine metal_calc(n, x) bind(c, name = 'mymetal')
            integer, value :: n
            real, intent(in out) :: x(n)
        end subroutine metal_calc
    end interface
end module test_m


program test
    use :: test_m
    implicit none
    integer, parameter :: n = 101
    integer :: i
    real :: x0(n), x1(n), eps = epsilon(0.0) 
    
    x0 = [(real(i), i = 0, n - 1)] 
    x1 = sqrt(x0)
    ! calclation by C++(Metal GPU) via Swift
    call metal_calc(n, x0)
    print *, 'By GPU (Metal C++ via Swift)'
    print '(5es15.7)', x0
    print *
    print *, 'By CPU (Fortran)'
    print '(5es15.7)', x1
    print *
    print '(5es15.7)', x1 - x0

    print *
    print *, 'Machine epsilon =', eps
    print *, '   2 epsilin,       4 epsilon,       8 epsilon'
    print *, 2 * eps, 4 * eps, 8 * eps
end program test

実行結果

0~100 までの平方根Fortran 側と GPU 側で計算し結果を比較します。

所々、計算結果にわずかに違いがあって、その差の絶対値が machine epsilon の 2, 4, 8 倍になっています。丸めモードの違いにしては大きいような気もします。GPU 側は雑に計算しているのではないかと妄想します。この点に関しては心の片隅に留めておきたいと思います。

[a] M1:~/fortran/swift% xcrun -sdk macosx metal mymetal.metal 
[a] M1:~/fortran/swift% swiftc mymetal.swift -emit-library
[a] M1:~/fortran/swift% gfortran mymetal.f90 libmymetal.dylib 
[a] M1:~/fortran/swift% ./a.out                               
GPU calculation finished
 By GPU (Metal C++ via Swift)
  0.0000000E+00  1.0000000E+00  1.4142135E+00  1.7320508E+00  2.0000000E+00
  2.2360680E+00  2.4494898E+00  2.6457512E+00  2.8284271E+00  3.0000000E+00
  3.1622777E+00  3.3166249E+00  3.4641016E+00  3.6055512E+00  3.7416573E+00
  3.8729832E+00  4.0000000E+00  4.1231060E+00  4.2426405E+00  4.3588991E+00
  4.4721360E+00  4.5825758E+00  4.6904159E+00  4.7958317E+00  4.8989797E+00
  5.0000000E+00  5.0990195E+00  5.1961527E+00  5.2915025E+00  5.3851647E+00
  5.4772253E+00  5.5677643E+00  5.6568542E+00  5.7445626E+00  5.8309517E+00
  5.9160800E+00  6.0000000E+00  6.0827627E+00  6.1644139E+00  6.2449985E+00
  6.3245554E+00  6.4031243E+00  6.4807410E+00  6.5574389E+00  6.6332498E+00
  6.7082043E+00  6.7823300E+00  6.8556547E+00  6.9282031E+00  7.0000005E+00
  7.0710683E+00  7.1414289E+00  7.2111025E+00  7.2801104E+00  7.3484697E+00
  7.4161992E+00  7.4833145E+00  7.5498343E+00  7.6157737E+00  7.6811457E+00
  7.7459664E+00  7.8102503E+00  7.8740077E+00  7.9372540E+00  8.0000000E+00
  8.0622578E+00  8.1240387E+00  8.1853523E+00  8.2462120E+00  8.3066244E+00
  8.3666000E+00  8.4261503E+00  8.4852810E+00  8.5440035E+00  8.6023254E+00
  8.6602535E+00  8.7177982E+00  8.7749643E+00  8.8317604E+00  8.8881941E+00
  8.9442720E+00  9.0000000E+00  9.0553856E+00  9.1104336E+00  9.1651516E+00
  9.2195444E+00  9.2736187E+00  9.3273792E+00  9.3808317E+00  9.4339809E+00
  9.4868336E+00  9.5393925E+00  9.5916634E+00  9.6436510E+00  9.6953602E+00
  9.7467947E+00  9.7979593E+00  9.8488579E+00  9.8994951E+00  9.9498739E+00
  1.0000000E+01

 By CPU (Fortran)
  0.0000000E+00  1.0000000E+00  1.4142135E+00  1.7320508E+00  2.0000000E+00
  2.2360680E+00  2.4494898E+00  2.6457512E+00  2.8284271E+00  3.0000000E+00
  3.1622777E+00  3.3166249E+00  3.4641016E+00  3.6055512E+00  3.7416575E+00
  3.8729835E+00  4.0000000E+00  4.1231055E+00  4.2426405E+00  4.3588991E+00
  4.4721360E+00  4.5825758E+00  4.6904159E+00  4.7958317E+00  4.8989797E+00
  5.0000000E+00  5.0990195E+00  5.1961522E+00  5.2915025E+00  5.3851647E+00
  5.4772258E+00  5.5677643E+00  5.6568542E+00  5.7445626E+00  5.8309517E+00
  5.9160800E+00  6.0000000E+00  6.0827627E+00  6.1644139E+00  6.2449980E+00
  6.3245554E+00  6.4031243E+00  6.4807405E+00  6.5574384E+00  6.6332498E+00
  6.7082038E+00  6.7823300E+00  6.8556547E+00  6.9282031E+00  7.0000000E+00
  7.0710678E+00  7.1414285E+00  7.2111025E+00  7.2801099E+00  7.3484693E+00
  7.4161983E+00  7.4833150E+00  7.5498343E+00  7.6157732E+00  7.6811457E+00
  7.7459669E+00  7.8102498E+00  7.8740077E+00  7.9372540E+00  8.0000000E+00
  8.0622578E+00  8.1240387E+00  8.1853523E+00  8.2462111E+00  8.3066235E+00
  8.3666000E+00  8.4261494E+00  8.4852810E+00  8.5440035E+00  8.6023254E+00
  8.6602545E+00  8.7177982E+00  8.7749643E+00  8.8317604E+00  8.8881941E+00
  8.9442720E+00  9.0000000E+00  9.0553856E+00  9.1104336E+00  9.1651516E+00
  9.2195444E+00  9.2736187E+00  9.3273792E+00  9.3808317E+00  9.4339809E+00
  9.4868326E+00  9.5393925E+00  9.5916634E+00  9.6436510E+00  9.6953602E+00
  9.7467947E+00  9.7979593E+00  9.8488579E+00  9.8994951E+00  9.9498739E+00
  1.0000000E+01

  0.0000000E+00  0.0000000E+00  0.0000000E+00  0.0000000E+00  0.0000000E+00
  0.0000000E+00  0.0000000E+00  0.0000000E+00  0.0000000E+00  0.0000000E+00
  0.0000000E+00  0.0000000E+00  0.0000000E+00  0.0000000E+00  2.3841858E-07
  2.3841858E-07  0.0000000E+00 -4.7683716E-07  0.0000000E+00  0.0000000E+00
  0.0000000E+00  0.0000000E+00  0.0000000E+00  0.0000000E+00  0.0000000E+00
  0.0000000E+00  0.0000000E+00 -4.7683716E-07  0.0000000E+00  0.0000000E+00
  4.7683716E-07  0.0000000E+00  0.0000000E+00  0.0000000E+00  0.0000000E+00
  0.0000000E+00  0.0000000E+00  0.0000000E+00  0.0000000E+00 -4.7683716E-07
  0.0000000E+00  0.0000000E+00 -4.7683716E-07 -4.7683716E-07  0.0000000E+00
 -4.7683716E-07  0.0000000E+00  0.0000000E+00  0.0000000E+00 -4.7683716E-07
 -4.7683716E-07 -4.7683716E-07  0.0000000E+00 -4.7683716E-07 -4.7683716E-07
 -9.5367432E-07  4.7683716E-07  0.0000000E+00 -4.7683716E-07  0.0000000E+00
  4.7683716E-07 -4.7683716E-07  0.0000000E+00  0.0000000E+00  0.0000000E+00
  0.0000000E+00  0.0000000E+00  0.0000000E+00 -9.5367432E-07 -9.5367432E-07
  0.0000000E+00 -9.5367432E-07  0.0000000E+00  0.0000000E+00  0.0000000E+00
  9.5367432E-07  0.0000000E+00  0.0000000E+00  0.0000000E+00  0.0000000E+00
  0.0000000E+00  0.0000000E+00  0.0000000E+00  0.0000000E+00  0.0000000E+00
  0.0000000E+00  0.0000000E+00  0.0000000E+00  0.0000000E+00  0.0000000E+00
 -9.5367432E-07  0.0000000E+00  0.0000000E+00  0.0000000E+00  0.0000000E+00
  0.0000000E+00  0.0000000E+00  0.0000000E+00  0.0000000E+00  0.0000000E+00
  0.0000000E+00

 Machine epsilon =   1.19209290E-07
    2 epsilin,       4 epsilon,       8 epsilon
   2.38418579E-07   4.76837158E-07   9.53674316E-07
[a] M1:~/fortran/swift% 

感想

一応、アイデアの原理的な検証を達しましたが、やっつけは好ましくないので少しづつマニュアル類を読んで正則な書き方が出来る様になれたらよかろうかと思います。

Swift で GPU を利用する部分は、昔のバッチジョブのジョブカードを書く類推で、プログラムの代わりにカーネル・サブルーチンを、入出力装置を装置番号に結び付ける代わりにサブルーチン引数を引数 index 番号に、それぞれパイプラインとメモリーバッファ上で結び付けて、ジョブカードに書く代わりに cmdbuffer 上にエンコーダーで積んで並べて、commit でジョブのキューにで投入と考えるとまぁなんとなくイメージがつかめなくもないです。つまりここでの swift は JCL (Job Control Language) なのでエラーで即死はむしろ吉かとw

それにつけても Fortran は気持ちいいですね。

基礎から学ぶ Metal〜MetalによるGPUプログラミング入門

基礎から学ぶ Metal〜MetalによるGPUプログラミング入門

  • 作者:林 晃
  • 発売日: 2021/01/20
  • メディア: 単行本(ソフトカバー)