Algorytm mnożenia macierzy - Matrix multiplication algorithm

Ponieważ mnożenie macierzy jest tak centralną operacją w wielu algorytmach numerycznych , zainwestowano wiele pracy w uczynienie algorytmów mnożenia macierzy wydajnymi. Zastosowania mnożenia macierzy w problemach obliczeniowych można znaleźć w wielu dziedzinach, w tym w obliczeniach naukowych i rozpoznawaniu wzorców oraz w pozornie niepowiązanych problemach, takich jak liczenie ścieżek na wykresie . Zaprojektowano wiele różnych algorytmów mnożenia macierzy na różnych typach sprzętu, w tym w systemach równoległych i rozproszonych , gdzie praca obliczeniowa jest rozłożona na wiele procesorów (być może w sieci).

Bezpośrednie zastosowanie matematycznej definicji mnożenia macierzy daje algorytm, który zajmuje czas rzędu n 3 operacji na polach , aby pomnożyć dwie macierze n × n przez to pole ( Θ( n 3 ) w notacji big O ). Lepsze asymptotyczne ograniczenia czasu potrzebnego do mnożenia macierzy są znane od algorytmu Strassena w latach 60., ale nadal nie wiadomo, jaki jest czas optymalny (tj. jaka jest złożoność obliczeniowa mnożenia macierzy ). Od grudnia 2020 r. algorytm mnożenia macierzy o najlepszej asymptotycznej złożoności działa w czasie O( n 2,3728596 ) , podanym przez Josha Almana i Virginię Vassilevską Williams , jednak algorytm ten jest algorytmem galaktycznym ze względu na duże stałe i nie można go praktycznie zrealizować.

Algorytm iteracyjny

Definicja mnożenia macierzy , że jeśli C = AB dla n x m macierzy A i m x s macierzy B , następnie C, to n x s matrycy z wpisami

Na tej podstawie można skonstruować prosty algorytm, który zapętla indeksy i od 1 do n oraz j od 1 do p , obliczając powyższe za pomocą pętli zagnieżdżonej:

  • Wejście: macierze A i B
  • Niech C będzie nową macierzą o odpowiednim rozmiarze
  • Dla i od 1 do n :
    • Dla j od 1 do p :
      • Niech suma = 0
      • Dla k od 1 do m :
        • Ustaw sumę ← sum + A ik × B kj
      • Ustaw C ij ← suma
  • Powrót C

Algorytm ten zajmuje czas Θ( nmp ) (w notacji asymptotycznej ). Powszechnym uproszczeniem na potrzeby analizy algorytmów jest założenie, że wszystkie dane wejściowe są macierzami kwadratowymi o rozmiarze n × n , w którym to przypadku czas działania wynosi Θ( n 3 ) , tj. sześcienny o rozmiarze wymiaru.

Zachowanie w pamięci podręcznej

Image
Ilustracja kolejności wierszy i kolumn głównych

Trzy pętle w iteracyjnym mnożeniu macierzy można dowolnie zamieniać między sobą bez wpływu na poprawność lub asymptotyczny czas działania. Jednak kolejność może mieć znaczny wpływ na praktyczną wydajność ze względu na wzorce dostępu do pamięci i wykorzystanie przez algorytm pamięci podręcznej ; to, która kolejność jest najlepsza, zależy również od tego, czy macierze są przechowywane w kolejności wiersz-główny , kolumna-główny, czy połączenie obu.

W szczególności w wyidealizowanym przypadku w pełni asocjacyjnej pamięci podręcznej składającej się z M bajtów i b bajtów na linię pamięci podręcznej (tj.m/bcache), powyższy algorytm jest suboptymalny dla A i B przechowywanych w kolejności wiersz-główny. Kiedy n >m/b, każda iteracja wewnętrznej pętli (jednoczesne przeciągnięcie przez wiersz A i kolumnę B ) powoduje chybienie pamięci podręcznej podczas uzyskiwania dostępu do elementu B . Oznacza to, że w najgorszym przypadku algorytm powoduje Θ( n 3 ) braków w pamięci podręcznej. Począwszy od 2010 r. szybkość pamięci w porównaniu z procesorami jest taka, że ​​w przypadku dużych macierzy dominują błędy w pamięci podręcznej, a nie rzeczywiste obliczenia.

