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

KonstantinCommentsb13a68d

master
8.8 KiB274 linesraw
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{
35	uint4 arg0Size: packoffset( c0 );
36	uint4 arg0Strides: packoffset( c1 );
37	uint4 arg1Strides: packoffset( c3 );
38	uint4 resultSize: packoffset( c4 );
39	uint4 resultStrides: packoffset( c5 );
40}
41
42groupshared float 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{
55	uint4 rsi4 = ( THREADS_Y * arg1Strides.y ) * uint4( 0, 1, 2, 3 ) + rsi;
56	[unroll]
57	for( uint iv = 0; iv < heightVectors; iv++, rsi4 += THREADS_Y * 4 * arg1Strides.y )
58	{
59		float4 r = 0;
60		uint4 rsiRow = rsi4;
61		for( uint j = 0; j < w; j++, rsiRow += arg1Strides.x )
62		{
63			// One TILE_SIZE * 4 bytes coalesced load, broadcasted into THREADS_Y copies
64			const float s0 = tile0[ j ][ thread.x ];
65			float4 s1 = 0.0;
66			[unroll]
67			for( uint k = 0; k < 4; k++ )
68			{
69				const uint i = ( iv * 4 + k ) * THREADS_Y + thread.y;
70				if( i < h )
71					s1[ k ] = arg1[ rsiRow[ k ] ];
72			}
73			// Multiply and accumulate
74			r = mad( s0, s1, r );
75		}
76		// Accumulate into the output tile
77		acc[ 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]
86	for( uint iv = 0; iv < heightVectors; iv++ )
87	{
88		float4 r = 0;
89		for( uint j = 0; j < TILE_SIZE; j++ )
90		{
91			// One TILE_SIZE * 4 bytes coalesced load, broadcasted into THREADS_Y copies
92			const float s0 = tile0[ j ][ thread.x ];
93			float4 s1;
94			[unroll]
95			for( uint k = 0; k < 4; k++ )
96			{
97				const uint i = ( iv * 4 + k ) * THREADS_Y + thread.y;
98				// THREADS_Y broadcasts, each one is 4 bytes broadcasted into TILE_SIZE copies
99				s1[ k ] = tile1[ i ][ j ];
100			}
101			// Multiply and accumulate
102			r = mad( s0, s1, r );
103		}
104		// Accumulate into the output tile
105		acc[ 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{
113	uint i;
114	if( rowMajor )
115	{
116		rsi += arg0Strides.y * thread.y;
117		for( i = thread.y; i < h; i += THREADS_Y, rsi += arg0Strides.y * THREADS_Y )
118		{
119			if( thread.x < w )
120				tile0[ thread.x ][ i ] = arg0[ rsi + thread.x * arg0Strides.x ];
121			else
122				tile0[ thread.x ][ i ] = 0.0;
123		}
124	}
125	else
126	{
127		// Unlike width which is smaller for the last tile, the height is always the same, and all these tiles are zero-initialized
128		if( thread.x >= h )
129			return;
130
131		rsi += arg0Strides.x * thread.y;
132		for( i = thread.y; i < w; i += THREADS_Y, rsi += arg0Strides.x * THREADS_Y )
133			tile0[ i ][ thread.x ] = arg0[ rsi + thread.x * arg0Strides.y ];
134
135		if( i >= TILE_SIZE )
136			return;
137		for( ; i < TILE_SIZE; i += THREADS_Y )
138			tile0[ 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{
145	uint i;
146	if( rowMajor )
147	{
148		rsi += thread.y * arg1Strides.y;
149
150		for( i = thread.y; i < h; i += THREADS_Y, rsi += arg1Strides.y * THREADS_Y )
151		{
152			if( thread.x < w )
153				tile1[ i ][ thread.x ] = arg1[ rsi + thread.x * arg1Strides.x ];
154			else
155				tile1[ i ][ thread.x ] = 0.0;
156		}
157	}
158	else
159	{
160		// Unlike width which is smaller for the last tile, the height is always the same, and all these tiles are zero-initialized
161		if( thread.x >= h )
162			return;
163
164		rsi += thread.y * arg1Strides.x;
165		for( i = thread.y; i < w; i += THREADS_Y, rsi += arg1Strides.x * THREADS_Y )
166			tile1[ thread.x ][ i ] = arg1[ rsi + thread.x * arg0Strides.y ];
167		if( i >= TILE_SIZE )
168			return;
169		for( ; i < TILE_SIZE; i += THREADS_Y )
170			tile1[ 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{
177	if( thread.x >= size.x )
178		return;
179
180	const uint4 prod4 = pos * resultStrides;
181	const uint2 prod2 = prod4.xy + prod4.zw;
182	uint rdi = prod2.x + prod2.y;
183	rdi += resultStrides.y * thread.y;
184	rdi += resultStrides.x * thread.x;
185
186	const uint4 offsets = THREADS_Y * uint4( 0, 1, 2, 3 );	//< a compile-time constant vector
187	uint4 rdi4 = resultStrides.y * offsets + rdi;
188
189	[unroll]
190	for( uint iv = 0; iv < heightVectors; iv++, rdi4 += resultStrides.y * THREADS_Y * 4 )
191	{
192		const float4 source = acc[ iv ];
193		[unroll]
194		for( uint k = 0; k < 4; k++ )
195		{
196			const uint i = ( iv * 4 + k ) * THREADS_Y + thread.y;
197			if( i < size.y )
198				result[ 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
207	for( uint i = 0; i < TILE_SIZE; i += THREADS_Y )
208	{
209		tile0[ i + thread.y ][ thread.x ] = 0.0;
210#if !STREAM_SECOND_MATRIX
211		tile1[ 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.
216	float4 acc[ heightVectors ];
217	// Zero out the accumulators
218	[unroll]
219	for( i = 0; i < heightVectors; i++ )
220		acc[ i ] = 0.0;
221
222	const uint2 resultPos = group.xy * TILE_SIZE;
223	const uint2 layer = uint2( group.z % resultSize.z, group.z / resultSize.z );
224	uint rsi0 = resultPos.x * arg0Strides.y + layer.x * arg0Strides.z + layer.y * arg0Strides.w;
225	uint rsi1 = resultPos.y * arg1Strides.y + layer.x * arg1Strides.z + layer.y * arg1Strides.w;
226
227	const uint rsi0Inc = TILE_SIZE * arg0Strides.x;
228	const uint rsi1Inc = TILE_SIZE * arg1Strides.x;
229
230	const uint completeTiles = arg0Size.x / TILE_SIZE;
231	const 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
234	const uint2 outputSize = min( TILE_SIZE, resultSize.xy - resultPos );
235
236	const bool2 loadOrder = LOAD_ORDER;
237
238#if STREAM_SECOND_MATRIX
239	rsi1 += thread.y * arg1Strides.y;
240#endif
241	for( ; rsi0 < rsi0AndAligned; rsi0 += rsi0Inc, rsi1 += rsi1Inc )
242	{
243		loadTile0( rsi0, thread, TILE_SIZE, outputSize.x, loadOrder.x );
244#if STREAM_SECOND_MATRIX
245		GroupMemoryBarrierWithGroupSync();
246		multiplyTiles( rsi1, thread, TILE_SIZE, outputSize.y, acc );
247#else
248		loadTile1( rsi1, thread, TILE_SIZE, outputSize.y, loadOrder.y );
249		GroupMemoryBarrierWithGroupSync();
250		multiplyTiles( 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.
256		GroupMemoryBarrierWithGroupSync();
257	}
258
259	const uint rem = arg0Size.x % TILE_SIZE;
260	if( 0 != rem )
261	{
262		loadTile0( rsi0, thread, rem, outputSize.x, loadOrder.x );
263#if STREAM_SECOND_MATRIX
264		GroupMemoryBarrierWithGroupSync();
265		multiplyTiles( rsi1, thread, rem, outputSize.y, acc );
266#else
267		loadTile1( rsi1, thread, rem, outputSize.y, loadOrder.y );
268		GroupMemoryBarrierWithGroupSync();
269		multiplyTiles( thread, acc );
270#endif
271	}
272
273	storeTile( thread, uint4( resultPos, layer ), outputSize, acc );
274}