Chương 6: Lập trình GPU (GPU Programming / GPUプログラミング) lần đầu tiên … Hello World bằng OpenCL

Estimated reading: 16 minutes 22 views

Bắt đầu từ nội dung xử lý của Mã máy chủ (Host code / ホスト・コード) đóng vai trò dọn cỗ để thực thi.

_____________________

Từ đây, chúng ta sẽ tiến hành tạo một chương trình GPU hoạt động trên Raspberry Pi với môi trường phát triển (Development environment / 開発環境) đã được xây dựng ở Chương 3 của Phần 2.

Trong bài viết này, chúng tôi sẽ tạo một chương trình Hello World phiên bản OpenCL. Về mặt hoạt động, nó chỉ đơn giản là in ra “Hello, World” trên đầu ra tiêu chuẩn (Standard output / 標準出力), nhưng để nó có thể chạy trên GPU thì cần phải có bước “dọn cỗ” chuẩn bị. (Ban biên tập)

1. Nội dung của Mã máy chủ (Host code / ホスト・コード)

● Vai trò của Mã máy chủ

Mã nguồn (Source code / ソースコード) ở Danh sách 1 là Mã máy chủ thực thi trên CPU chính. Mã máy chủ đảm nhận vai trò thiết lập, chuẩn bị nhằm giúp Mã hạt nhân (Kernel code / カーネル・コード) chạy được trên GPU, cụ thể nó sẽ tiến hành các bước xử lý sau:

・Xử lý ①: Lấy Device ID của OpenCL

・Xử lý ②: Chọn thiết bị (Device / デバイス) sử dụng

・Xử lý ③: Hiển thị thông tin thiết bị OpenCL

・Xử lý ④: Chuẩn bị thực thi thiết bị OpenCL

・Xử lý ⑤: Chuẩn bị thực thi Mã hạt nhân OpenCL

・Xử lý ⑥: Thực thi Hàm hạt nhân (Kernel function / カーネル関数) OpenCL

● Xử lý ①: Lấy thông tin thiết bị OpenCL

Bằng cách gọi các API sau, ta có thể lấy được thông tin thiết bị OpenCL.

・clGetPlatformIDs() … Lấy Platform ID

・clGetDeviceIDs() … Lấy Device ID

Chi tiết được trình bày ở Bảng 1.

Bảng 1: Xử lý ①… Lấy Platform ID và Device ID của OpenCL

Hạng mục (Item / 項目)Nội dung (Content / 内容)
Tóm tắt xử lý (Processing summary / 処理概要)Lấy danh sách thiết bị OpenCL có trong hệ thống và quyết định thiết bị sẽ sử dụng
Mã nguồn (Source code / ソースコード)

// Lấy Platform ID của OpenCL

ret = clGetPlatformIDs(MAX_PLATFORM_NUM, platform_ids, &ret_num_platforms);

printf("clGetPlatformIDs(): num_platforms=%d\n", ret_num_platforms);

// Lấy Device ID của OpenCL

ret = clGetDeviceIDs(platform_ids[0], CL_DEVICE_TYPE_DEFAULT, MAX_GPU_UNIT, device_ids, &ret_num_devices);

printf("clGetDeviceIDs(): num_devices=%d\n", ret_num_devices);

device_id = device_ids[0];

Kết quả thực thi (Execution result / 実行結果)

clGetPlatformIDs(): num_platforms=1

clGetDeviceIDs(): num_devices=1

Giải thích (Explanation / 説明)

・Nền tảng (Platform / プラットフォーム): Chỉ thư viện thời gian chạy (Runtime library / ランタイム・ライブラリ) của OpenCL. Nếu hệ thống cài 2 bộ Runtime library thì sẽ lấy được 2 Platform ID.

・Device ID (デバイスID): ID để nhận diện thiết bị OpenCL. Nếu hệ thống có 2 thẻ GPU của cùng một hãng, sẽ lấy được 2 Device ID.

● Xử lý ②… Chọn thiết bị sử dụng

Trong Danh sách 1, hệ thống được lập trình để đơn giản chọn thiết bị OpenCL đầu tiên trong số các thiết bị có sẵn.

device_id = device_ids[0];

