FFT速度比較

FFT速度比較 #

Pythonにおける高速フーリエ変換 (FFT) の実装をテーマに、主要なライブラリ (NumPy、SciPy、pyFFTW、CuPy、PyTorch) の性能を比較します。 特に、データサイズの違いが計算速度に与える影響を検証し、各ライブラリの特徴を考察します。

1. まとめ #

FFT点数 $N$ の範囲 最も高速なライブラリ
$N \leq 2^{14}$ pyFFTW (FFTW_MEASURE 以上)
$N \geq 2^{14}$ PyTorch (GPU使用)
  • NumPy, SciPyに比べて, pyFFTWは2倍から5倍程度速いです。

メインの結果である、FFT-IFFTにかかる平均実行時間 vs FFT点数 $N$ の結果を示します。

2. 比較対象 #

2.1. 各ライブラリの特徴 #

ライブラリ 主な特徴 高速性 対応環境 備考
NumPy 基本的なFFT機能を提供。扱いやすく広く使用されている 普通 CPU 小規模データの処理に適している
SciPy NumPyの改良版FFTを提供。高度なオプションあり 高い CPU 単精度演算や分散処理にも対応
pyFFTW FFTWのPythonラッパー。高速かつ高度なカスタマイズ性を持つ 非常に高い CPU マルチスレッドやメモリ整列をサポート
CuPy NVIDIA GPUを活用した高速なFFT計算が可能 圧倒的に高い GPU (NVIDIAのみ) GPU並列計算に特化。大規模データ処理に最適
PyTorch 機械学習フレームワークの一部として高性能なFFTを提供 高い CPU, GPU GPU対応で大規模データ処理が可能。
自作FFT 再帰関数で実装したFFT。理論的な学習やアルゴリズムの理解に有用 低い CPU 実装の工夫次第で性能改善の余地あり

pyFFTWはプランナーフラグ (Planner Flags) 1234という、いわば最適化オプションを選択できますので、それらを採用した場合の速度比較も行います。

  • Planner Flags
    • FFTW_ESTIMATE
    • FFTW_MEASURE
    • FFTW_PATIENT
    • FFTW_EXHAUSTIVE

2.2. fft-ifftを行うコード例 #

各ライブラリで、FFTを行うまでに必要なコードを示します。ベンチマークに用いるコードとは異なりますが、使用方法を理解するために示します。

対象とする関数は、次のコードで作られた入出力 (変数名fx) がnumpy.ndarrayで、要素の型はnumpy.complex128を仮定します。 fxのFFT実施後を変数gkに、gkのIFFT実施後を変数gxとします。数値的な誤差を除いて、計算前後のfx, gxは同じになります。

import numpy as np
import scipy as sp
import cupy as cp
import torch
import pyfftw

# - データ生成
N = 2**10  # FFT点数
fx = np.random.random(N) + 1.0j * np.random.random(N)

# - Numpy fft
gk = np.fft.fft(fx)   # FFTを実行
gx = np.fft.ifft(gk)  # IFFTを実行

# - Scipy fft
gk = sp.fft.fft(fx)   # FFTを実行
gx = sp.fft.ifft(gk)  # IFFTを実行

# - Self-made fft
gk = fft_my(fx)       # FFTを実行
gx = ifft_my(gk) / N  # IFFTを実行 (規格化も実施)

# - Cupy fft
fx_cp = cp.array(fx)               # NumPy配列をCuPy配列に変換 (非同期処理)
gk_cp = cp.fft.fft(fx_cp)             # FFTを実行 (非同期処理)
gx_cp = cp.fft.ifft(gk_cp)            # IFFTを実行 (非同期処理)
cp.cuda.Stream.null.synchronize()  # GPU計算が終了するのを待つ (ここで同期)
gk = cp.asnumpy(gk_cp)             # 結果をCPU (NumPy) に戻す
gx = cp.asnumpy(gx_cp)             # 結果をCPU (NumPy) に戻す

