Juliaで線形代数が遅い?BLASスレッド数の確認とトラブルシューティング徹底解説

2025-05-27

LinearAlgebra.BLAS.get_num_threads()とは

この関数は、Juliaが線形代数演算(行列の乗算、分解など)のために使用するBLAS (Basic Linear Algebra Subprograms) ライブラリが、現在何個のCPUスレッドを使用するように設定されているかを取得するためのものです。

BLASとは

BLASは、ベクトルと行列の基本的な線形代数操作(例えば、ベクトルとベクトルの内積、行列とベクトルの積、行列と行列の積など)を効率的に実行するための標準的なライブラリです。Juliaは通常、高速なオープンソースのBLASライブラリであるOpenBLASを使用しています(Intel製CPUの場合はMKLを使用することもできます)。これらのBLASライブラリは、マルチスレッドに対応しており、複数のCPUコアを利用して線形代数計算を高速化できます。

スレッド数について

  • 環境変数
    Juliaを起動する前に、OMP_NUM_THREADSOPENBLAS_NUM_THREADS などの環境変数を設定することでもBLASのスレッド数を制御できます。
  • 設定方法
    スレッド数を変更したい場合は、BLAS.set_num_threads(n) を使用します(n は設定したいスレッド数)。
  • 確認方法
    using LinearAlgebra を実行した後、BLAS.get_num_threads() を呼び出すことで、現在のBLASスレッド数を確認できます。
  • デフォルト
    Juliaはデフォルトで、お使いのマシンのプロセッサ数に応じてBLASのスレッド数を設定しようとします。

Juliaには、一般的な並列処理のための独自のマルチスレッド機能(Threads.@threads マクロなど)もあります。しかし、BLASのスレッドはこれとは独立して動作します。

  • このため、一般的には、Juliaのマルチスレッドを積極的に使う場合は、BLASのスレッド数を1に設定する(BLAS.set_num_threads(1))ことが推奨されることが多いです。ただし、この最適な設定は、使用しているBLASライブラリ(OpenBLASかMKLかなど)や具体的な計算内容によって異なります。
  • JuliaのマルチスレッドとBLASのマルチスレッドを両方使用する場合、コアの過剰使用(oversubscription)により、かえってパフォーマンスが低下する可能性があります。例えば、Juliaの各スレッドがそれぞれBLAS呼び出しを行い、そのBLAS呼び出し自体もマルチスレッドで動作する場合、CPUリソースが奪い合いになり、効率が悪くなることがあります。


LinearAlgebra.BLAS.get_num_threads()関数自体が直接エラーを発生させることは稀ですが、この関数が返す値が期待と異なる場合や、この関数に関連するBLASのスレッド設定が原因でパフォーマンス上の問題が発生することがよくあります。

UndefVarError: BLAS not defined または ERROR: LoadError: UndefVarError: LinearAlgebra not defined

エラーの原因
BLASモジュールやLinearAlgebraモジュールがロードされていないために発生します。JuliaはREPL起動時に自動的に多くの標準ライブラリをロードしますが、スクリプト内で使用する場合は明示的にロードする必要があります。

トラブルシューティング

  • スクリプトやREPLのセッションの冒頭で、以下のいずれかまたは両方を記述して、必要なモジュールをロードしてください。
    using LinearAlgebra
    # または
    using LinearAlgebra.BLAS
    
    using LinearAlgebraとすることで、BLASもアクセス可能になります。

期待されるスレッド数と異なる値が返される

