วิธีการทางบัญญัติในการตรวจสอบข้อผิดพลาดโดยใช้ CUDA runtime API คืออะไร


258

เมื่อดูถึงคำตอบและความคิดเห็นเกี่ยวกับคำถาม CUDA และในแท็ก CUDA ของ Wikiฉันเห็นว่าบ่อยครั้งจะแนะนำว่าสถานะการส่งคืนการเรียก API ทุกครั้งควรตรวจสอบข้อผิดพลาด เอกสาร API ที่มีฟังก์ชั่นเช่นcudaGetLastError, cudaPeekAtLastErrorและcudaGetErrorStringแต่สิ่งที่เป็นวิธีที่ดีที่สุดที่จะใส่กันเหล่านี้ได้อย่างน่าเชื่อถือจับและรายงานข้อผิดพลาดโดยไม่ต้องมีจำนวนมากรหัสพิเศษ?


13
ตัวอย่าง CUDA ของ NVIDIA ประกอบด้วยส่วนหัว helper_cuda.h ที่มีมาโครเรียกว่าgetLastCudaErrorและcheckCudaErrorsทำสิ่งที่อธิบายไว้ในคำตอบที่ยอมรับได้ ดูตัวอย่างสำหรับการสาธิต เพียงแค่เลือกที่จะติดตั้งตัวอย่างพร้อมกับชุดเครื่องมือและคุณจะได้มัน
chappjc

@ chappjc ฉันไม่คิดว่าคำถามและคำตอบนี้อ้างว่าเป็นของจริงหากนี่คือสิ่งที่คุณหมายถึง แต่ก็มีข้อดีที่จะให้ความรู้แก่ผู้คนโดยใช้การตรวจสอบข้อผิดพลาดของ CUDA
JackOLantern

@ Jackolantern ไม่นั่นไม่ใช่สิ่งที่ฉันหมายถึง คำถาม & คำตอบนี้มีประโยชน์กับฉันมากและแน่นอนว่าจะหาได้ง่ายกว่าส่วนหัวบางส่วนใน SDK ฉันคิดว่ามันมีค่าที่จะชี้ให้เห็นว่านี่เป็นวิธีที่ NVIDIA จัดการกับมันและจะมองหาอะไรเพิ่มเติม ฉันจะทำให้ความคิดเห็นของฉันอ่อนลงถ้าทำได้ :)
chappjc

เครื่องมือการดีบักช่วยให้คุณสามารถ "เข้าใกล้" ซึ่งข้อผิดพลาดที่เริ่มมีการปรับปรุงอย่างมากตั้งแต่ปี 2012 ใน CUDA ฉันไม่ได้ทำงานกับ debuggers ที่ใช้ GUI แต่วิกิแท็ก CUDAระบุถึงบรรทัดคำสั่ง cuda-gdb เครื่องมือนี้เป็นเครื่องมือที่ทรงพลังมากเพราะช่วยให้คุณสามารถก้าวไปสู่การแปรปรวนและเธรดที่เกิดขึ้นจริงบน GPU (ต้องใช้สถาปัตยกรรม 2.0+ ส่วนใหญ่)
opetrenko

@bluefeet: อะไรคือการจัดการกับการแก้ไขที่คุณย้อนกลับ? ดูเหมือนว่าไม่มีอะไรเปลี่ยนแปลงจริง ๆ ใน markdown แต่ได้รับการยอมรับว่าเป็นการแก้ไข ที่ทำงานมีอะไรที่ชั่วร้ายไหม?
talonmies

คำตอบ:


304

น่าจะเป็นวิธีที่ดีที่สุดในการตรวจสอบข้อผิดพลาดในรหัส API ของรันไทม์คือการกำหนดฟังก์ชั่นการจัดการสไตล์และยืนยันแมโคแรปเปอร์ดังนี้:

#define gpuErrchk(ans) { gpuAssert((ans), __FILE__, __LINE__); }
inline void gpuAssert(cudaError_t code, const char *file, int line, bool abort=true)
{
   if (code != cudaSuccess) 
   {
      fprintf(stderr,"GPUassert: %s %s %d\n", cudaGetErrorString(code), file, line);
      if (abort) exit(code);
   }
}