# - CuPy (Plan)
fx_cp = cp.array(fx)               # NumPy配列をCuPy配列に変換(非同期処理)
cp.cuda.Stream.null.synchronize()  # GPUの転送が完了するのを待つ
plan = cp.cuda.cufft.Plan1d(N, cp.cuda.cufft.CUFFT_Z2Z, 1) # Plan1dを使用して計画を作成
gk_cp = cp.empty_like(fx_cp)       # デバイスメモリ上に入力と出力の配列を準備
gx_cp = cp.empty_like(fx_cp)       # デバイスメモリ上に入力と出力の配列を準備
plan.fft(fx_cp, gk_cp, cp.cuda.cufft.CUFFT_FORWARD)  # FFTを実行  (gk = F[fx])
plan.fft(gk_cp, gx_cp, cp.cuda.cufft.CUFFT_INVERSE)  # IFFTを実行 (gx = F^{-1}[gk])
gx_cp /= N  # IFFTの結果を規格化
cp.cuda.Stream.null.synchronize()  # GPU計算が終了するのを待つ
gk = cp.asnumpy(gk_cp)  # 結果をCPU(NumPy)に戻す
gx = cp.asnumpy(gx_cp)  # 結果をCPU(NumPy)に戻す

# - PyTorch (CPU)
fx_tc = torch.tensor(fx, dtype=torch.complex128)  # NumPy配列をPyTorchの複素数テンソルに変換
gk_tc = torch.fft.fft(fx_tc)   # FFTを実行
gx_tc = torch.fft.ifft(gk_tc)  # IFFTを実行
gk = gk_tc.numpy()  # PyTorchの複素数テンソルをNumPy配列に変換(.numpy())
gx = gx_tc.numpy()  # PyTorchの複素数テンソルをNumPy配列に変換(.numpy())

# - PyTorch (GPU)
fx_tc = torch.tensor(fx, dtype=torch.complex128, device="cuda") # NumPy配列をPyTorchの複素数テンソルに変換(complex128)し、GPUへ転送
gk_tc = torch.fft.fft(fx_tc)   # FFTを実行
gx_tc = torch.fft.ifft(gk_tc)  # IFFTを実行
gk = gk_tc.cpu().numpy()  # 結果をCPUに戻し(.cpu()), NumPy配列に変換(.numpy())
gx = gx_tc.cpu().numpy()  # 結果をCPUに戻し(.cpu()), NumPy配列に変換(.numpy())

# - fftw (planner_effort = "FFTW_MEASURE" の場合)
fft_objects = pyfftw.builders.fft(fx, planner_effort="FFTW_MEASURE")    # fxの型、サイズでFFT実行用オブジェクト生成
ifft_objects = pyfftw.builders.ifft(fx, planner_effort="FFTW_MEASURE")  # fxの型、サイズでIFFT実行用オブジェクト生成
gk = fft_object(fx)   # FFTを実行
gx = ifft_object(gk)  # IFFTを実行

自作コードについては、付録に記載します。

3. ベンチマーク用コード #

ベンチマークに使用したコードを下記に置きます。

fft_benchmark_v1.01.zip (8kB)

特にCuPyとpyFFTWは次の時間が重要になるので、コードをもう少し説明します。

  • CuPy
    • CPU ⇔ GPU 間の転送時間
  • pyFFTW
    • 初めて計画を作成する場合の作成時間

3.1. CuPy #

CuPyではGPUで計算を行うため、計算は

  1. CPUからGPUにデータを転送
  2. GPU上でFFT, IFFTを計算
  3. GPUからCPUにデータを転送

という流れで計算が行われます。それぞれの時間を計測するために次のようにベンチマークコードを作成しました。

  # Transfer from CPU to GPU
  start_tmp = time.perf_counter()
  cp_array = cp.array(fx)  # Convert NumPy array to CuPy array (asynchronous operation)
  cp.cuda.Stream.null.synchronize()
  end_tmp = time.perf_counter()
  benchmarks[fft_lib]["cpu2gpu_transfer_time_ms"].append((end_tmp - start_tmp) * 1000)

  start = time.perf_counter()
  for fft_repeat in range(fft_repeats):
     gk = cp.fft.fft(cp_array)  # Compute FFT (asynchronous operation)
     gx = cp.fft.ifft(gk)  # Compute IFFT (asynchronous operation)
     cp.cuda.Stream.null.synchronize()  # Wait for GPU computations to finish (synchronization occurs here)
  end = time.perf_counter()
  benchmarks[fft_lib]["elapsed_times_ms"].append(((end - start) * 1000) / fft_repeats) # Average

  # Transfer from GPU to CPU
  start_tmp = time.perf_counter()
  gx = cp.asnumpy(gx)  # Transfer results back to CPU (NumPy)
  cp.cuda.Stream.null.synchronize()
  end_tmp = time.perf_counter()
  benchmarks[fft_lib]["gpu2cpu_transfer_time_ms"].append((end_tmp - start_tmp) * 1000)

