serving-llms-on-epyc

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Serving LLMs on AMD EPYC™ (vLLM + zentorch, CPU)

在AMD EPYC™上部署LLM(vLLM + zentorch,CPU版)

Bring up a single vLLM OpenAI endpoint on an AMD EPYC™ host with the zentorch CPU backend, sized to the hardware. Container-first (Docker or Podman); conda/host is the fallback. An installed AMD Instinct GPU does not disqualify the host: select this skill when the endpoint itself should run on the EPYC CPU.
This is single-socket serving: one instance pinned to one socket and its memory (vLLM scales poorly across sockets, so we do not span them). On a dual-socket host it runs on a single socket; the multi-socket answer is multiple instances (one per socket), which is out of scope for this single-instance recipe.
Hard rule for this skill: on any failure, report the cause + logs and STOP. Do not retry, do not debug. (Debugging is a separate workflow.)
The agent does the serve flow itself -- pull, configure, launch, poll -- using the runtime
validate.py
reports. Never hand the user per-serve commands. Like serving-llms-on-instinct, an accessible container runtime is a one-time prerequisite: if
validate.py
finds none, report its one-time fix (make docker accessible / install podman / provide a conda env) and stop. Do not attempt
sudo
or privilege escalation.
在配备zentorch CPU后端的AMD EPYC™主机上搭建单个vLLM OpenAI端点,根据硬件规格进行适配。优先采用容器化方案(Docker或Podman);conda/主机环境作为备选方案。即使主机已安装AMD Instinct GPU也可使用本流程:当端点需运行在EPYC CPU上时,选择本技能即可。
本流程为单插槽部署:将一个实例固定到单个插槽及其内存上(vLLM跨插槽扩展性较差,因此不跨插槽部署)。在双插槽主机上,本流程仅在单个插槽上运行;多插槽场景的解决方案是多实例部署(每个插槽一个实例),这不在本单实例流程的覆盖范围内。
本技能的硬性规则:任何失败发生时,上报原因及日志并终止流程。请勿重试,也不要进行调试。(调试属于单独的工作流。)
代理将自行完成部署流程——拉取镜像、配置参数、启动服务、轮询状态——借助
validate.py
报告的运行时信息。绝不要让用户执行单独的部署命令。与serving-llms-on-instinct流程类似,可访问的容器运行时是一项一次性前置条件:如果
validate.py
未检测到容器运行时,需上报对应的一次性修复方案(配置Docker可访问权限/安装Podman/提供conda环境)并终止流程。请勿尝试使用
sudo
或提升权限。

Data file

数据文件

Read
data/epyc.json
directly. It holds the container image, mandatory CPU run flags, supported precision, the model-support policy, the default model, and the verified throughput-flag gotcha. Its
vllm_version
and image tag are one validated default stack; keep them aligned and do not hardcode either from memory.
直接读取
data/epyc.json
文件。该文件包含容器镜像、强制CPU运行标志、支持的精度、模型支持策略、默认模型以及已验证的吞吐量标志注意事项。其中的
vllm_version
和镜像标签是一组经过验证的默认技术栈;需保持两者一致,不要凭记忆硬编码任何一项。

Step 1: Detect the CPU

步骤1:检测CPU

bash
python3 scripts/detect.py            # add --host user@box for a remote host
Returns
cpu_model
,
is_amd_epyc
,
epyc_generation
(Naples/Rome/Milan/Genoa/Bergamo/Siena/Turin/Venice or EPYC 4004/4005),
zen_arch
,
is_supported_epyc
,
avx512
,
logical_cores
,
physical_cores
,
sockets
,
numa_nodes
,
memory_gb
.
Three hard gates -- stop if any fails:
  • is_amd_epyc
    is
    false
    -> stop: this skill targets AMD EPYC. (Other x86 may work but is unsupported here.)
  • is_supported_epyc
    is
    false
    -> stop: this recipe supports only the AMD EPYC 9000 series for now -- Genoa (9004), Turin (9005), and Venice (9006). Other EPYC (Bergamo, Siena, EPYC 4004/4005, pre-Zen4) may even expose AVX-512, but ISA compatibility alone does not make them supported targets for this skill; stop.
  • avx512
    is
    false
    -> stop: the zentorch CPU path requires AVX-512, i.e. Zen4+ on the supported 9000-series parts above. Pre-Zen4 EPYC (Naples / Rome / Milan) is not supported -- say so and stop rather than launching into a load-time failure.
