Самый быстрый способ вычислить множество умножений матрицы на матрицу 3x3

Мне нужно вычислить комбинацию многих матриц вращения 3x3.

Вот сравнение нанесения functools.reduce на matmul с numpy и cupy:

import timeit
from functools import reduce
import numpy as np
import cupy as cp
from pyrr.matrix33 import create_from_axis_rotation

# generate random rotation matrices
axes = np.random.rand(10000, 3)
angles = np.pi * np.random.rand(10000)
rotations = [create_from_axis_rotation(*params) for params in zip(axes, angles)]

# then reduce with matmul

xp = np # numpy
xp_rotations = [xp.asarray(rotation) for rotation in rotations]
timexp = timeit.timeit("reduce(xp.matmul, xp_rotations)", number=10, globals=globals())
print(f"{xp.__name__}: {timexp * 1000:0.3}ms")

xp = cp # cupy
xp_rotations = [xp.asarray(rotation) for rotation in rotations]
timexp = timeit.timeit("reduce(xp.matmul, xp_rotations)", number=10, globals=globals())
print(f"{xp.__name__}: {timexp * 1000:0.3}ms")

На хорошей машине с графическим процессором Titan это дает:

numpy: 1.63e+02ms
cupy: 8.78e+02ms

По какой-то причине GPU работает намного медленнее.

В любом случае, есть ли способ вычислить это значительно быстрее?

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

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


def reduce_loop(matrices):
    """ non-optimized reduce """
    mat = matrices[0]
    for _mat in matrices[1:]:
        mat = mat @ _mat
    return mat

def reduce_split(matrices): 
    """ reduce by multiplying pairs of matrices recursively """
    if len(matrices) == 1:
        return matrices[0]
    neven = (len(matrices) // 2) * 2
    reduced = matrices[:neven:2] @ matrices[1:neven:2]
    if len(matrices) > neven:  # len(matrices) is odd
        reduced[-1] = reduced[-1] @ matrices[-1]
    return reduce_split(reduced)

time = timeit.timeit("reduce_loop(rotations)", number=10, globals=globals())
print(f"reduce_loop: {time * 1000:0.3}ms")

time = timeit.timeit("reduce_split(rotations)", number=10, globals=globals())
print(f"reduce_split: {time * 1000:0.3}ms")

Предоставление:

reduce_loop: 2.14e+02ms
reduce_split: 24.5ms

Я уверен, что это не оптимально, но он использует оптимизацию numpy (и, возможно, cupy).


person piliv    schedule 28.10.2020    source источник
comment
Проверьте этот вопрос о кватернионе.   -  person Quang Hoang    schedule 28.10.2020
comment
@QuangHoang спасибо, это было интересно, хотя мне не хотелось заново реализовывать все, используя кватернионы. Однако, похоже, до сих пор ведутся споры о том, действительно ли умножение кватернионов быстрее, чем умножение матриц.   -  person piliv    schedule 30.10.2020


Ответы (1)


  1. functools.reduce() был удален из ядра python, поскольку он неэффективен и не является pythonic. Эквивалента cuPy нет, только хост-версия в библиотеке functools

  2. ваш код cuPy тратит большую часть своего времени на бесплодное копирование данных с хоста на устройство и обратно... тысячи раз, потому что reduce() работает только на хосте, а не на графическом процессоре. Вы нагружаете шину PCI, а не GPU

  3. рассмотрите возможность превращения списка «поворотов» в матрицу cuPy, а затем используйте шаг (не список python)

  4. используйте ядро ​​сокращения cuPy для выполнения matmul https://docs.cupy.dev/en/stable/reference/generated/cupy.ReductionKernel.html

person Stripedbass    schedule 29.10.2020
comment
Спасибо. По поводу 1 и 2: когда именно GPU передает данные CPU, мне до сих пор неясно. Реализованные мной функции псевдоредукции (см. редактирование) не лучше работают на графическом процессоре и не должны передавать данные обратно в каждом цикле. Однако это правда, что GPU действительно бесполезен для выполнения умножения матриц 3x3. 4. Не смог найти способ использовать эти функции (ни cupy.fuse, что вроде проще). Документов по-прежнему не хватает. - person piliv; 30.10.2020