← Kembali ke Blog

Jalankan API LLM Sendiri dengan vLLM: Server, Streaming, Tool Call, dan Metrik Nyata

Satu model 8B muat di GPU 24GB. Tapi sepuluh user bersamaan nggak akan muat kalau kamu bungkus model.generate() pakai FastAPI. Wrapper seperti itu memproses satu request per forward pass, jadi sebagian besar GPU nganggur sambil nunggu token terakhir selesai.

vLLM mengganti loop itu dengan scheduler. Request baru ikut masuk ke batch yang sedang jalan di antara langkah decode, KV cache disimpan dalam blok, bukan dialokasikan penuh per request, dan lapisan HTTP-nya mengikuti API OpenAI. Kode client yang tadinya nembak model hosted tetap jalan setelah kamu ganti base_url.

Berikut urutan setup yang saya sarankan di mesin baru: install, flag launch yang benar-benar mengubah perilaku, client streaming, tool calling, dan metrik yang kasih tahu apakah mesinnya masih kuat.

Yang perlu disiapkan

  • Host Linux dengan GPU NVIDIA. Contoh di sini target kartu 24GB (RTX 4090, L4, A10G). Cek dulu pakai nvidia-smi.
  • Driver yang cukup baru untuk CUDA 12. Wheel pip sudah bawa runtime CUDA sendiri, jadi toolkit penuh nggak perlu.
  • Python 3.11 sampai 3.14. Metadata PyPI vLLM 0.32.0 nulis >=3.11,<3.15. Pilih 3.12 kalau nggak ada alasan lain.
  • uv, plus sekitar 25GB disk untuk bobot model dan cache kompilasi.
  • Nggak perlu token Hugging Face untuk Qwen/Qwen3-8B. Checkpoint gated seperti Llama butuh HF_TOKEN.

Langkah 1: install vLLM

curl -LsSf https://astral.sh/uv/install.sh | sh
uv venv ~/vllm --python 3.12
source ~/vllm/bin/activate
uv pip install vllm
vllm --version

Versi terbaru saat artikel ini ditulis adalah 0.32.0, rilis 18 Oktober 2026. Jangan install ke interpreter sistem. vLLM mematok versi torch dan CUDA tertentu, dan bakal bentrok dengan paket lain di sana.

Opsional tapi berguna:

vllm download-kernels

Perintah ini ngambil kernel yang sudah dikompilasi, jadi startup pertama nggak lama-lama compile.

Langkah 2: jalankan server

export VLLM_API_KEY="$(openssl rand -hex 24)"

vllm serve Qwen/Qwen3-8B \
  --served-model-name local-8b \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.90 \
  --api-key "$VLLM_API_KEY" \
  --enable-auto-tool-choice \
  --tool-call-parser hermes

Arti tiap flag:

--served-model-name bikin alias yang muncul di /v1/models. Tanpa ini, semua client harus pakai path repo Hugging Face lengkap, dan rusak begitu kamu ganti checkpoint.

--max-model-len 8192 batasi konteks di 8K token. Qwen3-8B sebenarnya sanggup 40.960, tapi vLLM menghitung ukuran KV cache dari angka ini. Jendela lebih besar berarti request bersamaan lebih sedikit di kartu yang sama. 8K cukup buat chat dan jawaban RAG.

--gpu-memory-utilization 0.90 adalah porsi VRAM yang boleh dipakai vLLM untuk bobot, aktivasi, CUDA graph, dan KV cache. Turunkan kalau kartunya dipakai proses lain.

--api-key cuma mengunci request di path /v1, /v2, dan /inference. Route lain di port yang sama tetap terbuka, termasuk /invocations yang punya kemampuan inferensi sama. Bind ke 127.0.0.1 atau taruh reverse proxy di depan, jangan buka port 8000 langsung ke internet.

