Bạn có từng thắc mắc vì sao mình phải mất cả tiếng đồng hồ quý giá chỉ để render 1 video 20 phút trên con CPU của mình? Bạn phải chịu đựng cái cảnh 10FPS mỗi lần chơi Dota 2? Bạn phải mất cả ngày để huấn luyện mô hình YOLO (của DoanhPV :>)? Đừng lo, hãy sắm cho mình một chiếc GPU Nvidia GeForce RTX 3080 Ti Suprim X 12G, mọi vấn đề của bạn đã được giải quyết :D.
Vì sao một con GPU có thể giải quyết nhiều vấn đề như vậy? Sự khác biệt giữa CPU và GPU là gì? Tiềm năng của GPU trong tương lai sẽ còn mạnh mẽ đến đâu?
Xin chào mọi người, mình là TienLM19 và chào mừng mọi người đến với phần đầu tiên của series bài viết về lập trình song song.
1. Bối cảnh
Trong thời đại phát triển công nghệ số, phần mềm dường như đã len lỏi ở khắp nơi trong cuộc sống mỗi người. Để phục vụ đại đa số người dùng thiết bị thông minh, các phần mềm thường không đòi hỏi cao về yêu cầu phần cứng của thiết bị, một chiếc smartphone hoặc một chiếc PC tầm trung hoàn toàn có thể đáp ứng đủ các nhu cầu cơ bản như xem phim, lướt web hay sử dụng các phần mềm văn phòng, v.v. Tuy nhiên, điều đó chỉ đúng nếu như chương trình chỉ phải thực hiện một khối lượng tác vụ không đáng kể và không có những yêu cầu cao về tốc độ thực thi chương trình, vì đa phần các phần mềm này được lập trình để xử lý các tác vụ theo phong cách tính toán tuần tự (serial computing). Đối với coders, chắc hẳn ai cũng đã quen thuộc với lối tư duy lập trình tuần tự. Các thuật toán, từ kinh điển đến hiện đại, đa phần đều được cài đặt theo lối tư duy tuần tự này.
Tưởng tượng đến việc mua phiếu ăn mỗi 12 giờ trưa, mọi người xếp thành một hàng và người bán sẽ lần lượt đưa phiếu cho từng người một. Chuyện sẽ không có gì đáng nói nếu như bỗng một ngày, mọi người lại xếp thành hai hàng thay vì một như bình thường và người bán, như thường lệ, chỉ có một. Tình huống trở nên phức tạp hơn khi người bán phiếu ăn lúc này lại phải bán cho một lúc hai hàng người đang “đói meo” sau một buổi sáng làm việc. Tương tự trong lập trình, khi đối mặt với việc phải thực hiện cùng một lúc “nhiều” phép toán khác nhau (“nhiều” ở đây được xem là hàng triệu đến hàng tỷ phép toán :D) và có sự ràng buộc chặt chẽ về thời gian thực thi, thậm chí là ở mức độ thời gian thực (real time), lập trình tuần tự trên dường như không phải là hướng tiếp cận hiệu quả.
Một vị hiền triết đã từng nói:
“Modern problems require modern solutions”
Đúng vậy, những vấn đề mang tính đột phá cần những hướng tiếp cận mang tính thời đại. Chúng ta đã chứng kiến sự bùng nổ của lĩnh vực Deep Learning, là kết quả của sự phát triển vượt bậc trong lĩnh vực phần cứng mà trên hết không thể không nhắc tới chính là Bộ xử lý đồ hoạ, hay GPU. GPU ban đầu được thiết kế để tăng tốc độ render đồ hoạ 3D nhờ vào sức mạnh tính toán song song (parallel computing). Khi nhận thấy được sức mạnh song song của GPU, các nhà phát triển đã cung cấp các API thân thiện và linh hoạt, giúp các lập trình viên dễ dàng hơn trong việc khai thác sức mạnh của tính toán song song trên GPU và ứng dụng vào giải quyết các bài toán có độ phức tạp tính toán cao (thậm chí là rất cao :D) với độ trễ thấp. Vậy, lập trình song song (parallel programming) trên GPU là như thế nào?
2. Lập trình tuần tự và lập trình song song
Với lập trình tuần tự, khi giải quyết một bài toán nhất định, thông thường chúng ta sẽ chia nhỏ bài toán thành các tác vụ rời rạc, mỗi tác vụ đóng một vai trò nhất định trong việc giải quyết bài toán được đặt ra, các tác vụ lần lượt được thực hiện và tạo ra một chương trình hoàn chỉnh (hình 2.1).
Trong khi đó đối với lập trình song song, bài toán của chúng ta cũng sẽ được chia thành những tác vụ nhỏ rời rạc, tuy nhiên khác với lập trình tuần tự, các tác vụ này được thực hiện đồng thời với nhau trong cùng một khoảng thời gian và được thực thi một cách độc lập (hình 2.2).
Vì các tác vụ trong lập trình song song độc lập lẫn nhau, chúng ta cần phải nắm rõ bài toán cần giải quyết hay thậm chí là định nghĩa lại bài toán, để các tác vụ cần thiết trong bài toán ít ràng buộc lẫn nhau nhất có thể, nhằm gia tăng nhiều hơn khả năng song song hoá.
Dưới đây là một ví dụ minh hoạ cho lập trình song song, bài toán cộng 2 vector:
Thông thường, khi thực hiện việc cộng 2 vector, một vòng lặp for đơn giản bằng Python là đã giải quyết được vấn đề này.
for i in range(len(a)): c[i] = a[i] + b[i]
Tuy nhiên, khi quan sát kỹ hơn, ta nhận thấy một điều rằng để tính được giá trị c[i], chương trình phải thực hiện xong việc tính giá trị của c[i – 1] (i > 0), trong khi kết quả tính toán của c[i] không phụ thuộc vào kết quả tính toán của c[i – 1]. Hãy tưởng tượng đến việc, 2 vector không chỉ dừng lại ở kích thước 8 phần tử như ví dụ trên, mà số lượng phần tử có thể lên đến hàng chục triệu hay hàng trăm triệu phần tử, việc lần lượt tính tổng các phần tử sẽ mất rất nhiều thời gian của chúng ta (thời gian là vàng bạc :D).
Bằng việc nhìn ra tính độc lập giữa các phần tử đầu ra của phép tính tổng 2 vector, ta có thể sử dụng khả năng tính toán song song để giải quyết bài toán này. Dưới đây là pseudo-code chương trình song song cho bài toán (pseudo-code convention sử dụng bên dưới được tham khảo từ nguồn này :D):
n = a.length() forall(i in (1..n)) // start n GPU threads with i id { c[i] = a[i] + b[i] // each GPU thread execute a summation }
Ở đây, ta có thể thấy được mỗi luồng xử lý được GPU cung cấp sẽ đảm nhiệm việc tính tổng cho một phần tử trong vector c đầu ra, qua đó trong cùng một thời điểm đã có n luồng đồng thời cùng tính tổng cho n phần tử có trong vector c, giúp tiết kiệm rất nhiều thời gian tính toán. Các chi tiết kỹ thuật về việc vì sao GPU lại có nhiều luồng như vậy hay GPU điều phối các luồng đó như thế nào, do tính chất tổng quan của phần này, mời mọi người theo dõi những phần sau của series :D.
Dù khả năng của tính toán song song trên GPU là vô cùng mạnh mẽ, song GPU không được tạo ra để thay thế CPU, suy cho cùng CPU và GPU đều có những thế mạnh riêng của mình, vậy những điểm mạnh đó là gì?
3. CPU và GPU
Cái nhìn tổng quan về CPU và GPU trong lập trình song song được minh hoạ như hình 3.1.
Sự khác biệt to lớn nhất giữa CPU và GPU chính là số lượng nhân (cores), GPU có số lượng nhân gấp nhiều lần so với số lượng nhân của CPU, số nhân GPU có thể tính đến hàng ngàn nhân trong khi CPU chỉ có 6 nhân (như ví dụ trên :D). Tuy nhiên, nếu ví nhân của GPU là những anh công nhân cần mẫn với nguồn năng lượng dồi dào, thì nhân CPU sẽ là những nhà PhD tầm cỡ. CPU đảm nhận nhiều nhiệm vụ phức tạp trong một hệ thống máy tính, bao gồm cả việc quản lý tài nguyên, tiến trình của hệ thống, giúp hệ thống hoạt động một cách trơn tru nhất. Qua đó, nhân CPU tuy ít nhưng vô cùng mạnh mẽ, phức tạp và là nơi điều khiển toàn bộ hoạt động của hệ thống. Ngược lại, tuy số lượng nhân GPU là vô cùng lớn, các nhân GPU này lại có thiết kế đơn giản hơn và yếu hơn, thích hợp cho các tác vụ thiên về tính toán hơn là các tác vụ thiên về điều khiển như CPU. Do tính chất này, GPU được thiết kế để tối ưu hoá thông lượng (throughput), còn CPU do phải đảm nhiệm việc duy trì một hệ thống và đảm bảo được tính mượt mà của hệ thống nên CPU được thiết kế để tối ưu hoá độ trễ (latency).
Để minh hoạ về độ trễ và thông lượng, hãy tưởng tượng chúng ta có một bài toán đơn giản như sau: ta cần đưa một đoàn người đi từ Tp. HCM ra Hà Nội, quãng đường cần đi ước tính khoảng 1500km và có 2 loại phương tiện ta cần cân nhắc (hình 3.2):
Ứng cử viên đầu tiên của chúng ta là chiếc Lamborghini Sian với vận tốc cực đại vào khoảng 350km/h, ứng cử viên tiếp theo là chiếc xe buýt 53 với vận tốc đón khách vào khoảng 50km/h. Ở đây, chiếc Lamborghini mạnh mẽ và tốc độ đại diện cho CPU, còn chiếc xe buýt 53 với thiết kế đơn giản với vận tốc “đón khách” sẽ đại diện cho GPU. Tất nhiên, chiếc xe buýt 53 có hẳn 40 chỗ ngồi, trong khi chiếc Lamborghini chỉ có 2 chỗ ngồi. Cùng làm một phép toán đơn giản xem đâu sẽ là lựa chọn hợp lý.
Lamborghini Sian | Bus 53 | |
Distance (km) | 1500 | 1500 |
Max speed (km/h) | 350 | 50 |
Seats | 2 | 40 |
Latency (h) | 1500/350 = ~4.29 | 1500/50 = 30 |
Throughput (people/h) | 2/4.29 = ~0.47 | 40/30 = ~1.33 |
Qua kết quả trên, rõ ràng một người đi bằng Lamborghini ra Hà Nội sẽ nhanh hơn rất nhiều so với đi bằng xe buýt 53. Ngược lại, xe buýt 53 lại đưa được nhiều người đến Hà Nội hơn sau một khoảng thời gian nhất định so với Lamborghini. Ví dụ trên cũng đã phần nào nói lên được điểm mạnh và yếu của CPU và GPU.
Vì thế, thay vì sinh ra để thay thế lẫn nhau, một sự kết hợp giữa CPU và GPU, kết hợp giữa hệ thống tối ưu về tính toán tuần tự và hệ thống tối ưu về tính toán song song sẽ tạo nên một kiến trúc vô cùng mạnh mẽ được gọi là kiến trúc bất đồng nhất CPU-GPU (CPU-GPU heterogenous architectures). Đây cũng là mô hình kiến trúc đang được áp dụng cho các hệ thống điện toán hiệu năng cao hay HPC (high-performance computing) (hình 3.3).
4. Ứng dụng của lập trình song song
Nhờ vào khả năng tính toán song song, rất nhiều lĩnh vực đã có những bước tiến lớn như:
- Ngành công nghiệp game: đây được xem là thành công nổi bật nhất của GPU khi ngành công nghiệp game đã phát triển vượt bậc trong suốt hơn 20 năm qua. Cuộc đua của 2 ông lớn NVIDIA và AMD đã đem lại nhiều sự cải tiến với GPU và đem đến nhiều công nghệ mới hỗ trợ cho các vấn đề về lighting, shadow, texture, anti-aliasing, v.v trong đồ họa 3D và gần nhất là các công nghệ như Ray Tracing, DLSS (Deep Learning Super Sampling) của nhà NVIDIA và AMD FidelityFX Super Resolution đến từ nhà AMD.
- Truyền thông đa phương tiện (Multimedia): sự phát triển của GPU cung cấp nhiều sức mạnh hơn cho những nhà sáng tạo nội dung (content creator), phục vụ cho nhiều công việc khác nhau như chỉnh sửa hình ảnh, video; hỗ trợ truy xuất (render) đồ họa 2D lẫn 3D cho những nhà thiết kế đồ họa chuyên nghiệp (graphic designers); cung cấp những bộ công cụ mạnh mẽ cho các nhà làm phim, dựng phim phục vụ cho ngành công nghiệp điện ảnh, v.v.
- Khoa học: tính toán song song được xem là xương sống cho rất nhiều lĩnh vực nghiên cứu khác nhau như vật lý, hóa học, địa chất, thiên văn, v.v. GPU hỗ trợ mạnh mẽ cho tác vụ phức tạp và cần nhiều sự tính toán như tác vụ mô phỏng (simulation) phục vụ cho lĩnh vực vật lý, thiên văn và các tác vụ yêu cầu khả năng tính toán hiệu quả trong thời gian thực phục vụ cho lĩnh vực khí tượng, địa chất, v.v.
5. Tham khảo
- Sách: Professional CUDA C Programming – John Cheng, Max Grossman and Ty McKercher
- 9 Parallel Processing Examples & Applications
- Parallel Programming Primer | Princetone Research Computing
- https://my.eng.utah.edu/~cs4960-01/lecture4.pdf