【CuPy性能比較編 第5回】ベンチマーク対決!NumPy vs CuPy - 線形代数・ソート・FFTの速度を徹底比較 🚀

「行列の掛け算みたいな、もっと複雑な計算だと速度はどうなるの?」
「SVD(特異値分解)やFFT(高速フーリエ変換)もGPUで速くなるって本当?」
「どういう種類の計算が、特にGPUでの高速化に向いているんだろう?」

こんにちは! CuPy探検隊、隊長のPythonistaです! 前回の性能比較編 第1部では、基本的な要素ごとの演算でNumPyとCuPyの性能を比較し、配列サイズとデータ転送コストの重要性を学びましたね。CuPyが真価を発揮するのは、大規模なデータセットであることが見えてきました。

シリーズ第5回、そして性能比較編の第2部となる今回は、いよいよGPUコンピューティングが得意とする、より実践的で計算負荷の高い処理に焦点を当てます! 具体的には、

  • ディープラーニングや科学技術計算の心臓部である行列積
  • より高度な線形代数演算(例: 特異値分解)
  • データ処理の定番であるソート
  • 信号処理や画像解析で使われる高速フーリエ変換 (FFT)

といった、より複雑な操作でベンチマーク対決を行います。前回の基本演算以上にドラマチックな性能差を目の当たりにし、GPUがなぜこれほどまでに注目されるのか、その理由を深く理解していきましょう!


1. ベンチマークの準備(おさらいと今回の工夫)

今回も、公正な比較を行うために、前回確立したベンチマークの「お作法」に従います。

  • 環境の明記: 性能はハードウェア・ソフトウェア環境に大きく依存します。(皆さんもご自身の環境でぜひお試しください!)
  • ウォームアップ: 計測前に必ずGPUのウォームアップ処理を行います。
  • 正確な時間計測: GPU上の純粋な計算時間はcp.cuda.Event()を使って精密に測定します。

今回のベンチマークでは、2次元配列(行列)を扱うことが多くなるため、それに合わせてベンチマーク用の関数を少し調整します。

import numpy as np
import cupy as cp
import time
import matplotlib.pyplot as plt

# ベンチマーク関数 (2つの配列を入力とする演算にも対応)
def benchmark_operation_2d(op_name, np_op, cp_op, sizes):
    np_times = []
    cp_compute_times = []
    # 今回は計算時間のみに焦点を当てるため、トータル時間は省略
    
    print(f"\n--- {op_name} のベンチマーク開始 ---")
    
    # ウォームアップ
    dummy_np_arr = np.random.rand(10, 10).astype(np.float32)
    dummy_cp_arr = cp.asarray(dummy_np_arr)
    _ = cp_op(dummy_cp_arr, dummy_cp_arr) # 2引数でも動くように
    cp.cuda.Stream.null.synchronize()
    print("ウォームアップ完了。")
    
    for n in sizes:
        print(f"配列サイズ: {n}x{n}")
        # 正方行列を作成
        np_arr1 = np.random.rand(n, n).astype(np.float32)
        np_arr2 = np.random.rand(n, n).astype(np.float32)
        
        # NumPy (CPU) time
        start_cpu = time.time()
        _ = np_op(np_arr1, np_arr2)
        np_times.append(time.time() - start_cpu)
        
        # CuPy (GPU) compute time
        cp_arr1 = cp.asarray(np_arr1)
        cp_arr2 = cp.asarray(np_arr2)
        
        start_compute = cp.cuda.Event()
        end_compute = cp.cuda.Event()
        start_compute.record()
        _ = cp_op(cp_arr1, cp_arr2)
        end_compute.record()
        end_compute.synchronize()
        cp_compute_times.append(cp.cuda.get_elapsed_time(start_compute, end_compute) / 1000.0)
        
    return np_times, cp_compute_times

(このベンチマーク関数は、演算が1つの引数を取る場合にも少し工夫すれば再利用できます。)


2. ベンチマーク対決①:行列積 (@ 演算子)

行列積は、2つの行列から新しい行列を生成する演算で、ディープラーニングにおけるニューラルネットワークの計算や、3Dグラフィックスの座標変換など、非常に多くの分野で使われる計算の塊です。多数の掛け算と足し算から成り立っており、並列化との相性が抜群です。

# 行列積のベンチマーク
matrix_sizes = [100, 200, 500, 1000, 2000, 4000]
np_dot_times, cp_dot_times = benchmark_operation_2d(
    "Matrix Multiplication",
    lambda x, y: x @ y,
    lambda x, y: x @ y,
    matrix_sizes
)

# 結果をプロット
plt.figure(figsize=(10, 6))
plt.plot(matrix_sizes, np_dot_times, marker='o', label='NumPy CPU Time')
plt.plot(matrix_sizes, cp_dot_times, marker='s', label='CuPy GPU Compute Time')
plt.xscale('log')
plt.yscale('log')
plt.title('Performance Comparison: Matrix Multiplication')
plt.xlabel('Matrix Size N (for N x N matrix)')
plt.ylabel('Time (seconds)')
plt.legend()
plt.grid(True, which="both", ls="--")
plt.show()


結果はいかがでしょうか?おそらく、配列サイズが大きくなるにつれて、NumPyとCuPyの差が対数グラフ上でも劇的に開いていくのが分かるはずです。行列積は、GPUが得意とする「単純な計算を大量に、同時に行う」という特性に完璧にマッチしているため、数十倍から数百倍の速度向上が見られることも珍しくありません。


