毕昇编译器异构算子开发基本思想

本文介绍了使用毕昇编译器进行异构算子开发的基本流程,包括定义任务队列、管理Host和Device数据、数据迁移、提交任务以及取回数据。重点讨论了如何找到并操作Host端的数据指针,以及如何在Device端执行核函数进行计算。文章通过一个矩阵更新操作的例子,详细解释了工作群组(work-group)的概念和并行执行的原理。
摘要由CSDN通过智能技术生成

毕昇编译器异构算子开发基本思想

为了更好的阅读体验,请移步笔者博客

队列

首先定义任务队列,任务队列用来管理device上的可执行任务。

queue Q(ascend_selector{});

queue来自命名空间sycl,需要引入头文件#include <sycl/sycl.hpp>

Host数据

想要将host上的数据搬移到device,一个核心思想就是找到host端的数据指针。如果待迁移算子中的数据封装度较高,大体上可以分为两种情况:

  • 指针传递式的封装。
  • 数据结构式的封装。

如果只是指针传递式的封装,即封装过程仅仅是将数据指针一层一层传递过来,则是比较简单的情况,只需取得该指针即可。

如果是数据结构式的封装,即封装过程中使得子类无法读取到数据的指针,则是较为复杂的情况,迁移的工作量可能会比较大。但依然没有脱离最核心的思想——找到host端的数据指针。

这里举一个简单的例子,在host端定义两个数组。

constexpr int M = 32;
constexpr int N = 16;

// 模拟M*M的矩阵
auto* ptrDataA = new int[M * M];
// 模拟N*N的矩阵
auto *ptrDataB = new int[N * N];

ptrDataAptrDataB是host端的数据指针。

Device数据

为了将host端的数据搬移到device端,需要先在device端申请内存,申请的大小根据实际情况决定。这里需要将两个矩阵都搬移至device上,故申请如下大小的内容。

auto *devA = malloc_device<int>(M * M, Q);
auto *devB = malloc_device<int>(N * N, Q);

devAdevB分别为device端的数据指针,这两片空间处于Global Memory中,但此时仅仅是申请了内存,这两片设备内存中并不存在任何数据。接下来就需要将host端的数据正式拷贝到device端。

Q.memcpy(devA, ptrDataA, M * M * sizeof(int));
Q.memcpy(devB, ptrDataB, N * N * sizeof(int));

memcpy()接口定义如下。

void memcpy(void *Dst, const void *Src, size_t Size);
  • Dst为目标地址。
  • Src为源地址。
  • Size为待搬移数据的大小。

提交任务

截止目前,数据已经在device端准备完毕,接下来就可以进行任务的提交。任务以核函数的形式,作为参数传递给launch()接口。

首先来看一下launch()的定义。

template <typename KernelName = detail::auto_name, typename KernelType>
event launch(size_t NumWorkGroups, _KERNELFUNCPARAM(KernelFunc) _CODELOCPARAM(&CodeLoc)) 

接口定义虽然比较复杂,还涉及到一些宏,但我们只需要关注两个参数:

  • NumWorkGroups为work-group的数量。
  • KernelFunc为核函数。

核函数以lambda表达式的方式进行定义。

auto KernelFunc = [=](group<1> group) {
    // TODO
};

接下来进行任务的提交。

Q.launch<class Test>(N, KernelFunc);

该行代码指定了Nwork-group,并传入一个核函数KernelFunc作为device端执行的实际操作。

当然也可以直接将任务提交与核函数定义的代码合并,省去单独定义KernelFunc的步骤。

Q.launch<class Test>(N, [=](group<1> group) {
    // TODO
});

C++中的lambda表达式定义方式如下。

auto func = [capture] (params) mutable throw() -> return-type { func_body };

[capture]:用来捕获一定范围的变量。

  • [ ]不捕获任何变量;
  • [&]引用捕获,捕获外部作用域所有变量,在函数体内当作引用使用;
  • [=]值捕获,捕获外部作用域所有变量,在函数体内创建一个拷贝使用;
  • [=, &a]值捕获外部作用域所有变量,按引用捕获a变量;
  • [a]值捕获外部作用域所有变量,按引用捕获a变量;
  • [this]捕获当前类中的this指针。

(params):参数列表。

mutable:当使用值捕获时,加上mutable关键字就可以对捕获到的值进行修改。

throw():用于函数体抛出异常

return-type:用来显式指定返回类型,当不需要返回值或返回类型明确的情况下,可以将->return-type一同省略

{ func_body }:函数体。