3.1.1. CuPy (Plan1d) #

もしCuPyで同一の型の同一サイズのFFTを行う場合、pyFFTWのように計画を事前に作成しておくことができます56

これによる高速化も考えましょう。計画を作成する場合、FFTのコードは次のように書けます。

  # Plan making using Plan1d
  plan = cp.cuda.cufft.Plan1d(N, cp.cuda.cufft.CUFFT_Z2Z, 1)

  # Prepare input and output arrays on GPU memory
  gk = cp.empty_like(cp_array)
  gx = cp.empty_like(cp_array)

  start = time.perf_counter()
  for fft_repeat in range(fft_repeats):
      plan.fft(cp_array, gk, cp.cuda.cufft.CUFFT_FORWARD)  # Compute FFT (asynchronous operation)
      plan.fft(gk, gx, cp.cuda.cufft.CUFFT_INVERSE)  # Compute IFFT (asynchronous operation)
      gx /= N  # Normalize
      cp.cuda.Stream.null.synchronize()  # Wait for GPU computations to finish (synchronization occurs here)
  end = time.perf_counter()

3.2. pyFFTWの実行手順とその計画 #

pyFFTWの計算は、次の手順で行われます。

  1. pyFFTWの計画作成と計画に従ったpyFFTWオブジェクト生成 (事前準備、実行環境依存)
  2. FFT, IFFTの実行

初めて計算を行う場合、1.の計画の立て方によってはpyFFTWオブジェクト生成までに数秒から数十分掛かる場合がありますので、その時間は重要です。 ただし、pyFFTWオブジェクトの生成に必要な計画は、同一サイズ、同一の型、同一のプランナーフラグ(さらに同一のスレッド数)であればファイルに保存しておくことができるので、計算結果を保存しておけば次回の計算に使いまわすことができます。 後程示しますが、一度生成してしまえば10ミリ秒程度でpyFFTWオブジェクトが生成されます。

FFTWの計画とは、実行環境で最も高速に動作するように最適化をするものです。 基本的には次のような考えです。

  • 計画を念入りに行えばその後のFFT-IFFTが高速
  • 計画を高速に行えばその後のFFT-IFFTは低速

しかしながら、計画を念入りに行っても余り計算速度の向上が見られないほどが多く、ほどほどの方が良いです。

具体的に次の4つが計画の立て方として指定することができます4

  • FFTW_ESTIMATE: ヒューリスティックかつ高速にプランを決定
  • FFTW_MEASURE: いくつかのFFTを実際に計算し、実行時間を計測してプランを決定
  • FFTW_PATIENT: より広範囲のアルゴリズムでパラメータを調整・実行時間を計測し、プランを決定
  • FFTW_EXHAUSTIVE: 早さに関係なさそうなアルゴリズムも含め、調整・計測し、プランを決定

FFTW_MEASUREがバランスが良いようで、デフォルトではこれが選択されます。

3.3. PyTorch #

PyTorchのFFTは、計画作成が不要であり、簡潔なコードで利用できます。計算に必要なリソースは内部で自動的に最適化されるため、初回の準備時間が短く、即座にFFTを実行できます.

PyTorchのFFTは、CPUとGPUの両方に対応しており、大規模データの処理にも適しています。ただし、計画を作成して再利用する仕組みは提供されていないため、非常に高速な繰り返し計算が必要な場合には他のライブラリと比較することを検討する必要があります。

4. 結果 #

4.1. FFT-IFFTの平均実行時間比較 #

FFT-IFFTの平均実行時間は次の通りになります。

