⚡ vLLM instead of Ollama · ใช้ vLLM แทน Ollama · 用 vLLM 取代 Ollama

Ollama is for one person at one machine. vLLM is for a class of thirty asking at once. They answer at the same address, in the same shape. Your code barely changes. Here is the swap, and the measurement that shows why you would bother. · Ollama เหมาะกับคนเดียวเครื่องเดียว ส่วน vLLM เหมาะกับนักเรียนสามสิบคนที่ถามพร้อมกัน ทั้งคู่ตอบที่อยู่เดียวกัน รูปแบบเดียวกัน โค้ดของคุณจึงแทบไม่ต้องแก้ นี่คือวิธีสลับ และตัวเลขที่บอกว่าทำไมถึงคุ้ม · Ollama 适合一个人用一台机器;vLLM 适合三十个学生同时发问。两者在同一个地址、用同样的格式回答,所以你的代码几乎不用改。这里讲怎么换,以及一个说明为什么值得换的实测。

WHAT YOU GET A vLLM server of your own. It serves a model to everyone who asks. The same agent code from the classes page. Two lines changed, and it runs against this. And a number that tells you when the swap is worth making. เซิร์ฟเวอร์ vLLM ของคุณเอง ที่เสิร์ฟโมเดลให้ทุกคนที่ถามเข้ามา โค้ดเอเจนต์ชุดเดิมจากหน้าเรื่องคลาส ทำงานกับมันได้โดยแก้แค่สองบรรทัด และตัวเลขที่บอกคุณว่าเมื่อไหร่การสลับถึงจะคุ้ม 一台你自己的 vLLM 服务器,来多少人问就服务多少人。还有那份「类」教程里的智能体代码,只改两行就能对着它跑。外加一个数字,告诉你什么时候值得换。

🤔 Tutorial 1 — Why leave Ollama at all · บทที่ 1 — ทำไมต้องย้ายจาก Ollama · 教程 1 —— 为什么要离开 Ollama

1 One person, or a class of thirty · คนเดียว หรือทั้งห้องสามสิบคน · 一个人,还是三十人一个班

🇬🇧 English

Ollama is built for you at your desk. One question, then the next one.

It downloads models for you. It starts them when asked. It stops them after.

vLLM assumes a crowd. It loads one model and keeps it loaded.

Then it answers everybody at the same time, in one pass.

🇹🇭 ไทย

Ollama สร้างมาเพื่อคุณคนเดียวที่โต๊ะของคุณ ถามทีละคำถาม

มันดาวน์โหลดโมเดลให้ เปิดให้เมื่อถูกเรียก และปิดเมื่อคุณเลิกถาม

ส่วน vLLM คิดเผื่อคนทั้งกลุ่ม มันโหลดโมเดลตัวเดียวแล้วคาไว้อย่างนั้น

จากนั้นมันตอบทุกคนพร้อมกันในรอบเดียว

🇨🇳 中文

Ollama 是为坐在桌前的你一个人做的。问一个,再问下一个。

它替你下载模型,要用时启动,你不问了它就停掉。

vLLM 一开始就假设有一群人。它加载一个模型,然后一直挂着。

接着它在一轮里同时回答所有人。

 OllamavLLM
Who it is forทำมาเพื่อใคร给谁用的one person, one machineคนเดียว เครื่องเดียว一个人,一台机器many people, one serverหลายคน เซิร์ฟเวอร์เดียว很多人,一台服务器
Getting a modelเอาโมเดลมา拿到模型ollama pull, then it is yoursollama pull แล้วมันก็เป็นของคุณollama pull,然后它就是你的了named at startup, fetched from Hugging Faceระบุตอนเปิดเซิร์ฟเวอร์ ดึงมาจาก Hugging Face启动时指定,从 Hugging Face 取
Models at onceโมเดลพร้อมกัน同时几个模型many, swapped in and out for youหลายตัว สลับเข้าออกให้เอง很多个,自动换进换出one per server, and it staysเซิร์ฟเวอร์ละตัว และมันอยู่อย่างนั้น一台服务器一个,且一直在
Precision by defaultความละเอียดโดยปริยาย默认精度squeezed down to 4 bitsบีบลงเหลือ 4 บิต压到 4 位full size, as the model was trainedเต็มขนาด อย่างที่โมเดลถูกเทรนมา原样大小,跟训练时一致
Memory it takesหน่วยความจำที่ใช้吃多少内存what the model needs, while it runsเท่าที่โมเดลต้องใช้ ระหว่างทำงาน跑的时候,模型需要多少就多少a fixed share, claimed at startupสัดส่วนคงที่ จองไว้ตั้งแต่เปิด固定一份,启动时就占住
Setting it upการติดตั้ง安装难度one installer, no flagsตัวติดตั้งเดียว ไม่ต้องใส่แฟล็ก一个安装包,不用加参数a container and six flagsคอนเทนเนอร์หนึ่งตัวกับแฟล็กหกตัว一个容器加六个参数
THE HONEST ANSWER Most people should stay on Ollama. If one person is asking, vLLM buys you nothing. It costs you an afternoon. Move when several people ask at the same time. Or when answers must not slow down as more arrive. Tutorial 4 measures exactly that. Decide with a number, not a feeling. คนส่วนใหญ่ควรอยู่กับ Ollama ต่อไป ถ้ามีคนถามคนเดียว vLLM ไม่ได้ให้อะไรเพิ่ม แต่กินเวลาคุณไปทั้งบ่าย ย้ายเมื่อมีหลายคนถามพร้อมกัน หรือเมื่อคำตอบต้องไม่ช้าลงตอนคนเพิ่มขึ้น บทที่ 4 วัดเรื่องนี้ตรง ๆ คุณจะได้ตัดสินใจด้วยตัวเลข ไม่ใช่ความรู้สึก 大多数人应该继续用 Ollama。如果只有一个人在问,vLLM 什么也没给你,还搭进去一个下午。等到好几个人同时问、或者人一多答案就不能变慢的时候再换。教程 4 测的正是这件事,好让你靠数字决定,而不是靠感觉。

2 They both speak the same API · ทั้งคู่พูด API เดียวกัน · 它们说的是同一套 API

🇬🇧 English

This is why the swap is small. OpenAI published a shape for these requests.

Everyone copied it. Ollama copied it. vLLM copied it.

So the same POST works on both. Only the address and the model name change.

🇹🇭 ไทย

นี่คือเหตุผลที่การสลับทำได้ง่าย OpenAI ประกาศรูปแบบของคำขอพวกนี้ไว้

แล้วทุกคนก็ลอกตาม Ollama ลอก vLLM ก็ลอก

POST ชุดเดียวกันจึงใช้ได้กับทั้งคู่ เปลี่ยนแค่ที่อยู่กับชื่อโมเดล

🇨🇳 中文

这就是为什么换起来很小。OpenAI 定了这类请求的格式。

然后大家都照抄。Ollama 抄了,vLLM 也抄了。

所以同一个 POST 两边都能用。变的只有地址和模型名字。