Carry
epyc_generation
/
avx512
through the later phases -- e.g. Venice packs up to 256 cores/socket, which the thread-binding in Step 5 sizes from.
bash
python3 scripts/detect.py            # 若为远程主机,添加--host user@box参数
返回
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
存在三个硬性检查项——若任意一项不通过则终止流程:
  • is_amd_epyc
    false
    → 终止:本技能针对AMD EPYC处理器。(其他x86处理器可能可用,但不在本流程的支持范围内。)
  • is_supported_epyc
    false
    → 终止:本流程目前仅支持AMD EPYC 9000系列——Genoa(9004)、Turin(9005)和Venice(9006)。其他EPYC处理器(Bergamo、Siena、EPYC 4004/4005、Zen4之前的型号)即使支持AVX-512指令集,也不具备本技能所需的ISA兼容性;请终止流程。
  • avx512
    false
    → 终止:zentorch CPU路径必须依赖AVX-512指令集,即上述支持的9000系列处理器中的Zen4+架构。Zen4之前的EPYC处理器(Naples/Rome/Milan)不被支持——请明确告知用户并终止流程,避免后续加载时失败。
epyc_generation
/
avx512
参数带入后续流程——例如Venice每个插槽最多支持256核,步骤5中的线程绑定将据此配置。

Step 2: Validate the runtime and environment

步骤2:验证运行时与环境

bash
python3 scripts/validate.py --image <image from data/epyc.json> --generation <epyc_generation from detect>
Returns
ready
,
requires_confirmation
,
runtime
(
docker
,
podman
, or null),
runtime_detail
,
conda_path_available
,
stack
,
compatibility
,
ram_gb
, and
errors/warnings/advisories
. Pick the path:
  • runtime
    is
    docker
    or
    podman
    -> container path (Step 6), used verbatim.
  • runtime
    null but
    conda_path_available: true
    -> conda/host path.
  • runtime
    null and no conda ->
    ready
    is false. Report the one-time onboarding
    fix
    (make docker accessible / install podman / conda env) and stop.
Do not proceed if
ready
is
false
.
Stack-compatibility gate.
validate.py
probes the selected runtime for its exact
vllm
/
zentorch
/
torch
versions and the active vLLM platform, then sets
compatibility.status
:
  • proceed
    -> the stack is the validated default (or a validated family on a Zen platform); continue.
  • blocked
    -> a stock CPU platform is active, so zentorch acceleration is not on (error). Report
    compatibility.message
    and stop.
  • confirmation_required
    (
    requires_confirmation: true
    ) -> Venice on a vLLM other than the pinned default. This recipe has not been validated on Venice with that version. Surface
    compatibility.message
    , recommend the pinned
    vllm_version
    image from
    data/epyc.json
    , and stop for an explicit user go/no-go before launching. On the pinned default vLLM, Venice proceeds with no warning.
