yum-mirror/slang

Making it easier to work with shaders

git clone https://git.yummers.dev/yum-mirror/slang

Yong HeAdd MLP training examples. (#7550)f28f67d98

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