yum-mirror/slang
Making it easier to work with shaders
git clone https://git.yummers.dev/yum-mirror/slang
f28f67d98
master
1// In this example, we implement a simple multi-layer perceptron (MLP) training loop on 2// Vulkan (through slang-rhi), using cooperative vector intrinsics. 3// 4// The simple MLP is trained to approximate a polynomial expression. 5// The network contains one hidden layer with 16 neurons. It takes 4 inputs and produces 4 6// outputs. 7 8#include "core/slang-basic.h" 9#include "examples/example-base/example-base.h" 10#include "external/slang-rhi/include/slang-rhi.h" 11#include "slang-com-ptr.h" 12#include "slang.h" 13 14static const ExampleResources resourceBase ("mlp-training-coopvec" ); 15 16typedef uint16_t NFloat ; 17 18// Define the sizes of the layers in the MLP. 19static const int kLayerSizes []= {4 ,16 ,4 }; 20static const int kLayerCount = sizeof (kLayerSizes ) /sizeof (int )- 1 ; 21 22using Slang ::ComPtr ; 23 24struct Kernel 25{ 26ComPtr < rhi::IShaderProgram > program ; 27ComPtr < rhi::IComputePipeline > pipeline ; 28explicit operatorbool () {return program && pipeline ; } 29}; 30 31struct ClearBufferParams 32{ 33 rhi::DeviceAddress buffer ; 34uint32_t count ; 35}; 36 37struct LearnGradParams 38{ 39 rhi::DeviceAddress networkBuffer ; 40 rhi::DeviceAddress lossBuffer ; 41 rhi::DeviceAddress inputs ; 42uint32_t count ; 43}; 44 45struct AdjustParamsParams 46{ 47 rhi::DeviceAddress adamStates ; 48 rhi::DeviceAddress params ; 49 rhi::DeviceAddress gradients ; 50uint32_t count ; 51}; 52 53struct ExampleProgram :public TestBase 54{ 55ComPtr < rhi::IDevice > gDevice ; 56 57ComPtr < slang::ISession > gSlangSession ; 58ComPtr < slang::IModule > gSlangModule ; 59Kernel gLearnGradProgram ; 60Kernel gAdjustParamProgram ; 61 62// Sub-allocated buffer range for each network layer's parameters (weights, biases, gradients). 63// 64struct NetworkParameterAllocation 65 { 66size_t weightsOffset ; 67size_t weightsSize ; 68size_t biasOffset ; 69size_t biasSize ; 70size_t weightsGradOffset ; 71size_t biasGradOffset ; 72size_t weightsGradTrainingOffset ; 73size_t weightsGradTrainingSize ; 74 }; 75 76SlangResult execute (int argc ,char * argv []) 77 { 78parseOption (argc ,argv ); 79 80 rhi::DeviceDesc deviceDesc ; 81deviceDesc .slang .targetProfile = "spirv_1_6" ; 82deviceDesc .deviceType = rhi::DeviceType ::Vulkan ; 83 84gDevice = rhi::getRHI ()-> createDevice (deviceDesc ); 85if (!gDevice ) 86return SLANG_FAIL ; 87 88SLANG_RETURN_ON_FAIL (loadShaderKernels ()); 89 90// Create a buffer to hold all network parameters (weights, biases, gradients). 91// This buffer is arranged as following: 92// (segment 1): | weights0 | bias0 | weights1 | bias1 | ... | weightsN | biasN | 93// (segment 2): | weightsGrad0 | biasGrad0 | weightsGrad1 | biasGrad1 | ... | 94// (segment 3): | weightsGradTraining0 | weightsGradTraining1 | ... | 95// 96// Where the first segment contains all weights and biases for each layer in row-major 97// layout. The second segment contains gradients for weights and biases in row-major layout. 98// The third segment contains gradients for weights in training-optimal layout. 99// The training-optimal layout is used to accumulate gradients for weights with the 100// `coopVecOuterProductAccumulate` intrinsic, which requires the destination to be in 101// training-optimal layout. 102// After accumulating gradients, we will convert them to row-major layout (i.e. copy 103// them back into the second segment) so we can read them in the optimization kernel. 104 105// Total size of all network parameters. 106size_t networkParamsBufferSize ; 107 108// Offset for the second segment, where gradients for weights and biases in row-major layout 109// start. 110size_t networkGraidentOffset ; 111 112// Offset for the third segment, where gradients for weights in training-optimal layout 113// start. 114size_t networkGradientTrainingOffset ; 115 116// Sub-allocated weight/Bias offsets for each layer. 117 std::vector < NetworkParameterAllocation > layerAllocations ; 118 119// Allocate storage for network parameters, filling in `layerRowMajorAllocations`, 120// `networkParamsBufferSize`, `networkGraidentOffset` and `networkGradientTrainingOffset`. 121// 122allocateNetworkParameterStorage ( 123layerAllocations , 124networkParamsBufferSize , 125networkGraidentOffset , 126networkGradientTrainingOffset ); 127 128// We'll initialize the buffer with random values in the range [-1, 1]. 129 std::vector < uint16_t > initParams ; 130srand (1072 ); 131for (int i = 0 ;i < networkParamsBufferSize /sizeof (NFloat );i ++ ) 132 { 133float v = rand () / (float )RAND_MAX ; 134v = v * 2.0f - 1.0f ;// Normalize to [-1, 1] 135initParams .push_back (floatToHalf (v )); 136 } 137auto networkParamsBuffer = createBuffer (networkParamsBufferSize ,initParams .data ()); 138 139// Create a buffer for holding the Adam optimizer state for each network parameter. 140static const size_t kAdamStateSize = sizeof (NFloat )* 2 + sizeof (int32_t ); 141auto adamStateBuffer = createBuffer (initParams .size ()* kAdamStateSize ); 142clearBuffer (adamStateBuffer ); 143 144// Prepare buffer for the `network` struct that holds pointers to network parameters for 145// each layer. 146 std::vector < uint64_t > networkConstantBufferData ; 147for (int i = 0 ;i < kLayerCount ;i ++ ) 148 { 149networkConstantBufferData .push_back ( 150networkParamsBuffer -> getDeviceAddress ()+ layerAllocations [i ].weightsOffset ); 151networkConstantBufferData .push_back ( 152networkParamsBuffer -> getDeviceAddress ()+ 153layerAllocations [i ].weightsGradTrainingOffset ); 154networkConstantBufferData .push_back ( 155networkParamsBuffer -> getDeviceAddress ()+ layerAllocations [i ].biasOffset ); 156networkConstantBufferData .push_back ( 157networkParamsBuffer -> getDeviceAddress ()+ layerAllocations [i ].biasGradOffset ); 158 } 159auto networkConstantBuffer = createBuffer ( 160networkConstantBufferData .size ()* sizeof (uint64_t ), 161networkConstantBufferData .data ()); 162 163// Create buffer for input data. 164static const int inputCount = 32 ; 165 std::vector < float > inputBufferData ; 166for (int i = 0 ;i < inputCount ;i ++ ) 167 { 168inputBufferData .push_back ((float )rand () /RAND_MAX ); 169 } 170auto inputBuffer = createBuffer (inputCount * sizeof (float ),inputBufferData .data ()); 171 172// Create buffer for receiving current loss value. 173auto lossBuffer = createBuffer (sizeof (uint64_t )); 174 175auto queue = gDevice -> getQueue (rhi::QueueType ::Graphics ); 176 177// Run training loop. 178for (int k = 0 ;k < 1000 ;k ++ ) 179 { 180clearBuffer (lossBuffer ); 181 182// Clear weight gradients in the parameter buffer to 0. 183clearBuffer ( 184networkParamsBuffer , 185 rhi::BufferRange { 186networkGradientTrainingOffset , 187networkParamsBufferSize - networkGradientTrainingOffset }); 188// Compute gradients for weights and biases. 189// The weight gradients are stored in the training-optimal layout. 190 { 191LearnGradParams entryPointParams = {}; 192entryPointParams .inputs = inputBuffer -> getDeviceAddress (); 193entryPointParams .count = inputCount /2 ; 194entryPointParams .lossBuffer = lossBuffer -> getDeviceAddress (); 195entryPointParams .networkBuffer = networkConstantBuffer -> getDeviceAddress (); 196dispatchKernel ( 197gLearnGradProgram , 198entryPointParams , 199 (entryPointParams .count + 255 ) /256 ); 200 } 201// Copy weight gradients from training-optimal layout to row-major layout, 202// so we can read them in the `adjustParameters` kernel. 203 { 204 std::vector < rhi::ConvertCooperativeVectorMatrixDesc > matrixDescs ; 205for (int i = 0 ;i < kLayerCount ;i ++ ) 206 { 207 rhi::ConvertCooperativeVectorMatrixDesc desc = {}; 208desc .rowCount = kLayerSizes [i + 1 ]; 209desc .colCount = kLayerSizes [i ]; 210desc .dstComponentType = rhi::CooperativeVectorComponentType ::Float16 ; 211desc .dstSize = & layerAllocations [i ].weightsSize ; 212desc .dstData .deviceAddress = networkParamsBuffer -> getDeviceAddress ()+ 213layerAllocations [i ].weightsGradOffset ; 214desc .dstLayout = rhi::CooperativeVectorMatrixLayout ::RowMajor ; 215desc .dstStride = getNetworkLayerWeightStride (i ); 216desc .srcComponentType = rhi::CooperativeVectorComponentType ::Float16 ; 217desc .srcSize = layerAllocations [i ].weightsGradTrainingSize ; 218desc .srcData .deviceAddress = networkParamsBuffer -> getDeviceAddress ()+ 219layerAllocations [i ].weightsGradTrainingOffset ; 220desc .srcLayout = rhi::CooperativeVectorMatrixLayout ::TrainingOptimal ; 221matrixDescs .push_back (desc ); 222 } 223auto encoder = queue -> createCommandEncoder (); 224encoder -> convertCooperativeVectorMatrix ( 225matrixDescs .data (), 226 (uint32_t )matrixDescs .size ()); 227ComPtr < rhi::ICommandBuffer > commandBuffer ; 228encoder -> finish (commandBuffer .writeRef ()); 229queue -> submit (commandBuffer ); 230 } 231// Adjust parameters in row-major buffer (adam optimize). 232 { 233AdjustParamsParams entryPointParams = {}; 234entryPointParams .adamStates = adamStateBuffer -> getDeviceAddress (); 235entryPointParams .params = networkParamsBuffer -> getDeviceAddress (); 236entryPointParams .count = 237 (networkGradientTrainingOffset - networkGraidentOffset ) /sizeof (NFloat ); 238entryPointParams .gradients = 239networkParamsBuffer -> getDeviceAddress ()+ networkGraidentOffset ; 240dispatchKernel ( 241gAdjustParamProgram , 242entryPointParams , 243 (entryPointParams .count + 255 ) /256 ); 244 } 245 246// Print loss value every 10 iterations. 247if ((k + 1 ) %10 == 0 ) 248 { 249queue -> waitOnHost (); 250ComPtr < ISlangBlob > blob ; 251gDevice -> readBuffer (lossBuffer ,0 ,sizeof (float ),blob .writeRef ()); 252printf ("Loss after %d iterations: %f\n" ,k + 1 ,* (float * )blob -> getBufferPointer ()); 253 } 254 } 255return SLANG_OK ; 256 } 257 258// Allocate storage for network parameters, including weights, biases, and gradients. 259void allocateNetworkParameterStorage ( 260 std::vector < NetworkParameterAllocation >& paramStorage , 261size_t & outParamBufferSize , 262size_t & outGradientOffset , 263size_t & outGradientTrainingOffset ) 264 { 265outParamBufferSize = 0 ; 266 267auto allocRowMajorStorage = [& ](size_t size ) 268 { 269size = (size + 63 ) /64 * 64 ; 270size_t offset = outParamBufferSize ; 271outParamBufferSize += size ; 272return offset ; 273 }; 274 275for (int i = 0 ;i < kLayerCount ;i ++ ) 276 { 277size_t biasSize = getNetworkLayerBiasCount (i )* sizeof (NFloat ); 278NetworkParameterAllocation layerStorage = {}; 279layerStorage .weightsSize = getNetworkLayerWeightCount (i )* sizeof (NFloat ); 280layerStorage .weightsOffset = allocRowMajorStorage (layerStorage .weightsSize ); 281layerStorage .biasSize = biasSize ; 282layerStorage .biasOffset = allocRowMajorStorage (biasSize ); 283paramStorage .push_back (layerStorage ); 284 } 285 286// Alloc storage for weight and bias gradients (row major layout). 287outGradientOffset = outParamBufferSize ; 288for (int i = 0 ;i < kLayerCount ;i ++ ) 289 { 290paramStorage [i ].weightsGradOffset = allocRowMajorStorage (paramStorage [i ].weightsSize ); 291paramStorage [i ].biasGradOffset = allocRowMajorStorage (paramStorage [i ].biasSize ); 292 } 293 294// Alloc training-optimal storage for weight gradients. 295outGradientTrainingOffset = outParamBufferSize ; 296for (int i = 0 ;i < kLayerCount ;i ++ ) 297 { 298// Allocate space for gradients in training-optimal layout. 299 rhi::ConvertCooperativeVectorMatrixDesc matrixDesc = {}; 300matrixDesc .srcComponentType = rhi::CooperativeVectorComponentType ::Float16 ; 301matrixDesc .srcSize = paramStorage [i ].weightsSize ; 302matrixDesc .srcData .hostAddress = nullptr ; 303matrixDesc .srcLayout = rhi::CooperativeVectorMatrixLayout ::RowMajor ; 304matrixDesc .srcStride = getNetworkLayerWeightStride (i ); 305matrixDesc .dstComponentType = rhi::CooperativeVectorComponentType ::Float16 ; 306matrixDesc .dstSize = & paramStorage [i ].weightsGradTrainingSize ; 307matrixDesc .dstData .hostAddress = nullptr ; 308matrixDesc .dstLayout = rhi::CooperativeVectorMatrixLayout ::TrainingOptimal ; 309matrixDesc .dstStride = 0 ; 310matrixDesc .rowCount = kLayerSizes [i + 1 ]; 311matrixDesc .colCount = kLayerSizes [i ]; 312gDevice -> convertCooperativeVectorMatrix (& matrixDesc ,1 ); 313paramStorage [i ].weightsGradTrainingOffset = 314allocRowMajorStorage (paramStorage [i ].weightsGradTrainingSize ); 315 } 316 } 317 318// Dispatch a compute kernel with the given arguments and number of work groups. 319template < typename Args > 320void dispatchKernel (Kernel & kernel ,Args & args ,size_t numWorkGroups ) 321 { 322auto queue = gDevice -> getQueue (rhi::QueueType ::Graphics ); 323ComPtr < rhi::ICommandEncoder > encoder ; 324queue -> createCommandEncoder (encoder .writeRef ()); 325 { 326auto computeEncoder = encoder -> beginComputePass (); 327auto rootShaderObject = computeEncoder -> bindPipeline (kernel .pipeline .get ()); 328rootShaderObject -> getEntryPoint (0 )-> setData (rhi::ShaderOffset (),& args ,sizeof (args )); 329computeEncoder -> dispatchCompute (numWorkGroups ,1 ,1 ); 330computeEncoder -> end (); 331 } 332ComPtr < rhi::ICommandBuffer > commandBuffer ; 333encoder -> finish (commandBuffer .writeRef ()); 334queue -> submit (commandBuffer ); 335 } 336 337// Create a buffer with the specified size and optional initial data. 338ComPtr < rhi::IBuffer > createBuffer (size_t size ,void * initData = nullptr ) 339 { 340 rhi::BufferDesc bufferDesc = {}; 341bufferDesc .size = size ; 342bufferDesc .defaultState = rhi::ResourceState ::UnorderedAccess ; 343bufferDesc .usage = rhi::BufferUsage ::CopySource | rhi::BufferUsage ::CopyDestination | 344 rhi::BufferUsage ::UnorderedAccess ; 345bufferDesc .memoryType = rhi::MemoryType ::DeviceLocal ; 346return gDevice -> createBuffer (bufferDesc ,initData ); 347 } 348 349void clearBuffer (rhi::IBuffer * buffer , rhi::BufferRange range = rhi::kEntireBuffer ) 350 { 351auto queue = gDevice -> getQueue (rhi::QueueType ::Graphics ); 352auto encoder = queue -> createCommandEncoder (); 353encoder -> clearBuffer (buffer ,range ); 354auto cmdBuffer = encoder -> finish (); 355queue -> submit (cmdBuffer ); 356 } 357 358SlangResult loadShaderKernels () 359 { 360Slang ::String path = resourceBase .resolveResource ("kernels.slang" ); 361 362gSlangSession = createSlangSession (gDevice ); 363gSlangModule = compileShaderModuleFromFile (gSlangSession ,path .getBuffer ()); 364if (!gSlangModule ) 365return SLANG_FAIL ; 366 367gLearnGradProgram = loadComputeProgram (gSlangModule ,"learnGradient" ); 368if (!gLearnGradProgram ) 369return SLANG_FAIL ; 370 371gAdjustParamProgram = loadComputeProgram (gSlangModule ,"adjustParameters" ); 372if (!gAdjustParamProgram ) 373return SLANG_FAIL ; 374 375return SLANG_OK ; 376 } 377 378Kernel loadComputeProgram (slang::IModule * slangModule ,char const * entryPointName ) 379 { 380ComPtr < slang::IEntryPoint > entryPoint ; 381slangModule -> findEntryPointByName (entryPointName ,entryPoint .writeRef ()); 382 383ComPtr < slang::IComponentType > linkedProgram ; 384entryPoint -> link (linkedProgram .writeRef ()); 385 386if (isTestMode ()) 387 { 388printEntrypointHashes (1 ,1 ,linkedProgram ); 389 } 390 391Kernel result ; 392 393 rhi::ComputePipelineDesc desc ; 394auto program = gDevice -> createShaderProgram (linkedProgram ); 395desc .program = program .get (); 396result .program = program ; 397result .pipeline = gDevice -> createComputePipeline (desc ); 398return result ; 399 } 400 401static inline unsigned short floatToHalf (float val ) 402 { 403uint32_t x = 0 ; 404memcpy (& x ,& val ,sizeof (float )); 405 406unsigned short bits = (x >>16 )& 0x8000 ; 407unsigned short m = (x >>12 )& 0x07ff ; 408unsigned int e = (x >>23 )& 0xff ; 409if (e < 103 ) 410return bits ; 411if (e > 142 ) 412 { 413bits |=0x7c00u ; 414bits |=e == 255 && (x & 0x007fffffu ); 415return bits ; 416 } 417if (e < 113 ) 418 { 419m |=0x0800u ; 420bits |= (m >> (114 - e ))+ ((m >> (113 - e ))& 1 ); 421return bits ; 422 } 423bits |= ((e - 112 ) <<10 ) | (m >>1 ); 424bits += m & 1 ; 425return bits ; 426 } 427 428int getNetworkLayerWeightStride (int i ) {return kLayerSizes [i ]* sizeof (NFloat ); } 429 430int getNetworkLayerWeightCount (int i ) {return kLayerSizes [i ]* kLayerSizes [i + 1 ]; } 431 432int getNetworkLayerBiasCount (int i ) {return kLayerSizes [i + 1 ]; } 433 434ComPtr < slang::ISession > createSlangSession (rhi::IDevice * device ) 435 { 436ComPtr < slang::ISession > slangSession = device -> getSlangSession (); 437return slangSession ; 438 } 439 440ComPtr < slang::IModule > compileShaderModuleFromFile ( 441 slang::ISession * slangSession , 442char const * filePath ) 443 { 444ComPtr < slang::IModule > slangModule ; 445ComPtr < slang::IBlob > diagnosticBlob ; 446Slang ::String path = resourceBase .resolveResource (filePath ); 447slangModule = slangSession -> loadModule (path .getBuffer (),diagnosticBlob .writeRef ()); 448diagnoseIfNeeded (diagnosticBlob ); 449 450return slangModule ; 451 } 452}; 453 454int exampleMain (int argc ,char ** argv ) 455{ 456ExampleProgram app ; 457if (SLANG_FAILED (app .execute (argc ,argv ))) 458 { 459return -1 ; 460 } 461return 0 ; 462}