Qwen3.6-35B บน DGX Spark — จาก FP8 สู่ NVFP4 + บทเรียนจาก Atlas
สารบัญ
- สรุปความเร็ว — เทียบทุก config ที่วัดจริง
- TL;DR
- จุดเริ่มต้น: FP8 ทำงานได้ดี แต่ไม่ flexible
- Atlas: pure Rust engine ที่ทุกคนบอกว่าเร็วกว่า vLLM
- บทเรียน #1: bottleneck ของ Atlas อยู่ที่ scheduler ไม่ใช่ memory
- NVFP4: weights เล็กลงครึ่ง แต่ throughput สูงกว่า
- บทเรียน #2: memory footprint ต่างกันมาก
- บทเรียน #3: NVFP4 ไม่ทำให้ reasoning แย่ลง
- วิธีใช้ reasoning model กับ vLLM
- เทียบ reasoning latency
- FP8 vs NVFP4: ตัวไหนฉลาดกว่า?
- บทเรียน #4: max_num_batched_tokens = sweet spot ที่ 32768
- ทำไมเพิ่ม batched_tokens ไม่ช่วย?
- เทียบ workflow: ก่อน vs หลัง
- เทียบ NVFP4 vs FP8 ทุกมิติ
- Setup ที่ใช้ตอนนี้
- สรุป: ทำไม NVFP4 ถึงดีกว่า (สำหรับ use case ของผม)
- ข้อควรระวัง
- อ้างอิง
ผมใช้ Qwen3.6-35B-A3B-FP8 บน DGX Spark มาเดือนกว่า ผ่าน vLLM v0.23.0 (ตัวล่าสุด) recipe ของตัวเอง throughput อยู่ที่ ~290 tok/s ที่ 12 parallel — ก็ถือว่าใช้ได้ ไม่ได้แย่ แต่พอไปเจอ claim ใน Discord ว่า "NVFP4 + vLLM nightly ได้ 236.97 tok/s ที่ 10 concurrency" ก็เริ่มสนใจ
คำถามคือ — ทำไม NVFP4 ถึงเร็วกว่า? ทำไม nightly ถึงดีกว่า stable? และที่สำคัญที่สุด — NVFP4 แลกกับ quality เท่าไหร่?
บทความนี้เป็นบันทึกการทดสอบจริง ตั้งแต่ FP8 เดิม → ลอง Atlas (Rust engine) → กลับมาใช้ vLLM + NVFP4 — พร้อมบทเรียนเรื่อง architecture mismatch ที่เจอตอนลอง Atlas
บทความก่อนหน้า: Qwen3.6-35B Sampling Guide — พื้นฐานการเลือก temperature/top_p สำหรับงานต่างๆ
สรุปความเร็ว — เทียบทุก config ที่วัดจริง
| Concurrency | vLLM FP8 (เดิม) | vLLM NVFP4 (ใหม่) | Atlas NVFP4 | ที่ดีที่สุด |
|---|---|---|---|---|
| 1 (single) | 55.4 tok/s | 58.4 tok/s | 78.4 tok/s | Atlas +41.5% |
| 3 | 106.0 | 106.7 | 63.5 | vLLM NVFP4 |
| 6 | 177.2 | 169.0 | 65.6 | vLLM FP8 |
| 12 | 292.8 | 335.8 tok/s | 81.4 | vLLM NVFP4 +14.7% |
นอกจากความเร็ว ยังได้ฟรี: memory ลด 75% (30-50 GB → 7.94 GB), context 2x (128K → 256K), reasoning 7/7 — รายละเอียดทั้งหมดอยู่ด้านล่าง
TL;DR
ทดสอบ Qwen3.6-35B-A3B บน DGX Spark เปรียบเทียบ FP8 vs NVFP4 และลอง Atlas (Rust engine) — สรุปคือ vLLM + NVFP4 ให้ throughput สูงสุด 336 tok/s @ 12 parallel พร้อม memory ลดลง 75% รองรับ context 256K และ reasoning quality ผ่าน 7/7 (รวม cognitive reflection trap) ส่วน Atlas เหมาะกับงาน single-stream low-latency มากกว่า เพราะ scheduler ออกแบบเพื่อ latency ไม่ใช่ batch throughput
จุดเริ่มต้น: FP8 ทำงานได้ดี แต่ไม่ flexible
recipe qwen3.6-35b-a3b-base-128k-v23.yaml ของผมใช้:
--quantization fp8 # FP8 weights
--kv-cache-dtype fp8 # FP8 KV cache
--max-model-len 131072 # 128K context
--gpu-memory-utilization 0.75 # ใช้ VRAM 75%
--speculative-config mtp K=2 # MTP speculative
ผลคือ 12 parallel = 293 tok/s aggregate, single stream = 55 tok/s. ใช้งานได้ แต่มีข้อจำกัด:
- 256K context ทำไม่ได้ — KV cache หมด
- Memory headroom น้อย — mem 0.75 = ใช้ไป 96 GB / 128 GB
- Container 20 GB — image ใหญ่, deploy ช้า
- v0.23.0 stable — features ใหม่ต้องรอ release
Atlas: pure Rust engine ที่ทุกคนบอกว่าเร็วกว่า vLLM
พอไปอ่าน NVIDIA developer forum เจอ Atlas:
"Atlas is a pure Rust LLM inference engine with 20+ custom kernels compiled directly for SM121... 2.8x faster than NVIDIA's stock vLLM image."
จุดที่น่าสนใจ:
- Pure Rust + CUDA — ไม่มี Python, ไม่มี PyTorch, ไม่มี torch.compile
- Container 2.5 GB (vs vLLM 20 GB)
- Cold start <2 min (vs vLLM ~10 min)
- OpenAI + Anthropic API compatible
- KV high-precision layers auto — เก็บ layers สำคัญใน BF16
ผมเลยลอง sparkrun run @atlas/qwen3.6-35b-a3b-nvfp4 --solo --port 8002 แล้ว benchmark:
| Parallel | Atlas NVFP4 | vLLM FP8 | ผลต่าง |
|---|---|---|---|
| 1 | 78.4 tok/s | 55.4 | Atlas +41.5% |
| 3 | 63.5 | 106.0 | vLLM +67% |
| 6 | 65.6 | 177.2 | vLLM +170% |
| 12 | 81.4 | 292.8 | vLLM +260% |
Atlas ชนะที่ single-stream (+41.5%) แต่ vLLM ชนะ concurrent (3.6x ที่ 12 parallel)
ทำไมถึงเป็นแบบนั้น?
บทเรียน #1: bottleneck ของ Atlas อยู่ที่ scheduler ไม่ใช่ memory
ลองปรับ Atlas recipe เพื่อเพิ่ม concurrent:
kv_cache_dtype: nvfp4(ครึ่ง memory)max_num_seqs: 12max_batch_size: 16gpu_memory_utilization: 0.75ssm_cache_slots: 0
ผลคือ เหมือนเดิมเป๊ะ — 78 tok/s ที่ 1 parallel, 81 tok/s ที่ 12 parallel
ค้นดูใน Atlas GitHub issues + forum thread พบว่า:
"Atlas optimized for low-latency single-stream, not batch throughput" "Slai scheduling + TBT deadline=100ms kills slow batches aggressively"
Atlas ใช้ slai policy ที่ optimize สำหรับ token-by-token latency ต่ำ (100ms deadline). เวลา request ใน batch ช้า จะถูก kill ทันที → effective batch ไม่ scale.
Note: ไม่ใช่ทุก engine optimize เหมือนกัน
vLLM ใช้ continuous batching — ส่ง request เข้า batch ตลอดเวลา ทำให้ aggregate throughput scale เกือบ linear. Atlas เลือก SLAI scheduling — latency ต่ำแต่ throughput จำกัด. ไม่มี engine ที่ "ดีกว่า" ในทุก use case
NVFP4: weights เล็กลงครึ่ง แต่ throughput สูงกว่า
กลับมา vLLM + ลอง recipe ที่ claim 236.97 tok/s:
# Qwen3.6-35B-A3B-NVFP4-MTP.yaml
model: RedHatAI/Qwen3.6-35B-A3B-NVFP4
runtime: vllm
builder: eugr
container: ghcr.io/spark-arena/dgx-vllm-eugr-nightly:latest
mods:
- mods/fix-qwen3-coder-next
- mods/fix-qwen3.6-chat-template
defaults:
max_model_len: 262144 # 256K!
gpu_memory_utilization: 0.5 # ครึ่งเดียว!
kv_cache_dtype: fp8
speculative_config: '{"method": "mtp", "num_speculative_tokens": 2}'
load_format: instanttensor
tool_call_parser: qwen3_coder
reasoning_parser: qwen3
ผลลัพธ์:
| Parallel | vLLM NVFP4 | vLLM FP8 (เดิม) | ผลต่าง |
|---|---|---|---|
| 1 | 58.4 | 55.4 | +5.4% |
| 3 | 106.7 | 106.0 | +0.7% |
| 6 | 169.0 | 177.2 | -4.6% |
| 12 | 335.8 | 292.8 | +14.7% |
335 tok/s ที่ 12 parallel — เกิน claim 236.97 ของ community
บทเรียน #2: memory footprint ต่างกันมาก
ตรวจ container memory ตอน serve:
| Metric | FP8 (เดิม) | NVFP4 (ใหม่) | ผลต่าง |
|---|---|---|---|
| Container MEM | 30-50 GB | 7.94 GiB | -75% |
| Model weights | ~35 GB | ~16.3 GB | -53% |
| System RAM used | ~70 GB | 69 GiB | เท่าเดิม |
| Mem % | ~30% | 6.52% | -77% |
NVFP4 weights = 4-bit = ครึ่งหนึ่งของ FP8. เมื่อ weights เล็กลง KV cache + batch processing มี room เหลือเยอะ — เลย scale concurrent ได้ดีกว่า.
# ตรวจ memory ตอน serve
docker stats sparkrun_xxx_solo --no-stream
# ได้ MEM USAGE 7.94GiB / 121.7GiB (6.52%)
บทเรียน #3: NVFP4 ไม่ทำให้ reasoning แย่ลง
หลังจาก migrate เป็น NVFP4 ผม test reasoning quality ด้วย 7 คำถามมาตรฐาน — GSM8K math, formal logic, common sense, และ cognitive reflection trap (คำถามที่คนส่วนใหญ่ตอบผิด):
| # | ประเภท | คำถาม | คำตอบที่ถูก | Model ตอบ | ผล |
|---|---|---|---|---|---|
| 1 | math | Janet ขายไข่: 16-3-4=9 ฟอง × $2 = ? | $18 | $18 | ✅ |
| 2 | logic | ตะกร้า 5 แอปเปิ้ล หยิบ 3 ออก "คุณ" มีกี่ลูก | 3 | 3 (ไม่ใช่ 2) | ✅ |
| 3 | math | ซื้อบ้าน $80K + ซ่อม $50K ค่าเพิ่ม 150% กำไรเท่าไหร่ | $70,000 | $70,000 | ✅ |
| 4 | logic | "ดอกกุหลาบทุกดอกเป็นดอกไม้" จริงไหม | Yes | Yes | ✅ |
| 5 | logic-trap | ไม้เบสบอล+ลูก $1.10 ไม้แพงกว่า $1 ลูกราคา? | $0.05 | $0.05 (ไม่ใช่ $0.10!) | ✅ |
| 6 | common-sense | vacuum ลดเวลาครึ่งหนึ่ง ปกติ 60 นาที → ? | 30 | 30 นาที | ✅ |
| 7 | formal logic | "กุหลาบทุกดอกเป็นดอกไม้, ดอกไม้บางชนิดเหี่ยวเร็ว → กุหลาบบางดอกเหี่ยวเร็ว" ได้ไหม | No | No (syllogism invalid) | ✅ |
Score: 7/7 (100%) — รวม cognitive reflection trap (ข้อ 5) ที่คนส่วนใหญ่ตอบผิด
ตัวอย่างเต็มของข้อ 3 (Josh flipping house):
Step 1: Total Cost = $80,000 + $50,000 = $130,000
Step 2: New Value = $80,000 + (150% × $80,000) = $200,000
Step 3: Profit = $200,000 - $130,000 = $70,000
คำตอบตรงกับ expected เป๊ะ พร้อม step-by-step reasoning
วิธีใช้ reasoning model กับ vLLM
Qwen3.6 เป็น reasoning model — output มี 2 fields:
content: คำตอบสุดท้ายreasoning: thinking process (อาจยาวหลายร้อย tokens)
ถ้าอยากได้คำตอบตรง ๆ ไม่ต้องดู reasoning → เพิ่ม flag นี้ใน request:
{
"chat_template_kwargs": {"enable_thinking": false}
}
หรือถ้าอยากได้ reasoning process ด้วย → ตั้ง max_tokens: 2048+ (ไม่งั้น content จะ null เพราะถูกตัดตอน reasoning ยังไม่จบ)
เทียบ reasoning latency
| Question type | เวลาเฉลี่ย |
|---|---|
| Simple (Q2, Q6) | 1.2 - 2.0s |
| Multi-step math (Q1, Q3) | 3.8 - 7.5s |
| Formal logic (Q4, Q7) | 5.1 - 7.4s |
ทุกคำถามตอบใน < 8 วินาที ถือว่า interactive-friendly
FP8 vs NVFP4: ตัวไหนฉลาดกว่า?
จาก model card ของ NVIDIA เทียบ BF16 baseline กับ NVFP4:
| Benchmark | BF16 | NVFP4 | Delta |
|---|---|---|---|
| MMLU Pro | 85.6 | 85.0 | −0.6 |
| GPQA Diamond | 84.9 | 84.8 | −0.1 |
| AIME 2025 | 89.2 | 88.8 | −0.4 |
| τ²-Bench Telecom | 95.5 | 94.7 | −0.8 |
| SciCode | 40.8 | 40.6 | −0.2 |
| IFBench | 62.3 | 62.8 | +0.5 |
| MMMU Pro | 74.1 | 74.5 | +0.4 |
NVFP4 ต่างจาก BF16 แค่ 0.1–0.8 คะแนน และบาง task ยังได้สูงกว่าด้วย
ส่วน independent benchmark จาก sch0tten/nvfp4-benchmark ที่เทียบ BF16 / FP8 / INT4-AWQ / NVFP4 บน Blackwell 96GB โดยตรง พบว่า:
- FP8 เสีย quality น้อยที่สุด — MMLU-Pro delta แค่ −0.1 ถึง −0.7 จาก BF16
- NVFP4 สำหรับ Qwen3.6-35B-A3B (MoE) เสียแค่ −0.5 คะแนน MMLU-Pro จาก BF16 — อยู่ใน noise floor (test-retest drift ~0.35 คะแนน)
- ความแตกต่างระหว่าง FP8 กับ NVFP4 สำหรับ MoE model แทบไม่ต่างกันเลย เพราะ MoE กระจาย quantization error ไปทั่ว expert pool
- quality cost ของ 4-bit ไปอยู่ที่ knowledge (MMLU-Pro) เท่านั้น — math (GSM8K), code (HumanEval/MBPP), instruction-following ไม่ต่างจาก BF16
Note: NVFP4 quantization ไม่กระทบ reasoning capability
ผล test ของผม (7/7) สอดคล้องกับ benchmark ทางการ — NVFP4 เสีย quality จาก BF16 แค่ < 1 คะแนน และสำหรับ MoE model อยู่ในระดับ noise. FP8 ฉลาดกว่า NVFP4 เล็กน้อย แต่ไม่มีนัยสำคัญสำหรับ Qwen3.6-35B-A3B. ส่วน claim ของ Atlas team ว่า "FP8 garbled, NVFP4 valid" น่าจะเป็นปัญหาเฉพาะของ Atlas engine ไม่ใช่ปัญหาของ FP8 format เอง
บทเรียน #4: max_num_batched_tokens = sweet spot ที่ 32768
หลังจาก NVFP4 stable ผมลอง tune --max-num-batched-tokens จาก 32768 → 65536 ด้วย sparkrun override:
sparkrun run recipes/Qwen3.6-35B-A3B-NVFP4-MTP.yaml \
-o max_num_batched_tokens=65536 --port 8001
ผล benchmark (apples-to-apples, prompt 500 tokens × 12 reqs):
| Parallel | 32768 (default) | 65536 (override) | Δ |
|---|---|---|---|
| 1 (12 reqs back-to-back) | 258.5 | 263.2 | +1.8% |
| 3 | 318.7 | 324.6 | +1.8% |
| 6 | 322.5 | 321.2 | -0.4% |
| 12 | 318.2 | 321.3 | +1.0% |
±1-2% = noise range — 65536 ไม่ได้ throughput win
ทำไมเพิ่ม batched_tokens ไม่ช่วย?
ทั้ง 32768 และ 65536 plateau ที่ ~320 tok/s aggregate — bottleneck อยู่ที่อื่น:
- Scheduler limit concurrent requests ก่อน batched_tokens จะเต็ม
- MTP acceptance ~88% — speculative decoding ทำงานเต็มที่แล้ว batch ใหญ่ขึ้นไม่ช่วย
- KV cache bandwidth — อาจเป็นคอขวดจริงที่ GPU
การเพิ่ม batched_tokens 32K→65K ใช้ memory มากขึ้นแต่ไม่ได้ throughput → rollback เป็น 32768
Lesson: ไม่ใช่ทุก "น่าลอง" จะคุ้ม
ผม assume ว่า batched_tokens สูงขึ้น = throughput สูงขึ้น — แต่จริง ๆ bottleneck อยู่ที่ scheduler + MTP ไม่ใช่ batch size. ลองแล้วไม่คุ้ม rollback ได้ ไม่ต้อง force commit recipe default ไว้ที่ 32768 ดีที่สุด
เทียบ workflow: ก่อน vs หลัง
Note: nightly + mods = แลกระหว่าง features กับ stability
eugr nightly มี optimizations ใหม่กว่า stable — แต่อาจ break. mods (
fix-qwen3-coder-next,fix-qwen3.6-chat-template) fix known issues. ผมเก็บ recipeqwen3.6-35b-a3b-base-128k-v23.yamlไว้เป็น fallback ถ้า NVFP4 พัง
เทียบ NVFP4 vs FP8 ทุกมิติ
| Metric | vLLM FP8 (v23, เดิม) | vLLM NVFP4 (eugr, ใหม่) | Δ |
|---|---|---|---|
| Throughput @ 12 parallel | 292.8 tok/s | 335.8 tok/s | +14.7% |
| Throughput @ 6 parallel | 177.2 tok/s | 169.0 tok/s | -4.6% |
| Memory (container RSS) | 30-50 GB | 7.94 GiB | -75% |
| Memory (model weights) | ~35 GB | ~16.3 GB | -53% |
| Context length | 128K | 256K | +100% |
| Reasoning quality | ~BF16 (NVIDIA eval) | 7/7 (GSM8K + logic) | ✅ verified |
| Cold start | ~10 min | ~3 min (InstantTensor) | -70% |
| VRAM headroom @ mem=0.5 | N/A (mem 0.75 used) | ~68 GB free | ✅ |
NVFP4 ชนะ FP8 ครบทุกมิติ ที่สำคัญที่สุดคือ context length เพิ่มเป็น 256K (จาก 128K) — เปิดทางให้ process เอกสารยาว ๆ หรือ reasoning หลาย round โดยไม่ต้อง chunk
Setup ที่ใช้ตอนนี้
# 1. Install sparkrun (บน DGX)
export PATH="$HOME/.local/bin:$PATH"
which sparkrun # v0.2.38
# 2. Save recipe ที่ ~/spark-vllm-docker/recipes/
ls ~/spark-vllm-docker/recipes/Qwen3.6-35B-A3B-NVFP4-MTP.yaml
# 3. Deploy
~/spark-vllm-docker/recipes/Qwen3.6-35B-A3B-NVFP4-MTP.yaml'
# 4. Verify (port 8001)
curl -s http://10.0.0.246:8001/v1/models | jq
# → id: "RedHatAI/Qwen3.6-35B-A3B-NVFP4"
# → max_model_len: 262144
สรุป: ทำไม NVFP4 ถึงดีกว่า (สำหรับ use case ของผม)
| Use case | Winner | Why |
|---|---|---|
| Single chat / agent | Atlas (NVFP4, 78 tok/s) | low latency, fast first token |
| Batch processing (backtest) | vLLM NVFP4 (336 tok/s @ 12p) | continuous batching |
| 256K context | vLLM NVFP4 | 256K works, FP8 ไม่ได้ |
| Memory-constrained | vLLM NVFP4 (7.94 GB container) | เหลือ memory เยอะ |
| Reasoning tasks | vLLM NVFP4 (7/7 verified) | reasoning quality intact |
ผมเลือก vLLM NVFP4 เพราะ workload หลักคือ bot4k backtest — concurrent throughput สำคัญกว่า single-stream latency.
ข้อควรระวัง
- eugr nightly unstable —
nightlytag เปลี่ยนได้ทุกวัน. ถ้า deploy ใหม่อาจ break. ควร pin commit hash - mem 0.5 ต่ำมาก — ทำงานได้ดีกับ NVFP4 (16 GB weights) แต่ถ้าใช้ model ใหญ่กว่า (122B+) อาจต้องเพิ่ม
- Atlas ยังไม่ fit งาน concurrent — แต่ถ้าใช้ interactive เดี่ยว Atlas คือตัวเลือกที่ดีที่สุดตอนนี้ (78 tok/s, 1/8 image size)
- Reasoning model latency — reasoning tasks ใช้เวลา 5-8s ต่อ request (vs < 2s สำหรับ simple chat). ถ้าใช้ใน hot path ที่ต้องการ response เร็ว ควร disable thinking ด้วย
chat_template_kwargs: {enable_thinking: false} - batched_tokens = 32768 sweet spot — อย่า override เป็น 65536 หรือสูงกว่าโดยไม่จำเป็น. bottleneck อยู่ที่ scheduler/MTP ไม่ใช่ batch size. ใช้ recipe default ไว้
อ้างอิง
- Atlas: Open-source inference engine for DGX Spark — Atlas intro, benchmarks, community feedback
- Atlas GitHub (Avarok-Cybersecurity/atlas) — Engine source, FP8 vs NVFP4 verdict, AMD port
- Atlas Recipe Registry — 18 official recipes
- eugr/spark-vllm-docker — Spark vLLM framework + mods
- sparkrun (scitrera) — CLI launcher
- Qwen/Qwen3.6-35B-A3B-FP8 — Original FP8 model
- RedHatAI/Qwen3.6-35B-A3B-NVFP4 — NVFP4 quantized variant
- vLLM documentation — KV cache options, MTP, speculative decoding
- Best Qwen Models Ranked (insiderllm) — Qwen family overview, Qwen3.6 ties Sonnet 4.6
- sch0tten/nvfp4-benchmark — Independent BF16/FP8/INT4-AWQ/NVFP4 quality + throughput benchmark
- NVFP4: What 4-Bit Really Costs on Blackwell (URE) — Independent 16-arm benchmark analysis
- Accelerating LLMs with NVFP4 quantization (Red Hat) — NVFP4 vs FP8 vs BF16 accuracy recovery
เนื้อหานี้มีประโยชน์ไหม? ช่วยสนับสนุนค่ากาแฟให้ผู้เขียนสักแก้ว
Buy Me a Coffee