65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2024年4月18日

CPOL

9分钟阅读

viewsIcon

39943

机器学习介绍,包含训练线性回归模型的 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,使得该线“接近”所有这些点,如下图所示。

 

What is Linear Regression?- Spiceworks - Spiceworks

另一个维基百科示例

undefined

 

给定 [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:第一次尝试。

© . All rights reserved.