your codea POST with JSONPOST พร้อม JSON一个带 JSON 的 POST
/v1/chat/completionsthe shape OpenAI setรูปแบบที่ OpenAI กำหนดOpenAI 定下的格式
:11434 or :8000Ollama, or vLLMOllama หรือ vLLMOllama,或者 vLLM
the same JSON backchoices[0].messagechoices[0].messagechoices[0].message
THE SHAPE MATCHES. THE MODEL NAME DOES NOT. This is the one thing that catches everybody. Ollama names a model it downloaded, so qwen2.5:7b. vLLM names whatever you started it with, written out in full, so Qwen/Qwen2.5-7B-Instruct. Send Ollama's name to vLLM and you get a 404. It will not guess. Step 4 asks each server what it calls itself. It takes two seconds and saves the argument. นี่คือเรื่องเดียวที่ดักทุกคน Ollama เรียกชื่อโมเดลที่มันดาวน์โหลดมา เช่น qwen2.5:7b ส่วน vLLM เรียกชื่อสิ่งที่คุณสั่งให้มันเปิด เขียนเต็ม ๆ เช่น Qwen/Qwen2.5-7B-Instruct ถ้าส่งชื่อของ Ollama ไปให้ vLLM คุณจะได้ 404 ไม่ใช่การเดาให้ ขั้นที่ 4 ถามเซิร์ฟเวอร์แต่ละตัวว่ามันเรียกตัวเองว่าอะไร ใช้เวลาสองวินาทีและตัดปัญหาไปได้เลย 这是唯一一件谁都会栽的事。Ollama 用的是它下载下来的那个名字,比如 qwen2.5:7b;vLLM 用的是你启动它时给的那串完整名字,比如 Qwen/Qwen2.5-7B-Instruct。把 Ollama 的名字发给 vLLM,你会拿到 404,它不会替你猜。第 4 步就是去问每台服务器它管自己叫什么,两秒钟,省掉一场争论。

🚀 Tutorial 2 — Get a server running · บทที่ 2 — เปิดเซิร์ฟเวอร์ให้ได้ก่อน · 教程 2 —— 先把服务器跑起来

3 One container, and the flags that matter · คอนเทนเนอร์เดียว กับแฟล็กที่สำคัญ · 一个容器,和那几个要紧的参数

🇬🇧 English

Install vLLM with pip and you pick a CUDA version. You then live with it.

Use the container. It has the right one already.

The flags below are not decoration. Four of them stop something going wrong.

🇹🇭 ไทย

ถ้าติดตั้ง vLLM ด้วย pip เท่ากับคุณกำลังเลือกเวอร์ชัน CUDA ไปตลอดชีวิต

ใช้คอนเทนเนอร์ดีกว่า มันมีตัวที่ถูกต้องมาให้แล้ว

แฟล็กข้างล่างไม่ใช่ของประดับ สี่ตัวในนั้นกันไม่ให้เกิดปัญหา

🇨🇳 中文

用 pip 装 vLLM,等于给自己这辈子选定了一个 CUDA 版本。

用容器。里面那个版本已经是对的了。

下面这些参数不是装饰。其中四个是在挡事故。

📄 serve_vllm.sh

#!/bin/sh
# serve_vllm.sh — start one vLLM server, and leave the rest of the machine
# alone.
#
# Ollama is a service you install once and then forget. vLLM is a process
# you start with the model already chosen. That is the first difference,
# and every other difference follows from it.
#
# The table on the page explains every flag below. The two settings most
# worth changing are up here as variables.

set -eu

# What to serve. vLLM fetches this from Hugging Face the first time, so the
# name is a repository, not a nickname. The -AWQ build is four bits and
# 5.6 GB; drop that suffix for the full sixteen bit weights and 15 GB.
MODEL=${MODEL:-Qwen/Qwen2.5-7B-Instruct-AWQ}

PORT=${PORT:-8000}

# Which image. This line is not a detail. The ordinary
# vllm/vllm-openai:latest carries a PyTorch built for cards up to sm_120,
# and this machine's Blackwell chip is sm_121, so the server dies on
# startup saying no kernel image is available for execution on the device.
# vLLM's own DGX Spark writeup names the CUDA 13 nightly track instead.
# A nightly moves under you, so pin the digest once something works.
IMAGE=${IMAGE:-vllm/vllm-openai:cu130-nightly}

# The share of the card vLLM claims at startup and keeps. The default is
# 0.9, which takes almost everything. This box runs other things.
FRACTION=${FRACTION:-0.25}

# The longest prompt plus answer the server will accept. It also decides
# how big the reserved cache has to be. Qwen2.5 offers 32768, which needs
# far more memory than one demo should take.
MAXLEN=${MAXLEN:-8192}

exec docker run --rm --name vllm \
  --gpus all \
  --ipc=host \
  -p "${PORT}":8000 \
  -v "${HOME}/.cache/huggingface:/root/.cache/huggingface" \
  "${IMAGE}" \
  --model "${MODEL}" \
  --gpu-memory-utilization "${FRACTION}" \
  --max-model-len "${MAXLEN}" \
  --enable-auto-tool-choice \
  --tool-call-parser hermes
FlagWhat it does · ทำอะไร · 做什么
--gpus allWithout it the container has no card and vLLM will not start.ถ้าไม่มี คอนเทนเนอร์จะไม่เห็นการ์ดจอ และ vLLM จะไม่เริ่มทำงาน不加它,容器里就没有显卡,vLLM 起不来。
--ipc=hostDocker gives a container 64 MB of shared memory. vLLM needs more. Leaving this out looks like a hang, not an error.Docker ให้หน่วยความจำร่วมกับคอนเทนเนอร์แค่ 64 MB แต่ vLLM ต้องการมากกว่านั้น ถ้าไม่ใส่ อาการจะเหมือนค้าง ไม่ใช่ขึ้นข้อผิดพลาดDocker 只给容器 64 MB 共享内存,vLLM 要得更多。不加这个,看起来像卡住,而不是报错。
-v ~/.cache/huggingfaceThe model downloads from Hugging Face on first start. Mount the cache or it downloads again every restart.โมเดลถูกดาวน์โหลดจาก Hugging Face ตอนเริ่มครั้งแรก ต้องเมานต์แคชไว้ ไม่งั้นมันดาวน์โหลดใหม่ทุกครั้งที่รีสตาร์ท模型是第一次启动时从 Hugging Face 下的。挂上缓存,否则每次重启都重下一遍。
--gpu-memory-utilizationThe share of the card vLLM claims at startup and keeps. The default is 0.9, which takes almost everything.สัดส่วนของการ์ดที่ vLLM จองไว้ตั้งแต่เปิดและถือไว้ตลอด ค่าเริ่มต้นคือ 0.9 ซึ่งกินเกือบหมดvLLM 启动时占住并一直保留的显存比例。默认是 0.9,几乎全拿走。
--max-model-lenThe longest prompt plus answer it will take. It also sets how big the reserved cache must be.ความยาวสูงสุดของคำถามบวกคำตอบที่มันรับ และเป็นตัวกำหนดว่าแคชที่จองต้องใหญ่แค่ไหน它能接受的「提示词 + 回答」最大长度。它同时决定预留缓存要开多大。
--enable-auto-tool-choiceEvery request carrying tools is refused without it. The 400 names this flag.ถ้าไม่มี ทุกคำขอที่แนบเครื่องมือมาจะถูกปฏิเสธด้วย 400 ที่บอกชื่อแฟล็กตัวนี้不加它,任何带工具的请求都会被 400 拒绝,而且报错里点名这个参数。
--tool-call-parser hermesMust match the model family. Qwen2.5 already carries a Hermes-style template, so hermes is the one.ต้องตรงกับตระกูลของโมเดล Qwen2.5 มีเทมเพลตแบบ Hermes มาให้อยู่แล้ว จึงใช้ hermes必须和模型家族对上。Qwen2.5 自带 Hermes 风格的模板,所以选 hermes。

🖥️ WHAT IT PRINTED · starting the server, first time, on an NVIDIA GB10

$ sh serve_vllm.sh