The gate only runs once the image is local. If
validate.py
reports the image is not pulled, pull it (or let Step 6 pull it) and re-run
validate.py
so the gate probes the real stack rather than only the tag.
bash
python3 scripts/validate.py --image <data/epyc.json中的镜像> --generation <detect步骤获取的epyc_generation>
返回
ready
requires_confirmation
runtime
docker
podman
或null)、
runtime_detail
conda_path_available
stack
compatibility
ram_gb
以及
errors/warnings/advisories
。根据结果选择路径:
  • runtime
    docker
    podman
    → 采用容器化路径(步骤6),直接使用返回值。
  • runtime
    为null但
    conda_path_available: true
    → 采用conda/主机路径。
  • runtime
    为null且无conda环境 →
    ready
    为false。上报一次性配置方案(配置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。本流程未在该组合下完成验证。需展示
    compatibility.message
    ,推荐使用
    data/epyc.json
    中固定的
    vllm_version
    镜像,并在启动前等待用户明确确认是否继续。若使用固定默认版本的vLLM,Venice处理器可直接继续执行,无需警告。
该检查仅在镜像已拉取到本地后执行。如果
validate.py
报告镜像未拉取,需先拉取镜像(或让步骤6自动拉取),然后重新运行
validate.py
,确保检查基于真实技术栈而非仅镜像标签。

Step 3: Resolve and validate the model

步骤3:解析并验证模型

If the user named no model, use
default_model
from
data/epyc.json
(
Qwen/Qwen3-0.6B
-- ungated, tiny, fast first success). Otherwise use theirs.
Check that vLLM actually supports the model (do not blanket-block multimodal). Pass the vLLM version the model will actually run on: use
stack.vllm
from
validate.py
when it was probed (the conda env may differ from the pin), else the
vllm_version
from
data/epyc.json
.
bash
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>
  • Exit 0 = vLLM serves it as a generation endpoint, or support is undeterminable (gated/offline) -- proceed; launch confirms.
  • Exit 1 = stop: the architecture is not in vLLM's registry, it is a
    pooling
    /embedding/reranker (not a chat/completion endpoint), or it is a multimodal model with no usable chat template (
    launchable: false
    ). Report the printed
    message
    and stop.
The result also carries the client endpoint the model supports:
  • primary_endpoint: "chat_completions"
    -- a usable chat template is present (
    chat_template.status: present
    ); serve and hand off
    /v1/chat/completions
    .
  • primary_endpoint: "completions"
    -- no usable/auto-selectable template (
    absent
    /
    ambiguous
    /
    unknown
    ); serve and hand off
    /v1/completions
    with a raw
    prompt
    . Chat can still be enabled by passing
    --chat-template <file>
    (or, for
    ambiguous
    , choosing one of
    chat_template.names
    ); never invent one.
  • Carry
    primary_endpoint
    ,
    supported_endpoints
    , and
    chat_template
    through to verification (Step 7) and the handoff (Step 8).
  • A
    multimodal
    model is allowed; a vLLM-supported multimodal arch may still hit a GPU-only kernel on CPU, which surfaces at load (the no-retry rule then applies).
Precision/dtype: native CPU dtypes are
bf16
(default),
fp16
,
fp32
. Use
bfloat16
unless the user asks otherwise.
For gated models (Llama, Gemma)
HF_TOKEN
must be set and the license accepted on HuggingFace; if not, stop and say so.
若用户未指定模型,使用
data/epyc.json
中的
default_model
Qwen/Qwen3-0.6B
——无权限限制、体积小、可快速验证成功)。否则使用用户指定的模型。
检查vLLM是否支持该模型(不要全面禁用多模态模型)。传入模型实际运行的vLLM版本:若已通过
validate.py
探测到版本,使用
stack.vllm
;否则使用
data/epyc.json
中的
vllm_version
bash
python3 scripts/check_model.py --model-id <模型> --revision <版本或main> --vllm-version <validate步骤获取的stack.vllm,否则为data/epyc.json中的vllm_version>
  • 退出码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 <文件>
    (若为
    ambiguous
    ,可选择
    chat_template.names
    中的一个模板)启用聊天功能;请勿自行创建模板。
  • primary_endpoint
    supported_endpoints
    chat_template
    带入后续验证(步骤7)与交付环节(步骤8)。
  • 允许使用
    multimodal
    模型;vLLM支持的多模态架构在CPU上运行时可能会调用GPU专属内核,该问题会在加载时暴露(此时需遵循不重试规则)。
精度/dtype:原生CPU支持的dtype为
bf16
(默认)、
fp16
fp32
。除非用户指定,否则默认使用
bfloat16
对于有权限限制的模型(Llama、Gemma),必须设置
HF_TOKEN
并在HuggingFace上接受许可;若未满足条件,需终止流程并告知用户。

Step 4: Check it fits host RAM

步骤4:检查模型是否适配主机内存

