Lapack: Pérdida de precisión en todas las consultas de espacio de trabajo de precisión simple, implícitas y explícitas

Creado en 19 jul. 2021  ·  6Comentarios  ·  Fuente: Reference-LAPACK/lapack

Hola todos,

Recientemente me encontré con un error al usar ssyevd a través de la interfaz lapacke.
Señala un problema en la interfaz de lapack en general. Dice así:

De acuerdo con la interfaz estándar de lapack, muchas rutinas como ssyevd tienes que llamar dos veces:
Una vez para preguntarle a la rutina cuánta memoria temporal necesita para un determinado tamaño de matriz,
y solo entonces llame a la rutina en serio con las secciones de memoria temporal requeridas
como parámetros.

Si observa de cerca esta primera llamada, debería devolver el tamaño de memoria requerido, por ejemplo
para una rutina como ssyevd, ves que incluso de acuerdo con la documentación de lapack,
el requisito de memoria se devuelve mediante un puntero a un valor flotante .
Entonces, al calcular la memoria, pasa por una serie de valores:

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

Es int64 en el caso de una interfaz ilp64, de lo contrario sería int32.
Entonces, en esencia, tenemos un acortamiento intermedio del valor de memoria de
63 bit a 24 bit !!!
(o más preciso, a 24 bits entre los bits establecidos más externos en la representación flotante IEEE 754)
Incluso en el caso de números enteros de 32 bits, tiene un acortamiento de 31 bits a 24 bits.

Entonces, si llamara al cálculo de requisitos de memoria como en el pasado 'a mano'
es posible que tenga la oportunidad de ver dónde sale mal, pero aún así no puede evitar el cortocircuito
a un valor flotante. Si utiliza la asignación automática de memoria a través de la moderna interfaz lapacke
ni siquiera tienes idea de lo que podría estar mal, ya que la rutina anuncia que debes cuidar
de toda la gestión de la memoria por sí misma!

Esto sucede en todas las rutinas de precisión simple (s / c) en lapack, que calculan
el requisito de memoria como paso intermedio.
Cambiar a doble precisión usaría una referencia de doble precisión en su lugar,
aumentando el valor intermedio a 53 bits en su lugar, que todavía no está cerca de los 64 bits
uno asumiría con una interfaz de 64 bits.

Solución alternativa, cuatro formas posibles:

  1. Si desea utilizar rutinas lapack simples o complejas, no utilice la asignación automática de memoria a través de la interfaz C lapacke
  2. Si usa el método de función lapack de dos llamadas, para el cálculo de memoria use la rutina doble (!)
  3. Eche un vistazo a la implementación de referencia de la rutina lapack y calcule la memoria requerida por su cuenta
  4. Utilice únicamente tamaños de matriz pequeños cuando utilice matrices flotantes / complejas

Otras personas han tropezado con esto, pero no lo siguieron hasta la causa real, p. Ej.
La compilación de openBLAS con soporte int64 falla en una entrada válida para ssyevd

Hay que enfatizar que, al menos con el método de dos llamadas, esto no es un error, sino un defecto de diseño.
En el caso de la asignación automática de memoria de lapacke, debe considerarse un error bastante grave.

Saludos,

oxídico

Bug

Comentario más útil

Cambiar lwork a IN / OUT sería una buena solución originalmente, pero no es compatible con versiones anteriores. La aplicación tendría que saber si la versión LAPACK era <= 3.10 (digamos) o> 3.10 para saber dónde obtener el trabajo. Peor aún, hay casos en los que las aplicaciones pasan un valor constante, esperando que permanezca constante, por lo que LAPACK cambiar su comportamiento para sobrescribir ese valor sería muy perjudicial (UB). Por ejemplo, en 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 solución que propuse hace algunos años e implementé en MAGMA es simplemente en sgesdd, etc., para redondear un poco el trabajo devuelto en el trabajo [1], por lo que el valor devuelto es siempre> = el valor deseado. Consulte https://bitbucket.org/icl/magma/src/master/control/magma_zauxiliary.cpp y utilícelo en https://bitbucket.org/icl/magma/src/master/src/zgesdd.cpp. (Consulte un comunicado de la versión de precisión simple generada). Básicamente, reemplace

    WORK( 1 ) = MAXWRK

con

    WORK( 1 ) = lapack_roundup_lwork( MAXWRK )

donde la función lapack_roundup_lwork redondea ligeramente, como lo hace magma_*make_lwork . En MAGMA, redondeé multiplicando por (1 + eps), usando eps de precisión simple pero haciendo el cálculo por duplicado. Entonces, las aplicaciones existentes se comportarán correctamente sin necesidad de cambiar sus consultas de espacio de trabajo.

Después de más pruebas, encontré para lwork> 2 ^ 54, necesita usar la definición de C / C ++ / Fortran de epsilon = 1.19e-07 (también conocido como ulp), en lugar de la definición de LAPACK slamch ("eps") = 5.96e- 08 (también conocido como redondeo de unidades, u). Si usa ulp, parece que el cálculo se puede hacer en single.