Resolved architecture: Qwen2ForCausalLM
Using max model len 8192
The model is convertible to awq_marlin during runtime. Using awq_marlin kernel.
Model loading took 5.2 GiB memory and 843.057112 seconds
GPU KV cache size: 395,872 tokens
Maximum concurrency for 8,192 tokens per request: 48.32x
init engine (profile, create kv cache, warmup model) took 66.10 s
INFO:     Started server process [1]
INFO:     Application startup complete.

Stopping it and starting the same command again, with the model now cached:

Model loading took 5.2 GiB memory and 36.585409 seconds
Maximum concurrency for 8,192 tokens per request: 48.65x
INFO:     Application startup complete.

4 Ask it what the model is called · ถามมันว่าโมเดลชื่ออะไร · 问它模型叫什么名字

🇬🇧 English

Do this before you write anything else against a new server.

Both servers answer at /v1/models. Both reply in the same shape.

What comes back is different. That difference is what breaks things.

🇹🇭 ไทย

ทำสิ่งนี้ก่อนจะเขียนอย่างอื่นกับเซิร์ฟเวอร์ตัวใหม่

ทั้งคู่ตอบที่ /v1/models และตอบด้วยรูปแบบเดียวกัน

แต่สิ่งที่ได้กลับมาต่างกัน และความต่างนั้นแหละที่ทำให้พัง

🇨🇳 中文

对着一台新服务器写别的代码之前,先做这件事。

两边都在 /v1/models 上应答,回的格式也一样。

回来的内容不一样,而坏事的正是这个不一样。

📄 check_vllm.py

"""check_vllm.py — is the server up, and what is it actually called?

Ollama and vLLM both answer at /v1, the shape OpenAI invented. That is the
whole reason you can swap one for the other. But they disagree about what a
model is called, and that one difference breaks more first attempts than
anything else on this page.

Run this before you write any other code against a new server.
"""
import json
import os
import sys
import urllib.error
import urllib.request

# Two servers, two ports. Ollama picks 11434, vLLM picks 8000. Neither is
# a standard; they are just the defaults each project chose.
SERVERS = {
    "vLLM": os.environ.get("VLLM_URL", "http://localhost:8000/v1"),
    "Ollama": os.environ.get("OLLAMA_URL", "http://localhost:11434/v1"),
}


def ask(url, path):
    """GET one endpoint and hand back the parsed JSON, or an excuse."""
    # Neither server requires this header, and both ignore what is in
    # it. Every OpenAI client sends one anyway, so it is here to match.
    request = urllib.request.Request(
        url + path, headers={"Authorization": "Bearer none"})
    try:
        with urllib.request.urlopen(request, timeout=15) as response:
            return json.load(response), None
    except urllib.error.URLError as trouble:
        # A server that is not running looks exactly like one that is
        # running somewhere else. Say which URL failed, not just that it did.
        return None, str(getattr(trouble, "reason", trouble))
    except Exception as trouble:
        return None, str(trouble)


def main():
    for name, url in SERVERS.items():
        print(name, "at", url)
        listing, trouble = ask(url, "/models")
        if trouble:
            print("   not answering:", trouble)
            print()
            continue

        # The reply is the same shape from both: {"data": [{"id": ...}]}.
        # What differs is what is in the id, and that is the point.
        for entry in listing.get("data", []):
            print("   model id:", entry["id"])
        print()

    print("The id is not a nickname you choose.")
    print("Ollama names a model it downloaded:   qwen2.5:14b")
    print("vLLM names the thing you started it with, in full.")
    print("Send the wrong one and you get a 404, not a guess.")


if __name__ == "__main__":
    main()

🖥️ WHAT IT PRINTED · both servers, asked what they serve

vLLM at http://spark.local:8000/v1
   model id: Qwen/Qwen2.5-7B-Instruct-AWQ

Ollama at http://spark.local:11434/v1
   model id: qwen2.5:1.5b
   model id: qwen2.5:7b
   model id: nomic-embed-text:latest
   model id: gemma2:9b
   model id: llama3.1:8b
   model id: scb10x/typhoon2.1-gemma3-12b:latest
   model id: qwen2.5:14b
   model id: llama3.2:3b

The id is not a nickname you choose.
Ollama names a model it downloaded:   qwen2.5:14b
vLLM names the thing you started it with, in full.
Send the wrong one and you get a 404, not a guess.

5 One question, no library · หนึ่งคำถาม ไม่ต้องใช้ไลบรารี · 一个问题,不用任何库

🇬🇧 English

No vLLM library. No OpenAI library either. Just a POST with JSON in it.

That is all those libraries do underneath.

Read this once and the swap stops feeling like magic.

🇹🇭 ไทย

ไม่มีไลบรารี vLLM ไม่มีไลบรารี OpenAI ด้วย มีแค่ POST ที่มี JSON อยู่ข้างใน

ไลบรารีพวกนั้นก็ทำแค่นี้อยู่ข้างล่าง

อ่านอันนี้สักรอบ แล้วการสลับจะเลิกดูเหมือนเวทมนตร์

🇨🇳 中文

没有 vLLM 的库,也没有 OpenAI 的库。只有一个带 JSON 的 POST。

那些库在底下做的就是这件事。

读一遍,换服务器这事就不再像魔法了。

📄 ask_vllm.py

"""ask_vllm.py — one question, no SDK, and the same code for both servers.

There is no vLLM library here. No OpenAI library either. Just a POST with
some JSON in it. That is all any of those libraries do underneath.

Read it once and the swap stops being mysterious. You change a URL and a
model name. Everything else is the same bytes on the wire.
"""
import json
import os
import sys
import time
import urllib.request

# Which server, and what it calls the model. The pair always travels
# together: a vLLM model id sent to Ollama is a 404, and the other way too.
SERVERS = {
    "vllm": (os.environ.get("VLLM_URL", "http://localhost:8000/v1"),
             os.environ.get("VLLM_MODEL", "Qwen/Qwen2.5-7B-Instruct-AWQ")),
    "ollama": (os.environ.get("OLLAMA_URL", "http://localhost:11434/v1"),
               os.environ.get("OLLAMA_MODEL", "qwen2.5:7b")),
}

QUESTION = "What is the past tense of go? Answer with one word."


def ask(url, model, question):
    """Send one chat completion and hand back the answer and the timing."""
    # This body is the OpenAI chat format. Both servers read it. The only
    # required fields are the model and the messages.
    body = json.dumps({
        "model": model,
        "messages": [{"role": "user", "content": question}],

        # Sampling settled at 0 so two servers can be compared fairly.
        # Leave it out and each picks its own default, and you end up
        # measuring the dice rather than the server.
        "temperature": 0,
        "max_tokens": 20,
    }).encode()

    request = urllib.request.Request(
        url + "/chat/completions", data=body,
        headers={"Content-Type": "application/json",
                 # Ignored by both and required by neither. Every
                 # OpenAI client sends one, so this matches them.
                 "Authorization": "Bearer none"})

    started = time.time()
    with urllib.request.urlopen(request, timeout=300) as response:
        reply = json.load(response)
    took = time.time() - started

    # The answer is buried the same distance down in both servers.
    said = reply["choices"][0]["message"]["content"].strip()

    # usage is how many tokens went in and came out. Both servers fill
    # it in, so it is the one number you can compare across them.
    usage = reply.get("usage") or {}
    return said, took, usage