RAM is the ceiling on CPU (weights + KV cache both live in RAM). Run on ONE line:
bash
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>
Exit 0 = fits, exit 1 = does not fit. If
fit.fits
is false: do not launch. Tell the user
required_gb
vs
ram_gb
and the printed
fit.action
-- reduce
--max-model-len
to
fit.suggested_max_model_len
and retry, or use a smaller model.
--max-model-len
and
--num-prompts
are the two knobs that move KV. Extra flag:
--weight-gb N
overrides weights if a model has no HF metadata (rare). KV cache is bf16-only on zentorch CPU (no fp8 KV).
内存是CPU部署的上限(模型权重与KV缓存均存储在内存中)。执行以下单行命令:
bash
python3 scripts/estimate_memory.py --model-id <模型> --revision <版本或main> --ram-gb <detect步骤获取的memory_gb> --max-model-len <4096或用户指定值> --num-prompts <1或期望并发数>
退出码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缓存占用的两个关键参数。额外标志:
--weight-gb N
可在模型无HF元数据时覆盖权重内存估算(罕见情况)。zentorch CPU上的KV缓存仅支持bf16格式(无fp8 KV缓存)。

Step 5: Size the CPU runtime from the hardware

步骤5:根据硬件配置CPU运行参数

bash
eval "$(python3 scripts/cpu_tune.py)"      # or --format json to inspect
A single instance runs on one socket, with its memory (vLLM scales poorly across sockets).
cpu_tune.py
exports
VLLM_CPU_OMP_THREADS_BIND
(the chosen socket's physical cores) and
VLLM_CPU_KVCACHE_SPACE
(sized from that socket's local RAM, not whole-system, so the KV pool stays on-socket). It does not set
OMP_NUM_THREADS
(vLLM derives it) or
VLLM_CPU_NUM_OF_RESERVED_CPU
(vLLM's own default).
Socket choice on a dual-socket host (load-aware): it samples per-socket CPU busy% (~0.5s) and prefers a free socket -- both free → socket 0; one free → that socket; both busy (≥
--busy-threshold
, default 15%) → it
warning
s and proceeds on the least-busy socket
.
--socket N
forces a choice. Single-socket hosts use socket 0.
For the chosen socket it also emits the memory-bound pin:
container_cpuset
(
--cpuset-cpus=<cores> --cpuset-mems=<nodes>
) for the container path, and
conda_launch_prefix
(
numactl --cpunodebind/--membind
, falling back to
taskset
CPU-only, or empty-with-note if neither tool exists) for conda. Surface
warning
to the user
if set. On NPS2/NPS4 a socket spans multiple NUMA nodes; memory is bound across them and
nps_note
flags that finer binding could add performance.
bash
eval "$(python3 scripts/cpu_tune.py)"      # 或使用--format json参数查看详细结果
单个实例将运行在单个插槽及其内存上(vLLM跨插槽扩展性较差)。
cpu_tune.py
会导出
VLLM_CPU_OMP_THREADS_BIND
(所选插槽的物理核心)和
VLLM_CPU_KVCACHE_SPACE
(根据该插槽本地内存而非整机内存配置,确保KV缓存始终位于插槽本地)。该脚本不会设置
OMP_NUM_THREADS
(vLLM会自动推导)或
VLLM_CPU_NUM_OF_RESERVED_CPU
(使用vLLM默认值)。
双插槽主机上的插槽选择(基于负载):脚本会采样每个插槽的CPU使用率(约0.5秒),优先选择空闲插槽——若两个插槽均空闲则选择插槽0;若仅一个空闲则选择该插槽;若两个插槽均繁忙(使用率≥
--busy-threshold
,默认15%)→ 发出警告并选择使用率最低的插槽
--socket N
参数可强制指定插槽。单插槽主机默认使用插槽0。
对于所选插槽,脚本还会输出内存绑定参数:容器化路径使用
container_cpuset
--cpuset-cpus=<核心> --cpuset-mems=<节点>
),conda路径使用
conda_launch_prefix
numactl --cpunodebind/--membind
,若该工具不可用则回退到
taskset
仅绑定CPU,若两者均不可用则为空并附带说明)。若脚本返回
warning
,需告知用户
。在NPS2/NPS4架构中,一个插槽包含多个NUMA节点;内存会跨节点绑定,
nps_note
会提示更精细的节点绑定可提升性能。

Step 6: Confirm the plan, then launch (container-first)

步骤6:确认部署方案,然后启动服务(优先容器化)

Before launching, present this summary and wait for the user to confirm -- do not launch unprompted. This is the human gate before anything runs:
FieldValue
Model / kind
<model>
--
text
or
multimodal
(from
check_model.py
)
Pathcontainer (
<runtime>
, image from
data/epyc.json
) or conda/host
Precision
bfloat16
(or the user's choice)
Fitrequired
<required_gb>
GB vs
<ram_gb>
GB RAM
CPU sizingsocket
<chosen_socket>
(
<socket_choice_reason>
), bind
<VLLM_CPU_OMP_THREADS_BIND>
, KV
<VLLM_CPU_KVCACHE_SPACE>
GB (socket-local), mem bound to nodes
<numa_nodes_on_socket>
HardwareEPYC
<epyc_generation>
(
<zen_arch>
),
<physical_cores>
cores, AVX-512
<avx512>
Port
<port>
If
cpu_tune.py
returned a
warning
(e.g. all sockets busy), include it here so the user sees it before confirming.
Proceed only on a clear "go". If the user declines or wants changes (model,
--max-model-len
, port), stop and adjust -- do not launch.
Build the launch from
data/epyc.json
. The CLI is
vllm serve <model>
. Do not pass
--device cpu
on vLLM >= 0.20 -- the zentorch plugin auto-selects the CPU platform and
vllm serve
rejects the flag. Only add it if
vllm serve --help
lists it (older vLLM).
Container path (
runtime
from validate.py). The agent runs these itself, including the pull.
RT
is the resolved runtime verbatim:
bash
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 (NO --shm-size: it conflicts with --ipc=host on podman)
  <hf_cache_mount> \
  <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/host path (no container runtime,
conda_path_available
true).
eval
-ing cpu_tune already exported the env vars; prefix the launch with
conda_launch_prefix
from cpu_tune so memory is bound to the chosen socket (empty → unpinned, with a note):
bash
<conda_launch_prefix from cpu_tune> vllm serve <model> --dtype bfloat16 --port <port> --max-model-len <len> &
启动前,需向用户展示以下汇总信息并等待用户确认——请勿未经确认直接启动。这是实际运行前的人工检查环节:
字段
模型/类型
<模型>
--
text
multimodal
(来自
check_model.py
部署路径容器(
<runtime>
,镜像来自
data/epyc.json
)或conda/主机
精度
bfloat16
(或用户指定的精度)
内存适配所需
<required_gb>
GB vs 可用
<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
会因“无法识别的参数:--device cpu”报错。仅当
vllm serve --help
中列出该参数时(旧版vLLM)才添加。
容器化路径
validate.py
返回的
runtime
)。代理将自行执行以下命令,包括拉取镜像。
RT
为解析后的运行时命令:
bash
RT="<validate.py返回的runtime: docker | podman>"
$RT rm -f vllm-epyc 2>/dev/null               # 清除之前运行残留的容器(避免名称冲突)
$RT pull <data/epyc.json中的镜像>          # 代理自动拉取;请勿让用户执行
$RT run -d --name vllm-epyc \
  <data/epyc.json中的run_flags>            # --ipc=host --network=host(请勿添加--shm-size:在podman上会与--ipc=host冲突)
  <hf_cache_mount> \
  <cpu_tune返回的container_cpuset>             # --cpuset-cpus=<核心> --cpuset-mems=<节点>
  --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} \
  <data/epyc.json中的镜像> \
  vllm serve <model> --dtype bfloat16 --port <port> --max-model-len <len>
conda/主机路径(无容器运行时,
conda_path_available
为true)。执行cpu_tune脚本时已导出环境变量;启动命令前需添加cpu_tune返回的
conda_launch_prefix
,确保内存绑定到所选插槽(若为空则不绑定,并附带说明):
bash
<cpu_tune返回的conda_launch_prefix> vllm serve <model> --dtype bfloat16 --port <port> --max-model-len <len> &

e.g. numactl --cpunodebind=0 --membind=0 vllm serve ...

示例:numactl --cpunodebind=0 --membind=0 vllm serve ...


Optional throughput flags are **opt-in and must move together** (see Gotchas):
`TORCHINDUCTOR_FREEZING=1` + `VLLM_USE_AOT_COMPILE=0` (+ `ZENTORCH_WEIGHT_PREPACK=1`).
The base launch sets none of them.

可选吞吐量标志**需同时启用**(参见注意事项):`TORCHINDUCTOR_FREEZING=1` + `VLLM_USE_AOT_COMPILE=0`(+ `ZENTORCH_WEIGHT_PREPACK=1`)。基础启动命令不包含任何这些标志。

Step 7: Poll until up and responsive

步骤7:轮询直到服务启动并响应

A 503 while loading is normal. Poll
/health
until the server answers, confirm the served model is listed, then prove the selected endpoint works (from
primary_endpoint
in Step 3). CPU first-token compile can take a minute or two. Track a
healthy
flag so a timeout is a failure, not a fall-through.
bash
undefined
加载过程中返回503状态码属于正常情况。轮询
/health
端点直到服务器响应,确认已部署的模型已注册,然后验证所选端点是否可用(来自步骤3的
primary_endpoint
)。CPU首次编译可能需要1-2分钟。需跟踪
healthy
标志,超时则判定为失败,而非继续执行。
bash
undefined

1. container alive (conda: process alive) + /health, with a real timeout

1. 容器存活(conda环境:进程存活)+ /health端点响应,设置超时时间

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; }
healthy="" for i in $(seq 1 120); do $RT inspect -f '{{.State.Running}}' vllm-epyc 2>/dev/null | grep -q true || { echo "失败:容器已退出"; $RT logs --tail 50 vllm-epyc; break; } curl -sf http://localhost:<port>/health >/dev/null 2>&1 && { healthy=1; echo "健康状态正常"; break; } sleep 3 done [ -n "$healthy" ] || { echo "失败:超时前未进入健康状态"; $RT logs --tail 50 vllm-epyc; }

