Numpy `matmul` работает примерно в 100 раз хуже, чем `dot` при просмотре массива

Я обратил внимание на то, что функция matmul в numpy работает значительно хуже, чем функция dot при умножении представлений массива. В этом случае мой вид массива является реальной частью сложного массива. Вот некоторый код, который воспроизводит проблему:

import numpy as np
from timeit import timeit
N = 1300
xx = np.random.randn(N, N) + 1j
yy = np.random.randn(N, N) + 1J

x = np.real(xx)
y = np.real(yy)
assert np.shares_memory(x, xx)
assert np.shares_memory(y, yy)

dot = timeit('np.dot(x,y)', number = 10, globals = globals())
matmul = timeit('np.matmul(x,y)', number = 10, globals = globals())

print('time for np.matmul: ', matmul)
print('time for np.dot: ', dot)

На моей машине вывод такой:

time for np.matmul:  23.023062199994456
time for np.dot:  0.2706864000065252

Это явно как-то связано с общей памятью, поскольку замена np.real(xx) на np.real(xx).copy() устраняет несоответствие производительности.

Троллинг документов numpy не особенно помог, поскольку перечисленные различия не обсуждали детали реализации при работе с представлениями памяти.

Возможный дубликат: stackoverflow.com/questions/64914877/…. Я пока не буду отмечать это как дубликат, если кто-то еще может добавить более точное объяснение.

hpaulj 14.04.2023 00:03

Связанный: github.com/numpy/numpy/issues/23123

Chrysophylaxs 14.04.2023 00:48

@hpaulj Да, похоже на дубликат, за исключением того, что другой вопрос касается определенного типа данных. Я могу воспроизвести проблему с обычными поплавками и представлениями массива, такими как [::2,::2]. Как упоминалось в проблеме github, связанной с @Chrysophylaxs, это похоже на ошибку с несмежными массивами.

Simon Tartakovksy 14.04.2023 16:24

Мои тайминги показывают, что dot обычно использует copy, когда входные данные не могут быть отправлены напрямую в BLAS. matmul использует более медленный метод расчета.

hpaulj 14.04.2023 16:36

Код типа BLAS может обрабатывать непрерывные данные только для ограниченного числа dtypes. numpy разработчикам приходится выбирать между преобразованием других массивов в совместимую форму или использованием другого кода, что будет не так быстро.

hpaulj 14.04.2023 16:49

Я отправил выпуск github по теме. Думаю, мы узнали здесь все, что могли.

Simon Tartakovksy 14.04.2023 19:26
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
5
6
103
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Эти тайминги показывают, что dot выполняет copy с real:

In [22]: timeit np.dot(xx.real,xx.real)
232 ms ± 3.34 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [23]: timeit np.dot(xx.real.copy(),xx.real.copy())
232 ms ± 4.18 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Применение этого к matmul дает почти такое же время:

In [24]: timeit np.matmul(xx.real.copy(),xx.real.copy())
231 ms ± 3.54 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Опять же, matmul с real идут медленным путем. matmul/dot оба работают хуже, когда им даются int массивы, хотя и не так медленно, как в случае matmul real. matmul/dot также может обрабатывать object dtypes, но это еще медленнее.

Таким образом, под обложкой происходит многое, чего мы, пользователи уровня Python, не видим (и не задокументировано).

редактировать

У меня был соблазн изменить заголовок, чтобы сосредоточиться на комплексно-действительном, но я решил проверить другой view - кусочек массива с плавающей запятой.

In [42]: y=xx.real.copy()[::2,::2];y.shape,y.dtype
Out[42]: ((650, 650), dtype('float64'))

In [43]: timeit np.dot(y,y)
36.4 ms ± 63.4 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [44]: timeit np.dot(y.copy(),y.copy())
35.6 ms ± 191 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Опять же очевидно, что dot использует copies представления. matmul не:

In [45]: timeit np.matmul(y,y)
1.89 s ± 3.01 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

но с копиями время такое же, как точка:

In [46]: timeit np.matmul(y.copy(),y.copy())
35.3 ms ± 102 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Итак, я предполагаю, что dot обычно создает copy, если он не может отправить массивы непосредственно в процедуры BLAS. matmul вместо этого, очевидно, выбирает более медленный маршрут.

редактировать

Хотя их обработка двумерных массивов аналогична, dot и matmul сильно различаются тем, как они обрабатывают трехмерные массивы. На самом деле основной причиной добавления @ было предоставление удобного «пакетного» понятия для умножения матриц.

Придерживаясь больших сложных массивов, давайте сделаем один в 3 раза больше:

In [49]: yy=np.array([xx,xx,xx]);yy.shape
Out[49]: (3, 1300, 1300)

In [50]: timeit np.dot(xx,xx)
794 ms ± 12.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)    
In [51]: timeit np.dot(xx,yy)       # (yy,xx) same timings
55.5 s ± 151 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [52]: timeit np.matmul(xx,yy)    # (yy,yy) same
2.58 s ± 362 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

matmul только что увеличил время на 3; dot к 70. Я мог бы больше исследовать вещи, но не с таймингами в минутном диапазоне.

Похоже, что matmul по умолчанию использует глупую реализацию O (n ^ 3) без учета кеша ЦП, если решает, что BLAS не может выполнять вычисления. Я думаю, это объясняет, почему существует такой большой разрыв в производительности с большими массивами. Действительно, похоже, что точка делает копию, чтобы вместо этого передать массив в BLAS. Все кажется последовательным. Остается только один вопрос... Почему это нигде не задокументировано и требует чтения исходного кода?

Simon Tartakovksy 14.04.2023 17:43

Другие вопросы по теме