Lapack: 所有单精度工作区查询中的精度损失,隐式和显式

创建于 2021-07-19  ·  6评论  ·  资料来源: Reference-LAPACK/lapack

大家好,

最近我在通过 lapacke 接口使用 ssyevd 时偶然发现了一个错误。
它通常指向 lapack 接口中的一个问题。 它是这样的:

根据lapack标准接口,很多像ssyevd这样的例程你必须调用两次:
曾经询问例程对于某个矩阵大小需要多少暂存内存,
然后才用所需的临时内存部分认真调用例程
作为参数。

如果您仔细查看第一个调用,它应该返回所需的内存大小,例如
对于像 ssyevd 这样的例程,即使根据 lapack 文档,您也会看到,
内存要求通过指向浮点值的指针传回。
因此,在计算内存时,它会通过一系列值:

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

如果是 ilp64 接口,则为 int64,否则为 int32。
所以本质上,我们有一个中间缩短的内存值
63 位到 24 位!!!
(或更准确地说,在 IEEE 754 浮点表示中最外层设置位之间的 24 位)
即使在 32 位整数的情况下,您也可以将 31 位缩短为 24 位。

因此,如果您像过去一样“手动”调用内存需求计算
你可能有机会看到哪里出了问题,但你仍然无法防止短路
为浮点值。 如果您通过现代 lapacke 接口使用自动内存分配
你甚至不知道哪里出了问题,因为例行公事宣传要小心
所有内存管理本身!

这发生在 lapack 中的所有单精度例程 (s/c) 中,它计算
内存要求作为中间步骤。
然后切换到双精度将使用双精度引用,
将中间值增加到 53 位,这仍然不接近 64 位
人们会假设使用 64 位接口。

解决方法,四种可能的方法:

  1. 如果要使用单个或复杂的 lapack 例程,请不要通过 C lapacke 接口使用自动内存分配
  2. 如果您使用两次调用 lapack 函数方法,对于内存计算,请使用 double(!) 例程
  3. 看一下lapack例程的参考实现,自己计算所需内存
  4. 使用浮点/复杂矩阵时仅使用小矩阵大小

其他人偶然发现了这一点,但没有追查到真正的原因,例如
具有 int64 支持的 openBLAS 构建在 ssyevd 的有效输入上失败

必须强调,至少对于两次调用方法,这不是错误,而是设计缺陷。
在 lapacke 自动内存分配的情况下,它必须被认为是一个相当严重的错误。

问候,

氧化机

最有用的评论

将 lwork 更改为 IN/OUT 本来是一个不错的解决方案,但它不向后兼容。 然后,应用程序必须知道 LAPACK 版本是 <= 3.10(比如说)还是 > 3.10 才能知道从哪里获取 lwork。 更糟糕的是,在某些情况下,应用程序传入一个 const 值——期望它保持 const——所以 LAPACK 改变其行为以覆盖该值将是非常有害的 (UB)。 例如,在 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] );

我几年前提出并在 MAGMA 中实现的解决方案只是在 sgesdd 等中,将 work[1] 中返回的 lwork 向上舍入一点,因此返回值始终 >= 预期值。 请参阅https://bitbucket.org/icl/magma/src/master/control/magma_zauxiliary.cpp并在https://bitbucket.org/icl/magma/src/master/src/zgesdd.cpp 中使用

    WORK( 1 ) = MAXWRK

    WORK( 1 ) = lapack_roundup_lwork( MAXWRK )

函数lapack_roundup_lwork它稍微四舍五入,就像magma_*make_lwork那样。 在 MAGMA 中,我通过乘以 (1 + eps) 进行四舍五入,使用单精度 eps 但以双精度进行计算。 然后现有的应用程序将正确运行而无需更改其工作区查询。

经过更多测试,我发现对于 lwork > 2^54,它需要使用 epsilon = 1.19e-07(又名 ulp)的 C/C++/Fortran 定义,而不是 LAPACK 定义 slamch("eps") = 5.96e- 08(又名单位舍入,u)。 如果使用ulp,看起来计算可以单次完成。

所有6条评论

