最快的线性系统求解小平方矩阵(10x10)


9

我对通过线性系统求解小矩阵(10x10)(有时也称为矩阵)来优化地狱非常感兴趣。有没有现成的解决方案?矩阵可以假定为非奇异的。

此求解器将在Intel CPU上执行超过1000000次(以微秒为单位)。我说的是计算机游戏中使用的优化级别。无论是在特定于汇编和体系结构的代码中进行编码,还是研究精度或可靠性方面的折衷并使用浮点hack(我都使用-ffast-math编译标志,这都没有问题)。解决甚至可能在大约20%的时间内失败!

Eigen的partialPivLu在我当前的基准测试中是最快的,当使用-O3和良好的编译器进行优化时,性能优于LAPACK。但是现在我要手工制作一个定制的线性求解器。任何建议将不胜感激。我将使我的解决方案开源,并会在出版物等方面获得关键见解。

相关:用块对角矩阵求解线性系统 的速度什么是最快的方法来反转数百万个矩阵? https://stackoverflow.com/q/50909385/1489510


7
这看起来像一个舒展的目标。假设我们使用最快的Skylake-X Xeon Platinum 8180,理论峰值吞吐量为4个单精度TFLOP,并且一个10x10系统需要解决大约700个(大约2n ** 3/3)浮点运算。然后,理论上可以在175微秒内解决一批1M这样的系统。这是无法超过的光速数字。您可以使用现有最快的代码分享当前正在实现的性能吗?顺便说一句,数据是单精度还是双精度?
njuffa

@njuffa是的,我的目标是接近1ms,但是微型又是另外一回事。对于微型计算机,我考虑通过检测经常发生的相似矩阵来利用批处理中的增量逆结构。根据处理器的不同,Perf的当前范围为10-500ms。精度是两倍甚至复杂的两倍。单精度确实较慢。
rfabbri

@njuffa我可以降低或提高速度精度
rfabbri

2
似乎精度/准确性不是您的首要任务。对于您的目标,也许在相对较少的评估数处被截断的迭代方法有用吗?特别是如果您有合理的初步猜测。
斯潘塞·布林格森

1
你枢轴吗?您能进行QR因式分解而不是高斯消除吗?您是否交错系统,以便可以使用SIMD指令并一次执行多个系统?您是否编写没有循环且没有间接寻址的直线程序?您想要什么精度?我将如何调节您的系统?它们是否具有可以利用的结构。
卡尔·克里斯蒂安

Answers:


7

使用在编译时将行数和列数编码为该类型的本征矩阵类型,可以使您比LAPACK更具优势,在LAPACK中,矩阵大小仅在运行时才知道。这些额外的信息使编译器可以执行完全或部分循环展开,从而消除了许多分支指令。如果您要使用现有的库而不是编写自己的内核,那么具有可将矩阵大小作为C ++模板参数包括在内的数据类型可能是必不可少的。我所知道的唯一一个这样做的库是blaze,因此可能值得对Eigen进行基准测试。

如果您决定推出自己的实现,则可能会发现PETSc对其块CSR格式所做的工作是一个有用的示例,尽管PETSc本身可能并不是您所想到的正确工具。他们没有写出循环,而是为小型矩阵向量乘法显式地写出了每个操作(请参见其存储库中的该文件)。这样可以保证没有分支指令像您可能会得到的循环一样。带有AVX指令的代码版本是如何实际使用向量扩展的一个很好的示例。例如,此功能使用__m256d数据类型以同时对四个双精度进行运算。通过仅使用LU分解而不是矩阵向量乘法的方式使用向量扩展名显式写出所有运算,可以显着提高性能。与其实际编写C代码,不如使用脚本来生成它。当您对一些操作进行重新排序以更好地利用指令流水线方法时,看看是否存在明显的性能差异也可能很有趣。

您还可以从STOKE工具中获得一些收益,该工具将随机探索可能的程序转换的空间以找到更快的版本。


发射 我已经成功使用了Map <const Matrix <complex,10,10>> AA(A)之类的特征。将检查其他内容。
rfabbri

Eigen也有AVX,甚至还有complex.h标头。为什么要使用PETSc?在这种情况下,很难与本征竞争。对于问题,我甚至专门针对Eigen进行了改进,并采用了近似枢轴策略,而不是将max占用一列,而是在发现另一个大于3个数量级的轴时立即交换了枢轴。
rfabbri