这里以矩阵的update操作为例,该操作给定两个矩阵AB,以及两个参数lefttop,将矩阵A(left, top)元素开始的与矩阵B大小相同的子矩阵替换为矩阵B

Q.launch<class Test>(N, [=](group<1> group) {
    __local int UBBuf[N];
    size_t groupId = group.get_id();
    dmi::memcpy_blocks(UBBuf, &devB[groupId * N], N * sizeof(int) / 32);
    dmi::memcpy_blocks(&devA[top * M + groupId * M + left], UBBuf, N * sizeof(int) / 32);
});

我们来一行一行解析上面的代码。

首先看第一行__local int UBBuf[N],该语句利用空间制导符__local在Unified Buffer中申请了一片大小为N的内存空间。

第二行size_t groupId = group.get_id()利用get_id()接口获取到了当前的work-group的id。

第三行dmi::memcpy_blocks(UBBuf, &devB[groupId * N], N * sizeof(int) / 32),利用命名空间dmi下的memcpy_blocks()接口进行连续数据的拷贝,将之前定义的Global Memory中devB的数据并行的拷贝至Unified Buffer中的UBBuf中。可以观察到拷贝的源地址是利用groupId计算得来的,这一点后面会详细解释。

第四行dmi::memcpy_blocks(&devA[top * M + groupId * M + left], UBBuf, N * sizeof(int) / 32),同样是利用连续数据拷贝的接口,将Unified Buffer中的UBBuf中的数据,拷贝到Global Memory中devA的正确位置,从而实现update操作。拷贝的目的地址同样是利用groupId与参数lefttop计算得来。

work-group的理解

以上面提到的矩阵update()操作为例。

在这里插入图片描述

groupId == 0时,&devA[top * M + groupId * M + left]计算的结果为&devA[top * M + left],即图中groupId=0箭头所指的那一行数据。而groupId == 1时,同理,计算结果为&devA[top * M + M + left],即比groupId == 0时多向前指了一行数据,也即图中groupId=1箭头所指的数据。

理解work-group最重要的一点就是,要意识到所有group都是并行的,是同时执行的。

取回数据

通过核函数使device执行完任务后,最后一步就是要将运算结果从device上取回host,也即从Global Memory中搬移回Host Memory中。

Q.memcpy(ptrDataA, devA, M * M * sizeof(int));

最后可以利用wait()接口对device端的任务进行同步。

Q.wait();

完整示例

#include <iostream>
#include <vnl/vnl_matrix.h>
#include <sycl/sycl.hpp>
#include <bisheng/bisheng.hpp>
using namespace sycl;

constexpr int M = 32;
constexpr int N = 16;
constexpr int left = 4;
constexpr int top = 7;

int main(int argc, char const *argv[])
{
    queue Q(ascend_selector{});

    // host端数据指针
    auto *ptrDataA = new int[M * M];
    auto *ptrDataB = new int[N * N];

    // 初始化数据
    for (int i = 0; i < M * M; i++)
    {
        ptrDataA[i] = 1;
    }
    for (int i = 0; i < N * N; i++)
    {
        ptrDataB[i] = 2;
    }
    
    // 申请device端内存
    auto *devA = malloc_device<int>(M * M, Q);
    auto *devB = malloc_device<int>(N * N, Q);

    // 将host端的数据拷贝至device端,此时数据在Global Memory中
    Q.memcpy(devA, ptrDataA, M * M * sizeof(int));
    Q.memcpy(devB, ptrDataB, N * N * sizeof(int));

    // 提交任务
    // <class Test>是为该任务命名,Test可以根据实际情况修改为合适的名称
    Q.launch<class Test>(N, [=](group<1> group) {
        // 申请Unified Buffer内存
        __local int UBA[N];
        // 获取group id
        size_t groupId=group.get_id();
        // 将矩阵B的数据并行地拷贝进Unified Buffer
        dmi::memcpy_blocks(UBA, &devB[groupId * N], N * sizeof(int) / 32);
        // 将Unified Buffer中的数据并行的拷贝进矩阵A的正确位置
        dmi::memcpy_blocks(&devA[top * M + groupId * M + left], UBA, N * sizeof(int) / 32);
    });

    // 将结果从Global Memory中取回Host Memory
    Q.memcpy(ptrDataA, devA, M * M * sizeof(int));

    // 任务同步
    Q.wait();

    // 输出矩阵A,检查update操作是否正确
    for (int i = 0; i < M * M; i++)
    {
        if (i % M == 0)
            std::cout << std::endl;
        std::cout << ptrDataA[i] << " ";
    }

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GoldPancake

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值