Mở đầu: khi băng thông đã đủ, bức tường tiếp theo là tính toán

Bài 9 giải quyết việc đưa dữ liệu tới đúng chỗ nhanh nhất (UMA loại bỏ bước sao chép PCIe). Nhưng có dữ liệu nhanh chưa đủ — CPU vô hướng vẫn phải TÍNH TOÁN từng phần tử MỘT theo thứ tự. Đồ hoạ 3D và deep learning đều xoay quanh MỘT phép toán lõi: nhân ma trận. Bài này so sánh 3 kiến trúc phần cứng xử lý CÙNG phép toán đó với thông lượng khác biệt tới hàng nghìn lần.


📚 Điều kiện tiên quyết
Nên đọc Bài 9 (Apple Silicon & UMA) — bài này tiếp nối trực tiếp câu chuyện "tăng tốc phần cứng chuyên dụng" sang khía cạnh TÍNH TOÁN thay vì băng thông.

1. Kiến trúc SIMD vs SIMT

SIMD (Single Instruction, Multiple Data) — CPU hiện đại có các thanh ghi vector rộng (vd AVX-256 xử lý 8 số 32-bit CÙNG lúc trong MỘT lệnh) — vẫn là kiến trúc scalar cơ bản, chỉ "nới rộng" từng lệnh để xử lý nhiều dữ liệu hơn. SIMT (Single Instruction, Multiple Threads) — cách GPU tổ chức: hàng nghìn Shader Core nhỏ, đơn giản, chạy CÙNG một lệnh nhưng trên hàng nghìn luồng dữ liệu độc lập song song — khác về BẢN CHẤT so với việc "nới rộng lệnh" của SIMD.

⚠️ Cạm bẫy: GPU không thể thay thế CPU cho mọi chương trình
GPU cực mạnh cho khối lượng công việc SONG SONG THUẦN TUÝ (mỗi luồng làm việc độc lập, không rẽ nhánh khác nhau) — nhưng RẤT YẾU với code rẽ nhánh tuần tự phức tạp (if/else lồng nhau, vòng lặp phụ thuộc dữ liệu lẫn nhau): mỗi Shader Core đơn giản hơn hẳn một nhân CPU, và khi các luồng trong CÙNG một nhóm (warp) rẽ nhánh KHÁC hướng nhau, GPU phải chạy TUẦN TỰ cả 2 nhánh cho từng nhóm luồng — mất hẳn lợi thế song song.

2. Kiến trúc NPU & Apple AMX

NPU (Neural Processing Unit) và Tensor Core là phần cứng CHUYÊN DỤNG chỉ để tăng tốc MỘT phép toán: nhân ma trận (và các biến thể tích chập convolution) — nhanh hơn hàng chục lần so với chạy phép toán tương đương trên CPU thông thường, nhờ thiết kế phần cứng CHỈ để làm một việc thay vì linh hoạt như CPU. Apple AMX (Apple Matrix Coprocessor) là khối đồng xử lý ma trận ẨN ngay trong lõi CPU Apple Silicon — không cần chuyển dữ liệu sang GPU (tiếp nối tinh thần UMA của Bài 9), CPU tự "gọi thẳng" tập lệnh AMX riêng khi chạy qua Accelerate framework hoặc CoreML.

simd_vs_simt.txt (khác biệt tổ chức song song)
SIMD (CPU vector, vd AVX-256):
  1 lenh --> xu ly CUNG LUC 8 so 32-bit trong 1 thanh ghi rong
  Van la 1 loi thuc thi (control flow), chi "no rong" DU LIEU

SIMT (GPU, vd CUDA/Metal):
  1 lenh --> hang nghin Shader Core doc lap chay CUNG lenh do
  MOI Shader Core co du lieu RIENG - that su nhieu LUONG thuc thi song song
  (neu cac luong trong 1 nhom RE NHANH khac huong -> phai chay TUAN TU ca 2 nhanh)