Optymalnym wariantem algorytmu iteracyjnego dla A i B w układzie wiersz-główny jest wersja kafelkowa , w której macierz jest domyślnie podzielona na kwadratowe kafelki o rozmiarze M przez M :

  • Wejście: macierze A i B
  • Niech C będzie nową macierzą o odpowiednim rozmiarze
  • Wybierz rozmiar płytki T = Θ( M )
  • Dla I od 1 do n w krokach T :
    • Dla J od 1 do p w krokach T :
      • Dla K od 1 do m w krokach T :
        • Pomnóż A I : I + T , K : K + T i B K : K + T , J : J + T przez C I : I + T , J : J + T , czyli:
        • Dla i od I do min( I + T , n ) :
          • Dla j od J do min( J + T , p ) :
            • Niech suma = 0
            • Dla k od K do min( K + T , m ) :
              • Ustaw sumę ← sum + A ik × B kj
            • Zbiór C ijC ij + suma
  • Powrót C

W wyidealizowanym modelu pamięci podręcznej ten algorytm wiąże się tylko z Θ(n 3/b M) braki w pamięci podręcznej; dzielnik b M wynosi kilka rzędów wielkości na nowoczesnych maszynach, tak że rzeczywiste obliczenia dominują w czasie działania, a nie w chybieniach pamięci podręcznej.

Algorytm dziel i zwyciężaj

Alternatywą dla algorytmu iteracyjnego jest algorytm dziel i zwyciężaj do mnożenia macierzy. Opiera się to na partycjonowaniu bloków

co działa dla wszystkich macierzy kwadratowych, których wymiary są potęgami dwójki, tj. kształty wynoszą 2 n × 2 n dla niektórych n . Produkt macierzy jest teraz

który składa się z ośmiu mnożeń par podmacierzy, po których następuje krok dodawania. Algorytm dziel i przejęcie oblicza mniejsze mnożenia rekurencyjnie stosując skalarne mnożenia C 11 = 11 b 11 jak pudełka podstawy.

Złożoność tego algorytmu w funkcji n jest określona przez rekurencję

uwzględnienie ośmiu rekurencyjnych wywołań macierzy o rozmiarze n /2 i Θ( n 2 ) w celu zsumowania czterech par wynikowych macierzy z uwzględnieniem elementów. Zastosowanie twierdzenia głównego dla rekurencji typu dziel i zwyciężaj pokazuje, że ta rekurencja ma rozwiązanie Θ( n 3 ) , takie samo jak algorytm iteracyjny.

Macierze niekwadratowe

Wariant tego algorytmu, który działa dla macierzy o dowolnych kształtach i jest szybszy w praktyce, dzieli macierze na dwie zamiast na cztery podmacierze w następujący sposób. Podział macierzy oznacza teraz podzielenie jej na dwie części o jednakowej wielkości lub możliwie zbliżone do równych rozmiarów w przypadku nieparzystych wymiarów.

  • Dane wejściowe: macierze A o rozmiarze n × m , B o rozmiarze m × p .
  • Przypadek podstawowy: jeśli max( n , m , p ) jest poniżej pewnego progu, użyj rozwiniętej wersji algorytmu iteracyjnego.
  • Przypadki rekurencyjne:
  • Jeśli max( n , m , p ) = n , podziel A poziomo:
  • W przeciwnym razie, jeśli max( n , m , p ) = p , podziel B w pionie:
  • W przeciwnym razie max( n , m , p ) = m . Podziel A w pionie i B w poziomie:

Zachowanie w pamięci podręcznej

Współczynnik chybienia pamięci podręcznej w rekurencyjnym mnożeniu macierzy jest taki sam, jak w przypadku kafelkowej wersji iteracyjnej, ale w przeciwieństwie do tego algorytmu, algorytm rekurencyjny nie zwraca uwagi na pamięć podręczną : nie ma parametru dostrajania wymaganego do uzyskania optymalnej wydajności pamięci podręcznej i zachowuje się dobrze w środowisko wieloprogramowe, w którym rozmiary pamięci podręcznej są efektywnie dynamiczne ze względu na inne procesy zajmujące miejsce w pamięci podręcznej. (Prosty algorytm iteracyjny również nie uwzględnia pamięci podręcznej, ale w praktyce jest znacznie wolniejszy, jeśli układ macierzy nie jest dostosowany do algorytmu.)

Liczba chybień pamięci podręcznej spowodowanych przez ten algorytm na maszynie z M wierszami idealnej pamięci podręcznej, każdy o rozmiarze b bajtów, jest ograniczona przez

Algorytmy subsześcienne

Image
Poprawa estymacji wykładnika ω w czasie dla złożoności obliczeniowej mnożenia macierzy .

Istnieją algorytmy, które zapewniają lepsze czasy działania niż te proste. Jako pierwszy odkryto algorytm Strassena , opracowany przez Volkera Strassena w 1969 roku i często określany jako „szybkie mnożenie macierzy”. Opiera się na sposobie mnożenia dwóch macierzy 2 × 2, który wymaga tylko 7 mnożenia (zamiast 8), kosztem kilku dodatkowych operacji dodawania i odejmowania. Zastosowanie tego rekurencyjnie daje algorytm z mnożnikiem kosztu . Algorytm Strassena jest bardziej złożony, a stabilność numeryczna jest zmniejszona w porównaniu z algorytmem naiwnym, ale jest szybszy w przypadkach, gdy n > 100 lub więcej i pojawia się w kilku bibliotekach, takich jak BLAS . Jest to bardzo przydatne w przypadku dużych macierzy nad dokładnymi domenami, takimi jak pola skończone , gdzie stabilność numeryczna nie jest problemem.

