ติดตั้ง vLLM + LiteLLM บนการ์ด 16GB

คู่มือฉบับลงมือจริง — Qwen3.5-9B w4a16 บน RTX 5060 Ti (Blackwell sm_120)

1. ภาพรวมระบบ

vLLM คืออะไร และ PagedAttention ทำงานอย่างไร

vLLM (Very Large Language Model) คือ inference engine สำหรับ LLM ที่ออกแบบมาเพื่อรีดประสิทธิภาพ GPU ให้สูงสุด โดยใช้เทคนิคขั้นสูงหลายอย่างที่ทำให้สามารถให้บริการ inference แบบ real-time ได้รวดเร็วและประหยัดทรัพยากรมาก

PagedAttention คือหัวใจสำคัญของ vLLM เทคนิคนี้จัดการหน่วยความจำ KV cache (Key-Value cache) แบบแบ่งหน้า (page-based) คล้ายกับการจัดการหน่วยความจำใน OS ที่ใช้ virtual memory แทนที่จะจอง memory block ใหญ่ๆ แบบเดิม

การทำงานของ PagedAttention:

  • แบ่ง KV cache เป็นหน้า (pages): แต่ละ token ที่ประมวลผลจะสร้าง KV cache ซึ่งถูกแบ่งเป็นหน้าเล็กๆ แทนที่จะจอง memory ใหญ่ตั้งแต่ต้น
  • จัดการ fragmentation: เมื่อ context ยาวขึ้นและมีการ truncate หน้าเก่าที่ไม่ใช้แล้ว vLLM จะคืน memory กลับมา pool ทำให้ไม่เกิด memory waste
  • Dynamic block allocation: แต่ละ request จะขอใช้เฉพาะหน้า KV ที่จำเป็นจริงๆ ตามจำนวน token ที่ต้องการประมวลผล
  • Shared memory pool: ทุก request แชร์ pool เดียวกัน ทำให้สามารถรองรับ concurrent requests ได้มากขึ้น

ผลลัพธ์คือ vLLM สามารถรองรับ context ยาวได้ (เช่น 32K, 128K tokens) บน GPU ขนาดเล็กได้อย่างมีประสิทธิภาพ ลด memory usage ได้ 3-5 เท่าเมื่อเทียบกับ HuggingFace transformers แบบดั้งเดิม

ความแตกต่างระหว่าง vLLM และ Ollama

ด้าน vLLM Ollama
เป้าหมายหลัก High-performance inference สำหรับ production Developer-friendly local LLM runtime
Memory Management PagedAttention จัดการ KV cache แบบ page-based จอง memory แบบ static block
Concurrent Requests รองรับสูง (100+ requests) ด้วย continuous batching รองรับต่ำ (1-4 requests)
Context Length รองรับยาว (32K-128K+) ได้ดี จำกัดตาม memory ของ GPU
Quantization รองรับหลายแบบ (AWQ, GPTQ, FP8) รองรับ GGUF quantization
API Compatibility OpenAI-compatible API OpenAI-compatible API

สรุป: Ollama เหมาะสำหรับ developer ที่ต้องการลองเล่น LLM แบบง่าย ส่วน vLLM เหมาะสำหรับ production ที่ต้องการ performance สูงและรองรับ concurrent users มาก

LiteLLM คือ proxy อะไร และทำงานอย่างไร

LiteLLM คือ API proxy layer ที่ทำหน้าที่เป็น "universal translator" สำหรับ LLM APIs รวม API ของหลายเจ้าไว้ใน endpoint เดียวกัน ทำให้แอปพลิเคชันของคุณไม่ต้องเขียน code แยกสำหรับแต่ละ provider