⚠️ Cạm bẫy: lượng tử hoá (Quantization) sai làm sập chất lượng mô hình
Để chạy nhanh hơn trên NPU/Tensor Core, trọng số mô hình AI thường bị lượng tử hoá từ FP32 (32-bit dấu phẩy động) xuống INT8 (8-bit số nguyên) — giảm 4 lần dung lượng và tăng tốc đáng kể. Nhưng nếu làm KHÔNG CẨN THẬN (không hiệu chỉnh đúng khoảng giá trị, bỏ qua các lớp nhạy cảm), độ chính xác mô hình có thể SỤT GIẢM NGHIÊM TRỌNG — lượng tử hoá là kỹ thuật cần đo đạc cẩn thận, không phải "bật công tắc là xong".
accelerator_comparison.txt (so sánh nhanh 3 hướng tăng tốc)
Kien truc      | Muc dich chinh              | Vi du
---------------|------------------------------|---------------------------
CPU + SIMD     | Da nang, phep toan da dang   | AVX/AVX-512 (x86), NEON (ARM)
GPU (SIMT)     | Song song HANG NGHIN luong   | Do hoa, huan luyen mo hinh lon
NPU/TensorCore | Chuyen dung nhan ma tran     | Suy luan AI tren thiet bi di dong
Apple AMX      | Nhan ma tran AN trong CPU    | Accelerate framework, CoreML
                 (khong can chuyen du lieu sang GPU - tiep noi tinh than UMA Bai 9)

3. Bài toán tính toán thông lượng FLOPs

Nhân 2 ma trận $N \times N$ theo thuật toán tuần tự kinh điển: mỗi trong $N^2$ phần tử kết quả cần $N$ phép nhân + $(N-1)$ phép cộng — tổng số phép tính dấu phẩy động (FLOPs):

$$\text{Total FLOPs} = N^2 \times (2N - 1) = 2N^3 - N^2$$

Verified thật: nhân ma trận 1024×1024 cần đúng 2.146.435.072 FLOPs (~2,15 tỷ). So sánh thời gian tính CÙNG phép toán này trên 3 kiến trúc — CPU vô hướng (4 GFLOPS), CPU SIMD (32 GFLOPS, tương đương AVX 8-wide = 8× scalar), và GPU (10 TFLOPS):

flops_compare.js (trích engine dùng chung cpu-core.js)
function matrixMultiplyFlops(n) {
  return 2 * Math.pow(n, 3) - Math.pow(n, 2);
}
function computeTimeSeconds(flops, flopsPerSecond) {
  return flops / flopsPerSecond;
}
// Verified: matrixMultiplyFlops(1024) = 2.146.435.072 FLOPs
// Verified: scalar (4 GFLOPS)  -> 0,5366 giay
// Verified: SIMD (32 GFLOPS)   -> 0,0671 giay (nhanh hon scalar DUNG 8 lan = 32/4)
// Verified: GPU (10 TFLOPS)    -> 0,000215 giay = 0,215 ms (nhanh hon scalar 2500 lan)

SIMD nhanh hơn scalar đúng 8 lần — CHÍNH XÁC bằng độ rộng vector (không phải một con số ước lượng). GPU nhanh hơn scalar tới 2500 lần — chênh lệch khổng lồ này đến từ việc GPU có hàng nghìn Shader Core làm việc song song thật sự, khác hẳn việc chỉ "nới rộng" một lệnh như SIMD.

⚠️ Bỏ qua overhead nạp dữ liệu khi ma trận quá nhỏ
Trước khi GPU/AMX có thể tính, dữ liệu phải được nạp vào thanh ghi/bộ nhớ chuyên dụng của nó — một chi phí CỐ ĐỊNH, không phụ thuộc kích thước ma trận. Với ma trận ĐỦ LỚN (vd 1024×1024), chi phí này không đáng kể so với thời gian tính. Nhưng verified thật: với ma trận 4×4 quá nhỏ (chỉ 112 FLOPs), overhead nạp dữ liệu 0,1ms khiến GPU CHẬM HƠN cả CPU vô hướng đơn giản (dù CPU vô hướng "yếu" hơn hẳn về lý thuyết) — tính toán quá ít để bù lại chi phí khởi động.
small_matrix_pitfall.js (verified bằng engine — overhead vượt lợi ích)
// Ma tran 4x4: chi 112 FLOPs - qua nho de GPU "dang" cong suc
const cmpSmall = compareComputeMethods(4, 4, 32, 10, 0.0001); // 0,1ms overhead GPU
// cmpSmall.scalarTimeSeconds ~ 0.000000028 s (khong overhead)
// cmpSmall.gpuTimeSeconds    ~ 0.0001 s (GAN NHU TOAN BO la overhead, tinh toan that ~0)
// Verified: cmpSmall.gpuTimeSeconds > cmpSmall.scalarTimeSeconds -> GPU CHAM HON!

4. Mô hình Roofline: 10 TFLOPS chỉ có nghĩa nếu kịp nạp dữ liệu

