SYCL学习笔记

基于 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::rangesycl::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内核可以内联到主程序流中,这提高了可读性。

在一个设备上进行计算时,你需要创建一个工作队列:

sycl::queue q(sycl::default_selector{});

默认选择器优先选择GPU(如果有的话)和CPU(如果没有的话)。
使用下面的代码创建与特定设备类型关联的队列:
在这里插入图片描述
hostCPU selectors 可能会导致显著不同的结果,即使它们针对的是相同的硬件。
主机选择器可能使用一个为调试而优化的顺序实现,而CPU选择器则使用OpenCL运行时,并在所有核心上运行。
OpenCL技术的即时编译器可能会生成不同的代码,因为它使用的是完全不同的编译器。
不要仅仅因为主机是CPU,就认为主机和CPU在SYCL中是同一个意思。

使用缓冲区管理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项目

以下是之前在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 2020 USM

虽然前面的程序功能完美,并且可以在各种平台上实现,但一些用户发现它相当冗长。
此外,它不兼容那些需要使用指针管理内存的库和框架。
为了解决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);

SYCL学习笔记_第1张图片如果你正在使用设备分配( 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。

SYCL 2020简洁语法

最后,如果你想知道为什么这些程序中都需要 不透明的处理器 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

你可能感兴趣的:(高性能计算,HPC,学习,intel,sycl,opencl)