--enable-auto-tool-choice dan --tool-call-parser hermes bikin model bisa ngeluarin function call. Chat template Qwen memakai format tool Hermes, dan pasangan flag ini yang juga dipakai contoh resmi vLLM untuk Qwen3. Kalau client-mu nggak pernah set strict di tool-nya, tambahkan --tool-strict-level parameter supaya semua schema argumen ditegakkan dari sisi server.

Dua baris di log startup menentukan kapasitasmu. Baca dulu sebelum tuning apa pun:

GPU KV cache size: N tokens
Maximum concurrency for 8,192 tokens per request: Mx

Kalau M di bawah ekspektasi, retry logic di client nggak akan menolong. Kamu butuh --max-model-len lebih kecil, checkpoint terkuantisasi, atau GPU tambahan.

Langkah 3: cek server dan streaming completion

curl -s http://127.0.0.1:8000/v1/models \
  -H "Authorization: Bearer $VLLM_API_KEY" | jq '.data[].id'
import os
from openai import OpenAI

client = OpenAI(
    base_url="http://127.0.0.1:8000/v1",
    api_key=os.environ["VLLM_API_KEY"],
)

stream = client.chat.completions.create(
    model="local-8b",
    messages=[
        {"role": "system", "content": "Jawab dalam dua paragraf pendek."},
        {"role": "user", "content": "Jelaskan continuous batching."},
    ],
    temperature=0.3,
    max_tokens=400,
    stream=True,
)

for chunk in stream:
    text = chunk.choices[0].delta.content
    if text:
        print(text, end="", flush=True)

Install dulu pip install openai. Client apa pun yang kompatibel OpenAI bisa dipakai, termasuk SDK JavaScript, LangChain, dan client gaya Anthropic yang juga disediakan vLLM. Streaming penting buat latensi yang dirasakan user: time to first token turun jadi biaya prefill plus satu langkah decode.

Langkah 4: pasang tool calling

tools = [{
    "type": "function",
    "function": {
        "name": "get_order_status",
        "description": "Cari status pesanan berdasarkan ID.",
        "strict": True,
        "parameters": {
            "type": "object",
            "properties": {"order_id": {"type": "string"}},
            "required": ["order_id"],
            "additionalProperties": False,
        },
    },
}]

messages = [{"role": "user", "content": "Pesanan A-1042 sudah sampai mana?"}]
resp = client.chat.completions.create(
    model="local-8b", messages=messages, tools=tools, tool_choice="auto"
)
msg = resp.choices[0].message

if msg.tool_calls:
    call = msg.tool_calls[0]
    messages.append(msg)
    messages.append({
        "role": "tool",
        "tool_call_id": call.id,
        "content": '{"status": "in transit", "eta": "2 days"}',
    })
    final = client.chat.completions.create(
        model="local-8b", messages=messages, tools=tools
    )
    print(final.choices[0].message.content)

strict: True menegakkan schema argumen saat decoding, bukan cuma envelope call-nya. Ini jalan rapi kalau schema-nya mengikuti gaya strict OpenAI: additionalProperties: false di tiap object, semua field masuk required, dan field opsional ditulis sebagai ["string", "null"]. Tool tanpa strict tetap punya argumen bebas, walaupun tool lain di request yang sama memilih ikut.

Kebanyakan framework agent nggak pernah set field ini, makanya ada --tool-strict-level di sisi server. function menegakkan envelope dan nama fungsi untuk semua request; parameter juga memaku tiap schema argumen, seolah semua tool strict. Di mode auto, default untuk sebagian besar parser, grammar baru aktif begitu ada minimal satu tool yang ikut.

Validasi sendiri tetap perlu. Grammar bisa menjamin JSON valid, tapi tetap bisa lolos nilai yang sah secara bentuk tapi salah secara bisnis, misalnya {"order_id": "1042"} padahal sistemmu mengharapkan A-1042. Pengecekan itu tempatnya di handler.

Langkah 5: bikin prefix caching benar-benar kepakai

