C#のMKLのスレッドセーフ
Parallel.Forなどで、マルチスレッド処理を行い、その処理にいMKL(dgemm等)を用いていると結果が不安定(シーケンシャルと異なる結果)になることがある。これは、MKLも一部の関数がマルチスレッドを使用しているためである。不安定にならないように、スレッドセーフで行うための方法例を記載する。
lockを使う
処理を排他的にするためにlockする。var syncObj = new object();
Parallel.For(0, n, i =>
{
lock (syncObj)
{
MKL Processes
}
});
この方法は、メモリアクセスのスレッドセーフと同じで、lock内の処理を他のスレッドで実行できないようにlockするための方法である。ただし、MKLを使った処理がメインとなる並列処理の場合は、マルチスレッドの効果が十分にないので、その場合はシーケンシャルで処理するほうがよい。
MKL使用時のバッファーが不安定になる原因にもなるので、後述のバッファーを解放させることも必要である。
MKLのスレッド数を制限する
MKLが使用するスレッド数を制限する。[DllImport(mkl_rt.2.dll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true, SetLastError = false)]
internal static extern int mkl_set_num_threads_local(ref int num);
[DllImport(mkl_rt.2.dll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true, SetLastError = false)]
internal static extern void mkl_set_dynamic(ref int flag);
[DllImport(mkl_rt.2.dll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true, SetLastError = false)]
internal static extern int mkl_get_dynamic();
Parallel.For(0, n, i =>
{
// Get original setting
int oriDynamics = mkl_get_dynamic();
int dynamics = 0;
// Off dynamical threads
mkl_set_dynamic(ref dynamics);
int threads = 1;
// Set number of thread and get last threads number
int threads_last = mkl_set_num_threads_local(ref threads);
MKL Processes
// Set previous threads number
threads = mkl_set_num_threads_local(ref threads_last);
// Set previous setting
mkl_set_dynamic(ref oriDynamics);
});
MKLのサポート関数にて、MKLを使用時にスレッド数を一つにして、メモリが競合しないようにする。mkl_set_num_threads_localで制限してもdynamicがオンの状態だと、十分に制限できないことがあるようなので、dynamicをオフにしておく。この記述したスレッドでしか有効でないために、各スレッド毎に記述する必要がある。
スレッド制限するため、1スレッドあたりの処理は若干遅くなるが、大きく影響はしなかった。
大規模データの場合
大規模データは、計算後のバッファーも大きいので、影響を及ぼすことがあった。対策として、以下のようにスレッドの最後やMKLの演算後に不要になるタイミングで、バッファーを解放し、ガベージ コレクションを強制実行し、少しでもバッファーの影響をなくす。 mkl_thread_free_buffers();
GC.Collect();
mkl_thread_free_buffersは、使用しているスレッドのバッファーの解放である。