Có một giả định lặng lẽ trong toàn bộ Mục 3 mà ta phải lôi ra ánh sáng: mọi con số thời gian ở trên đều tính bằng cách lấy FLOPs chia cho thông lượng ĐỈNH. Tức là ngầm giả định phần cứng luôn có sẵn dữ liệu để tính — băng thông bộ nhớ vô hạn. Không phần cứng nào như vậy. Muốn tính thì phải nạp dữ liệu vào đã, và với rất nhiều bài toán, chính bước nạp đó mới là thứ quyết định thời gian.

Mô hình Roofline (Williams, 2009) cân hai giới hạn đó với nhau bằng một đại lượng duy nhất: cường độ số học (arithmetic intensity) — số phép tính làm được trên mỗi byte đọc từ bộ nhớ.

$$\text{AI} = \frac{\text{số FLOP}}{\text{số byte phải chạm tới bộ nhớ}} \qquad \text{Thông lượng đạt được} = \min(\text{FLOPS}_{\text{đỉnh}},\; \text{AI} \times \text{Băng thông})$$

Chia hai vế cho nhau được điểm gãy (ridge point) — cường độ tối thiểu để hết nghẽn băng thông. Với GPU 10 TFLOPS và băng thông 600 GB/s: $10\times10^{12} / 600\times10^{9} = \mathbf{16{,}67}$ FLOP/byte. Dưới mức đó là nghẽn bộ nhớ, trên mức đó là nghẽn tính toán.

mái băng thông AI × 600 GB/s mái FLOPS đỉnh — 10 TFLOPS điểm gãy 16,67 nghẽn BĂNG THÔNG nghẽn TÍNH TOÁN cộng vector: AI 0,167 — chỉ 1% công suất nhân ma trận: AI 170 — 100% thông lượng đạt được cường độ số học (FLOP/byte) →
Mái nhà có hai phần: dốc bên trái là băng thông, phẳng bên phải là FLOPS đỉnh. Bài toán nằm ở đâu dưới mái nhà đó quyết định mua phần cứng nào là có ích.

Đặt hai phép toán lên cùng GPU đó. Nhân ma trận $N \times N$ làm $2N^3 - N^2$ phép tính nhưng chỉ cần chạm $3N^2$ phần tử — số phép tính tăng theo $N^3$ còn dữ liệu chỉ tăng theo $N^2$, nên càng lớn càng "lãi". Cộng vector thì ngược lại: mỗi phần tử 2 phép tính mà phải đọc x, đọc y, ghi y — tỷ lệ đó cố định, không cải thiện dù mảng to đến đâu.

roofline.js (verified bằng engine — cùng GPU, 100% vs 1%)
const PEAK = 10e12;  // 10 TFLOPS
const BW   = 600e9;  // 600 GB/s  -> ridge point = 16.67 FLOP/byte

// Matrix multiply 1024x1024, FP32, each matrix touched once (ideal blocking)
rooflineAttainable(matmulArithmeticIntensity(1024, 4), PEAK, BW);
// AI = 170.58 -> { bound: 'compute', attainable: 10 TFLOPS, fractionOfPeak: 1.00 }

// Vector add y[i] = a*x[i] + y[i] on the SAME hardware
rooflineAttainable(vectorAddArithmeticIntensity(4), PEAK, BW);
// AI = 0.167  -> { bound: 'memory',  attainable: 100 GFLOPS, fractionOfPeak: 0.01 }

// Same GPU. 100% of peak against 1% of peak. Peak FLOPS predicts neither.
⚠️ Cạm bẫy: so sánh phần cứng bằng FLOPS đỉnh
Verified: trên CÙNG một GPU, nhân ma trận dùng được 100% công suất còn cộng vector chỉ 1% — chênh nhau 100 lần, mà con số FLOPS đỉnh in trên hộp thì giống hệt nhau. Với một bài toán nghẽn băng thông, mua card có FLOPS gấp đôi sẽ cho đúng 0% cải thiện: mái nhà phẳng bên phải cao lên, nhưng bài toán đang nằm ở dốc bên trái. Cái cần mua là băng thông, hoặc cái cần sửa là thuật toán — chia khối để tái dùng dữ liệu qua cache (Bài 7) chính là cách kéo AI sang phải.

