C++ 开发者的机器学习——硬方法:DirectML





5.00/5 (8投票s)
机器学习介绍,包含训练线性回归模型的 C++ 代码。
来源
https://github.com/WindowsNT/DirectMLTest
引言
许多人讨论,尤其是在 ChatGPT 等模型出现之后,机器学习在人工智能中的益处。本文试图从我作为机器学习业余爱好者但总体低级编程专家的角度,解释一些基础知识,并使用 DirectML 进行实际的训练场景。
背景
本文面向硬核 Windows C++ 开发人员,熟悉 COM 和 DirectX,介绍如何使用 DirectML 进行机器学习。为了(更加)简单起见,通常会使用 TensorFlow、PyTorch 和 Python,但由于我本人坚持使用 C++,我想探索 DirectML 的内部原理。
DirectML 是一个低级层(PyTorch 在其之上),所以请准备好应对大量困难、繁琐的工作。
机器学习概述。
我强烈推荐 《机器学习入门》。机器学习总体上是指计算机解决给定 x 的 f(x) = y 的能力。X 和 Y 可能不仅仅是单个变量,而是一组(大的)变量。机器学习有三种基本类型:
- 监督学习。这意味着我有一组给定的 [x,y],并训练一个模型来“学习”它,并为未知的 x 计算 y。一个简单的例子是 1990 年到 2024 年汽车型号的价格集合(x = 年份,y = 价格),然后查询模型在 2025 年的可能价格。另一个例子是学生练习课程的小时数以及他们是否会在考试中及格,这基于一组学习了特定小时数(x)并通过或未通过(y)的学生。
- 无监督学习。这意味着我们试图从中学习的 [x,y] 集合是不完整的。一个例子是杀毒软件检测,其中 AV 试图学习的不是明确的集合,而是基于相似性模式。
- 强化学习。这基于 f(x) = y 的反向,即对于给定的 y,我们试图找到有效的 x。一个例子是自动驾驶系统,它理所当然地认为它不能发生碰撞(y),并找到所有会导致这种情况的 x。
在我们的示例中,我们将坚持最简单的形式,即监督学习。
这种模式的“Hello world”是**简单线性回归**。也就是说,对于一组给定的 [x,y] 变量,我们试图创建一个线性函数 f(x) = a + bx,使得该线“接近”所有这些点,如下图所示。
另一个维基百科示例
给定 [x,y] 集合计算 a 和 b 的公式如下:
- B = (n*Σxy - ΣxΣy) / ((n*Σx^2) - (Σx)^2)
- A = (Σy - (B * Σx))/n
因此,我们拥有的 [x,y] 集合越多,我们的模型为未知 x 提供良好答案的可能性就越大。这就是“训练”。
当然,这已经很老了,即使我的 Casio FX 100C 也有一个“LR”模式来输入这些。但是,现在是时候讨论 GPU 核心以及它们在这里的表现了。
GPU 与 CPU
GPU 核心是许多迷你 CPU;也就是说,它们能够执行简单的数学运算,如加法、乘法、三角函数、对数等。如果您查看 HLSL/GLSL 代码,例如这个**ShaderToy 灰度**,您会看到一个简单的算法,包含幂、平方根、点积等,以 1920x1080 的分辨率执行 2073600 次,或对于 30fps 的视频每秒执行 62208000 次。我的 RTX 4060 有 3072 个这样的“CPU”。
因此,让这些核心执行简单但大规模的数学运算,并且比我们的 CPU 快得多,这一点至关重要。
CPU 上的线性回归
当然,在 CPU C++ 代码中找到 y = A+Bx 公式是微不足道的。给定包含 N 个 (x,y) 对元素的两个数组 `xs` 和 `ys`,那么
void LinearRegressionCPU(float* px,float* py,size_t n) { auto beg = GetTickCount64(); float Sx = 0, Sy = 0,Sxy = 0,Sx2 = 0; for (size_t i = 0; i < n; i++) { Sx += px[i]; Sx2 += px[i] * px[i]; Sy += py[i]; Sxy += px[i] * py[i]; } float B = (n * Sxy - Sx * Sy) / ((n * Sx2) - (Sx * Sx)); float A = (Sy - (B * Sx)) / n; auto end = GetTickCount64(); printf("Linear Regression CPU:\r\nSx = %f\r\nSy = %f\r\nSxy = %f\r\nSx2 = %f\r\nA = %f\r\nB = %f\r\n%zi ticks\r\n\r\n", Sx,Sy,Sxy,Sx2,A,B,end - beg); }
现在开始你的地狱。使用 DirectML 在 GPU 上将实现相同的结果,但需要大量额外且真正复杂的代码。机器学习不是很有趣吗?
张量
张量是矩阵的泛化,矩阵是向量的泛化,向量是数字的泛化。也就是说,数字是 x,向量是 [x y z],2x2 矩阵是一个有 2 行 2 列的表,而张量可以有任意数量的维度。
DirectML 可以将张量数据“上传”到我们的 GPU,“执行”一组算子(数学函数),并将结果“返回”到 CPU。
DirectML 和 DirectMLX
DirectML 是一个低级 DirectX12 API 代码,能够对 GPU 上的张量进行操作。请在此处 MSDN 文档开始。我们将逐步介绍代码中的三个操作:复制、加法和线性回归。
**DirectMLX** 是一个仅标头的辅助集合,允许您轻松构建图。还记得 DirectShow 或 Direct2D 过滤器吗?图描述了输入和输出以及它们之间应用的算子。我们将借助 DirectMLX 创建三个图。
DirectML **结构**列表指示了您需要在张量上执行哪些算子。
我从 **HelloDirectML** 开始,并将其扩展以进行实际的线性回归训练。
开始我们的旅程
初始化 DirectX 12。也就是说,枚举 DXGI 适配器,创建 DirectX 12 设备,创建其命令分配器、命令队列和命令列表接口。
HRESULT InitializeDirect3D12() { CComPtr<ID3D12Debug> d3D12Debug; // Throws if the D3D12 debug layer is missing - you must install the Graphics Tools optional feature #if defined (_DEBUG) THROW_IF_FAILED(D3D12GetDebugInterface(IID_PPV_ARGS(&d3D12Debug))); d3D12Debug->EnableDebugLayer(); #endif CComPtr<IDXGIFactory4> dxgiFactory; CreateDXGIFactory1(IID_PPV_ARGS(&dxgiFactory)); CComPtr<IDXGIAdapter> dxgiAdapter; UINT adapterIndex{}; HRESULT hr{}; do { dxgiAdapter = nullptr; dxgiAdapter = 0; THROW_IF_FAILED(dxgiFactory->EnumAdapters(adapterIndex, &dxgiAdapter)); ++adapterIndex; d3D12Device = 0; hr = ::D3D12CreateDevice( dxgiAdapter, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&d3D12Device)); if (hr == DXGI_ERROR_UNSUPPORTED) continue; THROW_IF_FAILED(hr); } while (hr != S_OK); D3D12_COMMAND_QUEUE_DESC commandQueueDesc{}; commandQueueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; commandQueueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; commandQueue = 0; THROW_IF_FAILED(d3D12Device->CreateCommandQueue( &commandQueueDesc, IID_PPV_ARGS(&commandQueue))); commandAllocator = 0; THROW_IF_FAILED(d3D12Device->CreateCommandAllocator( D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator))); commandList = 0; THROW_IF_FAILED(d3D12Device->CreateCommandList( 0, D3D12_COMMAND_LIST_TYPE_DIRECT, commandAllocator, nullptr, IID_PPV_ARGS(&commandList))); return S_OK; }
使用 **DMLCreateDevice** 初始化 DirectML。在调试模式下,您可以使用 DML_CREATE_DEVICE_FLAG_DEBUG。
创建 DirectML 算子图
算子图描述了哪个算子在哪个张量上运行。在我们的代码中,根据定义的方法,我们有三组。
1. “abs”算子,它将 abs() 函数应用于张量的每个元素。
auto CreateCompiledOperatorAbs(std::initializer_list<UINT32> j,UINT64* ts = 0) { dml::Graph graph(dmlDevice); dml::TensorDesc desc = { DML_TENSOR_DATA_TYPE_FLOAT32, j }; dml::Expression input1 = dml::InputTensor(graph, 0, desc); dml::Expression output = dml::Abs(input1); if (ts) *ts = desc.totalTensorSizeInBytes; return graph.Compile(DML_EXECUTION_FLAG_ALLOW_HALF_PRECISION_COMPUTATION, { output }); }
例如,“j”变量为 {2,2},用于创建一个 2x2 的 float32 张量,即 8 个浮点数,32 字节。我们有一个具有此描述的输入张量,并且输出张量是通过执行 dml::Abs() 函数创建的。DirectMLX 简化了这些算子的创建。
此外,我们返回“总输入张量大小”(以字节为单位),以便我们知道稍后缓冲区的大小。最后一行编译图并返回一个 IDMLCompiledOperator。
2. “add”算子现在接受 2 个输入并产生 1 个输出,因此
auto CreateCompiledOperatorAdd(std::initializer_list<UINT32> j, UINT64* ts = 0) { dml::Graph graph(dmlDevice); auto desc1 = dml::TensorDesc(DML_TENSOR_DATA_TYPE_FLOAT32, j); auto input1 = dml::InputTensor(graph, 0, desc1); auto desc2 = dml::TensorDesc(DML_TENSOR_DATA_TYPE_FLOAT32, j); auto input2 = dml::InputTensor(graph, 1, desc2); auto output = dml::Add(input1,input2); if (ts) *ts = desc1.totalTensorSizeInBytes + desc2.totalTensorSizeInBytes; return graph.Compile(DML_EXECUTION_FLAG_ALLOW_HALF_PRECISION_COMPUTATION, { output }); }
我也用 {2,2} 张量调用它,这样我们就有了两个输入张量,所以我们现在需要 64 字节(将在 *ts 中返回)。我们使用 dml::Add 来创建输出。
3. 线性回归算子更复杂。
auto CreateCompiledOperatorLinearRegression(UINT32 N, UINT64* ts = 0) { dml::Graph graph(dmlDevice); auto desc1 = dml::TensorDesc(DML_TENSOR_DATA_TYPE_FLOAT32, { 1,N }); auto desc2 = dml::TensorDesc(DML_TENSOR_DATA_TYPE_FLOAT32, { 1,N }); auto input1 = dml::InputTensor(graph, 0, desc1); auto input2 = dml::InputTensor(graph, 1, desc2); // Create first output tensor, calculate Sx by adding all first row of the tensor and going to the output tensor (in which , we will only take the last element as the sum) auto o1 = dml::CumulativeSummation(input1, 1, DML_AXIS_DIRECTION_INCREASING, false); // Sy, similarily auto o2 = dml::CumulativeSummation(input2, 1, DML_AXIS_DIRECTION_INCREASING, false); // xy, we calculate multiplication auto o3 = dml::Multiply(input1, input2); // Sxy auto o4 = dml::CumulativeSummation(o3, 1, DML_AXIS_DIRECTION_INCREASING, false); // x*x, we calculate multiplication auto o5 = dml::Multiply(input1, input1); // Sx2 auto o6 = dml::CumulativeSummation(o5, 1, DML_AXIS_DIRECTION_INCREASING, false); auto d1 = desc1.totalTensorSizeInBytes; while (d1 % DML_MINIMUM_BUFFER_TENSOR_ALIGNMENT) d1++; auto d2 = desc2.totalTensorSizeInBytes; while (d2 % DML_MINIMUM_BUFFER_TENSOR_ALIGNMENT) d2++; if (ts) *ts = d1 + d2; return graph.Compile(DML_EXECUTION_FLAG_ALLOW_HALF_PRECISION_COMPUTATION, { o1,o2,o3,o4,o5,o6 }); }
我们有 2 个输入张量,一个是包含 x 值的 [1xN] 张量,另一个是包含 y 值的 [1xN] 张量,用于线性回归。
现在我们有 6 个输出张量:
- 一个用于 Σx
- 一个用于 Σy
- 一个用于 xy
- 一个用于 Σxy
- 一个用于 x^2
- 一个用于 Σx^2
对于求和,我们使用 CumulativeSummation 算子,它将张量轴上的所有值求和到一个另一个张量。
此外,我们必须处理对齐问题,因为 DirectML 缓冲区必须对齐到 DML_MINIMUM_BUFFER_TENSOR_ALIGNMENT。
加载我们的数据
张量可以有填充和步幅,但在我们的示例中,张量是紧凑的(无填充,无步幅)。因此,在 Abs 或 Add 的情况下,我们只创建一个包含 4 个浮点数的 inputTensorElementArray 向量。在线性回归的情况下,我们从 5 对 xy 数据加载。
std::vector<float> xs = { 10,15,20,25,30,35 }; std::vector<float> ys = { 1003,1005,1010,1008,1014,1022 }; size_t N = xs.size();
但是,您可以调用 RandomData(),它将用 32MB 的随机浮点数填充这些缓冲区。
创建初始化器
在 DirectML 中,必须调用算子“初始化器”并进行一次配置。
IDMLCompiledOperator* dmlCompiledOperators[] = { dmlCompiledOperator }; THROW_IF_FAILED(dmlDevice->CreateOperatorInitializer( ARRAYSIZE(dmlCompiledOperators), dmlCompiledOperators, IID_PPV_ARGS(&dmlOperatorInitializer)));
绑定初始化器
**绑定**在 DirectML 中只是选择缓冲区的部分分配给张量。例如,如果您有一个 32 字节的输入缓冲区,您可能有 2 个输入张量,一个从 0-15,另一个从 0-16。
在我们的 Abs 示例中,输入张量是 16 字节(4 个浮点数),输出张量也是 16 字节(4 个浮点数)。
在我们的 Add 示例中,输入 32 字节,第一个张量 16 字节,第二个张量 16 字节,输出 16 字节。
在我们的线性回归示例中,如果我们有 5 对 (x,y),我们需要 2 个张量,每个张量包含 5 个浮点数(一个用于 x,一个用于 y),以及 6 个张量,每个张量包含 5 个浮点数,用于保存我们上面讨论的求和结果。使用 DirectML 绑定可以将输入和输出缓冲区映射到张量。
因此,我们首先创建一个堆。
void CreateHeap() { // You need to initialize an operator exactly once before it can be executed, and // the two stages require different numbers of descriptors for binding. For simplicity, // we create a single descriptor heap that's large enough to satisfy them both. initializeBindingProperties = dmlOperatorInitializer->GetBindingProperties(); executeBindingProperties = dmlCompiledOperator->GetBindingProperties(); descriptorCount = std::max( initializeBindingProperties.RequiredDescriptorCount, executeBindingProperties.RequiredDescriptorCount); // Create descriptor heaps. D3D12_DESCRIPTOR_HEAP_DESC descriptorHeapDesc{}; descriptorHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; descriptorHeapDesc.NumDescriptors = descriptorCount; descriptorHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; THROW_IF_FAILED(d3D12Device->CreateDescriptorHeap( &descriptorHeapDesc, IID_PPV_ARGS(&descriptorHeap))); // Set the descriptor heap(s). SetDescriptorHeaps(); }
然后我们在堆上创建一个绑定表。
DML_BINDING_TABLE_DESC dmlBindingTableDesc{}; CComPtr<IDMLBindingTable> dmlBindingTable; void CreateBindingTable() { dmlBindingTableDesc.Dispatchable = dmlOperatorInitializer; dmlBindingTableDesc.CPUDescriptorHandle = descriptorHeap->GetCPUDescriptorHandleForHeapStart(); dmlBindingTableDesc.GPUDescriptorHandle = descriptorHeap->GetGPUDescriptorHandleForHeapStart(); dmlBindingTableDesc.SizeInDescriptors = descriptorCount; THROW_IF_FAILED(dmlDevice->CreateBindingTable( &dmlBindingTableDesc, IID_PPV_ARGS(&dmlBindingTable))); }
有时 DirectML 需要额外的临时或持久内存。我们检查
temporaryResourceSize = std::max(initializeBindingProperties.TemporaryResourceSize, executeBindingProperties.TemporaryResourceSize);
如果此值不为零,则为 DirectML 创建更多临时内存。
auto x1 = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT); auto x2 = CD3DX12_RESOURCE_DESC::Buffer(temporaryResourceSize, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS); THROW_IF_FAILED(d3D12Device->CreateCommittedResource( &x1, D3D12_HEAP_FLAG_NONE, &x2, D3D12_RESOURCE_STATE_COMMON, nullptr, IID_PPV_ARGS(&temporaryBuffer))); RebindTemporary();
对于“持久资源”也发生同样的情况。
persistentResourceSize = std::max(initializeBindingProperties.PersistentResourceSize, executeBindingProperties.PersistentResourceSize);
现在我们需要一个命令记录器。
dmlDevice->CreateCommandRecorder( IID_PPV_ARGS(&dmlCommandRecorder));
将初始化器“录制”到 DirectX 12 命令列表中。
dmlCommandRecorder->RecordDispatch(commandList, dmlOperatorInitializer, dmlBindingTable);
然后我们像之前一样关闭并“执行”列表。
void CloseExecuteResetWait() { THROW_IF_FAILED(commandList->Close()); ID3D12CommandList* commandLists[] = { commandList }; commandQueue->ExecuteCommandLists(ARRAYSIZE(commandLists), commandLists); CComPtr<ID3D12Fence> d3D12Fence; THROW_IF_FAILED(d3D12Device->CreateFence( 0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&d3D12Fence))); auto hfenceEventHandle = ::CreateEvent(nullptr, true, false, nullptr); THROW_IF_FAILED(commandQueue->Signal(d3D12Fence, 1)); THROW_IF_FAILED(d3D12Fence->SetEventOnCompletion(1, hfenceEventHandle)); ::WaitForSingleObjectEx(hfenceEventHandle, INFINITE, FALSE); THROW_IF_FAILED(commandAllocator->Reset()); THROW_IF_FAILED(commandList->Reset(commandAllocator, nullptr)); CloseHandle(hfenceEventHandle); }
此函数完成后,“初始化器”就完成了,无需再次调用。
绑定算子
我们现在用算子而不是初始化器“重置”绑定表。
dmlBindingTableDesc.Dispatchable = dmlCompiledOperator; THROW_IF_FAILED(dmlBindingTable->Reset(&dmlBindingTableDesc));
如果需要,我们将重新绑定临时和持久内存。
ml.RebindTemporary(); ml.RebindPersistent();
绑定输入
我们将只绑定一个输入缓冲区,其累计字节总数(所有输入张量的 'tensorInputSize')。
CComPtr<ID3D12Resource> uploadBuffer; CComPtr<ID3D12Resource> inputBuffer; auto x1 = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD); auto x2 = CD3DX12_RESOURCE_DESC::Buffer(tensorInputSize); THROW_IF_FAILED(ml.d3D12Device->CreateCommittedResource( &x1, D3D12_HEAP_FLAG_NONE, &x2, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&uploadBuffer))); auto x3 = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT); auto x4 = CD3DX12_RESOURCE_DESC::Buffer(tensorInputSize, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS); THROW_IF_FAILED(ml.d3D12Device->CreateCommittedResource( &x3, D3D12_HEAP_FLAG_NONE, &x4, D3D12_RESOURCE_STATE_COPY_DEST, nullptr, IID_PPV_ARGS(&inputBuffer)));
现在将数据上传到 GPU。
D3D12_SUBRESOURCE_DATA tensorSubresourceData{}; tensorSubresourceData.pData = inputTensorElementArray.data(); tensorSubresourceData.RowPitch = static_cast<LONG_PTR>(tensorInputSize); tensorSubresourceData.SlicePitch = tensorSubresourceData.RowPitch; ::UpdateSubresources(ml.commandList,inputBuffer,uploadBuffer,0,0,1,&tensorSubresourceData); auto x9 = CD3DX12_RESOURCE_BARRIER::Transition(inputBuffer,D3D12_RESOURCE_STATE_COPY_DEST,D3D12_RESOURCE_STATE_UNORDERED_ACCESS); ml.commandList->ResourceBarrier( 1,&x9);
有关资源屏障的更多信息,请参阅**此处**。
绑定输入张量
对于我们的“abs”方法,只有一个输入张量需要绑定。
DML_BUFFER_BINDING inputBufferBinding{ inputBuffer, 0, tensorInputSize }; DML_BINDING_DESC inputBindingDesc{ DML_BINDING_TYPE_BUFFER, &inputBufferBinding }; ml.dmlBindingTable->BindInputs(1, &inputBindingDesc);
对于“add”和“linear regression”方法,有两个输入张量。
if (Method == 2 || Method == 3) { // split the input buffer to half to add two tensors DML_BUFFER_BINDING inputBufferBinding[2] = {}; inputBufferBinding[0].Buffer = inputBuffer; inputBufferBinding[0].Offset = 0; inputBufferBinding[0].SizeInBytes = tensorInputSize / 2; inputBufferBinding[1].Buffer = inputBuffer; inputBufferBinding[1].Offset = tensorInputSize /2; inputBufferBinding[1].SizeInBytes = tensorInputSize / 2; DML_BINDING_DESC inputBindingDesc[2] = {}; inputBindingDesc[0].Type = DML_BINDING_TYPE_BUFFER; inputBindingDesc[0].Desc = &inputBufferBinding[0]; inputBindingDesc[1].Type = DML_BINDING_TYPE_BUFFER; inputBindingDesc[1].Desc = &inputBufferBinding[1]; ml.dmlBindingTable->BindInputs(2, inputBindingDesc); }
正如您所看到的,我们将输入缓冲区“分成两半”。
绑定输出张量
对于“Abs”或“Add”,我们只有一个输出,等于输入。
CComPtr<ID3D12Resource> outputBuffer; if (Method == 1) { auto x5 = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT); auto x6 = CD3DX12_RESOURCE_DESC::Buffer(tensorOutputSize, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS); THROW_IF_FAILED(ml.d3D12Device->CreateCommittedResource( &x5, D3D12_HEAP_FLAG_NONE, &x6, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, nullptr, IID_PPV_ARGS(&outputBuffer))); DML_BUFFER_BINDING outputBufferBinding{ outputBuffer, 0, tensorOutputSize }; DML_BINDING_DESC outputBindingDesc{ DML_BINDING_TYPE_BUFFER, &outputBufferBinding }; ml.dmlBindingTable->BindOutputs(1, &outputBindingDesc); }
对于“线性回归”,我们有 6 个输出张量,因此我们将它们分成几部分。我们之前保存了张量大小。
auto x5 = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT); auto x6 = CD3DX12_RESOURCE_DESC::Buffer(tensorOutputSize, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS); THROW_IF_FAILED(ml.d3D12Device->CreateCommittedResource( &x5, D3D12_HEAP_FLAG_NONE, &x6, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, nullptr, IID_PPV_ARGS(&outputBuffer))); DML_BUFFER_BINDING outputBufferBinding[6] = {}; outputBufferBinding[0].Buffer = outputBuffer; outputBufferBinding[0].Offset = 0; outputBufferBinding[0].SizeInBytes = Method3TensorSizes[0]; // Buffer 1 is Sx, we want N floats (in which only the final we are interested in), also aligned to DML_MINIMUM_BUFFER_TENSOR_ALIGNMENT outputBufferBinding[1].Buffer = outputBuffer; outputBufferBinding[1].Offset = Method3TensorSizes[0]; outputBufferBinding[1].SizeInBytes = Method3TensorSizes[1]; // Same for Sy outputBufferBinding[2].Buffer = outputBuffer; outputBufferBinding[2].Offset = Method3TensorSizes[0] + Method3TensorSizes[1]; outputBufferBinding[2].SizeInBytes = Method3TensorSizes[2]; // Same for xy outputBufferBinding[3].Buffer = outputBuffer; outputBufferBinding[3].Offset = Method3TensorSizes[0] + Method3TensorSizes[1] + Method3TensorSizes[2]; outputBufferBinding[3].SizeInBytes = Method3TensorSizes[3]; // Same for Sxy outputBufferBinding[4].Buffer = outputBuffer; outputBufferBinding[4].Offset = Method3TensorSizes[0] + Method3TensorSizes[1] + Method3TensorSizes[2] + Method3TensorSizes[3]; outputBufferBinding[4].SizeInBytes = Method3TensorSizes[4]; // Same for xx outputBufferBinding[5].Buffer = outputBuffer; outputBufferBinding[5].Offset = Method3TensorSizes[0] + Method3TensorSizes[1] + Method3TensorSizes[2] + Method3TensorSizes[3] + Method3TensorSizes[4]; outputBufferBinding[5].SizeInBytes = Method3TensorSizes[5]; // Same for Sxx DML_BINDING_DESC od[6] = {}; od[0].Type = DML_BINDING_TYPE_BUFFER; od[0].Desc = &outputBufferBinding[0]; od[1].Type = DML_BINDING_TYPE_BUFFER; od[1].Desc = &outputBufferBinding[1]; od[2].Type = DML_BINDING_TYPE_BUFFER; od[2].Desc = &outputBufferBinding[2]; od[3].Type = DML_BINDING_TYPE_BUFFER; od[3].Desc = &outputBufferBinding[3]; od[4].Type = DML_BINDING_TYPE_BUFFER; od[4].Desc = &outputBufferBinding[4]; od[5].Type = DML_BINDING_TYPE_BUFFER; od[5].Desc = &outputBufferBinding[5]; ml.dmlBindingTable->BindOutputs(6, od);
准备就绪!
我们像以前一样“录制”,但现在不是初始化器,而是编译后的算子。
dmlCommandRecorder->RecordDispatch(commandList, dmlCompiledOperator, dmlBindingTable);
然后我们使用 CloseExecuteResetWait() 函数关闭并执行命令列表,就像之前一样。
读回结果
我们想从 GPU 读回数据,所以我们会使用 ID3D12Resource map 来获取它。
// The output buffer now contains the result of the identity operator, // so read it back if you want the CPU to access it. CComPtr<ID3D12Resource> readbackBuffer; auto x7 = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_READBACK); auto x8 = CD3DX12_RESOURCE_DESC::Buffer(tensorOutputSize); THROW_IF_FAILED(ml.d3D12Device->CreateCommittedResource( &x7, D3D12_HEAP_FLAG_NONE, &x8, D3D12_RESOURCE_STATE_COPY_DEST, nullptr, IID_PPV_ARGS(&readbackBuffer))); auto x10 = CD3DX12_RESOURCE_BARRIER::Transition(outputBuffer,D3D12_RESOURCE_STATE_UNORDERED_ACCESS,D3D12_RESOURCE_STATE_COPY_SOURCE); ml.commandList->ResourceBarrier(1,&x10); ml.commandList->CopyResource(readbackBuffer, outputBuffer); ml.CloseExecuteResetWait(); D3D12_RANGE tensorBufferRange{ 0, static_cast<SIZE_T>(tensorOutputSize) }; FLOAT* outputBufferData{}; THROW_IF_FAILED(readbackBuffer->Map(0, &tensorBufferRange, reinterpret_cast<void**>(&outputBufferData)));
现在 `outputBufferData` 是指向输出缓冲区的指针。对于我们的线性回归,我们知道从哪里获取数据。
float Sx = 0, Sy = 0, Sxy = 0, Sx2 = 0; if (Method == 3) { // Output 1, char* o = (char*)outputBufferData; Sx = outputBufferData[N - 1]; o += Method3TensorSizes[0]; outputBufferData = (float*)o; Sy = outputBufferData[N - 1]; o += Method3TensorSizes[1]; outputBufferData = (float*)o; o += Method3TensorSizes[2]; outputBufferData = (float*)o; Sxy = outputBufferData[N - 1]; o += Method3TensorSizes[3]; outputBufferData = (float*)o; o += Method3TensorSizes[4]; outputBufferData = (float*)o; Sx2 = outputBufferData[N - 1]; }
我们需要张量 1、2、4 和 6 的最后一个元素(张量 3 和 5 用于中间计算)。
最后
float B = (N * Sxy - Sx * Sy) / ((N * Sx2) - (Sx * Sx)); float A = (Sy - (B * Sx)) / N; // don't forget to unmap! D3D12_RANGE emptyRange{ 0, 0 }; readbackBuffer->Unmap(0, &emptyRange);
现在,如果您以方法 3 运行应用程序
Linear Regression CPU: Sx = 135.000000 Sy = 6062.000000 Sxy = 136695.000000 Sx2 = 3475.000000 A = 994.904785 B = 0.685714 Linear Regression GPU: Sx = 135.000000 Sy = 6062.000000 Sxy = 136695.000000 Sx2 = 3475.000000 A = 994.904785 B = 0.685714
呼!
是的,这很难。是的,这是机器学习。是的,这是人工智能。不要被它迷惑;这很难。您需要大量学习才能使其正常工作。
当然,您还需要决定您自己的 f(x) = y 问题,使用大量变量来“训练”模型,并使用大量算子和张量。
但我希望我已为您敲响了发令枪。
祝你好运。
历史
2024-04-18:第一次尝试。