GPGPU与好,坏和……丑陋的Jcuda

在上一篇文章《 GPGPU for Java编程》中,我们展示了如何设置环境以从Java代码中执行CUDA。 但是,上一篇文章仅着重于设置环境,而没有涉及并行性的话题。 在本文中,我们将看到如何利用GPU来做最擅长的事情:并行处理。 通过此示例,我们将采用一些指标,并查看在哪些方面GPU处理比使用CPU更强或更弱……当然,正如标题所暗示的,结尾处有一个丑陋的部分。

将通过设计一个示例来开始我们的GPU并行性探索,该示例与大多数可用GPGPU文档中的示例有所不同,该示例主要由具有较强图形或科学背景的人编写。 这些示例中的大多数都谈论矢量加法或其他一些数学构造。 让我们来研究一个有点类似于业务情况的示例。 因此,让我们首先假设我们有一个产品列表,每个产品都有一个代码标签和一个价格,并且我们想对所有代码为“ abc”的产品施加10%的开销。

我们将首先在C语言中实现示例,以在CPU和GPU处理之间进行一些性能评估。 之后,我们当然会在Java中实现相同的功能,但是我们将避免进行任何测量,因为它们在Java中有些棘手,因为我们必须考虑垃圾收集,及时编译等问题。

//============================================================================
// Name        : StoreDiscountExample.cu
// Author      : Spyros Sakellariou
// Version     : 1.0
// Description : The Good the Bad and the Ugly
//============================================================================

#include <iostream>
#include <sys/time.h>
 
typedef struct {
  char code[3];
 float listPrice;
} product;

void printProducts(long size, product * myProduct){
printf("Price of First item=%f,%s\n",myProduct[0].listPrice,myProduct[0].code);
printf("Price of Second item=%f,%s\n",myProduct[1].listPrice,myProduct[1].code);
printf("Price of Middle item=%f,%s\n",myProduct[(size-1)/2].listPrice,myProduct[(size-1)/2].code);
printf("Price of Almost Last item=%f,%s\n",myProduct[size-2].listPrice,myProduct[size-2].code);
printf("Price of Last item=%f,%s\n",myProduct[size-1].listPrice,myProduct[size-1].code);
}

float calculateMiliseconds (timeval t1,timeval t2) {
        float elapsedTime;
 elapsedTime = (t2.tv_sec - t1.tv_sec) * 1000.0;
  elapsedTime += (t2.tv_usec - t1.tv_usec) / 1000.0;
  return elapsedTime;
}
 
 

__global__ void kernel(long size, product *products)
{
  long kernelid = threadIdx.x + blockIdx.x * blockDim.x;
 while(kernelid < size) {
    if (products[kernelid].code[0]=='a' && products[kernelid].code[1]=='b' && products[kernelid].code[2]=='c')
  products[kernelid].listPrice*=1.10;
    kernelid += blockDim.x * gridDim.x;
  }
}

