シリーズ: Apple Silicon AI技術スタック 完全解説
難易度: ★★☆☆☆(初級〜中級)
想定読者: オーディオ/信号処理をする人、リアルタイム処理が必要な人
TL;DR
- vDSPはAccelerateフレームワーク内のデジタル信号処理ライブラリ
- FFT、畳み込み、ベクトル演算などが超高速
- 音声処理、オーディオ分析、科学計算で威力を発揮
- GPU転送オーバーヘッドがないため、リアルタイム処理に最適
GPUだけじゃない:CPUの最適化も重要
ここまでGPU関連の技術を見てきた。でも、全てをGPUで処理すればいいというわけではない。
- データ転送のオーバーヘッド
- レイテンシ要件
- 省電力性
- 小規模データ
こういった場面では、CPUで処理した方が効率的なことも多い。そんなとき頼りになるのがvDSPだ。
vDSPとは?
vDSPはAccelerateフレームワークの一部で、デジタル信号処理に特化したライブラリ。
Apple公式のプログラミングガイドによると:
"The vDSP API provides mathematical functions for applications such as speech, sound, audio, and video processing, diagnostic medical imaging, radar signal processing, seismic analysis, and scientific data processing."
(vDSP APIは、音声、サウンド、オーディオ、ビデオ処理、診断医療画像、レーダー信号処理、地震分析、科学データ処理などのアプリケーション向けの数学関数を提供します)
出典:Apple Developer - vDSP Programming Guide
名前の由来は "vector Digital Signal Processing"。ベクトル演算を使った高速デジタル信号処理、ということ。
Accelerateフレームワーク全体像
vDSPを理解するには、まずAccelerateフレームワークの全体像を把握しておこう:
"The Accelerate framework provides high-performance, energy-efficient computation on the CPU by leveraging its vector-processing capability."
(Accelerateフレームワークは、CPUのベクトル処理能力を活用して、高性能でエネルギー効率の高い計算を提供します)
出典:Apple Developer - Accelerate Overview
Accelerateのコンポーネント
Accelerate Framework
├── vDSP - デジタル信号処理(FFT、畳み込み等)
├── vImage - 画像処理
├── BLAS - 基本線形代数(ベクトル・行列演算)
├── LAPACK - 高度な線形代数(固有値、連立方程式等)
├── BNNS - CPU上のニューラルネットワーク
├── vForce - ベクトル化された数学関数
└── Sparse - スパース行列演算
これらすべてが、Apple Siliconのベクトル演算ユニット(ARM NEON)を活用して高速化されている。
vDSPでできること
vDSPの機能は多岐にわたる:
"The vDSP functions operate on real and complex data types. The functions include data type conversions, fast Fourier transforms (FFTs), and vector-to-vector and vector-to-scalar operations."
(vDSP関数は実数と複素数データ型に対して動作します。関数にはデータ型変換、高速フーリエ変換(FFT)、ベクトル対ベクトルおよびベクトル対スカラー演算が含まれます)
出典:Apple Developer - About the vDSP API
主な機能カテゴリ
| カテゴリ | 機能 | 用途例 |
|---|---|---|
| フーリエ変換 | FFT、DFT、DCT | スペクトル分析、圧縮 |
| 畳み込み | 1D/2D畳み込み、相関 | フィルタリング、特徴検出 |
| ベクトル演算 | 加減乗除、内積、ノルム | 数値計算全般 |
| Biquadフィルタ | IIRフィルタ | オーディオイコライザ |
| データ変換 | Float32⇔Float64、Int⇔Float | データ前処理 |
実践:FFTでオーディオ周波数分析
オーディオ信号の周波数成分を分析するFFTは、vDSPの代表的なユースケースだ。
C言語スタイル(低レベル)
import Accelerate
let n = 1024 // サンプル数
let log2n = vDSP_Length(log2(Float(n)))
// FFTセットアップを作成(事前計算、再利用可能)
guard let fftSetup = vDSP_create_fftsetup(log2n, FFT_RADIX2) else {
fatalError("FFT setup failed")
}
defer { vDSP_destroy_fftsetup(fftSetup) }
// 入力信号(実数)
var signal: [Float] = Array(repeating: 0, count: n)
// サイン波を生成(例:440Hz)
for i in 0..<n {
signal[i] = sin(2.0 * .pi * 440.0 * Float(i) / 44100.0)
}
// Split Complex形式に変換
var realPart = [Float](repeating: 0, count: n/2)
var imagPart = [Float](repeating: 0, count: n/2)
realPart.withUnsafeMutableBufferPointer { realBP in
imagPart.withUnsafeMutableBufferPointer { imagBP in
var splitComplex = DSPSplitComplex(
realp: realBP.baseAddress!,
imagp: imagBP.baseAddress!
)
signal.withUnsafeBytes { signalBytes in
let signalPtr = signalBytes.bindMemory(to: DSPComplex.self).baseAddress!
vDSP_ctoz(signalPtr, 2, &splitComplex, 1, vDSP_Length(n/2))
}
// FFT実行
vDSP_fft_zrip(fftSetup, &splitComplex, 1, log2n, FFTDirection(kFFTDirection_Forward))
// マグニチュード計算
var magnitudes = [Float](repeating: 0, count: n/2)
vDSP_zvabs(&splitComplex, 1, &magnitudes, 1, vDSP_Length(n/2))
// 最大値のインデックス(=支配的な周波数)
var maxMagnitude: Float = 0
var maxIndex: vDSP_Length = 0
vDSP_maxvi(magnitudes, 1, &maxMagnitude, &maxIndex, vDSP_Length(n/2))
let dominantFrequency = Float(maxIndex) * 44100.0 / Float(n)
print("Dominant frequency: \(dominantFrequency) Hz")
}
}
Swift Overlayスタイル(モダン)
最近のvDSPはSwiftフレンドリーなAPIも提供している:
import Accelerate
let signal: [Float] = ... // 入力信号
let n = signal.count
// FFTを作成
let fft = vDSP.FFT(
log2n: vDSP_Length(log2(Float(n))),
radix: .radix2,
ofType: DSPSplitComplex.self
)!
// フォワードFFT
var realPart = [Float](repeating: 0, count: n/2)
var imagPart = [Float](repeating: 0, count: n/2)
signal.withUnsafeBufferPointer { signalPtr in
var splitComplex = DSPSplitComplex(realp: &realPart, imagp: &imagPart)
fft.forward(input: signalPtr.baseAddress!, output: &splitComplex)
}
// マグニチュード計算(Swift風)
let magnitudes = zip(realPart, imagPart).map { sqrt($0*$0 + $1*$1) }
Biquadフィルタ:オーディオイコライザの実装
Logic ProやGarageBandのようなオーディオアプリでイコライザを実装するなら、Biquadフィルタがある:
import Accelerate
// Biquadフィルタの係数(ローパスフィルタの例)
// これらの係数は目的の周波数特性に応じて計算
let b0: Float = 0.292893218813
let b1: Float = 0.585786437627
let b2: Float = 0.292893218813
let a1: Float = 0.0
let a2: Float = 0.171572875254
// フィルタ係数配列
var coefficients: [Float] = [b0, b1, b2, a1, a2]
// 遅延要素(フィルタの状態、ステレオなら2チャンネル分)
var delays = [Float](repeating: 0, count: 2)
// 入力オーディオ
var inputSignal: [Float] = ...
var outputSignal = [Float](repeating: 0, count: inputSignal.count)
// フィルタ処理
vDSP_biquad(
&coefficients,
&delays,
&inputSignal, 1,
&outputSignal, 1,
vDSP_Length(inputSignal.count)
)
リアルタイムオーディオ処理で、この手の計算が毎フレーム必要になる。vDSPなしでは考えられない。
マルチバンドイコライザ
複数のBiquadフィルタを連結することで、マルチバンドイコライザを実装できる:
// 5バンドイコライザの係数(各バンドごとに5係数)
var allCoefficients: [Float] = [
// バンド1: 60Hz
b0_1, b1_1, b2_1, a1_1, a2_1,
// バンド2: 230Hz
b0_2, b1_2, b2_2, a1_2, a2_2,
// ... 続く
]
var delays = [Float](repeating: 0, count: 2 * 5) // 5セクション × 2遅延
// マルチセクションBiquad
vDSP_biquadm(
&allCoefficients,
&delays,
&inputSignal, 1,
&outputSignal, 1,
vDSP_Length(inputSignal.count),
5 // セクション数
)
ベクトル演算:高速な配列処理
単純な配列演算もvDSPなら超高速:
import Accelerate
let a: [Float] = [1, 2, 3, 4, 5]
let b: [Float] = [5, 4, 3, 2, 1]
var result = [Float](repeating: 0, count: 5)
// 要素ごとの加算
vDSP_vadd(a, 1, b, 1, &result, 1, vDSP_Length(5))
// result = [6, 6, 6, 6, 6]
// 要素ごとの乗算
vDSP_vmul(a, 1, b, 1, &result, 1, vDSP_Length(5))
// result = [5, 8, 9, 8, 5]
// スカラー乗算
var scalar: Float = 2.0
vDSP_vsmul(a, 1, &scalar, &result, 1, vDSP_Length(5))
// result = [2, 4, 6, 8, 10]
// 内積
var dotProduct: Float = 0
vDSP_dotpr(a, 1, b, 1, &dotProduct, vDSP_Length(5))
// dotProduct = 35
// 総和
var sum: Float = 0
vDSP_sve(a, 1, &sum, vDSP_Length(5))
// sum = 15
// 平均
var mean: Float = 0
vDSP_meanv(a, 1, &mean, vDSP_Length(5))
// mean = 3.0
「forループで書けばいいじゃん」と思うかもしれないが、vDSPはSIMD命令(NEON)を使って並列処理する。特に大きな配列では圧倒的な差が出る。
vDSP vs Neural Engine / GPU
「でも、GPUやNeural Engineの方が速いんじゃないの?」という疑問があるだろう。
Apple Developer Forumsでの回答:
"vDSP and veclib DONT use ANE."
(vDSPとveclibはANE(Neural Engine)を使用しません)
これは制限ではなく、むしろ設計上の選択だ。
vDSPの強み
| 特徴 | vDSP (CPU) | GPU | Neural Engine |
|---|---|---|---|
| レイテンシ | 最小 | 転送オーバーヘッドあり | 転送オーバーヘッドあり |
| 性能予測 | 安定 | バッチサイズ依存 | モデル依存 |
| 省電力 | 小規模データで有利 | 大規模で有利 | ML特化 |
| 可用性 | 常に利用可能 | デバイス依存 | A11以降 |
使い分けガイド
小さなデータ(< 数千要素)
→ vDSP / Accelerate
リアルタイム処理(オーディオ等)
→ vDSP(レイテンシ最小)
大規模バッチ処理
→ GPU / MPS
ML推論
→ Core ML / Neural Engine
機械学習との関係:前処理・後処理
機械学習パイプラインでもvDSPは活躍する。
音声認識の前処理例
// 音声認識モデルの入力を作る流れ
// 1. 音声波形を取得
let waveform: [Float] = ... // 44100Hzのオーディオ
// 2. プリエンファシス(vDSP)
var emphasized = [Float](repeating: 0, count: waveform.count)
let alpha: Float = 0.97
for i in 1..<waveform.count {
emphasized[i] = waveform[i] - alpha * waveform[i-1]
}
// 3. フレーム分割とウィンドウ関数適用(vDSP)
let frameSize = 400
let hopSize = 160
var window = [Float](repeating: 0, count: frameSize)
vDSP_hann_window(&window, vDSP_Length(frameSize), Int32(vDSP_HANN_NORM))
// 4. 各フレームにFFT(vDSP)
for frameStart in stride(from: 0, to: emphasized.count - frameSize, by: hopSize) {
var frame = Array(emphasized[frameStart..<frameStart+frameSize])
var windowedFrame = [Float](repeating: 0, count: frameSize)
vDSP_vmul(frame, 1, window, 1, &windowedFrame, 1, vDSP_Length(frameSize))
// FFT実行...
}
// 5. メルフィルタバンク適用(vDSP行列演算)
// ...
// 6. Core MLモデルに入力
let mlInput = ...
let prediction = try model.prediction(input: mlInput)
前処理をvDSPで高速化し、推論をCore ML/Neural Engineで実行。これがApple流の最適パイプラインだ。
パフォーマンス Tips
1. セットアップを再利用
// ❌ 毎回作成(遅い)
for _ in 0..<1000 {
let fftSetup = vDSP_create_fftsetup(log2n, FFT_RADIX2)
// 使用...
vDSP_destroy_fftsetup(fftSetup)
}
// ✅ 一度作成して再利用
let fftSetup = vDSP_create_fftsetup(log2n, FFT_RADIX2)!
for _ in 0..<1000 {
// fftSetupを使用...
}
vDSP_destroy_fftsetup(fftSetup)
2. ストライドを活用
インターリーブデータも効率的に処理:
// ステレオオーディオ(LRLRLR...)から左チャンネルだけ処理
let stereo: [Float] = ...
var leftMagnitudes = [Float](repeating: 0, count: stereo.count / 2)
// ストライド2で左チャンネルのみアクセス
vDSP_vabs(stereo, 2, &leftMagnitudes, 1, vDSP_Length(stereo.count / 2))
3. インプレース演算
可能な場合は同じバッファに結果を書き込む:
var data: [Float] = ...
// インプレース(メモリ節約)
vDSP_vabs(data, 1, &data, 1, vDSP_Length(data.count))
まとめ:GPUだけが全てじゃない
機械学習の文脈でCPU演算が軽視されがちだが、vDSPのような高度に最適化されたライブラリは依然として重要だ。
特に:
- リアルタイムオーディオ処理
- 信号の前処理・後処理
- レイテンシクリティカルな処理
- 小〜中規模のベクトル演算
こういった場面では、vDSPが最適解になる。Accelerateフレームワーク全体を含めて、Apple Siliconの機械学習スタックの重要な一角を担っている。
次に読む
- Core ML基礎解説 - vDSPで前処理したデータをMLモデルへ
- Metal基礎解説 - GPU処理との使い分け
- シリーズ目次に戻る