Kiến trúc lập trình song song CUDA và cơ chế quản lý tài nguyên

Khái niệm Host, Device và Lập trình dị bộ

Trong mô hình lập trình CUDA, hệ thống được chia thành hai thực thể phần cứng tách biệt, mỗi bên sở hữu không gian bộ nhớ riêng:

  • Host (Máy chủ): Bao gồm CPU và bộ nhớ RAM của hệ thống. CPU được tối ưu hóa cho các tác vụ xử lý tuần tự phức tạp. Tuy nhiên, CPU thường trở thành nút thắt cổ chai khi đối mặt với các khối lượng tính toán song song khổng lồ do cấu trúc lõi không được thiết kế cho thông lượng dữ liệu cao.
  • Device (Thiết bị): Bao gồm GPU và bộ nhớ video (VRAM) chuyên dụng. GPU sở hữu hàng ngàn lõi xử lý nhỏ, cực kỳ hiệu quả trong việc thực thi đồng thời các phép toán độc lập.
  • Heterogeneous Parallel Programming (Lập trình song song dị bộ): Đây là phương pháp kết hợp sức mạnh của cả Host và Device. CUDA (Compute Unified Device Architecture) là nền tảng mở rộng dựa trên ngôn ngữ C, cho phép lập trình viên điều phối công việc giữa hai thành phần này.

Thông thường, luồng điều khiển chính của ứng dụng vẫn nằm ở Host. Khi gặp các phân đoạn mã có khả năng song song hóa cao, Host sẽ đẩy dữ liệu và lệnh thực thi xuống Device thông qua bus PCI Express. Do tốc độ truyền tải của PCI Express chậm hơn nhiều so với tốc độ xử lý nội tại của CPU và GPU, việc giảm thiểu trao đổi dữ liệu giữa Host và Device là quy tắc tối quan trọng để tối ưu hiệu năng.

Phân cấp luồng xử lý (Threads Hierarchy)

CUDA tổ chức các luồng thực thi theo một cấu trúc phân tầng chặt chẽ để ánh xạ hiệu quả lên phần cứng GPU:

  • Thread (Luồng): Đơn vị thực thi nhỏ nhất. Mỗi thread chạy một bản sao của hàm Kernel trên một tập dữ liệu riêng biệt.
  • Block (Khối): Tập hợp các thread. Các thread trong cùng một block có thể chia sẻ bộ nhớ và đồng bộ hóa với nhau.
  • Grid (Lưới): Tập hợp các block. Một lần gọi hàm Kernel sẽ tạo ra một Grid duy nhất bao phủ toàn bộ thiết bị.

NVIDIA gọi mô hình này là SIMT (Single Instruction Multiple Thread). Khác với SIMD truyền thống, SIMT cho phép các thread thực thi cùng một chỉ thị nhưng có thể có các nhánh rẽ hướng khác nhau, mặc dù điều này có thể ảnh hưởng đến hiệu suất nếu không được tối ưu (warp divergence).

Kích thước và Thứ tự không gian

Cả Grid và Block đều có thể được tổ chức theo cấu trúc 1 chiều, 2 chiều hoặc 3 chiều bằng cách sử dụng kiểu dữ liệu dim3. Ví dụ, một Grid có thể chứa 6 block sắp xếp 2x3, và mỗi block chứa 12 thread sắp xếp 3x4. Tổng cộng sẽ có 72 luồng thực thi song song đồng thời trên GPU.

Quy trình thực thi một chương trình CUDA

Mọi chương trình CUDA đều bắt đầu từ hàm main() trên CPU. Khi một hàm Kernel được gọi (launch), nó sẽ được thực thi dưới dạng một Grid trên GPU. Một đặc điểm quan trọng là việc gọi Kernel là không đồng bộ (asynchronous): CPU sẽ tiếp tục thực thi các dòng lệnh tiếp theo ngay lập tức mà không đợi GPU hoàn thành, trừ khi có lệnh đồng bộ hóa tường minh.

1. Cấu pháp khởi chạy Kernel

Để chỉ định cấu hình thực thi, chúng ta sử dụng toán tử <<<...>>> giữa tên hàm và danh sách tham số:

// Định nghĩa kích thước Grid và Block
dim3 blocksPerGrid(width, height, depth);
dim3 threadsPerBlock(x, y, z);

// Khởi chạy Kernel
processDataKernel<<<blocksPerGrid, threadsPerBlock>>>(param1, param2);

2. Quản lý bộ nhớ Device

Trước khi GPU có thể xử lý, dữ liệu phải được chuyển từ RAM (Host) sang VRAM (Device). Quy trình gồm các bước:

  • Cấp phát: Sử dụng cudaMalloc để giữ chỗ trên bộ nhớ GPU.
  • Sao chép: Sử dụng cudaMemcpy để đẩy dữ liệu xuống hoặc lấy kết quả về.
  • Giải phóng: Sử dụng cudaFree để giải phóng tài nguyên khi hoàn tất.

Hàm cudaMemcpy yêu cầu hướng truyền tải cụ thể thông qua các hằng số như cudaMemcpyHostToDevice hoặc cudaMemcpyDeviceToHost.

Ví dụ minh họa mã nguồn

Dưới đây là cấu trúc cơ bản của một chương trình CUDA thực hiện xử lý trên GPU:

int main() {
    size_t dataSize = sizeof(float) * 1024;
    float *h_input, *d_buffer;

    // Cấp phát bộ nhớ trên Host
    h_input = (float*)malloc(dataSize);

    // 1. Cấp phát bộ nhớ trên Device
    cudaMalloc((void**)&d_buffer, dataSize);

    // 2. Chuyển dữ liệu từ Host sang Device
    cudaMemcpy(d_buffer, h_input, dataSize, cudaMemcpyHostToDevice);

    // 3. Thiết lập cấu hình thực thi (1 Block, 1024 Threads)
    dim3 gridConfig(1);
    dim3 blockConfig(1024);

    // 4. Gọi Kernel xử lý
    computeAlgorithm<<<gridConfig, blockConfig>>>(d_buffer);

    // 5. Lấy kết quả từ Device về Host
    cudaMemcpy(h_input, d_buffer, dataSize, cudaMemcpyDeviceToHost);

    // 6. Giải phóng tài nguyên
    cudaFree(d_buffer);
    free(h_input);

    return 0;
}

Thẻ: CUDA gpu-computing Parallel-Programming NVIDIA SIMT

Đăng vào ngày 30 tháng 9 lúc 13:15