13
16

次世代の高速計算。CPU NumPy のコードを GPU CuPy コードに自動変換。

Last updated at Posted at 2024-08-14

f4467e63-1d39-470a-8701-f21df769fbbc.png

ショートストーリー: 「コードの彼方」

東京の繁忙な街並みを見下ろす高層ビルの一室で、若きプログラマ、翔太が熱心に作業していた。彼のモニターには、NumPyで書かれたコードが表示されている。スクリーンに映し出される数式とデータは、彼の心の中で複雑なパズルのように組み合わさっている。

翔太は、日々の仕事に加えて、自身のプロジェクトにも取り組んでいた。それは、計算処理を高速化するためにNumPyで書かれた数値計算コードをCuPyに変換するというものだ。彼の目標は、GPUの力を借りて計算を爆速で処理することだった。夜も更け、東京の街はネオンの光に包まれていたが、翔太はキーボードに向かい続けた。

「ループを含むコードが多いなぁ…。これをCuPyでどのように変換し、最適化すれば良いのか…。」翔太はつぶやいた。彼のモニターには、次のようなNumPyのコードが表示されている。


import numpy as np
a = np.arange(10)
result = np.zeros_like(a)
for i in range(len(a)):
    result[i] = a[i] * 2

翔太はこのコードの処理を、GPUの力を借りて効率よく実行できるように、CuPy用に変換しなければならない。彼は手を動かしながら、頭の中でその変換過程を思い描いていた。

変換が完了すると、次のようなCuPyコードが生まれた。


import cupy as cp
a = cp.arange(10)
result = cp.zeros_like(a)
for i in range(len(a)):
    result[i] = a[i] * 2

「これで、計算がどれだけ速くなるか…。」翔太は期待に胸を膨らませていた。彼は変換後のCuPyコードを実行し、計算結果を確認することにした。モニターに映し出された結果は、彼の努力が実を結んでいることを示していた。

夜の静けさの中で、翔太は次のプロジェクトに思いを巡らせながら、コードの最適化に取り組んでいた。東京の高層ビルの一室から、彼の計算処理は新たな境地へと踏み出し、次第に世界へと広がっていった。

翔太のプログラムが生成する計算結果は、彼自身の成長を反映しているようだった。数式の中に秘められた未来の可能性を感じながら、彼はまた一歩、デジタルの世界を探索するのだった。

NumPyコードを文字列として受け取り、その中のNumPy関数をCuPy対応の関数に置き換えることで、自動的にCuPyコードに変換します。

コードの説明
convert_numpy_to_cupy_code() 関数は、NumPyのコードをCuPyコードに変換します。この例では、すべての np. を cp. に置き換えます。
run_code_and_get_output() 関数は、コード文字列を実行し、変数 result の値を取得します。この変数が出力の基準となります。
サンプルのNumPyコードをCuPyコードに変換し、両方のコードを実行してその出力を表示します。
このコードを実行すると、NumPyコードがCuPyコードに変換され、両方のコードが実行され、その結果が表示されます。この方法なら、汎用的にNumPyコードをCuPyコードに変換し、GPUでの実行が可能になります。

コードの実行結果。 準備したNumPyサンプルコードの説明

サンプルコード1: 1次元配列 a と b を足し合わせます。
サンプルコード2: 2つの行列のドット積を計算します。
サンプルコード3: 0から2πまでの区間を100分割した配列 a に対して、sin関数を適用します。
サンプルコード4: 3x3のランダムな行列 a の逆行列を計算します。
サンプルコード5: 配列 a の累積和を計算します。
このコードを実行すると、各NumPyコードがCuPyコードに変換され、NumPyとCuPyの両方で計算結果が表示されます。

サンプルNumPyコード 1:

a = np.array([1, 2, 3, 4])
b = np.array([10, 20, 30, 40])
result = a + b

NumPy出力: [11 22 33 44]

サンプルCuPyコード 1:

a = cp.array([1, 2, 3, 4])
b = cp.array([10, 20, 30, 40])
result = a + b

CuPy出力: [11 22 33 44]