ฟีเจอร์หลักของ LiteLLM:

  • Single Endpoint: เรียก API เดียว (เช่น http://localhost:4000/v1/chat/completions) แล้ว LiteLLM จะ forward request ไปยัง backend ที่เหมาะสมตาม configuration
  • Multi-Provider Support: รองรับ OpenAI, Anthropic, OpenRouter, Google VertexAI, AWS Bedrock, และ endpoint ในบ้านเราเอง (local vLLM/Ollama)
  • Virtual Keys / API Key Management: สร้าง API key เสมือนสำหรับแต่ละ user หรือ project โดยไม่ต้อง expose key จริงของ provider แอปของคุณใช้ virtual key นี้เรียก API ได้เลย LiteLLM จะจัดการ mapping ไปยัง key จริง
  • Usage Tracking & Logging: ดูการใช้งานผ่าน /ui endpoint (เช่น http://localhost:4000/ui) สามารถดู:
  • จำนวน request ต่อ model/provider
  • Token usage (input/output tokens)
  • Latency และ response time
  • Cost tracking (ถ้าใช้ paid API)
  • Rate limit และ quota usage

ตัวอย่างการใช้งาน: แอปของคุณเรียก POST /v1/chat/completions กับ LiteLLM:4000 โดยใส่ model="qwen3.5-9b" LiteLLM จะ forward ไปยัง vLLM:8900 ที่รันโมเดลนั้นอยู่

เส้นทางของ request

ตามลูกศรใน figure การไหลของ request มีดังนี้:

  1. แอป / ai-local → LiteLLM :4000 แอปของคุณส่ง request HTTP ไปยัง LiteLLM proxy ที่ port 4000 พร้อม payload ที่ต้องการ (prompt, temperature, model name)
  2. LiteLLM :4000 → vLLM :8900 LiteLLM จะ parse request และ forward ไปยัง backend ที่เหมาะสม (ในกรณีนี้คือ vLLM ที่ port 8900) โดยแปลง API format ให้เข้ากันได้
  3. vLLM :8900 → GPU 16GB vLLM จะรับ request และส่งไปยัง GPU (RTX 5060 Ti 16GB) เพื่อประมวลผล inference โดยใช้ PagedAttention จัดการ KV cache
  4. GPU → vLLM → LiteLLM → แอป ผลลัพธ์จาก GPU จะถูกส่งกลับผ่าน vLLM → LiteLLM → แอปของคุณในลำดับกลับกัน

ฮาร์ดแวร์ที่ใช้

รายการฮาร์ดแวร์ที่ใช้ในคู่มือนี้:

  • GPU RTX 5060 Ti 16GB สถาปัตยกรรม Blackwell compute capability 12.0 - นี่คือเพดานของคู่มือนี้เพราะเป็น GPU entry-level ที่ยังรัน LLM ขนาด 9B-14B ได้ดี มี VRAM เพียงพอสำหรับ quantized models (w4a16, w8a16) และรองรับ concurrent requests ได้ประมาณ 2-4 requests พร้อมกัน
  • ไดรเวอร์ NVIDIA 580.173.02 สำหรับรองรับ CUDA 12.x และ Blackwell architecture
  • ระบบปฏิบัติการ Ubuntu Linux (แนะนำ Ubuntu 24.04 LTS)
  • โมเดล RedHatAI/Qwen3.5-9B-quantized.w4a16 ขนาด 11GB ใช้ quantization 4-bit weight activation 16-bit

ทำไม RTX 5060 Ti 16GB ถึงเป็นเพดาน: GPU นี้มี VRAM 16GB ซึ่งเพียงพอสำหรับรันโมเดลขนาด 9B-14B ใน quantized format ได้ดี แต่ไม่เพียงพอสำหรับโมเดลขนาด 70B+ แบบ full precision การเพิ่ม VRAM จะช่วยรองรับโมเดลที่ใหญ่ขึ้นและ concurrent requests ได้มากขึ้น

แอป / ai-local LiteLLM :4000 vLLM :8900 GPU 16GB

2. เตรียมเครื่องก่อนติดตั้ง

ตรวจการ์ดจอและไดรเวอร์ก่อน

ตรวจสอบการ์ดจอและไดรเวอร์ NVIDIA ด้วยคำสั่ง:

nvidia-smi

ผลลัพธ์จะแสดงตารางที่มีหลายคอลัมน์ที่สำคัญ:

  • GPU Name: ชื่อการ์ดจอ เช่น NVIDIA GeForce RTX 5060 Ti, NVIDIA GeForce RTX 5090, หรือ NVIDIA H100
  • Driver Version: เวอร์ชันไดรเวอร์ที่ติดตั้งอยู่ เช่น 580.173.02, 550.54.14, 560.35.03
  • VRAM (Video Memory): ความจำของการ์ดจอ เช่น 16384 MiB (16GB), 8192 MiB (8GB)
  • Processes: แสดงชื่อโปรแกรมที่กำลังใช้ GPU และ VRAM ที่ใช้
  • Power Usage: กำลังไฟที่ใช้ (Watt)
  • Temperature: อุณหภูมิของการ์ดจอ (°C)

ตัวอย่าง output สำหรับ RTX 5060 Ti + ไดรเวอร์ 580.173.02:

+-----------------------------------------------------------------------------+
| NVIDIA-SMI 580.173.02   Driver Version: 580.173.02   CUDA Version: 12.9     |
|-------------------------------+----------------------+----------------------+
| GPU  Name/UUID                 | Memory-Usage | Persistence-in Process |
|-------------------------------+----------------------+----------------------+
|   0  NVIDIA GeForce RTX 5060 Ti | 1024MiB / 16384MiB | On / Default   |
|-------------------------------+----------------------+----------------------+

ต้องเห็นชื่อการ์ดและเลขไดรเวอร์ 580 ขึ้นไป ถ้าเป็น Blackwell ต้องใช้ CUDA 12.9 ขึ้นไปเท่านั้น

ทำไม Blackwell sm_120 ต้อง CUDA 12.9 ขึ้นไป

สถาปัตยกรรม Blackwell (RTX 50xx series) ใช้ CUDA compute capability sm_120 ซึ่ง:

  • เป็นสถาปัตยกรรมใหม่ล่าสุดที่ NVIDIA ออกแบบมาเฉพาะสำหรับ Blackwell
  • CUDA Toolkit 12.x (12.0, 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8) ไม่รองรับ sm_120
  • CUDA Toolkit 12.9 เป็นเวอร์ชันแรกที่มี support สำหรับ sm_120
  • PyTorch 2.6+ และ vLLM 0.8+ ใช้ CUDA 12.x เป็น default และต้องการ sm_120 สำหรับ Blackwell
  • ถ้าใช้ CUDA 12.8 หรือต่ำกว่า จะไม่สามารถ compile หรือ run code ที่ใช้ sm_120 ได้

คำสั่งเช็ค CUDA version ที่ติดตั้ง:

nvidia-smi | grep -i "cuda version"

ปิดบริการที่แย่ง VRAM

ถ้ามี Ollama รันอยู่ให้หยุดและปิดถาวร:

systemctl stop ollama
systemctl disable ollama

ถ้ามี ComfyUI หรือโปรแกรมอื่นกิน VRAM ให้ปิดด้วย เช็คด้วย nvidia-smi ดูช่อง Processes

วิธีปิด ComfyUI:

# ปิด ComfyUI (ถ้ารันด้วย python)
pkill -f "python.*comfyui"
# หรือถ้ารันด้วย uvicorn
pkill -f "uvicorn.*comfyui"
# หรือถ้ารันด้วย nodejs
pkill -f "node.*comfyui"

วิธีดู process ที่กิน VRAM:

nvidia-smi --query-gpu=process_name,used_memory --format=csv

ผลลัพธ์จะแสดงชื่อ process และ VRAM ที่ใช้ เช่น:

process_name,used_memory
python,8192
comfyui,4096

ถ้าเห็น process ที่ไม่ต้องการ ให้ kill ด้วย:

pkill -f "python.*comfyui"

เช็ค VRAM ว่างจริง

ตรวจสอบ VRAM ที่เหลือว่างด้วยคำสั่ง:

nvidia-smi --query-gpu=memory.used,memory.total --format=csv

ต้องเหลือว่างอย่างน้อย 15GB จาก 16GB

เช็คพื้นที่ดิสก์

ตรวจสอบพื้นที่ดิสก์ว่างด้วยคำสั่ง:

df -h

ต้องว่างอย่างน้อย 25GB เพราะ:

  • venv PyTorch+CUDA + dependencies ~10GB
  • โมเดล LLM ~11GB (Qwen3.5-9B w4a16)
  • cache และ temporary files ~4GB

ตัวอย่าง output:

Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1       100G   40G   60G  41% /

ถ้าไม่พอให้เพิ่ม space หรือลบไฟล์ที่ไม่จำเป็นออก

เช็ค RAM free

ตรวจสอบ RAM ที่เหลือว่าง:

free -h

ตัวอย่าง output:

              total        used        free      shared  buff/cache   available
Mem:           16G         2.5G         8.0G        500M       5.0G         11.0G

ต้องเหลือ free อย่างน้อย 4GB สำหรับระบบปฏิบัติการและ buffer

3. ติดตั้ง vLLM

ทำไมต้อง venv แยกที่ /opt/vllm? การสร้าง virtual environment แยกต่างหากที่ /opt/vllm มีความสำคัญหลายประการ: (1) ป้องกันการ conflict กับ Python packages ของระบบหลักที่อาจมีเวอร์ชันต่างกัน (2) ทำให้ deployment ง่ายขึ้น เพราะสามารถ copy directory ทั้งชุดไป server อื่นได้โดยไม่ต้องติดตั้งใหม่ (3) แยก dependencies ของ vLLM ออกจากแอปอื่นที่ใช้ Python เดียวกัน (4) ง่ายต่อการ rollback หรือ uninstall หากต้องการ (5) มีสิทธิ์ access ที่ชัดเจนสำหรับ /opt/* ซึ่งมักเป็น standard สำหรับ production environments

สร้าง virtualenv แยกที่ /opt/vllm เพื่อไม่ปนกับ Python ระบบ:

python3 -m venv /opt/vllm

ติดตั้ง pip และ vllm ใน virtualenv:

/opt/vllm/bin/pip install --upgrade pip
/opt/vllm/bin/pip install vllm

ทำไม pip install vllm ใช้เวลานาน? การติดตั้ง vLLM ผ่าน pip ใช้เวลานาน 10-20 นาที เพราะ vLLM มี dependencies ขนาดใหญ่มาก: (1) PyTorch 2.x มีขนาดประมาณ 2-3 GB สำหรับ CPU-only และมากกว่านั้นสำหรับ CUDA-enabled wheels (2) CUDA libraries ที่ compile มาด้วย (cuDNN, NCCL, cuBLAS, cuSPARSE) รวมกันอีกหลาย GB (3) FlashAttention และ FlashInfer ที่ compile มาด้วยสำหรับ GPU acceleration (4) Transformers library ที่ใช้สำหรับ model architecture (5) numpy และ scipy ที่ใช้สำหรับ numerical operations การดาวน์โหลดและ unpack packages เหล่านี้จึงใช้เวลานาน โดยเฉพาะบนเครือข่ายที่มี bandwidth จำกัด

ปัญหาการ์ด Blackwell sm_120 และ FlashInfer: เจาะลึก การ์ด Blackwell (sm_120) เป็น GPU รุ่นใหม่ล่าสุดของ NVIDIA ที่ใช้ architecture H100/H200 แต่ FlashInfer ซึ่งเป็น JIT compiler ของ vLLM สำหรับ optimize inference มีข้อจำกัด: (1) FlashInfer ใช้ nvcc (NVIDIA CUDA Compiler) เพื่อ compile kernels ตอนรันจริง (runtime JIT) (2) nvcc ของระบบ (system nvcc) เป็น CUDA 12.0 ซึ่งเก่าเกินไปสำหรับ Blackwell ที่ต้องการ CUDA 13.3 ขึ้นไป (3) ข้อความ error "FlashInfer requires GPUs with sm75 or higher" เป็น ข้อความหลอก เพราะ sm_120 > sm_75 จริง แต่ปัญหาไม่ใช่ GPU architecture แต่เป็น nvcc version (4) วิธีแก้: ชี้ CUDA_HOME ของ venv ไปที่ CUDA 13.x ที่ติดตั้งใน venv (/opt/vllm/lib/python3.12/site-packages/nvidia/cu13) แทนที่จะใช้ system CUDA (5) ต้องมี ninja ติดตั้งด้วย เพราะ vLLM ใช้ ninja สำหรับ compile CUDA kernels (pip install vllm จะติดตั้ง ninja มาด้วยโดยอัตโนมัติในหลายกรณี แต่ควรตรวจสอบ)

ตรวจว่าติดตั้งสำเร็จ:

/opt/vllm/bin/vllm --version

4. เลือกและโหลดโมเดล

ทำไมต้อง w4a16 บนการ์ด 16GB

การเลือกโมเดล Qwen3.5-9B-w4a16 เป็นทางเลือกเดียวที่ใช้งานได้จริงบนการ์ด 16GB เนื่องจากเทคนิค quantization แบบ w4a16 (weight 4-bit, activation 16-bit) ช่วยลดขนาดโมเดลลงได้อย่างมีประสิทธิภาพ โดย:

  • Weights 4-bit: น้ำหนักของโมเดลถูกบีบอัดจาก 16-bit (FP16) เหลือ 4-bit ลดพื้นที่เก็บข้อมูลลง 4 เท่า ทำให้โมเดล 9B ขนาดเดิม 150GB ลดเหลือเพียง 11GB
  • Activations 16-bit: การคำนวณ activation ยังคงใช้ 16-bit เพื่อรักษาความแม่นยำในการ inference ไม่สูญเสียคุณภาพการตอบกลับ
  • ประหยัด VRAM: การลด bit depth ของ weights ช่วยประหยัด VRAM ได้มาก แต่ต้องแลกกับการใช้ memory bandwidth เพิ่มขึ้นเล็กน้อยในการ decode

VRAM Math: ทำไมพอดี 16GB

การคำนวณ VRAM usage ของ Qwen3.5-9B-w4a16 บนการ์ด 16GB:

องค์ประกอบ ขนาด คำอธิบาย
Model Weights (w4a16) 11GB น้ำหนักโมเดล 9B แบบ quantized 4-bit weights + 16-bit activations
KV Cache (FP8) 2.4GB Key-Value cache สำหรับ context window 8K-16K tokens ใช้ FP8 precision
System Reserve 1.21GB สำรองสำหรับ CUDA kernel, allocator overhead, และ buffer อื่นๆ
Total 16GB พอดีกับ VRAM ของการ์ด 16GB (A100/A10/A30)

หมายเหตุ: หากใช้ FP16 แทน w4a16 จะกิน 18GB+ ไม่พอสำหรับการ์ด 16GB

VRAM Math: 11GB (weights) + 2.4GB (KV cache FP8) + 1.21GB (reserve) = 14.61GB ≈ 16GB (เหลือ buffer เพิ่มเติมเล็กน้อย)

ทำไมโมเดล 27B ใส่ไม่ได้ทั้งที่ GPTQ-Int4

แม้ GPTQ-Int4 จะบีบอัดโมเดลได้ดี แต่ Qwen3.5-27B มีปัญหาเฉพาะ:

  • Vocabulary Size ใหญ่: Qwen3.5 ใช้ vocab ขนาด 248,320 tokens (ใหญ่กว่า GPT-3.5 ที่ 32,000 tokens มาก)
  • Embedding Layer บวม: embedding layer มีขนาดเท่ากับ vocab size ทำให้หนักมาก
  • LM Head บวม: output layer (lm_head) ก็ขนาดเท่า vocab เช่นกัน
  • ผลรวม: แม้ weights หลักบีบได้ แต่ embedding+lm_head รวมกันกินพื้นที่มหาศาล
โมเดล ขนาดน้ำหนัก สถานะ เหตุผล
Qwen3.5-27B-FP8 30.87GB ❌ ใส่ไม่ลง FP16 quantization ไม่ลดขนาดพอ
Qwen3.5-27B-GPTQ-Int4 30.24GB ❌ ใส่ไม่ลง embedding+lm_head บวมจาก vocab 248k
Qwen3.5-9B-w4a16 11GB ✅ ใช้ได้จริง quantization 4-bit ลดลง 4 เท่า พอดี 16GB

โหลดโมเดลด้วย huggingface-cli

ขั้นตอนการดาวน์โหลดโมเดล:

  1. ตรวจสอบว่า huggingface-cli ติดตั้งแล้ว:
pip install huggingface_hub
  1. ดาวน์โหลดโมเดล:
/opt/vllm/bin/hf download RedHatAI/Qwen3.5-9B-quantized.w4a16

หรือใช้:

huggingface-cli download RedHatAI/Qwen3.5-9B-quantized.w4a16 --local-dir /tmp/vllm/models/qwen3.5-9b-w4a16
  1. ตรวจสอบไฟล์ที่โหลดเสร็จ:
ls -lh ~/.cache/huggingface/hub/models--RedHatAI--Qwen3.5-9B-quantized.w4a16/

หรือ:

ls -lh /tmp/vllm/models/qwen3.5-9b-w4a16/

ไฟล์หลักที่ควรมี:

  • model.safetensors - weights หลัก (4-bit)
  • config.json - configuration ของโมเดล
  • tokenizer.json - tokenizer สำหรับ text processing

⚠️ คำเตือน: DFlash และ Speculative Decoding

⚠️ คำเตือน: อย่าเปิด speculative decoding แบบ DFlash (draft model) บนการ์ด 16GB เพราะ draft model มี lm_head ขนาด 1.89GB จาก vocab 248,320 tokens ทำให้ VRAM ล้นทันที

รายละเอียดปัญหา:

  • Draft Model: speculative decoding ใช้ draft model ขนาดเล็กช่วย generate tokens ก่อนส่งให้ target model
  • LM Head แยก: draft model ของ Qwen3.5 ต้องโหลด lm_head แบบ bf16 แยกต่างหาก 1.89GB เพราะ vocab ใหญ่
  • VRAM Calculation: เมื่อโหลด target model เสร็จแล้ว เหลือว่าง 1.21GB จึง OOM ทันที

คำแนะนำ: ปิด speculative decoding หรือใช้ draft model ขนาดเล็กมากๆ (เช่น 1B-3B) ที่ใช้ vocab มาตรฐาน

VRAM Usage โมเดล 11GB KV cache 2.4GB สำรอง 1.21GB

5. โมเดลชื่ออะไร เอามาจากไหน และวิธีทำเอง

โมเดลที่คู่มือใช้ชื่อเต็มว่า RedHatAI/Qwen3.5-9B-quantized.w4a16 อยู่บน Hugging Face Hub คนทำคือ org RedHatAI (Red Hat AI ต่อยอดจาก Neural Magic) ทำโดย quantize จากโมเดลต้นฉบับ Qwen/Qwen3.5-9B

w4a16 คือ weight 4-bit + activation 16-bit จึงเล็กลง ~4 เท่า

วิธีโหลดสำเร็จรูป

ใช้คำสั่ง:

/opt/vllm/bin/hf download RedHatAI/Qwen3.5-9B-quantized.w4a16

วิธีทำเองด้วยไลบรารี llmcompressor

ติดตั้งไลบรารีก่อน:

pip install llmcompressor

จากนั้นรัน Python script แบบใน pre:

from llmcompressor import oneshot

oneshot(model="Qwen/Qwen3.5-9B", scheme="W4A16", dataset="ultrachat_200k", output_dir="Qwen3.5-9B-quantized.w4a16")

ความต้องการเครื่อง

RAM อย่างน้อย 32GB ดิสก์ว่าง 40GB รันบน CPU ล้วนก็ได้ไม่ต้องมี GPU แต่ใช้เวลาหลายชั่วโมง

แนะนำ: ถ้ามีสำเร็จรูปอยู่แล้ว แนะนำโหลดเลยเร็วกว่าทำเองมาก ใช้ทำเองเมื่อโมเดลที่อยากได้ยังไม่มีใคร quantize

6. ตั้งให้รันอัตโนมัติด้วย systemd

เพื่อให้บริการ vLLM รันอัตโนมัติเมื่อระบบบูต เราต้องสร้าง launcher script และ systemd unit file

สร้างสคริปต์ launcher

สร้างไฟล์สคริปต์ที่ /opt/vllm/serve-qwen.sh แล้วรันคำสั่ง chmod +x เพื่อให้มีสิทธิ์ execute

#!/usr/bin/env bash
set -euo pipefail
exec /opt/vllm/bin/vllm serve RedHatAI/Qwen3.5-9B-quantized.w4a16 --attention-backend FLASHINFER --max-model-len 32768 --max-num-batched-tokens 16384 --max-num-seqs 1 --gpu-memory-utilization 0.95 --enable-prefix-caching --kv-cache-dtype fp8 --enable-auto-tool-choice --tool-call-parser qwen3_xml --reasoning-parser qwen3 --host 0.0.0.0 --port 8900

เหตุผลที่ต้องแยกเป็นสคริปต์: systemd จะตัดเครื่องหมายคำพูดในบรรทัด ExecStart ทำให้ค่า JSON พัง การแยกคำสั่งเป็นสคริปต์จะแก้ปัญหาได้

คำอธิบาย flag ใน serve-qwen.sh

แต่ละ flag มีหน้าที่ดังนี้:

  • --attention-backend FLASHINFER: ใช้ FlashInfer backend สำหรับ attention mechanism เพื่อเพิ่มความเร็วในการ inference โดยเฉพาะกับโมเดลขนาดใหญ่
  • --max-model-len 32768: กำหนดความยาวสูงสุดของ input sequence ที่รองรับได้ 32,768 tokens
  • --max-num-batched-tokens 16384: กำหนดจำนวน tokens สูงสุดที่ประมวลผลพร้อมกันใน batch 16,384 tokens
  • --max-num-seqs 1: กำหนดจำนวน sequence สูงสุดที่ประมวลผลพร้อมกัน 1 sequence
  • --gpu-memory-utilization 0.95: ใช้ GPU memory 95% ของที่ติดตั้งไว้
  • --enable-prefix-caching: เปิดใช้งาน prefix caching เพื่อเพิ่มประสิทธิภาพเมื่อมี request ที่ share prefix เดียวกัน
  • --kv-cache-dtype fp8: ใช้ fp8 dtype สำหรับ KV cache เพื่อลดการใช้ memory และเพิ่มความเร็ว
  • --enable-auto-tool-choice: เปิดใช้งาน auto tool choice สำหรับโมเดลที่รองรับ tool calling
  • --tool-call-parser qwen3_xml: ใช้ parser แบบ XML สำหรับ parsing tool calls ของ Qwen3
  • --reasoning-parser qwen3: ใช้ reason parser ของ Qwen3 สำหรับ parsing reasoning steps
  • --host 0.0.0.0: เปิดรับ connection จากทุก interface (public)
  • --port 8900: เปิดรับ connection ที่ port 8900

สร้าง unit file

สร้างไฟล์ unit ที่ /etc/systemd/system/vllm.service ด้วยเนื้อหาต่อไปนี้

[Unit]
Description=vLLM OpenAI-compatible server
After=network-online.target

[Service]
Type=simple
User=root
Environment=CUDA_HOME=/opt/vllm/lib/python3.12/site-packages/nvidia/cu13
Environment=PATH=/opt/vllm/lib/python3.12/site-packages/nvidia/cu13/bin:/opt/vllm/bin:/usr/bin:/bin
ExecStart=/opt/vllm/serve-qwen.sh
Restart=on-failure
TimeoutStartSec=900

[Install]
WantedBy=multi-user.target

บรรทัด CUDA_HOME สำคัญมาก: ถ้าไม่ใส่ FlashInfer จะ compile ไม่ผ่านบนการ์ด Blackwell

คำอธิบาย unit file directives

แต่ละ directive ใน unit file มีหน้าที่ดังนี้:

[Unit]

  • Description: บรรทัดอธิบาย service สำหรับ display ใน systemctl list-units
  • After=network-online.target: รอให้ network-online.target สำเร็จก่อน (สำหรับระบบที่ใช้ network-online.target)

[Service]

  • Type=simple: กำหนดว่า service เป็น foreground process (ไม่ใช้ Type=forking)
  • User=root: รัน service ด้วย user root (ในกรณีนี้จำเป็นเพราะต้องเข้าถึง CUDA resources)
  • Environment=CUDA_HOME=...: กำหนด environment variable CUDA_HOME ชี้ไปยัง CUDA 13.0 ใน venv
  • Environment=PATH=...: เพิ่ม /opt/vllm/bin และ CUDA bin เข้า PATH เพื่อใช้คำสั่ง vllm และ nvcc
  • ExecStart=/opt/vllm/serve-qwen.sh: เรียกใช้ launcher script ที่สร้างไว้
  • Restart=on-failure: restart service เมื่อ service ล้มเหลว
  • TimeoutStartSec=900: กำหนด timeout 900 วินาที (15 นาที) สำหรับ startup เพราะการโหลดโมเดลรอบแรกใช้เวลานานมาก

[Install]

  • WantedBy=multi-user.target: ให้ service นี้ถูก enable เมื่อระบบ boot เป็น multi-user mode

เรื่องราว CUDA_HOME: ปัญหาและวิธีแก้

การตั้งค่า CUDA_HOME เป็นเรื่องซับซ้อนที่ต้องผ่านหลายขั้นตอน:

  1. nvcc หา CUDA 12.0: เมื่อรัน FlashInfer build nvcc จะหา CUDA toolkit ที่ระบบติดตั้งอยู่ ซึ่งมักเป็น CUDA 12.0
  2. FlashInfer พ่นข้อความหลอก sm75: ข้อความนี้เป็นข้อความหลอก — การ์ดเราเป็น sm_120 สูงกว่า sm75 เสียอีก ต้นเหตุจริงคือ nvcc ของระบบ (CUDA 12.0) เก่าเกินกว่าจะ compile โค้ดสำหรับ sm_120 ได้
  3. แก้ด้วยชี้ CUDA_HOME ไป cu13: ต้องชี้ CUDA_HOME ไปยัง CUDA 13.0 ที่ติดตั้งใน venv (/opt/vllm/lib/python3.12/site-packages/nvidia/cu13) และเพิ่ม /opt/vllm/bin เข้า PATH เพราะ venv มีคำสั่ง ninja ที่จำเป็น
  4. Error ต่อ "CUDA compiler and CUDA toolkit headers are incompatible": แม้จะแก้ CUDA_HOME แล้ว แต่ nvcc 13.3 (ที่มากับ CUDA 13) ไม่ตรงกับ CUDA runtime headers 13.0 ที่ FlashInfer ใช้ ทำให้ compile ล้มเหลวอีก
  5. แก้ด้วย pip install: แก้ปัญหาด้วย pip install "nvidia-cuda-runtime==13.3.29" "nvidia-cuda-nvrtc==13.3.*" เพื่อให้ CUDA runtime headers และ nvcc version match กัน

ดู log

ดู log ของ service ด้วยคำสั่ง:

journalctl -u vllm -f

7. ติดตั้งและต่อ LiteLLM

รันด้วย Docker Compose

ที่โฟลเดอร์ /opt/litellm ต้องมี 2 service คือ litellm-db ใช้ image postgres:16-alpine และ litellm ใช้ image ghcr.io/berriai/litellm:main-stable เปิดพอร์ต 4000

version: '3.8'

services:
  litellm-db:
    image: postgres:16-alpine
    container_name: litellm-db
    environment:
      POSTGRES_USER: litellm
      POSTGRES_PASSWORD: litellm_password
      POSTGRES_DB: litellm
    volumes:
      - ./litellm-db-data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    networks:
      - litellm-network

  litellm:
    image: ghcr.io/berriai/litellm:main-stable
    container_name: litellm
    ports:
      - "4000:4000"
    volumes:
      - ./config.yaml:/app/config.yaml
    environment:
      - DATABASE_URL=postgresql://litellm:litellm_password@litellm-db:5432/litellm
      - STORE_MODEL_IN_DB=True
      - MASTER_KEY=${MASTER_KEY}
    depends_on:
      - litellm-db
    networks:
      - litellm-network

networks:
  litellm-network:
    driver: bridge

เพิ่มโมเดล vLLM เข้า LiteLLM

ถ้าตั้ง store_model_in_db เป็น true ค่าใน config.yaml จะถูก database ทับ ต้องเพิ่มผ่าน API แทน โดยยิง POST ไปที่ /model/new

vLLM ไม่ต้องใช้ key แต่ OpenAI SDK ที่ LiteLLM เรียกใช้บังคับต้องมี api_key ถ้าไม่ใส่จะเจอ error ว่า api_key client option must be set ให้ใส่ค่าอะไรก็ได้
เลข 172.17.0.1 คือ IP ของเครื่องแม่เมื่อมองจากใน Docker container เพราะเป็น default gateway ของ network docker0 ถ้าใช้ 127.0.0.1 จะชี้กลับเข้า container ตัวเอง

ทดสอบการเพิ่มโมเดล vLLM

ใช้ curl ส่ง POST request ไปที่ /model/new พร้อม Authorization header และ JSON body ที่ระบุพารามิเตอร์ของ vLLM

curl -X POST "http://localhost:4000/model/new" \
  -H "Authorization: Bearer sk-1234" \
  -H "Content-Type: application/json" \
  -d '{
    "model_name": "litellm-vllm",
    "litellm_params": {
      "model": "openai/Qwen3.5-9B-quantized.w4a16",
      "api_base": "http://172.17.0.1:8900/v1",
      "api_key": "dummy"
    }
  }'

ทดสอบการเรียกใช้โมเดล

หลังจากเพิ่มโมเดลสำเร็จ สามารถทดสอบการเรียกใช้ผ่าน endpoint chat completions ได้ทันที

curl http://localhost:4000/v1/chat/completions \
  -H "Authorization: Bearer sk-1234" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "litellm-vllm",
    "messages": [
      {"role": "user", "content": "สวัสดีครับ"}
    ]
  }'
ถ้า config.yaml เดิมชี้ ollama ที่ 172.17.0.1:11434 ให้แก้ชี้ vLLM :8900 แทน โดยแก้ api_base ใน config.yaml จาก http://172.17.0.1:11434/v1 เป็น http://172.17.0.1:8900/v1

8. จูนให้ได้ประสิทธิภาพสูงสุด

ค่า flag สำคัญ

flag ค่าที่ใช้ เหตุผล กลไกเชิงเทคนิค
--gpu-memory-utilization 0.95 ใช้ VRAM 95% เหลือ 5% กันระบบล้ม กำหนดสัดส่วน VRAM ที่ vLLM จองไว้สำหรับ model weights และ KV cache โดยค่า 0.95 หมายความว่าจอง 95% ของ GPU memory ทั้งหมด (เช่น RTX 5060 Ti 16GB จะจอง 15.2GB) เหลือ 5% สำหรับระบบปฏิบัติการและกระบวนการอื่น ๆ เพื่อป้องกัน OOM เมื่อโหลดหลายโมเดลหรือมี overhead อื่น ๆ
--max-model-len 32768 เพดาน context ตั้งสูงเกินจะ OOM กำหนดความยาว context สูงสุดที่ vLLM จะรองรับ (หน่วยเป็น tokens) ค่า 32768 tokens หมายความว่าผู้ใช้สามารถส่ง prompt ยาวสูงสุด 32,768 tokens ได้ โดย vLLM จะจอง memory สำหรับ KV cache ตามค่านี้ล่วงหน้า (แม้จะไม่ใช้ทั้งหมด) เพื่อรองรับ use case ที่ต้องการ context window ยาว เช่น RAG, long document summarization
--kv-cache-dtype fp8 บีบ KV cache ครึ่งหนึ่ง ได้ context ยาวขึ้นเท่าตัว กำหนด dtype ของ KV cache เป็น fp8 (float8) แทน fp16 fp8 มีขนาด 16-bit ต่ำกว่า fp16 ที่ 32-bit ทำให้บีบอัด KV cache ครึ่งหนึ่ง (50% memory saving) โดยยังคงความแม่นยำเพียงพอสำหรับการ inference ส่งผลให้สามารถรองรับ context window ยาวขึ้นได้เท่าตัวใน VRAM เดียวกัน
--enable-prefix-caching เปิด prompt ซ้ำไม่ต้อง prefill ใหม่ เปิดใช้งาน prefix caching สำหรับ prompt prefix ซ้ำ เช่น system prompt หรือส่วนต้นของ conversation ที่ซ้ำกัน vLLM จะ cache KV cache ของ prefix ไว้ใน memory เมื่อมี request ที่ใช้ prefix เดียวกัน vLLM จะ reuse KV cache นั้นแทนที่จะคำนวณ prefill ใหม่ ลด latency และ GPU utilization สำหรับ request ที่มีส่วนต้นซ้ำกัน
--max-num-seqs 1 การ์ดเล็กรับทีละ request จำกัดจำนวน sequence ที่ประมวลผลพร้อมกัน (concurrent requests) ค่า 1 หมายความว่ารับทีละ request เดียว ป้องกัน OOM บนการ์ดเล็ก ๆ เพราะแต่ละ sequence ต้องจอง KV cache แยกกัน การจำกัดที่ 1 ช่วยควบคุม memory usage ให้คงที่และ predictable สำหรับ deployment ขนาดเล็ก

ผลทดสอบจริง context กับความเร็ว

วัดจากเครื่องจริง RTX 5060 Ti 16GB ทุกค่ารันซ้ำ 2 ถึง 3 รอบ

วิธีอ่านตาราง: เมื่อ input tokens ยาวขึ้น (เช่น จาก 1K เป็น 32K) เวลาตอบกลับกลับสั้นลงหรือคงที่ เพราะ vLLM ใช้ prefill phase (decode prompt) ครั้งเดียวสำหรับ input ยาว แล้วใช้ decode phase (generate tokens) ต่อมาซึ่งเร็วมากและใช้ GPU compute น้อยกว่า prefill การที่ input tokens 1363 ใช้เวลา 5.6 วินาที แต่ 31813 tokens ใช้เวลาเท่ากัน 4.7 วินาที แสดงว่า prefill phase สำหรับ input ยาวใช้เวลานาน แต่ decode phase เร็วมากจนรวมแล้วเวลาไม่เพิ่มขึ้นมาก

input tokens เวลาตอบ ความเร็ว
1363 5.6 วินาที 48.0 tok/s
4426 11.3 วินาที 48.6 tok/s
12406 4.4 วินาที 44.3 tok/s
20576 4.8 วินาที 44.8 tok/s
28750 4.7 วินาที 44.9 tok/s
31813 4.7 วินาที 44.9 tok/s
ประสิทธิภาพไม่ตกเลยจนถึงเพดาน 32768 tokens ความเร็วลดแค่ 48 เหลือ 45 tok/s คิดเป็น 6 เปอร์เซ็นต์
tok/s 1K 4K 12K 20K 28K 32K

วิธีวัดเอง

ทดสอบด้วยตัวเองด้วย curl POST /v1/chat/completions วัดเวลา:

curl -s -X POST "http://localhost:8000/v1/chat/completions" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "your-model",
    "messages": [{"role": "user", "content": "Hello"}],
    "max_tokens": 100
  }' | jq '.usage.completion_tokens' && time curl -s -X POST "http://localhost:8000/v1/chat/completions" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "your-model",
    "messages": [{"role": "user", "content": "Hello"}],
    "max_tokens": 100
  }'

9. ปัญหาที่เจอบ่อยและวิธีแก้

อาการ สาเหตุจริง วิธีแก้
FlashInfer requires GPUs with sm75 or higher nvcc CUDA 12.0 Environment=CUDA_HOME=/opt/vllm/lib/python3.12/site-packages/nvidia/cu13
CUDA compiler and CUDA toolkit headers are incompatible nvcc 13.3 vs runtime 13.0 pip install nvidia-cuda-runtime==13.3.29 nvidia-cuda-nvrtc==13.3.*
CUDA out of memory ตอนโหลด draft model การ์ด 16GB ไม่พอ speculative_decoding: false (ปิด speculative decoding)
no healthy deployments for this model LiteLLM เก็บโมเดลใน database POST /model/new (เพิ่มโมเดลใหม่)
api_key client option must be set litellm ต้องการ api_key litellm_params: { "api_key": "sk-xxx" } (ใส่ api_key อะไรก็ได้)
request ค้างคิว ยิงใหม่ไม่ตอบ max-num-seqs เป็น 1 ทำให้คิวตัน systemctl restart vllm (รีสตาร์ทเซิร์ฟเวอร์)
โมเดลโหลดค้าง/รีสตาร์ทไม่จบ TimeoutStartSec น้อยเกินไป TimeoutStartSec=900 + journalctl -u vllm -f (ดูความคืบหน้า)
LiteLLM ยังชี้ ollama เก่า config.yaml ชี้ ollama_chat ที่ 172.17.0.1:11434 แก้ config.yaml: เปลี่ยนจาก ollama_chat ที่ 172.17.0.1:11434 เป็น openai ที่ 172.17.0.1:8900