2. the served model is registered

2. 验证已部署模型已注册

curl -sf --max-time 30 http://localhost:<port>/v1/models

Then exercise the endpoint the model actually supports. Use deterministic
sampling and a small output cap for the smoke check:

```bash
curl -sf --max-time 30 http://localhost:<port>/v1/models

然后测试模型实际支持的端点。使用确定性采样并设置较小的输出上限进行冒烟测试:

```bash

primary_endpoint == chat_completions

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}'
curl -sf --max-time 180 http://localhost:<port>/v1/chat/completions -H 'Content-Type: application/json'
-d '{"model":"<已部署模型>","messages":[{"role":"user","content":"hi"}],"max_tokens":16,"temperature":0}'

primary_endpoint == completions (no chat template)

primary_endpoint == completions (无聊天模板)

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}'

Confirm the response is JSON with a non-error `choices[0]` (chat: `message.content`;
completion: `text`). An HTTP 200 that carries an `error` payload is **not** success.
Resource sanity (your validation list): `$RT stats --no-stream vllm-epyc`.

**If the server never becomes healthy, `/v1/models` omits the model, or the
endpoint returns an error/empty `choices`: print the container/process logs,
state the failing phase, and STOP. Do not retry. Do not start a debugging loop.**
curl -sf --max-time 180 http://localhost:<port>/v1/completions -H 'Content-Type: application/json'
-d '{"model":"<已部署模型>","prompt":"Hello, world","max_tokens":16,"temperature":0}'