3. ベンチマーク対決②:高度な線形代数演算 (linalg)

行列積だけでなく、より複雑な線形代数のアルゴリズムもGPUで高速化できます。ここでは代表例として、データの次元削減や主成分分析などに応用される特異値分解(SVD)の性能を比較してみましょう。

# SVDのベンチマーク
# benchmark_operation_2dを少し変更して1引数に対応させます
# (ここでは簡単のため、コードの主要部分を再掲します)
np_svd_times = []
cp_svd_times = []
linalg_sizes = [100, 200, 500, 1000, 2000] # SVDは計算が重いのでサイズを調整

for n in linalg_sizes:
    print(f"SVD - Testing array size: {n}x{n}")
    np_arr = np.random.rand(n, n).astype(np.float32)
    # NumPy
    start_cpu = time.time()
    _ = np.linalg.svd(np_arr)
    np_svd_times.append(time.time() - start_cpu)
    # CuPy
    cp_arr = cp.asarray(np_arr)
    start_gpu = cp.cuda.Event()
    end_gpu = cp.cuda.Event()
    start_gpu.record()
    _ = cp.linalg.svd(cp_arr)
    end_gpu.record()
    end_gpu.synchronize()
    cp_svd_times.append(cp.cuda.get_elapsed_time(start_gpu, end_gpu) / 1000.0)

# 結果をプロット
plt.figure(figsize=(10, 6))
plt.plot(linalg_sizes, np_svd_times, marker='o', label='NumPy CPU Time (SVD)')
plt.plot(linalg_sizes, cp_svd_times, marker='s', label='CuPy GPU Compute Time (SVD)')
plt.xscale('log')
plt.yscale('log')
plt.title('Performance Comparison: Singular Value Decomposition (SVD)')
plt.xlabel('Matrix Size N (for N x N matrix)')
plt.ylabel('Time (seconds)')
plt.legend()
plt.grid(True, which="both", ls="--")
plt.show()


SVDのような複雑なアルゴリズムも、内部的には多数の行列演算などの並列化可能な処理で構成されているため、GPUによる高速化の恩恵を大きく受けられます。


4. ベンチマーク対決③:ソートと高速フーリエ変換 (FFT)

線形代数以外にも、GPUが得意とする処理はたくさんあります。

  • ソート (cp.sort): データの並び替えは一見逐次的な処理に見えますが、高度な並列アルゴリズムが存在し、GPUで高速に実行できます。
  • 高速フーリエ変換 (cp.fft.fft): 信号処理や画像解析で使われるFFTは、その計算構造から並列化との相性が非常に良いことで知られています。

これらのベンチマークも、上記SVDの例と同様のコードで、対象の関数をnp.sort/cp.sortnp.fft.fft/cp.fft.fftに置き換えることで簡単に試すことができます。(コードは省略しますが、ぜひ試してみてください!)


5. 考察:なぜこれらの処理はGPUで速くなるのか?

今回試した行列積、線形代数演算、ソート、FFTといった処理がGPUで劇的に高速化されるのは、それらのアルゴリズムが大量の独立した計算に分解できるからです。

例えば、行列積C = A @ B を考えてみましょう。結果の行列Cのある一つの要素 $C_{ij}$ を計算するには、行列Aのi行目と行列Bのj列目の要素ごとの積を計算し、それらを全て足し合わせる必要があります。重要なのは、$C_{ij}$の計算と、$C_{kl}$(別の要素)の計算は、互いに全く依存せず、独立して行えるという点です。

GPUは何千もの小さなコアを持っています。これらのコアに「君は$C_{11}$を計算して」「君は$C_{12}$を計算して」「君は$C_{13}$を…」というように、大量の独立した計算を一度に割り当てることで、CPUが一つ一つ(あるいは数個ずつ)計算するよりも遥かに早く全体の計算を終えることができるのです。

SVDやFFTなども、内部的にはこのような並列化可能な行列演算やベクトル演算の膨大な組み合わせで構成されているため、GPUのアーキテクチャがその性能を最大限に引き出せるのです。


まとめ:計算集約的なタスクはGPUの独擅場!

今回は、性能比較編の第2部として、より実践的で計算負荷の高い処理におけるNumPyとCuPyの性能を比較しました。

  • 行列積のような高密度な線形代数演算は、GPUで最も高速化の恩恵を受けられる処理の一つである。
  • 特異値分解(SVD)高速フーリエ変換(FFT)ソートといった、内部的に高度な並列アルゴリズムで実装されている処理も、GPU上で非常に高速に動作する。
  • これらの処理が高速な理由は、GPUが持つ膨大な数のコアによる大規模な並列処理能力にある。

もしあなたのプログラムに、今回紹介したような計算集約的な処理が含まれており、かつ扱うデータサイズが大きい場合、CuPyを導入することで劇的なパフォーマンス向上が見込めるでしょう。

さて、これでCuPyの性能比較も一区切りです。次回はいよいよシリーズ最終回!これまでの結果を元に、どのような場面でNumPyを使い、どのような場面でCuPyに切り替えるべきか、その実践的な使い分けガイドをお届けします。お楽しみに!

【番外編】NumpyとCupyベンチマーク用コード

その他の投稿はこちら

コメント

このブログの人気の投稿

タイトルまとめ

これで迷わない!WindowsでPython環境構築する一番やさしい方法 #0

【Python標準ライブラリ完結!】11の冒険をありがとう!君のPython力が飛躍する「次の一歩」とは? 🚀