Tuần này trên Dev.to có một bài khá thú vị: Gemma 4 decode trên cloud T4 chậm đi khoảng 7% khi card nóng lên. Đọc xong mình nhớ lại chuyện team mình từng gặp. Bọn mình benchmark một model 8B trên con GPU thuê theo giờ, chạy 30 giây thì được 42 tokens/s. Dựa vào con số đó, team tính capacity cho production. Lên production, chạy liên tục cả ngày thì throughput thực tế chỉ còn khoảng 37–38 tokens/s, và hệ thống thiếu capacity gần 10%. Lỗi không nằm ở model hay ở llama.cpp. Lỗi nằm ở cách bọn mình đo. Bài này chia sẻ cách mình benchmark LLM inference bây giờ, để con số đo được gần với thực tế khi chạy lâu dài.
Tại sao benchmark 30 giây gần như vô nghĩa
GPU hiện đại không chạy ở một xung nhịp cố định. Lấy NVIDIA T4 làm ví dụ: TDP chỉ có 70W, boost clock tối đa 1590 MHz, và card được làm mát thụ động. Card không có quạt riêng mà phụ thuộc vào luồng gió của server. Khi bạn mới bắt đầu chạy, card còn mát, driver đẩy clock lên cao. Sau vài phút, nhiệt độ và công suất chạm ngưỡng, driver bắt đầu hạ clock. Hiện tượng này gọi là throttling.
Có vài nguyên nhân throttling hay gặp:
- SW Power Cap: chạm giới hạn công suất. Với T4 thì xảy ra rất thường xuyên.
- HW Slowdown / HW Thermal Slowdown: quá nhiệt ở mức phần cứng.
- SW Thermal Slowdown: driver chủ động hạ clock để giữ nhiệt độ.
Với LLM, giai đoạn decode chủ yếu bị giới hạn bởi memory bandwidth, nên clock SM giảm thì tốc độ không giảm tỷ lệ thuận. Giai đoạn prefill thì bị giới hạn bởi compute, nên ảnh hưởng nặng hơn nhiều. Vì vậy, nếu bạn chỉ đo một con số chung chung kiểu "tokens/s" trong 30 giây đầu, bạn đang đo GPU ở trạng thái đẹp nhất. Production thì không bao giờ ở trạng thái đó.
flowchart LR
A[Bắt đầu chạy<br/>GPU mát] --> B[Boost clock tối đa]
B --> C[Nhiệt độ + công suất tăng]
C --> D{Chạm power cap<br/>hoặc thermal limit?}
D -- Có --> E[Driver hạ SM clock]
E --> F[Steady state<br/>throughput thấp hơn]
D -- Không --> B
Bước 1: Quan sát GPU trong lúc benchmark
Trước khi đo tokens/s, cần biết GPU đang làm gì. nvidia-smi có sẵn chế độ log liên tục. Mở một terminal riêng và chạy:
# Log mỗi giây ra CSV, chạy song song với benchmark
nvidia-smi \
--query-gpu=timestamp,temperature.gpu,clocks.sm,clocks.mem,power.draw,power.limit,clocks_throttle_reasons.active \
--format=csv,nounits -l 1 > gpu_log.csv &
# Xem chi tiết lý do throttle tại thời điểm hiện tại
nvidia-smi -q -d PERFORMANCE
# Xem clock tối đa mà card hỗ trợ để so sánh
nvidia-smi -q -d CLOCK | grep -A3 "Max Clocks"
Với driver 535 trở lên, NVIDIA đổi tên trường thành clocks_event_reasons.active. Tên cũ vẫn dùng được trên đa số bản driver, nhưng nếu gặp lỗi Field ... is not a valid field thì bạn đổi sang tên mới. Giá trị clocks_throttle_reasons.active là một bitmask. Ví dụ 0x0000000000000004 là SW Power Cap, 0x0000000000000020 là SW Thermal Slowdown.
Khi đọc file gpu_log.csv, mình thường thấy cùng một pattern: phút đầu tiên clocks.sm đứng ở mức 1590, đến phút thứ 3–5 thì tụt dần về 1200–1350 và đứng yên ở đó. Mức đứng yên này mới là con số bạn cần dùng để tính capacity.
Bước 2: Benchmark đủ lâu và đo theo time series
llama-bench của llama.cpp tiện để so sánh nhanh, nhưng mặc định chỉ lặp 5 lần (-r 5), mỗi lần rất ngắn. Mình dùng nó theo cách này:
# Warm-up + nhiều lần lặp, xuất JSON để phân tích
./llama-bench -m models/gemma-3-4b-it-Q4_K_M.gguf \
-ngl 99 -p 512 -n 256 -r 50 -o json > bench.json
Cách hiệu quả hơn là bắn request liên tục vào llama-server, đồng thời ghi lại metric của GPU qua NVML. Script dưới đây dùng package nvidia-ml-py (pip install nvidia-ml-py requests):
import time, json, statistics, requests
import pynvml as nv
URL = "http://127.0.0.1:8080/completion"
DURATION = 15 * 60 # chạy 15 phút
PROMPT = "Giải thích cơ chế B-tree index trong PostgreSQL. " * 20
nv.nvmlInit()
h = nv.nvmlDeviceGetHandleByIndex(0)
rows, start = [], time.time()
while time.time() - start < DURATION:
r = requests.post(URL, json={"prompt": PROMPT, "n_predict": 256,
"cache_prompt": False}).json()
t = r["timings"]
rows.append({
"t": round(time.time() - start),
"prefill_tps": t["prompt_per_second"],
"decode_tps": t["predicted_per_second"],
"temp": nv.nvmlDeviceGetTemperature(h, nv.NVML_TEMPERATURE_GPU),
"sm_mhz": nv.nvmlDeviceGetClockInfo(h, nv.NVML_CLOCK_SM),
"power_w": nv.nvmlDeviceGetPowerUsage(h) / 1000,
"throttle": hex(nv.nvmlDeviceGetCurrentClocksThrottleReasons(h)),
})
print(rows[-1])
json.dump(rows, open("run.json", "w"))
def summary(sub, label):
d = [x["decode_tps"] for x in sub]
p = [x["prefill_tps"] for x in sub]
print(f"{label}: decode median={statistics.median(d):.1f} "
f"p10={statistics.quantiles(d, n=10)[0]:.1f} | "
f"prefill median={statistics.median(p):.0f}")
summary([x for x in rows if x["t"] < 60], "Cold (0-60s)")
summary([x for x in rows if x["t"] > DURATION - 300], "Steady (5 phút cuối)")
Có ba điểm cần lưu ý trong script:
-
cache_prompt: False: nếu không tắt, prefill sẽ dùng lại KV cache từ request trước và con số prefill bị đẹp giả. - Tách prefill và decode: hai phase này phản ứng với throttling rất khác nhau, gộp chung lại sẽ che mất vấn đề.
- So sánh cold và steady: đây là con số quan trọng nhất của cả bài. Nếu chênh lệch quá 5% thì bạn phải dùng số steady để tính capacity.
sequenceDiagram
participant S as Bench script
participant L as llama-server
participant G as GPU (NVML)
loop Mỗi request trong 15 phút
S->>L: POST /completion (cache_prompt=false)
L-->>S: timings (prefill/decode tps)
S->>G: Đọc temp, SM clock, power, throttle
G-->>S: Metrics
S->>S: Ghi 1 dòng time series
end
S->>S: So sánh cold vs steady (median, p10)
Bước 3: Giảm thiệt hại khi đã biết card bị throttle
Đo xong rồi thì xử lý thế nào? Mình có vài cách đã thử thực tế:
-
Khóa clock để có hiệu năng ổn định:
sudo nvidia-smi -lgc 1200,1200(lock GPU clock). Nghe thì giống tự làm chậm card, nhưng throughput sẽ ổn định, latency p99 dễ đoán hơn, và nhiều trường hợp hiệu năng trung bình còn không giảm vì card không bị dao động clock liên tục. Muốn bỏ khóa thì chạysudo nvidia-smi -rgc. Trên cloud VM có thể bạn không có quyền này. -
Bật persistence mode:
sudo nvidia-smi -pm 1để driver không unload giữa các lần chạy, giúp tránh việc request đầu tiên bị chậm. - Chọn quantization hợp với workload: decode bị giới hạn bởi bandwidth, nên Q4_K_M thường ít bị ảnh hưởng bởi throttling hơn bản FP16 vì phải đọc ít dữ liệu hơn mỗi token.
-
Giảm batch prefill khi card nóng: nếu workload của bạn nặng prompt (RAG với context dài), hãy giảm
-ub(ubatch size) để giảm power spike. -
Theo dõi ở production: export metric NVML qua
dcgm-exportervào Prometheus và đặt alert khiDCGM_FI_DEV_CLOCK_THROTTLE_REASONSkhác 0 liên tục quá 10 phút.
Thêm một chuyện nữa: trên cloud, hai instance cùng loại T4 có thể cho kết quả chênh nhau vài phần trăm, vì phụ thuộc vào vị trí rack và luồng gió của từng server. Đừng benchmark trên một instance rồi áp con số đó cho cả fleet.
Kết luận
Benchmark LLM mà chỉ chạy 30 giây thì kết quả là con số marketing, không phải con số vận hành. Những việc bạn nên làm ngay:
- Chạy benchmark tối thiểu 10–15 phút, và ghi lại SM clock, nhiệt độ, công suất cùng lúc với tokens/s.
- Báo cáo prefill và decode riêng, dùng median và p10 thay vì giá trị trung bình.
- Tính capacity bằng số steady state, không dùng số đo khi card còn mát. Nếu chênh lệch lớn hơn 5%, hãy ghi rõ con số đó trong tài liệu sizing.
- Cân nhắc khóa clock nếu bạn cần latency dễ đoán hơn là throughput đỉnh.
- Đưa throttle reasons vào monitoring để biết khi nào production đang chạy dưới spec.
GPU không nói dối, nó chỉ trả lời đúng câu hỏi bạn đặt ra. Nếu muốn biết hiệu năng khi chạy cả ngày, hãy đo trong điều kiện chạy cả ngày.
Top comments (0)