เมื่อ timeout ไม่ใช่ปัญหา แต่ cache เป็นพระเอก: LiteLLM + DGX Spark Journey
สารบัญ
- เริ่มต้น: เมื่อ API เริ่มทำตัวแปลก
- การตรวจสอบ: แคชหรือไม่แคช?
- จุดเปลี่ยน: เฉลยปัญหา timeout
- เจาะลึก: DGX Direct กับ LiteLLM
- การค้นพบที่ไม่คาดคิด: ปรากฏการณ์ที่น่าสนใจของ max_tokens
- ขีดจำกัด Context: 32K ไม่ได้แคบอย่างที่คิด
- ตรวจสอบ Debug Log จาก Backtest จริง
- คำถามต่อ: ใช้ qwen3.6-35b กับงานอื่นได้ไหม?
- การแก้ไข: ใช้ LiteLLM เป็น Config Multiplexer
- Summary: สิ่งที่ได้เรียนรู้
- Code Settings ที่ใช้กับ Backtest
เรื่องมันเริ่มจากปัญหาที่น่ารำคาญมากตอนเที่ยงคืนครับ API ที่ผมใช้รัน backtest trading bot มัน timeout แบบสุ่ม — บาง call ผ่าน บาง call ไม่ผ่าน แถมพอลองยิง prompt เดิมซ้ำ ก็ได้ output กลับมาทันที ฟังดูขัดแย้งใช่ไหมครับ?
ผมใช้เวลาทั้งคืนไล่หาสาเหตุ จนในที่สุดก็พบว่า ปัญหาไม่ได้อยู่ที่ vLLM ไม่ได้อยู่ที่ DGX Spark และไม่ได้อยู่ที่ model เลย — มันอยู่ที่ LiteLLM proxy layer ที่ผมตั้งใจใช้เป็น "แค่ทางผ่าน" ตั้งแต่ต้น
TL;DR — ปัญหา timeout แบบสุ่มบน LiteLLM ไม่ได้มาจาก vLLM หรือ DGX Spark แต่มาจาก cache layer ใน LiteLLM ที่ทำให้ prompt ซ้ำตอบเร็ว และ proxy timeout ที่สั้นเกินไป ที่ตัด request ก่อน vLLM จะทำงานเสร็จ แก้ได้โดยใช้ LiteLLM เป็น config multiplexer โดยไม่ต้องแก้ที่ DGX
เริ่มต้น: เมื่อ API เริ่มทำตัวแปลก
ผมใช้ LiteLLM (10.0.0.155:4000) เป็น gateway ไปยัง DGX Spark (10.0.0.246:8000) ที่รัน qwen3.6-35b ผ่าน vLLM ตั้งแต่ต้นปี ทุกอย่างเคยทำงานได้ดี จนกระทั่ง backtest รอบใหม่เริ่มมีอาการแปลก ๆ
# Call แรก - timeout
$ curl http://10.0.0.155:4000/v1/chat/completions ...
# ❌ timeout after 30s
# Call ที่สอง - prompt เดิมเป๊ะ
$ curl http://10.0.0.155:4000/v1/chat/completions ... # same prompt
# ✅ 200 OK in 200ms
แบบนี้ซ้ำ ๆ หลายครั้ง ผมเริ่มสงสัยว่า "มันใช้ cache หรือเปล่า?"
การตรวจสอบ: แคชหรือไม่แคช?
ผมตั้งสมมติฐานว่า LiteLLM มี cache layer ที่ทำให้ prompt ที่ซ้ำกันได้รับคำตอบเร็วขึ้น เพื่อทดสอบ ผมเลยยิง prompt เดียวกัน 3 ครั้งติด:
| Call | Prompt | Time | Note |
|---|---|---|---|
| T1 (cold) | "เขียนฟังก์ชันบวกเลข" | 20ms | ตอบเร็วมาก น่าสงสัย |
| T2 (same) | เดิม | 82ms | ยังเร็วอยู่ |
| T3 (same) | เดิม | 242ms | เริ่มช้าลงนิดหน่อย |
| T4 (diff) | "อธิบาย Kubernetes pod" | 3,577ms | ช้าลง 100 เท่า! |
ผลลัพธ์ชัดเจนว่า T1, T2, T3 ได้ cache hit ทั้งหมด (cache จาก test เมื่อวาน) แต่ T4 เป็น cold call จริง ๆ ที่ใช้เวลา 3.5 วินาที
Note: Cache TTL - ตอนนี้ผมยืนยันแล้วว่า LiteLLM ใช้ Redis เป็น cache layer และ TTL ยาวมาก (อย่างน้อย 24 ชั่วโมง หรืออาจ indefinite) ถ้าเปิด cache ไว้ prompt ที่เคย call แล้วสำเร็จ จะอยู่ใน cache ไปนานมาก
จุดเปลี่ยน: เฉลยปัญหา timeout
เมื่อเข้าใจเรื่อง cache แล้ว ปัญหา timeout ก็กระจ่างชัดทันที:
Note: ทำไม call 1 ถึง cache ได้ทั้งที่ timeout? - เพราะ vLLM process เสร็จและ return JSON กลับมา LiteLLM แค่รับ response แล้ว cache ลง Redis ก่อน ที่จะ forward กลับไปยัง client ถ้า client timeout ก่อน ก็ไม่เป็นไร - cache ถูกเขียนไปแล้ว
ข้อค้นพบสำคัญ: root cause ของ timeout ไม่ใช่ DGX แต่เป็น LiteLLM proxy timeout ที่สั้นเกินไป DGX Spark ทำงานปกติ 8-15 วินาที แต่ proxy timeout ตัด request ก่อน
เจาะลึก: DGX Direct กับ LiteLLM
หลังจากรู้แล้วว่า LiteLLM เป็นต้นเหตุ ผมอยากรู้ว่า DGX ตรง ๆ จะเร็วกว่าไหม ผมเลยทดสอบเทียบ:
| Test | DGX Direct (10.0.0.246) | LiteLLM (10.0.0.155) |
|---|---|---|
| Cold call | 7.5s | 7.8s |
| Cache hit | ไม่มี | 0.2s |
| reasoning_content | ไม่มี field | มี (2,501 chars) |
| JSON parse | ต้องทำเอง | automatic |
ผลลัพธ์ที่ได้น่าสนใจ:
- Latency เท่ากัน - overhead ของ LiteLLM แทบไม่มี
- Cache มีประโยชน์มาก - เร็วขึ้น 100 เท่า แต่...
- Cache ไม่ช่วย backtest - เพราะ prompt ในการ backtest ไม่ซ้ำกัน (แต่ละ session มี timestamp, market data ต่างกัน)
- reasoning_content ต่างกัน - เพราะ LiteLLM มี
enable_reasoningflag เปิดอยู่ แต่ vLLM ตรง ๆ ไม่ได้เปิด
การค้นพบที่ไม่คาดคิด: ปรากฏการณ์ที่น่าสนใจของ max_tokens
จากนั้นผมลองปรับ max_tokens ดู คาดว่ายิ่งน้อยยิ่งเร็ว แต่ผลลัพธ์กลับตรงกันข้าม:
| max_tokens | Time | Used | Note |
|---|---|---|---|
| 2000 | 31.1s | 1969/2000 (98%) | ใช้จนเกือบเต็ม budget |
| 4000 | 15.8s | 967/4000 (24%) | Model หยุดเองเร็ว |
นี่คือ paradox ที่ผมไม่เคยคิดมาก่อน — ในการทดสอบนี้ max_tokens ที่มากกว่ากลับเร็วกว่า เพราะ model หยุดเองเมื่อตอบเสร็จ ไม่ใช่ generate จนครบ budget
Note: ทำไมเป็นแบบนี้? - ใน reasoning model
max_tokensรวมถึงทั้ง hidden reasoning tokens และ visible output การตั้งค่า 2000 แล้วใช้ไป 1969/2000 แสดงว่า model อาจถูกตัดกลางทางก่อนจะตอบเสร็จ หรือต้องใช้เวลาคิดเพื่อ "บีบ" reasoning ให้พอดี budget ทำให้ช้าลง ในขณะที่ budget 4000 ให้ model มีที่ว่างคิดและหยุดเองตามธรรมชาติที่ 967 tokens จึงเร็วกว่า
ขีดจำกัด Context: 32K ไม่ได้แคบอย่างที่คิด
อีกเรื่องที่ผมเข้าใจผิดมาตลอด - ผมคิดว่า qwen3.6-35b รองรับ context 256K tokens แต่จริง ๆ แล้ว DGX Spark ของผมใช้ recipe สำหรับ deploy แบบ 32K throughput ทำให้ max_model_len = 32,768
ผมเลยลองคำนวณ token budget ต่อ call:
4,300 / 32,768 = 13% - ใช้ไปแค่นี้เอง เหลือเฟือมาก
Note: caution -
max_tokens + prompt_tokens ≤ 32,768ถ้า prompt ใส่ market data ยาว ๆ + history หลาย ๆ trade อาจตึงได้ แนะนำให้ validate ก่อน call
ตรวจสอบ Debug Log จาก Backtest จริง
ผมเปิด LiteLLM log ดูว่า backtest จริง ๆ ใช้ tokens เท่าไหร่:
Input: 306 tokens (system + user prompt)
Output: 3 tokens (HOLD)
แค่นี้เอง! เพราะ system prompt บอกชัดเจนว่า "Respond with exactly one word: BUY, SELL, or HOLD. Do not explain." - model เลยข้าม thinking ไปเลย ตอบแค่คำเดียว
ตรงนี้ผมได้ข้อค้นพบสำคัญ:
Note: System prompt design > max_tokens tuning - prompt ที่ดี (657 chars ที่บอกชัด) ทำให้ output สั้น, reasoning น้อย, ไม่ต้องปรับ max_tokens, ไม่ต้อง JSON parse นี่คือ pattern ที่ดีที่สุดสำหรับ LLM-based automation
คำถามต่อ: ใช้ qwen3.6-35b กับงานอื่นได้ไหม?
หลังจากปรับแต่ง backtest เสร็จ ผมเริ่มคิดต่อ - ถ้าวันหลังอยากใช้ model ตัวเดียวกับ commit message generator, Q&A tool, หรืองานอื่น ๆ จะทำได้ไหม?
คำตอบสั้น ๆ คือ ทำได้ แต่ไม่เหมาะนัก
| กรณีใช้งาน | ความต้องการ | พฤติกรรมของ qwen3.6-35b |
|---|---|---|
| Backtest (reasoning) | คิดลึก, output ยาว | ✅ เหมาะมาก |
| Commit message | ตอบสั้น, เร็ว, ไม่ต้องคิด | ❌ reasoning 2,000 tokens เสียเปล่า |
| Simple Q&A | ตอบตรง ๆ | ❌ คิดเยอะเกินจำเป็น |
ข้อค้นพบเกี่ยวกับโครงสร้าง model:
Provider models (M3, M2.7, mimo-v2.5-pro) = General-purpose, adaptive reasoning, เร็ว, ราคาถูก ใช้ได้กับหลาย use case
Specialized models (qwen3.6-35b) = Reasoning-specialized, always think, ช้า, แต่ reasoning quality สูง
ผมเข้าใจแล้วว่าทำไม provider models ถึง versatile — พวกมันถูก train มาให้ "ตัดสินใจ" ว่าจะคิดลึกแค่ไหนตามความเหมาะสม แต่ specialized model แบบ qwen3.6-35b จะทำ reasoning เสมอ เพราะ "เชื่อ" ว่า reasoning = better quality
การแก้ไข: ใช้ LiteLLM เป็น Config Multiplexer
แทนที่จะไปแก้ recipe ของ DGX หรือสร้าง vLLM instance ใหม่ ผมเจอวิธีที่ง่ายกว่ามาก - ใช้ LiteLLM เป็น config multiplexer:
# ใน LiteLLM WebUI เพิ่ม mapping ใหม่
- model_name: qwen3.6-35b # เดิม - สำหรับ backtest
api_base: http://10.0.0.246:8000/v1
reasoning_effort: medium
- model_name: qwen3.6-35b-gpt # ใหม่ - สำหรับ tools
api_base: http://10.0.0.246:8000/v1
reasoning_effort: low
max_tokens: 500
ใช้งาน:
// Backtest - ใช้ full reasoning
const model = this.openai.chat('qwen3.6-35b')
// Commit message, Q&A - ใช้ fast mode
const model = this.openai.chat('qwen3.6-35b-gpt')
ทั้งสองชี้ไปยัง vLLM endpoint เดียวกัน แค่ config ต่างกันใน LiteLLM layer ไม่ต้องแก้ DGX ไม่ต้องสร้าง instance ใหม่ และไม่ต้องใช้ VRAM เพิ่ม
Note: Middleware-first architecture - ตั้งแต่ต้นผมตั้งใจใช้ LiteLLM เป็น "HTTP interceptor สำหรับ debug logs" ปรากฏว่าโครงสร้างนี้รองรับ use case ใหม่ ๆ ได้อีกมาก โดยไม่ต้องออกแบบใหม่
Summary: สิ่งที่ได้เรียนรู้
- Timeout อาจไม่ใช่ปัญหาจริง - ถ้ามี cache layer ที่ทำงานถูก, retry จะ hit cache ได้เสมอ
- max_tokens Paradox - ใน reasoning model budget ที่มากกว่าอาจทำให้ model หยุดเองตามธรรมชาติได้เร็วกว่า แทนที่จะถูกตัดหรือบีบ reasoning จนช้าลง
- Context limit ไม่ได้แคบ - ถ้าใช้ recipe ที่เหมาะสม, 32K ก็เพียงพอ
- System prompt design สำคัญที่สุด - constraint ที่ดี = output สั้น = reasoning น้อย = เร็ว
- Middleware ที่ดี = Pay once, benefit twice - debug logs เริ่มต้น → routing multiplexing ในอนาคต
- ไม่มี model เดียวที่ดีกับทุก use case - แต่ใช้ config layer multiplexing ช่วยได้
Code Settings ที่ใช้กับ Backtest
const result = await generateText({
model,
system: options.systemPrompt,
messages: [{ role: 'user', content: options.userPrompt }],
timeout: 60_000,
maxOutputTokens: 3000, // fixed, ครอบคลุมทุก case
temperature: process.env.BACKTEST_MODE === 'true' ? 0 : 0.2,
providerOptions: {
openai: {
reasoningEffort: process.env.BACKTEST_MODE === 'true' ? 'low' : 'medium',
},
},
})
// Strip markdown ก่อน parse
const text = result.text
.replace(/^```(?:json)?\s*/i, '')
.replace(/\s*```\s*$/i, '')
.trim()
// Whitelist validation
function parseDecision(text: string): 'BUY' | 'SELL' | 'HOLD' {
const cleaned = text.trim().toUpperCase().replace(/[^A-Z]/g, '')
if (['BUY', 'SELL', 'HOLD'].includes(cleaned)) {
return cleaned
}
return 'HOLD' // safe default
}
สรุปสุดท้าย สิ่งที่ผมได้จากการ debug คืนนั้นไม่ใช่แค่ "API ใช้งานได้แล้ว" แต่คือ ความเข้าใจว่า infrastructure layer ที่ตั้งใจทำเป็น "ทางผ่าน" ตั้งแต่ต้น สามารถกลายเป็น control plane ที่ทรงพลังได้ ถ้าเราออกแบบมันดีตั้งแต่ต้น
และสำหรับใครที่เจอปัญหา timeout แบบสุ่มบน LiteLLM - ลองดูว่า cache layer เปิดอยู่ไหม แล้วลอง retry ดูครับ บางทีปัญหาอาจอยู่ที่ proxy timeout ไม่ใช่ที่ model เลย
อ้างอิง:
- LiteLLM Routing Documentation
- Redis Caching in LiteLLM
- vLLM Engine Args
- HTTP Timeouts: The Developer's Guide
- Qwen3.6-35B-A3B-FP8 Model Card
- Thinking Mode in LLMs
- max_tokens vs max_completion_tokens (OpenAI)
เนื้อหานี้มีประโยชน์ไหม? ช่วยสนับสนุนค่ากาแฟให้ผู้เขียนสักแก้ว
Buy Me a Coffee