จากนั้นคุณสามารถรวมการเรียก API แต่ละครั้งด้วยgpuErrchkแมโครซึ่งจะประมวลผลสถานะการส่งคืนของการเรียก API ที่มันตัดคำตัวอย่างเช่น:

gpuErrchk( cudaMalloc(&a_d, size*sizeof(int)) );

หากมีข้อผิดพลาดในการโทรข้อความที่เป็นข้อความอธิบายข้อผิดพลาดและไฟล์และบรรทัดในรหัสของคุณที่เกิดข้อผิดพลาดจะถูกปล่อยออกมาstderrและแอปพลิเคชันจะออกจาก คุณสามารถแก้ไขได้gpuAssertเพื่อยกระดับข้อยกเว้นมากกว่าการโทรexit()ในแอปพลิเคชันที่ซับซ้อนยิ่งขึ้นหากจำเป็น

คำถามที่สองเกี่ยวข้องกับการตรวจสอบข้อผิดพลาดในการเรียกใช้เคอร์เนลซึ่งไม่สามารถห่อหุ้มด้วยการเรียกมาโครโดยตรงเช่นการเรียก API รันไทม์มาตรฐาน สำหรับเมล็ดบางอย่างเช่นนี้:

kernel<<<1,1>>>(a);
gpuErrchk( cudaPeekAtLastError() );
gpuErrchk( cudaDeviceSynchronize() );

จะตรวจสอบอาร์กิวเมนต์การเรียกใช้ที่ไม่ถูกต้องก่อนจากนั้นบังคับให้โฮสต์รอจนกระทั่งเคอร์เนลหยุดและตรวจสอบข้อผิดพลาดในการเรียกใช้งาน การซิงโครไนซ์สามารถตัดออกได้ถ้าคุณมีการบล็อกการเรียก API ตามมาดังนี้:

kernel<<<1,1>>>(a_d);
gpuErrchk( cudaPeekAtLastError() );
gpuErrchk( cudaMemcpy(a_h, a_d, size * sizeof(int), cudaMemcpyDeviceToHost) );

ในกรณีที่การcudaMemcpyโทรสามารถกลับข้อผิดพลาดที่เกิดขึ้นระหว่างการดำเนินการเคอร์เนลหรือจากการคัดลอกหน่วยความจำของตัวเอง สิ่งนี้อาจสร้างความสับสนให้กับผู้เริ่มต้นและฉันขอแนะนำให้ใช้การซิงโครไนซ์อย่างชัดเจนหลังจากการเรียกใช้เคอร์เนลระหว่างการดีบักเพื่อให้เข้าใจได้ง่ายขึ้นว่าปัญหาอาจเกิดขึ้นที่ใด

โปรดทราบว่าเมื่อใช้CUDA Dynamic Parallelismวิธีการที่คล้ายกันมากสามารถและควรนำไปใช้กับการใช้งานของ CUDA runtime API ในเคอร์เนลของอุปกรณ์รวมถึงหลังจากที่เคอร์เนลของอุปกรณ์เปิดตัว:

#include <assert.h>
#define cdpErrchk(ans) { cdpAssert((ans), __FILE__, __LINE__); }
__device__ void cdpAssert(cudaError_t code, const char *file, int line, bool abort=true)
{
   if (code != cudaSuccess)
   {
      printf("GPU kernel assert: %s %s %d\n", cudaGetErrorString(code), file, line);
      if (abort) assert(0);
   }
}

8
@harrism: ฉันไม่คิดอย่างนั้น Community Wiki มีไว้สำหรับคำถามหรือคำตอบที่แก้ไขบ่อยๆ นี่ไม่ใช่หนึ่งในนั้น
talonmies

1
เราไม่ควรเพิ่มcudaDeviceReset()ก่อนออกด้วยหรือ และเป็นข้อสำหรับการยกเลิกการจัดสรรหน่วยความจำ?
Aurelius

2
@talonmies: สำหรับการโทรรันไทม์ Async CUDA เช่น cudaMemsetAsync และ cudaMemcpyAsync มันต้องมีการซิงโครไนซ์อุปกรณ์ gpu และเธรดโฮสต์ผ่านการเรียกไปยัง gpuErrchk (cudaDeviceSynchronize ()) หรือไม่
nurabha