int main( int argc, char** argv)
{
  timeval t1,t2;
  cudaEvent_t eStart,eStop;
  float elapsedTime;
  long threads = 256;
  long blocks = 1024;
  long size = 9000000;
  char *product1 = "abc";
  char *product2 = "bcd";
  product *myProduct;
  product *dev_Product;

 printf("blocks=%d x threads=%d total threads=%d total number of products=%d\n\n",blocks,threads,threads*blocks,size);

 myProduct=(product*)malloc(sizeof(myProduct)*size);
  cudaMalloc((void**)&dev_Product,sizeof(dev_Product)*size);
 cudaEventCreate(&eStart);
 cudaEventCreate(&eStop);
 
 gettimeofday(&t1, NULL);
  for (long i = 0; i<size; i++){
   if (i%2==0)
    strcpy(myProduct[i].code,product1);
   else
   strcpy(myProduct[i].code,product2);
  myProduct[i].listPrice = i+1;
 }
  gettimeofday(&t2, NULL);
  printf ( "Initialization time %4.2f ms\n", calculateMiliseconds(t1,t2) );
 printProducts(size,myProduct);
  cudaMemcpy(dev_Product,myProduct,sizeof(dev_Product)*size,cudaMemcpyHostToDevice);
  
 cudaEventRecord(eStart,0);
  kernel<<<blocks,threads>>>(size,dev_Product);
  cudaEventRecord(eStop,0);
  cudaEventSynchronize(eStop);
  
  cudaMemcpy(myProduct,dev_Product,sizeof(dev_Product)*size,cudaMemcpyDeviceToHost);
  
  cudaEventElapsedTime(&elapsedTime,eStart,eStop);
  printf ( "\nCuda Kernel Time=%4.2f ms\n", elapsedTime );
  printProducts(size,myProduct);
 
 
  gettimeofday(&t1, NULL);
  long j=0;
   while (j < size){
     if (myProduct[j].code[0]=='a' && myProduct[j].code[1]=='b' && myProduct[j].code[2]=='c')
        myProduct[j].listPrice*=0.5;
     j++;
 
  }
  gettimeofday(&t2, NULL);
 
  printf ( "\nCPU Time=%4.2f ms\n", calculateMiliseconds(t1,t2) );
  printProducts(size,myProduct);
 
  cudaFree(dev_Product);
  free(myProduct);
}

在第11至14行中,定义了一个结构,其中包含我们的产品,该产品带有用于产品代码的字符数组和用于其价格的浮点数。

在第16和24行中,定义了两种实用程序方法,一种打印某些产品(以便我们查看是否完成工作),另一种将原始日期差转换为毫秒。 请注意,使用标准C时钟功能将无法使用,因为其粒度不足以测量毫秒。

第33行是编写我们的内核的位置。 与上一篇文章相比,这看起来有些复杂,因此让我们进一步剖析…

