白球比低是什么原因| 精神内科一般检查什么| 盆腔镜检查是查什么的| 植物神经紊乱中医叫什么病| 梦见打群架是什么意思| 不什么不| 6月7日是什么星座| 天上九头鸟地上湖北佬是什么意思| 1963年属什么生肖| 心眼多是什么意思| 宫颈hsil是什么意思| 便血是什么原因| 参乌健脑胶囊适合什么人吃| 男人跑马是什么原因| hw是什么牌子| 嬉皮士是什么意思| 2011年是什么生肖| 大黄蜂是什么车| 1.15是什么星座| 丽江机场叫什么名字| 999是什么意思| 阴疽是什么病| 色斑是什么原因引起的| 卡其色是什么颜色| 澳门是什么时候被葡萄牙占领的| 隐形眼镜没有护理液用什么代替| 秀才相当于现在的什么学历| 什么食物降血压| 雷声什么| 胰腺炎是什么症状| 儿童发育迟缓挂什么科| 酒后吐吃什么可以缓解| 是什么意思啊| 目眩是什么症状| 芝兰是什么意思| 状元郎是什么生肖| 家里为什么会进蝙蝠| 膀胱炎吃什么药最见效| 促黄体生成素是什么意思| 血小板偏低是什么意思| 排尿少是什么原因| 后顶焦度是什么意思| 转氨酶高是什么原因造成的| 一月30号是什么星座| birkin是什么意思| 书中自有颜如玉是什么意思| 什么的足迹| 肉桂茶适合什么人喝| 一根筋是什么意思| 15年婚姻是什么婚| 体检什么时候去最好| 谷氨酸钠是什么东西| 这个表情什么意思| 杰五行属性是什么| 为什么要睡觉| 向日葵什么时候成熟| 怀孕初期有什么表现| 宝宝消化不好吃什么调理| 夏天脸上皮肤痒是什么原因| 卡介苗是什么| 女人出轨有什么表现| 什么什么三什么成语| 蜱虫是什么样子的| 嘴角上火是什么原因| 冷暴力是什么意思| 尿酸高会引起什么疾病| 媚是什么意思| 颈椎吃什么药| 实性结节什么意思| 为什么下雨会打雷| 手腕血管疼是什么原因| 梦见自己怀孕生孩子是什么意思| 偶尔耳鸣是什么原因| 糖类抗原CA125高是什么意思| 便秘吃什么药效果好| 金鸡报晓是什么意思| 四库是指什么| 斗牛为什么用红色的布| 黑t恤搭配什么裤子| 孩子睡觉咬牙齿是什么原因引起的| 功夫2什么时候上映| 植物功能紊乱吃什么药| 月经不干净是什么原因| 血小板高是什么原因| 后羿射日什么意思| 人中浅的女人代表什么| 什么是高压氧| 马眼是什么意思| 逸搏心律什么意思| 猿人头是什么牌子| 身份证x代表什么| 天天射精对身体有什么危害| 什么是岩茶| 牙周炎是什么| 轩字属于五行属什么| 什么是基础代谢| 彩虹什么颜色| 免疫治疗是什么意思| 乳腺病是什么意思| 血脂异常是什么意思| 查幽门螺旋杆菌挂什么科| 干燥症是什么症状| 蛇为什么会咬人| 什么是spa| 五色土有什么风水作用| 军校是干什么的| 大姨妈吃什么| 感情洁癖什么意思| 馋肉是身体里缺什么| 大虾不能和什么一起吃| 壁虎吃什么食物| 熬夜对心脏有什么影响| 大疱性皮肤病是什么病| 小孩咳嗽吃什么药| 舅舅的老婆叫什么| 梦见自己掉河里了是什么意思| 男人喝劲酒有什么好处| 奇门遁甲什么意思| 婵字五行属什么| 乙肝三项检查什么| 精华液是什么| pocky是什么意思| 争辩的近义词是什么| 诠释的意思是什么| 脸部麻木是什么的前兆| 为什么眼睛会疼| 莘字五行属什么| 驾驶证c1和c2有什么区别| 土豆吃多了有什么坏处| 城市户口和农村户口有什么区别| 肾结石不能吃什么| 胆囊炎吃什么药好得快| 层峦叠翠的意思是什么| 微信上面有个耳朵是什么意思| 毒鸡汤是什么意思| 风水是什么意思| 大德是什么意思| 为什么叫川普| 妇科tct检查什么| zro是什么牌子| 卡马西平片是什么药| 阴茎插入阴道什么感觉| 70年出生属什么生肖| 黄体破裂是什么意思| wis是什么牌子| 小肚子疼吃什么药| 6.14是什么星座| e大饼是什么牌子| 书到用时方恨少什么意思| 射手座的幸运色是什么| 什么是肾阴虚| 来源朋友验证消息是什么意思| 舌苔黄腻厚是什么原因| 趁什么不什么| 高血脂不能吃什么| 吃什么水果降火最快| 月经期吃什么水果好| 什么家| 教唆什么意思| 剪不断理还乱是什么意思| 91年是什么命| 胎动频繁到什么程度说明缺氧| 血脂高会导致什么后果| 慢性阑尾炎吃什么药好| 5月26号是什么日子| 阴阳两虚吃什么中成药| 焦虑吃什么药| 阴茎硬度不够吃什么药| nap是什么意思| 属猴女和什么属相最配| 梦见手链断了是什么意思| 二便是什么意思| 萨瓦迪卡什么意思| 中元节是什么时候| 属鼠的和什么属相不合| 小孩口臭吃什么药效果最好| 黄金芽是什么茶| 腋下异味挂什么科| 什么的垂下| 天丝是什么面料| 心悸是什么原因引起的| 金晨为什么叫大喜| 贝壳吃什么食物| 亦字五行属什么| 重情重义是什么意思| 非油炸是什么意思| 桂圆不能和什么一起吃| 腰间盘突出挂什么科| 9月份是什么季节| 芦笋炒什么好吃| 农历11月14日是什么星座| 尿白细胞弱阳性什么意思| 什么的武松| 梦见背死人是什么意思| 色拉油是什么| 岁月如歌下一句是什么| 囊肿有什么症状| 属兔什么命| 第三者责任险是什么意思| 梦见自己小便是什么意思| foxer是什么牌子| 藏在什么里的爱| category是什么意思| 多吃火龙果有什么好处和坏处| 眉毛上的痣代表什么| 非那根又叫什么| 130是什么意思| 尿黄是什么病| conch是什么牌子| 12月23日什么星座| 体液是什么| 为什么打哈欠会流眼泪| 三峡大坝什么时候建成的| 旖旎风光是什么意思| 乙肝表面抗体定量偏高什么意思| 离岸是什么意思| 脖子粗挂什么科| 肠脂膜炎是什么病严重吗| 舟可是什么字| 四川人喜欢吃什么| 狗咬了不能吃什么| 人参归脾丸适合什么人吃| 肺部结节是什么意思| dna里面有什么| 弯弯的彩虹像什么| 你会不会突然的出现是什么歌| 挚肘是什么意思| 金陵十三钗是什么意思| 糖类抗原199是什么意思| 痛风什么东西不可以吃| 高血压挂号要挂什么科| 甲基是什么| 老放臭屁是什么原因| 骨古头坏死吃什么药| 三聚磷酸钠是什么东西| aosc是什么病| 脾胃不好吃什么水果| 骨折移位有什么感觉| 阿莫西林和头孢有什么区别| 榴莲和什么相克| 手指发白是什么原因| 冬天怕冷夏天怕热是什么体质| 正县级是什么级别| ca199检查是什么意思| 脂肪肝吃什么中药| 梦见打死黄鼠狼是什么意思| 梦见洗头是什么预兆| 急性腮腺炎吃什么药| 梦到自己长白头发是什么意思| 空腹喝可乐有什么危害| 晚上睡觉小腿抽筋是什么原因| 什么水果糖分低| 地狱不空誓不成佛是什么意思| 灰色是什么颜色调出来的| 怀孕6个月吃什么好| 草字头加全念什么| 腱鞘炎要挂什么科| 说话快的人什么性格| 荠菜长什么样子图片| 新疆在古代叫什么| 拜复乐是什么药| 什么是尿常规检查| 洁癖是什么意思| 什么地什么| 百度

