Các câu hỏi phỏng vấn CUDA C++ phổ biến và hướng dẫn tối ưu hóa

Câu hỏi 1: Tại sao việc khởi chạy kernel trong CUDA là bất đồng bộ?

  • Khi một kernel được khởi chạy, CPU không đợi GPU hoàn thành.
  • Bất đồng bộ cho phép CPU và GPU hoạt động song song.
  • Để chờ kết quả, cần gọi hàm cudaDeviceSynchronize().
  • Bất đồng bộ là cơ chế cốt lõi của CUDA để thực hiện nhiều tác vụ cùng lúc (như sao chép dữ liệu và tính toán).
kernel<<<grid, block>>>();
printf("CPU tiếp tục ngay lập tức!\n"); // CPU không đợi GPU
cudaDeviceSynchronize();  // Chờ đợi GPU hoàn thành

Câu hỏi 2: Sự khác biệt giữa cudaStreamSynchronize()cudaDeviceSynchronize()?

  • cudaDeviceSynchronize(): Đợi tất cả các tác vụ trong tất cả các luồng hoàn thành.
  • cudaStreamSynchronize(stream): Chỉ đợi các tác vụ trong một luồng cụ thể hoàn thành.
cudaStream_t s1, s2;
cudaStreamCreate(&s1);
cudaStreamCreate(&s2);

kernelA<<<grid, block, 0, s1>>>();
kernelB<<<grid, block, 0, s2>>>();

cudaStreamSynchronize(s1);  // Chỉ đợi A hoàn thành

Câu hỏi 3: Sự khác biệt giữa __syncthreads()__threadfence()?

  • __syncthreads(): Đồng bộ hóa thực thi các thread trong cùng một block.
  • __threadfence(): Đảm bảo tính nhất quán của dữ liệu trong toàn bộ grid.
__global__ void kernel(int *data) {
    int idx = threadIdx.x;
    data[idx] = idx;
    __threadfence();   // Đảm bảo dữ liệu được ghi vào bộ nhớ toàn cục
    __syncthreads();   // Đồng bộ hóa các thread
}

Câu hỏi 4: Cách phân bổ bộ nhớ chia sẻ động trong kernel CUDA?

  • Sử dụng từ khóa extern __shared__.
  • Kích thước bộ nhớ chia sẻ được chỉ định bằng tham số thứ ba khi khởi chạy kernel.
__global__ void dynamicSharedMemory(float *input) {
    extern __shared__ float sharedData[];
    int idx = threadIdx.x;
    sharedData[idx] = input[idx] * 2;
    __syncthreads();
    input[idx] = sharedData[idx];
}

int main() {
    int N = 256;
    float *d_input;
    cudaMalloc(&d_input, N * sizeof(float));
    dynamicSharedMemory<<<1, N, N * sizeof(float)>>>(d_input);
}

Câu hỏi 5: CUDA hỗ trợ kernel mẫu như thế nào?

  • Kernel có thể được định nghĩa dưới dạng mẫu C++.
  • Compiler tạo ra phiên bản cụ thể của kernel khi gọi với toán tử <<>>.
  • Dùng để hỗ trợ nhiều loại dữ liệu khác nhau.
template <typename T>
__global__ void templateKernel(T *a, T *b, T *c, int size) {
    int idx = threadIdx.x;
    c[idx] = a[idx] + b[idx];
}

int main() {
    templateKernel<float><<<1, 256>>>(d_a, d_b, d_c, 256);
    templateKernel<int><<<1, 256>>>(d_ai, d_bi, d_ci, 256);
}

Câu hỏi 6: Tại sao sử dụng cudaDeviceProp? Có thể lấy thông tin gì?

  • Được sử dụng để truy vấn các thuộc tính của thiết bị.
  • Một số thuộc tính quan trọng: name, multiProcessorCount, sharedMemPerBlock, maxThreadsPerBlock, warpSize, l2CacheSize.
  • Giúp tối ưu hóa hiệu năng và lập lịch thuật toán.
cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, 0);
printf("GPU: %s, SMs=%d, Warp=%d\n", prop.name, prop.multiProcessorCount, prop.warpSize);

