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 :
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
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:
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.
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 :
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
avec
où la fonction
lapack_roundup_lwork
arrondit légèrement, comme le faitmagma_*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.