yum-archive/TaSTT-Whisper
High-performance GPGPU inference of OpenAI's Whisper automatic speech recognition (ASR) model
git clone https://git.yummers.dev/yum-archive/TaSTT-Whisper
b13a68d
master
1// This compute shader implements matrix*matrix product, using tiling and many other tricks to improve the performance 2// This one here is _the_ most expensive shader in the model. Optimized heavily, as a result the readability ain't great. 3 4#ifndef TILE_SIZE 5static const uint TILE_SIZE = 32 ; 6#endif 7#ifndef THREADS_Y 8static const uint THREADS_Y = 8 ; 9#endif 10// The above values have a following constraint: TILE_SIZE = THREADS_Y * N * 4 where N is an integer 11 12#ifndef STREAM_SECOND_MATRIX 13// Funfact: enabling this on 1080Ti ruins the performance, by a factor of 3.5 14#define STREAM_SECOND_MATRIX 0 15#endif 16 17#ifndef LOAD_ORDER 18 19// Load with coalesced loads from global memory whenever possible, store into groupshared buffer with random stores 20// #define LOAD_ORDER bool2( ( 1 == arg0Strides[ 0 ] ) || ( 1 != arg0Strides[ 1 ] ), ( 1 == arg1Strides[ 0 ] ) || ( 1 != arg1Strides[ 1 ] ) ) 21 22// Load with random loads from global memory, store into groupshared buffer with coalesced stores 23// On my AMD iGPU inside Ryzen 7 5700G, there's whopping 15% performance win with that tactics, from 6.67 to 5.66 seconds for this shader. 24// My nVidia GPU does about the same 25#define LOAD_ORDER bool2( false, true ) 26 27#endif 28 29Buffer < float > arg0 :register ( t0 ); 30Buffer < float > arg1 :register ( t1 ); 31RWBuffer < float > result :register ( u0 ); 32 33cbuffer Constants :register ( b0 ) 34{ 35uint4 arg0Size :packoffset ( c0 ); 36uint4 arg0Strides :packoffset ( c1 ); 37uint4 arg1Strides :packoffset ( c3 ); 38uint4 resultSize :packoffset ( c4 ); 39uint4 resultStrides :packoffset ( c5 ); 40} 41 42groupsharedfloat tile0 [ TILE_SIZE ][ TILE_SIZE ]; 43#if ! STREAM_SECOND_MATRIX 44groupshared float tile1 [ TILE_SIZE ][ TILE_SIZE ]; 45#endif 46 47// Count of FP32 accumulators we need in every thread of the shader 48static const uint heightScalars = TILE_SIZE / THREADS_Y ; 49// The local accumulators are float4 vectors, compute count of these vectors 50static const uint heightVectors = ( heightScalars + 3 ) / 4 ; 51 52#if STREAM_SECOND_MATRIX 53void multiplyTiles ( uint rsi , const uint3 thread , const uint w , const uint h , inout float4 acc [ heightVectors ] ) 54{ 55uint4 rsi4 = ( THREADS_Y * arg1Strides . y ) * uint4 ( 0 , 1 , 2 , 3 ) + rsi ; 56[ unroll ] 57for ( uint iv = 0 ; iv < heightVectors ; iv ++ , rsi4 += THREADS_Y * 4 * arg1Strides . y ) 58{ 59float4 r = 0 ; 60uint4 rsiRow = rsi4 ; 61for ( uint j = 0 ; j < w ; j ++ , rsiRow += arg1Strides . x ) 62{ 63// One TILE_SIZE * 4 bytes coalesced load, broadcasted into THREADS_Y copies 64const float s0 = tile0 [ j ][ thread . x ]; 65float4 s1 = 0.0 ; 66[ unroll ] 67for ( uint k = 0 ; k < 4 ; k ++ ) 68{ 69const uint i = ( iv * 4 + k ) * THREADS_Y + thread . y ; 70if ( i < h ) 71s1 [ k ] = arg1 [ rsiRow [ k ] ]; 72} 73// Multiply and accumulate 74r = mad ( s0 , s1 , r ); 75} 76// Accumulate into the output tile 77acc [ iv ] += r ; 78} 79} 80#else 81// Compute resTemp += tile0 * tile1, for TILE_SIZE^2 square matrices 82// The group size is TILE_SIZE*THREADS_Y threads in this shader 83void multiplyTiles ( const uint3 thread , inout float4 acc [ heightVectors ] ) 84{ 85[ unroll ] 86for ( uint iv = 0 ; iv < heightVectors ; iv ++ ) 87{ 88float4 r = 0 ; 89for ( uint j = 0 ; j < TILE_SIZE ; j ++ ) 90{ 91// One TILE_SIZE * 4 bytes coalesced load, broadcasted into THREADS_Y copies 92const float s0 = tile0 [ j ][ thread . x ]; 93float4 s1 ; 94[ unroll ] 95for ( uint k = 0 ; k < 4 ; k ++ ) 96{ 97const uint i = ( iv * 4 + k ) * THREADS_Y + thread . y ; 98// THREADS_Y broadcasts, each one is 4 bytes broadcasted into TILE_SIZE copies 99s1 [ k ] = tile1 [ i ][ j ]; 100} 101// Multiply and accumulate 102r = mad ( s0 , s1 , r ); 103} 104// Accumulate into the output tile 105acc [ iv ] += r ; 106} 107} 108#endif 109 110// Note we transposed these tiles while loading 111void loadTile0 ( uint rsi , const uint3 thread , const uint w , const uint h , const bool rowMajor ) 112{ 113uint i ; 114if ( rowMajor ) 115{ 116rsi += arg0Strides . y * thread . y ; 117for ( i = thread . y ; i < h ; i += THREADS_Y , rsi += arg0Strides . y * THREADS_Y ) 118{ 119if ( thread . x < w ) 120tile0 [ thread . x ][ i ] = arg0 [ rsi + thread . x * arg0Strides . x ]; 121else 122tile0 [ thread . x ][ i ] = 0.0 ; 123} 124} 125else 126{ 127// Unlike width which is smaller for the last tile, the height is always the same, and all these tiles are zero-initialized 128if ( thread . x >= h ) 129return ; 130 131rsi += arg0Strides . x * thread . y ; 132for ( i = thread . y ; i < w ; i += THREADS_Y , rsi += arg0Strides . x * THREADS_Y ) 133tile0 [ i ][ thread . x ] = arg0 [ rsi + thread . x * arg0Strides . y ]; 134 135if ( i >= TILE_SIZE ) 136return ; 137for ( ; i < TILE_SIZE ; i += THREADS_Y ) 138tile0 [ i ][ thread . x ] = 0.0 ; 139} 140} 141 142#if ! STREAM_SECOND_MATRIX 143void loadTile1 ( uint rsi , const uint3 thread , const uint w , const uint h , const bool rowMajor ) 144{ 145uint i ; 146if ( rowMajor ) 147{ 148rsi += thread . y * arg1Strides . y ; 149 150for ( i = thread . y ; i < h ; i += THREADS_Y , rsi += arg1Strides . y * THREADS_Y ) 151{ 152if ( thread . x < w ) 153tile1 [ i ][ thread . x ] = arg1 [ rsi + thread . x * arg1Strides . x ]; 154else 155tile1 [ i ][ thread . x ] = 0.0 ; 156} 157} 158else 159{ 160// Unlike width which is smaller for the last tile, the height is always the same, and all these tiles are zero-initialized 161if ( thread . x >= h ) 162return ; 163 164rsi += thread . y * arg1Strides . x ; 165for ( i = thread . y ; i < w ; i += THREADS_Y , rsi += arg1Strides . x * THREADS_Y ) 166tile1 [ thread . x ][ i ] = arg1 [ rsi + thread . x * arg0Strides . y ]; 167if ( i >= TILE_SIZE ) 168return ; 169for ( ; i < TILE_SIZE ; i += THREADS_Y ) 170tile1 [ thread . x ][ i ] = 0.0 ; 171} 172} 173#endif 174 175void storeTile ( const uint3 thread , const uint4 pos , const uint2 size , in float4 acc [ heightVectors ] ) 176{ 177if ( thread . x >= size . x ) 178return ; 179 180const uint4 prod4 = pos * resultStrides ; 181const uint2 prod2 = prod4 . xy + prod4 . zw ; 182uint rdi = prod2 . x + prod2 . y ; 183rdi += resultStrides . y * thread . y ; 184rdi += resultStrides . x * thread . x ; 185 186const uint4 offsets = THREADS_Y * uint4 ( 0 , 1 , 2 , 3 ); //< a compile-time constant vector 187uint4 rdi4 = resultStrides . y * offsets + rdi ; 188 189[ unroll ] 190for ( uint iv = 0 ; iv < heightVectors ; iv ++ , rdi4 += resultStrides . y * THREADS_Y * 4 ) 191{ 192const float4 source = acc [ iv ]; 193[ unroll ] 194for ( uint k = 0 ; k < 4 ; k ++ ) 195{ 196const uint i = ( iv * 4 + k ) * THREADS_Y + thread . y ; 197if ( i < size . y ) 198result [ rdi4 [ k ] ] = source [ k ]; 199} 200} 201} 202 203[ numthreads ( TILE_SIZE , THREADS_Y , 1 ) ] 204void main ( uint3 group : SV_GroupID , uint3 thread : SV_GroupThreadID ) 205{ 206// Zero out these shared buffers 207for ( uint i = 0 ; i < TILE_SIZE ; i += THREADS_Y ) 208{ 209tile0 [ i + thread . y ][ thread . x ] = 0.0 ; 210#if ! STREAM_SECOND_MATRIX 211tile1 [ i + thread . y ][ thread . x ] = 0.0 ; 212#endif 213} 214// Despite inside GPU cores, the shared memory is still much slower than registers 215// For this reason, this shader accumulates numbers in local variables. Only uses groupshared memory for tiles of the argument matrices. 216float4 acc [ heightVectors ]; 217// Zero out the accumulators 218[ unroll ] 219for ( i = 0 ; i < heightVectors ; i ++ ) 220acc [ i ] = 0.0 ; 221 222const uint2 resultPos = group . xy * TILE_SIZE ; 223const uint2 layer = uint2 ( group . z % resultSize . z , group . z / resultSize . z ); 224uint rsi0 = resultPos . x * arg0Strides . y + layer . x * arg0Strides . z + layer . y * arg0Strides . w ; 225uint rsi1 = resultPos . y * arg1Strides . y + layer . x * arg1Strides . z + layer . y * arg1Strides . w ; 226 227const uint rsi0Inc = TILE_SIZE * arg0Strides . x ; 228const uint rsi1Inc = TILE_SIZE * arg1Strides . x ; 229 230const uint completeTiles = arg0Size . x / TILE_SIZE ; 231const uint rsi0AndAligned = rsi0 + rsi0Inc * completeTiles ; 232// Output tile size 233// Normally TILE_SIZE^2, less than that for the tiles at the right and bottom edges of the output matrix 234const uint2 outputSize = min ( TILE_SIZE , resultSize . xy - resultPos ); 235 236const bool2 loadOrder = LOAD_ORDER ; 237 238#if STREAM_SECOND_MATRIX 239rsi1 += thread . y * arg1Strides . y ; 240#endif 241for ( ; rsi0 < rsi0AndAligned ; rsi0 += rsi0Inc , rsi1 += rsi1Inc ) 242{ 243loadTile0 ( rsi0 , thread , TILE_SIZE , outputSize . x , loadOrder . x ); 244#if STREAM_SECOND_MATRIX 245GroupMemoryBarrierWithGroupSync (); 246multiplyTiles ( rsi1 , thread , TILE_SIZE , outputSize . y , acc ); 247#else 248loadTile1 ( rsi1 , thread , TILE_SIZE , outputSize . y , loadOrder . y ); 249GroupMemoryBarrierWithGroupSync (); 250multiplyTiles ( thread , acc ); 251#endif 252// Need one moar barrier here. 253// Otherwise, some threads of the group are loading the next tile into tile0/tile1 groupshared buffers on the next iteration of the loop, 254// while other threads of the same group are still computing the matrix product, and getting incorrect values from that groupshared buffer. 255// The missing barrier only caused a bug on AMD, and only with "ggml-large.bin" model; no idea why that is. 256GroupMemoryBarrierWithGroupSync (); 257} 258 259const uint rem = arg0Size . x % TILE_SIZE ; 260if ( 0 != rem ) 261{ 262loadTile0 ( rsi0 , thread , rem , outputSize . x , loadOrder . x ); 263#if STREAM_SECOND_MATRIX 264GroupMemoryBarrierWithGroupSync (); 265multiplyTiles ( rsi1 , thread , rem , outputSize . y , acc ); 266#else 267loadTile1 ( rsi1 , thread , rem , outputSize . y , loadOrder . y ); 268GroupMemoryBarrierWithGroupSync (); 269multiplyTiles ( thread , acc ); 270#endif 271} 272 273storeTile ( thread , uint4 ( resultPos , layer ), outputSize , acc ); 274}