サンプルNumPyコード 2:

a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
result = np.dot(a, b)

NumPy出力: [[19 22]
[43 50]]

サンプルCuPyコード 2:

a = cp.array([[1, 2], [3, 4]])
b = cp.array([[5, 6], [7, 8]])
result = cp.dot(a, b)

CuPy出力: [[19 22]
[43 50]]

サンプルNumPyコード 3:

a = np.linspace(0, 2*np.pi, 100)
result = np.sin(a)

NumPy出力: [ 0.00000000e+00 6.34239197e-02 1.26592454e-01 1.89251244e-01
2.51147987e-01 3.12033446e-01 3.71662456e-01 4.29794912e-01
4.86196736e-01 5.40640817e-01 5.92907929e-01 6.42787610e-01
6.90079011e-01 7.34591709e-01 7.76146464e-01 8.14575952e-01
8.49725430e-01 8.81453363e-01 9.09631995e-01 9.34147860e-01
9.54902241e-01 9.71811568e-01 9.84807753e-01 9.93838464e-01
9.98867339e-01 9.99874128e-01 9.96854776e-01 9.89821442e-01
9.78802446e-01 9.63842159e-01 9.45000819e-01 9.22354294e-01
8.95993774e-01 8.66025404e-01 8.32569855e-01 7.95761841e-01
7.55749574e-01 7.12694171e-01 6.66769001e-01 6.18158986e-01
5.67059864e-01 5.13677392e-01 4.58226522e-01 4.00930535e-01
3.42020143e-01 2.81732557e-01 2.20310533e-01 1.58001396e-01
9.50560433e-02 3.17279335e-02 -3.17279335e-02 -9.50560433e-02
-1.58001396e-01 -2.20310533e-01 -2.81732557e-01 -3.42020143e-01
-4.00930535e-01 -4.58226522e-01 -5.13677392e-01 -5.67059864e-01
-6.18158986e-01 -6.66769001e-01 -7.12694171e-01 -7.55749574e-01
-7.95761841e-01 -8.32569855e-01 -8.66025404e-01 -8.95993774e-01
-9.22354294e-01 -9.45000819e-01 -9.63842159e-01 -9.78802446e-01
-9.89821442e-01 -9.96854776e-01 -9.99874128e-01 -9.98867339e-01
-9.93838464e-01 -9.84807753e-01 -9.71811568e-01 -9.54902241e-01
-9.34147860e-01 -9.09631995e-01 -8.81453363e-01 -8.49725430e-01
-8.14575952e-01 -7.76146464e-01 -7.34591709e-01 -6.90079011e-01
-6.42787610e-01 -5.92907929e-01 -5.40640817e-01 -4.86196736e-01
-4.29794912e-01 -3.71662456e-01 -3.12033446e-01 -2.51147987e-01
-1.89251244e-01 -1.26592454e-01 -6.34239197e-02 -2.44929360e-16]

サンプルCuPyコード 3:

a = cp.linspace(0, 2*cp.pi, 100)
result = cp.sin(a)

