Lapack: Perte de précision dans toutes les requêtes d'espace de travail en simple précision, implicite et explicite

Créé le 19 juil. 2021  ·  6Commentaires  ·  Source: Reference-LAPACK/lapack

Bonjour tous le monde,

récemment, je suis tombé sur une erreur lors de l'utilisation de ssyevd via l'interface lapacke.
Cela indique un problème dans l'interface de lapack en général. Ça va comme ça:

Selon l'interface standard de lapack, de nombreuses routines comme ssyevd doivent être appelées deux fois :
Une fois pour demander à la routine de combien de mémoire scratch elle a besoin pour une certaine taille de matrice,
et seulement ensuite appeler la routine pour de bon avec les sections de mémoire de travail requises
comme paramètres.

Si vous regardez attentivement ce premier appel, qui devrait renvoyer la taille de mémoire requise, par exemple
pour une routine comme ssyevd, vous voyez que même selon la documentation de lapack,
l'exigence de mémoire est renvoyée via un pointeur vers une valeur flottante .
Ainsi, lors du calcul de la mémoire, il passe par une série de valeurs :

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

C'est int64 dans le cas d'une interface ilp64, sinon ce serait int32.
Donc, en substance, nous avons un raccourcissement intermédiaire de la valeur de mémoire de
63 bits à 24 bits !!!
(ou plus précisément, à 24 bits entre les bits définis les plus à l'extérieur dans la représentation flottante IEEE 754)
Même dans le cas d'entiers 32 bits, vous avez un raccourcissement de 31 bits à 24 bits.

Donc, si vous appeliez le calcul des besoins en mémoire comme par le passé « à la main »
vous aurez peut-être la chance de voir où cela ne va pas, mais vous ne pouvez toujours pas empêcher le court-circuit
à une valeur flottante. Si vous utilisez l'allocation de mémoire automatique via l'interface lapacke moderne
vous n'avez même pas une idée de ce qui pourrait ne pas aller, car la routine annonce de prendre soin
de toute la gestion de la mémoire par elle-même !

Cela se produit dans toutes les routines en simple précision (s/c) dans lapack, qui calculent
l'exigence de mémoire comme étape intermédiaire.
Le passage à la double précision utiliserait alors une référence à double précision à la place,
augmenter la valeur intermédiaire à 53 bits à la place, ce qui n'est toujours pas proche des 64 bits
on pourrait supposer avec une interface 64 bits.

Solution de contournement, quatre manières possibles :

  1. Si vous souhaitez utiliser des routines lapack simples ou complexes, n'utilisez pas l'allocation mémoire automatique via l'interface C lapacke
  2. Si vous utilisez la méthode de la fonction lapack à deux appels, pour le calcul de la mémoire utilisez la routine double(!)
  3. Jetez un œil à l'implémentation de référence de la routine lapack et calculez vous-même la mémoire requise
  4. N'utilisez que de petites tailles de matrice lors de l'utilisation de matrices flottantes/complexes

D'autres personnes sont tombées sur cela, mais ne l'ont pas suivi jusqu'à la vraie cause, par exemple
La construction d'openBLAS avec le support int64 échoue sur une entrée valide pour ssyevd

Il faut souligner qu'au moins avec la méthode à deux appels, ce n'est pas un bogue, mais un défaut de conception.
Dans le cas de l'allocation automatique de mémoire lapacke, cela doit être considéré comme un bug assez grave.

Salutations,

oxydateur

Bug

Commentaire le plus utile

Changer lwork en IN/OUT serait une bonne solution à l'origine, mais ce n'est pas rétrocompatible. L'application devrait alors savoir si la version LAPACK était <= 3.10 (disons) ou > 3.10 pour savoir où se procurer le lwork. Pire encore, il existe des cas où les applications transmettent une valeur const - en s'attendant à ce qu'elle reste const - donc LAPACK modifier son comportement pour écraser cette valeur serait très préjudiciable (UB). Par exemple, dans 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] );

La solution que j'ai proposée il y a quelques années et implémentée dans MAGMA est simplement dans sgesdd, etc., pour arrondir un peu le lwork renvoyé dans work[1], de sorte que la valeur renvoyée est toujours >= la valeur souhaitée. Voir https://bitbucket.org/icl/magma/src/master/control/magma_zauxiliary.cpp et utiliser dans https://bitbucket.org/icl/magma/src/master/src/zgesdd.cpp. (Voir une version pour la version simple précision générée.) Remplacez essentiellement

    WORK( 1 ) = MAXWRK

avec

    WORK( 1 ) = lapack_roundup_lwork( MAXWRK )

où la fonction lapack_roundup_lwork arrondit légèrement, comme le fait magma_*make_lwork . Dans MAGMA, j'ai arrondi en multipliant par (1 + eps), en utilisant des eps simple précision mais en faisant le calcul en double. Ensuite, les applications existantes se comporteront correctement sans qu'il soit nécessaire de modifier leurs requêtes d'espace de travail.

Après plus de tests, j'ai trouvé pour lwork > 2^54, il doit utiliser la définition C/C++/Fortran de epsilon = 1.19e-07 (alias ulp), plutôt que la définition LAPACK slamch("eps") = 5.96e- 08 (alias arrondi d'unité, u). Si vous utilisez ulp, il semble que le calcul puisse être effectué en un seul.

