Lapack: Perda de precisão em todas as consultas de espaço de trabalho de precisão única, implícita e explícita

Criado em 19 jul. 2021  ·  6Comentários  ·  Fonte: Reference-LAPACK/lapack

Olá pessoal,

Recentemente, encontrei um erro ao usar o ssyevd por meio da interface do lapacke.
Isso aponta para um problema na interface do lapack em geral. É assim:

De acordo com a interface padrão do lapack, muitas rotinas como ssyevd você tem que chamar duas vezes:
Uma vez, para perguntar à rotina de quanta memória temporária ela precisa para um determinado tamanho de matriz,
e só então chamar a rotina a sério com as seções de memória de rascunho necessárias
como parâmetros.

Se você olhar atentamente para esta primeira chamada, que deve retornar o tamanho de memória necessário, por exemplo
para uma rotina como ssyevd, você vê que mesmo de acordo com a documentação do lapack,
o requisito de memória é passado de volta por meio de um ponteiro para um valor flutuante .
Portanto, ao calcular a memória, ela passa por uma série de valores:

calculate memory       ->    store value in reference   ->  retrieve the value for use (allocation)
    int64                            float                              int64

É int64 no caso de uma interface ilp64, caso contrário, seria int32.
Então, em essência, temos um encurtamento intermediário do valor da memória de
63 bits a 24 bits !!!
(ou mais preciso, para 24 bits entre os bits definidos mais externos na representação flutuante IEEE 754)
Mesmo no caso de inteiros de 32 bits, você tem um encurtamento de 31 bits para 24 bits.

Então, se você chamar o cálculo do requisito de memória como no passado 'manualmente'
você pode ter a chance de ver onde está errado, mas ainda assim você não pode evitar o curto-circuito
para um valor flutuante. Se você usar a alocação automática de memória por meio da interface lapacke moderna
você nem tem ideia do que pode estar errado, pois a rotina anuncia para cuidar
de todo o gerenciamento de memória por si só!

Isso acontece em todas as rotinas de precisão simples (s / c) em lapack, que calculam
o requisito de memória como uma etapa intermediária.
Mudar para precisão dupla usaria, em vez disso, uma referência de precisão dupla,
aumentando o valor intermediário para 53 bits em vez disso, que ainda não está perto dos 64 bits
seria de se supor com uma interface de 64 bits.

Solução alternativa, quatro maneiras possíveis:

  1. Se você quiser usar rotinas lapack simples ou complexas, não use a alocação automática de memória por meio da interface C lapacke
  2. Se você usar o método de função lapack de duas chamadas, para o cálculo de memória use a rotina double (!)
  3. Dê uma olhada na implementação de referência da rotina lapack e calcule a memória necessária por conta própria
  4. Use apenas tamanhos de matriz pequenos ao usar matrizes flutuantes / complexas

Outras pessoas tropeçaram nisso, mas não seguiram até a causa real, por exemplo,
A compilação openBLAS com suporte a int64 falha na entrada válida para ssyevd

É preciso enfatizar que, pelo menos com o método de duas chamadas, isso não é um bug, mas uma falha de design.
No caso da alocação automática de memória lapacke, ela deve ser considerada um bug bastante grave.

Cumprimentos,

oxidante

Bug

Comentários muito úteis

Mudar lwork para IN / OUT seria uma boa solução originalmente, mas não é compatível com versões anteriores. O aplicativo teria então que saber se a versão do LAPACK era <= 3,10 (digamos) ou> 3,10 para saber onde obter o trabalho. Pior, há casos em que os aplicativos passam um valor const - esperando que ele permaneça constante - então LAPACK mudar seu comportamento para sobrescrever esse valor seria muito prejudicial (UB). Por exemplo, em MAGMA:

    const magma_int_t ineg_one = -1;
    ...
            magma_int_t query_magma, query_lapack;
            magma_zgesdd( *jobz, M, N,
                          unused, lda, runused,
                          unused, ldu,
                          unused, ldv,
                          dummy, ineg_one,  // overwriting ineg_one would break MAGMA
                          #ifdef COMPLEX
                          runused,
                          #endif
                          iunused, &info );
            assert( info == 0 );
            query_magma = (magma_int_t) MAGMA_Z_REAL( dummy[0] );

A solução que propus há alguns anos e implementei no MAGMA é simplesmente no sgesdd, etc., para arredondar um pouco o trabalho retornado no trabalho [1], de forma que o valor retornado seja sempre> = o valor pretendido. Consulte https://bitbucket.org/icl/magma/src/master/control/magma_zauxiliary.cpp e use em https://bitbucket.org/icl/magma/src/master/src/zgesdd.cpp. (Veja um release para a versão de precisão única gerada.) Substitua basicamente

    WORK( 1 ) = MAXWRK

com

    WORK( 1 ) = lapack_roundup_lwork( MAXWRK )

onde a função lapack_roundup_lwork arredonda ligeiramente, como magma_*make_lwork faz. No MAGMA, eu arredondei multiplicando por (1 + eps), usando eps de precisão simples, mas fazendo o cálculo em dobro. Assim, os aplicativos existentes se comportarão corretamente sem a necessidade de alterar suas consultas de espaço de trabalho.

Após mais testes, descobri que para lwork> 2 ^ 54, ele precisa usar a definição C / C ++ / Fortran de epsilon = 1.19e-07 (também conhecida como ulp), em vez da definição LAPACK slamch ("eps") = 5,96e- 08 (também conhecido como arredondamento de unidade, u). Se estiver usando o ulp, parece que o cálculo pode ser feito de uma só vez.

Todos 6 comentários