問題の原因
BLAS.get_num_threads()が、ユーザーが意図した、またはシステムが提供するはずのコア数と異なる値を返すことがあります。これはいくつかの原因が考えられます。

  • BLAS.set_num_threads()の呼び出し忘れ
    スクリプト内でスレッド数を設定しようとしたが、その設定が実行される前にget_num_threads()を呼び出した場合。
  • システムのリソース認識
    Dockerコンテナ内や仮想環境で実行している場合、物理的なコア数とは異なるリソース制限が適用されている可能性があります。
  • BLASライブラリの特定
    JuliaがロードしているBLASライブラリ(OpenBLAS、MKL、Accelerateなど)によって、スレッド数の管理方法やデフォルトの挙動が異なります。
  • 環境変数の設定
    Julia起動前に設定された環境変数(OMP_NUM_THREADSOPENBLAS_NUM_THREADSMKL_NUM_THREADSなど)が、Julia内のデフォルト設定やBLAS.set_num_threads()よりも優先される場合があります。

トラブルシューティング

  • システムリソースの確認
    • コンテナや仮想環境を使用している場合は、その環境のリソース制限を確認してください。
  • 使用しているBLASライブラリの確認
    • using LinearAlgebraの後に、BLAS.vendor()を実行すると、JuliaがどのBLASライブラリを使用しているか(例: :openblas, :mkl, :apple)を確認できます。
    • 使用しているBLASライブラリのドキュメントを確認し、スレッド設定に関する推奨事項や特有の挙動を理解してください。
  • BLAS.set_num_threads()の確認
    • スクリプト内でBLAS.set_num_threads(n)を呼び出した後、BLAS.get_num_threads()を呼び出していることを確認してください。
    • 例:
      using LinearAlgebra
      BLAS.set_num_threads(4) # スレッド数を4に設定
      println(BLAS.get_num_threads()) # 4と表示されることを期待
      
  • 環境変数の確認と調整
    • ターミナルでJuliaを起動する前に、関連する環境変数が設定されていないか確認してください。例えば、Linux/macOSではecho $OMP_NUM_THREADS
    • もし意図しない値が設定されている場合は、設定を解除するか、適切な値に設定し直してからJuliaを起動してみてください。
    • 特定のBLASライブラリを使用している場合は、そのライブラリ特有の環境変数を確認してください。

線形代数演算のパフォーマンスが期待通りでない

問題の原因
get_num_threads()が返す値は正しいのに、大規模な行列演算が遅い場合、スレッド数の設定がパフォーマンスのボトルネックになっている可能性があります。

  • キャッシュ効率の悪さ
    • データアクセスパターンがキャッシュに優しくない場合、パフォーマンスが低下します。
  • メモリアクセスの問題
    • スレッド数が適切でも、メモリ帯域幅がボトルネックになっている場合があります(特に大規模な行列演算)。
  • スレッド数の不足
    • 利用可能なコア数を十分に活用できていない可能性があります。
  • スレッド数の過剰設定(Oversubscription)
    • BLASのスレッド数を物理コア数よりも多く設定している場合、コンテキストスイッチのオーバーヘッドが増加し、かえってパフォーマンスが低下することがあります。
    • Juliaの一般的なマルチスレッド(Threads.@threads)とBLASのマルチスレッドを同時に使用している場合、それぞれがCPUコアを奪い合い、コアの過剰使用を引き起こすことがあります。

トラブルシューティング

  • メモリとキャッシュの最適化
    • データ構造やアルゴリズムを見直し、メモリアクセスの局所性を高めることを検討してください。
    • 可能であれば、より多くのメモリ帯域幅を持つシステムを検討することも有効です。
  • プロファイリング
    • Juliaの組み込みプロファイラ(@profile)や、外部のプロファイリングツール(例えばPerf.jlなど)を使用して、計算時間のボトルネックがBLASの呼び出しにあるのか、それとも他の部分にあるのかを特定してください。
  • BLAS.set_num_threads(1)の検討
    • Threads.@threadsなど、Juliaのネイティブな並列処理機能を使用している場合は、BLASのスレッドを1に設定することで、オーバーサブスクリプションを避け、全体のパフォーマンスが向上する可能性があります。

特定のプラットフォームや環境での問題