Ctorch开发日志——矩阵乘法优化及数学原理

随着项目的推进,本作者遇到了目前最棘手的问题,即矩阵乘法的优化

但是有句话说得好

百度 乐曲以长音结尾,则蕴含微调的妙处。

“你越棘手,我越兴奋”

那么,如下是本作者如何把\(O(MNK)\)\(O(n^3)\))的朴素矩阵乘法一步一步优化到\(O(n^{2.81})\) 的全过程

测试环境

macOS Tahoe 26 Beta 2
M3 Pro 11核
18GB
CLion & Cmake
计时器:ctime
矩阵:1024 * 1024 @ 1024 * 1024
为保证准确,时间均为5次测量取平均值

朴素算法实现 & 测速

朴素实现的数学原理

其实就是把矩阵乘的数学公式重写一遍:

\[A={\left[ a_{ij}\right]_{m \times n}} \]

\[B={\left[ b_{ij}\right]_{n \times s}} \]

\[C= {A \times B}=\left[ c_{ij}\right]_{m \times s} \]

\[= \left[ \sum \limits_{k=1}^{n}a_{ik}b_{kj}\right]_{m \times s} \]

注意:矩阵乘法不满足交换律
只有左矩阵的列数与右矩阵的行数相同的两个矩阵才能相乘
乘积矩阵的行数等于左矩阵的行数,列数等于右矩阵的列数