CuPy出力: [ 0.00000000e+00 6.34239197e-02 1.26592454e-01 1.89251244e-01
2.51147987e-01 3.12033446e-01 3.71662456e-01 4.29794912e-01
4.86196736e-01 5.40640817e-01 5.92907929e-01 6.42787610e-01
6.90079011e-01 7.34591709e-01 7.76146464e-01 8.14575952e-01
8.49725430e-01 8.81453363e-01 9.09631995e-01 9.34147860e-01
9.54902241e-01 9.71811568e-01 9.84807753e-01 9.93838464e-01
9.98867339e-01 9.99874128e-01 9.96854776e-01 9.89821442e-01
9.78802446e-01 9.63842159e-01 9.45000819e-01 9.22354294e-01
8.95993774e-01 8.66025404e-01 8.32569855e-01 7.95761841e-01
7.55749574e-01 7.12694171e-01 6.66769001e-01 6.18158986e-01
5.67059864e-01 5.13677392e-01 4.58226522e-01 4.00930535e-01
3.42020143e-01 2.81732557e-01 2.20310533e-01 1.58001396e-01
9.50560433e-02 3.17279335e-02 -3.17279335e-02 -9.50560433e-02
-1.58001396e-01 -2.20310533e-01 -2.81732557e-01 -3.42020143e-01
-4.00930535e-01 -4.58226522e-01 -5.13677392e-01 -5.67059864e-01
-6.18158986e-01 -6.66769001e-01 -7.12694171e-01 -7.55749574e-01
-7.95761841e-01 -8.32569855e-01 -8.66025404e-01 -8.95993774e-01
-9.22354294e-01 -9.45000819e-01 -9.63842159e-01 -9.78802446e-01
-9.89821442e-01 -9.96854776e-01 -9.99874128e-01 -9.98867339e-01
-9.93838464e-01 -9.84807753e-01 -9.71811568e-01 -9.54902241e-01
-9.34147860e-01 -9.09631995e-01 -8.81453363e-01 -8.49725430e-01
-8.14575952e-01 -7.76146464e-01 -7.34591709e-01 -6.90079011e-01
-6.42787610e-01 -5.92907929e-01 -5.40640817e-01 -4.86196736e-01
-4.29794912e-01 -3.71662456e-01 -3.12033446e-01 -2.51147987e-01
-1.89251244e-01 -1.26592454e-01 -6.34239197e-02 -2.44929360e-16]

サンプルNumPyコード 4:

a = np.random.random((3, 3))
result = np.linalg.inv(a)

NumPy出力: [[ 2.88040223 0.01677109 -1.48851269]
[-1.00439963 1.71686048 0.17754098]
[-2.51106643 -0.30921126 2.38082397]]

サンプルCuPyコード 4:

a = cp.random.random((3, 3))
result = cp.linalg.inv(a)

CuPy出力: [[ 2.23042299 0.65155587 -0.97010415]
[ 1.70116735 -0.72564488 0.91349467]
[-4.62024927 1.23513862 0.8480497 ]]

サンプルNumPyコード 5:

a = np.array([1, 2, 3, 4, 5])
result = np.cumsum(a)

NumPy出力: [ 1 3 6 10 15]

サンプルCuPyコード 5:

a = cp.array([1, 2, 3, 4, 5])
result = cp.cumsum(a)

CuPy出力: [ 1 3 6 10 15]

import numpy as np
import cupy as cp

def convert_numpy_to_cupy_code(numpy_code):
    # NumPy -> CuPy変換用のマッピング
    numpy_to_cupy = {
        "np.": "cp."
    }
    
    # NumPyコードをCuPyコードに置換
    cupy_code = numpy_code
    for np_func, cp_func in numpy_to_cupy.items():
        cupy_code = cupy_code.replace(np_func, cp_func)
    
    return cupy_code

def run_code_and_get_output(code_str, use_cupy=False):
    # グローバル名前空間に変数を保持するための辞書
    local_vars = {}
    
    # コードを実行
    exec(code_str, globals(), local_vars)
    
    # 出力変数 'result' を取得
    result = local_vars.get('result', None)
    
    return result

# サンプルNumPyコード1
numpy_code_1 = """
a = np.array([1, 2, 3, 4])
b = np.array([10, 20, 30, 40])
result = a + b
"""

# サンプルNumPyコード2
numpy_code_2 = """
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
result = np.dot(a, b)
"""

# サンプルNumPyコード3
numpy_code_3 = """
a = np.linspace(0, 2*np.pi, 100)
result = np.sin(a)
"""

# サンプルNumPyコード4
numpy_code_4 = """
a = np.random.random((3, 3))
result = np.linalg.inv(a)
"""

# サンプルNumPyコード5
numpy_code_5 = """
a = np.array([1, 2, 3, 4, 5])
result = np.cumsum(a)
"""

# サンプルコードリスト
numpy_codes = [numpy_code_1, numpy_code_2, numpy_code_3, numpy_code_4, numpy_code_5]