1
@rfabbri我并不是建议您为此使用PETSc,只是建议他们在特定情况下所做的工作具有指导意义。我已经编辑了答案以使答案更清楚。
Daniel Shapero

4

另一个想法可能是使用生成方法(编写程序的程序)。编写一个(元)程序,该程序吐出C / C ++指令序列以在10x10系统上执行非透视** LU。.基本上取k / i / j循环嵌套并将其展平为O(1000)行左右。标量算术。然后将生成的程序输入到优化的编译器中。我认为这很有趣,删除循环会暴露每个数据相关性和冗余子表达式,并为编译器提供了最大的机会来对指令进行重新排序,以便它们可以很好地映射到实际硬件(例如,执行单元的数量,危害/停滞,因此上)。

如果您碰巧知道所有矩阵(甚至只有几个矩阵),则可以通过调用SIMD内部函数/函数(SSE / AVX)而不是标量代码来提高吞吐量。在这里,您将利用实例之间令人尴尬的并行性,而不是在单个实例中追求任何并行性。例如,您可以使用AVX256内在函数同时执行4个双精度LU,方法是将4个矩阵打包在“整个”寄存器上,并对所有矩阵进行相同的操作**。

**因此,重点放在未透视的LU上。透视会以两种方式破坏这种方法。首先,由于枢轴选择,它引入了分支,这意味着您的数据依赖关系并不为人所知。其次,这意味着不同的SIMD“插槽”将必须执行不同的操作,因为实例A的旋转可能不同于实例B。每列对角线)。


由于矩阵是如此之小,如果预先缩放比例,则可能可以消除旋转。甚至没有预先透视矩阵。我们所需要的只是条目之间的差异在2-3个数量级之内。
rfabbri

2

您的问题导致两个不同的考虑。

首先,您需要选择正确的算法。因此,应该考虑矩阵是否具有任何结构的问题。例如,当矩阵对称时,Cholesky分解比LU更有效。当您只需要有限的精度时,迭代方法可以更快。

其次,您需要有效地实现算法。为此,您需要了解算法的瓶颈。您的实现受内存传输速度还是计算速度的限制。因为你只考虑10×10矩阵,您的矩阵应完全适合CPU缓存。因此,您应该利用处理器的SIMD单元(SSE,AVX等)和内核,以便每个周期进行尽可能多的计算。

总之,问题的答案在很大程度上取决于您考虑的硬件和矩阵。可能没有确定的答案,您必须尝试一些方法才能找到最佳方法。


到目前为止,Eigen已经进行了大量优化,使用了SEE,AVX等,我在初步测试中尝试了迭代方法,但它们没有帮助。我尝试了英特尔MKL,但没有比具有优化GCC标志的Eigen好。我目前正在尝试手工制作比Eigen更好和更简单的东西,并使用迭代方法进行更详细的测试。
rfabbri

1

我会尝试逐块反转。

https://zh.wikipedia.org/wiki/Invertible_matrix#Blockwise_inversion

Eigen使用优化的例程来计算4x4矩阵的逆,这可能是您将获得的最佳结果。尝试尽可能多地使用它。

http://www.eigen.tuxfamily.org/dox/Inverse__SSE_8h_source.html

左上方:8x8。右上方:8x2。左下角:2x8。右下:2x2。使用优化的4x4反转代码反转8x8。其余为矩阵产品。

编辑:使用6x6、6x4、4x6和4x4块显示比我上面描述的要快一些。

using namespace Eigen;

template<typename Scalar, int tl_size, int br_size>
Matrix<Scalar, tl_size + br_size, tl_size + br_size> blockwise_inversion(const Matrix<Scalar, tl_size, tl_size>& A, const Matrix<Scalar, tl_size, br_size>& B, const Matrix<Scalar, br_size, tl_size>& C, const Matrix<Scalar, br_size, br_size>& D)
{
    Matrix<Scalar, tl_size + br_size, tl_size + br_size> result;

    Matrix<Scalar, tl_size, tl_size> A_inv = A.inverse().eval();
    Matrix<Scalar, br_size, br_size> DCAB_inv = (D - C * A_inv * B).inverse();

    result.topLeftCorner<tl_size, tl_size>() = A_inv + A_inv * B * DCAB_inv * C * A_inv;
    result.topRightCorner<tl_size, br_size>() = -A_inv * B * DCAB_inv;
    result.bottomLeftCorner<br_size, tl_size>() = -DCAB_inv * C * A_inv;
    result.bottomRightCorner<br_size, br_size>() = DCAB_inv;

    return result;
}