在第35行中,我们定义了kernelid参数。 此参数将保存正在执行的线程的唯一线程ID。 CUDA为每个线程分配一个线程ID和块ID号,该ID仅对自己的维度唯一。 在我们的示例中,我们指示GPU启动256个线程和1024个块,因此实际上GPU将执行262144个线程。 很好! 尽管CUDA在执行过程中为我们提供了threadIdx.xblockIdx.x参数,但是我们需要手动创建唯一的线程ID才能知道我们当前所在的线程。唯一的线程ID需要从0到262143开始,因此,我们可以通过将要执行的每个块的线程数(使用CUDA参数blockDim.x乘以当前块并将其添加到当前线程中来轻松创建它,从而:

唯一线程ID =当前线程ID +当前块ID *每个块的线程数

如果您想继续阅读,您已经意识到,尽管26.2万个线程令人印象深刻,但我们的数据集由900万个项目构成,因此我们的线程一次需要处理多个项目。 为此,我们在第36行中设置了一个循环,以检查我们的线程ID是否不会超出我们的产品数据数组。 循环使用线程id作为其索引,但是我们使用以下公式对其进行递增:

索引增量+ =每块线程数*块总数

因此,每个线程将执行每个循环9百万/ 262千次,这意味着它将处理约34个项目。

其余的内核代码非常简单且易于说明,每当我们找到带有代码“ abc”的乘积时,便将其乘以1.1(我们的10%开销)。 注意,不能在内核内部使用strcpy函数。 如果尝试将出现编译错误。 不太好!

在第45和46行进入主要功能时,我们定义了两个C计时器( t1t2 )和两个CUDA事件计时器( eStarteStop )。 我们需要CUDA计时器,因为内核是异步执行的,并且我们的内核函数会立即返回,而我们的计时器将仅测量完成函数调用所花费的时间。 内核代码立即返回的事实意味着我们允许CPU在GPU代码执行期间执行其他任务。 这也很好!

计时器后面的参数不言自明:我们定义了线程数,块数,产品数组的大小等。myproduct指针将用于CPU处理,而dev_product指针将用于GPU处理。

在第58至61行中,我们为myproduct和dev_product分配了RAM和GPU内存,并且我们还创建了CUDA计时器来帮助我们测量内核执行时间。

在第63至73行中,我们使用代码和价格初始化myproduct ,然后打印CPU完成任务所花费的时间。 我们还从阵列中打印一些样品产品,以确保正确完成作业。

在第74到85行中,我们将产品数组复制到GPU内存,执行内核以指示要执行的线程和块的数量,然后将结果复制回myproduct数组。 我们打印执行内核和一些示例产品所花费的时间,以确保我们再次正确完成了工作。

最后,在第88至99行中,我们让CPU执行与GPU相似的过程,即对GPU增加了开销的所有产品进行50%的折扣。 我们打印执行CPU任务所花费的时间,并打印一些示例产品以确保完成工作。

让我们编译并运行以下代码:

# nvcc StoreDiscountExample.cu -o StoreDiscountExample
# ./StoreDiscountExample
blocks=1024 x threads=256 total threads=262144 total number of products=9000000

Initialization time 105.81 ms
Price of First item=1.000000,abc
Price of Second item=2.000000,bcd
Price of Middle item=4500000.000000,bcd
Price of Almost Last item=8999999.000000,abc
Price of Last item=9000000.000000,bcd

Cuda Kernel Time=1.38 ms
Price of First item=1.100000,abc
Price of Second item=2.000000,bcd
Price of Middle item=4500000.000000,bcd
Price of Almost Last item=9899999.000000,abc
Price of Last item=9000000.000000,bcd

CPU Time=59.58 ms
Price of First item=0.550000,abc
Price of Second item=2.000000,bcd
Price of Middle item=4500000.000000,bcd
Price of Almost Last item=4949999.500000,abc
Price of Last item=9000000.000000,bcd
#

哇! GPU花费了1.38毫秒来完成CPU花费了59.58毫秒(当然,具体数字取决于您的硬件)。 很好!

稍等片刻! 在决定删除所有代码并开始重新编写CUDA中的所有内容之前,有一个陷阱:我们忽略了一些严重的问题,那就是要衡量将900万条记录从RAM复制到GPU内存再返回所需的时间。 这是从第74行到第85行的代码,更改为使用计时器来衡量将产品列表复制到GPU内存以及从GPU内存复制产品列表的时间:

gettimeofday(&t1, NULL);
cudaMemcpy(dev_Product,myProduct,sizeof(dev_Product)*size,cudaMemcpyHostToDevice);

cudaEventRecord(eStart,0);
kernel<<<blocks,threads>>>(size,dev_Product);
cudaEventRecord(eStop,0);
cudaEventSynchronize(eStop);

cudaMemcpy(myProduct,dev_Product,sizeof(dev_Product)*size,cudaMemcpyDeviceToHost);
gettimeofday(&t2, NULL);
printf ( "\nCuda Total Time=%4.2f ms\n", calculateMiliseconds(t1,t2));
cudaEventElapsedTime(&elapsedTime,eStart,eStop);
printf ( "Cuda Kernel Time=%4.2f ms\n", elapsedTime );
printProducts(size,myProduct);

让我们编译并运行以下代码:

# nvcc StoreDiscountExample.cu -o StoreDiscountExample
# ./StoreDiscountExample
blocks=1024 x threads=256 total threads=262144 total number of products=9000000

Initialization time 108.31 ms
Price of First item=1.000000,abc
Price of Second item=2.000000,bcd
Price of Middle item=4500000.000000,bcd
Price of Almost Last item=8999999.000000,abc
Price of Last item=9000000.000000,bcd

Cuda Total Time=55.13 ms
Cuda Kernel Time=1.38 ms
Price of First item=1.100000,abc
Price of Second item=2.000000,bcd
Price of Middle item=4500000.000000,bcd
Price of Almost Last item=9899999.000000,abc
Price of Last item=9000000.000000,bcd

CPU Time=59.03 ms
Price of First item=0.550000,abc
Price of Second item=2.000000,bcd
Price of Middle item=4500000.000000,bcd
Price of Almost Last item=4949999.500000,abc
Price of Last item=9000000.000000,bcd

#

请注意,CUDA总时间为55毫秒,仅比在单个线程中使用CPU快4毫秒。 这是不好的!

因此,尽管GPU在并行任务执行方面非常快,但是当我们在RAM和GPU内存之间来回复制项目时,代价是很大的。 有一些高级技巧,例如可以使用直接内存访问,但道德的道理是在决定使用GPU时必须非常小心。 如果您的算法需要大量数据移动,那么GPGPU可能不是答案。

由于我们已经完成了性能测试,因此让我们看一下如何使用jcuda实现相同的功能。

这是Java部分的代码:

import static jcuda.driver.JCudaDriver.*;
import jcuda.*;
import jcuda.driver.*;
import jcuda.runtime.JCuda;


public class StoreDiscountExample {
 public static void main(String[] args) {
  int threads = 256;
 int blocks = 1024;
 final int size = 9000000;
  byte product1[] = "abc".getBytes();
 byte product2[] = "bcd".getBytes();
  byte productList[] = new byte[size*3];
  float productPrices[] = new float[size];
  long size_array[] = {size};
   
  cuInit(0);
  CUcontext pctx = new CUcontext();
  CUdevice dev = new CUdevice();
  cuDeviceGet(dev, 0);
  cuCtxCreate(pctx, 0, dev);
  CUmodule module = new CUmodule();
  cuModuleLoad(module, "StoreDiscountKernel.ptx");
  CUfunction function = new CUfunction();
  cuModuleGetFunction(function, module, "kernel");
 
  
  int j=0;
  for (int i = 0; i<size; i++){
   j=i*3;
  if (i%2==0) {
    productList[j]=product1[0];
    productList[j+1]=product1[1];
    productList[j+2]=product1[2];
   }
   else {
    productList[j]=product2[0];
    productList[j+1]=product2[1];
    productList[j+2]=product2[2];
   }
     
    productPrices[i] = i+1;
    
   }
   
  printSamples(size, productList, productPrices);
   
  CUdeviceptr size_dev = new CUdeviceptr();
  cuMemAlloc(size_dev, Sizeof.LONG);
  cuMemcpyHtoD(size_dev, Pointer.to(size_array), Sizeof.LONG);
   
  CUdeviceptr productList_dev = new CUdeviceptr();
  cuMemAlloc(productList_dev, Sizeof.BYTE*3*size);
  cuMemcpyHtoD(productList_dev, Pointer.to(productList), Sizeof.BYTE*3*size);
   
  CUdeviceptr productPrice_dev = new CUdeviceptr();
  cuMemAlloc(productPrice_dev, Sizeof.FLOAT*size);
  cuMemcpyHtoD(productPrice_dev, Pointer.to(productPrices), Sizeof.FLOAT*size);  
   
  Pointer kernelParameters = Pointer.to( 
   Pointer.to(size_dev),
   Pointer.to(productList_dev),
   Pointer.to(productPrice_dev)
  );
   
  cuLaunchKernel(function, 
   blocks, 1, 1, 
   threads, 1, 1, 
   0, null, 
   kernelParameters, null);
   
  cuMemcpyDtoH(Pointer.to(productPrices), productPrice_dev, Sizeof.FLOAT*size);
   
  printSamples(size, productList, productPrices);
  
  JCuda.cudaFree(productList_dev);
  JCuda.cudaFree(productPrice_dev);
  JCuda.cudaFree(size_dev);
 }
  

 public static void printSamples(int size, byte[] productList, float[] productPrices) {
   System.out.print(String.copyValueOf(new String(productList).toCharArray(), 0, 3));System.out.println(" "+productPrices[0]);
  System.out.print(String.copyValueOf(new String(productList).toCharArray(), 3, 3));System.out.println(" "+productPrices[1]);
  System.out.print(String.copyValueOf(new String(productList).toCharArray(), 6, 3));System.out.println(" "+productPrices[2]);
  System.out.print(String.copyValueOf(new String(productList).toCharArray(), 9, 3));System.out.println(" "+productPrices[3]);
  System.out.print(String.copyValueOf(new String(productList).toCharArray(), (size-2)*3, 3));System.out.println(" "+productPrices[size-2]);
  System.out.print(String.copyValueOf(new String(productList).toCharArray(), (size-1)*3, 3));System.out.println(" "+productPrices[size-1]);
 } 
}

从第14、15和16行开始,我们看到我们不能再使用产品结构或类。 实际上,在jcuda中,将作为参数传递给内核的所有内容都必须是一维数组。 因此,在第14行中,我们创建了表示为一维数组的字节的二维数组。 产品列表数组大小等于产品数量乘以每个产品代码的字节大小(在我们的情况下只有3个字节)。 我们还创建了另一个数组,以浮动价格存储产品价格,最后,我们的产品列表的大小也需要放入一维数组中。
我想到现在您可能已经猜到了我要说的是:这简直太丑了!

在第29至45行中,我们填充产品列表和产品价格数组,然后在调用内核函数之前,通过创建CUDA设备指针,分配内存并将数据复制到GPU内存,将它们传递给内核进行处理。

由于我们必须将所有内容都转换为基元的一维数组,因此我们的内核代码也需要进行一些更改:

extern "C"

__global__ void kernel(long *size, char *productCodes, float *productPrices)
{
 long kernelid = threadIdx.x + blockIdx.x * blockDim.x;
 long charIndex = kernelid*3;
 while(kernelid < size[0]) {
    if (productCodes[charIndex]=='a' && productCodes[charIndex+1]=='b' && productCodes[charIndex+2]=='c')
      productPrices[kernelid]*=1.10;
         kernelid += blockDim.x * gridDim.x;
         charIndex = kernelid*3;
        }
}

唯一的区别是,我们将kernelid索引乘以3,以便在productCodes数组中找到正确的起始字符。

让我们编译并运行Java示例:

# nvcc -ptx StoreDiscountKernel.cu -o StoreDiscountKernel.ptx
# javac -cp ~/GPGPU/jcuda/JCuda-All-0.4.0-beta1-bin-linux-x86_64/jcuda-0.4.0-beta1.jar StoreDiscountExample.java
#java -cp ~/GPGPU/jcuda/JCuda-All-0.4.0-beta1-bin-linux-x86_64/jcuda-0.4.0-beta1.jar StoreDiscountExample
abc 1.0                                                                                                                                                                                
bcd 2.0                                                                                                                                                                                
abc 3.0                                                                                                                                                                                
bcd 4.0                                                                                                                                                                                
abc 8999999.0
bcd 9000000.0
abc 1.1
bcd 2.0
abc 3.3
bcd 4.0
abc 9899999.0
bcd 9000000.0
#

尽管代码很丑陋,但其工作方式与C语言类似。

因此,这里总结了我们在GPU处理和jcuda方面的经验:

良好 :非常快的性能(我的硬件是AMD Phenom II四核3.4Ghz CPU和具有336核的NVIDIA Geforce GTX 560)

良好 :异步操作让CPU执行其他任务

:内存副本会带来可观的性能损失

丑陋 :如果您想从Java内部执行CUDA内核,Jcuda无疑是有用的,但是必须将所有内容都转换为基元的一维数组实在不方便。

在我们之前的文章中,有一些关于OpenCL(GPGPU的CUDA替代品)的Java工具的非常有趣的评论。 在下一篇文章中,我们将研究这些工具,看看它们是否比Jcuda更“漂亮”。

参考: GPGPU与我们的W4G合作伙伴 Spyros Sakellariou的 Jcuda the Good,Bad和…Ugly 一起使用

相关文章 :


翻译自: https://www.javacodegeeks.com/2011/10/gpgpu-with-jcuda-good-bad-and-ugly.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值