Libtorch中tensor读写方法对比

  • Post author:
  • Post category:其他


Libtorch中许多API与Pytorch中十分类似,但是在C++环境下tensor数据处理的效率问题十分有必要引起重视。本人在开发对数据处理实时性要求比较苛刻的系统中遇到一些关于tensor的创建与读取的问题,此处做一个总结,供大家共同参考讨论。



1.tensor数据初始化



(1)一维tensor创建

使用Liborch库最初始的需求则是创建tensor。如果对于元素比较多的tensor,推荐使用torch::from_blob的方法从数组中创建元素。如果直接给tensor赋值,则耗时特别长。以下给出一个对比例子:

const int ele_n = 1000000;
//直接给创建好的tensor赋值
clock_t start1 = clock();
torch::Tensor test_tensor = torch::zeros({ ele_n }, options).to(at::kCPU);
for (int m = 0; m < ele_n; m++)
	test_tensor[m] = m;
clock_t finish1 = clock();
cout << "tensor time: " << (double)(finish1 - start1) / CLOCKS_PER_SEC << endl;

//从vector中创建tensor
start1 = clock();
std::vector<int> test_vector;
for (int m = 0; m < ele_n; m++)
	test_vector.push_back(m);
torch::Tensor vector_tensor = torch::from_blob(test_vector.data(), { ele_n });
finish1 = clock();
cout << "vector time: " << (double)(finish1 - start1) / CLOCKS_PER_SEC << endl;

//从数组中创建tensor
start1 = clock();
int *test_array = new int[ele_n];
for (int m = 0; m < ele_n; m++)
	test_array[m] = m;
torch::Tensor array_tensor = torch::from_blob(test_array, { ele_n });
finish1 = clock();
cout << "array time: " << (double)(finish1 - start1) / CLOCKS_PER_SEC << endl;
delete[] test_array;

耗时结果如下:

tensor time: 43.13
vector time: 1.013
array time: 0.004

可以看到直接给tensor赋值耗时是十分恐怖的。vector耗时会比较合理,适用于创建未知长度的数组。如果已知元素个数,推荐先创建一个数组,然后把数组的指针交给tensor。当然如果元素数量少,tensor创建耗时差异是可以忽略的。

如果给GPU上的tensor直接赋值呢?这个耗时会更恐怖。代码如下:

const int ele_n = 1000000;
clock_t start1 = clock();
torch::Tensor test_tensor = torch::zeros({ ele_n }, options).to(at::kCPU);
for (int m = 0; m < ele_n; m++)
	test_tensor[m] = m;
clock_t finish1 = clock();
cout << "tensor CPU time: " << (double)(finish1 - start1) / CLOCKS_PER_SEC << endl;

start1 = clock();
torch::Tensor test_tensor_cuda = torch::zeros({ ele_n }, options).to(at::kCUDA);
for (int m = 0; m < ele_n; m++)
	test_tensor_cuda[m] = m;
finish1 = clock();
cout << "tensor CUDA time: " << (double)(finish1 - start1) / CLOCKS_PER_SEC << endl;

耗时结果如下:

tensor CPU time: 38.812
tensor CUDA time: 190.182

因此不要直接给GPU上的tensor赋值。推荐的方法是先在CPU上从数组中创建一个tensor,再把这个tensor赋值到GPU中。



(2)创建多维度tensor



1)直接在栈上创建数组

一维tensor创建好之后,通过torch.view()实际就可以获得多维度的tensor。如果希望更加直观一些,则需要从多维数组中创建tensor。采用torch::from_blob直接从多维数组指针即可以创建多维tensor。但此处也有几个需要注意的点。

对于维度已知的tensor,直接创建数组然后获得tensor,代码如下:

float stack_array[2][2][2];
for (size_t i = 0; i < 2; i++)
	for (size_t j = 0; j < 2; j++)
		for (size_t k = 0; k < 2; k++)
			*(*(*(stack_array + i) + j) + k) = i + j + k;
torch::Tensor test_array1 = torch::from_blob(stack_array, { 2, 2, 2 }, dtype(torch::kFloat)).to(at::kCPU);

这样就创建好了一个3维tensor。



2)从堆上开辟数组

当tensor维度比较大时,就需要从堆上开辟空间创建数组了,代码如下:

int ele_n = 20000;
float ((*_array_test)[2])[2] = new float[ele_n][2][2];
for (size_t i = 0; i < 2; i++)
	for (size_t j = 0; j < 2; j++)
		for (size_t k = 0; k < 2; k++)
			*(*(*(_array_test + i) + j) + k) = i+j+k;