template<typename Scalar, int tl_size, int br_size>
Matrix<Scalar, tl_size + br_size, tl_size + br_size> my_inverse(const Matrix<Scalar, tl_size + br_size, tl_size + br_size>& mat)
{
    const Matrix<Scalar, tl_size, tl_size>& A = mat.topLeftCorner<tl_size, tl_size>();
    const Matrix<Scalar, tl_size, br_size>& B = mat.topRightCorner<tl_size, br_size>();
    const Matrix<Scalar, br_size, tl_size>& C = mat.bottomLeftCorner<br_size, tl_size>();
    const Matrix<Scalar, br_size, br_size>& D = mat.bottomRightCorner<br_size, br_size>();

    return blockwise_inversion<Scalar,tl_size,br_size>(A, B, C, D);
}

template<typename Scalar>
Matrix<Scalar, 10, 10> invert_10_blockwise_8_2(const Matrix<Scalar, 10, 10>& input)
{
    Matrix<Scalar, 10, 10> result;

    const Matrix<Scalar, 8, 8>& A = input.topLeftCorner<8, 8>();
    const Matrix<Scalar, 8, 2>& B = input.topRightCorner<8, 2>();
    const Matrix<Scalar, 2, 8>& C = input.bottomLeftCorner<2, 8>();
    const Matrix<Scalar, 2, 2>& D = input.bottomRightCorner<2, 2>();

    Matrix<Scalar, 8, 8> A_inv = my_inverse<Scalar, 4, 4>(A);
    Matrix<Scalar, 2, 2> DCAB_inv = (D - C * A_inv * B).inverse();

    result.topLeftCorner<8, 8>() = A_inv + A_inv * B * DCAB_inv * C * A_inv;
    result.topRightCorner<8, 2>() = -A_inv * B * DCAB_inv;
    result.bottomLeftCorner<2, 8>() = -DCAB_inv * C * A_inv;
    result.bottomRightCorner<2, 2>() = DCAB_inv;

    return result;
}

template<typename Scalar>
Matrix<Scalar, 10, 10> invert_10_blockwise_6_4(const Matrix<Scalar, 10, 10>& input)
{
    Matrix<Scalar, 10, 10> result;

    const Matrix<Scalar, 6, 6>& A = input.topLeftCorner<6, 6>();
    const Matrix<Scalar, 6, 4>& B = input.topRightCorner<6, 4>();
    const Matrix<Scalar, 4, 6>& C = input.bottomLeftCorner<4, 6>();
    const Matrix<Scalar, 4, 4>& D = input.bottomRightCorner<4, 4>();

    Matrix<Scalar, 6, 6> A_inv = my_inverse<Scalar, 4, 2>(A);
    Matrix<Scalar, 4, 4> DCAB_inv = (D - C * A_inv * B).inverse().eval();

    result.topLeftCorner<6, 6>() = A_inv + A_inv * B * DCAB_inv * C * A_inv;
    result.topRightCorner<6, 4>() = -A_inv * B * DCAB_inv;
    result.bottomLeftCorner<4, 6>() = -DCAB_inv * C * A_inv;
    result.bottomRightCorner<4, 4>() = DCAB_inv;

    return result;
}

这是使用一百万个Eigen::Matrix<double,10,10>::Random()矩阵和Eigen::Matrix<double,10,1>::Random()向量进行一次基准测试的结果。在我所有的测试中,逆运算总是更快。我的求解例程涉及计算逆,然后将其乘以向量。有时它比本征快,有时则不然。我的基准打标方法可能有缺陷(未禁用涡轮增压等)。同样,本征的随机函数可能无法代表真实数据。

  • 本征局部枢轴逆:3036毫秒
  • 我对8x8上限的求逆:1638毫秒
  • 我对6x6上限的求逆:1234毫秒
  • 本征局部枢轴求解:1791毫秒
  • 我用8x8上限的解决方案:1739毫秒
  • 我用6x6上限的解决方案:1286毫秒

我非常感兴趣,看看是否有人可以进一步优化它,因为我有一个有限元应用程序,可以将成千上万个10x10矩阵求逆(是的,我确实需要逆的各个系数,因此直接求解线性系统并不总是一种选择) 。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.