Acho que deve ter feito sentido na época (retornar o tamanho por meio do ponteiro da matriz de trabalho), mas me pergunto o que nos impede de transformar o especificador de tamanho em uma variável de entrada / saída e retornar o valor exato lá também? Um chamador "moderno" verificaria isso primeiro e recorreria ao membro da matriz work apenas se lwork ainda fosse -1, um chamador "antigo" não notaria nenhuma mudança.

Também provavelmente o LWORK necessário naquela época era fisicamente grande demais para acertar esse problema e o ilp64 tornou isso óbvio. Estou sonhando com os dias em que o valor NB é derivado em tempo de execução, em vez do esquema de duas chamadas.

Aqui estão dois tópicos de discussão relacionados a isso:
https://icl.cs.utk.edu/lapack-forum/viewtopic.php?t=1418
http://icl.cs.utk.edu/lapack-forum/archives/lapack/msg00827.html

Isso é especialmente um problema para algoritmos que requerem um espaço de trabalho O (n ^ 2). Para algoritmos que requerem um espaço de trabalho O (n * nb), isso é menos problemático.

Sim, esta é uma falha de design.

@ martin-frbg: como seria a mudança proposta para a interface C _work? Temos LWORK como INPUT apenas lá. Mudar LWORK para INPUT / OUTPUT ocorre uma grande mudança. Você tem uma ideia para resolver este problema? Ver:
https://github.com/Reference-LAPACK/lapack/blob/aa631b4b4bd13f6ae2dbab9ae9da209e1e05b0fc/LAPACKE/src/lapacke_dgeqrf_work.c#L35

Eu estava pensando que também poderíamos criar algumas sub-rotinas de alocação de espaço de trabalho, como LAPACK_dgeqrf__workspace_query () e isso retornaria o espaço de trabalho necessário.

Bem, meu plano astuto realmente não funciona quando sóbrio ...
Mas esses são, na verdade, dois problemas, eu acho, um o tamanho do trabalho transbordando um lapack_int e o outro "apenas" uma representação incorreta devido à precisão limitada - eu me pergunto se seria possível arredondar o tamanho calculado para antecipar o último às custas de "alguma" memória não utilizada?

Sim, esta é uma falha de design.

Posso ver 2 falhas diferentes aqui:

  1. No LAPACK: as rotinas retornam o tamanho do trabalho usando uma variável real.
  2. No LAPACKE: as rotinas propagam a falha do LAPACK.

A ideia de @martin-frbg é uma boa solução para (1). O novo código Fortran poderia usar o valor de retorno de LWORK em vez de WORK (1). Podemos tentar modificar o código com algum procedimento (semi-) automático de substituição. Em ssyevd.f , por exemplo, poderíamos substituir

  ELSE IF( LQUERY ) THEN
     RETURN
  END IF

de

  ELSE IF( LQUERY ) THEN
     LWORK = LOPT
     RETURN
  END IF

Adicionar LAPACKE_dgeqrf__work_query() , como @langou sugere, resolve (2), embora haja muito trabalho associado a essa modificação.

Mudar lwork para IN / OUT seria uma boa solução originalmente, mas não é compatível com versões anteriores. O aplicativo teria então que saber se a versão do LAPACK era <= 3,10 (digamos) ou> 3,10 para saber onde obter o trabalho. Pior, há casos em que os aplicativos passam um valor const - esperando que ele permaneça constante - então LAPACK mudar seu comportamento para sobrescrever esse valor seria muito prejudicial (UB). Por exemplo, em MAGMA:

    const magma_int_t ineg_one = -1;
    ...
            magma_int_t query_magma, query_lapack;
            magma_zgesdd( *jobz, M, N,
                          unused, lda, runused,
                          unused, ldu,
                          unused, ldv,
                          dummy, ineg_one,  // overwriting ineg_one would break MAGMA
                          #ifdef COMPLEX
                          runused,
                          #endif
                          iunused, &info );
            assert( info == 0 );
            query_magma = (magma_int_t) MAGMA_Z_REAL( dummy[0] );

A solução que propus há alguns anos e implementei no MAGMA é simplesmente no sgesdd, etc., para arredondar um pouco o trabalho retornado no trabalho [1], de forma que o valor retornado seja sempre> = o valor pretendido. Consulte https://bitbucket.org/icl/magma/src/master/control/magma_zauxiliary.cpp e use em https://bitbucket.org/icl/magma/src/master/src/zgesdd.cpp. (Veja um release para a versão de precisão única gerada.) Substitua basicamente

    WORK( 1 ) = MAXWRK

com

    WORK( 1 ) = lapack_roundup_lwork( MAXWRK )

onde a função lapack_roundup_lwork arredonda ligeiramente, como magma_*make_lwork faz. No MAGMA, eu arredondei multiplicando por (1 + eps), usando eps de precisão simples, mas fazendo o cálculo em dobro. Assim, os aplicativos existentes se comportarão corretamente sem a necessidade de alterar suas consultas de espaço de trabalho.

Após mais testes, descobri que para lwork> 2 ^ 54, ele precisa usar a definição C / C ++ / Fortran de epsilon = 1.19e-07 (também conhecida como ulp), em vez da definição LAPACK slamch ("eps") = 5,96e- 08 (também conhecido como arredondamento de unidade, u). Se estiver usando o ulp, parece que o cálculo pode ser feito de uma só vez.

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

Questões relacionadas

Peter9606 picture Peter9606  ·  7Comentários

hokb picture hokb  ·  16Comentários

nboelter picture nboelter  ·  3Comentários

christoph-conrads picture christoph-conrads  ·  26Comentários

Dichloromethane picture Dichloromethane  ·  11Comentários