我想当时它一定是有意义的(通过工作数组指针返回大小),但我想知道是什么阻止我们将大小说明符转换为输入/输出变量并将确切值传回那里? 然后,“现代”调用者将首先检查并仅在 lwork 仍为 -1 时才求助于 work 数组成员,“旧”调用者将注意到没有任何变化。

也可能当时所需的 LWORK 在物理上太大而无法解决这个问题,而 ilp64 让它变得很明显。 我梦想着在运行时派生 NB 值而不是两次调用方案的日子。

以下是与此相关的两个讨论主题:
https://icl.cs.utk.edu/lapack-forum/viewtopic.php?t=1418
http://icl.cs.utk.edu/lapack-forum/archives/lapack/msg00827.html

对于需要 O(n^2) 工作空间的算法来说,这尤其是一个问题。 对于需要 O(n*nb) 工作空间的算法,这不是问题。

是的,这是一个设计缺陷。

@martin-frbg:您提议对 C _work 接口进行怎样的更改? 我们只有在那里有 LWORK 作为输入。 将 LWORK 更改为 INPUT/OUTPUT 有一个重大变化。 你有解决这个问题的想法吗? 看:
https://github.com/Reference-LAPACK/lapack/blob/aa631b4b4bd13f6ae2dbab9ae9da209e1e05b0fc/LAPACKE/src/lapacke_dgeqrf_work.c#L35

我想我们还可以创建一些工作空间分配子例程,例如 LAPACK_dgeqrf__workspace_query(),这将返回所需的工作空间。

好吧,我的狡猾计划在清醒时真的不起作用......
但这实际上是我认为的两个问题,一个是工作大小溢出 lapack_int,另一个是由于精度有限而“仅”是误传 - 我想知道是否有可能将计算出的大小四舍五入以预测后者,但代价是“一些”未使用的内存?

是的,这是一个设计缺陷。

我可以在这里看到 2 个不同的缺陷:

  1. 在 LAPACK 上:例程使用实变量返回工作大小。
  2. 在 LAPACKE 上:例程从 LAPACK 传播缺陷。

@martin-frbg 的想法是(1)的一个很好的解决方案。 新的 Fortran 代码可以使用 LWORK 的返回值而不是 WORK(1)。 我们可以尝试使用一些(半)自动替换程序来修改代码。 例如,在ssyevd.f ,我们可以替换

  ELSE IF( LQUERY ) THEN
     RETURN
  END IF

经过

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

添加LAPACKE_dgeqrf__work_query() ,正如@langou 所建议的,解决了(2),尽管有很多与此修改相关的工作。

将 lwork 更改为 IN/OUT 本来是一个不错的解决方案,但它不向后兼容。 然后,应用程序必须知道 LAPACK 版本是 <= 3.10(比如说)还是 > 3.10 才能知道从哪里获取 lwork。 更糟糕的是,在某些情况下,应用程序传入一个 const 值——期望它保持 const——所以 LAPACK 改变其行为以覆盖该值将是非常有害的 (UB)。 例如,在 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] );

我几年前提出并在 MAGMA 中实现的解决方案只是在 sgesdd 等中,将 work[1] 中返回的 lwork 向上舍入一点,因此返回值始终 >= 预期值。 请参阅https://bitbucket.org/icl/magma/src/master/control/magma_zauxiliary.cpp并在https://bitbucket.org/icl/magma/src/master/src/zgesdd.cpp 中使用

    WORK( 1 ) = MAXWRK

    WORK( 1 ) = lapack_roundup_lwork( MAXWRK )

函数lapack_roundup_lwork它稍微四舍五入,就像magma_*make_lwork那样。 在 MAGMA 中,我通过乘以 (1 + eps) 进行四舍五入,使用单精度 eps 但以双精度进行计算。 然后现有的应用程序将正确运行而无需更改其工作区查询。

经过更多测试,我发现对于 lwork > 2^54,它需要使用 epsilon = 1.19e-07(又名 ulp)的 C/C++/Fortran 定义,而不是 LAPACK 定义 slamch("eps") = 5.96e- 08(又名单位舍入,u)。 如果使用ulp,看起来计算可以单次完成。

此页面是否有帮助?
0 / 5 - 0 等级