2
โปรดทราบว่าการซิงโครไนซ์อย่างชัดเจนหลังจากการเรียกใช้เคอร์เนลไม่ผิด แต่สามารถเปลี่ยนแปลงประสิทธิภาพการดำเนินการอย่างรุนแรงและซีแมนทิกส์ interleaving หากคุณกำลังใช้การสอดประสานการทำการซิงโครไนซ์อย่างชัดเจนสำหรับการดีบักอาจซ่อนคลาสทั้งหมดของข้อบกพร่องซึ่งอาจยากต่อการติดตามในการสร้าง Release
masterxilo

มีวิธีใดที่จะได้รับข้อผิดพลาดเฉพาะเพิ่มเติมสำหรับการประมวลผลเคอร์เนล? ข้อผิดพลาดทั้งหมดที่ฉันได้รับเพียงแค่แจ้งหมายเลขบรรทัดจากรหัสโฮสต์ไม่ใช่จากเคอร์เนล
Azmisov

70

คำตอบของ talonmies ข้างต้นเป็นวิธีที่ดีในการยกเลิกใบสมัครในassertลักษณะแบบ

บางครั้งเราอาจต้องการรายงานและกู้คืนจากเงื่อนไขข้อผิดพลาดในบริบท C ++ ซึ่งเป็นส่วนหนึ่งของแอปพลิเคชันขนาดใหญ่

ต่อไปนี้เป็นวิธีย่อที่สมเหตุสมผลในการทำเช่นนั้นโดยการยกเว้น C ++ ที่ได้มาจากการstd::runtime_errorใช้thrust::system_error:

#include <thrust/system_error.h>
#include <thrust/system/cuda/error.h>
#include <sstream>

void throw_on_cuda_error(cudaError_t code, const char *file, int line)
{
  if(code != cudaSuccess)
  {
    std::stringstream ss;
    ss << file << "(" << line << ")";
    std::string file_and_line;
    ss >> file_and_line;
    throw thrust::system_error(code, thrust::cuda_category(), file_and_line);
  }
}

นี้จะรวมชื่อไฟล์หมายเลขบรรทัดและคำอธิบายภาษาอังกฤษของการcudaError_tเป็น.what()สมาชิกของข้อยกเว้นโยน:

#include <iostream>

int main()
{
  try
  {
    // do something crazy
    throw_on_cuda_error(cudaSetDevice(-1), __FILE__, __LINE__);
  }
  catch(thrust::system_error &e)
  {
    std::cerr << "CUDA error after cudaSetDevice: " << e.what() << std::endl;

    // oops, recover
    cudaSetDevice(0);
  }

  return 0;
}

ผลลัพธ์:

$ nvcc exception.cu -run
CUDA error after cudaSetDevice: exception.cu(23): invalid device ordinal

ไคลเอ็นต์ของsome_functionสามารถแยกแยะข้อผิดพลาด CUDA จากข้อผิดพลาดชนิดอื่น ๆ ได้หากต้องการ:

try
{
  // call some_function which may throw something
  some_function();
}
catch(thrust::system_error &e)
{
  std::cerr << "CUDA error during some_function: " << e.what() << std::endl;
}
catch(std::bad_alloc &e)
{
  std::cerr << "Bad memory allocation during some_function: " << e.what() << std::endl;
}
catch(std::runtime_error &e)
{
  std::cerr << "Runtime error during some_function: " << e.what() << std::endl;
}
catch(...)
{
  std::cerr << "Some other kind of error during some_function" << std::endl;

  // no idea what to do, so just rethrow the exception
  throw;
}

เนื่องจากthrust::system_errorเป็น a std::runtime_errorเราสามารถจัดการกับข้อผิดพลาดประเภทเดียวกันได้หากเราไม่ต้องการความแม่นยำของตัวอย่างก่อนหน้า:

try
{
  // call some_function which may throw something
  some_function();
}
catch(std::runtime_error &e)
{
  std::cerr << "Runtime error during some_function: " << e.what() << std::endl;
}

1
ส่วนหัวของแรงขับดูเหมือนจะถูกจัดเรียงใหม่ คือตอนนี้ได้อย่างมีประสิทธิภาพ<thrust/system/cuda_error.h> <thrust/system/cuda/error.h>
chappjc

