Genta Hirauchi

公開日:2020/03/11
更新日:2020/08/03

【Swift Metal】dispatchThreadgroupsの最適化ついて解説

  • GPGPUのThreadやThreadgroupについてよくわからない
  • 正しく設定して、パフォーマンスをあげたい

MetalのGPGPUの実装では、Thread及びThreadgroupの理解が必要となります。

ThreadやThreadgroupの数を正しく設定することで、パフォーマンスを上げることも可能です。

本記事では、dispatchThreadgroupsで設定する、threadsPerThreadgroupとthreadgroupsPerGridの最適化の方法について解説致します。

目次

dispatchThreadgroupsついて

MetalでGPGPUの実装をする際、Thread及びThreadgroupの数は、MTLComputeCommandEncoderのdispatchThreadgroupsで設定します。

dispatchThreadgroupsは、以下のように定義されております。

func dispatchThreadgroups(_ threadgroupsPerGrid: MTLSize,
                            threadsPerThreadgroup: MTLSize)

【公式ドキュメント】 : dispatchThreadgroups(_:threadsPerThreadgroup:) – MTLComputeCommandEncoder | Apple Developer Documentation

引数には、threadgroupsPerGridthreadsPerThreadgroupを指定します。

threadgroupsPerGridは、GridにおけるThreadgroupの数のことです。MTLSize(width: Int, height: Int, depth: Int)で指定します。

threadsPerThreadgroupは、ThreadgroupにおけるThreadの数のことです。Threadgroup同様、MTLSizeで指定します。

1D、2D、3Dについて

threadgroupsPerGrid及びthreadsPerThreadgroupは、MTLSize(width: Int, height: Int, depth: Int)で指定すると紹介しました。

このMTLSizeのwidth、height、depthの内、widthしか使用しない(heightとdepthに1を指定する)場合は1D(1次元)となります。Thread及びThreadgroupが線状で配置されるイメージです。

MTLSizeのwidth、heightを使用する場合は2D(2次元)となります。Thread及びThreadgroupが面状で配置されるイメージです。

MTLSizeのwidth、height、depthを使用する場合は3D(3次元)となります。Thread及びThreadgroupが立体状で配置されるイメージです。

設定値を最適化し、パフォーマンスを上げる方法

ここからは、threadsPerThreadgroup及びthreadgroupsPerGridの設定値を最適化し、パフォーマンスを上げる方法を紹介致します。

最適化のためには、Threadの最大設定可能数と、GPUによって同時実行されるスレッド数を知る必要があります。

Threadの最大設定可能数

Threadの最大設定可能数は、ComputePipelineStateの、maxTotalThreadsPerThreadgroupで取得することができます。

【公式ドキュメント】 : maxTotalThreadsPerThreadgroup – MTLComputePipelineState | Apple Developer Documentation

threadsPerThreadgroupとして設定するMTLSizeのwidthとheightとdepthを掛け合わせた数が、この最大設定可能数以内に収まるようにする必要があります。

GPUによって同時実行されるスレッド数

GPUによって同時実行されるスレッド数は、ComputePipelineStateの、threadExecutionWidthで取得することができます。

【公式ドキュメント】 : threadExecutionWidth – MTLComputePipelineState | Apple Developer Documentation

threadsPerThreadgroupのwidthには、この値の倍数を指定する必要があります。

threadsPerThreadgroupの最適化

ここからは、最適化のサンプルコードを紹介していきます。
※ maxTotalThreadsPerThreadgroupは1024、threadExecutionWidthは32であることを前提として実装しております。

【1Dの場合】

let maxTotalThreadsPerThreadgroup = computePipelineState.maxTotalThreadsPerThreadgroup // 1024
let threadExecutionWidth          = computePipelineState.threadExecutionWidth // 32
let width  = maxTotalThreadsPerThreadgroup / threadExecutionWidth * threadExecutionWidth // 1024 / 32 * 32 = 1024
let height = 1
let depth  = 1
let threadsPerThreadgroup = MTLSize(width: width, height: height, depth: depth) // MTLSize(width: 1024, height: 1, depth: 1)

