基于 DPC++
SYCL源自OpenCL技术,且运行模型类似。
编写风格属于 现代C++
OpenMP 5.0提供了许多与SYCL和dpc++相同的特性
// konw *Z *X *Y A length
// 单精度
for(int i; i<length; i++){
Z[i] = A * X[i] + Y[i];
}
//SYCL
h.parallel_for<class saxpy> (sycl::range<1>{length}, [=](sycl::id<1> it){
const int i = it[0];
Z[i] += A * X[i] + Y[i];
}
Parallel_for是一个并行for循环
循环体表示为lambda。lambda是类似**[…]{…}**的代码。
循环迭代器由sycl::range和sycl::id表示。在我们的简单示例中,两者都是一维的,如<1>所示。SYCL的范围和id可以是一、二或三维的。(OpenCL技术和CUDA*有相同的局限性。)
对于parallel_for 的 < class saxpy > 模板参数。这只是命名内核的一种方法,对于使用不同于SYCL设备编译器的宿主c++编译器使用SYCL是必要的。在这种情况下,两个编译器需要一种方法来商定内核名称。在许多SYCL编译器中,例如Intel®oneAPI dpc++编译器,这是不必要的。
选项-fsycl-unnamed-lambda指示编译器不查找名称。
在h.parallel_for中使用h的目的将在本文后面介绍。
SYCL编程模型支持异构运行。
SYCL内核可以内联到主程序流中,这提高了可读性。
在一个设备上进行计算时,你需要创建一个工作队列:
sycl::queue q(sycl::default_selector{});
默认选择器优先选择GPU(如果有的话)和CPU(如果没有的话)。
使用下面的代码创建与特定设备类型关联的队列:
host 和 CPU selectors 可能会导致显著不同的结果,即使它们针对的是相同的硬件。
主机选择器可能使用一个为调试而优化的顺序实现,而CPU选择器则使用OpenCL运行时,并在所有核心上运行。
OpenCL技术的即时编译器可能会生成不同的代码,因为它使用的是完全不同的编译器。
不要仅仅因为主机是CPU,就认为主机和CPU在SYCL中是同一个意思。
在SYCL中管理数据的规范方法是使用缓冲区 buffers。
SYCL缓冲区是一个不透明的容器。这是一个优雅的设计,但一些应用程序需要指针,这是由统一共享内存(USM)扩展提供的,稍后讨论。
// T is a data type, e.g., float
std::vector<T> h_X(length,xval);
sycl::buffer<T,1> d_X { h_X.data(), sycl::range<1>(h_X.size()) }
在主机上分配了一个c++容器,然后将其移交给SYCL。
在调用SYCL缓冲区的析构函数之前,您只能通过SYCL机制访问数据。
SYCL访问器是使用缓冲区进行SYCL数据管理的重要方面(后面会解释)。
因为设备代码可能需要主机不同的编译器或代码生成机制,所以有必要清楚地识别设备代码的各个部分。
下图显示了在SYCL 1.2.1中是怎样的。
我们使用submit方法将任务排队到设备队列q。
该方法返回一个不透明的处理程序,在本例中是通过parallel_for。
q.submit([&](sycl::handler& h) {
...
h.parallel_for<class nstream> (sycl::range<1>{length}, [=] (sycl::id<1> i) {
...
});
});
q.wait();
使用wait()方法同步设备的执行。
有更精细的同步设备运行的方法,但我们从最简单的开始
有些人可能会觉得前面的代码冗长,尤其是与Kokkos这样的模型相比。Intel oneAPI dpc++编译器支持简洁的语法,这将在下一节中介绍。
SYCL访问器是这个SYCL程序的最后一部分。GPU程序员可能不熟悉访问器,但与其他方法相比,它们有一些很好的特性。
SYCL允许程序员显式地使用copy()方法移动数据。
但是,访问器方法不需要这个方法,因为它们生成了一个数据流图,编译器和运行时可以使用它在正确的时间移动数据。
当按顺序调用多个内核时,这是有效的。在这种情况下,SYCL实现推断数据被重用,并且没有必要将其复制回主机。
此外,还可以异步调度数据移动(在运行的设备重叠的情况下)。
虽然专业的GPU程序员可以手动完成此操作,但SYCL访问器通常比OpenCL程序(程序员必须显式移动数据)的性能更好。
q.submit([&](sycl::handler& h){
auto X = d_X.template get_access<sycl::access::mode::read>(h);
auto Y = d_Y.template get_access<sycl::access::mode::read>(h);
auto Z = d_Z.template get_access<sycl::access::mode::read_write>(h);
h.parallel_for<class nstream> (sycl::range<1>{length}, [=] (sycl::id<1> i ){
...
});
});
因为假定指针是内存的句柄的编程模型很难使用SYCL访问器,USM扩展使访问器变得不必要。USM在数据移动和同步方面给程序员带来了更大的负担,但在希望使用指针的遗留代码中有助于兼容性。
以下是之前在SYCL中描述的SAXPY程序的所有组件:
std::vector<float> h_X(length,xval);
std::vector<float> h_Y(length,yval):
std::vector<float> h_Z(length,zval);
try{
sycl::queue q(sycl::default_selector{});
const float A(aval);
sycl::buffer<float,1> d_X { h_X.data(), sycl::range<1>(h_X.size()) };
sycl::buffer<float,1> d_Y { h_Y.data(), sycl::range<1>(h_Y.size()) };
sycl::buffer<float,1> d_Z { h_Z.data(), sycl::range<1>(h_Z.size()) };
q.submit([&](sycl::handler& h){
auto X = d_X.template get_access<sycl::access::mode::read>(h);
auto Y = d_Y.template get_access<sycl::access::mode::read>(h);
auto Z = d_Z.template get_access<sycl::access::mode::read_write>(h);
h.parallel_for<class nstream>(sycl::range<1>{length}, [=] (sycl::id<1> it){
const int i = it[0];
Z[i] += A * X[i] + Y[i];
});
});
q.wait;
}
catch (sycl::exception & e){
std::cout << e.what() << std::endl;
return 1;
}
虽然前面的程序功能完美,并且可以在各种平台上实现,但一些用户发现它相当冗长。
此外,它不兼容那些需要使用指针管理内存的库和框架。
为了解决SYCL 1.2.1中的这个问题,Intel在dpc++中开发了一个名为USM的扩展,该扩展支持基于指针的内存管理。
USM支持两种重要的使用模型,如下图所示。第一个支持在主机和设备之间自动移动数据。第二种是用于显式地在设备分配中来回移动数据。
详细信息在SYCL 2020临时规范中,但要开始,您需要知道的一切都在下面的图像中。参数q是与设备相关的队列,分配的数据存储在那里(永久或临时):
// shared
auto d_X = sycl::malloc_shared<float>(length, q);
// device
auto d_X = sycl::malloc_device<float>(length,q);
// deallocation
sycl::free(d_X, q);
如果你正在使用设备分配( device allocation ),数据必须显式移动(例如,使用SYCL memcpy方法),它的行为与std::memcpy相同(目标在左边):
const size_t bytes = length * sizeof(flozt);
// d_Z <- h_Z
q.memcpy(d_Z, h_Z.data(), bytes);
q.wait();
// h_Z <- d_Z
q.memcpy(h_Z,data(), d_Z, bytes);
q.wait();
访问器不再需要在USM,这意味着你可以简化以前的内核代码:
q.submit([&](sycl::handler& h){
h.parallel_for<class saxpy>(sycl::range<1>{length}, ::id<1> i)
d_Z[i] += A * d_X[i] + d_Y[i];
});
});
可以在名为saxpy-usm的存储库中找到这两个版本USM的完整工作示例。cc和saxpy-usm2.cc。
最后,如果你想知道为什么这些程序中都需要 不透明的处理器 h,那么事实证明它根本不需要。
以下是在SYCL 2020临时规范中添加的等效实现
另外,您还可以利用SYCL 2020临时规范中: lambda名称是可选的这一点。
这两个小的改变使SYCL内核的长度与本教程开头列出的原始c++循环相同:
q.parralel_for(sycl::range<1>{length}, [=](sycl::id<1> i){
d_Z[i] += A * d_X[i] + d_Y[i];
});
从在CPU上顺序运行的三行代码开始,到在CPU、gpu、fpga和其他设备上并行运行的三行代码结束。
并不是所有事情都像SAXPY那样简单,但是现在您知道SYCL不会让简单的事情变得困难。
它建立在各种现代c++特性和通用概念(如“并行”)的基础上,而不是引入新的东西来学习。
from intel Program SYCL with the Data Parallel C++ compiler