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
13.1 KiB389 linesraw
1// In this example, we implement a simple multi-layer perceptron (MLP) training loop on
2// Vulkan (through slang-rhi). See also the mlp-training-coopvec example, which
3// implements the same MLP training loop using cooperative vector intrinsics for better
4// performance.
5//
6// The simple MLP is trained to approximate a polynomial expression.
7// The network contains one hidden layer with 16 neurons. It takes 4 inputs and produces 4
8// outputs.
9
10#include "core/slang-basic.h"
11#include "examples/example-base/example-base.h"
12#include "external/slang-rhi/include/slang-rhi.h"
13#include "slang-com-ptr.h"
14#include "slang.h"
15
16#include <string>
17
18using Slang::ComPtr;
19
20static const ExampleResources resourceBase("mlp-training");
21
22typedef uint16_t NFloat;
23
24static const int kLayerSizes[] = {4, 16, 4};
25static const int kLayerCount = sizeof(kLayerSizes) / sizeof(int) - 1;
26
27int getNetworkLayerWeightStride(int i)
28{
29    return kLayerSizes[i] * sizeof(NFloat);
30}
31
32int getNetworkLayerWeightCount(int i)
33{
34    return kLayerSizes[i] * kLayerSizes[i + 1];
35}
36
37int getNetworkLayerBiasCount(int i)
38{
39    return kLayerSizes[i + 1];
40}
41
42struct Kernel
43{
44    ComPtr<rhi::IShaderProgram> program;
45    ComPtr<rhi::IComputePipeline> pipeline;
46    operator bool() { return program && pipeline; }
47};
48
49struct ClearBufferParams
50{
51    rhi::DeviceAddress buffer;
52    uint32_t count;
53};
54
55struct LearnGradParams
56{
57    rhi::DeviceAddress networkBuffer;
58    rhi::DeviceAddress lossBuffer;
59    rhi::DeviceAddress inputs;
60    uint32_t count;
61};
62
63struct AdjustParamsParams
64{
65    rhi::DeviceAddress adamStates;
66    rhi::DeviceAddress params;
67    rhi::DeviceAddress gradients;
68    uint32_t count;
69};
70
71struct ExampleProgram : public TestBase
72{
73    ComPtr<rhi::IDevice> gDevice;
74
75    ComPtr<slang::ISession> gSlangSession;
76    ComPtr<slang::IModule> gSlangModule;
77    Kernel gLearnGradProgram;
78    Kernel gAdjustParamProgram;
79
80    // Sub-allocated buffer range for each network layer's parameters (weights, biases, gradients).
81    //
82    struct NetworkParameterAllocation
83    {
84        size_t weightsOffset;
85        size_t weightsSize;
86        size_t biasOffset;
87        size_t biasSize;
88        size_t weightsGradOffset;
89        size_t biasGradOffset;
90    };
91
92    SlangResult execute(int argc, char* argv[])
93    {
94        parseOption(argc, argv);
95
96        rhi::DeviceDesc deviceDesc;
97        deviceDesc.slang.targetProfile = "spirv_1_6";
98        deviceDesc.deviceType = rhi::DeviceType::Vulkan;
99
100        gDevice = rhi::getRHI()->createDevice(deviceDesc);
101        if (!gDevice)
102            return SLANG_FAIL;
103
104        SLANG_RETURN_ON_FAIL(loadShaderKernels());
105
106        // Create a buffer to hold all network parameters (weights, biases, gradients).
107        // This buffer is arranged as following:
108        // (segment 1): | weights0 | bias0 | weights1 | bias1 | ... | weightsN | biasN |
109        // (segment 2): | weightsGrad0 | biasGrad0 | weightsGrad1 | biasGrad1 | ... |
110        //
111        // Where the first segment contains all weights and biases for each layer in row-major
112        // layout. The second segment contains gradients for weights and biases in row-major layout.
113
114        // Total size of all network parameters.
115        size_t paramBufferSize;
116
117        // Offset for the second segment, where gradients for weights and biases in row-major layout
118        // start.
119        size_t gradientOffset;
120
121        // Sub-allocated weight/Bias offsets for each layer.
122        std::vector<NetworkParameterAllocation> layerAllocations;
123        allocateNetworkParameterStorage(layerAllocations, paramBufferSize, gradientOffset);
124
125        std::vector<uint16_t> initParams;
126        srand(1072);
127        for (int i = 0; i < paramBufferSize / sizeof(NFloat); i++)
128        {
129            if (i < gradientOffset / sizeof(NFloat))
130            {
131                float v = rand() / (float)RAND_MAX;
132                v = v * 2.0f - 1.0f; // Normalize to [-1, 1]
133                initParams.push_back(floatToHalf(v));
134            }
135            else
136            {
137                // Initialize gradients to zero.
138                initParams.push_back(0);
139            }
140        }
141        auto networkParamsBuffer = createBuffer(paramBufferSize, initParams.data());
142
143        static const size_t kAdamStateSize = sizeof(NFloat) * 2 + sizeof(int32_t);
144        auto adamStateBuffer = createBuffer(initParams.size() * kAdamStateSize);
145        clearBuffer(adamStateBuffer);
146
147        std::vector<uint64_t> networkConstantBufferData;
148        auto paramBufferAddr = networkParamsBuffer->getDeviceAddress();
149        for (int i = 0; i < kLayerCount; i++)
150        {
151            networkConstantBufferData.push_back(
152                paramBufferAddr + layerAllocations[i].weightsOffset);
153            networkConstantBufferData.push_back(
154                paramBufferAddr + layerAllocations[i].weightsGradOffset);
155            networkConstantBufferData.push_back(paramBufferAddr + layerAllocations[i].biasOffset);
156            networkConstantBufferData.push_back(
157                paramBufferAddr + layerAllocations[i].biasGradOffset);
158        }
159        auto networkConstantBuffer = createBuffer(
160            networkConstantBufferData.size() * sizeof(uint64_t),
161            networkConstantBufferData.data());
162
163        static const int inputCount = 32;
164        std::vector<float> inputBufferData;
165        for (int i = 0; i < inputCount; i++)
166        {
167            inputBufferData.push_back((float)rand() / RAND_MAX);
168        }
169        auto inputBuffer = createBuffer(inputCount * sizeof(float), inputBufferData.data());
170
171        // Create buffer for receiving current loss value.
172        auto lossBuffer = createBuffer(sizeof(uint64_t));
173
174        auto queue = gDevice->getQueue(rhi::QueueType::Graphics);
175
176        for (int k = 0; k < 1000; k++)
177        {
178            clearBuffer(lossBuffer);
179
180            // Compute gradients.
181            {
182                LearnGradParams entryPointParams = {};
183                entryPointParams.inputs = inputBuffer->getDeviceAddress();
184                entryPointParams.count = inputCount / 2;
185                entryPointParams.lossBuffer = lossBuffer->getDeviceAddress();
186                entryPointParams.networkBuffer = networkConstantBuffer->getDeviceAddress();
187                dispatchKernel(
188                    gLearnGradProgram,
189                    entryPointParams,
190                    (entryPointParams.count + 255) / 256);
191            }
192            // Adjust parameters in row-major buffer (adam optimize).
193            {
194                AdjustParamsParams entryPointParams = {};
195                entryPointParams.adamStates = adamStateBuffer->getDeviceAddress();
196                entryPointParams.params = networkParamsBuffer->getDeviceAddress();
197                entryPointParams.count = (paramBufferSize - gradientOffset) / sizeof(NFloat);
198                entryPointParams.gradients =
199                    networkParamsBuffer->getDeviceAddress() + gradientOffset;
200                dispatchKernel(
201                    gAdjustParamProgram,
202                    entryPointParams,
203                    (entryPointParams.count + 255) / 256);
204            }
205            if ((k + 1) % 10 == 0)
206            {
207                queue->waitOnHost();
208                ComPtr<ISlangBlob> blob;
209                gDevice->readBuffer(lossBuffer, 0, sizeof(float), blob.writeRef());
210                printf("Loss after %d iterations: %f\n", k + 1, *(float*)blob->getBufferPointer());
211            }
212        }
213        return SLANG_OK;
214    }
215
216    // Allocate storage for network parameters, including weights, biases, and gradients.
217    void allocateNetworkParameterStorage(
218        std::vector<NetworkParameterAllocation>& paramStorage,
219        size_t& outParamBufferSize,
220        size_t& outGradientOffset)
221    {
222        outParamBufferSize = 0;
223
224        auto allocRowMajorStorage = [&](size_t size)
225        {
226            size = (size + 63) / 64 * 64;
227            size_t offset = outParamBufferSize;
228            outParamBufferSize += size;
229            return offset;
230        };
231
232        for (int i = 0; i < kLayerCount; i++)
233        {
234            size_t biasSize = getNetworkLayerBiasCount(i) * sizeof(NFloat);
235            NetworkParameterAllocation layer = {};
236            layer.weightsSize = getNetworkLayerWeightCount(i) * sizeof(NFloat);
237            layer.weightsOffset = allocRowMajorStorage(layer.weightsSize);
238            layer.biasSize = biasSize;
239            layer.biasOffset = allocRowMajorStorage(biasSize);
240            paramStorage.push_back(layer);
241        }
242
243        // Alloc storage for gradients.
244        outGradientOffset = outParamBufferSize;
245        for (int i = 0; i < kLayerCount; i++)
246        {
247            paramStorage[i].weightsGradOffset = allocRowMajorStorage(paramStorage[i].weightsSize);
248            paramStorage[i].biasGradOffset = allocRowMajorStorage(paramStorage[i].biasSize);
249        }
250    }
251
252    template<typename Args>
253    void dispatchKernel(Kernel& kernel, Args& args, size_t numWorkGroups)
254    {
255        auto queue = gDevice->getQueue(rhi::QueueType::Graphics);
256        ComPtr<rhi::ICommandEncoder> encoder;
257        queue->createCommandEncoder(encoder.writeRef());
258        {
259            auto computeEncoder = encoder->beginComputePass();
260            auto rootShaderObject = computeEncoder->bindPipeline(kernel.pipeline.get());
261            rootShaderObject->getEntryPoint(0)->setData(rhi::ShaderOffset(), &args, sizeof(args));
262            computeEncoder->dispatchCompute(numWorkGroups, 1, 1);
263            computeEncoder->end();
264        }
265        ComPtr<rhi::ICommandBuffer> commandBuffer;
266        encoder->finish(commandBuffer.writeRef());
267        queue->submit(commandBuffer);
268    }
269
270    // Create a buffer with the specified size and optional initial data.
271    ComPtr<rhi::IBuffer> createBuffer(size_t size, void* initData = nullptr)
272    {
273        rhi::BufferDesc bufferDesc = {};
274        bufferDesc.size = size;
275        bufferDesc.defaultState = rhi::ResourceState::UnorderedAccess;
276        bufferDesc.usage = rhi::BufferUsage::CopySource | rhi::BufferUsage::CopyDestination |
277                           rhi::BufferUsage::UnorderedAccess;
278        bufferDesc.memoryType = rhi::MemoryType::DeviceLocal;
279        return gDevice->createBuffer(bufferDesc, initData);
280    }
281
282    void clearBuffer(rhi::IBuffer* buffer)
283    {
284        auto queue = gDevice->getQueue(rhi::QueueType::Graphics);
285        auto encoder = queue->createCommandEncoder();
286        encoder->clearBuffer(buffer);
287        auto cmdBuffer = encoder->finish();
288        queue->submit(cmdBuffer);
289    }
290
291    Kernel loadComputeProgram(slang::IModule* slangModule, char const* entryPointName)
292    {
293        ComPtr<slang::IEntryPoint> entryPoint;
294        slangModule->findEntryPointByName(entryPointName, entryPoint.writeRef());
295
296        ComPtr<slang::IComponentType> linkedProgram;
297        entryPoint->link(linkedProgram.writeRef());
298
299        if (isTestMode())
300        {
301            printEntrypointHashes(1, 1, linkedProgram);
302        }
303
304        Kernel result;
305
306        rhi::ComputePipelineDesc desc;
307        auto program = gDevice->createShaderProgram(linkedProgram);
308        desc.program = program.get();
309        result.program = program;
310        result.pipeline = gDevice->createComputePipeline(desc);
311        return result;
312    }
313
314    inline unsigned short floatToHalf(float val)
315    {
316        uint32_t x = 0;
317        memcpy(&x, &val, sizeof(float));
318
319        unsigned short bits = (x >> 16) & 0x8000;
320        unsigned short m = (x >> 12) & 0x07ff;
321        unsigned int e = (x >> 23) & 0xff;
322        if (e < 103)
323            return bits;
324        if (e > 142)
325        {
326            bits |= 0x7c00u;
327            bits |= e == 255 && (x & 0x007fffffu);
328            return bits;
329        }
330        if (e < 113)
331        {
332            m |= 0x0800u;
333            bits |= (m >> (114 - e)) + ((m >> (113 - e)) & 1);
334            return bits;
335        }
336        bits |= ((e - 112) << 10) | (m >> 1);
337        bits += m & 1;
338        return bits;
339    }
340
341    ComPtr<slang::ISession> createSlangSession(rhi::IDevice* device)
342    {
343        ComPtr<slang::ISession> slangSession = device->getSlangSession();
344        return slangSession;
345    }
346
347    ComPtr<slang::IModule> compileShaderModuleFromFile(
348        slang::ISession* slangSession,
349        char const* filePath)
350    {
351        ComPtr<slang::IModule> slangModule;
352        ComPtr<slang::IBlob> diagnosticBlob;
353        Slang::String path = resourceBase.resolveResource(filePath);
354        slangModule = slangSession->loadModule(path.getBuffer(), diagnosticBlob.writeRef());
355        diagnoseIfNeeded(diagnosticBlob);
356
357        return slangModule;
358    }
359
360    SlangResult loadShaderKernels()
361    {
362        Slang::String path = resourceBase.resolveResource("kernels.slang");
363
364        gSlangSession = createSlangSession(gDevice);
365        gSlangModule = compileShaderModuleFromFile(gSlangSession, path.getBuffer());
366        if (!gSlangModule)
367            return SLANG_FAIL;
368
369        gLearnGradProgram = loadComputeProgram(gSlangModule, "learnGradient");
370        if (!gLearnGradProgram)
371            return SLANG_FAIL;
372
373        gAdjustParamProgram = loadComputeProgram(gSlangModule, "adjustParameters");
374        if (!gAdjustParamProgram)
375            return SLANG_FAIL;
376
377        return SLANG_OK;
378    }
379};
380
381int exampleMain(int argc, char** argv)
382{
383    ExampleProgram app;
384    if (SLANG_FAILED(app.execute(argc, argv)))
385    {
386        return -1;
387    }
388    return 0;
389}