Lapack: Hilangnya presisi di semua kueri ruang kerja presisi tunggal, implisit dan eksplisit

Dibuat pada 19 Jul 2021  ·  6Komentar  ·  Sumber: Reference-LAPACK/lapack

Halo semua,

baru-baru ini saya menemukan kesalahan saat menggunakan ssyevd melalui antarmuka lapacke.
Ini menunjukkan masalah di antarmuka lapack secara umum. Ini berjalan seperti ini:

Menurut antarmuka standar lapack, banyak rutinitas seperti ssyevd yang harus Anda panggil dua kali:
Sekali untuk menanyakan rutin berapa banyak memori awal yang dibutuhkan untuk ukuran matriks tertentu,
dan baru kemudian panggil rutin dengan sungguh-sungguh dengan bagian memori awal yang diperlukan
sebagai parameter.

Jika Anda melihat lebih dekat pada panggilan pertama ini, yang seharusnya mengembalikan ukuran memori yang diperlukan, misalnya
untuk rutinitas seperti ssyevd, Anda melihat bahwa bahkan menurut dokumentasi lapack,
kebutuhan memori dilewatkan kembali melalui pointer ke nilai float .
Jadi saat menghitung memori, ia melewati serangkaian nilai:

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

Ini adalah int64 dalam kasus antarmuka ilp64, jika tidak maka akan menjadi int32.
Jadi intinya, kami memiliki pemendekan perantara dari nilai memori dari
63 bit hingga 24 bit !!!
(atau lebih tepatnya, hingga 24bit antara set bit terluar dalam representasi float IEEE 754)
Bahkan dalam kasus bilangan bulat 32bit, Anda memiliki pemendekan dari 31 bit menjadi 24 bit.

Jadi, jika Anda akan memanggil perhitungan kebutuhan memori seperti di masa lalu 'dengan tangan'
Anda mungkin memiliki kesempatan untuk melihat di mana kesalahannya, tetapi Anda tetap tidak dapat mencegah korslet
ke nilai mengambang. Jika Anda menggunakan alokasi memori otomatis melalui antarmuka lapacke modern
Anda bahkan tidak tahu apa yang salah, karena rutinitas mengiklankan untuk berhati-hati
dari semua manajemen memori dengan sendirinya!

Ini terjadi di semua rutinitas presisi tunggal (s/c) di lapack, yang menghitung
kebutuhan memori sebagai langkah perantara.
Beralih ke presisi ganda kemudian akan menggunakan referensi presisi ganda,
meningkatkan nilai perantara menjadi 53 bit, yang masih belum mendekati 64 bit
orang akan berasumsi dengan antarmuka 64bit.

Solusi, empat cara yang mungkin:

  1. Jika Anda ingin menggunakan rutinitas lapack tunggal atau kompleks, jangan gunakan alokasi memori otomatis melalui antarmuka C lapacke
  2. Jika Anda menggunakan metode fungsi lapack dua panggilan, untuk perhitungan memori gunakan rutinitas double(!)
  3. Lihat implementasi referensi dari rutinitas lapack dan hitung sendiri memori yang diperlukan
  4. Hanya gunakan ukuran matriks kecil saat menggunakan matriks float/kompleks

Orang lain telah menemukan ini, tetapi tidak menindaklanjutinya ke penyebab sebenarnya, misalnya
build openBLAS dengan dukungan int64 gagal pada input yang valid untuk ssyevd

Kita harus menekankan, bahwa setidaknya dengan metode dua panggilan, ini bukan bug, tetapi cacat desain.
Dalam hal alokasi memori otomatis lapacke, itu harus dianggap sebagai bug yang cukup parah.

Salam,

pengoksidasi

Bug

Komentar yang paling membantu

Mengubah lwork menjadi IN/OUT akan menjadi solusi yang bagus pada awalnya, tetapi tidak kompatibel ke belakang. Aplikasi kemudian harus mengetahui apakah versi LAPACK adalah <= 3.10 (katakanlah) atau > 3.10 untuk mengetahui di mana mendapatkan lwork. Lebih buruk lagi, ada kasus di mana aplikasi meneruskan nilai const — mengharapkannya tetap const — jadi LAPACK mengubah perilakunya untuk menimpa nilai itu akan sangat merugikan (UB). Misalnya, di 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] );

Solusi yang saya usulkan beberapa tahun yang lalu dan diimplementasikan di MAGMA hanya di sgesdd, dll., untuk membulatkan pekerjaan yang dikembalikan dalam pekerjaan[1] sedikit ke atas, sehingga nilai yang dikembalikan selalu >= nilai yang diinginkan. Lihat https://bitbucket.org/icl/magma/src/master/control/magma_zauxiliary.cpp dan gunakan di https://bitbucket.org/icl/magma/src/master/src/zgesdd.cpp. (Lihat rilis untuk versi presisi tunggal yang dihasilkan.) Pada dasarnya ganti

    WORK( 1 ) = MAXWRK

dengan

    WORK( 1 ) = lapack_roundup_lwork( MAXWRK )

di mana fungsi lapack_roundup_lwork membulatkannya sedikit, seperti yang dilakukan magma_*make_lwork . Di MAGMA, saya membulatkan dengan mengalikan dengan (1 + eps), menggunakan eps presisi tunggal tetapi melakukan perhitungan secara ganda. Kemudian aplikasi yang ada akan berperilaku dengan benar tanpa perlu mengubah kueri ruang kerjanya.

Setelah pengujian lebih lanjut, saya menemukan untuk lwork > 2^54, perlu menggunakan definisi C/C++/Fortran dari epsilon = 1.19e-07 (alias ulp), daripada definisi LAPACK slamch("eps") = 5.96e- 08 (alias pembulatan unit, u). Jika menggunakan ulp, sepertinya perhitungannya bisa dilakukan secara tunggal.

