Juliaで行列計算を高速化!LinearAlgebra.BLAS.gemm() の使い方と最適化

2025-05-27

LinearAlgebra.BLAS.gemm() は、Juliaの標準ライブラリである LinearAlgebra モジュールに含まれる、BLAS (Basic Linear Algebra Subprograms) の gemm 関数を直接呼び出すための関数です。BLASは、基本的なベクトルと行列の演算を効率的に行うための標準的なインターフェースを提供するライブラリ群です。gemm は、General Matrix-Matrix Multiplication の略で、一般的な行列同士の積を計算するために使用されます。

具体的には、LinearAlgebra.BLAS.gemm() は、以下の行列演算を実行します。

C=α⋅op(A)⋅op(B)+β⋅C

ここで、

  • op(X) は、X 自身であるか、その転置 (XT)、または共役転置 (XH) のいずれかを表します。
  • α, β はスカラー値です。
  • A, B, C は行列です。

LinearAlgebra.BLAS.gemm() 関数の基本的な呼び出し方は以下のようになります。

LinearAlgebra.BLAS.gemm(transA::Char, transB::Char, alpha::Number, A::AbstractMatrix, B::AbstractMatrix, beta::Number, C::AbstractMatrix)

それぞれの引数の意味は以下の通りです。

  • C::AbstractMatrix: 結果を格納する行列 C です。この行列は、演算結果で上書きされます。必要に応じて、初期値を設定しておくことができます。
  • beta::Number: スカラー β の値です。結果の行列 C にこの値が掛けられます。
  • B::AbstractMatrix: 右側の行列 B です。
  • A::AbstractMatrix: 左側の行列 A です。
  • alpha::Number: スカラー α の値です。積 op(A)⋅op(B) にこの値が掛けられます。
  • transB::Char: 行列 B をそのまま使うか ('N' または 'n')、転置 ('T' または 't')、共役転置 ('C' または 'c') するかを指定する文字です。
  • transA::Char: 行列 A をそのまま使うか ('N' または 'n')、転置 ('T' または 't')、共役転置 ('C' または 'c') するかを指定する文字です。

この関数の重要な特徴と利点

  • インプレース演算
    結果を引数として渡された行列 C に直接書き込む(インプレース演算)ため、余分なメモリ割り当てを減らすことができます。
  • 低レベルアクセス
    LinearAlgebra.BLAS.gemm() は、BLASの関数に直接アクセスするため、より細かい制御が可能になります。
  • 高いパフォーマンス
    BLASは、ハードウェアに合わせて最適化された実装が提供されていることが多く、Juliaの標準的な行列の乗算 (* 演算子) よりも高速に動作する場合があります。特に大規模な行列の計算においてその差が顕著になります。
using LinearAlgebra

A = [1.0 2.0; 3.0 4.0]
B = [5.0 6.0; 7.0 8.0]
C = zeros(2, 2) # 結果を格納する行列を初期化

# C = 1.0 * A * B + 0.0 * C (つまり、C = A * B)
LinearAlgebra.BLAS.gemm('N', 'N', 1.0, A, B, 0.0, C)
println("C = A * B:\n", C)

# C = 2.0 * A' * B + 1.0 * C
LinearAlgebra.BLAS.gemm('T', 'N', 2.0, A, B, 1.0, C)
println("\nC = 2.0 * A' * B + 元のC:\n", C)