确认响应为JSON格式且包含非错误的`choices[0]`(聊天模型:`message.content`;补全模型:`text`)。返回HTTP 200但包含`error`负载**不代表成功**。资源状态检查:`$RT stats --no-stream vllm-epyc`。

**若服务器始终无法进入健康状态、`/v1/models`未列出模型,或端点返回错误/空`choices`:打印容器/进程日志,说明失败阶段并终止流程。请勿重试,也不要进入调试循环。**

Step 8: On success, hand over the endpoint

步骤8:部署成功后交付端点

Give the user everything needed to call the server. Print a connection table:
FieldValue
Base URL
http://localhost:<port>/v1
(the trailing
/v1
matters)
Served model
<served-model>
(the id from
/v1/models
)
Endpoint
/v1/chat/completions
or
/v1/completions
(from
primary_endpoint
)
Whychat = a chat template is present; completions = no template (raw prompts)
Runtime / port
<runtime>
/
<port>
SizingOMP threads, KV GB,
--max-model-len
, socket / NUMA pinning
Stop
$RT rm -f vllm-epyc
(container) or
kill <pid>
(conda)
Then a ready-to-run example for the selected endpoint.
Chat model (
primary_endpoint: chat_completions
):
bash
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}'
Base/prompt model (
primary_endpoint: completions
):
bash
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 client (point
base_url
at the local server; the SDK requires a non-empty key, so any placeholder works when the server has no auth):
python
from openai import OpenAI

