Andrew Moa Blog Site

矩阵乘法运算(二)-基于BLAS库的加速运算

BLAS最初是采用fortran开发的线性代数库,后来移植到C/C++上,作为现代高性能计算的核心组件,已经形成了一套标准。有开源的实现如Netlib BLAS、GotoBLAS及其后继者OpenBLAS,商业上各个厂商针对自家平台都有相应的实现,比如Intel的MKL、NVIDIA的CUDA、AMD的AOCL和ROCm。其中有针对CPU平台进行优化的,也有采用GPU并行加速的。本文通过使用不同BLAS库实现矩阵运算,分析不同实现间的性能差异。

1. CPU并行加速BLAS库

1.1 Intel MKL

main.c文件同矩阵乘法运算(一)-使用OpenMP加速循环计算 中的2.1,blas.c引入mkl的blas库,并使用gemm函数执行矩阵乘法运算。

#include <mkl_cblas.h>

void matrix_multiply_float(int n, float A[], float B[], float C[])
{
    cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
                n, n, n, 1.0, A, n, B, n, 0.0, C, n);
}

void matrix_multiply_double(int n, double A[], double B[], double C[])
{
    cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
                n, n, n, 1.0, A, n, B, n, 0.0, C, n);
}

CMakeLists.txt包含了mkl和openmp库文件,mkl底层默认使用openmp进行并行化,因此要链接到openmp库。

cmake_minimum_required(VERSION 3.13)
project(mkl LANGUAGES C)
set(CMAKE_C_STANDARD 11)

set(EXECUTE_FILE_NAME ${PROJECT_NAME}_${CMAKE_C_COMPILER_FRONTEND_VARIANT}_${CMAKE_C_COMPILER_ID}_${CMAKE_C_COMPILER_VERSION})
string(TOLOWER ${EXECUTE_FILE_NAME} EXECUTE_FILE_NAME)

set(MKL_LINK static)

# Enable OpenMP
find_package(OpenMP REQUIRED)

# Enable MKL
find_package(MKL CONFIG REQUIRED)

set(SRC_LIST
    src/main.c
    src/blas.c
)

add_executable(${EXECUTE_FILE_NAME} ${SRC_LIST})

target_compile_options(${EXECUTE_FILE_NAME} PUBLIC
    $<TARGET_PROPERTY:MKL::MKL,INTERFACE_COMPILE_OPTIONS>
)
target_include_directories(${EXECUTE_FILE_NAME} PUBLIC
    $<TARGET_PROPERTY:MKL::MKL,INTERFACE_INCLUDE_DIRECTORIES>
)
target_link_libraries(${EXECUTE_FILE_NAME} PUBLIC
    OpenMP::OpenMP_C
    $<LINK_ONLY:MKL::MKL>
)

编译机器是AMD笔记本,处理器是AI 9 365w,在Windows下使用vs2022的clang-cl编译并运行,Release程序执行效果如下:

PS D:\example\efficiency_v3\c\mkl\build\Release> ."D:/example/efficiency_v3/c/mkl/build/Release/mkl_msvc_clang_19.1.5.exe" -l 10 -n 12
Using float precision for matrix multiplication.
1       : 4096 x 4096 Matrix multiply wall time : 0.218935s(627.761Gflops)
2       : 4096 x 4096 Matrix multiply wall time : 0.211711s(649.183Gflops)
3       : 4096 x 4096 Matrix multiply wall time : 0.215178s(638.722Gflops)
4       : 4096 x 4096 Matrix multiply wall time : 0.223452s(615.072Gflops)
5       : 4096 x 4096 Matrix multiply wall time : 0.202687s(678.085Gflops)
6       : 4096 x 4096 Matrix multiply wall time : 0.203175s(676.455Gflops)
7       : 4096 x 4096 Matrix multiply wall time : 0.225790s(608.702Gflops)
8       : 4096 x 4096 Matrix multiply wall time : 0.204435s(672.287Gflops)
9       : 4096 x 4096 Matrix multiply wall time : 0.217666s(631.421Gflops)
10      : 4096 x 4096 Matrix multiply wall time : 0.217374s(632.270Gflops)
Average Gflops: 642.996, Max Gflops: 678.085
Average Time: 0.214040s, Min Time: 0.202687s
PS D:\example\efficiency_v3\c\mkl\build\Release> ."D:/example/efficiency_v3/c/mkl/build/Release/mkl_msvc_clang_19.1.5.exe" -l 10 -n 12 -double
Using double precision for matrix multiplication.
1       : 4096 x 4096 Matrix multiply wall time : 0.400238s(343.393Gflops)
2       : 4096 x 4096 Matrix multiply wall time : 0.365257s(376.280Gflops)
3       : 4096 x 4096 Matrix multiply wall time : 0.375613s(365.906Gflops)
4       : 4096 x 4096 Matrix multiply wall time : 0.353108s(389.226Gflops)
5       : 4096 x 4096 Matrix multiply wall time : 0.380444s(361.260Gflops)
6       : 4096 x 4096 Matrix multiply wall time : 0.381736s(360.036Gflops)
7       : 4096 x 4096 Matrix multiply wall time : 0.392378s(350.272Gflops)
8       : 4096 x 4096 Matrix multiply wall time : 0.382949s(358.897Gflops)
9       : 4096 x 4096 Matrix multiply wall time : 0.401440s(342.365Gflops)
10      : 4096 x 4096 Matrix multiply wall time : 0.413794s(332.143Gflops)
Average Gflops: 357.978, Max Gflops: 389.226
Average Time: 0.384696s, Min Time: 0.353108s