Đây cũng là lý do nhân ma trận, chứ không phải phép toán nào khác, trở thành thứ mọi phần cứng AI đều đua nhau tăng tốc: nó là một trong số ít phép toán có cường độ số học tăng theo kích thước, nên càng đắp thêm FLOPS càng có chỗ dùng.
🔍 Con số AI 170 là CẬN TRÊN, không phải thứ tự nhiên có
Công thức trên giả định mỗi ma trận chỉ phải đọc ĐÚNG MỘT LẦN. Vòng lặp ba tầng ngây thơ không đạt được điều đó — nó đọc lại từng hàng và từng cột nhiều lần, nên AI thực tế thấp hơn nhiều và bài toán có thể rơi ngược về phía nghẽn băng thông. Thứ kéo nó lên gần cận trên là chia khối (tiling): nạp một khối nhỏ vừa cache rồi tái dùng triệt để trước khi bỏ đi — đúng nguyên lý Temporal Locality của Bài 7, và là lý do thư viện BLAS nhanh hơn vòng lặp tự viết hàng chục lần dù cùng số phép tính.

5. Phân kỳ warp: cái giá của một luồng lạc loài

Pitfall ở Mục 1 đã nói GPU "phải chạy tuần tự cả 2 nhánh" khi các luồng rẽ khác hướng. Hiện tượng đó có tên: phân kỳ warp (warp divergence), và đáng đo cho ra con số vì trực giác ở đây sai.

GPU không lập lịch từng luồng riêng lẻ. Nó gom thành nhóm — NVIDIA gọi là warp (32 luồng), AMD gọi là wavefront (64) — và cả nhóm dùng CHUNG một bộ đếm chương trình. Cả warp bắt buộc thực hiện cùng một lệnh tại một thời điểm. Nên khi gặp if mà các luồng đi khác hướng, phần cứng không có lựa chọn nào ngoài chạy nhánh then với các luồng còn lại bị TẮT, rồi chạy tiếp nhánh else với nhóm kia bị tắt.

warp_divergence.js (verified bằng engine — 1 luồng cũng đủ)
// All 32 threads take the same path
warpDivergence(new Array(32).fill(true));
// -> { passes: 1, efficiency: 1.00, wastedSlots: 0 }

// The warp splits evenly, 16 and 16
warpDivergence(Array.from({ length: 32 }, (_, i) => i < 16));
// -> { passes: 2, efficiency: 0.50, wastedSlots: 32 }

// ONE thread out of 32 goes the other way
warpDivergence(Array.from({ length: 32 }, (_, i) => i === 0));
// -> { passes: 2, efficiency: 0.50, wastedSlots: 32 }   identical to the 16/16 split
⚠️ Cạm bẫy: tưởng phân kỳ ít thì thiệt hại ít
Verified: một warp chia đôi 16/16 tụt xuống hiệu suất 50%. Nhưng một warp chỉ có ĐÚNG MỘT luồng đi khác hướng cũng tụt xuống đúng 50% — y hệt. Thiệt hại không tỷ lệ với số luồng lạc loài, vì cái quyết định là warp phải chạy bao nhiêu LƯỢT, mà một luồng khác hướng đã đủ bắt nó chạy hai lượt.

Hệ quả thực tế: if (threadId % 2 == 0) làm hỏng MỌI warp, còn if (threadId / 32 % 2 == 0) — cùng chia đôi số luồng — thì không warp nào phân kỳ, vì ranh giới rơi đúng vào biên warp. Cùng một điều kiện logic, cùng số luồng mỗi nhánh, hiệu năng khác nhau gấp đôi. Đó là lý do lập trình GPU quan tâm tới việc luồng nào nằm cùng warp với luồng nào, chứ không chỉ quan tâm thuật toán.

6. Thực hành: So sánh nhân ma trận Scalar vs SIMD vs GPU/AMX

Đổi kích thước ma trận và thông số 3 kiến trúc để xem thời gian tính thay đổi trực tiếp — thử giảm N xuống rất nhỏ (vd 4) để tự tay thấy pitfall overhead GPU verified ở trên:

⚡ So sánh Scalar vs SIMD vs GPU/AMX
Scalar
SIMD
GPU/AMX

