| @@ -1 +1,6 @@ | |||
| 1 | .idea/ | 1 | .idea/ |
| 2 | +__pycache__/ | ||
| 3 | +*.pyc | ||
| 4 | + | ||
| 5 | +# infernex-deploy skill 测试(暂存本地,不入 PR) | ||
| 6 | +skills/infernex-deploy/test/ | ||
| @@ -0,0 +1,92 @@ | |||||||||||||||||||||||||||||||||
| 1 | +--- | ||||||||||||||||||||||||||||||||
| 2 | +name: infernex-deploy | ||||||||||||||||||||||||||||||||
| 3 | +description: >- | ||||||||||||||||||||||||||||||||
| 4 | + 在 Kubernetes(昇腾 NPU)上部署或卸载 InferNex(openFuyao AI 推理 Helm Chart)。 | ||||||||||||||||||||||||||||||||
| 5 | + 包含环境采集、intent 配置确认(C1 门)、预检深度选择(C2 门,硬件 checker 默认跳过、 | ||||||||||||||||||||||||||||||||
| 6 | + 连通性 ping 默认关闭)、软件+硬件预检(C3 门确认报告)、helm 安装/升级、网关推理验证、 | ||||||||||||||||||||||||||||||||
| 7 | + 仅卸载 InferNex。不包含 InferNex-Bridge。 | ||||||||||||||||||||||||||||||||
| 8 | + 触发词:部署 InferNex、安装 infernex、一键推理部署、预检 infernex、跑 checker、 | ||||||||||||||||||||||||||||||||
| 9 | + 升级 release、卸载 infernex 等。 | ||||||||||||||||||||||||||||||||
| 10 | +metadata: | ||||||||||||||||||||||||||||||||
SKILL.md 版本号与 PR 版本不一致。PR 描述明确为 v1.0.0 基线,但 SKILL.md 中 version 为 "1.4.0"。建议修正为 "1.0.0" 或在 PR 描述中说明版本号演进逻辑。 改动建议
![]() ![]() PR 描述称「v1.0.0,社区第一版基线」,但 SKILL.md frontmatter 中 version 已为 "1.4.0"。经 commit 历史核实(11 个提交逐步演进),1.4.0 为当前真实版本,PR 描述未及时更新。建议同步 PR 描述中的版本号,或在 SKILL.md 中注明版本演进说明。 ![]() ![]() | |||||||||||||||||||||||||||||||||
| 11 | + author: openFuyao | ||||||||||||||||||||||||||||||||
SKILL.md 版本号与 PR 标题不一致。PR 标题为 v1.0.0,但 SKILL.md 中 version 为 "1.4.0"。需确认是否为有意为之,若非则应同步为 "1.0.0"。 改动建议
![]() ![]() | |||||||||||||||||||||||||||||||||
| 12 | + version: "1.4.0" | ||||||||||||||||||||||||||||||||
| 13 | + language: zh-CN | ||||||||||||||||||||||||||||||||
| 14 | +--- | ||||||||||||||||||||||||||||||||
| 15 | + | ||||||||||||||||||||||||||||||||
| 16 | +# InferNex 部署 Skill | ||||||||||||||||||||||||||||||||
metadata.version 为 "1.0.0" 与 PR 描述中 v0.3.4 不一致。建议统一版本号或明确 1.0.0 为 Skill 接口版本。 改动建议
![]() ![]() | |||||||||||||||||||||||||||||||||
| 17 | + | ||||||||||||||||||||||||||||||||
| 18 | +通过 Helm 在 Kubernetes 上部署 **InferNex**。不要求用户工作区有 InferNex 源码——先跑 `scripts/bootstrap.sh` 拉 Chart(默认不拉 checker)。 | ||||||||||||||||||||||||||||||||
| 19 | + | ||||||||||||||||||||||||||||||||
| 20 | +**不在范围内:** InferNex-Bridge、Bridge CRD、LLMInferenceService。 | ||||||||||||||||||||||||||||||||
| 21 | + | ||||||||||||||||||||||||||||||||
| 22 | +执行集群/出网命令前读 `references/ops.md`(权限与交互)。**会话流程与确认门先读 `references/gates.md`**。其余按场景查下表。 | ||||||||||||||||||||||||||||||||
| 23 | + | ||||||||||||||||||||||||||||||||
| 24 | +--- | ||||||||||||||||||||||||||||||||
| 25 | + | ||||||||||||||||||||||||||||||||
| 26 | +## 硬门禁(不可跳过) | ||||||||||||||||||||||||||||||||
[严重] SKILL.md 元数据 version 与 PR 描述不一致 PR 描述明确标注 v1.0.0(社区第一版基线),但 SKILL.md 中 version 为 改动建议
![]() ![]() | |||||||||||||||||||||||||||||||||
| 27 | + | ||||||||||||||||||||||||||||||||
| 28 | +1. **主 Chart 基线** — 从 `${INFERNEX_CHART_DIR}/values.yaml` 复制再 patch;禁止整份复制 `examples/`。 | ||||||||||||||||||||||||||||||||
| 29 | +2. **确认门必经** — 部署链路 C1(配置确认)→ C2(预检深度)→ C3(预检报告确认)不可跳过;**预检通过后须 C3 确认才开始部署,不再自动继续**;卸载必经 U1。问题与选项措辞逐字来自 `gates.md`,禁止改写/增删/合并;确认对象(intent / report / plan)落盘 `.infernex/deploy/<ts>/`。 | ||||||||||||||||||||||||||||||||
| 30 | +3. **checker 经 C2 门选择** — 每次部署主动询问硬件检查(默认推荐跳过);一旦执行,`summary.failed > 0` 为硬性,未清零不得 helm。未跑 checker 仍须完成集群检查、values 校验、helm 渲染。checker 的连通性 ping 默认关闭(不传 `--enable-connectivity-check`,跳过 H-07 Step2 / H-10),仅 C2b 开启时执行并采集 SSH 凭据;旧缓存二进制报 unknown flag 时先 `--refresh-checker` 刷新。 | ||||||||||||||||||||||||||||||||
| 31 | +4. **预检脚本驱动、判定数据化** — 预检必须跑 `scripts/preflight_run.py`(清单/触发条件/判定命令全部数据化在 `references/checks.yaml` v2;产出 `preflight-report.json/.md`);禁止绕过执行器按自然语言自行判定、增删检查项或改报告措辞。硬性问题未解决前不得安装;改集群资源前须用户同意;自动修复仅限白名单(C3r)。 | ||||||||||||||||||||||||||||||||
| 32 | +5. **LWS 须真就绪** — 不只看 CRD;见 `preflight.md` S-02。 | ||||||||||||||||||||||||||||||||
| 33 | +6. **推理通了才算成功** — 经网关 `POST /v1/chat/completions` → 200;Pod Running 不算完成。交付 `http://<节点IP>:<gatewayPort>`(默认 **30088**),勿用 port-forward 当正式入口。 | ||||||||||||||||||||||||||||||||
| 34 | +7. **卸载三项点选(U1 门)** — 固定:「仅 helm uninstall(轻量)」/「全部清理」/「取消」,确认对象为 uninstall-plan(列明将删与不删)。全部清理含 PVC、orphan LWS,以及 Chart 装到 `eagle-eye`/`nats`/`scaling-system` 的 controller 与 CR。仍不删 LWS Operator、npu-operator、metrics-server、Bridge 无关 ns。见 `gates.md` / `flow.md` §卸载。 | ||||||||||||||||||||||||||||||||
| 35 | +8. **默认不要 SSH** — 节点 IP 用 `kubectl get nodes -o wide`;仅 C2c(连通性检查)/ 白名单节点修复才采集 SSH(点选 UI,不回显明文)。 | ||||||||||||||||||||||||||||||||
| 36 | +9. **确定性渲染(禁止手改)** — 模式 / 拓扑 / 模型 / 命名 / RSG 字段的 patch **只能**由 `scripts/render_values.py` 从 intent 生成(内嵌 S-14~S-22 自检 + golden UT);禁止手改这些字段,禁止同一参数既写 Helm 字段又写 `extraArgs`。推理参数只经 `intent.inferenceParams`(已知键 + extraArgs)重新渲染。算法说明见 `values_rules.md`。 | ||||||||||||||||||||||||||||||||
| 37 | +10. **点选交互(有则必用)** — 凡需用户作答必须用结构化提问工具;门的问题与选项逐字来自 `gates.md`。完整规则(平台工具、回退、不弹窗)见 `ops.md` §点选交互。 | ||||||||||||||||||||||||||||||||
| 38 | +11. **长等待可续跑** — Helm 后立即发 interim 进度;用 `wait_inference.py` 写 checkpoint;用户说「继续等待 infernex」→ 读 state 续跑,禁止重装。见 `flow.md`。 | ||||||||||||||||||||||||||||||||
| 39 | +12. **Helm 禁止 `--wait`** — RSG/证书 Job 为 post-install hook,与 `helm … --wait` 会死锁;安装只用 `helm upgrade --install`(无 `--wait`),就绪靠 §6 轮询。 | ||||||||||||||||||||||||||||||||
| 40 | + | ||||||||||||||||||||||||||||||||
| 41 | +另:**禁止** `kubectl logs -f`、禁止猜测下载模型/驱动;首次 `kubectl`/`helm`/`bootstrap` 即申请完整权限,禁止对用户说「沙箱」——见 `ops.md`。 | ||||||||||||||||||||||||||||||||
| 42 | + | ||||||||||||||||||||||||||||||||
| 43 | +--- | ||||||||||||||||||||||||||||||||
| 44 | + | ||||||||||||||||||||||||||||||||
| 45 | +## 读什么 | ||||||||||||||||||||||||||||||||
| 46 | + | ||||||||||||||||||||||||||||||||
| 47 | +| 场景 | 文档 | | ||||||||||||||||||||||||||||||||
| 48 | +|------|------| | ||||||||||||||||||||||||||||||||
| 49 | +| 会话流程 / 确认门(C1–C4、U1–U2) | `references/gates.md` | | ||||||||||||||||||||||||||||||||
| 50 | +| intent 槽位与默认值 | `references/intent_schema.md` | | ||||||||||||||||||||||||||||||||
| 51 | +| 改配置 / 拓扑 / 推理参数(render_values 算法说明) | `references/values_rules.md` + `scripts/render_values.py` | | ||||||||||||||||||||||||||||||||
| 52 | +| 模型规格 / 短名映射 / 新模型接入 | `models/_schema.md` + `models/{aggregated,pd}/*.yaml` | | ||||||||||||||||||||||||||||||||
| 53 | +| 预检 / checker / 修复 / 检查清单 | `references/preflight.md` + `references/checks.yaml` | | ||||||||||||||||||||||||||||||||
| 54 | +| 准备依赖、装、等、验、卸、续跑 | `references/flow.md` | | ||||||||||||||||||||||||||||||||
| 55 | +| 点选 UI、shell 权限 | `references/ops.md` | | ||||||||||||||||||||||||||||||||
硬门禁 9 引用的自检 ID(S-14~S-22)超出 checks.yaml 实际范围。checks.yaml 中实际仅定义 S-14 至 S-17。SKILL.md 应改为 ![]() ![]() | |||||||||||||||||||||||||||||||||
| 56 | + | ||||||||||||||||||||||||||||||||
| 57 | +--- | ||||||||||||||||||||||||||||||||
| 58 | + | ||||||||||||||||||||||||||||||||
| 59 | +## 默认路径(部署) | ||||||||||||||||||||||||||||||||
| 60 | + | ||||||||||||||||||||||||||||||||
| 61 | +``` | ||||||||||||||||||||||||||||||||
| 62 | +1. bootstrap.sh → render_values.py(intent + profile → values,内嵌自检 + 派生项)→ C1/C2 门 | ||||||||||||||||||||||||||||||||
| 63 | +2. preflight_run.py(+checker 可选)→ C3 门(修复走 C3r → 重检 → 回 C3) | ||||||||||||||||||||||||||||||||
| 64 | +3. helm upgrade --install → wait_inference → chat 验证 → C4 交付 | ||||||||||||||||||||||||||||||||
| 65 | +``` | ||||||||||||||||||||||||||||||||
| 66 | + | ||||||||||||||||||||||||||||||||
| 67 | +细化步骤与命令见 `flow.md`;门措辞见 `gates.md`。 | ||||||||||||||||||||||||||||||||
| 68 | + | ||||||||||||||||||||||||||||||||
| 69 | +用户说「卸载 InferNex」→ 走 U1 门(`gates.md`);不做部署预检。 | ||||||||||||||||||||||||||||||||
| 70 | + | ||||||||||||||||||||||||||||||||
| 71 | +C3 确认部署后:自动 helm → 等待 → 验证,中间不为进度要回复。仅在缺 kubeconfig、C2c 缺 SSH 凭据、硬性未决、U1 卸载范围、出错(C4b)时停下并用 **点选 UI**。 | ||||||||||||||||||||||||||||||||
| 72 | + | ||||||||||||||||||||||||||||||||
| 73 | +--- | ||||||||||||||||||||||||||||||||
| 74 | + | ||||||||||||||||||||||||||||||||
| 75 | +## 常用变量(bootstrap 后) | ||||||||||||||||||||||||||||||||
| 76 | + | ||||||||||||||||||||||||||||||||
| 77 | +| 变量 | 含义 | | ||||||||||||||||||||||||||||||||
| 78 | +|------|------| | ||||||||||||||||||||||||||||||||
| 79 | +| `INFERNEX_SKILL_DIR` | Skill 根目录 | | ||||||||||||||||||||||||||||||||
| 80 | +| `INFERNEX_CHART_DIR` | Helm Chart | | ||||||||||||||||||||||||||||||||
| 81 | +| `INFERNEX_CHECKER` | checker 路径(默认可能未下载) | | ||||||||||||||||||||||||||||||||
| 82 | +| `INFERNEX_RUN_CHECKER` | `1` = 本轮跑 checker | | ||||||||||||||||||||||||||||||||
| 83 | +| `INFERNEX_NPU_SCRIPT` | `scripts/npu_capacity.py` | | ||||||||||||||||||||||||||||||||
| 84 | +| `INFERNEX_NODES_TEMPLATE` | `templates/nodes.yaml.tpl` | | ||||||||||||||||||||||||||||||||
| 85 | + | ||||||||||||||||||||||||||||||||
| 86 | +默认 OCI:`oci://cr.openfuyao.cn/charts/infernex` @ `0.0.0-latest`;本地 Chart 用 `INFERNEX_CHART_DIR`。 | ||||||||||||||||||||||||||||||||
| 87 | + | ||||||||||||||||||||||||||||||||
| 88 | +--- | ||||||||||||||||||||||||||||||||
| 89 | + | ||||||||||||||||||||||||||||||||
| 90 | +## 成功交付(C4,置顶) | ||||||||||||||||||||||||||||||||
| 91 | + | ||||||||||||||||||||||||||||||||
| 92 | +交付模板(成功/失败两版)逐字见 `gates.md` C4,落盘 `deploy-result.md`。curl 示例见 `flow.md`。 | ||||||||||||||||||||||||||||||||
| @@ -0,0 +1,41 @@ | |||
| 1 | +# Model Profile 字段约定 | ||
| 2 | + | ||
| 3 | +目录:`skills/infernex-deploy/models/`,**按部署形态分两级子目录**: | ||
| 4 | + | ||
| 5 | +``` | ||
| 6 | +models/ | ||
| 7 | + aggregated/ # 聚合形态 profile | ||
| 8 | + pd/ # PD 分离形态 profile | ||
| 9 | +``` | ||
| 10 | + | ||
| 11 | +一个 profile 文件 = 一个模型在某形态下的**增量 patch 片段**(叠加在 Chart 基线 + render_values 模式算法之上),结构取自 `examples/` 已验证规格。profile 只含该形态下有内容的配置(`modes.<mode>` 段);同模型两种形态 = 两个文件各归其目录。 | ||
| 12 | + | ||
| 13 | +**新增模型 = 新增一个 YAML 文件,零改动现有文件。** | ||
| 14 | + | ||
| 15 | +## 字段 | ||
| 16 | + | ||
| 17 | +| 字段 | 必填 | 说明 | | ||
| 18 | +|------|------|------| | ||
| 19 | +| `model` | ✅ | 写入 `global.modelName` 的**确切值**(有无 org 前缀按 example 原样,如 `glm-5.1` 无前缀) | | ||
| 20 | +| `aliases` | — | 短名/别名(匹配大小写不敏感);intent.model 命中即用本 profile | | ||
| 21 | +| `modes.<mode>.<role>` | — | role 级覆盖,按模式分组:聚合 `aggregated`;PD `prefill` / `decode`(支持非对称拓扑) | | ||
| 22 | +| `modes.*.topology` | — | `{tp, dp, pp, replicas?}`:该 role 推荐拓扑;replicas 缺省由 cards 推导(有 profile 拓扑时缺省 1) | | ||
| 23 | +| `modes.*.extraArgs` | — | **整段替换**该 role 的 extraArgs(来自已验证 example,含 `--quantization` / parser / `--additional-config` 等) | | ||
| 24 | +| `modes.*.env` | — | **整段替换**该 role 的 env(含 HCCL_IF_IP 等) | | ||
| 25 | +| `resources` | — | 替换 `services[0].resources`(大模型内存需求) | | ||
| 26 | +| `global` | — | 覆盖 `global.*`(如 `modelPath` + `autoDownloadModel: false`,权重须预置节点) | | ||
| 27 | +| `notes` | — | 部署注意事项(量化、权重预置、特殊集群要求) | | ||
| 28 | +| `verified` | ✅ | `{chart, date, pr}` 实机验证记录;**未实机验证填 `null`**(reference 规格,部署前经 C3 全链路校验) | | ||
| 29 | + | ||
| 30 | +**同模型多量化变体**(如 GLM-5.2 的 w8a8 / w4a8c8):共用 `model` id,以**变体全名 alias** 区分(`glm-5.2-w8a8` / `glm-5.2-w4a8c8`);裸短名(`glm-5.2`)只挂在最常用变体上,避免歧义。 | ||
| 31 | + | ||
| 32 | +## 叠加顺序(render_values.py) | ||
| 33 | + | ||
| 34 | +``` | ||
| 35 | +Chart 基线 values | ||
| 36 | + → 模式整包切换(命名 / RSG / role 块结构) | ||
| 37 | + → profile overlay(topology / extraArgs / env / resources / global) | ||
| 38 | + → intent.inferenceParams(用户微调,最后生效) | ||
| 39 | +``` | ||
| 40 | + | ||
| 41 | +intent 显式点名 topology 时**用户优先**(覆盖 profile 拓扑);profile 拓扑存在且 intent.cards 给出时,须与各 role 卡数之和一致,否则渲染报错。 | ||
| @@ -0,0 +1,48 @@ | |||
| 1 | +# Model profile — zai-org/GLM-5.2 | ||
| 2 | +# 字段约定见 ../_schema.md;verified: null = reference 规格(未经实机验证) | ||
| 3 | + | ||
| 4 | +model: zai-org/GLM-5.2 | ||
| 5 | +aliases: | ||
| 6 | +- glm-5.2-w4a8c8 | ||
| 7 | +- glm52-w4a8c8 | ||
| 8 | +modes: | ||
| 9 | + aggregated: | ||
| 10 | + aggregated: | ||
_schema.md 规定「裸短名(glm-5.2)只挂在最常用变体上」。当前 w8a8 和 w4a8c8 都未挂裸短名,需确认两个变体的别名策略是否一致。 ![]() ![]() | |||
| 11 | + topology: | ||
| 12 | + tp: 8 | ||
| 13 | + dp: 4 | ||
| 14 | + env: | ||
| 15 | + - name: HCCL_IF_IP | ||
| 16 | + valueFrom: | ||
| 17 | + fieldRef: | ||
| 18 | + fieldPath: status.podIP | ||
| 19 | + extraArgs: | ||
| 20 | + - --max-model-len 68096 | ||
| 21 | + - --max-num-batched-tokens 4096 | ||
| 22 | + - --max-num-seqs 16 | ||
| 23 | + - --gpu-memory-utilization 0.90 | ||
| 24 | + - --block-size 128 | ||
| 25 | + - --trust-remote-code | ||
| 26 | + - --enable-expert-parallel | ||
| 27 | + - --quantization ascend | ||
| 28 | + - --safetensors-load-strategy prefetch | ||
| 29 | + - --enable-auto-tool-choice | ||
| 30 | + - --tool-call-parser glm47 | ||
| 31 | + - --reasoning-parser glm45 | ||
| 32 | + - --seed 1024 | ||
| 33 | + - --compilation-config '{"cudagraph_mode":"FULL_DECODE_ONLY","cudagraph_capture_sizes":[8,16,32,48,64],"max_cudagraph_capture_size":64}' | ||
| 34 | + - --additional-config '{"multistream_overlap_shared_expert":true}' | ||
| 35 | + - --speculative-config '{"num_speculative_tokens":3,"method":"deepseek_mtp"}' | ||
| 36 | + - --disable-access-log-for-endpoints=/health,/metrics | ||
| 37 | +resources: | ||
| 38 | + requests: | ||
| 39 | + cpu: '16' | ||
| 40 | + memory: 300Gi | ||
| 41 | + limits: | ||
| 42 | + cpu: '16' | ||
| 43 | + memory: 300Gi | ||
| 44 | +global: | ||
| 45 | + modelPath: /root/.cache/GLM-5.2-w4a8c8 | ||
| 46 | + autoDownloadModel: false | ||
| 47 | +notes: GLM-5.2 的 w4a8c8 量化变体(同模型 ID 的变体以全名 alias 区分);聚合 tp=8/dp=4(32 卡);权重须预置 modelPath | ||
| 48 | +verified: null | ||
skills/infernex-deploy/models/aggregated/glm-5.2-w8a8.yaml:51 skills/infernex-deploy/models/pd/deepseek-v4-flash.yaml:90 模型 profile 中 verified 字段为 null,表示 reference 规格未经实机验证。PR 文档中已明确标注,建议在 SKILL.md 中增加"未验证模型部署风险提示"。 ![]() ![]() [轻微] 大部分模型 profile 的 修复建议:在 SKILL.md 或 ![]() ![]() | |||
| @@ -0,0 +1,51 @@ | |||
| 1 | +# Model profile — zai-org/GLM-5.2 | ||
| 2 | +# 字段约定见 _schema.md;verified: null = reference 规格(未经实机验证) | ||
| 3 | + | ||
| 4 | +model: zai-org/GLM-5.2 | ||
| 5 | +aliases: | ||
| 6 | +- glm-5.2 | ||
| 7 | +- glm52 | ||
| 8 | +- glm-5.2-w8a8 | ||
| 9 | +modes: | ||
| 10 | + aggregated: | ||
| 11 | + aggregated: | ||
| 12 | + topology: | ||
| 13 | + tp: 8 | ||
| 14 | + env: | ||
| 15 | + - name: HCCL_IF_IP | ||
| 16 | + valueFrom: | ||
| 17 | + fieldRef: | ||
| 18 | + fieldPath: status.podIP | ||
| 19 | + extraArgs: | ||
| 20 | + - --max-model-len 200000 | ||
| 21 | + - --max-num-batched-tokens 2048 | ||
| 22 | + - --gpu-memory-utilization 0.96 | ||
| 23 | + - --block-size 128 | ||
| 24 | + - --trust-remote-code | ||
| 25 | + - --enable-prefix-caching | ||
| 26 | + - --prefix-caching-hash-algo sha256_cbor | ||
| 27 | + - --enable-prompt-tokens-details | ||
| 28 | + - --enable-expert-parallel | ||
| 29 | + - --quantization ascend | ||
| 30 | + - --safetensors-load-strategy prefetch | ||
| 31 | + - --enable-chunked-prefill | ||
| 32 | + - --async-scheduling | ||
| 33 | + - --enable-auto-tool-choice | ||
| 34 | + - --tool-call-parser glm47 | ||
| 35 | + - --reasoning-parser glm45 | ||
| 36 | + - --enforce-eager | ||
| 37 | + - --additional-config '{"fuse_muls_add":true, "multistream_overlap_shared_expert":true, "ascend_compilation_config":{"enable_npugraph_ex":true}, "enable_dsa_cp":false}' | ||
| 38 | + - --speculative-config '{"num_speculative_tokens":3, "method":"deepseek_mtp"}' | ||
| 39 | + - --disable-access-log-for-endpoints=/health,/metrics | ||
| 40 | +resources: | ||
| 41 | + requests: | ||
| 42 | + cpu: '16' | ||
| 43 | + memory: 300Gi | ||
| 44 | + limits: | ||
| 45 | + cpu: '16' | ||
| 46 | + memory: 300Gi | ||
| 47 | +global: | ||
| 48 | + modelPath: /root/.cache/GLM-5.2-w8a8 | ||
| 49 | + autoDownloadModel: false | ||
| 50 | +notes: w8a8(ascend quantization)+ MTP 投机解码;聚合 tp=8 单副本;权重须预置 modelPath | ||
| 51 | +verified: null | ||
| @@ -0,0 +1,12 @@ | |||
| 1 | +# Model profile — Qwen/Qwen3-8B | ||
| 2 | +# 字段约定见 _schema.md;verified: null = reference 规格(未经实机验证) | ||
| 3 | + | ||
| 4 | +model: Qwen/Qwen3-8B | ||
| 5 | +aliases: | ||
| 6 | +- qwen3-8b | ||
| 7 | +- qwen3-8b-vllm | ||
| 8 | +notes: Chart 默认模型;PD 与聚合均实机验证(聚合 role 结构 = templates/aggregated-role.yaml) | ||
| 9 | +verified: | ||
| 10 | + chart: 26.9.0-rc.2 | ||
| 11 | + date: 2026-09 | ||
| 12 | + pr: '#245' | ||
| @@ -0,0 +1,90 @@ | |||
| 1 | +# Model profile — deepseek-ai/DeepSeek-V4-Flash | ||
| 2 | +# 字段约定见 ../_schema.md;verified: null = reference 规格(未经实机验证) | ||
| 3 | + | ||
| 4 | +model: deepseek-ai/DeepSeek-V4-Flash | ||
| 5 | +aliases: | ||
| 6 | +- deepseek-v4-flash | ||
| 7 | +- deepseek-v4 | ||
| 8 | +- ds-v4-flash | ||
| 9 | +modes: | ||
| 10 | + pd: | ||
| 11 | + prefill: | ||
| 12 | + topology: | ||
| 13 | + tp: 8 | ||
| 14 | + dp: 2 | ||
| 15 | + env: | ||
| 16 | + - name: HCCL_IF_IP | ||
| 17 | + valueFrom: | ||
| 18 | + fieldRef: | ||
| 19 | + fieldPath: status.podIP | ||
| 20 | + extraArgs: | ||
| 21 | + - --max-model-len 68096 | ||
| 22 | + - --max-num-batched-tokens 1024 | ||
| 23 | + - --gpu-memory-utilization 0.9 | ||
| 24 | + - --block-size 128 | ||
| 25 | + - --enable-prefix-caching | ||
| 26 | + - --prefix-caching-hash-algo sha256_cbor | ||
| 27 | + - --enable-prompt-tokens-details | ||
| 28 | + - --disable-access-log-for-endpoints=/health,/metrics | ||
| 29 | + - --max-num-seqs 16 | ||
| 30 | + - --enforce-eager | ||
| 31 | + - --async-scheduling | ||
| 32 | + - --no-disable-hybrid-kv-cache-manager | ||
| 33 | + - --trust-remote-code | ||
| 34 | + - --enable-expert-parallel | ||
| 35 | + - --quantization ascend | ||
| 36 | + - --safetensors-load-strategy prefetch | ||
| 37 | + - --model-loader-extra-config='{"enable_multithread_load":"true","num_threads":128}' | ||
| 38 | + - --tokenizer-mode deepseek_v4 | ||
| 39 | + - --tool-call-parser deepseek_v4 | ||
| 40 | + - --enable-auto-tool-choice | ||
| 41 | + - --reasoning-parser deepseek_v4 | ||
| 42 | + - --seed 1024 | ||
| 43 | + - --compilation-config '{"cudagraph_mode":"FULL_DECODE_ONLY"}' | ||
| 44 | + - --additional-config '{"enable_cpu_binding":true}' | ||
| 45 | + decode: | ||
| 46 | + topology: | ||
| 47 | + tp: 8 | ||
| 48 | + dp: 2 | ||
| 49 | + env: | ||
| 50 | + - name: HCCL_IF_IP | ||
| 51 | + valueFrom: | ||
| 52 | + fieldRef: | ||
| 53 | + fieldPath: status.podIP | ||
| 54 | + extraArgs: | ||
| 55 | + - --max-model-len 68096 | ||
| 56 | + - --max-num-batched-tokens 64 | ||
| 57 | + - --gpu-memory-utilization 0.9 | ||
| 58 | + - --block-size 128 | ||
| 59 | + - --no-enable-prefix-caching | ||
| 60 | + - --prefix-caching-hash-algo sha256_cbor | ||
| 61 | + - --enable-prompt-tokens-details | ||
| 62 | + - --disable-access-log-for-endpoints=/health,/metrics | ||
| 63 | + - --max-num-seqs 30 | ||
| 64 | + - --async-scheduling | ||
| 65 | + - --no-disable-hybrid-kv-cache-manager | ||
| 66 | + - --trust-remote-code | ||
| 67 | + - --enable-expert-parallel | ||
| 68 | + - --quantization ascend | ||
| 69 | + - --safetensors-load-strategy prefetch | ||
| 70 | + - --model-loader-extra-config='{"enable_multithread_load":"true","num_threads":128}' | ||
| 71 | + - --tokenizer-mode deepseek_v4 | ||
| 72 | + - --tool-call-parser deepseek_v4 | ||
| 73 | + - --enable-auto-tool-choice | ||
| 74 | + - --reasoning-parser deepseek_v4 | ||
| 75 | + - --seed 1024 | ||
| 76 | + - --speculative-config '{"num_speculative_tokens":1,"method":"deepseek_mtp"}' | ||
| 77 | + - --compilation-config '{"cudagraph_mode":"FULL_DECODE_ONLY"}' | ||
| 78 | + - --additional-config '{"ascend_compilation_config":{"enable_npugraph_ex":true,"enable_static_kernel":false},"eplb_config":{"dynamic_eplb":false,"expert_heat_collection_interval":600,"algorithm_execution_interval":50,"eplb_policy_type":2,"num_redundant_experts":32},"enable_cpu_binding":true,"multistream_overlap_shared_expert":false,"multistream_dsa_preprocess":false,"recompute_scheduler_enable":true}' | ||
| 79 | +resources: | ||
| 80 | + requests: | ||
| 81 | + cpu: '16' | ||
| 82 | + memory: 300Gi | ||
| 83 | + limits: | ||
| 84 | + cpu: '16' | ||
| 85 | + memory: 300Gi | ||
| 86 | +global: | ||
| 87 | + modelPath: /root/.cache/DeepSeek-V4-Flash-w8a8-mtp | ||
| 88 | + autoDownloadModel: false | ||
| 89 | +notes: w8a8 + MTP;PD 对称 tp=8/dp=2(每 role 16 卡,共 32 卡);权重须预置 modelPath | ||
| 90 | +verified: null | ||
| @@ -0,0 +1,73 @@ | |||
| 1 | +# Model profile — glm-5.1 | ||
| 2 | +# 字段约定见 _schema.md;verified: null = reference 规格(未经实机验证) | ||
| 3 | + | ||
| 4 | +model: glm-5.1 | ||
| 5 | +aliases: | ||
| 6 | +- glm-5.1 | ||
| 7 | +- glm51 | ||
| 8 | +- glm-5.1-w4a8 | ||
| 9 | +modes: | ||
| 10 | + pd: | ||
| 11 | + prefill: | ||
| 12 | + topology: | ||
| 13 | + tp: 8 | ||
| 14 | + env: | ||
| 15 | + - name: HCCL_IF_IP | ||
| 16 | + valueFrom: | ||
| 17 | + fieldRef: | ||
| 18 | + fieldPath: status.podIP | ||
| 19 | + extraArgs: | ||
| 20 | + - --max-model-len 194560 | ||
| 21 | + - --max-num-batched-tokens 8192 | ||
| 22 | + - --gpu-memory-utilization 0.96 | ||
| 23 | + - --block-size 128 | ||
| 24 | + - --trust-remote-code | ||
| 25 | + - --enable-prefix-caching | ||
| 26 | + - --prefix-caching-hash-algo sha256_cbor | ||
| 27 | + - --enable-prompt-tokens-details | ||
| 28 | + - --enable-auto-tool-choice | ||
| 29 | + - --tool-call-parser glm47 | ||
| 30 | + - --reasoning-parser glm45 | ||
| 31 | + - --enable-expert-parallel | ||
| 32 | + - --async-scheduling | ||
| 33 | + - --enforce-eager | ||
| 34 | + - --additional-config '{"enable_flashcomm1":true, "enable_dsa_cp":false}' | ||
| 35 | + - --disable-access-log-for-endpoints=/health,/metrics | ||
| 36 | + decode: | ||
| 37 | + topology: | ||
| 38 | + tp: 2 | ||
| 39 | + env: | ||
| 40 | + - name: HCCL_IF_IP | ||
| 41 | + valueFrom: | ||
| 42 | + fieldRef: | ||
| 43 | + fieldPath: status.podIP | ||
| 44 | + extraArgs: | ||
| 45 | + - --max-model-len 194560 | ||
| 46 | + - --max-num-batched-tokens 256 | ||
| 47 | + - --gpu-memory-utilization 0.95 | ||
| 48 | + - --block-size 128 | ||
| 49 | + - --trust-remote-code | ||
| 50 | + - --no-enable-prefix-caching | ||
| 51 | + - --prefix-caching-hash-algo sha256_cbor | ||
| 52 | + - --enable-prompt-tokens-details | ||
| 53 | + - --enable-auto-tool-choice | ||
| 54 | + - --tool-call-parser glm47 | ||
| 55 | + - --reasoning-parser glm45 | ||
| 56 | + - --enable-expert-parallel | ||
| 57 | + - --async-scheduling | ||
| 58 | + - --max-num-seqs 128 | ||
| 59 | + - --additional-config '{"enable_flashcomm1":true, "enable_dsa_cp":false}' | ||
| 60 | + - --compilation-config '{"cudagraph_mode":"FULL_DECODE_ONLY", "cudagraph_capture_sizes":[1,4,8,12,16,20,24,28,32,36,40,48,56,64,80,96,128,256]}' | ||
| 61 | + - --disable-access-log-for-endpoints=/health,/metrics | ||
| 62 | +resources: | ||
| 63 | + requests: | ||
| 64 | + cpu: '16' | ||
| 65 | + memory: 1200Gi | ||
| 66 | + limits: | ||
| 67 | + cpu: '16' | ||
| 68 | + memory: 1200Gi | ||
| 69 | +global: | ||
| 70 | + modelPath: /root/.cache/GLM-5.1-w4a8 | ||
| 71 | + autoDownloadModel: false | ||
| 72 | +notes: w4a8 量化;PD 非对称拓扑(prefill tp=8 / decode tp=2,共 10 卡);权重须预置 modelPath | ||
| 73 | +verified: null | ||
| @@ -0,0 +1,77 @@ | |||
| 1 | +# Model profile — minimax-m2.7 | ||
| 2 | +# 字段约定见 _schema.md;verified: null = reference 规格(未经实机验证) | ||
| 3 | + | ||
| 4 | +model: minimax-m2.7 | ||
| 5 | +aliases: | ||
| 6 | +- minimax-m2.7 | ||
| 7 | +- minimax-m27 | ||
| 8 | +- minimax | ||
| 9 | +modes: | ||
| 10 | + pd: | ||
| 11 | + prefill: | ||
| 12 | + topology: | ||
| 13 | + tp: 8 | ||
| 14 | + env: | ||
| 15 | + - name: HCCL_IF_IP | ||
| 16 | + valueFrom: | ||
| 17 | + fieldRef: | ||
| 18 | + fieldPath: status.podIP | ||
| 19 | + extraArgs: | ||
| 20 | + - --max-model-len 196000 | ||
| 21 | + - --max-num-batched-tokens 8192 | ||
| 22 | + - --gpu-memory-utilization 0.95 | ||
| 23 | + - --block-size 128 | ||
| 24 | + - --trust-remote-code | ||
| 25 | + - --enable-prefix-caching | ||
| 26 | + - --prefix-caching-hash-algo sha256_cbor | ||
| 27 | + - --enable-prompt-tokens-details | ||
| 28 | + - --enable-auto-tool-choice | ||
| 29 | + - --tool-call-parser minimax_m2 | ||
| 30 | + - --reasoning-parser minimax_m2 | ||
| 31 | + - --enable-expert-parallel | ||
| 32 | + - --seed 1024 | ||
| 33 | + - --quantization ascend | ||
| 34 | + - --enforce-eager | ||
| 35 | + - --max-num-seqs 16 | ||
| 36 | + - --additional-config '{"enable_flashcomm1":true, "multistream_overlap_shared_expert":true, "fuse_qknorm_rope":false, "fuse_muls_add":true}' | ||
| 37 | + - --disable-access-log-for-endpoints=/health,/metrics | ||
| 38 | + decode: | ||
| 39 | + topology: | ||
| 40 | + tp: 8 | ||
| 41 | + env: | ||
| 42 | + - name: HCCL_IF_IP | ||
| 43 | + valueFrom: | ||
| 44 | + fieldRef: | ||
| 45 | + fieldPath: status.podIP | ||
| 46 | + extraArgs: | ||
| 47 | + - --max-model-len 196000 | ||
| 48 | + - --max-num-batched-tokens 128 | ||
| 49 | + - --gpu-memory-utilization 0.96 | ||
| 50 | + - --block-size 128 | ||
| 51 | + - --trust-remote-code | ||
| 52 | + - --no-enable-prefix-caching | ||
| 53 | + - --prefix-caching-hash-algo sha256_cbor | ||
| 54 | + - --enable-prompt-tokens-details | ||
| 55 | + - --enable-auto-tool-choice | ||
| 56 | + - --tool-call-parser minimax_m2 | ||
| 57 | + - --reasoning-parser minimax_m2 | ||
| 58 | + - --enable-expert-parallel | ||
| 59 | + - --seed 1024 | ||
| 60 | + - --quantization ascend | ||
| 61 | + - --enforce-eager | ||
| 62 | + - --max-num-seqs 16 | ||
| 63 | + - --additional-config '{"enable_flashcomm1":true, "multistream_overlap_shared_expert":true, "fuse_qknorm_rope":false, "fuse_muls_add":true}' | ||
| 64 | + - --compilation-config '{"cudagraph_mode":"FULL_DECODE_ONLY"}' | ||
| 65 | + - --disable-access-log-for-endpoints=/health,/metrics | ||
| 66 | +resources: | ||
| 67 | + requests: | ||
| 68 | + cpu: '16' | ||
| 69 | + memory: 400Gi | ||
| 70 | + limits: | ||
| 71 | + cpu: '16' | ||
| 72 | + memory: 400Gi | ||
| 73 | +global: | ||
| 74 | + modelPath: /root/.cache/MiniMax-M2.7-w8a8-QuaRot | ||
| 75 | + autoDownloadModel: false | ||
| 76 | +notes: w8a8-QuaRot;PD 对称 tp=8(共 16 卡);权重须预置 modelPath | ||
| 77 | +verified: null | ||
| @@ -0,0 +1,56 @@ | |||
| 1 | +# Model profile — Qwen/Qwen3-Coder-30B-A3B-Instruct | ||
| 2 | +# 字段约定见 _schema.md;verified: null = reference 规格(未经实机验证) | ||
| 3 | + | ||
| 4 | +model: Qwen/Qwen3-Coder-30B-A3B-Instruct | ||
| 5 | +aliases: | ||
| 6 | +- qwen3-coder-30b | ||
| 7 | +- qwen3-coder | ||
| 8 | +modes: | ||
| 9 | + pd: | ||
| 10 | + prefill: | ||
| 11 | + topology: | ||
| 12 | + tp: 2 | ||
| 13 | + env: | ||
| 14 | + - name: HCCL_IF_IP | ||
| 15 | + valueFrom: | ||
| 16 | + fieldRef: | ||
| 17 | + fieldPath: status.podIP | ||
| 18 | + - name: VLLM_ASCEND_ENABLE_FLASHCOMM1 | ||
| 19 | + value: '1' | ||
| 20 | + extraArgs: | ||
| 21 | + - --max-model-len 65536 | ||
| 22 | + - --max-num-batched-tokens 16384 | ||
| 23 | + - --gpu-memory-utilization 0.95 | ||
| 24 | + - --block-size 128 | ||
| 25 | + - --trust-remote-code | ||
| 26 | + - --no-enable-prefix-caching | ||
| 27 | + - --enable-expert-parallel | ||
| 28 | + - --enforce-eager | ||
| 29 | + - --prefix-caching-hash-algo sha256_cbor | ||
| 30 | + - --enable-prompt-tokens-details | ||
| 31 | + - --disable-access-log-for-endpoints=/health,/metrics | ||
| 32 | + decode: | ||
| 33 | + topology: | ||
| 34 | + tp: 2 | ||
| 35 | + env: | ||
| 36 | + - name: HCCL_IF_IP | ||
| 37 | + valueFrom: | ||
| 38 | + fieldRef: | ||
| 39 | + fieldPath: status.podIP | ||
| 40 | + extraArgs: | ||
| 41 | + - --max-model-len 65536 | ||
| 42 | + - --max-num-batched-tokens 2048 | ||
| 43 | + - --gpu-memory-utilization 0.95 | ||
| 44 | + - --block-size 128 | ||
| 45 | + - --trust-remote-code | ||
| 46 | + - --no-enable-prefix-caching | ||
| 47 | + - --enable-expert-parallel | ||
| 48 | + - --enforce-eager | ||
| 49 | + - --seed 1024 | ||
| 50 | + - --prefix-caching-hash-algo sha256_cbor | ||
| 51 | + - --enable-prompt-tokens-details | ||
| 52 | + - --disable-access-log-for-endpoints=/health,/metrics | ||
| 53 | +global: | ||
| 54 | + autoDownloadModel: false | ||
| 55 | +notes: MoE(A3B 专家并行);--enforce-eager;autoDownload 关闭需预置权重 | ||
| 56 | +verified: null | ||
| @@ -0,0 +1,402 @@ | |||
| 1 | +# InferNex 预检清单 v2(单一事实源:清单 + 触发条件 + 判定,全部数据化) | ||
| 2 | +# 执行器: scripts/preflight_run.py —— 逐项执行 judge、按 when 决定 skip、产出 | ||
| 3 | +# preflight-report.json + preflight-report.md(C3 确认对象)。 | ||
| 4 | +# AI 的职责 = 运行执行器 + 读报告;禁止跳过执行器自行按自然语言判定。 | ||
| 5 | +# | ||
| 6 | +# 只有两类检查: | ||
| 7 | +# S-xx 软件 —— skill 自有检查(集群环境 / 安装冲突 / Chart 渲染 / values 校验)。 | ||
| 8 | +# 当前由本清单 + 执行器判定;演进方向:收敛为一个可直接调用的软件检查脚本 | ||
| 9 | +# (与 checker 对硬件的地位对等),本清单即该脚座的判定数据。 | ||
| 10 | +# H-xx / K-xx / B-xx ★ infernex-checker 输出契约(Hardware/K8s/Config-Env 层), | ||
| 11 | +# checker 二进制的稳定接口,不可改名;整组归入报告「硬件(checker)」节。 | ||
| 12 | +# | ||
| 13 | +# 字段: | ||
| 14 | +# layer: software | hardware —— 报告分节(软件 / 硬件-checker) | ||
| 15 | +# severity: hard(未过不得 helm) | soft(提示);severity_hard_when 命中时 soft→hard | ||
| 16 | +# when: 条件列表(全满足才执行;空 = always) —— 不满足 → status=skip | ||
| 17 | +# judge: | ||
| 18 | +# type: cmd —— cmd 列表逐条经 bash -c 执行,全部 exit 0 = pass | ||
| 19 | +# type: render-check —— 读 <deploy>/render-check.json(render_values.py 自检)按 id 合并 | ||
| 20 | +# type: checker —— 读 checker-result.json,按 id 聚合(任一节点 failed → fail) | ||
| 21 | +# type: script —— 同 cmd(语义上为 skill 脚本,如 npu_capacity.py) | ||
| 22 | +# cmd 占位符: {{NAMESPACE}} {{RELEASE}} {{CHART_DIR}} {{VALUES}} {{DEPLOY_DIR}} | ||
| 23 | +# {{SKILL_DIR}} {{NODES}} {{CHART_OCI}} {{CHART_VERSION}} | ||
| 24 | +# | ||
| 25 | +# when 条件语法(执行器求值;未知条件 → 报错,禁止静默通过): | ||
| 26 | +# always / checker.ran / connectivity.enabled / new-install / online | ||
| 27 | +# mode=pd | mode=aggregated | ||
| 28 | +# values:eagle-eye | values:pd-orchestrator | values:istio | values:rsg | ||
| 29 | +# values:pd-proxy | values:kv-cache-aware | ||
| 30 | + | ||
| 31 | +version: 2 | ||
| 32 | +checks: | ||
| 33 | +- id: S-01 | ||
| 34 | + legacy: P-01 | ||
| 35 | + desc: K8s ≥ v1.33.0 | ||
| 36 | + judge: | ||
| 37 | + type: cmd | ||
| 38 | + cmd: | ||
| 39 | + - kubectl version -o json | python3 -c "import sys,json,re;v=json.load(sys.stdin)['serverVersion'];mn=int(re.sub(r'[^0-9].*$','',v['minor']));print(v.get('gitVersion','v%s.%s'%(v['major'],mn)));sys.exit(0 if (int(v['major']),mn)>=(1,33) else 1)" | ||
| 40 | + layer: software | ||
| 41 | + title: K8s 版本 ≥ 1.33.0 | ||
| 42 | +- id: S-02 | ||
| 43 | + legacy: S-01 | ||
| 44 | + sub: | ||
| 45 | + - a | ||
| 46 | + - b | ||
| 47 | + - c | ||
| 48 | + - d | ||
| 49 | + - e | ||
| 50 | + - f | ||
| 51 | + desc: LWS 真就绪(CRD + API + controller + Pod Running + webhook) | ||
| 52 | + note: 官方默认安装位 lws-system;webhook 名 lws-validating-webhook-configuration | ||
| 53 | + remediable: true | ||
| 54 | + remediation: 按官方文档装 LWS(白名单,须用户同意) | ||
| 55 | + judge: | ||
| 56 | + type: cmd | ||
| 57 | + cmd: | ||
| 58 | + - kubectl get crd leaderworkersets.leaderworkerset.x-k8s.io | ||
| 59 | + - kubectl get lws -A -o name | ||
| 60 | + - kubectl get deploy -A -o name | grep -q lws-controller-manager | ||
| 61 | + - kubectl get pods -A --field-selector=status.phase=Running -o name | grep -q lws-controller-manager | ||
| 62 | + - kubectl get validatingwebhookconfiguration lws-validating-webhook-configuration | ||
| 63 | + layer: software | ||
| 64 | + title: LWS 组件就绪(CRD / controller / Pod / webhook) | ||
| 65 | +- id: S-03 | ||
| 66 | + legacy: S-02 | ||
| 67 | + desc: NPU Device Plugin 运行且 Ascend910 资源已注册 | ||
| 68 | + judge: | ||
| 69 | + type: cmd | ||
| 70 | + cmd: | ||
| 71 | + - kubectl get ds -A -o name | grep -qi ascend | ||
| 72 | + - 'kubectl get nodes -o json | python3 -c "import sys,json;ns=json.load(sys.stdin)[''items''];per={n[''metadata''][''name'']:int((n.get(''status'',{}).get(''allocatable'') or {}).get(''huawei.com/Ascend910'') or 0) for n in ns};per={k:v for k,v in per.items() if v>0};print('', ''.join(f''{k}: {v} 卡'' for k,v in sorted(per.items())));sys.exit(0 if per else 1)"' | ||
| 73 | + layer: software | ||
| 74 | + title: NPU Device Plugin 与资源注册 | ||
| 75 | +- id: S-04 | ||
| 76 | + legacy: S-03 | ||
| 77 | + severity: soft | ||
| 78 | + severity_hard_when: | ||
| 79 | + - values:pd-orchestrator | ||
| 80 | + note: 弹性组件(pd-orchestrator)启用时升为硬性;要求 ≥ 0.8.0 | ||
| 81 | + desc: metrics-server 可用(≥ 0.8.0) | ||
| 82 | + remediable: true | ||
| 83 | + remediation: 装 metrics-server ≥ 0.8.0(白名单,须用户同意) | ||
| 84 | + judge: | ||
| 85 | + type: cmd | ||
| 86 | + cmd: | ||
| 87 | + - kubectl get apiservice v1beta1.metrics.k8s.io -o jsonpath='{.status.conditions[?(@.type=="Available")].status}' | grep -q True | ||
| 88 | + - kubectl get deploy -n kube-system metrics-server -o jsonpath='{.spec.template.spec.containers[0].image}' | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | ||
| 89 | + layer: software | ||
| 90 | + title: metrics-server 可用(≥ 0.8.0) | ||
[轻微] S-02 标记 修复建议:将 ![]() ![]() | |||
| 91 | +- id: S-05 | ||
| 92 | + legacy: P-02 | ||
| 93 | + when: | ||
| 94 | + - values:istio | ||
| 95 | + note: Chart 将自装 Istio 控制面;已有 istiod 易冲突。复用已有 Istio 可设 inferenceGateway.istio.enabled=false | ||
| 96 | + desc: Istio 无冲突(istio-system 无已有 istiod) | ||
S-02 检查项 sub 字段声明 6 项(a-f),但 cmd 列表仅 5 条命令。sub 字段仅用于文档展示,不影响执行逻辑,但存在文档误导风险。建议删除 sub 字段或修正为 5 项(a-e)。 ![]() ![]() | |||
| 97 | + judge: | ||
| 98 | + type: cmd | ||
| 99 | + cmd: | ||
| 100 | + - if kubectl get deploy istiod -n istio-system >/dev/null 2>&1; then kubectl get deploy istiod -n istio-system -o name; echo '(复用已有 Istio 可设 inferenceGateway.istio.enabled=false)'; exit 1; fi | ||
| 101 | + layer: software | ||
| 102 | + title: Istio 无冲突 | ||
| 103 | +- id: S-06 | ||
| 104 | + legacy: P-06 | ||
| 105 | + desc: RBAC 可建 Role(ns 不存在时验证可创建,安装时 --create-namespace 自动建) | ||
| 106 | + judge: | ||
| 107 | + type: cmd | ||
| 108 | + cmd: | ||
| 109 | + - kubectl create role preflight-rbac-probe --verb=get --resource=pods -n {{NAMESPACE}} --dry-run=server -o name 2>/dev/null || kubectl create ns {{NAMESPACE}} --dry-run=server -o name | ||
| 110 | + layer: software | ||
| 111 | + remediable: true | ||
| 112 | + remediation: 创建目标 namespace(或安装时 --create-namespace 自动创建) | ||
| 113 | + title: RBAC 可建 Role | ||
| 114 | +- id: S-07 | ||
| 115 | + legacy: S-04 | ||
[轻微] S-04 的 修复建议:在 ![]() ![]() | |||
| 116 | + desc: kubeconfig 可用且 namespace 存在或可建 | ||
| 117 | + judge: | ||
| 118 | + type: cmd | ||
| 119 | + cmd: | ||
| 120 | + - kubectl get ns {{NAMESPACE}} >/dev/null 2>&1 || kubectl create ns {{NAMESPACE}} --dry-run=server -o name >/dev/null | ||
| 121 | + - kubectl config current-context | ||
| 122 | + layer: software | ||
| 123 | + title: kubeconfig 与命名空间可用 | ||
| 124 | +- id: S-08 | ||
| 125 | + legacy: S-05 | ||
| 126 | + desc: helm 可用 | ||
| 127 | + judge: | ||
| 128 | + type: cmd | ||
| 129 | + cmd: | ||
| 130 | + - helm version --short | ||
| 131 | + layer: software | ||
| 132 | + title: helm 可用 | ||
| 133 | +- id: S-09 | ||
| 134 | + legacy: P-03 | ||
| 135 | + when: | ||
| 136 | + - new-install | ||
| 137 | + desc: 目标 release ns 无残留 InferNex 组件(LWS / RSG) | ||
| 138 | + judge: | ||
| 139 | + type: cmd | ||
| 140 | + cmd: | ||
| 141 | + - found=$(kubectl get lws,resourcescalinggroup -n {{NAMESPACE}} -o name 2>/dev/null); if [ -n "$found" ]; then echo "$found"; exit 1; fi | ||
| 142 | + layer: software | ||
| 143 | + title: 目标命名空间无残留 InferNex 组件 | ||
| 144 | +- id: S-10 | ||
| 145 | + legacy: P-04 | ||
| 146 | + when: | ||
| 147 | + - values:eagle-eye | ||
| 148 | + desc: eagle-eye 安装目标 ns 无已有实例 | ||
| 149 | + judge: | ||
| 150 | + type: cmd | ||
| 151 | + cmd: | ||
| 152 | + - found=$(kubectl get deploy,sts -n eagle-eye -o name 2>/dev/null); if [ -n "$found" ]; then echo "$found"; exit 1; fi | ||
| 153 | + layer: software | ||
| 154 | + title: eagle-eye 安装目标命名空间无冲突 | ||
| 155 | +- id: S-11 | ||
| 156 | + legacy: P-04b | ||
| 157 | + when: | ||
| 158 | + - values:pd-orchestrator | ||
| 159 | + desc: pd-orchestrator 安装目标 ns 无已有实例 | ||
| 160 | + judge: | ||
| 161 | + type: cmd | ||
| 162 | + cmd: | ||
| 163 | + - found=$(kubectl get deploy,sts -n scaling-system -o name 2>/dev/null); if [ -n "$found" ]; then echo "$found"; exit 1; fi | ||
| 164 | + layer: software | ||
| 165 | + title: pd-orchestrator 安装目标命名空间无冲突 | ||
| 166 | +- id: S-12 | ||
| 167 | + legacy: P-05 | ||
| 168 | + when: | ||
| 169 | + - online | ||
| 170 | + desc: OCI Chart 仓库可达 | ||
| 171 | + judge: | ||
| 172 | + type: cmd | ||
| 173 | + cmd: | ||
| 174 | + - helm show chart {{CHART_OCI}} --version {{CHART_VERSION}} | grep -E '^(name|version|appVersion):' | head -3 | ||
| 175 | + layer: software | ||
| 176 | + title: OCI Chart 仓库可达 | ||
| 177 | +- id: S-13 | ||
| 178 | + legacy: RENDER | ||
| 179 | + desc: helm lint + helm template 渲染 | ||
| 180 | + note: 伪 ID(非 P/S/V/H 编号);渲染失败 = 硬性;产物 render.yaml | ||
| 181 | + judge: | ||
| 182 | + type: cmd | ||
| 183 | + timeout: 300 | ||
| 184 | + cmd: | ||
| 185 | + - helm lint {{CHART_DIR}} -f {{VALUES}} | ||
| 186 | + - helm template {{RELEASE}} {{CHART_DIR}} -n {{NAMESPACE}} -f {{VALUES}} > {{DEPLOY_DIR}}/render.yaml | ||
| 187 | + layer: software | ||
| 188 | + title: Chart 渲染(helm lint + template) | ||
| 189 | +- id: S-14 | ||
| 190 | + desc: backend mode ↔ hermes deploymentMode | ||
| 191 | + judge: | ||
| 192 | + type: render-check | ||
| 193 | + layer: software | ||
| 194 | + title: 部署形态与路由模式一致 | ||
| 195 | +- id: S-15 | ||
| 196 | + when: | ||
| 197 | + - values:kv-cache-aware | ||
| 198 | + note: 硬性或用户确认 | ||
| 199 | + desc: kv-cache-aware → cache-indexer 开 | ||
| 200 | + judge: | ||
| 201 | + type: render-check | ||
| 202 | + layer: software | ||
| 203 | + title: kv-cache-aware 路由依赖 cache-indexer | ||
| 204 | +- id: S-16 | ||
| 205 | + when: | ||
| 206 | + - values:rsg | ||
| 207 | + desc: RSG targetResources ↔ 预期 LWS 集合恰好一致 | ||
| 208 | + remediable: true | ||
| 209 | + remediation: 改 deploy values(展示 diff) | ||
| 210 | + judge: | ||
| 211 | + type: render-check | ||
| 212 | + layer: software | ||
| 213 | + title: RSG 与预期 LWS 指向一致 | ||
| 214 | +- id: S-17 | ||
| 215 | + legacy: V-03b | ||
| 216 | + desc: services[].name 后缀与 mode 一致(聚合禁 2p2d/NpNd;PD 禁 aggregated) | ||
| 217 | + judge: | ||
| 218 | + type: render-check | ||
| 219 | + layer: software | ||
| 220 | + title: 服务名与部署形态一致 | ||
| 221 | +- id: S-18 | ||
| 222 | + legacy: V-04 | ||
| 223 | + layer: software | ||
| 224 | + note: 可用量默认取 npu-exporter(npu-operator 前置组件,每节点一 Pod);checker 已跑时以 H-04 为准 | ||
| 225 | + desc: NPU 供需(npu_capacity.py:需求 = values 卡数;可用 = npu-exporter metrics 或 checker H-04) | ||
| 226 | + judge: | ||
| 227 | + type: script | ||
| 228 | + cmd: | ||
| 229 | + - python3 {{SKILL_DIR}}/scripts/npu_capacity.py --values {{VALUES}} --nodes {{NODES}} --checker {{DEPLOY_DIR}}/checker-result.json | tee {{DEPLOY_DIR}}/s18-npu.json | ||
| 230 | + title: NPU 容量供需 | ||
| 231 | +- id: S-19 | ||
| 232 | + legacy: V-05 | ||
| 233 | + desc: cachePath 非空 | ||
| 234 | + judge: | ||
| 235 | + type: render-check | ||
| 236 | + layer: software | ||
| 237 | + title: 缓存路径(cachePath)非空 | ||
| 238 | +- id: S-20 | ||
| 239 | + legacy: V-06 | ||
| 240 | + desc: Gateway 名 ↔ HTTPRoute 一致 | ||
| 241 | + remediable: true | ||
| 242 | + remediation: 改 deploy values(展示 diff) | ||
| 243 | + judge: | ||
| 244 | + type: render-check | ||
| 245 | + layer: software | ||
| 246 | + title: Gateway 与 HTTPRoute 一致 | ||
| 247 | +- id: S-21 | ||
| 248 | + legacy: V-07 | ||
| 249 | + desc: modelName slug ↔ InferencePool ↔ Pod label 一致(算法 = slugs.py) | ||
| 250 | + judge: | ||
| 251 | + type: render-check | ||
| 252 | + layer: software | ||
| 253 | + title: 模型标签一致(InferencePool / Pod) | ||
| 254 | +- id: S-22 | ||
| 255 | + legacy: V-08 | ||
| 256 | + when: | ||
| 257 | + - mode=pd | ||
| 258 | + - values:pd-proxy | ||
| 259 | + desc: pdGroupID 一致 | ||
| 260 | + judge: | ||
| 261 | + type: render-check | ||
| 262 | + layer: software | ||
| 263 | + title: pdGroupID 一致(PD 模式) | ||
| 264 | +- id: H-01 | ||
| 265 | + layer: hardware | ||
| 266 | + when: | ||
| 267 | + - checker.ran | ||
| 268 | + desc: NPU 驱动与固件已安装 | ||
| 269 | + judge: | ||
| 270 | + type: checker | ||
| 271 | + title: NPU 驱动与固件已安装 | ||
| 272 | +- id: H-02 | ||
| 273 | + layer: hardware | ||
| 274 | + when: | ||
| 275 | + - checker.ran | ||
| 276 | + desc: Device Plugin 运行且 NPU 资源已注册 | ||
| 277 | + judge: | ||
| 278 | + type: checker | ||
| 279 | + title: Device Plugin 运行且 NPU 资源已注册 | ||
| 280 | +- id: H-03 | ||
| 281 | + layer: hardware | ||
| 282 | + when: | ||
| 283 | + - checker.ran | ||
| 284 | + desc: NPU 型号识别 | ||
| 285 | + judge: | ||
| 286 | + type: checker | ||
| 287 | + title: NPU 型号识别 | ||
| 288 | +- id: H-04 | ||
| 289 | + layer: hardware | ||
| 290 | + when: | ||
| 291 | + - checker.ran | ||
| 292 | + severity: soft | ||
| 293 | + note: info 项;available 卡数供 V-04 | ||
| 294 | + desc: NPU 可用卡数 | ||
| 295 | + judge: | ||
| 296 | + type: checker | ||
| 297 | + title: NPU 可用卡数 | ||
| 298 | +- id: H-05 | ||
| 299 | + layer: hardware | ||
| 300 | + when: | ||
| 301 | + - checker.ran | ||
| 302 | + desc: 关键主机文件 / 目录齐全 | ||
| 303 | + judge: | ||
| 304 | + type: checker | ||
| 305 | + title: 主机关键文件与目录齐全 | ||
| 306 | +- id: H-06 | ||
| 307 | + layer: hardware | ||
| 308 | + when: | ||
| 309 | + - checker.ran | ||
| 310 | + note: 禁止自动修(网络类) | ||
| 311 | + desc: hccn.conf 配置正确 | ||
| 312 | + judge: | ||
| 313 | + type: checker | ||
| 314 | + title: hccn.conf 网络配置正确 | ||
| 315 | +- id: H-07 | ||
| 316 | + layer: hardware | ||
| 317 | + when: | ||
| 318 | + - checker.ran | ||
| 319 | + note: Step2 连通性 ping 仅 C2b 开启时执行(--enable-connectivity-check),否则 checker 自身 skip | ||
| 320 | + desc: NIC TLS 开关状态一致(Step2 连通性) | ||
| 321 | + judge: | ||
| 322 | + type: checker | ||
| 323 | + title: 网卡 TLS 状态一致(连通性检查) | ||
| 324 | +- id: H-08 | ||
| 325 | + layer: hardware | ||
| 326 | + when: | ||
| 327 | + - checker.ran | ||
| 328 | + note: 禁止自动修(网络类) | ||
| 329 | + desc: NPU 卡间通信性能均衡 | ||
| 330 | + judge: | ||
| 331 | + type: checker | ||
| 332 | + title: NPU 卡间通信性能均衡 | ||
| 333 | +- id: H-09 | ||
| 334 | + layer: hardware | ||
| 335 | + when: | ||
| 336 | + - checker.ran | ||
| 337 | + desc: 跨节点慢卡汇总(checker 自身按条件 skip) | ||
| 338 | + judge: | ||
| 339 | + type: checker | ||
| 340 | + title: 跨节点慢卡汇总 | ||
| 341 | +- id: H-10 | ||
| 342 | + layer: hardware | ||
| 343 | + when: | ||
| 344 | + - checker.ran | ||
| 345 | + - connectivity.enabled | ||
| 346 | + desc: 跨节点连通性检查 | ||
| 347 | + judge: | ||
| 348 | + type: checker | ||
| 349 | + title: 跨节点连通性检查 | ||
| 350 | +- id: K-01 | ||
| 351 | + when: | ||
| 352 | + - checker.ran | ||
| 353 | + desc: CoreDNS Running、Service 域名可解析 | ||
| 354 | + judge: | ||
| 355 | + type: checker | ||
| 356 | + layer: hardware | ||
| 357 | + title: CoreDNS 运行、Service 域名可解析 | ||
| 358 | +- id: K-02 | ||
| 359 | + when: | ||
| 360 | + - checker.ran | ||
| 361 | + desc: 目标节点 Ready | ||
| 362 | + judge: | ||
| 363 | + type: checker | ||
| 364 | + layer: hardware | ||
| 365 | + title: 目标节点 Ready | ||
| 366 | +- id: K-03 | ||
| 367 | + when: | ||
| 368 | + - checker.ran | ||
| 369 | + desc: 目标节点无 taints | ||
| 370 | + judge: | ||
| 371 | + type: checker | ||
| 372 | + layer: hardware | ||
| 373 | + title: 目标节点无 taints | ||
| 374 | +- id: K-04 | ||
| 375 | + when: | ||
| 376 | + - checker.ran | ||
| 377 | + severity: soft | ||
| 378 | + note: info 项;可分配资源提示 | ||
| 379 | + desc: 节点可分配资源 | ||
| 380 | + judge: | ||
| 381 | + type: checker | ||
| 382 | + layer: hardware | ||
| 383 | + title: 节点可分配资源 | ||
| 384 | +- id: B-01 | ||
| 385 | + when: | ||
| 386 | + - checker.ran | ||
| 387 | + remediable: true | ||
| 388 | + remediation: SSH 在节点 mkdir -p cachePath(路径仅来自 values;白名单,须用户同意) | ||
| 389 | + desc: cachePath 目录存在且可写 | ||
| 390 | + judge: | ||
| 391 | + type: checker | ||
| 392 | + layer: hardware | ||
| 393 | + title: 缓存目录存在且可写 | ||
| 394 | +- id: B-02 | ||
| 395 | + when: | ||
| 396 | + - checker.ran | ||
| 397 | + note: 禁止自动修(驱动类) | ||
| 398 | + desc: 驱动与推理镜像版本兼容 | ||
| 399 | + judge: | ||
| 400 | + type: checker | ||
| 401 | + layer: hardware | ||
| 402 | + title: 驱动与推理镜像版本兼容 | ||
| @@ -0,0 +1,234 @@ | |||
| 1 | +# 部署流程(装 / 等 / 验 / 卸 / 续跑) | ||
| 2 | + | ||
| 3 | +交互与权限见 `ops.md`。改 values 见 `values_rules.md`。预检见 `preflight.md`。 | ||
| 4 | + | ||
| 5 | +过程目录:`./.infernex/deploy/<ts>/`(无写权限用 `~/.infernex/deploy/<ts>/` 或 `INFERNEX_DEPLOY_DIR`)。 | ||
| 6 | + | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +## 1. 准备依赖 | ||
| 10 | + | ||
| 11 | +每次**部署或预检**开始时(卸载不需要): | ||
| 12 | + | ||
| 13 | +```bash | ||
| 14 | +bash "${SKILL_DIR}/scripts/bootstrap.sh" # 默认不拉 checker | ||
| 15 | +# 用户指定跑 checker 时: | ||
| 16 | +# bash "${SKILL_DIR}/scripts/bootstrap.sh" --with-checker | ||
| 17 | +# 旧缓存 checker 无 --enable-connectivity-check 时刷新二进制: | ||
| 18 | +# bash "${SKILL_DIR}/scripts/bootstrap.sh" --with-checker --refresh-checker | ||
| 19 | +source "${INFERNEX_CACHE_DIR:-$HOME/.infernex/cache}/env.sh" | ||
| 20 | +``` | ||
| 21 | + | ||
| 22 | +| 资产 | 默认 | 覆盖 | | ||
| 23 | +|------|------|------| | ||
| 24 | +| Chart | OCI `0.0.0-latest` → cache | `INFERNEX_CHART_DIR` / `INFERNEX_CHART_VERSION` | | ||
| 25 | +| checker | 仅 `--with-checker` | `INFERNEX_CHECKER` | | ||
| 26 | +| 脚本/模板 | 随 Skill | — | | ||
| 27 | + | ||
| 28 | +离线:事先设好 `INFERNEX_CHART_DIR`(及可选 `INFERNEX_CHECKER`)。幂等,已缓存则复用。 | ||
| 29 | + | ||
| 30 | +values 基线: | ||
| 31 | + | ||
| 32 | +```bash | ||
| 33 | +mkdir -p ".infernex/deploy/<ts>" | ||
| 34 | +cp "${INFERNEX_CHART_DIR}/values.yaml" ".infernex/deploy/<ts>/values.yaml" | ||
| 35 | +``` | ||
| 36 | + | ||
| 37 | +不改 Chart 目录内文件;复杂拓扑改 deploy 下 values,少量字段可用 `--set`。 | ||
| 38 | + | ||
| 39 | +--- | ||
| 40 | + | ||
| 41 | +## 2. 采集与确认(C1 / C2 门) | ||
| 42 | + | ||
| 43 | +1. `kubectl config current-context`;`kubectl get nodes -o wide` | ||
| 44 | +2. 复制 `nodes.yaml` 模板,填 `name`/`ip`(Gateway 用首个 Ready 节点 IP)。**默认不要 SSH** | ||
| 45 | +3. 按用户需求填 `intent.yaml`(模板 `templates/intent.yaml.tpl`;槽位与默认见 `intent_schema.md`;缺槽用点选补问) | ||
| 46 | +4. **确定性渲染**:跑 `scripts/render_values.py`(intent + Chart 基线 + `models/` profile → values,含 S-14~S-22 内嵌自检;命令与算法见 `values_rules.md` 末节)。报告 `derived.profile` 显示命中档案;未命中 → C1 前点选确认完整 HF id,并提示可沉淀 profile(`models/_schema.md`)。读 report 的 `derived` 填 intent 派生项;**禁止手改**模式/拓扑/模型/命名/RSG 字段(推理调优 = intent.inferenceParams,重新渲染) | ||
| 47 | +5. **C1 门**(`gates.md`):按 C1 渲染模板出确认表(含派生项);确认后才算配置定稿 | ||
| 48 | +6. **C2 门**(`gates.md`):预检深度递进询问(硬件检查 → 连通性 → SSH 凭据),回答回写 `intent.yaml` | ||
| 49 | +7. 用户提版本 / 本地 Chart → 设环境变量后重跑 bootstrap;未提则不必问 | ||
| 50 | + | ||
| 51 | +简单路径「部署 InferNex」:C1 + C2a 两次点选,不问 SSH。 | ||
| 52 | +仅预检:C3 后停止,不进入 helm。 | ||
| 53 | +已有同名 release → 升级,跳过新装 ns 冲突检查,C1 附变更 diff。 | ||
| 54 | + | ||
| 55 | +--- | ||
| 56 | + | ||
| 57 | +## 3. 预检 → 4. 修复(可选)→ C3 门 | ||
| 58 | + | ||
| 59 | +跑 `scripts/preflight_run.py`(`preflight.md`;清单与判定 = `references/checks.yaml`,产出 `preflight-report.json/.md`)→ **C3 门**(`gates.md`)出报告确认:有硬性失败时**无部署选项**;修复走 C3r → 重跑执行器 → 回 C3。硬性清零 **且 C3 确认**前不得 helm。 | ||
| 60 | + | ||
| 61 | +--- | ||
| 62 | + | ||
| 63 | +## 5. Helm 安装 / 升级 | ||
| 64 | + | ||
| 65 | +**禁止**对 InferNex release 使用 `helm … --wait`:pd-orchestrator / RSG 的证书 Job 为 **post-install/post-upgrade hook**,`--wait` 会等 hook 完成,而 hook 又依赖 release 推进,易**死锁超时**。就绪改由下文轮询 / `wait_inference.py` 完成。 | ||
| 66 | + | ||
| 67 | +```bash | ||
| 68 | +source "${INFERNEX_CACHE_DIR:-$HOME/.infernex/cache}/env.sh" | ||
| 69 | +export NAMESPACE="${NAMESPACE:-ai-inference}" | ||
| 70 | +export RELEASE="${RELEASE:-infernex}" | ||
| 71 | +export VALUES=".infernex/deploy/<ts>/values.yaml" | ||
| 72 | + | ||
| 73 | +cd "${INFERNEX_CHART_DIR}" | ||
| 74 | +helm dependency build | ||
| 75 | +helm upgrade --install "$RELEASE" "${INFERNEX_CHART_DIR}" \ | ||
| 76 | + -n "$NAMESPACE" --create-namespace \ | ||
| 77 | + -f "$VALUES" | ||
| 78 | +``` | ||
| 79 | + | ||
| 80 | +Helm 命令返回成功 ≠ 可推理。立刻记 `helm status` / pods 到 `helm-post.txt`,再进入 §6。 | ||
| 81 | + | ||
| 82 | +--- | ||
| 83 | + | ||
| 84 | +## 6. 等待就绪 | ||
| 85 | + | ||
| 86 | +禁止固定 sleep 当门禁;禁止长时间空转不写 checkpoint。**禁止**用 `helm --wait` 代替本阶段。 | ||
| 87 | + | ||
| 88 | +| 阶段 | 含义 | | ||
| 89 | +|------|------| | ||
| 90 | +| A. 资源出现 | Helm 返回后轮询 Gateway/HTTPRoute/engine Pod(可短暂 `kubectl get`,勿 `--wait`) | | ||
| 91 | +| B. 推理就绪 | `/v1/models` 200;首启常 10–30 分钟,预算 90 分钟 | | ||
| 92 | + | ||
| 93 | +写 `endpoint.json`:`host`←nodes.yaml IP,`gatewayPort`←values(默认 30088),`baseURL=http://host:port`。 | ||
| 94 | + | ||
| 95 | +Helm 返回后**立即**发 interim(不需回复): | ||
| 96 | + | ||
| 97 | +```text | ||
| 98 | +⏳ Helm 已提交(未使用 --wait,避免 RSG 证书 Job hook 死锁),正在等待模型加载与网关推理就绪。 | ||
| 99 | +Endpoint(预期): http://<ip>:30088 | ||
| 100 | +进度: .infernex/deploy/<ts>/wait-state.json | ||
| 101 | +中断后可说「继续等待 infernex」续跑。 | ||
| 102 | +``` | ||
| 103 | + | ||
| 104 | +然后: | ||
| 105 | + | ||
| 106 | +```bash | ||
| 107 | +BASE="$(python3 -c "import json; print(json.load(open('.infernex/deploy/<ts>/endpoint.json'))['baseURL'])")" | ||
| 108 | +python3 "${INFERNEX_SKILL_DIR}/scripts/wait_inference.py" \ | ||
| 109 | + --base-url "$BASE" \ | ||
| 110 | + --state ".infernex/deploy/<ts>/wait-state.json" \ | ||
| 111 | + --namespace "${NAMESPACE:-ai-inference}" \ | ||
| 112 | + --release "${RELEASE:-infernex}" \ | ||
| 113 | + --timeout-min 90 --interval-sec 60 | ||
| 114 | +``` | ||
| 115 | + | ||
| 116 | +exit 0 → 验证;exit 2 → 失败交付,保留 state。单次 tool 时限紧可用 `--once` 多次续跑。 | ||
| 117 | +禁止 `kubectl logs -f`;日志用 `--tail=100`。 | ||
| 118 | + | ||
| 119 | +--- | ||
| 120 | + | ||
| 121 | +## 7. 推理验证与交付 | ||
| 122 | + | ||
| 123 | +完成条件:网关 `POST /v1/chat/completions` → 200(`/v1/models` 就绪只是前置)。 | ||
| 124 | + | ||
| 125 | +```bash | ||
| 126 | +BASE="http://${HOST}:${PORT}" | ||
| 127 | +MODEL=$(curl -sf "${BASE}/v1/models" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['data'][0]['id'])") | ||
| 128 | +curl -sf "${BASE}/v1/chat/completions" \ | ||
| 129 | + -H 'Content-Type: application/json' \ | ||
| 130 | + -d "{\"model\":\"${MODEL}\",\"messages\":[{\"role\":\"user\",\"content\":\"你好\"}],\"max_tokens\":32,\"stream\":false}" | ||
| 131 | +``` | ||
| 132 | + | ||
| 133 | +成功交付模板 = **C4**(`gates.md`,表格固定),落盘 `deploy-result.md`。渲染前实测收集:模型名(`/v1/models`)、Endpoint(节点 IP + nodePort)、资源落位(`kubectl get lws,pods -n <ns>` 与 NPU 占用)、耗时(helm 时间戳 → 就绪时间戳)。验证未过禁止写「已安装好」。 | ||
| 134 | + | ||
| 135 | +--- | ||
| 136 | + | ||
| 137 | +## 续跑 | ||
| 138 | + | ||
| 139 | +用户说「继续等待 infernex」/「检查部署状态」/「集群已就绪」: | ||
| 140 | + | ||
| 141 | +1. 读最新 deploy 下 `wait-state.json`、`endpoint.json` | ||
| 142 | +2. `phase == models_ready` 或 models API 已通 → 直接做 chat 验证 | ||
| 143 | +3. `waiting_models` → 续轮询,**禁止**重装 | ||
| 144 | +4. 无 state 但有 release → 重建 endpoint,再决定等或验 | ||
| 145 | + | ||
| 146 | +--- | ||
| 147 | + | ||
| 148 | +## 卸载 | ||
| 149 | + | ||
| 150 | +触发:「卸载 InferNex」等。跳过部署预检。 | ||
| 151 | + | ||
| 152 | +**永远不删(集群前置):** LWS Operator/CRD、npu-operator、metrics-server、`infernex-bridge-system` 等无关 ns。 | ||
| 153 | + | ||
| 154 | +### 范围语义(确认 = U1 门,问题与选项逐字见 `gates.md`) | ||
| 155 | + | ||
| 156 | +先扫描集群实际资源生成 `uninstall-plan.md`(每范围列将删资源 + 不删的共用组件),U1 门确认后执行。 | ||
| 157 | + | ||
| 158 | +选项措辞以 `gates.md` U1 为准;此处只定义范围语义: | ||
| 159 | + | ||
| 160 | +| 选项 | 含义 | | ||
| 161 | +|------|------| | ||
| 162 | +| ①(轻量) | 只卸主 release(含子 chart 随 release 删除的部分);不主动清 PVC、orphan LWS、`eagle-eye`/`nats`/`scaling-system` 残留 | | ||
| 163 | +| ②(全部清理) | 尽量清掉本次 InferNex Chart 装过的一切:主 release、PVC、orphan LWS,以及 Chart 装到 `eagle-eye` / `nats` / `scaling-system` 的 controller 与 CR(RSG / ElasticScaler / Tidal 等)和残留 ClusterRole 等 | | ||
| 164 | +| ③ | 不动集群 | | ||
| 165 | + | ||
| 166 | +多 release 时先点选要卸的 release/namespace,再回 U1。 | ||
| 167 | + | ||
| 168 | +### 「全部清理」执行顺序 | ||
| 169 | + | ||
| 170 | +U1 确认后执行。Helm **不要** `--wait`。 | ||
| 171 | + | ||
| 172 | +```bash | ||
| 173 | +export NAMESPACE="${NAMESPACE:-ai-inference}" | ||
| 174 | +export RELEASE="${RELEASE:-infernex}" | ||
| 175 | + | ||
| 176 | +# 1) 主 release(含子 chart:gateway/backend/hermes/indexer/eagle-eye/pd-orchestrator 等) | ||
| 177 | +helm uninstall "$RELEASE" -n "$NAMESPACE" || true | ||
| 178 | + | ||
| 179 | +# 2) RSG 扩容 orphan LWS + 残留 CR(release ns 与 scaling-system) | ||
| 180 | +kubectl delete resourcescalinggroup,elasticscaler,tidal -n "$NAMESPACE" --all --ignore-not-found 2>/dev/null || true | ||
| 181 | +kubectl delete resourcescalinggroup,elasticscaler,tidal -n scaling-system --all --ignore-not-found 2>/dev/null || true | ||
| 182 | +kubectl delete lws -n "$NAMESPACE" --all --ignore-not-found 2>/dev/null || true | ||
| 183 | + | ||
| 184 | +# 3) PVC | ||
| 185 | +kubectl delete pvc -n "$NAMESPACE" --all --ignore-not-found 2>/dev/null || true | ||
| 186 | + | ||
| 187 | +# 4) Chart 常用附属 ns(仅当为 InferNex 安装产物;不确定则先 get 再删) | ||
| 188 | +for ns in eagle-eye nats; do | ||
| 189 | + kubectl delete ns "$ns" --ignore-not-found --wait=false 2>/dev/null || true | ||
| 190 | +done | ||
| 191 | + | ||
| 192 | +# 5) scaling-system:清 InferNex/pd-orchestrator 残留;若 ns 内已空可删 ns(勿误伤他人工作负载) | ||
| 193 | +kubectl get all,cm,secret,sa,role,rolebinding -n scaling-system 2>/dev/null || true | ||
| 194 | +# 若确认仅剩本次安装:kubectl delete ns scaling-system --wait=false | ||
| 195 | + | ||
| 196 | +# 6) 主业务 ns | ||
| 197 | +kubectl delete ns "$NAMESPACE" --ignore-not-found --wait=false 2>/dev/null || true | ||
| 198 | + | ||
| 199 | +# 7) 残留 cluster-scoped(按 release 注解过滤,勿盲删) | ||
| 200 | +# kubectl get clusterrole,clusterrolebinding,validatingwebhookconfiguration,mutatingwebhookconfiguration -o json \ | ||
| 201 | +# | 过滤 meta.helm.sh/release-name==$RELEASE && release-namespace==$NAMESPACE 后 delete | ||
| 202 | +``` | ||
| 203 | + | ||
| 204 | +istio:若由本 Chart `inferenceGateway.istio.enabled` 装进 `istio-system`,全部清理时**可**卸本 release 留下的 istiod/相关资源;若集群原本就有 Istio、非本 release 创建 → **不要**删 `istio-system`。 | ||
| 205 | + | ||
| 206 | +### 「仅 helm uninstall」 | ||
| 207 | + | ||
| 208 | +```bash | ||
| 209 | +helm uninstall "$RELEASE" -n "$NAMESPACE" | ||
| 210 | +``` | ||
| 211 | + | ||
| 212 | +不主动删 `eagle-eye`/`nats`/`scaling-system`、PVC、orphan(用户后补清再用「全部清理」)。 | ||
| 213 | + | ||
| 214 | +### 删除结果交付 | ||
| 215 | + | ||
| 216 | +交付模板 = **U2**(`gates.md`),落盘 `uninstall-result.md`。 | ||
| 217 | + | ||
| 218 | +--- | ||
| 219 | + | ||
| 220 | +## 排障速查 | ||
| 221 | + | ||
| 222 | +| 现象 | 动作 | | ||
| 223 | +|------|------| | ||
| 224 | +| helm / 渲染失败 | `helm status`、render.yaml、events | | ||
| 225 | +| 卸载后 Pod/ns 卡 Terminating(istio sidecar 等 finalizer) | 先提示用户,再 `kubectl delete pod -n <ns> <pod> --force --grace-period=0`;ns 卡死再删对应 finalizer | | ||
| 226 | +| ImagePull | describe、imagePullSecrets | | ||
| 227 | +| Gateway 不转发 | HTTPRoute `parentRefs` ↔ `inferenceGateway.name` | | ||
| 228 | +| models OK、chat 5xx | vLLM 日志 `--tail=100`;Init 下载 / NPU / modelPath | | ||
| 229 | +| decode prefix 异常 | `pd.decode.enablePrefixCaching: false` | | ||
| 230 | +| 发现失败 | model label / `pdGroupID` / pdRole(`values_rules.md`) | | ||
| 231 | +| NPU 不够 | 减拓扑或加节点,勿自动下载 | | ||
| 232 | +| bootstrap pull 失败 | 网络、OCI 登录、或设 `INFERNEX_CHART_DIR` | | ||
| 233 | + | ||
| 234 | +port-forward 8000 仅诊断;交付仍须 Gateway URL。 | ||
| @@ -0,0 +1,157 @@ | |||
| 1 | +# 会话流程与确认门(状态机) | ||
| 2 | + | ||
| 3 | +所有需要用户作答的时刻都是**确认门(gate)**:问题与选项措辞**逐字**来自本文,禁止改写、增删或合并选项。执行细节见 `flow.md`;提问工具与权限见 `ops.md`;intent 槽位见 `intent_schema.md`。 | ||
| 4 | + | ||
| 5 | +## 通用规则 | ||
| 6 | + | ||
| 7 | +1. **每门必经** — 部署链路 C1→C2→C3 不可跳过(C2 子问按条件触发);卸载链路 U1 必经。 | ||
| 8 | +2. **选项由状态决定** — 状态→选项映射固定(如 C3 有硬性失败时部署选项不得出现),不临场发明选项。 | ||
| 9 | +3. **确认对象必落盘** `.infernex/deploy/<ts>/`:`intent.yaml`、`preflight-report`、`deploy-result.md`、`uninstall-plan.md`、`uninstall-result.md`。 | ||
| 10 | +4. **同对话点选** — 用平台提问工具一次提出(1~4 题);无工具才短聊天回退并说明。 | ||
| 11 | +5. **进度不设门** — helm 提交、模型加载等待只发普通消息(interim),不弹窗、不要求回复。 | ||
| 12 | +6. **回答回写对象** — 门的回答写回确认对象(如 C2 → `intent.yaml` 的 checker 段),保持单一事实源。 | ||
| 13 | + | ||
| 14 | +--- | ||
| 15 | + | ||
| 16 | +## 部署 / 升级链路 | ||
| 17 | + | ||
| 18 | +``` | ||
| 19 | +意图采集(自然语言) → 填 intent.yaml(槽位见 intent_schema.md) | ||
| 20 | + → C1 ──→ C2(a/b/c) ──→ 预检[checker(可选) + 软件] ──→ C3 | ||
| 21 | + │ │ └修复→ C3r → 重检 → 回 C3 | ||
| 22 | + │ ▼ | ||
| 23 | + └ 取消(终止) helm → 等待(interim) ─失败→ C4b | ||
| 24 | + │ | ||
| 25 | + ▼ | ||
| 26 | + C4 交付 | ||
| 27 | +``` | ||
| 28 | + | ||
| 29 | +升级复用同链路;C1 渲染「与当前 release 的变更 diff + 全量 intent」。仅预检:C3 后停止,不进入 helm。 | ||
| 30 | + | ||
| 31 | +### C1 配置确认门 | ||
| 32 | + | ||
| 33 | +| 项 | 内容 | | ||
| 34 | +|----|------| | ||
| 35 | +| 时机 | intent.yaml 填完;缺槽按 `intent_schema.md` 默认或点选补问 | | ||
| 36 | +| 对象 | `intent.yaml`(按 intent_schema.md 的 **C1 渲染模板**展示,每次一致) | | ||
| 37 | +| 问题 | 「以下部署配置请确认:」+ 渲染表 | | ||
| 38 | +| 选项 | ① 确认,进入预检 ② 修改配置 ③ 取消 | | ||
| 39 | + | ||
| 40 | +「修改配置」→ 改槽位 → **同模板**重渲染 → C1 重新提问;「取消」→ 终止,不碰集群。 | ||
| 41 | + | ||
| 42 | +### C2 预检深度门(紧跟 C1,逐子问递进;结果回写 intent.yaml) | ||
| 43 | + | ||
| 44 | +**C2a 硬件检查** | ||
| 45 | + | ||
| 46 | +- 问题:「是否运行硬件检查(infernex-checker)?软件预检将默认执行。」 | ||
| 47 | +- 选项:① 跳过硬件检查(推荐) ② 运行硬件检查(约 3–5 分钟) | ||
| 48 | +- 跳过 → `checker.enabled=false` → 直接软件预检;运行 → `checker.enabled=true` → C2b | ||
| 49 | + | ||
| 50 | +**C2b 连通性检查**(仅 C2a 选运行时问) | ||
| 51 | + | ||
| 52 | +- 问题:「是否开启网络连通性检查(逐卡 ping + 跨节点)?」 | ||
| 53 | +- 选项:① 跳过连通性检查(推荐) ② 开启(检测时间大幅增加,需节点 SSH 凭据) | ||
| 54 | +- 决定写 `checker.connectivity`;开启 → C2c;跳过 → 按 `preflight.md` §0 跑 checker(不带连通性 flag) | ||
| 55 | + | ||
| 56 | +**C2c SSH 凭据采集**(仅 C2b 选开启时) | ||
| 57 | + | ||
| 58 | +- 问题:「请提供目标节点 SSH 凭据(写入本地 nodes.yaml,对话中不回显):」 | ||
| 59 | +- 采集流程与不回显规则见 `ops.md` §SSH(写入 `nodes.yaml`) | ||
| 60 | +- checker 命令追加 `--enable-connectivity-check`(见 `preflight.md` §0) | ||
| 61 | + | ||
| 62 | +### C3 预检报告确认门 | ||
| 63 | + | ||
| 64 | +| 项 | 内容 | | ||
| 65 | +|----|------| | ||
| 66 | +| 时机 | 预检完成(checker 按选择执行 + 软件检查,`preflight.md`) | | ||
| 67 | +| 对象 | `preflight-report.json` + 渲染 md(schema 见 `preflight.md`;固定渲染:软件检查 / 硬件检查(checker)两节,**逐项列出(通过项不折叠),每项含名称 + 结果 + 版本/实测值说明;不展示内部编号**;汇总行 `X 通过 / Y 硬性失败 / Z 提示 / W 跳过`) | | ||
| 68 | +| 问题 | 「预检完成:X 通过 / Y 硬性失败 / Z 提示 / W 跳过。报告如下:」 | | ||
| 69 | + | ||
| 70 | +| 状态 | 选项 | | ||
| 71 | +|------|------| | ||
| 72 | +| 无硬性失败 | ① 确认,开始部署 ② 修复问题后重检 ③ 取消 | | ||
| 73 | +| 有硬性失败 | ① 修复问题后重检 ② 取消 (**部署选项不得出现**) | | ||
| 74 | + | ||
| 75 | +### C3r 修复确认(C3 选「修复」后) | ||
| 76 | + | ||
| 77 | +- 问题:「以下问题支持自动修复,请勾选:」+ 白名单修复项列表(`preflight.md` 修复白名单) | ||
| 78 | +- 选项:① 勾选自动修复(多选) ② 我手动处理后重检 ③ 取消 | ||
| 79 | +- 修复执行写 `remediation.log` → 重检 → 回 C3 重新渲染报告 | ||
| 80 | + | ||
| 81 | +### C4b 失败处置门(helm / 等待 / 验证失败时) | ||
| 82 | + | ||
| 83 | +- 问题:「部署未成功,失败阶段与原因如下:」+ 固定字段(失败阶段 / 原因 / 证据 / 建议) | ||
| 84 | +- 选项(固定 4 项):① 重试部署 ② 继续等待 ③ 修改配置 ④ 取消 | ||
| 85 | + | ||
| 86 | +### C4 部署结果交付(非提问,固定模板置顶;落盘 `deploy-result.md`) | ||
| 87 | + | ||
| 88 | +**成功(表格固定,值为实测):** | ||
| 89 | + | ||
| 90 | +```markdown | ||
| 91 | +## ✅ InferNex 安装成功 | ||
| 92 | + | ||
| 93 | +| 项 | 值 | | ||
| 94 | +|----|----| | ||
| 95 | +| 模型 | <实测模型名(/v1/models 返回)> | | ||
| 96 | +| 部署形态 | <聚合 / PD 分离>(<N> 卡:tp=<t>/dp=<d>/pp=<p>,replicas=<r>) | | ||
| 97 | +| Release | <release> @ <namespace>(Chart <版本>) | | ||
| 98 | +| Endpoint | http://<实测节点IP>:<nodePort> | | ||
| 99 | +| 推理验证 | POST /v1/chat/completions → 200(实测) | | ||
| 100 | +| 资源落位 | LWS <服务名派生的 LWS 列表>;Pod <n>/<n> Running;NPU <N> 卡 | | ||
| 101 | +| 增强组件 | eagle-eye <开/关>;pd-orchestrator <开/关> | | ||
| 102 | +| 硬件检查 | 本轮 <未运行 checker / 运行 checker(连通性 <开/关>)> | | ||
| 103 | +| 耗时 | helm <x>s → 就绪 <x>m<x>s(首启常 10–30 分钟) | | ||
| 104 | +| 产物 | .infernex/deploy/<ts>/(intent / values / preflight-report / deploy-result) | | ||
| 105 | +``` | ||
| 106 | + | ||
| 107 | +附 models / chat 的 curl 示例(`flow.md` §7)。后续操作提示一句:升级 = 「升级 InferNex」;卸载 = 「卸载 InferNex」;等待中断续跑 = 「继续等待 infernex」。 | ||
| 108 | + | ||
| 109 | +**失败(表格固定,交 C4b 处置):** | ||
| 110 | + | ||
| 111 | +```markdown | ||
| 112 | +## ❌ InferNex 部署未成功 | ||
| 113 | + | ||
| 114 | +| 项 | 值 | | ||
| 115 | +|----|----| | ||
| 116 | +| 失败阶段 | <helm | 等待 | 验证> | | ||
| 117 | +| 原因 | <一句话> | | ||
| 118 | +| 证据 | <日志 tail(--tail=100)摘要 / wait-state checkpoint> | | ||
| 119 | +| 当前集群状态 | <release 是否已装;Pod <状态>;NPU 占用> | | ||
| 120 | +| 建议 | <修复方向;处置选项见 C4b> | | ||
| 121 | +| 产物 | .infernex/deploy/<ts>/ | | ||
| 122 | +``` | ||
| 123 | + | ||
| 124 | +验证未过禁止写「已安装好」。 | ||
| 125 | + | ||
| 126 | +--- | ||
| 127 | + | ||
| 128 | +## 卸载链路 | ||
| 129 | + | ||
| 130 | +``` | ||
| 131 | +卸载意图 → 扫描集群实际资源 → uninstall-plan(按范围分组) | ||
| 132 | + → U1 → 执行(flow.md §卸载)→ U2 交付 | ||
| 133 | +``` | ||
| 134 | + | ||
| 135 | +### U1 卸载范围确认门 | ||
| 136 | + | ||
| 137 | +| 项 | 内容 | | ||
| 138 | +|----|------| | ||
| 139 | +| 时机 | 卸载意图 + 资源扫描完成(跳过部署预检) | | ||
| 140 | +| 对象 | `uninstall-plan.md`:每个范围列「将删除的资源」;共用组件明确标注「不会删除」 | | ||
| 141 | +| 问题 | 「以下资源将被清理,请选择卸载范围:」 | | ||
| 142 | +| 选项 | ① 仅 helm uninstall(轻量) ② 全部清理 ③ 取消 | | ||
| 143 | + | ||
| 144 | +多 release:先点选要卸的 release / namespace,再回 U1。范围的执行语义与顺序见 `flow.md` §卸载。 | ||
| 145 | + | ||
| 146 | +### U2 删除结果交付(非提问,固定模板;落盘 `uninstall-result.md`) | ||
| 147 | + | ||
| 148 | +```markdown | ||
| 149 | +## ✅ InferNex 已按「<范围>」卸载 | ||
| 150 | + | ||
| 151 | +| 项 | 值 | | ||
| 152 | +|----|----| | ||
| 153 | +| 已删除 | <清单:release / ns / PVC / orphan LWS / 附属 CR …> | | ||
| 154 | +| 已跳过 | <清单及原因> | | ||
| 155 | +| 失败/需人工 | <清单(若有)> | | ||
| 156 | +| 未动 | LWS Operator、npu-operator、metrics-server、infernex-bridge 等 | | ||
| 157 | +``` | ||
| @@ -0,0 +1,46 @@ | |||
| 1 | +# Intent 槽位表(C1 确认对象的 schema) | ||
| 2 | + | ||
| 3 | +模板:`templates/intent.yaml.tpl`。落盘 `.infernex/deploy/<ts>/intent.yaml`。 | ||
| 4 | + | ||
| 5 | +语义:**null = 用户未提及**,按「未说时默认」列处理。AI 只填用户说过或推导必须的槽;禁止臆测。 | ||
| 6 | + | ||
| 7 | +## 槽位 | ||
| 8 | + | ||
| 9 | +| 槽位 | 类型 | 未说时默认 | 说明 | | ||
| 10 | +|------|------|-----------|------| | ||
| 11 | +| `model` | string | Chart 默认 `Qwen/Qwen3-8B` | HF id 原样;短名/别名匹配 `models/` profile 的 `aliases`(命中 → canonical id + 模型级 overlay);未命中 → C1 前点选确认完整 HF id | | ||
| 12 | +| `mode` | `pd` \| `aggregated` | `pd` | yaml 配置值;**展示给用户时写作 聚合 / PD 分离**;切换须整包改(`values_rules.md` §4) | | ||
| 13 | +| `cards` | int | 4(2P2D) | 与 topology 并提时,点名拓扑优先 | | ||
| 14 | +| `topology.tp/dp/pp/replicas` | int | 全 1,`replicas` 凑 N | 推导规则 `values_rules.md` §3 | | ||
| 15 | +| `chart.version` | string | `0.0.0-latest` | 即 `INFERNEX_CHART_VERSION` | | ||
| 16 | +| `chart.dir` | path | —(走 OCI) | 即 `INFERNEX_CHART_DIR`;填了则忽略 version | | ||
| 17 | +| `namespace` | string | `ai-inference` | | | ||
| 18 | +| `release` | string | `infernex` | | | ||
| 19 | +| `nodePort` | int | 30088 | 对应 `inferenceGateway.service.nodePorts.http` | | ||
| 20 | +| `components.eagleEye` | bool | 随 Chart,不改 | 仅用户点名开/关才写 | | ||
| 21 | +| `components.pdOrchestrator` | bool | 随 Chart,不改 | 同上 | | ||
| 22 | +| `routing` | `kv-cache-aware` \| `random` \| `bucket` \| `prediction` | `kv-cache-aware` | 表见 `values_rules.md` §5 | | ||
| 23 | +| `inferenceParams` | map | —(不改) | 已知键 `maxModelLen` / `maxNumBatchedTokens` / `gpuMemoryUtilization` / `blockSize`(落 role extraArgs)+ `extraArgs`(追加原始 flag);其余键 `render_values.py` 拒绝 | | ||
| 24 | +| `checker.enabled` | bool | false(C2a 问) | C1 时为 null,渲染「预检时选择」 | | ||
| 25 | +| `checker.connectivity` | bool | false(C2b 问) | 仅 enabled=true 时问;开启须 C2c 采集凭据 | | ||
| 26 | + | ||
| 27 | +## C1 渲染模板(固定,每次一致) | ||
| 28 | + | ||
| 29 | +| 项 | 值 | | ||
| 30 | +|----|----| | ||
| 31 | +| 模型 | `<model 或「默认 Qwen/Qwen3-8B」>` | | ||
| 32 | +| 模式 | `<聚合 / PD 分离>` | | ||
| 33 | +| 拓扑 | `<cards> 卡(tp/dp/pp/replicas,或「默认推导」)` | | ||
| 34 | +| Chart | `<版本号 / 本地目录 / latest>` | | ||
| 35 | +| namespace / release | `<ns> / <release>` | | ||
| 36 | +| Gateway | NodePort `<port>` | | ||
| 37 | +| 增强组件 | eagle-eye `<开/关/默认>`;pd-orchestrator `<开/关/默认>` | | ||
| 38 | +| 路由 | `<profile>` | | ||
| 39 | +| 派生:服务名 / LWS / RSG | `<services[].name>`;LWS `<列表>`;RSG 指向 `<列表>`(按 `values_rules.md` patch 后回填,来自 Patch 后 checklist) | | ||
| 40 | +| 硬件检查 | 预检时选择(C2) | | ||
| 41 | + | ||
| 42 | +升级场景在此表前附「与当前 release 的变更 diff」。 | ||
| 43 | + | ||
| 44 | +## checker 段回填(C2) | ||
| 45 | + | ||
| 46 | +C2a / C2b 的回答**立即写回** `intent.yaml`,再进入预检执行——intent 是唯一事实源。 | ||
| @@ -0,0 +1,56 @@ | |||
| 1 | +# 交互与命令权限 | ||
| 2 | + | ||
| 3 | +目标:**尽量少用「结束本轮 → 用户再打字」**。需要作答时用平台点选对话框;进度与交付用普通消息。 | ||
| 4 | + | ||
| 5 | +--- | ||
| 6 | + | ||
| 7 | +## 点选交互(强制) | ||
| 8 | + | ||
| 9 | +1. **凡问必 UI** — 需要用户答案就调用提问工具;禁止 Markdown 菜单写「请回复确认/取消」后结束本轮。 | ||
| 10 | +2. **同轮提问** — 说明文字与工具调用同一轮;能合并的题放进一次调用(通常 1~4 题)。 | ||
| 11 | +3. **禁止假装无 UI** — 不得未尝试就宣称「无点选」并改聊天采集。 | ||
| 12 | +4. **无工具才回退** — 仅 Tool not found / 平台无此工具时,才短聊天回退并说明原因。 | ||
| 13 | +5. **不问不扰** — 进度心跳、helm/模型加载、最终交付 → 普通消息,不弹窗。 | ||
| 14 | +6. **密钥不回显** — 密码写入 `nodes.yaml`(或 `chmod 600` 文件)后,助手消息禁止复述明文。 | ||
| 15 | + | ||
| 16 | +### 平台工具 | ||
| 17 | + | ||
| 18 | +| 平台 | 工具 | 要点 | | ||
| 19 | +|------|------|------| | ||
| 20 | +| Cursor | `AskQuestion` | 以当前会话工具列表为准;有则必用。Plan 常有;Agent 以列表为准 | | ||
| 21 | +| Claude Code | `AskUserQuestion` | 选项 + Other | | ||
| 22 | +| OpenCode | `question` | TUI 选项 | | ||
| 23 | + | ||
| 24 | +用实际可用工具名,勿虚构。 | ||
| 25 | + | ||
| 26 | +### 常见必须点选的检查点(门的清单与逐字措辞见 `gates.md`) | ||
| 27 | + | ||
| 28 | +| 门 | 检查点 | | ||
| 29 | +|----|--------| | ||
| 30 | +| C1 | 配置确认 | | ||
| 31 | +| C2a–c | 硬件检查 → 连通性 → SSH 凭据(条件递进) | | ||
| 32 | +| C3 / C3r | 预检报告确认 / 白名单修复勾选 | | ||
| 33 | +| C4b | 失败处置 | | ||
| 34 | +| U1 | 卸载范围(多 release 先点选 release) | | ||
| 35 | + | ||
| 36 | +摘要可用 Markdown 表;**确认与填空必须走提问工具**。 | ||
| 37 | + | ||
| 38 | +### SSH(仅 checker 或节点修复) | ||
| 39 | + | ||
| 40 | +默认部署 **禁止** 问 SSH。需要时:勾选节点 → 用户 → 认证方式(私钥/路径/密码)→ Other 收集 → 写入 `nodes.yaml` → 试连失败再用 UI 重问。 | ||
| 41 | + | ||
| 42 | +### 与自动推进 | ||
| 43 | + | ||
| 44 | +- 缺输入 / 确认 / 破坏性选择 → **停 + 点选 UI** | ||
| 45 | +- 用户确认后 → 自治跑 helm / 等待 / 验证,不为进度要打字 | ||
| 46 | + | ||
| 47 | +--- | ||
| 48 | + | ||
| 49 | +## Shell 权限(减少弹窗) | ||
| 50 | + | ||
| 51 | +1. 本会话**第一次** `kubectl` / `helm` / `bootstrap.sh` 即申请完整权限(Cursor:`required_permissions: ["all"]`)。 | ||
| 52 | +2. 同轮能合并的命令不要拆成多次独立调用。 | ||
| 53 | +3. 权限类失败 → **静默**用完整权限重跑同一命令;禁止对用户说「沙箱 / sandbox / 无权限 / required_permissions」。 | ||
| 54 | +4. 仅弹窗挡住时说一句:`请在弹出的权限确认中点「允许」。本会话一般只需确认一次。` 不要让用户在聊天里回「allow」。 | ||
| 55 | + | ||
| 56 | +默认不跑 checker → 只用本机 `kubectl`/`helm`/`curl`,不要因此要节点密码。 | ||
| @@ -0,0 +1,68 @@ | |||
| 1 | +# 预检 | ||
| 2 | + | ||
| 3 | +权威前置:openFuyao 用户指南 [ai_inference_infernex.md](https://gitcode.com/openFuyao/sig-ai-inference/blob/main/docs/zh/ai_inference_infernex/user_guide/ai_inference_infernex.md)。 | ||
| 4 | +交互见 `ops.md`;values 规则见 `values_rules.md`;确认门见 `gates.md`。 | ||
| 5 | + | ||
| 6 | +## 执行(脚本驱动,禁止绕过) | ||
| 7 | + | ||
| 8 | +**清单与判定全部数据化在 `references/checks.yaml`(v2)**:每项含 `when` 触发条件(机器求值)与 `judge` 判定(cmd 命令 / checker 映射 / render-check 合并)。AI 的职责 = 运行执行器 + 读报告 + C3 门确认;**禁止**跳过执行器按自然语言自行判定、自行拼检查项或改报告措辞。 | ||
| 9 | + | ||
| 10 | +```bash | ||
| 11 | +source "${INFERNEX_CACHE_DIR:-$HOME/.infernex/cache}/env.sh" | ||
| 12 | +python3 "${INFERNEX_SKILL_DIR}/scripts/preflight_run.py" \ | ||
| 13 | + --deploy-dir .infernex/deploy/<ts> \ | ||
| 14 | + --namespace <ns> --release <release> \ | ||
| 15 | + --chart-dir "${INFERNEX_CHART_DIR}" | ||
| 16 | +``` | ||
| 17 | + | ||
| 18 | +退出码:0 = 无硬性失败;1 = 有硬性失败(C3 门不出部署选项);2 = 配置错误。 | ||
| 19 | + | ||
| 20 | +判定类型(cmd / script / render-check / checker)与 `when` 条件语法、占位符的定义见 `checks.yaml` 头部注释。 | ||
| 21 | + | ||
| 22 | +## 0. infernex-checker(可选,硬件层;C2 门选择后、执行器运行前跑) | ||
| 23 | + | ||
| 24 | +**C2a 选「运行硬件检查」时**(默认推荐跳过):`bootstrap.sh --with-checker`,再: | ||
| 25 | + | ||
| 26 | +```bash | ||
| 27 | +"${INFERNEX_CHECKER}" all \ | ||
| 28 | + --nodes .infernex/deploy/<ts>/nodes.yaml \ | ||
| 29 | + --values .infernex/deploy/<ts>/values.yaml \ | ||
| 30 | + --output .infernex/deploy/<ts>/checker-result.json | ||
| 31 | +``` | ||
| 32 | + | ||
| 33 | +**连通性检查(ping,含跨节点)默认关闭**——新版 checker 不传 `--enable-connectivity-check` 即跳过 H-07 Step2 / H-10,省去最耗时的逐卡 ping。仅 **C2b 门**选开启(须 C2c 采集 SSH 凭据)时追加 `--enable-connectivity-check`。 | ||
| 34 | + | ||
| 35 | +旧缓存 checker 报 `unknown flag: --enable-connectivity-check`(或跑出 Step2 ping)→ 先刷新二进制再跑: | ||
| 36 | + | ||
| 37 | +```bash | ||
| 38 | +bash "${INFERNEX_SKILL_DIR}/scripts/bootstrap.sh" --with-checker --refresh-checker | ||
| 39 | +``` | ||
| 40 | + | ||
| 41 | +勿重复实现 H-01~H-10、K-01~K-04、B-01~B-02(执行器从 checker-result.json 映射)。未跑 checker → **不要**要 SSH。 | ||
| 42 | + | ||
| 43 | +## 1. 报告(执行器产出,C3 确认对象) | ||
| 44 | + | ||
| 45 | +`.infernex/deploy/<ts>/preflight-report.json`(schema 固定:`summary{pass,fail_hard,info,skip}` + `software[]`/`hardware[]`,每项 `{id,status,severity,evidence,remediable,remediation}`)与 `preflight-report.md`(固定模板渲染:**软件检查 / 硬件检查(checker)两节**,逐项列出 `| 检查项 | 结果 | 说明 |`——面向用户展示中文名称与版本/实测值,不展示内部编号;置顶汇总行 `X 通过 / Y 硬性失败 / Z 提示 / W 跳过`)。C3 门问题与选项见 `gates.md`;有硬性失败时**无部署选项**。 | ||
| 46 | + | ||
| 47 | +## 2. 修复白名单(C3r;须用户同意) | ||
| 48 | + | ||
| 49 | +执行器报告的 `remediable=true` 项才可自动修复(与 `checks.yaml` 的 remediation 对应): | ||
| 50 | + | ||
| 51 | +| ID / 类型 | 内容 | | ||
| 52 | +|-----------|------| | ||
| 53 | +| **B-01** | SSH 在节点 `mkdir -p` cachePath(路径仅来自 values) | | ||
| 54 | +| **S-08** | 装 helm(钉版本) | | ||
| 55 | +| **S-02** | 按官方文档装 LWS(须同意) | | ||
| 56 | +| **S-04** | 装 metrics-server ≥0.8.0(须同意) | | ||
| 57 | +| **S-14/S-16/S-20** | 改 deploy values,展示 diff | | ||
| 58 | +| **helm dep** | `helm dependency build` | | ||
| 59 | + | ||
| 60 | +修复执行写 `remediation.log` → 重跑执行器 → 回 C3。 | ||
| 61 | + | ||
| 62 | +**禁止自动修:** NPU 不够(UI 问减拓扑/加节点/取消,不下载);B-02 驱动;H-06~08 网络;ImagePull/私有仓仅引导;HF 模型预检阶段不预拉;无 ID 臆测。 | ||
| 63 | + | ||
| 64 | +硬性未清零时用户要求「忽略并安装」→ **禁止 helm**。 | ||
| 65 | + | ||
| 66 | +--- | ||
| 67 | + | ||
| 68 | +部署后验证不属于预检通过条件 — 见 `flow.md`。 | ||
| @@ -0,0 +1,213 @@ | |||
| 1 | +# Values 变更规则 | ||
| 2 | + | ||
| 3 | +权威结构来自 **`${INFERNEX_CHART_DIR}/values.yaml`**(及 Chart 模板)。 | ||
| 4 | + | ||
| 5 | +**§1–§6 的命名 / 模式切换 / 拓扑推导 / 模型字段 / RSG 重写已固化为 `scripts/render_values.py`**——这些字段的 patch **只能**由该脚本从 `intent.yaml` 生成(SKILL.md 硬门禁 9),本文是它的**算法说明**,供评审与排障对照;手改这些字段 = S-16 类故障来源。`examples/` 只对照字段名,禁止整份当基线。 | ||
| 6 | + | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +## 意图 → patch(必遵) | ||
| 10 | + | ||
| 11 | +用户说清模型 / 模式 / 卡数后直接 patch;缺项才用点选 UI(`ops.md`)。 | ||
| 12 | + | ||
| 13 | +| 槽位 | 来源 | 未说时 | | ||
| 14 | +|------|------|--------| | ||
| 15 | +| 模型 | 「qwen3-8b」「GLM-5.2」等 | Chart 默认 `Qwen/Qwen3-8B` | | ||
| 16 | +| 模式 | 聚合 / PD | Chart 默认 **PD** | | ||
| 17 | +| 卡数 N | 「2 张卡」「8 卡」 | Chart 默认约 **4 卡**(2P2D) | | ||
| 18 | +| TP/DP/PP | 用户点名才改 | 默认全 1;用 **replicas** 凑 N | | ||
| 19 | +| 路由 profile | 用户点名才改 | Chart 默认 **kv-cache-aware** + indexer 开 | | ||
| 20 | + | ||
| 21 | +### 1. 模型 | ||
| 22 | + | ||
| 23 | +- HF id(含 `/`)→ 原样写 `global.modelName` | ||
| 24 | +- 短名/别名由 **`models/` profile 的 `aliases` 承载**(如 `qwen3-8b`、`glm-5.2`):`render_values.py` 命中即用 profile 的 canonical `model`;命中不到 → 报告提示 **C1 前点选确认完整 HF id**,勿猜仓库路径 | ||
| 25 | +- 同步 `hermes-router.inferenceExtension.routing.tokenizer.model`(字段存在时;脚本已固化) | ||
| 26 | + | ||
| 27 | +### 2. 两套 slug(勿混用) | ||
| 28 | + | ||
| 29 | +| 用途 | 算法 | 例 `Qwen/Qwen3-8B` | | ||
| 30 | +|------|------|---------------------| | ||
| 31 | +| **model label**(InferencePool / Pod `openfuyao.com/model`) | `/`→`-`,`.`→`-`,lower(与 Chart `vllm.modelName.label` 一致) | `qwen-qwen3-8b` | | ||
| 32 | +| **服务名** `services[].name` | 去 org 前缀;非 `[a-z0-9]`→`-`;压缩 `-` | `qwen3-8b` | | ||
| 33 | + | ||
| 34 | +可用:`python3 "${INFERNEX_SKILL_DIR}/scripts/slugs.py" --model 'Qwen/Qwen3-8B'`。 | ||
| 35 | + | ||
| 36 | +服务名按模式(**改 mode 时必须重算 name,禁止沿用 Chart 默认 `qwen3-8b-2p2d-01`**): | ||
| 37 | + | ||
| 38 | +| 模式 | `services[].name`(必改) | 预期 LWS 名 | | ||
| 39 | +|------|---------------------------|-------------| | ||
| 40 | +| aggregated | `{svc_slug}-aggregated-01` | `{name}-aggregated` | | ||
| 41 | +| PD | `{svc_slug}-{Np}p{Nd}d-01` | `{name}-prefill` / `{name}-decode` | | ||
| 42 | + | ||
| 43 | +例:用户要「聚合 + 2 卡 + Qwen3-8B」→ `name: qwen3-8b-aggregated-01`,LWS = `qwen3-8b-aggregated-01-aggregated`。 | ||
| 44 | +**错误(本次类故障)**:`mode: aggregated` 但 `name` 仍为 `qwen3-8b-2p2d-01` → Pod 变成 `qwen3-8b-2p2d-01-aggregated-*`,且 RSG 仍指 prefill/decode → **S-16 FAIL**。 | ||
| 45 | + | ||
| 46 | +改 `name` 后同步: | ||
| 47 | + | ||
| 48 | +- **`pd-orchestrator.resourcescalinggroup.instanceConfig.targetResources` 整表重写**(见下节,不是只改 mode) | ||
| 49 | +- **PD**:`pdGroupID` = 新 `name` | ||
| 50 | +- InferencePool **不**看服务名,看 model label | ||
| 51 | + | ||
| 52 | +### 3. 卡数 → 并行度 | ||
| 53 | + | ||
| 54 | +未指定 TP/DP/PP 时: | ||
| 55 | + | ||
| 56 | +- **聚合**:TP=DP=PP=1,`aggregated.replicas = N`(2 卡 → `replicas: 2`) | ||
| 57 | +- **PD**:N 偶数 → P/D 各 `N/2` 卡(各 replicas=`N/2`);奇数或点名拓扑 → 按点名,除不尽则 UI 问 | ||
| 58 | + | ||
| 59 | +公式:`N_role = replicas × DP × TP × PP`(有 `cardCount` 时按 Chart 逻辑)。patch 后跑: | ||
| 60 | + | ||
| 61 | +```bash | ||
| 62 | +python3 "${INFERNEX_NPU_SCRIPT}" \ | ||
| 63 | + --values .infernex/deploy/<ts>/values.yaml \ | ||
| 64 | + --nodes .infernex/deploy/<ts>/nodes.yaml | ||
| 65 | +``` | ||
| 66 | + | ||
| 67 | +### 4. 模式切换(PD ↔ 聚合)— 强制整包,禁止半改 | ||
| 68 | + | ||
| 69 | +Chart 默认是 **PD + `qwen3-8b-2p2d-01` + RSG 两项 prefill/decode**。用户说「聚合」时,下列 **全部必改**;只改 `mode` / 只改 `replicas` = 错误。 | ||
| 70 | + | ||
| 71 | +#### 4.1 字段对照(一次改齐) | ||
| 72 | + | ||
| 73 | +| 项 | → aggregated(用户说聚合) | → PD | | ||
| 74 | +|----|---------------------------|------| | ||
| 75 | +| `services[].name` | **`{slug}-aggregated-01`**(去掉 `2p2d` / `NpNd`) | **`{slug}-{Np}p{Nd}d-01`**(去掉 `aggregated`) | | ||
| 76 | +| `services[].mode` | `aggregated` | `pd` | | ||
| 77 | +| `hermes-router…deploymentMode` | `aggregate` | `pd` | | ||
| 78 | +| role 块 | 设 `aggregated.replicas=N` 等;可保留未用的 `pd:` 但 **不得**再靠它出 LWS | 设 `pd.prefill/decode.replicas` | | ||
| 79 | +| KV `connectorConfig` | 从 Chart **aggregated 示例**抄 store/`AscendStoreConnector` 结构 | 用主 Chart 默认 MultiConnector | | ||
| 80 | +| **RSG `targetResources`** | **仅 1 项**:`name: aggregated`,`resourceRef.name: {services[].name}-aggregated` | **恰好 2 项**:`…-prefill` + `…-decode` | | ||
| 81 | +| RSG `groupConfig.groupName` | `aggregated` | `pd`(或 Chart 默认) | | ||
| 82 | +| `pdGroupID` | 聚合可不依赖;建议删或忽略 | **= `services[].name`** | | ||
| 83 | + | ||
| 84 | +#### 4.2 聚合时 RSG 必须长这样(标量级,结构从 Chart/example 抄) | ||
| 85 | + | ||
| 86 | +主 Chart 默认(错误若仍保留在聚合部署里): | ||
| 87 | + | ||
| 88 | +```yaml | ||
| 89 | +targetResources: | ||
| 90 | + - name: prefill | ||
| 91 | + resourceRef: { name: qwen3-8b-2p2d-01-prefill, ... } | ||
| 92 | + - name: decode | ||
| 93 | + resourceRef: { name: qwen3-8b-2p2d-01-decode, ... } | ||
| 94 | +``` | ||
| 95 | + | ||
| 96 | +用户「聚合 + 2 卡」正确形态: | ||
| 97 | + | ||
| 98 | +```yaml | ||
| 99 | +# inference-backend.services[0] | ||
| 100 | +name: qwen3-8b-aggregated-01 # 禁止再叫 qwen3-8b-2p2d-01 | ||
| 101 | +mode: aggregated | ||
| 102 | +aggregated: | ||
| 103 | + replicas: 2 | ||
| 104 | + tensorParallelSize: 1 | ||
| 105 | + dataParallelSize: 1 | ||
| 106 | + pipelineParallelSize: 1 | ||
| 107 | + | ||
| 108 | +# pd-orchestrator.resourcescalinggroup.instanceConfig | ||
| 109 | +targetResources: | ||
| 110 | + - name: aggregated | ||
| 111 | + resourceRef: | ||
| 112 | + apiVersion: leaderworkerset.x-k8s.io/v1 | ||
| 113 | + kind: LeaderWorkerSet | ||
| 114 | + name: qwen3-8b-aggregated-01-aggregated # = {services[].name}-aggregated | ||
| 115 | +scalingStrategy: | ||
| 116 | + type: GroupReplication | ||
| 117 | + groupConfig: | ||
| 118 | + groupName: aggregated | ||
| 119 | +``` | ||
| 120 | + | ||
| 121 | +`helm template` 后自检(不过则禁止 helm install): | ||
| 122 | + | ||
| 123 | +```bash | ||
| 124 | +# 聚合:不得再出现默认 PD 服务名/RSG | ||
| 125 | +grep -E '2p2d|prefill|decode' .infernex/deploy/<ts>/values.yaml # name/RSG 段不应残留 PD 指向 | ||
| 126 | +grep 'kind: LeaderWorkerSet' -A2 .infernex/deploy/<ts>/render.yaml | ||
| 127 | +``` | ||
| 128 | + | ||
| 129 | +#### 4.3 反模式(预检 S-16 / 脏 LWS 名) | ||
| 130 | + | ||
| 131 | +| 错误做法 | 后果 | | ||
| 132 | +|----------|------| | ||
| 133 | +| 只把 `mode` 改成 `aggregated`,`name` 仍 `…-2p2d-01` | LWS/Pod 名带 `2p2d`+`aggregated` | | ||
| 134 | +| 改了 mode/name,RSG 仍指 `…-prefill`/`…-decode` | **S-16 硬性 FAIL** | | ||
| 135 | +| RSG 写成 `{name}` 漏了后缀 `-aggregated` | RSG 找不到 LWS | | ||
| 136 | +| 从 PD 切聚合却不改 `deploymentMode` | S-14 FAIL / 路由错 | | ||
| 137 | + | ||
| 138 | +需要 connector/mooncake 整段结构时:对照 `examples/*aggregated*.yaml` **字段结构**,或从当前 Chart values 改;**不要**整份复制 example 作基线。 | ||
| 139 | + | ||
| 140 | +### 5. 路由 profile | ||
| 141 | + | ||
| 142 | +| profile | 适用 | cache-indexer | | ||
| 143 | +|---------|------|---------------| | ||
| 144 | +| `kv-cache-aware`(默认) | 聚合与 PD | **必须开**;address 用 Chart 默认 | | ||
| 145 | +| `random` | 均可 | 可关(用户要「最简单/不要 indexer」时) | | ||
| 146 | +| `bucket` | **仅 PD** | 视场景 | | ||
| 147 | +| `prediction` / `""` | 见 Chart | 按 Chart | | ||
| 148 | + | ||
| 149 | +聚合默认也是 kv-cache-aware(不是必须 random)。kv-cache-aware 时保留 Chart 的 `global.env`(`PYTHONHASHSEED`、`VLLM_KV_EVENTS_USE_INT_BLOCK_HASHES`)与 mooncake `httpPort` 等默认。 | ||
| 150 | + | ||
| 151 | +### 6. 仅换模型 | ||
| 152 | + | ||
| 153 | +改 `modelName`(+ tokenizer.model);建议重算 `services[].name`(及 PD 的 `pdGroupID`/RSG)。不要只改模型却保留另一模型前缀的服务名。 | ||
| 154 | + | ||
| 155 | +### 7. Gateway NodePort | ||
| 156 | + | ||
| 157 | +默认 30088。改:`inferenceGateway.service.nodePorts.http`。 | ||
| 158 | + | ||
| 159 | +--- | ||
| 160 | + | ||
| 161 | +## 推理参数:Helm 字段 vs extraArgs | ||
| 162 | + | ||
| 163 | +决策顺序: | ||
| 164 | + | ||
| 165 | +1. Chart/role 上有同名 Helm 字段 → **只改字段** | ||
| 166 | +2. Chart 由 `dataParallelSize` / `kvTransferConfig` 等自动注入 → **不要**再写 extraArgs | ||
| 167 | +3. 其余 vLLM flag → `<role>.extraArgs` | ||
| 168 | +4. Mooncake master → `kvTransferConfig.mooncake.master.extraArgs`(与引擎 extraArgs 无关) | ||
| 169 | + | ||
| 170 | +常见映射(完整字段以 Chart 为准): | ||
| 171 | + | ||
| 172 | +| 意图 | 改 | | ||
| 173 | +|------|-----| | ||
| 174 | +| prefix cache | `<role>.enablePrefixCaching`(PD+Mooncake 时 decode 须 `false`) | | ||
| 175 | +| batch tokens / max len / 显存 / block | `maxNumBatchedTokens` / `maxModelLen` / `gpuMemoryUtilization` / `blockSize` | | ||
| 176 | +| TP/PP/DP/replicas | 对应 Helm 字段 | | ||
| 177 | +| `--max-num-seqs` / `--dtype` / `--enforce-eager` | extraArgs | | ||
| 178 | +| 310P 聚合 | 聚合模式 + extraArgs `--dtype float16`、`--enforce-eager`(保留主 Chart 默认 extraArgs 项,除非用户确认清空) | | ||
| 179 | + | ||
| 180 | +**禁止**在 extraArgs 重复写已有 Helm 字段或 `--data-parallel-*` / `--kv-transfer-config`。 | ||
| 181 | + | ||
| 182 | +改 `extraArgs`:追加时保留主 Chart 默认项;整段替换须在摘要列出最终完整列表。 | ||
| 183 | + | ||
| 184 | +--- | ||
| 185 | + | ||
| 186 | +## Patch 后 checklist(C1 渲染前必须过) | ||
| 187 | + | ||
| 188 | +**经 `render_values.py` 生成的 values:第 1/2/3/5/8 项已由脚本内嵌自检(S-14~S-22)保证**,读其 report 即可;以下清单用于人工复核与手改场景兜底。 | ||
| 189 | + | ||
| 190 | +1. `mode` ↔ `deploymentMode`(`aggregated`↔`aggregate`,`pd`↔`pd`) | ||
| 191 | +2. **模式与 `services[].name` 后缀一致**:聚合名含 `aggregated`、**不含** `2p2d`/`NpNd`;PD 名含 `NpNd`、**不含** `aggregated` | ||
| 192 | +3. **S-16**:RSG `targetResources[].resourceRef.name` 与上表 LWS 完全一致(聚合仅 1 项 `…-aggregated`;PD 两项 `…-prefill`+`…-decode`) | ||
| 193 | +4. model label:Pool 与 Pod `openfuyao.com/model` 一致(`slugs.py`) | ||
| 194 | +5. PD:`pdGroupID` = `services[].name` | ||
| 195 | +6. kv-cache-aware → indexer 开 | ||
| 196 | +7. NPU 合计 = 用户卡数(聚合:`aggregated.replicas`,默认 TP=DP=PP=1) | ||
| 197 | +8. 派生项回填 intent 呈现并在 **C1 门**渲染表展示(`intent_schema.md` 渲染模板末行):`services[].name`、预期 LWS 列表、RSG 指向、deploymentMode、NPU | ||
| 198 | + | ||
| 199 | +生成命令: | ||
| 200 | + | ||
| 201 | +```bash | ||
| 202 | +python3 "${INFERNEX_SKILL_DIR}/scripts/render_values.py" \ | ||
| 203 | + --intent .infernex/deploy/<ts>/intent.yaml \ | ||
| 204 | + --chart-dir "${INFERNEX_CHART_DIR}" \ | ||
| 205 | + --output .infernex/deploy/<ts>/values.yaml \ | ||
| 206 | + --report .infernex/deploy/<ts>/render-check.json | ||
| 207 | +``` | ||
| 208 | + | ||
| 209 | +同一 intent + 同一基线 → 输出 byte-identical(golden UT:`test/test_render_values.py`)。 | ||
| 210 | + | ||
| 211 | +预检 S-14~S-22 见 `preflight.md`。未过 checklist **禁止** helm。 | ||
| 212 | + | ||
| 213 | +字段对照(可选):https://gitcode.com/openFuyao/InferNex/tree/master/examples | ||
| @@ -0,0 +1,139 @@ | |||||||||||||||||||||||||||||||||||||||||||
| 1 | +#!/usr/bin/env bash | ||||||||||||||||||||||||||||||||||||||||||
[重要] 脚本文件缺少 shebang 与错误处理。修复建议:添加 ![]() ![]() | |||||||||||||||||||||||||||||||||||||||||||
| 2 | +# 将 InferNex Chart 下载/缓存到本地;checker 仅 --with-checker 时下载。 | ||||||||||||||||||||||||||||||||||||||||||
| 3 | +# Skill 与 git 仓库解耦:安装于任意位置(npx、powers 等)后均可运行。 | ||||||||||||||||||||||||||||||||||||||||||
| 4 | +set -euo pipefail | ||||||||||||||||||||||||||||||||||||||||||
| 5 | + | ||||||||||||||||||||||||||||||||||||||||||
| 6 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||||||||||||||||||||||||||||||||||||||||||
| 7 | +SKILL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" | ||||||||||||||||||||||||||||||||||||||||||
| 8 | + | ||||||||||||||||||||||||||||||||||||||||||
| 9 | +WITH_CHECKER=0 | ||||||||||||||||||||||||||||||||||||||||||
| 10 | +REFRESH_CHECKER=0 | ||||||||||||||||||||||||||||||||||||||||||
| 11 | +for arg in "$@"; do | ||||||||||||||||||||||||||||||||||||||||||
| 12 | + case "$arg" in | ||||||||||||||||||||||||||||||||||||||||||
| 13 | + --with-checker) WITH_CHECKER=1 ;; | ||||||||||||||||||||||||||||||||||||||||||
| 14 | + --refresh-checker) REFRESH_CHECKER=1 ;; | ||||||||||||||||||||||||||||||||||||||||||
| 15 | + -h|--help) | ||||||||||||||||||||||||||||||||||||||||||
| 16 | + echo "用法: bootstrap.sh [--with-checker] [--refresh-checker]" | ||||||||||||||||||||||||||||||||||||||||||
| 17 | + echo " 默认只拉 Chart。用户指定跑 infernex-checker 时加 --with-checker。" | ||||||||||||||||||||||||||||||||||||||||||
| 18 | + echo " --refresh-checker 丢弃已缓存的 checker 二进制并重新下载(旧版无 --enable-connectivity-check 时用)。" | ||||||||||||||||||||||||||||||||||||||||||
| 19 | + exit 0 | ||||||||||||||||||||||||||||||||||||||||||
| 20 | + ;; | ||||||||||||||||||||||||||||||||||||||||||
| 21 | + esac | ||||||||||||||||||||||||||||||||||||||||||
| 22 | +done | ||||||||||||||||||||||||||||||||||||||||||
| 23 | +if [[ "${INFERNEX_RUN_CHECKER:-0}" == "1" ]]; then | ||||||||||||||||||||||||||||||||||||||||||
| 24 | + WITH_CHECKER=1 | ||||||||||||||||||||||||||||||||||||||||||
| 25 | +fi | ||||||||||||||||||||||||||||||||||||||||||
| 26 | + | ||||||||||||||||||||||||||||||||||||||||||
| 27 | +CACHE_ROOT="${INFERNEX_CACHE_DIR:-${HOME}/.infernex/cache}" | ||||||||||||||||||||||||||||||||||||||||||
| 28 | +CHART_VERSION="${INFERNEX_CHART_VERSION:-0.0.0-latest}" | ||||||||||||||||||||||||||||||||||||||||||
| 29 | +CHART_OCI="${INFERNEX_CHART_OCI:-oci://cr.openfuyao.cn/charts/infernex}" | ||||||||||||||||||||||||||||||||||||||||||
| 30 | +CHECKER_VERSION="${INFERNEX_CHECKER_VERSION:-latest}" | ||||||||||||||||||||||||||||||||||||||||||
| 31 | + | ||||||||||||||||||||||||||||||||||||||||||
| 32 | +mkdir -p "${CACHE_ROOT}/bin" "${CACHE_ROOT}/chart" | ||||||||||||||||||||||||||||||||||||||||||
| 33 | + | ||||||||||||||||||||||||||||||||||||||||||
| 34 | +# --- Chart (OCI default) --- | ||||||||||||||||||||||||||||||||||||||||||
| 35 | +CHART_DIR="${INFERNEX_CHART_DIR:-${CACHE_ROOT}/chart/${CHART_VERSION}}" | ||||||||||||||||||||||||||||||||||||||||||
| 36 | + | ||||||||||||||||||||||||||||||||||||||||||
| 37 | +# 浮动 tag(0.0.0-latest 等)内容随时间漂移:缓存只判存在会用到过期 Chart, | ||||||||||||||||||||||||||||||||||||||||||
| 38 | +# 故浮动 tag 每次重新拉取;固定版本号内容不可变,命中缓存直接用。 | ||||||||||||||||||||||||||||||||||||||||||
| 39 | +# 判定规则:严格 semver(如 26.9.0-rc.2)视为固定版本;其余一律视为浮动(latest/dev/nightly/master…)。 | ||||||||||||||||||||||||||||||||||||||||||
| 40 | +FLOATING=1 | ||||||||||||||||||||||||||||||||||||||||||
| 41 | +if [[ "${CHART_VERSION}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then | ||||||||||||||||||||||||||||||||||||||||||
| 42 | + FLOATING=0 | ||||||||||||||||||||||||||||||||||||||||||
| 43 | +fi | ||||||||||||||||||||||||||||||||||||||||||
| 44 | + | ||||||||||||||||||||||||||||||||||||||||||
| 45 | +pull_chart() { | ||||||||||||||||||||||||||||||||||||||||||
| 46 | + local PULL_DIR="${CACHE_ROOT}/chart/.pull-${CHART_VERSION}" | ||||||||||||||||||||||||||||||||||||||||||
| 47 | + rm -rf "${PULL_DIR}" | ||||||||||||||||||||||||||||||||||||||||||
| 48 | + mkdir -p "${PULL_DIR}" | ||||||||||||||||||||||||||||||||||||||||||
| 49 | + helm pull "${CHART_OCI}" --version "${CHART_VERSION}" -d "${PULL_DIR}" || return 1 | ||||||||||||||||||||||||||||||||||||||||||
| 50 | + local TGZ | ||||||||||||||||||||||||||||||||||||||||||
| 51 | + TGZ="$(ls "${PULL_DIR}"/infernex-*.tgz 2>/dev/null | head -1)" | ||||||||||||||||||||||||||||||||||||||||||
| 52 | + if [[ -z "${TGZ}" ]]; then | ||||||||||||||||||||||||||||||||||||||||||
| 53 | + rm -rf "${PULL_DIR}" | ||||||||||||||||||||||||||||||||||||||||||
| 54 | + return 1 | ||||||||||||||||||||||||||||||||||||||||||
| 55 | + fi | ||||||||||||||||||||||||||||||||||||||||||
| 56 | + rm -rf "${CHART_DIR}" | ||||||||||||||||||||||||||||||||||||||||||
| 57 | + mkdir -p "${CHART_DIR}" | ||||||||||||||||||||||||||||||||||||||||||
| 58 | + tar -xzf "${TGZ}" -C "${CHART_DIR}" --strip-components=0 | ||||||||||||||||||||||||||||||||||||||||||
| 59 | + # tgz contains top-level infernex/ directory | ||||||||||||||||||||||||||||||||||||||||||
| 60 | + if [[ -d "${CHART_DIR}/infernex" ]]; then | ||||||||||||||||||||||||||||||||||||||||||
| 61 | + mv "${CHART_DIR}/infernex"/* "${CHART_DIR}/" | ||||||||||||||||||||||||||||||||||||||||||
| 62 | + rdir="${CHART_DIR}/infernex" | ||||||||||||||||||||||||||||||||||||||||||
[严重] FLOATING 正则错误地将 改动建议
![]() ![]() | |||||||||||||||||||||||||||||||||||||||||||
| 63 | + rmdir "${rdir}" 2>/dev/null || true | ||||||||||||||||||||||||||||||||||||||||||
浮空版本检测正则缺陷: ![]() ![]() | |||||||||||||||||||||||||||||||||||||||||||
| 64 | + fi | ||||||||||||||||||||||||||||||||||||||||||
| 65 | + rm -rf "${PULL_DIR}" | ||||||||||||||||||||||||||||||||||||||||||
使用 date -r 非 POSIX,macOS 下不可用。建议改用 stat -c %y(Linux)+ stat -f %m(macOS)条件判断,或用 Python os.path.getmtime。 ![]() ![]() | |||||||||||||||||||||||||||||||||||||||||||
| 66 | + echo "Chart 已缓存(最新):${CHART_DIR}" | ||||||||||||||||||||||||||||||||||||||||||
| 67 | +} | ||||||||||||||||||||||||||||||||||||||||||
| 68 | + | ||||||||||||||||||||||||||||||||||||||||||
| 69 | +if [[ -n "${INFERNEX_CHART_DIR:-}" && -f "${INFERNEX_CHART_DIR}/Chart.yaml" ]]; then | ||||||||||||||||||||||||||||||||||||||||||
| 70 | + echo "使用已有 INFERNEX_CHART_DIR=${INFERNEX_CHART_DIR}" | ||||||||||||||||||||||||||||||||||||||||||
| 71 | +elif [[ "${FLOATING}" == "0" && -f "${CHART_DIR}/Chart.yaml" ]]; then | ||||||||||||||||||||||||||||||||||||||||||
| 72 | + echo "使用缓存 Chart(固定版本,内容不可变):${CHART_DIR}" | ||||||||||||||||||||||||||||||||||||||||||
| 73 | +else | ||||||||||||||||||||||||||||||||||||||||||
| 74 | + if [[ "${FLOATING}" == "1" ]]; then | ||||||||||||||||||||||||||||||||||||||||||
| 75 | + echo "浮动版本 ${CHART_VERSION}:每次拉取最新 Chart ..." | ||||||||||||||||||||||||||||||||||||||||||
| 76 | + else | ||||||||||||||||||||||||||||||||||||||||||
| 77 | + echo "正在拉取 Chart ${CHART_OCI} 版本 ${CHART_VERSION} ..." | ||||||||||||||||||||||||||||||||||||||||||
| 78 | + fi | ||||||||||||||||||||||||||||||||||||||||||
| 79 | + if ! pull_chart; then | ||||||||||||||||||||||||||||||||||||||||||
| 80 | + if [[ "${FLOATING}" == "1" && -f "${CHART_DIR}/Chart.yaml" ]]; then | ||||||||||||||||||||||||||||||||||||||||||
| 81 | + MTIME="$(date -r "${CHART_DIR}/Chart.yaml" '+%Y-%m-%d' 2>/dev/null || echo unknown)" | ||||||||||||||||||||||||||||||||||||||||||
| 82 | + echo "⚠️ 远端不可达,回退使用缓存于 ${MTIME} 的 Chart(可能非最新):${CHART_DIR}" >&2 | ||||||||||||||||||||||||||||||||||||||||||
| 83 | + else | ||||||||||||||||||||||||||||||||||||||||||
| 84 | + echo "helm pull 失败:${CHART_OCI} @ ${CHART_VERSION}" >&2 | ||||||||||||||||||||||||||||||||||||||||||
| 85 | + exit 1 | ||||||||||||||||||||||||||||||||||||||||||
| 86 | + fi | ||||||||||||||||||||||||||||||||||||||||||
| 87 | + fi | ||||||||||||||||||||||||||||||||||||||||||
curl 下载缺少 --fail 选项,HTTP 错误(如 404)时不会返回非零退出码,可能导致下载错误内容后继续执行。 改动建议
![]() ![]() | |||||||||||||||||||||||||||||||||||||||||||
| 88 | +fi | ||||||||||||||||||||||||||||||||||||||||||
| 89 | + | ||||||||||||||||||||||||||||||||||||||||||
| 90 | +# --- infernex-checker binary(默认跳过) --- | ||||||||||||||||||||||||||||||||||||||||||
| 91 | +CHECKER_BIN="${INFERNEX_CHECKER:-${CACHE_ROOT}/bin/infernex-checker}" | ||||||||||||||||||||||||||||||||||||||||||
| 92 | + | ||||||||||||||||||||||||||||||||||||||||||
[轻微] pull_chart 未处理 tar 解压失败的明确报错。 改动建议
![]() ![]() | |||||||||||||||||||||||||||||||||||||||||||
| 93 | +if [[ "${WITH_CHECKER}" == "1" ]]; then | ||||||||||||||||||||||||||||||||||||||||||
| 94 | + if [[ "${REFRESH_CHECKER}" == "1" && -z "${INFERNEX_CHECKER:-}" ]]; then | ||||||||||||||||||||||||||||||||||||||||||
| 95 | + rm -f "${CHECKER_BIN}" | ||||||||||||||||||||||||||||||||||||||||||
| 96 | + fi | ||||||||||||||||||||||||||||||||||||||||||
| 97 | + if [[ -n "${INFERNEX_CHECKER:-}" && -x "${INFERNEX_CHECKER}" ]]; then | ||||||||||||||||||||||||||||||||||||||||||
| 98 | + echo "使用 INFERNEX_CHECKER=${INFERNEX_CHECKER}" | ||||||||||||||||||||||||||||||||||||||||||
| 99 | + elif [[ -x "${CHECKER_BIN}" ]]; then | ||||||||||||||||||||||||||||||||||||||||||
| 100 | + echo "使用缓存 checker:${CHECKER_BIN}" | ||||||||||||||||||||||||||||||||||||||||||
| 101 | + else | ||||||||||||||||||||||||||||||||||||||||||
| 102 | + ARCH="$(uname -m)" | ||||||||||||||||||||||||||||||||||||||||||
| 103 | + case "${ARCH}" in | ||||||||||||||||||||||||||||||||||||||||||
| 104 | + x86_64|amd64) DL_ARCH=amd64 ;; | ||||||||||||||||||||||||||||||||||||||||||
| 105 | + aarch64|arm64) DL_ARCH=arm64 ;; | ||||||||||||||||||||||||||||||||||||||||||
| 106 | + *) | ||||||||||||||||||||||||||||||||||||||||||
| 107 | + echo "不支持的架构:${ARCH}" >&2 | ||||||||||||||||||||||||||||||||||||||||||
| 108 | + exit 1 | ||||||||||||||||||||||||||||||||||||||||||
[重要] 修复建议:(a) 在 releases 发布时附带 ![]() ![]() | |||||||||||||||||||||||||||||||||||||||||||
| 109 | + ;; | ||||||||||||||||||||||||||||||||||||||||||
| 110 | + esac | ||||||||||||||||||||||||||||||||||||||||||
[轻微] bootstrap.sh 未校验 curl 可用性。建议增加 command -v curl >/dev/null 2>&1 检查。 ![]() ![]() | |||||||||||||||||||||||||||||||||||||||||||
| 111 | + URL="https://static.openfuyao.cn/openFuyao/infernex/releases/download/${CHECKER_VERSION}/bin/linux/${DL_ARCH}/infernex-checker" | ||||||||||||||||||||||||||||||||||||||||||
| 112 | + echo "正在下载 infernex-checker (${DL_ARCH}) ..." | ||||||||||||||||||||||||||||||||||||||||||
| 113 | + curl -fsSL "${URL}" -o "${CHECKER_BIN}" | ||||||||||||||||||||||||||||||||||||||||||
| 114 | + chmod +x "${CHECKER_BIN}" | ||||||||||||||||||||||||||||||||||||||||||
| 115 | + echo "checker 已缓存:${CHECKER_BIN}" | ||||||||||||||||||||||||||||||||||||||||||
| 116 | + fi | ||||||||||||||||||||||||||||||||||||||||||
| 117 | +else | ||||||||||||||||||||||||||||||||||||||||||
| 118 | + echo "跳过 checker 下载(默认不执行;用户指定时:bootstrap.sh --with-checker)" | ||||||||||||||||||||||||||||||||||||||||||
| 119 | +fi | ||||||||||||||||||||||||||||||||||||||||||
| 120 | + | ||||||||||||||||||||||||||||||||||||||||||
| 121 | +# --- env file for subsequent steps --- | ||||||||||||||||||||||||||||||||||||||||||
| 122 | +ENV_FILE="${CACHE_ROOT}/env.sh" | ||||||||||||||||||||||||||||||||||||||||||
| 123 | +cat > "${ENV_FILE}" <<EOF | ||||||||||||||||||||||||||||||||||||||||||
| 124 | +# Generated by infernex-deploy bootstrap.sh — 部署/预检前 source | ||||||||||||||||||||||||||||||||||||||||||
| 125 | +export INFERNEX_CACHE_DIR="${CACHE_ROOT}" | ||||||||||||||||||||||||||||||||||||||||||
| 126 | +export INFERNEX_SKILL_DIR="${SKILL_DIR}" | ||||||||||||||||||||||||||||||||||||||||||
| 127 | +export INFERNEX_CHART_DIR="${CHART_DIR}" | ||||||||||||||||||||||||||||||||||||||||||
| 128 | +export INFERNEX_CHART_VERSION="${CHART_VERSION}" | ||||||||||||||||||||||||||||||||||||||||||
| 129 | +export INFERNEX_CHECKER="${CHECKER_BIN}" | ||||||||||||||||||||||||||||||||||||||||||
| 130 | +export INFERNEX_NPU_SCRIPT="${SKILL_DIR}/scripts/npu_capacity.py" | ||||||||||||||||||||||||||||||||||||||||||
| 131 | +export INFERNEX_SLUGS_SCRIPT="${SKILL_DIR}/scripts/slugs.py" | ||||||||||||||||||||||||||||||||||||||||||
| 132 | +export INFERNEX_NODES_TEMPLATE="${SKILL_DIR}/templates/nodes.yaml.tpl" | ||||||||||||||||||||||||||||||||||||||||||
| 133 | +export INFERNEX_RUN_CHECKER="${WITH_CHECKER}" | ||||||||||||||||||||||||||||||||||||||||||
| 134 | +EOF | ||||||||||||||||||||||||||||||||||||||||||
| 135 | + | ||||||||||||||||||||||||||||||||||||||||||
| 136 | +echo "依赖准备完成。请执行:source ${ENV_FILE}" | ||||||||||||||||||||||||||||||||||||||||||
| 137 | +echo " INFERNEX_CHART_DIR=${CHART_DIR}" | ||||||||||||||||||||||||||||||||||||||||||
| 138 | +echo " INFERNEX_CHECKER=${CHECKER_BIN}" | ||||||||||||||||||||||||||||||||||||||||||
| 139 | +echo " INFERNEX_RUN_CHECKER=${WITH_CHECKER}" | ||||||||||||||||||||||||||||||||||||||||||
| @@ -0,0 +1,280 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| 1 | +#!/usr/bin/env python3 | ||||||||||||||||||||||||||||||||||||||||||||||
| 2 | +# Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||||||||||||||||||||||||||||||||||||||||||||||
| 3 | +# openFuyao is licensed under Mulan PSL v2. | ||||||||||||||||||||||||||||||||||||||||||||||
| 4 | +# You can use this software according to the terms and conditions of the Mulan PSL v2. | ||||||||||||||||||||||||||||||||||||||||||||||
| 5 | +# You may obtain a copy of Mulan PSL v2 at: | ||||||||||||||||||||||||||||||||||||||||||||||
| 6 | +# http://license.coscl.org.cn/MulanPSL2 | ||||||||||||||||||||||||||||||||||||||||||||||
| 7 | +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | ||||||||||||||||||||||||||||||||||||||||||||||
| 8 | +# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||||||||||||||||||||||||||||||||||||||||||||||
| 9 | +# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||||||||||||||||||||||||||||||||||||||||||||||
| 10 | +# See the Mulan PSL v2 for more details. | ||||||||||||||||||||||||||||||||||||||||||||||
| 11 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 12 | +"""计算 InferNex NPU 需求 vs 可用(S-18)。 | ||||||||||||||||||||||||||||||||||||||||||||||
| 13 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 14 | +需求(required):由 values.yaml 的 services[].replicas × dp × tp × pp 求和(与 Chart | ||||||||||||||||||||||||||||||||||||||||||||||
| 15 | +inference-backend _helpers.tpl 对齐),纯本地计算。 | ||||||||||||||||||||||||||||||||||||||||||||||
| 16 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 17 | +可用量(available)按优先级取: | ||||||||||||||||||||||||||||||||||||||||||||||
| 18 | + 1. --checker 存在且文件在 → checker 结果 H-04(主机层 npu-smi 数据,最精确) | ||||||||||||||||||||||||||||||||||||||||||||||
| 19 | + 2. npu-exporter metrics(默认)——npu-operator 是部署前置条件,其 npu-exporter | ||||||||||||||||||||||||||||||||||||||||||||||
| 20 | + DaemonSet 每节点一个 Pod(8082 端口暴露 metrics);空闲卡 = npu_chip_info_* | ||||||||||||||||||||||||||||||||||||||||||||||
| 21 | + 覆盖的全部卡 id − container_npu_* 中带容器标签的在用卡 id | ||||||||||||||||||||||||||||||||||||||||||||||
| 22 | + 3. 都不可用 → 只输出需求,不判定(skipped) | ||||||||||||||||||||||||||||||||||||||||||||||
| 23 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 24 | +用法: | ||||||||||||||||||||||||||||||||||||||||||||||
| 25 | + python3 npu_capacity.py \ | ||||||||||||||||||||||||||||||||||||||||||||||
| 26 | + --values .infernex/deploy/<ts>/values.yaml \ | ||||||||||||||||||||||||||||||||||||||||||||||
| 27 | + [--nodes .infernex/deploy/<ts>/nodes.yaml] \ | ||||||||||||||||||||||||||||||||||||||||||||||
| 28 | + [--checker .infernex/deploy/<ts>/checker-result.json] \ | ||||||||||||||||||||||||||||||||||||||||||||||
| 29 | + [--exporter-port 8082] | ||||||||||||||||||||||||||||||||||||||||||||||
| 30 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 31 | +退出码: 0=够用或无可用量数据; 1=可用量不足; 2=输入错误。 | ||||||||||||||||||||||||||||||||||||||||||||||
| 32 | +""" | ||||||||||||||||||||||||||||||||||||||||||||||
| 33 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 34 | +from __future__ import annotations | ||||||||||||||||||||||||||||||||||||||||||||||
| 35 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 36 | +import argparse | ||||||||||||||||||||||||||||||||||||||||||||||
| 37 | +import json | ||||||||||||||||||||||||||||||||||||||||||||||
| 38 | +import re | ||||||||||||||||||||||||||||||||||||||||||||||
| 39 | +import subprocess | ||||||||||||||||||||||||||||||||||||||||||||||
| 40 | +import sys | ||||||||||||||||||||||||||||||||||||||||||||||
| 41 | +import urllib.request | ||||||||||||||||||||||||||||||||||||||||||||||
| 42 | +from pathlib import Path | ||||||||||||||||||||||||||||||||||||||||||||||
| 43 | +from typing import Any | ||||||||||||||||||||||||||||||||||||||||||||||
| 44 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 45 | +try: | ||||||||||||||||||||||||||||||||||||||||||||||
| 46 | + import yaml | ||||||||||||||||||||||||||||||||||||||||||||||
| 47 | +except ImportError: | ||||||||||||||||||||||||||||||||||||||||||||||
| 48 | + print(json.dumps({"error": "需要 PyYAML:pip install pyyaml", "sufficient": False}, | ||||||||||||||||||||||||||||||||||||||||||||||
| 49 | + ensure_ascii=False), file=sys.stderr) | ||||||||||||||||||||||||||||||||||||||||||||||
| 50 | + sys.exit(2) | ||||||||||||||||||||||||||||||||||||||||||||||
| 51 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 52 | +EXPORTER_PORT = 8082 | ||||||||||||||||||||||||||||||||||||||||||||||
| 53 | +METRICS_TIMEOUT = 10.0 | ||||||||||||||||||||||||||||||||||||||||||||||
| 54 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 55 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 56 | +def _int(v, default=1): | ||||||||||||||||||||||||||||||||||||||||||||||
| 57 | + if v is None: | ||||||||||||||||||||||||||||||||||||||||||||||
| 58 | + return default | ||||||||||||||||||||||||||||||||||||||||||||||
| 59 | + return int(v) | ||||||||||||||||||||||||||||||||||||||||||||||
| 60 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 61 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 62 | +def role_npu(role): | ||||||||||||||||||||||||||||||||||||||||||||||
| 63 | + replicas = _int(role.get("replicas"), 1) | ||||||||||||||||||||||||||||||||||||||||||||||
| 64 | + tp = _int(role.get("tensorParallelSize"), 1) | ||||||||||||||||||||||||||||||||||||||||||||||
| 65 | + pp = _int(role.get("pipelineParallelSize"), 1) | ||||||||||||||||||||||||||||||||||||||||||||||
| 66 | + dp = _int(role.get("dataParallelSize"), 1) | ||||||||||||||||||||||||||||||||||||||||||||||
| 67 | + dp_local = _int(role.get("dataParallelSizeLocal"), 1) | ||||||||||||||||||||||||||||||||||||||||||||||
| 68 | + if dp_local < 1: | ||||||||||||||||||||||||||||||||||||||||||||||
| 69 | + dp_local = 1 | ||||||||||||||||||||||||||||||||||||||||||||||
| 70 | + if "cardCount" in role and role.get("cardCount") is not None: | ||||||||||||||||||||||||||||||||||||||||||||||
| 71 | + # Chart 特性:显式 cardCount = 每 Pod 卡数;此时 LWS 组内 worker 数为 dp // dp_local, | ||||||||||||||||||||||||||||||||||||||||||||||
| 72 | + # 单 LWS 副本总卡数 = lws_size * cardCount(与 Chart _helpers.tpl 的 sizing 逻辑对齐) | ||||||||||||||||||||||||||||||||||||||||||||||
| 73 | + cards_per_pod = _int(role["cardCount"], 0) | ||||||||||||||||||||||||||||||||||||||||||||||
| 74 | + lws_size = dp // dp_local if dp_local else 1 | ||||||||||||||||||||||||||||||||||||||||||||||
| 75 | + return replicas * lws_size * cards_per_pod | ||||||||||||||||||||||||||||||||||||||||||||||
| 76 | + return replicas * dp * tp * pp | ||||||||||||||||||||||||||||||||||||||||||||||
| 77 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 78 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 79 | +def service_npu(svc): | ||||||||||||||||||||||||||||||||||||||||||||||
| 80 | + breakdown = {} | ||||||||||||||||||||||||||||||||||||||||||||||
| 81 | + mode = svc.get("mode", "pd") | ||||||||||||||||||||||||||||||||||||||||||||||
| 82 | + if mode == "pd": | ||||||||||||||||||||||||||||||||||||||||||||||
| 83 | + pd = svc.get("pd") or {} | ||||||||||||||||||||||||||||||||||||||||||||||
| 84 | + for role in ("prefill", "decode"): | ||||||||||||||||||||||||||||||||||||||||||||||
| 85 | + breakdown[role] = role_npu(pd.get(role) or {}) | ||||||||||||||||||||||||||||||||||||||||||||||
| 86 | + elif mode == "aggregated": | ||||||||||||||||||||||||||||||||||||||||||||||
| 87 | + breakdown["aggregated"] = role_npu(svc.get("aggregated") or {}) | ||||||||||||||||||||||||||||||||||||||||||||||
| 88 | + return sum(breakdown.values()), breakdown | ||||||||||||||||||||||||||||||||||||||||||||||
| 89 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 90 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 91 | +def required_from_values(values): | ||||||||||||||||||||||||||||||||||||||||||||||
| 92 | + backend = values.get("inference-backend") or values.get("inference_backend") or {} | ||||||||||||||||||||||||||||||||||||||||||||||
| 93 | + services = backend.get("services") or [] | ||||||||||||||||||||||||||||||||||||||||||||||
| 94 | + details = [] | ||||||||||||||||||||||||||||||||||||||||||||||
| 95 | + total = 0 | ||||||||||||||||||||||||||||||||||||||||||||||
| 96 | + for svc in services: | ||||||||||||||||||||||||||||||||||||||||||||||
| 97 | + if not svc.get("enabled", True): | ||||||||||||||||||||||||||||||||||||||||||||||
| 98 | + continue | ||||||||||||||||||||||||||||||||||||||||||||||
| 99 | + n, breakdown = service_npu(svc) | ||||||||||||||||||||||||||||||||||||||||||||||
| 100 | + total += n | ||||||||||||||||||||||||||||||||||||||||||||||
| 101 | + details.append({ | ||||||||||||||||||||||||||||||||||||||||||||||
| 102 | + "service": svc.get("name", "unknown"), | ||||||||||||||||||||||||||||||||||||||||||||||
| 103 | + "mode": svc.get("mode", "pd"), | ||||||||||||||||||||||||||||||||||||||||||||||
| 104 | + "required": n, | ||||||||||||||||||||||||||||||||||||||||||||||
| 105 | + "breakdown": breakdown, | ||||||||||||||||||||||||||||||||||||||||||||||
| 106 | + }) | ||||||||||||||||||||||||||||||||||||||||||||||
| 107 | + return total, details | ||||||||||||||||||||||||||||||||||||||||||||||
| 108 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 109 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 110 | +def load_nodes(path): | ||||||||||||||||||||||||||||||||||||||||||||||
| 111 | + if not path or not path.is_file(): | ||||||||||||||||||||||||||||||||||||||||||||||
| 112 | + return set() | ||||||||||||||||||||||||||||||||||||||||||||||
| 113 | + with path.open(encoding="utf-8") as f: | ||||||||||||||||||||||||||||||||||||||||||||||
| 114 | + data = yaml.safe_load(f) or {} | ||||||||||||||||||||||||||||||||||||||||||||||
| 115 | + return {str(n["name"]) for n in data.get("nodes") or [] if n.get("name")} | ||||||||||||||||||||||||||||||||||||||||||||||
| 116 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 117 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 118 | +def available_from_checker(checker, node_filter): | ||||||||||||||||||||||||||||||||||||||||||||||
| 119 | + per_node = {} | ||||||||||||||||||||||||||||||||||||||||||||||
| 120 | + hw = checker.get("hardware") or {} | ||||||||||||||||||||||||||||||||||||||||||||||
| 121 | + for nr in hw.get("nodes") or []: | ||||||||||||||||||||||||||||||||||||||||||||||
| 122 | + name = nr.get("name") or "" | ||||||||||||||||||||||||||||||||||||||||||||||
| 123 | + if node_filter and name not in node_filter: | ||||||||||||||||||||||||||||||||||||||||||||||
| 124 | + continue | ||||||||||||||||||||||||||||||||||||||||||||||
| 125 | + for chk in nr.get("checks") or []: | ||||||||||||||||||||||||||||||||||||||||||||||
| 126 | + if chk.get("id") != "H-04": | ||||||||||||||||||||||||||||||||||||||||||||||
| 127 | + continue | ||||||||||||||||||||||||||||||||||||||||||||||
| 128 | + detail = chk.get("detail") or {} | ||||||||||||||||||||||||||||||||||||||||||||||
| 129 | + avail = detail.get("available") | ||||||||||||||||||||||||||||||||||||||||||||||
| 130 | + if avail is None: | ||||||||||||||||||||||||||||||||||||||||||||||
| 131 | + msg = chk.get("message") or "" | ||||||||||||||||||||||||||||||||||||||||||||||
| 132 | + if "available:" in msg: | ||||||||||||||||||||||||||||||||||||||||||||||
| 133 | + try: | ||||||||||||||||||||||||||||||||||||||||||||||
| 134 | + avail = int(msg.split("available:")[-1].strip().split("/")[0].strip()) | ||||||||||||||||||||||||||||||||||||||||||||||
| 135 | + except ValueError: | ||||||||||||||||||||||||||||||||||||||||||||||
| 136 | + avail = 0 | ||||||||||||||||||||||||||||||||||||||||||||||
| 137 | + else: | ||||||||||||||||||||||||||||||||||||||||||||||
| 138 | + avail = 0 | ||||||||||||||||||||||||||||||||||||||||||||||
[轻微] 修复建议:将 ![]() ![]() | |||||||||||||||||||||||||||||||||||||||||||||||
| 139 | + per_node[name] = int(avail) | ||||||||||||||||||||||||||||||||||||||||||||||
| 140 | + return sum(per_node.values()), per_node | ||||||||||||||||||||||||||||||||||||||||||||||
| 141 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 142 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 143 | +# ---- npu-exporter 路径 ---- | ||||||||||||||||||||||||||||||||||||||||||||||
| 144 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 145 | +def fetch_exporter_pods(): | ||||||||||||||||||||||||||||||||||||||||||||||
| 146 | + """kubectl 找 npu-exporter Pod(namespace 不固定,按名字匹配),返回 [{ip, node}]。""" | ||||||||||||||||||||||||||||||||||||||||||||||
| 147 | + proc = subprocess.run(["kubectl", "get", "pods", "-A", "-o", "json"], | ||||||||||||||||||||||||||||||||||||||||||||||
| 148 | + capture_output=True, text=True, check=False) | ||||||||||||||||||||||||||||||||||||||||||||||
| 149 | + if proc.returncode != 0: | ||||||||||||||||||||||||||||||||||||||||||||||
| 150 | + return [] | ||||||||||||||||||||||||||||||||||||||||||||||
| 151 | + try: | ||||||||||||||||||||||||||||||||||||||||||||||
| 152 | + items = json.loads(proc.stdout).get("items") or [] | ||||||||||||||||||||||||||||||||||||||||||||||
| 153 | + except json.JSONDecodeError: | ||||||||||||||||||||||||||||||||||||||||||||||
| 154 | + return [] | ||||||||||||||||||||||||||||||||||||||||||||||
| 155 | + out = [] | ||||||||||||||||||||||||||||||||||||||||||||||
| 156 | + for p in items: | ||||||||||||||||||||||||||||||||||||||||||||||
| 157 | + meta = p.get("metadata") or {} | ||||||||||||||||||||||||||||||||||||||||||||||
| 158 | + if "npu-exporter" not in str(meta.get("name", "")): | ||||||||||||||||||||||||||||||||||||||||||||||
| 159 | + continue | ||||||||||||||||||||||||||||||||||||||||||||||
| 160 | + status = p.get("status") or {} | ||||||||||||||||||||||||||||||||||||||||||||||
| 161 | + if status.get("phase") != "Running": | ||||||||||||||||||||||||||||||||||||||||||||||
| 162 | + continue | ||||||||||||||||||||||||||||||||||||||||||||||
| 163 | + ip = status.get("podIP") | ||||||||||||||||||||||||||||||||||||||||||||||
| 164 | + node = (p.get("spec") or {}).get("nodeName") | ||||||||||||||||||||||||||||||||||||||||||||||
| 165 | + if ip and node: | ||||||||||||||||||||||||||||||||||||||||||||||
| 166 | + out.append({"ip": ip, "node": node}) | ||||||||||||||||||||||||||||||||||||||||||||||
| 167 | + return out | ||||||||||||||||||||||||||||||||||||||||||||||
| 168 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 169 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 170 | +_ID_RE = re.compile(r'\bid="([^"]+)"') | ||||||||||||||||||||||||||||||||||||||||||||||
| 171 | +_LINE_RE = re.compile(r"^(\w+)\{([^}]*)\}\s") | ||||||||||||||||||||||||||||||||||||||||||||||
| 172 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 173 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 174 | +def parse_exporter_metrics(body): | ||||||||||||||||||||||||||||||||||||||||||||||
| 175 | + """解析 metrics 文本 → (全部卡 id, 在用卡 id)。纯函数,可单测。""" | ||||||||||||||||||||||||||||||||||||||||||||||
| 176 | + all_ids, used_ids = set(), set() | ||||||||||||||||||||||||||||||||||||||||||||||
| 177 | + for line in body.splitlines(): | ||||||||||||||||||||||||||||||||||||||||||||||
| 178 | + m = _LINE_RE.match(line) | ||||||||||||||||||||||||||||||||||||||||||||||
| 179 | + if not m: | ||||||||||||||||||||||||||||||||||||||||||||||
| 180 | + continue | ||||||||||||||||||||||||||||||||||||||||||||||
| 181 | + metric, labels = m.group(1), m.group(2) | ||||||||||||||||||||||||||||||||||||||||||||||
| 182 | + idm = _ID_RE.search(labels) | ||||||||||||||||||||||||||||||||||||||||||||||
| 183 | + if not idm: | ||||||||||||||||||||||||||||||||||||||||||||||
[重要] parse_exporter_metrics 正则对 OpenMetrics 格式过于脆弱。 改动建议
![]() ![]() | |||||||||||||||||||||||||||||||||||||||||||||||
| 184 | + continue | ||||||||||||||||||||||||||||||||||||||||||||||
| 185 | + if metric.startswith("npu_chip_info_"): | ||||||||||||||||||||||||||||||||||||||||||||||
| 186 | + all_ids.add(idm.group(1)) | ||||||||||||||||||||||||||||||||||||||||||||||
| 187 | + elif metric.startswith("container_npu_"): | ||||||||||||||||||||||||||||||||||||||||||||||
| 188 | + used_ids.add(idm.group(1)) | ||||||||||||||||||||||||||||||||||||||||||||||
| 189 | + return all_ids, used_ids | ||||||||||||||||||||||||||||||||||||||||||||||
| 190 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 191 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 192 | +def fetch_available_from_exporter(ip, port): | ||||||||||||||||||||||||||||||||||||||||||||||
| 193 | + url = f"http://{ip}:{port}/metrics" | ||||||||||||||||||||||||||||||||||||||||||||||
| 194 | + try: | ||||||||||||||||||||||||||||||||||||||||||||||
| 195 | + with urllib.request.urlopen(url, timeout=METRICS_TIMEOUT) as resp: | ||||||||||||||||||||||||||||||||||||||||||||||
| 196 | + body = resp.read().decode("utf-8", errors="replace") | ||||||||||||||||||||||||||||||||||||||||||||||
| 197 | + except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||
| 198 | + return None, {"error": f"{type(e).__name__}: {e}"} | ||||||||||||||||||||||||||||||||||||||||||||||
| 199 | + all_ids, used_ids = parse_exporter_metrics(body) | ||||||||||||||||||||||||||||||||||||||||||||||
| 200 | + if not all_ids: | ||||||||||||||||||||||||||||||||||||||||||||||
| 201 | + return None, {"error": "metrics 中无 npu_chip_info_*(exporter 异常?)"} | ||||||||||||||||||||||||||||||||||||||||||||||
| 202 | + available_ids = sorted(all_ids - used_ids, key=lambda x: (len(x), x)) | ||||||||||||||||||||||||||||||||||||||||||||||
| 203 | + return len(available_ids), { | ||||||||||||||||||||||||||||||||||||||||||||||
| 204 | + "total": len(all_ids), "used": len(all_ids & used_ids), | ||||||||||||||||||||||||||||||||||||||||||||||
| 205 | + "available_ids": available_ids, | ||||||||||||||||||||||||||||||||||||||||||||||
| 206 | + } | ||||||||||||||||||||||||||||||||||||||||||||||
| 207 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 208 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 209 | +def available_from_exporters(node_filter, port): | ||||||||||||||||||||||||||||||||||||||||||||||
| 210 | + pods = fetch_exporter_pods() | ||||||||||||||||||||||||||||||||||||||||||||||
| 211 | + if node_filter: | ||||||||||||||||||||||||||||||||||||||||||||||
| 212 | + pods = [p for p in pods if p["node"] in node_filter] | ||||||||||||||||||||||||||||||||||||||||||||||
| 213 | + if not pods: | ||||||||||||||||||||||||||||||||||||||||||||||
| 214 | + if node_filter: | ||||||||||||||||||||||||||||||||||||||||||||||
| 215 | + return None, {"error": f"nodes.yaml 中的节点 {sorted(node_filter)} 没有运行中的 npu-exporter Pod"} | ||||||||||||||||||||||||||||||||||||||||||||||
| 216 | + return None, {"error": "未发现运行中的 npu-exporter Pod(npu-operator 前置未就绪?)"} | ||||||||||||||||||||||||||||||||||||||||||||||
| 217 | + per_node, details, unreachable = {}, {}, [] | ||||||||||||||||||||||||||||||||||||||||||||||
| 218 | + for p in pods: | ||||||||||||||||||||||||||||||||||||||||||||||
| 219 | + n, detail = fetch_available_from_exporter(p["ip"], port) | ||||||||||||||||||||||||||||||||||||||||||||||
| 220 | + if n is None: | ||||||||||||||||||||||||||||||||||||||||||||||
| 221 | + unreachable.append(p["node"]) | ||||||||||||||||||||||||||||||||||||||||||||||
| 222 | + details[p["node"]] = detail | ||||||||||||||||||||||||||||||||||||||||||||||
| 223 | + continue | ||||||||||||||||||||||||||||||||||||||||||||||
| 224 | + per_node[p["node"]] = n | ||||||||||||||||||||||||||||||||||||||||||||||
| 225 | + details[p["node"]] = detail | ||||||||||||||||||||||||||||||||||||||||||||||
| 226 | + if not per_node: | ||||||||||||||||||||||||||||||||||||||||||||||
| 227 | + return None, {"error": f"所有 npu-exporter 均不可达: {unreachable}"} | ||||||||||||||||||||||||||||||||||||||||||||||
| 228 | + return sum(per_node.values()), { | ||||||||||||||||||||||||||||||||||||||||||||||
| 229 | + "per_node": per_node, "unreachable": unreachable, "details": details, | ||||||||||||||||||||||||||||||||||||||||||||||
| 230 | + } | ||||||||||||||||||||||||||||||||||||||||||||||
| 231 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 232 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 233 | +def main(): | ||||||||||||||||||||||||||||||||||||||||||||||
| 234 | + parser = argparse.ArgumentParser(description="InferNex NPU 容量检查 (S-18)") | ||||||||||||||||||||||||||||||||||||||||||||||
| 235 | + parser.add_argument("--values", required=True, type=Path) | ||||||||||||||||||||||||||||||||||||||||||||||
| 236 | + parser.add_argument("--checker", type=Path, default=None) | ||||||||||||||||||||||||||||||||||||||||||||||
| 237 | + parser.add_argument("--nodes", type=Path, default=None) | ||||||||||||||||||||||||||||||||||||||||||||||
| 238 | + parser.add_argument("--exporter-port", type=int, default=EXPORTER_PORT) | ||||||||||||||||||||||||||||||||||||||||||||||
| 239 | + args = parser.parse_args() | ||||||||||||||||||||||||||||||||||||||||||||||
| 240 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 241 | + with args.values.open(encoding="utf-8") as f: | ||||||||||||||||||||||||||||||||||||||||||||||
| 242 | + values = yaml.safe_load(f) or {} | ||||||||||||||||||||||||||||||||||||||||||||||
| 243 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 244 | + node_filter = load_nodes(args.nodes) | ||||||||||||||||||||||||||||||||||||||||||||||
| 245 | + required, service_details = required_from_values(values) | ||||||||||||||||||||||||||||||||||||||||||||||
| 246 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 247 | + out = {"check_id": "S-18", "required": required, "services": service_details} | ||||||||||||||||||||||||||||||||||||||||||||||
| 248 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 249 | + # 可用量来源优先级:checker H-04 → npu-exporter metrics → 无数据(skip 判定) | ||||||||||||||||||||||||||||||||||||||||||||||
| 250 | + if args.checker and args.checker.is_file(): | ||||||||||||||||||||||||||||||||||||||||||||||
| 251 | + with args.checker.open(encoding="utf-8") as f: | ||||||||||||||||||||||||||||||||||||||||||||||
| 252 | + checker = json.load(f) | ||||||||||||||||||||||||||||||||||||||||||||||
| 253 | + available, per_node = available_from_checker(checker, node_filter) | ||||||||||||||||||||||||||||||||||||||||||||||
| 254 | + source, extra = "checker-H04", {"per_node_available": per_node} | ||||||||||||||||||||||||||||||||||||||||||||||
| 255 | + else: | ||||||||||||||||||||||||||||||||||||||||||||||
| 256 | + available, extra = available_from_exporters(node_filter, args.exporter_port) | ||||||||||||||||||||||||||||||||||||||||||||||
| 257 | + source = "npu-exporter" if available is not None else "none" | ||||||||||||||||||||||||||||||||||||||||||||||
| 258 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 259 | + out["source"] = source | ||||||||||||||||||||||||||||||||||||||||||||||
| 260 | + out["available"] = available | ||||||||||||||||||||||||||||||||||||||||||||||
| 261 | + out.update(extra) | ||||||||||||||||||||||||||||||||||||||||||||||
| 262 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 263 | + if available is None: | ||||||||||||||||||||||||||||||||||||||||||||||
| 264 | + out["sufficient"] = True | ||||||||||||||||||||||||||||||||||||||||||||||
| 265 | + out["skipped"] = True | ||||||||||||||||||||||||||||||||||||||||||||||
| 266 | + out["skip_reason"] = extra.get("error", "no available-NPU data source") | ||||||||||||||||||||||||||||||||||||||||||||||
| 267 | + out["gap"] = None | ||||||||||||||||||||||||||||||||||||||||||||||
| 268 | + print(json.dumps(out, indent=2, ensure_ascii=False)) | ||||||||||||||||||||||||||||||||||||||||||||||
| 269 | + return 0 | ||||||||||||||||||||||||||||||||||||||||||||||
| 270 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 271 | + sufficient = available >= required if required > 0 else True | ||||||||||||||||||||||||||||||||||||||||||||||
| 272 | + out["sufficient"] = sufficient | ||||||||||||||||||||||||||||||||||||||||||||||
| 273 | + out["skipped"] = False | ||||||||||||||||||||||||||||||||||||||||||||||
| 274 | + out["gap"] = max(0, required - available) | ||||||||||||||||||||||||||||||||||||||||||||||
| 275 | + print(json.dumps(out, indent=2, ensure_ascii=False)) | ||||||||||||||||||||||||||||||||||||||||||||||
| 276 | + return 0 if sufficient else 1 | ||||||||||||||||||||||||||||||||||||||||||||||
| 277 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 278 | + | ||||||||||||||||||||||||||||||||||||||||||||||
| 279 | +if __name__ == "__main__": | ||||||||||||||||||||||||||||||||||||||||||||||
| 280 | + raise SystemExit(main()) | ||||||||||||||||||||||||||||||||||||||||||||||
| @@ -0,0 +1,367 @@ | |||||||||||||||||||||||||||||||||||
| 1 | +#!/usr/bin/env python3 | ||||||||||||||||||||||||||||||||||
| 2 | +# Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||||||||||||||||||||||||||||||||||
| 3 | +# openFuyao is licensed under Mulan PSL v2. | ||||||||||||||||||||||||||||||||||
| 4 | +# You can use this software according to the terms and conditions of the Mulan PSL v2. | ||||||||||||||||||||||||||||||||||
| 5 | +# You may obtain a copy of Mulan PSL v2 at: | ||||||||||||||||||||||||||||||||||
| 6 | +# http://license.coscl.org.cn/MulanPSL2 | ||||||||||||||||||||||||||||||||||
| 7 | +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | ||||||||||||||||||||||||||||||||||
| 8 | +# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||||||||||||||||||||||||||||||||||
| 9 | +# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||||||||||||||||||||||||||||||||||
| 10 | +# See the Mulan PSL v2 for more details. | ||||||||||||||||||||||||||||||||||
| 11 | + | ||||||||||||||||||||||||||||||||||
| 12 | +"""数据驱动的预检执行器:读 references/checks.yaml → 逐项执行 → preflight-report.json + .md。 | ||||||||||||||||||||||||||||||||||
| 13 | + | ||||||||||||||||||||||||||||||||||
| 14 | +判定逻辑全部来自 checks.yaml 的数据(when 条件 + judge 命令/checker 映射/render-check 合并), | ||||||||||||||||||||||||||||||||||
| 15 | +AI 的职责 = 运行本脚本 + 读报告;**禁止**绕过执行器按自然语言自行判定(SKILL.md 硬门禁 4)。 | ||||||||||||||||||||||||||||||||||
| 16 | + | ||||||||||||||||||||||||||||||||||
| 17 | +用法: | ||||||||||||||||||||||||||||||||||
| 18 | + python3 preflight_run.py \\ | ||||||||||||||||||||||||||||||||||
| 19 | + --deploy-dir .infernex/deploy/<ts> \\ | ||||||||||||||||||||||||||||||||||
| 20 | + --namespace ai-inference --release infernex \\ | ||||||||||||||||||||||||||||||||||
| 21 | + [--checks <skill>/references/checks.yaml] [--skill-dir <skill>] \\ | ||||||||||||||||||||||||||||||||||
| 22 | + [--chart-dir "$INFERNEX_CHART_DIR"] [--chart-oci oci://...] [--chart-version 0.0.0-latest] \\ | ||||||||||||||||||||||||||||||||||
| 23 | + [--checker-result <deploy>/checker-result.json] | ||||||||||||||||||||||||||||||||||
| 24 | + | ||||||||||||||||||||||||||||||||||
| 25 | +输入(deploy-dir 内): intent.yaml, values.yaml, render-check.json, nodes.yaml, [checker-result.json] | ||||||||||||||||||||||||||||||||||
| 26 | +输出(deploy-dir 内): preflight-report.json, preflight-report.md | ||||||||||||||||||||||||||||||||||
| 27 | +退出码: 0=无硬性失败; 1=有硬性失败; 2=输入/配置错误 | ||||||||||||||||||||||||||||||||||
| 28 | +""" | ||||||||||||||||||||||||||||||||||
| 29 | + | ||||||||||||||||||||||||||||||||||
| 30 | +from __future__ import annotations | ||||||||||||||||||||||||||||||||||
| 31 | + | ||||||||||||||||||||||||||||||||||
| 32 | +import argparse | ||||||||||||||||||||||||||||||||||
| 33 | +import json | ||||||||||||||||||||||||||||||||||
| 34 | +import re | ||||||||||||||||||||||||||||||||||
| 35 | +import shlex | ||||||||||||||||||||||||||||||||||
| 36 | +import subprocess | ||||||||||||||||||||||||||||||||||
| 37 | +import sys | ||||||||||||||||||||||||||||||||||
| 38 | +from pathlib import Path | ||||||||||||||||||||||||||||||||||
| 39 | +from typing import Any | ||||||||||||||||||||||||||||||||||
| 40 | + | ||||||||||||||||||||||||||||||||||
| 41 | +try: | ||||||||||||||||||||||||||||||||||
| 42 | + import yaml | ||||||||||||||||||||||||||||||||||
| 43 | +except ImportError: | ||||||||||||||||||||||||||||||||||
| 44 | + print("需要 PyYAML:pip install pyyaml", file=sys.stderr) | ||||||||||||||||||||||||||||||||||
| 45 | + sys.exit(2) | ||||||||||||||||||||||||||||||||||
| 46 | + | ||||||||||||||||||||||||||||||||||
| 47 | +DEFAULTS = {"severity": "hard", "when": ["always"], "layer": "software", "remediable": False} | ||||||||||||||||||||||||||||||||||
| 48 | +CMD_TIMEOUT_DEFAULT = 120 | ||||||||||||||||||||||||||||||||||
| 49 | +STATUS_ICON = {"pass": "✅", "fail": "❌", "info": "ℹ️", "skip": "⏭️"} | ||||||||||||||||||||||||||||||||||
| 50 | +STATUS_LABEL = {"pass": "✅ 通过", "fail": "❌ 未通过", "info": "ℹ️ 提示", "skip": "⏭️ 跳过"} | ||||||||||||||||||||||||||||||||||
| 51 | +COND_LABELS = { | ||||||||||||||||||||||||||||||||||
| 52 | + "checker.ran": "未运行硬件检查", | ||||||||||||||||||||||||||||||||||
| 53 | + "connectivity.enabled": "未开启连通性检查", | ||||||||||||||||||||||||||||||||||
| 54 | + "new-install": "升级场景跳过", | ||||||||||||||||||||||||||||||||||
| 55 | + "online": "使用本地 Chart", | ||||||||||||||||||||||||||||||||||
| 56 | + "mode=pd": "非 PD 分离模式", | ||||||||||||||||||||||||||||||||||
| 57 | + "mode=aggregated": "非聚合模式", | ||||||||||||||||||||||||||||||||||
| 58 | + "values:eagle-eye": "eagle-eye 未启用", | ||||||||||||||||||||||||||||||||||
| 59 | + "values:pd-orchestrator": "pd-orchestrator 未启用", | ||||||||||||||||||||||||||||||||||
| 60 | + "values:istio": "未启用 Chart 自带 Istio", | ||||||||||||||||||||||||||||||||||
| 61 | + "values:rsg": "pd-orchestrator 未启用", | ||||||||||||||||||||||||||||||||||
| 62 | + "values:pd-proxy": "非 PD 或未启用 proxy", | ||||||||||||||||||||||||||||||||||
| 63 | + "values:kv-cache-aware": "路由非 kv-cache-aware", | ||||||||||||||||||||||||||||||||||
| 64 | +} | ||||||||||||||||||||||||||||||||||
| 65 | + | ||||||||||||||||||||||||||||||||||
| 66 | + | ||||||||||||||||||||||||||||||||||
| 67 | +def die(msg: str) -> None: | ||||||||||||||||||||||||||||||||||
| 68 | + print(json.dumps({"error": msg}, ensure_ascii=False), file=sys.stderr) | ||||||||||||||||||||||||||||||||||
| 69 | + sys.exit(2) | ||||||||||||||||||||||||||||||||||
| 70 | + | ||||||||||||||||||||||||||||||||||
| 71 | + | ||||||||||||||||||||||||||||||||||
| 72 | +def load_yaml(path: Path) -> Any: | ||||||||||||||||||||||||||||||||||
| 73 | + with path.open(encoding="utf-8") as f: | ||||||||||||||||||||||||||||||||||
| 74 | + return yaml.safe_load(f) | ||||||||||||||||||||||||||||||||||
| 75 | + | ||||||||||||||||||||||||||||||||||
| 76 | + | ||||||||||||||||||||||||||||||||||
| 77 | +# K8s namespace / release 名的合法字符集(DNS-1123);用户输入直接进 bash 前必须过白名单 | ||||||||||||||||||||||||||||||||||
| 78 | +SAFE_NAME = re.compile(r"^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$") | ||||||||||||||||||||||||||||||||||
| 79 | + | ||||||||||||||||||||||||||||||||||
| 80 | + | ||||||||||||||||||||||||||||||||||
| 81 | +def validate_placeholders(placeholders: dict[str, str]) -> None: | ||||||||||||||||||||||||||||||||||
| 82 | + for key in ("NAMESPACE", "RELEASE"): | ||||||||||||||||||||||||||||||||||
| 83 | + v = placeholders.get(key, "") | ||||||||||||||||||||||||||||||||||
| 84 | + if not SAFE_NAME.match(v): | ||||||||||||||||||||||||||||||||||
| 85 | + die(f"非法 {key}: {v!r}(须符合 K8s DNS-1123 命名:小写字母数字与 .-)") | ||||||||||||||||||||||||||||||||||
| 86 | + | ||||||||||||||||||||||||||||||||||
| 87 | + | ||||||||||||||||||||||||||||||||||
| 88 | +def run_cmd(cmd: str, placeholders: dict[str, str], timeout: int) -> tuple[int, str]: | ||||||||||||||||||||||||||||||||||
| 89 | + """占位符替换后经 bash -c 执行;返回 (exit, 原始输出)。值经 shlex.quote 转义防注入。""" | ||||||||||||||||||||||||||||||||||
| 90 | + for k, v in placeholders.items(): | ||||||||||||||||||||||||||||||||||
| 91 | + cmd = cmd.replace("{{" + k + "}}", shlex.quote(str(v))) | ||||||||||||||||||||||||||||||||||
| 92 | + try: | ||||||||||||||||||||||||||||||||||
| 93 | + proc = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True, | ||||||||||||||||||||||||||||||||||
| 94 | + timeout=timeout, check=False) | ||||||||||||||||||||||||||||||||||
| 95 | + out = (proc.stdout + proc.stderr).strip() | ||||||||||||||||||||||||||||||||||
| 96 | + return proc.returncode, out # 保留原始多行;报告 JSON 存全量,md 展示层单行化 | ||||||||||||||||||||||||||||||||||
| 97 | + except subprocess.TimeoutExpired: | ||||||||||||||||||||||||||||||||||
| 98 | + return 124, f"超时(>{timeout}s)" | ||||||||||||||||||||||||||||||||||
| 99 | + | ||||||||||||||||||||||||||||||||||
[轻微] run_cmd 超时返回码 124 未在文档中说明。124 是 GNU 改动建议
![]() ![]() | |||||||||||||||||||||||||||||||||||
| 100 | + | ||||||||||||||||||||||||||||||||||
| 101 | +class Context: | ||||||||||||||||||||||||||||||||||
| 102 | + """when 条件的求值上下文(intent + 渲染 values + 集群状态 + checker 存在性)。""" | ||||||||||||||||||||||||||||||||||
| 103 | + | ||||||||||||||||||||||||||||||||||
| 104 | + def __init__(self, args: argparse.Namespace) -> None: | ||||||||||||||||||||||||||||||||||
| 105 | + self.deploy = Path(args.deploy_dir).resolve() | ||||||||||||||||||||||||||||||||||
| 106 | + self.intent: dict[str, Any] = {} | ||||||||||||||||||||||||||||||||||
| 107 | + self.values: dict[str, Any] = {} | ||||||||||||||||||||||||||||||||||
| 108 | + intent_p = self.deploy / "intent.yaml" | ||||||||||||||||||||||||||||||||||
| 109 | + values_p = Path(args.values) if args.values else self.deploy / "values.yaml" | ||||||||||||||||||||||||||||||||||
| 110 | + if intent_p.exists(): | ||||||||||||||||||||||||||||||||||
| 111 | + self.intent = load_yaml(intent_p) or {} | ||||||||||||||||||||||||||||||||||
| 112 | + if values_p.exists(): | ||||||||||||||||||||||||||||||||||
| 113 | + self.values = load_yaml(values_p) or {} | ||||||||||||||||||||||||||||||||||
| 114 | + self.checker_ran = args.checker_result is not None and Path(args.checker_result).is_file() | ||||||||||||||||||||||||||||||||||
| 115 | + chk = self.intent.get("checker") or {} | ||||||||||||||||||||||||||||||||||
| 116 | + self.connectivity = bool(chk.get("enabled")) and bool(chk.get("connectivity")) | ||||||||||||||||||||||||||||||||||
| 117 | + self.online = not (self.intent.get("chart") or {}).get("dir") | ||||||||||||||||||||||||||||||||||
| 118 | + self.mode = self.intent.get("mode") or "pd" | ||||||||||||||||||||||||||||||||||
| 119 | + # new-install: helm status 失败(release 不存在) | ||||||||||||||||||||||||||||||||||
| 120 | + proc = subprocess.run( | ||||||||||||||||||||||||||||||||||
| 121 | + ["helm", "status", args.release, "-n", args.namespace], | ||||||||||||||||||||||||||||||||||
run_cmd 函数将多行输出压缩为单行,丢失关键诊断信息。S-13(helm lint + helm template)的输出包含结构化错误信息,单行化后无法阅读。建议保留原始换行或截断至合理行数。 改动建议
![]() ![]() | |||||||||||||||||||||||||||||||||||
| 122 | + capture_output=True, text=True, check=False) | ||||||||||||||||||||||||||||||||||
| 123 | + self.new_install = proc.returncode != 0 | ||||||||||||||||||||||||||||||||||
[重要] _humanize_skip 正则对包含多组括号的 evidence 解析不准确。 改动建议
![]() ![]() | |||||||||||||||||||||||||||||||||||
| 124 | + | ||||||||||||||||||||||||||||||||||
| 125 | + svc = ((self.values.get("inference-backend") or {}).get("services") or [{}])[0] | ||||||||||||||||||||||||||||||||||
| 126 | + self.flags = { | ||||||||||||||||||||||||||||||||||
| 127 | + "values:eagle-eye": bool((self.values.get("eagle-eye") or {}).get("enabled")), | ||||||||||||||||||||||||||||||||||
| 128 | + "values:pd-orchestrator": bool( | ||||||||||||||||||||||||||||||||||
| 129 | + ((self.values.get("pd-orchestrator") or {}).get("resourcescalinggroup") or {}).get("enabled")), | ||||||||||||||||||||||||||||||||||
| 130 | + "values:istio": bool(((self.values.get("inferenceGateway") or {}).get("istio") or {}).get("enabled")), | ||||||||||||||||||||||||||||||||||
| 131 | + "values:rsg": bool( | ||||||||||||||||||||||||||||||||||
| 132 | + ((self.values.get("pd-orchestrator") or {}).get("resourcescalinggroup") or {}).get("enabled")), | ||||||||||||||||||||||||||||||||||
| 133 | + "values:pd-proxy": bool(((svc.get("pd") or {}).get("proxyServer") or {}).get("enabled")), | ||||||||||||||||||||||||||||||||||
| 134 | + "values:kv-cache-aware": (self.values.get("hermes-router", {}) | ||||||||||||||||||||||||||||||||||
| 135 | + .get("inferenceExtension", {}).get("routing", {}).get("profile") | ||||||||||||||||||||||||||||||||||
[轻微] 修复建议:在 SKILL.md 安全章节增加"checks.yaml 为 Skill 可信源,禁止从外部动态加载"的声明。 ![]() ![]() [轻微] 修复建议:对 ![]() ![]() | |||||||||||||||||||||||||||||||||||
| 136 | + == "kv-cache-aware"), | ||||||||||||||||||||||||||||||||||
| 137 | + } | ||||||||||||||||||||||||||||||||||
| 138 | + | ||||||||||||||||||||||||||||||||||
| 139 | + def eval_condition(self, cond: str) -> bool: | ||||||||||||||||||||||||||||||||||
| 140 | + if cond == "always": | ||||||||||||||||||||||||||||||||||
| 141 | + return True | ||||||||||||||||||||||||||||||||||
| 142 | + if cond == "checker.ran": | ||||||||||||||||||||||||||||||||||
| 143 | + return self.checker_ran | ||||||||||||||||||||||||||||||||||
| 144 | + if cond == "connectivity.enabled": | ||||||||||||||||||||||||||||||||||
| 145 | + return self.connectivity | ||||||||||||||||||||||||||||||||||
| 146 | + if cond == "new-install": | ||||||||||||||||||||||||||||||||||
| 147 | + return self.new_install | ||||||||||||||||||||||||||||||||||
| 148 | + if cond == "online": | ||||||||||||||||||||||||||||||||||
| 149 | + return self.online | ||||||||||||||||||||||||||||||||||
| 150 | + if cond in ("mode=pd", "mode=aggregated"): | ||||||||||||||||||||||||||||||||||
| 151 | + return self.mode == cond.split("=", 1)[1] | ||||||||||||||||||||||||||||||||||
| 152 | + if cond in self.flags: | ||||||||||||||||||||||||||||||||||
| 153 | + return self.flags[cond] | ||||||||||||||||||||||||||||||||||
| 154 | + die(f"checks.yaml 含未知 when 条件: {cond!r}(禁止静默通过,请修 checks.yaml)") | ||||||||||||||||||||||||||||||||||
| 155 | + | ||||||||||||||||||||||||||||||||||
| 156 | + | ||||||||||||||||||||||||||||||||||
| 157 | +def collect_checker_items(checker: dict[str, Any]) -> dict[str, list[dict[str, Any]]]: | ||||||||||||||||||||||||||||||||||
| 158 | + """把 checker-result.json 按 id 聚合(hardware.nodes[].checks / cross_nodes / k8s / config_env)。""" | ||||||||||||||||||||||||||||||||||
| 159 | + by_id: dict[str, list[dict[str, Any]]] = {} | ||||||||||||||||||||||||||||||||||
| 160 | + hw = checker.get("hardware") or {} | ||||||||||||||||||||||||||||||||||
| 161 | + for node in hw.get("nodes") or []: | ||||||||||||||||||||||||||||||||||
| 162 | + for chk in node.get("checks") or []: | ||||||||||||||||||||||||||||||||||
| 163 | + by_id.setdefault(str(chk.get("id")), []).append(chk) | ||||||||||||||||||||||||||||||||||
| 164 | + for chk in hw.get("cross_nodes") or []: | ||||||||||||||||||||||||||||||||||
| 165 | + by_id.setdefault(str(chk.get("id")), []).append(chk) | ||||||||||||||||||||||||||||||||||
| 166 | + for layer_key in ("k8s", "config_env"): | ||||||||||||||||||||||||||||||||||
| 167 | + for chk in ((checker.get(layer_key) or {}).get("checks") or []): | ||||||||||||||||||||||||||||||||||
| 168 | + by_id.setdefault(str(chk.get("id")), []).append(chk) | ||||||||||||||||||||||||||||||||||
| 169 | + return by_id | ||||||||||||||||||||||||||||||||||
| 170 | + | ||||||||||||||||||||||||||||||||||
| 171 | + | ||||||||||||||||||||||||||||||||||
| 172 | +def checker_aggregate(entries: list[dict[str, Any]]) -> tuple[str, str]: | ||||||||||||||||||||||||||||||||||
| 173 | + """多节点聚合:任一 failed → fail;全 skipped → skip;否则 pass / info(含 warning/info)。""" | ||||||||||||||||||||||||||||||||||
| 174 | + statuses = [str(e.get("status")) for e in entries] | ||||||||||||||||||||||||||||||||||
| 175 | + msgs = [f"[{e.get('node', '-')}] {e.get('message', '')}" for e in entries] | ||||||||||||||||||||||||||||||||||
| 176 | + if any(s == "failed" for s in statuses): | ||||||||||||||||||||||||||||||||||
| 177 | + return "fail", "; ".join(msgs)[:500] | ||||||||||||||||||||||||||||||||||
| 178 | + if all(s == "skipped" for s in statuses): | ||||||||||||||||||||||||||||||||||
| 179 | + return "skip", "; ".join(msgs)[:500] or "checker skipped" | ||||||||||||||||||||||||||||||||||
| 180 | + if any(s in ("info", "warning") for s in statuses): | ||||||||||||||||||||||||||||||||||
| 181 | + return "info", "; ".join(msgs)[:500] | ||||||||||||||||||||||||||||||||||
| 182 | + return "pass", "; ".join(msgs)[:500] | ||||||||||||||||||||||||||||||||||
| 183 | + | ||||||||||||||||||||||||||||||||||
| 184 | + | ||||||||||||||||||||||||||||||||||
| 185 | +def _humanize_skip(evidence: str) -> str: | ||||||||||||||||||||||||||||||||||
| 186 | + """skip 的 when 条件翻译为用户可读说明。""" | ||||||||||||||||||||||||||||||||||
| 187 | + import re as _re | ||||||||||||||||||||||||||||||||||
| 188 | + m = _re.search(r"\[(.+)\]", evidence) | ||||||||||||||||||||||||||||||||||
| 189 | + if not m: | ||||||||||||||||||||||||||||||||||
| 190 | + return evidence | ||||||||||||||||||||||||||||||||||
| 191 | + parts = [] | ||||||||||||||||||||||||||||||||||
| 192 | + for cond in (c.strip().strip("'") for c in m.group(1).split(",")): | ||||||||||||||||||||||||||||||||||
| 193 | + parts.append(COND_LABELS.get(cond, cond)) | ||||||||||||||||||||||||||||||||||
| 194 | + return ";".join(parts) | ||||||||||||||||||||||||||||||||||
| 195 | + | ||||||||||||||||||||||||||||||||||
| 196 | + | ||||||||||||||||||||||||||||||||||
| 197 | +def render_md(report: dict[str, Any], titles: dict[str, str] | None = None) -> str: | ||||||||||||||||||||||||||||||||||
| 198 | + titles = titles or {} | ||||||||||||||||||||||||||||||||||
| 199 | + s = report["summary"] | ||||||||||||||||||||||||||||||||||
[重要] values:pd-orchestrator 条件仅检查 resourcescalinggroup.enabled,遗漏顶层 enabled 标志。pd-orchestrator 的启用状态可能通过 改动建议
![]() ![]() | |||||||||||||||||||||||||||||||||||
| 200 | + lines = [ | ||||||||||||||||||||||||||||||||||
| 201 | + "## 预检报告", | ||||||||||||||||||||||||||||||||||
| 202 | + "", | ||||||||||||||||||||||||||||||||||
| 203 | + f"**{s['pass']} 通过 / {s['fail_hard']} 硬性失败 / {s['info']} 提示 / {s['skip']} 跳过**", | ||||||||||||||||||||||||||||||||||
| 204 | + "", | ||||||||||||||||||||||||||||||||||
| 205 | + ] | ||||||||||||||||||||||||||||||||||
| 206 | + for layer, title in (("software", "### 软件检查"), ("hardware", "### 硬件检查(infernex-checker)")): | ||||||||||||||||||||||||||||||||||
| 207 | + items = report.get(layer) or [] | ||||||||||||||||||||||||||||||||||
| 208 | + if not items: | ||||||||||||||||||||||||||||||||||
| 209 | + continue | ||||||||||||||||||||||||||||||||||
| 210 | + lines += [title, "", "| 检查项 | 结果 | 说明 |", "|--------|------|------|"] | ||||||||||||||||||||||||||||||||||
| 211 | + for it in items: | ||||||||||||||||||||||||||||||||||
| 212 | + name = titles.get(it["id"], it["id"]) | ||||||||||||||||||||||||||||||||||
| 213 | + status = STATUS_LABEL.get(it["status"], it["status"]) | ||||||||||||||||||||||||||||||||||
| 214 | + if it["status"] == "skip": | ||||||||||||||||||||||||||||||||||
| 215 | + ev = _humanize_skip(it.get("evidence") or "") | ||||||||||||||||||||||||||||||||||
| 216 | + else: | ||||||||||||||||||||||||||||||||||
| 217 | + ev = it.get("evidence") or "" | ||||||||||||||||||||||||||||||||||
| 218 | + if it["status"] == "fail" and it.get("remediation"): | ||||||||||||||||||||||||||||||||||
| 219 | + ev = f"{ev};可自动修复:{it['remediation']}" | ||||||||||||||||||||||||||||||||||
| 220 | + ev = " ".join(ev.split()).replace("|", "\\|") # 展示层折叠多行 | ||||||||||||||||||||||||||||||||||
| 221 | + lines.append(f"| {name} | {status} | {ev} |") | ||||||||||||||||||||||||||||||||||
| 222 | + lines.append("") | ||||||||||||||||||||||||||||||||||
[严重] Context.new_install 判定不可靠,helm status 非零即视为新装。 改动建议
![]() ![]() | |||||||||||||||||||||||||||||||||||
| 223 | + if s["fail_hard"] > 0: | ||||||||||||||||||||||||||||||||||
| 224 | + lines.append("存在硬性失败:C3 门不出现「开始部署」选项;须修复后重检。") | ||||||||||||||||||||||||||||||||||
| 225 | + return "\n".join(lines) + "\n" | ||||||||||||||||||||||||||||||||||
| 226 | + | ||||||||||||||||||||||||||||||||||
| 227 | + | ||||||||||||||||||||||||||||||||||
| 228 | +def main() -> int: | ||||||||||||||||||||||||||||||||||
| 229 | + ap = argparse.ArgumentParser(description="InferNex 预检执行器(数据驱动,产出 C3 报告)") | ||||||||||||||||||||||||||||||||||
| 230 | + ap.add_argument("--deploy-dir", required=True) | ||||||||||||||||||||||||||||||||||
| 231 | + ap.add_argument("--namespace", default="ai-inference") | ||||||||||||||||||||||||||||||||||
| 232 | + ap.add_argument("--release", default="infernex") | ||||||||||||||||||||||||||||||||||
| 233 | + ap.add_argument("--checks", type=Path, default=None) | ||||||||||||||||||||||||||||||||||
| 234 | + ap.add_argument("--skill-dir", type=Path, default=Path(__file__).resolve().parent.parent) | ||||||||||||||||||||||||||||||||||
| 235 | + ap.add_argument("--values", type=Path, default=None) | ||||||||||||||||||||||||||||||||||
| 236 | + ap.add_argument("--chart-dir", type=Path, default=None) | ||||||||||||||||||||||||||||||||||
| 237 | + ap.add_argument("--chart-oci", default="oci://cr.openfuyao.cn/charts/infernex") | ||||||||||||||||||||||||||||||||||
| 238 | + ap.add_argument("--chart-version", default="0.0.0-latest") | ||||||||||||||||||||||||||||||||||
| 239 | + ap.add_argument("--checker-result", type=Path, default=None) | ||||||||||||||||||||||||||||||||||
| 240 | + args = ap.parse_args() | ||||||||||||||||||||||||||||||||||
| 241 | + | ||||||||||||||||||||||||||||||||||
| 242 | + checks_path = args.checks or (args.skill_dir / "references" / "checks.yaml") | ||||||||||||||||||||||||||||||||||
| 243 | + spec = load_yaml(checks_path) | ||||||||||||||||||||||||||||||||||
| 244 | + if int(spec.get("version", 1)) < 2: | ||||||||||||||||||||||||||||||||||
| 245 | + die("checks.yaml 须为 v2(结构化 when + judge)") | ||||||||||||||||||||||||||||||||||
| 246 | + ctx = Context(args) | ||||||||||||||||||||||||||||||||||
| 247 | + | ||||||||||||||||||||||||||||||||||
[重要] 修复建议:在函数末尾添加 ![]() ![]() | |||||||||||||||||||||||||||||||||||
| 248 | + render_check: dict[str, Any] = {} | ||||||||||||||||||||||||||||||||||
| 249 | + rc_path = ctx.deploy / "render-check.json" | ||||||||||||||||||||||||||||||||||
| 250 | + if rc_path.exists(): | ||||||||||||||||||||||||||||||||||
| 251 | + render_check = {c["id"]: c for c in json.loads(rc_path.read_text()).get("checks", [])} | ||||||||||||||||||||||||||||||||||
| 252 | + | ||||||||||||||||||||||||||||||||||
| 253 | + checker_items: dict[str, list[dict[str, Any]]] = {} | ||||||||||||||||||||||||||||||||||
| 254 | + if ctx.checker_ran: | ||||||||||||||||||||||||||||||||||
| 255 | + checker_items = collect_checker_items( | ||||||||||||||||||||||||||||||||||
| 256 | + json.loads(Path(args.checker_result).read_text())) | ||||||||||||||||||||||||||||||||||
| 257 | + | ||||||||||||||||||||||||||||||||||
| 258 | + placeholders = { | ||||||||||||||||||||||||||||||||||
| 259 | + "NAMESPACE": args.namespace, "RELEASE": args.release, | ||||||||||||||||||||||||||||||||||
| 260 | + "CHART_DIR": str(args.chart_dir or ""), "VALUES": str(ctx.deploy / "values.yaml"), | ||||||||||||||||||||||||||||||||||
| 261 | + "DEPLOY_DIR": str(ctx.deploy), "SKILL_DIR": str(args.skill_dir), | ||||||||||||||||||||||||||||||||||
| 262 | + "NODES": str(ctx.deploy / "nodes.yaml"), | ||||||||||||||||||||||||||||||||||
| 263 | + "CHART_OCI": args.chart_oci, "CHART_VERSION": args.chart_version, | ||||||||||||||||||||||||||||||||||
| 264 | + } | ||||||||||||||||||||||||||||||||||
| 265 | + | ||||||||||||||||||||||||||||||||||
| 266 | + validate_placeholders(placeholders) | ||||||||||||||||||||||||||||||||||
| 267 | + report: dict[str, Any] = {"summary": {"pass": 0, "fail_hard": 0, "info": 0, "skip": 0}, | ||||||||||||||||||||||||||||||||||
| 268 | + "software": [], "hardware": []} | ||||||||||||||||||||||||||||||||||
| 269 | + for raw in spec.get("checks") or []: | ||||||||||||||||||||||||||||||||||
| 270 | + c = {**DEFAULTS, **raw} | ||||||||||||||||||||||||||||||||||
| 271 | + cid = str(c["id"]) | ||||||||||||||||||||||||||||||||||
| 272 | + if not all(ctx.eval_condition(x) for x in c["when"]): | ||||||||||||||||||||||||||||||||||
| 273 | + item = {"id": cid, "status": "skip", "severity": "soft", | ||||||||||||||||||||||||||||||||||
| 274 | + "evidence": f"条件不满足: {c['when']}", "remediable": False, "remediation": ""} | ||||||||||||||||||||||||||||||||||
| 275 | + else: | ||||||||||||||||||||||||||||||||||
| 276 | + severity = c["severity"] | ||||||||||||||||||||||||||||||||||
| 277 | + if severity == "soft" and any(ctx.eval_condition(x) for x in c.get("severity_hard_when") or []): | ||||||||||||||||||||||||||||||||||
| 278 | + severity = "hard" | ||||||||||||||||||||||||||||||||||
| 279 | + judge = c.get("judge") or {} | ||||||||||||||||||||||||||||||||||
| 280 | + jtype = judge.get("type") | ||||||||||||||||||||||||||||||||||
| 281 | + status, evidence = "skip", "无 judge 定义" | ||||||||||||||||||||||||||||||||||
| 282 | + if jtype == "cmd": | ||||||||||||||||||||||||||||||||||
| 283 | + ok, last_out = True, "" | ||||||||||||||||||||||||||||||||||
| 284 | + for cmd in judge.get("cmd") or []: | ||||||||||||||||||||||||||||||||||
| 285 | + code, out = run_cmd(cmd, placeholders, int(judge.get("timeout", CMD_TIMEOUT_DEFAULT))) | ||||||||||||||||||||||||||||||||||
| 286 | + if code != 0: | ||||||||||||||||||||||||||||||||||
| 287 | + ok, last_out = False, out[-300:] or f"执行失败(exit {code})" | ||||||||||||||||||||||||||||||||||
| 288 | + break | ||||||||||||||||||||||||||||||||||
| 289 | + last_out = out | ||||||||||||||||||||||||||||||||||
| 290 | + status = "pass" if ok else "fail" | ||||||||||||||||||||||||||||||||||
| 291 | + evidence = last_out or "exit 0" | ||||||||||||||||||||||||||||||||||
| 292 | + elif jtype == "script": | ||||||||||||||||||||||||||||||||||
| 293 | + ok, last_out, skipped_flag = True, "", False | ||||||||||||||||||||||||||||||||||
| 294 | + for cmd in judge.get("cmd") or []: | ||||||||||||||||||||||||||||||||||
| 295 | + code, out = run_cmd(cmd, placeholders, int(judge.get("timeout", CMD_TIMEOUT_DEFAULT))) | ||||||||||||||||||||||||||||||||||
| 296 | + try: | ||||||||||||||||||||||||||||||||||
| 297 | + data = json.loads(out) | ||||||||||||||||||||||||||||||||||
| 298 | + except Exception: | ||||||||||||||||||||||||||||||||||
| 299 | + data = None | ||||||||||||||||||||||||||||||||||
| 300 | + if isinstance(data, dict): | ||||||||||||||||||||||||||||||||||
| 301 | + if data.get("skipped"): | ||||||||||||||||||||||||||||||||||
| 302 | + skipped_flag = True | ||||||||||||||||||||||||||||||||||
| 303 | + if data.get("sufficient") is False: | ||||||||||||||||||||||||||||||||||
| 304 | + ok = False | ||||||||||||||||||||||||||||||||||
| 305 | + if "required" in data: # 容量类输出 → 友好摘要作 evidence | ||||||||||||||||||||||||||||||||||
| 306 | + req, avail = data.get("required"), data.get("available") | ||||||||||||||||||||||||||||||||||
| 307 | + src = data.get("source") or "" | ||||||||||||||||||||||||||||||||||
| 308 | + per = data.get("per_node") or data.get("per_node_available") or {} | ||||||||||||||||||||||||||||||||||
| 309 | + per_s = ",".join(f"{k}:{v}" for k, v in per.items()) | ||||||||||||||||||||||||||||||||||
| 310 | + if avail is None: | ||||||||||||||||||||||||||||||||||
| 311 | + last_out = f"需求 {req} 卡;可用量未知({data.get('skip_reason', '无数据源')})" | ||||||||||||||||||||||||||||||||||
| 312 | + else: | ||||||||||||||||||||||||||||||||||
| 313 | + last_out = f"需求 {req} 卡 / 可用 {avail} 卡({src}{';' + per_s if per_s else ''})" | ||||||||||||||||||||||||||||||||||
| 314 | + continue | ||||||||||||||||||||||||||||||||||
| 315 | + if code != 0: | ||||||||||||||||||||||||||||||||||
| 316 | + ok, last_out = False, out[-300:] or f"执行失败(exit {code})" | ||||||||||||||||||||||||||||||||||
| 317 | + break | ||||||||||||||||||||||||||||||||||
| 318 | + last_out = out[-1000:] | ||||||||||||||||||||||||||||||||||
| 319 | + status = "skip" if skipped_flag else ("pass" if ok else "fail") | ||||||||||||||||||||||||||||||||||
| 320 | + evidence = last_out or "exit 0" | ||||||||||||||||||||||||||||||||||
| 321 | + elif jtype == "render-check": | ||||||||||||||||||||||||||||||||||
| 322 | + if cid in render_check: | ||||||||||||||||||||||||||||||||||
| 323 | + status = render_check[cid]["status"] | ||||||||||||||||||||||||||||||||||
| 324 | + evidence = render_check[cid].get("evidence", "") | ||||||||||||||||||||||||||||||||||
| 325 | + else: | ||||||||||||||||||||||||||||||||||
| 326 | + status, evidence = "skip", "render-check 未含该项(不适用)" | ||||||||||||||||||||||||||||||||||
| 327 | + elif jtype == "checker": | ||||||||||||||||||||||||||||||||||
| 328 | + ids = judge.get("ids") or [cid] | ||||||||||||||||||||||||||||||||||
| 329 | + entries = [e for i in ids for e in checker_items.get(i, [])] | ||||||||||||||||||||||||||||||||||
| 330 | + if entries: | ||||||||||||||||||||||||||||||||||
| 331 | + status, evidence = checker_aggregate(entries) | ||||||||||||||||||||||||||||||||||
| 332 | + else: | ||||||||||||||||||||||||||||||||||
| 333 | + status, evidence = "skip", f"checker 结果中无 {ids}" | ||||||||||||||||||||||||||||||||||
| 334 | + else: | ||||||||||||||||||||||||||||||||||
| 335 | + die(f"checks.yaml {cid} 含未知 judge.type: {jtype!r}") | ||||||||||||||||||||||||||||||||||
| 336 | + item = {"id": cid, "status": status, "severity": severity, | ||||||||||||||||||||||||||||||||||
| 337 | + "evidence": evidence, "remediable": bool(c.get("remediable")), | ||||||||||||||||||||||||||||||||||
| 338 | + "remediation": c.get("remediation") or ""} | ||||||||||||||||||||||||||||||||||
| 339 | + | ||||||||||||||||||||||||||||||||||
| 340 | + layer = c["layer"] | ||||||||||||||||||||||||||||||||||
| 341 | + report[layer].append(item) | ||||||||||||||||||||||||||||||||||
| 342 | + if item["status"] == "pass": | ||||||||||||||||||||||||||||||||||
| 343 | + report["summary"]["pass"] += 1 | ||||||||||||||||||||||||||||||||||
| 344 | + elif item["status"] == "fail" and item["severity"] == "hard": | ||||||||||||||||||||||||||||||||||
| 345 | + report["summary"]["fail_hard"] += 1 | ||||||||||||||||||||||||||||||||||
| 346 | + elif item["status"] in ("info",): | ||||||||||||||||||||||||||||||||||
| 347 | + report["summary"]["info"] += 1 | ||||||||||||||||||||||||||||||||||
| 348 | + elif item["status"] == "skip": | ||||||||||||||||||||||||||||||||||
| 349 | + report["summary"]["skip"] += 1 | ||||||||||||||||||||||||||||||||||
| 350 | + elif item["status"] == "fail": # soft fail → 计入提示 | ||||||||||||||||||||||||||||||||||
| 351 | + item["status"] = "info" | ||||||||||||||||||||||||||||||||||
| 352 | + report["summary"]["info"] += 1 | ||||||||||||||||||||||||||||||||||
| 353 | + | ||||||||||||||||||||||||||||||||||
| 354 | + titles = {str(c.get("id")): str(c.get("title") or c.get("desc") or c.get("id")) | ||||||||||||||||||||||||||||||||||
| 355 | + for c in spec.get("checks") or []} | ||||||||||||||||||||||||||||||||||
| 356 | + json_path = ctx.deploy / "preflight-report.json" | ||||||||||||||||||||||||||||||||||
| 357 | + json_path.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") | ||||||||||||||||||||||||||||||||||
| 358 | + md_path = ctx.deploy / "preflight-report.md" | ||||||||||||||||||||||||||||||||||
| 359 | + md_path.write_text(render_md(report, titles), encoding="utf-8") | ||||||||||||||||||||||||||||||||||
| 360 | + | ||||||||||||||||||||||||||||||||||
| 361 | + print(render_md(report, titles)) | ||||||||||||||||||||||||||||||||||
| 362 | + print(f"报告: {json_path}\n {md_path}") | ||||||||||||||||||||||||||||||||||
| 363 | + return 1 if report["summary"]["fail_hard"] > 0 else 0 | ||||||||||||||||||||||||||||||||||
| 364 | + | ||||||||||||||||||||||||||||||||||
| 365 | + | ||||||||||||||||||||||||||||||||||
| 366 | +if __name__ == "__main__": | ||||||||||||||||||||||||||||||||||
| 367 | + raise SystemExit(main()) | ||||||||||||||||||||||||||||||||||
| @@ -0,0 +1,435 @@ | |||||||||||||||||||||||||
| 1 | +#!/usr/bin/env python3 | ||||||||||||||||||||||||
| 2 | +# Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||||||||||||||||||||||||
| 3 | +# openFuyao is licensed under Mulan PSL v2. | ||||||||||||||||||||||||
| 4 | +# You can use this software according to the terms and conditions of the Mulan PSL v2. | ||||||||||||||||||||||||
| 5 | +# You may obtain a copy of Mulan PSL v2 at: | ||||||||||||||||||||||||
| 6 | +# http://license.coscl.org.cn/MulanPSL2 | ||||||||||||||||||||||||
| 7 | +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | ||||||||||||||||||||||||
| 8 | +# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||||||||||||||||||||||||
| 9 | +# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||||||||||||||||||||||||
| 10 | +# See the Mulan PSL v2 for more details. | ||||||||||||||||||||||||
| 11 | + | ||||||||||||||||||||||||
| 12 | +"""intent.yaml + Chart 基线 values → 确定性 values.yaml(内嵌 S-14~S-22 自检)。 | ||||||||||||||||||||||||
| 13 | + | ||||||||||||||||||||||||
| 14 | +固化 values_rules.md §1–§4 的算法:slug 命名、模式整包切换、RSG 重写、卡数→并行度; | ||||||||||||||||||||||||
| 15 | +模型级差异经 `models/` profile overlay 叠加(字段约定见 models/_schema.md)。 | ||||||||||||||||||||||||
| 16 | +同一 intent + 同一 Chart 基线 + 同一 profile → 输出 byte-identical(golden UT 保证)。 | ||||||||||||||||||||||||
| 17 | +模式 / 拓扑 / 模型 / 命名 / RSG 相关 patch 只由本脚本产生(SKILL.md 硬门禁 9)。 | ||||||||||||||||||||||||
| 18 | + | ||||||||||||||||||||||||
| 19 | +用法: | ||||||||||||||||||||||||
| 20 | + python3 render_values.py \\ | ||||||||||||||||||||||||
| 21 | + --intent .infernex/deploy/<ts>/intent.yaml \\ | ||||||||||||||||||||||||
| 22 | + --chart-dir "${INFERNEX_CHART_DIR}" \\ | ||||||||||||||||||||||||
| 23 | + --output .infernex/deploy/<ts>/values.yaml \\ | ||||||||||||||||||||||||
| 24 | + [--report .infernex/deploy/<ts>/render-check.json] \\ | ||||||||||||||||||||||||
| 25 | + [--models-dir "${INFERNEX_SKILL_DIR}/models"] \\ | ||||||||||||||||||||||||
| 26 | + [--aggregated-template "${INFERNEX_SKILL_DIR}/templates/aggregated-role.yaml"] | ||||||||||||||||||||||||
docstring 声称"golden UT 保证"确定性渲染,但本次变更未包含任何测试文件(如 ![]() ![]() | |||||||||||||||||||||||||
| 27 | + | ||||||||||||||||||||||||
| 28 | +退出码: 0=全部自检通过; 1=自检失败; 2=输入/意图错误。 | ||||||||||||||||||||||||
| 29 | +报告 JSON 同时打印到 stdout(derived 供 C1 渲染派生行;checks 并入 preflight-report)。 | ||||||||||||||||||||||||
| 30 | +""" | ||||||||||||||||||||||||
| 31 | + | ||||||||||||||||||||||||
| 32 | +from __future__ import annotations | ||||||||||||||||||||||||
| 33 | + | ||||||||||||||||||||||||
| 34 | +import argparse | ||||||||||||||||||||||||
| 35 | +import copy | ||||||||||||||||||||||||
| 36 | +import json | ||||||||||||||||||||||||
| 37 | +import re | ||||||||||||||||||||||||
| 38 | +import sys | ||||||||||||||||||||||||
| 39 | +from pathlib import Path | ||||||||||||||||||||||||
| 40 | +from typing import Any | ||||||||||||||||||||||||
| 41 | + | ||||||||||||||||||||||||
| 42 | +try: | ||||||||||||||||||||||||
| 43 | + import yaml | ||||||||||||||||||||||||
| 44 | +except ImportError: | ||||||||||||||||||||||||
| 45 | + print("需要 PyYAML:pip install pyyaml", file=sys.stderr) | ||||||||||||||||||||||||
| 46 | + sys.exit(2) | ||||||||||||||||||||||||
| 47 | + | ||||||||||||||||||||||||
| 48 | +sys.path.insert(0, str(Path(__file__).resolve().parent)) | ||||||||||||||||||||||||
| 49 | +from slugs import model_label, service_slug # noqa: E402 | ||||||||||||||||||||||||
| 50 | + | ||||||||||||||||||||||||
| 51 | +# 推理参数:intent 键 → role extraArgs 里的 vLLM flag(values_rules.md「推理参数」) | ||||||||||||||||||||||||
| 52 | +INFERENCE_ARG_MAP = { | ||||||||||||||||||||||||
使用 sys.path.insert(0, ...) 导入 slugs 模块,依赖相对路径。在 pytest 环境下可能因工作目录不同而失败。建议在 slugs.py 中添加 if name == "main" 保护,或使用 importlib.import_module 的绝对导入方式。 ![]() ![]() | |||||||||||||||||||||||||
| 53 | + "maxModelLen": "--max-model-len", | ||||||||||||||||||||||||
| 54 | + "maxNumBatchedTokens": "--max-num-batched-tokens", | ||||||||||||||||||||||||
| 55 | + "gpuMemoryUtilization": "--gpu-memory-utilization", | ||||||||||||||||||||||||
| 56 | + "blockSize": "--block-size", | ||||||||||||||||||||||||
| 57 | +} | ||||||||||||||||||||||||
| 58 | + | ||||||||||||||||||||||||
| 59 | + | ||||||||||||||||||||||||
INFERENCE_ARG_MAP 仅允许 4 个推理参数键,与 values_rules.md 文档化能力不匹配。values_rules.md 文档化了大量可配置项(max-num-seqs、dtype、enforce-eager 等),但 apply_inference_params 对未知键直接 die() 拒绝。建议扩展 INFERENCE_ARG_MAP 或允许通过 extraArgs 传入任意 flag。 改动建议
![]() ![]() | |||||||||||||||||||||||||
| 60 | +def die(msg: str) -> None: | ||||||||||||||||||||||||
| 61 | + print(json.dumps({"error": msg}, ensure_ascii=False), file=sys.stderr) | ||||||||||||||||||||||||
| 62 | + sys.exit(2) | ||||||||||||||||||||||||
| 63 | + | ||||||||||||||||||||||||
| 64 | + | ||||||||||||||||||||||||
| 65 | +def load_yaml(path: Path) -> dict[str, Any]: | ||||||||||||||||||||||||
| 66 | + with path.open(encoding="utf-8") as f: | ||||||||||||||||||||||||
| 67 | + return yaml.safe_load(f) or {} | ||||||||||||||||||||||||
| 68 | + | ||||||||||||||||||||||||
| 69 | + | ||||||||||||||||||||||||
| 70 | +def load_profile(models_dir: Path | None, model_input: Any) -> tuple[dict[str, Any] | None, str | None]: | ||||||||||||||||||||||||
| 71 | + """按 model 精确值或 aliases 匹配 profile(大小写不敏感)。""" | ||||||||||||||||||||||||
| 72 | + if not model_input or not models_dir or not models_dir.is_dir(): | ||||||||||||||||||||||||
| 73 | + return None, None | ||||||||||||||||||||||||
| 74 | + target = str(model_input).strip().lower() | ||||||||||||||||||||||||
| 75 | + for p in sorted(models_dir.glob("**/*.yaml")): # 按形态分目录(aggregated/ pd/),递归匹配 | ||||||||||||||||||||||||
| 76 | + if p.name.startswith("_") or p.is_dir(): | ||||||||||||||||||||||||
| 77 | + continue | ||||||||||||||||||||||||
| 78 | + try: | ||||||||||||||||||||||||
| 79 | + prof = load_yaml(p) | ||||||||||||||||||||||||
| 80 | + except Exception: | ||||||||||||||||||||||||
| 81 | + continue | ||||||||||||||||||||||||
| 82 | + pm = str(prof.get("model") or "").strip().lower() | ||||||||||||||||||||||||
| 83 | + aliases = {str(a).strip().lower() for a in (prof.get("aliases") or [])} | ||||||||||||||||||||||||
load_profile 静默跳过 YAML 解析异常( ![]() ![]() | |||||||||||||||||||||||||
| 84 | + if target and target in ({pm} | aliases): | ||||||||||||||||||||||||
| 85 | + return prof, p.stem | ||||||||||||||||||||||||
[轻微] 修复建议:使用 ![]() ![]() | |||||||||||||||||||||||||
| 86 | + return None, None | ||||||||||||||||||||||||
| 87 | + | ||||||||||||||||||||||||
| 88 | + | ||||||||||||||||||||||||
| 89 | +def role_cards(topo: dict[str, Any]) -> int: | ||||||||||||||||||||||||
| 90 | + return topo["replicas"] * topo["dp"] * topo["tp"] * topo["pp"] | ||||||||||||||||||||||||
| 91 | + | ||||||||||||||||||||||||
| 92 | + | ||||||||||||||||||||||||
| 93 | +def resolve_role_topologies(intent: dict[str, Any], mode: str, cards: int | None, | ||||||||||||||||||||||||
| 94 | + profile_mode_cfg: dict[str, Any]) -> dict[str, dict[str, Any]]: | ||||||||||||||||||||||||
| 95 | + """intent 显式拓扑(单一,应用所有 role)> profile 按 role 拓扑 > 全 1 均分。 | ||||||||||||||||||||||||
| 96 | + | ||||||||||||||||||||||||
| 97 | + profile 分支语义:profile 给**并行度**(tp/dp/pp,replicas 可选),intent.cards 给**规模**—— | ||||||||||||||||||||||||
| 98 | + cards 须为各 role 基准卡数之和(replicas=profile 或 1)的正整数倍,倍数即副本缩放; | ||||||||||||||||||||||||
| 99 | + 未给 cards → 副本数取 profile(缺省 1),总卡数由 profile 决定。 | ||||||||||||||||||||||||
| 100 | + """ | ||||||||||||||||||||||||
| 101 | + t = intent.get("topology") or {} | ||||||||||||||||||||||||
| 102 | + named = any(t.get(k) is not None for k in ("tp", "dp", "pp", "replicas")) | ||||||||||||||||||||||||
| 103 | + roles = ["aggregated"] if mode == "aggregated" else ["prefill", "decode"] | ||||||||||||||||||||||||
| 104 | + cards = cards if cards is not None else 4 | ||||||||||||||||||||||||
| 105 | + | ||||||||||||||||||||||||
| 106 | + if named: | ||||||||||||||||||||||||
| 107 | + out = {} | ||||||||||||||||||||||||
| 108 | + for r in roles: | ||||||||||||||||||||||||
| 109 | + out[r] = {"tp": t.get("tp") or 1, "dp": t.get("dp") or 1, | ||||||||||||||||||||||||
| 110 | + "pp": t.get("pp") or 1, "replicas": t.get("replicas")} | ||||||||||||||||||||||||
| 111 | + for r in roles: | ||||||||||||||||||||||||
| 112 | + o = out[r] | ||||||||||||||||||||||||
| 113 | + if o["replicas"] is None: | ||||||||||||||||||||||||
| 114 | + per = cards if mode == "aggregated" else cards // 2 | ||||||||||||||||||||||||
| 115 | + denom = o["tp"] * o["dp"] * o["pp"] | ||||||||||||||||||||||||
| 116 | + if per % denom != 0: | ||||||||||||||||||||||||
| 117 | + die(f"卡数 {cards}({'该 role ' + str(per) + ' 卡'})不能被 tp×dp×pp={denom} 整除") | ||||||||||||||||||||||||
| 118 | + o["replicas"] = per // denom | ||||||||||||||||||||||||
| 119 | + return out | ||||||||||||||||||||||||
| 120 | + | ||||||||||||||||||||||||
| 121 | + out, used_profile = {}, False | ||||||||||||||||||||||||
| 122 | + for r in roles: | ||||||||||||||||||||||||
| 123 | + pt = (profile_mode_cfg.get(r) or {}).get("topology") or {} | ||||||||||||||||||||||||
| 124 | + if pt: | ||||||||||||||||||||||||
| 125 | + used_profile = True | ||||||||||||||||||||||||
| 126 | + out[r] = {"tp": pt.get("tp") or 1, "dp": pt.get("dp") or 1, | ||||||||||||||||||||||||
| 127 | + "pp": pt.get("pp") or 1, "replicas": pt.get("replicas")} | ||||||||||||||||||||||||
| 128 | + | ||||||||||||||||||||||||
| 129 | + if used_profile: | ||||||||||||||||||||||||
| 130 | + base = {r: (o["replicas"] if o["replicas"] is not None else 1) * o["tp"] * o["dp"] * o["pp"] | ||||||||||||||||||||||||
| 131 | + for r, o in out.items()} | ||||||||||||||||||||||||
| 132 | + base_sum = sum(base.values()) | ||||||||||||||||||||||||
| 133 | + if intent.get("cards") is None: | ||||||||||||||||||||||||
| 134 | + for o in out.values(): | ||||||||||||||||||||||||
| 135 | + if o["replicas"] is None: | ||||||||||||||||||||||||
| 136 | + o["replicas"] = 1 | ||||||||||||||||||||||||
| 137 | + return out | ||||||||||||||||||||||||
| 138 | + k, rem = divmod(cards, base_sum) | ||||||||||||||||||||||||
| 139 | + if rem != 0 or k < 1: | ||||||||||||||||||||||||
| 140 | + per_role = {r: b for r, b in base.items()} | ||||||||||||||||||||||||
| 141 | + die(f"intent.cards={cards} 须为 profile 基准卡数之和 {base_sum} 的正整数倍 {per_role};" | ||||||||||||||||||||||||
| 142 | + f"请调整 cards、或用 topology.replicas 显式指定副本") | ||||||||||||||||||||||||
| 143 | + for o in out.values(): | ||||||||||||||||||||||||
| 144 | + o["replicas"] = k * (o["replicas"] if o["replicas"] is not None else 1) | ||||||||||||||||||||||||
| 145 | + return out | ||||||||||||||||||||||||
| 146 | + | ||||||||||||||||||||||||
| 147 | + if mode == "aggregated": | ||||||||||||||||||||||||
| 148 | + for r in roles: | ||||||||||||||||||||||||
| 149 | + out[r] = {"tp": 1, "dp": 1, "pp": 1, "replicas": cards} | ||||||||||||||||||||||||
| 150 | + else: | ||||||||||||||||||||||||
| 151 | + if cards % 2 != 0: | ||||||||||||||||||||||||
| 152 | + die(f"PD 模式卡数 {cards} 为奇数且未点名拓扑:请在 intent.topology 明确 tp/dp/pp/replicas 或改偶数卡") | ||||||||||||||||||||||||
| 153 | + for r in roles: | ||||||||||||||||||||||||
| 154 | + out[r] = {"tp": 1, "dp": 1, "pp": 1, "replicas": cards // 2} | ||||||||||||||||||||||||
| 155 | + return out | ||||||||||||||||||||||||
| 156 | + | ||||||||||||||||||||||||
| 157 | + | ||||||||||||||||||||||||
| 158 | +def patch_extra_args(extra: list[str], flag: str, value: Any) -> list[str]: | ||||||||||||||||||||||||
| 159 | + """替换 role extraArgs 中已有 flag 的值;不存在则追加。保持其余项原样。""" | ||||||||||||||||||||||||
| 160 | + out, seen = [], False | ||||||||||||||||||||||||
| 161 | + pat = re.compile(rf"^{re.escape(flag)}(=|\s|$)") | ||||||||||||||||||||||||
| 162 | + for item in extra: | ||||||||||||||||||||||||
| 163 | + if pat.match(item): | ||||||||||||||||||||||||
| 164 | + if not seen: | ||||||||||||||||||||||||
| 165 | + out.append(f"{flag}={value}" if str(value) else flag) | ||||||||||||||||||||||||
| 166 | + seen = True | ||||||||||||||||||||||||
| 167 | + else: | ||||||||||||||||||||||||
| 168 | + out.append(item) | ||||||||||||||||||||||||
| 169 | + if not seen: | ||||||||||||||||||||||||
| 170 | + out.append(f"{flag}={value}" if str(value) else flag) | ||||||||||||||||||||||||
| 171 | + return out | ||||||||||||||||||||||||
| 172 | + | ||||||||||||||||||||||||
| 173 | + | ||||||||||||||||||||||||
| 174 | +def apply_inference_params(params: dict[str, Any], roles: list[dict[str, Any]]) -> None: | ||||||||||||||||||||||||
| 175 | + """把 intent.inferenceParams 落到各活跃 role 的 extraArgs;未知键报错(禁止臆测字段)。""" | ||||||||||||||||||||||||
| 176 | + unknown = [k for k in params if k not in INFERENCE_ARG_MAP and k != "extraArgs"] | ||||||||||||||||||||||||
| 177 | + if unknown: | ||||||||||||||||||||||||
| 178 | + die(f"inferenceParams 不支持的键 {unknown};支持 {list(INFERENCE_ARG_MAP)} 与 extraArgs(追加原始 flag)") | ||||||||||||||||||||||||
| 179 | + for role in roles: | ||||||||||||||||||||||||
| 180 | + extra = list(role.get("extraArgs") or []) | ||||||||||||||||||||||||
| 181 | + for key, val in params.items(): | ||||||||||||||||||||||||
| 182 | + if key == "extraArgs": | ||||||||||||||||||||||||
| 183 | + for arg in val: | ||||||||||||||||||||||||
| 184 | + if arg not in extra: | ||||||||||||||||||||||||
| 185 | + extra.append(arg) | ||||||||||||||||||||||||
| 186 | + elif val is not None: | ||||||||||||||||||||||||
| 187 | + extra = patch_extra_args(extra, INFERENCE_ARG_MAP[key], val) | ||||||||||||||||||||||||
| 188 | + role["extraArgs"] = extra | ||||||||||||||||||||||||
| 189 | + | ||||||||||||||||||||||||
| 190 | + | ||||||||||||||||||||||||
| 191 | +def set_role_parallel(role: dict[str, Any], topo: dict[str, Any]) -> None: | ||||||||||||||||||||||||
| 192 | + role["replicas"] = topo["replicas"] | ||||||||||||||||||||||||
| 193 | + role["tensorParallelSize"] = topo["tp"] | ||||||||||||||||||||||||
| 194 | + role["pipelineParallelSize"] = topo["pp"] | ||||||||||||||||||||||||
| 195 | + role["dataParallelSize"] = topo["dp"] | ||||||||||||||||||||||||
| 196 | + role.setdefault("dataParallelSizeLocal", 1) | ||||||||||||||||||||||||
| 197 | + | ||||||||||||||||||||||||
| 198 | + | ||||||||||||||||||||||||
| 199 | +def rewrite_rsg(rsg: dict[str, Any], mode: str, svc_name: str, ref_tpl: dict[str, Any]) -> None: | ||||||||||||||||||||||||
| 200 | + """RSG targetResources / groupName 整表重写(values_rules.md §4.1)。""" | ||||||||||||||||||||||||
| 201 | + if mode == "aggregated": | ||||||||||||||||||||||||
| 202 | + rsg["targetResources"] = [ | ||||||||||||||||||||||||
| 203 | + {"name": "aggregated", "resourceRef": {**ref_tpl, "name": f"{svc_name}-aggregated"}} | ||||||||||||||||||||||||
| 204 | + ] | ||||||||||||||||||||||||
| 205 | + group = "aggregated" | ||||||||||||||||||||||||
| 206 | + else: | ||||||||||||||||||||||||
| 207 | + rsg["targetResources"] = [ | ||||||||||||||||||||||||
| 208 | + {"name": "prefill", "resourceRef": {**ref_tpl, "name": f"{svc_name}-prefill"}}, | ||||||||||||||||||||||||
| 209 | + {"name": "decode", "resourceRef": {**ref_tpl, "name": f"{svc_name}-decode"}}, | ||||||||||||||||||||||||
| 210 | + ] | ||||||||||||||||||||||||
| 211 | + group = "pd" | ||||||||||||||||||||||||
| 212 | + rsg.setdefault("scalingStrategy", {}).setdefault("groupConfig", {})["groupName"] = group | ||||||||||||||||||||||||
| 213 | + | ||||||||||||||||||||||||
| 214 | + | ||||||||||||||||||||||||
| 215 | +def apply_profile_overlay(svc: dict[str, Any], values: dict[str, Any], mode: str, | ||||||||||||||||||||||||
| 216 | + profile: dict[str, Any] | None) -> None: | ||||||||||||||||||||||||
| 217 | + """profile 的 extraArgs / env / resources / global 整段替换(叠加在模式算法之上)。""" | ||||||||||||||||||||||||
| 218 | + if not profile: | ||||||||||||||||||||||||
| 219 | + return | ||||||||||||||||||||||||
| 220 | + cfg = (profile.get("modes") or {}).get(mode) or {} | ||||||||||||||||||||||||
| 221 | + for role_key, blk in cfg.items(): | ||||||||||||||||||||||||
| 222 | + if role_key == "aggregated" and mode == "aggregated": | ||||||||||||||||||||||||
| 223 | + target = svc.get("aggregated") | ||||||||||||||||||||||||
| 224 | + elif mode == "pd" and role_key in ("prefill", "decode"): | ||||||||||||||||||||||||
| 225 | + target = svc.get("pd", {}).get(role_key) | ||||||||||||||||||||||||
| 226 | + else: | ||||||||||||||||||||||||
| 227 | + continue # 与当前 mode 无关的 role 配置不叠加 | ||||||||||||||||||||||||
| 228 | + if target is None: | ||||||||||||||||||||||||
| 229 | + continue | ||||||||||||||||||||||||
| 230 | + if "extraArgs" in blk: | ||||||||||||||||||||||||
| 231 | + target["extraArgs"] = list(blk["extraArgs"]) | ||||||||||||||||||||||||
| 232 | + if "env" in blk: | ||||||||||||||||||||||||
| 233 | + target["env"] = copy.deepcopy(blk["env"]) | ||||||||||||||||||||||||
| 234 | + if "resources" in profile: | ||||||||||||||||||||||||
| 235 | + svc["resources"] = copy.deepcopy(profile["resources"]) | ||||||||||||||||||||||||
| 236 | + for k, v in (profile.get("global") or {}).items(): | ||||||||||||||||||||||||
| 237 | + values.setdefault("global", {})[k] = v | ||||||||||||||||||||||||
| 238 | + | ||||||||||||||||||||||||
| 239 | + | ||||||||||||||||||||||||
| 240 | +def walk_find_label(node: Any, path: str = "") -> list[tuple[str, str]]: | ||||||||||||||||||||||||
| 241 | + """找 values 中显式 model label 覆盖(S-21:路径含 model 的 label 键)。""" | ||||||||||||||||||||||||
| 242 | + hits: list[tuple[str, str]] = [] | ||||||||||||||||||||||||
| 243 | + if isinstance(node, dict): | ||||||||||||||||||||||||
| 244 | + for k, v in node.items(): | ||||||||||||||||||||||||
| 245 | + p = f"{path}.{k}" if path else k | ||||||||||||||||||||||||
| 246 | + if k.lower() == "label" and "model" in path.lower() and isinstance(v, str): | ||||||||||||||||||||||||
| 247 | + hits.append((p, v)) | ||||||||||||||||||||||||
| 248 | + else: | ||||||||||||||||||||||||
| 249 | + hits.extend(walk_find_label(v, p)) | ||||||||||||||||||||||||
| 250 | + elif isinstance(node, list): | ||||||||||||||||||||||||
| 251 | + for i, v in enumerate(node): | ||||||||||||||||||||||||
| 252 | + hits.extend(walk_find_label(v, f"{path}[{i}]")) | ||||||||||||||||||||||||
| 253 | + return hits | ||||||||||||||||||||||||
| 254 | + | ||||||||||||||||||||||||
| 255 | + | ||||||||||||||||||||||||
| 256 | +def main() -> int: | ||||||||||||||||||||||||
| 257 | + ap = argparse.ArgumentParser(description="InferNex 确定性 values 渲染(intent → values)") | ||||||||||||||||||||||||
| 258 | + ap.add_argument("--intent", required=True, type=Path) | ||||||||||||||||||||||||
| 259 | + ap.add_argument("--chart-dir", required=True, type=Path) | ||||||||||||||||||||||||
| 260 | + ap.add_argument("--output", required=True, type=Path) | ||||||||||||||||||||||||
| 261 | + ap.add_argument("--report", type=Path, default=None) | ||||||||||||||||||||||||
| 262 | + ap.add_argument("--models-dir", type=Path, | ||||||||||||||||||||||||
| 263 | + default=Path(__file__).resolve().parent.parent / "models") | ||||||||||||||||||||||||
| 264 | + ap.add_argument("--aggregated-template", type=Path, | ||||||||||||||||||||||||
| 265 | + default=Path(__file__).resolve().parent.parent / "templates" / "aggregated-role.yaml") | ||||||||||||||||||||||||
| 266 | + args = ap.parse_args() | ||||||||||||||||||||||||
| 267 | + | ||||||||||||||||||||||||
| 268 | + intent = load_yaml(args.intent) | ||||||||||||||||||||||||
| 269 | + baseline = load_yaml(args.chart_dir / "values.yaml") | ||||||||||||||||||||||||
| 270 | + | ||||||||||||||||||||||||
| 271 | + # --- 模型与 profile 解析(alias 命中 → canonical id;未命中 → C1 前点选确认) --- | ||||||||||||||||||||||||
| 272 | + model_input = intent.get("model") | ||||||||||||||||||||||||
| 273 | + profile, profile_name = load_profile(args.models_dir, model_input) | ||||||||||||||||||||||||
| 274 | + model = (profile or {}).get("model") or model_input \ | ||||||||||||||||||||||||
| 275 | + or baseline.get("global", {}).get("modelName") | ||||||||||||||||||||||||
| 276 | + if not model: | ||||||||||||||||||||||||
| 277 | + die("intent.model 未填且 Chart 基线无 global.modelName") | ||||||||||||||||||||||||
| 278 | + if model_input and not profile: | ||||||||||||||||||||||||
| 279 | + profile_note = (f"未命中 models/ 档案({model_input}):C1 前须点选确认完整 HF id;" | ||||||||||||||||||||||||
| 280 | + f"欢迎按 models/_schema.md 沉淀 profile") | ||||||||||||||||||||||||
| 281 | + else: | ||||||||||||||||||||||||
| 282 | + profile_note = f"命中 profile: {profile_name}" if profile else "未指定模型(Chart 默认)" | ||||||||||||||||||||||||
| 283 | + | ||||||||||||||||||||||||
| 284 | + mode = intent.get("mode") or "pd" | ||||||||||||||||||||||||
| 285 | + if mode not in ("pd", "aggregated"): | ||||||||||||||||||||||||
| 286 | + die(f"intent.mode 非法: {mode}") | ||||||||||||||||||||||||
| 287 | + routing = intent.get("routing") or "kv-cache-aware" | ||||||||||||||||||||||||
| 288 | + | ||||||||||||||||||||||||
| 289 | + profile_mode_cfg = ((profile or {}).get("modes") or {}).get(mode) or {} | ||||||||||||||||||||||||
| 290 | + topos = resolve_role_topologies(intent, mode, intent.get("cards"), profile_mode_cfg) | ||||||||||||||||||||||||
| 291 | + | ||||||||||||||||||||||||
| 292 | + svc_slug = service_slug(model) | ||||||||||||||||||||||||
| 293 | + if mode == "aggregated": | ||||||||||||||||||||||||
| 294 | + svc_name = f"{svc_slug}-aggregated-01" | ||||||||||||||||||||||||
| 295 | + expected_lws = [f"{svc_name}-aggregated"] | ||||||||||||||||||||||||
| 296 | + else: | ||||||||||||||||||||||||
| 297 | + svc_name = f"{svc_slug}-{role_cards(topos['prefill'])}p{role_cards(topos['decode'])}d-01" | ||||||||||||||||||||||||
| 298 | + expected_lws = [f"{svc_name}-prefill", f"{svc_name}-decode"] | ||||||||||||||||||||||||
| 299 | + | ||||||||||||||||||||||||
| 300 | + # --- patch(深拷贝基线,保持键序) --- | ||||||||||||||||||||||||
| 301 | + values = copy.deepcopy(baseline) | ||||||||||||||||||||||||
| 302 | + values.setdefault("global", {})["modelName"] = model | ||||||||||||||||||||||||
| 303 | + | ||||||||||||||||||||||||
| 304 | + hermes = values.setdefault("hermes-router", {}).setdefault("inferenceExtension", {}) | ||||||||||||||||||||||||
| 305 | + routing_blk = hermes.setdefault("routing", {}) | ||||||||||||||||||||||||
| 306 | + if "tokenizer" in routing_blk: | ||||||||||||||||||||||||
| 307 | + routing_blk["tokenizer"]["model"] = model | ||||||||||||||||||||||||
| 308 | + routing_blk["deploymentMode"] = "aggregate" if mode == "aggregated" else "pd" | ||||||||||||||||||||||||
| 309 | + routing_blk["profile"] = routing | ||||||||||||||||||||||||
| 310 | + | ||||||||||||||||||||||||
| 311 | + services = values.setdefault("inference-backend", {}).get("services") or [] | ||||||||||||||||||||||||
| 312 | + if len(services) != 1: | ||||||||||||||||||||||||
| 313 | + die(f"仅支持单 service(当前 {len(services)});多 service 见演进计划 M5") | ||||||||||||||||||||||||
| 314 | + svc = services[0] | ||||||||||||||||||||||||
| 315 | + svc["name"] = svc_name | ||||||||||||||||||||||||
| 316 | + svc["mode"] = mode | ||||||||||||||||||||||||
| 317 | + | ||||||||||||||||||||||||
| 318 | + if mode == "aggregated": | ||||||||||||||||||||||||
| 319 | + agg_tpl = load_yaml(args.aggregated_template) | ||||||||||||||||||||||||
| 320 | + svc["kvTransferConfig"] = copy.deepcopy(agg_tpl["kvTransferConfig"]) | ||||||||||||||||||||||||
| 321 | + svc["aggregated"] = copy.deepcopy(agg_tpl["aggregated"]) | ||||||||||||||||||||||||
| 322 | + svc.pop("pd", None) | ||||||||||||||||||||||||
| 323 | + svc.pop("pdGroupID", None) | ||||||||||||||||||||||||
| 324 | + set_role_parallel(svc["aggregated"], topos["aggregated"]) | ||||||||||||||||||||||||
| 325 | + active_roles = [svc["aggregated"]] | ||||||||||||||||||||||||
| 326 | + else: | ||||||||||||||||||||||||
| 327 | + svc["pdGroupID"] = svc_name | ||||||||||||||||||||||||
| 328 | + for role in ("prefill", "decode"): | ||||||||||||||||||||||||
| 329 | + set_role_parallel(svc.setdefault("pd", {}).setdefault(role, {}), topos[role]) | ||||||||||||||||||||||||
| 330 | + active_roles = [svc["pd"]["prefill"], svc["pd"]["decode"]] | ||||||||||||||||||||||||
| 331 | + | ||||||||||||||||||||||||
| 332 | + # --- profile overlay(模型级差异,叠加在模式算法之上) --- | ||||||||||||||||||||||||
| 333 | + apply_profile_overlay(svc, values, mode, profile) | ||||||||||||||||||||||||
| 334 | + | ||||||||||||||||||||||||
| 335 | + rsg = (values.setdefault("pd-orchestrator", {}) | ||||||||||||||||||||||||
| 336 | + .setdefault("resourcescalinggroup", {}).setdefault("instanceConfig", {})) | ||||||||||||||||||||||||
| 337 | + ref_tpl = {} | ||||||||||||||||||||||||
| 338 | + existing = rsg.get("targetResources") or [] | ||||||||||||||||||||||||
| 339 | + if existing and isinstance(existing[0].get("resourceRef"), dict): | ||||||||||||||||||||||||
| 340 | + ref_tpl = {k: v for k, v in existing[0]["resourceRef"].items() if k != "name"} | ||||||||||||||||||||||||
| 341 | + if not ref_tpl: | ||||||||||||||||||||||||
| 342 | + ref_tpl = {"apiVersion": "leaderworkerset.x-k8s.io/v1", "kind": "LeaderWorkerSet"} | ||||||||||||||||||||||||
| 343 | + rewrite_rsg(rsg, mode, svc_name, ref_tpl) | ||||||||||||||||||||||||
| 344 | + | ||||||||||||||||||||||||
| 345 | + if intent.get("nodePort"): | ||||||||||||||||||||||||
| 346 | + values.setdefault("inferenceGateway", {}).setdefault("service", {}) \ | ||||||||||||||||||||||||
| 347 | + .setdefault("nodePorts", {})["http"] = intent["nodePort"] | ||||||||||||||||||||||||
| 348 | + | ||||||||||||||||||||||||
| 349 | + components = intent.get("components") or {} | ||||||||||||||||||||||||
| 350 | + if components.get("eagleEye") is not None: | ||||||||||||||||||||||||
| 351 | + values.setdefault("eagle-eye", {})["enabled"] = components["eagleEye"] | ||||||||||||||||||||||||
| 352 | + if components.get("pdOrchestrator") is not None: | ||||||||||||||||||||||||
| 353 | + on = components["pdOrchestrator"] | ||||||||||||||||||||||||
| 354 | + pdo = values.setdefault("pd-orchestrator", {}) | ||||||||||||||||||||||||
| 355 | + for sub in ("resourcescalinggroup", "elastic-scaler", "tidal"): | ||||||||||||||||||||||||
| 356 | + if sub in pdo and isinstance(pdo[sub], dict): | ||||||||||||||||||||||||
| 357 | + pdo[sub]["enabled"] = on | ||||||||||||||||||||||||
| 358 | + | ||||||||||||||||||||||||
| 359 | + params = intent.get("inferenceParams") or {} | ||||||||||||||||||||||||
| 360 | + if params: | ||||||||||||||||||||||||
| 361 | + apply_inference_params(params, active_roles) | ||||||||||||||||||||||||
| 362 | + | ||||||||||||||||||||||||
| 363 | + # --- 内嵌自检(V 系列;结果并入 preflight-report) --- | ||||||||||||||||||||||||
| 364 | + checks: list[dict[str, Any]] = [] | ||||||||||||||||||||||||
| 365 | + | ||||||||||||||||||||||||
| 366 | + def add(cid: str, ok: bool, evidence: str) -> None: | ||||||||||||||||||||||||
| 367 | + checks.append({"id": cid, "status": "pass" if ok else "fail", "evidence": evidence}) | ||||||||||||||||||||||||
| 368 | + | ||||||||||||||||||||||||
| 369 | + dm = routing_blk.get("deploymentMode") | ||||||||||||||||||||||||
| 370 | + add("S-14", (mode == "aggregated" and dm == "aggregate") or (mode == "pd" and dm == "pd"), | ||||||||||||||||||||||||
入口处 ![]() ![]() | |||||||||||||||||||||||||
| 371 | + f"mode={mode}, deploymentMode={dm}") | ||||||||||||||||||||||||
| 372 | + | ||||||||||||||||||||||||
| 373 | + if mode == "aggregated": | ||||||||||||||||||||||||
| 374 | + ok_b = "aggregated" in svc_name and not re.search(r"(\d+p\d+d|2p2d)", svc_name) | ||||||||||||||||||||||||
| 375 | + else: | ||||||||||||||||||||||||
| 376 | + ok_b = bool(re.search(r"-\d+p\d+d-\d+$", svc_name)) and "aggregated" not in svc_name | ||||||||||||||||||||||||
| 377 | + add("S-17", ok_b, f"name={svc_name}") | ||||||||||||||||||||||||
| 378 | + | ||||||||||||||||||||||||
| 379 | + got_refs = sorted(r["resourceRef"]["name"] for r in rsg.get("targetResources") or []) | ||||||||||||||||||||||||
| 380 | + add("S-16", got_refs == sorted(expected_lws), | ||||||||||||||||||||||||
| 381 | + f"RSG refs={got_refs} vs 预期 LWS={sorted(expected_lws)}") | ||||||||||||||||||||||||
| 382 | + | ||||||||||||||||||||||||
| 383 | + label_hits = walk_find_label(values, "values") | ||||||||||||||||||||||||
| 384 | + expect_label = model_label(model) | ||||||||||||||||||||||||
| 385 | + ok7 = all(v == expect_label for _, v in label_hits) | ||||||||||||||||||||||||
| 386 | + add("S-21", ok7, | ||||||||||||||||||||||||
| 387 | + f"显式 label 覆盖 {label_hits or '无'};期望 {expect_label}" if label_hits else | ||||||||||||||||||||||||
| 388 | + f"无显式覆盖(Chart 派生);算法 label={expect_label}") | ||||||||||||||||||||||||
| 389 | + | ||||||||||||||||||||||||
| 390 | + proxy_on = bool(svc.get("pd", {}).get("proxyServer", {}).get("enabled")) | ||||||||||||||||||||||||
| 391 | + if mode == "pd" and proxy_on: | ||||||||||||||||||||||||
| 392 | + add("S-22", svc.get("pdGroupID") == svc_name, f"pdGroupID={svc.get('pdGroupID')} vs name={svc_name}") | ||||||||||||||||||||||||
| 393 | + | ||||||||||||||||||||||||
| 394 | + cache_path = values.get("global", {}).get("cachePath") | ||||||||||||||||||||||||
| 395 | + add("S-19", bool(cache_path), f"cachePath={cache_path!r}") | ||||||||||||||||||||||||
| 396 | + | ||||||||||||||||||||||||
| 397 | + if routing == "kv-cache-aware": | ||||||||||||||||||||||||
| 398 | + indexer_on = bool(values.get("cache-indexer", {}).get("enabled")) | ||||||||||||||||||||||||
| 399 | + add("S-15", indexer_on, f"routing=kv-cache-aware, cache-indexer.enabled={indexer_on}") | ||||||||||||||||||||||||
| 400 | + | ||||||||||||||||||||||||
| 401 | + gw_name = values.get("inferenceGateway", {}).get("name") | ||||||||||||||||||||||||
| 402 | + route_gw = values.get("hermes-router", {}).get("httpRoute", {}).get("inferenceGatewayName") | ||||||||||||||||||||||||
| 403 | + add("S-20", gw_name == route_gw, f"gateway={gw_name!r} ↔ httpRoute={route_gw!r}") | ||||||||||||||||||||||||
| 404 | + | ||||||||||||||||||||||||
| 405 | + # --- 输出 --- | ||||||||||||||||||||||||
| 406 | + args.output.parent.mkdir(parents=True, exist_ok=True) | ||||||||||||||||||||||||
| 407 | + with args.output.open("w", encoding="utf-8") as f: | ||||||||||||||||||||||||
| 408 | + yaml.safe_dump(values, f, sort_keys=False, default_flow_style=False, | ||||||||||||||||||||||||
| 409 | + allow_unicode=True, width=4096) | ||||||||||||||||||||||||
| 410 | + | ||||||||||||||||||||||||
| 411 | + total_cards = sum(role_cards(o) for o in topos.values()) | ||||||||||||||||||||||||
| 412 | + report = { | ||||||||||||||||||||||||
| 413 | + "render_values": "ok", | ||||||||||||||||||||||||
| 414 | + "derived": { | ||||||||||||||||||||||||
| 415 | + "model": model, "model_label": expect_label, "mode": mode, | ||||||||||||||||||||||||
| 416 | + "service_name": svc_name, "lws": expected_lws, | ||||||||||||||||||||||||
| 417 | + "rsg_refs": got_refs, "cards_total": total_cards, | ||||||||||||||||||||||||
| 418 | + "per_role": {r: {**o, "role_cards": role_cards(o)} for r, o in topos.items()}, | ||||||||||||||||||||||||
| 419 | + "deployment_mode": dm, "routing": routing, | ||||||||||||||||||||||||
| 420 | + "profile": {"matched": bool(profile), "file": profile_name, "note": profile_note}, | ||||||||||||||||||||||||
| 421 | + }, | ||||||||||||||||||||||||
| 422 | + "checks": checks, | ||||||||||||||||||||||||
| 423 | + } | ||||||||||||||||||||||||
| 424 | + text = json.dumps(report, indent=2, ensure_ascii=False) | ||||||||||||||||||||||||
| 425 | + print(text) | ||||||||||||||||||||||||
| 426 | + if args.report: | ||||||||||||||||||||||||
| 427 | + args.report.parent.mkdir(parents=True, exist_ok=True) | ||||||||||||||||||||||||
| 428 | + args.report.write_text(text + "\n", encoding="utf-8") | ||||||||||||||||||||||||
| 429 | + | ||||||||||||||||||||||||
| 430 | + failed = [c for c in checks if c["status"] == "fail"] | ||||||||||||||||||||||||
| 431 | + return 1 if failed else 0 | ||||||||||||||||||||||||
| 432 | + | ||||||||||||||||||||||||
| 433 | + | ||||||||||||||||||||||||
| 434 | +if __name__ == "__main__": | ||||||||||||||||||||||||
| 435 | + raise SystemExit(main()) | ||||||||||||||||||||||||


新增
skills/infernex-deploy/test/目录(当前不存在),且 PR 描述中未提及测试计划。若测试文件确属后续迭代,应保留此条目并在 PR 中说明;否则移除。