Lập trình song song dữ liệu trên GPU OpenCL
Trang 1ĐẠI HỌC QUỐC GIA TP HỒ CHÍ MINH TRƯỜNG ĐẠI HỌC KHOA HỌC TỰ NHIÊN
Trang 2Mục lục
I Giới thiệu về OpenCL 4
1 Tổng quan 4
2 Lịch sử hình thành 5
3 Đặc điểm 5
3.1 OpenCL, một chuẩn lập trình mở 5
3.2 Tận dụng tối đa các tài nguyên có thể có của máy tính 5
4 Ngôn ngữ 6
5 Hạ tầng (Platform) 6
6 Tầm vực của OpenCL 7
II Kiến trúc OpenCL trên nền hệ điều hành Mac OS X 8
1 Sơ lược về Mac OS X 8
2 Framework & Runtime 8
3 Compiler 9
4 Operation Model 9
4.1 Platform Model 9
4.2 Execution Model 10
4.3 Memory Model 11
4.4 Programming Model 12
III Workflow phát triển chương trình OpenCL 13
1 Các bước viết một chương trình OpenCL 13
1.1 Xác định những nhiệm vụ nào có thể thực hiện song song 13
1.2 Viết các kernel và các hàm bổ trợ 13
1.3 Setup context 13
1.4 Viết mã lệnh để biên dịch và build chương trình OpenCL 13
1.5 Khởi tạo các đối tượng memory object 14 1.6 Lập hàng đợi lệnh có thứ tự (enqueue command) để điều khiển việc thực thi liên tục và đồng bộ các kernel, đọc và ghi dữ liệu, và thao tác trên các memory object 14
Trang 31.7 Đọc giá trị trả về 14
2 Viết Kernel 14
3 Truy vấn thiết bị 16
4 Khởi tạo OpenCL Context 16
5 Khởi tạo Program Object 17
6 Build Program Executable 19
7 Khởi tạo Kernel Object 22
8 Khởi tạo Memory Object 22
9 Thực thi các kernel 22
9.1 Xác định số chiều của dữ liệu: 23
9.2 Xác định số lượng work-item 23
9.3 Chọn kích thước cho work-group 23
9.4 Enqueue Kernel Execution 25
10 Nhận kết quả trả về 28
10.1 Chờ cho đến khi các kernel hoàn tất thực thi 28
10.2 Đọc kết quả 29
11 Giải phóng bộ nhớ 29
12 Debug chương trình OpenCL 29
IV Performance 31
1 GPGPU Performance 31
1.1 Số thực 31
1.2 Bandwidth 32
1.3 Nhận xét 32
2 CPU Performance 33
2.1 Số học 33
2.2 Bandwidth 34
2.3 Nhận xét 34
IV Tài liệu tham khảo 35
Trang 4I Giới thiệu về OpenCL
1 Tổng quan
OpenCL (Open Computing Language) là chuẩn mở (công bố vào 12/2008) hỗ trợ lập trình song song trên các thiết bị (bao gồm cả GPU), được đề xuất bởi Apple và nhượng lại quyền phát triển cho Khronos Group Dù chỉ mới ra đời nhưng OpenCL lại nhận được rất nhiều sự hỗ trợ từ các nhà sản xuất phần cứng
Danh sách các nhà sản xuất phần cứng ủng hộ OpenCL
Trang 52 Lịch sử hình thành
OpenCL ban đầu được đề xuất và phát triển bởi Apple sau này được phát triển thêm với
sự hợp tác của AMD, IBM, Intel,và nVidia Sau đó Apple nhượng lại quyền phát triển cho Khronos Group (tổ chức đang nắm giữ các chuẩn mở khác như OpenGL, OpenAL…)
• 16/06/2008: Nhóm Khronos Compute Working được thành lập với các đại diện
đến từ các công ty CPU, GPU, thiết bị nhúng và các vi xử lý khác
• 18/11/2008: đưa ra đặc tả kĩ thuật OpenCL 1.0
• 08/12/2008: bản OpenCL 1.0 chính thức được phát hành
• 20/04/2009: nVidia ra mắt OpenCL driver và SDK để phát triển trong chương
trình OpenCL Early Access
• 05/08/2009: AMD giới thiệu công cụ phát triển đầu tiên cho nền tảng OpenCL như là một phần của chương trình ATI Stream SDK v2.0 Beta
sự ủng hộ của rất nhiều các nhà sản xuất
OpenCL được phát triển theo xu hướng tận dụng được tất cả các thiết bị tính toán
có thể thực thi song song Điều đó có nghĩa là nếu ta có một CPU đa nhân với OpenCL thì có thể lập trình để thực thi các tác vụ song song trên CPU đó Hơn nữa, OpenCL hỗ trợ lập trình song song tác vụ (task-parallel programming) và cả lập trình song song dữ liệu (data-parallel programming)
Trang 64 Ngôn ngữ
OpenCL sử dụng ngôn ngữ OpenCL-C dựa trên chuẩn C99 cho lập trình kernel và
IEEE-754 (chuẩn dấu chấm động cho số học) vì thế nên cú pháp hoàn toàn giống với C/C++
5 Hạ tầng (Platform)
Do được phát triển theo hướng độc lập với hạ tầng phần cứng nên OpenCL tự xây dựng một lớp phần cứng trừu tượng cho bản thân mình và độc lập hoàn toàn với hạ tầng phần cứng của thiết bị
Mô hình hạ tầng:
• Một host bao gồm nhiều Compute Device (Core – CPU / SM – GPU / …)
• Một Compute Device (CPU / GPU / …) bao gồm nhiều Compute Unit
• Một Compute Unit có thể được phân chia thành một hoặc nhiều Processing Element (vd: 1 SP trong SM GPU)
Trang 7ATI:
• Radeon 4850
• Radeon 4870
Đối với CPU, OpenCL hỗ trợ các chip thuộc cả hai hãng lớn hiện nay là Intel và AMD
Do OpenCL vẫn còn khá mới cho nên chưa nhiều các hãng thiết kế phần cứng hỗ trợ chuẩn này Nhưng tương lai rất hứa hẹn khi OpenCL là chuẩn được nhiều “ông lớn” trong ngành công nghiệp phần cứng hỗ trợ nhất Và dù mới chỉ ra đời không lâu nhưng các đại gia này đã chính thức hỗ trợ OpenCL trong loạt sản phẩm hiệu năng cao của mình
Về nền tảng hệ điều hành: OpenCL có thể chạy được trên cả Mac OS X, Windows và
Linux
Trang 8II Kiến trúc OpenCL trên nền hệ điều hành Mac OS X
1 Sơ lược về Mac OS X
Hệ máy Macintosh của Apple nổi tiếng vì thiết kế đẹp và tinh tế, hệ thống phần cứng cao cấp, sự tối đồng bộ tối ưu giữa phần cứng với phần mềm Hệ điều hành Mac OS X góp phần không nhỏ vào thành công đó nhờ vào tính đơn giản ổn định, hệ màu chuẩn, ít bị tổn hại cùng với các công nghệ tiên tiến (Grand Central Dispatch, 64-bit,…) trong đó có OpenCL, và có thể nói Mac OS X Snow Leopard 10.6 là hệ điều hành đầu tiên trực tiếp đưa OpenCL vào “Core” của mình
2 Framework & Runtime
OpenCL framework trong Mac OS X cung cấp
đầy đủ các headers cần thiết để dễ dàng thực
hiện biên dịch mã nguồn OpenCL cũng như
giao tiếp với OpenCL Runtime Chỉ đơn giản
với một dòng lệnh #include
<opencl.h>, ta có thể sử dụng các API của
OpenCL mà không cần phải khai báo gì thêm
trong chương trình
Theo hình vẽ minh họa bên, mô hình làm việc
của OpenCL cũng tương tự như CAL của ATI
hay CUDA của nVidia
Một điều dễ thấy là OpenCL runtime làm việc
trực tiếp với driver của phần cứng, vì thế một
số ý kiến cho rằng OpenCL chỉ là một chuẩn về
ngôn ngữ lập trình song song nên không thể chạy nhanh hơn CAL/CUDA là hoàn toàn sai lệch Vì làm việc trực tiếp với driver của phần cứng nên OpenCL runtime có thể coi như tương đương với CAL/CUDA và việc hiệu năng có thể cao hơn CAL/CUDA là bình thường
Một số hãng phần cứng như ATI đưa OpenCL vào bộ công cụ lập trình song song của mình, nhưng ở mức nào đó chỉ về mặt cú pháp hình thức chứ không sử dụng OpenCL
Trang 9runtime và tầng giao tiếp với driver vẫn giữ CAL mà ko sử dụng OpenCL runtime Đó là
lý do STREAM chạy chậm hơn OpenCL vì một số nguyên nhân nào đó
3 Compiler
OpenCL compiler trong Mac OS X sử dụng LLVM Khi biên dịch một chương trình OpenCL, trước hết các chỉ thị trong chương trình đó sẽ được dịch sang một dạng biểu diễn trung gian (Intermediate Representation – IR) Sau đó, LLVM sẽ dịch và tối ưu hóa
IR sang mã phù hợp với thiết bị mà chương trình sẽ thực thi Điểm nhấn nằm ở chỗ: chương trình chỉ cần viết một lần nhưng vẫn có thể chạy trên nhiều kiến trúc phần cứng khác nhau Và ứng với mỗi hệ thống phần cứng, lần đầu tiên biên dịch của chương trình
sẽ được “cache” lại để tránh việc biên dịch trùng lặp không cần thiết
4 Operation Model
Sự vận hành của OpenCL được mô tả bởi cụm các model có mối liên hệ với nhau, bao gồm Platform Model (mô hình hạ tầng), Execution Model (mô hình thực thi), Memory Model (mô hình bộ nhớ), và Programming Model (mô hình lập trình)
Như đã nói ở trên, OpenCL
device sẽ làm việc với host
device – là device điều khiển
chương trình hoạt động Khi
chương trình thực thi, host sẽ
tạo ra một môi trường trừu
tượng hay còn gọi là môi
trường ảo (context) và cung
cấp các thiết bị tính toán (compute device) cùng với khoảng vùng nhớ nhất định
mà chương trình sẽ sử dụng Bên cạnh đó một “hàng đợi lệnh có thứ tự” (command queue) cũng sẽ được tạo ra để chương trình có thể điều phối các lệnh trong kernel và thực hiện các thao tác truy xuất tới bộ nhớ
Trang 10Lưu ý: tốc độ truy xuất giữa các device sẽ chậm hơn rất nhiều so với tốc độ giao
tiếp nội bộ của các thành phần trong device đó và bản thân host device (vd: CPU) cũng có thể là một OpenCL device
Kernel: là một tập các lệnh được viết ra để thực thi trên một thiết bị hỗ trợ
OpenCL (OpenCL device) Tập các kernel và các hàm bổ trợ (helper function)
được gọi là Program
Khi biên dịch chương trình, các kernel được biên dịch thành kernel object và tương tự với program ta có program object
Việc thực thi một chương trình OpenCL bao gồm nhiều thực thi một cách đồng thời các instance của một kernel trên một hoặc nhiều OpenCL device trên command queue được điều phối bởi ứng dụng host (host application) Mỗi
instance của một kernel gọi là một work-item Mỗi work-item thực thi cùng một
đoạn mã lệnh nhưng trên các vùng dữ liệu khác nhau và mỗi một work-item chạy trên một single-core của multiprocessor Khi ấn định thực thi chương trình trên một device nào đó, ta xác định số lượng work-item cần thiết để hoàn tất việc xử lý
dữ liệu mà ta sẽ gọi là index space (không gian chỉ mục) OpenCL hỗ trợ index
space tối đa là 3 chiều
Các work-item có thể nhóm lại thành những work-group OpenCL cũng có cơ chế
đồng bộ hóa tính toán giữa các work-item trong một work-group nhưng không hỗ trợ tương tự giữa các work-group với nhau
Mỗi work-item trong chương trình có một định danh duy nhất - global ID để hỗ
trợ truy xuất trong index space Ví dụ, một work-item trong không gian chỉ mục 2 chiều có giá trị X là 23 và Y là 6 sẽ mang global ID (23, 6) Tương tự, mỗi work-
Trang 11group cũng sẽ có một định danh duy nhất work-group ID, để xác định vị trí của
work-group trong index space
OpenCL cũng cho phép định vị trí của một work-item trong work-group thông qua
local ID
Ta có thể hình dung sự tương tự giữa OpenCL với CUDA, mỗi work-item tương
đương một thread, và mỗi work-group tương đương với một thread block
Memory object: là một handle tới vùng nhớ global (xem 4.3) được sử dụng để
lưu dữ liệu từ ứng dụng vào vùng nhớ của thiết bị để thao tác Có 2 loại chính:
buffer object và image object, với buffer object có thể chứa bất cứ loại dữ liệu
nào và image object được sử dụng đặc thù cho các dữ liệu ảnh Host application dùng command-queue để thực hiện thao tác đọc và ghi lên memory object
OpenCL phân chia tầm vực bộ nhớ vào bốn loại sau:
- Global memory: có thể đọc và ghi
bởi tất cả các work-item trong các
work-group Đây chính là vùng nhớ
được cấp phát đã mô tả trong Platform
Model
- Constant memory: là một vùng trên
global memory chỉ hỗ trợ việc đọc bởi
các work-item và giữ giá trị không đổi
suốt quá trình thực thi của một kernel
Giá trị trên constant memory được
cung cấp bởi host application
- Local memory: có thể được đọc ghi bởi một work-group đặc thù và giữ giá trị
chia sẻ bởi các work-item trong work-group đó
- Private memory: chỉ có thể truy xuất bởi một work-item duy nhất
Việc sử dụng bộ nhớ hiệu quả và tốc độ phụ thuộc rất nhiều vào cách dùng bốn loại bộ nhớ trên Trong đó private memory và local memory cho tốc độ cao nhất, vùng nhớ cho tốc độ truy xuất chậm chính là global memory
Trang 12Các khái niệm về tầm vực của vùng nhớ trong OpenCL cũng tương tự với CUDA:
OpenCL hỗ trợ hai mô hình lập trình song song chính: song song dữ liệu
(data-parallel) và song song tác vụ (task-(data-parallel)
Các tiến trình song song dữ liệu thực thi nhiều instance có cùng kernel một cách đồng thời, mỗi instance xử lý một tập dữ liệu riêng biệt Mỗi tập dữ liệu liên kết với một điểm trong không gian chỉ mục một, hai hay ba chiều
Song song tác vụ lại tương tự như những tiến trình thực thi đa luồng có tính chất độc lập nhau, mỗi process thực hiện những nhiệm vụ khác nhau Trong OpenCL, lập trình song song tác vụ bao gồm việc lập hàng đợi nhiều kernel, và để OpenCL thực hiện chúng một cách song song sử dụng các thiết bị tính toán có thể có
Trang 13III Workflow phát triển chương trình OpenCL
1 Các bước viết một chương trình OpenCL
Tiến trình phát triển một chương trình OpenCL bao gồm các bước dưới đây
Để chương trình đạt được hiệu quả cao nhất, trước tiên ta phải xác định những gì
có thể thực hiện đồng thời từ đó dễ dàng suy ra cách tổ chức bộ nhớ cũng như chi phí phù hợp cho chương trình
Để thực hiện tính toán song song trên OpenCL device, bắt buộc phải viết các kernel Các kernel được đóng gói và biên dịch khi chương trình thực thi
Sử dụng các hàm có trong OpenCL framework để tìm và quyết định thiết bị nào sẽ dùng trong chương trình Sau đó khởi tạo môi trường ảo bao gồm memory object
và command queue
Sau khi đã xác định được OpenCL device và setup context, chúng ta sẽ viết mã lệnh cho host application để biên dịch mã nguồn chương trình và sử dụng các kernel object
từ mã nguồn đã biên dịch Các lệnh sau được thực hiện liên tục theo thứ tự:
a Hàm clCreateProgramWithSource để khởi tạo chương trình từ mã nguồn OpenCL-C cho trước, hoặc nếu có sẵn đoạn mã đã được biên dịch trước đó (được
“cache” từ lần thực thi trước, ví dụ chương trình ngoài như các thư viện), gọi hàm clCreateProgramWithBinary Các hàm này sẽ liên kết các kernels và các
hàm bổ trợ vào một chương trình và trả về một program object
Trang 14b Gọi hàm clBuildProgram để biên dịch program object phù hợp với các thiết
bị đặc thù đang có của hệ thống
clCreateKernelsInProgram để tạo các kernel object trong một chương trình OpenCL, hay nói khác đi, ta extract các đối tượng kernel đã được biên dịch
từ một program object cho trước
Để giữ các dữ liệu nhập xuất và trả về giá trị cho các đối số đầu vào (input object), memory object tham gia vào nhiệm vụ thao tác vùng nhớ giữa host device và OpenCL device
Để thực thi một kernel, ta phải tuân theo các bước sau:
a Gọi hàm clSetKernelArg để truyền các tham số đầu vào (parameter value) vào kernel
b Xác định kích thước work-group và lập index space để thực thi kernel
c Đưa lệnh thực thi kernel vào command queue
Enqueue command để đọc giá trị xuất từ work-item và đưa nó vào host memory
2 Viết Kernel
Kernel được viết bằng ngôn ngữ OpenCL-C có cú pháp giống với C với một số điểm riêng biệt Một kernel có dạng như sau:
Trang 15Lưu ý:
1 Một kernel luôn được khai báo với tiết đầu tố kernel
2 Khi thực thi một kernel, ta dùng hàm clSetKernelArg để truyền giá trị vào các tham số được định nghĩa ở trên
3 Các hàm get_global_id và get_local_size để lấy thông tin về item khi thực thi kernel
work-4 mul24 là hàm toán học có sẵn trong OpenCL-C, và có rất nhiều hàm có khả năng tính toán hiệu suất cao được hỗ trợ sẵn cho cả dữ liệu có hướng lẫn vector
5 Kernel có thể được gọi từ một kernel khác trong cùng một chương trình OpenCL
Trang 163 Truy vấn thiết bị
Mỗi chương trình OpenCL đòi hỏi phải có một context, bao gồm danh sách các OpenCL device tồn tại trên hệ thống Sử dụng hàm clGetDeviceIDs để truy vấn danh sách thiết bị trên máy hỗ trợ OpenCL Ta có thể giới hạn việc truy vấn dựa vào đặc thù của loại thiết bị hoặc kết hợp các thiết bị (vd: chỉ dùng GPU, CPU hay kết hợp cả 2), bên cạnh đó ta cũng có thể giới hạn số lượng thiết bị muốn sử dụng
Ví dụ: giả sử chúng ta muốn thực thi code trên GPU và không quan tâm có bao nhiêu GPU sử dụng được vì ta chỉ cần một Ta gán CL_DEVICE_TYPE_GPU vào tham số device_type trong hàm clGetDeviceIDs và gán num_entires = 1, OpenCL sẽ trả về ID của GPU đầu tiên mà nó tìm thấy
4 Khởi tạo OpenCL Context
Một khi đã xác định được sẽ sử dụng OpenCL device nào để tính toán và có ít nhất một thiết bị sử dụng được, chúng ta bắt tay vào khởi tạo OpenCL context nhằm phục vụ cho việc nhónm các thiết bị lại với nhau để có thể chia sẻ vùng nhớ giữa các compute device, hoặc chúng ta cũng có thể khởi tạo context từ một OpenGL context đã tồn tại trước đó
Trang 17nếu có nhu cầu kết hợp OpenGL và OpenCL với nhau Việc chia sẻ bộ nhớ giữa 2 context hoàn toàn có thể thực hiện được
Để khởi tạo một context, trước tiên ta phải xác định thiết bị nào sẽ dùng (kết quả trả về từ hàm clGetDeviceIDs), và truyền nó vào hàm clCreateContext
5 Khởi tạo Program Object
Một chương trình OpenCL bao gồm một tập các kernel, các hàm bổ trợ có thể gọi từ kernel (các kernel luôn phải bắt đầu bằng từ khóa kernel) Tuy nhiên, những hàm
bổ trợ này có thể không thực thi đúng vai trò như một entry point từ OpenCL API Có nghĩa là, ta chỉ có thể enqueue các kernel đã thông báo như trên Một program object đóng gói chương trình nguồn OpenCL, đi kèm với phiên bản thực thi được build lần trước của chương trình, cũng như build options, build log, và danh sách các thiết bị mà chương trình đã biên dịch để dùng trước đó
Ta có thể khởi tạo một program object trực tiếp từ mã nguồn của chương trình OpenCL
và biên dịch nó trực tiếp vào thời điểm thực thi ứng dụng (application runtime) Thêm vào đó, ta cũng có thể build program object sử dụng mã nhị phân của lần build thành công trước đó để tránh phải build khi thực thi ứng dụng