def main():
    # One name on the command line, or both if you give none.
    wanted = sys.argv[1:] or list(SERVERS)

    for name in wanted:
        if name not in SERVERS:
            print(name, "is not a server I know about")
            continue
        url, model = SERVERS[name]
        print(name, "->", url, "model", model)
        try:
            # The first question of the day also loads the model, and on a
            # cold server that is nearly all of the time measured. Ask once
            # and throw it away, or the number below is a lie.
            _, cold, _ = ask(url, model, "Say ok.")
            print("   first call: %.2f s (loading the model, not answering)"
                  % cold)
            said, took, usage = ask(url, model, QUESTION)
        except Exception as trouble:
            print("   failed:", trouble)
            print()
            continue

        print("   answer :", repr(said))
        print("   took   : %.2f s" % took)
        print("   tokens : %s in, %s out" % (usage.get("prompt_tokens", "?"),
                                             usage.get("completion_tokens", "?")))

        # No tokens-a-second here on purpose. A two word answer is mostly
        # network and prompt reading, so that number would say more about
        # the wifi than the server. race.py measures speed properly.
        print()


if __name__ == "__main__":
    main()

🖥️ WHAT IT PRINTED · the same question, both servers

vllm -> http://spark.local:8000/v1 model Qwen/Qwen2.5-7B-Instruct-AWQ
   first call: 17.53 s (loading the model, not answering)
   answer : 'went'
   took   : 2.95 s
   tokens : 42 in, 2 out

ollama -> http://spark.local:11434/v1 model qwen2.5:7b
   first call: 5.92 s (loading the model, not answering)
   answer : 'went'
   took   : 0.18 s
   tokens : 42 in, 2 out

🛠️ Tutorial 3 — The agent cannot tell · บทที่ 3 — เอเจนต์แยกไม่ออก · 教程 3 —— 智能体分不出来

6 Same tool, same agent, other server · เครื่องมือเดิม เอเจนต์เดิม คนละเซิร์ฟเวอร์ · 同一个工具、同一个智能体,换个服务器

🇬🇧 English

The tool below is copied from the classes page. Not one character changed.

So is the way the agent is built. Only the server and the model name differ.

Run it twice, once against each, and read the two transcripts.

🇹🇭 ไทย

เครื่องมือข้างล่างคัดลอกมาจากหน้าเรื่องคลาสโดยไม่เปลี่ยนสักตัวอักษร

วิธีสร้างเอเจนต์ก็เหมือนกัน ต่างกันแค่เซิร์ฟเวอร์กับชื่อโมเดล

รันสองรอบ รอบละเซิร์ฟเวอร์ แล้วอ่านผลทั้งสองเทียบกัน

🇨🇳 中文

下面这个工具是从「类」那一页原样抄来的,一个字符都没改。

智能体的搭法也一样。不同的只有服务器和模型名字。

跑两遍,一边一次,然后把两份记录对着读。

📄 tool_vllm.py

"""tool_vllm.py — the agent from the classes page, pointed at vLLM instead.

The tool is copied from agent_window.py without a character changed. So is
the way the agent is built. The only difference is the two lines at the top
that say where the server is and what the model is called.

That is the claim this file is here to test. Run it against both servers
and read the two transcripts side by side.

    python tool_vllm.py vllm
    python tool_vllm.py ollama
"""
import json
import os
import sys
import time
import urllib.request

from smolagents import ActionStep, FinalAnswerStep
from smolagents import OpenAIServerModel, ToolCallingAgent, tool

SERVERS = {
    "vllm": (os.environ.get("VLLM_URL", "http://localhost:8000/v1"),
             os.environ.get("VLLM_MODEL", "Qwen/Qwen2.5-7B-Instruct-AWQ")),
    "ollama": (os.environ.get("OLLAMA_URL", "http://localhost:11434/v1"),
               os.environ.get("OLLAMA_MODEL", "qwen2.5:7b")),
}

BITKUB = "https://api.bitkub.com/api/v3/market/ticker"
MAX_STEPS = 6

QUESTIONS = ["What is the past tense of go?",
             "What is BTC worth in baht right now?"]


@tool
def coin_price(symbol: str) -> str:
    """Look up what one coin is worth in Thai baht, right now.

    Args:
        symbol: the coin's ticker, for example BTC or ETH
    """
    # No double quotes above. smolagents shows that description to the
    # model, and a model that copies them back breaks its own JSON.
    pair = symbol.upper().strip() + "_THB"
    with urllib.request.urlopen(BITKUB + "?sym=" + pair, timeout=30) as response:
        coins = json.load(response)
    if not coins:
        return "no coin called " + symbol + " on Bitkub"
    return ("1 " + symbol.upper() + " = "
            + format(float(coins[0]["last"]), ",.2f") + " baht")


def main():
    name = sys.argv[1] if len(sys.argv) > 1 else "vllm"
    url, model_id = SERVERS[name]
    print("===", name, "===")
    print("server :", url)
    print("model  :", model_id)
    print()

    # This is the whole swap. OpenAIServerModel does not know or care
    # which program is answering at that address.
    llm = OpenAIServerModel(model_id=model_id, api_base=url, api_key="none")

    for question in QUESTIONS:
        agent = ToolCallingAgent(tools=[coin_price], model=llm,
                                 instructions="You are Kru Nari, a patient "
                                 "English teacher. Teach in short, plain "
                                 "English, and keep every reply under three "
                                 "sentences. If, and only if, someone asks "
                                 "what a coin is worth, you may use your "
                                 "coin_price tool to look it up.",
                                 verbosity_level=0)
        print("You:", question)
        used, answer = [], "?"
        started = time.time()
        try:
            for step in agent.run(question, stream=True, max_steps=MAX_STEPS):
                if isinstance(step, ActionStep):
                    used += [call.name for call in (step.tool_calls or [])]
                if isinstance(step, FinalAnswerStep):
                    answer = str(step.output)
        except Exception as trouble:
            answer = "failed: " + str(trouble)[:80]
        print("   tools called:", ", ".join(used) or "none")
        print("   Kru Nari    :", answer)
        print("   took        : %.1f s" % (time.time() - started))
        print()

    # The number the tool really returned, so the answer above can be
    # checked against it rather than believed.
    print("coin_price itself says:", coin_price("BTC"))


if __name__ == "__main__":
    main()

🖥️ WHAT IT PRINTED · the same agent against vLLM

=== vllm ===
server : http://spark.local:8000/v1
model  : Qwen/Qwen2.5-7B-Instruct-AWQ

You: What is the past tense of go?
   tools called: final_answer
   Kru Nari    : went
   took        : 22.1 s

You: What is BTC worth in baht right now?
   tools called: coin_price, final_answer
   Kru Nari    : 1 BTC is worth 2,796,092.45 baht right now.
   took        : 1.9 s

coin_price itself says: 1 BTC = 2,796,092.45 baht

🖥️ WHAT IT PRINTED · the same agent against Ollama

=== ollama ===
server : http://spark.local:11434/v1
model  : qwen2.5:7b

You: What is the past tense of go?
   tools called: final_answer
   Kru Nari    : went
   took        : 7.2 s

You: What is BTC worth in baht right now?
   tools called: coin_price, final_answer
   Kru Nari    : 1 BTC is currently worth 2,790,025.63 baht.
   took        : 2.0 s

coin_price itself says: 1 BTC = 2,790,025.63 baht
READ THE TWO TRANSCRIPTS TOGETHER Same file. Same tool. Same question. One environment variable apart. If the tool got called on both, the agent could not tell them apart. Nor could you, without the header line. That is the whole claim of this page. It is why the swap is cheap. ไฟล์เดียวกัน เครื่องมือเดียวกัน คำถามเดียวกัน ต่างกันแค่ตัวแปรสภาพแวดล้อมตัวเดียว ถ้าเครื่องมือถูกเรียกทั้งสองฝั่ง แปลว่าเอเจนต์แยกไม่ออกว่าเซิร์ฟเวอร์ไหนตอบ และคุณเองก็แยกไม่ออกถ้าไม่มีบรรทัดหัวเรื่อง นั่นคือข้อเสนอทั้งหมดของหน้านี้ และเป็นเหตุผลที่การสลับมีราคาถูก 同一个文件,同一个工具,同一个问题,只差一个环境变量。如果两边都调用了工具,那就说明智能体分不出是哪台服务器在回答——没有开头那行标题,你也分不出来。这就是这一页的全部主张,也是换服务器成本很低的原因。