เมื่อเร็ว ๆ นี้ฉันคิดว่าไลบรารี่ของฉันจะช่วยแก้ปัญหาที่คุณแนะนำ - ส่วนใหญ่และมีน้ำหนักเบาพอที่จะทดแทนได้ (ดูคำตอบของฉัน)
einpoklum

27

The C ++ - วิธีบัญญัติ: อย่าตรวจสอบข้อผิดพลาด ... ใช้การเชื่อม C ++ ซึ่งทำให้เกิดข้อยกเว้น

ฉันเคยถูกรบกวนจากปัญหานี้ และฉันเคยมีวิธีแก้ปัญหาฟังก์ชั่นมาโคร - ห่อหุ้ม - ฟังก์ชั่นเช่นเดียวกับในคำตอบของ Talonmies และ Jared แต่โดยสุจริต? มันทำให้การใช้ CUDA Runtime API น่าเกลียดและ C-like มากยิ่งขึ้น

ดังนั้นฉันจึงเข้าหาสิ่งนี้ในวิธีที่แตกต่างและเป็นพื้นฐานมากกว่า สำหรับตัวอย่างของผลลัพธ์นี่เป็นส่วนหนึ่งของvectorAddตัวอย่างCUDA - ด้วยการตรวจสอบข้อผิดพลาดที่สมบูรณ์ของการเรียกใช้ API ทุกครั้ง:

// (... prepare host-side buffers here ...)

auto current_device = cuda::device::current::get();
auto d_A = cuda::memory::device::make_unique<float[]>(current_device, numElements);
auto d_B = cuda::memory::device::make_unique<float[]>(current_device, numElements);
auto d_C = cuda::memory::device::make_unique<float[]>(current_device, numElements);

cuda::memory::copy(d_A.get(), h_A.get(), size);
cuda::memory::copy(d_B.get(), h_B.get(), size);

// (... prepare a launch configuration here... )

cuda::launch(vectorAdd, launch_config,
    d_A.get(), d_B.get(), d_C.get(), numElements
);    
cuda::memory::copy(h_C.get(), d_C.get(), size);

// (... verify results here...)

อีกครั้ง - ข้อผิดพลาดที่อาจเกิดขึ้นทั้งหมดจะถูกตรวจสอบและข้อยกเว้นหากเกิดข้อผิดพลาด (caveat: หากเคอร์เนลทำให้เกิดข้อผิดพลาดบางอย่างหลังจากการเปิดตัวมันจะถูกจับหลังจากความพยายามที่จะคัดลอกผลไม่ใช่ก่อนเพื่อให้แน่ใจว่าเคอร์เนล จำเป็นต้องตรวจสอบข้อผิดพลาดระหว่างการเรียกใช้และการคัดลอกด้วยcuda::outstanding_error::ensure_none()คำสั่ง)

รหัสข้างต้นใช้ของฉัน

เครื่องห่อหุ้มแบบ Modern-C ++ สำหรับไลบรารี CUDA Runtime API (Github)

โปรดทราบว่าข้อยกเว้นจะมีทั้งคำอธิบายสตริงและรหัสสถานะ CUDA runtime API หลังจากการเรียกที่ล้มเหลว

เชื่อมโยงไปยังวิธีตรวจสอบข้อผิดพลาดของ CUDA โดยอัตโนมัติด้วยโปรแกรมเสริมเหล่านี้:


10

วิธีการแก้ปัญหาที่กล่าวถึงที่นี่ทำงานได้ดีสำหรับฉัน โซลูชันนี้ใช้ฟังก์ชัน cuda ในตัวและใช้ง่ายมาก

รหัสที่เกี่ยวข้องจะถูกคัดลอกด้านล่าง:

#include <stdio.h>
#include <stdlib.h>

__global__ void foo(int *ptr)
{
  *ptr = 7;
}

int main(void)
{
  foo<<<1,1>>>(0);

  // make the host block until the device is finished with foo
  cudaDeviceSynchronize();

  // check for error
  cudaError_t error = cudaGetLastError();
  if(error != cudaSuccess)
  {
    // print the CUDA error message and exit
    printf("CUDA error: %s\n", cudaGetErrorString(error));
    exit(-1);
  }

  return 0;
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.