Lapack: Genauigkeitsverlust bei allen Workspace-Abfragen mit einfacher Genauigkeit, implizit und explizit

Erstellt am 19. Juli 2021  ·  6Kommentare  ·  Quelle: Reference-LAPACK/lapack

Hallo zusammen,

vor kurzem bin ich über einen Fehler bei der Verwendung von ssyevd über die Lapacke-Schnittstelle gestolpert.
Es weist auf ein Problem in der Lapack-Schnittstelle im Allgemeinen hin. Es geht so:

Laut der Lapack-Standardschnittstelle müssen viele Routinen wie ssyevd zweimal aufgerufen werden:
Um die Routine einmal zu fragen, wie viel Arbeitsspeicher sie für eine bestimmte Matrixgröße benötigt,
und erst dann die Routine ernsthaft mit den benötigten Scratch-Memory-Abschnitten aufrufen
als Parameter.

Wenn Sie sich diesen ersten Aufruf genau ansehen, sollte dieser die erforderliche Speichergröße zurückgeben, z
für eine Routine wie ssyevd sehen Sie, dass selbst gemäß der Lapack-Dokumentation
der Speicherbedarf wird über einen Zeiger auf einen Float- Wert zurückgemeldet.
Bei der Berechnung des Speichers durchläuft er also eine Reihe von Werten:

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

Bei einer ilp64-Schnittstelle ist es int64, ansonsten wäre es int32.
Im Wesentlichen haben wir also eine Zwischenverkürzung des Gedächtniswertes von
63 Bit auf 24 Bit !!!
(oder genauer auf 24bit zwischen den äußersten gesetzten Bits in IEEE 754-Float-Darstellung)
Selbst bei 32-Bit-Integern haben Sie eine Verkürzung von 31 Bit auf 24 Bit.

Wenn Sie also die Speicherbedarfsberechnung wie in der Vergangenheit 'von Hand' nennen würden
Sie haben vielleicht die Chance zu sehen, wo es schief geht, aber Sie können den Leerverkauf immer noch nicht verhindern
auf einen Float-Wert. Wenn Sie die automatische Speicherzuweisung über die moderne Lapacke-Schnittstelle nutzen
du hast nicht mal eine ahnung woran es liegen könnte, da die routine wirbt damit aufzupassen
aller Speicherverwaltung von selbst!

Dies geschieht in allen einfachen Präzisionsroutinen (s/c) in lapack, die berechnen
der Speicherbedarf als Zwischenschritt.
Der Wechsel zu doppelter Genauigkeit würde dann stattdessen eine Referenz mit doppelter Genauigkeit verwenden.
Erhöhen des Zwischenwertes stattdessen auf 53 Bit, was immer noch nicht in der Nähe der 64 Bit liegt
würde man bei einer 64bit Schnittstelle annehmen.

Problemumgehung, vier Möglichkeiten:

  1. Wenn Sie einzelne oder komplexe Lapack-Routinen verwenden möchten, verwenden Sie nicht die automatische Speicherzuweisung über die C lapacke-Schnittstelle
  2. Wenn Sie die Lapack-Funktionsmethode mit zwei Aufrufen verwenden, verwenden Sie für die Speicherberechnung die double(!)-Routine
  3. Schauen Sie sich die Referenzimplementierung der Lapack-Routine an und berechnen Sie den benötigten Speicher selbst
  4. Verwenden Sie nur kleine Matrixgrößen, wenn Sie Float-/Komplexmatrizen verwenden

Andere Leute sind darüber gestolpert, haben es aber nicht bis zur wahren Ursache verfolgt, zB
openBLAS-Build mit int64-Unterstützung schlägt bei gültiger Eingabe für ssyevd fehl

Man muss betonen, dass es sich zumindest bei der Two-Call-Methode nicht um einen Bug, sondern um einen Designfehler handelt.
Im Fall der automatischen Speicherzuweisung von Lapacke muss dies als ziemlich schwerwiegender Fehler angesehen werden.

Grüße,

Oxidationsmittel

Bug

Hilfreichster Kommentar

