Низкая производительность функции, вызывающей Fortran Intel MKL dgemm против numpy, но также и против matmul

Я открываю для себя fortran и ctypes в python, поскольку я планирую предоставить python библиотеку fortran с использованием intel MKL, библиотеку, которая на данный момент вызывается из самой библиотеки c, вызываемой из библиотеки c++... наконец, используемой c#...

Для начала мне удалось написать простой код на Фортране, определяющий различные функции умножения матриц: наивную, одну с использованием fortran matmul и одну с использованием Intel MKL dgemm:

        subroutine matmultorig(M1, M2, M3, M, N, K) bind(c, name='matmultorig')
            !DEC$ ATTRIBUTES DLLEXPORT :: matmultorig
            use iso_c_binding, only: c_double, c_int
        
            integer(c_int),intent(in) :: M, N, K
            real(c_double), intent(in) :: M1(M, N), M2(N, K)
            real(c_double), intent(inout):: M3(M, K)

            M3 = matmul(M1,M2)
        
        end subroutine

        subroutine matmultmy(M1, M2, M3, M, N, K) bind(c, name='matmultmy')
            !DEC$ ATTRIBUTES DLLEXPORT :: matmultmy
            use iso_c_binding, only: c_double, c_int
        
            integer(c_int),intent(in) :: M, N, K
            real(c_double), intent(in) :: M1(M, N), M2(N, K)
            real(c_double), intent(inout):: M3(M, K)
        
            integer :: i,j,l
        
            do i = 1,M
                do j = 1,K
                    M3(i,j) = 0.
                    do l = 1,N
                        M3(i,j) = M3(i,j) + M1(i,l) * M2(l,j)
                    end do
                end do
            end do
        
        end subroutine

        subroutine matmultmkl(M1, M2, M3, M, N, K) bind(c, name='matmultmkl')
            !DEC$ ATTRIBUTES DLLEXPORT :: matmultmkl
            use iso_c_binding, only: c_double, c_int
        
            integer(c_int),intent(in) :: M, N, K
            real(c_double), intent(in) :: M1(M, N), M2(N, K)
            real(c_double), intent(inout):: M3(M, K)

            CALL DGEMM('N','N',M,K,N,1.,M1,M,M2,N,0.,M3,M)
        
        end subroutine 

Я компилирую фортран, используя файл .bat (я под окнами):

@Echo off

setlocal ENABLEDELAYEDEXPANSION
SET "IFORT_INITIAL_FLAGS=-c -fpp"
SET "IFORT_OPTIMIZATION_FLAGS=/O3"

ifort %IFORT_OPTIMIZATION_FLAGS% %IFORT_INITIAL_FLAGS% /I"C:\Program Files (x86)\IntelSWTools\compilers_and_libraries_2020.4.311\windows\mkl\include" -o test.obj test.f
ifort -dll -o mylib.dll test.obj /link /LIBPATH:"C:\Program Files (x86)\IntelSWTools\compilers_and_libraries_2020.4.311\windows\mkl\lib\intel64_win" mkl_intel_lp64.lib mkl_intel_thread.lib mkl_core.lib libiomp5md.lib 

Наконец, я написал следующий скрипт Python, который я выполняю из кода Visual Studio:

from ctypes import *
import time

import os
os.add_dll_directory(r"C:/Program Files (x86)/IntelSWTools/compilers_and_libraries_2020.4.311/windows/redist/intel64_win/mkl")
os.add_dll_directory(r"C:/Program Files (x86)/IntelSWTools/compilers_and_libraries_2020.4.311/windows/redist/intel64_win/compiler")

import numpy as np

mylib = CDLL(r"C:/path/to/the/fortran/mylib.dll")

mylib.matmultmy.argtypes = [ POINTER(c_double), 
                                POINTER(c_double), 
                                POINTER(c_double),
                                POINTER(c_int),
                                POINTER(c_int),
                                POINTER(c_int) ]

mylib.matmultorig.argtypes = [ POINTER(c_double), 
                                POINTER(c_double), 
                                POINTER(c_double),
                                POINTER(c_int),
                                POINTER(c_int),
                                POINTER(c_int) ]

mylib.matmultmkl.argtypes = [ POINTER(c_double), 
                                POINTER(c_double), 
                                POINTER(c_double),
                                POINTER(c_int),
                                POINTER(c_int),
                                POINTER(c_int) ]

# Setup    
M=500
N=500
K=500

a = np.empty((M,N), dtype=c_double)
b = np.empty((N,K), dtype=c_double)
c = np.empty((M,K), dtype=c_double)

a[:] = np.random.rand(M,N)
b[:] = np.random.rand(N,K)


# Fortran my call
start = time.time()
mylib.matmultmy( a.ctypes.data_as(POINTER(c_double)), 
                    b.ctypes.data_as(POINTER(c_double)), 
                    c.ctypes.data_as(POINTER(c_double)), 
                    c_int(M), c_int(N), c_int(K) )