Tóm lược

  • ✅ SIMD "nới rộng" một lệnh CPU để xử lý nhiều dữ liệu cùng lúc; SIMT (GPU) chạy hàng nghìn luồng thật sự độc lập — khác nhau về bản chất tổ chức song song.
  • ✅ NPU/Tensor Core và Apple AMX là phần cứng CHUYÊN DỤNG chỉ để tăng tốc nhân ma trận.
  • ✅ Verified: nhân ma trận 1024×1024 = 2.146.435.072 FLOPs. SIMD nhanh hơn scalar đúng 8 lần (= độ rộng vector); GPU nhanh hơn scalar 2500 lần.
  • ✅ Pitfall: với ma trận quá nhỏ (4×4), overhead nạp dữ liệu vào GPU khiến nó CHẬM HƠN CPU vô hướng — verified thật bằng engine.
  • ✅ Pitfall: GPU rất yếu với code rẽ nhánh tuần tự phức tạp — không thể thay thế CPU cho mọi việc.
  • ✅ Roofline: thông lượng đạt được = $\min(\text{FLOPS}_{\text{đỉnh}}, \text{AI} \times \text{băng thông})$. Verified trên CÙNG một GPU 10 TFLOPS / 600 GB/s: nhân ma trận (AI 170) dùng 100% công suất, cộng vector (AI 0,167) chỉ 1% — FLOPS đỉnh không đủ để đoán hiệu năng.
  • ✅ Pitfall: với bài toán nghẽn băng thông, mua phần cứng nhiều FLOPS hơn cho đúng 0% cải thiện. Chia khối để tái dùng cache (Bài 7) mới là thứ kéo AI sang phía nghẽn tính toán.
  • ✅ Phân kỳ warp: verified, chỉ MỘT luồng trong 32 đi khác hướng đã kéo hiệu suất cả warp xuống 50% — đúng bằng thiệt hại của warp chia đôi 16/16. Thiệt hại không tỷ lệ với số luồng lạc loài.

Cả bài này đi theo một hướng: muốn nhanh hơn thì đắp thêm phần cứng chuyên dụng — nhiều nhân hơn, khối ma trận riêng, băng thông rộng hơn. Nhưng "đắp thêm" nghĩa là con chip to ra, mà chip càng to thì xác suất dính một hạt bụi làm hỏng nó càng cao, và giá thành tăng nhanh hơn hẳn diện tích. Đó là bức tường kinh tế đứng sau mọi bức tường kỹ thuật ở trên — Bài 11 sẽ tính đúng cái giá đó, và cho thấy vì sao ngành công nghiệp buộc phải cắt con chip lớn thành nhiều mảnh nhỏ (chiplet).

Trắc nghiệm ôn tập

Câu 1

Khác biệt CỐT LÕI giữa SIMD (CPU vector) và SIMT (GPU) là gì?

Câu 2

Verified: nhân ma trận 1024×1024 trên SIMD (32 GFLOPS) nhanh hơn scalar (4 GFLOPS) đúng 8 lần. Con số 8 lần này đến từ đâu?

Câu 3

Verified: với ma trận 4×4 (chỉ 112 FLOPs), GPU (có overhead nạp dữ liệu 0,1ms) lại CHẬM HƠN CPU vô hướng — dù GPU "mạnh hơn" hẳn về lý thuyết. Vì sao?

Câu 4

Vì sao lượng tử hoá (quantization) FP32→INT8 cần được đo đạc cẩn thận thay vì áp dụng đại trà?

Tải file code thực hành minh họa bài học

File JavaScript CPUJS — thư viện kiến trúc máy tính mini dùng xuyên suốt cả 12 bài, Bài 10 vừa thêm matrixMultiplyFlops(), computeTimeSeconds(), compareComputeMethods(), rooflineAttainable(), matmulArithmeticIntensity(), vectorAddArithmeticIntensity(), warpDivergence() — đếm FLOPs nhân ma trận, so sánh thời gian tính giữa 3 kiến trúc, mô hình Roofline (cường độ số học vs băng thông) và phân kỳ warp, kèm self-test đối chiếu đúng mọi con số trong bài (chạy node cpu-core.js, không cần cài thêm gì):

Tải về cpu-core.js

📖 Tài liệu tham khảo

  • Tổng quan kiến trúc SIMD/SIMT: Wikipedia — SIMD — bao gồm cả phần so sánh với SIMT (Single Instruction, Multiple Threads) trên GPU.
  • Apple AMX & Accelerate: Apple Developer — Accelerate framework — tài liệu chính thức về các thư viện tăng tốc phần cứng trên Apple Silicon.
  • Lượng tử hoá mô hình AI: Wikipedia — Quantization — nguyên lý lượng tử hoá tín hiệu áp dụng cho trọng số mạng thần kinh nhân tạo.

Bài viết liên quan trong series

Bài 9: Apple Silicon & Kiến Trúc Bộ Nhớ Thống Nhất (UMA) Bài 11: Điểm Cuối Định Luật Moore & Đóng Gói Chiplet Quay lại Lộ trình Kiến Trúc Máy Tính

Bình luận