Automatic prefix caching aktif secara default di vLLM V1. Matikan dengan --no-enable-prefix-caching kalau memang mau dimatikan. Satu hal yang perlu tahu: request yang minta prompt logprobs bakal mengabaikan cache dan menghitung ulang seluruh prompt, jadi jangan pakai opsi itu di jalur panas.

Cache-nya melakukan hash token per blok dan cuma menyimpan blok yang penuh. Prefix yang sama harus cocok persis sampai batas blok, kalau nggak, reuse berhenti di titik beda pertama. Dari situ ada dua kebiasaan:

  1. Taruh semua yang statis di depan: system prompt, schema tool, dokumen hasil retrieval. Bagian yang berubah, seperti pertanyaan user atau timestamp, taruh di belakang.
  2. Jangan selipkan apa pun di awal system prompt. Satu baris "Waktu sekarang: 14:02:11" di blok pertama mengubah hash-nya, dan semua request jadi cache miss.

Di mesin multi-tenant, kirim cache_salt di body request. Salt itu ikut di-hash di blok pertama, jadi tenant satu nggak bisa memakai token cache tenant lain. Ini menutup side channel timing yang bisa bocorin apakah orang lain sudah menanyakan hal serupa.

Langkah 6: baca metrik sebelum menebak

curl -s http://127.0.0.1:8000/metrics | grep -E 'prefix_cache|num_requests|kv_cache_usage'

Yang perlu dilihat:

vllm:prefix_cache_hits dibanding vllm:prefix_cache_queries ngasih hit rate dalam satuan token. Di workload RAG dengan system prompt panjang yang dipakai bersama, angka di bawah 0.2 berarti layout prompt-mu mematikan cache.

vllm:num_requests_waiting naik sementara vllm:num_requests_running datar artinya admission dibatasi KV cache, bukan compute. Nambah compute nggak akan nolong.

vllm:kv_cache_usage_perc mendekati 1.0 plus vllm:num_preemptions yang naik artinya request di-evict di tengah generasi lalu dijalankan ulang nanti. Itu muncul sebagai tail latency yang jelek.

vllm:time_to_first_token_seconds adalah histogram yang dirasakan user. p99 delapan detik di produk chat itu masalah produk, bukan masalah metrik.

Untuk Prometheus, nama counter biasanya pakai akhiran _total. Pastikan dulu lewat grep di atas sebelum menempel query, karena nama yang diekspor beda antar build:

sum(rate(vllm:prefix_cache_hits_total[5m]))
/
sum(rate(vllm:prefix_cache_queries_total[5m]))

Uji beban pakai benchmark bawaan, bukan curl dalam loop:

vllm bench serve \
  --backend openai-chat \
  --base-url http://127.0.0.1:8000 \
  --model local-8b \
  --dataset-name random \
  --random-input-len 2000 \
  --random-output-len 200 \
  --num-prompts 200 \
  --max-concurrency 16 \
  --request-rate 8 \
  --header "Authorization=Bearer $VLLM_API_KEY" \
  --save-result

Jalankan sekali dengan --max-concurrency 1 sebagai baseline, lalu di concurrency yang kamu perkirakan di produksi. Laporannya menampilkan TTFT, waktu per output token, dan inter-token latency di p99. Kalau throughput cuma naik sedikit antara dua run itu, batching nggak membantu dan KV pool yang jadi bottleneck.

Langkah 7: hitung KV cache-nya

Qwen3-8B punya 36 layer, 8 KV head, dan head_dim 128. Di bf16, biaya per token:

2 (K dan V) x 36 layer x 8 head x 128 head_dim x 2 byte = 147.456 byte

Itu 144 KiB per token. Sequence 8K butuh 1,125 GiB KV cache. Sequence 32K butuh 4,5 GiB. Bobot model sekitar 16GB, jadi di kartu 24GB dengan utilization 0.90 tersisa sekitar 4 sampai 5GB untuk KV setelah aktivasi dan CUDA graph. Anggap tiga sampai empat sequence 8K yang jalan bersamaan.