stop = time.time()
print(f"Fortran my \t {stop - start}s")

# Fortran matmul call
start = time.time()
mylib.matmultorig( a.ctypes.data_as(POINTER(c_double)), 
                    b.ctypes.data_as(POINTER(c_double)), 
                    c.ctypes.data_as(POINTER(c_double)), 
                    c_int(M), c_int(N), c_int(K) )
stop = time.time()
print(f"Fortran matmul \t {stop - start}s")

# Fortran mkl call
start = time.time()
mylib.matmultmkl( a.ctypes.data_as(POINTER(c_double)), 
                    b.ctypes.data_as(POINTER(c_double)), 
                    c.ctypes.data_as(POINTER(c_double)), 
                    c_int(M), c_int(N), c_int(K) )
stop = time.time()
print(f"Fortran mkl \t {stop - start}s")

# Numpy
start = time.time()
c = a.dot(b)
stop = time.time()
print(f"Numpy \t\t {stop - start}s")

Результат:

Fortran my       0.11234903335571289s
Fortran matmul   0.023325443267822266s
Fortran mkl      0.5279343128204346s
Numpy            0.001001596450805664s

Я играл с теми же идеями, но в контексте python С++ pybind11 здесь (те же размеры матрицы):

pybind11 и numpy для матричного продукта

где цифры сейчас несопоставимы с моим случаем, но, по крайней мере, numpy и intel mkl были примерно на одном уровне производительности. Здесь функция, вызывающая dgemm, требует в 500 раз больше матричного произведения numpy. Я подозреваю, что это из-за второстепенной сортировки и в основном из-за привязки c. Однако я обнаружил все это как два дня назад, так что если у профи есть идея ...


person Olorin    schedule 27.02.2021    source источник
comment
Вы все еще вызываете dgemm с аргументами одинарной точности - 1. это одинарная точность. Он должен быть двойным. Прежде чем вы даже посмотрите на время, я бы ОЧЕНЬ настоятельно рекомендовал добавить код, который проверяет, что вы получаете один и тот же результат в каждом случае. Если вы не получите таких же результатов, время будет бессмысленным.   -  person Ian Bush    schedule 27.02.2021
comment
Ничего себе, я ВООБЩЕ не понял, что вы имели в виду в прошлый раз с аргументами двойной точности. Я думал, что установка c_double в фортране и в питоне действительно позволила везде использовать двойные числа... Что я делаю неправильно...   -  person Olorin    schedule 27.02.2021
comment
1.0 является константой точности по умолчанию в Fortran. Точность по умолчанию почти всегда очень похожа на одинарную точность IEEE. Все аргументы DGEMM должны иметь двойную точность, не только переменные, но и константы — нет никакого волшебного повышения до удвоения.   -  person Ian Bush    schedule 27.02.2021
comment
Также обратите внимание, что в древней истории я заметил, что самый первый вызов MKL занимал значительно больше времени, чем любой другой, предположительно загружая библиотеку или аналогичную настройку. Как только вы убедитесь, что ответы одинаковы в каждом случае, я бы поменял местами вызов Numpy и mkl и посмотрел, имеет ли это какое-то значение. Но также обратите внимание, что прошло много времени с тех пор, как я в последний раз смотрел на это.   -  person Ian Bush    schedule 27.02.2021
comment
Господи я глупый... 0D, а не 0.. Это действительно все изменило, и да, результаты с точностью до последней цифры сравнялись. Мне действительно очень жаль.   -  person Olorin    schedule 27.02.2021
comment
На самом деле, вы хотите 0._c_double. Я получаю 2 мс с matmul() и 1,2 мс с MKL.   -  person Vladimir F    schedule 27.02.2021
comment
@VladimirF Хорошо ... В чем разница между 0D и 0._c_double ?... (Не кричите на меня, пожалуйста... :))   -  person Olorin    schedule 27.02.2021
comment
Так будет почти всегда. Один - Фортран double precision, а другой - real(c_double). Может быть по-другому, если использовать флаги для продвижения реальных типов Fortran, подобные флагу -double_size=16.   -  person Vladimir F    schedule 27.02.2021
comment
Извините, флаг был просто примером, правильный синтаксис был бы -double-size 128.   -  person Vladimir F    schedule 27.02.2021
comment
Хорошо я понял. Спасибо ВладимирФ и ЯнБуш!   -  person Olorin    schedule 27.02.2021
comment
Вы хотели, чтобы ваша процедура matmultmy была намеренно неэффективной? Fortran — это язык со столбцами.   -  person steve    schedule 27.02.2021
comment
@ Стив Точно. ;) Я знаю. Но всегда забывай.   -  person Olorin    schedule 27.02.2021