問題の原因
OS(Windows, Linux, macOS)、Juliaのバージョン、使用しているBLASライブラリのバージョン、または特定のハードウェア構成(特に異なるCPUアーキテクチャ)によって、BLASのスレッド挙動が異なることがあります。

  • GitHub Issueの検索
    • JuliaのGitHubリポジトリや関連するBLASライブラリのIssueトラッカーで、同様の問題が報告されていないか検索してみてください。既知のバグであれば、解決策やワークアラウンドが見つかるかもしれません。
  • BLASライブラリの再インストール/変更
    • 稀に、JuliaにバンドルされているBLASライブラリに問題がある場合があります。特定のケースでは、MKLなどの別のBLASライブラリを手動で設定することで問題が解決することがあります。
  • Juliaのバージョンアップグレード
    • 古いJuliaのバージョンでは、BLASの連携やスレッド管理にバグが存在する可能性があります。最新の安定版Juliaにアップグレードすることを検討してください。


LinearAlgebra.BLAS.get_num_threads() は、現在Juliaが線形代数演算に使用しているBLASライブラリのスレッド数を取得するために使われます。これは、BLASのスレッド数を設定するBLAS.set_num_threads()と組み合わせて、パフォーマンスチューニングを行う際によく利用されます。

現在のBLASスレッド数の確認

最も基本的な使用例です。

using LinearAlgebra # LinearAlgebraモジュールをロード

# 現在のBLASスレッド数を取得して表示
current_blas_threads = BLAS.get_num_threads()
println("現在のBLASスレッド数: ", current_blas_threads)

# システムのCPUスレッド数を表示(参考)
println("システムのCPUスレッド数: ", Sys.CPU_THREADS)

出力例

現在のBLASスレッド数: 8
システムのCPUスレッド数: 16

(この例では、物理コア数が8、ハイパースレッディングで論理コア数が16のシステムを想定しています。BLASはデフォルトで物理コア数に近い値を設定することが多いです。)

BLASスレッド数の設定とパフォーマンス比較

BLASのスレッド数を変更し、それが線形代数演算のパフォーマンスにどう影響するかを確認する例です。大規模な行列乗算をベンチマークとして使用します。

using LinearAlgebra
using BenchmarkTools # ベンチマーク用のパッケージ

# 大きな行列を定義
N = 2000
A = rand(N, N)
B = rand(N, N)
C = zeros(N, N)

println("=== BLASスレッド数によるパフォーマンス比較 ===")

# --- ケース1: BLASスレッド数を1に設定 ---
BLAS.set_num_threads(1)
current_threads = BLAS.get_num_threads()
println("\nBLASスレッド数: ", current_threads)
println("行列乗算 (A * B):")
@btime $A * $B; # 行列乗算のベンチマーク($で補間し、グローバル変数を回避)

# --- ケース2: BLASスレッド数をシステムの最大論理コア数に設定 ---
# Sys.CPU_THREADS は論理コア数を返します。
# 物理コア数を設定したい場合は、環境変数などに応じて調整が必要です。
# 例: Intel CPUでハイパースレッディング有効なら、Sys.CPU_THREADS / 2 が物理コア数に近い
BLAS.set_num_threads(Sys.CPU_THREADS)
current_threads = BLAS.get_num_threads()
println("\nBLASスレッド数: ", current_threads)
println("行列乗算 (A * B):")
@btime $A * $B;

# --- ケース3: JuliaのネイティブスレッドとBLASスレッドの組み合わせ ---
# Juliaを `julia -t auto` などで起動していることを前提とします。
# この場合、BLASスレッドは1に設定することが推奨されることが多いです。
# JuliaのThreadsモジュールのスレッド数も確認できます。
using Base.Threads
if nthreads() > 1
    println("\nJuliaのネイティブスレッド数: ", nthreads())
    println("BLASスレッド数を1に設定し、Juliaの並列処理との干渉を避ける")
    BLAS.set_num_threads(1)
    current_threads = BLAS.get_num_threads()
    println("現在のBLASスレッド数: ", current_threads)

    # ここではJuliaのマルチスレッドを使った明示的な行列乗算は示しませんが、
    # 実際にはここでJuliaのスレッドを使った並列処理を行うと良いでしょう。
    # 例: 複数の小さな行列乗算を並列に実行する場合など
    println("行列乗算 (A * B) (Juliaスレッド併用想定):")
    @btime $A * $B;
