この内容は、2016年12月14日にshibuya.swift#6で発表した内容に加筆を行ったものです。加筆内容は、主にGPUコンピューティング、生命や知性に関する個人的な観点です。
発表時のスライドはこちら。
MetalでiOSアプリに宿る生命
MetalテクノロジーはiOS8から使用可能なったオーバーヘッドの小さいローレベルな
コンピュータグラフィックスAPIです。
グラフィック用のOpenGLと並列計算用のOpenCLに似た機能が一つのAPIに統合されています。
CPUではせいぜい数コアしかありませんが、GPUには数千以上のコアがあり並列演算を得意としています。
シーケンシャルな演算が得意なCPU、並列演算が得意なGPU、両者をうまく組み合わせることにより今までになかった高い演算能力を備えたアプリができる可能性があります。
Metalを使用する場合、CPUとGPUの連携は以下の図のように行われます。
developer.apple.com/metal
CPUからGPUへの命令は、コマンドバッファで行われます。コマンドバッファの内部には、エンコードされたGPUへのコマンドが含まれています。
また、CPUとGPUにはシステムメモリ上に共有のメモリ領域があります。このメモリ領域は、モードによってCPU、GPU両者からアクセス可能か、GPUからのみアクセス可能か変更することができます。
それでは、MetalのAPIを紹介していきます。これらのAPIは、CPU側のコードで記述します。
まず、MTLDeviceです。MTLDeviceのオブジェクトは単一のGPUのインターフェイスです。別の言葉で言うと、1個のGPUを抽象化したものです。
その証拠に、MTLDeviceのnameプロパティでGPUの名前を取得することができます。
device = MTLCreateSystemDefaultDevice()
print device.name // Apple A8 GPU
このMTLDeviceを含め、多くのMetal関連のAPIはクラスではなくプロパティで記述されています。その理由ですが、様々な理由Metalが動作するデバイス毎にオブジェクトの型が異なるので、実装よりもインターフェイスを重視したためと思われます。
(参考)objc.io
Metal関連のオブジェクトの多くは、このMTLDeviceもしくはMTLDeviceから生成されれたオブジェクトから生成されます。
そのようなオブジェクトをいくつか紹介します。
MTLLibrary: GPU用の関数が記述されたシェーダー(後述)へのインターフェイス
MTLFunction: Metal Shading Language(後述)で記述されたGPUで実行される関数
MTLComputePipelineState: MTLFunctionをGPU用にコンパイルされたコードに変換
MTLComputeCommandEncoder: 上記を含むコマンドをGPU用にエンコード
MTLCommandBuffer: GPUで実行されるコマンドを格納
MTLCommandQueue: 上記のキュー、コマンドの実行順を管理
GPUで実行される関数を含んだコマンドがキューに入れられ、GPUに空きができ次第先にキューに入った順番に実行されることになります。
それでは、Metal Shading Languageで記述されたシェーダーについて解説します。
以下は、最低限のコードで喜寿したGPUコンピューティング用のシェーダーです。
#include <metal_stdlib>
using namespace metal;
お
kernel void addOne(const device float *inputData [[ buffer(0) ]],
device float *outputData [[ buffer(1) ]],
uint id [[ thread_position_in_grid ]])
{
float result = inputData[id];
result += 1.0f;
outputData[id] = result;
}
kernelの記述により、このaddOneという名前の関数はコンピューティング用の関数となります。
inputDataに1.0を加えて返すだけのシンプルな関数です。
idでスレッドを指定していますが、これによりどのスレッドで実行されているか識別することができます。これはポインタのアドレスの指定に用いられています。
それでは、CPUで計算した場合、Metalで計算した場合のパフォーマンスを比較してみましょう。入力に1を足して1を引くだけの無為な計算をループさせるのですが、CPU側ではこれを1万xN回繰り返します。Metal側では、1万スレッドを用いてシェーダー内でN回のループを行います。
以下はMetal側のコンピューティング用の関数です。
kernel void addAndSubtract(const device float *inputData [[ buffer(0) ]],
device float *outputData [[ buffer(1) ]],
uint id [[ thread_position_in_grid ]])
{
float result = inputData[id];
for (uint i=0; i<N; i++){
result += 1.0f;
result -= 1.0f;
}
outputData[id] = result;
}
結果を以下に示します。
N数が十分大きい場合はCPUとMetalで50-60倍程度のパフォーマンス差となりました。
ただ、この図ではN数が小さい場合が比較できないので、縦軸を対数としてみます。
N数が小さい場合、CPUの方がMetalよりもパフォーマンスが上であることがわかります。これは、処理のサイズが小さい場合は、処理そのものよりもGPU側へのデータの転送や設定が所要時間を決定するためだと考えれれます。
一般的に、GPU演算ではGPUへのドローコールを最低限にするのが望ましいとされていますが、スレッド内の処理のサイズが小さいと全処理中に占めるドローコールの割合が大きくなってしまいます。
Meatlの性能を十分に発揮するには、CPU/GPU間のやりとりを最小限にし、kernel関数である程度大きな処理を行うことが大事なようです。
それでは、実装方法、特性が分かってきたところで何か応用を行ってみましょう。
群知能は、シンプルなルールで動く個体が集団になった時に高度な振る舞いを示す人工知能の一種です。このような振る舞いを示す自然現象の例には、鳥の群れや魚の群れ、蟻や蜂の巣、粘菌や細菌のコロニーなどがあります。脳も神経細胞を個体と捉えると一種の群知能と言えるかもしれません。
今回は、以下のルールに基づき個体の動きを決定します。
a. 等距離: 各個体が等距離を保つように動く
b. 並行: 各個体が他の個体と同じ向きになるように動く
c. 等速: 各個体は他の個体と同じ速度になるように動く
これを数式で表すと、以下のようになります。
a.等距離化の式
$ \Delta\Phi_d = \alpha \left(\sum_{k=0}^n (\theta_k - \rho) exp(-b (d_k - a)^2) - \sum_{k=0}^n (\psi_k - \rho) exp(-b d_k^2) \right) \Delta t$
$\Delta\Phi_d$: 角度の変化
$\alpha$: 角度変化の大きさを決める定数
$n$: 自身を除いた個体数
$\theta_k$: 自身から他の個体へ向かうベクトルの向き(角度)
$\psi_k$: 他の個体から自身へ向かうベクトルの向き(角度)
$\rho$: 自身の向き(角度)
$a$: 個体間の望ましい距離を決める定数
$b$: 影響範囲の広さを決める定数
$d_k$: 自身と他の個体間の距離
$\Delta t$: 時間間隔
b.並行化の式
$ \Delta\Phi_p = \beta \sum_{k=0}^n (\omega_k - \rho) exp(-b d_k^2) \Delta t$
$\Delta\Phi_p$: 角度の変化
$\beta$: 角度変化の大きさを決める定数
$n$: 自身を除いた個体数
$\omega_k$: 他の個体の向き(角度)
$\rho$: 自身の向き(角度)
$a$: 個体間の距離を決める定数
$b$: 影響範囲の広さを決める定数
$d_k$: 自身と他の個体間の距離
$\Delta t$: 時間間隔
c.等速化の式
$ \Delta V = \gamma \sum_{k=0}^n (v_k-v) exp(-b d_k^2) \Delta t$
$\Delta V$: 速度の変化
$\gamma$: 速度変化の大きさを決める定数
$n$: 自身を除いた個体数
$v_k$: 他の個体の速度
$v$: 自身の速度
$a$: 個体間の距離を決める定数
$b$: 影響範囲の広さを決める定数
$d_k$: 自身と他の個体間の距離
$\Delta t$: 時間間隔
シェーダーのコードは以下のようになります。
kernel void move(…
for (uint i=0; i<nodeCount; i++){
…
// 等距離
float attraction = exp(-b * (distance - a)*(distance - a));
float repulsion = exp(-b * distance * distance);
dAngle += alpha * (nearAngle*attraction + farAngle*repulsion)*interval;
// 並行
float parallelAngleDif = getRangedAngle(node.angle - currentNode.angle);
dAngle += beta * parallelAngleDif * exp(-b * distance * distance) * interval;
// 等速度
float nodeVelocity = sqrt(node.velocityX*node.velocityX + node.velocityY*node.velocityY);
velocity += gamma * (nodeVelocity - velocity) * exp(-b * distance * distance);;
}
等距離、並行、等速度の式に基づき、各個体が他の全ての個体に対しての相互作用を毎フレームごとに計算します。
実装するためのクラス構成は以下の通りです。
(UIImageViewのサブクラス)
↑
ViewController.swift
↑↓
MetalManager.swift
↑
Shader.metal
それでは、実行結果を実演してみます。
こちらは一つのパラメータの設定例です。
下記は動画へのリンクです。
群知能1
最初はそれぞれの個体がランダムな方向を向いており、ランダムな速度ですが、時間が経過すると、多くの魚が群れを形成し同じ方向に動く傾向が見えてきます。また、小規模なグループが一斉に旋回する様子も観察できます。イメージとしては魚の群れや鳥の群れに近いかと思います。
次は別なパラメータの設定例です。
下記は動画へのリンクです。
群知能2
この場合、魚たちが輪になって回転を始めます。内外二つの輪がそれぞれ逆方向に回転します。歯車や自動車のホイールのようです。
そして、以下もまた別なパラメータの設定例です。
下記は動画へのリンクです。
群知能3
魚たちが集まって、まるで一つの生き物であるような動きをします。姿形はクラゲのようでありますが、無数の個体が集まって一つの個体を形成する様子は粘菌のようでもあり、魚を細胞とするならば多細胞生物に例えることもできるかと思います。
今回は、Metalを純粋にロジックのみに用いてアプリを実装してみました。
200の個体を用いたのですが、それぞれの相互作用を考えたので200x200で40000回のの演算を毎フレームごと、1秒間に60回行ったことになります。しかも、三角関数、指数、平方根などを含むCPUではコストの高い処理を多く含んでいました。あまりパフォーマンスを考慮したコードになっていないので、無駄な処理も多く含まれています。
それにも関わらず、フレームが落ちることは全くありませんでした。なおかつ、CPUの消費はわずか35%ほどでした。
exp関数だけでも秒間1000万回(!)使用していることになります。
改めて、MetalによるGPUコンピューティングの威力を感じた次第です。今回は群知能を対象としましたが、他の人工知能の分野への応用も期待できるかと思います。
今回の観察結果ですが、パラーメタの設定により急激に全体の振る舞いが変わる様子は、物理学でいう相転移に相当するかもしれません。また、個々の個体からは想像しがたい振る舞いを集団が示す様子は、複雑系、もしくは創発に当たるでしょう。
創発とは部分の性質の単純な総和にとどまらない性質が、全体に発現することです。例えば比較的単純なメカニズムの神経細胞の集合から、脳の複雑な機能が発現するのも一種の創発と考えられるかと思います。
人は千数百億個の神経細胞を持つのですが、線虫の仲間にはわずか302個しか神経細胞を持たないC.エレガンスという仲間がいるそうです。そして、その神経細胞同士の五千数百個のつながりは全て明らかになっています。
たったこれだけの神経細胞とニューラルネットワークで、この線虫は物理刺激に対する回避運動を行い、温度や化学物質の濃度と餌の有無を紐づけて記憶するそうです。まるで機械学習ですね。
(参考)エレガントな線虫行動から探る神経機能
人と同等の処理の能力を持ったニューラルネットワークを作ることは最先端のスパコンでも現在は無理ですが、線虫のような、いわば原初の知性とでもいうようなものであれば、MetalとiOSでも実現できそうに思えるのです。
並列演算という意味で、脳はGPUと似ているとよく言われます。スマートフォンは、現時点でもある意味外付けの脳のようなものですが、Metalの並列演算によりより生物の脳らしくなるのではないでしょうか。
これらを別の言葉で言うと、iOSのアプリに生命が宿るのではないか、と考えています。
本来であればこの考えをもっと実証できるコードを今回のAdventCalendarに間に合わせたかったのですが、間に合わなかったのでそれは次回に持ち越しとします。
それでは皆さま、よいお年を。
今回の内容に関して、技術的な詳細は以下の記事をご参考ください。
[iOS] MetalでGPUコンピューティング (1) 最小限のコードの記述と特性の把握
[iOS] MetalでGPUコンピューティング (2) 群知能
[iOS] MetalでGPUコンピューティング (3) MTLDevice
[iOS] MetalでGPUコンピューティング (4) MTKView
[iOS] MetalでGPUコンピューティング (5) MTLLibrary
[iOS] MetalでGPUコンピューティング (6) MTLCommandQueue
[iOS] MetalでGPUコンピューティング (7) MTLCommandBuffer
[iOS] MetalでGPUコンピューティング (8) MTLComputeCommandEncoder
[iOS] MetalでGPUコンピューティング (9) MTLComputePipelineState
[iOS] MetalでGPUコンピューティング(10) Metal Shading Languageで記述されたライフゲームのロジック
[iOS] MetalでGPUコンピューティング(11) MTLRenderCommandEncoder
[iOS] MetalでGPUコンピューティング(12) MTLRenderPipelineState