ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
CPUでリアルタイムのML推論をサポート
BNNSGraphを使用して、CPUでの機械学習モデルの実行を高速化する方法を解説します。BNNSGraphを使用してCPUで機械学習モデルをコンパイルし、実行する方法を示すとともに、実行時のメモリ割り当てが不要であることや、音声または信号の処理モデルをシングルスレッドで実行できることなど、BNNSGraphが提供するリアルタイムの保証についてもご紹介します。
関連する章
- 0:00 - Introduction
- 1:18 - Introducing BNNS Graph
- 6:47 - Real-time processing
- 8:00 - Adopting BNNS Graph
- 14:52 - BNNS Graph in Swift
リソース
関連ビデオ
WWDC24
-
ダウンロード
こんにちは AppleのVector & Numerics Groupの Simon Gladmanです 今日は 機械学習ライブラリである Basic Neural Network Subroutines(BNNS)の 素晴らしい新機能について説明します
ここ数年 BNNSはCPUにおける機械学習の 推論とトレーニングのための 包括的なAPIを提供してきました
現在Appleでは BNNSをより高速化し エネルギー効率を高め 操作しやすさを格段に向上させています このセッションでは CPUベースの機械学習用の 新たな優れたAPIである BNNS Graphをご紹介します
まず新しいAPIを紹介し 機械学習とAIモデルを最適化する方法を いくつか説明します 次に BNNS Graphが リアルタイムのユースケースの要求に 応えるための重要な機能を見ていきます そして後ほど アプリでBNNS Graphを採用するために 必要な手順について説明します 最後に BNNS Graphを Swiftで実装する方法に加え SwiftUI/Swift Chartsと連携させる 方法もご紹介します では ぜひお気に入りの椅子に座って お茶でも飲みながら BNNS Graphについて見ていきましょう
まず始める前に BNNSが Appleの機械学習フレームワークの 全体的なスタックに どのように適合するかを説明します BNNSは Accelerateフレームワークの ライブラリの1つで これを使用することで モデルをアプリに統合できます
BNNSはCPUでの機械学習を加速させ Appleの機械学習フレームワークである Core MLで使用されています
CPUベースの機械学習のための 新しいAPIであるBNNS Graphは BNNSライブラリが 個々の機械学習プリミティブでなく グラフ全体を使用できるようにします
トレーニングはAppleプラットフォームに モデルを導入するための最初のステップです モデルのトレーニングが完了したら デバイスに導入するためのモデルの準備 つまり最適化と変換を行う必要があります モデルの準備ができたら アプリケーションに統合する準備が整います このセッションでは ワークフローの統合部分に焦点を当てます
BNNS Graphを理解するために まず従来のBNNS APIを見てみましょう
これまで BNNSはレイヤーに重点を置いた 一連のAPIを提示し 個々のパフォーマンスプリミティブを 提供してきました これは機械学習に使用される構成要素です 畳み込みのような それぞれの演算では通常 多くの詳細情報を構成する必要があります
n次元配列記述子を使って レイヤーへの引数と そのプロパティを指定します つまり 入力の記述子 出力の記述子 そして 畳み込みカーネルの記述子または 重み行列を作成します そしてお察しのとおり BNNSは畳み込みバイアスを もう1つの配列記述子として 想定していました
次に これらの配列記述子を使用して パラメータ構造を作成し そのパラメータ構造を レイヤー自体を作成する関数に渡します 最後に 推論の際にレイヤーを適用します
BNNSを使用して 既存のモデルを実装する場合は 各レイヤーを BNNSプリミティブとしてコーディングし すべての中間テンソルのコードを 記述する必要があります
Appleではここ数年 新しいAPIである BNNS Graphの開発に取り組んできました BNNS Graphは 複数のレイヤーと それらのレイヤー間の データフローで構成される グラフ全体を取得し それを単一のオブジェクトとして使用します それがグラフオブジェクトです つまり レイヤーごとに コードを記述する必要はありません さらに 皆さんもユーザーも より高速なパフォーマンスと 優れたエネルギー効率の恩恵を受けられます では BNNS Graphを アプリに統合するための ワークフローの簡単な概要を見てみましょう
Core MLモデルパッケージ つまりmlpackageファイルから始めます mlpackageの作成方法の詳細については 「Deploy machine learning and AI models on-device withCoreML」 セッションをご覧ください
Xcodeは 自動的にパッケージを mlmodelcファイルにコンパイルします そのあと mlmodelcファイルから グラフを構築するコードを記述します 最後に グラフをラップする コンテキストを作成します 推論を実行するのはそのコンテキストです BNNS Graphは モデル全体を認識しているため 以前は不可能だった多くの最適化を 実行することができます しかもこれらの最適化は 無料で利用できます
これはモデルの小さなセクションの例です このセクションの最初のレイヤーは テンソルAとBの要素ごとの加算を実行し 結果をCに書き込みます 次のレイヤーは畳み込みを実行します そのあと モデルは畳み込み結果に 活性化関数を適用します 最後に スライスレイヤーは 要素のサブセットを テンソルDに書き込みます
BNNS Graphの最適化には 数学的変換が含まれます この例では スライス操作が最後のレイヤーです つまり前述のすべての操作は スライスだけでなく テンソル全体に作用します
数学的変換の最適化により スライスがモデルの先頭に移動されるため BNNSで計算する必要があるのは そのスライス内の要素のサブセットのみです
レイヤー融合による最適化では いくつかのレイヤーを 1つの操作に結合します この例では BNNSが畳み込みレイヤーと 活性化レイヤーを融合させています もう1つの最適化はコピー省略です スライス操作により スライス内のデータを 新しいテンソルにコピーできます
BNNS Graphはスライスを最適化し 元のデータにウインドウを渡します
可能な場合は テンソルが メモリを共有するようにすることで BNNS Graphは メモリ使用量を最適化し 不要な割り当てを排除することもできます この例では テンソルAとCは 同じメモリを共有することができ テンソルBとDも同じメモリを共有できます
BNNS Graphの 重み再パッキング最適化では 例えば 行優先レイアウトからの重みを ブロックされた反復順序に再パッキングして キャッシュの局所性を 向上させることができます
こうした最適化のメリットを得るために コードを記述する必要はありません ただ「それだけ」で実行されます また パフォーマンスは 以前のBNNSプリミティブよりも 平均して2倍以上高速になります
そのため新しいAPIは リアルタイムのユースケースに最適です ここでは そのようなユースケースを見ていきます BNNS GraphをAudio Unitに追加して オーディオを操作するというものです
Audio Unitを使用すると iOSやmacOSアプリで オーディオやMIDIデータを 作成・変更できます これにはLogic ProやGarageBandなどの 音楽制作アプリが含まれます 機械学習を使用したAudio Unitは さまざまな機能を提供できます たとえば音声を分離して ボーカルを分離または削除したり コンテンツに基づいて 音声をさまざまな領域に分割したり 音色変換を適用して 楽器を別の楽器のように 聞こえるようにしたりなどです
このデモでは 内容をシンプルにし オーディオを「ビットクラッシュ」すなわち 量子化して歪んだ効果を与える Audio Unitを作成します
リアルタイム処理の主な要件は 実行フェーズ中のメモリ割り当てや マルチスレッドを回避することです それが起こると カーネルコードへの コンテキストスイッチが発生し リアルタイムの期限に 間に合わなくなる可能性があります BNNS Graphを使用すると モデルのコンパイルと実行を 細かく制御することができ メモリ割り当てなどのタスクのほか 実行をシングルスレッドと マルチスレッドのどちらで行うかも 管理できます
では次に BNNS Graphを採用した Audio Unitプロジェクトの 作成方法を説明します
Xcodeでは Audio Unitの作成が 非常に簡単です Audio Unit Extension Appを作成する テンプレートが用意されているからです
まずbitcrusher mlpackageファイルを プロジェクトナビゲータに ドラッグアンドドロップします
Xcodeはmlpackageを mlmodelcファイルにコンパイルします このファイルを使用して BNNS Graphをインスタンス化します これで最初の2つのステップが完了しました
これで Xcode内でmlmodelcファイルを 使用できるようになったので BNNS Graphオブジェクトを作成し 変更可能なコンテキストにラップする 準備ができました グラフを作成するのは1回のみなので 通常 これをバックグラウンドスレッドで 実行するのは アプリの起動後か ユーザーが機械学習に依存する機能を 初めて使用したときです
グラフのコンパイルでは コンパイルされたCore MLモデルを処理して 最適化された BNNS Graphオブジェクトにします このオブジェクトには 推論で呼び出されるカーネルのリストと 中間テンソルの メモリレイアウトマップが含まれています
Xcodeテンプレートは ビジネスロジックとユーザーインターフェイスに SwiftとSwiftUIを リアルタイム処理にC++を使用します
C++ DSPカーネルヘッダファイルでは このプロジェクトの すべての信号処理が行われます このファイルに BNNS Graphコードを追加します
そのために まずmlmodelcのパスを取得し BNNS Graphをビルドして コンテキストを作成し 引数の型を設定して ワークスペースを作成します ではコードの記述方法を見てみましょう
これはmlmodelcファイルへのパスを 取得するコードです 先に説明したように mlpackageファイルだけを プロジェクトにコピーした結果 Xcodeによって パッケージから mlmodelcファイルが生成されています
実行中にグラフが1つのスレッドだけを 使用するように指定するには デフォルト設定で コンパイルオプション構造を作成します デフォルトの動作では BNNS Graphがマルチスレッドで実行されます そこで SetTargetSingleThread関数を 呼び出して それを変更します
次にコンパイルオプションと mlmodelcファイルへのパスを使用して グラフをコンパイルします GraphCompileFromFile関数で グラフが作成されます ここでは2番目の引数としてNULLを渡し ソースモデル内のすべての関数が コンパイルされるように指定しています
コンパイルが完了したら コンパイルオプションの割り当てを 安全に解除できます
これでステップ3が完了し グラフが作成されました これで不変のグラフができたので ContextMake関数によって グラフが可変のコンテキストでラップされます BNNS Graphでは 動的な形状やその他の 特定の実行オプションをサポートするために 可変のコンテキストが必要です コンテキストでは コールバック関数を設定して 出力とワークスペースメモリを 自分で管理することもできます
BNNS Graphは テンソル構造を処理して 形状 ストライド そしてランクを指定し 基になるデータを ポイントすることもできれば 基になるデータへのポインタを 直接処理することもできます
このデモでは オーディオバッファを直接処理するので コンテキストの引数はポインタになるように 指定します
BNNSがオーディオデータを 処理している間は メモリを割り当てないようにする 必要があります そのためこの初期化中に ページ境界に揃えたワークスペースを 作成します
コンテキストを更新して 処理するデータの最大サイズが 認識されるようにする必要があります そのためには Audio Unitがレンダリングできる 最大フレームに基づく形状を SetDynamicShapes関数に渡します
次に GetWorkspaceSize関数が ワークスペースに割り当てる必要のある メモリの量を返します
ワークスペースメモリは ページ境界に揃っている必要があるので aligned_allocを呼び出して ワークスペースを作成します
ここで注意すべきなのは 元のPythonコードの引数の順序が mlmodelcファイルの引数の順序と 同じではない可能性があることです GraphGetArgumentPosition関数は それぞれの引数の正しい位置を返します この位置は後で実行関数に渡されます
これでコンテキストが適切に構成されました
次に進む前に 他のオプションについても 簡単に触れておきます コンパイルオプションを処理しているときに 最適化の環境設定を行う機会がありました BNNSはデフォルトで グラフのパフォーマンスを最適化します これはAudio Unitに最適です
パフォーマンスが最適化されるということは BNNS Graphオブジェクトの フットプリントが増加する場合でも 追加作業はコンパイルフェーズに 移動される可能性があります
ただし アプリのフットプリントが重要な場合は サイズを最適化することができます サイズを最適化すると データは 可能な限り小さい形式で残されますが 変換を実行するためのコストが原因で 実行パフォーマンスが 低下する可能性があります
もう1つ便利なのは BNNS Graphには NaNAndInfinityChecksを 有効にする関数が含まれていることです このデバッグ設定は 16ビットの アキュムレータがオーバーフローしたときに テンソル内の無限大などの問題を 検出するのに役立ちます ただし このチェックを製品コードで 有効にすることは望ましくありません
これで グラフとコンテキストを初期化し シングルスレッド実行を指定して ワークスペースを作成しましたので BNNSで割り当ては実行されません これでグラフを実行する準備ができました では その実行に必要なコードを 見てみましょう
SetBatchSize関数は 入力信号形状と出力信号形状の 最初の次元のサイズを フレーム内のオーディオサンプルの数に 設定します この場合 2番目の引数は ソースファイル内の関数名を参照します ただしソースファイルには 関数が1つしか含まれていないため NULLを渡すことができます
そして5つの引数 出力信号と入力信号 そして量子化の量を定義するスカラ値を 配列として実行関数に渡します 追加する最初の引数は出力信号です data_ptrフィールドと data_ptr_sizeフィールドを 現在のオーディオチャネルの outputBufferに基づいて指定します
次の引数は入力信号です ここではdata_ptrフィールドと data_ptr_sizeフィールドを指定しますが これは現在のオーディオチャネルの inputBufferに基づきます
次の引数は3つのスカラ値で ユーザーインターフェイスのスライダから 取得されるものです
これで関数を実行できます GraphContextExecute関数は コンテキスト 引数 そしてもちろん重要なワークスペースを 受け付けます 戻り値として 出力ポインタには推論の結果が含まれます SetBatchSize関数と同様に 2番目の引数は ソースファイル内の関数名を参照します ソースファイルには関数が1つしか 含まれていないため NULLを渡します
最後に BNNS GraphをSwiftプロジェクトに 統合する方法を見てみましょう
Swiftの使用に最適なユースケースの1つは BNNS GraphをAudio Unitの SwiftUIコンポーネントに 実装することです それにより Audio Unitの ユーザーインターフェイスに オーディオ信号自体と同じ パラメータを使用して 同じモデルで処理された正弦波を 表示することができます Audio Unitの ユーザーインターフェイスコンポーネントは Swift UIを使用して sampleCount要素を含むデータに ビットクラッシャーモデルを適用し 滑らかな正弦波を表示します
srcChartDataバッファには 正弦波表現が格納され dstChartDataバッファには BNNS Graphが効果を適用した後の 正弦波データが格納されます
これら3つのバッファには ユーザーがユーザーインターフェイスの スライダで制御するスカラ値が格納されます
APIは CとSwiftで一貫しているので 先ほど説明した オーディオ処理コードと同様に グラフとコンテキストを定義しましょう
Swiftではリアルタイムの安全性は 保証されませんが C++のようにワークスペースを提供することで BNNS Graphが実行中に メモリ割り当てを行う必要がなくなります
そのためこれはAudio Unitの パフォーマンスと エネルギー効率の向上に役立ちます
次に引数インデックス変数を宣言し 引数配列内で引数がどこにあるかを 定義します
ここに表示されているのは 波形表示コンポーネントの初期化メソッドに 含まれるコードです ここで グラフ コンテキスト ワークスペースを作成します
最初のステップは Xcodeがmlpackageからコンパイルした mlmodelcファイルへのパスを 取得することです
次に mlmodelcを BNNS Graphオブジェクトにコンパイルします
そのあと C++コードで行ったのと同じように グラフコンテキストを作成します
ユーザーインターフェイスと オーディオ処理の両方に 同じコンテキストを使用しないのはなぜかと 疑問に思うかもしれません それはコンテキストが 一度に1つのスレッドしか 実行できないからです Audio Unitがデータを処理している間に ユーザーがスライダの1つを 調整する可能性もあるため プロジェクトの各部分に 別々のコンテキストが必要です
ここで簡単なチェックを行い BNNSがグラフとコンテキストを 正常に作成したことを確認します
オーディオ処理のコードで行ったように ソースと宛先のチャートデータへのポインタを 直接処理することにします そのためコンテキストに 引数がテンソルではなく ポインタであることを伝えます
この場合 バッチサイズ つまりソースと宛先の信号データバッファの 最初の次元のサイズは 例に挙げた正弦波のサンプル数です
SetBatchSize関数が返されたあと GetWorkspaceSize関数は サンプル数の正しいサイズを返します
次に 引数配列のインデックスを計算します ユーザーがスライダの値を変更するたびに SwiftはupdateChartData() 関数を呼び出し 正弦波に ビットクラッシャー効果を適用します
updateChartData()関数の 最初のステップは スカラ値を 対応するストレージに コピーすることです
次にインデックスを使用して 引数配列を正しい順序で作成します
これで サンプルの正弦波に対して ビットクラッシャーを実行できます 実行関数が返されると SwiftUIは ユーザーインターフェイスのチャートを更新し ビットクラッシャーされた 正弦波を表示します
ここでXcodeに戻ります 波形を表示するSwiftチャートは すでに追加してあるので 引数を格納するバッファを宣言します
グラフ コンテキスト そして引数のインデックスも宣言します
イニシャライザ内で グラフとコンテキストを初期化し
ワークスペースを作成して 引数のインデックスを計算します 次にupdateChartData()関数で インデックスを使用して引数関数を作成し 正しい順序を確認してから
グラフを実行します
Xcodeの実行ボタンを押して アプリを起動し 動作しているビットクラッシャーを 聞いてみましょう
ありがとうございました では今日の内容をまとめたいと思います BNNS Graphが提供する APIを使用すると 高パフォーマンスで エネルギー効率が高く リアルタイムでレイテンシの影響を受けやすい 機械学習をCPUで実現できます これはオーディオアプリに最適です 改めて ご視聴ありがとうございました
-
-
0:01 - Create the graph
// Get the path to the mlmodelc. NSBundle *main = [NSBundle mainBundle]; NSString *mlmodelc_path = [main pathForResource:@"bitcrusher" ofType:@"mlmodelc"]; // Specify single-threaded execution. bnns_graph_compile_options_t options = BNNSGraphCompileOptionsMakeDefault(); BNNSGraphCompileOptionsSetTargetSingleThread(options, true); // Compile the BNNSGraph. bnns_graph_t graph = BNNSGraphCompileFromFile(mlmodelc_path.UTF8String, NULL, options); assert(graph.data); BNNSGraphCompileOptionsDestroy(options);
-
0:02 - Create context and workspace
// Create the context. context = BNNSGraphContextMake(graph); assert(context.data); // Set the argument type. BNNSGraphContextSetArgumentType(context, BNNSGraphArgumentTypePointer); // Specify the dynamic shape. uint64_t shape[] = {mMaxFramesToRender, 1, 1}; bnns_graph_shape_t shapes[] = { (bnns_graph_shape_t) {.rank = 3, .shape = shape}, (bnns_graph_shape_t) {.rank = 3, .shape = shape} }; BNNSGraphContextSetDynamicShapes(context, NULL, 2, shapes); // Create the workspace. workspace_size = BNNSGraphContextGetWorkspaceSize(context, NULL) + NSPageSize(); workspace = (char *)aligned_alloc(NSPageSize(), workspace_size);
-
0:03 - Calculate indices
// Calculate indices into the arguments array. dst_index = BNNSGraphGetArgumentPosition(graph, NULL, "dst"); src_index = BNNSGraphGetArgumentPosition(graph, NULL, "src"); resolution_index = BNNSGraphGetArgumentPosition(graph, NULL, "resolution"); saturationGain_index = BNNSGraphGetArgumentPosition(graph, NULL, "saturationGain"); dryWet_index = BNNSGraphGetArgumentPosition(graph, NULL, "dryWet");
-
0:04 - Execute graph
// Set the size of the first dimension. BNNSGraphContextSetBatchSize(context, NULL, frameCount); // Specify the direct pointer to the output buffer. arguments[dst_index] = { .data_ptr = outputBuffers[channel], .data_ptr_size = frameCount * sizeof(outputBuffers[channel][0]) }; // Specify the direct pointer to the input buffer. arguments[src_index] = { .data_ptr = (float *)inputBuffers[channel], .data_ptr_size = frameCount * sizeof(inputBuffers[channel][0]) }; // Specify the direct pointer to the resolution scalar parameter. arguments[resolution_index] = { .data_ptr = &mResolution, .data_ptr_size = sizeof(float) }; // Specify the direct pointer to the saturation gain scalar parameter. arguments[saturationGain_index] = { .data_ptr = &mSaturationGain, .data_ptr_size = sizeof(float) }; // Specify the direct pointer to the mix scalar parameter. arguments[dryWet_index] = { .data_ptr = &mMix, .data_ptr_size = sizeof(float) }; // Execute the function. BNNSGraphContextExecute(context, NULL, 5, arguments, workspace_size, workspace);
-
0:05 - Declare buffers
// Create source buffer that represents a pure sine wave. let srcChartData: UnsafeMutableBufferPointer<Float> = { let buffer = UnsafeMutableBufferPointer<Float>.allocate(capacity: sampleCount) for i in 0 ..< sampleCount { buffer[i] = sin(Float(i) / ( Float(sampleCount) / .pi) * 4) } return buffer }() // Create destination buffer. let dstChartData = UnsafeMutableBufferPointer<Float>.allocate(capacity: sampleCount) // Create scalar parameter buffer for resolution. let resolutionValue = UnsafeMutableBufferPointer<Float>.allocate(capacity: 1) // Create scalar parameter buffer for resolution. let saturationGainValue = UnsafeMutableBufferPointer<Float>.allocate(capacity: 1) // Create scalar parameter buffer for resolution. let mixValue = UnsafeMutableBufferPointer<Float>.allocate(capacity: 1)
-
0:06 - Declare indices
// Declare BNNSGraph objects. let graph: bnns_graph_t let context: bnns_graph_context_t // Declare workspace. let workspace: UnsafeMutableRawBufferPointer // Create the indices into the arguments array. let dstIndex: Int let srcIndex: Int let resolutionIndex: Int let saturationGainIndex: Int let dryWetIndex: Int
-
0:07 - Create graph and context
// Get the path to the mlmodelc. guard let fileName = Bundle.main.url( forResource: "bitcrusher", withExtension: "mlmodelc")?.path() else { fatalError("Unable to load model.") } // Compile the BNNSGraph. graph = BNNSGraphCompileFromFile(fileName, nil, BNNSGraphCompileOptionsMakeDefault()) // Create the context. context = BNNSGraphContextMake(graph) // Verify graph and context. guard graph.data != nil && context.data != nil else { fatalError()}
-
0:08 - Finish initialization
// Set the argument type. BNNSGraphContextSetArgumentType(context, BNNSGraphArgumentTypePointer) // Set the size of the first dimension. BNNSGraphContextSetBatchSize(context, nil, UInt64(sampleCount)) // Create the workspace. workspace = .allocate(byteCount: BNNSGraphContextGetWorkspaceSize(context, nil), alignment: NSPageSize()) // Calculate indices into the arguments array. dstIndex = BNNSGraphGetArgumentPosition(graph, nil, "dst") srcIndex = BNNSGraphGetArgumentPosition(graph, nil, "src") resolutionIndex = BNNSGraphGetArgumentPosition(graph, nil, "resolution") saturationGainIndex = BNNSGraphGetArgumentPosition(graph, nil, "saturationGain") dryWetIndex = BNNSGraphGetArgumentPosition(graph, nil, "dryWet")
-
0:09 - Create arguments array
// Copy slider values to scalar parameter buffers. resolutionValue.initialize(repeating: resolution.value) saturationGainValue.initialize(repeating: saturationGain.value) mixValue.initialize(repeating: mix.value) // Specify output and input arguments. var arguments = [(dstChartData, dstIndex), (srcChartData, srcIndex), (resolutionValue, resolutionIndex), (saturationGainValue, saturationGainIndex), (mixValue, dryWetIndex)] .sorted { a, b in a.1 < b.1 } .map { var argument = bnns_graph_argument_t() argument.data_ptr = UnsafeMutableRawPointer(mutating: $0.0.baseAddress!) argument.data_ptr_size = $0.0.count * MemoryLayout<Float>.stride return argument }
-
0:10 - Execute graph
// Execute the function. BNNSGraphContextExecute(context, nil, arguments.count, &arguments,
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。