Bộ 01 · Vật lý của tốc độ
Vì sao decode bị giới hạn bởi bộ nhớ, prefill bởi phép tính, và batching thay đổi cả hai thế nào. Bài 01–04, 06, 09, 15, 16, 24, 25, 28, 44, 59–61. Trả lời thành tiếng mỗi câu trong khoảng một phút, rồi mới xem thẻ.
Q1 · "Quảng cáo 300 token/s được không?" ưu tiên
Bài: 02, 06, 16, 24
Ghi chú
Ta serve Llama 3 8B ở BF16 trên một H100 với đúng một người dùng online. PM muốn quảng cáo streaming 300 token/s, và benchmark của một đồng nghiệp đã cho 2.000 token/s. Có hứa được không?
Trả lời trong 60 giây
- Không. Với một người dùng (batch 1), mỗi bước decode phải đọc toàn bộ trọng số từ HBM một lần, nên một bước không bao giờ nhanh hơn số byte trọng số ÷ băng thông bộ nhớ.
- Model 8B ở BF16 nặng 16 GB, nên mỗi bước mất ít nhất 16 GB ÷ 3,35 TB/s ≈ 4,8 ms mỗi token. Mỗi bước sinh 1 token, nên trần là 1 ÷ 4,8 ms ≈ 200 token/s, và kernel thật rơi vào khoảng 150 tới 180. Đó là lý do ta không hứa được 300.
- Vậy benchmark 2.000 token/s đang sai. Lỗi thường gặp là không gọi GPU synchronize trước khi dừng đồng hồ, quan trọng vì kernel chạy bất đồng bộ và đồng hồ dừng trước khi việc xong, và đếm token prompt như token đầu ra.
- Muốn nhanh hơn cho mỗi người dùng có hai đòn bẩy: đọc ít byte hơn mỗi bước (lượng tử hoá), hoặc lấy nhiều token hơn từ mỗi lần đọc trọng số (speculative decoding).
Các con số chính: mức sàn decode = byte trọng số ÷ băng thông = 16 × 10⁹ ÷ 3,35 × 10¹² ≈ 4,8 ms.
Câu hỏi tiếp theo hay gặp: "Baseline 70B FP8 của ta có khoẻ không, với TPOT 14 ms trên hai H100 dùng tensor parallelism?" Với tensor parallelism, trọng số được chia cho hai GPU, mỗi GPU đọc nửa của mình cùng lúc, nên băng thông cộng lại thành 6,7 TB/s. 70 GB trọng số khi đó mất 70 GB ÷ 6,7 TB/s ≈ 10,4 ms, đó là mức sàn. MBU là mức sàn ÷ số đo, nên 10,4 ÷ 14 ≈ 74%, là khoẻ.
Mẹo
Có lần tôi nói trần đến từ TFLOPS. Decode bị giới hạn bởi băng thông bộ nhớ. Với tensor parallelism, hãy cộng băng thông của các GPU.
Q2 · Vì sao prefill 500 token chỉ gấp khoảng 2 lần một bước decode? ưu tiên
Bài: 04, 15, 16, 44
Ghi chú
Prefill một prompt 500 token mất khoảng 8 ms; một token decode mất khoảng 4,8 ms. Vì sao gần nhau thế, và 30 người dùng đang stream bị đứng hình bao lâu khi có người dán vào một tài liệu 8.000 token?
Trả lời trong 60 giây
- Mỗi pha bị giới hạn bởi một tài nguyên khác nhau, và thời gian là giới hạn nào chậm hơn: thời gian ≈ max(FLOP ÷ FLOPS đỉnh, byte ÷ băng thông).
- Decode sinh một token mỗi bước, nên nó làm rất ít phép tính cho mỗi byte đọc vào. Vì thế nó bị giới hạn bởi việc đọc trọng số, mất khoảng 4,8 ms.
- Prefill xử lý cả 500 token prompt song song trong khi chỉ đọc trọng số một lần, nên phép tính chiếm ưu thế. Lượt đó tốn 2 × 8 × 10⁹ × 500 ≈ 8 × 10¹² FLOP, và 8 × 10¹² ÷ 989 TFLOPS ≈ 8 ms. Đó là lý do nó chỉ gấp khoảng 2 lần một bước decode.
- Dán 8.000 token tốn 2 × 8 × 10⁹ × 8.000 ≈ 1,3 × 10¹⁴ FLOP, mất 1,3 × 10¹⁴ ÷ 989 TFLOPS ≈ 130 ms. Mọi luồng khác phải chờ nó, và vì 130 ÷ 4,8 ≈ 27, mỗi người dùng đứng hình khoảng 27 bước decode. Chunked prefill sửa được chuyện này (bộ 03).
Các con số chính: FLOP của forward pass ≈ 2 × số tham số × số token; H100 BF16 dense ≈ 989 TFLOPS.
Câu hỏi tiếp theo hay gặp: "Điểm gãy (ridge point) là bao nhiêu?" Đó là số FLOP mỗi byte nơi hai giới hạn gặp nhau: 989 TFLOPS ÷ 3,35 TB/s ≈ 295 FLOP mỗi byte. Nếu công việc làm ít FLOP mỗi byte hơn mức đó, bạn memory-bound, và nếu nhiều hơn, bạn compute-bound.
Mẹo
Tôi đã chia byte cho FLOPS. Đó không phải là thời gian. Byte chia cho băng thông, FLOP chia cho FLOPS, và chi phí prefill phải tăng theo độ dài prompt.
Q3 · "32 người dùng nghĩa là chậm hơn 32 lần, đúng không?" ưu tiên
Bài: 02, 16, 25, 28
Ghi chú
Một PM nói serve 32 người dùng cùng lúc làm mỗi người chậm đi 32 lần. Llama 3 8B ở BF16 trên H100; mỗi người dùng giữ khoảng 1.000 token KV với 128 KiB mỗi token. Thực sự chuyện gì xảy ra với tốc độ mỗi người và tổng throughput?
Trả lời trong 60 giây
- Một người dùng một mình: mỗi bước decode đọc toàn bộ 16 GB trọng số, mất 16 GB ÷ 3,35 TB/s ≈ 4,8 ms. Mỗi bước sinh 1 token, nên người dùng nhận được 1 ÷ 4,8 ms ≈ 200 token/s.
- 32 người dùng trong một batch: 16 GB trọng số vẫn chỉ đọc một lần mỗi bước, chia cho cả 32 người. Chi phí thêm là KV cache riêng của mỗi người: 32 × 1.000 token × 128 KB ≈ 4,1 GB. Bước đó đọc 16 + 4,1 ≈ 20,1 GB, nên mất 20,1 GB ÷ 3,35 TB/s ≈ 6,0 ms, và một bước đó sinh 32 token, mỗi người một token.
- Mỗi người dùng cảm nhận gì: họ vẫn nhận 1 token mỗi bước, nhưng một bước giờ mất 6,0 ms thay vì 4,8 ms. Vì 6,0 ÷ 4,8 = 1,25, mỗi người chậm hơn 25%: khoảng 167 token/s thay vì 200.
- GPU sinh ra bao nhiêu: 32 token mỗi 6 ms, nên 32 ÷ 0,006 s ≈ 5.300 token/s. So với 200 token/s khi một mình, đó là 5.300 ÷ 200 ≈ 25 lần.
- Là 25 lần chứ không phải trọn 32 lần vì bước dài thêm 1,25 lần: 32 ÷ 1,25 ≈ 25,6.
- Cái giá của batching là bộ nhớ KV, tăng theo kích thước batch × độ dài context.
Các con số chính: bước ≈ (trọng số + batch × context × KV mỗi token) ÷ băng thông; tổng token/s = batch ÷ bước; mỗi người = 1 ÷ bước.
Câu hỏi tiếp theo hay gặp: "Server FastAPI của ta gọi generate() và 20 người dùng chờ 40 s, trong khi phần tính toán của GPU gần như rảnh. Mua GPU lớn hơn?" Không: request chạy từng cái một, nên hàng đợi mới là vấn đề. Continuous batching cho cả 20 người chia nhau mỗi lần đọc trọng số.
Mẹo
Tôi đã cộng KV của một người thay vì của 32 người. Nếu câu hỏi nói 32 người dùng, con số 32 phải xuất hiện trong tổng số byte. Kiểm tra ms → token/s so với batch 1 (4,8 ms ≈ 200).
Q4 · Khi nào decode thành compute-bound? vững
Bài: 02, 04, 61
Ghi chú
Với context ngắn, bạn cứ tăng batch decode. Ở batch nào decode chuyển từ memory-bound sang compute-bound, và vì sao trên thực tế nó không xảy ra sớm như vậy?
Trả lời trong 60 giây
- Với mỗi token, decode BF16 làm 2 FLOP mỗi tham số và đọc 2 byte mỗi tham số. Byte trọng số được chia chung cho cả batch, nên batch B làm 2 × B FLOP cho cùng 2 byte, nghĩa là intensity ≈ kích thước batch FLOP mỗi byte.
- Decode thành compute-bound khi intensity đó vượt điểm gãy ≈ 295 FLOP mỗi byte của H100, tức ở batch khoảng 300.
- Trên thực tế nó xảy ra muộn hơn nhiều vì KV cache. Lượng đọc KV tăng theo batch × context, và khác với trọng số, lượng đọc KV của attention không bao giờ được chia giữa các người dùng. Vì vậy với context dài, những byte thêm đó giữ decode ở trạng thái memory-bound lâu hơn nhiều.
Các con số chính: điểm gãy = FLOPS đỉnh ÷ băng thông ≈ 989 ÷ 3,35 ≈ 295.
Câu hỏi tiếp theo hay gặp: "Vậy khi đã compute-bound thì batching mua được gì?" Không thêm gì cho mỗi lần đọc trọng số. Phép tính đã là nút thắt, nên tổng throughput đi ngang trong khi latency mỗi người dùng tiếp tục tăng. Đó là bức tường bão hoà trong lần quét concurrency.
Mẹo
nói rõ tỷ số là gì (FLOP mỗi byte) và rằng byte trọng số được batch chia chung còn byte KV thì không.
Q5 · "GPU-util 100%, ta cần thêm GPU" ưu tiên
Bài: 09, 16, 59, 60
Ghi chú
Một người dùng chat với Llama 3 8B trên H100. Dashboard hiện GPU-util 100%, công suất 350 W trên mức trần 700 W, và 6 ms mỗi token decode. Quản lý muốn thêm GPU. Nó đã dùng hết công suất chưa?
Trả lời trong 60 giây
- GPU-util chỉ cho biết có kernel nào đó chạy trong mỗi cửa sổ lấy mẫu, tức một duty cycle. Nó không nói gì về việc kernel đó dùng bao nhiêu phần của chip, nên 100% không có nghĩa là đã dùng hết.
- Thước đo tốt hơn là MBU. Mỗi bước 6 ms đọc 16 GB trọng số, nên băng thông đạt được là 16 GB ÷ 6 ms ≈ 2,7 TB/s, và 2,7 ÷ 3,35 ≈ 80% băng thông đỉnh. Trong khi đó phần tính toán (MFU) gần như rảnh.
- Đó là lý do nửa công suất là đúng kỳ vọng: decode bị giới hạn bởi bộ nhớ, nên tensor core phần lớn thời gian ngồi chờ dữ liệu.
- Vậy cách sửa là nhiều người dùng hơn trên mỗi GPU (batching), không phải thêm GPU.
Các con số chính: MBU = băng thông đạt được ÷ băng thông đỉnh = mức sàn ÷ thời gian bước đo được.
Câu hỏi tiếp theo hay gặp: "Vậy bạn sẽ đặt gì lên dashboard thay vào đó?" Tôi sẽ hiện MBU và MFU, để thấy bộ nhớ và tính toán thực sự bận tới đâu. Bên cạnh đó là token/s, mức dùng KV cache, số request đang chạy và đang chờ, và các percentile TTFT, TPOT.
Mẹo
Tôi đã đọc nửa công suất thành "nửa số đơn vị ngồi không, nên kernel cần thêm việc", và chia cho điểm gãy thay vì băng thông đỉnh.
Q6 · Một triệu token tốn bao nhiêu? ưu tiên
Bài: 01, 02
Ghi chú
Một H100 thuê giá 3 USD một giờ. Một triệu token đầu ra tốn bao nhiêu khi có một người dùng (~200 token/s) và khi batch 64 người dùng (~4.300 token/s)?
Trả lời trong 60 giây
- GPU tính tiền theo giờ, nên trước hết đổi tốc độ ra token mỗi giờ: token/s × 3.600.
- Một người dùng: 200 × 3.600 = 720.000 token mỗi giờ, nên một triệu token mất 1.000.000 ÷ 720.000 ≈ 1,4 giờ. Với 3 USD một giờ, đó là ≈ 4,17 USD mỗi triệu.
- Batch 64 người dùng: 4.300 × 3.600 ≈ 15,5 triệu token mỗi giờ, nên một triệu token tốn 3 USD ÷ 15,5 ≈ 0,19 USD. Rẻ hơn khoảng 21 lần.
- Giờ GPU có giá như nhau trong cả hai trường hợp, chỉ số token bạn lấy ra từ nó là thay đổi. Đó là lý do batching là đòn bẩy chi phí chính.
Các con số chính: chi phí mỗi 1 triệu = USD/giờ ÷ (token/s × 3.600) × 10⁶.
Câu hỏi tiếp theo hay gặp: "Thời gian bước với 64 người dùng đến từ đâu?" Mỗi bước đọc 16 GB trọng số một lần, cộng KV cache của mọi người dùng: 64 người × 4.096 token × 128 KB ≈ 34 GB. Tổng cộng khoảng 50 GB, nên bước mất 50 GB ÷ 3,35 TB/s ≈ 15 ms. Mỗi người nhận 1 token mỗi bước, nên 1 ÷ 15 ms ≈ 67 token/s mỗi người, và 64 × 67 ≈ 4.300 token/s tổng cộng.
Mẹo
Tôi đã chia tiền cho giờ thay vì nhân, và trượt một bậc mười (10⁶ ÷ 200 là 5.000 s, không phải 500).
Q7 · Launch kernel và định luật Amdahl ưu tiên
Bài: 03, 04, 09, 60
Ghi chú
Một bước decode launch khoảng 300 kernel, mỗi cái khoảng 4 µs thời gian CPU, so với bước lý tưởng 4,8 ms. Nó tốn bao nhiêu, và cắt giảm bằng cách nào? Riêng một chuyện khác, một đồng nghiệp làm attention (30% của bước) nhanh hơn 3 lần và khẳng định decode giờ nhanh hơn 3 lần. Speedup thật là bao nhiêu?
Trả lời trong 60 giây
- Overhead launch: 300 kernel × 4 µs mỗi cái = 1,2 ms thời gian CPU mỗi bước. So với bước 4,8 ms, 1,2 ÷ 4,8 = 25%, nên một phần tư bước dành cho việc launch kernel.
- Cách sửa là CUDA graph, ghi lại bước một lần rồi phát lại bằng một lần launch, và kernel fusion, để còn ít kernel phải launch hơn.
- Amdahl: attention là 30% của bước, nên 70% còn lại không đổi. Thời gian mới là 0,7 + 0,3 ÷ 3 = 0,8 lần thời gian cũ, nên speedup là 1 ÷ 0,8 = 1,25 lần, không phải 3 lần.
- Kể cả attention miễn phí, 70% kia vẫn còn đó, nên trần là 1 ÷ 0,7 ≈ 1,43 lần. Bài học là tăng tốc phần chiếm phần lớn thời gian.
Các con số chính: speedup tổng = 1 ÷ ((1 − p) + p ÷ s).
Câu hỏi tiếp theo hay gặp: "Hai tối ưu: attention 30% nhanh gấp 3, MLP 50% nhanh gấp 2?" Làm việc với thời gian, không phải speedup. Phần không đụng tới là 100 − 30 − 50 = 20%, attention co lại còn 0,3 ÷ 3 = 0,1, và MLP co lại còn 0,5 ÷ 2 = 0,25. Thời gian mới là 0,2 + 0,1 + 0,25 = 0,55, nên speedup là 1 ÷ 0,55 ≈ 1,8 lần. Speedup không bao giờ cộng hay nhân với nhau; bạn cộng các thời gian mới.
Mẹo
Tôi đã đưa "ít byte hơn" làm cách sửa cho overhead launch (cách đó tăng tốc phần băng thông), và đưa trần là "70%" thay vì một tỷ số thời gian.
Tự xây
exercises/03_04_amdahl.py: một bộ tính Amdahl cho nhiều tối ưu không chồng lấp. Nó luyện cách cộng dồn thời gian thay vì cộng speedup. Chạyuv run exercises/03_04_amdahl.py.