概括一下就是

乘积矩阵第i行第j列处的元素等于左矩阵的第i行与右矩阵的第j列对应元素乘积之和

那么,这个很简单,上代码吧
为了测试,所有的矩阵保证满足乘法条件且为2维

时间复杂度 \(O(n^3)\)

// 原始版本(未优化)
void matrix_mult(float* A, float* B, float* C, int N) {
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++)
            for (int k = 0; k < N; k++)
                C[i*N + j] += A[i*N + k] * B[k*N + j];
}

在实际测试中,此算法跑出了1898.46ms优秀成绩

优化一:循环优化

你可能会疑惑,循环优化是什么
故名思义,就是对原有的ijk的循环重新更换顺序为ikj

一个更大的问题来了,凭什么仅仅改变顺序就快了许多

那么不妨看看访问顺序

算法1(朴素实现):

在这个顺序中,最内层循环是k,它遍历A的一行和B的一列。
对于A的访问是连续的(因为A[i][k]在内存中是按行存储的,所以k增加时是连续访问),
但是B的访问是不连续的(因为B[k][j]在内存中是按行存储,k增加时访问的是不同行的同一列,所以是跳跃访问)。这样对B的访问会导致缓存失效。

so,真正影响到速度的,就是缓存,在顺序读取中,缓存可以加载一整行,不必跳跃元素访问

那么,有没有一种循环顺序,使得对三个数组均为顺序访问呢
有的兄弟,有的,让我们欢迎仍为\(O(n^{3})\)的优化算法出场
——“ikj”循环优化

在这个顺序中,最内层循环是j。对于A的访问,固定i和k,所以每次内层循环A[i][k]是常数。对于B的访问,是B[k][j],由于j是连续的,所以B的访问是连续的(因为同一行连续列)。同时,C的访问也是连续的(C[i][j])。这样,所有的内存访问都是连续的,因此性能更好。

可以自行验证,对于三个数组,均为顺序访问
给出如下代码:
时间复杂度 \(O(n^3)\)

// 优化后(行优先访问)
void matrix_mult_opt1(float* A, float* B, float* C, int N) {
    for (int i = 0; i < N; i++)
        for (int k = 0; k < N; k++)  // k循环提到中间
            for (int j = 0; j < N; j++)
                C[i*N + j] += A[i*N + k] * B[k*N + j];
}

在实际测试中,此算法跑出了1462.89ms的优秀成绩
提升:1898.46ms-1462.89ms = 435.57ms 提高22.9%

进阶提升 优化二:矩阵分块算法

顾名思义,分块算法即是把矩阵分为多个小矩阵,对每个矩阵操作后再组合出结果,类似分块算法

那么,它为什么快呢

在计算机中,共有三种CPU缓存以及普通内存,即L1、L2、L3 Cache和内存
前三种的速度要比内存快很多很多,大概只有一个CPU周期的延迟,而普通内存可以达到上百周期延迟
而比他们更快的就是寄存器,直接接触CPU,0延迟
唯一的问题是,寄存器的大小只够存储单个值

新的问题来了,怎样把一个巨大的矩阵放到只有128k-1m的L1缓存中呢

聪明的你一定想到了把原矩阵划分为多个小矩阵,每一个都能放到L1内进行运算
那么恭喜你,你已经知道了矩阵分块算法的原理