🏁 Tutorial 4 — Eight people at once · บทที่ 4 — แปดคนพร้อมกัน · 教程 4 —— 八个人同时问

7 The measurement that decides it · การวัดที่ตัดสินเรื่องนี้ · 一测就知道该用哪个

🇬🇧 English

Everything above this point works on both servers. So why move?

This is why. Ask once, then ask eight times at the same moment.

A batching server answers eight in about the time one took.

A server that queues takes eight times as long. Measure it yourself.

🇹🇭 ไทย

ทุกอย่างก่อนหน้านี้ทำงานได้กับทั้งสองเซิร์ฟเวอร์ แล้วจะย้ายไปทำไม

เพราะเรื่องนี้ ถามหนึ่งครั้ง แล้วถามแปดครั้งพร้อมกัน

เซิร์ฟเวอร์ที่รวมงานเป็นชุด จะตอบแปดคำถามในเวลาราว ๆ เท่ากับตอบคำถามเดียว

เซิร์ฟเวอร์ที่ต่อคิวจะใช้เวลาแปดเท่า ลองวัดเองดู

🇨🇳 中文

上面所有东西在两边都能跑。那为什么要换?

为的是这个。先问一次,再同一时刻问八次。

会成批处理的服务器,回答八个的时间和回答一个差不多。

会排队的服务器要花八倍时间。自己测一下。

📄 race.py

"""race.py — one request each, then eight at once. This is the whole point.

One person asking one question does not need vLLM. Ollama is easier and it
is fast enough. The difference shows up when several people ask at the same
moment, which is what a classroom is.

So this asks once, then asks eight times at once, and compares the wall
clock. A server that answers eight in about the time it took to answer one
is batching them. A server that takes eight times as long is queueing them.

Set AT_ONCE to send more than eight. That is challenge 4: raise it until
the wall clock stops being flat, and you have found your class size limit.
"""
import json
import os
import statistics
import sys
import threading
import time
import urllib.request

SERVERS = {
    "vllm": (os.environ.get("VLLM_URL", "http://localhost:8000/v1"),
             os.environ.get("VLLM_MODEL", "Qwen/Qwen2.5-7B-Instruct-AWQ")),
    "ollama": (os.environ.get("OLLAMA_URL", "http://localhost:11434/v1"),
               os.environ.get("OLLAMA_MODEL", "qwen2.5:7b")),
}

# Eight different questions, so no server can win by caching an answer it
# has already given. They are all about the same length on purpose.
QUESTIONS = [
    "Name one fruit that grows in northern Thailand.",
    "Name one river that runs through Chiang Mai.",
    "Name one animal that lives in a rice field.",
    "Name one tool a carpenter uses every day.",
    "Name one vegetable that is green and leafy.",
    "Name one instrument you blow air into.",
    "Name one bird that cannot fly at all.",
    "Name one metal that rusts when it gets wet.",
]

# Short answers keep the test about the server and not about the model
# writing an essay.
MAX_TOKENS = 30

# How many ask at the same moment. Eight is a small group; set AT_ONCE
# higher to find where your server stops keeping up. Challenge 4.
AT_ONCE = int(os.environ.get("AT_ONCE", "8"))


def crowd(size):
    """Build `size` questions, none of them the same as another.

    Past eight it starts reusing the list, so a number is added to keep
    every prompt distinct. Two identical prompts would let a server look
    fast by answering the second one from a cache.
    """
    out = []
    for number in range(size):
        question = QUESTIONS[number % len(QUESTIONS)]
        if number >= len(QUESTIONS):
            question = "%s (answer number %d)" % (question, number)
        out.append(question)
    return out


def ask(url, model, question, out):
    """One request. Appends (seconds, tokens out) to a shared list."""
    body = json.dumps({"model": model,
                       "messages": [{"role": "user", "content": question}],
                       "temperature": 0, "max_tokens": MAX_TOKENS}).encode()
    request = urllib.request.Request(
        url + "/chat/completions", data=body,
        headers={"Content-Type": "application/json",
                 "Authorization": "Bearer none"})
    started = time.time()
    try:
        with urllib.request.urlopen(request, timeout=600) as response:
            reply = json.load(response)
        took = time.time() - started
        tokens = (reply.get("usage") or {}).get("completion_tokens", 0)

        # list.append is safe from several threads at once. Python's own
        # lock covers it, so no lock of our own is needed here.
        out.append((took, tokens))
    except Exception as trouble:
        out.append((time.time() - started, 0))
        print("      one request failed:", str(trouble)[:70])


def one_at_a_time(url, model, question):
    """The warm single request. Also loads the model if it is cold."""
    out = []
    ask(url, model, question, out)
    return out[0]


def all_at_once(url, model, questions):
    """Fire every question on its own thread and wait for the lot."""
    out = []
    threads = [threading.Thread(target=ask, args=(url, model, q, out))
               for q in questions]

    # The clock starts before the threads, so it includes the queueing
    # that a busy server does. That queueing is exactly what we measure.
    started = time.time()
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    return time.time() - started, out


def main():
    wanted = sys.argv[1:] or list(SERVERS)
    for name in wanted:
        url, model = SERVERS[name]
        print("===", name, "-", model, "===")

        # A cold model loads on the first request and that time is not
        # the server's fault. Ask once and throw it away.
        print("   warming up...")
        try:
            one_at_a_time(url, model, "Say ok.")
        except Exception as trouble:
            print("   not answering:", str(trouble)[:80], "\n")
            continue

        alone_took, _ = one_at_a_time(url, model, QUESTIONS[0])
        print("   one request alone     : %.2f s" % alone_took)

        wall, results = all_at_once(url, model, crowd(AT_ONCE))
        each = [t for t, _ in results]
        tokens = sum(n for _, n in results)
        print("   %d all at once%s: %.2f s wall clock"
              % (AT_ONCE, " " * (10 - len(str(AT_ONCE))), wall))
        print("      slowest of them   : %.2f s" % max(each))
        print("      median of them    : %.2f s" % statistics.median(each))
        print("      tokens out, total : %d" % tokens)
        if wall:
            print("      tokens a second   : %.1f" % (tokens / wall))

        # If they take about as long as one did, they overlapped. If
        # they take N times as long, they stood in a queue.
        print("   %d cost %.1fx one request" % (AT_ONCE, wall / alone_took))
        print()


if __name__ == "__main__":
    main()

🖥️ WHAT IT PRINTED · one request, then eight at the same moment

=== vllm - Qwen/Qwen2.5-7B-Instruct-AWQ ===
   warming up...
   one request alone     : 0.69 s
   8 all at once         : 0.70 s wall clock
      slowest of them   : 0.70 s
      median of them    : 0.69 s
      tokens out, total : 206
      tokens a second   : 292.9
   8 cost 1.0x one request

=== ollama - qwen2.5:7b ===
   warming up...
   one request alone     : 0.85 s
   8 all at once         : 4.98 s wall clock
      slowest of them   : 4.98 s
      median of them    : 2.89 s
      tokens out, total : 208
      tokens a second   : 41.7
   8 cost 5.8x one request

🚧 Tutorial 5 — What will bite you · บทที่ 5 — เรื่องที่จะกัดคุณ · 教程 5 —— 会咬人的地方

8 Six differences that cost an evening · หกความต่างที่กินเวลาทั้งคืน · 六个能耗掉你一晚上的差别