widthには、maxTotalThreadsPerThreadgroup / threadExecutionWidth * threadExecutionWidthの結果を設定しております。こうすることで、maxTotalThreadsPerThreadgroupに最も近い、threadExecutionWidthの倍数の値がwidthに設定されるようになります。

height及びdepthには、1Dなので1を指定しております。

threadsPerThreadgroupは、MTLSize(width: 1024, height: 1, depth: 1)となりました。width(1024) * height(1) * depth(1) の値が、maxTotalThreadsPerThreadgroup(1024)の値と等しいので、最適化されていることがわかります。

【2Dの場合】

let maxTotalThreadsPerThreadgroup = computePipelineState.maxTotalThreadsPerThreadgroup // 1024
let threadExecutionWidth          = computePipelineState.threadExecutionWidth // 32
let width  = threadExecutionWidth // 32
let height = maxTotalThreadsPerThreadgroup / width // 1024 / 32 = 32
let depth  = 1
let threadsPerThreadgroup = MTLSize(width: width, height: height, depth: depth) // MTLSize(width: 32, height: 32, depth: 1)

widthには、threadExecutionWidthの値を設定しております。

heightには、maxTotalThreadsPerThreadgroup / widthの結果を設定しております。

depthには、2Dなので1を指定しております。

threadsPerThreadgroupは、MTLSize(width: 32, height: 32, depth: 1)となりました。width(32) * height(32) * depth(1) の値が、maxTotalThreadsPerThreadgroup(1024)の値と等しいので、最適化されていることがわかります。

【3Dの場合】

let maxTotalThreadsPerThreadgroup = computePipelineState.maxTotalThreadsPerThreadgroup // 1024
let threadExecutionWidth          = computePipelineState.threadExecutionWidth // 32
let width  = threadExecutionWidth // 32
let height = 8
let depth  = maxTotalThreadsPerThreadgroup / width / height // 1024 / 32 / 8 = 4
let threadsPerThreadgroup = MTLSize(width: width, height: height, depth: depth) // MTLSize(width: 32, height: 8, depth: 4)

widthには、threadExecutionWidthの値を設定しております。

heightには、32以下で、32を割り切れる値を設定しております。

depthには、maxTotalThreadsPerThreadgroup / width / heightの結果を設定しております。

threadsPerThreadgroupは、MTLSize(width: 32, height: 32, depth: 1)となりました。width(32) * height(8) * depth(4) の値が、maxTotalThreadsPerThreadgroup(1024)の値と等しいので、最適化されていることがわかります。

threadgroupsPerGridの最適化

続いて、threadgroupsPerGridのサンプルコードを紹介致します。
※ 10000の処理を並列実行させることを前提として実装しております。

【1Dの場合】

let count = 10000 // 並列処理数

let maxTotalThreadsPerThreadgroup = computePipelineState.maxTotalThreadsPerThreadgroup // 1024
let threadExecutionWidth          = computePipelineState.threadExecutionWidth // 32
let width  = maxTotalThreadsPerThreadgroup / threadExecutionWidth * threadExecutionWidth // 1024 / 32 * 32 = 1024
let height = 1
let depth  = 1
let threadsPerThreadgroup = MTLSize(width: width, height: height, depth: depth) // MTLSize(width: 1024, height: 1, depth: 1)

// width: (10000 + 1024 - 1) / 1024 = 10
// threadsPerThreadgroup(1024 * 1 * 1) * threadgroupsPerGrid(10 * 1 * 1) = 10240
// 総Thread数(10240) > 並列処理数(10000) となる
let threadgroupsPerGrid = MTLSize(width: (count + width - 1) / width, height: 1, depth: 1)

widthには、(count + width – 1) / widthの結果を設定しております。

height及びdepthには、1Dなので1を指定しております。

threadgroupsPerGridは、MTLSize(width: 10, height: 1, depth: 1)となりました。threadsPerThreadgroup(1024 * 1 * 1) * threadgroupsPerGrid(10 * 1 * 1)の値(10240)が、並列処理数(10000)以上となるように設定されております。

本当に10が最適値なのかということについてですが、
1024 * 1 * 1 * 9 * 1 * 1 = 9216
1024 * 1 * 1 * 10 * 1 * 1 = 10240
1024 * 1 * 1 * 11 * 1 * 1 = 11264
という結果からも、10が最適値であることが確認いただけると思います。