lwork in IN/OUT zu ändern wäre ursprünglich eine nette Lösung, aber es ist nicht abwärtskompatibel. Die Anwendung müsste dann wissen, ob die LAPACK-Version <= 3.10 (sagen wir) oder > 3.10 war, um zu wissen, wo sie die Arbeit bekommt. Schlimmer noch, es gibt Fälle, in denen Anwendungen einen konstanten Wert übergeben – in der Erwartung, dass er konstant bleibt – sodass LAPACK sein Verhalten ändern würde, um diesen Wert zu überschreiben, wäre sehr schädlich (UB). Zum Beispiel in 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] );

Die Lösung, die ich vor einigen Jahren vorgeschlagen und in MAGMA implementiert habe, ist einfach in sgesdd usw., um die in work[1] zurückgegebene Arbeit etwas aufzurunden, sodass der zurückgegebene Wert immer >= der beabsichtigte Wert ist. Siehe https://bitbucket.org/icl/magma/src/master/control/magma_zauxiliary.cpp und verwenden in https://bitbucket.org/icl/magma/src/master/src/zgesdd.cpp. (Siehe ein Release für die generierte Single-Precision-Version.) Grundsätzlich ersetzen

    WORK( 1 ) = MAXWRK

mit

    WORK( 1 ) = lapack_roundup_lwork( MAXWRK )

wobei die Funktion lapack_roundup_lwork etwas aufrundet, wie es magma_*make_lwork tut. In MAGMA habe ich aufgerundet, indem ich mit (1 + eps) multipliziert habe, eps mit einfacher Genauigkeit verwendet, aber die Berechnung doppelt ausgeführt habe. Dann verhalten sich vorhandene Anwendungen korrekt, ohne dass ihre Arbeitsbereichsabfragen geändert werden müssen.

Nach weiteren Tests habe ich für lwork > 2^54 festgestellt, dass es die C/C++/Fortran-Definition von epsilon = 1.19e-07 (auch bekannt als ulp) verwenden muss und nicht die LAPACK-Definition slamch("eps") = 5.96e- 08 (auch bekannt als Einheitsrundung, u). Wenn Sie ulp verwenden, sieht es so aus, als ob die Berechnung einzeln durchgeführt werden kann.

Alle 6 Kommentare

Ich denke, es muss damals Sinn gemacht haben (die Größe über den Arbeitsarrayzeiger zurückzugeben), aber ich frage mich, was uns davon abhält, den Größenbezeichner in eine In-/Out-Variable zu verwandeln und auch dort den genauen Wert zurückzugeben? Ein "moderner" Aufrufer würde dies dann zuerst überprüfen und nur dann auf das Array-Mitglied work zurückgreifen, wenn lwork immer noch -1 war, ein "alter" Aufrufer würde keine Änderung bemerken.

Außerdem war das erforderliche LWORK damals wahrscheinlich physisch viel zu groß, um dieses Problem zu lösen, und ilp64 machte es offensichtlich. Ich träume von den Tagen, an denen der NB-Wert zur Laufzeit anstelle des Zwei-Aufruf-Schemas abgeleitet wird.

Hier sind zwei Diskussionsthreads dazu:
https://icl.cs.utk.edu/lapack-forum/viewtopic.php?t=1418
http://icl.cs.utk.edu/lapack-forum/archives/lapack/msg00827.html

Dies ist insbesondere ein Problem für Algorithmen, die einen O(n^2)-Arbeitsbereich erfordern. Für Algorithmen, die einen O(n*nb)-Arbeitsbereich erfordern, ist dies weniger problematisch.

Ja, das ist ein Konstruktionsfehler.

@martin-frbg: Wie würde Ihre vorgeschlagene Änderung für die C_work-Schnittstelle aussehen? Wir haben dort nur LWORK als INPUT. Wenn Sie LWORK in INPUT/OUTPUT ändern, gibt es eine große Änderung. Haben Sie eine Idee, dieses Problem zu lösen? Sehen:
https://github.com/Reference-LAPACK/lapack/blob/aa631b4b4bd13f6ae2dbab9ae9da209e1e05b0fc/LAPACKE/src/lapacke_dgeqrf_work.c#L35