🇬🇧 English

The API is the same. The habits around it are not.

Each of these was hit while writing this page. None of them is in an error message.

🇹🇭 ไทย

API เหมือนกัน แต่นิสัยรอบ ๆ มันไม่เหมือน

ทุกข้อข้างล่างเจอมาจริงตอนเขียนหน้านี้ และไม่มีข้อไหนอยู่ในข้อความแจ้งข้อผิดพลาดเลย

🇨🇳 中文

API 是一样的,围着它的习惯不一样。

下面每一条都是写这页时真撞上的,而且没有一条会出现在报错信息里。

1. THE ORDINARY IMAGE MAY NOT RUN ON YOUR CARD Check this before you download nine gigabytes. This page did not. vllm/vllm-openai:latest carries a PyTorch built for cards up to sm_120. The GB10 chip in an NVIDIA DGX Spark is sm_121. One number apart, and the server will not start at all. vLLM's own writeup names the CUDA 13 nightly track instead. That is the image used above. Your card has a number too. Find it before you pull, not after. ตรวจเรื่องนี้ก่อนจะดาวน์โหลดเก้ากิกะไบต์ อย่างที่หน้านี้ไม่ได้ทำ vllm/vllm-openai:latest มี PyTorch ที่สร้างมาสำหรับการ์ดถึงรุ่น sm_120 ส่วนชิป GB10 ใน NVIDIA DGX Spark คือ sm_121 ต่างกันเลขเดียว แล้วเซิร์ฟเวอร์ก็เริ่มทำงานไม่ได้เลย เอกสารของ vLLM เองสำหรับเครื่องนั้นบอกให้ใช้สาย CUDA 13 nightly แทน ซึ่งคืออิมเมจที่อยู่ใน serve_vllm.sh ข้างบน การ์ดของคุณก็มีเลขนี้เหมือนกัน หาให้เจอก่อนดึงอิมเมจ ไม่ใช่หลังจากนั้น 在下载九个 GB 之前先查这件事——这一页就没查。vllm/vllm-openai:latest 里的 PyTorch 只编到 sm_120 这一档的显卡。NVIDIA DGX Spark 里的 GB10 芯片是 sm_121。就差一个数,服务器根本起不来。vLLM 自己写那台机器的文章说要改用 CUDA 13 nightly 那条线,也就是上面 serve_vllm.sh 里用的镜像。你的显卡也有这么一个数。拉镜像之前先找到它,别等之后。

🖥️ WHAT IT PRINTED · asking the card its own number, before pulling anything

$ docker run --rm --gpus all --entrypoint nvidia-smi IMAGE \
      --query-gpu=name,compute_cap,memory.total --format=csv

name, compute_cap, memory.total [MiB]
NVIDIA GB10, 12.1, [N/A]
2. THE MEMORY IS GONE BEFORE ANYONE ASKS ANYTHING Ollama takes memory while it answers. Then it gives it back. vLLM takes its share the moment it starts. It keeps that until you stop the server. That share is the KV cache. It is reserved up front. So it never has to ask for more mid answer. It is why vLLM is fast and why it is a bad neighbour. On a machine running anything else, set --gpu-memory-utilization yourself. The default is 0.9. Ollama ใช้หน่วยความจำตอนกำลังตอบ แล้วคืนให้ ส่วน vLLM ยึดส่วนแบ่งของมันทันทีที่เริ่ม และถือไว้จนกว่าคุณจะปิดเซิร์ฟเวอร์ ส่วนแบ่งนั้นคือ KV cache ที่จองไว้ล่วงหน้า มันจะได้ไม่ต้องไปขอหน่วยความจำกลางคันตอนตอบ นี่คือเหตุผลที่ vLLM เร็ว และเป็นเหตุผลที่มันเป็นเพื่อนบ้านที่ไม่ดี ถ้าเครื่องนั้นรันอย่างอื่นอยู่ด้วย ให้ตั้ง --gpu-memory-utilization เอง ค่าเริ่มต้นคือ 0.9 Ollama 是答题时占内存,答完还回去。vLLM 一启动就把自己那份拿走,一直握到你关掉服务器为止。那一份就是 KV 缓存,提前预留好,这样答到一半就不用再去要内存。这是 vLLM 快的原因,也是它不好相处的原因。机器上还跑着别的东西,就自己设 --gpu-memory-utilization。默认值是 0.9。
3. THE SAME MODEL NAME IS NOT THE SAME WEIGHTS Ollama squeezes by default and does not mention it. qwen2.5:7b is a four bit copy. That is why it is 4.7 GB, not 15. vLLM loads whatever you name, at whatever size that thing is. Name Qwen/Qwen2.5-7B-Instruct and you get sixteen bit weights. That is a 15 GB download. Name the -AWQ one, as this page does, and you get 5.6 GB. Same model, different files. You choose out loud, instead of being handed a default. A comparison that ignores this compares two different things. So both servers here run a four bit 7B. Ollama บีบโมเดลให้โดยปริยาย และไม่บอกคุณ qwen2.5:7b คือสำเนาสี่บิต นั่นคือเหตุผลที่มันมีขนาด 4.7 GB ไม่ใช่ 15 ส่วน vLLM โหลดสิ่งที่คุณระบุชื่อ ที่ขนาดของสิ่งนั้นจริง ๆ ถ้าระบุ Qwen/Qwen2.5-7B-Instruct คุณจะได้น้ำหนักสิบหกบิตเต็ม ๆ และต้องดาวน์โหลด 15 GB ถ้าระบุ Qwen/Qwen2.5-7B-Instruct-AWQ อย่างที่หน้านี้ทำ คุณจะได้สี่บิตและ 5.6 GB โมเดลเดียวกัน แต่คนละไฟล์ คุณเลือกเอง ไม่ใช่ถูกยัดค่าเริ่มต้นให้ การเทียบความเร็วที่มองข้ามเรื่องนี้คือการเทียบของคนละอย่าง นั่นคือเหตุผลที่ทั้งสองเซิร์ฟเวอร์ตรงนี้รัน 7B สี่บิตเหมือนกัน Ollama 默认就替你压过了,而且不说。qwen2.5:7b 是个四位的副本,所以它是 4.7 GB 而不是 15。vLLM 你报什么名字它就加载什么,那东西多大就是多大。报 Qwen/Qwen2.5-7B-Instruct,你拿到完整的十六位权重,外加 15 GB 的下载;报 Qwen/Qwen2.5-7B-Instruct-AWQ(这一页用的就是它),你拿到四位和 5.6 GB。同一个模型,不同的文件。你是自己明着选,而不是被塞一个默认值。忽略这点的速度比较,比的是两样不同的东西——所以这里两台服务器跑的都是四位的 7B。
4. ONE MODEL PER SERVER, AND IT DOES NOT SWAP Ollama holds several models. It swaps whichever you name into memory. vLLM serves exactly the one you started it with. Asking for another name is a 404, not a load. If you need two models you run two servers on two ports. That sounds worse. It is usually better. Nobody waits for somebody else's model to load. Ollama เก็บโมเดลไว้หลายตัว แล้วสลับตัวที่คุณเรียกเข้าหน่วยความจำ ส่วน vLLM เสิร์ฟเฉพาะตัวที่คุณสั่งเปิดเท่านั้น ถ้าขอชื่ออื่นจะได้ 404 ไม่ใช่การโหลดให้ ถ้าต้องใช้สองโมเดล ก็รันสองเซิร์ฟเวอร์บนสองพอร์ต ฟังดูแย่กว่า แต่ปกติแล้วดีกว่า เพราะคำตอบของใครก็ไม่ต้องรอโมเดลของคนอื่นโหลดเสร็จ Ollama 手里攥着好几个模型,你点哪个就把哪个换进内存。vLLM 只服务你启动时指定的那一个。要别的名字就是 404,它不会替你加载。需要两个模型,就在两个端口上跑两台服务器。听着更差,实际通常更好:谁的回答都不用等别人的模型加载完。
5. TOOL CALLING IS OFF, BUT IT SAYS SO LOUDLY The two tool flags are not optional. This page guessed wrong about why. It expected the server to take your tool list and answer in prose. It does not. vLLM returns a 400 and names the missing flag. That holds whether tool_choice is auto, required, or left out. All three were tried. So this one costs you a minute, not an evening. Other servers are not all this honest. Check a tool was really called. A confident answer proves nothing. แฟล็กเครื่องมือสองตัวนั้นไม่ใช่ของเลือกได้ และหน้านี้เดาเหตุผลผิด มันคาดว่าเซิร์ฟเวอร์จะรับรายการเครื่องมือแล้วเงียบ ๆ ตอบเป็นข้อความธรรมดา แต่ไม่ใช่ vLLM ตอบกลับด้วย 400 และบอกชื่อแฟล็กที่ขาด ไม่ว่าคุณจะส่ง tool_choice เป็น auto เป็น required หรือไม่ส่งเลย ลองมาแล้วทั้งสามแบบ เรื่องนี้จึงกินเวลาคุณหนึ่งนาที ไม่ใช่ทั้งคืน แต่เซิร์ฟเวอร์อื่นไม่ได้ซื่อแบบนี้ทุกตัว ให้ตรวจว่าเครื่องมือถูกเรียกจริงไหม แทนที่จะเชื่อคำตอบที่ฟังดูมั่นใจ 那两个工具参数不是可选的,而这一页把原因猜错了。它以为服务器会收下你的工具清单,然后闷声用散文回答。并不会。vLLM 会回 400 并点名缺的那个参数——不管你把 tool_choice 传成 auto、传成 required,还是根本不传。三种都试过了。所以这件事花你一分钟,不是一晚上。但别的服务器未必都这么老实,所以要去检查工具到底有没有被调用,而不是信一句听起来很笃定的回答。
6. THE FIRST START IS NOT THE NORMAL START The first time you name a model, vLLM downloads it. Then it gets ready, which takes a while longer. It looks stuck. It is not. Mount the cache so the download happens once. Read the log, not the clock. The server says when it is listening. Ollama hides all this behind pull. That is nicer, until you need to know what is happening. ครั้งแรกที่คุณระบุชื่อโมเดล vLLM จะดาวน์โหลดมันจาก Hugging Face แล้วใช้เวลาเตรียมตัวอีกพักใหญ่ก่อนจะตอบอะไรได้ มันดูเหมือนค้าง แต่ไม่ได้ค้าง ให้เมานต์แคชไว้ การดาวน์โหลดจะได้เกิดครั้งเดียว และให้อ่านล็อกแทนที่จะดูนาฬิกา เซิร์ฟเวอร์จะบอกเองเมื่อมันพร้อมรับคำขอ ส่วน Ollama ซ่อนเรื่องพวกนี้ไว้หลัง pull ซึ่งสบายกว่า จนกระทั่งวันที่คุณต้องรู้ว่าเกิดอะไรขึ้น 第一次指定某个模型时,vLLM 会先从 Hugging Face 把它下下来,然后还要准备好一阵子才肯回答。看着像卡住了,其实没有。把缓存挂上,下载就只发生一次;另外要读日志而不是看表,服务器会自己说它开始监听了。Ollama 把这些都藏在 pull 后面,很舒服——直到你需要知道到底发生了什么的那天。

