关于 serving-llms-on-epyc
该技能指导在 AMD EPYC CPU 上部署单实例 vLLM OpenAI 端点,面向需要 CPU 推理服务的用户。它按硬件检测、内存估算和单插槽资源绑定来规划启动,并要求失败时停止而不重试。
主要功能
- 01检测 EPYC 型号与指令集
- 02验证运行时及软件栈兼容性
- 03检查模型支持和主机内存
- 04按插槽配置 CPU 线程与内存
- 05启动并验证 vLLM 端点
适用场景
- 01在 EPYC 主机上提供 CPU 推理端点
- 02判断模型是否适合当前主机内存
- 03在双插槽主机上为实例选择单个插槽
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ amd/ skills --skill serving-llms-on-epyc安装后在 Claude Code 中输入/serving-llms-on-epyc确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.federated.jsonreference.mdskill-card.mddata/epyc.jsonevals/evals.jsonmachine.ymlscripts/check_model.pycpu_tune.pydetect.pyestimate_memory.pyvalidate.py详细介绍
在 AMD EPYC™ 上提供 LLM 服务(vLLM + zentorch,CPU)
在 AMD EPYC™ 主机上使用 zentorch CPU 后端启动单个 vLLM OpenAI 端点,并根据硬件配置容量。优先使用容器(Docker 或 Podman);conda/主机作为备用方案。即使主机已安装 AMD Instinct GPU,也不影响选择:当端点本身应运行在 EPYC CPU 上时,选择此技能。
这是单插槽服务:一个实例固定在一个插槽及其内存上(vLLM 跨插槽扩展效果不佳,因此不跨插槽运行)。在双插槽主机上,它会运行在单个插槽上;多插槽方案是多个实例(每个插槽一个),超出本单实例方案的范围。
此技能的硬性规则:任何失败时,都要报告原因和日志,然后停止。不要重试,也不要调试。(调试属于单独的工作流。)
Agent 自行执行服务流程——拉取、配置、启动、轮询——并使用运行时生成的 validate.py 报告。绝不要向用户提供逐项服务命令。与 serving-llms-on-instinct 类似,可访问的容器运行时是一次性的前置条件:如果 validate.py 未找到容器运行时,请报告一次性修复方法(使 Docker 可访问/安装 Podman/提供 conda 环境),然后停止。不要尝试使用 sudo 或提升权限。
数据文件
直接读取 data/epyc.json。其中包含容器镜像、必需的 CPU 运行标志、支持的精度、模型支持策略、默认模型,以及已验证的吞吐量标志注意事项。其 vllm_version 和镜像标签组成一组已验证的默认堆栈;保持两者一致,不要凭记忆硬编码其中任何一项。
步骤 1:检测 CPU
python3 scripts/detect.py # add --host user@box for a remote host
返回 cpu_model、is_amd_epyc、epyc_generation
(Naples/Rome/Milan/Genoa/Bergamo/Siena/Turin/Venice 或 EPYC 4004/4005)、
zen_arch、is_supported_epyc、avx512、logical_cores、physical_cores、
sockets、numa_nodes、memory_gb。
根据 detect.py 的结果选择服务路径:
is_amd_epyc为false-> 停止:此技能面向 AMD EPYC。(其他 x86 处理器可能可用,但不在此处的支持范围内。)avx512为false-> 此 CPU 无法运行 zentorch(其 bf16 路径需要 AVX-512 BF16,即avx512_bf16,该特性仅在 Zen4 及更新架构上提供)。这是 Zen4 之前的 EPYC(Naples / Rome / Milan,7000 系列)。不要就此结束——它仍然是 EPYC 主机:提供 stock vLLM CPU 路径(纯 vLLM,不使用 zentorch 加速——速度较慢,但已验证可在 EPYC 7763/Milan 上运行)。只有在用户明确同意后才继续,启动官方 stock vLLM CPU 镜像(步骤 6 的“Stock vLLM”变体);如果用户拒绝,则停止。is_supported_epyc为false且avx512为true(例如 Bergamo / Siena / EPYC 4004/4005)-> zentorch 路径未针对该代处理器验证。请获得用户明确确认后再尝试未验证的 zentorch 路径,或者提供同样的 stock vLLM 方案。- 否则(9000 系列——Genoa/Turin/Venice——支持 AVX-512 BF16)-> 使用已验证的 zentorch 路径。继续。
validate.py(步骤 2)会报告 zentorch_capable,并为 stock/未验证路径设置 requires_confirmation,因此它也会强制执行此路由。
在后续阶段保留 epyc_generation / avx512——例如,Venice 每个插槽最多可容纳 256 个核心,步骤 5 的线程绑定配置会根据此信息进行调整。
步骤 2:验证运行时和环境
python3 scripts/validate.py --image <image from data/epyc.json> --generation <epyc_generation from detect> --avx512 <avx512 from detect>
这也会强制执行 AVX-512 门槛:如果 CPU 不支持 AVX-512,validate.py
会返回阻断性 error(ready: false),因此无论步骤 1 中如何描述,流程都会在此处停止——不拉取镜像,也不启动。(如果省略 --avx512,它会自行读取本地 CPU 信息,因此即使没有传入该值,也会执行此门槛。)
返回 ready、requires_confirmation、zentorch_capable(false 表示 zentorch
无法运行;请在步骤 6 中使用标准 vLLM 版本)、runtime(docker、podman 或
null)、runtime_detail、conda_path_available、stack、compatibility、hf_cache
(解析后的 HF 缓存挂载路径——启动时使用 hf_cache.mount)、ram_gb,以及
errors/warnings/advisories。如果设置了 requires_confirmation(标准/未经验证的
路径),请告知用户并在启动前征得用户同意。选择路径:
runtime为docker或podman-> 容器路径(步骤 6),按原样使用。runtime为 null,但conda_path_available: true-> conda/主机路径。runtime为 null 且没有 conda ->ready为 false。报告一次性入门设置的fix(使 docker 可用/安装 podman/设置 conda 环境),然后停止。
如果 ready 为 false,请勿继续。
堆栈兼容性门槛。 validate.py 会探测所选运行时的确切 vllm/zentorch/torch
版本和当前启用的 vLLM 平台,然后设置 compatibility.status:
proceed-> 堆栈是经过验证的默认版本(或 Zen 平台上经过验证的版本系列);继续。blocked-> 当前启用的是标准 CPU 平台,因此 zentorch 加速未启用(错误)。报告compatibility.message并停止。confirmation_required(requires_confirmation: true)-> Venice 使用的 vLLM 不是固定的默认版本。此配方尚未在该版本的 Venice 上经过验证。告知用户compatibility.message,建议使用data/epyc.json中固定vllm_version的镜像,并在启动前 停止以等待用户明确确认是否继续。使用固定的默认 vLLM 版本时,Venice 可继续运行,且不会发出警告。
只有在镜像已存在于本地时才会运行该门槛。如果 validate.py 报告镜像尚未拉取,请先拉取镜像(或让步骤 6 拉取),然后重新运行 validate.py,以便门槛探测实际堆栈,而不只是镜像标签。
步骤 3:解析并验证模型
如果用户没有指定模型,请使用 data/epyc.json 中的 default_model
(Qwen/Qwen3-0.6B——无需权限门槛、小巧、快速获得首次成功)。否则使用用户指定的模型。
检查 vLLM 是否确实支持该模型(不要一概禁止多模态模型)。
传入模型实际运行所用的 vLLM 版本:如果 validate.py 已探测到该版本,请使用
stack.vllm(conda 环境中的版本可能与固定版本不同);否则使用
data/epyc.json 中的 vllm_version。
python3 scripts/check_model.py --model-id <model> --revision <rev or main> --vllm-version <stack.vllm from validate, else vllm_version from data/epyc.json>
- 退出码为 0 = vLLM 将其作为生成端点提供服务,或无法确定是否支持(需要权限/离线)——继续;启动时会确认。
- 退出码为 1 = 停止:架构不在 vLLM 的注册表中、属于
pooling/嵌入/重排序模型(不是聊天/补全端点),或是没有可用聊天模板的多模态模型(launchable: false)。报告打印出的message并停止。
结果还包含该模型支持的客户端端点:
primary_endpoint: "chat_completions"——存在可用聊天模板(chat_template.status: present);提供服务并移交/v1/chat/completions。primary_endpoint: "completions"——没有可用/可自动选择的模板(absent/ambiguous/unknown);提供服务并移交/v1/completions,使用原始prompt。也可以通过传入--chat-template <file>启用聊天(或者对于ambiguous,从chat_template.names中选择一个);切勿自行编造模板。- 将
primary_endpoint、supported_endpoints和chat_template传递到验证(步骤 7)和移交环节(步骤 8)。 - 允许使用多模态模型;vLLM 支持的多模态架构在 CPU 上仍可能触发仅适用于 GPU 的内核问题,该问题会在加载时显现(届时遵循不重试规则)。
精度/数据类型:原生 CPU 数据类型为 bf16(默认)、fp16、fp32。除非用户另有要求,否则使用
bfloat16。
对于受限模型(Llama、Gemma),必须设置 HF_TOKEN 并在 HuggingFace 上接受许可;否则停止并说明原因。
步骤 4:检查主机 RAM 是否足够
CPU 上的 RAM 是上限(权重和 KV 缓存都占用 RAM)。在同一行运行:
python3 scripts/estimate_memory.py --model-id <model> --revision <rev or main> --ram-gb <memory_gb from detect> --max-model-len <4096 or user value> --num-prompts <1 or desired concurrency>
退出码为 0 = 足够,退出码为 1 = 不足。如果 fit.fits 为 false:不要启动。
告知用户 required_gb 与 ram_gb 的数值,以及打印出的 fit.action——将
--max-model-len 降至 fit.suggested_max_model_len 后重试,或改用更小的模型。--max-model-len 和 --num-prompts 是影响 KV 缓存的两个参数。
额外标志:如果模型没有 HF 元数据(少见),可使用 --weight-gb N 覆盖权重大小。在 zentorch CPU 上,KV 缓存仅支持 bf16(不支持 fp8 KV)。
步骤 5:根据硬件配置 CPU 运行时
eval "$(python3 scripts/cpu_tune.py)" # or --format json to inspect
单个实例在一个插槽及其内存上运行(vLLM 跨插槽扩展效果不佳)。cpu_tune.py 会导出 VLLM_CPU_OMP_THREADS_BIND(所选插槽的物理核心)和 VLLM_CPU_KVCACHE_SPACE(根据该插槽的本地 RAM而非整个系统的内存配置,确保 KV 池位于同一插槽)。它不会设置 OMP_NUM_THREADS(由 vLLM 推导)或 VLLM_CPU_NUM_OF_RESERVED_CPU(使用 vLLM 自身的默认值)。
双插槽主机上的插槽选择(考虑负载):它会采样每个插槽的 CPU 忙碌率(约 0.5 秒),并优先选择空闲插槽——两个都空闲时选插槽 0;一个空闲时选该插槽;两个都繁忙(≥ --busy-threshold,默认 15%)时,会发出 warning,并选择最不繁忙的插槽继续运行。--socket N 可强制选择插槽。单插槽主机使用插槽 0。
对于选定的插槽,它还会输出受内存约束的绑定配置:容器路径使用 container_cpuset(--cpuset-cpus=<cores> --cpuset-mems=<nodes>);conda 使用 conda_launch_prefix(numactl --cpunodebind/--membind,若不可用则仅绑定 CPU 的 taskset;如果两者都不存在,则为空并附带说明)。如果设置了 warning,请告知用户。在 NPS2/NPS4 上,一个插槽跨越多个 NUMA 节点;内存会跨这些节点绑定,nps_note 会提示,更精细的绑定可能提升性能。
第 6 步:确认计划,然后启动(优先使用容器)
启动前,先呈现以下摘要并等待用户确认——未经提示不得启动。这是任何内容运行前必须经过的人工确认关卡:
| 字段 | 值 |
|---|---|
| 模型 / 类型 | <model>——text 或 multimodal(来自 check_model.py) |
| 后端 | zentorch 加速(Zen4+)或原版 vLLM CPU / 不使用 zentorch(zentorch_capable:false——未加速;已在 Milan 上验证) |
| 路径 | 容器(<runtime>,镜像来自 data/epyc.json)或 conda/主机 |
| 精度 | bfloat16(或用户选择的精度) |
| 内存适配 | 所需 <required_gb> GB,RAM 为 <ram_gb> GB |
| CPU 配置 | 插槽 <chosen_socket>(<socket_choice_reason>),绑定 <VLLM_CPU_OMP_THREADS_BIND>,KV <VLLM_CPU_KVCACHE_SPACE> GB(插槽本地),内存绑定到 <numa_nodes_on_socket> 个节点 |
| 硬件 | EPYC <epyc_generation>(<zen_arch>),<physical_cores> 个核心,AVX-512 <avx512> |
| 端口 | <port> |
如果 cpu_tune.py 返回了 warning(例如所有插槽都繁忙),也要在此处列出,让用户在确认前看到。
只有用户明确表示“开始”后才继续。如果用户拒绝或想要更改(模型、--max-model-len、端口),请停止并调整——不得启动。
根据 data/epyc.json 构建启动命令。CLI 为 vllm serve <model>。
在 vLLM >= 0.20 中不要传入 --device cpu——zentorch 插件会自动选择 CPU 平台,而 vllm serve 会拒绝该标志。只有在 vllm serve --help 列出此选项时才添加(适用于较旧版本的 vLLM)。
先选择一个空闲端口。 使用 --network=host 时,端口会直接绑定到主机,因此端口被占用会导致硬性失败(不会重新映射)。选择一个空闲端口——例如 PORT=8000; while ss -ltn "sport = :$PORT" | grep -q LISTEN; do PORT=$((PORT+1)); done——并在启动、健康检查轮询和交接中使用 $PORT。
挂载 validate.py 解析出的 HF 缓存。 使用 validate.py 中的 hf_cache.mount(它会跟踪符号链接并标记 NFS/root-squash),而不是直接使用 ~/.cache/huggingface,否则在 NFS 主目录下,绑定挂载可能会在容器启动时失败。
容器路径(来自 validate.py 的 runtime)。Agent 会自行运行这些命令,包括拉取镜像。RT 是原样解析出的运行时:
RT="<runtime from validate.py: docker | podman>"
$RT rm -f vllm-epyc 2>/dev/null # clear any leftover container from a prior run (name collision otherwise)
$RT pull <image from data/epyc.json> # agent pulls; do not ask the user to
$RT run -d --name vllm-epyc \
<run_flags from data/epyc.json> # --ipc=host --network=host --cap-add=SYS_NICE (SYS_NICE = NUMA membind; NO --shm-size with --ipc=host)
<hf_cache.mount from validate.py> \ # resolved real path, e.g. -v /scratch/you/hf:/root/.cache/huggingface
<container_cpuset from cpu_tune> # --cpuset-cpus=<cores> --cpuset-mems=<nodes>
--env VLLM_CPU_OMP_THREADS_BIND="$VLLM_CPU_OMP_THREADS_BIND" \
--env VLLM_CPU_KVCACHE_SPACE=$VLLM_CPU_KVCACHE_SPACE \
--env HF_TOKEN=${HF_TOKEN} \
<image from data/epyc.json> \
vllm serve <model> --dtype bfloat16 --port <port> --max-model-len <len>
Conda/主机路径(无容器运行时,conda_path_available 为 true)。eval cpu_tune 已导出环境变量;在启动命令前加上来自 cpu_tune 的 conda_launch_prefix,以将内存绑定到选定的插槽(为空 → 不固定,并附带说明):
<conda_launch_prefix from cpu_tune> vllm serve <model> --dtype bfloat16 --port <port> --max-model-len <len> &
# e.g. numactl --cpunodebind=0 --membind=0 vllm serve ...
原版 vLLM 路径(不使用 zentorch)——仅当 validate.py 报告
zentorch_capable: false(Zen4 之前的 EPYC,例如 Milan)且用户已确认使用未加速的回退方案时采用。使用官方原版 vLLM CPU 镜像,不要使用 zentorch 镜像:vllm/vllm-openai-cpu:latest-x86_64(其 ENTRYPOINT 为 vllm serve,因此将 <model> --dtype ... --port ... 作为参数传入)。使用与上述容器启动方式相同的环境配置和标志(VLLM_CPU_OMP_THREADS_BIND、--cpuset-cpus/--cpuset-mems、--cap-add=SYS_NICE、--ipc=host --network=host、解析出的 HF 缓存挂载):
RT="<runtime>"
$RT rm -f vllm-epyc 2>/dev/null
$RT run -d --name vllm-epyc \
--ipc=host --network=host --cap-add=SYS_NICE \
<container_cpuset from cpu_tune> <hf_cache.mount from validate.py> \
--env VLLM_CPU_OMP_THREADS_BIND="$VLLM_CPU_OMP_THREADS_BIND" \
--env VLLM_CPU_KVCACHE_SPACE=$VLLM_CPU_KVCACHE_SPACE --env HF_TOKEN=${HF_TOKEN} \
vllm/vllm-openai-cpu:latest-x86_64 \
<model> --dtype bfloat16 --port <port> --max-model-len <len>
此路径未加速(不使用 zentorch),但已在 EPYC 7763(Milan/Zen3)上通过 --dtype bfloat16 验证可用——bf16 可在没有 AVX-512 的原版 vLLM CPU 上运行,因此无需使用 fp32。如果加载仍然失败,请遵循不重试规则(报告并停止)。
可选的吞吐量标志为选择启用,且必须一起启用(参见 Gotchas):
TORCHINDUCTOR_FREEZING=1 + VLLM_USE_AOT_COMPILE=0(+ ZENTORCH_WEIGHT_PREPACK=1)。
基础启动命令不设置其中任何标志。
步骤 7:轮询直至服务启动并响应
加载期间出现 503 是正常现象。轮询 /health 直至服务响应,确认服务中的模型已列出,然后验证所选端点可用(来自步骤 3 中的 primary_endpoint)。CPU 首次编译可能需要一两分钟。
跟踪 healthy 标志,确保超时会被判定为失败,而不是继续执行。
# 1. container alive (conda: process alive) + /health, with a real timeout
healthy=""
for i in $(seq 1 120); do
$RT inspect -f '{{.State.Running}}' vllm-epyc 2>/dev/null | grep -q true || { echo "FAILED: container exited"; $RT logs --tail 50 vllm-epyc; break; }
curl -sf http://localhost:<port>/health >/dev/null 2>&1 && { healthy=1; echo "HEALTHY"; break; }
sleep 3
done
[ -n "$healthy" ] || { echo "FAILED: not healthy before timeout"; $RT logs --tail 50 vllm-epyc; }
# 2. the served model is registered
curl -sf --max-time 30 http://localhost:<port>/v1/models
然后测试模型实际支持的端点。冒烟检查时使用确定性采样,并限制输出长度:
# primary_endpoint == chat_completions
curl -sf --max-time 180 http://localhost:<port>/v1/chat/completions -H 'Content-Type: application/json' \
-d '{"model":"<served-model>","messages":[{"role":"user","content":"hi"}],"max_tokens":16,"temperature":0}'
# primary_endpoint == completions (no chat template)
curl -sf --max-time 180 http://localhost:<port>/v1/completions -H 'Content-Type: application/json' \
-d '{"model":"<served-model>","prompt":"Hello, world","max_tokens":16,"temperature":0}'
确认响应为 JSON,且 choices[0] 不包含错误(chat:message.content;completion:text)。HTTP 200 响应中若包含 error 负载,不算成功。
资源检查(验证清单):$RT stats --no-stream vllm-epyc。
如果服务始终未达到健康状态、/v1/models 未列出该模型,或端点返回错误/空的 choices:打印容器/进程日志,说明失败阶段,然后停止。不要重试。不要进入调试循环。
步骤 8:成功后,提供端点信息
向用户提供调用服务所需的一切信息。打印连接表:
| 字段 | 值 |
|---|---|
| 基础 URL | http://localhost:<port>/v1(末尾的 /v1 很重要) |
| 服务中的模型 | <served-model>(/v1/models 中的 id) |
| 端点 | /v1/chat/completions 或 /v1/completions(来自 primary_endpoint) |
| 原因 | chat = 存在聊天模板;completions = 没有模板(原始提示词) |
| 运行时/端口 | <runtime> / <port> |
| 资源配置 | OMP 线程数、KV GB、--max-model-len、套接字/NUMA 绑定 |
| 停止 | $RT rm -f vllm-epyc(容器)或 kill <pid>(conda) |
然后提供一个适用于所选端点且可直接运行的示例。
Chat 模型(primary_endpoint: chat_completions):
curl -s http://localhost:<port>/v1/chat/completions -H 'Content-Type: application/json' \
-d '{"model":"<served-model>","messages":[{"role":"user","content":"Hello"}],"max_tokens":128,"temperature":0.7}'
基础/提示词模型(primary_endpoint: completions):
curl -s http://localhost:<port>/v1/completions -H 'Content-Type: application/json' \
-d '{"model":"<served-model>","prompt":"Hello, world","max_tokens":128,"temperature":0.7}'
OpenAI Python 客户端(将 base_url 指向本地服务;SDK 要求密钥非空,因此服务未启用身份验证时可使用任意占位符):
from openai import OpenAI
client = OpenAI(base_url="http://localhost:<port>/v1", api_key="EMPTY")
model = client.models.list().data[0].id
# chat model:
r = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": "Hello"}],
max_tokens=128, temperature=0.7,
)
print(r.choices[0].message.content)
# base/prompt model:
r = client.completions.create(model=model, prompt="Hello, world", max_tokens=128)
print(r.choices[0].text)
需要一并提供的参数说明(完整列表见 reference.md):
max_tokens限制输出;prompt_tokens + max_tokens必须<= --max-model-len。temperature(0 = 确定性/贪心,高值 = 更随机);调整top_p或temperature,不要同时调整两者。stream: true会以流式方式(SSE)传输令牌,而不是等待后一次性返回响应。- 模型的
generation_config.json可以设置采样默认值;请传入明确的值以确保设置生效。
离线(单实例批处理)
如果要进行一次性离线运行而不是启动服务,请将步骤 6-8 替换为一次 vllm bench throughput(或离线 LLM.generate),使用相同的资源配置环境,等待运行完成,并报告指标。仍然遵守不重试/不调试的规则。
注意事项
完整列表请参阅 reference.md。以下是关键事项:
vllm serve中已移除--device cpu,适用于 vLLM >= 0.20。zentorch 插件会自动选择 CPU。传入此参数会导致vllm serve报错,提示 "unrecognized arguments: --device cpu"。- 仅设置
TORCHINDUCTOR_FREEZING=1会导致 engine-core 初始化崩溃,适用于 vLLM 0.23 / zentorch 2.11(AssertionError: expected OutputCode, got function)。只有同时设置VLLM_USE_AOT_COMPILE=0才能正常工作。切勿只设置其中一个。 /dev/shm:使用--ipc=host,不要使用--shm-size。 vLLM 需要较大的/dev/shm(容器默认的 64MB 太小)。基础配置使用--ipc=host,以共享主机的大容量共享内存。也不要传入--shm-size:podman 会报错 "cannot set shmsize when running in the host IPC Namespace",而且在 docker 中这是多余的。如果改为隔离 IPC(移除--ipc=host),则添加--shm-size=16g——二选一,不能同时使用。- NUMA / socket:单个实例固定在一个 socket 及其内存上——使用 CPU 绑定和
--cpuset-mems(容器)/numactl --membind(conda),并根据该 socket 的本地 RAM 设置 KV 大小。在双 socket 主机上,cpu_tune.py会根据负载选择空闲 socket;如果两个 socket 都繁忙,则会发出warning。对于 NPS2/NPS4(多节点 socket),会提供nps_note,指出更精细的逐节点绑定可能带来额外收益。 - 无 root 的 podman +
--cpuset-cpus/--cpuset-mems:这些是 cgroup 限制,在未委派 cpuset cgroup 的无 root podman 中可能会被忽略或拒绝(cgroup v1,或未委派该控制器的 v2)。这不会导致致命错误:容器内仍会通过VLLM_CPU_OMP_THREADS_BIND绑定 CPU 线程;只是容器级内存固定会失效(NUMA 局部性降低)。如果运行明确因 cpuset 标志而报错,请移除这些标志后继续——不要将其视为启动失败。 - HF 缓存挂载:默认挂载
~/.cache/huggingface。如果HF_HOME指向其他位置(共享主机上很常见,例如/proj/.../vllm),请改为将该路径挂载到/root/.cache/huggingface,否则模型会在容器内重新下载。 - 重复使用容器名称:如果上次运行留下了
vllm-epyc,run会报错,提示 "name already in use"——步骤 6 会先通过$RT rm -f vllm-epyc清除它。