# 各NumPyコードに対して変換と実行
for i, numpy_code in enumerate(numpy_codes, 1):
    print(f"サンプルNumPyコード {i}:")
    print(numpy_code)
    numpy_result = run_code_and_get_output(numpy_code)
    print(f"NumPy出力: {numpy_result}\n")
    
    # NumPyコードをCuPyコードに変換
    cupy_code = convert_numpy_to_cupy_code(numpy_code)
    
    print(f"サンプルCuPyコード {i}:")
    print(cupy_code)
    cupy_result = run_code_and_get_output(cupy_code)
    print(f"CuPy出力: {cupy_result}\n")

Loopを含むものも変換できるように拡張。

実行結果。

サンプルNumPyコード 1:

import numpy as np
a = np.arange(10)
result = np.zeros_like(a)
for i in range(len(a)):
result[i] = a[i] * 2

NumPy出力: [ 0 2 4 6 8 10 12 14 16 18]

サンプルCuPyコード 1:

import numpy as np
a = cp.arange(10)
result = cp.zeros_like(a)
for i in range(len(a)):
result[i] = a[i] * 2

CuPy出力: [ 0 2 4 6 8 10 12 14 16 18]

サンプルNumPyコード 2:

import numpy as np
a = np.random.random((3, 3))
result = np.zeros(a.shape)
for i in range(a.shape[0]):
for j in range(a.shape[1]):
result[i, j] = a[i, j] ** 2

NumPy出力: [[0.36568423 0.14840099 0.47208339]
[0.02818748 0.1091292 0.69040921]
[0.07521296 0.12878428 0.59117168]]

サンプルCuPyコード 2:

import numpy as np
a = cp.random.random((3, 3))
result = cp.zeros(a.shape)
for i in range(a.shape[0]):
for j in range(a.shape[1]):
result[i, j] = a[i, j] ** 2

CuPy出力: [[0.87610568 0.23063074 0.33611547]
[0.66264349 0.0203733 0.72235846]
[0.01486236 0.12217227 0.00266635]]

サンプルNumPyコード 3:

import numpy as np
a = np.linspace(0, 10, 100)
result = np.zeros_like(a)
for i in range(len(a)):
result[i] = np.sin(a[i])

NumPy出力: [ 0. 0.10083842 0.20064886 0.2984138 0.39313661 0.48385164
0.56963411 0.64960951 0.72296256 0.78894546 0.84688556 0.8961922
0.93636273 0.96698762 0.98775469 0.99845223 0.99897117 0.98930624
0.96955595 0.93992165 0.90070545 0.85230712 0.79522006 0.73002623
0.65739025 0.57805259 0.49282204 0.40256749 0.30820902 0.21070855
0.11106004 0.01027934 -0.09060615 -0.19056796 -0.28858706 -0.38366419
-0.47483011 -0.56115544 -0.64176014 -0.7158225 -0.7825875 -0.84137452
-0.89158426 -0.93270486 -0.96431712 -0.98609877 -0.99782778 -0.99938456
-0.99075324 -0.97202182 -0.94338126 -0.90512352 -0.85763861 -0.80141062
-0.73701276 -0.66510151 -0.58640998 -0.50174037 -0.41195583 -0.31797166
-0.22074597 -0.12126992 -0.0205576 0.0803643 0.18046693 0.27872982
0.37415123 0.46575841 0.55261747 0.63384295 0.7086068 0.77614685
0.83577457 0.8868821 0.92894843 0.96154471 0.98433866 0.99709789
0.99969234 0.99209556 0.97438499 0.94674118 0.90944594 0.86287948
0.8075165 0.74392141 0.6727425 0.59470541 0.51060568 0.42130064
0.32770071 0.23076008 0.13146699 0.03083368 -0.07011396 -0.17034683
-0.26884313 -0.36459873 -0.45663749 -0.54402111]

サンプルCuPyコード 3:

import numpy as np
a = cp.linspace(0, 10, 100)
result = cp.zeros_like(a)
for i in range(len(a)):
result[i] = cp.sin(a[i])