Nếu có nhiều thiết bị OpenCL tồn tại trong hệ thống, bạn sẽ cần sử dụng hàm clGetDeviceInfo() để lấy thông tin như tên hãng, mã model của từng thiết bị và viết mã để chọn thiết bị muốn sử dụng.

● Xử lý ③… Hiển thị thông tin thiết bị OpenCL

Bằng cách gọi API sau, bạn có thể lấy tên hãng hoặc tên thiết bị của từng thiết bị OpenCL.

・clGetDeviceInfo() … Lấy thông tin thiết bị OpenCL

Danh sách 1 (List 1): Mã máy chủ (Host code / ホスト・コード) thực thi trên CPU chính (chap4/opencl-hello/main.c)

				
					#include <stdio.h>
#include <string.h>
#include <assert.h>

#define CL_TARGET_OPENCL_VERSION 300
#include <CL/cl.h>
#include <CL/cl_ext.h>

// Định nghĩa các biến toàn cục trong file
static cl_device_id     device_id;
static cl_context       context = NULL;
static cl_command_queue queue = NULL;
static cl_program       program = NULL;
static cl_kernel        kernel = NULL;

// Khai báo nguyên mẫu (Prototype) của hàm cục bộ trong file
static void set_work_size(size_t *ws, cl_int x, cl_int y);

#define MAKE_STRING(x) #x
#define MAX_PLATFORM_NUM 4
#define MAX_GPU_UNIT     4
#define KERNEL_FUNC      "kernel_hello"
                         // Phải khớp với mã hạt nhân (Kernel code)

// Mã nguồn (Source code) của hàm hạt nhân
const char *kernel_code = MAKE_STRING(
  __kernel void kernel_hello(void)
  {
    printf("[Kernel] Hello, World!\n");
  }
);

int main(int argc, char *argv[])
{
  char  device_vendor[40], device_name[80];
  size_t  size, gws[2], lws[2];
  cl_uint  num;
  cl_platform_id  platform_ids[MAX_PLATFORM_NUM];
  cl_device_id  device_ids[MAX_GPU_UNIT];
  cl_queue_properties queue_properties[] = {
    CL_QUEUE_PROPERTIES, CL_QUEUE_PROFILING_ENABLE,
    0 };

  // ① Lấy OpenCL Device ID ---------------------
  // ・Lấy OpenCL Platform ID
  clGetPlatformIDs(MAX_PLATFORM_NUM, platform_ids, &num);

  // ・Lấy OpenCL Device ID
  clGetDeviceIDs(platform_ids[0], CL_DEVICE_TYPE_DEFAULT, MAX_GPU_UNIT, device_ids, &num);

  // ② Chọn OpenCL Device sẽ sử dụng -----------
  // ・Chọn thiết bị OpenCL đầu tiên
  device_id = device_ids[0];

  // ③ Hiển thị thông tin thiết bị OpenCL -----------------
  clGetDeviceInfo(device_id, CL_DEVICE_VENDOR, sizeof(device_vendor), device_vendor, &size);
  device_vendor[size] = 0;
  printf("[Host] device_vendor=%s\n", device_vendor);
  clGetDeviceInfo(device_ids[0], CL_DEVICE_NAME, sizeof(device_name), device_name, &size);
  device_name[size] = 0;
  printf("[Host] device_name=%s\n", device_name);

  // ④ Chuẩn bị thực thi thiết bị OpenCL -----------------
  // ・Tạo OpenCL Context
  context = clCreateContext(NULL, 1, &device_id, NULL, NULL, NULL);

  // ・Tạo OpenCL Command Queue
  queue = clCreateCommandQueueWithProperties(context, device_id, queue_properties, NULL);

  // ⑤ Build (Biên dịch) chương trình hạt nhân OpenCL --------
  // ・Tạo chương trình hạt nhân từ mã nguồn
  size = strlen(kernel_code);
  program = clCreateProgramWithSource(context, 1, &kernel_code, &size, NULL);

  // ・Build chương trình hạt nhân
  clBuildProgram(program, 1, &device_id, NULL, NULL, NULL);

  // ・Tạo OpenCL Kernel (Hạt nhân)
  kernel = clCreateKernel(program, KERNEL_FUNC, NULL);

  // ⑥ Thực thi OpenCL Kernel ---------------------
  // ・Thực thi Kernel
  set_work_size(gws, 1, 0);
  set_work_size(lws, 1, 0);
  clEnqueueNDRangeKernel(queue, kernel, 1, NULL, gws, lws, 0, NULL, NULL);

  // Đợi cho đến khi toàn bộ task đưa vào Command Queue kết thúc
  clFinish(queue);

  // Giải phóng các tài nguyên (Resource)
  clReleaseKernel(kernel);
  clReleaseProgram(program);
  clReleaseCommandQueue(queue);
  clReleaseContext(context);

  return 0;
}