Semua 6 komentar

Saya kira itu pasti masuk akal pada saat itu (untuk mengembalikan ukuran melalui penunjuk array kerja), tetapi saya ingin tahu apa yang membuat kita tidak mengubah penentu ukuran menjadi variabel masuk/keluar dan mengembalikan nilai yang tepat di sana juga? Penelepon "modern" kemudian akan memeriksanya terlebih dahulu dan menggunakan anggota array kerja hanya jika lwork masih -1, penelepon "lama" tidak akan melihat ada perubahan.

Juga mungkin LWORK yang diperlukan saat itu secara fisik terlalu besar untuk mengatasi masalah ini dan ilp64 membuatnya jelas. Saya memimpikan hari-hari di mana nilai NB diturunkan saat runtime alih-alih skema dua panggilan.

Berikut adalah dua utas diskusi yang terkait dengan ini:
https://icl.cs.utk.edu/lapack-forum/viewtopic.php?t=1418
http://icl.cs.utk.edu/lapack-forum/archives/lapack/msg00827.html

Ini terutama merupakan masalah untuk algoritma yang membutuhkan ruang kerja O(n^2). Untuk algoritme yang memerlukan ruang kerja O(n*nb), ini bukan masalah.

Ya, ini adalah kesalahan desain.

@martin-frbg: bagaimana perubahan yang Anda usulkan untuk antarmuka C _work? Kami memiliki LWORK sebagai INPUT hanya di sana. Mengubah LWORK menjadi INPUT/OUTPUT ada perubahan besar. Apakah Anda punya ide untuk menyelesaikan masalah ini? Lihat:
https://github.com/Reference-LAPACK/lapack/blob/aa631b4b4bd13f6ae2dbab9ae9da209e1e05b0fc/LAPACKE/src/lapacke_dgeqrf_work.c#L35

Saya berpikir bahwa kami juga dapat membuat beberapa subrutin alokasi ruang kerja seperti LAPACK_dgeqrf__workspace_query() dan ini akan mengembalikan ruang kerja yang diperlukan.

Welp, rencana licik saya tidak benar-benar bekerja ketika sadar ...
Tetapi ini sebenarnya adalah dua masalah yang saya pikir, satu ukuran kerja yang meluap dari lapack_int dan yang lainnya "hanya" salah representasi karena presisi terbatas - saya ingin tahu apakah mungkin untuk mengumpulkan ukuran yang dihitung untuk mengantisipasi yang terakhir dengan mengorbankan "beberapa" memori yang tidak terpakai?

Ya, ini adalah kesalahan desain.

Saya dapat melihat 2 kekurangan yang berbeda di sini:

  1. Pada LAPACK: rutinitas mengembalikan ukuran pekerjaan menggunakan variabel nyata.
  2. Di LAPACKE: rutinitas menyebarkan cacat dari LAPACK.

Ide @martin-frbg adalah solusi yang baik untuk (1). Kode Fortran baru dapat menggunakan nilai kembalian LWORK alih-alih WORK(1). Kita dapat mencoba memodifikasi kode dengan beberapa prosedur penggantian (semi-)otomatis. Di ssyevd.f , misalnya, kita bisa mengganti

  ELSE IF( LQUERY ) THEN
     RETURN
  END IF

oleh

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

Menambahkan LAPACKE_dgeqrf__work_query() , seperti yang disarankan @langou , menyelesaikan (2), meskipun ada banyak pekerjaan yang terkait dengan modifikasi ini.

Mengubah lwork menjadi IN/OUT akan menjadi solusi yang bagus pada awalnya, tetapi tidak kompatibel ke belakang. Aplikasi kemudian harus mengetahui apakah versi LAPACK adalah <= 3.10 (katakanlah) atau > 3.10 untuk mengetahui di mana mendapatkan lwork. Lebih buruk lagi, ada kasus di mana aplikasi meneruskan nilai const — mengharapkannya tetap const — jadi LAPACK mengubah perilakunya untuk menimpa nilai itu akan sangat merugikan (UB). Misalnya, di 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] );

Solusi yang saya usulkan beberapa tahun yang lalu dan diimplementasikan di MAGMA hanya di sgesdd, dll., untuk membulatkan pekerjaan yang dikembalikan dalam pekerjaan[1] sedikit ke atas, sehingga nilai yang dikembalikan selalu >= nilai yang diinginkan. Lihat https://bitbucket.org/icl/magma/src/master/control/magma_zauxiliary.cpp dan gunakan di https://bitbucket.org/icl/magma/src/master/src/zgesdd.cpp. (Lihat rilis untuk versi presisi tunggal yang dihasilkan.) Pada dasarnya ganti

    WORK( 1 ) = MAXWRK

dengan

    WORK( 1 ) = lapack_roundup_lwork( MAXWRK )

di mana fungsi lapack_roundup_lwork membulatkannya sedikit, seperti yang dilakukan magma_*make_lwork . Di MAGMA, saya membulatkan dengan mengalikan dengan (1 + eps), menggunakan eps presisi tunggal tetapi melakukan perhitungan secara ganda. Kemudian aplikasi yang ada akan berperilaku dengan benar tanpa perlu mengubah kueri ruang kerjanya.

Setelah pengujian lebih lanjut, saya menemukan untuk lwork > 2^54, perlu menggunakan definisi C/C++/Fortran dari epsilon = 1.19e-07 (alias ulp), daripada definisi LAPACK slamch("eps") = 5.96e- 08 (alias pembulatan unit, u). Jika menggunakan ulp, sepertinya perhitungannya bisa dilakukan secara tunggal.

Apakah halaman ini membantu?
0 / 5 - 0 peringkat