Tune NVFP4 ให้แรงขึ้น 20% — เรื่องของการขยับ 3 ค่า
สารบัญ
25 มิถุนายน 2569 — บันทึกการ tune Qwen3.6-35B-A3B-NVFP4 บน DGX Spark รอบที่สอง — ขยับแค่ 3 ค่า แต่ได้ +20% throughput ที่ concurrency 6
เรื่องมันเริ่มจาก backtest ค้าง
เมื่อวานผมรัน backtest ของ bot4k ผ่าน LiteLLM proxy ที่ชี้ไป vLLM บน DGX Spark — concurrency 6 (6 backtest jobs รันพร้อมกัน) ผลคือ throughput ตกลงเหลือ 169 tok/s ทั้งที่ benchmark ก่อนหน้านี้เคยได้ 335 tok/s ที่ concurrency 12
ผมนั่งดู nvidia-smi แล้วก็เข้าใจทันที — KV cache หมด vLLM เริ่ม pre-empt request (บังคับให้บาง request รอ) เพราะผมเซต gpu_memory_utilization: 0.5 ไว้ตอนแรก (เน้น safety — ไม่อยาก OOM)
แต่ตอนนี้ model NVFP4 ใช้ VRAM แค่ ~16 GB (จาก 128 GB unified memory) เหลือเฟือ — 0.5 ปล่อยให้ KV cache มีน้อยเกินไปจนรับ batch ใหญ่ไม่ได้
Note: ทำไมต้อง
0.5ตอนแรก — เพราะ NVFP4 เป็น Blackwell-only feature ผมไม่มีข้อมูลว่า quantization quality เป็นยังไง กลัวว่าถ้ากิน memory เยอะแล้วจะเจอ bug เลยเซต conservative ไว้ก่อน พอ benchmark 7/7 GSM8K ผ่านแล้ว (verified 2026-06-20) ถึงค่อย ๆ ขยับขึ้น
3 ค่าที่ขยับ
ผมเปลี่ยนแค่ 3 flags ใน recipe YAML — ไม่ได้แตะอย่างอื่น:
# v1 (เดิม — safe แต่ throughput ตกที่ high concurrency)
gpu_memory_utilization: 0.5
max_num_batched_tokens: 32768
max_num_seqs: 4 # ← default ใน vLLM 0.23
# v2 (ใหม่ — tuned สำหรับ batch workload)
gpu_memory_utilization: 0.7
max_num_batched_tokens: 65536
max_num_seqs: 16
gpu_memory_utilization: 0.5 → 0.7
ค่านี้บอก vLLM ว่า "ใช้ VRAM ได้ถึง 70% ของ total" (≈ 90 GB จาก 128 GB) เหลือ 30% ให้ OS + activations + safety margin
ผมขยับทีละ 0.1 ก่อนหน้านี้ (0.5 → 0.6 → 0.7) แต่ benchmark ที่ 0.6 ไม่ต่างจาก 0.5 มากนัก เลยข้ามไป 0.7 ตรง ๆ
Note: ทำไมไม่ 0.85 — community บอกว่า 0.85+ บน GB10 มีความเสี่ยง OOM (unified memory ของ Blackwell มี kernel-level overhead ที่ vLLM ประมาณไม่ได้แม่นยำ) ผมเซต 0.7 เพราะ verified ว่า NVFP4 ใช้แค่ 16 GB + KV cache ขนาด 73 GB พอดี
max_num_batched_tokens: 32768 → 65536
ค่านี้บอก vLLM ว่า "หนึ่ง forward pass รับ tokens ได้สูงสุดเท่านี้" ถ้า concurrency 6 แต่ละ request มี prompt 2K + generation 1K = รวม 18K tokens ต่อ round ซึ่ง 32K ก็พอ — แต่พอ prompt ใหญ่ขึ้น (8K+) หรือ generation ยาวขึ้น (4K+) มันจะคอขวด
ผม bump เป็น 65K เพื่อ safety margin — บน GB10 unified memory ค่านี้แทบไม่กระทบ throughput เลย (เพราะ compute-bound ไม่ใช่ memory-bound ที่ batch size นี้)
max_num_seqs: 4 → 16
ค่านี้บอก vLLM ว่า "รับ concurrent requests ได้สูงสุด 16" ค่า default ใน vLLM 0.23 คือ 4 (เพื่อกัน OOM) แต่พอ KV cache ใหญ่ขึ้นแล้ว เราก็รับได้เยอะขึ้น
ผมเซต 16 เพราะ backtest ของผมมี concurrency สูงสุด 12 — ต้องการ buffer
Note: ค่านี้มีผลข้างเคียง — ถ้า latency-sensitive workload (single user chat) ค่า 16 จะทำให้ TTFT (time to first token) สูงขึ้นเล็กน้อย เพราะ vLLM รอให้ครบ batch ก่อนค่อย process — แต่ throughput gain ชนะขาดสำหรับ batch workload
ผล benchmark — มี surprise
ผมรัน concurrency sweep (1, 3, 6, 12) เทียบ v1 vs v2:
| Parallel | v1 (gpu=0.5) | v2 (gpu=0.7) | Δ |
|---|---|---|---|
| 1 | 58.4 | 55.6 | −5% ⚠️ |
| 3 | 106.7 | 111.8 | +5% |
| 6 | 169.0 | 202.3 | +20% 🚀 |
| 12 | 335.8 | 339.2 | +1% |
สิ่งที่ไม่คาดคิด: ที่ concurrency 1 v2 ช้าลง 5% — เพราะ overhead จาก max_num_batched_tokens: 65536 (over-allocation สำหรับ single user) vLLM พยายามเติม batch ให้ใกล้ 65K แต่ single request มีแค่ ~20 tokens มันเลย padding เยอะโดยเปล่าประโยชน์
สิ่งที่คาดคิดแต่ยังดีใจ: ที่ concurrency 6 (sweet spot ของผม) v2 ชนะ v1 ถึง 20% — ตรงตามที่ตั้งใจไว้ KV cache ใหญ่ขึ้น → batch ใหญ่ขึ้น → throughput สูงขึ้น
สิ่งที่คาดคิด: ที่ concurrency 12 ผลต่างแค่ 1% — เพราะ v1 ก็ saturate ได้ที่ 335 tok/s แล้ว vLLM ติดที่ compute bound (GB10 ทำ floating-point ได้จำกัด) ไม่ใช่ memory bound
Note: Insight สำคัญ —
gpu_memory_utilizationสูง ≠ เร็วเสมอ มันขึ้นกับ workload: batch workload ชนะ single user ช้าลง ถ้าใช้ chat UI ที่มี user ทีละคน request ควรใช้ v1 (gpu_mem=0.5) ถ้าใช้ batch processing (backtest, batch inference) ควรใช้ v2 (gpu_mem=0.7)
Deploy command
image: sparkrun-eugr-vllm
port: 8000
env:
VLLM_MARLIN_USE_ATOMIC_ADD: '1'
runtime:
tensor_parallel_size: 1
gpu_memory_utilization: 0.7
max_num_batched_tokens: 65536
max_num_seqs: 16
max_model_len: 262144
kv_cache_dtype: fp8
enable_prefix_caching: true
load_format: instanttensor
attention_backend: flashinfer
speculative_config: '{"method":"mtp","num_speculative_tokens":2}'
chat_template_kwargs:
enable_thinking: false
model: RedHatAI/Qwen3.6-35B-A3B-NVFP4
parser:
tool-call: qwen3_coder
reasoning: qwen3
Note: v2 vs v1 ใน recipe file — ผมเก็บ v1 ไว้เป็น fallback สำหรับ single-user workload deploy v2 ถ้า latency สำคัญกว่า throughput
สิ่งที่ผมเรียนรู้
-
gpu_memory_utilizationไม่ใช่ universal knob — มันแลกระหว่าง throughput กับ stability ขึ้นกับ workload ผม tune เป็น 3 รอบ (0.5 → 0.6 → 0.7) ก่อนเจอ sweet spot -
max_num_batched_tokensมี overhead ที่ single user — bump ขึ้นเฉพาะตอนมี batch workload จริง ไม่ใช่ default -
Benchmark ต้องครอบคลุม concurrency range — ถ้าผม benchmark แค่ concurrency 1 ผมจะไม่มีวันรู้ว่า v2 ดีกว่าที่ concurrency 6 (และที่สำคัญกว่านั้น — จะรู้ว่ามันแย่ลงที่ concurrency 1)
-
NVFP4 verified stable ที่ gpu_mem=0.7 — ก่อนหน้านี้ผมกลัวว่า 0.7+ จะทำให้ Blackwell quantization มีปัญหา ผ่านมา 1 ชั่วโมงยังไม่เจอ crash หรือ quality degradation ใน GSM8K benchmark
-
MTP cold-cache trap ยังอยู่ — ก่อน benchmark ผมต้อง warmup 2 requests เสมอ (acceptance 6-12% → 72-76%) ไม่งั้นตัวเลขจะห่วยกว่าความเป็นจริงมาก
เปรียบเทียบกับบทความก่อนหน้า
ถ้าอ่าน Qwen3.6 NVFP4 Atlas attempt มาก่อน — บทความนั้นเล่าเรื่อง deploy attempt แรก (Atlas cluster) ที่ fail บทความนี้คือ attempt ที่สอง บน DGX Spark ของตัวเอง ที่ประสบความสำเร็จ
ถ้าสนใจเรื่อง reasoning control + sampling parameters ดู Qwen3.6-35B-A3B Sampling Guide — บทความนั้นเจาะลึก enable_thinking, chat_template_kwargs, temperature/top_p
ถ้าสนใจเรื่อง MTP speculative decoding (ทำไม K=2 ถึงเป็น sweet spot) ดู blog series Qwen3.6 Sampling Guide — มี section "MTP K=2 vs K=4 vs noMTP" ที่ benchmark ครบ
อ้างอิง
- vLLM Engine Args Documentation — full list of runtime parameters
- Qwen3.6-35B-A3B-NVFP4 Model Card — NVFP4 quantization specs, GSM8K benchmarks
- NVIDIA Blackwell Architecture Whitepaper — unified memory architecture details
- sparkrun CLI Documentation — recipe format,
run/stop/statuscommands - LiteLLM Proxy Docs — virtual model routing, multiplexing
- Related post: Qwen3.6 NVFP4 Atlas attempt — failed first attempt, lessons learned
- Related post: Qwen3.6-35B-A3B Sampling Guide — reasoning control, temperature, top_p
เนื้อหานี้มีประโยชน์ไหม? ช่วยสนับสนุนค่ากาแฟให้ผู้เขียนสักแก้ว
Buy Me a Coffee