🏆 Tutorial 6 — Four challenges · บทที่ 6 — โจทย์สี่ข้อ · 教程 6 —— 四道题

9 Try first. The answers are folded away. · ลองเองก่อน เฉลยพับซ่อนไว้ · 先自己试。答案折起来放着。

🇬🇧 English

Each of these breaks something on purpose. Then you read the wreckage.

That is the fastest way to learn a server. Break it while nobody is using it.

🇹🇭 ไทย

โจทย์แต่ละข้อจงใจทำให้บางอย่างพัง แล้วให้คุณอ่านซากของมัน

นั่นคือวิธีเรียนรู้เซิร์ฟเวอร์ที่เร็วที่สุด ทำให้มันพังตอนที่ยังไม่มีใครใช้

🇨🇳 中文

每道题都故意弄坏点什么,然后让你去读残骸。

这是学会一台服务器最快的办法。趁没人用的时候把它弄坏。

1Serve something else, and find its new name · เสิร์ฟโมเดลอื่น แล้วหาชื่อใหม่ของมัน · 换一个模型来服务,然后找出它的新名字

🇬🇧 English

Stop the server. Start it again with a different model.

Then run check_vllm.py and see what the id became.

Now make it short. Your code should not carry a long path around.

🇹🇭 ไทย

หยุดเซิร์ฟเวอร์ แล้วเปิดใหม่ด้วยโมเดลคนละตัว

จากนั้นรัน check_vllm.py ดูว่า id กลายเป็นอะไร

ทีนี้ทำให้มันสั้นลง โค้ดของคุณจะได้ไม่ต้องแบกพาธยาว ๆ ไปด้วย

🇨🇳 中文

把服务器停掉,换一个模型重新启动。

然后跑 check_vllm.py,看看 id 变成了什么。

接着把它改短,免得你的代码一直拖着一长串路径。

Show the answerดูเฉลย看答案
--served-model-name IS THE WHOLE ANSWER The id is whatever you passed to --model, written out in full. Add --served-model-name tutor and the server answers to tutor instead. The model on disk does not change. Only the name on the wire does. Do this on any real deployment. You can then swap the model underneath. No client code changes at all. id คือสิ่งที่คุณส่งให้ --model เขียนเต็ม ๆ ถ้าเติม --served-model-name tutor เซิร์ฟเวอร์จะตอบในชื่อ tutor แทน โมเดลบนดิสก์ไม่ได้เปลี่ยน เปลี่ยนแค่ชื่อที่วิ่งบนสาย เรื่องนี้ควรทำในงานจริงทุกครั้ง เพราะคุณจะสลับโมเดลข้างใต้ได้โดยไม่ต้องแตะโค้ดฝั่งผู้ใช้สักบรรทัด id 就是你传给 --model 的那一串,原样写全。加上 --served-model-name tutor,服务器就改用 tutor 这个名字应答。磁盘上的模型没变,变的只是线上那个名字。真正上线时这值得一做:以后换掉底下的模型,客户端代码一行都不用动。

2Starve it of memory and read the error · อดหน่วยความจำมัน แล้วอ่านข้อผิดพลาด · 把内存卡到不够,然后读那个报错

🇬🇧 English

Start the server with --gpu-memory-utilization 0.02.

It will refuse. Read what it says, because it tells you what the memory is for.

🇹🇭 ไทย

เปิดเซิร์ฟเวอร์ด้วย --gpu-memory-utilization 0.02

มันจะปฏิเสธ อ่านสิ่งที่มันบอก เพราะมันกำลังบอกว่าหน่วยความจำนั้นเอาไว้ทำอะไร

🇨🇳 中文

用 --gpu-memory-utilization 0.02 启动服务器。

它会拒绝。把它说的话读完,因为它正在告诉你那块内存是干什么用的。

Show the answerดูเฉลย看答案

🖥️ WHAT IT PRINTED · started with --gpu-memory-utilization 0.02

$ FRACTION=0.02 sh serve_vllm.sh

ValueError: No available memory for the cache blocks. Try increasing
`gpu_memory_utilization` when initializing the engine.