Todos 6 comentarios

Supongo que debe haber tenido sentido en ese momento (para devolver el tamaño a través del puntero de la matriz de trabajo), pero me pregunto qué nos impide convertir el especificador de tamaño en una variable de entrada / salida y devolver el valor exacto allí también. Un llamador "moderno" comprobaría eso primero y recurriría al miembro de la matriz de trabajo solo si lwork todavía era -1, un llamador "antiguo" no notaría ningún cambio.

También probablemente el LWORK requerido en ese entonces era físicamente demasiado grande para resolver este problema e ilp64 lo hizo obvio. Estoy soñando con los días en los que el valor NB se deriva en tiempo de ejecución en lugar del esquema de dos llamadas.

Aquí hay dos hilos de discusión relacionados con esto:
https://icl.cs.utk.edu/lapack-forum/viewtopic.php?t=1418
http://icl.cs.utk.edu/lapack-forum/archives/lapack/msg00827.html

Esto es especialmente un problema para los algoritmos que requieren un espacio de trabajo O (n ^ 2). Para el algoritmo que requiere un espacio de trabajo O (n * nb), esto es un problema menor.

Sí, esto es un defecto de diseño.

@ martin-frbg: ¿cómo sería el cambio propuesto para la interfaz C _work? Tenemos LWORK como ENTRADA solo allí. Al cambiar LWORK a INPUT / OUTPUT hay un cambio importante. ¿Tienes una idea para solucionar este problema? Ver:
https://github.com/Reference-LAPACK/lapack/blob/aa631b4b4bd13f6ae2dbab9ae9da209e1e05b0fc/LAPACKE/src/lapacke_dgeqrf_work.c#L35

Estaba pensando que también podríamos crear algunas subrutinas de asignación de espacio de trabajo como LAPACK_dgeqrf__workspace_query () y esto devolvería el espacio de trabajo necesario.

Bueno, mi astuto plan no funciona realmente cuando estoy sobrio ...
Pero estos son en realidad dos problemas, creo, uno el tamaño del trabajo desborda un lapack_int y el otro "solo" una tergiversación debido a una precisión limitada. Me pregunto si sería posible redondear el tamaño calculado para anticipar el último a expensas de ¿"alguna" memoria no utilizada?

Sí, esto es un defecto de diseño.

Puedo ver 2 defectos diferentes aquí:

  1. En LAPACK: las rutinas devuelven el tamaño del trabajo utilizando una variable real.
  2. En LAPACKE: las rutinas propagan la falla de LAPACK.

La idea de @ martin-frbg es una buena solución para (1). El nuevo código de Fortran podría usar el valor de retorno de LWORK en lugar de WORK (1). Podemos intentar modificar el código con algún procedimiento (semi) automático para su reemplazo. En ssyevd.f , por ejemplo, podríamos reemplazar

  ELSE IF( LQUERY ) THEN
     RETURN
  END IF

por

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

Agregar LAPACKE_dgeqrf__work_query() , como sugiere @langou , resuelve (2), aunque hay mucho trabajo asociado con esta modificación.

Cambiar lwork a IN / OUT sería una buena solución originalmente, pero no es compatible con versiones anteriores. La aplicación tendría que saber si la versión LAPACK era <= 3.10 (digamos) o> 3.10 para saber dónde obtener el trabajo. Peor aún, hay casos en los que las aplicaciones pasan un valor constante, esperando que permanezca constante, por lo que LAPACK cambiar su comportamiento para sobrescribir ese valor sería muy perjudicial (UB). Por ejemplo, en 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 solución que propuse hace algunos años e implementé en MAGMA es simplemente en sgesdd, etc., para redondear un poco el trabajo devuelto en el trabajo [1], por lo que el valor devuelto es siempre> = el valor deseado. Consulte https://bitbucket.org/icl/magma/src/master/control/magma_zauxiliary.cpp y utilícelo en https://bitbucket.org/icl/magma/src/master/src/zgesdd.cpp. (Consulte un comunicado de la versión de precisión simple generada). Básicamente, reemplace

    WORK( 1 ) = MAXWRK

con

    WORK( 1 ) = lapack_roundup_lwork( MAXWRK )

donde la función lapack_roundup_lwork redondea ligeramente, como lo hace magma_*make_lwork . En MAGMA, redondeé multiplicando por (1 + eps), usando eps de precisión simple pero haciendo el cálculo por duplicado. Entonces, las aplicaciones existentes se comportarán correctamente sin necesidad de cambiar sus consultas de espacio de trabajo.

Después de más pruebas, encontré para lwork> 2 ^ 54, necesita usar la definición de C / C ++ / Fortran de epsilon = 1.19e-07 (también conocido como ulp), en lugar de la definición de LAPACK slamch ("eps") = 5.96e- 08 (también conocido como redondeo de unidades, u). Si usa ulp, parece que el cálculo se puede hacer en single.

¿Fue útil esta página
0 / 5 - 0 calificaciones