static void set_work_size(size_t *ws, cl_int x, cl_int y)
{
  ws[0] = x;
  ws[1] = y;
}				
			

Chi tiết được trình bày ở Bảng 2.

Bảng 2: Xử lý ③… Hiển thị thông tin thiết bị OpenCL (tên hãng, tên thiết bị, v.v.)

Hạng mục (Item / 項目)Nội dung (Content / 内容)
Tóm tắt xử lý (Processing summary / 処理概要)Hiển thị thông tin thiết bị OpenCL (Tên hãng, tên thiết bị)
Mã nguồn (Source code / ソースコード)

// Hiển thị thông tin thiết bị OpenCL

clGetDeviceInfo(device_id, CL_DEVICE_VENDOR, sizeof(device_vendor), device_vendor, &size);

device_vendor[ret_size] = 0;

printf("[Host] device_vendor=%s\n", device_vendor);

clGetDeviceInfo(device_ids[0], CL_DEVICE_NAME, sizeof(device_name), device_name, &size);

device_name[ret_size] = 0;

printf("[Host] device_name=%s\n", device_name);

Kết quả thực thi (Execution result / 実行結果)

[Host] device_vendor=Unknown vendor

[Host] device_name=V3D 7.1.7.0

Giải thích (Explanation / 説明)

Nếu có nhiều thiết bị OpenCL tồn tại trong hệ thống, tốt nhất bạn nên thường xuyên kiểm tra xem thiết bị mình đang sử dụng có đúng hay không

 

● Xử lý ④… Chuẩn bị thực thi thiết bị OpenCL

Bằng cách gọi các API sau, bạn có thể lấy thông tin thiết bị OpenCL. (Ghi chú từ BTV: Dòng này dịch sát theo bản gốc nhưng nội dung hàm bên dưới lại là tạo Context và Queue, có thể tạp chí gốc bị lỗi copy-paste từ Xử lý ③ xuống).

・clCreateContext() … Tạo Context (Ngữ cảnh / コンテキスト) OpenCL

・clCreateCommandQueueWithProperties() … Tạo Command Queue (Hàng đợi lệnh / コマンド・キュー) OpenCL

Chi tiết xem ở Bảng 3(a), Bảng 3(b).

Bảng 3: Xử lý ④… Chuẩn bị thực thi thiết bị OpenCL

(a) Tạo OpenCL Context

Hạng mục (Item / 項目)Nội dung (Content / 内容)
Tóm tắt xử lý (Processing summary / 処理概要)Tạo OpenCL Context (Ngữ cảnh / コンテキスト)
Mã nguồn (Source code / ソースコード)

// Tạo OpenCL Context

context = clCreateContext(NULL, 1, &device_id, NULL, NULL, NULL);

Kết quả thực thi (Execution result / 実行結果)–
Giải thích (Explanation / 説明)

・Context (Ngữ cảnh / コンテキスト) giống như con trỏ tệp (FILE *) thu được khi gọi fopen() trong xử lý I/O tệp (File I/O).

・Do đó, việc gọi clCreateContext() mang ý nghĩa thông báo cho Thư viện thời gian chạy (Runtime library / ランタイム・ライブラリ) rằng nó sẽ mở thiết bị OpenCL và từ nay sẽ sử dụng thiết bị đó.

(b) Tạo OpenCL Command Queue (Hàng đợi lệnh / コマンド・キュー)

Hạng mục (Item / 項目)Nội dung (Content / 内容)
Tóm tắt xử lý (Processing summary / 処理概要)Tạo OpenCL Command Queue (Hàng đợi lệnh / コマンド・キュー)
Mã nguồn (Source code / ソースコード)

// Tạo OpenCL Command Queue

queue = clCreateCommandQueueWithProperties(context, device_id, queue_properties, NULL);