else
    println("\nJuliaがシングルスレッドモードで起動されています。(`julia -t auto`などで起動してください)")
end

実行方法のヒント

  1. BenchmarkTools パッケージのインストール
    using Pkg
    Pkg.add("BenchmarkTools")
    
  2. Juliaの起動時にスレッド数を指定
    Juliaのネイティブスレッドを使用する場合は、Juliaを起動する際に -t オプションでスレッド数を指定します。
    • システムが利用可能な論理コア数を全て使用する場合: julia -t auto
    • 特定の数のスレッドを使用する場合(例: 4スレッド): julia -t 4
    • シングルスレッドで起動する場合: julia または julia -t 1

出力例(環境による)

=== BLASスレッド数によるパフォーマンス比較 ===

BLASスレッド数: 1
行列乗算 (A * B):
  759.030 ms (2 allocations: 30.52 MiB)

BLASスレッド数: 8
行列乗算 (A * B):
  105.123 ms (2 allocations: 30.52 MiB)

Juliaのネイティブスレッド数: 8
BLASスレッド数を1に設定し、Juliaの並列処理との干渉を避ける
現在のBLASスレッド数: 1
行列乗算 (A * B) (Juliaスレッド併用想定):
  760.500 ms (2 allocations: 30.52 MiB)

この出力例では、BLASスレッドを8に設定した場合が最も高速であり、BLASスレッドを1に設定した場合は大幅に遅くなっていることがわかります。これは、この行列乗算がBLASのマルチスレッド機能によって高速化されていることを示しています。

  • 最適なスレッド数
    最適なBLASスレッド数は、CPUアーキテクチャ、システムの物理コア数、使用するBLASライブラリ、そして実行する線形代数演算のサイズや種類によって大きく異なります。常にベンチマークを実行して最適な設定を見つけることが推奨されます。一般的に、BLASは大規模な行列演算でそのマルチスレッド能力を最大限に発揮します。
  • BLASのベンダー
    Juliaが使用するBLASライブラリ(OpenBLAS, MKL, Accelerateなど)によって、スレッド管理の挙動が異なります。BLAS.vendor()関数で、現在使用されているBLASを確認できます。
    using LinearAlgebra
    println("BLAS Vendor: ", BLAS.vendor())
    
  • オーバーサブスクリプション
    上記の例で、もしJuliaをjulia -t 8で起動し、さらにBLAS.set_num_threads(8)を設定した場合、合計で16個の論理スレッドが使用される可能性があります(BLASライブラリの実装による)。これは物理コア数を超過し、CPUの切り替えオーバーヘッドが増大してかえってパフォーマンスが低下する「オーバーサブスクリプション」を引き起こす可能性があります。


BLASのスレッド数を直接操作すること自体に代替手段は多くありませんが、BLASのスレッド数に影響を与える他の方法や、BLASのスレッド数を制御する意図の背景にある「線形代数計算の並列化」という目的に対する代替アプローチは存在します。

環境変数を介したBLASスレッド数の設定

BLAS.set_num_threads() は実行時にスレッド数を設定する関数ですが、Juliaを起動する前に環境変数を設定することでもBLASスレッド数を制御できます。これは、スクリプトの冒頭で毎回BLAS.set_num_threads()を記述する手間を省きたい場合や、システムレベルで設定を強制したい場合に便利です。