Ich dachte, wir könnten auch einige Subroutinen für die Arbeitsbereichszuweisung erstellen, wie zum Beispiel LAPACK_dgeqrf__workspace_query( ) und dies würde den benötigten Arbeitsbereich zurückgeben.

Naja, nüchtern geht mein listiger Plan nicht wirklich auf...
Aber das sind eigentlich zwei Probleme, denke ich, zum einen die Arbeitsgröße, die ein lapack_int überläuft und zum anderen "nur" eine Fehldarstellung aufgrund begrenzter Präzision - ich frage mich, ob es möglich wäre, die berechnete Größe aufzurunden, um letztere auf Kosten von zu antizipieren "etwas" ungenutzter Speicher ?

Ja, das ist ein Konstruktionsfehler.

Ich sehe hier 2 verschiedene Fehler:

  1. Auf LAPACK: Routinen geben die Arbeitsgröße unter Verwendung einer realen Variablen zurück.
  2. Auf LAPACKE: Routinen verbreiten den Fehler von LAPACK.

Die Idee von @martin-frbg ist eine gute Lösung für (1). Neuer Fortran-Code könnte den Rückgabewert von LWORK anstelle von WORK(1) verwenden. Wir können versuchen, den Code mit einem (halb-)automatischen Verfahren zum Ersetzen zu ändern. In ssyevd.f könnten wir zum Beispiel ersetzen

  ELSE IF( LQUERY ) THEN
     RETURN
  END IF

von

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

Das Hinzufügen von LAPACKE_dgeqrf__work_query() , wie @langou vorschlägt, löst (2), obwohl mit dieser Modifikation viel Arbeit verbunden ist.

lwork in IN/OUT zu ändern wäre ursprünglich eine nette Lösung, aber es ist nicht abwärtskompatibel. Die Anwendung müsste dann wissen, ob die LAPACK-Version <= 3.10 (sagen wir) oder > 3.10 war, um zu wissen, wo sie die Arbeit bekommt. Schlimmer noch, es gibt Fälle, in denen Anwendungen einen konstanten Wert übergeben – in der Erwartung, dass er konstant bleibt – sodass LAPACK sein Verhalten ändern würde, um diesen Wert zu überschreiben, wäre sehr schädlich (UB). Zum Beispiel in 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] );

Die Lösung, die ich vor einigen Jahren vorgeschlagen und in MAGMA implementiert habe, ist einfach in sgesdd usw., um die in work[1] zurückgegebene Arbeit etwas aufzurunden, sodass der zurückgegebene Wert immer >= der beabsichtigte Wert ist. Siehe https://bitbucket.org/icl/magma/src/master/control/magma_zauxiliary.cpp und verwenden in https://bitbucket.org/icl/magma/src/master/src/zgesdd.cpp. (Siehe ein Release für die generierte Single-Precision-Version.) Grundsätzlich ersetzen

    WORK( 1 ) = MAXWRK

mit

    WORK( 1 ) = lapack_roundup_lwork( MAXWRK )

wobei die Funktion lapack_roundup_lwork etwas aufrundet, wie es magma_*make_lwork tut. In MAGMA habe ich aufgerundet, indem ich mit (1 + eps) multipliziert habe, eps mit einfacher Genauigkeit verwendet, aber die Berechnung doppelt ausgeführt habe. Dann verhalten sich vorhandene Anwendungen korrekt, ohne dass ihre Arbeitsbereichsabfragen geändert werden müssen.

Nach weiteren Tests habe ich für lwork > 2^54 festgestellt, dass es die C/C++/Fortran-Definition von epsilon = 1.19e-07 (auch bekannt als ulp) verwenden muss und nicht die LAPACK-Definition slamch("eps") = 5.96e- 08 (auch bekannt als Einheitsrundung, u). Wenn Sie ulp verwenden, sieht es so aus, als ob die Berechnung einzeln durchgeführt werden kann.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

Dichloromethane picture Dichloromethane  ·  11Kommentare

Peter9606 picture Peter9606  ·  7Kommentare

weslleyspereira picture weslleyspereira  ·  5Kommentare

5tefan picture 5tefan  ·  3Kommentare

h-vetinari picture h-vetinari  ·  8Kommentare