已合并
feat(skills): add infernex-deploy agent skill (v1.0.0 - v1.4.0) #245
feat(skills): add infernex-deploy agent skill (v1.0.0 - v1.4.0) #245
已合并
xinhongchen创建于 9月10日
共 26 个文件变更+3304-0
@@ -1 +1,6 @@
1.idea/1.idea/
2+__pycache__/
3+*.pyc
4+ 
5+# infernex-deploy skill 测试(暂存本地,不入 PR)
openFuyao-bot
openFuyao-botopenFuyao-bot15 天前

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

likedislike
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:
openFuyao-botopenFuyao-bot
openFuyao-botopenFuyao-bot9月30日

SKILL.md 版本号与 PR 版本不一致。PR 描述明确为 v1.0.0 基线,但 SKILL.md 中 version 为 "1.4.0"。建议修正为 "1.0.0" 或在 PR 描述中说明版本号演进逻辑。

改动建议
10
+ ---
11
+ name: infernex-deploy
12
+ description: >-
13
+ 在 Kubernetes(昇腾 NPU)上部署或卸载 InferNex(openFuyao AI 推理 Helm Chart)。
14
+ 包含环境采集、intent 配置确认(C1 门)、预检深度选择(C2 门,硬件 checker 默认跳过、
15
+ 连通性 ping 默认关闭)、软件+硬件预检(C3 门确认报告)、helm 安装/升级、网关推理验证、
16
+ 仅卸载 InferNex。不包含 InferNex-Bridge。
17
+ 触发词:部署 InferNex、安装 infernex、一键推理部署、预检 infernex、跑 checker、
18
+ 升级 release、卸载 infernex 等。
10
- metadata:
19
+ metadata:
20
+ author: openFuyao
21
+ version: "1.0.0"
22
+ language: zh-CN
23
+ ---
应用建议
likedislike
openFuyao-botopenFuyao-bot29 天前

PR 描述称「v1.0.0,社区第一版基线」,但 SKILL.md frontmatter 中 version 已为 "1.4.0"。经 commit 历史核实(11 个提交逐步演进),1.4.0 为当前真实版本,PR 描述未及时更新。建议同步 PR 描述中的版本号,或在 SKILL.md 中注明版本演进说明。

likedislike
11+ author: openFuyao
openFuyao-bot
openFuyao-botopenFuyao-bot9月30日

SKILL.md 版本号与 PR 标题不一致。PR 标题为 v1.0.0,但 SKILL.md 中 version 为 "1.4.0"。需确认是否为有意为之,若非则应同步为 "1.0.0"。

改动建议
11
+ ---
12
+ name: infernex-deploy
13
+ description: >-
14
+ 在 Kubernetes(昇腾 NPU)上部署或卸载 InferNex(openFuyao AI 推理 Helm Chart)。
15
+ 包含环境采集、intent 配置确认(C1 门)、预检深度选择(C2 门,硬件 checker 默认跳过、
16
+ 连通性 ping 默认关闭)、软件+硬件预检(C3 门确认报告)、helm 安装/升级、网关推理验证、
17
+ 仅卸载 InferNex。不包含 InferNex-Bridge。
18
+ 触发词:部署 InferNex、安装 infernex、一键推理部署、预检 infernex、跑 checker、
19
+ 升级 release、卸载 infernex 等。
20
+ metadata:
11
- author: openFuyao
21
+ author: openFuyao
22
+ version: "1.0.0"
23
+ language: zh-CN
24
+ ---
应用建议
likedislike
12+ version: "1.4.0"
13+ language: zh-CN
14+---
15+ 
16+# InferNex 部署 Skill
openFuyao-bot
openFuyao-botopenFuyao-bot9月10日

metadata.version 为 "1.0.0" 与 PR 描述中 v0.3.4 不一致。建议统一版本号或明确 1.0.0 为 Skill 接口版本。

改动建议
16
+ metadata:
17
+ author: openFuyao
18
+ version: "0.3.4"
16
- # InferNex 部署 Skill
19
+ language: zh-CN
应用建议
likedislike
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+## 硬门禁(不可跳过)
openFuyao-bot
openFuyao-botopenFuyao-bot9月30日

[严重] SKILL.md 元数据 version 与 PR 描述不一致

PR 描述明确标注 v1.0.0(社区第一版基线),但 SKILL.md 中 version 为 "1.4.0"。应同步为 "1.0.0",或确认 1.4.0 为有意跳版并说明版本演进逻辑。