使用しているBLASライブラリによって環境変数の名前が異なります。

  • Intel MKL (MKL.jl パッケージ経由で使用する場合)
    MKL_NUM_THREADS または OMP_NUM_THREADS

    export MKL_NUM_THREADS=4
    julia
    
  • OpenBLAS (Juliaのデフォルト)
    OPENBLAS_NUM_THREADS または OMP_NUM_THREADSOPENBLAS_NUM_THREADSの方が優先度が高いことが多いです。)

    例 (Linux/macOS の Bash)

    export OPENBLAS_NUM_THREADS=4
    julia
    

    または

    export OMP_NUM_THREADS=4
    julia
    

注意点

  • 異なる種類のBLASライブラリが同じOMP_NUM_THREADSを共有する可能性があるため、複数のBLASライブラリを切り替えて使用する場合は注意が必要です。
  • 環境変数はJuliaのプロセス全体に影響を与えます。特定のスクリプトや関数だけで一時的にスレッド数を変更したい場合は、BLAS.set_num_threads()の方が適しています。

BLASスレッド数の設定とJuliaネイティブスレッドの協調

これは「代替手段」というよりは「BLASスレッド数制御のベストプラクティス」に近いですが、非常に重要です。JuliaにはBase.Threadsモジュールによる独自のマルチスレッド機能があります。

  • 推奨されるアプローチ

    • BLASのマルチスレッドを活用する場合
      Juliaをシングルスレッドで起動し (julia または julia -t 1)、BLASのスレッド数をシステムの物理コア数に合わせて設定します。この場合、Juliaのスクリプト内で明示的な @threads マクロなどは使用しません。
      using LinearAlgebra
      BLAS.set_num_threads(Sys.CPU_THREADS ÷ 2) # 物理コア数に設定 (ハイパースレッディングの場合)
      # あるいは、手動でコア数を指定
      # BLAS.set_num_threads(8) 
      
    • Juliaのネイティブスレッドを積極的に活用する場合
      Juliaをマルチスレッドで起動し (julia -t auto または julia -t N)、BLASのスレッド数を1に設定します (BLAS.set_num_threads(1))。これにより、Julia自身が並列処理のスケジューリングを制御し、BLASの呼び出しは各Juliaスレッド内でシングルスレッドで実行されます。
      # Juliaを `julia -t auto` などで起動していることを前提
      using LinearAlgebra
      using Base.Threads
      
      println("Juliaネイティブスレッド数: ", nthreads())
      BLAS.set_num_threads(1) # BLASスレッドを1に設定
      println("BLASスレッド数: ", BLAS.get_num_threads())
      
      # 例: Juliaスレッドを使った並列処理
      function my_parallel_computation(A, B)
          results = Vector{Matrix{Float64}}(undef, nthreads())
          @threads for i in 1:nthreads()
              # 各スレッドで個別の(シングルスレッドの)BLAS演算
              results[i] = A[i] * B[i] 
          end
          return results
      end
      
  • 問題点
    JuliaのネイティブスレッドとBLASのマルチスレッドを両方とも多くのスレッドに設定すると、CPUコアの「オーバーサブスクリプション」が発生し、パフォーマンスが低下する可能性があります。例えば、Juliaが8スレッドで起動し、その各JuliaスレッドがBLAS関数を呼び出し、BLASも8スレッドを使用しようとすると、合計で64スレッドが(論理的に)動作しようとし、これはシステムのリソースを圧倒します。

この協調アプローチは、アプリケーションの具体的なワークロードと、BLASが提供する最適化の粒度(非常に大きな行列演算に強い)とJuliaネイティブスレッドの柔軟性(より細かい粒度での並列化が可能)を考慮して選択されます。