想要在AMD机器上正常运行Intel mkl程序,建议在环境变量中加入MKL_DEBUG_CPU_TYPE=5

阅读时长17分钟
Andrew Moa

矩阵乘法运算(一)-使用OpenMP加速循环计算

说到矩阵,只要是理工科都会想到被线性代数课支配的恐惧。矩阵乘法运算,往大了说各种工业和科研数值计算离不开它,往小了说各种跑分软件也会用到它,矩阵乘法运算的耗时也是评判计算机浮点运算性能的重要指标。本文的目的是通过矩阵乘法运算验证各种实现方式的性能差异,并对比不同计算平台的性能差异,为高性能计算开发提供参考。

1. 矩阵乘法算法

矩阵乘法运算也成为矩阵点乘,可以用下图表示1,通常由A矩阵对应行和B矩阵对应列的元素相乘并累加,形成C矩阵对应行列的值。要求A矩阵的列数要与B矩阵的行数相等,C矩阵的尺寸则相当于B矩阵的行数×A矩阵的列数。

06d7ac8e02b5dab1769e5284772bdf9e.png

用C语言表示一个M×N矩阵点乘一个N×T矩阵,一般写成3层循环嵌套的形式。

...
	for (int i = 0; i < M; i++)
	{
		for (int j = 0; j < T; j++)
		{
			C[i,j] = 0.0;
			for (int k = 0; k < N; k++)
			{
				C[i,j] += A[i,k] * B[k,j];
			}
		}
	}
...

通常认为一个N×N维矩阵的乘法次数为N的3次方,用O(n3)表示计算复杂度。也有一些算法,将其中某些乘法运算转化成加法,减少了乘法运算次数。比如Strassen算法2,理论上计算复杂度只有O(n2.807),降低了大矩阵相乘的耗时。更新的算法比如Coppersmith-Winograd方法,计算复杂度更是号称只有O(n2.3727)。这种算法上的差异不在本文讨论之列。

2. 循环嵌套并行化

现代计算机处理器一般都为多核心,为充分利用处理器性能,使用openmp等并行库对计算程序进行并行优化不失为一个好的选项。使用单核心进行循环嵌套求解没有实际意义,这里不做演示,有兴趣的可以在下面的代码中单独去掉并行库后进行编译计算。

2.1 C实现

C实现很简单,通过三层循环嵌套实现矩阵相乘算法。这里通过openmp将前两层循环合并分配到不同线程中展开计算3,以充分利用多核心处理器的性能。以下是openmp.c的示例代码。

#include <omp.h>

void matrix_multiply_float(int n, float A[], float B[], float C[])
{
#pragma omp parallel for collapse(2) shared(A, B, C)
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; j++)
		{
			C[i * n + j] = 0;
			for (int k = 0; k < n; k++)
			{
				C[i * n + j] += A[i * n + k] * B[k * n + j];
			}
		}
	}
}

void matrix_multiply_double(int n, double A[], double B[], double C[])
{
#pragma omp parallel for collapse(2) shared(A, B, C)
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; j++)
		{
			C[i * n + j] = 0;
			for (int k = 0; k < n; k++)
			{
				C[i * n + j] += A[i * n + k] * B[k * n + j];
			}
		}
	}
}

main.c中定义一些命令行开关,用户可以自定义矩阵尺寸、循环次数、计算精度等。这里定义了一个性能参数,GFLOPs,用来衡量单位时间内执行浮点运算的能力,1GFLOPs相当于1秒内执行109次浮点运算。