client = OpenAI(base_url="http://localhost:<port>/v1", api_key="EMPTY")
model = client.models.list().data[0].id
向用户提供调用服务器所需的全部信息。打印连接信息表:
字段
基础URL
http://localhost:<port>/v1
(末尾的
/v1
至关重要)
已部署模型
<已部署模型>
(来自
/v1/models
的ID)
端点
/v1/chat/completions
/v1/completions
(来自
primary_endpoint
说明chat = 存在聊天模板;completions = 无模板(需传入原始prompt)
运行时/端口
<runtime>
/
<port>
配置信息OMP线程数、KV缓存大小、
--max-model-len
、插槽/NUMA绑定
停止命令
$RT rm -f vllm-epyc
(容器)或
kill <pid>
(conda)
然后提供针对所选端点的可直接运行示例。
聊天模型(
primary_endpoint: chat_completions
):
bash
curl -s http://localhost:<port>/v1/chat/completions -H 'Content-Type: application/json' \
  -d '{"model":"<已部署模型>","messages":[{"role":"user","content":"Hello"}],"max_tokens":128,"temperature":0.7}'
基础/补全模型(
primary_endpoint: completions
):
bash
curl -s http://localhost:<port>/v1/completions -H 'Content-Type: application/json' \
  -d '{"model":"<已部署模型>","prompt":"Hello, world","max_tokens":128,"temperature":0.7}'
OpenAI Python客户端(将
base_url
指向本地服务器;SDK要求非空密钥,因此当服务器无认证时可使用任意占位符):
python
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)
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)

Argument guidance to pass along (see [reference.md](reference.md) for the full list):
- `max_tokens` caps the **output**; `prompt_tokens + max_tokens` must be `<= --max-model-len`.
- `temperature` (0 = deterministic/greedy, higher = more random); tune `top_p` *or*
  `temperature`, not both.
- `stream: true` streams tokens (SSE) instead of one blocking response.
- The model's `generation_config.json` can set sampling defaults; pass explicit
  values to be sure.
r = client.completions.create(model=model, prompt="Hello, world", max_tokens=128) print(r.choices[0].text)

同时提供参数使用指南(完整列表参见[reference.md](reference.md)):
- `max_tokens`限制**输出**长度;`prompt_tokens + max_tokens`必须`<= --max-model-len`。
- `temperature`(0 = 确定性/贪婪采样,值越高越随机);请调整`top_p`或`temperature`中的一个,不要同时调整。
- `stream: true`会以SSE方式流式返回令牌,而非一次性返回完整响应。
- 模型的`generation_config.json`可设置采样默认值;建议传入明确参数以确保效果。

Offline (single-instance batch)

离线(单实例批量处理)

For a one-shot offline run instead of a server, replace Step 6-8 with a single
vllm bench throughput
(or an offline
LLM.generate
) using the same sized env, wait for completion, and report the metrics. Same no-retry / no-debug rule.
若需一次性离线运行而非搭建服务器,可将步骤6-8替换为单个
vllm bench throughput
命令(或离线
LLM.generate
),使用相同的环境配置,等待执行完成后上报指标。同样遵循不重试/不调试规则。

Gotchas

注意事项

