Julia: API Matrix Multiplication

Criado em 28 set. 2017  ·  208Comentários  ·  Fonte: JuliaLang/julia

Atualmente, existe a seguinte linha no código matmult esparso:

https://github.com/JuliaLang/julia/blob/056b374919e11977d5a8d57b446ad1c72f3e6b1d/base/sparse/linalg.jl#L94 -L95

Estou assumindo que isso significa que queremos os métodos A_mul_B*!(α,A,B,β,C) = αAB + βC mais gerais que sobrescrevem C (a API BLAS gemm ) para matrizes densas. Esse ainda é o caso? (Também parece possível manter ambas as APIs, ou seja, manter os métodos A_mul_B*!(C,A,B) , que seriam simplesmente definidos como A_mul_B*!(C,A,B) = A_mul_B*!(1,A,B,0,C) .)

Eu pessoalmente gostaria de ter a API gemm definida para todos os tipos de array (isso foi expresso em outro lugar ). Implementar esses métodos para arrays densos parece bastante simples, uma vez que eles chamariam diretamente gemm et al. O caso esparso já foi implementado. A única modificação real seria o matmult genérico puro de julia, que não aceita α e β argumentos.

Isso levaria a um código genérico que funciona com qualquer tipo de matriz / número. Atualmente, tenho uma implementação simples de expm (depois de fazer a modificação em _generic_matmatmult! ) que funciona com bigfloats e arrays esparsos.

linear algebra

Comentários muito úteis

Sugiro uma única rodada de votação de aprovação com prazo de 10 dias a partir de agora. Votação para aprovação significa: Todos votam em todas as opções que julgam preferíveis a continuar a discussão. Pessoas que preferem ter seu nome menos preferido agora do que uma discussão contínua devem votar em todos os três. Se nenhuma opção for amplamente aprovada, ou se o esquema de votação em si não obtiver aprovação generalizada, devemos continuar a discussão. Em caso de quase-empate entre as opções aprovadas, @tkf decide (privilégio do autor de RP).

+1: Eu concordo com este esquema de votação e dei meus votos de aprovação.
-1: Discordo deste esquema de votação. Se muitas pessoas ou pessoas muito importantes selecionarem essa opção, a votação será discutível.

Coração: mul! é preferível à discussão contínua.
Foguete: muladd! é preferível à discussão contínua.
Viva: addmul! é preferível à discussão contínua.

Sugiro provisoriamente que 75% de aprovação e 5 devem definitivamente fazer um quorum (ou seja, 75% das pessoas que votaram, incluindo discordância com todo o procedimento de votação, e pelo menos 5 pessoas aprovaram a opção vencedora; se a participação for baixa , então 5/6 ou 6/8 formam o quorum, mas 4/4 unânimes podem ser considerados como falha).

Todos 208 comentários

Ref. # 9930, # 20053, # 23552. Melhor!

Obrigado pelas referências. Suponho que esse problema tenha mais a ver com a adição dos métodos gemm -style do que com uma reformulação da API, mas pode ser fechado se acharmos que ainda é muito semelhante ao # 9930.

Como ponto de partida, haveria suporte para que _generic_matmatmul! tivesse a API gemm ? É uma mudança bastante simples e puramente aditiva / não separável, já que o método atual seria simplesmente implementado tendo α=1 e β=0 . Posso fazer o PR. Eu provavelmente iria de forma semelhante para esta versão (nessa versão eu cortei todo o material de transposição porque não precisava, não faria isso aqui).

Sim. Isso seria um bom começo. Precisamos considerar a ordem dos argumentos. Originalmente, eu pensei que era mais natural seguir a ordem do BLAS, mas nós somos bastante consistentes sobre ter os argumentos de saída primeiro, o que também é o caso para os três argumentos atuais A_mul_B! . Além disso, como você já observou, a versão de três argumentos corresponderia à versão de cinco argumentos com α=1 e β=0 e os argumentos de valor padrão são os últimos. Claro, não temos necessariamente que usar a sintaxe de valor padrão para isso, mas faria sentido usá-la aqui.

Por que não apenas introduzir uma função gemm genérica?

Sim. Isso seria um bom começo. Precisamos considerar a ordem dos argumentos. Originalmente, eu pensei que era mais natural seguir a ordem do BLAS, mas nós somos bastante consistentes sobre ter argumentos de saída primeiro, o que também é o caso para o atual A_mul_B! De três argumentos. Além disso, como você já indicou, a versão de três argumentos corresponderia à versão de cinco argumentos com α = 1 e β = 0 e os argumentos de valor padrão são os últimos. Claro, não temos necessariamente que usar a sintaxe de valor padrão para isso, mas faria sentido usá-la aqui.

Parece bom. Podemos continuar a discussão sobre a ordem real dos argumentos e renomeação do método em # 9930. Isso é mais sobre apenas ter a versão de cinco argumentos disponível, então vou manter a interface Ax_mul_Bx!(α,A,B,β,C) atual.

Por que não apenas introduzir uma função genérica de gemm?

Você está sugerindo renomear _generic_matmatmul! para gemm! além das alterações acima?

Para ser mais claro, acho que deveríamos acabar tendo um único método mul(C,A,B,α=1,β=0) , junto com os tipos de transposição / adjunto preguiçosos para despachar, mas isso será um outro PR.

Por que não apenas introduzir uma função genérica de gemm?

Acho que gemm é um nome impróprio em Julia. No BLAS, a parte ge indica que as matrizes são gerais , ou seja, não têm estrutura especial, a primeira m é multiplicação e a lista m é matriz . Em Julia, a parte ge (geral) é codificada na assinatura, assim como a última m (matriz), então acho que devemos chamá-la de mul! .

É a notação mul!(α, A, B, β, C) de SparseArrays.jl

https://github.com/JuliaLang/julia/blob/160a46704fd1b349b5425f104a4ac8b323ea85af/stdlib/SparseArrays/src/linalg.jl#L32

"finalizado" como a sintaxe oficial? E isso seria além de mul!(C, A, B) , lmul!(A, B) e rmul!(A,B) ?

Não sou muito fã de ter A , B e C em pedidos diferentes.

A notação mul!(α, A, B, β, C) de SparseArrays.jl foi "finalizada" como a sintaxe oficial?

Eu diria que não. Originalmente, gostei da ideia de seguir o BLAS (e a ordem também correspondeu à forma como isso é geralmente escrito matematicamente), mas agora acho que faz sentido apenas adicionar os argumentos de escala como quarto e quinto argumentos opcionais.

Então, apenas para esclarecer, você gostaria de argumentos opcionais no sentido

function mul!(C, A, B, α=1, β=0)
 ...
end

A outra opção seriam argumentos opcionais de palavra-chave

function mul!(C, A, B; α=1, β=0)
...
end

mas não tenho certeza se as pessoas estão muito felizes com Unicode.

Estou muito feliz com Unicode mas é verdade que tentamos sempre ter uma opção ascii e seria possível aqui. Os nomes α e β também não são super intuitivos, a menos que você conheça o BLAS, então acho que usar argumentos posicionais é a melhor solução aqui.

Na minha opinião, uma nomenclatura mais lógica seria deixar muladd!(A, B, C, α=1, β=1) mapear as várias rotinas BLAS que fazem multiplicação e adição . ( gemm como acima, mas também, por exemplo, axpy quando A é um escalar.)

A função mul! poderia então ter uma interface como mul!(Y, A, B, ...) recebendo um número arbitrário de argumentos (assim como * faz), desde que todos os resultados intermediários possam ser armazenados em Y. ( Um kwarg opcional pode especificar a ordem de multiplicação, com um padrão razoável)

mul!(Y, A::AbstractVecOrMat, B:AbstractVecOrMat, α::Number) teria a implementação padrão muladd!(A, B, Y, α=α, β=0) , assim como as outras permutações de duas matrizes / vetores e um escalar.

Outra votação para ter mul!(C, A, B, α, β) definido para matrizes densas. Isso permitiria escrever código genérico para matrizes densas e esparsas. Eu queria definir essa função no meu pacote não linear de mínimos quadrados, mas acho que esse é o tipo de pirataria.

Também fui tentado a escrever métodos mul!(C, A, B, α, β) para o pacote MixedModels e me envolver em um tipo de pirataria, mas seria muito melhor se tais métodos estivessem na classe LinearAlgebra pacote. Ter métodos para um muladd! genérico para esta operação também estaria bom para mim.

Sou a favor, mas acho que provavelmente deveria ter um nome diferente de apenas mul! . muladd! parece razoável, mas certamente estou aberto a sugestões.

Talvez mulinc! para multiplicação-incremento?

Talvez possamos ter algo como addmul!(C, A, B, α=1, β=1) ?

Esta não é uma forma de muladd! ? Ou a ideia por trás de chamá-lo de addmul! que ele modifica o argumento add em vez do argumento multiply? Alguém mudaria o argumento de multiplicação?

Observe que alteramos os elementos que não são os primeiros em alguns casos, por exemplo, lmul! e ldiv! , então poderíamos fazê-los na ordem "muladd" usual (ou seja, muladd!(A,B,C) ). A questão é que ordem α e β devem ir? Uma opção seria fazer a palavra-chave argumentos?

Não seria bom se você deixasse uma opção para os implementadores despacharem os tipos de escalares α e β? É fácil adicionar açúcares para os usuários finais.

Achei que já tivéssemos combinado mul!(C, A, B, α, β) com valores padrão para α , β . Estamos usando esta versão em https://github.com/JuliaLang/julia/blob/b8ca1a499ff4044b9cb1ba3881d8c6fbb1f3c03b/stdlib/SparseArrays/src/linalg.jl#L32 -L50. Acho que alguns pacotes também estão usando esse formulário, mas não me lembro qual no topo da minha cabeça.

Obrigado! Seria bom se isso fosse documentado.

Achei que já tivéssemos combinado mul!(C, A, B, α, β) com valores padrão para α , β .

SparseArrays o usa, mas não me lembro de ter sido discutido em lugar nenhum.

De certa forma, o nome muladd! é mais natural porque é uma multiplicação seguida por uma adição. No entanto, os valores padrão de α e β, muladd!(C, A, B, α=1, β=0) (observe que o padrão para β é zero, não um), transforme-o novamente em mul!(C, A, B) .

Parece ser um caso de pedantismo versus consistência chamar isso de mul! ou muladd! e eu acho que o caso do método existente em SparseArrays seria contra mul! . Encontro-me no curioso caso de argumentar em prol da consistência, apesar de minha citação favorita de Oscar Wilde: "Consistência é o último refúgio do sem imaginação."

De certa forma, o nome muladd! é mais natural porque é uma multiplicação seguida por uma adição. No entanto, os valores padrão de α e β, muladd!(C, A, B, α=1, β=0) (observe que o padrão para β é zero, não um), transforme-o novamente em mul!(C, A, B) .

Há uma exceção interessante quando C contém Infs ou NaNs: teoricamente, se β==0 , o resultado ainda deveria ser NaNs. Isso não acontece na prática porque o BLAS e nosso código de matriz esparsa verificam explicitamente β==0 seguida, substituem-no por zeros.

Você pode considerar que ter padrões de α=true, β=false uma vez que true e false são "fortes" 1 e 0 respectivamente, no sentido de que true*x é sempre x e false*x são sempre zero(x) .

lmul! também deve ter aquele comportamento excepcional: https://github.com/JuliaLang/julia/issues/28972

true e false são "fortes" 1 e 0 respectivamente, no sentido de que true*x é sempre x e false*x é sempre zero(x) .

Eu não sabia disso !:

julia> false*NaN
0.0

FWIW, estou muito feliz com a legibilidade da sintaxe LazyArrays.jl para esta operação:

y .= α .* Mul(A,x) .+ β .* y

Nos bastidores, ele cai para mul!(y, A, x, α, β) , para matrizes compatíveis com BLAS (em faixas e strided).

Eu não sabia disso!

É parte do que faz im = Complex(false, true) funcionar.

SparseArrays o usa, mas não me lembro de ter sido discutido em lugar nenhum.

Foi discutido acima em https://github.com/JuliaLang/julia/issues/23919#issuecomment -365463941 e implementado em https://github.com/JuliaLang/julia/pull/26117 sem quaisquer objeções. Não temos as versões α,β no caso denso, então o único lugar neste repo onde uma decisão teria um efeito imediato seria SparseArrays .

E quanto a LinearAlgebra.BLAS.gemm! ? Não deveria ser empacotado como mul! 5-ário também?

Deveria, mas ninguém o fez ainda. Existem muitos métodos em matmul.jl .

Foi discutido acima em # 23919 (comentário) e implementado em # 26117 sem quaisquer objeções.

Bem, considere esta minha objeção. Eu preferiria um nome diferente.

Por que seria um nome diferente? Tanto no caso denso quanto no esparso, o algoritmo básico faz a multiplicação e a adição.

Se dermos nomes diferentes a essas funções, teremos mul!(C,A,B) = dgemm(C,A,B,1,0) e muladd!(C,A,B,α, β) = dgemm(C,A,B,α, β) .

A única vantagem que eu vejo é se realmente dividir os métodos e salvar um if β==0 chamada no C = A*B caso.

Para sua informação, comecei a trabalhar nisso em # 29634 para adicionar a interface a matmul.jl . Espero terminar quando o nome e a assinatura forem decididos :)