【2Dの場合】

// 並列処理数
let xCount = 100
let yCount = 100

let maxTotalThreadsPerThreadgroup = computePipelineState.maxTotalThreadsPerThreadgroup // 1024
let threadExecutionWidth          = computePipelineState.threadExecutionWidth // 32
let width  = threadExecutionWidth // 32
let height = maxTotalThreadsPerThreadgroup / width // 1024 / 32 = 32
let depth  = 1
let threadsPerThreadgroup = MTLSize(width: width, height: height, depth: depth) // MTLSize(width: 32, height: 32, depth: 1)

// width : (100 + 32 - 1) / 32 = 4
// height: (100 + 32 - 1) / 32 = 4
// threadsPerThreadgroup(32 * 32 * 1) * threadgroupsPerGrid(4 * 4 * 1) = 16384
// 総Thread数(16384) > 並列処理数(10000) となる
let threadgroupsPerGrid = MTLSize(width: (xCount + width - 1) / width, height: (yCount + height - 1) / height, depth: 1)

widthには、(xCount + width – 1) / widthの結果を設定しております。

heightには、(yCount + height – 1) / heightの結果を設定しております。

depthには、2Dなので1を指定しております。

threadgroupsPerGridは、MTLSize(width: 4, height: 4, depth: 1)となりました。threadsPerThreadgroup(32 * 32 * 1) * threadgroupsPerGrid(4 * 4 * 1)の値(16384)が、並列処理数(10000)以上となるように設定されております。

heightを3にすれば、threadsPerThreadgroup(32 * 32 * 1) * threadgroupsPerGrid(4 * 3 * 1) = 12288で、よりいいのではないかと思うかもしれませんが、その場合、threadsPerThreadgroupのheight(32) * threadgroupsPerGridのheight(3)が96となり、yCountの100未満となるため、NGです。

【3Dの場合】

// 並列処理数
let xCount = 100
let yCount = 10
let zCount = 10

let maxTotalThreadsPerThreadgroup = computePipelineState.maxTotalThreadsPerThreadgroup // 1024
let threadExecutionWidth          = computePipelineState.threadExecutionWidth // 32
let width  = threadExecutionWidth // 32
let height = 8
let depth  = maxTotalThreadsPerThreadgroup / width / height // 1024 / 32 / 8 = 4
let threadsPerThreadgroup = MTLSize(width: width, height: height, depth: depth) // MTLSize(width: 32, height: 8, depth: 4)

// width : (100 + 32 - 1) / 32 = 4
// height: (10  + 8  - 1) / 8  = 2
// depth : (10  + 4  - 1) / 4  = 3
// threadsPerThreadgroup(32 * 8 * 4) * threadgroupsPerGrid(4 * 2 * 3) = 24576
// 総Thread数(24576) > 並列処理数(10000) となる
let threadgroupsPerGrid = MTLSize(width: (xCount + width - 1) / width, height: (yCount + height - 1) / height, depth: (zCount + depth - 1) / depth)

widthには、(xCount + width – 1) / widthの結果を設定しております。

heightには、(yCount + height – 1) / heightの結果を設定しております。

depthには、(zCount + depth – 1) / depthの結果を設定しております。

threadgroupsPerGridは、MTLSize(width: 4, height: 2, depth: 3)となりました。threadsPerThreadgroup(32 * 32 * 1) * threadgroupsPerGrid(4 * 2 * 3)の値(24576)が、並列処理数(10000)以上となるように設定されております。

depthを2にすると、threadsPerThreadgroupのdepth(4) * threadgroupsPerGridのdepth(2)が8となり、zCountの10をしたまわるため、3が最適値であることがわかります。

以上、threadsPerThreadgroup及びthreadgroupsPerGridの設定値を最適化し、パフォーマンスを上げる方法でした。

まとめ

  • Thread及びThreadgroupの数は、MTLComputeCommandEncoderのdispatchThreadgroupsで設定する
  • Threadの最大設定可能数は、ComputePipelineStateの、maxTotalThreadsPerThreadgroupで取得できる
  • GPUによって同時実行されるスレッド数は、ComputePipelineStateの、threadExecutionWidthで取得できる