改动建议
26
+ metadata:
26
- ## 硬门禁(不可跳过)
27
+ author: openFuyao
28
+ version: "1.0.0"
29
+ language: zh-CN
应用建议
likedislike
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` |
openFuyao-bot
openFuyao-botopenFuyao-bot9月30日

硬门禁 9 引用的自检 ID(S-14~S-22)超出 checks.yaml 实际范围。checks.yaml 中实际仅定义 S-14 至 S-17。SKILL.md 应改为 S-14~S-17,或补充 S-18~S-22 检查项。

likedislike
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:
openFuyao-bot
openFuyao-botopenFuyao-bot9月30日

此条代码评论区间+8至+10

_schema.md 规定「裸短名(glm-5.2)只挂在最常用变体上」。当前 w8a8 和 w4a8c8 都未挂裸短名,需确认两个变体的别名策略是否一致。

likedislike
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
openFuyao-botopenFuyao-bot
openFuyao-botopenFuyao-bot9月30日

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 中增加"未验证模型部署风险提示"。

likedislike
openFuyao-botopenFuyao-bot13 天前

此条代码评论区间+46至+48

[轻微] 大部分模型 profile 的 verified 字段为 null,仅 qwen3-8b.yaml 标注实机验证。SKILL.md 中未对 verified: null 的 profile 做显著风险提示。

修复建议:在 SKILL.md 或 _schema.md 中增加"verified: null = reference 规格,部署前须经 C3 全链路校验"的醒目提示。

likedislike
@@ -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)
openFuyao-bot
openFuyao-botopenFuyao-bot13 天前

此条代码评论区间+75至+90

[轻微] S-02 标记 remediable: true,但修复项为"按官方文档装 LWS",属于复杂手动操作,无法自动修复。remediable 标志与 remediation 字段语义不一致,可能误导 C3r 自动修复流程。

修复建议:将 remediable 改为 false,或在 remediation 中明确列出需用户执行的 kubectl 命令序列。

likedislike
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)
openFuyao-bot
openFuyao-botopenFuyao-bot29 天前

此条代码评论区间+85至+96

S-02 检查项 sub 字段声明 6 项(a-f),但 cmd 列表仅 5 条命令。sub 字段仅用于文档展示,不影响执行逻辑,但存在文档误导风险。建议删除 sub 字段或修正为 5 项(a-e)。

likedislike
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
openFuyao-bot
openFuyao-botopenFuyao-bot13 天前

此条代码评论区间+105至+115

[轻微] S-04 的 severity_hard_when 在 pd-orchestrator 启用时将 metrics-server 可用性检查升为硬性,但依赖关系未在文档中充分说明。

修复建议:在 note 字段中补充"pd-orchestrator 依赖 metrics-server 获取节点资源数据,故启用时升为硬性"。

likedislike
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
openFuyao-bot
openFuyao-botopenFuyao-bot9月30日

[重要] 脚本文件缺少 shebang 与错误处理。修复建议:添加 #!/bin/bash -euo pipefail 或类似 shebang,确保脚本失败时立即退出。

likedislike
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"
openFuyao-bot
openFuyao-botopenFuyao-bot13 天前

此条代码评论区间+59至+62

[严重] FLOATING 正则错误地将 0.0.0-latest 等浮动 tag 识别为固定版本。正则 ^v?[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ 中 [0-9A-Za-z.-]+ 允许任意字母、数字、点、减号,导致 0.0.0-latest、0.0.0-dev 等浮动 tag 均匹配成功,被误判为固定版本。浮动 tag 内容随时间漂移,缓存会过期。

改动建议
59
- # tgz contains top-level infernex/ directory
59
+ # 仅允许 semver 风格的 prerelease(数字/短标识符),排除浮动 tag
60
+ if [[ "${CHART_VERSION}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$ ]] \
60
- if [[ -d "${CHART_DIR}/infernex" ]]; then
61
+ && [[ ! "${CHART_VERSION}" =~ ~(latest|dev|nightly|master|main|head)~ ]]; then
61
- mv "${CHART_DIR}/infernex"/* "${CHART_DIR}/"
62
- rdir="${CHART_DIR}/infernex"
62
+ FLOATING=0
63
+ fi
应用建议
likedislike
63+ rmdir "${rdir}" 2>/dev/null || true
openFuyao-bot
openFuyao-botopenFuyao-bot15 天前

浮空版本检测正则缺陷:0.0.0-latest 匹配 semver 正则被误判为固定版本,导致 Chart 缓存不刷新。应增加显式排除列表排除 latest、dev、nightly、master 等已知浮空标签名。

likedislike
64+ fi
65+ rm -rf "${PULL_DIR}"
openFuyao-bot
openFuyao-botopenFuyao-bot15 天前

使用 date -r 非 POSIX,macOS 下不可用。建议改用 stat -c %y(Linux)+ stat -f %m(macOS)条件判断,或用 Python os.path.getmtime。

likedislike
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
openFuyao-bot
openFuyao-botopenFuyao-bot29 天前

curl 下载缺少 --fail 选项,HTTP 错误(如 404)时不会返回非零退出码,可能导致下载错误内容后继续执行。

改动建议
87
- fi
87
+ curl -fsSL --fail "${URL}" -o "${CHECKER_BIN}"
应用建议
likedislike
88+fi
89+ 
90+# --- infernex-checker binary(默认跳过) ---
91+CHECKER_BIN="${INFERNEX_CHECKER:-${CACHE_ROOT}/bin/infernex-checker}"
92+ 
openFuyao-bot
openFuyao-botopenFuyao-bot13 天前

此条代码评论区间+74至+92

[轻微] pull_chart 未处理 tar 解压失败的明确报错。tar -xzf 若失败(如 tgz 损坏、磁盘满),set -euo pipefail 会触发脚本退出,但错误信息不够明确。

改动建议
74
+ tar -xzf "${TGZ}" -C "${CHART_DIR}" --strip-components=0 || \
74
- if [[ "${FLOATING}" == "1" ]]; then
75
+ { echo "tar 解压失败:${TGZ}" >&2; exit 1; }
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
88
- fi
89
-
90
- # --- infernex-checker binary(默认跳过) ---
91
- CHECKER_BIN="${INFERNEX_CHECKER:-${CACHE_ROOT}/bin/infernex-checker}"
应用建议
likedislike
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
openFuyao-bot
openFuyao-botopenFuyao-bot13 天前

此条代码评论区间+98至+108

[重要] bootstrap.sh 下载 infernex-checker 二进制时仅依赖 HTTPS 传输层安全,无 SHA256 校验或签名验证,存在供应链投毒风险。

修复建议:(a) 在 releases 发布时附带 SHA256SUMS 与签名文件;(b) 下载后校验 sha256sum -c SHA256SUMS;(c) 或通过 cosign/sigstore 做签名验证。

likedislike
109+ ;;
110+ esac
openFuyao-bot
openFuyao-botopenFuyao-bot9月12日

[轻微] bootstrap.sh 未校验 curl 可用性。建议增加 command -v curl >/dev/null 2>&1 检查。

likedislike
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
openFuyao-bot
openFuyao-botopenFuyao-bot13 天前

此条代码评论区间+118至+138

[轻微] fetch_exporter_pods 使用 kubectl get pods -A -o json 拉取全集群 Pod 列表,在大规模集群中可能响应缓慢。

修复建议:将 -A 限定为 npu-operator 命名空间,或通过字段选择器 --field-selector=status.phase=Running 缩小范围。

likedislike
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:
openFuyao-bot
openFuyao-botopenFuyao-bot13 天前

此条代码评论区间+175至+183

[重要] parse_exporter_metrics 正则对 OpenMetrics 格式过于脆弱。_LINE_RE 假设 metrics 行格式为 metric_name{label1="val1",...} value,但 OpenMetrics 规范允许标签值中包含转义引号、Unicode 字符,以及无法匹配的格式(如无标签的 metric、多行展开的文本格式)。

改动建议
175
+ # 使用成熟的 OpenMetrics 解析库替代手写正则
176
+ from prometheus_client.parser import text_string_to_metric_families
177
+
175
- """解析 metrics 文本 → (全部卡 id, 在用卡 id)。纯函数,可单测。"""
178
+ def parse_exporter_metrics(text: str) -> dict[str, float]:
179
+ result = {}
180
+ try:
176
- all_ids, used_ids = set(), set()
181
+ for family in text_string_to_metric_families(text):
177
- for line in body.splitlines():
182
+ for sample in family.samples:
178
- m = _LINE_RE.match(line)
179
- if not m:
180
- continue
181
- metric, labels = m.group(1), m.group(2)
183
+ result[sample.name] = sample.value
182
- idm = _ID_RE.search(labels)
183
- if not idm:
184
+ except Exception:
185
+ # 解析失败时降级,记录 warning
186
+ pass
187
+ return result
应用建议
likedislike
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+ 
openFuyao-bot
openFuyao-botopenFuyao-bot13 天前

此条代码评论区间+97至+99

[轻微] run_cmd 超时返回码 124 未在文档中说明。124 是 GNU timeout 命令的标准退出码,调用方将 124 视为失败(status=fail)符合预期但缺乏显式说明。

改动建议
97
+ def run_cmd(cmd: str, placeholders: dict[str, str], timeout: int) -> tuple[int, str]:
98
+ """占位符替换后经 bash -c 执行;返回 (exit, 原始输出)。
99
+
97
- except subprocess.TimeoutExpired:
100
+ 超时时返回码为 124(GNU timeout 标准退出码)。
98
- return 124, f"超时(>{timeout}s)"
101
+ """
应用建议
likedislike
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],
openFuyao-bot
openFuyao-botopenFuyao-bot9月30日

此条代码评论区间+112至+121

run_cmd 函数将多行输出压缩为单行,丢失关键诊断信息。S-13(helm lint + helm template)的输出包含结构化错误信息,单行化后无法阅读。建议保留原始换行或截断至合理行数。

改动建议
112
+ out = (proc.stdout + proc.stderr).strip()
113
+ # 保留原始换行,截断至 500 行避免内存膨胀
114
+ lines = out.split('\n')
112
- if values_p.exists():
115
+ if len(lines) > 500:
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")
116
+ out = '\n'.join(lines[:500] + ['... (truncated)'])
118
- self.mode = self.intent.get("mode") or "pd"
119
- # new-install: helm status 失败(release 不存在)
120
- proc = subprocess.run(
117
+ return proc.returncode, out
121
- ["helm", "status", args.release, "-n", args.namespace],
应用建议
likedislike
122+ capture_output=True, text=True, check=False)
123+ self.new_install = proc.returncode != 0
openFuyao-bot
openFuyao-botopenFuyao-bot13 天前

此条代码评论区间+117至+123

[重要] _humanize_skip 正则对包含多组括号的 evidence 解析不准确。\[(.+)\] 使用贪婪匹配,若 evidence 包含多组括号会匹配到最后一个 ],导致提取的内容包含多余文本。

改动建议
117
+ def _humanize_skip(evidence: str) -> str:
118
+ import re as _re
119
+ m = _re.search(r"\[(.+?)\]", evidence)
120
+ if not m:
121
+ return evidence
122
+ parts = []
117
- self.online = not (self.intent.get("chart") or {}).get("dir")
123
+ for cond in (c.strip().strip("'") for c in m.group(1).split(",")):
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],
122
- capture_output=True, text=True, check=False)
123
- self.new_install = proc.returncode != 0
124
+ parts.append(COND_LABELS.get(cond, cond))
125
+ return ";".join(parts)
应用建议
likedislike
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")
openFuyao-botopenFuyao-bot
openFuyao-botopenFuyao-bot13 天前

此条代码评论区间+122至+135

[轻微] run_cmd 使用 bash -c 执行命令,若 checks.yaml 被篡改(如供应链攻击),可直接执行任意 shell 命令。

修复建议:在 SKILL.md 安全章节增加"checks.yaml 为 Skill 可信源,禁止从外部动态加载"的声明。

likedislike
openFuyao-botopenFuyao-bot13 天前

此条代码评论区间+127至+135

[轻微] run_cmd 将 stdout/stderr 拼接后原样存入报告,无输出大小上限,单条命令输出过大时可能撑爆内存或使报告不可读。

修复建议:对 out 做长度截断(如 out[:10000])并在 evidence 中标注 [已截断,原始长度 N]。

likedislike
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"]
openFuyao-bot
openFuyao-botopenFuyao-bot13 天前

此条代码评论区间+196至+199

[重要] values:pd-orchestrator 条件仅检查 resourcescalinggroup.enabled,遗漏顶层 enabled 标志。pd-orchestrator 的启用状态可能通过 components.pdOrchestrator(用户显式设置)或 Chart 默认 pd-orchestrator.enabled 控制。若 Chart 默认 pd-orchestrator.enabled=true 而 resourcescalinggroup.enabled=false,S-11 会被错误跳过。

改动建议
196
-
196
+ "values:pd-orchestrator": bool(
197
- def render_md(report: dict[str, Any], titles: dict[str, str] | None = None) -> str:
197
+ ((self.values.get("pd-orchestrator") or {}).get("enabled") or
198
- titles = titles or {}
199
- s = report["summary"]
198
+ (self.intent.get("components") or {}).get("pdOrchestrator"))),
应用建议
likedislike
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("")
openFuyao-bot
openFuyao-botopenFuyao-bot13 天前

此条代码评论区间+218至+222

[严重] Context.new_install 判定不可靠,helm status 非零即视为新装。helm status 返回非零的原因包括但不限于:release 不存在、helm 未安装、集群不可达、认证失败、网络超时等。任何非零返回都会被误判为 new_install=True,导致 S-09 被错误跳过。

改动建议
218
+ proc = subprocess.run(
218
- if it["status"] == "fail" and it.get("remediation"):
219
+ ["helm", "status", args.release, "-n", args.namespace],
219
- ev = f"{ev};可自动修复:{it['remediation']}"
220
+ capture_output=True, text=True, check=False)
220
- ev = " ".join(ev.split()).replace("|", "\\|") # 展示层折叠多行
221
+ # helm status: release not exist → "NOT FOUND";其他错误需区分
222
+ self.new_install = (proc.returncode == 0 or
221
- lines.append(f"| {name} | {status} | {ev} |")
223
+ "not found" in proc.stdout.lower() or
222
- lines.append("")
224
+ "not found" in proc.stderr.lower())
应用建议
likedislike
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+ 
openFuyao-bot
openFuyao-botopenFuyao-bot13 天前

此条代码评论区间+234至+247

[重要] Context.eval_condition 对未知条件静默返回 None,导致含未知 when 条件的检查项被跳过,违反 checks.yaml 注释中"未知条件 → 报错,禁止静默通过"的契约。

修复建议:在函数末尾添加 raise ValueError(f"未知 when 条件: {cond!r}"),并在 run_check 调用处捕获异常、以 status=skip + evidence 含错误信息的方式记录。

likedislike
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"]
openFuyao-bot
openFuyao-botopenFuyao-bot15 天前

docstring 声称"golden UT 保证"确定性渲染,但本次变更未包含任何测试文件(如 test/test_render_values.py)。若测试确属后续迭代,应在 PR 描述中明确说明并附测试计划。

likedislike
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 = {
openFuyao-bot
openFuyao-botopenFuyao-bot9月30日

使用 sys.path.insert(0, ...) 导入 slugs 模块,依赖相对路径。在 pytest 环境下可能因工作目录不同而失败。建议在 slugs.py 中添加 if name == "main" 保护,或使用 importlib.import_module 的绝对导入方式。

likedislike
53+ "maxModelLen": "--max-model-len",
54+ "maxNumBatchedTokens": "--max-num-batched-tokens",
55+ "gpuMemoryUtilization": "--gpu-memory-utilization",
56+ "blockSize": "--block-size",
57+}
58+ 
59+ 
openFuyao-bot
openFuyao-botopenFuyao-bot9月30日

此条代码评论区间+54至+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。

改动建议
54
+ INFERENCE_ARG_MAP = {
55
+ "maxModelLen": "--max-model-len",
54
56
  "maxNumBatchedTokens": "--max-num-batched-tokens",
57
+ "maxNumSeqs": "--max-num-seqs",
55
58
  "gpuMemoryUtilization": "--gpu-memory-utilization",
56
59
  "blockSize": "--block-size",
60
+ "dtype": "--dtype",
61
+ "enforceEager": "--enforce-eager",
57
- }
62
+ }
58
-
应用建议
likedislike
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 [])}
openFuyao-bot
openFuyao-botopenFuyao-bot15 天前

load_profile 静默跳过 YAML 解析异常(except Exception: continue),模型档案格式错误时无日志、无报错,用户可能困惑为何短名未命中。建议记录 warning 日志标明出错文件路径。

likedislike
84+ if target and target in ({pm} | aliases):
85+ return prof, p.stem
openFuyao-bot
openFuyao-botopenFuyao-bot13 天前

此条代码评论区间+70至+85

[轻微] load_profile 使用 p.stem 作为 profile 名称,未携带目录信息,可能导致同名文件冲突。

修复建议:使用 p.relative_to(models_dir).with_suffix('') 作为 profile 标识,或在 report 中输出完整路径以辅助调试。

likedislike
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"),
openFuyao-bot
openFuyao-botopenFuyao-bot15 天前

入口处 raise SystemExit(main()) 与 die() 函数中 sys.exit(2) 风格不一致。建议统一使用 sys.exit() 或 raise SystemExit()。

likedislike
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())