See reference.md for the full list. The load-bearing ones:
  • --device cpu
    was removed
    from
    vllm serve
    in vLLM >= 0.20. The zentorch plugin auto-selects CPU. Passing it makes
    vllm serve
    error with "unrecognized arguments: --device cpu".
  • TORCHINDUCTOR_FREEZING=1
    alone crashes engine-core init
    on vLLM 0.23 / zentorch 2.11 (
    AssertionError: expected OutputCode, got function
    ). It only works with
    VLLM_USE_AOT_COMPILE=0
    set alongside it. Never set one without the other.
  • /dev/shm
    — use
    --ipc=host
    , not
    --shm-size
    .
    vLLM needs a large
    /dev/shm
    (the 64MB container default is too small). The base recipe uses
    --ipc=host
    , which shares the host's large shared memory. Do not also pass
    --shm-size
    : podman errors with "cannot set shmsize when running in the host IPC Namespace", and it is redundant on docker. If you instead isolate IPC (drop
    --ipc=host
    ), then add
    --shm-size=16g
    — one or the other, never both.
  • NUMA / socket: one instance is pinned to one socket plus its memory -- CPU bind +
    --cpuset-mems
    (container) /
    numactl --membind
    (conda), with KV sized from that socket's local RAM. On a dual-socket host
    cpu_tune.py
    picks a free socket by load and
    warning
    s if both are busy. NPS2/NPS4 (multi-node socket) gets an
    nps_note
    that finer per-node binding could add more.
  • Rootless podman +
    --cpuset-cpus
    /
    --cpuset-mems
    : these are cgroup limits and may be ignored or rejected on rootless podman without cpuset cgroup delegation (cgroup v1, or v2 without the controller delegated). This is not fatal: CPU thread binding still applies via
    VLLM_CPU_OMP_THREADS_BIND
    inside the container; only the container-level memory pin is lost (reduced NUMA locality). If the run errors specifically on the cpuset flags, drop them and proceed -- do not treat it as a launch failure.
  • HF cache mount: the default mounts
    ~/.cache/huggingface
    . If
    HF_HOME
    points elsewhere (common on shared hosts, e.g.
    /proj/.../vllm
    ), mount that path to
    /root/.cache/huggingface
    instead, or the model re-downloads inside the container.
  • Container name reuse: a leftover
    vllm-epyc
    from a prior run makes
    run
    fail with "name already in use" -- Step 6 clears it first with
    $RT rm -f vllm-epyc
    .
完整列表参见reference.md。关键注意事项:
  • --device cpu
    已被移除
    :在vLLM >= 0.20版本中,
    vllm serve
    不再支持该参数。zentorch插件会自动选择CPU平台。传入该参数会导致
    vllm serve
    报错:“无法识别的参数:--device cpu”。
  • 单独设置
    TORCHINDUCTOR_FREEZING=1
    会导致引擎核心初始化崩溃
    :在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会报错“在主机IPC命名空间中无法设置shmsize”,且在Docker上该参数是冗余的。若需隔离IPC(移除
    --ipc=host
    ),则添加
    --shm-size=16g
    ——二选一,请勿同时使用。
  • NUMA/插槽:单个实例固定到单个插槽及其内存——CPU绑定 +
    --cpuset-mems
    (容器)/
    numactl --membind
    (conda),KV缓存根据该插槽本地内存配置。在双插槽主机上,
    cpu_tune.py
    会根据负载选择空闲插槽,若两个插槽均繁忙则发出警告。NPS2/NPS4(多节点插槽)会附带
    nps_note
    ,提示更精细的节点绑定可提升性能。
  • 无根Podman +
    --cpuset-cpus
    /
    --cpuset-mems
    :这些是cgroup限制,在无根Podman环境中若未配置cpuset cgroup委托(cgroup v1,或v2未委托控制器),可能会被忽略或拒绝。这不致命:容器内部的CPU线程绑定仍会通过
    VLLM_CPU_OMP_THREADS_BIND
    生效;仅容器级内存绑定失效(NUMA局部性降低)。若运行时因cpuset标志报错,可移除这些标志后继续执行——请勿将其视为启动失败。
  • HF缓存挂载:默认挂载
    ~/.cache/huggingface
    。若
    HF_HOME
    指向其他路径(共享主机上常见,例如
    /proj/.../vllm
    ),需将该路径挂载到容器内的
    /root/.cache/huggingface
    ,否则模型会在容器内重新下载。
  • 容器名称重复:之前运行残留的
    vllm-epyc
    容器会导致
    run
    命令报错“名称已被使用”——步骤6会先执行
    $RT rm -f vllm-epyc
    清除残留容器。