次の点が分かります。

  • $N$が小さいときはpyFFTW, $N$が大きいときは PyTorch(GPU) が速い

    • $N \leq 2\times 10^4 \approx 2^{14}$の時、pyFFTW がもっとも高速
    • $N \geq 2\times 10^4 \approx 2^{14}$の時、PyTorch(GPU) がもっとも高速
  • CPUで計算するライブラリ (NumPy, SciPy, pyFFTW, PyTorch(CPU) ) の中では、pyFFTWが速い

    • 場合によっては PyTorch(CPU) が pyFFTW よりも若干速くなる時があります。
    • NumPy, SciPyに比べて2倍から5倍程度速い
  • pyFFTWの中では、$N\geq 2^{16}$以上で FFTW_PATIENT, FFTW_EXHAUSTIVE が若干早い

  • CuPyでは$N\approx 2^{26}$でメモリスワップらしき動作が起こっている

    • $N\approx 2^{26}$で計算が遅くなっています。この要素数の配列は約1GBです。実行環境のGPUメモリは8GBありますが、タスクマネージャーで見ていたところ共有GPUメモリが使用され始めていたので、この要素数のFFTを行う場合はメモリが足りていないのだと思われます。
  • CuPyをプログラム内で初めて使用する場合、オーバーヘッドらしき時間が掛かる

    • $N=4$の結果が不自然であることから、何らかのオーバーヘッド時間があるようです。ベンチマークを$N=4$から始めるのではなく、$N=8$から始めても$N=4$と同じようなオーバーヘッド時間が見受けられました。

4.2. pyFFTW #

平均実行時間比較で, CPUで計算を行うライブラリではFFTWが高速であるという結論になりました。 しかし上記の実行時間には、計画作成とFFTWオブジェクト作成時間を含めていませんので、これを比較しましょう。

4.2.1. 計画作成時間 #

計画を新規に作成する場合、次の表のとおりの時間が掛かります。あくまで、私の実行環境における速度比較なので、一例として見て下さい。

4.2.2. 計画作成後のオーバーヘッド時間 #

計画は一度計算すればファイルに書き出して保存しておくことができ、例えばFFT点数と型が同じであればその環境での計画作成時間を省略することができます。 その場合、計画作成後は作成済みの計画を読み出すだけであり、次のような時間だけが掛かります。

全ての場合で、1秒もかからずに読み込みが完了していることが分かります。

4.3. CuPy #

4.3.1. CPU-GPU間の転送時間 #

CuPy利用時の転送時間は次の通りになります。

転送が一度だけで良いなら余り問題ではないですが、何度もCPUとGPUをデータが行き来するとすぐに増えてしまう時間が掛かっています。

CPUからGPUへの転送の方が時間が掛かるようです。

また、CuPyをプログラム内で初めて使用する場合、更にオーバーヘッドがかかるようです(FFT点数が4点の場合の結果から)。

5. 付録 #

5.1. CuPyインストール #

5.1.1. インストールされているか確認 #

nvcc --version

5.1.2. インストール手順 #

  1. NVIDIAの公式サイトにアクセス: CUDA Toolkit 12.6 Update 3 Downloads -nVIDIA DEVELOPER

  2. インストールオプションを選択: Operating System: Windows Architecture: x86_64 Version: 適切なWindowsバージョンを選択 (例: Windows 10、Windows 11) Installer Type: EXE (Local) を推奨

  3. ダウンロードしてインストーラーを実行: インストール時、すべてのデフォルトオプションを選択してインストールします。

  4. インストール後、以下のディレクトリが作成されるはずです: C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.6

5.1.3. CuPy #

nvidia-smi

> nvidia-smi
Tue Jan 14 00:11:30 2025
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 566.03                 Driver Version: 566.03         CUDA Version: 12.7     |
|-----------------------------------------+------------------------+----------------------+
| GPU  Name                  Driver-Model | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA GeForce RTX 3060 Ti   WDDM  |   00000000:0D:00.0  On |                  N/A |
| 33%   56C    P0             47W /  200W |    1285MiB /   8192MiB |      2%      Default |
|                                         |                        |                  N/A |
+-----------------------------------------+------------------------+----------------------+
...
pip install cupy-cuda12x
import cupy as cp