Kết quả thực thi (Execution result / 実行結果)–
Giải thích (Explanation / 説明)

・Command Queue là một bộ đệm FIFO (Vào trước ra trước) dùng để lưu trữ các yêu cầu (Yêu cầu thực thi) từ ứng dụng (Application / アプリケーション) gửi tới thiết bị OpenCL.

・Các yêu cầu được ném vào Command Queue sẽ được thực thi theo đúng thứ tự đã đưa vào.

・Bằng cách tạo nhiều Command Queue cho một thiết bị OpenCL, bạn có thể cho chạy đồng thời nhiều yêu cầu (ví dụ như xử lý copy dữ liệu DMA và thực thi Kernel chạy song song).

 

● Xử lý ⑤… Chuẩn bị thực thi Mã hạt nhân (Kernel code / カーネル・コード) OpenCL

Bằng cách gọi các API sau, bạn có thể biên dịch / liên kết (Compile/Link / コンパイル/リンク) Mã hạt nhân OpenCL để tạo ra một chương trình có khả năng thực thi trên GPU (Mã máy trần GPU (GPU native code / GPUネイティブ・コード)). Chi tiết xem ở Bảng 4.

・clCreateProgramWithSource() … Tạo Chương trình hạt nhân (Kernel program / カーネル・プログラム)

・clBuildProgram() … Build (Biên dịch) Chương trình hạt nhân

・clCreateKernel() … Tạo Hạt nhân (Kernel / カーネル) OpenCL

Bảng 4: Xử lý ⑤… Chuẩn bị thực thi Mã hạt nhân OpenCL

Hạng mục (Item / 項目)Nội dung (Content / 内容)
Tóm tắt xử lý (Processing summary / 処理概要)Tiến hành các bước chuẩn bị thực thi Mã hạt nhân
Mã nguồn (Source code / ソースコード)

// Tạo chương trình hạt nhân từ mã nguồn

program = clCreateProgramWithSource(context, 1, &kernel_code, &size, NULL);

// Build (Biên dịch) chương trình hạt nhân

clBuildProgram(program, 1, &device_id, NULL, NULL, NULL);

// Tạo OpenCL Kernel (Hạt nhân)

kernel = clCreateKernel(program, KERNEL_FUNC, NULL);

Kết quả thực thi (Execution result / 実行結果)–
Giải thích (Explanation / 説明)Không rõ cụ thể từng lệnh gọi API thực hiện điều gì bên dưới, nhưng tóm lại khi thực thi 3 bước trên, Mã hạt nhân (Kernel code / カーネル・コード) sẽ được biên dịch và sẵn sàng chạy.

● Xử lý ⑥… Thực thi Hàm hạt nhân (Kernel function / カーネル関数) OpenCL

Bằng cách gọi API dưới đây, bạn có thể cho thực thi Mã hạt nhân đã được tạo ở Xử lý ⑤ trên GPU.

・clEnqueueNDRangeKernel(queue, kernel, 1, NULL, gws, lws, 0, NULL, NULL);

・clFinish(queue);

Chi tiết được trình bày ở Bảng 5.

Bảng 5: Xử lý ⑥… Thực thi Hàm hạt nhân OpenCL

Hạng mục (Item / 項目)Nội dung (Content / 内容)
Tóm tắt xử lý (Processing summary / 処理概要)Thực thi Hàm hạt nhân (Kernel function / カーネル関数)
Mã nguồn (Source code / ソースコード)

// Thiết lập Work Size (Kích thước công việc)

size_t gws[2], lws[2];

gws[0] = 1;

lws[0] = 1;

// Thực thi Kernel

clEnqueueNDRangeKernel(queue, kernel, 1, NULL, gws, lws, 0, NULL, NULL);

clFinish(queue); // Đợi cho đến khi quá trình thực thi Kernel kết thúc

Kết quả thực thi (Execution result / 実行結果)–
Giải thích (Explanation / 説明)

・Khi gọi clEnqueueNDRangeKernel(), yêu cầu thực thi Kernel sẽ được ném vào Command Queue.

・Hàm hạt nhân được thực thi bất đồng bộ (Asynchronous / 非同期) so với lời gọi API. Tức là, ngay tại thời điểm hàm clEnqueueNDRangeKernel() trả về kết quả (Return), việc thực thi Hàm hạt nhân vẫn chưa hoàn tất.

