Tiny GPU: GPU tối giản được triển khai bằng Verilog
(github.com/adam-maj)- tiny-gpu là một triển khai GPU tối giản dựa trên Verilog để học từ nền tảng cách GPU hoạt động ở cấp phần cứng; nó tập trung vào các nguyên lý chung của GPGPU và bộ tăng tốc ML hơn là phần cứng chuyên dụng cho đồ họa
- Phần triển khai gồm chưa đến 15 tệp Verilog có tài liệu, tài liệu kiến trúc và ISA, kernel cộng/nhân ma trận, cùng hỗ trợ mô phỏng kernel và truy vết thực thi
- GPU chạy một kernel duy nhất tại một thời điểm; sau khi nạp bộ nhớ chương trình và bộ nhớ dữ liệu, đặt
thread_count, rồi đưa tín hiệustartlên cao để bắt đầu kernel - Để đơn giản hóa, mỗi core xử lý một block tại một thời điểm; mỗi thread có ALU, LSU, PC và register file riêng, nhưng giả định rằng tất cả thread hội tụ về cùng PC sau mỗi lệnh
- Hầu hết các tính năng như cache nhiều tầng, shared memory, memory coalescing, pipelining, warp scheduling, branch divergence và barrier của GPU hiện đại đều bị lược bỏ, ưu tiên cấu trúc phục vụ học tập
Vấn đề tiny-gpu muốn giải quyết
- Có nhiều tài liệu để học về CPU, từ kiến trúc đến tín hiệu điều khiển, nhưng các chi tiết kỹ thuật cấp thấp của GPU hiện đại phần lớn vẫn là độc quyền do thị trường cạnh tranh
- Có nhiều tài liệu lập trình GPU, nhưng hầu như không có tài liệu để học cách GPU hoạt động ở cấp phần cứng
- Các triển khai GPU mã nguồn mở như Miaow và VeriGPU hướng tới tính hoàn chỉnh và khả năng vận hành nên có cấu trúc phức tạp
- tiny-gpu loại bỏ nhiều độ phức tạp của card đồ họa cấp sản xuất và tập trung vào các thành phần cốt lõi phổ biến trong bộ tăng tốc phần cứng hiện đại
- Các thành phần quan trọng của kiến trúc GPU
- Cách mô hình lập trình SIMD được triển khai trong phần cứng
- Cách GPU xử lý băng thông bộ nhớ hạn chế
Kiến trúc tổng thể
- tiny-gpu được thiết kế để chỉ chạy một kernel tại một thời điểm
- Quy trình chạy kernel như sau
- Nạp mã kernel vào bộ nhớ chương trình toàn cục
- Nạp dữ liệu cần thiết vào bộ nhớ dữ liệu
- Chỉ định số thread sẽ chạy trong thanh ghi điều khiển thiết bị
- Đặt tín hiệu
startlên high để chạy kernel
- GPU gồm các đơn vị sau
- Thanh ghi điều khiển thiết bị
- Dispatcher
- Số lượng compute core có thể thay đổi
- Memory controller cho bộ nhớ dữ liệu và bộ nhớ chương trình
- Cache
Thực thi kernel và phân bổ thread
- Thanh ghi điều khiển thiết bị có vai trò lưu metadata thực thi kernel; trong tiny-gpu, nó chỉ lưu
thread_count, tức tổng số thread sẽ chạy - Dispatcher phân phối thread cho nhiều compute core khi kernel bắt đầu
- Gom các nhóm thread có thể chạy song song thành block
- Gửi block đến core khả dụng để xử lý
- Thông báo hoàn tất thực thi kernel khi tất cả block đã xử lý xong
- Core đã đơn giản hóa xử lý một block tại một thời điểm
- Mỗi thread có ALU, LSU, PC và register file chuyên dụng
- Việc quản lý thực thi lệnh của thread trên các tài nguyên này là một trong những bài toán khó của GPU
Cấu trúc bộ nhớ và controller
- GPU được tạo để giao tiếp với bộ nhớ toàn cục bên ngoài; để đơn giản hóa, bộ nhớ dữ liệu và bộ nhớ chương trình được tách riêng
- Đặc tả bộ nhớ dữ liệu
- Địa chỉ hóa 8 bit
- Tổng cộng 256 hàng
- Dữ liệu 8 bit
- Mỗi hàng lưu giá trị nhỏ hơn 256
- Đặc tả bộ nhớ chương trình
- Địa chỉ hóa 8 bit
- Tổng cộng 256 hàng
- Dữ liệu 16 bit
- Theo ISA, mỗi lệnh là 16 bit
- Memory controller theo dõi các yêu cầu bộ nhớ đến từ core, giới hạn yêu cầu theo băng thông bộ nhớ ngoài thực tế và chuyển phản hồi đến đúng tài nguyên
- Mỗi memory controller có số kênh cố định tùy theo băng thông bộ nhớ toàn cục
- Cache là tính năng đang được phát triển; nó lưu dữ liệu lấy từ bộ nhớ ngoài vào SRAM trong thiết bị để các yêu cầu sau có thể lấy nhanh hơn và băng thông bộ nhớ có thể được dùng cho dữ liệu mới
Cấu trúc bên trong core
- Mỗi core có một scheduler duy nhất để quản lý thực thi thread
- Scheduler của tiny-gpu thực thi lệnh của một block đến hết rồi lấy block mới, đồng thời chạy lệnh của tất cả thread theo thứ tự đồng bộ
- Với scheduler nâng cao hơn, có thể tăng mức sử dụng tài nguyên bằng pipelining và warp scheduling
- Ràng buộc chính của scheduler phát sinh từ độ trễ khi load và store dữ liệu từ bộ nhớ toàn cục
- Hầu hết lệnh có thể chạy đồng bộ
- Các thao tác load-store như
LDRvàSTRlà bất đồng bộ, nên việc thực thi lệnh phải được tổ chức quanh thời gian chờ dài
- Fetcher lấy bất đồng bộ lệnh tại program counter hiện tại từ bộ nhớ chương trình
- Decoder giải mã lệnh đã lấy thành tín hiệu điều khiển để thực thi thread
- Register file của mỗi thread lưu dữ liệu đang được tính toán và cho phép mẫu SIMD
- Các thanh ghi chỉ đọc chứa
%blockIdx,%blockDim,%threadIdx - Kernel có thể chạy với dữ liệu khác nhau tùy theo ID thread cục bộ
- Các thanh ghi chỉ đọc chứa
- ALU của mỗi thread xử lý các lệnh số học
ADD,SUB,MUL,DIV CMPxuất ra kết quả cho biết hiệu của hai thanh ghi là âm, 0 hay dương, rồi lưu kết quả vào thanh ghiNZPcủa đơn vị PC- LSU của mỗi thread truy cập bộ nhớ dữ liệu toàn cục, xử lý
LDR,STRvà độ trễ bộ nhớ bất đồng bộ - PC của mỗi thread quyết định lệnh tiếp theo sẽ thực thi
- Mặc định tăng 1 sau mỗi lệnh
BRnzprẽ nhánh đến một hàng bộ nhớ chương trình cụ thể nếu điều kiện trong thanh ghiNZPdoCMPtrước đó thiết lập là đúng- Vòng lặp và câu lệnh điều kiện được triển khai theo cách này
- Để đơn giản hóa, tiny-gpu giả định tất cả thread hội tụ về cùng PC sau mỗi lệnh
- Trong GPU thực tế, từng thread có thể rẽ nhánh đến các PC khác nhau; khi đó nhóm thread đang được xử lý cùng nhau sẽ tách thành nhiều luồng thực thi, gọi là branch divergence
ISA
- tiny-gpu triển khai ISA 11 lệnh để chạy các kernel đơn giản dạng proof-of-concept như cộng ma trận và nhân ma trận
- Các lệnh được hỗ trợ
BRnzp: nhảy đến hàng khác trong bộ nhớ chương trình nếu điều kiệnNZPkhớpCMP: so sánh giá trị của hai thanh ghi và lưu kết quả vào thanh ghiNZPADD,SUB,MUL,DIV: các phép toán số học cơ bản cho tensor mathLDR: load dữ liệu từ bộ nhớ toàn cụcSTR: store dữ liệu vào bộ nhớ toàn cụcCONST: load giá trị hằng vào thanh ghiRET: tín hiệu kết thúc thực thi thread hiện tại
- Mỗi thanh ghi được chỉ định bằng 4 bit, tổng cộng có 16 thanh ghi
- 13 thanh ghi từ
R0đếnR12là thanh ghi tự do có thể đọc/ghi - 3 thanh ghi cuối là thanh ghi đặc biệt chỉ đọc, cung cấp
%blockIdx,%blockDim,%threadIdxcần thiết cho SIMD
- 13 thanh ghi từ
Luồng thực thi
- Khi thực thi lệnh, mỗi core tuân theo luồng điều khiển gồm các bước sau
FETCH: lấy lệnh tiếp theo tại PC hiện tạiDECODE: giải mã lệnh thành tín hiệu điều khiểnREQUEST: nếu cầnLDRhoặcSTR, gửi yêu cầu dữ liệu đến bộ nhớ toàn cụcWAIT: nếu cần, chờ phản hồi từ bộ nhớ toàn cụcEXECUTE: thực hiện tính toán trên dữ liệuUPDATE: cập nhật register file và thanh ghiNZP
- Luồng điều khiển này được xây dựng nhằm đơn giản và dễ hiểu
- Trong triển khai thực tế, có thể gộp một số bước để tối ưu thời gian xử lý, hoặc điều phối nhiều lệnh trên tài nguyên core bằng pipelining
- Mỗi thread tính toán trên dữ liệu trong register file chuyên dụng của mình theo cùng một đường thực thi
- Nó giống sơ đồ CPU, nhưng khác ở chỗ
%blockIdx,%blockDim,%threadIdxnằm trong các thanh ghi chỉ đọc, cho phép chức năng SIMD
Kernel ví dụ
- Các kernel cộng ma trận và nhân ma trận được viết để làm proof-of-concept cho ISA
- Các tệp kiểm thử trong repository có thể mô phỏng hoàn chỉnh các kernel này trên GPU, đồng thời tạo trạng thái bộ nhớ dữ liệu và toàn bộ truy vết thực thi
-
Cộng ma trận
matadd.asmcộng hai ma trận 1 x 8- Phép cộng theo từng phần tử của 8 phần tử được thực hiện trên các thread riêng biệt
- Sử dụng các thanh ghi
%blockIdx,%blockDim,%threadIdxđể minh họa lập trình SIMD - Bao gồm quản lý bộ nhớ bất đồng bộ bằng các lệnh
LDRvàSTR
-
Nhân ma trận
matmul.asmnhân hai ma trận 2 x 2- Tính tích vô hướng của hàng và cột tương ứng theo từng phần tử
- Dùng
CMPvàBRnzpđể minh họa rẽ nhánh bên trong thread - Vì tất cả nhánh đều hội tụ lại, nó hoạt động trong triển khai tiny-gpu hiện tại
Mô phỏng
- Để chạy mô phỏng kernel, cần iverilog và cocotb
- Các bước chuẩn bị
- Cài trình biên dịch Verilog và cocotb bằng
brew install icarus-verilogvàpip3 install cocotb - Tải phiên bản mới nhất của sv2v, giải nén và thêm binary vào
$PATH - Chạy
mkdir buildtại root của repository
- Cài trình biên dịch Verilog và cocotb bằng
- Chạy mô phỏng kernel bằng
make test_mataddvàmake test_matmul - Kết quả thực thi được xuất ra các tệp log trong
test/logs- Trạng thái bộ nhớ dữ liệu ban đầu
- Toàn bộ truy vết thực thi của kernel
- Trạng thái bộ nhớ dữ liệu cuối cùng
- Ở phần đầu mỗi tệp log có ma trận đầu vào, và ở bộ nhớ dữ liệu cuối cùng ở phần cuối có ma trận kết quả
- Truy vết thực thi bao gồm trạng thái thực thi của tất cả thread trên mọi core ở từng chu kỳ
- Lệnh hiện tại
- PC
- Giá trị thanh ghi
- Thông tin trạng thái
Các tính năng GPU nâng cao cố ý lược bỏ
- tiny-gpu lược bỏ hầu hết các yếu tố cải thiện hiệu năng và tính năng của GPU hiện đại để đơn giản hóa
-
Cache nhiều tầng và shared memory
- GPU hiện đại dùng nhiều tầng cache để giảm truy cập bộ nhớ toàn cục
- tiny-gpu chỉ triển khai một tầng cache duy nhất để lưu dữ liệu gần đây giữa tài nguyên yêu cầu và memory controller
- Cache nhiều tầng giảm thời gian load bằng cách cache dữ liệu thường dùng gần nơi sử dụng hơn
- GPU cũng có thể dùng shared memory để các thread trong cùng block trao đổi kết quả chung
-
Memory coalescing
- Nhiều thread đang chạy song song thường truy cập các địa chỉ liên tiếp, chẳng hạn các phần tử liền kề của ma trận
- Memory coalescing phân tích các yêu cầu bộ nhớ đang xếp hàng và gộp các yêu cầu liền kề thành một transaction
- Mục đích là giảm thời gian dành cho địa chỉ hóa và xử lý các yêu cầu cùng nhau
-
Pipelining
- Core của tiny-gpu chỉ bắt đầu lệnh tiếp theo sau khi thực thi xong một lệnh của một nhóm thread
- GPU hiện đại stream việc thực thi nhiều lệnh tuần tự, trong khi vẫn đảm bảo lệnh có phụ thuộc được chạy theo thứ tự
- Điều này tăng mức sử dụng tài nguyên để tài nguyên core không bị nhàn rỗi trong các tình huống như chờ yêu cầu bộ nhớ bất đồng bộ
-
Warp scheduling
- Chia block thành các warp, tức các batch thread có thể chạy cùng nhau
- Khi một warp đang chờ, core thực thi lệnh của warp khác, qua đó xử lý nhiều warp đồng thời trên một core
- Tương tự pipelining nhưng xử lý lệnh của các thread khác nhau
-
Branch divergence
- tiny-gpu giả định tất cả thread trong một batch đều ở cùng PC sau mỗi lệnh
- Trên thực tế, từng thread có thể rẽ nhánh đến dòng khác tùy theo dữ liệu
- Các thread có PC khác nhau sẽ được tách thành các luồng thực thi riêng, và cũng cần quản lý thời điểm chúng hội tụ lại
-
Đồng bộ hóa và barrier
- GPU hiện đại có thể đặt barrier để buộc nhóm thread trong cùng block chờ cho đến khi tất cả đều tới một điểm nhất định
- Điều này hữu ích khi các thread cần trao đổi dữ liệu dùng chung, nhằm đảm bảo việc xử lý dữ liệu đã hoàn tất
Công việc tiếp theo
- Các hạng mục cải tiến trong tương lai như sau
- Thêm cache lệnh đơn giản
- Xây dựng adapter để có thể dùng GPU trong Tiny Tapeout 7
- Thêm branch divergence cơ bản
- Thêm memory coalescing cơ bản
- Thêm pipelining cơ bản
- Tối ưu luồng điều khiển và cách dùng thanh ghi để cải thiện cycle time
- Viết kernel đồ họa cơ bản hoặc thêm phần cứng đồ họa đơn giản để minh họa chức năng đồ họa
- Người dùng muốn cải thiện repository có thể đóng góp bằng PR
1 bình luận
Ý kiến trên Hacker News
Ngoại lệ là Intel đã công bố khá nhiều tài liệu kỹ thuật về GPU: https://kiwitree.net/~lina/intel-gfx-docs/prm/
Sổ tay i810/815 cũng có thể tìm thấy trên mạng, và nếu bỏ qua khoảng trống kỳ lạ ở giai đoạn trước 965 khi thiếu 855/910/915/945, thì việc tài liệu hóa nhìn chung khá đều đặn
Bao gồm cả tài liệu kiến trúc tập lệnh cho các sản phẩm hiện tại và trước đây, nhưng có vẻ giống tài liệu nhắm đến người triển khai hơn là phần giải thích cấp cao cho những người đam mê quan tâm
Ước gì mọi công ty đều làm theo cách này
Tuy nhiên tôi thấy nó gần với bộ đồng xử lý SIMD hơn
Tôi nghĩ để gọi là GPU thì tối thiểu phải có một dạng xuất hiển thị nào đó
Tôi biết thuật ngữ này đã trở nên khá lỏng lẻo khi gần đây Nvidia và các hãng khác cũng bán các biến thể kiến trúc đồ họa chỉ dành cho máy chủ dưới tên GPU, nhưng trong thiết kế GPU, phần đồ họa đến nay vẫn chiếm một phần đáng kể độ phức tạp
GPU không xuất hình vẫn hữu ích
Ở chỗ tôi làm có khoảng 75 workstation gắn Quadro tầm trung, nhưng card chỉ có mini-DisplayPort còn công ty chỉ mua cáp HDMI, nên tất cả đều đang cắm vào đồ họa tích hợp
Dù vậy các card đó vẫn tăng tốc phần mềm và xử lý đồ họa, chỉ là không xuất ra màn hình mà thôi
Cũng có ví dụ khác: https://github.com/jbush001/NyuziProcessor
Cần sản lượng ở mức nào để TSMC hay một foundry khác có thể sản xuất các bộ xử lý kiểu này một cách kinh tế?
Tôi muốn thử FPGA, nhưng nói thật là ngay cả việc biết bắt đầu từ đâu cũng khó, và cả lĩnh vực này tạo cảm giác khá đáng ngại
Mục tiêu cuối cùng là làm một card tăng tốc cho LLM; dù đó là mục tiêu hoàn toàn tự đặt ra, tôi nghĩ nó sẽ có nhiều phần trùng với dự án này, và có lẽ chỉ khác ở phần offload bộ nhớ để tải các mô hình lớn hơn
Nhập môn FPGA phải được chia nhỏ thành nhiều kỹ năng con, và cũng cần điều chỉnh kỳ vọng
Ta không kỳ vọng một kỹ sư phần mềm ngay từ đầu đã xây dựng cả máy tính từ nguyên lý, viết kiến trúc tập lệnh, hiểu mã máy, chuyển nó thành assembly, rồi phát triển cả ngôn ngữ lập trình để tạo ứng dụng bằng mã Python
Cách đúng là bắt đầu từ phía trên rồi đi xuống dưới của stack
Nếu tập trung vào việc trừu tượng hóa độ phức tạp và xây dựng hệ thống bằng các IP có sẵn, thiết kế FPGA khá dễ
Thường thì tôi sẽ khuyên dùng thứ như MATLAB, vì có thể tạo ứng dụng ban đầu bằng HDL Coder trên DevKit có reference design
Nếu không, bạn sẽ gánh một khối lượng học khổng lồ: kiến trúc tính toán số, Verilog, timing, transceiver/I/O, lập kế hoạch chân, Quartus/Vivado, mô phỏng/xác minh, hệ thống nhúng, v.v.
Tóm lại, hãy bắt đầu từ thiết kế cấp hệ thống, học cách lấy các IP kiểu cắm là chạy và kết nối chúng ở tầng cao nhất, rồi đưa mô-đun đó vào một thiết kế tham chiếu dựng sẵn
Sau đó có thể dần bóc từng lớp để lộ độ phức tạp bên dưới
Có thể vừa đọc Harris&Harris vừa làm thực hành
Khi hoàn thành các bài thực hành và cuốn sách, đó là lúc bắt đầu dự án của riêng mình
Nhân tiện, HackerMojo cũng có buổi gặp hằng tuần, và dù không ở Valley bạn vẫn có thể tham gia qua Zoom
Bắt đầu từ đó là được
Phần cứng thực ra vừa là phần dễ, vừa là phần khó nếu xét về mặt chế tạo
alwaystuần tự ở đây không?Khi đó có một trang tên là opencores, tập hợp nhiều dự án HDL mã nguồn mở
Không biết ngày nay có trình mô phỏng HDL phân tán quy mô lớn cấp HPC nào dùng ổn không
Việc tận dụng GPU hiện đại cho mô phỏng ở mức RTL nghe có vẻ hợp lý
Hay đó không phải cùng trang mà là một nơi tương tự khác?
Ở những thứ như CUDA core hiện đại, việc có phép chia như một lệnh thật sự có phổ biến không, hay thường được mô phỏng bằng phần mềm?
Mạch chia phần cứng thực sự chiếm rất nhiều diện tích, nên tôi không ngờ nó lại nằm trong ALU của GPU
Trong Verilog thì viết một dòng
DIV: begin alu_out_reg <= rs / rt; endquá dễ, nhưng một dòng đó ngốn rất nhiều siliconNếu chỉ mô phỏng Verilog thì có thể không thấy được điều đó
Dự án dừng ở mức mô phỏng; để biến nó thành phần cứng thật thì cần nhiều việc hơn rất nhiều
Cá nhân tôi nghĩ những thứ như thế nên được gọi bằng tên khác
Khoảng cách giữa hai bên đang thu hẹp và cả hai đều bổ sung tính năng của nhau, nhưng vẫn có khác biệt đáng kể
Theo tôi, điều này liên quan đến định luật Amdahl [0]
Theo nghĩa đó, CPU có thể được gọi là bộ xử lý tối ưu cho độ trễ, còn GPU là bộ xử lý tối ưu cho thông lượng
Cụ thể hơn, [1] cũng có thể gọi CPU là bộ xử lý cho phụ thuộc dữ liệu dài và sâu, còn GPU là bộ xử lý cho phụ thuộc dữ liệu rộng và phẳng
[0]: https://en.wikipedia.org/wiki/Amdahl%27s_law
[1]: https://en.wikipedia.org/wiki/Data_dependency
Tensor chỉ là mảng n chiều
Có thể đặt phần mềm hoặc firmware lên đó để khiến nó hoạt động giống GPU
Tôi đã cố ghép các mảnh từ mã nguồn EDK2, nhưng không rõ phần nào là đặc thù của QEMU
tiny-gpugiả định mọi luồng “hội tụ” về cùng một bộ đếm chương trình sau mỗi lệnh là một đơn giản hóa quá ngây thơTrong GPU thực tế, từng luồng có thể rẽ nhánh sang PC khác, và xuất hiện phân kỳ nhánh, nơi một nhóm luồng ban đầu được xử lý cùng nhau tách ra thành các luồng thực thi riêng
Lẽ ra nên thử lập trình GPU trước khi làm GPU bằng silicon
Hơn nữa, gọi là SIMD cũng có vẻ không hẳn đúng
Đây chính là người trước đây từng nối các mạch của người khác lại để làm LED nhấp nháy rồi nói là mình đã tạo CPU
__syncthreads()sau mỗi lần thực thi sao?