
Làm chủ CUDA và Máy tính hiệu năng cao, Phần V
Mastering CUDA and High-Performance Computing, Part V
Bài viết này đi sâu vào các kỹ thuật tối ưu hóa CUDA nâng cao, vượt ra ngoài những phương pháp cơ bản như coalescing và occupancy. Nó giải thích rằng sau khi giải quyết các nút thắt hiệu năng ban đầu, các nhà phát triển nên tập trung vào việc hiểu **instruction pipeline** để đạt được những cải tiến sâu hơn. Điểm mấu chốt là việc làm chủ tối ưu hóa GPU đòi hỏi phải tìm hiểu kỹ lưỡng hơn về cách các **instruction** được xử lý, từ đó cho phép tinh chỉnh phức tạp hơn so với các giải pháp bề nổi.
Đi sâu từ nội bộ trình biên dịch đến tính toán song song hiệu suất cao
Quy trình hướng dẫn
Có một thời điểm trong công việc tối ưu hóa GPU diễn ra sau phiên lập hồ sơ đầu tiên, sau khi bạn đã nội hóa số học đường mái nhà và ngừng theo đuổi tỷ lệ sử dụng vì mục đích riêng.
Bạn đã sửa chữa những điều hiển nhiên. Sự kết hợp là sạch sẽ. Bộ nhớ chia sẻ được lát gạch. Xung đột ngân hàng không còn nữa. Áp suất đăng ký được đo và cho phép. Cường độ số học nằm phía trên điểm sườn núi. Bạn chạy kernel.
Trình phân tích vẫn hiển thị các điểm dừng.
Các quầy hàng khác nhau. Các gian hàng bộ nhớ được giảm xuống mức chấp nhận được, nhưng có điều gì đó khác đang xảy ra.
SM đang hoạt động, không chờ đợi trên DRAM nhưng vẫn chậm hơn mức trần lý thuyết bởi một yếu tố mà mô hình bộ nhớ không giải thích được.
Đây là lúc trọng tâm chuyển từ chuyển động dữ liệu sang luồng lệnh. Từ nơi dữ liệu tồn tại cho đến cách các hướng dẫn được lên lịch, ban hành và ngừng hoạt động.
Bài học thực sự thứ hai về lập trình GPU là: quy trình hướng dẫn không minh bạch.
Nó có độ sâu, mức độ nguy hiểm và mức trần thông lượng hoàn toàn độc lập với băng thông bộ nhớ. Nó có thể là nút thắt cổ chai ngay cả khi không có bộ nhớ.
Để hiểu được nó đòi hỏi phải tìm hiểu sâu hơn về SM hơn hầu hết các hướng dẫn CUDA từng đi.
Dòng lệnh bên dưới kernel
Mỗi hạt nhân CUDA bạn viết đều được biên dịch thành PTX (Trình diễn trung gian Parallel Thread eXecution của NVIDIA) và từ đó tới SASS: Streaming ASSembler, tập lệnh gốc của GPU.
PTX có thể di động qua nhiều thế hệ GPU trong nhiều dòng GPU. SASS thì không. Nó được gắn với một kiến trúc cụ thể, mã hóa các ràng buộc về thời gian dành riêng cho đường ống trực tiếp thành các bit điều khiển lệnh và là thứ duy nhất thực sự thực thi trên phần cứng.
Hầu hết nhà phát triển CUDA không bao giờ nhìn vào SASS. Đây là một sai lầm: không phải vì bạn cần viết nó mà vì đây là nơi duy nhất bạn có thể xác minh những gì trình biên dịch thực sự tạo ra, xác nhận rằng lựa chọn hướng dẫn phù hợp với ý định của bạn và xác định các điểm nghẽn trong quy trình mà mô hình cấp cao không thể phát hiện được.
Con đường dịch:
Nguồn CUDA (.cu)
↓ [nvcc / clang front-end: Phân tích cú pháp C++, khởi tạo mẫu]
PTX (ISA ảo độc lập với mục tiêu; mở, được ghi lại)
↓ [ptxas: tối ưu hóa dành riêng cho máy, phân bổ đăng ký, tạo SASS]
SASS (cubin; khóa kiến trúc, nhị phân đóng)
↓ [Thời gian chạy CUDA: tải kernel, liên kết tham số, gửi SM]
Đơn vị thực thi SM
Để kiểm tra SASS đối với mọi tệp nhị phân được biên dịch, hãy sử dụng:
# Từ tệp nhị phân CUDA đã biên dịch
cuobjdump --dump-sass my_kernel.cubin
# Hoặc trực tiếp từ tệp PTX được biên dịch cho mục tiêu
ptxas -arch=sm_80 kernel.ptx -o kernel.cubin
cuobjdump --dump-sass kernel.cubin
Khi bạn viết float x = a * b + c, trình biên dịch sẽ đưa ra một lệnh FFMA duy nhất (hợp nhất nhân-cộng) thực hiện cả hai thao tác trong một đường dẫn với một bước làm tròn duy nhất.
Điều này khác về mặt ngữ nghĩa với hai thao tác riêng biệt: (a * b) + c được tính toán với các vòng FFMA một lần ở cuối; float t = a * b; t = t + c; làm tròn hai lần.
Trình biên dịch sẽ kết hợp theo mặc định. Tắt bằng -fmad=false nếu khả năng tái tạo bằng số trong quá trình triển khai có vấn đề, chấp nhận thông lượng giảm gần một nửa trên mã được tính toán.
Luồng lệnh SASS không phải là bản chép lại một-một từ nguồn của bạn. Đây là nỗ lực tốt nhất của trình biên dịch để ánh xạ ý định của bạn tới mô hình thực thi thực tế của phần cứng.
Hiểu được mô hình đó là điều tạo nên sự khác biệt giữa việc đọc SASS dưới dạng nhiễu và đọc nó dưới dạng chẩn đoán.
Độ trễ so với thông lượng
Sự khác biệt về mặt khái niệm quan trọng nhất trong lập luận ở cấp độ quy trình là giữa hướng dẫn độ trễ và hướng dẫn thông lượng. Chúng chi phối các nút thắt cổ chai khác nhau, yêu cầu các cách khắc phục khác nhau và thường bị nhầm lẫn.
Độ trễ là số chu kỳ xung nhịp tính từ thời điểm một lệnh được đưa ra cho đến khi kết quả của lệnh đó có sẵn cho lệnh tiếp theo phụ thuộc vào lệnh đó. Lệnh phụ thuộc không thể phát ra cho đến khi khoảng thời gian này trôi qua.
Thông lượng là nghịch đảo của tốc độ mà các lệnh độc lập có thể truyền qua một đơn vị thực thi, được biểu thị bằng số chu kỳ trên mỗi lệnh. Nó thể hiện công suất ở trạng thái ổn định của quy trình, hoàn toàn bỏ qua sự phụ thuộc vào dữ liệu.
Bảng sau cung cấp các giá trị đo được cho kiến trúc Ampere (A100, sm_80), bắt nguồn từ công trình đo điểm chuẩn vi mô của Abdelkhalik et al. (2022) và được chứng thực qua nhiều nguồn độc lập:
Độ trễ lệnh (chu kỳ) Thông lượng (cyc/instr, trên mỗi SMSP)
────────────────────────────────────── ─────────────────── ────────────────────
FFMA (FP32) 4 0,25 (4 FMA/chu kỳ)
FADD / FMUL (FP32) 4 0,25
IMAD / IADD3 (INT32) 4 0,25
DFMA (FP64) 8 2.0
MUFU.RCP / RSQ 16 4.0
MUFU.SIN / COS 16 4.0
MUFU.EX2 / LG2 16 4.0
HFMA2 (FP16×2) 4 0,25
────────────────────────────────────── ─────────────────── ────────────────────
Tải bộ nhớ dùng chung 23 ~1.0
Kho bộ nhớ dùng chung 19 ~1.0
Lượt truy cập bộ đệm L1 (LDG) ~33 ~1.0
Lượt truy cập bộ đệm L2 (LDG) ~ 200 ~1,0
HBM không được lưu vào bộ nhớ đệm (LDG) ~290–566† ~1,0†Phạm vi phản ánh hai phương pháp đo lường khác nhau: truy đuổi con trỏ thông qua các mảng thường trú đầy đủ (290 chu kỳ, Abdelkhalik và cộng sự) so với truy đuổi con trỏ thông qua một nhóm hoạt động lớn hơn L2, buộc phải thực hiện các chuyến đi khứ hồi đầy đủ HBM và đo được ~566 chu kỳ trên A100 (Shi và cộng sự, 2025). Số thấp hơn xấp xỉ độ trễ L2-miss; số trên gần với tốc độ khứ hồi DRAM nguội thực sự trong điều kiện bão hòa băng thông.
Thông lượng MUFU của 4 chu kỳ cho mỗi lệnh cần được nhấn mạnh. MUFU thực thi các chức năng siêu việt (sinf, cosf, expf, logf, rsqrtf, rcpf) thông qua Đơn vị chức năng đặc biệt, một đường dẫn riêng biệt từ các đơn vị FMA FP32.
Trên A100, mỗi phân vùng phụ SM có một SFU. SFU đó có thể đưa ra một lệnh MUFU cứ sau 4 chu kỳ, trong khi ống FP32 có thể đưa ra bốn FMA mỗi chu kỳ.
Một hạt nhân kết hợp mức độ sử dụng siêu việt nhiều với số học FP32 sẽ đạt thông lượng SFU rất lâu trước khi nó đạt đến thông lượng FP32. Điều này quan trọng đối với các hàm kích hoạt ML (tanhf, expf trong softmax) và bất kỳ hạt nhân khoa học nào sử dụng lượng giác.
Bây giờ là thông tin chi tiết cơ bản: Độ trễ FFMA là 4 chu kỳ, nhưng thông lượng FFMA là 1 lệnh trên 0,25 chu kỳ (4 lệnh mỗi chu kỳ). Bốn FMA FP32 độc lập có thể đi vào đường ống đồng thời trong mỗi chu kỳ.
Nếu không có phần phụ thuộc nào liên kết chúng, cả bốn phần sẽ thực thi song song và quy trình sẽ phát hành một phần mỗi chu kỳ. Nếu mọi lệnh đều đọc kết quả của lệnh trước đó thì quy trình phải đợi 4 chu kỳ giữa mỗi lần phát hành. Trần thông lượng chưa được thực hiện.
Đây không phải là giả thuyết. Đây là chế độ hoạt động vượt trội dành cho các vòng tích lũy đơn giản.
SMSP: đơn vị thực thi thực sự
Một chi tiết quan trọng mà sơ đồ SM của phần trước đã tóm tắt: trên Ampere (và Volta và Turing), SM không phải là nguyên khối. Nó được chia thành bốn phân vùng phụ SM, mỗi phân vùng SMSP được chỉ định trong không gian tên chỉ số Nsight Computing .
Mỗi SMSP chứa:
Một bộ lập lịch dọc (một lệnh được đưa ra trong mỗi chu kỳ)
Một đơn vị điều phối
Bộ nhớ đệm lệnh L0 (riêng tư đối với SMSP)
Tệp thanh ghi 16K×32-bit (64 KB, 16.384 thanh ghi 32-bit cho mỗi SMSP; bốn SMSP mang lại tổng cộng 65.536 cho mỗi SM)
32 lõi CUDA FP32 (trên GA100; Ampere GA10x chơi game sử dụng cách phân chia khác)
16 lõi INT32
8 lõi FP64
1 Tensor Core thế hệ thứ ba
8 đơn vị tải/lưu trữ
SMSP, không phải SM, là nguyên tử lập kế hoạch. Warp được gán cho một SMSP cụ thể khi khởi chạy và duy trì ở đó trong suốt vòng đời của nó. Nó không di chuyển giữa các SMSP.
Tất cả số liệu ngăn chặn sợi dọc trong Nsight Computing mang tiền tố smsp__ đều là bộ đếm trên mỗi SMSP; sm__ số liệu tổng hợp trên cả bốn chỉ số. Phân mục này quan trọng vì hai lý do.
Đầu tiên, nó giải thích kích thước nhóm dọc trên mỗi SMSP. Trên Ampe, mỗi SMSP có thể lưu trữ tối đa 16 sợi dọc. Bốn SMSP mang lại tối đa 64-warp-per-SM . Bộ lập lịch hoạt động trên mỗi SMSP, chọn một sợi dọc đủ điều kiện cho mỗi chu kỳ từ nhóm 16 sợi cục bộ của nó.
Do đó, mức trần thực tế cho việc ẩn độ trễ được xác định theo SMSP chứ không phải theo SM. Một hạt nhân có 32 sợi dọc thường trú trên mỗi SM có 8 sợi trên mỗi SMSP, thường đủ để ẩn độ trễ khi độ trễ HBM là nút thắt cổ chai.
Thứ hai, nó hiển thị phân vùng tệp đăng ký. 65.536 đăng ký trên mỗi SM được phân bổ vật lý trên bốn SMSP, mỗi SMSP là 16.384.
Khi bạn tính toán rằng 32 thanh ghi trên mỗi luồng cho phép 2.048 luồng trên mỗi SM khi chiếm hết công suất, 2.048 luồng đó được trải rộng về mặt vật lý 512 trên mỗi SMSP, mỗi luồng chứa 32 thanh ghi, sử dụng hết 16.384 luồng có sẵn.
Ràng buộc là trên mỗi SMSP và việc vi phạm nó trong một SMSP sẽ giới hạn toàn bộ SM.
Chỉ số Nsight Computing smsp__warps_active.avg.pct_of_peak_sustained_active báo cáo tỷ lệ sợi dọc hoạt động trên mỗi SMSP tính trung bình theo thời gian.
Nó có nhiều thông tin hơn sm__warps_active để chẩn đoán giới hạn sử dụng vì nó phản ánh khả năng lập kế hoạch thực tế của đơn vị thực hiện việc lập kế hoạch.
Phần cứng thực thi biểu đồ phụ thuộc
Trước khi một warp có thể đưa ra lệnh tiếp theo, bộ lập lịch warp phải xác minh rằng tất cả các toán hạng nguồn cho lệnh đó đều khả dụng.
Cơ chế phần cứng cho việc này là bảng điểm, một tệp đăng ký trên mỗi SMSP để theo dõi những đăng ký nào có nội dung chưa được ghi từ các hướng dẫn trong chuyến bay.
Mỗi lệnh được ban hành ghi vào sổ đăng ký sẽ đánh dấu đăng ký là “đang chờ xử lý” trong bảng điểm. Khi lệnh hoàn thành và kết quả được ghi vào tệp thanh ghi, dấu sẽ bị xóa.
Nếu bộ lập lịch chọn một sợi dọc để phát hành và lệnh tiếp theo của sợi dọc đọc một thanh ghi vẫn được đánh dấu đang chờ xử lý thì sợi dọc đó sẽ bị đình trệ. Nó không đủ điều kiện. Bộ lập lịch sẽ chuyển sang sợi dọc tiếp theo.
CUDA phân biệt hai miền bảng điểm dựa trên nguồn của kết quả đang chờ xử lý:
Bảng điểm ngắn (Chỉ số Nsight: smsp__pcsamp_warps_issue_stalled_short_scoreboard): theo dõi các hướng dẫn có độ trễ đủ ngắn để phần cứng sử dụng đồng hồ đếm ngược cố định thay vì tín hiệu hoàn thành.
Điều này bao gồm: số học FP32/INT32/FP16 từ ống FMA, tải bộ nhớ dùng chung (độ trễ 23 chu kỳ), kết quả SFU/MUFU (16 chu kỳ), tải không đổi được lập chỉ mục và hướng dẫn biểu quyết ở cấp độ dọc.
Phần cứng biết chính xác chu kỳ mà tại đó kết quả sẽ sẵn sàng và bỏ chặn mục nhập bảng điểm trong chu kỳ đó.
Bảng điểm dài (Chỉ số Nsight: smsp__pcsamp_warps_issue_stalled_long_scoreboard): theo dõi các hướng dẫn có thời gian hoàn thành không cố định trước.
Điều này bao gồm tất cả các lần tải từ bộ nhớ chung ( lượt truy cập L1 (~33 chu kỳ), lượt truy cập L2 (~200 chu kỳ), HBM (~290–566 chu kỳ)) và bất kỳ nội dung nào đi qua đường dẫn L1TEX.
Phần cứng không thể dự đoán khi nào dữ liệu sẽ đến; nó chờ tín hiệu ghi lại rõ ràng từ hệ thống con bộ nhớ.
Sự phân chia này có ý nghĩa chẩn đoán quan trọng. stall_long_scoreboard cao nghĩa là các luồng đang chờ dữ liệu từ DRAM. Cách khắc phục là chiếm chỗ (nhiều đường cong hơn để trao đổi trong khi chờ), tìm nạp trước hoặc bố cục dữ liệu được cơ cấu lại.
stall_short_scoreboard cao nghĩa là các luồng đang chờ kết quả số học hoặc bộ nhớ dùng chung: một nút thắt cổ chai phụ thuộc trong chính luồng lệnh. Cách khắc phục là song song ở cấp độ hướng dẫn chứ không phải chiếm chỗ.
Lớp gian hàng thứ ba hoàn thành bức tranh: MIO ga (stall_mio_throttle). Điều này xuất hiện khi đầu vào FIFO tới đường dẫn I/O bộ nhớ đã đầy: có quá nhiều yêu cầu bộ nhớ chưa xử lý đã được thực hiện và các yêu cầu mới không thể được chấp nhận.
Nó khác với stall_long_scoreboard. Điều thứ hai có nghĩa là một sợi dọc đang chờ một kết quả cụ thể. Điều đầu tiên có nghĩa là Warp thậm chí chưa thể gửi yêu cầu mới.
Điều tiết MIO là dấu hiệu của quyền truy cập bộ nhớ toàn cầu nặng nhưng kết hợp kém, trong đó nhiều hoạt động bộ nhớ 32 giao dịch độc lập đang tràn ngập hàng đợi cùng một lúc.
Và điều thứ tư: điều tiết đường ống toán học (stall_math_pipe_throttle). Điều này xuất hiện khi một sợi dọc sẵn sàng đưa ra lệnh FP32 (hoặc FP64 hoặc tensor) nhưng đường dẫn thực thi đã bị chiếm bởi các lệnh từ các sợi dọc khác.
Đây là một bước dừng tốt: nó có nghĩa là thông lượng số học, chứ không phải bộ nhớ hay phần phụ thuộc, mới là mức trần thực tế. Trên hạt nhân liên kết tính toán được tinh chỉnh tốt, stall_math_pipe_throttle sẽ chiếm ưu thế trong việc phân tích tình trạng ngừng hoạt động.
Chẩn đoán phân biệt trong phần Thống kê trạng thái Warp của Nsight Computing là công cụ phân tích đơn lẻ mạnh mẽ nhất hiện có sau đường mái. Việc đọc lý do đình trệ chính sẽ ánh xạ trực tiếp đến lớp tối ưu hóa cần thiết:
stall_long_scoreboardchiếm ưu thế → độ trễ bộ nhớ. Khắc phục: nhiều cong vênh hơn, xếp lớp tốt hơn, tìm nạp trước không đồng bộ.stall_short_scoreboardchiếm ưu thế → chuỗi phụ thuộc số học. Khắc phục: ILP, hủy kiểm soát vòng lặp, bộ tích lũy độc lập.stall_mio_throttlechiếm ưu thế → bão hòa hàng đợi yêu cầu bộ nhớ. Khắc phục: kết hợp lại, tải vector hóa, giảm số lượng lệnh bộ nhớ.stall_math_pipe_throttlechiếm ưu thế → giới hạn tính toán. Khắc phục: lõi tensor nếu sử dụng FP16/FP32 vô hướng, độ chính xác hỗn hợp hoặc chỉ chấp nhận rằng bạn đã đạt đến mức cao nhất.stall_barrierchiếm ưu thế → cấu trúc đồng bộ hóa. Khắc phục: giảm phạm vi rào cản, phân công công việc tốt hơn, nhóm hợp tác.-
stall_not_selectedchiếm ưu thế → quá nhiều đường cong đủ điều kiện, không đủ băng thông vấn đề. Phạt trên ~20%; cao bất thường có nghĩa là bạn đăng ký nhiều với ILP rất cao và có khả năng tăng độ phức tạp trên mỗi chuỗi.
Chuỗi phụ thuộc
Hãy xem xét một ví dụ điển hình: tích số chấm trên một mảng cố định, không được kiểm soát.
float acc = 0,0f;
cho (int i = 0; i < N; i++) {
acc += a[i] * b[i];
Sau khi tải tất cả a[i] và b[i] vào các thanh ghi (bây giờ bỏ qua việc tải), phần thân vòng lặp bên trong sẽ biên dịch thành một chuỗi lệnh FFMA , mỗi lần ghi vào acc và đọc kết quả của lần trước:
FFMA R4, R6, R8, R4 // acc += a[0]*b[0]; R4 phụ thuộc vào R4
FFMA R4, R10, R12, R4 // acc += a[1]*b[1]; R4 phụ thuộc vào R4 từ trước
FFMA R4, R14, R16, R4 // acc += a[2]*b[2]; R4 phụ thuộc vào R4 từ trước
...
Mỗi FFMA ghi vào R4 và đọc từ R4. Bảng điểm ngắn đánh dấu R4 là đang chờ xử lý trong 4 chu kỳ sau mỗi lần phát hành. Lệnh tiếp theo đọc R4, thấy nó đang chờ xử lý và dừng lại. Tỷ lệ phát hành hiệu quả là 1 FFMA trên 4 chu kỳ.
Đường dẫn FP32 trên A100 SMSP có thể phát ra 4 FMA mỗi chu kỳ khi được cung cấp công việc độc lập. Sự tích lũy ngây thơ này sử dụng 1/16 công suất đó.
Cách khắc phục là các bộ tích lũy độc lập, phá vỡ chuỗi nối tiếp thành các chuỗi song song mà phần cứng có thể xen kẽ:
float acc0 = 0,0f, acc1 = 0,0f, acc2 = 0,0f, acc3 = 0,0f;
vì (int i = 0; i < N; i += 4) {
acc0 += a[i+0] * b[i+0];
acc1 += a[i+1] * b[i+1];
acc2 += a[i+2] * b[i+2];
acc3 += a[i+3] * b[i+3];
}
float acc = (acc0 + acc1) + (acc2 + acc3);
SASS hiện có bốn chuỗi phụ thuộc độc lập. Sau FFMA R4, ..., R4, bộ lập lịch có thể phát hành ngay FFMA R5, ..., R5, FFMA R6, ..., R6 và FFMA R7, ..., R7.
Khi Độ trễ 4 chu kỳ của FFMA đầu tiên hết hạn và R4 sẵn sàng, lệnh FFMA R4 tiếp theo có thể phát ra. Đường ống chạy với thông lượng gần đạt mức cao nhất.
Đây không phải là một sự tối ưu hóa vi mô bên lề. Trên các hạt nhân liên kết tính toán với chuỗi tích lũy dài (rút gọn, tích số chấm, GEMM nhỏ được viết không có lõi tensor), sự khác biệt giữa dạng tích lũy nối tiếp và dạng tích lũy độc lập thường là 4–8× thông lượng.
Trình biên dịch thực hiện việc hủy kiểm soát này một cách tự động trong nhiều trường hợp khi #pragma unroll được sử dụng hoặc khi số lần ngắt vòng lặp được biết tại thời điểm biên dịch và nhỏ.
Nó không bỏ cuộn một cách đáng tin cậy trên các phần thân vòng lặp phức tạp, thông qua ranh giới lệnh gọi hàm hoặc khi bộ tích lũy được truy cập thông qua con trỏ (mà trình biên dịch có thể giả định là bí danh).
Để xác minh: kiểm tra SASS. Nếu bạn thấy FFMA R4, ..., R4 lặp lại với cùng một thanh ghi đích thì chuỗi phụ thuộc được được tuần tự hóa. Nếu bạn thấy FFMA R4, FFMA R5, FFMA R6, FFMA R7 quay vòng, trình biên dịch đã tìm thấy biểu mẫu tích lũy độc lập.
Trình phân tích tài nguyên có thể xác nhận thông qua smsp__pcsamp_warps_issue_stalled_short_scoreboard — nếu số liệu này không tầm thường trên hạt nhân giới hạn tính toán thì biểu đồ phụ thuộc đang hạn chế thông lượng.
Đăng ký xung đột ngân hàng
Tệp đăng ký trên mỗi SMSP là SRAM bốn dãy, trong đó mỗi dãy có chiều rộng 4 byte. Một Lệnh FFMA đọc tối đa ba thanh ghi nguồn (hai số nhân và một phần phụ) và một đích.
Nếu hai hoặc nhiều nguồn đăng ký một ánh xạ lệnh đơn vào cùng một dãy thì các lần đọc sẽ được tuần tự hóa. Mỗi thanh ghi tại chỉ mục R ánh xạ tới ngân hàng R % 4 (trên Ampere; các kiến trúc trước đó sử dụng các mô-đun và độ rộng khác nhau).
Đối với FFMA có nguồn R4, R8, R12: các ngân hàng là 0, 0, 0. Cả ba lần đọc đều xung đột. Tệp đăng ký phải thực hiện ba thao tác đọc tuần tự vào ngân hàng 0, thêm độ trễ cho lệnh ngay cả khi không tồn tại sự phụ thuộc dữ liệu.
Đối với FFMA có nguồn R4, R5, R6: các ngân hàng là 0, 1, 2. Không có xung đột. Cả ba vấn đề đọc đều diễn ra song song.
Điều này gây ra hậu quả trực tiếp cho việc hủy bỏ vòng lặp tích lũy. Xét bốn bộ tích lũy R4, R5, R6, R7. Nếu nhân cho mỗi vùng đất ở:
FFMA R4, R8, R12, R4 // các ngân hàng: 0, 0, 0, 0 → xung đột trên R8, R12, R4
FFMA R5, R9, R13, R5 // ngân hàng: 1, 1, 1, 1 → xung đột
Tất cả ba nguồn của mỗi bản đồ FFMA tới cùng một ngân hàng (vì 8%4=0, 12%4=0, 4%4=0). Mọi hướng dẫn đều dừng lại trong nội bộ. Lợi ích thông lượng của ILP bị ảnh hưởng một phần do xung đột ngân hàng đăng ký.
Bộ cấp phát thanh ghi của trình biên dịch nhận thức được điều này và cố gắng gán các thanh ghi để tránh xung đột trong các chuỗi lệnh nóng. Nhưng nó hoạt động dưới hạn chế áp suất đăng ký và không phải lúc nào cũng làm như vậy.
Khi cần gán thanh ghi thủ công để có hiệu suất cao nhất (như trong hạt nhân sgemm được điều chỉnh bằng tay), điều này đòi hỏi sự chú ý rõ ràng đến việc phân phối ngân hàng.
Để kiểm tra: trong đầu ra SASS từ Chế độ xem nguồn của Nsight Comput, chỉ báo xung đột ngân hàng xuất hiện theo hướng dẫn khi bộ đếm phần cứng smsp__sass_data_bank_conflicts_pipe_fma_cycles_active khác 0.
Các giá trị trên 5% trên hạt nhân liên quan đến điện toán cho thấy việc phân bổ đăng ký đang hạn chế thông lượng đường ống.
Kiểm soát sự phân kỳ
Sự khác biệt được hiểu rõ ở cấp độ khái niệm nhưng thường bị mô hình hóa sai về mặt định lượng.
Mô hình thực thi SIMT trên GPU NVIDIA hậu Pascal (Volta trở lên) sử dụng lập lịch luồng độc lập: mỗi luồng có bộ đếm và ngăn xếp chương trình riêng, đồng thời phần cứng có thể hội tụ lại các luồng đã phân kỳ.
Điều này đã thay thế mô hình tiền Volta trong đó các luồng bị khóa ở bước khóa cấp độ dọc bằng mặt nạ SIMD rõ ràng.
Điều lập kế hoạch luồng độc lập cung cấp:
Các luồng có thể phân kỳ mà không bị mắc kẹt vĩnh viễn trong các đường dẫn thực thi riêng biệt cho đến khi có điểm hội tụ rõ ràng.
Bộ lập lịch có thể xen kẽ các hướng dẫn từ các nhóm phụ khác nhau trong một sợi dọc để cải thiện việc sử dụng.
Những gì nó không cung cấp:
Việc thực thi SIMT vẫn còn trên toàn bộ 32. Khi một sợi dọc phân kỳ, phần cứng sẽ thực thi đường dẫn đã lấy và đường dẫn không được lấy tuần tự, che giấu các luồng không hoạt động. Những thay đổi về lập lịch luồng độc lập khi có thể xảy ra sự hội tụ lại, chứ không phải liệu cả hai đường dẫn có phải thực thi hay không.
Chi phí thực sự của một điều kiện phân kỳ không phải là “hiệu suất 50% nếu các luồng chia thành 50/50.”. Đó là tổng thời gian thực hiện cho tất cả các đường dẫn riêng biệt thông qua điều kiện, không phải là mức tối đa.
Nếu một nửa luồng đi theo đường dẫn A (10 hướng dẫn) và một nửa đi theo đường dẫn B (20 hướng dẫn), thì tổng thời gian thực hiện dọc là ~30 chu kỳ lệnh, không phải ~20.
Đối với các điều kiện lồng nhau, chi phí sẽ được gộp lại. Hạt nhân có điều kiện lồng nhau hai cấp trong đó mỗi cấp có độ phân kỳ 50/50 có thể thấy tốc độ chậm lại 4× so với thực thi hội tụ hoàn toàn.
Mã hoạt động SASS BRA (nhánh) được bắt đầu bằng một đánh giá vị từ. Tất cả các chủ đề đều đánh giá vị ngữ; sau đó sợi dọc sẽ xuất hiện dọc theo đường dẫn đã chọn với các luồng không được xác định trước được che dấu.
Điểm hội tụ được mã hóa trong các lệnh SSY (đặt đồng bộ hóa) và SYNC trong SASS, được chèn bởi trình biên dịch.
Chỉ số chẩn đoán là smsp__sass_thread_inst_executed_op_control.sum liên quan đến tổng hướng dẫn: chi phí kiểm soát cao so với hướng dẫn số học cho thấy có sự phân kỳ lớn hoặc chi phí vòng lặp.
Phần Bộ đếm nguồn của Nsight Computing hiển thị số lần thực thi luồng theo lệnh; các hướng dẫn trong đường dẫn đã chọn của một nhánh khác nhau hiển thị ít lần thực thi luồng hơn so với các hướng dẫn bên ngoài nhánh.
ý nghĩa thực tế: dành cho các hạt nhân có phân nhánh phụ thuộc vào dữ liệu (phân tích cú pháp, duyệt cây, xử lý định dạng thưa thớt), giảm thiểu số lượng các đường dẫn thực thi mỗi dọc riêng biệt quan trọng hơn việc giảm thiểu tổng số điều kiện.
Một câu điều kiện phân kỳ 32 chiều sẽ tốt hơn 8 câu điều kiện 2 chiều.
Kết luận
Sau khi phân tích đường mái, kết hợp bộ nhớ và sắp xếp cẩn thận, hầu hết các nhân GPU không còn bị giới hạn bởi băng thông nữa: chúng bị giới hạn bởi chính đường dẫn lệnh.
Hiểu quy trình có nghĩa là nhìn ngoài PTX và vào SASS: luồng lệnh thực được thực thi bởi phần cứng, với độ trễ, trần thông lượng và phân vùng tài nguyên.
Tắc nghẽn ở cấp độ lệnh (chuỗi phụ thuộc, xung đột ngân hàng đăng ký và phân kỳ kiểm soát) thường chi phối hiệu suất ngay cả khi tình trạng thiếu bộ nhớ ở mức tối thiểu.
Các số liệu như bảng điểm ngắn và dài không ổn định, việc sử dụng ống toán học và hoạt động sai lệch cấp SMSP cung cấp cánh cửa đáng tin cậy duy nhất để nhìn vào các giới hạn ẩn này.
Bài học thực tế rất đơn giản nhưng sâu sắc: để đẩy hạt nhân tính toán lên đỉnh cao về mặt lý thuyết, bạn phải coi quy trình hướng dẫn là địa hình hạng nhất.
Giải phóng các vòng lặp thành các bộ tích lũy độc lập, phân bổ đăng ký số dư để tránh xung đột ngân hàng, giảm thiểu các đường dẫn khác nhau và tính đến các đơn vị chuyên biệt như SFU.
Chỉ bằng cách suy luận ở cấp độ luồng lệnh, thay vì chỉ di chuyển dữ liệu, bạn mới có thể đạt đến giới hạn thực sự của hiệu suất GPU.
Tác giả: Lorenzo Bradanini