・Bằng cách gọi clFinish(), bạn có thể đợi cho đến khi việc thực thi Kernel hoàn thành.

2. Nội dung của Mã hạt nhân (Kernel code / カーネル・コード)

Đoạn mã nguồn (Source code / ソースコード) của Mã hạt nhân được hiển thị bên dưới.

				
					__kernel void kernel_hello(void)
{
    printf("[Kernel] Hello, World!\n");
}				
			

Hàm hạt nhân này chỉ đơn giản gọi hàm printf() để hiển thị câu thông báo. Tuy nhiên, khi biên dịch Mã hạt nhân, cần phải chuẩn bị mã nguồn của Kernel dưới dạng chuỗi ký tự (String data / 文字列データ) trên bộ nhớ (Memory / メモリ). Do đó, trong file main.c, chúng ta sử dụng Macro (マクロ) MAKE_STRING() để lưu trữ mã nguồn của Kernel vào một mảng const char.
Hàm hạt nhân không thể trả về giá trị bằng lệnh return giống như các hàm trong ngôn ngữ C thông thường. Hơn nữa, có quy định là hàm hạt nhân phải được gắn Định danh (Identifier / 識別子) __kernel. Do đó, hàm hạt nhân chắc chắn sẽ phải có bộ từ khóa bổ nghĩa (Modifier / 修飾子) __kernel void.

3. Build & Thực thi chương trình Hello World

● Build chương trình (Program build / プログラムのビルド)

Nếu bạn chạy tệp Shell script (シェル・スクリプト) được chuẩn bị kèm theo mã nguồn, chương trình Hello World sẽ được Build và tạo ra file thực thi.

Bash

$ chmod 744 mk.sh      <- Cấp quyền thực thi cho Shell script
$ ./mk.sh
gcc -O2 -g -o main main.c -lOpenCL

● Thực thi chương trình (Program execution / プログラムの実行)

Kết quả chạy chương trình Hello World trên Raspberry Pi 5 được thể hiện dưới đây.

Bash

$ ./main
[Host] device_vendor=Unknown vendor
[Host] device_name=V3D 7.1.7.0
[Kernel] Hello, World!

Thông qua việc gọi API clEnqueueNDRangeKernel(), Hàm hạt nhân đã được thực thi trên GPU, và kết quả là câu thông báo “[Kernel] Hello, World!” đã hiển thị thành công.

4. Thêm Xử lý lỗi (Error handling / エラー処理) vào chương trình Hello World

Trong chương trình Hello World chúng ta vừa xem xét, nhằm mục đích tối giản hóa mã nguồn cho dễ hiểu, chúng ta đã ngó lơ hoàn toàn các lỗi sinh ra khi gọi OpenCL API. Tuy nhiên trong các chương trình thực tế, nếu có lỗi phát sinh ở đâu đó, bạn nên thêm phần xử lý lỗi vào để có thể nhận ra ngay lập tức, việc này sẽ giúp quá trình Gỡ lỗi (Debug / デバッグ) trở nên dễ dàng hơn.
Do đó, tôi đã chuẩn bị sẵn một đoạn mã nguồn trong chap4/opencl-hello-error-check, trong đó bổ sung chức năng dừng thực thi chương trình ngay lập tức mỗi khi có lỗi xảy ra hoặc để hiển thị tiến trình của các lời gọi API. Khi chạy chương trình này, một loạt các thông báo sau sẽ hiện ra.

Plaintext

[Host] device_vendor=Unknown vendor
[Host] device_name=V3D 7.1.7.0
[Host] clCreateContext(): done.
[Host] clCreateCommandQueueWithProperties(): done.
[Host] clCreateProgramWithSource(): done.
[Host] clBuildProgram(): done.
[Host] clCreateKernel(): done.
[Host] clEnqueueNDRangeKernel(): done.
[Kernel] Hello, World!
[Host] clFinish(): done.

Chương trình có thể được tải xuống từ trang sau:
https://www.cqpub.co.jp/interface/download/contents2025.htm

Leave a Comment

Chia sẻ:

Chương 6: Lập trình GPU (GPU Programming / GPUプログラミング) lần đầu tiên … Hello World bằng OpenCL

Or copy link

CONTENTS