- GPU có tốc độ tính toán vượt xa tốc độ truy cập bộ nhớ, vì vậy hệ thống phân cấp bộ nhớ trở thành nút thắt cổ chai của hiệu năng
- Tùy theo cường độ tính toán (Arithmetic Intensity, AI), tác vụ được phân thành trạng thái bị giới hạn bởi bộ nhớ hoặc bị giới hạn bởi tính toán; ngưỡng của GPU A100 là khoảng 13 FLOPs/Byte
- Hai chiến lược chính để tối ưu hiệu năng là gộp phép toán (Fusion) và chia ô (Tiling); Fusion giảm các lần qua lại bộ nhớ không cần thiết, còn Tiling tối đa hóa việc tái sử dụng dữ liệu
- Việc hiểu các đặc tính cấu trúc của phần cứng GPU như đồng bộ hóa, Coalesced Load, xử lý xung đột bank là rất quan trọng để viết kernel hiệu năng cao
- Các yếu tố bổ sung như mức chiếm dụng (Occupancy), giảm thiểu phân nhánh luồng, lượng tử hóa (Quantization) cũng ảnh hưởng lớn đến hiệu năng thực tế
Cấu trúc phân cấp compute và bộ nhớ của GPU
- GPU nói chung có tốc độ xử lý phép toán số học cao hơn rất nhiều so với băng thông bộ nhớ
- Ví dụ, NVIDIA A100 đạt khoảng 19.5 TFLOPS (dấu phẩy động 32-bit), trong khi băng thông bộ nhớ chỉ ở mức khoảng 1.5TB/s
- Trong lúc đọc 4 byte dữ liệu, GPU có thể thực hiện hàng chục phép tính, nên việc di chuyển dữ liệu mới là nút thắt hiệu năng
- Bộ nhớ toàn cục (VRAM) là bộ nhớ off-chip chậm nơi toàn bộ dữ liệu tồn tại, còn Streaming Multiprocessor (SM) chịu trách nhiệm tính toán
- Mỗi SM có Shared Memory (SRAM) on-chip tốc độ cao, có thể được dùng như một bộ nhớ đệm do chương trình trực tiếp quản lý
- Thread là đơn vị thực thi nhỏ nhất, và mỗi thread có một tập thanh ghi riêng
- 32 thread tạo thành một Warp, còn Block là lưới thread sẽ được thực thi trên cùng một SM
Các vùng hiệu năng: memory-bound vs compute-bound
- Hiệu năng của kernel sẽ rơi vào một trong hai dạng: bị giới hạn bởi bộ nhớ (tốc độ di chuyển dữ liệu) hoặc bị giới hạn bởi tính toán (năng lực xử lý của SM)
- Cường độ tính toán (AI) được định nghĩa là Total FLOPs / Total Bytes Accessed, và đây là một chỉ số quan trọng
- Mô hình Roofline: trên đồ thị có trục x là AI và trục y là FLOPS/s, nó biểu diễn hiệu năng thực tế mà kernel có thể đạt được
- Nếu AI thấp và bị memory-bound thì nằm trên đường chéo (bậc băng thông bộ nhớ)
- Nếu AI cao và bị compute-bound thì nằm trên đường ngang (bậc hiệu năng tính toán tối đa)
- Ridge Point của A100 là 19.5 TFLOPS / 1.5 TB/s ≈ 13 FLOPs/Byte
- Tăng AI sẽ cải thiện hiệu năng và có thể đưa kernel sang trạng thái compute-bound
Chiến lược tăng cường độ tính toán
- Mô hình đơn giản: một thread tính một giá trị C[i,j] → AI = 0.25 (rất thấp, memory-bound)
- Ngay cả khi một thread tính ô 2x2 thì AI = 0.5 (vẫn thấp)
- Để tăng AI, nhiều thread phải cùng nạp các tile lớn vào Shared Memory theo đơn vị block để tối đa hóa tái sử dụng dữ liệu
- Nhờ sự phối hợp giữa các thread trong block, có thể nâng AI > 13 để đi vào vùng compute-bound
Trạng thái bị giới hạn bởi overhead
- Có thể phát sinh overhead trong quá trình CPU (host) giao việc cho GPU
- Nếu kernel GPU quá nhỏ hoặc quá nhiều, GPU sẽ rơi vào trạng thái chờ việc
- Các framework hiện đại áp dụng thực thi bất đồng bộ để xếp hàng trước command stream, nhờ đó giảm thiểu overhead
Hai chiến lược cốt lõi để tăng hiệu năng: Fusion và Tiling
Operator Fusion (gộp phép toán)
- Với chuỗi phép toán đơn giản, ví dụ: y = relu(x + 1), nếu mỗi phép toán chạy bằng một kernel riêng thì dữ liệu sẽ phải qua lại bộ nhớ toàn cục
- Fusion gộp nhiều phép toán vào một kernel duy nhất, không lưu giá trị trung gian vào bộ nhớ toàn cục mà xử lý trong thanh ghi rồi chỉ ghi kết quả cuối cùng
- Ví dụ: Triton, torch.compile Inductor và các trình biên dịch JIT có thể tự động hóa việc này
Tiling (chia ô)
- Trong các phép toán phức tạp như nhân ma trận, mô hình một thread xử lý đơn lẻ có AI thấp
- Sau khi chia thành các tile theo block, toàn bộ thread trong block sẽ phối hợp nạp tile dữ liệu vào Shared Memory để tái sử dụng dữ liệu ở quy mô lớn
- Việc tính toán đi theo mẫu 3 bước: "Load (global -> Shared Memory) - Synchronize (đồng bộ) - Compute (tính toán)"
Coalesced Load và vector hóa
- Khi chuyển dữ liệu từ bộ nhớ toàn cục sang Shared Memory, Coalesced Access (32 thread trong một warp truy cập vùng 128 byte liên tiếp) là rất quan trọng
- Vector hóa (ví dụ:
float4) cho phép nạp nhiều phần tử trong một lần, giúp tiết kiệm tài nguyên phần cứng và tận dụng tối đa băng thông bộ nhớ
- Căn chỉnh dữ liệu (alignment) là bắt buộc, và giá trị K theo số byte trong ma trận cần là bội số của 4 để đạt hiệu quả
Các bank của Shared Memory và xung đột bank
- Shared Memory gồm 32 bank độc lập, vì vậy 32 thread trong một warp cần truy cập vào các bank khác nhau để tránh xung đột
- Truy cập theo hàng không gây xung đột, còn truy cập theo cột sẽ gây xung đột (nhiều thread chạm cùng một bank)
- Với tile B, chiến lược "nạp và chuyển vị" sẽ lưu dữ liệu dưới dạng chuyển vị trong Shared Memory, nhờ đó khi tính toán có thể ưu tiên truy cập theo hàng để tránh xung đột bank
Mẫu tính toán on-chip tốc độ cao
Chiến lược cơ bản 1: một thread tính một output
- Dưới giới hạn BLOCK_DIM=32, AI tối đa chỉ là 8 nên không thể đi vào vùng compute-bound
Chiến lược 2: một thread tính nhiều output
- Khi đặt BLOCK_DIM=16, TILE_DIM=64, một thread sẽ tính đầu ra 4x4 → AI=16
- Vì AI>13 nên theo chuẩn A100 có thể đạt hiệu năng compute-bound
- Có thể tính toán hiệu quả bằng cách dùng các lệnh nạp vector hóa như
float4 từ Shared Memory
Giới hạn thực tế của tiling: lượng tử hóa tile
- Nếu kích thước ma trận không phải bội số của kích thước tile, các block biên sẽ phải tính trên vùng lớn hơn thực tế (phép tính thừa) và cần được đệm thêm
- Các thread ở biên dùng điều kiện guard để chặn truy cập bộ nhớ không cần thiết, nhưng vòng lặp tính toán vẫn chạy như cũ nên vẫn phát sinh phép tính rác (ví dụ: C += A * 0)
Các yếu tố tinh chỉnh hiệu năng bổ sung
Mức chiếm dụng (Occupancy) và che giấu độ trễ
- Khi một warp phải chờ lâu do đọc bộ nhớ, SM sẽ lập tức chuyển sang warp khác để giảm thời gian nhàn rỗi (che giấu độ trễ, latency hiding)
- Nếu cấp phát đồng thời nhiều Thread Block, có thể giảm thời gian chờ nhờ mức chiếm dụng cao
- Nếu kích thước block hoặc tile quá lớn, số block thường trú sẽ giảm, làm giảm occupancy và kéo theo giảm hiệu năng
Giảm thiểu phân nhánh luồng
- Nếu phát sinh phân nhánh if-else bên trong warp, hai nhánh sẽ bị thực thi tuần tự, khiến hiệu năng hiệu dụng có thể giảm còn khoảng một nửa
- Cần giảm phân nhánh bằng mã không nhánh như
min, max, v.v.
Lượng tử hóa (Quantization)
- Khi giảm độ chính xác từ FP32 xuống FP16/BFP16, lượng dữ liệu cần di chuyển giảm một nửa và số dữ liệu có thể xử lý tăng gấp đôi
- Trên A100, tính toán FP16 có thể đạt 312 TFLOPS (tối đa cao hơn tới 16 lần so với 19.5 TFLOPS của FP32)
- Nhờ lượng tử hóa, có thể đồng thời dịch chuyển sang bên phải trên Roofline (hiệu quả bộ nhớ) và đi lên trên (hiệu năng tính toán tối đa)
Tóm tắt tổng thể
- Giới hạn bản chất của hiệu năng GPU đến từ sự mất cân bằng giữa băng thông bộ nhớ và năng lực tính toán on-chip
- Việc tăng hiệu năng đạt được bằng tối đa hóa tái sử dụng dữ liệu (Tiling) và giảm thiểu lưu lượng bộ nhớ trung gian (Fusion)
- Cần hiểu các đặc tính cấu trúc phần cứng (warp, bank, coalesced access, đồng bộ hóa) để có thể viết và tối ưu kernel hiệu năng cao
- Trong thực tế, các yếu tố như occupancy, giảm phân nhánh, lượng tử hóa ảnh hưởng trực tiếp đến tốc độ thực tế
- Thiết kế tính toán GPU hiệu năng cao đòi hỏi phải đồng thời cân nhắc việc tăng AI về mặt lý thuyết, tận dụng đặc tính phần cứng, cũng như cách bố trí và kích thước dữ liệu thực tế
1 bình luận
Bình luận Hacker News
Tò mò không biết việc tối ưu hóa toàn bộ chương trình ở cấp độ compiler hiện đang được làm tốt đến đâu; cảm giác cách làm hiện tại là tối ưu từng kiến trúc LLM một có phần bị chậm nhịp
Chia sẻ trải nghiệm chạy
llama.cppvàvllmtrên cùng một chiếc 4070 để cố xử lý nhiều prompt hơn theo batch; từ batch 8 trở đillama.cppchậm đi nghiêm trọng, và dù mức sử dụng GPU trông vẫn ổn nhưng thực tế đã xuất hiện nút thắt cổ chai, trong khivllmxử lý tốt hơn hẳnvllmdùng paged KV cache và layout fully coalesced mà GPU ưa thích nên cho hiệu năng tối ưu cho batch, trong khillama.cppdùng flat layout tốt cho prompt đơn lẻ nên trong tình huống batch, pattern truy cập bộ nhớ L2 bị phá vỡ và tốc độ giảmChia sẻ trải nghiệm rằng khi interleave KV tensor trong
llama.cpptừ[seq, head, dim]sang dạng[head, seq, dim], tức là đi theo cáchvllmcấp dữ liệu cho fused attention kernel, hiệu năng tính toán tăng ngay khoảng 2 lầnNguyên nhân của nút thắt không nằm ở bản thân GPU mà ở cách thiết kế truy cập shared memory và global read;
vllmnhắm đúng điểm đó bằng cách thay đổi layoutViệc phân tích nút thắt này mất hơn 2 ngày, không thể nhận ra chỉ bằng biểu đồ mức sử dụng GPU, và phần lớn là học được qua quá trình thử-sai
Đặt câu hỏi liệu có cách nào để lặp lại các thí nghiệm kiểu này dễ hơn, theo phong cách hot reload hay không
Có ý kiến chỉ ra rằng dù nói GPU không phải nút thắt, trên thực tế sự kém hiệu quả của memory layout cuối cùng vẫn trở thành nút thắt làm giảm hiệu suất tính toán của GPU
Nhắc đến dự án
nano-vllmdo nhân viên DeepSeek công bố hôm qua; chỉ khoảng 1.200 dòng nhưng được cho là ghi nhận hiệu năng còn nhanh hơnvllmthuần https://github.com/GeeeekExplorer/nano-vllmHỏi liệu layout đã thay đổi đó đã được đưa lên pull request cho
llama.cppchưa, vì mức cải thiện 2 lần có thể là lợi ích rất lớn cho mọi ngườiCũng gợi ý thử dự án
ik_llama.cpphttps://github.com/ikawrakow/ik_llama.cppNhận xét đây là một bài viết có nhiều thông tin hữu ích, và nội dung này thực chất là câu chuyện về những yếu tố mà NVIDIA lựa chọn khi phát triển kiến trúc GPU, đồng thời nhấn mạnh đừng hiểu sai sự khác biệt với các hãng khác
Ví dụ, AMD Instinct MI300 có tối đa 160 TFLOPS ở FP32 và băng thông HBM3/3E 6TB/s, làm thay đổi ridge-point; điều này tương ứng 27 FLOPs/byte, gấp đôi mức 13 FLOPs/byte của A100. Bộ nhớ HBM dung lượng lớn (128~256GB) cũng làm thay đổi cả trade-off thực tế giữa tiling depth và occupancy. Tuy nhiên, loại GPU này đắt tiền và có trade-off là không hỗ trợ CUDA
Có ý kiến cho rằng cho đến khi AMD quan tâm nhiều hơn tới phần mềm tính toán, GPU của NVIDIA vẫn sẽ là bên có chỗ đứng rõ rệt
Spoiler là điều thực sự quan trọng không phải bản thân cơ chế hoạt động của GPU, mà là cách tận dụng nó cho các phép tính machine learning
relucùng việc nhắc đến torch thì không liên quan nhiều đến machine learningCó ý kiến cho rằng nhất định phải dùng màu tương phản, nhấn mạnh tính dễ đọc
Chia sẻ trải nghiệm dùng
font-weight: 300; phần lớn designer dùng Mac phát triển theo các tùy chọn font smoothing nên thường thiết lập để nhìn ra cảm giác là "normal", trong khi Mac khiến font mảnh cũng trông dày hơn một chút. Vì vậy designer có xu hướng dùng font mảnh hơn để tạo cảm giác "bình thường". Kèm theo liên kết liên quan https://news.ycombinator.com/item?id=23553486Phỏng đoán tác giả có thể đang chỉnh sửa và định dạng bằng dark mode; nếu bật
edge://flags/#enable-force-darkthì liên kết nhìn rõ hơnCó ý kiến nói rằng các liên kết và chú thích trong code block đặc biệt khó đọc, cần phải cố gắng hơn bình thường mới đọc được; đề xuất tăng tương phản, đồng thời đánh giá chất lượng nội dung tự thân là rất tốt
Chỉ trích đây là một sai lầm lớn khi website dùng alpha transparency cho văn bản, làm giảm độ tương phản một cách nghiêm trọng
Đề xuất rằng tiêu đề phù hợp hơn có lẽ nên là những sự thật cơ bản về GPU Nvidia; giải thích thêm rằng thuật ngữ WARP cũng là đặc trưng của GPU Nvidia hiện đại, còn GPU Nvidia khoảng năm 2003 là phần cứng chỉ phục vụ render video game, hoàn toàn khác với GPU tính toán đa dụng ngày nay. Tóm lại, nội dung bài đăng không phải là phần giải thích mang tính tổng quát có thể áp dụng cho mọi GPU
Có ý kiến cảm ơn vì đây thực sự là tài liệu nhập môn rất tốt; khi tự lắp một AI PC, người viết đã phải tìm hiểu về GPU suốt nhiều ngày, và bài này giúp hệ thống hóa rất tốt cả những điều cốt lõi bắt buộc phải biết lẫn lĩnh vực ứng dụng giá trị cao là generative AI, nên rất hữu ích. Đặc biệt, sơ đồ cấu trúc phân cấp bộ nhớ của GPU A100 được đánh giá là cực kỳ hữu dụng
Thắc mắc về việc sử dụng sơ đồ ASCII