Pytaniem otwartym w informatyce teoretycznej jest to, jak dobrze algorytm Strassena można ulepszyć pod względem asymptotycznej złożoności . Mnożenie macierzy wykładnik , zazwyczaj oznaczone jest najmniejsza liczba rzeczywista, dla której każdy matrycy na pola mogą być mnożone przez siebie za pomocą działania pola. Obecnie najlepszym zawodnikiem jest Josh Alman i Virginia Vassilevska Williams . Algorytm ten, podobnie jak wszystkie inne najnowsze algorytmy w tej dziedzinie badań, jest uogólnieniem algorytmu Coppersmitha-Winograda, który został podany przez Dona Coppersmitha i Shmuela Winograda w 1990 roku. Koncepcja koncepcyjna tych algorytmów jest podobna do algorytmu Strassena: sposób jest przeznaczony do mnożenia dwóch macierzy k × k z mniej niż k 3 mnożeń, a technika ta jest stosowana rekurencyjnie. Jednak stały współczynnik ukryty przez notację Big O jest tak duży, że te algorytmy są warte zachodu tylko w przypadku macierzy, które są zbyt duże do obsługi na współczesnych komputerach.

Algorytm Freivaldsa jest prostym algorytmem Monte Carlo, który przy danych macierzach A , B i C weryfikuje w czasie Θ ( n 2 ) jeśli AB = C .

Algorytmy równoległe i rozproszone

Równoległość pamięci współdzielonej

Dziel i zwyciężaj algorytm zarysowane wcześniej można parallelized dwojako dla wieloprocesorowych wspólną pamięcią . Opierają się one na fakcie, że osiem rekurencyjnych mnożeń macierzy w

mogą być wykonywane niezależnie od siebie, podobnie jak cztery podsumowania (chociaż algorytm musi „połączyć” mnożenia przed wykonaniem podsumowań). Wykorzystując pełną równoległość problemu, otrzymujemy algorytm, który można wyrazić w pseudokodzie typu fork-join :

Pomnóż procedurę ( C , A , B ) :

  • Przypadek bazowy: jeśli n = 1 , ustaw c 11a 11 × b 11 (lub pomnóż małą macierz bloków).
  • W przeciwnym razie przydziel miejsce na nową macierz T o kształcie n × n , wtedy:
    • Podział A na A 11 , A 12 , A 21 , A 22 .
    • Podział B na B 11 , B 12 , B 21 , B 22 .
    • Partycja C do C 11 , C 12 , C 21 , C 22 .
    • Podział T na T 11 , T 12 , T 21 , T 22 .
    • Wykonanie równoległe:
      • Pomnóż widelec ( C 11 , A 11 , B 11 ) .
      • Pomnóż widelec ( C 12 , A 11 , B 12 ) .
      • Pomnóż widelec ( C 21 , A 21 , B 11 ) .
      • Pomnóż widelec ( C 22 , A 21 , B 12 ) .
      • Pomnóż widelec ( T 11 , A 12 , B 21 ) .
      • Pomnóż widelec ( T 12 , A 12 , B 22 ) .
      • Pomnóż widelec ( T 21 , A 22 , B 21 ) .
      • Pomnóż widelec ( T 22 , A 22 , B 22 ) .
    • Dołącz (poczekaj na zakończenie równoległych widełek).
    • dodaj ( C , T ) .
    • Zwolnij T .

Procedura add( C , T ) dodaje T do C , z uwzględnieniem elementów:

  • Podstawowy przypadek: jeśli n = 1 , ustaw c 11c 11 + t 11 (lub zrób krótką pętlę, być może rozwiniętą).
  • Inaczej:
    • Partycja C do C 11 , C 12 , C 21 , C 22 .
    • Podział T na T 11 , T 12 , T 21 , T 22 .
    • Równolegle:
      • Dodatek widelca ( C 11 , T 11 ) .
      • Dodatek widelca ( C 12 , T 12 ) .
      • Widelec add ( C 21 , T 21 ) .
      • Widelec add ( C 22 , T 22 ) .
    • Dołącz .

W tym przypadku fork jest słowem kluczowym, które sygnalizuje, że obliczenia mogą być wykonywane równolegle z resztą wywołania funkcji, podczas gdy join czeka na zakończenie wszystkich wcześniej "rozwidlonych" obliczeń. partycja osiąga swój cel tylko przez manipulację wskaźnikiem.