torch::Tensor test_array1 = torch::from_blob(_array_test, { ele_n, 2, 2 }, dtype(torch::kFloat)).to(at::kCPU);
...
delete[]_array_test;

此时需要注意,释放数组指针后test_array1也无法访问了,因此也选择合适的释放时机。

上述这样的三维数组,只是0维度的指针是在堆上创建的,1维度和2维度实际都是在栈上创建的,如果想在0,1维度都在堆上开辟呢?

很自然想到定义如下的三维数组:

int ele_n0 = 20000;
int ele_n1 = 20000;
float ((*_array_test)[ele_n1])[2] = new float[ele_n][ele_n1][2];

这样来初始化三维数组是不允许的,这样的初始化方法只是表面第0个维度是在堆上,第1和第2个维度都是在栈上创建。在栈上是不可以开辟可变长度的数组(即不可以用变量定义数组的长度)。

如果有多个维度都需要从堆上初始化,数组的初始化方法要改。以下我们以二维数组来说明这个问题。接下来的代码是声明两个维度都在堆上的数组:

	int number = 2;//为了能够输出tensor,此处选择的长度较小

	float **array2D = new float*[number];
	for (int i = 0; i < number; i++)
	{
		int r = 2;
		array2D[i] = new float[r];
		for (int j = 0; j < r; j++)
			array2D[i][j] = i + j;
	}

	torch::Tensor array2D_tensor = torch::from_blob(array2D, { 2, 2 }).to(at::kCPU);
	std::cout << "array2D: " << array2D[1][1] << endl;
	std::cout << "array2D_tensor: " << std::endl << array2D_tensor << std::endl;;

	//delete
	for (int i = 0; i < number; i++)
	{
		delete[] array2D[i];
	}
	delete[] array2D;

输出结果如下:

array2D: 2
array2D_tensor:
-1.2604e+36  2.8446e-43
-1.2604e+36  2.8446e-43
[ CPUFloatType{2,2} ]

可以看到二维数组初始化确实成功了,但创建的tensor却并没有成功。本人在这儿尝试了很多次也没有找到从堆空间二维数组初始化tensor的方法。

如果我们用下述的方法来创建:

	int number = 2;

	float **array2D = new float*[number];
	for (int i = 0; i < number; i++)
	{
		int r = 2;
		float p[2];
		array2D[i] = p;
		for (int j = 0; j < r; j++)
			array2D[i][j] = i + j;
	}
	//这样一种二维数组初始化方法应该与float (*array2D)[2] = new float[number][2];类似
	torch::Tensor array2D_tensor = torch::from_blob(array2D, { 2, 2 }).to(at::kCPU);
	std::cout << "array2D: " << array2D[1][1] << endl;
	std::cout << "array2D_tensor: " << std::endl << array2D_tensor << std::endl;;
	delete[] array2D;

结果与上述类似。此时实际只是第0个维度在堆上创建,第1个维度是在栈上创建。猜测可能第1个维度是自己声明的,torch::from_blob没有办法正确寻址导致。

因此如果有多个维度都需要在堆上创建的数组,是没有办法用来初始化方法的。解决方法只有先创建一个低维度的数组,如上例,先在堆上开辟一维数组,再把tensor展成相应的维度。这种处理方法需要特别注意数组初始化元素的次序:

float *array2D = new float [number*2];
torch::Tensor array2D_tensor = torch::from_blob(array2D, { 2, 2 }).to(at::kCPU);



3)截取元素创建tensor

如果声明了一个3*3的数组,但只想截取前2行创建tensor,创建示例代码如下:

float test_partial[3][3] = { {1, 2, 3}, {4, 5, 6 },{7, 8, 9} };
torch::Tensor partial_tensor = torch::from_blob(test_partial, { 2, 3 },  dtype(torch::kFloat32));
std::cout << "partial_tensor: " << std::endl << partial_tensor <<  std::endl;

结果为:

partial_tensor:
1  2  3
4  5  6

可以看到只截取了前2行创建了tensor。



4)几种创建多维数组的方法比较

接下来我们对比在栈空间,堆空间的裸指针与智能指针创建三维数组的效率,代码如下:

