关于 benchmark-vllm-model
用于测量 Chalk 中 vLLM 模型每 Token 成本的技能。它结合持续负载测试、GPU 饱和度检查和云端 SKU 定价,生成可复现的成本估算。
主要功能
- 01在 GPU 上启动 vLLM
- 02扫描提示词长度与并发数
- 03测量服务端 Token 吞吐量
- 04用 DCGM 验证 GPU 饱和度
- 05依据 Cloud Billing SKU 计算成本
- 06报告输入 Token 和请求成本
适用场景
- 01比较模型服务配置
- 02估算每百万输入 Token 成本
- 03评估 GPU 限制下的吞吐量
- 04编写可复现的部署成本报告
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ chalk-ai/ chalk-ai-plugins --skill benchmark-vllm-model安装后在 Claude Code 中输入/benchmark-vllm-model确认已加载。
附带文件scripts/bench.py
详细介绍
在 Chalk 中对 vLLM 上的模型进行每 token 成本基准测试
得出有依据的每 token 成本:在 GPU 上运行模型,将负载提升至饱和,确认瓶颈确实是 GPU,然后用权威云服务定价除以实测吞吐量。
顺序很重要。没有饱和度检查的吞吐量数据无法证伪,而从博客文章中摘来的每小时价格通常是错误的(参见第 5 步)。
1. 运行模型服务
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 会返回 webUrl(可公开访问)和 podName。轮询 GET $webUrl/health——权重加载完成前会返回 503,加载完成后返回 200。下载 7B 模型预计需要几分钟。
GPU 选择。 只有标记为 chalk.ai/workload-type=compute 的 GPU 池才能运行容器/沙箱工作负载。在 staging 集群上,这类 GPU 池是 l4-gpus(L4、24GB、原生支持 bf16),上限为 2 个节点,每个节点 1 个 GPU。缺少此标签的池(例如 t4-gpus)会导致 Pod 处于 Unschedulable 状态,控制器会在 300s 后放弃——这比冷启动 GPU 节点的自动扩缩容时间还短,因此有效请求也可能纯粹因容量不足而失败。释放一个 GPU 后重试。
Turing(T4)不原生支持 bf16;在该系列 GPU 上使用 --dtype float16。
2. 会影响结果的服务参数
对计划运行的配置进行基准测试,并说明设置了哪些参数。这些参数会显著影响结果:
| 参数 | 影响原因 |
|---|---|
--max-num-seqs | 限制并发序列数。设置过高会让调度器在短请求上浪费时间——提高该值反而可能使吞吐量下降。请逐步调整测试。 |
--max-num-batched-tokens | 预填充分块预算。对于提示词占比高/输出较短的工作负载,这是关键限制因素。 |
--enable-prefix-caching | 在 vLLM V1 中默认启用。这会抬高表面上的提示词吞吐量(参见注意事项)。 |
--limit-mm-per-prompt '{"image":1}' | 多模态模型会根据每个提示词中的最坏情况图片数量来估算峰值激活内存,从而占用 KV 缓存。请固定该值。 |
--dtype | Ada/Hopper 使用 bf16;Turing 使用 fp16。 |
--enforce-eager | 跳过 CUDA 图捕获:启动更快,解码更慢。适用于只输出 1 个 token 的情况,但不适合通用服务。若基准测试结果要用于推广,不要启用此参数。 |
--api-key | 容器的 webUrl 可公开解析。未认证的 LLM 端点会暴露在外。请与 chalk container run --authenticated 配合使用。 |
3. 施加负载
使用 scripts/bench.py(随此技能附带)。它会扫描提示词长度和并发数,并在稳定状态窗口内读取 vLLM 自身的 Prometheus 计数器(vllm:prompt_tokens_total、vllm:generation_tokens_total、vllm:request_success_total)来获取吞吐量。
./scripts/bench.py --base-url "$URL" --model "$MODEL" \
--input-lens 128,512 --concurrency 8,32,64 --duration 30
方法规则,每条都针对一种实际故障模式:
- 使用持续运行窗口,而不是固定请求数。 不足 10 秒的窗口会受到抖动的显著影响。测量窗口至少使用 20 秒,并在测量前进行预热。
- 使用服务端计数器,而不是客户端计时。 这样可以排除客户端开销和广域网延迟对测量的影响。
- 逐步提高并发数,直到吞吐量不再上升。 只测试一个并发级别,无法判断饱和点在哪里。
- 使用唯一的提示词前缀。 相同的提示词会命中前缀缓存,抬高测试结果(附带脚本会为每个请求设置不同的种子)。
- 留意客户端瓶颈。 如果高并发下吞吐量下降,应先怀疑负载生成器的 CPU,而不是服务端。可在更高性能的机器上重跑,或在集群内部运行。
驱动程序的运行位置。 可以先从笔记本电脑运行。若要排除网络路径的影响,可在集群内部将其作为容器运行(使用 --cpu 6;vLLM 镜像已包含 Python 和 httpx),并将请求指向服务端的Pod IP(kubectl get pod <podName> -o jsonpath='{.status.podIP}'),然后再对公共 webUrl 重复测试。如果两者结果一致,则入口流量不是瓶颈——应实际测量,而不是想当然。
4. 确认瓶颈在 GPU
只有硬件已达到饱和时,吞吐量数据才代表 硬件 性能。 检查运行模型的节点上的 DCGM:
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)'
在运行期间采样。GPU_UTIL 达到 100,且 POWER_USAGE 达到显卡的 TDP
(L4 为 72W,A100 40GB 为 300W),说明瓶颈在 GPU,测得的数据是真实的。
利用率远低于 100% 则说明你测量的是客户端、网络或调度器上限,而非 GPU。
请注意,导出器会按固定间隔抓取数据,因此连续样本相同是正常的。
5. 获取真实的 $/小时价格
不要相信第三方定价页面或网页搜索摘要。 对于 GPU 机器系列,加速器通常是 与 vCPU 和 RAM 分开的 SKU,而聚合网站经常会漏掉它,导致节点价格低估 3 倍。
直接查询 GCP 的 Cloud Billing Catalog(Compute Engine 服务
6F81-5844-456A),将构成节点的 SKU 价格相加:
# paginate skus?pageSize=5000¤cyCode=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.
示例:us-central1 中的 g2-standard-8(8 vCPU、32 GiB、1x L4):
| 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. 计算成本
$/sec = node_hourly / 3600
$/1M in-tok = $/sec / prompt_tok_per_s * 1e6
$/1M req = $/sec / rps * 1e6
报告 $/1M 输入 token 和 $/1M 请求,并提供按需 / Spot / CUD 方案的数据。
选择合适的单位。 对于只输出一个 token 的分类器或防护栏,输出 token 数与请求数相同,因此“$/输出 token”是一个退化指标——应优先报告 $/分类。对于聊天或编码模型,解码占主导,$/输出 token 才是有意义的指标;请将 --output-tokens 设为符合实际的生成长度,而不是 1。
注意事项
- 前缀缓存会抬高 $/输入 token。 共享的系统提示词会被缓存,因此
prompt_tokens_total会统计实际上没有重新计算的 token。随附脚本会报告命中率。与按每个输入 token 向你收费的供应商相比,这样比较是 公平的,但这并不代表你实际的计算余量——请说明你引用的是哪种数据。 - 饱和状态下的数据是下限,不是预测值。 这些数据假设 100% 占空比。实际利用率为 20% 时,成本应乘以 5。在大多数生产部署中,GPU 空闲时间是主要成本。
- 工具调用模型需要正确的解析器和检查点。
--enable-auto-tool-choice --tool-call-parser hermes,并使用 instruct 模型。 一些针对 Coder 调优的检查点会将工具调用输出为带代码围栏的 JSON 文本,解析器会忽略这类内容,因此在tool_choice: auto下,tool_calls会返回 null,而tool_choice: required可以正常工作(引导式解码会强制生成指定格式)。 - 已完成的容器 Pod 会被垃圾回收,其日志也会随之删除。一次性驱动脚本的末尾加上
sleep,这样你仍然可以用kubectl logs查看日志。 chalk container exec尚未对 k8s 容器实现 (only implemented for host containers)。通过入口点执行任务,或者使用--compute-class host/ 沙箱 API。- 将脚本通过管道传入
bash会导致静默故障,前提是脚本调用了会读取 stdin 的命令——子进程会吞掉脚本剩余内容。请将脚本写入文件,并添加exec </dev/null。 - 沙箱没有网络出口,也没有可启用网络出口的标志;即使是公共 IP 也会被阻止。若工作负载必须访问其他目标,请使用
chalk container run --route <CIDR>。
报告
请注明模型、GPU、所有非默认服务标志、测得的吞吐量、DCGM 饱和证据,以及每小时成本背后的 SKU 明细。若缺少这些信息,按令牌计算的成本就无法复现,不应引用。