Algorytm ten ma krytyczną długość ścieżki w Θ (log 2 n ) kroków, co oznacza, że zajmuje to dużo czasu na maszynie idealny z nieskończonej liczby procesorów; W związku z tym, że ma maksymalny możliwy przyspieszenie w Θ ( n 3 / log 2 N ) na każdej fizycznego komputera. Algorytm nie jest praktyczny ze względu na koszt komunikacji związany z przenoszeniem danych do iz tymczasowej macierzy T , ale bardziej praktyczny wariant osiąga przyspieszenie Θ( n 2 ) bez użycia tymczasowej macierzy.

Image
Mnożenie macierzy bloków. W algorytmie 2D każdy procesor odpowiada za jedną podmacierz C . W algorytmie 3D każda para podmacierzy z A i B, która jest mnożona, jest przypisywana do jednego procesora.

Algorytmy unikające komunikacji i rozproszone

W nowoczesnych architekturach z pamięcią hierarchiczną koszt ładowania i przechowywania elementów macierzy wejściowej zwykle dominuje nad kosztem arytmetyki. Na pojedynczej maszynie jest to ilość danych przesyłanych między pamięcią RAM a pamięcią podręczną, podczas gdy na maszynie wielowęzłowej z pamięcią rozproszoną jest to ilość przesyłana między węzłami; w obu przypadkach nazywa się to przepustowością komunikacji . Naiwny algorytm wykorzystujący trzy zagnieżdżone pętle wykorzystuje szerokość pasma komunikacyjnego Ω( n 3 ) .

Algorytm Cannona , znany również jako algorytm 2D , jest algorytm komunikacji unikania że ścianki każdej matrycy wprowadza się do matrycy bloku, którego elementy są podmatryce o wielkości M / 3 o M / 3 , w którym M ma rozmiar szybkiej pamięci. Algorytm naiwny jest następnie używany na macierzach blokowych, obliczając iloczyny podmacierzy w całości w pamięci szybkiej. Zmniejsza to pasmo komunikacyjne do O ( n 3 / M ) , która jest optymalna asymptotycznie (algorytmów wykonywania omów ( N 3 ) obliczeń).

W układzie rozproszonym z p procesorów ułożonych w siatkę p przez p 2D, do każdego procesora można przypisać jedną podmacierz wyniku, a iloczyn może być obliczony z każdym procesorem transmitującym O ( n 2 / p ) słów, co jest asymptotycznie optymalne przy założeniu, że każdy węzeł przechowuje minimum O ( n 2 / p ) elementów. Można to poprawić dzięki algorytmowi 3D, który układa procesory w siatkę kostki 3D, przypisując każdy iloczyn dwóch podmacierzy wejściowych do jednego procesora. Podmacierze wyników są następnie generowane przez wykonanie redukcji w każdym wierszu. Algorytm ten przesyła O ( n 2 / p 2/3 ) słów na procesor, co jest asymptotycznie optymalne. Wymaga to jednak powtórzenia każdego elementu macierzy wejściowej p 1/3 razy, a zatem wymaga współczynnika p 1/3 więcej pamięci niż jest to potrzebne do przechowywania danych wejściowych. Ten algorytm można połączyć z Strassen, aby jeszcze bardziej skrócić czas pracy. Algorytmy „2,5D” zapewniają ciągły kompromis między wykorzystaniem pamięci a przepustowością komunikacji. Na nowoczesnych rozproszonych środowiskach obliczeniowych, takich jak MapReduce , opracowano wyspecjalizowane algorytmy mnożenia.

Algorytmy dla siatek

Image
Mnożenie macierzy zakończone w krokach 2n-1 dla dwóch macierzy n×n na siatce okablowanej krzyżowo.

Istnieje wiele algorytmów mnożenia na siatkach . W przypadku mnożenia dwóch n × n na standardowej dwuwymiarowej siatce przy użyciu algorytmu 2D Cannona , można wykonać mnożenie w 3 n -2 krokach, chociaż liczba ta jest redukowana do połowy tej liczby dla powtarzanych obliczeń. Standardowa tablica jest nieefektywna, ponieważ dane z dwóch macierzy nie docierają jednocześnie i muszą być uzupełnione zerami.

Rezultat jest jeszcze szybszy na dwuwarstwowej siatce z krzyżowym drutem, gdzie potrzebne są tylko 2 n -1 kroków. Wydajność ulega dalszej poprawie w przypadku powtarzanych obliczeń, co prowadzi do 100% wydajności. Macierz siatek połączonych krzyżowo może być postrzegana jako szczególny przypadek nieplanarnej (tj. wielowarstwowej) struktury przetwarzania.

Zobacz też

Bibliografia

Dalsza lektura