CuPy出力: [ 0. 0.10083842 0.20064886 0.2984138 0.39313661 0.48385164
0.56963411 0.64960951 0.72296256 0.78894546 0.84688556 0.8961922
0.93636273 0.96698762 0.98775469 0.99845223 0.99897117 0.98930624
0.96955595 0.93992165 0.90070545 0.85230712 0.79522006 0.73002623
0.65739025 0.57805259 0.49282204 0.40256749 0.30820902 0.21070855
0.11106004 0.01027934 -0.09060615 -0.19056796 -0.28858706 -0.38366419
-0.47483011 -0.56115544 -0.64176014 -0.7158225 -0.7825875 -0.84137452
-0.89158426 -0.93270486 -0.96431712 -0.98609877 -0.99782778 -0.99938456
-0.99075324 -0.97202182 -0.94338126 -0.90512352 -0.85763861 -0.80141062
-0.73701276 -0.66510151 -0.58640998 -0.50174037 -0.41195583 -0.31797166
-0.22074597 -0.12126992 -0.0205576 0.0803643 0.18046693 0.27872982
0.37415123 0.46575841 0.55261747 0.63384295 0.7086068 0.77614685
0.83577457 0.8868821 0.92894843 0.96154471 0.98433866 0.99709789
0.99969234 0.99209556 0.97438499 0.94674118 0.90944594 0.86287948
0.8075165 0.74392141 0.6727425 0.59470541 0.51060568 0.42130064
0.32770071 0.23076008 0.13146699 0.03083368 -0.07011396 -0.17034683
-0.26884313 -0.36459873 -0.45663749 -0.54402111]

サンプルNumPyコード 4:

import numpy as np
a = np.random.random(10)
result = np.zeros_like(a)
for i in range(len(a)):
result[i] = np.sqrt(a[i])

NumPy出力: [0.98220904 0.93947968 0.97913994 0.9304005 0.71093787 0.48606598
0.28792615 0.92034992 0.99204895 0.90116959]

サンプルCuPyコード 4:

import numpy as np
a = cp.random.random(10)
result = cp.zeros_like(a)
for i in range(len(a)):
result[i] = cp.sqrt(a[i])

CuPy出力: [0.98583283 0.63233212 0.3183179 0.92395057 0.84106222 0.54324482
0.60656986 0.73230696 0.86783054 0.73059482]

サンプルNumPyコード 5:

import numpy as np
a = np.arange(10)
result = np.zeros_like(a)
for i in range(len(a)):
result[i] = np.exp(a[i])

NumPy出力: [ 1 2 7 20 54 148 403 1096 2980 8103]

サンプルCuPyコード 5:

import numpy as np
a = cp.arange(10)
result = cp.zeros_like(a)
for i in range(len(a)):
result[i] = cp.exp(a[i])

CuPy出力: [ 1 2 7 20 54 148 403 1096 2980 8103]

コードの説明

convert_numpy_to_cupy_code 関数:

NumPyの関数やメソッドをCuPyの対応するものに変換します。
run_code_and_get_output 関数:

コードを実行し、結果を取得します。
サンプルNumPyコード:

ループを含む5つのNumPyコードを定義しています。
コードの実行:

各NumPyコードをCuPyコードに変換し、両方の結果を表示します。
このコードを実行することで、NumPyコードとそのCuPyコードが表示され、それぞれの出力結果が確認できます。

import numpy as np
import cupy as cp

def convert_numpy_to_cupy_code(numpy_code):
    # NumPy -> CuPy変換用のマッピング
    numpy_to_cupy = {
        "np.": "cp.",
        "np.zeros": "cp.zeros",
        "np.ones": "cp.ones",
        "np.random.random": "cp.random.random",
        "np.arange": "cp.arange",
        "np.sum": "cp.sum",
        "np.mean": "cp.mean",
        "np.dot": "cp.dot",
        "np.linalg.inv": "cp.linalg.inv",
        "np.linspace": "cp.linspace",
        "np.sin": "cp.sin",
        "np.cumsum": "cp.cumsum",
        "np.sqrt": "cp.sqrt",
        "np.exp": "cp.exp",
        "np.log": "cp.log",
    }
    
    # NumPyコードをCuPyコードに置換
    cupy_code = numpy_code
    for np_func, cp_func in numpy_to_cupy.items():
        cupy_code = cupy_code.replace(np_func, cp_func)
    
    return cupy_code