更确切的说,先定义一个常数 \(blocks \in N^{+}\) 作为划分的单位矩阵的行列,
对于原矩阵A,我们把其中的 \({blocks \times blocks}\) 个元素划分为一个新矩阵,记为\(a^{'}_{\cdots}\),我们定义:

\[ a^{'}_{11} = \begin{pmatrix} a_{11} & \cdots & a_{1blocks} \\ \vdots & \ddots &\vdots \\ a_{blocks1} & \cdots & a_{blocks_{ }blocks} \end{pmatrix} \]

其余以此类推,新矩阵\(A^{'}\)即变为

\[A^{'}=\begin{pmatrix} a^{'}_{11} & \cdots & a^{'}_{1n} \\ \vdots & \ddots & \vdots \\ a^{'}_{m1} & \cdots & a^{'}_{mn} \end{pmatrix} \]

正如同我们可以把 \(f(x)\) 中的 \(x\) 替换为任意多项式(函数),我们同样也可以把矩阵中的每个元素换为一个矩阵,其运算规则仍然成立


so,显而易见的,分块矩阵算法有如下公式:

对于整体而言,

\[\begin{array}{c} C={A^{'}\times B^{'}}=\left[ c_{ij}\right]_{m \times s} = \left[ \sum \limits_{k=1}^{n}a^{'}_{ik}b^{'}_{kj}\right]_{m \times s} \end{array}\]

其中:

\[ a^{'}_{ij} = \begin{pmatrix} a_{[(i-1) \times blocks]{[(j-1)\times blocks]}} & \cdots & a_{[(i-1) \times blocks]{[j\times blocks]}} \\ \vdots & \ddots &\vdots \\ a_{[i\times blocks]{[(j-1)\times blocks]}} & \cdots & a_{[i\times blocks]{[j\times blocks]}} \end{pmatrix} \]

其中的每个乘法 \(a'{ik} \times b'{kj}\) 是子矩阵乘法
这里为了方便看,默认原矩阵的行列均为 blocks 的倍数
那么下一个很自然的问题就是

若行列不为blocks的倍数,怎么办

分两种情况:

  1. $ min(m,n) < blocks $
  2. $ \exists m,n \nmid blocks $

对于1,直接执行普通矩阵乘法即可,因为整个矩阵均可放于L1、L2 Cache中
对于2,我们定义分块矩阵的大小为$ p,q $

\[p = min(blocks,M- i \times blocks) \]

\[q = min(blocks,N - j \times blocks) \]

其中,

\[M,N为被分块矩阵的行列 \]

\[i,j为分块矩阵a^{'}_{ij}的下标 \]

至此,分块矩阵的全部问题已经解决

给出如下代码:
时间复杂度\(O(n^3)\)

void block_mult(float* A, float* B, float* C, int N, int BLOCK) {
    // 清除结果矩阵
    memset(C, 0, N*N*sizeof(float));
    // 三层分块循环
    for (int i0 = 0; i0 < N; i0 += BLOCK) {
        int i_end = min(i0 + BLOCK, N);  // 计算行边界
        for (int k0 = 0; k0 < N; k0 += BLOCK) {
            int k_end = min(k0 + BLOCK, N);  // 计算中间维度边界
            for (int j0 = 0; j0 < N; j0 += BLOCK) {
                int j_end = min(j0 + BLOCK, N);  // 计算列边界
                // 核心计算:只处理完整块内的元素
                for (int i = i0; i < i_end; i++) {
                    for (int k = k0; k < k_end; k++) {
                        float a_val = A[i*N + k];  // 一次加载A元素
                        // 内层循环:连续访问B和C
                        for (int j = j0; j < j_end; j++) {
                            C[i*N + j] += a_val * B[k*N + j];
                        }
                    }
                }
            }
        }
    }
}

由于矩阵过小时,分块算法优势不大,且会增加调用开销,因此,这里的测试,\(m,n\)为2048

实测结果:\(blocks = 512\) 时,用时 11515.2ms

而不使用分块仅循环优化的算法 用时 16028.9ms
朴素实现 用时 18053.45ms
提升:16028.9ms-11515.2ms = \(4513.2ms\) 提高:\(28.1\%\)

高手过招 优化三 :并行与SIMD

何为并行与SIMD
并行:多线程同时处理多个分块
SIMD:乘加一体,即一条CPU指令同时处理乘与加
我们这里使用Apple的AMX(Apple Matrix协处理器)(也属于CPU的一部分,并非GPU优化)
对于x86架构和其余ARM架构的处理器,可以使用AVX、AVX_512、SSE等SIMP指令集

它的特性有:

  • Apple Silicon芯片(M1/M2/M3等)内置的专用矩阵运算单元
  • 可并行处理大量16位浮点(FP16)或整数(INT8)运算

每个AMX单元包含:

  • 8个32KB的寄存器文件
  • 可同时执行2048次乘加运算(16x16x8矩阵)
  • 专用数据通路减少内存访问延迟

以及自动分块
如下是使用AMX的SIMP的代码:
时间复杂度:\(O(n^3)\)

// 使用Apple的AMX加速BLAS库
            cblas_sgemm(CblasRowMajor,   // 行主序存储
                        CblasNoTrans,   // 不转置A
                        CblasNoTrans,   // 不转置B
                        M,              // A的行数
                        N,              // B的列数
                        K,              // 公共维度
                        1.0f,           // alpha系数
                        a_data,         // A数据指针
                        K,              // A的列步幅(lda)
                        b_data,         // B数据指针
                        N,              // B的列步幅(ldb)
                        0.0f,           // beta系数
                        r_data,         // 结果数据指针
                        N);             // 结果的列步幅(ldc)

那么,本次优化最吓人、最恐怖的一次数据来了:
实测数据:202.831ms(4096*4096)
而标准分块+循环优化算法,在2048*2048时,就已经11515.2ms
提升:11312.37 ms 提高:98.2%

数学手段 优化4:Strassen算法 & 变种

温馨提示:到这里已经是高等数学内容了(实不相瞒,前面其实也是),有点小烧脑,不过欢迎各位继续跟作者一起尝试,本作者大约花了3小时搞完这一部分的证明

介绍:

Strassen算法是一种通过数学变换减少乘法次数的高效矩阵乘/卷积算法

简要推导:

我们设有如下两个矩阵相乘:

\[\begin{pmatrix} a_{11} & a_{12} \\ a_{21} & a_{22} \end{pmatrix} \times \begin{pmatrix} b_{11} & b_{12} \\ b_{21} & b_{22} \end{pmatrix} = \begin{pmatrix} c_{11} & c_{12} \\ c_{21} &c_{22} \end{pmatrix} \]

传统计算需要8次乘法:

\[c_{11} = a_{11}\times b_{11} + a_{12}\times b_{21} \\ \]

\[c_{12} = a_{11}\times b_{12} + a_{12}\times b_{22} \\ \]

\[c_{21} = a_{21}\times b_{11} + a_{22}\times b_{21} \\ \]

\[c_{22} = a_{21}\times b_{12} + a_{22}\times b_{22} \\ \]

而Strassen算法只需7次乘
接下来,是Strassen算法最精妙绝伦的一步:
作者定义了7个矩阵:

\[\begin{align*} M_1 &= (a_{11} + a_{22})(b_{11} + b_{22}) \\ M_2 &= (a_{21} + a_{22})b_{11} \\ M_3 &= a_{11}(b_{12} - b_{22}) \\ M_4 &= a_{22}(b_{21} - b_{11}) \\ M_5 &= (a_{11} + a_{12})b_{22} \\ M_6 &= (a_{21} - a_{11})(b_{11} + b_{12}) \\ M_7 &= (a_{12} - a_{22})(b_{21} + b_{22}) \end{align*} \]

真正让人惊讶的是下一步:
作者构建的7个矩阵,可以通过有限次的组合成为结果矩阵的一个元素
什么意思呢,让我们尝试展开其中一项:

\[c_{11} = M_1 + M_4 - M_5 + M_7 \\ \]

\[\begin{align*} &=(a_{11} + a_{22})(b_{11} + b_{22}) + a_{22}(b_{21} - b_{11}) - (a_{11} + a_{12})b_{22} + (a_{12} - a_{22})(b_{21} + b_{22}) \\ \end{align*}\]

\[\begin{align*} = & \ \ a_{11}b_{11} + \cancel{a_{11}b_{22}} + \cancel{a_{22}b_{11}} + \cancel{a_{22}b_{22}} \\ &+ \ \cancel{a_{22}b_{21}} - \cancel{a_{22}b_{11}} \\ &- \ \cancel{a_{11}b_{22}} - a_{12}b_{22} \\ &+ \ a_{12}b_{21} + \cancel{a_{12}b_{22}} - \cancel{a_{22}b_{21}} - \cancel{a_{22}b_{22}} \\ = & \ \ a_{11}b_{11} + a_{12}b_{21} \end{align*}\]

由此,可以类似的推出\(c_{12}、c_{21}、c_{22}\)均与正常计算一致
最后的结果矩阵为

\[C= \begin{pmatrix} M_{1}+M_{4}-M_{5}+M_{7} & M_{3}+M_{5} \\ M_{2}+M_{4} & M_{1}-M_{2}+M_{3}+M_{6} \end{pmatrix} \]

接下来,我们证明其对于n>2时仍然成立:
由于( n=2 )时,我们已经证明其正确
所以,在n>2时,我们采取分块
将原矩阵分为4块,此时我们将其中的子矩阵看为一个元素
那么此时又回归到了标准的2x2的Strassen算法
由此在$n,m \mid 2 $时Strassen算法正确
那么,下一个很自然的问题就是

\(n,m \nmid {2} ,结论是否成立\)

我们的做法是,将矩阵分块,分为几个\(2^k \times 2^k\)的子矩阵以及几个符合矩阵乘规则的小矩阵
显然由于前文的分块算法的正确性,此时的分块仍然正确,对于几个\(2^k \times 2^k\)的矩阵,我们使用Strassen算法进行计算
现在来计算一下Strassen算法的时间复杂度:
设$ T(n) $ 为计算 $ n \times n $ 矩阵乘法的时间:
\(T(n) = 7T\left(\frac{n}{2}\right) + O(n^2)\)

  • $ 7T(n/2) $:7 个子问题递归计算
  • $O(n^2) $:矩阵加减法开销(共 18 次加减法)

根据主定理1,
$ a = 7,b = 2,f(n)=\Theta(n^2) $
\(log _b a = log_27 \approx 2.807\)
由于\(log _b a > 2\),所以\(f(n) = O({n^{log_b a-\epsilon})} = O(n^{log_27}) \approx O(n^{2.807})\)
更精确的,复杂度为\(\Theta(n^{2.807})\)

具体的算法为:
1.先将矩阵AB分块,分成大小为 \({blocks \times blocks}\) 的若干块以及几个任意大小的子块
2.对于 \({blocks \times blocks}\) 的子块,我们使用Strassen算法计算
3.在递归过程中,若方阵大小(因为Strassen算法开始时为方阵)n = 128,则使用循环优化的矩阵乘法
4.否则,继续按照Strassen算法递归计算直至n = 128
5.对于不是 \({blocks \times blocks}\) 的子块,使用循环优化的矩阵乘法计算
给出如下代码:
近似时间复杂度\(O(n^{2.81})\)

const int BLOCK_SIZE = 2048;
const int STRASSEN_THRESHOLD = 128;

// 标准矩阵乘法 (用于小矩阵和边界处理)
void standard_matmul(const float* A, const float* B, float* C, int n, int m, int p, int lda, int ldb, int ldc) {
    for (int i = 0; i < n; ++i) {
        for (int k = 0; k < m; ++k) {
            float a = A[i * lda + k];
            for (int j = 0; j < p; ++j) {
                C[i * ldc + j] += a * B[k * ldb + j];
            }
        }
    }
}

// 循环优化矩阵乘法 (n=128时使用)
void optimized_matmul(const float* A, const float* B, float* C, int n, int lda, int ldb, int ldc) {
    for (int i = 0; i < n; ++i) {
        for (int k = 0; k < n; ++k) {
            float a = A[i * lda + k];
            for (int j = 0; j < n; ++j) {
                C[i * ldc + j] += a * B[k * ldb + j];
            }
        }
    }
}

// 矩阵加法
void matrix_add(const float* A, const float* B, float* C, int n, int lda, int ldb, int ldc) {
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            C[i * ldc + j] = A[i * lda + j] + B[i * ldb + j];
        }
    }
}

// 矩阵减法
void matrix_subtract(const float* A, const float* B, float* C, int n, int lda, int ldb, int ldc) {
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            C[i * ldc + j] = A[i * lda + j] - B[i * ldb + j];
        }
    }
}

// 结果累加到目标矩阵
void matrix_add_to_target(float* T, const float* S, int n, int ldt, int lds) {
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            T[i * ldt + j] += S[i * lds + j];
        }
    }
}

// Strassen 矩阵乘法 (递归实现)
void strassen_matmul(const float* A, const float* B, float* C, int n, int lda, int ldb, int ldc) {
    // 递归基: n <= 128 使用优化乘法
    if (n <= STRASSEN_THRESHOLD) {
        optimized_matmul(A, B, C, n, lda, ldb, ldc);
        return;
    }

    int half = n / 2;
    // 定义子矩阵指针
    const float* A11 = A;
    const float* A12 = A + half;
    const float* A21 = A + half * lda;
    const float* A22 = A + half * lda + half;
    
    const float* B11 = B;
    const float* B12 = B + half;
    const float* B21 = B + half * ldb;
    const float* B22 = B + half * ldb + half;
    
    float* C11 = C;
    float* C12 = C + half;
    float* C21 = C + half * ldc;
    float* C22 = C + half * ldc + half;

    // 分配临时矩阵
    std::vector<float> S1(half * half);
    std::vector<float> S2(half * half);
    std::vector<float> S3(half * half);
    std::vector<float> S4(half * half);
    std::vector<float> S5(half * half);
    std::vector<float> S6(half * half);
    std::vector<float> S7(half * half);
    std::vector<float> S8(half * half);
    std::vector<float> S9(half * half);
    std::vector<float> S10(half * half);
    
    std::vector<float> P1(half * half);
    std::vector<float> P2(half * half);
    std::vector<float> P3(half * half);
    std::vector<float> P4(half * half);
    std::vector<float> P5(half * half);
    std::vector<float> P6(half * half);
    std::vector<float> P7(half * half);

    // 计算S矩阵
    matrix_subtract(B12, B22, S1.data(), half, ldb, ldb, half);    // S1 = B12 - B22
    matrix_add(A11, A12, S2.data(), half, lda, lda, half);         // S2 = A11 + A12
    matrix_add(A21, A22, S3.data(), half, lda, lda, half);         // S3 = A21 + A22
    matrix_subtract(B21, B11, S4.data(), half, ldb, ldb, half);    // S4 = B21 - B11
    matrix_add(A11, A22, S5.data(), half, lda, lda, half);         // S5 = A11 + A22
    matrix_add(B11, B22, S6.data(), half, ldb, ldb, half);         // S6 = B11 + B22
    matrix_subtract(A12, A22, S7.data(), half, lda, lda, half);    // S7 = A12 - A22
    matrix_add(B21, B22, S8.data(), half, ldb, ldb, half);         // S8 = B21 + B22
    matrix_subtract(A11, A21, S9.data(), half, lda, lda, half);    // S9 = A11 - A21
    matrix_add(B11, B12, S10.data(), half, ldb, ldb, half);        // S10 = B11 + B12

    // 递归计算P矩阵
    strassen_matmul(A11, S1.data(), P1.data(), half, lda, half, half);      // P1 = A11 * S1
    strassen_matmul(S2.data(), B22, P2.data(), half, half, ldb, half);      // P2 = S2 * B22
    strassen_matmul(S3.data(), B11, P3.data(), half, half, ldb, half);      // P3 = S3 * B11
    strassen_matmul(A22, S4.data(), P4.data(), half, lda, half, half);      // P4 = A22 * S4
    strassen_matmul(S5.data(), S6.data(), P5.data(), half, half, half, half); // P5 = S5 * S6
    strassen_matmul(S7.data(), S8.data(), P6.data(), half, half, half, half); // P6 = S7 * S8
    strassen_matmul(S9.data(), S10.data(), P7.data(), half, half, half, half);// P7 = S9 * S10

    // 组合结果矩阵 (累加到C)
    // C11 = P5 + P4 - P2 + P6
    matrix_add_to_target(C11, P5.data(), half, ldc, half);
    matrix_add_to_target(C11, P4.data(), half, ldc, half);
    matrix_add_to_target(C11, P6.data(), half, ldc, half);
    for (int i = 0; i < half; ++i) {
        for (int j = 0; j < half; ++j) {
            C11[i * ldc + j] -= P2[i * half + j];
        }
    }
    
    // C12 = P1 + P2
    matrix_add_to_target(C12, P1.data(), half, ldc, half);
    matrix_add_to_target(C12, P2.data(), half, ldc, half);
    
    // C21 = P3 + P4
    matrix_add_to_target(C21, P3.data(), half, ldc, half);
    matrix_add_to_target(C21, P4.data(), half, ldc, half);
    
    // C22 = P5 + P1 - P3 - P7
    matrix_add_to_target(C22, P5.data(), half, ldc, half);
    matrix_add_to_target(C22, P1.data(), half, ldc, half);
    for (int i = 0; i < half; ++i) {
        for (int j = 0; j < half; ++j) {
            C22[i * ldc + j] -= (P3[i * half + j] + P7[i * half + j]);
        }
    }
}

// 分块矩阵乘法
void matrix_multiply(const float* A, const float* B, float* C, int n, int m, int p, int lda, int ldb, int ldc) {
    // 初始化输出矩阵为0
    std::memset(C, 0, n * ldc * sizeof(float));
    
    // 分块处理
    for (int i = 0; i < n; i += BLOCK_SIZE) {
        int i_end = std::min(i + BLOCK_SIZE, n);
        int i_size = i_end - i;
        
        for (int k = 0; k < m; k += BLOCK_SIZE) {
            int k_end = std::min(k + BLOCK_SIZE, m);
            int k_size = k_end - k;
            
            for (int j = 0; j < p; j += BLOCK_SIZE) {
                int j_end = std::min(j + BLOCK_SIZE, p);
                int j_size = j_end - j;
                
                // 当前块指针
                const float* A_block = A + i * lda + k;
                const float* B_block = B + k * ldb + j;
                float* C_block = C + i * ldc + j;
                
                // 完整块使用Strassen算法
                if (i_size == BLOCK_SIZE && k_size == BLOCK_SIZE && j_size == BLOCK_SIZE) {
                    strassen_matmul(A_block, B_block, C_block, BLOCK_SIZE, lda, ldb, ldc);
                } 
                // 非完整块使用标准乘法
                else {
                    standard_matmul(A_block, B_block, C_block, i_size, k_size, j_size, lda, ldb, ldc);
                }
            }
        }
    }
}
}

当然,实际测试中,我们使用Ctorch框架的Tensor类Op::Add,与此代码会略有差异
最终测试结果:1005.65ms
P.S.提升不明显的原因是矩阵过小,如果使用Tranformer架构的巨型矩阵测试,\(O(n^{2.81})\)的优势会非常明显

最终的方案:

我们使用多函数策略:
1.若dim<128 此时的拷贝开销已经大于AMX的优化,因此使用循环优化
2.128<dim<4096 AMX的最优区间,使用纯AMX
3.dim>4096 分块,对于能够分为\(2^k\)的块,使用Strassen算法,递归到2048使用AMX
对于不是\(2^k\)的块,直接使用AMX计算,同时,每个分块使用单一线程

最后的结果:

测试矩阵:16384163842(\(2^{14}\)
标准:30251ms
循环优化:24580ms
分块:20498ms
最终方案(SIMP+多线程+分块+Strassen):8267.97ms

最后

如果你希望既追求高性能又追求简洁的框架,那么Ctorch将是你的最优选择
尽管这个项目还在开发中,但是可以先小小的期待一下
欢迎贡献,如有错误,请各位不吝赐教,谢谢
2025.8.2

posted @ 2025-08-04 21:48  Ghost-Face  阅读(63)  评论(0)    收藏  举报
梦见骆驼是什么意思 恐龙的祖先是什么 吃什么能美白 什么叫邪淫 乙状结肠腺瘤是什么病
请问支气管炎吃什么药最有效 丁二醇是什么 蒋字五行属什么 多心是什么意思 同比增长是什么意思
认知障碍是什么意思 马铃薯什么时候传入中国 农历六月十四是什么日子 bossini是什么牌子 静脉曲张什么症状
8月14是什么星座 名落孙山是什么意思 眼睛有眼屎是什么原因 健康证需要检查什么项目 棍子鱼又叫什么鱼
buy是什么意思hcv7jop4ns5r.cn 君子兰的寓意是什么hcv9jop6ns2r.cn 缺维生素b吃什么食物hcv8jop6ns0r.cn 戊肝抗体igg阳性是什么意思hcv8jop7ns7r.cn 比目鱼又叫什么鱼hcv8jop7ns6r.cn
手腕疼挂什么科hcv8jop7ns3r.cn 李维斯属于什么档次hcv8jop7ns6r.cn 什么是腱鞘炎hcv8jop8ns3r.cn 舌头根发麻是什么原因hcv8jop7ns7r.cn 蜂胶有什么作用和功效hcv8jop9ns4r.cn
另起炉灶是什么意思hcv9jop3ns7r.cn 体温偏低是什么原因hcv7jop7ns2r.cn 减肥期间适合吃什么hcv8jop2ns3r.cn 儿童去火吃什么药hcv8jop7ns6r.cn 梦到孩子被蛇咬是什么意思hcv8jop4ns7r.cn
燕麦片热量高为什么还能减肥zsyouku.com 坐小月子可以吃什么水果hcv9jop0ns2r.cn 骆驼是什么牌子hcv7jop6ns9r.cn 拿什么东西不用手hcv9jop0ns9r.cn 水的ph值是什么意思hcv8jop3ns9r.cn
百度