ITADN

GEMM performance on M2

#29Closed0xekez 创建于 2025-09-03
0
0xekezcommented
I modified the test code to [print latency](https://github.com/philipturner/metal-flash-attention/blob/main/Tests/FlashAttentionTests/GEMM/LaplacianTest.swift#L232) and for multiplying two 4096x4096 matrices the latency is about a second on my M2. For that size, MPSMatrixMultiplication takes about 0.06 seconds on my machine. @liuliu, @philipturner mentioned over email you might be a good person to talk to about this. In my experiments, I've written a double-buffered tiled matrix multiply for the M2 which takes 0.1 seconds (below), but even then it is limited by how quickly it can feed data to the ALU, getting ~80% utilization. I tried both the morton order loading and the simd_async_copy instructions in this repo, but both slowed down loading. @philipturner mentioned this may be because they have disabled the simd_async_copy instructions, and my benchmarks are also suggestive of this, as using simd_async_copy causes a lot of integer math to be done by the ALU, suggesting it may be being emulated. The mystery remains though how MPS achieves such good performance, so I thought I'd make this issue to try and figure it out. <details> <summary>The fastest kernel I've been able to come up with</summary> ```c++ // assumes n,m,k are divisible by 8. // assumes thread width is 32. // Let (SW*8)x(SW*8) be the dimensions of a threadgroup tile in C. // SW*SW is the number of 8x8 simdgroup tiles in a threadgroup tile. // Let TW be the number of threads in a simdgroup and TMAX the max // threads in a threadgroup. Pick SW s.t. TW*SW*SW <= T_MAX. // // Launch with threadgroup shape (TW,SW,SW) and grid shape // (TW,SW*ceil(ceil(m/8)/SW),SW*ceil(ceil(n/8)/SW). // Grid shape is (cols,rows) as simdgroups use (col,row) indicies. // // Grid shape needs to be a multiple of SW so we always have a full // threadgroup. To see this, consider the case where a full // threadgroup would hang off the edge of C to the right. In this // case, we want to load tiles of A as normal, but don't nesecarially // need to load all the columns of B. The requirement that the A tile // is loaded as normal means we need an entire threadgroup working. #include <metal_simdgroup_matrix> #include <metal_compute> using namespace metal; inline void load_tile( const device float* src, const ushort2 src_tg_pos, const ushort2 src_shape, // (cols, rows) threadgroup float* dst, const ushort start ) { for (ushort i = 0; i < 2; i++) { ushort idx = start+i; ushort2 pos = src_tg_pos + ushort2(idx%(SW*8), idx/(SW*8)); dst[idx] = select(0., src[pos.x+pos.y*src_shape.x], all(pos<src_shape)); } } template<ushort DIM> inline void simdgroup_multiply( threadgroup float* A, threadgroup float* B, ushort2 c_pos, thread simdgroup_float8x8 &acc ) { simdgroup_float8x8 A_simd[DIM]; simdgroup_float8x8 B_simd[DIM]; for (ushort i = 0; i < DIM; ++i) simdgroup_load(A_simd[i], A, DIM*8, ulong2(i*8, c_pos.y*8)); for (ushort i = 0; i < DIM; ++i) simdgroup_load(B_simd[i], B, DIM*8, ulong2(c_pos.x*8, i*8)); for (ushort i = 0; i < DIM; ++i) simdgroup_multiply_accumulate(acc, A_simd[i], B_simd[i], acc); } kernel void matmul( constant ushort& n, constant ushort& k, constant ushort& m, constant float& alpha, constant float& beta, const device float* A, const device float* B, device float* C, ushort3 t_pos [[thread_position_in_grid]], ushort3 t_tg_pos [[thread_position_in_threadgroup]], ushort3 tg_pos [[threadgroup_position_in_grid]] ) { ulong2 c_origin = ulong2(t_pos.yz*8); ushort2 c_tg_origin = tg_pos.yz*SW*8; ushort2 a_tg_origin = ushort2(0,c_tg_origin.y); ushort2 b_tg_origin = ushort2(c_tg_origin.x,0); alignas(16) threadgroup float A_tg[2][SW*8*SW*8]; alignas(16) threadgroup float B_tg[2][SW*8*SW*8]; simdgroup_float8x8 acc(0.); ushort buffer = 0; ushort start = (t_tg_pos.y+t_tg_pos.z*SW)*8*8 + t_tg_pos.x*2; ushort k_tiles = ((k/8)+SW-1)/SW; // preload load_tile(A, a_tg_origin, ushort2(k,n), A_tg[buffer], start); load_tile(B, b_tg_origin, ushort2(m,k), B_tg[buffer], start); for (ushort l = 1; l < k_tiles; l++) { buffer ^= 1; ushort2 a_tg_pos = a_tg_origin+ushort2(l*SW*8,0); ushort2 b_tg_pos = b_tg_origin+ushort2(0,l*SW*8); threadgroup_barrier(metal::mem_flags::mem_threadgroup); load_tile(A, a_tg_pos, ushort2(k,n), A_tg[buffer], start); load_tile(B, b_tg_pos, ushort2(m,k), B_tg[buffer], start); simdgroup_multiply<SW>(A_tg[buffer^1],B_tg[buffer^1],t_tg_pos.yz,acc); } // post threadgroup_barrier(metal::mem_flags::mem_threadgroup); simdgroup_multiply<SW>(A_tg[buffer],B_tg[buffer],t_tg_pos.yz,acc); if (c_origin.x<m&&c_origin.y<n) { simdgroup_float8x8 c_simd; simdgroup_load(c_simd,C,m,c_origin); simdgroup_multiply(c_simd,c_simd, simdgroup_float8x8(beta)); simdgroup_multiply_accumulate(c_simd,acc,simdgroup_float8x8(alpha),c_simd); simdgroup_store(c_simd,C,m,c_origin); } } ``` </details>
关闭于 2025-09-16 3 条评论