Qwen3.6-35B บน DGX Spark: ทำไม v5 ถึงเป็น sweet spot
สารบัญ
บันทึก 26 มิถุนายน 2569 — ตี 2 — เรื่องของการ tune vLLM บน GB10
เมื่อวาน (2026-06-25) ผมรัน benchmark v2 ที่ gpu_mem=0.7 แล้ว RAM ขึ้นไป 99% เครื่องเกือบค้าง ส่วน v1 ที่ gpu_mem=0.5 ก็เจออาการเดียวกันตอนมี request stuck วันนี้เลยลอง tune ต่อจนมาถึง v5 ที่ gpu_mem=0.75 ซึ่งน่าจะเป็น sweet spot สำหรับ workload จริงที่ผมใช้
เรื่องมันเริ่มจาก RAM ขึ้น 99%
ตอนแรกผมใช้ v1 (gpu_mem=0.5) มาหลายวันแล้ว ทุกอย่างดูปกติ จนกระทั่งมีแชทค้างไป 1-2 request GPU ขึ้นไป 90%+ prefix cache สะสมจน KV cache เต็ม RAM เลยขึ้นไป 99% (เหลือ 1.3 Gi) เครื่องเริ่มอืด
ผมเข้าใจว่า gpu_mem ต่ำคือต้นเหตุ ก็เลยเพิ่มเป็น v2 (gpu_mem=0.7) แต่พอ benchmark จริง ผลออกมา:
v1 (gpu_mem=0.5): C=6 = 169 tok/s
v2 (gpu_mem=0.7): C=6 = 202 tok/s (+20%)
v2 (gpu_mem=0.7): RAM = 99% (ยังเจอปัญหาเดิม)
ปรากฏว่า gpu_mem เพิ่มขึ้นช่วย throughput แต่ RAM ก็ยังเต็มเหมือนเดิม ผมเลยลอง v3 (gpu_mem=0.85) — ผล benchmark ออกมา 209 / 377 / 835 tok/s ซึ่งดูดีเกินจริง พอไล่ดูแล้วพบว่ามันเป็น anomaly (measurement bug) ลบออกจาก notes ทิ้ง
ทำไม RAM ถึงเต็มทั้งที่ gpu_mem ต่ำ
ผมใช้เวลาสักพักกว่าจะเข้าใจ ตอนแรกคิดว่า gpu_mem=0.5 น่าจะเหลือ RAM เยอะ แต่พอ trace ดูดี ๆ พบว่า:
- v1 (gpu_mem=0.5) เจอ RAM 99% เมื่อมี stuck request
- v2 (gpu_mem=0.7) เจอ RAM 99% เหมือนกัน
- v3 (gpu_mem=0.85) เจอ RAM 99% + ค้าง
สรุปคือ RAM 99% ไม่ได้เกี่ยวกับ gpu_mem โดยตรง แต่เป็นเพราะ stuck request ทำให้ prefix cache สะสมจน KV cache เต็ม ซึ่ง vLLM allocate KV cache ใน unified memory (DGX Spark GB10) ไม่ใช่ VRAM เพียงอย่างเดียว
Note: Unified memory quirk — DGX Spark GB10 ใช้ unified memory 121 Gi ระหว่าง CPU กับ GPU ไม่เหมือน discrete GPU ที่ VRAM แยกจาก RAM ดังนั้นถ้า KV cache โต มันกินพื้นที่จาก RAM ทั้งหมด ไม่ใช่แค่ VRAM ส่วนหนึ่ง
v5 Config — สิ่งที่เปลี่ยนจาก v1
ผมเทียบ v1 กับ v5 ดูแล้วมี 4 จุดที่ปรับ:
| Flag | v1 | v5 | ความเหมือน/ต่าง |
|---|---|---|---|
gpu_memory_utilization | 0.5 | 0.75 | +0.25 |
max_num_seqs | (default 256) | 8 | match workload 1-6 |
max_num_batched_tokens | 32768 | 32768 | เท่ากัน |
max_model_len | 262144 | 262144 | เท่ากัน |
| MTP K | 2 | 2 | เท่ากัน |
แล้วก็มี 3 env var ที่เพิ่มเข้ามาใน v5:
VLLM_MARLIN_USE_ATOMIC_ADD=1 # NVFP4 GEMM kernel
VLLM_ALLOW_LONG_MAX_MODEL_LEN=1 # safety net สำหรับ 256K
VLLM_USE_FLASHINFER_SAMPLER=1 # sampler kernel เร็วกว่า default
Note: ทำไม max_num_seqs=8 — ผมดู workload จริงจาก LiteLLM logs แล้วเจอว่ามี concurrent 1-6 request ส่วนใหญ่ default 256 มัน oversized สำหรับเครื่องเดียวที่มี GPU แค่ 1 ตัว
Benchmark จริง — v1 vs v2 vs v4 vs v5
ผมใช้ Python threads + curl POST, max_tokens=300, temperature=0.0 รัน 12 concurrent 10/12 successful ทุก version:
| Parallel | v1 (gpu=0.5) | v2 (gpu=0.7) | v4 (gpu=0.80) | v5 (gpu=0.75) |
|---|---|---|---|---|
| 1 | 58.4 | 55.6 | 57.0 | 59.2 |
| 3 | 106.7 | 111.8 | 104.0 | 105.0 |
| 6 | 169.0 | 202.3 | 208.0 | 207.0 |
| 12 | 335.8 | 339.2 | 233.5 | 235.2 |
Note: วิธีอ่านตาราง — เลขคือ tok/s aggregate ยิ่งสูงยิ่งเร็ว v5 ชนะ v1 ที่ C=6 (+22%) แต่แพ้ v1 ที่ C=12 (-30%) เพราะ max_num_seqs=8 cap ไว้
ผลที่น่าสนใจคือ v5 ≈ v4 ทุก concurrency แปลว่าการลด gpu_mem จาก 0.80 เป็น 0.75 ไม่ได้ทำให้ throughput ตก ผมเลยตัดสินใจใช้ v5 เพราะได้ RAM safety เพิ่ม (97 Gi vs 103 Gi)
ทำไม v5 เป็น sweet spot
สรุปง่าย ๆ คือ:
- RAM safety — v5 = RAM 80% (97 Gi used, 23 Gi free, headroom ~14 GB) vs v1, v2 = RAM 99% ตอนมี stuck request
- Throughput ดีที่ C=6 — 207 tok/s ดีกว่า v1 (169) 22%
- FlashInfer sampler — env var ใหม่ คาดว่า +5-10% sampling throughput (ยังไม่ได้ A/B test)
- Match workload — max_num_seqs=8 ตรงกับ concurrent 1-6 ที่ใช้จริง
ทดลองใช้จริงก่อนตัดสินใจต่อ
ผมยังไม่ได้ tune v6 เพราะตอนนี้ v5 ดูเป็นจุดสมดุลที่ดีแล้ว จะลองใช้จริงดูว่า:
- chat 1-3 concurrent ใช้งานลื่นไหม
- backtest 6 concurrent throughput OK ไหม
- memory pressure ระหว่างใช้งาน 24/7 เป็นยังไง
ถ้าทุกอย่างโอเคก็ใช้ v5 ต่อ ถ้ามีปัญหาค่อยกลับมา tune v6
สิ่งที่ v5 ไม่ได้
ผมต้องบอกตรง ๆ ว่า v5 มีข้อจำกัด:
- C=12 throughput ลดลง 30% (235 vs 335) เพราะ max_num_seqs=8 cap ถ้ามี burst request 12+ พร้อมกัน มันจะคอขวด
- KV cache เล็กกว่า v4 (5.23M vs 5.74M tokens) เพราะ gpu_mem ต่ำกว่า
- ยังไม่ได้ A/B test FlashInfer sampler เทียบกับไม่ใส่ env var
- ยังไม่ได้ทดสอบ chat agent workload จริง benchmark ใช้ synthetic prompt ไม่ใช่ conversation ยาว ๆ
Note: C=12 throughput ลดลง 30% คืออะไร — ถ้ามี 12 user ยิง request พร้อมกัน v5 จะตอบได้ 235 tok/s รวม vs v1 ทำได้ 335 tok/s แปลว่า per-user latency v5 ช้ากว่าตอน burst แต่ workload จริงของผมไม่เคยเกิน 6 concurrent
Setup จริงบน DGX Spark
สำหรับคนที่สนใจลอง ผมใช้ recipe แบบนี้:
# Qwen3.6-35B-A3B-NVFP4-MTP-v5.yaml
gpu_memory_utilization: 0.75
max_num_batched_tokens: 32768
max_num_seqs: 8
max_model_len: 262144
kv_cache_dtype: fp8
attention_backend: flashinfer
load_format: instanttensor
prefix_caching: true
tool_call_parser: qwen3_coder
reasoning_parser: qwen3
tensor_parallel_size: 1
speculative_config:
method: mtp
num_speculative_tokens: 2
env:
VLLM_MARLIN_USE_ATOMIC_ADD: '1'
VLLM_ALLOW_LONG_MAX_MODEL_LEN: '1'
VLLM_USE_FLASHINFER_SAMPLER: '1'
sparkrun run Qwen3.6-35B-A3B-NVFP4-MTP-v5.yaml
# 3 min startup: load + compile + autotune
# ตอนเสร็จ vLLM จะฟังที่ port 8000
ผมใช้ LiteLLM proxy เป็นตัวกลาง ตั้ง timeout=60s, num_retries=1 เพื่อกัน stuck request ค้างนาน
เรียนรู้จาก journey นี้
สิ่งที่ผมได้จากการ tune v1 → v5:
- อย่าเพิ่ม gpu_mem เพราะคิดว่ายิ่งสูงยิ่งดี v0.95 ก็ไม่ได้แปลว่าเร็วกว่า v0.75 เสมอไป ขึ้นอยู่กับ workload จริง
- RAM 99% ไม่ได้เกิดจาก gpu_mem ต่ำ เกิดจาก stuck request → prefix cache สะสม ต้องแก้ที่ LiteLLM timeout ไม่ใช่ gpu_mem
- Benchmark anomaly คือเรื่องจริง v3 ที่ผมเชื่อว่าดี 209-835 tok/s พอ trace แล้วเป็น measurement bug ลบทิ้ง ดีกว่าเก็บไว้หลอกตัวเอง
- max_num_seqs สำคัญ default 256 oversized สำหรับ single GPU ผมตั้ง 8 เพราะ workload จริงไม่เคยเกิน 6
อ้างอิง
- Qwen3.6-35B-A3B-NVFP4 (HuggingFace) — Model card, NVFP4 quantization, MTP spec
- vLLM Documentation — gpu_memory_utilization, max_num_seqs, speculative decoding
- FlashInfer Attention Backend — Attention kernel สำหรับ Blackwell
- DGX Spark GB10 Specs — Unified memory architecture
- sparkrun CLI — Recipe-based vLLM deployment
เนื้อหานี้มีประโยชน์ไหม? ช่วยสนับสนุนค่ากาแฟให้ผู้เขียนสักแก้ว
Buy Me a Coffee