Jadikan itu estimasi, karena memori aktivasi dan graph capture berubah tergantung batch size dan model. Angka sebenarnya ada di log startup.

Langkah 8: jalankan sebagai service

docker run --runtime nvidia --gpus all \
  -v ~/.cache/huggingface:/root/.cache/huggingface \
  --env VLLM_API_KEY="$VLLM_API_KEY" \
  -p 127.0.0.1:8000:8000 \
  --ipc=host \
  vllm/vllm-openai:latest \
  --model Qwen/Qwen3-8B \
  --served-model-name local-8b \
  --max-model-len 8192 \
  --max-num-queued-reqs 64

Bind mount bikin bobot nggak perlu diunduh ulang tiap restart. --ipc=host wajib begitu kamu menaikkan --tensor-parallel-size di atas 1. --max-num-queued-reqs 64 membatasi antrean: lewat dari itu server balas HTTP 503, jadi load balancer atau client bisa retry ke instance lain daripada latensi tumbuh tanpa batas.

Health check: curl -f http://127.0.0.1:8000/health. Pakai restart policy di container, atau unit systemd dengan Restart=always, plus startup probe dengan initialDelaySeconds yang longgar. Memuat bobot 16GB dan capture CUDA graph butuh menit, bukan detik.

Kapan pakai vLLM, kapan pakai alternatif

Ollama pilihan tepat untuk laptop, demo, atau satu developer yang iterasi prompt. Dia ngurus download model, jalanin kuantisasi GGUF, dan cuma butuh satu perintah. Dia bisa melayani request paralel, tapi tuning-nya untuk satu user, bukan antrean.

Server llama.cpp jadi pilihan kalau nggak ada GPU NVIDIA: inferensi CPU, Apple Silicon, VRAM kecil. Lebih lambat per token untuk model besar, tapi jalan di tempat vLLM nggak bisa.

vLLM layak dipakai begitu beberapa user nembak model yang sama, kamu butuh endpoint kompatibel OpenAI buat kode yang sudah ada, dan prefix caching atau metrik KV cache mengubah tagihanmu. Dia juga kasih tensor parallelism untuk model yang nggak muat di satu kartu.

API hosted tetap menang dari sisi biaya total di volume rendah. Satu GPU 24GB sewaan bulanan lebih mahal daripada beberapa juta token dari provider. Titik impasnya tergantung campuran token-mu, jadi ukur dulu sebelum migrasi apa pun.

Kalau ada error

ValueError: The model's max seq len is larger than the maximum number of tokens that can be stored in KV cache artinya jendela konteks yang kamu set nggak muat di cache yang tersedia. Turunkan --max-model-len atau kosongkan VRAM.

OOM saat memuat bobot artinya checkpoint-nya nggak muat. Cek nvidia-smi apakah ada proses sisa, lalu pindah ke checkpoint FP8 atau AWQ daripada mengecilkan konteks.

Warmup gagal tepat setelah vllm serve biasanya karena arsitektur model butuh vLLM lebih baru dari yang terpasang. Baca traceback-nya, di situ disebut nama class modelnya.

Response 503 saat queue cap dipasang itu fiturnya bekerja, bukan bug. Tambah instance atau naikkan cap kalau kamu lebih suka mengantre daripada menolak beban.

Penutup

Tiga perintah yang perlu dihafal: vllm serve dengan batas konteks, curl /metrics, dan vllm bench serve. Sisanya tuning di sekitar tiga itu.

Langkah lanjutan yang paling terasa: taruh server di belakang Caddy atau nginx dengan auth beneran, tambah scrape Prometheus dan alert untuk num_preemptions, lalu ukur prefix cache hit rate di prompt asli kamu. Kalau hit rate-nya rendah, rapikan prompt dulu sebelum beli GPU lebih besar. Biasanya lebih murah.

Referensi

Butuh Bantuan Implementasi?

Saya membantu tim mendesain dan membangun infrastruktur cloud scalable, pipeline DevOps, dan sistem production-grade.

Konsultasi Gratis