benchmark-vllm-model
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseBenchmark $/token for a model on vLLM in Chalk
在 Chalk 中对 vLLM 模型的 $/token 进行基准测试
Produces a defensible cost-per-token number: serve the model on a GPU, drive it
to saturation, confirm the GPU is actually the limiter, then divide authoritative
cloud pricing by measured throughput.
The order matters. A throughput number without a saturation check is unfalsifiable,
and a $/hr figure pulled from a blog post is usually wrong (see step 5).
得出一项经得起质疑的每 token 成本数据:在 GPU 上服务该模型,将负载推至饱和状态,确认真正的瓶颈确实是 GPU,然后用权威云定价除以实测吞吐量。
顺序很重要。未经饱和度检查的吞吐量数字无法证伪,而从博客文章中摘取的 $/hr 数字通常不正确(见步骤 5)。
1. Serve the model
1. 服务模型
sh
chalk container run --dir <chalk-project> --json --no-spinners \
-i vllm/vllm-openai:latest -n bench-server \
--gpu nvidia-l4:1 --cpu 4 --memory 16Gi --port 8000 --lifetime 2h \
--env "HF_TOKEN=$HF_TOKEN" --env HF_HOME=/root/.cache/huggingface \
--await-running --await-timeout 20m \
-e vllm -- serve <MODEL> --port 8000 \
--dtype bfloat16 --max-model-len 16384 --gpu-memory-utilization 0.90chalk container get --name bench-serverwebUrlpodNameGET $webUrl/healthGPU selection. Only GPU pools labelled can
run container/sandbox workloads. On the staging cluster that is (L4,
24GB, native bf16), capped at 2 nodes x 1 GPU. A pool that lacks the label
(e.g. ) will leave the pod Unschedulable and the controller gives up
after 300s — shorter than a cold GPU node autoscale, so a valid request can
fail purely on capacity. Free a GPU and retry.
chalk.ai/workload-type=computel4-gpust4-gpusTuring (T4) has no native bf16; use there.
--dtype float16sh
chalk container run --dir <chalk-project> --json --no-spinners \
-i vllm/vllm-openai:latest -n bench-server \
--gpu nvidia-l4:1 --cpu 4 --memory 16Gi --port 8000 --lifetime 2h \
--env "HF_TOKEN=$HF_TOKEN" --env HF_HOME=/root/.cache/huggingface \
--await-running --await-timeout 20m \
-e vllm -- serve <MODEL> --port 8000 \
--dtype bfloat16 --max-model-len 16384 --gpu-memory-utilization 0.90chalk container get --name bench-serverwebUrlpodNameGET $webUrl/healthGPU 选择。 只有带有 标签的 GPU 池才能运行 container/sandbox 工作负载。在 staging 集群中,该池为 (L4、24GB、原生支持 bf16),上限为 2 个节点 x 1 个 GPU。如果池缺少该标签(例如 ),pod 会保持 Unschedulable 状态,controller 会在 300s 后放弃——这比冷 GPU 节点自动扩缩容所需的时间更短,因此一个有效请求可能纯粹因为容量不足而失败。请释放 GPU 后重试。
chalk.ai/workload-type=computel4-gpust4-gpusTuring(T4)不支持原生 bf16;在该架构上应使用 。
--dtype float162. Serving flags that change the answer
2. 会影响结果的服务参数
Benchmark the config you intend to run, and say which flags you set. These
materially move the result:
| Flag | Why it matters |
|---|---|
| Caps concurrent sequences. Too high wastes scheduler time on short requests — throughput can fall as you raise it. Sweep it. |
| Prefill chunk budget. The binding constraint for prompt-heavy / short-output workloads. |
| On by default in vLLM V1. Inflates apparent prompt throughput (see pitfalls). |
| Multimodal models profile peak activation memory against worst-case images per prompt, stealing KV cache. Pin it. |
| bf16 on Ada/Hopper; fp16 on Turing. |
| Skips CUDA graph capture: faster startup, slower decode. Fine for 1-token outputs, wrong for general serving. Don't leave it on in a benchmark you intend to generalize. |
| The container |
应对你计划实际运行的配置进行基准测试,并明确说明设置了哪些参数。以下参数会显著影响结果:
| 参数 | 为何重要 |
|---|---|
| 限制并发序列数量。设置过高会浪费 scheduler 在短请求上的时间——随着该值增大,吞吐量反而可能下降。应对其进行参数扫描。 |
| Prefill 分块预算。对于 prompt 较多或输出较短的工作负载,这是关键约束。 |
| 在 vLLM V1 中默认开启。会抬高表面上的 prompt 吞吐量(见常见陷阱)。 |
| 多模态模型会根据每个 prompt 的最坏情况图像数量评估峰值 activation memory,从而挤占 KV cache。请明确固定该值。 |
| Ada/Hopper 使用 bf16;Turing 使用 fp16。 |
| 跳过 CUDA graph capture:启动更快,但 decode 更慢。适用于只输出 1 个 token 的场景,不适合通用服务。在准备推广结果的基准测试中,不要一直启用它。 |
| container 的 |
3. Drive load
3. 施加负载
Use (bundled with this skill). It sweeps prompt length and
concurrency, and reads throughput from vLLM's own Prometheus counters
(, ,
) across a steady-state window.
scripts/bench.pyvllm:prompt_tokens_totalvllm:generation_tokens_totalvllm:request_success_totalsh
./scripts/bench.py --base-url "$URL" --model "$MODEL" \
--input-lens 128,512 --concurrency 8,32,64 --duration 30Methodology rules, each of which fixes a real failure mode:
- Sustained windows, not fixed request counts. Sub-10s windows are dominated by jitter. Use >=20s and a warm ramp before the measured window.
- Server-side counters, not client timing. Removes client overhead and WAN latency from the measurement.
- Sweep concurrency until throughput stops rising. One concurrency level tells you nothing about where saturation is.
- Unique prompt prefixes. Identical prompts hit the prefix cache and inflate results (bundled script seeds each request).
- Watch for client-side bottlenecks. If throughput drops at high concurrency, suspect the load generator's CPU before the server. Re-run from a bigger box, or from inside the cluster.
Where to run the driver. From a laptop is fine to start. To rule out the
network path, run it as a container inside the cluster (, the vLLM image
already has Python + httpx) and point it at the server's pod IP
(), then repeat against
the public . If the two agree, the ingress is not your bottleneck —
measure this rather than assuming it.
--cpu 6kubectl get pod <podName> -o jsonpath='{.status.podIP}'webUrl使用 (随本 skill 附带)。它会扫描 prompt 长度和 concurrency,并从 vLLM 自身的 Prometheus counters(、、)读取稳态窗口内的吞吐量。
scripts/bench.pyvllm:prompt_tokens_totalvllm:generation_tokens_totalvllm:request_success_totalsh
./scripts/bench.py --base-url "$URL" --model "$MODEL" \
--input-lens 128,512 --concurrency 8,32,64 --duration 30方法规则如下,每一条都用于修正一种实际失败模式:
- 使用持续窗口,而不是固定请求数。 少于 10s 的窗口会被抖动主导。测量窗口前应先进行预热,并确保窗口时长 >=20s。
- 使用服务端 counters,而不是客户端计时。 这样可以从测量中排除客户端开销和 WAN 延迟。
- 持续扫描 concurrency,直到吞吐量不再上升。 单一 concurrency 水平无法告诉你系统在哪里达到饱和。
- 使用唯一的 prompt 前缀。 相同 prompt 会命中 prefix cache 并夸大结果(附带脚本会为每个请求生成不同前缀)。
- 留意客户端瓶颈。 如果 concurrency 较高时吞吐量反而下降,应先怀疑负载生成器的 CPU,而不是服务器。请在配置更高的机器上重新运行,或从集群内部运行。
压测驱动程序的运行位置。 可以先从 laptop 开始。为了排除网络路径的影响,请将其作为 container 在集群内运行(,vLLM 镜像已经包含 Python + httpx),并将其指向服务器的 pod IP(),然后再对公开的 重复测试。如果两者结果一致,说明 ingress 不是瓶颈——应当实测确认,而不是直接假设。
--cpu 6kubectl get pod <podName> -o jsonpath='{.status.podIP}'webUrl4. Confirm the GPU is the limiter
4. 确认 GPU 确实是瓶颈
A throughput number is only a hardware number if the hardware was saturated.
Check DCGM on the node running the model:
sh
NODE=$(kubectl -n ns-<env> get pod <podName> -o jsonpath='{.spec.nodeName}')
DCGM=$(kubectl -n chalk-telemetry get pods -o wide | grep "dcgm.*$NODE" | awk '{print $1}')
kubectl -n chalk-telemetry port-forward "$DCGM" 19400:9400 &
curl -s localhost:19400/metrics | grep -E '^DCGM_FI_DEV_(GPU_UTIL|POWER_USAGE|FB_USED)'Sample it during the run. at 100 and at the card's TDP
(72W for L4, 300W for A100 40GB) means you are GPU-bound and the number is real.
Utilization well under 100% means you measured your client, the network, or a
scheduler cap — not the GPU. Note the exporter scrapes on an interval, so
identical consecutive samples are expected.
GPU_UTILPOWER_USAGE只有在硬件达到饱和时,吞吐量数字才真正代表硬件性能。请检查运行模型的节点上的 DCGM:
sh
NODE=$(kubectl -n ns-<env> get pod <podName> -o jsonpath='{.spec.nodeName}')
DCGM=$(kubectl -n chalk-telemetry get pods -o wide | grep "dcgm.*$NODE" | awk '{print $1}')
kubectl -n chalk-telemetry port-forward "$DCGM" 19400:9400 &
curl -s localhost:19400/metrics | grep -E '^DCGM_FI_DEV_(GPU_UTIL|POWER_USAGE|FB_USED)'应在测试运行期间采样。 达到 100,且 达到 GPU 卡的 TDP(L4 为 72W,A100 40GB 为 300W),意味着系统受 GPU 限制,所得数字是真实的。如果利用率明显低于 100%,则你测到的是客户端、网络或 scheduler 限制,而不是 GPU。请注意,exporter 按固定间隔抓取 metrics,因此连续出现相同采样值属于正常现象。
GPU_UTILPOWER_USAGE5. Get the real $/hr
5. 获取真实的 $/hr
Do not trust third-party pricing pages or a web search summary. For GPU
machine families the accelerator is often a separate SKU from vCPU and RAM,
and aggregator sites frequently omit it — which understates the node by 3x.
Query GCP's Cloud Billing Catalog directly (Compute Engine service
), summing the SKUs that make up the node:
6F81-5844-456Apy
undefined不要信任第三方定价页面或网页搜索摘要。 对于 GPU 机器族,accelerator 往往是与 vCPU 和 RAM 分开的独立 SKU,而聚合定价网站经常将其遗漏——这会使节点成本被低估 3 倍。
直接查询 GCP 的 Cloud Billing Catalog(Compute Engine service ),并汇总构成该节点的所有 SKU:
6F81-5844-456Apy
undefinedpaginate skus?pageSize=5000¤cyCode=USD with an Authorization: Bearer token
paginate skus?pageSize=5000¤cyCode=USD with an Authorization: Bearer token
from gcloud auth print-access-token
, filter serviceRegions for your region,
gcloud auth print-access-tokenfrom gcloud auth print-access-token
, filter serviceRegions for your region,
gcloud auth print-access-tokenthen match description against your machine family and accelerator.
then match description against your machine family and accelerator.
Worked example, `g2-standard-8` (8 vCPU, 32 GiB, 1x L4) in `us-central1`:
| SKU | Rate | Qty | Subtotal |
|---|---|---|---|
| G2 Instance Core running in Americas | $0.024988 /hr | 8 | $0.199904 |
| G2 Instance Ram running in Americas | $0.002927 /GiB-hr | 32 | $0.093664 |
| **Nvidia L4 GPU running in Americas** | **$0.560040 /hr** | 1 | **$0.560040** |
| | | | **$0.853608 /hr** |
Omitting the GPU SKU yields $0.2936/hr — a number that appears on public pricing
aggregators and is wrong. Sanity-check by computing spot the same way and
confirming it lands at the expected discount.
Also pull Spot and 1yr/3yr CUD rates; they are separate SKUs and are what a
production deployment would actually pay.
示例:`g2-standard-8`(8 vCPU、32 GiB、1x L4),位于 `us-central1`:
| SKU | 费率 | 数量 | 小计 |
|---|---|---|---|
| G2 Instance Core running in Americas | $0.024988 /hr | 8 | $0.199904 |
| G2 Instance Ram running in Americas | $0.002927 /GiB-hr | 32 | $0.093664 |
| **Nvidia L4 GPU running in Americas** | **$0.560040 /hr** | 1 | **$0.560040** |
| | | | **$0.853608 /hr** |
省略 GPU SKU 后得到 $0.2936/hr——这个数字会出现在公共定价聚合网站上,但它是错误的。请使用相同方法计算 spot 价格进行合理性检查,并确认其降幅符合预期。
还应获取 Spot 和 1yr/3yr CUD 费率;它们属于独立 SKU,也是生产环境实际会支付的价格。6. Compute the cost
6. 计算成本
$/sec = node_hourly / 3600
$/1M in-tok = $/sec / prompt_tok_per_s * 1e6
$/1M req = $/sec / rps * 1e6Report $/1M input tokens and $/1M requests, plus the on-demand / spot /
CUD variants.
Pick the right unit. For a classifier or guardrail that emits a single token,
output tokens are identical to requests and "$/output token" is a degenerate
metric — lead with $/classification. For a chat or coding model, decode
dominates and $/output token is the meaningful figure; set to a
realistic completion length rather than 1.
--output-tokens$/sec = node_hourly / 3600
$/1M in-tok = $/sec / prompt_tok_per_s * 1e6
$/1M req = $/sec / rps * 1e6应报告 $/1M input tokens 和 $/1M requests,同时提供按需、spot 和 CUD 变体。
选择正确的单位。 对于只输出单个 token 的分类器或 guardrail,output tokens 与 requests 相同,“$/output token”是一个退化指标——应重点报告 $/classification。对于聊天或编程模型,decode 占主导,因此 $/output token 才是有意义的指标;应将 设置为符合实际的 completion 长度,而不是 1。
--output-tokensPitfalls
常见陷阱
- Prefix caching inflates $/input-token. A shared system prompt gets cached,
so counts tokens that were never recomputed. The bundled script reports the hit rate. It is a fair comparison against a vendor who bills you for every input token, but it is not your real compute headroom — say which one you are quoting.
prompt_tokens_total - Saturation numbers are a floor, not a forecast. These are 100%-duty-cycle figures. At 20% real utilization, multiply by 5. Idle GPU time is the dominant cost in most production deployments.
- Tool-calling models need the right parser and the right checkpoint.
plus an instruct model. Some Coder-tuned checkpoints emit tool calls as fenced JSON text that the parser ignores, so
--enable-auto-tool-choice --tool-call-parser hermesreturns null undertool_callswhiletool_choice: autoworks (guided decoding forces the shape).tool_choice: required - Completed container pods are garbage-collected, taking their logs. End a
one-shot driver script with so you can still
sleepit.kubectl logs - is unimplemented for k8s containers (
chalk container exec). Drive work via the entrypoint, or useonly implemented for host containers/ the sandbox API.--compute-class host - Piping a script into breaks silently if it invokes anything that reads stdin — the child eats the rest of the script. Write it to a file and add
bash.exec </dev/null - Sandboxes have no network egress and no flag to grant it; even public IPs
are blocked. Use when the workload must reach anything.
chalk container run --route <CIDR>
- Prefix caching 会抬高 $/input-token。 共享 system prompt 会被缓存,因此 会统计从未重新计算的 token。附带脚本会报告命中率。与按每个 input token 向用户收费的 vendor 比较时,这仍是公平的对比,但它不是你真实的计算余量——应明确说明所引用的是哪一种口径。
prompt_tokens_total - 饱和状态下的数字是下限,而不是预测值。 这些是 100% duty cycle 下的数据。如果实际利用率为 20%,则需要乘以 5。在大多数生产部署中,空闲 GPU 时间才是主要成本。
- Tool-calling 模型需要正确的 parser 和 checkpoint。 使用 ,并搭配 instruct 模型。部分经过 Coder 调优的 checkpoint 会将 tool call 输出为围栏形式的 JSON 文本,导致 parser 忽略它;因此在
--enable-auto-tool-choice --tool-call-parser hermes下,tool_choice: auto会返回 null,而tool_calls可以正常工作(guided decoding 会强制输出结构)。tool_choice: required - 已完成的 container pod 会被垃圾回收,并连同日志一起删除。 在一次性驱动脚本末尾加入 ,以便之后仍可执行
sleep。kubectl logs - 尚未针对 k8s container 实现(
chalk container exec)。请通过 entrypoint 驱动工作,或使用only implemented for host containers/ sandbox API。--compute-class host - 如果脚本通过管道传递给 后调用了任何读取 stdin 的内容,就会静默中断——子进程会吞掉脚本的剩余部分。请将脚本写入文件,并添加
bash。exec </dev/null - Sandbox 没有网络出口,也没有可用于授权网络出口的参数;即使公共 IP 也会被阻止。当工作负载必须访问外部资源时,请使用 。
chalk container run --route <CIDR>
Reporting
报告要求
State the model, GPU, every non-default serving flag, the measured throughput,
the DCGM saturation evidence, and the SKU breakdown behind the $/hr. A
cost-per-token figure without those is not reproducible and should not be quoted.
应说明模型、GPU、每个非默认服务参数、实测吞吐量、DCGM 饱和度证据,以及 $/hr 背后的 SKU 明细。缺少这些信息的每 token 成本数据不可复现,不应被引用。