Câu hỏi 7: Khóa từ __restrict__ có chức năng gì?

  • Thông báo cho trình biên dịch rằng con trỏ này không trùng lặp với các con trỏ khác.
  • Cho phép trình biên dịch tối ưu hóa mạnh mẽ hơn.
  • Thường được sử dụng đối với các tham số kernel quan trọng về hiệu năng.
__global__ void addition(const float *__restrict__ arr1,
                         const float *__restrict__ arr2,
                         float *__restrict__ result, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) result[idx] = arr1[idx] + arr2[idx];
}

Câu hỏi 8: CUDA thực hiện tối ưu hóa khởi chạy chậm như thế nào?

  • CUDA lưu trữ yêu cầu khởi chạy kernel cho đến khi xảy ra các sự kiện như truyền dữ liệu, gọi cudaDeviceSynchronize(), hoặc luồng được đồng bộ hóa.
  • Những kernel nhỏ có thể được tự động xử lý cùng lúc, cải thiện hiệu suất khởi chạy.
kernel1<<<1, 1>>>();
kernel2<<<1, 1>>>();
cudaDeviceSynchronize(); // Hai kernel được gửi cùng lúc tại đây

Câu hỏi 9: Tại sao GPU giỏi trong việc tính toán ma trận?

  • Cấu trúc của GPU bao gồm nhiều đơn vị SIMD cùng băng thông cao.
  • Tốt ở việc tính toán song song (áp dụng cùng một phép tính lên nhiều dữ liệu).
  • Phép nhân ma trận phù hợp với mô hình này.
  • Mỗi block thread xử lý một phần nhỏ của ma trận, tận dụng tính cục bộ dữ liệu.
  • Bộ nhớ chia sẻ được sử dụng để lưu trữ các phần nhỏ, tăng tốc độ truy cập.
__global__ void matrixMultiply(const float *A, const float *B, float *C, int N) {
    int row = blockIdx.y * blockDim.y + threadIdx.y;
    int col = blockIdx.x * blockDim.x + threadIdx.x;
    float sum = 0;
    for (int k = 0; k < N; ++k)
        sum += A[row * N + k] * B[k * N + col];
    C[row * N + col] = sum;
}

Câu hỏi 10: CUDA cấp phát bộ nhớ động trên thiết bị làm thế nào? Rủi ro gì?

  • Từ CUDA 8 trở đi, hỗ trợ cấp phát bộ nhớ động trên thiết bị bằng malloc()/free().
  • Có thể cấp phát bộ nhớ động bên trong kernel.
  • Rủi ro: Chi phí cấp phát cao, dễ gây rạn vỡ bộ nhớ, thường không dùng trong các đường dẫn mã chạy thường xuyên.
__global__ void exampleAllocation() {
    int *ptr = (int*)malloc(sizeof(int));
    if (ptr) {
        *ptr = threadIdx.x;
        free(ptr);
    }
}

Câu hỏi 11: Lưu ý gì khi sử dụng __syncthreads() trong vòng lặp?

  • Tất cả các thread phải thực hiện cùng số lần gọi __syncthreads().
  • Nếu không, sẽ gây ra deadlock (các thread trong warp chờ đợi).
  • Thường đặt __syncthreads() trong khối lệnh không điều kiện.
if (threadIdx.x < 16)
    __syncthreads(); // Sai: Chỉ một số thread thực hiện đồng bộ, gây deadlock

// Đúng:
if (threadIdx.x < 16)
    doSomething();
__syncthreads(); // Tất cả các thread đều thực hiện đồng bộ

Câu hỏi 12: Hàm thiết bị CUDA có thể gọi đệ quy không?

  • Từ CUDA 5.0 (compute capability ≥ 3.5) trở đi, hỗ trợ gọi đệ quy.
  • Tuy nhiên, chi phí hiệu năng cao (không gian ngăn xếp, thanh ghi, tràn).
  • Mặc định, ngăn xếp nhỏ; có thể tăng kích thước ngăn xếp bằng cách sử dụng:
cudaDeviceSetLimit(cudaLimitStackSize, newSize);
__device__ int fibonacci(int n) {
    if (n <= 1) return n;
    return fibonacci(n-1) + fibonacci(n-2);
}

__global__ void exampleKernel() {
    printf("%d\n", fibonacci(5)); // In ra 5
}

Thẻ: CUDA C++ Kernel Sync benchmarking

Đăng vào ngày 15 tháng 6 lúc 21:30