特殊なBLASライブラリまたは最適化パッケージ

  • ThreadPinning.jl
    CPUコアへのスレッドの割り当て(ピンニング)を細かく制御するためのパッケージです。オーバーサブスクリプションの回避や、キャッシュ効率の最適化に役立ちます。BLASスレッドとJuliaスレッドのピンニングを調整する機能を提供します。

    using Pkg
    Pkg.add("ThreadPinning")
    
    using ThreadPinning
    using LinearAlgebra
    
    # JuliaスレッドとBLASスレッドの情報を表示(ピンニング推奨事項を含む)
    threadinfo(hints=true, blas=true)
    
    # 例: BLASスレッドを物理コアにピンニング
    # openblas_pinthreads(:cores) # OpenBLASの場合
    
  • MKL.jl
    Intel CPUを使用している場合、JuliaのデフォルトのOpenBLASではなく、Intel Math Kernel Library (MKL) を利用するMKL.jlパッケージを導入することで、パフォーマンスが向上する可能性があります。MKLは独自のスレッド管理を持ち、MKL.set_num_threads() のような関数を提供することもあります(ただし、BLAS.set_num_threads()もMKLに転送されることが多い)。

    using Pkg
    Pkg.add("MKL")
    
    using MKL # MKL.jlをロードすることでBLASバックエンドがMKLに切り替わる
    using LinearAlgebra
    
    # MKL独自のスレッド設定関数があれば利用(MKL.jlのドキュメントを確認)
    # MKL.set_num_threads(4) 
    
    println("現在のBLASスレッド数 (MKL): ", BLAS.get_num_threads())
    

線形代数演算がBLASの得意とする大規模な行列乗算や分解に限定されない場合、またはBLASのマルチスレッドと競合させたくない場合、Juliaネイティブの並列化機能を活用することが代替手段となります。

  • Distributed モジュール
    複数のプロセス(異なるCPUコア、さらには異なるマシン)にわたる並列処理には、Distributed モジュールが使われます。これは、メモリを共有しないため、BLASのスレッドとの競合を気にすることなく、各プロセスで独立したBLASスレッド設定を利用できます。ただし、プロセス間通信のオーバーヘッドが発生します。

    # コマンドラインで複数のプロセスを起動
    julia -p 4
    

    Julia REPL:

    using Distributed
    addprocs(3) # 現在のプロセスに3つのワーカープロセスを追加(合計4プロセス)
    
    @everywhere using LinearAlgebra # 各ワーカーにモジュールをロード
    
    # 各プロセスでBLASスレッド数を設定(例: 1)
    @everywhere BLAS.set_num_threads(1) 
    
    # 負荷の高い計算を分散
    @distributed for i in 1:10
        A = rand(1000, 1000)
        B = rand(1000, 1000)
        C = A * B # 各プロセス内でBLASがシングルスレッドで実行される
        println("Process $(myid()) completed iteration $i")
    end
    
  • @spawn / @sync
    より柔軟なタスク並列処理には、@spawn@sync を使用します。これにより、非同期にタスクを生成し、後で結果を同期させることができます。

    using Base.Threads
    
    function parallel_matrix_multiplications(matrices)
        results = Vector{Matrix{Float64}}(undef, length(matrices))
        @sync for i in eachindex(matrices)
            @spawn results[i] = matrices[i][1] * matrices[i][2] # 各タスクで行列乗算
        end
        return results
    end
    
    # 例の準備
    matrix_pairs = [ (rand(50,50), rand(50,50)) for _ in 1:nthreads()*2 ]
    @time parallel_matrix_multiplications(matrix_pairs)
    
  • @threads マクロ
    Base.Threads モジュールの @threads マクロは、for ループを複数のJuliaスレッドで並列に実行するために使われます。これは、各要素に対する独立した計算や、複数の小さな線形代数演算を並列化するのに適しています。

    using Base.Threads
    
    function parallel_sum(arr)
        s = Atomic{Int}(0) # アトミック操作で競合を避ける
        @threads for x in arr
            atomic_add!(s, x)
        end
        return s[]
    end
    
    data = 1:1_000_000
    @time parallel_sum(data)
    

LinearAlgebra.BLAS.get_num_threads() を直接置き換える関数はほとんどありませんが、BLASのスレッド数を制御する方法としては、環境変数の使用が代替手段となります。