Uma vantagem de muladd! seria que podemos ter muladd!(A, B, C) ternário (ou muladd!(C, A, B) ?) Com o α = β = true padrão (conforme mencionado na sugestão original https: //github.com/JuliaLang/julia/issues/23919#issuecomment-402953987). O método muladd!(A, B, C) é semelhante a muladd para Number s, então acho que é um nome mais natural, especialmente se você já conhece muladd .

@andreasnoack Parece que sua discussão anterior é sobre assinatura de método e preferência por argumentos posicionais em vez de argumentos de palavra-chave, não sobre o nome do método. Você tem objeções ao nome muladd! ? (A existência de mul! 5-ária em SparseArrays pode ser uma, mas definir o invólucro compatível com versões anteriores não é difícil.)

Ter ambos mul! e muladd! parece redundante quando o primeiro é apenas o último com valores padrão para α e β . Além disso, a parte add foi canonizada pelo BLAS. Se pudéssemos apresentar uma aplicação de álgebra linear genérica confiável para muladd! , eu gostaria de ouvir sobre isso, mas, caso contrário, prefiro evitar a redundância.

Além disso, eu preferiria fortemente que mantivéssemos a propriedade de zero forte de false separada da discussão de mul! . IMO, qualquer valor zero de β deve ser forte como no BLAS e como nos cinco argumentos atuais mul! métodos. Ou seja, esse comportamento deve ser consequência de mul! e não do tipo β . A alternativa seria difícil de trabalhar. Por exemplo, mul!(Matrix{Float64}, Matrix{Float64}, Matrix{Float64}, 1.0, 0.0) ~ poderia ~ não poderia usar o BLAS.

Não podemos mudar o que o BLAS faz, mas _requerendo_ comportamento de zero forte para floats significa que toda implementação precisará de um branch para verificar o zero.

Se pudéssemos apresentar uma aplicação de álgebra linear genérica confiável para muladd!

@andreasnoack Com isso, suponho que você queira dizer "aplicação para _três-argumentos_ muladd! ", pois do contrário você não concordaria em incluir cinco argumentos mul! ?

Mas ainda posso dar um exemplo em que muladd!(A, B, C) seja útil. Por exemplo, se você deseja construir uma rede de "mundo pequeno", é útil ter uma soma "preguiçosa" de uma matriz em faixas e uma matriz esparsa. Você pode escrever algo como:

A :: SparseMatrixCSC
B :: BandedMatrix
x :: Vector  # input
y :: Vector  # output

# Compute `y .= (A .+ B) * x` efficiently:
fill!(y, 0)
muladd!(x, A, y)  # y .+= A * x
muladd!(x, B, y)  # y .+= B * x

Mas não me importo de escrever true s manualmente, pois posso simplesmente embrulhá-lo para meu uso. Ter a função de cinco argumentos como uma API documentada estável é o objetivo mais importante aqui.

Voltando ao ponto:

Ter ambos mul! e muladd! parece redundante quando o primeiro é apenas o último com valores padrão para α e β .

Mas temos alguns * implementados em termos de mul! com o "valor padrão" do array de saída inicializado adequadamente. Acho que existem exemplos de "atalhos" em Base e bibliotecas padrão? Acho que faz sentido ter mul! e muladd! , embora mul! seja apenas um atalho de muladd! .

Eu preferiria fortemente que mantivéssemos a propriedade de zero forte de false separada da discussão de mul!

Concordo que seria construtivo focar na discussão do nome da versão de cinco argumentos da multiplicação-adição primeiro ( mul! vs muladd! ).

Não fiz um bom trabalho quando pedi um caso de uso genérico em que você precisava de muladd para trabalhar genericamente em matrizes e números. A versão numérica seria muladd sem o ponto de exclamação, então o que eu perguntei realmente não fazia sentido.

Seu exemplo poderia ser escrito apenas como

mul!(y, A, x, 1, 1)
mul!(y, B, x, 1, 1)

então ainda não vejo necessidade de muladd! . Acha que esse caso é tão comum que escrever 1, 1 é muito prolixo?

Mas temos alguns * implementados em termos de mul! com o "valor padrão" do array de saída inicializado adequadamente. Acho que existem exemplos de "atalhos" em Base e bibliotecas padrão?

Eu não entendo este. Você poderia tentar elaborar? Quais são os atalhos de que você está falando aqui?

então ainda não vejo a necessidade de muladd! . Acha que esse caso é tão comum que escrever 1, 1 é muito prolixo?

Acho que muladd! também é mais descritivo quanto ao que realmente faz (embora talvez devesse ser addmul! ).

Não tenho problemas com o nome muladd! . Primeiramente, não acho que devamos ter funções para isso e, secundariamente, não acho que vale a pena substituir mul! em favor de muladd! / addmul! .

Acha que esse caso é tão comum que escrever 1, 1 é muito prolixo?

Não. Estou totalmente bem em chamar a função de cinco argumentos, desde que seja uma API pública. Eu apenas tentei dar um exemplo em que preciso apenas da versão de três argumentos (como pensei que fosse seu pedido).

Quais são os atalhos de que você está falando aqui?

https://github.com/JuliaLang/julia/blob/f068f21d6099632bd5543ad065d5de96943c9181/stdlib/LinearAlgebra/src/matmul.jl#L140 -L143

Acho que * definido aqui pode ser considerado um atalho de mul! . É "apenas" mul! com um valor padrão. Então, por que não deixar mul! ser muladd! / addmul! com valores padrão?

Existem rmul! e lmul! definidos como "atalhos" semelhantes também:

https://github.com/JuliaLang/julia/blob/f068f21d6099632bd5543ad065d5de96943c9181/stdlib/LinearAlgebra/src/triangular.jl#L478 -L479

descontinuando mul!

Achei que a discussão era sobre adicionar uma nova interface ou não. Se precisarmos descontinuar mul! para adicionar uma nova API, não acho que valha a pena.

Os principais argumentos que consigo pensar são:

  • conceitualmente, a forma de 5 argumentos faz mais do que apenas "multiplicar" e transmite isso de forma mais clara.
  • você pode escrever addmul!(C, A, B) vez de mul!(C,A,B,1,1) ou mul!(C,A,B,true,true) .

Acho que * definido aqui pode ser considerado um atalho de mul! . É "apenas" mul! com um valor padrão. Então, por que não deixar mul! ser muladd! / addmul! com valores padrão?

Porque * é a maneira padrão de multiplicar matrizes e como a maioria dos usuários faria isso. Em comparação, muladd! não estaria nem perto de * em uso. Além disso, é até mesmo um operador existente, enquanto muladd! / addmul! seria uma nova função.

Não pense que rmul! e lmul! encaixam neste padrão porque eles geralmente não são versões de valor padrão de métodos mul! fora do lugar.

Simon resume os benefícios muito bem no post logo acima. A questão é se os benefícios são grandes o suficiente para justificar uma função extra de renomeação (o que significa depreciação de mul! ). É onde discordamos. Acho que não vale a pena.

Quando você diz que não vale a pena renomear, você leva em consideração que a API não é totalmente pública? Com isso, quero dizer que não está na documentação de Julia.

Eu sei que LazyArrays.jl (e outros pacotes?) Já o usa, então seguir cegamente o semver não seria bom. Mesmo assim, não é tão público quanto outras funções.

mul! é exportado de LinearAlgebra e amplamente usado, portanto, definitivamente teremos que descontinuá-lo neste ponto. É uma pena que não tenhamos discutido quando A_mul_B! se tornou mul! ou pelo menos antes de 0.7 porque seria um momento muito melhor para renomear a função.

Que tal usar mul! agora e atualizar o nome de LinearAlgebra v2.0 quando podemos atualizar os stdlibs separadamente?

LazyArrays.jl não usa mul! pois não é flexível para muitos tipos de matrizes (e dispara um bug de lentidão do compilador quando você sobrescreve StridedArray s). Dá uma construção alternativa do formulário

y .= Mul(A, x)

que considero mais descritivo. O análogo de 5 argumentos é

y .= a .* Mul(A, x) .+ b .* y

Eu argumentaria a favor de descontinuar mul! e passar para a abordagem LazyArrays.jl em LinearAlgebra.jl, mas isso vai ser um caso difícil de fazer.

LowRankApprox.jl usa mul! , mas posso alterá-lo para usar a abordagem LazyArrays.jl e, assim, evitar o bug do compilador.

ESTÁ BEM. Achei que houvesse apenas duas propostas. Mas, aparentemente, existem cerca de três propostas ?:

  1. três e cinco argumentos mul!
  2. três e cinco argumentos muladd!
  3. três argumentos mul! e cinco argumentos muladd!

( muladd! pode ser chamado de addmul! )

Eu estava pensando que estamos comparando 1 e 3. Meu entendimento agora é que @andreasnoack está comparando 1 e 2.

Eu diria que 2 não é uma opção, já que mul! três argumentos é uma API pública e amplamente usada. O que eu quis dizer com "a API não é completamente pública" foi que cinco argumentos mul! não estão documentados .

Sim, meu plano era manter mul! (como uma forma de 3 arg e possivelmente 4 arg). Eu acho que vale a pena, pois 3 arg mul! e addmul! teriam um comportamento diferente, ou seja, dado addmul!(C, A, B, α, β) , teríamos:

mul!(C, A, B) = addmul!(C, A, B, 1, 0)
mul!(C, A, B, α) = addmul!(C, A, B, α, 0)
addmul!(C, A, B) = addmul!(C, A, B, 1, 1)
addmul!(C, A, B, α) = addmul!(C, A, B, α, 1)

No entanto, você pode não querer realmente implementá-los desta forma na prática, por exemplo, pode ser mais simples apenas o 4-arg mul! e addmul! separadamente e definir o 5-arg addmul! as:

addmul!(C, A, B, α, β) = addmul!(C .= β .* C, A, B, α)

Colisão!

No entanto, você pode não querer realmente implementá-los desta forma na prática, por exemplo, pode ser mais simples apenas o mul de 4 arg! e addmul! separadamente e defina o addmul de 5 arg! Como:
addmul!(C, A, B, α, β) = addmul!(C .= β .* C, A, B, α)

Por que não fazer isso da maneira ideal imediatamente? O objetivo de não fazer isso é que você só precisa visitar os elementos de C uma vez, o que é definitivamente mais eficiente para matrizes grandes. Além disso, mal posso acreditar que o código seria mais longo definindo apenas o 5-arg addmul! versus o 4-arg mul! e addmul! separadamente.

Para sua informação, modifiquei a implementação de LinearAlgebra de _generic_matmatmul! para obter 5 argumentos em LazyArrays: https://github.com/JuliaArrays/LazyArrays.jl/blob/8a50250fc6cf3f2402758088227769cf2de2e053/src/linalg/Lasm70.jlasm70

Aqui é chamado de:

materialize!(MulAdd(α, A, b, β, c)) 

mas o código real (em tiled_blasmul! ) seria fácil de traduzir de volta para LinearAlgebra.

O que pode ser feito para tentar acelerar esse processo? As coisas nas quais estou trabalhando realmente se beneficiariam de uma API de multiplicação de matriz unificada com mul + add no local

A última versão de Strided.jl agora também suporta 5 argumentos mul!(C,A,B,α,β) , despachando para BLAS quando possível, e usando sua própria implementação (multithread) caso contrário.

@Jutho ótimo pacote! existe um roteiro para o que vem a seguir? o plano pode ser eventualmente fundir-se com LinearAlgebra?

Essa nunca foi minha intenção, mas não me oponho, se isso for solicitado em algum momento. No entanto, acho que meu uso liberal de funções @generated (embora haja apenas uma) na funcionalidade mapreduce pode não ser adequado para o Base.

Meu roteiro pessoal: este é principalmente um pacote de baixo nível para ser usado por pacotes de nível superior, ou seja, a nova versão do TensorOperations e algum outro pacote no qual estou trabalhando. No entanto, um pouco mais de suporte para álgebra linear básica seria bom (por exemplo, aplicar norm a StridedView atualmente cai para uma implementação bastante lenta de norm no Julia Base). E se eu tiver tempo e aprender a trabalhar com GPUs, tente implementar um igualmente geral mapreducekernel para GPUArray s.

Acho que o consenso até agora é:

  1. Devemos manter mul!(C, A, B)
  2. Precisamos de _algumas_ função de 5 argumentos para multiplicação inplace-add C = αAB + βC

Eu sugiro primeiro focar em qual deve ser o nome da função de 5 argumentos e discutir API adicional mais tarde (como 3 e 4 argumentos addmul! ). Mas este é o "recurso" que recebemos de _não_ usando mul! então é difícil não misturar.

@andreasnoack É a sua preocupação com depreciação / renomeando resolvido por @simonbyrne 's comentário acima https://github.com/JuliaLang/julia/issues/23919#issuecomment -431046516? Acho que não há necessidade de depreciação.

Para sua informação, acabei de concluir a implementação # 29634. Agradeço se alguém familiarizado com LinearAlgebra puder analisá-lo.

Acho que é mais simples e melhor nomear tudo mul! . Também evita a descontinuação. Se realmente quisermos um nome diferente, muladd é melhor.

Algo mais a ser levado em consideração ao discutir a API mul! :

Quando scale! foi embora e foi absorvido pela transição 0,6 -> 0,7, fiquei um pouco triste porque para mim, a multiplicação escalar (uma propriedade dos espaços vetoriais) era muito diferente de multiplicar os objetos entre eles (uma propriedade das álgebras ) No entanto, abracei totalmente a abordagem mul! e aprecio muito a capacidade de rmul!(vector,scalar) e lmul!(scalar,vector) quando a multiplicação de escalares não é comutativa. Mas agora estou cada vez mais incomodado com o nome não-Juliano de duas outras operações de espaço vetorial no local: axpy! e sua generalização axpby! . Eles também poderiam ser absorvidos por mul! / muladd! / addmul! . Embora seja um pouco estranho, se um dos dois fatores em A*B já for um escalar, não há necessidade de um fator escalar adicional α .
Mas talvez então, em analogia com

mul!(C, A, B, α, β)

também pode haver um

add!(Y, X, α, β)

para substituir axpby! .

@andreasnoack é sua preocupação com depreciação / renomeando resolvido por @simonbyrne 's comentário acima # 23919 (comentário)? Acho que não há necessidade de depreciação.

Consulte o último parágrafo de https://github.com/JuliaLang/julia/issues/23919#issuecomment -430952179. Ainda acho que a introdução de uma nova função não vale a pena. Se fizermos isso de qualquer maneira, acho que deveríamos descontinuar o atual 5 argumentos mul! .

@Jutho Acho que renomear acp(b)y! para add! seria uma boa ideia.

Veja o último parágrafo de # 23919 (comentário) . Ainda acho que a introdução de uma nova função não vale a pena.

Sim, eu li e respondi que cinco argumentos mul! não foram documentados e não faziam parte da API pública. Portanto, _técnicamente_ não há necessidade de descontinuação. Veja o último parágrafo de https://github.com/JuliaLang/julia/issues/23919#issuecomment -430975159 (Claro, seria gentil ter uma suspensão de uso de qualquer maneira, então eu já implementei em # 29634.)

Aqui, estou assumindo que a declaração da API pública devido à documentação de uma assinatura (por exemplo, mul!(C, A, B) ) não se aplica a outras assinaturas (por exemplo, mul!(C, A, B, α, β) ). Se _não_ for o caso, acho que Julia e seu stdlib estão expondo muitos detalhes internos. Por exemplo, aqui está a assinatura documentada de Pkg.add

https://github.com/JuliaLang/julia/blob/0d713926f85dfa3e4e0962215b909b8e47e94f48/stdlib/Pkg/src/Pkg.jl#L76 -L79

Considerando que a definição real é

https://github.com/JuliaLang/julia/blob/0d713926f85dfa3e4e0962215b909b8e47e94f48/stdlib/Pkg/src/API.jl#L69 -L70

https://github.com/JuliaLang/julia/blob/0d713926f85dfa3e4e0962215b909b8e47e94f48/stdlib/Pkg/src/API.jl#L27 -L33

Se a existência da documentação de pelo menos uma assinatura de Pkg.add implica que outras assinaturas são API públicas, Pkg.jl não pode remover o comportamento devido aos detalhes de implementação sem alterar a versão principal, por exemplo: Pkg.add(...; mode = :develop) executa Pkg.develop(...) ; todos os argumentos de palavra-chave para Context! são suportados (o que pode realmente ser pretendido).

Mas esta é apenas minha impressão de qualquer maneira. Você considera que mul!(C, A, B, α, β) foi tão público quanto mul!(C, A, B) ?

Acho que estamos conversando. O que estou dizendo é que (ainda) não acho que vale a pena introduzir outra função. Daí minha referência ao meu comentário anterior. Isso é separado da discussão sobre a depreciação dos cinco argumentos mul! .

No entanto, se decidirmos adicionar outra função, acho que seria melhor descontinuar cinco argumentos mul! vez de apenas quebrá-los. Claro, ele não é tão comumente usado como mul! três argumentos, mas por que não desaprová-lo em vez de apenas quebrá-lo?

Isso é separado da discussão sobre a depreciação dos cinco argumentos mul! .

Minha interpretação do último parágrafo do seu comentário https://github.com/JuliaLang/julia/issues/23919#issuecomment -430952179 foi que você reconheceu os benefícios @simonbyrne listados https://github.com/JuliaLang/julia/issues / 23919 # issuecomment -430809383 para uma nova função de cinco argumentos, mas considerou que eles eram menos valiosos em comparação com a manutenção da _ API pública_ (como você mencionou "renomear" e "depreciação"). É por isso que pensei se considerar se os cinco argumentos mul! foram públicos ou não era importante.

Mas você também mencionou a justificativa de ter "uma função extra" que, suponho, é a que está se referindo agora. Você está argumentando que os cálculos _C = AB_ e _C = αAB + βC_ são semelhantes o suficiente para que o mesmo nome possa descrever ambos? Na verdade, eu discordo, pois pode haver outras maneiras de generalizar três argumentos mul! : por exemplo, por que não mul!(y, A₁, A₂, ..., Aₙ, x) para _y = A₁ A₂ ⋯ Aₙ x_ https://github.com/JuliaLang/julia / issues / 23919 # issuecomment -402953987?

por que não desaprová-lo em vez de apenas quebrá-lo?

Como eu disse nos comentários anteriores, eu concordo que descontinuar os cinco argumentos mul! é a coisa certa a se fazer _se_ introduzíssemos outra função. Este código já existe no meu PR # 29634.

Você está argumentando que os cálculos C = AB e C = αAB + βC são semelhantes o suficiente para que o mesmo nome possa descrever ambos?

Sim, visto que o primeiro é apenas o último com β=0 . É justo argumentar que muladd! / addmul! é um nome mais preciso para C = αAB + βC mas chegar lá exigiria a introdução de outra função de multiplicação de matriz ( muladd! / addmul! ) ou renomear mul! e não acho que valha a pena agora. Se isso tivesse acontecido na primavera, teria sido mais fácil considerar uma mudança.

Na verdade, eu discordo, pois pode haver outras maneiras de generalizar o mul de três argumentos !:

Julia por acaso definiu os métodos de multiplicação de matrizes no local sem os argumentos α e β mas a tradição de multiplicação de matrizes é realmente baseada em BLAS-3 e aí a função geral de multiplicação de matrizes é C = αAB + βC .

renomeando mul!

Você quer dizer renomeá-lo em stdlib ou em módulo / código de usuário downstream? Se você se refere ao anterior, ele já está feito (para LinearAlgebra e SparseArrays) em # 29634, então não acho que você precise se preocupar com isso. Se você quer dizer o último, acho que novamente se resume a uma discussão pública ou não.

a tradição de multiplicação de matrizes é realmente baseada no BLAS-3

Mas Julia já se afastou da convenção de nomenclatura do BLAS. Então, não seria bom ter um nome mais descritivo?

Você quer dizer renomeá-lo em stdlib ou em módulo / código de usuário downstream?

29634 não renomeia a função mul! . Ele adiciona a nova função addmul! .

Mas Julia já se afastou da convenção de nomenclatura do BLAS.

Não estou falando sobre a nomenclatura. Pelo menos não exatamente porque o Fortran 77 tem algumas limitações que não temos em termos de nomes de função e despacho. Estou falando sobre o que está sendo calculado. A função geral de multiplicação de matrizes em BLAS-3 calcula C = αAB + βC e em Julia, foi mul! (fka A_mul_B! ).

Então, não seria bom ter um nome mais descritivo?

Seria, e eu já disse isso várias vezes. O problema é que não é muito mais agradável termos duas funções de multiplicação de matrizes que basicamente fazem a mesma coisa.

29634 não renomeia a função mul! . Ele adiciona a nova função addmul! .

O que eu quis dizer é que mul! cinco argumentos foi renomeado para addmul! .

O problema é que não é muito mais agradável termos duas funções de multiplicação de matrizes que basicamente fazem a mesma coisa.

Eu sinto que se eles são basicamente iguais ou não é um tanto subjetivo. Acho que _C = αAB + βC_ e _Y = A₁ A₂ ⋯ Aₙ X_ são generalizações matematicamente válidas de _C = AB_. A menos que _C = αAB + βC_ seja a generalização única, não acho que o argumento seja forte o suficiente. Também depende se você conhece a API BLAS e não tenho certeza se esse é o conhecimento básico para usuários típicos de Julia.

Além disso, _C = AB_ e _C = αAB + βC_ são computacionalmente muito diferentes, pois o conteúdo de C é usado ou não. É um parâmetro apenas de saída para o primeiro e um parâmetro de entrada-saída para o último. Acho que essa diferença merece uma dica visual. Se eu vejo mul!(some_func(...), ...) e mul! tem a forma de cinco argumentos, eu tenho que contar o número de argumentos (o que é difícil quando eles são os resultados da chamada de função, já que você precisa combinar parênteses) para ver se some_func faz algum cálculo ou apenas uma alocação. Se tivermos addmul! então posso esperar imediatamente que some_func em mul!(some_func(...), ...) apenas faça alocação.

Eu sinto que se eles são basicamente iguais ou não é um tanto subjetivo. Eu acho que C = αAB + βC e Y = A₁ A₂ ⋯ Aₙ X são generalizações matematicamente válidas de C = AB. A menos que C = αAB + βC seja a generalização única, não acho que o argumento seja forte o suficiente.

Pode não ser a generalização única, no entanto, é uma generalização que pode ser calculada a um custo quase idêntico e que forma uma primitiva útil para construir outros algoritmos de álgebra linear. Em muitas ocasiões, eu quis ter um beta diferente de zero ao implementar vários algoritmos relacionados à álgebra linear e sempre tive que voltar para BLAS.gemm! . De qualquer forma, outras generalizações como a que você mencionou não podem ser calculadas de uma só vez sem temporários intermediários, portanto, uma versão no local é muito menos útil. Além disso, eles geralmente não são tão úteis quanto uma operação primitiva.

Também depende se você conhece a API BLAS e não tenho certeza se esse é o conhecimento básico para usuários típicos de Julia.

Enquanto os argumentos padrão α=1 e β=0 ainda estiverem em vigor, os três arg mul! farão o que qualquer usuário de Julia razoavelmente esperaria sem o fundo do BLAS. Para as opções mais avançadas, é preciso consultar o manual, como se faria com qualquer idioma e qualquer função. Além disso, esta única chamada mul! não apenas substitui gemm mas também gemv e trmv (que estranhamente não tem α e β parâmetros na API BLAS) e provavelmente muitos outros.

Eu concordo que BLAS-3 é a generalização certa em termos de computação e compõe muito bem. Estou trazendo outra generalização possível apenas porque acho que não é "exclusivo o suficiente" para justificar o uso do mesmo nome. Consulte também o argumento somente saída vs argumento de entrada-saída no último parágrafo de https://github.com/JuliaLang/julia/issues/23919#issuecomment -441267056. Acho que um nome diferente torna a leitura / revisão do código mais fácil.

Além disso, esta única chamada mul! não apenas substitui gemm mas também gemv e trmv (que estranhamente não tem α e β parâmetros na API BLAS) e provavelmente muitos outros.

Sim, já implementado no # 29634 e está pronto para ser usado assim que o nome for decidido (e for revisado)!

Estou tendo dificuldade em acompanhar esta conversa (é um pouco longa e extensa ... desculpe!), A proposta principal é algo como mul!(C, A, B; α=true, β=false) ?

Não creio que o argumento de palavra-chave para α e β esteja na mesa. Por exemplo, @andreasnoack descartou argumentos de palavra-chave em https://github.com/JuliaLang/julia/issues/23919#issuecomment -365762889. @simonbyrne mencionou argumentos de palavra-chave em https://github.com/JuliaLang/julia/issues/23919#issuecomment -426881998, mas sua sugestão mais recente https://github.com/JuliaLang/julia/issues/23919#issuecomment -431046516 é posicional argumentos.

Ainda não decidimos o nome (ou seja, mul! vs addmul! vs muladd! ) e acho que esse é o tópico central (ou pelo menos é meu desejo).

Como você geralmente resolve esse tipo de controvérsia? Votação? Triagem?

a proposta principal é algo como mul! (C, A, B; α = verdadeiro, β = falso)?

Eu gosto disso, mas sem os kwargs.

Não vi a palavra-chave. Também estou hesitante em adicionar palavras-chave Unicode. Acho que argumentos posicionais com esses valores padrão estão bem. Quanto à próxima votação, a minha está em mul! . Eu acho que esta é uma generalização de mul! que é suficientemente específica e útil para não precisar de um novo nome.

Só para coletar dados (pelo menos no momento), vamos fazer a votação:

Qual é o seu nome de função favorito para _C = αAB + βC_?

  • : +1: mul!
  • : -1: addmul!
  • : sorriso: muladd!
  • : tada: outra coisa

Para mim, addmul! parece descrever (A+B)C vez de AB + C .

No começo eu votei em mul! então olhei para a operação e pensei "ela está fazendo uma multiplicação e depois uma adição, obviamente devemos chamá-la de muladd! . Agora não consigo pensar em chamá-la O fato de estar no lugar é claramente indicado por ! e a parte do dimensionamento parece adequada para argumentos de palavra-chave.

ele está fazendo uma multiplicação e, em seguida, uma adição, obviamente devemos chamá-lo de muladd!

Somente se você usar o valor padrão β=true , mas para qualquer outro valor é apenas algo mais geral novamente. Então, de que adianta não chamá-lo de mul! , onde qualquer outro valor além do valor padrão β=false também fornece algo mais geral? E como você ordenaria os argumentos, comparando com muladd(x,y,z) = x*y + z ? Vai ficar um pouco confuso, não?

Acho que muladd! tem a desvantagem de soar descritivo quando não é: um nome descritivo seria algo como scalemuladd! para mencionar a parte do dimensionamento.

Portanto, prefiro mul! , pois é indefinido o suficiente para não gerar expectativas.

Dito isso, eu chamo a versão preguiçosa em LazyArrays.jl de MulAdd .

Eu prefiro muladd! a mul! porque é bom manter distinta uma função que nunca usa o valor de C ( mul! ) de uma função que o usa ( muladd! ).

  • Tecnicamente é uma multiplicação de matrizes: [AC] * [Bα; Iβ] ou, veja o comentário abaixo, [αA βC] * [B; EU]
  • ~ Já temos um 5-arg mul! para matrizes esparsas, o mesmo para linalg denso seria consistente ~ (não é um argumento novo)

Portanto, eu seria a favor de chamá-lo de mul! .

  • Tecnicamente é uma multiplicação de matriz: [AC] * [Bα; Iβ]

... se eltype tem multiplicação comutativa

... se eltype for comutativo.

IIRC de uma discussão com @andreasnoack Julia apenas define gemm / gemv como y <- A * x * α + y * β porque isso faz mais sentido.

@haampie Isso é bom saber! Como eu implementei o contrário em # 29634.

Isso é de ajuda limitada, mas

       C = α*A*B + β*C

é a melhor maneira que posso encontrar para expressar a operação e, portanto, talvez uma macro <strong i="8">@call</strong> C = α*A*B + β*C ou <strong i="10">@call_specialized</strong> ... ou algo parecido seria uma interface natural - também para situações semelhantes. Então, a função subjacente pode ser chamada de qualquer coisa.

@mschauer LazyArrays.jl de @dlfivefifty tem uma ótima sintaxe para chamar 5 argumentos mul! como sua sintaxe (e mais!).

Acho que precisamos estabelecer a API baseada em função primeiro e para que os autores do pacote possam começar a sobrecarregá-la para suas matrizes especializadas. Então a comunidade Julia pode começar a experimentar sugestões para chamá-lo.

Apenas se você usar o valor padrão β=true , mas para qualquer outro valor é apenas algo mais geral novamente. Então, de que adianta não chamá-lo de mul! , onde qualquer outro valor além do valor padrão β=false também fornece algo mais geral? E como você ordenaria os argumentos, comparando com muladd(x,y,z) = x*y + z ? Vai ficar um pouco confuso, não?

Claro que há algum dimensionamento lá, mas os "ossos" da operação são claramente multiplicados e adicionados. Eu também aceitaria muladd!(A, B, C, α=true, β=false) para corresponder à assinatura de muladd . Teria de ser documentado, é claro, mas nem é preciso dizer. Isso meio que me faz desejar que muladd tenha pegado a parte aditiva primeiro, mas o navio navegou naquela.

E como você ordenaria os argumentos, em comparação com muladd(x,y,z) = x*y + z ? Vai ficar um pouco confuso, não?

Esta é a razão pela qual prefiro addmul! a muladd! . Podemos ter certeza de que a ordem dos argumentos não tem nada a ver com o escalar muladd . (Embora eu prefira muladd! a mul! )

FWIW aqui está um resumo dos argumentos até agora. (Eu tentei ser neutro, mas sou pro- muladd! / addmul! então mantenha isso em mente ...)

A principal discordância é que se _C = AB_ e _C = αAB + βC_ são ou não diferentes o suficiente para dar um novo nome ao último.

Eles são semelhantes o suficiente porque ...

  1. É BLAS-3 e muito bem composto. Portanto, _C = αAB + βC_ é uma generalização óbvia de _C = AB_ (https://github.com/JuliaLang/julia/issues/23919#issuecomment-441246606, https://github.com/JuliaLang/julia/issues/ 23919 # issuecomment-441312375, etc.)

  2. _ " muladd! tem a desvantagem de soar descritivo quando não é: um nome descritivo seria algo como scalemuladd! para mencionar a parte da escala." _ --- https://github.com/ JuliaLang / julia / issues / 23919 # issuecomment -441819470

  3. _ "Tecnicamente, é uma multiplicação de matriz: [AC] * [Bα; Iβ]" _ --- https://github.com/JuliaLang/julia/issues/23919#issuecomment -441825009

Eles são diferentes o suficiente porque ...

  1. _C = αAB + βC_ é mais do que multiplicar (https://github.com/JuliaLang/julia/issues/23919#issuecomment-430809383, https://github.com/JuliaLang/julia/issues/23919#issuecomment-427075792, https://github.com/JuliaLang/julia/issues/23919#issuecomment-441813176, etc.).

  2. Pode haver outras generalizações de mul! como Y = A₁ A₂ ⋯ Aₙ X (https://github.com/JuliaLang/julia/issues/23919#issuecomment-402953987 etc.)

  3. Parâmetro input-only vs input-output: é confuso ter uma função que usa os dados em C base no número de argumentos (https://github.com/JuliaLang/julia/issues/23919#issuecomment -441267056, https://github.com/JuliaLang/julia/issues/23919#issuecomment-441824982)

Outra razão pela qual mul! é melhor porque ...:

  1. A matriz esparsa já o possui. Portanto, é bom para compatibilidade com versões anteriores. Contra-argumento: cinco argumentos mul! não estão documentados, portanto, não precisamos considerá-lo como uma API pública.

e por que muladd! / addmul! é melhor porque ...:

  1. Podemos ter diferentes "funções úteis" de três ou quatro argumentos para mul! e muladd! / addmul! separadamente (https://github.com/JuliaLang/julia/issues / 23919 # issuecomment-402953987, https://github.com/JuliaLang/julia/issues/23919#issuecomment-431046516, etc.). Contra-argumento: escrever mul!(y, A, x, 1, 1) não é muito prolixo em comparação com mul!(y, A, x) (https://github.com/JuliaLang/julia/issues/23919#issuecomment-430674934, etc.)

Obrigado pelo resumo objetivo @tkf

Eu também ficaria bem com muladd! (A, B, C, α = true, β = false) para corresponder à assinatura de muladd.

Espero que para uma função chamada mulladd! o padrão seja β=true . Ainda assim, acho que esta ordem de argumentos, que é ditada por muladd , será muito confusa em relação a mul!(C,A,B)

Talvez eu esteja enganado, mas eu acho que a maioria das pessoas / aplicativos / código de alto nível (que ainda não estão satisfeitas apenas com o operador de multiplicação * ) precisam de mul! . A capacidade de também misturar βC , com β=1 ( true ) ou de outra forma, será usada em código de nível inferior, por pessoas que sabem que a API BLAS para multiplicação de matrizes permite esta. Eu imagino que essas pessoas procurariam por essa funcionalidade em mul! , que é a interface Julia estabelecida para gemm , gemv , ... Adicionando um novo nome (o que confunde ordem oposta do argumento) não parece valer a pena; Não consigo ver o ganho?

Eu imagino que essas pessoas procurariam por essa funcionalidade em mul! , que é a interface Julia estabelecida para gemm , gemv , ... Adicionando um novo nome (o que confunde ordem oposta do argumento) não parece valer a pena; Não consigo ver o ganho?

Acho que a descoberta não é um grande problema, pois podemos simplesmente mencionar muladd! em mul! docstring. Aqueles que são qualificados o suficiente para conhecer o BLAS sabem onde procurar uma API, certo?

Com relação aos argumentos posicionais versus palavras-chave: Não é discutido aqui ainda, mas acho que C = αAB + βC com α sendo uma matriz diagonal pode ser implementado de forma tão eficiente e fácil quanto α escalar. Tal extensão requer que possamos despachar no tipo α que é impossível com o argumento de palavra-chave.

Além disso, para eltype não comutativo, você pode querer calcular C = ABα + Cβ eficiente chamando muladd!(α', B', A', β', C') (ordem hipotética do argumento). Pode exigir que você seja capaz de despachar em wrappers preguiçosos Adjoint(α) e Adjoint(β) . (Eu não uso números não comutativos em Julia pessoalmente, então isso provavelmente é muito hipotético.)

Eu concordo com o ponto de @Jutho de que esta função multiplicação-adição é uma API de baixo nível para programadores qualificados, como implementadores de bibliotecas. Acho que a extensibilidade tem uma alta prioridade para esta API e o argumento posicional é o caminho a percorrer.

Outro argumento para evitar o argumento de palavra-chave é o que @andreasnoack disse antes https://github.com/JuliaLang/julia/issues/23919#issuecomment -365762889:

Os nomes α e β também não são super intuitivos, a menos que você conheça o BLAS

@tkf , claro, meu argumento é: o número de usos reais de β != 0 será menor do que β == 0 , e aqueles que precisam dele não ficarão surpresos ao descobrir que isso é um pouco mais geral comportamento sob mul! . Portanto, não vejo ganho em separar isso com um novo nome, especialmente porque a ordem do argumento é confusa (com muladd! pelo menos). Se for um método novo, também simpatizo com seu argumento para addmul! .

aqueles que precisam não ficarão surpresos ao descobrir este comportamento um pouco mais geral em mul! .

Eu concordo com este ponto.

Portanto, não vejo vantagem em separar isso com um novo nome,

A menos que você veja alguns danos, acho que é um ganho global, pois há outras pessoas vendo benefícios.

especialmente porque a ordem do argumento está confusa (com muladd! pelo menos)

Acho que você consideraria isso um dano e entendi. Só que acho que outros benefícios de muladd! / addmul! são mais importantes.

Acho que você consideraria isso um dano e entendi. Acho que outros benefícios para o muladd! / Addmul! são mais importantes.

Esse é realmente o problema, junto com o fato de que mul! sempre foi o único ponto de entrada em várias operações BLAS relacionadas à multiplicação, mesmo que seja restrito por não dar acesso total a α e β. E agora com muladd! , haveria dois pontos de entrada diferentes, dependendo apenas de uma ligeira diferença na operação solicitada que pode ser facilmente capturada por um argumento (e, de fato, que é capturada por um argumento na API BLAS) . Acho que foi um erro da Julia, em primeiro lugar, não oferecer acesso total à API do BLAS (então, obrigado por corrigir esse @tkf). Apesar de ser uma convenção de nomenclatura antiga e horrível de fortran, acho que esses caras sabiam por que estavam fazendo as coisas dessa maneira. Mas, da mesma forma, acho que esta família de operações (ou seja, a família de 2 parâmetros de operações parametrizadas por α e β) pertence junto em um único ponto de entrada, como no BLAS.

O contra-argumento mais válido na minha opinião é a diferença entre se os dados originais em C serão ou não acessados. Mas dado o fato de Julia ter adotado a multiplicação por false como uma forma de garantir um resultado zero, mesmo quando o outro fator é NaN , acho que isso também está resolvido. Mas talvez esse fato precise ser comunicado / documentado melhor (já faz um tempo que não li a documentação), e também só recentemente soube disso. (É por isso que em KrylovKit.jl, eu exijo a existência de um método fill! , para inicializar um tipo de usuário do tipo vetor arbitrário com zeros. Mas agora eu sei que posso apenas rmul!(x,false) vez disso, portanto, não preciso impor que fill! seja implementado).

Acho que outros benefícios para o muladd! / Addmul! são mais importantes.

Então, deixe-me reverter a pergunta: quais são os outros benefícios de ter um novo método? Li seu resumo novamente, mas vejo apenas o ponto de acessar C , que acabei de comentar.

Mencionei à minha esposa esta manhã que houve uma longa conversa de dois meses na comunidade de Julia sobre o nome de uma operação. Ela sugeriu que fosse chamado de "Fred!" - sem acrônimo, sem significado profundo, apenas um bom nome. Só estou divulgando isso em nome dela.

Que bom que ela incluiu um ponto de exclamação! 😄

Em primeiro lugar, apenas por precaução, deixe-me esclarecer que minha preocupação vem quase apenas da legibilidade do código, não da capacidade de escrita ou descoberta. Você escreve o código uma vez, mas lê muitas vezes.

quais são esses outros benefícios de ter um novo método?

Como você comentou, acho que o argumento do parâmetro output vs input-output é o mais importante. Mas esta é apenas uma das razões pelas quais eu acho que _C = αAB + βC_ é diferente de _C = AB_. Também acho que o simples fato de que eles são diferentes no sentido de que a primeira expressão é o "superconjunto" estrito do último precisa de uma indicação visual clara no código. Um nome diferente ajuda um programador intermediário (ou um pequeno programador avançado sem foco) a ler o código e perceber que ele está usando algo mais estranho do que mul! .

Acabei de verificar a enquete (você precisa clicar em "Carregar mais" acima) novamente e parece que alguns votos foram movidos de mul! para muladd! ? Da última vez que vi, mul! estava ganhando. Vamos registrar aqui antes que eles se mexessem: rindo:

  • mul! : 6
  • addmul! : 2
  • muladd! : 8
  • outra coisa: 1

Um pouco mais sério, ainda acho que esses dados não mostram que mul! ou muladd! são mais claros do que os outros. (Embora mostre que addmul! é uma minoria: sob :)

Parece que estamos presos. Como seguimos em frente?

Em vez disso, chame-o de gemm! ?

Basta chamá-lo de gema! em vez de?

Espero que seja uma piada ... a menos que você esteja propondo gemm!(α, A::Matrix, x::Vector, β, y::Vector) = gemv!(α, A, x, β, y) para o vetor de matriz *.

Podemos deixar a interface mul! que já existe (com matrizes esparsas) por enquanto para que possamos mesclar o PR e ter as melhorias, e nos preocupar se queremos adicionar muladd! em outro PR ?

Talvez já esteja claro para todos aqui, mas eu só queria enfatizar que a votação não é

  • mul! vs muladd!

mas

  • mul! vs ( mul! e muladd! )

ou seja, tendo duas funções de multiplicação mutantes em vez de uma única.

Decidi não postar mais, pois sempre que postei a favor de mul! , os votos pareceram passar de mul! para ( mul! e muladd! ).

No entanto, eu tenho uma pergunta? Se seguirmos com a maioria dos votos atuais e tivermos simultaneamente mul!(C,A,B) e muladd!(A,B,C,α=true,β=true) , e eu quero preparar um PR que substitua axpy! e axpby! um nome mais juliano add! , deveria ser add!(y, x, α=true, β=true) ou add!(x, y, α=true, β=true) (onde, para maior clareza, y é mutado). Ou alguma outra coisa?

Caso não seja óbvio, muladd!(A,B,C) violaria a convenção de que os argumentos mutados vão primeiro .

Podemos deixar a interface mul! que já existe

@jebej Acho que esse argumento de "compatibilidade com versões anteriores" é discutido extensivamente. Ainda assim, não convence ninguém (olhando a enquete, não sou só eu).

se preocupe se queremos adicionar muladd! em outro PR?

É ruim quebrar a API pública. Portanto, se dissermos mul! então é mul! para sempre (embora em teoria LinearAlgebra possa alterar sua versão principal para quebrar a API).

Quero preparar um PR que substitua axpy! e axpby! por um nome mais juliano add! , deve ser add!(y, x, α=true, β=true) ou add!(x, y, α=true, β=true)

@Jutho Obrigado, isso seria ótimo! Acho que escolher a ordem do argumento seria simples, uma vez que decidíssemos a assinatura de chamada da API multiply-add.

muladd!(A,B,C) violaria a convenção de que os argumentos mutados vão primeiro .

@simonbyrne Mas (como você já mencionou em https://github.com/JuliaLang/julia/issues/23919#issuecomment-426881998), lmul! e ldiv! mutate não primeiro argumento. Portanto, acho que não precisamos excluir muladd!(A,B,C,α,β) da escolha, mas sim contá-lo como um ponto negativo para esta assinatura.

(Mas eu diria para ir com muladd!(α, A, B, β, C) se vamos ter uma API de "ordem textual".)

A propósito, uma coisa que não entendo pelo resultado da votação é a assimetria de muladd! e addmul! . Se você escrever C = βC + αAB , acho que addmul! é mais natural.

@tkf É sobre qual operação você faz primeiro. Para mim, addmul! sugere que você faça uma adição primeiro e depois multiplique, como em (A+B)C . Claro que é subjetivo. Mas bons nomes devem apelar à intuição.

Ah, eu entendi esse ponto.

Como isso ainda está travado, minha proposta teria o padrão de uso consistindo em definições de função com (indo com @callexpr pelo segundo)

@callexpr(C .= β*C + α*A*B) = implementation(C, β, α, A, B)
@callexpr(C .= β*C + A*B) = implementation(C, β, true, A, B)

e talvez uma forma mais amigável de envio (usando @callname pelo segundo)

function @callname(β*C + A*B)(C::Number, β::Number, A::Number, B::Number)
     β*C + A*B
end

e chamadas

@callexpr(A .= 2*C + A*B)
@callexpr(2*3 + 3*2)

e ninguém precisa se preocupar (ou saber) como callexpr transforma operações algébricas em um nome de função único (que não depende dos símbolos do argumento, apenas das operações e da ordem das operações.)
Pensei um pouco na implementação e deve ser bem exequível.

@mschauer Acho que é uma direção interessante. Você pode abrir um novo problema? A API que você está propondo pode resolver muitos outros problemas. Eu acho que ele precisa passar por um processo de design cuidadoso do que resolver uma única instância do problema que pode resolver.

Então, ouvi o boato de que o congelamento de recursos do 1.1 será na próxima semana. Embora o próximo lançamento menor seja "apenas" daqui a quatro meses, seria muito bom se pudéssemos tê-lo em 1.1 ...

De qualquer forma, também precisamos decidir a assinatura da chamada (ordem dos argumentos e palavra-chave ou não) antes de fundir o PR.

Então, vamos votar novamente (pois descobri que é um bom estimulante).

_Se usarmos muladd! para _C = ABα + Cβ_, qual é sua assinatura de chamada favorita?

  • : +1: muladd!(C, A, B, α, β)
  • : -1: muladd!(A, B, C, α, β)
  • : smile: muladd!(C, A, B; α, β) (como: +1 :, mas com argumentos de palavra-chave)
  • : tada: muladd!(A, B, C; α, β) (como: -1 :, mas com argumentos de palavra-chave)
  • : confused: muladd!(A, B, α, C, β)
  • : coração: outra coisa

Se você tiver outros nomes de argumento de palavra-chave em mente, vote naqueles que usam α e β e, em seguida, comente sobre quais nomes são melhores.

Como ainda não decidimos qual deve ser o nome, precisamos fazê-lo por mul! também:

_Se usarmos mul! para _C = ABα + Cβ_, qual é a sua assinatura de chamada favorita?

  • : +1: mul!(C, A, B, α, β)
  • : -1: mul!(A, B, C, α, β)
  • : smile: mul!(C, A, B; α, β) (como: +1 :, mas com argumentos de palavra-chave)
  • : tada: mul!(A, B, C; α, β) (isso é impossível)
  • : confused: mul!(A, B, α, C, β)
  • : coração: outra coisa

NOTA: Não estamos mudando a API existente mul!(C, A, B)

NOTA: Não estamos mudando a API existente mul!(C, A, B)

Não prestei atenção suficiente a esse fato - já temos mul! e isso é o que significa:

mul!(Y, A, B) -> Y

Calcula o produto matriz-matriz ou matriz-vetor A*B e armazena o resultado em Y , substituindo o valor existente de Y . Observe que Y não deve ter como apelido A ou B .

Considerando isso, parece muito natural apenas expandir assim:

mul!(Y, A, B) -> Y
mul!(Y, A, B, α) -> Y
mul!(Y, A, B, α, β) -> Y

Calcula o produto matriz-matriz ou matriz-vetor A*B e armazena o resultado em Y , substituindo o valor existente de Y . Observe que Y não deve ser igual a A ou B . Se um valor escalar, α , for fornecido, o α*A*B será calculado em vez de A*B . Se um valor escalar, β for fornecido, então α*A*B + β*Y será calculado. A mesma restrição de alias se aplica a essas variantes.

No entanto, acho que há uma grande preocupação com isso: parece pelo menos tão natural para mul!(Y, A, B, C, D) computar A*B*C*D no lugar em Y - e essa noção genérica se choca muito mal com mul!(Y, A, B, α, β) computando α*A*B + β*C . Além disso, parece-me que computar A*B*C*D em Y é algo que seria útil e possível de ser feito de forma eficiente, evitando alocações intermediárias, então eu realmente não gostaria de bloquear esse significado .

Com essa outra generalização natural de mul! em mente, aqui está outro pensamento:

mul!(Y, α, A, B) # Y .= α*A*B

Isso se encaixa no modelo geral de mul!(out, args...) onde você calcula e escreve em out multiplicando args juntos. Ele depende do despacho para lidar com α sendo escalar em vez de torná-lo um caso especial - é apenas mais uma coisa que você está multiplicando. Quando α é um escalar e A , B e Y são matrizes, podemos enviar para o BLAS para fazer isso de forma super eficiente. Caso contrário, podemos ter uma implementação genérica.

Além disso, se você estiver em um campo não comutativo (por exemplo, quatérnios), então você pode controlar de que lado a escala de α acontece: mul!(Y, A, B, α) escala de α a direita em vez da esquerda:

mul!(Y, A, B, α) # Y .= A*B*α

Sim, não podemos chamar BLAS para quatérnios, mas é genérico e provavelmente ainda podemos fazer isso de forma razoavelmente eficiente (talvez até transformando-o em algumas chamadas BLAS de alguma forma).

Assumindo essa abordagem para Y .= α*A*B a próxima pergunta se torna: que tal dimensionar e incrementar Y ? Comecei a pensar em palavras-chave para isso, mas então o campo não comutativo veio à mente que parecia muito estranho e limitado. Então, comecei a pensar sobre essa API - o que parece um pouco estranho no início, mas tenha paciência:

mul!((β, Y), α, A, B) # Y .= β*Y .+ α*A*B

Um pouco estranho, mas funciona. E em um campo não comutativo, você pode pedir para multiplicar Y por β à direita assim:

mul!((Y, β), α, A, B) # Y .= Y*β .+ α*A*B

Em geral, em um campo não comutativo, você pode dimensionar à esquerda e à direita desta forma:

mul!((β₁, Y, β₂), α₁, A, B, α₂) # Y .= β₁*Y*β₂ + α₁*A*B*α₂

Agora, é claro, isso é um pouco estranho e não há operação BLAS para isso, mas é uma generalização do GEMM que nos permite expressar _muito_ de coisas e que podemos despachar para operações BLAS trivialmente, sem nem mesmo fazer qualquer desagradável if / else ramos.

Eu realmente gosto da sugestão de @StefanKarpinski como uma chamada de API subjacente, mas também estou pensando se é assim que realmente queremos expor as coisas aos usuários. IMO, no final, deve parecer simples, como uma macro associada:

@affine! Y = β₁*Y*β₂ + α₁*A*B*α₂

A função subjacente seria algo como @StefanKarpinski propõe.

Mas devemos ir mais longe aqui. Eu realmente acho que, se você fizer uma API para ele e uma função genérica, alguém fará uma biblioteca Julia que faz isso de forma eficiente, então eu concordo que não devemos nos limitar a BLAS aqui. Coisas como MatrixChainMultiply.jl já estão construindo DSLs para cálculos de matrizes múltiplas e DiffEq está fazendo seu próprio trabalho com expressões de operador afim. Se tivermos apenas uma representação para uma expressão afim na Base, podemos definir todo o nosso trabalho como estando na mesma coisa.

@dlfivefifty olhou para álgebra linear preguiçosa antes, eu acho que realmente deveria ser revivido aqui. Construir representações preguiçosas de transmissão foi crucial para fazer as operações elementares funcionarem em matrizes abstratas e em hardware computacional alternativo. Precisamos do mesmo para álgebra linear. Uma representação de expressões algébricas lineares nos permitiria definir novos kernels BLAS em tempo real de um Julia BLAS ou transferir as equações para uma GPU / TPU.

Essencialmente, todos os cálculos na computação científica se resumem a operações algébricas lineares e por elemento, portanto, ter uma descrição de alto nível de ambos parece instrumental para construir ferramentas para metaprogramar e explorar novos designs.

Eu teria que pensar mais sobre esta proposta, mas, por enquanto, comentarei que não acho que você gostaria de calcular A*B*C sem um temporário. Parece-me que você teria que pagar com muitas operações aritméticas para evitar o temporário.

Não acho que você gostaria de calcular A*B*C sem um temporário.

Porém, para mul! você já tem uma matriz de saída. Não tenho certeza se isso ajuda ou não. De qualquer forma, parece um detalhe de implementação. A API mul!(Y, A, B, C...) expressa o que você deseja computar e permite que a implementação escolha a melhor maneira de fazê-lo, que era o objetivo geral aqui.

Eu realmente gosto da sugestão de @StefanKarpinski como uma chamada de API subjacente, mas também estou pensando se é assim que realmente queremos expor as coisas aos usuários.

@ChrisRackauckas : Acho que as coisas em que você se meteu podem e devem ser exploradas em pacotes externos - preguiça, escrever a computação que você deseja e deixar algum tipo de otimização passar, selecionar peças que correspondam a certos padrões algébricos que ele sabe como otimizar, etc. Usar mul! assim parece exatamente o tipo de operação genérica, mas fácil de entender, que queremos neste nível.

Observe que não há debate real sobre mul!(Y, α, A, B) - basicamente significa Y .= α*A*B pois o que mais significaria? Então, para mim, a única questão em aberto aqui é se usar uma tupla com uma matriz e escalares à esquerda e / ou à direita é uma maneira razoável de expressar que queremos incrementar e escalar a matriz de saída. Os casos gerais seriam:

  1. mul!(Y::Matrx, args...) : Y .= *(args...)
  2. mul!((β, Y)::{Number, Matrix}, args...) : Y .= β*Y + *(args...)
  3. mul!((Y, β)::{Matrix, Number}, args...) : Y .= Y*β + *(args...)
  4. mul!((β₁, Y, β₂)::{Number, Matrix, Number}, args...) : Y .= β₁*Y*β₂ + *(args...)

Nada mais seria permitido para o primeiro argumento. Isso poderia ser adotado como uma convenção mais geral para outras operações onde faz sentido substituir ou acumular na matriz de saída, opcionalmente combinada com escala.

Não me ocorreu "fundir" mul!(out, args...) e uma interface semelhante ao GEMM! Gosto da extensibilidade disso (mas comece a escrever a resposta abaixo e agora não tenho certeza ...)

Mas minha preocupação é que seja fácil de usar como interface de sobrecarga. Precisamos contar com o sistema de tipos para funcionar bem para tuplas aninhadas. As tuplas aninhadas funcionam tão bem quanto as tuplas planas no sistema de tipos de Julia? Estou me perguntando se algo como " Tuple{Tuple{A1,B1},C1,D1} é mais específico do que Tuple{Tuple{A2,B2},C2,D2} iff Tuple{A1,B1,C1,D1} é mais específico do que Tuple{A2,B2,C2,D2} ". Caso contrário, seria complicado usar como API de sobrecarga.

Observe que precisamos despachar os tipos escalares para usar o hack da reinterpretação para as matrizes complexas (isto é de PR # 29634, portanto, não preste atenção ao nome da função):

https://github.com/JuliaLang/julia/blob/fae1a7a3ae646c7ea1c08982976b57096fb0ae8d/stdlib/LinearAlgebra/src/matmul.jl#L157 -L169

Outra preocupação é que esta é uma interface um tanto limitada para um executor de gráfico de computação. Acho que o objetivo principal da interface multiplicação-adição é fornecer uma API de sobrecarga para permitir que os implementadores de bibliotecas definam um pequeno kernel de computação reutilizável que pode ser implementado de forma eficiente. Isso significa que só podemos implementar _C = ABα_ e não, por exemplo, _αAB_ (consulte https://github.com/JuliaLang/julia/pull/29634#issuecomment-443103667). O suporte a _α₁ABα₂_ para eltype não comutativo requer uma matriz temporária ou o aumento do número de operações aritméticas. Não está claro qual usuário deseja e, idealmente, isso deve ser configurável. Neste ponto, precisamos de uma representação gráfica de computação separada do mecanismo de execução. Acho que é melhor explorar isso em pacotes externos (por exemplo, LazyArrays.jl, MappedArrays.jl). No entanto, se pudermos encontrar uma estratégia de implementação que cubra a maior parte do caso de uso em algum ponto, faria sentido usar mul! como ponto de entrada principal. Acho que este é mais um motivo para favorecer muladd! ; alocar um espaço para a futura API de chamada.

Eu teria que pensar mais sobre essa proposta, mas, por enquanto, apenas comentarei que não acho que você gostaria de calcular A B C sem um temporário. Parece-me que você teria que pagar com muitas operações aritméticas para evitar o temporário.

Você pode de fato provar que qualquer contração de um número arbitrário de tensores, a maneira mais eficiente de avaliar a coisa toda é sempre usando contrações aos pares. Portanto, a multiplicação de várias matrizes é apenas um caso especial disso, você deve multiplicá-las aos pares (a melhor ordem é, obviamente, um problema não trivial). É por isso que acho que mul!(Y,X1,X2,X3...) não é um primitivo tão útil. E no final, isso é o que eu acho que mul! é, é uma operação primitiva que os desenvolvedores podem sobrecarregar para seus tipos específicos. Qualquer operação mais complicada pode então ser escrita usando uma construção de nível superior, por exemplo, usando macros, e poderia, por exemplo, construir um gráfico de computação, que no final é avaliado chamando operações primitivas como mul! . Claro, essa primitiva poderia ser suficientemente geral para incluir casos como o não comutativo que @StefanKarpinski menciona.

Contanto que nenhuma multiplicação de matriz / contração de tensor esteja envolvida, é verdade que pensar em termos de operações primitivas não é tão útil e pode ser benéfico fundir tudo junto como a transmissão faz.

Em geral, concordo que seria bom ter um tipo de gráfico de representação / computação preguiçoso padrão no Base, mas não acho que mul! seja a maneira de construí-lo.

@tkf :

Mas minha preocupação é que seja fácil de usar como interface de sobrecarga. Precisamos contar com o sistema de tipos para funcionar bem para tuplas aninhadas. As tuplas aninhadas funcionam tão bem quanto as tuplas planas no sistema de tipos de Julia?

Sim, estamos todos bem nessa frente. Não tenho certeza de onde o aninhamento entra, mas passar algumas coisas em uma tupla é tão eficiente quanto passar todas elas como argumentos imediatos - é implementado exatamente da mesma maneira.

Isso significa que só podemos implementar _C = ABα_ e não, por exemplo, _αAB_

Estou confuso ... você pode escrever mul!(C, A, B, α) e mul!(C, α, A, B) . Você pode até escrever mul!(C, α₁, A, α₂, B, α₃) . Esta parece ser de longe a API genérica de multiplicação de matrizes mais flexível que foi proposta até agora.

Outra preocupação é que esta é uma interface um tanto limitada para um executor de gráfico de computação. Acho que o objetivo principal da interface multiplicação-adição é fornecer uma API de sobrecarga para permitir que os implementadores de bibliotecas definam um pequeno kernel de computação reutilizável que pode ser implementado de forma eficiente.

Neste ponto, precisamos de uma representação gráfica de computação separada do mecanismo de execução.

Esse pode ser o caso, mas este não é o lugar para isso - isso pode e deve ser desenvolvido em pacotes externos. Tudo o que precisamos para resolver esse problema específico é uma API de multiplicação de matriz que generaliza o que pode ser despachado para operações BLAS - que é exatamente o que isso faz.

@Jutho

Portanto, a multiplicação de várias matrizes é apenas um caso especial disso, você deve multiplicá-las aos pares (a melhor ordem é, obviamente, um problema não trivial). É por isso que acho que mul!(Y,X1,X2,X3...) não é um primitivo tão útil.

A operação mul! permitiria à implementação escolher a ordem de multiplicação, que é uma propriedade útil. Na verdade, a capacidade de fazer isso foi o motivo pelo qual fizemos a operação * analisada como n-ária em primeiro lugar e o mesmo raciocínio se aplica ainda mais a mul! já que se você a estiver usando , você provavelmente se preocupa o suficiente com o desempenho.

Em geral, não posso dizer se você está argumentando a favor ou contra minha proposta de mul! .

Não tenho certeza de onde entra o aninhamento, mas passar algumas coisas em uma tupla é tão eficiente quanto passar todas elas como argumentos imediatos

Eu não estava preocupado com a eficiência, mas sim com o despacho e as ambigüidades do método, uma vez que mesmo a LinearAlgebra atual é um pouco frágil (o que pode ser devido à minha falta de compreensão do sistema de tipos; às vezes me surpreende). Eu estava mencionando a tupla aninhada porque pensei que a resolução do método é feita pela intersecção do tipo de tupla de todos os argumentos posicionais. Isso dá a você uma tupla plana. Se você usar uma tupla no primeiro argumento, terá uma tupla aninhada.

Isso significa que só podemos implementar _C = ABα_ e não, por exemplo, _αAB_

Estou confuso ... você pode escrever mul!(C, A, B, α) e mul!(C, α, A, B) .

Eu quis dizer "nós só podemos implementar _C = ABα_ tão eficientemente quanto _C = AB_ e outros candidatos como _αAB_ não podem ser implementados eficientemente em todas as combinações de tipos de matriz." (Por eficiência, quero dizer a grande complexidade de tempo O.) Não estou muito certo de que esse seja realmente o caso, mas pelo menos para a matriz esparsa outras duas opções estão fora.

Neste ponto, precisamos de uma representação gráfica de computação separada do mecanismo de execução.

Esse pode ser o caso, mas este não é o lugar para isso - isso pode e deve ser desenvolvido em pacotes externos.

Esse é exatamente o meu ponto. Eu sugiro ver esta API como um bloco de construção mínimo para tal uso (claro, esse não é o propósito completo). A implementação e o design de vararg mul! podem ser feitos depois que as pessoas exploraram o espaço de design em pacotes externos.

O despacho de tipos até mesmo para o mul! atual já está “quebrado”: ​​há um crescimento combinatório nas substituições de ambigüidade necessárias para trabalhar com tipos de array composíveis como SubArray e Adjoint .

A solução é usar características e LazyArrays.jl tem uma versão de prova de conceito de mul! com características.

Mas esta é mais uma discussão sobre implementação do que API. Mas usar tuplas para agrupar termos parece errado: não é para isso que existe o sistema de tipos? Nesse caso, você chega à solução LazyArrays.jl.

O mul! operação permitiria à implementação escolher a ordem de multiplicação, que é uma propriedade útil. Na verdade, a capacidade de fazer isso foi o motivo pelo qual fizemos a * operação ser analisada como n-ária em primeiro lugar, e o mesmo raciocínio se aplica ainda mais a mul! já que, se você o está usando, provavelmente se preocupa o suficiente com o desempenho.

Que * analisa como n -ary é extremamente útil. Eu o uso no TensorOperations.jl para implementar a macro @tensoropt , que de fato otimiza a ordem de contração. O fato de eu achar uma versão n -ary de mul! menos útil é porque não faz muito sentido, de uma perspectiva de eficiência, fornecer um local pré-alocado para colocar o resultado, se todos os intermediários arrays ainda precisam ser alocados dentro da função e então gc'ed. Na verdade, em TensorOperations.jl, várias pessoas notaram que a alocação de grandes temporários é um dos lugares em que o gc de Julia está tendo um desempenho muito ruim (muitas vezes levando a tempos de gc de 50%).

Portanto, eu restringiria mul! ao que é realmente uma operação primitiva, como também preconizado por @tkf se bem entendi: multiplicar duas matrizes em uma terceira, com coeficientes possivelmente escalares. Sim, podemos pensar na maneira mais geral de fazer isso para álgebras não comutativas, mas por agora acho que a necessidade imediata é o acesso conveniente à funcionalidade fornecida pelo BLAS (gemm, gemv, ...) que Julia é mul! Falta o invólucro

Não não gosto da sua proposta com tuplas, mas posso prever uma potencial confusão
Restringir do caso 4 ao caso 2 de 3 parece implicar valores padrão β₁ = 1 e β₂ = 1 (ou na verdade true ). Mas então, se nenhum for especificado, de repente significa β₁ = β₂ = 0 ( false ). Claro, a sintaxe é um pouco diferente, já que você escreve mul!(Y, args...) , não mul!((Y,), args...) . No final das contas, é uma questão de documentação, então eu só queria apontar isso.

Portanto, em resumo, não, eu realmente não me oponho a essa sintaxe, embora seja um novo tipo de paradigma que está sendo introduzido e, provavelmente, também deva ser seguido em outros lugares. O que me oponho é imediatamente querer generalizar isso para a multiplicação de um número arbitrário de matrizes, das quais, como argumentado acima, não vejo benefício.

@dlfivefifty : Mas esta é mais uma discussão sobre implementação do que API. Mas usar tuplas para agrupar termos parece errado: não é para isso que existe o sistema de tipos? Nesse caso, você chega à solução LazyArrays.jl.

Mas não vamos usar arrays totalmente preguiçosos aqui - já existem LazyArrays para isso. Enquanto isso, precisamos de alguma forma de expressar o dimensionamento Y . Usar tuplas parece uma abordagem simples e leve para expressar essa pequena quantidade de estrutura. Alguém tem alguma outra sugestão? Poderíamos ter lscale e / ou rscale palavras-chave para β₁ e β₂ , mas isso não parece mais elegante e perderíamos a capacidade de despacho sobre isso, o que não é crucial, mas é bom ter.

@Jutho : Portanto, eu restringiria mul! ao que é realmente uma operação primitiva, como também preconizado por @tkf se bem entendi: multiplicar duas matrizes em uma terceira, com possivelmente coeficientes escalares. Sim, podemos pensar na maneira mais geral de fazer isso para álgebras não comutativas, mas por agora acho que a necessidade imediata é o acesso conveniente à funcionalidade fornecida pelo BLAS (gemm, gemv, ...) que Julia é mul! Falta o invólucro

Eu estou bem em definir apenas um pequeno subconjunto de operações para mul! , talvez até mesmo apenas aquelas que correspondem estruturalmente a chamadas BLAS válidas. Isso seria:

# gemm: alpha = 1.0, beta = 0.0
mul!(Y::Matrix, A::Matrix, B::Matrix) # gemm! Y, A

# gemm: alpha = α, beta = 0.0 (these all do the same thing for BLAS types)
mul!(Y::Matrix, α::Number, A::Matrix, B::Matrix)
mul!(Y::Matrix, A::Matrix, α::Number, B::Matrix)
mul!(Y::Matrix, A::Matrix, B::Matrix, α::Number)

# gemm: alpha = α, beta = β (these all do the same thing for BLAS types)
mul!((β::Number, Y::Matrix), α::Number, A::Matrix, B::Matrix)
mul!((β::Number, Y::Matrix), A::Matrix, α::Number, B::Matrix)
mul!((β::Number, Y::Matrix), A::Matrix, B::Matrix, α::Number)
mul!((Y::Matrix, β::Number), α::Number, A::Matrix, B::Matrix)
mul!((Y::Matrix, β::Number), A::Matrix, α::Number, B::Matrix)
mul!((Y::Matrix, β::Number), A::Matrix, B::Matrix, α::Number)

# gemm: alpha = α, beta = β₁*β₂ (these all do the same thing for BLAS types)
mul!((β₁::Number, Y::Matrix, β₂::Number), α::Number, A::Matrix, B::Matrix)
mul!((β₁::Number, Y::Matrix, β₂::Number), A::Matrix, α::Number, B::Matrix)
mul!((β₁::Number, Y::Matrix, β₂::Number), A::Matrix, B::Matrix, α::Number)

Para quê? Por que permitir tanta variação em como expressar as operações BLAS?

  1. Porque permite que as pessoas expressem sua intenção - se a intenção é multiplicar à esquerda ou à direita ou em ambos, por que não permitir que as pessoas expressem isso e escolham a implementação certa?

  2. Podemos ter fallbacks genéricos que fazem a coisa certa, mesmo para tipos de elementos não comutativos.

O ponto principal deste problema é ter uma generalização da multiplicação de matrizes no local que inclua gemas! embora seja mais genérico do que gemm !. Do contrário, por que não continuar escrevendo gemm! ?

Mas não vamos usar matrizes totalmente preguiçosas aqui

Não estou dizendo "arrays lazy completos", estou sugerindo lazy no mesmo sentido que Broadcasted , que é finalmente removido em tempo de compilação. Essencialmente, eu adicionaria Applied para representar a aplicação preguiçosa de uma função e, em vez de usar tuplas (que não contêm contexto), você teria algo como

materialize!(applied(+, applied(*, α, A, B), applied(*, β, C)))

Isso poderia ser revestido de açúcar como a notação .* da transmissão para torná-lo mais legível, mas acho que o que se deseja é imediatamente claro, ao contrário da proposta baseada em tupla.

@StefanKarpinski , como disse antes, certamente concordo que devemos contemplar uma interface que seja à prova de futuro e generalize corretamente para outros tipos de números. O único problema, eu acho, é que sua lista não está completa. Em princípio, você poderia ter:

mul!((β₁::Number, Y::Matrix, β₂::Number), α₁::Number, A::Matrix, α₂::Number, B::Matrix, α₃::Number)

e todas as suas versões reduzidas, ou seja, se todos os 5 argumentos escalares podem estar ausentes, são 2 ^ 5 = 32 possibilidades diferentes. E isso combinado com todas as possibilidades de diferentes matrizes ou vetores.

Eu concordo com @dlfivefifty que uma abordagem semelhante à transmissão é mais viável.

Sim, percebi que deixei algumas opções de fora, mas 32 métodos não me parecem tão malucos, afinal não precisamos escrevê-los à mão. Adicionando um "broadcast-like sistema" ou um sistema de avaliação preguiçosa que nos permite escrever materialize!(applied(+, applied(*, α, A, B), applied(*, β, C))) parece ser uma adição muito maior e forma fora do escopo para esta questão. Tudo o que queremos é alguma forma de soletrar a multiplicação geral da matriz que seja genérica e nos permita enviar para o BLAS. Se não concordarmos com isso, estou inclinado a permitir que as pessoas continuem ligando para gemm! diretamente.

Sim, provavelmente é verdade; Presumi que seria mais fácil com os argumentos escalares na parte de trás, para fornecer padrões facilmente. Mas se com alguma metaprogramação @eval pudermos facilmente gerar todas as 32 definições, isso é igualmente bom. (Observe, como você certamente sabe, mul não é apenas gemm! mas também gemv e trmm e ...).

Deixe-me acrescentar que não é apenas um invólucro do BLAS. Existem outros métodos especializados puramente Julia em stdlib. Além disso, é importante ter isso como uma API de sobrecarga: os autores do pacote podem definir mul! para seus tipos de matriz especiais.

Acho que esta é a minha postura:

  1. Podemos também suportar mul!(C, A, B, a, b) agora, uma vez que já existe em SparseArrays.jl
  2. Não devemos fazer mais nada porque o despacho em tipos de matriz não é escalonável. (Como mantenedor de BandedMatrices.jl, BlockArrays.jl, LowRankApprox.jl, etc. Posso afirmar isso por experiência própria.)
  3. O projeto baseado em traços se adapta bem, mas seria melhor ir all-in e fazer uma transmissão como Applied , uma vez que o padrão de projeto já está estabelecido. Isso terá que esperar até Julia 2.0, com um protótipo continuando a ser desenvolvido em LazyArrays.jl que atende às minhas necessidades.

@dlfivefifty Você acha que a dificuldade na desambiguação de mul!((Y, β), α, A, B) API é igual a mul!(Y, A, B, α, β) ? Considerar os envoltórios de matriz como Transpose introduz a dificuldade, incluindo 2 e 3 tuplas parece aumentar mais a dificuldade (embora eu saiba que tupla é um caso especial no sistema de tipos de Julia).

  1. Podemos também suportar mul!(C, A, B, a, b) agora, uma vez que já existe em SparseArrays.jl

O fato de alguém ter decidido que mul!(C, A, B, a, b) deveria significar C .= b*C + a*A*B sem pensar até o fim não é, enfaticamente, uma boa razão para dobrar o valor. Se mul! é a versão local de * então não vejo como mul!(out, args...) pode significar qualquer coisa além de out .= *(args...) . Francamente, é assim que você acaba com um sistema que é uma bagunça de APIs inconsistentes e mal planejadas que só existem por acidente histórico. A função mul! não é exportada de SparseArrays _and_ esse método específico não está documentado, então esta é realmente a razão mais frágil possível para entrincheirar um método mal concebido que provavelmente só foi adicionado porque a função não foi é público! Proponho desfazer esse erro e excluir / renomear o método mul! .

Pelo resto desta discussão, parece que não devemos fazer mais nada, já que todos os interessados ​​querem fazer algo mais sofisticado com características e / ou preguiça fora da biblioteca padrão. Estou bem com isso, pois deletar coisas é sempre bom.

Pelo resto desta discussão, parece que não devemos fazer mais nada, já que todos os interessados ​​querem fazer algo mais sofisticado com características e / ou preguiça fora da biblioteca padrão. Estou bem com isso, pois deletar coisas é sempre bom.

Parece que você está ficando um pouco farto, o que é compreensível. No entanto, não acho que essa conclusão seja verdadeira. Se você está confiante de que a sugestão atual pode ser implementada de uma forma escalável e ainda torna conveniente para os desenvolvedores de pacotes sobrecarregar essa definição para seus próprios tipos de matriz e vetor (como mencionado por @tkf), essa seria uma ótima maneira frente.

Em particular, eu acho que os desenvolvedores de pacotes precisam apenas implementar:

mul!((β₁, Y::MyVecOrMat, β₂), α₁, A::MyMat, α₂, B:: MyVecOrMat, α₃)

e talvez, por exemplo,

mul!((β₁, Y::MyVecOrMat, β₂), α₁, A::Adjoint{<:MyMat}, α₂, B:: MyVecOrMat, α₃)
...

enquanto Julia Base (ou melhor, biblioteca padrão LinearAlgebra) cuida de lidar com todos os valores padrão, etc.

Eu acho que os desenvolvedores de pacotes precisam apenas implementar:

mul!((β₁, Y::MyVecOrMat, β₂), α₁, A::MyMat, α₂, B:: MyVecOrMat, α₃)

Sugiro documentar

mul!((Y, β), A, B, α)

como a assinatura a ser sobrecarregada. Isso ocorre porque outros locais para α mudam a grande complexidade do tempo. Consulte: https://github.com/JuliaLang/julia/pull/29634#issuecomment -443103667. Isso dá aos números não comutativos um tratamento não de primeira classe. Mas AFAICT ninguém aqui está realmente usando números não comutativos e acho que devemos esperar até que haja uma necessidade real.

Uma coisa que gosto sobre a abordagem de @StefanKarpinski é que podemos implementar um método especializado para mul!((Y, β), α::Diagonal, A, B) para _algumas_tipo de matriz de A (por exemplo, Adjoint{_,<:SparseMatrixCSC} ) sem alterar a complexidade do tempo . (Isso é importante para meu aplicativo.) Obviamente, seguir esse caminho exigiria uma discussão mais aprofundada na API, especialmente sobre como consultar a existência de um método especializado. Ainda assim, ter a oportunidade de estender a API é ótimo.

Se alguém esclarecer minha preocupação com as ambigüidades dos métodos, serei totalmente favorável à abordagem de grupo por tupla.

Isso ocorre porque outras localizações para α mudam a grande complexidade do tempo O.

Isso é algo de matriz esparsa especificamente? Eu não sou bem o argumento, especialmente não para matrizes densas. Na implementação vinculada, você mostra um caso em que α está entre A e B ?

Eu acho que os desenvolvedores de pacotes precisam apenas implementar ...

Isso é muito simplificado. Vamos supor que temos uma matriz que se comporta como uma matriz strided, como PseudoBlockMatrix de BlockArrays.jl. Para oferecer suporte total a gemm! , precisamos substituir cada permutação de PseudoBlockMatrix por (1) ela mesma, (2) StridedMatrix , (3) Adjoint s por si mesma , (4) Transpose s de si mesmo, (5) Adjoint s de StridedMatrix , (6) Transpose s de StridedMatrix , e possivelmente outros. Isso já é 6 ^ 3 = 216 combinações diferentes. Então você quer apoiar trmm! e tem que fazer o mesmo com UpperTriangular , UnitUpperTriangular , seus adjuntos, suas transposições e assim por diante. Então gsmm! com Symmetric e Hermitian .

Porém, em muitas aplicações, não queremos apenas trabalhar com as matrizes, mas também com suas subvisualizações, particularmente para matrizes de bloco onde queremos trabalhar com blocos. Agora precisamos adicionar todas as permutas de visualizações de nossa matriz junto com as 6 combinações acima.

Agora temos milhares de substituições, envolvendo StridedMatrix , que é um tipo de união muito complicado. Isso é demais para o compilador, fazendo com que o tempo using demore mais de minutos, em vez de segundos.

Nesse ponto, percebe-se que o mul! atual e, por extensão, as extensões propostas de mul! , apresentam falhas de design e, portanto, os desenvolvedores de pacotes não devem se preocupar com isso. Felizmente, LazyArrays.jl fornece uma solução temporária usando características.

Então, eu meio que concordo com @StefanKarpinski em apenas deixar as coisas como estão até que um redesenho mais substancial seja buscado, já que gastar esforço em algo que tem falhas de design não é um bom uso do tempo de ninguém.

@dlfivefifty , estava me referindo apenas a como os argumentos escalares são tratados. Todas as complicações com diferentes tipos de matrizes que atualmente já existem para mul!(C,A,B) permanecerão naturalmente.

Isso ocorre porque outras localizações para α mudam a grande complexidade do tempo O.

Isso é algo de matriz esparsa especificamente? Eu não sou bem o argumento, especialmente não para matrizes densas. Na implementação vinculada, você mostra um caso em que α está entre A e B ?

@Jutho Acho que em geral você não pode colocar α na posição mais interna do loop. Por exemplo, neste caso, você pode apoiar α₁*A*B*α₃ mas não A*α₂*B

https://github.com/JuliaLang/julia/blob/11c5680d5620b0b64420055e8474a2b8cf757010/stdlib/LinearAlgebra/src/matmul.jl#L661 -L670

Acho que pelo menos α₁ ou α₂ em α₁*A*α₂*B*α₃ deve ser 1 para evitar o aumento da complexidade de tempo assintótico.

@dlfivefifty Mas mesmo LazyArrays.jl precisa de algumas funções primitivas para enviar, certo? Meu entendimento é que ele resolve o "inferno do despacho", mas não diminui o número de "kernels" de computação que as pessoas precisam implementar.

Não, não existe “primitivo” da mesma forma que Broadcasted não tem um “primitivo”. Mas sim, no momento, isso não resolve a questão do “kernel”. Acho que o próximo passo é redesenhar para usar um tipo Applied preguiçoso com ApplyStyle . Então poderia haver MulAddStyle para reconhecer operações do tipo BLAS, de uma forma que essa ordem não importe.

Eu chamaria materialize! ou copyto! primitivo. Pelo menos é o bloco de construção do mecanismo de transmissão. Da mesma forma, suponho que LazyArrays.jl tenha que reduzir sua representação preguiçosa para funções com loops ou ccall s para bibliotecas externas em algum ponto, certo? Seria ruim se o nome dessa função fosse mul! ?

Isso é muito simplificado. Vamos supor que temos uma matriz que se comporta como uma matriz strided, como PseudoBlockMatrix de BlockArrays.jl. Para apoiar totalmente o gemm! precisamos substituir cada permutação de PseudoBlockMatrix com (1) ela mesma, (2) StridedMatrix, (3) Adjuntos de si mesma, (4) Transposes de si mesma, (5) Adjuntos de StridedMatrix, (6) Transposes de StridedMatrix, e possivelmente outros . Isso já é 6 ^ 3 = 216 combinações diferentes. Então você quer apoiar trmm! e você precisa fazer o mesmo com UpperTriangular, UnitUpperTriangular, seus adjoints, seus transposes e assim por diante. Então gsmm! com simétrica e hermitiana.
Porém, em muitas aplicações, não queremos apenas trabalhar com as matrizes, mas também com suas subvisualizações, particularmente para matrizes de bloco onde queremos trabalhar com blocos. Agora precisamos adicionar todas as permutas de visualizações de nossa matriz junto com as 6 combinações acima.
Agora temos milhares de substituições, envolvendo StridedMatrix, que é um tipo de união muito complicado. Isso é demais para o compilador, fazendo com que o tempo de uso leve mais de minutos, em vez de segundos.

Eu certamente concordo que a união atual do tipo StridedArray é uma das principais falhas de design. Apoiei sua tentativa de corrigir isso em algum momento.

Em Strided.jl, só implemento mul! quando todas as matrizes envolvidas são do meu próprio tipo personalizado (Abstract)StridedView , sempre que houver alguma mistura nos tipos de A, B e C, deixo Julia Base / LinearAlgebra cuidam disso. Claro, isso deve ser usado em um ambiente de macro @strided , que tenta converter todos os tipos de Base possíveis no tipo StridedView . Aqui, StridedView pode representar subvisualizações, transposições e adjuntos e certas remodelações, todas com o mesmo tipo (paramétrico). No geral, o código de multiplicação completo tem cerca de 100 linhas:
https://github.com/Jutho/Strided.jl/blob/master/src/abstractstridedview.jl#L46 -L147
O fallback nativo de Julia no caso de o BLAS não se aplicar é implementado usando a funcionalidade mais geral mapreducedim! fornecida por esse pacote e não é menos eficiente do que a de LinearAlgebra ; mas também é multithread.

Acho que pelo menos α₁ ou α₂ em α₁*A*α₂*B*α₃ deve ser 1 para evitar o aumento da complexidade de tempo assintótico.

@tkf , eu assumiria que se esses coeficientes escalares assumirem um valor padrão one(T) , ou melhor ainda, true , a propagação constante e as otimizações do compilador eliminarão automaticamente essa multiplicação. no loop mais interno, quando é um ambiente autônomo. Portanto, ainda seria conveniente apenas definir a forma mais geral.

Não tenho certeza se podemos confiar na propagação constante para eliminar todas as multiplicações por 1 ( true ). Por exemplo, eltype pode ser Matrix . Nesse caso, acho que true * x (onde x::Matrix ) tem que criar uma cópia alocada no heap de x . Julia pode fazer alguma mágica para eliminar isso?

@Jutho Acho que esse benchmark mostra que Julia não consegue eliminar as multiplicações intermediárias em alguns casos:

function simplemul!((β₁, Y, β₂), α₁, A, α₂, B, α₃)
    <strong i="7">@assert</strong> size(Y, 1) == size(A, 1)
    <strong i="8">@assert</strong> size(Y, 2) == size(B, 2)
    <strong i="9">@assert</strong> size(A, 2) == size(B, 1)
    <strong i="10">@inbounds</strong> for i in 1:size(A, 1), j = 1:size(B, 2)
        acc = zero(α₁ * A[i, 1] * α₂ * B[1, j] * α₃ +
                   α₁ * A[i, 1] * α₂ * B[1, j] * α₃)
        for k = 1:size(A, 2)
            acc += A[i, k] * α₂ * B[k, j]
        end
        Y[i, j] = α₁ * acc * α₃ + β₁ * Y[i, j] * β₂
    end
    return Y
end

function simplemul!((Y, β), A, B, α)
    <strong i="11">@assert</strong> size(Y, 1) == size(A, 1)
    <strong i="12">@assert</strong> size(Y, 2) == size(B, 2)
    <strong i="13">@assert</strong> size(A, 2) == size(B, 1)
    <strong i="14">@inbounds</strong> for i in 1:size(A, 1), j = 1:size(B, 2)
        acc = zero(A[i, 1] * B[1, j] * α +
                   A[i, 1] * B[1, j] * α)
        for k = 1:size(A, 2)
            acc += A[i, k] * B[k, j]
        end
        Y[i, j] = acc * α + Y[i, j] * β
    end
    return Y
end

fullmul!(Y, A, B) = simplemul!((false, Y, false), true, A, true, B, true)
minmul!(Y, A, B) = simplemul!((Y, false), A, B, true)

using LinearAlgebra
k = 50
n = 50
A = [randn(k, k) for _ in 1:n, _ in 1:n]
B = [randn(k, k) for _ in 1:n]
Y = [zeros(k, k) for _ in 1:n]
<strong i="15">@assert</strong> mul!(copy(Y), A, B) == fullmul!(copy(Y), A, B) == minmul!(copy(Y), A, B)

using BenchmarkTools
<strong i="16">@btime</strong> mul!($Y, $A, $B)     # 63.845 ms (10400 allocations: 99.74 MiB)
<strong i="17">@btime</strong> fullmul!($Y, $A, $B) # 80.963 ms (16501 allocations: 158.24 MiB)
<strong i="18">@btime</strong> minmul!($Y, $A, $B)  # 64.017 ms (10901 allocations: 104.53 MiB)

Bom benchmark. Eu também já havia notado que de fato não eliminará essas alocações por alguns experimentos semelhantes. Para tais casos, pode ser útil definir um tipo de singleton de propósito especial One que apenas define *(::One, x::Any) = x e *(x::Any, ::One) = x , e que nenhum tipo de usuário precisa se preocupar agora. Então, o valor padrão, no mínimo para α₂ , poderia ser One() .

Ah, sim, isso é inteligente! Eu primeiro pensei que agora estava OK em apoiar α₁ * A * α₂ * B * α₃ mas então acho que encontrei outro problema: É matematicamente ambíguo o que devemos fazer quando (digamos) A é uma matriz de matriz e α₁ é uma matriz. Não será um problema se nós _nunca_ suportarmos argumentos não escalares nas posições α . No entanto, isso torna impossível apresentar Y .= β₁*Y*β₂ + *(args...) como o modelo mental de mul!((β₁, Y, β₂), args...) . Além disso, seria muito bom se matrizes diagonais pudessem ser passadas para α₁ ou α₂ , pois às vezes pode ser calculado quase "gratuitamente" (e importante em aplicativos). Acho que existem duas rotas:

(1) Vá com mul!((β₁, Y, β₂), α₁, A, α₂, B, α₃) mas ao sobrecarregar o método, os argumentos α e β devem aceitar Diagonal . É fácil definir um caminho de chamada para que o código do usuário final ainda possa invocá-lo por meio de valores escalares. Mas para que isso funcione de forma eficiente, a "versão O (1)" de Diagonal(fill(λ, n)) https://github.com/JuliaLang/julia/pull/30298#discussion_r239845163 deve ser implementada em LinearAlgebra. Observe que as implementações para escalar e diagonal α não são muito diferentes; frequentemente está apenas trocando α e α.diag[i] . Portanto, não acho que seja um fardo para os autores do pacote.

Isso resolve a ambigüidade que mencionei acima, porque agora você pode chamar mul!(Y, α * I, A, B) quando A é uma matriz de matriz e α é uma matriz que deve ser tratada como eltype de A .

(2) Talvez a rota acima (1) ainda seja muito complexa? Em caso afirmativo, vá com muladd! vez disso por agora. Se quisermos oferecer suporte a mul!((β₁, Y, β₂), α₁, A, α₂, B, α₃) , migrar para ele mantendo a compatibilidade com versões anteriores não é difícil.

Neste ponto, tenho que me perguntar se não deveríamos apenas ter um matmul!(C, A, B, α, β) muito restrito e especializado, que está definido para funcionar apenas para esta assinatura geral:

matmul!(
    C :: VecOrMatT,
    A :: Matrix{T},
    B :: VecOrMatT,
    α :: Union{Bool,T} = true,
    β :: Union{Bool,T} = false,
) where {
    T <: Number,
    VecOrMatT <: VecOrMat{T},
}

Além disso, é realmente incrível que essa assinatura possa ser escrita e enviada.

Essa é essencialmente a minha sugestão (2), certo? (Estou supondo que A :: Matrix{T} não significa literalmente Core.Array{T,2} ; caso contrário, é mais ou menos apenas gemm! )

Eu ficaria feliz com isso como uma solução temporária e posso apoiá-la parcialmente nos pacotes que mantenho ("parcial" por causa do problema das características pendentes), embora acrescente outro nome à mistura: mul! , muladd! e agora matmul! .

... Não é hora de alguém postar “triage diz ...” e ligar?

Além disso, é realmente incrível que essa assinatura possa ser escrita e enviada.

O fato de que você pode despachar exatamente nessa assinatura não é um argumento para apenas fazer disso um método de mul! . Ele também pode ser totalmente obsoleto, caso surja alguma solução mais geral.

tornando este um método de mul!

Se usarmos mul!(C, A, B, α, β) não há como generalizá-lo para mul!((β₁, Y, β₂), α₁, A, α₂, B, α₃) e similares sem quebrar a compatibilidade. (Talvez seja um "recurso", já que estamos livres dessa discussão para sempre: sorria :).

Além disso, deixe-me notar que limitar o tipo de elemento para Number _and_ mantendo a compatibilidade com o atual 3-arg mul! (que já suporta o tipo de elemento não Number ) irá introduzir muito de duplicação.

Não tenho ideia do que você esperaria que mul!((β₁, Y, β₂), α₁, A, α₂, B, α₃) fizesse ... então acho que é um recurso.

Colisão. Fico triste por ter que usar as funções BLAS em todos os lugares para obter um melhor desempenho no local.

@StefanKarpinski Você poderia trazer isso à triagem?

Podemos discutir, embora eu não tenha certeza do resultado que isso terá sem algumas pessoas linalg na chamada, o que normalmente não existe hoje em dia. Para ser franco, gastei uma boa quantidade de tempo e esforço tentando levar esta discussão a algum tipo de resolução e todas as ideias parecem ter sido rejeitadas de uma forma ou de outra, então eu praticamente acabei de verificar esta questão em este ponto. Se alguém pudesse fazer um resumo do problema e por que as várias propostas são inadequadas, isso seria útil para ter uma discussão de triagem produtiva. Caso contrário, não acho que seremos capazes de fazer muito.

Acho que a falta de consenso significa que não é o momento certo para incorporar isso ao StdLib.

Por que não apenas um pacote MatMul.jl que implementa uma das sugestões que os usuários down podem usar? Não vejo por que estar no StdLib é tão importante na prática. Fico feliz em apoiar isso nos pacotes que mantenho.

Estou pensando apenas em uma bela versão Juliana de gema! e gemv! correspondendo ao que já temos em SparseArrays. Por @andreasnoack acima:

Achei que já tivéssemos combinado mul!(C, A, B, α, β) com valores padrão para α , β . Estamos usando esta versão em

julia / stdlib / SparseArrays / src / linalg.jl

Linhas 32 a 50 em b8ca1a4

...
Acho que alguns pacotes também estão usando esse formulário, mas não me lembro qual no topo da minha cabeça.

Essa sugestão teve 7 polegares para cima e nenhum polegar para baixo. Por que simplesmente não implementamos essa função para vetores / matrizes densas? Essa seria uma solução simples que abrange o caso de uso mais comum, certo?

ESTÁ BEM. Então eu acho que ainda não há consenso sobre se há um consenso: sweat_smile:

_I_ pensei que quase todo mundo [*] queria essa API e é só uma questão de nome e assinatura da função. Comparando com _não_ tendo esta API, achei que todos estão satisfeitos com qualquer uma das opções (digamos mul!((β₁, Y, β₂), α₁, A, α₂, B, α₃) , muladd!(C, A, B, α, β) e mul!(C, A, B, α, β) ). A menos que alguém possa apresentar um argumento convincente de que uma determinada API é muito pior do que _não_ tê-la, ficaria feliz com qualquer decisão da triagem.

@StefanKarpinski Mas sinta-se à vontade para remover a tag triage se achar que a discussão ainda não está consolidada o suficiente.

[*] OK, @dlfivefifty , acho que você tem dúvidas até mesmo sobre o atual 3-arg mul! . Mas isso exigiria mudar a interface de 3 arg mul! do zero, então pensei que isso está muito além do escopo desta discussão (que, estive interpretando como sobre _adicionar_ alguma forma de variante de 5 arg). Acho que precisamos de algo que funcione "o suficiente" até que o LazyArrays.jl amadureça.

Por que não apenas um pacote MatMul.jl que implementa uma das sugestões que os usuários down podem usar?

@dlfivefifty Acho que tê-lo no LinearAlgebra.jl é importante porque é uma função de interface (API sobrecarregável). Além disso, como mul!(C::AbstractMatrix, A::AbstractVecOrMat, B::AbstractVecOrMat) é implementado em LinearAlgebra.jl, não poderemos definir mul! em termos de MatMul.muladd! . Claro que existem algumas soluções alternativas, mas é muito mais agradável ter uma implementação direta, especialmente considerando que "apenas" requer a decisão do nome e da assinatura.

Por que simplesmente não implementamos essa função para vetores / matrizes densas?

@chriscoey Infelizmente, este não é o único favorito para todos: https://github.com/JuliaLang/julia/issues/23919#issuecomment -441717678. Aqui está meu resumo dos prós e contras desta e de outras opções https://github.com/JuliaLang/julia/issues/23919#issuecomment -441865841. (Veja os comentários de outras pessoas também)

Da triagem: Existem planos de longo prazo para ter APIs de contração de tensor genéricas, incluindo suporte de compilador e seleção para BLAS, mas a médio prazo, basta escolher qualquer API que atenda às suas necessidades imediatas. Se corresponder a BLAS, escolher nomes BLAS parece razoável.

@Keno , você pode compartilhar alguma informação sobre a API de contração de tensor genérico e o suporte ao compilador? Posso ter algumas informações interessantes para compartilhar também, embora não em público (ainda).

Nenhum design de API foi feito em nada disso, apenas um sentido genérico de que devemos tê-lo. Estou ciente de que você está trabalhando em algumas dessas coisas, então seria bom ter uma sessão de design no momento apropriado, mas não acho que chegamos lá ainda.

Se corresponder a BLAS, escolher nomes BLAS parece razoável.

Isso é completamente contrário ao que temos feito até agora para os nomes das funções da álgebra linear genérica.

Qual é o plano para β == 0 forte / fraco na versão geral proposta de BLAS.gemm!(α, A, B, β, C) ?

Se baixarmos para BLAS chamadas, ele se comportará como um zero forte, embora agora seja inconsistente com lmul! . Não consigo pensar em uma solução para isso além de voltar a generic_muladd! if β == 0 .

Qual é o plano para forte / fraco β == 0

Foi apenas brevemente discutido em torno do meu comentário em https://github.com/JuliaLang/julia/issues/23919#issuecomment -430139849, portanto, a triagem provavelmente não abordou essa questão.

@Keno Embora não tenha havido nenhum projeto de API ainda, você prevê que "APIs incluindo suporte ao compilador e seleção para BLAS" seriam definidas como mutantes ou imutáveis, como a álgebra linear XLA, a fim de ajudar o compilador? Ou seja, você acha que mul! e / ou muladd! fariam parte de tais APIs?

de ping @Keno para o @andreasnoack é questão em https://github.com/JuliaLang/julia/issues/23919#issuecomment -475534454

Desculpe, eu deveria voltar a este problema (especialmente eu solicitei a triagem), mas não sabia qual seria a próxima ação dada a decisão da triagem.

Se corresponder a BLAS, escolher nomes BLAS parece razoável.

Como @andreasnoack apontou, não podemos usar (digamos) gemm! porque queremos suportar multiplicação de vetores de matriz etc. Mas acho que podemos simplesmente ignorar essa decisão de triagem (que diz apenas "Se corresponder a BLAS" ; não faz).

basta escolher qualquer API que atenda às suas necessidades imediatas.

Então, acho que podemos seguir essa direção. Acho que isso significa esquecer a API baseada em tupla sugerida por @StefanKarpinski e "apenas" escolher um de mul! / muladd! / addmul! .

Nós meio que voltamos à discussão original. Mas eu acho que é bom termos uma restrição para não discutir mais sobre API.

Alguma ideia de como escolher um nome de mul! / muladd! / addmul! ?


@chriscoey Acho melhor discutir a API futura em outro lugar. Esse problema já é muito longo e não poderemos fazer nenhum progresso a menos que nos concentremos em uma solução de médio prazo. Que tal abrir uma nova edição (ou linha de discurso)?

Sugiro uma única rodada de votação de aprovação com prazo de 10 dias a partir de agora. Votação para aprovação significa: Todos votam em todas as opções que julgam preferíveis a continuar a discussão. Pessoas que preferem ter seu nome menos preferido agora do que uma discussão contínua devem votar em todos os três. Se nenhuma opção for amplamente aprovada, ou se o esquema de votação em si não obtiver aprovação generalizada, devemos continuar a discussão. Em caso de quase-empate entre as opções aprovadas, @tkf decide (privilégio do autor de RP).

+1: Eu concordo com este esquema de votação e dei meus votos de aprovação.
-1: Discordo deste esquema de votação. Se muitas pessoas ou pessoas muito importantes selecionarem essa opção, a votação será discutível.

Coração: mul! é preferível à discussão contínua.
Foguete: muladd! é preferível à discussão contínua.
Viva: addmul! é preferível à discussão contínua.

Sugiro provisoriamente que 75% de aprovação e 5 devem definitivamente fazer um quorum (ou seja, 75% das pessoas que votaram, incluindo discordância com todo o procedimento de votação, e pelo menos 5 pessoas aprovaram a opção vencedora; se a participação for baixa , então 5/6 ou 6/8 formam o quorum, mas 4/4 unânimes podem ser considerados como falha).

O congelamento de recursos para 1.3 ocorre por volta de 15 de agosto: https://discourse.julialang.org/t/release-1-3-branch-date-approaching-aug-15/27233?u=chriscoey. Espero que possamos fundi-lo até lá 😃. Obrigado a todos que já votaram!

Ainda precisamos decidir o comportamento de β == 0 https://github.com/JuliaLang/julia/issues/23919#issuecomment -475420149, que é ortogonal à decisão do nome. Além disso, o conflito de mesclagem em meu PR precisa ser resolvido e o PR precisa de alguma revisão dos detalhes de implementação (por exemplo, a abordagem que usei lá para lidar com undef s no array de destino). Podemos descobrir outros problemas durante a revisão. Então, não tenho certeza se pode chegar a 1.3 ....

Re: β == 0 , eu acho @andreasnoack 's comentário https://github.com/JuliaLang/julia/issues/23919#issuecomment -430139849 (meu resumo: β == 0 -handling deve ser BLAS -compatível para aproveitar o BLAS tanto quanto possível) faz sentido. É difícil encontrar opiniões opostas além do argumento de β == 0 BLAS?

@simonbyrne Em relação ao seu comentário https://github.com/JuliaLang/julia/issues/23919#issuecomment -430375349, não acho que a ramificação explícita seja um grande problema porque é principalmente um β != 0 ? rmul!(C, β) : fill!(C, zero(eltype(C))) onelinear. Além disso, para uma implementação muito genérica onde você precisa manipular, por exemplo, C = Matrix{Any}(undef, 2, 2) , a implementação requer manipulação explícita de "zero forte" de qualquer maneira (consulte a função auxiliar _modify! em meu https PR: // github .com / JuliaLang / julia / pull / 29634 / files # diff-e5541a621163d78812e05b4ec9c33ef4R37). Então, eu acho que o manuseio tipo BLAS é a melhor escolha aqui. O que você acha?

É possível ter zeros fracos de alto desempenho? No sentido que desejaríamos:

julia> A = [NaN 0;
                     1 0]

julia> b = [0.0,0];

julia> 0.0*A*b
2-element Array{Float64,1}:
 NaN  
   0.0

julia> false*A*b
2-element Array{Float64,1}:
 0.0
 0.0

Ou seja, precisaríamos determinar manualmente quais linhas deveriam ser NaN se reduzirmos para BLAS (que usa 0 forte).

@dlfivefifty Acho que o BLAS pode lidar com NaN em A e B , mas não em C ?

Eu acho que não há como fazer _C = α * A * B + 0 * C_ ciente de NaN eficientemente usando BLAS.gemm! etc. [1] e é daí que @andreasnoack .

[1] você precisa salvar isnan.(C) algum lugar e "poluir" C depois

@chethega já se passaram mais de 10 dias desde a votação

@chriscoey Você está certo, a votação está encerrada.

Eu sou muito ruim no github para obter uma lista completa das pessoas que votaram (isso seria necessário para computar quantas pessoas votaram). No entanto, quando olho os números, parece bastante claro que mul! tem um apoio esmagador (provavelmente gerenciando um quorum de aprovação de 75%), e o segundo candidato muladd! está bem abaixo de 50%.

Não houve uma única objeção ao esquema de votação. Eu chamo a votação, mul! ganhou e o nome está decidido. @tkf pode continuar fazendo isso voar :)

@chethega Obrigado, foi um bom esquema para decidir isso!

Aliás, não posso fazer o rebase imediatamente (mas talvez dentro de algumas semanas), então se alguém quiser fazer o rebase ou reimplementação, não espere por mim.

Infelizmente, não votamos na semântica NaN. O congelamento de recursos ocorre na próxima semana e não temos tempo suficiente para uma votação significativa.

Proponho que tenhamos um referendo não vinculativo para coletar um instantâneo do clima no tópico.

Vejo as seguintes opções:

  1. Continue discutindo, espero que de alguma forma o consenso seja alcançado antes do prazo, ou que o pessoal central nos conceda uma extensão, ou algo assim. : tada: (editar para esclarecimento: esta é a opção padrão, ou seja, o que acontece se não conseguirmos chegar a um consenso sobre uma opção diferente. O resultado mais provável é que 5-arg mul! seja adiado até 1.4.)
  2. Mescle o novo recurso, com comportamento NaN indefinido como barreira. Assim que chegarmos a um consenso, atualizamos o código e / ou documentos para obter o comportamento NaN decidido (zeros fortes x zeros fracos). O backstop de comportamento NaN definido pela implementação indefinido pode acabar indefinido, mas isso não está em votação hoje. (implemente o que for mais rápido; os usuários que se importam precisam chamar métodos diferentes). :afirmativo:
  3. Gemm significa Gemm! Merge para 1.3 com compromisso documentado de zeros fortes. :coração:
  4. NaN significa NaN ! Merge para 1.3 com compromisso documentado para zeros fracos. : olhos:
  5. Faça alguma coisa, qualquer coisa, antes de atingir o penhasco 1.3. :foguete:
  6. Rejeite a votação. :polegares para baixo:
  7. Escrever em
  8. Downthread proposto: arranjo alternativo de !(alpha === false) && iszero(alpha) && !all(isfinite, C) && throw(ArgumentError()) e documentamos que essa verificação de erro provavelmente será retirada em favor de outra coisa. :confuso:

Sinta-se à vontade para selecionar várias opções possivelmente contraditórias e @tkf / triage sinta-se à vontade para desconsiderar a enquete.

Edit: Atualmente apenas: tada: (paciência) e: rocket: (impaciência) são contraditórios, mas ambos são compatíveis com todos os outros. Conforme esclarecido downthread, a triagem contará com sorte o resultado da votação em alguma data não especificada entre Quarta, 14 e Quinta, 15, e levará isso em consideração de alguma maneira não especificada. Novamente, isso significa "votação de aprovação", ou seja, selecione todas as opções que desejar, não apenas a sua favorita; entende-se que: rocket: não desaprova: thumbsup :,: heart :,: eyes: and: confused :

Eu votaria em zero forte (: coração :) se fosse "Unir para 1.x" em vez de "Unir para 1.3". As opções não fariam sentido se todos os "Merge for 1.3" fossem "Merge 1.x"?

Obrigado @chethega. @tkf Estou na posição de realmente precisar do novo mul! O mais rápido possível, sem se preocupar muito com a decisão do NaN (desde que o desempenho não seja comprometido).

Você verificou LazyArrays.jl? Para sua informação, tem um ótimo suporte para multiplicação de matrizes fundidas. Você também pode usar BLAS.gemm! etc. com segurança, pois são métodos públicos https://docs.julialang.org/en/latest/stdlib/LinearAlgebra/#LinearAlgebra.BLAS.gemm!

Eu realmente preciso do mul genérico! uma vez que usamos uma grande variedade de matrizes densas estruturadas e esparsas e simples, para representar diferentes problemas de otimização de forma mais eficiente. Estou aqui pela genericidade e velocidade.

Eu vejo. E acabei de lembrar que discutimos coisas em LazyArrays.jl, então é claro que você já sabia ...

Com relação ao "ASAP", o ciclo de lançamento de quatro meses de Julia é, pelo menos como um design, para evitar "merge rush" antes do congelamento de recursos. Sei que não é justo eu dizer isso porque já tentei a mesma coisa antes ... Mas acho que alguém precisa mencionar isso como um lembrete. O lado bom é que Julia é super fácil de construir. Você pode começar a usá-lo assim que for mesclado até a próxima versão.

Editar: liberar -> mesclar

Obrigado. Acho que os prazos são motivadores úteis e gostaria de evitar que isso se torne obsoleto novamente. Então, eu proporia que tentássemos usar o prazo como uma meta.

É ótimo que você esteja injetando energia ativamente neste tópico!

Eu realmente preciso do mul genérico! uma vez que usamos uma grande variedade de matrizes densas estruturadas e esparsas e simples, para representar diferentes problemas de otimização de forma mais eficiente. Estou aqui pela genericidade e velocidade.

Um mul! 5 argumentos não funcionará bem quando você tiver muitos tipos diferentes: você precisará combinatorialmente muitas substituições para evitar ambigüidade. Esta é uma das motivações por trás do sistema LazyArrays.jl MemoryLayout . É usado para matrizes "estruturadas e esparsas" precisamente por esse motivo em BandedMatrices.jl e BlockBandedMatrices.jl. (Aqui, mesmo subvisualizações de matrizes em faixas são enviadas para rotinas BLAS em faixas.)

Obrigado, vou tentar LazyArrays novamente.

Como o 5-arg mul parece ser geralmente considerado um paliativo temporário (até que alguma solução como LazyArrays possa ser usada no 2.0), acho que podemos ter como objetivo mesclá-lo sem necessariamente ser a solução ideal ou perfeita a longo prazo.

@chethega quando você acha que devemos contar as contagens para o novo voto não obrigatório?

@tkf Claro, você está certo que zeros fortes / fracos / undef fazem sentido para 1.x também.

No entanto, acho que há algumas pessoas que preferem ter 1,3 mul! agora do que esperar até 1,4 para obter 5-arg mul! . Se não houvesse prazo, esperaria um pouco mais e demoraria mais um pouco pensando em como fazer uma votação adequada (pelo menos 10 dias para votação). Mais importante, não podemos ter uma votação significativa sem primeiro apresentar e comparar implementações concorrentes sobre a velocidade e elegância de zeros fracos / fortes. Eu pessoalmente suspeito que zeros fracos podem ser feitos quase tão rápido quanto zeros fortes, primeiro verificando iszero(alpha) , depois varrendo a matriz para !isfinite valores, e só então usando um caminho lento com alocação extra; mas eu prefiro semântica zero forte de qualquer maneira.

@chethega quando você acha que devemos contar as contagens para o novo voto não obrigatório?

A triagem deve tomar uma decisão (atraso / forte / fraco / recuo) esta semana para o alfa 1,3. Acho que quinta-feira 15 ou quarta-feira 14 são opções sensatas para a triagem fazer uma contagem e levar isso em consideração. Provavelmente não poderei entrar na quinta-feira, então outra pessoa terá que contar.

Realisticamente, é normal ser conservador aqui, perder o prazo, continuar a discussão e aguardar 1.4.

Por outro lado, podemos já ter chegado a um consenso sem perceber: @andreasnoack apresentou alguns argumentos poderosos de que um coeficiente zero deveria ser um zero forte. Pode ser que ele tenha conseguido convencer todos os fracos proponentes zero. Pode muito bem ser que haja uma grande maioria que realmente queira 5 arg mul !, de preferência no ano passado, e realmente não se importe com esse pequeno detalhe. Se fosse esse o caso, seria uma pena atrasar ainda mais o recurso, só porque ninguém quer encerrar a discussão.

Por que não apenas lançar um erro por enquanto:

β == 0.0 && any(isnan,C) && throw(ArgumentError("use β = false"))

Por que não apenas lançar um erro por enquanto

Eu adicionei essa opção à enquete. Ótima ideia de compromisso!

Só para definir as expectativas: o congelamento de recursos para 1.3 é em três dias, então basicamente não há como isso acontecer a tempo. Estamos sendo bastante rígidos quanto ao congelamento de recursos e ramificações, uma vez que é a única parte do ciclo de lançamento em que podemos realmente controlar o tempo.

O trabalho já está feito em https://github.com/JuliaLang/julia/pull/29634 . Só precisa ser ajustado e reformulado.

@tkf para # 29634 você poderia listar o trabalho que ainda

Os TODOs que consigo pensar em ATM são:

Meu PR implementa a semântica BLAS de manipulação de β = 0 . Portanto, outro tratamento, como lançar um erro, também deve ser implementado.

Meu PR implementa a semântica BLAS de manipulação de β = 0 .

Desculpe, minha memória estava velha; minha implementação não foi consistente e propaga NaN _sometimes_. Portanto, TODO adicional é tornar o comportamento de β = 0.0 consistente.

O tipo MulAddMul seria apenas para uso interno, certo?

Sim, é um detalhe totalmente interno. Minhas preocupações eram (1) pode haver muitas especializações (beta = 0 etc. está codificado no parâmetro de tipo) e (2) isso diminui a legibilidade do código-fonte.

Essas são preocupações válidas. Já produzimos uma tonelada de especializações no código de álgebra linear, então é bom pensar se realmente precisamos nos especializar aqui. Em geral, penso que devemos otimizar para matrizes pequenas, uma vez que não é gratuito (como você disse, isso complica o código-fonte e pode aumentar o tempo de compilação) e as pessoas ficam melhor usando StaticArrays para pequenas multiplicações de matrizes. Conseqüentemente, eu tento apenas verificar os valores em tempo de execução, mas sempre podemos ajustar isso mais tarde se mudarmos de ideia, então não devemos deixar isso causar atrasos.

Os zeros suaves para sua informação têm implementações simples:

if iszero(β) && β !== false && !iszero(α)
   lmul!(zero(T),y) # this handles soft zeros correctly
   BLAS.gemv!(α, A, x, one(T), y) # preserves soft zeros
elseif iszero(α) && iszero(β)
   BLAS.gemv!(one(T), A, x, one(T), y) # puts NaNs in the correct place
   lmul!(zero(T), y) # everything not NaN should be zero
elseif iszero(α) && !iszero(β)
   BLAS.gemv!(one(T), A, x, β, y) # puts NaNs in the correct place
   BLAS.gemv!(-one(T), A, x, one(T), y) # subtracts out non-NaN changes
end

@andreasnoack Desculpe, esqueci que realmente precisamos da especialização para otimizar o loop mais interno para algumas matrizes estruturadas como mul!(C, A::BiTriSym, B, α, β) https://github.com/JuliaLang/julia/pull/29634#issuecomment -440510551. Algumas especializações podem ser removidas, mas isso é realmente mais trabalho (atrasos).

Conseqüentemente, eu tento apenas verificar os valores em tempo de execução, mas sempre podemos ajustar isso mais tarde se mudarmos de ideia, então não devemos deixar isso causar atrasos.

Ótimo!

@andreasnoack Muito obrigado por revisar e mesclar isso a tempo!

Agora que ele foi mesclado para 1.3, ele começou me deixa muito nervoso com a implementação: smile :

Não precisa se preocupar, haverá muito tempo para 1.3 RCs + PkgEvals resolverem os bugs.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

arshpreetsingh picture arshpreetsingh  ·  3Comentários

Keno picture Keno  ·  3Comentários

StefanKarpinski picture StefanKarpinski  ·  3Comentários

m-j-w picture m-j-w  ·  3Comentários

manor picture manor  ·  3Comentários