CUDA編程實戰:初入江湖

CUDA編程實戰:初入江湖

本文由小肉包老師原創,版權所有,歡迎轉載,本文首發地址 https://jinfagang.github.io 。但請保留這段版權信息,多謝合作,有任何疑問歡迎通過微信聯繫我交流:jintianiloveu

CUDA看來是必備的技能了。因爲在很多地方會需要,比如編寫pytorch自定義層,編寫TensorRT的plugin的時候,都會用到cuda編程。但其實這門技術入門比較難。本篇文章將從一個很微小的角度,來傳授大家這門手藝。

看完這篇教程之後,你應該可以學會如何自己編寫kernel來寫一個nms的操作了。

凡事需要有一個正確的領導,同時需要遵循循序漸進的不變法則,我們列一個提綱,一個任務一個任務來完成。

  • 先學會CUDA的hello world;
  • 學會如何用cudaMalloc開闢內存;
  • 學會如何拷貝數據從cpu到gpu;
  • 學會如何拷貝數據從gpu到cpu;
  • 第一個帶參數的kernel;
  • 開始逆天殺神。

GPU和CPU的數據拷貝

#include <cuda.h>
#include <cuda_runtime.h>
#include <vector>
#include <iostream>
#include <math.h>

int main(void)
{   
  
  float dets[6][4] = {
    {23, 34, 56, 76},
    {11, 23, 45, 45},
    {12, 22, 47, 47},
    {9, 45, 56, 65},
    {20, 37, 55, 75},
  };

  std::cout << sizeof(dets) << std::endl;

  float *dev_dets, *dev_scores;
  cudaMemcpy(dev_dets, dets, sizeof(dets), cudaMemcpyHostToDevice);
  std::cout << "copied data to GPU.\n";

  // get back copied cuda data
  float *host_dets;
  cudaMemcpy(host_dets, dev_dets, sizeof(dets), cudaMemcpyDeviceToHost);
  std::cout << "copied from cuda back to host.\n";
  for (int i=0;i<sizeof(dets)/sizeof(float);i++) {
    std::cout << host_dets[i] << " ";
  }

}
	

上面這個小代碼的作用就是,將dets這個二維的數組,拷貝到GPU,然後我們再從GPU拷貝回CPU,並且查看數據是否正確。

當你打印的時候,你會發現,數據是拷貝過去了,但是拷貝回來的時候數值不對了:

copied data to GPU.
copied from cuda back to host.
194582 -4.09377 4.97457e-07 -1.54942e+26 -403.131 5.08783e+10 -128.165 1.21334e-38 0 -2.18535e-33 131241 -nan -6.19379e-31 1.17302e-38 0.068985 2.8026e-44 -nan 3.04014e-20 1.79366e-43 -2.85228e-39 0 8.70721e-31 2.88443e-41 3.33458e-38 %

這是什麼原因造成的呢?

原因在於,我們在將數據從host拷貝到GPU上的時候,並沒有事先開闢內存空間,這其實也是很多人容易犯的一個錯誤,在實際調用 cudaMemcpy之前,需要先開闢內存:

float dets[6][4] = {
    {23, 34, 56, 76},
    {11, 23, 45, 45},
    {12, 22, 47, 47},
    {9, 45, 56, 65},
    {20, 37, 55, 75},
  };
  float scores[6] = {0.7, 0.6, 0.8, 0.4, 0.2, 0.6};
  float iou_threshold = 0.2;
  // copy data to gpu
  std::cout << sizeof(dets) << std::endl;
  std::cout << sizeof(scores) << std::endl;

  float *dev_dets, *dev_scores;
  cudaError_t err = cudaSuccess;
  err = cudaMalloc((void **)&dev_scores, sizeof(scores));
  err = cudaMalloc((void **)&dev_dets, sizeof(dets));
  if (err != cudaSuccess) {
    printf("cudaMalloc failed!");
    return 1;
  }
  cudaMemcpy(dev_dets, dets, sizeof(dets), cudaMemcpyHostToDevice);
  cudaMemcpy(dev_scores, scores, sizeof(scores), cudaMemcpyHostToDevice);
  std::cout << "copied data to GPU.\n";

  // get back copied cuda data
  float host_dets[sizeof(dets)/sizeof(float)];
  float host_scores[6];
  cudaMemcpy(&host_dets, dev_dets, sizeof(dets), cudaMemcpyDeviceToHost);
  cudaMemcpy(&host_scores, dev_scores, sizeof(scores), cudaMemcpyDeviceToHost);
  std::cout << "copied from cuda back to host.\n";
  std::cout << "host_dets size: " << sizeof(host_dets) << std::endl;
  for (int i=0;i<sizeof(dets)/sizeof(float);i++) {
    std::cout << host_dets[i] << " ";
  }
  std::cout << std::endl;
  for (int i=0;i<sizeof(scores)/sizeof(float);i++) {
    std::cout << static_cast<float>(host_scores[i]) << " ";
  }
  std::cout << std::endl;

  cudaFree(dev_dets);
  cudaFree(dev_scores);

  std::cout << "done.\n";

將上面的代碼,修改成這樣,就沒錯了。上面的代碼,我們演示了兩個東西:

  • 將CPU的數據,拷貝到了GPU;
  • 將GPU的數據又拷貝回了CPU。

大家需要注意的是,如果你直接通過取值符號試圖獲取GPU上指針所指向的具體數值,那麼你會得到一個段錯誤,顯然你沒有辦法直接從GPU地址獲取它的值,除非你將那一部分數據拷貝到CPU上。所以啊,大家經常用cuda的時候,你如果要把一個tensor轉爲numpy,你需要首先 a.cpu().numpy()其實這個操作背後,就是拷貝一份數據到CPU的操作。

嘗試對數據進行操作

接下來我們來嘗試一個更難的操作,操作什麼呢?我們想辦法對上面的scores這個數據進行排序。其實排序很簡單了,但是我們不僅僅要對scores排序,我們還希望拿到排序之前對應的下標。

這裏就需要引入一個叫做 thrust的庫了。這個庫要說名氣也沒啥名氣,但是你可以把它看作是英偉達官方的,CUDA界的 STL。標準模板庫。這裏面提供很多類似於C++的模板操作,那麼爲什麼有了STL還需要這麼一個東西呢?本質原因是,我們運算的時候,所有的數據,大部分實在GPU上運算的,當然Thrust也支持CPU類型的數據運算。此時C++的標準模板庫事肯定用不了的,必須要上thrust了。

上面提出的這個問題操作,解決辦法有很多種,其中一種方法可以這樣:

thrust::device_vector<int> sorted_indices(sizeof(scores)/sizeof(float));
thrust::sequence(sorted_indices.begin(), sorted_indices.end(), 0);
thrust::sort_by_key(thrust::device, dev_scores, dev_scores+sizeof(scores)/sizeof(float), sorted_indices.begin());
printf("sorted done.\n");
cudaMemcpy(&host_scores, dev_scores, sizeof(scores), cudaMemcpyDeviceToHost);
for (int i=0;i<sizeof(scores)/sizeof(float);i++) {
std::cout << static_cast<float>(host_scores[i]) << " ";
}
std::cout << std::endl;
for(auto index: sorted_indices) {
std::cout << index << " ";
}
std::cout << std::endl;

很多人看這個會很頭痛,爲什麼呢?因爲它實在是太底層了,全都是內存級別的操作。但是你理解了這基本的原理,它的運算過程也就很好理解了。這個的計算結果應嘎是:

sorted done.
0.2 0.4 0.6 0.6 0.7 0.8 
4 3 1 5 0 2 
done.

也就是對scores進行排序,然後返回排序之後對應的原數據的indices。

最後扔一個問題:如果我們像從大到小排序而不是默認的從小到大,應該怎麼搞?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章