阅读时长34分钟
Andrew Moa

Rust图形界面库初探

最近打算用rust把以前写的代码重构一下,涉及到GUI界面怎么选择的问题。rust正式发布不过十年光景,在GUI开发这方面还不如老牌的C/C++,有诸如wxWidgets、qt、gtk+等众多知名又久经考验的GUI界面库。本文选取几款rust的GUI库,简单实现一个边界层计算器,做个横向对比。

1. slint

slint 近期的宣传不可谓不卖力,号称要打造成下一代gui工具包,看来野心不小。slint通过自定义的声明式语言定义ui界面,在vscode下编程可以通过插件预览,也可以通过官方的slintpad 网站预览。

cargo配置文件如下,slint-build用于将.slint界面文件翻译成.rs文件。

[package]
name = "sLayers-rs"
version = "0.1.0"
edition = "2024"

[dependencies]
slint = "1.8.0"

[build-dependencies]
slint-build = "1.8.0"

[profile.release]
strip = true
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"

ui界面文件dialog.slint。slint可以把需要调用的参数定义到ui界面中,并自动隐式生成set_get_方法用于在rs文件中设置这些参数的值。同样的,界面中定义的回调函数也会自动隐式生成on_方法,用于在rs文件中调用。需要注意的是,在rs中更新了ui界面中的参数,关联的控件并不会自动更新显示,需要手动更新控件显示。

import { Button, LineEdit, SpinBox, CheckBox, GridBox, VerticalBox } from "std-widgets.slint";

export component Dialog inherits Window { title: "Layers"; in-out property <string> tt: "3.0"; in-out property <int> nl: 5; in-out property <string> ft: "0.3"; in-out property <string> gr: "1.5"; callback calculate_first_thickness(); callback calculate_num_layers(); callback calculate_growth_rate(); callback calculate_total_thickness();

callback calculate_value();
VerticalBox {
    Text {
        text: &#34;Calculate fluid boundary layer parameters.\nCalculate the selected parameters based on the others.&#34;;
    }

    GridBox {
        Row {
            b_t := CheckBox {
                text: &#34;Total thickness (mm)&#34;;
                checked: true;
                enabled: !self.checked;
                toggled() =&gt; {
                    if self.checked {
                        b_n.checked = false;
                        b_f.checked = false;
                        b_g.checked = false;
                    }
                }
            }

            e_t := LineEdit {
                text: root.tt;
                input-type: decimal;
                read-only: b_t.checked;
            }
        }

        Row {
            b_n := CheckBox {
                text: &#34;Number of layers&#34;;
                checked: false;
                enabled: !self.checked;
                toggled() =&gt; {
                    if self.checked {
                        b_t.checked = false;
                        b_f.checked = false;
                        b_g.checked = false;
                    }
                }
            }

            e_n := SpinBox {
                value: root.nl;
                minimum: 1;
            }
        }

        Row {
            b_f := CheckBox {
                text: &#34;First thickness (mm)&#34;;
                checked: false;
                enabled: !self.checked;
                toggled() =&gt; {
                    if self.checked {
                        b_n.checked = false;
                        b_t.checked = false;
                        b_g.checked = false;
                    }
                }
            }

            e_f := LineEdit {
                text: root.ft;
                input-type: decimal;
                read-only: b_f.checked;
            }
        }

        Row {
            b_g := CheckBox {
                text: &#34;Growth rate&#34;;
                checked: false;
                enabled: !self.checked;
                toggled() =&gt; {
                    if self.checked {
                        b_n.checked = false;
                        b_f.checked = false;
                        b_t.checked = false;
                    }
                }
            }

            e_g := LineEdit {
                text: root.gr;
                input-type: decimal;
                read-only: b_g.checked;
            }
        }
    }

    Button {
        text: &#34;Calculate&#34;;
        clicked =&gt; {
            root.tt = e_t.text;
            root.nl = e_n.value;
            root.ft = e_f.text;
            root.gr = e_g.text;
            if b_t.checked {
                root.calculate_total_thickness();
            } else if b_n.checked {
                root.calculate_num_layers();
            } else if b_f.checked {
                root.calculate_first_thickness();
            } else if b_g.checked {
                root.calculate_growth_rate();
            }
            e_t.text = root.tt;
            e_g.text = root.gr;
            e_f.text = root.ft;
            e_n.value = root.nl;
        }
    }
}

}

build.rs构建脚本调用slint-build.slint文件翻译成.rs文件。

阅读时长9分钟
Andrew Moa