chalk-ai/chalk-ai-plugins

benchmark-vllm-model

Benchmark throughput and cost-per-token of a model served on vLLM in Chalk's container infrastructure.

查看源码
仓库原始内容

按源仓库内容呈现,保留标题、案例、代码、表格、链接以及原文引用的演示图片。

Benchmark $/token for a model on vLLM in Chalk

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).

1. Serve the model

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.90

chalk container get --name bench-server returns webUrl (publicly reachable) and podName. Poll GET $webUrl/health — it returns 503 until weights are loaded, then 200. Expect several minutes for a 7B download.

GPU selection. Only GPU pools labelled chalk.ai/workload-type=compute can run container/sandbox workloads. On the staging cluster that is l4-gpus (L4, 24GB, native bf16), capped at 2 nodes x 1 GPU. A pool that lacks the label (e.g. t4-gpus) 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.

Turing (T4) has no native bf16; use --dtype float16 there.

2. Serving flags that change the answer

Benchmark the config you intend to run, and say which flags you set. These materially move the result:

FlagWhy it matters
--max-num-seqsCaps concurrent sequences. Too high wastes scheduler time on short requests — throughput can fall as you raise it. Sweep it.
--max-num-batched-tokensPrefill chunk budget. The binding constraint for prompt-heavy / short-output workloads.
--enable-prefix-cachingOn by default in vLLM V1. Inflates apparent prompt throughput (see pitfalls).
--limit-mm-per-prompt '{"image":1}'Multimodal models profile peak activation memory against worst-case images per prompt, stealing KV cache. Pin it.
--dtypebf16 on Ada/Hopper; fp16 on Turing.
--enforce-eagerSkips 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.
--api-keyThe container webUrl is publicly resolvable. An unauthenticated LLM endpoint is exposed. Pair with chalk container run --authenticated.

3. Drive load

Use scripts/bench.py (bundled with this skill). It sweeps prompt length and concurrency, and reads throughput from vLLM's own Prometheus counters (vllm:prompt_tokens_total, vllm:generation_tokens_total, vllm:request_success_total) across a steady-state window.

sh
./scripts/bench.py --base-url "$URL" --model "$MODEL" \
  --input-lens 128,512 --concurrency 8,32,64 --duration 30

Methodology 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 (--cpu 6, the vLLM image already has Python + httpx) and point it at the server's pod IP (kubectl get pod <podName> -o jsonpath='{.status.podIP}'), then repeat against the public webUrl. If the two agree, the ingress is not your bottleneck — measure this rather than assuming it.

4. Confirm the GPU is the limiter

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. GPU_UTIL at 100 and POWER_USAGE 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.

5. Get the real $/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 6F81-5844-456A), summing the SKUs that make up the node:

py
# paginate skus?pageSize=5000&currencyCode=USD with an Authorization: Bearer token
# from `gcloud auth print-access-token`, filter serviceRegions for your region,
# then match description against your machine family and accelerator.

Worked example, g2-standard-8 (8 vCPU, 32 GiB, 1x L4) in us-central1:

SKURateQtySubtotal
G2 Instance Core running in Americas$0.024988 /hr8$0.199904
G2 Instance Ram running in Americas$0.002927 /GiB-hr32$0.093664
Nvidia L4 GPU running in Americas$0.560040 /hr1$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.

6. Compute the cost

$/sec        = node_hourly / 3600
$/1M in-tok  = $/sec / prompt_tok_per_s * 1e6
$/1M req     = $/sec / rps * 1e6

Report $/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 --output-tokens to a realistic completion length rather than 1.

Pitfalls

  • Prefix caching inflates $/input-token. A shared system prompt gets cached,

so prompt_tokens_total 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.

  • 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.

--enable-auto-tool-choice --tool-call-parser hermes plus an instruct model. Some Coder-tuned checkpoints emit tool calls as fenced JSON text that the parser ignores, so tool_calls returns null under tool_choice: auto while tool_choice: required works (guided decoding forces the shape).

  • Completed container pods are garbage-collected, taking their logs. End a

one-shot driver script with sleep so you can still kubectl logs it.

  • `chalk container exec` is unimplemented for k8s containers

(only implemented for host containers). Drive work via the entrypoint, or use --compute-class host / the sandbox API.

  • Piping a script into `bash` breaks silently if it invokes anything that

reads stdin — the child eats the rest of the script. Write it to a file and add exec </dev/null.

  • Sandboxes have no network egress and no flag to grant it; even public IPs

are blocked. Use chalk container run --route <CIDR> when the workload must reach anything.

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.

来自同一仓库

更多 Skills

全部 Skills