Tous les 6 commentaires

Je suppose que cela devait avoir du sens à l'époque (pour renvoyer la taille via le pointeur du tableau de travail), mais je me demande ce qui nous empêche de transformer le spécificateur de taille en une variable d'entrée/sortie et de lui renvoyer également la valeur exacte? Un appelant "moderne" vérifierait d'abord cela et recourrait au membre du tableau de travail uniquement si lwork était toujours à -1, un "ancien" appelant ne remarquerait aucun changement.

De plus, le LWORK requis à l'époque était probablement trop gros pour résoudre ce problème et ilp64 l'a rendu évident. Je rêve des jours où la valeur NB est dérivée au moment de l'exécution au lieu du schéma à deux appels.

Voici deux fils de discussion à ce sujet :
https://icl.cs.utk.edu/lapack-forum/viewtopic.php?t=1418
http://icl.cs.utk.edu/lapack-forum/archives/lapack/msg00827.html

C'est particulièrement un problème pour les algorithmes qui nécessitent un espace de travail O(n^2). Pour l'algorithme qui nécessite un espace de travail O(n*nb), c'est moins un problème.

Oui, c'est un défaut de conception.

@martin-frbg : quelle serait votre proposition de changement pour l'interface C_work ? Nous avons LWORK comme INPUT seulement là. Changer LWORK en INPUT/OUTPUT il y a un changement majeur. Avez-vous une idée pour résoudre ce problème ? Voir:
https://github.com/Reference-LAPACK/lapack/blob/aa631b4b4bd13f6ae2dbab9ae9da209e1e05b0fc/LAPACKE/src/lapacke_dgeqrf_work.c#L35

Je pensais que nous pourrions également créer des sous-routines d'allocation d'espace de travail telles que LAPACK_dgeqrf__workspace_query() et cela renverrait l'espace de travail nécessaire.

Eh bien, mon plan astucieux ne fonctionne pas vraiment quand il est sobre...
Mais ce sont en fait deux problèmes je pense, l'un la taille de travail débordant d'un lapack_int et l'autre "seulement" une fausse représentation due à une précision limitée - je me demande s'il serait possible d'arrondir la taille calculée pour anticiper cette dernière au détriment de "un peu" de mémoire inutilisée ?

Oui, c'est un défaut de conception.

Je peux voir 2 défauts différents ici:

  1. Sur LAPACK : les routines retournent la taille du travail à l'aide d'une variable réelle.
  2. Sur LAPACKE : les routines propagent la faille depuis LAPACK.

L'idée de @martin-frbg est une bonne solution à (1). Le nouveau code Fortran pourrait utiliser la valeur de retour de LWORK au lieu de WORK(1). Nous pouvons essayer de modifier le code avec une procédure (semi-)automatique de remplacement. Dans ssyevd.f , par exemple, nous pourrions remplacer

  ELSE IF( LQUERY ) THEN
     RETURN
  END IF

par

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

L'ajout de LAPACKE_dgeqrf__work_query() , comme le suggère @langou , résout (2), bien qu'il y ait beaucoup de travail associé à cette modification.

Changer lwork en IN/OUT serait une bonne solution à l'origine, mais ce n'est pas rétrocompatible. L'application devrait alors savoir si la version LAPACK était <= 3.10 (disons) ou > 3.10 pour savoir où se procurer le lwork. Pire encore, il existe des cas où les applications transmettent une valeur const - en s'attendant à ce qu'elle reste const - donc LAPACK modifier son comportement pour écraser cette valeur serait très préjudiciable (UB). Par exemple, dans 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] );

La solution que j'ai proposée il y a quelques années et implémentée dans MAGMA est simplement dans sgesdd, etc., pour arrondir un peu le lwork renvoyé dans work[1], de sorte que la valeur renvoyée est toujours >= la valeur souhaitée. Voir https://bitbucket.org/icl/magma/src/master/control/magma_zauxiliary.cpp et utiliser dans https://bitbucket.org/icl/magma/src/master/src/zgesdd.cpp. (Voir une version pour la version simple précision générée.) Remplacez essentiellement

    WORK( 1 ) = MAXWRK

avec

    WORK( 1 ) = lapack_roundup_lwork( MAXWRK )

où la fonction lapack_roundup_lwork arrondit légèrement, comme le fait magma_*make_lwork . Dans MAGMA, j'ai arrondi en multipliant par (1 + eps), en utilisant des eps simple précision mais en faisant le calcul en double. Ensuite, les applications existantes se comporteront correctement sans qu'il soit nécessaire de modifier leurs requêtes d'espace de travail.

Après plus de tests, j'ai trouvé pour lwork > 2^54, il doit utiliser la définition C/C++/Fortran de epsilon = 1.19e-07 (alias ulp), plutôt que la définition LAPACK slamch("eps") = 5.96e- 08 (alias arrondi d'unité, u). Si vous utilisez ulp, il semble que le calcul puisse être effectué en un seul.

Cette page vous a été utile?
0 / 5 - 0 notes