RuntimeError: Engine core initialization failed. See root cause above.
$ echo $?
1
THE MEMORY IS FOR THE CACHE, NOT THE MODEL Read the words cache blocks. The weights fitted fine. What did not fit is the KV cache. That is the room vLLM keeps for conversations in progress. That is what the fraction is really buying. On this machine 0.25 bought 395,872 tokens of it. The server said so at startup: Maximum concurrency for 8,192 tokens per request: 48.32x. Forty-eight people, stated up front. Raise the fraction and that number goes up. It refuses to start rather than run badly. That is the kinder failure. อ่านคำว่า cache blocks ให้ดี น้ำหนักโมเดลใส่ได้สบาย สิ่งที่ใส่ไม่ลงคือ KV cache ซึ่งเป็นที่ว่างที่ vLLM กันไว้ให้บทสนทนาที่กำลังดำเนินอยู่ นั่นคือสิ่งที่สัดส่วนนั้นซื้อจริง ๆ บนเครื่องนี้ 0.25 ซื้อมาได้ 395,872 โทเคน และเซิร์ฟเวอร์ก็บอกไว้ตั้งแต่ตอนเปิด Maximum concurrency for 8,192 tokens per request: 48.32x สี่สิบแปดคน บอกไว้ล่วงหน้า ถ้าเพิ่มสัดส่วน ตัวเลขนั้นก็เพิ่มตาม และมันเลือกที่จะไม่เริ่มทำงานเลย ดีกว่าเริ่มแล้วทำงานได้แย่ ๆ ซึ่งเป็นความล้มเหลวที่ใจดีกว่า 把 cache blocks 这两个词读清楚。权重装得下,装不下的是 KV 缓存——vLLM 给正在进行的对话留的地方。那个比例真正买的是这个。在这台机器上,0.25 买到了 395,872 个 token,而服务器启动时就说了:Maximum concurrency for 8,192 tokens per request: 48.32x。四十八个人,提前告诉你。把比例调高,这个数就跟着涨。而且它宁可不启动,也不带病运行——这是比较仁慈的那种失败。

3Take the tool flags away and watch it go quiet · เอาแฟล็กเครื่องมือออก แล้วดูมันเงียบไป · 把工具参数拿掉,看它安静下来

🇬🇧 English

Restart without --enable-auto-tool-choice and its parser.

Then run tool_vllm.py again and ask for the price.

Guess what happens before you run it. Then run it.

Most servers of this kind stay quiet. This one does not.

🇹🇭 ไทย

รีสตาร์ทโดยไม่ใส่ --enable-auto-tool-choice กับตัวแยกของมัน

แล้วรัน tool_vllm.py อีกครั้ง ถามราคาดู

ลองเดาก่อนว่าจะเกิดอะไรขึ้น แล้วค่อยรัน

เซิร์ฟเวอร์แบบนี้ส่วนใหญ่จะเงียบ แต่ตัวนี้ไม่

🇨🇳 中文

去掉 --enable-auto-tool-choice 和它的解析器,重启。

然后再跑一次 tool_vllm.py,问价格。

先猜猜会发生什么,然后再跑。

这类服务器大多会闷声不响。这一个不会。

Show the answerดูเฉลย看答案

🖥️ WHAT IT PRINTED · the same server, started without the two tool flags

$ python tool_vllm.py vllm

You: What is the past tense of go?
Error code: 400 - tool_choice="required" requires --tool-call-parser to be set
   tools called: none

Asked three more ways, to see whether it is ever quiet about it:

  tool_choice omitted    -> HTTP 400 | "auto" tool choice requires
                            --enable-auto-tool-choice and --tool-call-parser
  tool_choice=auto       -> HTTP 400 | "auto" tool choice requires
                            --enable-auto-tool-choice and --tool-call-parser
  tool_choice=required   -> HTTP 400 | tool_choice="required" requires
                            --tool-call-parser to be set
IT REFUSES, AND IT NAMES THE FLAG This page expected a silent failure and was wrong. vLLM returns a 400 and says which flag is missing, whichever way you ask. Omit tool_choice, send auto, or send required. All three refuse, and each names the flag it wants. That is the kind of error you want. It costs you one minute instead of an evening. Do not assume the same of every server. The version that hurts is a server that takes the tool list and answers in prose anyway. You catch that by checking a tool was really called. Do not trust the reply alone. หน้านี้คาดว่ามันจะล้มเหลวแบบเงียบ ๆ และคาดผิด vLLM ตอบกลับด้วย 400 พร้อมบอกว่าขาดแฟล็กตัวไหน ไม่ว่าคุณจะถามด้วยวิธีไหน ไม่ใส่ tool_choice ส่ง auto หรือส่ง required ทั้งสามแบบถูกปฏิเสธ และแต่ละแบบก็บอกชื่อแฟล็กที่มันต้องการ นั่นคือข้อผิดพลาดแบบที่คุณอยากได้ มันกินเวลาคุณหนึ่งนาที แทนที่จะเป็นทั้งคืน แต่อย่าคิดว่าเซิร์ฟเวอร์ทุกตัวจะเป็นแบบนี้ เซิร์ฟเวอร์ที่รับรายการเครื่องมือแล้วเงียบ ๆ ตอบเป็นข้อความธรรมดา คือเวอร์ชันที่เจ็บ และคุณจะเจอมันได้ด้วยการตรวจว่าเครื่องมือถูกเรียกจริงไหม ไม่ใช่เชื่อคำตอบ 这一页本来以为它会悄无声息地失败,猜错了。vLLM 会回 400,并说清楚缺的是哪个参数,不管你怎么问。不传 tool_choice、传 auto、传 required——三种都被拒绝,而且每一种都点名它要的那个参数。这正是你想要的那种报错:花你一分钟,而不是一晚上。但别以为每台服务器都这样。收下工具清单然后闷声用散文回答的那种,才是真的疼,而你要靠检查工具到底有没有被调用来发现它,而不是信那句回答。

4Find where the batching runs out · หาจุดที่การรวมงานเป็นชุดเอาไม่อยู่ · 找出成批处理撑不住的那个点

🇬🇧 English

Eight at once was easy. Try thirty-two. Then a hundred.

Plot the wall clock against the number asking.

Somewhere it stops being flat. Find that place, because it is your class size limit.

🇹🇭 ไทย

แปดพร้อมกันยังง่าย ลองสามสิบสอง แล้วลองร้อย

วาดกราฟเวลาที่ใช้เทียบกับจำนวนคนที่ถาม

จะมีจุดหนึ่งที่เส้นเลิกแบน หาจุดนั้นให้เจอ เพราะนั่นคือขีดจำกัดขนาดห้องเรียนของคุณ

🇨🇳 中文

八个同时问还算轻松。试试三十二个,再试一百个。

把耗时对同时提问的人数画出来。

总有一处它不再是平的。找到那一点,那就是你的班级人数上限。

Show the answerดูเฉลย看答案

🖥️ WHAT IT PRINTED · thirty-two at the same moment, challenge 4

=== vllm - Qwen/Qwen2.5-7B-Instruct-AWQ ===
   warming up...
   one request alone     : 0.68 s
   32 all at once        : 0.93 s wall clock
      slowest of them   : 0.92 s
      median of them    : 0.90 s
      tokens out, total : 755
      tokens a second   : 811.7
   32 cost 1.4x one request

=== ollama - qwen2.5:7b ===
   warming up...
   one request alone     : 0.80 s
   32 all at once        : 19.35 s wall clock
      slowest of them   : 19.33 s
      median of them    : 10.02 s
      tokens out, total : 833
      tokens a second   : 43.1
   32 cost 24.2x one request