def run_code_and_get_output(code_str, use_cupy=False):
    # グローバル名前空間に変数を保持するための辞書
    local_vars = {}
    
    # コードを実行
    exec(code_str, globals(), local_vars)
    
    # 出力変数 'result' を取得
    result = local_vars.get('result', None)
    
    return result

# サンプルNumPyコード1: ループを含むコード
numpy_code_1 = """
import numpy as np
a = np.arange(10)
result = np.zeros_like(a)
for i in range(len(a)):
    result[i] = a[i] * 2
"""

# サンプルNumPyコード2: ループを含むコード
numpy_code_2 = """
import numpy as np
a = np.random.random((3, 3))
result = np.zeros(a.shape)
for i in range(a.shape[0]):
    for j in range(a.shape[1]):
        result[i, j] = a[i, j] ** 2
"""

# サンプルNumPyコード3: ループを含むコード
numpy_code_3 = """
import numpy as np
a = np.linspace(0, 10, 100)
result = np.zeros_like(a)
for i in range(len(a)):
    result[i] = np.sin(a[i])
"""

# サンプルNumPyコード4: ループを含むコード
numpy_code_4 = """
import numpy as np
a = np.random.random(10)
result = np.zeros_like(a)
for i in range(len(a)):
    result[i] = np.sqrt(a[i])
"""

# サンプルNumPyコード5: ループを含むコード
numpy_code_5 = """
import numpy as np
a = np.arange(10)
result = np.zeros_like(a)
for i in range(len(a)):
    result[i] = np.exp(a[i])
"""

# サンプルコードリスト
numpy_codes = [numpy_code_1, numpy_code_2, numpy_code_3, numpy_code_4, numpy_code_5]

# 各NumPyコードに対して変換と実行
for i, numpy_code in enumerate(numpy_codes, 1):
    print(f"サンプルNumPyコード {i}:")
    print(numpy_code)
    numpy_result = run_code_and_get_output(numpy_code)
    print(f"NumPy出力: {numpy_result}\n")
    
    # NumPyコードをCuPyコードに変換
    cupy_code = convert_numpy_to_cupy_code(numpy_code)
    
    print(f"サンプルCuPyコード {i}:")
    print(cupy_code)
    
    # CuPyコードを実行するためには、NumPyの部分をCuPyに変換し、実行環境にCuPyが必要です。
    # ここでは変換後のコードを実行し、出力を取得します。
    cupy_result = run_code_and_get_output(cupy_code)
    print(f"CuPy出力: {cupy_result}\n")

追記。

GPU CuPy コードに自動変換。はできたけどこれではループがあるので速くはなっていません。

このように配列操作として、初めて同時並列計算として実行されます。メッシュグリッドを使うことで、初めて同時並列計算として実行されます。
元のコード:


import cupy as cp

a = cp.array([1, 2])
result = cp.zeros(len(a))
for i in range(len(a)):
    result[i] = a[i] * 2

変換されたコード: 2基の演算ユニットで計算します。


import cupy as cp

a = cp.array([1, 2])
grid = cp.meshgrid(a, indexing='ij')
result = grid[0] * 2

処理結果:


[2. 4.]

処理するループ数: 2
元のコード:


import cupy as cp

a = cp.array([1, 2])
b = cp.array([3, 4])
result = cp.zeros((len(a), len(b)))
for i in range(len(a)):
    for j in range(len(b)):
        result[i, j] = a[i] * b[j]

変換されたコード: 4基の演算ユニットで計算します。


import cupy as cp

a = cp.array([1, 2])
b = cp.array([3, 4])
grid = cp.meshgrid(a, b, indexing='ij')
result = grid[0] * grid[1]

処理結果:


[[3. 4.]
 [6. 8.]]
13
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
16