float stack_array[1000][640][3];//栈上申请三维数组
	clock_t start = clock();
	for (size_t i = 0; i < 1000; i++)
		for (size_t j = 0; j < 640; j++)
			for (size_t k = 0; k < 3; k++)
				*(*(*(stack_array + i) + j) + k) = i + j + k;
	clock_t finish = clock();
	std::cout << "stack time: " << (double)(finish - start) / CLOCKS_PER_SEC << std::endl;

	
	float((*_array_test)[640])[3] = new float[1000][640][3];//常规裸指针
	start = clock();
	for (size_t i = 0; i < 1000; i++)
		for (size_t j = 0; j < 640; j++)
			for (size_t k = 0; k < 3; k++)
				*(*(*(_array_test + i) + j) + k) = i + j + k;
	finish = clock();
	std::cout << "New time: " << (double)(finish - start) / CLOCKS_PER_SEC << std::endl;

	start = clock();
	for (size_t i = 0; i < 1000; i++)
		for (size_t j = 0; j < 640; j++)
			for (size_t k = 0; k < 3; k++)
				_array_test[i][j][k] = i + j + k;
	finish = clock();
	std::cout << "New [] time: " << (double)(finish - start) / CLOCKS_PER_SEC << std::endl;
	delete[] _array_test;

	int testa = 640;
	std::shared_ptr<double[][640][3]>pad(new double[1000][640][3]);//shared_ptr智能指针
	start = clock();
	for (size_t i = 0; i < 1000; i++)
		for (size_t j = 0; j < 640; j++)
			for (size_t k = 0; k < 3; k++)
				pad[i][j][k] = i + j + k;
	finish = clock();
	std::cout << "shared_prt time: " << (double)(finish - start) / CLOCKS_PER_SEC << std::endl;

输出结果如下:

stack time: 0.01
New time: 0.006
New [] time: 0.015
shared_prt time: 0.116

可以比较明显看到智能指针虽然用起来比较方便,不需要手动释放,但是耗时相比于裸指针要多了许多。在栈空间和堆空间开辟数组耗时差不多,直接采用指针访问效率稍高。但是在栈空间上内存往往有限,当开辟规模过大的数组时,推荐从堆空间上开辟。



2.tensor数据访问读取

与读取数据类似,从CPU上直接访问tensor的数据非常耗时。官方提供了tensor.accessor<>方法进行数据读取。此外,也可以通过指针的方法访问数据。以下给出三种数据访问的方法代码:

torch::Tensor transform_tensor = torch::zeros({ 102400 },dtype(torch::kFloat)).to(at::kCPU);

clock_t start = clock();//直接从tensor中读取数据
transform_tensor = transform_tensor.view({ 640, 80,  2 });
float temp;
for (int i = 0; i < 640; i++)
{
	for (int j = 0; j < 80; j++)
		for(int k = 0; k< 2; k++)
	{
			temp = transform_tensor[i][j][k].item().toFloat();

	}

}
clock_t finish = clock();
std::cout << "tensor access: " << (double)(finish - start) / CLOCKS_PER_SEC << endl;

start = clock();//从accessor中读取数据
float acc = 0;
auto foo_2 = transform_tensor.accessor<float, 3>();
for (int i = 0; i < 640; i++)
{
	for (int j = 0; j < 80; j++)
	{
		for (int k = 0; k < 2; k++)
			temp = foo_2[i][j][k];

	}
		
}

finish = clock();
std::cout << "accessor access: " << (double)(finish - start) / CLOCKS_PER_SEC << endl;

start = clock();//通过指针读取数据
float* result3 = (float* )(transform_tensor.data_ptr());
//const float* result3 = reinterpret_cast<const float *>(transform_tensor.data_ptr());也可以采用指针强制转换的方法

for (int i = 0; i < 640; i++)
{
	for (int j = 0; j < 80; j++)
	{
		for (int k = 0; k < 2; k++)
		{
			temp = result3[80 * 2 * i + j * 2 + k] ;
		}
	}
}

finish = clock();
std::cout << "Pointer access: " << (double)(finish-start)/CLOCKS_PER_SEC << endl;

程序运行结果如下:

tensor access: 10.25
accessor access: 0.017
Pointer access: 0.001

可以看到直接访问tensor是非常耗时,采用accessor方法能够有较为明显的效率提升。直接采用指针访问tensor效率能够进一步提升。采用accessor方法是最为保险不容易出错的。

实际的使用过程中发现,用指针访问tensor时,tensor在内存空间的存储方式可能与想象的不一样,比较容易出错。如果一定要用指针访问tensor,可以检查一下访问结果是否与采用accessor方法获取到的数据结果一致。

此外官方还提供了从CUDA上访问tensor数据的方法,看了一下参考可能需要研究CUDA编程,此处提供一些参考资料,供大家参考。

1.https://blog.csdn.net/u013271656/article/details/106793636

2.https://blog.csdn.net/weixin_33860722/article/details/90653211

实际上tensor从GPU转移到CPU耗时还是比较少的(相比从CPU转移到GPU),因此如果需要访问GPU上的tensor还是建议先转移到CPU上后再用上述的方法访问。



版权声明:本文为weixin_41496173原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。