numpy matmul очень медленный, когда один из двух — np.array(dtype = np.complex128).real

#python #numpy

#python #numpy

Вопрос:

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

Рассмотрим следующий пример:

 import numpy as np
from time import time

class timeit():
    def __init__(self, string):
        self.string = string

    def __enter__(self):
        self.t0 = time()

    def __exit__(self, *args):
        print(f'{self.string} : {time() - self.t0}')


A  = np.random.rand(200, 1000)   0j
B = np.random.rand(1000, 5000)

with timeit('with complex'):
    out = A @ B

Ar = A.real
with timeit('after .real'):
    out = Ar @ B

Ai = (A * 1j).imag
with timeit('after .imag'):
    out = Ai @ B

with timeit('after .astype(float)'):
    out = A.astype(np.float64()) @ B

with timeit('after .real.astype(float)'):
    out = A.real.astype(np.float64()) @ B
  

Вывод

 with complex : 0.09374785423278809
after .real : 1.9792003631591797
after .imag : 1.717487096786499
after .astype(float) : 0.016920804977416992
after .real.astype(float) : 0.017952442169189453
  

Обратите внимание, что когда один из двух массивов A.real или A.imag операция выполняется в 20 раз медленнее (число может увеличиваться в сотни раз медленнее, если массивы больше).

Использование A.astype(np.float64) выполняется очень быстро, но каждый раз выдает предупреждение, даже если мнимая часть равна нулю.

Кажется, что единственное быстрое и тихое решение A.real.astype(float) , но, честно говоря, оно выглядит довольно уродливо для меня.

Проверяя адрес памяти этого массива, я получаю следующее

 def aid(x):
    # This function returns the memory
    # block address of an array.
    return x.__array_interface__['data'][0]

print(f'ID(A.real) == ID(A): {aid(A.real) == aid(A)}')
print(f'ID(A.imag) == ID(A): {aid(A.imag) == aid(A)}')
print(f'ID(A.astype) == ID(A): {aid(A.astype(np.float64())) == aid(A)}')
print(f'ID(A.real.astype) == ID(A): {aid(A.real.astype(np.float64())) == aid(A)}')

  

это возвращает

 ID(A.real) == ID(A): True
ID(A.imag) == ID(A): False
ID(A.astype) == ID(A): False
ID(A.real.astype) == ID(A): False
  

Это, по-видимому, указывает на то, что A.real имеет тот же адрес памяти A A.astype(np.float64) , что и , в то время как нет. Может ли это быть причиной такого поведения? Однако A и A.imag имеют разные адреса памяти, но все равно matmul очень медленно.

Это ошибка? Является ли решение A.real.astype(np.float64) тем, которое я должен использовать?

Комментарии:

1. Или, возможно, pickle неправильно выбрасывает большую часть ваших данных.

2. Не могу воспроизвести это в моей системе. оба дают мне примерно 0,2 с, как и ожидалось.

3. @mCoding проверьте редактирование. Также numpy.array.copy() решает проблему. На мой взгляд, это не имеет никакого отношения к pickle

4. Не зная, как были созданы исходные массивы, я не думаю, что мы сможем помочь. Я не могу придумать хранилище или свойство массива, которое замедляло бы работу и не проходило бы через pickle or copy .

5. @hpaulj это был в основном мой вопрос. Какое свойство может быть причиной этого и не проходить через pickle or copy ? Или … какое свойство я должен проверить в исходном массиве, что позволило бы мне воспроизвести ошибку? Я попытаюсь привести минимальный рабочий пример, но проблема кажется трудной для воспроизведения (даже если это происходит систематически в моем коде)

Ответ №1:

Это не место в памяти или расположение, которое имеет значение. Это маршрут, который @ выбирается в зависимости от типа ввода. A.real это не «новый» массив; это метод доступа к реальным значениям сложного dtype . У меня нет времени, чтобы представить мои полные результаты синхронизации, но вот пара быстрых результатов

 In [2]: timeit A@B
436 ms ± 32.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
  

добавление real замедляет этот путь:

 In [3]: timeit A.real@B
3.93 s ± 4.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [4]: %%timeit a = A.real
   ...: a@B
3.92 s ± 3.29 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
  

Но создание нового массива с плавающей запятой ускоряет процесс:

 In [5]: %%timeit a = A.real.copy()
   ...: a@B 
101 ms ± 496 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
  

A.real фактически не выполняет никаких вычислений:

 In [6]: timeit A.real    
203 ns ± 9.42 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
  

И создание нового массива из реальных значений не так уж и медленно:

 In [7]: timeit A.real.copy()
239 µs ± 3.54 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
  

Включение встроенной копии не сокращает время:

 In [8]: timeit A.real.copy()@B
102 ms ± 1.08 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
  

dot не беспокоит real :

 In [9]: timeit A.real.dot(B)
106 ms ± 3.49 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
  

A.dot(B) раз так же, как A@B .

Таким образом, сложная оценка примерно в 4 раза медленнее, чем плавающая. Учитывая, что A это в два раза больше средних значений, это звучит разумно.

dot обрабатывает real правильно, извлекая реальные значения без особых хлопот.

@ имеет какую-то ошибку, отправляя ее на медленный трек.

Комментарии:

1. Так вы думаете, это связано с ошибкой в реализации @ ? В предыдущей версии вопроса я заметил, что .copy() это решает проблему. Мой вопрос был … почему?

2. matmul по-видимому, выбирает разные маршруты вычисления в зависимости от характера входных данных. Для прямой прямой оценки массивов с плавающей запятой он передает задачу непосредственно высокооптимизированным подпрограммам типа BLAS. Но это скомпилированный код, поэтому его сложнее исследовать. Могут быть некоторые обсуждения по перемещению github.