次元(サイズ)に関するエラー

  • トラブルシューティング
    • 行列 A と B の形状(size(A)size(B)) を確認し、内側の次元が一致しているか確認してください。
    • transAtransB'T''C' を指定している場合は、転置後の形状で内側の次元が一致するか確認してください。例えば、A が (m,k) の場合、'T' を指定すると (k,m) になります。
    • 意図した行列の積の順序と、gemm 関数の引数の順序が正しいか確認してください。
  • 原因
    行列 A の列数(内側の次元)と行列 B の行数(内側の次元)が一致していない場合に発生します。行列の積 A⋅B を計算するためには、A の列数と B の行数が同じである必要があります。また、転置や共役転置 ('T', 't', 'C', 'c') を指定した場合、それに応じて次元が変化するため、その点も考慮する必要があります。
  • エラー内容
    DimensionMismatch エラーが発生し、「matrix A has dimensions (m, k) but matrix B has dimensions (k', n)」のようなメッセージが表示される。

型に関するエラー

  • トラブルシューティング
    • 各引数の型を再確認してください。alphabeta は数値型(Number)、ABC は行列型(AbstractMatrix のサブタイプ、例えば Array{Float64, 2} など)である必要があります。
    • 必要に応じて、convert() 関数などを用いて型を明示的に変換してみてください。
  • 原因
    引数の型が gemm 関数が期待する型と一致していない場合に発生します。例えば、alphabeta に数値型以外のものを渡したり、行列 A, B, CAbstractMatrix のサブタイプでないものを渡したりした場合などです。
  • エラー内容
    MethodError が発生し、「no method matching gemm(...)」のようなメッセージが表示される。

transA、transB の指定ミス

  • トラブルシューティング
    • transAtransB に指定できるのは、'N' (または 'n')、'T' (または 't')、'C' (または 'c') のいずれかであることを再確認してください。大文字・小文字は区別されません。
    • 転置や共役転置が必要かどうか、意図した操作と一致しているかを確認してください。
  • 原因
    transAtransB に誤った文字(例えば 'N''T''C' 以外の文字)を指定した場合、予期しない動作を引き起こす可能性があります。
  • エラー内容
    計算結果が期待通りにならない。エラーメッセージは表示されないことが多い。

結果の格納に関するエラー

  • トラブルシューティング
    • α⋅op(A)⋅op(B) の結果の次元を計算し、C の次元がそれに一致しているか確認してください。例えば、A が (m,k)、B が (k,n) で transAtransB'N' の場合、C は (m,n) の次元を持つ必要があります。
    • C を適切な型と次元で初期化していることを確認してください。必要に応じて zeros()similar() 関数を利用してください。
  • 原因
    • C の次元が、α⋅op(A)⋅op(B) の結果の次元と一致していない場合。
    • C が適切に初期化されていない場合(特に β=0 の場合)。
  • エラー内容
    C の内容が期待通りに更新されない、またはエラーが発生する(稀)。

BLAS ライブラリの問題(稀)

  • トラブルシューティング
    • Julia を再起動してみる。
    • Julia のバージョンを更新してみる。
    • 異なる BLAS バックエンドを試してみる(Julia の設定によります)。例えば、OpenBLAS や MKL など。Julia のビルドオプションや環境変数で制御できる場合があります。
  • エラー内容
    非常に稀ですが、BLASライブラリ自体に問題がある場合、予期しないエラーが発生することがあります。
  • ドキュメントを参照する
    Julia の公式ドキュメントや LinearAlgebra.BLAS.gemm のヘルプ (? LinearAlgebra.BLAS.gemm) を参照すると、関数の詳細な仕様や引数の説明を確認できます。
  • 簡単な例で試す
    問題が複雑な場合に、小さな行列や簡単な計算で gemm 関数の動作を確認してみることで、理解を深めることができます。
  • 引数の型と形状を常に確認する
    size() 関数や typeof() 関数を使って、行列やスカラーの型と形状を調べることが有効です。
  • エラーメッセージをよく読む
    Julia のエラーメッセージは、問題の原因を特定するための重要な情報を含んでいます。


例1: 基本的な行列の積 (C=A⋅B)

using LinearAlgebra

# 行列 A と B を定義
A = [1.0 2.0; 3.0 4.0]
B = [5.0 6.0; 7.0 8.0]

# 結果を格納する行列 C を初期化 (A の行数 x B の列数)
C = zeros(size(A, 1), size(B, 2))

# gemm 関数を使って C = 1.0 * A * B + 0.0 * C を計算
LinearAlgebra.BLAS.gemm('N', 'N', 1.0, A, B, 0.0, C)

println("行列 A:\n", A)
println("\n行列 B:\n", B)
println("\nC = A * B:\n", C)

この例では、2x2 の行列 AB の積を計算しています。

  • 0.0 は、初期の行列 C に掛けられるスカラー β です。したがって、結果は C=A⋅B となります。
  • 1.0 は、積 A⋅B に掛けられるスカラー α です。
  • 'N' は、行列をそのまま(転置や共役転置なしに)使用することを意味します。

例2: 行列の転置との積 (C=AT⋅B)

using LinearAlgebra

# 行列 A と B を定義
A = [1.0 2.0; 3.0 4.0]
B = [5.0 6.0; 7.0 8.0]

# 結果を格納する行列 C を初期化 (A の列数 x B の列数)
C = zeros(size(A, 2), size(B, 2))

# gemm 関数を使って C = 1.0 * A' * B + 0.0 * C を計算
LinearAlgebra.BLAS.gemm('T', 'N', 1.0, A, B, 0.0, C)

println("行列 A:\n", A)
println("\n行列 B:\n", B)
println("\nC = A' * B:\n", C)

この例では、行列 A の転置 AT と行列 B の積を計算しています。

  • 'T' は、行列 A を転置して使用することを意味します。

例3: スカラー倍率の適用 (C=2.0⋅A⋅B)

using LinearAlgebra

# 行列 A と B を定義
A = [1.0 2.0; 3.0 4.0]
B = [5.0 6.0; 7.0 8.0]
C = zeros(2, 2)

# gemm 関数を使って C = 2.0 * A * B + 0.0 * C を計算
LinearAlgebra.BLAS.gemm('N', 'N', 2.0, A, B, 0.0, C)

println("行列 A:\n", A)
println("\n行列 B:\n", B)
println("\nC = 2.0 * A * B:\n", C)

この例では、行列の積 A⋅B にスカラー値 2.0 を掛けています。

例4: 結果の行列への加算 (C=A⋅B+Cinitial​)

using LinearAlgebra

# 行列 A と B を定義
A = [1.0 2.0; 3.0 4.0]
B = [5.0 6.0; 7.0 8.0]

# 結果を格納する行列 C を初期化 (初期値を持つ)
C = [0.5 0.5; 0.5 0.5]

# gemm 関数を使って C = 1.0 * A * B + 1.0 * C を計算
LinearAlgebra.BLAS.gemm('N', 'N', 1.0, A, B, 1.0, C)

println("行列 A:\n", A)
println("\n行列 B:\n", B)
println("\n初期の行列 C:\n", [0.5 0.5; 0.5 0.5])
println("\nC = A * B + 初期C:\n", C)

この例では、行列の積 A⋅B の結果に、初期値を持つ行列 C を加えています。

  • 1.0 は、初期の行列 C に掛けられるスカラー β です。したがって、結果は C=A⋅B+Cinitialとなります。
  • 1.0 は、積 A⋅B に掛けられるスカラー α です。
using LinearAlgebra

# 浮動小数点数と整数の行列を定義
A = [1.0 2.0; 3.0 4.0]
B = [5 6; 7 8]

# 結果を格納する浮動小数点数の行列 C を初期化
C = zeros(Float64, size(A, 1), size(B, 2))

# gemm 関数を使って計算 (型は自動的に適切に処理される)
LinearAlgebra.BLAS.gemm('N', 'N', 1.0, A, B, 0.0, C)

println("行列 A (Float64):\n", A)
println("\n行列 B (Int64):\n", B)
println("\nC = A * B (Float64):\n", C)


標準の行列乗算演算子 *

最も一般的で、多くの場合において推奨されるのは、Julia の標準的な行列乗算演算子 * を使用する方法です。

A = [1.0 2.0; 3.0 4.0]
B = [5.0 6.0; 7.0 8.0]

C = A * B

println("C = A * B:\n", C)
  • 注意点
    • gemm のように、α や β を直接指定して結果を既存の行列に加算するような操作は、追加の演算が必要になります(例: C .= alpha .* (A * B) .+ beta .* C).
  • 利点
    • 簡潔で直感的
      数学の表記に近い形で行列の積を記述できます。
    • 高レベルな抽象化
      転置や共役転置も A'A'' のように簡潔に記述できます。
    • 自動的な最適化
      Julia は内部で、行列のサイズや型に応じて最適な計算方法を選択します。多くの場合、BLAS の gemm 関数が効率的に利用されます。

LinearAlgebra.mul! (インプレース乗算)

結果を新しい配列に割り当てるのではなく、既存の配列に直接書き込むインプレース演算を行うための関数です。メモリ割り当てを減らしたい場合に有効です。

using LinearAlgebra

A = [1.0 2.0; 3.0 4.0]
B = [5.0 6.0; 7.0 8.0]
C = zeros(2, 2)

mul!(C, A, B) # C = A * B (結果は C に格納される)
println("C after mul!(C, A, B):\n", C)

# 転置との積も可能
D = zeros(2, 2)
mul!(D, transpose(A), B) # D = A' * B
println("\nD after mul!(D, transpose(A), B):\n", D)
  • 注意点
    • 結果を格納する行列を事前に用意する必要があります。
    • gemm の α は暗黙的に 1.0 となります。α=1.0 の場合は、別途スカラー倍の操作が必要です。
    • β=0 のような加算操作は、mul! 単独では行えません。
  • 利点
    • メモリ効率が良い
      大規模な行列演算で特に有効です。
    • パフォーマンスの向上
      不要なメモリ割り当てとガベージコレクションを削減できる可能性があります。

ブロードキャスティング (.) を利用した要素ごとの演算の組み合わせ

行列の積を直接行うわけではありませんが、要素ごとの演算と組み合わせることで、gemm のような効果を得ることも可能です(ただし、効率は一般的に劣ります)。

A = [1.0 2.0; 3.0 4.0]
B = [5.0 6.0; 7.0 8.0]
C = zeros(2, 2)

for i in 1:size(A, 1)
    for j in 1:size(B, 2)
        C[i, j] = sum(A[i, k] * B[k, j] for k in 1:size(A, 2))
    end
end

println("C calculated using broadcasting (loop):\n", C)
  • 注意点
    • 標準の * 演算子や gemm に比べて、一般的にパフォーマンスが劣ります。大規模な行列演算には不向きです。
  • 利点
    • 行列積の基本的な仕組みを理解するのに役立ちます。
    • より複雑な演算を要素ごとに行う場合に柔軟性があります。

特殊な構造を持つ行列に対する専用の関数

行列が対称行列、エルミート行列、三角行列などの特殊な構造を持つ場合、LinearAlgebra モジュールには、それらの構造を活かしたより効率的な演算を行うための専用の関数が用意されています。例えば、SymMatrix 型や Hermitian 型などを使用し、それらに特化した乗算関数を利用できます。

using LinearAlgebra

A_sym = Symmetric([1.0 2.0; 2.0 3.0])
B = [4.0 5.0; 6.0 7.0]
C = A_sym * B

println("C = Symmetric(A) * B:\n", C)
  • 注意点
    • 適用できるのは、特定の構造を持つ行列のみです。
  • 利点
    • 特殊な構造を利用することで、計算量やメモリ使用量を削減できる場合があります。

並列計算のためのライブラリ (Threads.@threads, Distributed)

大規模な行列演算の場合、並列計算を利用することで処理時間を大幅に短縮できます。Julia の標準機能や、Threads モジュール、Distributed モジュールなどを活用できます。

using LinearAlgebra

A = rand(1000, 1000)
B = rand(1000, 1000)
C = zeros(1000, 1000)

Threads.@threads for i in 1:size(A, 1)
    for j in 1:size(B, 2)
        C[i, j] = sum(A[i, k] * B[k, j] for k in 1:size(A, 2))
    end
end

println("C calculated using Threads.@threads (first few elements):\n", C[1:5, 1:5])
  • 注意点
    • 並列化のオーバーヘッドが発生する場合があります。
    • データの共有や競合に注意する必要があります。
  • 利点
    • 大規模な計算を高速化できます。

LinearAlgebra.BLAS.gemm() は、低レベルで高性能な行列積計算のための強力なツールですが、多くの場合、より高レベルな * 演算子やインプレース演算の mul! 関数で十分かつ簡潔に目的を達成できます。行列の構造が特殊な場合や、並列計算を行いたい場合には、それぞれの状況に適した代替手段を検討すると良いでしょう。