# CUDAデバイスの数を取得
print("CUDAデバイス数:", cp.cuda.runtime.getDeviceCount())

# CUDAバージョンを取得
print("CUDAバージョン:", cp.cuda.runtime.runtimeGetVersion() / 1000)

# GPUの名前を取得
device_id = 0  # 使用するGPUのID
device_properties = cp.cuda.runtime.getDeviceProperties(device_id)
print("GPU名:", device_properties["name"])
> python .\main.py
CUDAデバイス数: 1
CUDAバージョン: 12.06
GPU名: b'NVIDIA GeForce RTX 3060 Ti'
>

5.2. pyFFTWのインストール #

pip install pyfftw

5.2. PyTorchのインストール #

PyTorchはCUDAバージョンに合わせてインストールしなければなりません。

CUDAのバージョンは、Powershellでnvidia-smiを入力して確認できます。私の場合は次の結果になり、CUDAのバージョンは12.7だと分かります。

> nvidia-smi
Tue Jan 14 00:11:30 2025
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 566.03                 Driver Version: 566.03         CUDA Version: 12.7     |
|-----------------------------------------+------------------------+----------------------+
| GPU  Name                  Driver-Model | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA GeForce RTX 3060 Ti   WDDM  |   00000000:0D:00.0  On |                  N/A |
...

インストールコマンドは、次のページからコマンドを生成することができます。

Start Locally -PyTorch

CUDAのバージョンに対応するPyTorchが無い場合、CUDAの互換性(一般的には下位互換)に基づき、CUDA 12.xに対応したPyTorchをインストールすれば良いです。

私はCUDA 12.7に対し、PyTorch 12.1をインストールしました。

5.3. グラフの妥当性確認 #

ここでは、3.1. FFT-IFFTの平均実行時間比較 で示したグラフが、本当にFFTの計算になっているか確かめておきましょう。 FFTの計算になっているならば計算量は$O(N\ln N)$, 通常のDFTであれば$O(N^2)$になっているはずです。

計算量と計算時間は比例すると仮定します。点数を$N$とすると、FFTの計算量は$O(N\ln N)$ですので、計算時間$T$は比例定数$c$を用いて

$$ \begin{align} T=c N\ln N \end{align} $$

と書けます。両辺の対数を取れば

$$ \begin{align} \ln(T)&= \ln(N)+\ln(\ln N)+\ln(c) &&\\ \end{align} $$

$$ \begin{align} \ln(T)&\approx \ln(N) &&(N\to \infty) \\ \end{align} $$

となるので、x,y軸を対数にとった時、$N\to\infty$の漸近で比例するように見えます。
実際、図を見ますと N が10倍になると T が10倍になる増え方をしていますので、FFTが実際に適用されていることが分かります。

5.4. 自作FFT, IFFT #

次のように、再帰を利用してFFT, IFFTを書くことができます7

def fft_my(x):
    # https://pythonnumericalmethods.studentorg.berkeley.edu/notebooks/chapter24.03-Fast-Fourier-Transform.html
    """
    A recursive implementation of
    the 1D Cooley-Tukey FFT, the
    input should have a length of
    power of 2.
    """
    N = len(x)

    if N == 1:
        return x
    else:
        X_even = fft_my(x[::2])
        X_odd = fft_my(x[1::2])
        factor = np.exp(-2j * np.pi * np.arange(N) / N)

        X = np.concatenate([X_even + factor[: int(N / 2)] * X_odd, X_even + factor[int(N / 2) :] * X_odd])
        return X


def ifft_my(x):
    # https://pythonnumericalmethods.studentorg.berkeley.edu/notebooks/chapter24.03-Fast-Fourier-Transform.html
    """
    A recursive implementation of
    the 1D Cooley-Tukey FFT, the
    input should have a length of
    power of 2.
    """
    N = len(x)

    if N == 1:
        return x
    else:
        X_even = ifft_my(x[::2])
        X_odd = ifft_my(x[1::2])
        factor = np.exp(2j * np.pi * np.arange(N) / N)

        X = np.concatenate([X_even + factor[: int(N / 2)] * X_odd, X_even + factor[int(N / 2) :] * X_odd])
        return X

6. 参考文献 #