Por que expressar cálculos como multiplicações de matrizes os torna mais rápidos?

18

No tutorial MNist do Google usando o TensorFlow , é exibido um cálculo no qual uma etapa é equivalente à multiplicação de uma matriz por um vetor. O Google primeiro mostra uma imagem na qual cada multiplicação e adição numérica que seria aplicada ao cálculo é gravada na íntegra. Em seguida, eles mostram uma imagem na qual é expressa como uma multiplicação de matrizes, alegando que esta versão do cálculo é, ou pelo menos pode ser, mais rápida:

Se escrevermos isso como equações, obtemos:

equação escalar

Podemos "vetorizar" esse procedimento, transformando-o em multiplicação de matrizes e adição de vetores. Isso é útil para eficiência computacional. (Também é uma maneira útil de pensar.)

equação vetorial

Eu sei que equações como essa geralmente são escritas no formato de multiplicação de matrizes por profissionais de aprendizado de máquina e, é claro, podem ver vantagens em fazê-lo do ponto de vista da dispersão do código ou da compreensão da matemática. O que não entendo é a afirmação do Google de que a conversão do formato longhand para o formato matricial "é útil para a eficiência computacional"

Quando, por que e como seria possível obter melhorias de desempenho no software, expressando cálculos como multiplicações de matrizes? Se eu fosse calcular a multiplicação de matrizes na segunda imagem (baseada em matriz), como humano, faria isso sequencialmente fazendo cada um dos cálculos distintos mostrados na primeira imagem (escalar). Para mim, eles não passam de duas notações para a mesma sequência de cálculos. Por que é diferente para o meu computador? Por que um computador seria capaz de executar o cálculo da matriz mais rapidamente que o escalar?

Mark Amery
fonte

Respostas:

19

Isso pode parecer óbvio, mas os computadores não executam fórmulas , eles executam código e quanto tempo essa execução leva depende diretamente do código que eles executam e apenas indiretamente de qualquer conceito implementado por esse código. Dois trechos de código logicamente idênticos podem ter características de desempenho muito diferentes. Alguns motivos que provavelmente surgem especificamente na multiplicação de matrizes:

  • Usando vários threads. Quase não existe CPU moderna que não tenha múltiplos núcleos, muitos têm até 8, e máquinas especializadas para computação de alto desempenho podem facilmente ter 64 em vários soquetes. Escrever código da maneira óbvia, em uma linguagem de programação normal, usa apenas um deles. Em outras palavras, ele pode usar menos de 2% dos recursos de computação disponíveis da máquina em que está sendo executada.
  • Usando instruções SIMD (confundidamente, isso também é chamado de "vetorização", mas em um sentido diferente do que está nas citações de texto da pergunta). Em essência, em vez de 4 ou 8 instruções aritméticas escalares, forneça à CPU uma instrução que execute aritmética em 4 ou 8 registros ou mais em paralelo. Isso pode literalmente fazer alguns cálculos (quando eles são perfeitamente independentes e adequados ao conjunto de instruções) 4 ou 8 vezes mais rápido.
  • Fazendo uso mais inteligente do cache . O acesso à memória é mais rápido se forem temporal e espacialmente coerentes , ou seja, acessos consecutivos são para endereços próximos e, ao acessar um endereço duas vezes, você o acessa duas vezes em sucessão rápida, em vez de com uma longa pausa.
  • Usando aceleradores como GPUs. Esses dispositivos são bestas muito diferentes das CPUs e programá-los com eficiência é uma forma de arte totalmente própria. Por exemplo, eles têm centenas de núcleos, agrupados em grupos de algumas dezenas de núcleos, e esses grupos compartilham recursos - eles compartilham alguns KiB de memória que são muito mais rápidos que a memória normal e quando qualquer núcleo do grupo executa um ifTodos os outros membros desse grupo precisam esperar por ela.
  • Distribua o trabalho em várias máquinas (muito importante em supercomputadores!), Que introduz um enorme conjunto de novas dores de cabeça, mas pode, é claro, dar acesso a recursos computacionais muito maiores.
  • Algoritmos mais inteligentes. Para a multiplicação de matrizes, o algoritmo O (n ^ 3) simples, otimizado adequadamente com os truques acima, geralmente é mais rápido que os subcúbicos para tamanhos razoáveis ​​de matriz, mas às vezes eles vencem. Para casos especiais, como matrizes esparsas, é possível escrever algoritmos especializados.

Muitas pessoas inteligentes escreveram códigos muito eficientes para operações comuns de álgebra linear , usando os truques acima e muito mais e geralmente até com truques estúpidos específicos da plataforma. Portanto, transformar sua fórmula em uma multiplicação de matrizes e implementar esse cálculo chamando para uma biblioteca de álgebra linear madura se beneficia desse esforço de otimização. Por outro lado, se você simplesmente escrever a fórmula da maneira óbvia em uma linguagem de alto nível, o código da máquina que é gerado eventualmente não usará todos esses truques e não será tão rápido. Isso também é verdade se você pegar a formulação da matriz e implementá-la chamando uma rotina de multiplicação de matriz ingênua que você mesmo escreveu (novamente, da maneira óbvia).

Tornar o código rápido exige trabalho e, muitas vezes, bastante trabalho se você deseja a última gota de desempenho. Como muitos cálculos importantes podem ser expressos como combinação de algumas operações de álgebra linear, é econômico criar código altamente otimizado para essas operações. Seu caso de uso especializado único? Ninguém se importa com isso, exceto você, portanto, otimizar o diabo não é econômico.

Comunidade
fonte
4

(esparsa) A multiplicação de vetores matriciais é altamente paralelizável. O que é muito útil se seus dados forem grandes e você tiver um farm de servidores à sua disposição.

Isso significa que você pode dividir a matriz e o vetor em pedaços e deixar que máquinas separadas façam parte do trabalho. Em seguida, compartilhe alguns de seus resultados e obtenha o resultado final.

No seu exemplo, as operações seriam as seguintes

  1. configurar uma grade de processadores, cada um segurando um Wx, y de acordo com suas coordenadas na grade

  2. transmita o vetor de origem ao longo de cada coluna (custo O(log height))

  3. ter cada processador para a multiplicação localmente (custo O(width of submatrix * heightof submatrix))

  4. recolher o resultado ao longo de cada linha usando uma soma (custo O(log width))

Esta última operação é válida porque a soma é associativa.

Isso também permite a criação de redundância e evita a necessidade de colocar todas as informações em uma única máquina.

Para matrizes 4x4 pequenas, como você vê nos gráficos, é porque a CPU possui instruções e registros especiais para lidar com essas operações.

catraca arrepiante
fonte
-1

A coisa mais instrutiva seria comparar o desempenho do seu código com o desempenho da multiplicação de matrizes implementada já.

Sempre há alguma otimização de nível inferior em que você não pensou, aqui você pode encontrar um exemplo:

https://simulationcorner.net/index.php?page=fastmatrixvector

O castigador
fonte