dsh-runnpu 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/compute-mode/preset/agent.cordis.yml +368 -0
- package/compute-mode/preset/preset.yml +3 -0
- package/compute-mode/preset/skills/runnpu/SKILL.md +233 -0
- package/compute-mode/preset/skills/runnpu/VENDOR.txt +11 -0
- package/compute-mode/preset/skills/runnpu/examples/01-dev-environment.sh +26 -0
- package/compute-mode/preset/skills/runnpu/examples/02-training-job.sh +26 -0
- package/compute-mode/preset/skills/runnpu/examples/03-inference-service.sh +24 -0
- package/compute-mode/preset/skills/runnpu/examples/04-fractional-vnpu.sh +14 -0
- package/compute-mode/preset/skills/runnpu/examples/05-troubleshoot.sh +17 -0
- package/compute-mode/preset/skills/runnpu/references/admin.md +60 -0
- package/compute-mode/preset/skills/runnpu/references/data-sources.md +35 -0
- package/compute-mode/preset/skills/runnpu/references/templates-assets.md +61 -0
- package/compute-mode/preset/skills/runnpu/references/training-runs.md +56 -0
- package/compute-mode/preset/skills/runnpu/references/troubleshooting.md +75 -0
- package/compute-mode/preset/skills/runnpu/references/workload-types.md +47 -0
- package/cordis.patch.yml +80 -0
- package/dist/chunk-JLZMOSQC.mjs +1 -0
- package/dist/chunk-JUBIWGKW.mjs +4 -0
- package/dist/chunk-JZXLCA2E.mjs +1 -0
- package/dist/chunk-KQOFBYGE.mjs +9 -0
- package/dist/chunk-XMZG2NP7.mjs +2 -0
- package/dist/chunk-XQPU3AJN.mjs +1 -0
- package/dist/chunk-YKASEMP3.mjs +1 -0
- package/dist/chunk-YPL4VUYF.mjs +4 -0
- package/dist/compute-node.mjs +1 -0
- package/dist/compute-tools.mjs +41 -0
- package/dist/connectors.mjs +3 -0
- package/dist/dsh-runnpu.mjs +43 -0
- package/dist/json.mjs +1 -0
- package/dist/pins.mjs +1 -0
- package/dist/series.mjs +1 -0
- package/dist/training-node.mjs +2 -0
- package/dist/training-tools.mjs +30 -0
- package/dist/ui-host.mjs +1 -0
- package/lib/client.js +189 -0
- package/package.json +78 -0
- package/training-mode/preset/agent.cordis.yml +343 -0
- package/training-mode/preset/preset.yml +3 -0
- package/training-mode/preset/skills/runnpu/SKILL.md +233 -0
- package/training-mode/preset/skills/runnpu/VENDOR.txt +11 -0
- package/training-mode/preset/skills/runnpu/examples/01-dev-environment.sh +26 -0
- package/training-mode/preset/skills/runnpu/examples/02-training-job.sh +26 -0
- package/training-mode/preset/skills/runnpu/examples/03-inference-service.sh +24 -0
- package/training-mode/preset/skills/runnpu/examples/04-fractional-vnpu.sh +14 -0
- package/training-mode/preset/skills/runnpu/examples/05-troubleshoot.sh +17 -0
- package/training-mode/preset/skills/runnpu/references/admin.md +60 -0
- package/training-mode/preset/skills/runnpu/references/data-sources.md +35 -0
- package/training-mode/preset/skills/runnpu/references/templates-assets.md +61 -0
- package/training-mode/preset/skills/runnpu/references/training-runs.md +56 -0
- package/training-mode/preset/skills/runnpu/references/troubleshooting.md +75 -0
- package/training-mode/preset/skills/runnpu/references/workload-types.md +47 -0
- package/training-mode/preset/skills/swanlab-skill/LICENSE +21 -0
- package/training-mode/preset/skills/swanlab-skill/SKILL.md +163 -0
- package/training-mode/preset/skills/swanlab-skill/VENDOR.txt +9 -0
- package/training-mode/preset/skills/swanlab-skill/references/ANALYSIS_GUIDE.md +83 -0
- package/training-mode/preset/skills/swanlab-skill/references/CLI_REFERENCE.md +510 -0
- package/training-mode/preset/skills/swanlab-skill/references/SDK_QUICKSTART.md +341 -0
- package/training-mode/preset/skills/swanlab-skill/references/SWANLAB_CONCEPTS.md +523 -0
- package/training-mode/preset/skills/swanlab-skill/scripts/plot_metrics.py +353 -0
- package/training-mode/preset/skills/swanlab-skill/scripts/runs_benchmark.py +550 -0
- package/training-mode/preset/skills/training-research/SKILL.md +33 -0
- package/training-mode/preset/skills/training-research/references/pipeline.md +45 -0
- package/training-mode/preset/skills/training-research/references/research.md +76 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 场景:给用户建一个带 Jupyter + 持久工作盘的开发环境,交付入口 URL。
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
PROJECT=main
|
|
6
|
+
NAME=nb-demo
|
|
7
|
+
|
|
8
|
+
# 1. 建负载:1 张整卡 + 50Gi 工作盘 + Jupyter;昇腾镜像 root 运行所以 baseline
|
|
9
|
+
runnpu workload create --project "$PROJECT" --name "$NAME" --type workspace \
|
|
10
|
+
--image m.daocloud.io/quay.io/ascend/vllm-ascend:v0.22.1rc1 \
|
|
11
|
+
--xpu 1 --security baseline --jupyter \
|
|
12
|
+
--workspace-pool shared-cephfs --workspace-gib 50 \
|
|
13
|
+
--wait
|
|
14
|
+
|
|
15
|
+
# 2. 拿入口(tools[].url 平台登录态直达;没 url 说明还没 ready 或没权限)
|
|
16
|
+
runnpu workload get "$PROJECT/$NAME" | python3 -c '
|
|
17
|
+
import json,sys
|
|
18
|
+
d=json.load(sys.stdin)
|
|
19
|
+
for t in d["summary"].get("tools") or []:
|
|
20
|
+
print(t["name"], "ready:", t["ready"], "url:", t.get("url","-"))'
|
|
21
|
+
|
|
22
|
+
# 3.(可选)验证卡真的可用
|
|
23
|
+
runnpu workload exec "$PROJECT/$NAME" <<'SH'
|
|
24
|
+
npu-smi info | head -8
|
|
25
|
+
exit
|
|
26
|
+
SH
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 场景:提交一个单机训练任务,带重试与超时,完成后自动清理;轮询到终态并交付结果。
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
PROJECT=main
|
|
6
|
+
NAME=train-demo
|
|
7
|
+
|
|
8
|
+
runnpu workload create --project "$PROJECT" --name "$NAME" --type training-job \
|
|
9
|
+
--image m.daocloud.io/quay.io/ascend/vllm-ascend:v0.22.1rc1 --xpu 1 --security baseline \
|
|
10
|
+
--mount datavolume:/data,name=dv-main-trainset,ro \
|
|
11
|
+
--mount newvolume:/output,pool=shared-cephfs,size=20Gi,persistency=project \
|
|
12
|
+
--command bash --command -c --arg "python /data/train.py --out /output" \
|
|
13
|
+
--backoff-limit 2 --active-deadline-seconds 7200 --ttl-after-finished 86400
|
|
14
|
+
|
|
15
|
+
# 训练可能超过 --wait 的 10 分钟上限,所以自己轮询
|
|
16
|
+
while :; do
|
|
17
|
+
st=$(runnpu workload get "$PROJECT/$NAME" | python3 -c 'import json,sys; print(json.load(sys.stdin)["summary"]["status"])')
|
|
18
|
+
case "$st" in completed|failed|rejected|stopped) break;; esac
|
|
19
|
+
sleep 30
|
|
20
|
+
done
|
|
21
|
+
echo "终态:$st"
|
|
22
|
+
runnpu workload get "$PROJECT/$NAME" | python3 -c '
|
|
23
|
+
import json,sys; d=json.load(sys.stdin)
|
|
24
|
+
p=d.get("progress") or {}
|
|
25
|
+
print("succeeded:",p.get("succeeded"),"failed:",p.get("failed"),"retries:",p.get("retries_used"))'
|
|
26
|
+
[ "$st" = failed ] && runnpu workload logs "$PROJECT/$NAME" --tail 100
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 场景:部署 vLLM 推理服务(权重在项目 PVC 里),nodePort 对外,实测端点可用。
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
PROJECT=main
|
|
6
|
+
NAME=qwen-svc
|
|
7
|
+
# 权重 PVC 必须带 npu.run/project-id=<项目slug> 标签(否则挂载被拒为"不存在")
|
|
8
|
+
WEIGHTS_PVC=model-weights
|
|
9
|
+
|
|
10
|
+
runnpu workload create --project "$PROJECT" --name "$NAME" --type inference \
|
|
11
|
+
--image m.daocloud.io/quay.io/ascend/vllm-ascend:v0.22.1rc1 --xpu 1 --security baseline \
|
|
12
|
+
--mount "pvc:/models,name=$WEIGHTS_PVC,ro" \
|
|
13
|
+
--command bash --command -c \
|
|
14
|
+
--arg "python -m vllm.entrypoints.openai.api_server --model /models/qwen --served-model-name qwen --port 8000" \
|
|
15
|
+
--port http:8000,expose=nodePort --replicas 1 --wait
|
|
16
|
+
|
|
17
|
+
# nodePort 在工具观测里;ready 可能误报 false(run-npu#553),以实测为准
|
|
18
|
+
NP=$(runnpu workload get "$PROJECT/$NAME" | python3 -c '
|
|
19
|
+
import json,sys
|
|
20
|
+
for t in json.load(sys.stdin)["summary"].get("tools") or []:
|
|
21
|
+
if t.get("node_port"): print(t["node_port"]); break')
|
|
22
|
+
NODE_IP=$(runnpu node list | python3 -c 'import json,sys; print(json.load(sys.stdin)["items"][0]["ip"])' 2>/dev/null || echo "<节点IP>")
|
|
23
|
+
echo "端点:http://$NODE_IP:$NP/v1"
|
|
24
|
+
curl -s --max-time 10 "http://$NODE_IP:$NP/v1/models" | head -c 300 || echo "(模型加载中,稍后重试)"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 场景:分卡(vNPU)——单张卡切 25% 算力 + 16GiB 显存。
|
|
3
|
+
# 只支持单卡切分:--xpu 必须为 1(ADR-0030 §6.3 起 CRD 不支持多卡分卡)。
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
runnpu workload create --project main --name frac-demo --type workspace \
|
|
7
|
+
--image m.daocloud.io/quay.io/ascend/vllm-ascend:v0.22.1rc1 --security baseline \
|
|
8
|
+
--xpu 1 --xpu-core 25 --xpu-memory-mib 16384 \
|
|
9
|
+
--command bash --command -c --arg "sleep infinity" --wait
|
|
10
|
+
|
|
11
|
+
# 配额按份额扣:1 × 25 核 = 0.25 卡当量
|
|
12
|
+
runnpu project quota get main | python3 -c '
|
|
13
|
+
import json,sys; d=json.load(sys.stdin)
|
|
14
|
+
for u in d.get("cluster",{}).get("usage",[]) or []: print(u)'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 场景:负载不 running,按决策树定位(只读,随便跑)。
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
W=${1:?用法: 05-troubleshoot.sh <project>/<name>}
|
|
5
|
+
|
|
6
|
+
echo "== 1. 状态与原因"
|
|
7
|
+
runnpu workload get "$W" | python3 -c '
|
|
8
|
+
import json,sys; d=json.load(sys.stdin)["summary"]
|
|
9
|
+
print(d["status"], "-", (d.get("reason") or {}).get("text",""))'
|
|
10
|
+
|
|
11
|
+
echo "== 2. 事件(调度/准入/镜像拉取)"
|
|
12
|
+
runnpu workload events "$W" | python3 -c '
|
|
13
|
+
import json,sys
|
|
14
|
+
for e in json.load(sys.stdin)["items"][:10]: print(e.get("time",""), e.get("reason",""), e.get("message","")[:100])'
|
|
15
|
+
|
|
16
|
+
echo "== 3. 容器日志尾部(CrashLoop 时看 symbol lookup error → 镜像 LD 问题,见 troubleshooting.md)"
|
|
17
|
+
runnpu workload logs "$W" --tail 30 2>/dev/null || echo "(还没有容器日志)"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# 管理面:项目 / 配额 / 成员 / 用户 / 设置 / 审计
|
|
2
|
+
|
|
3
|
+
## 项目与配额
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
runnpu project list / get <slug>
|
|
7
|
+
runnpu project create --slug asr --name 语音识别 --pool ascend910b4 --guaranteed 4 --limit 4 \
|
|
8
|
+
--storage-pool shared-cephfs --storage-gib 200
|
|
9
|
+
runnpu project delete asr --yes # 项目里不能有未终止负载;负载卡 Terminating 时会失败(见 troubleshooting)
|
|
10
|
+
runnpu project quota get asr # 声明的配额 + 集群侧实际(cluster 段空 = 配额没落到集群)
|
|
11
|
+
runnpu project quota set asr --pool ascend910b4=8,4 --storage-pool shared-cephfs=500
|
|
12
|
+
runnpu project quota set asr --priority normal --workload-type workspace
|
|
13
|
+
runnpu project repair asr --action resync # 配额重新物化;release_grant_finalizer 是破窗动作,必须 --yes
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- 单位是**卡**:`--pool <池>=<保底>,<可借增量>`;借用部分可被抢占。
|
|
17
|
+
- `quota set` 是**整份替换**:给了 `--pool` 就替换全部池段——改一个池前先 `quota get`,把要保留的一并传入。
|
|
18
|
+
- 建项目前先 `runnpu cluster get` 看池剩余;集群总卡被占满时新项目配额会被拒(负载报 QuotaGrantNotFound)。
|
|
19
|
+
|
|
20
|
+
## 项目成员
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
runnpu project members list asr
|
|
24
|
+
runnpu project members add asr --user alice@example.com --role 项目管理员 # 角色名 runnpu api GET /roles
|
|
25
|
+
runnpu project members remove asr --user alice@example.com --yes # 默认移除全部角色;--role 只删一个
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 用户与 Access Key
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
runnpu user list [--q alice] [--type robot] [--status disabled]
|
|
32
|
+
runnpu user create --type local --email alice@example.com --display-name Alice # 初始密码只返回一次,原样转给用户
|
|
33
|
+
runnpu user create --type robot --name ci-bot --display-name "CI 机器人" --owner admin@example.com
|
|
34
|
+
runnpu user disable|enable alice@example.com
|
|
35
|
+
runnpu user access-key create ci-bot --name pipeline --expires-in-days 90 # 明文 token 只返回一次
|
|
36
|
+
runnpu user access-key list|revoke …
|
|
37
|
+
runnpu access-key list|create|revoke … # 我自己的
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- API token **不能**再签发 token(403):`access-key create` 只在会话登录下可用,机器人 key 由管理员签发。
|
|
41
|
+
- 禁用账号后其登录报 401 `account_disabled`;已有会话/token 同样被拒。
|
|
42
|
+
|
|
43
|
+
## 系统设置与审计
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
runnpu settings get
|
|
47
|
+
runnpu settings set storage.show_unnamed_pools true # 点路径 → 嵌套 PATCH,自动解析 bool/数字
|
|
48
|
+
runnpu audit list --limit 20 [--actor a@b.com] [--action workload.] [--target-kind user] # --action 前缀匹配
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 未封装端点
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
runnpu api GET /projects/form-meta # 池容量、其它项目已分配
|
|
55
|
+
runnpu api GET '/overview/trends?range=24h'
|
|
56
|
+
runnpu api GET /roles
|
|
57
|
+
runnpu api POST /workloads -d '{...}' # CLI 表达不了的字段(探针/autoscaling/configmap items…)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
路径 = `api/openapi.yaml` 的 path 去掉 `/api/v1` 前缀。
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# 数据源与数据卷
|
|
2
|
+
|
|
3
|
+
`--mount <类型>:<挂载点>[,键=值…][,ro]`,可重复。**整段创建后不可修改**(CRD 拒绝任何增删改),
|
|
4
|
+
"再挂一个盘"= 重建负载。挂载点必须是非根绝对路径、彼此不重叠。
|
|
5
|
+
|
|
6
|
+
| 类型 | 必填键 | 可选键 | 备注 |
|
|
7
|
+
|---|---|---|---|
|
|
8
|
+
| `pvc` | `name=` | `subpath=` | ⚠️ **PVC 必须带 `npu.run/project-id=<项目slug>` 标签**,否则被拒为 `PersistentVolumeClaimNotFound`(授权门槛,不是真的不存在;`kubectl label pvc <名> npu.run/project-id=<slug>`) |
|
|
9
|
+
| `datavolume`(别名 `dv`) | `name=`(**集群卷名 cr_name**,`runnpu datavolume list` 查,不是显示名) | | 归属项目可读写;共享清单内 / 全平台共享**必须加 `,ro`**(否则 422 `cross_project_readonly`) |
|
|
10
|
+
| `newvolume` | `pool=` `size=` | `persistency=workload\|project` `accessmode=` | 计入项目存储配额;`project` 持久卷在负载删除后保留 |
|
|
11
|
+
| `nfs` | `server=` `path=` | | 直接投影,用前确认节点能挂 |
|
|
12
|
+
| `s3` | `bucket=` `credential=` | `prefix=` | credential 是同项目登记的凭据名 |
|
|
13
|
+
| `git` | `url=` | `revision=` `depth=` `credential=` | |
|
|
14
|
+
| `hostpath` | `path=` | `kind=` | 高危:需 `workloads:hostpath` 权限 + 项目策略白名单(前缀+kind+读写位),写入还要策略放行 |
|
|
15
|
+
| `emptydir` | | `size=` `medium=disk\|memory` | 只有 `medium=memory` 计内存 |
|
|
16
|
+
| `configmap` / `secret` | `name=` | | 逐键投影(items[])CLI 表达不了,走 `runnpu api` |
|
|
17
|
+
|
|
18
|
+
## 受管卷(newvolume / 工作区)要点
|
|
19
|
+
|
|
20
|
+
- `--workspace-pool` + `--workspace-gib` 是 `newvolume:/workspace,…` 的顺手写法。
|
|
21
|
+
- 卷根由平台初始化为 `root:20000` mode 770 + ACL,容器进程带补充组 20000——**restricted 非 root 也可写**。
|
|
22
|
+
- 容量计入项目在该池的存储配额(`runnpu project quota get` 看余量;不足 422 `exceeds_quota`)。
|
|
23
|
+
- `persistency=workload` 随负载删除;`project` 保留为项目持久卷(删除走回收流程,可能有延迟)。
|
|
24
|
+
|
|
25
|
+
## 数据卷命令组
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
runnpu datavolume list [--project X] # 「集群卷名」列 = mount 时的 name
|
|
29
|
+
runnpu datavolume create --project main --name 训练集A --pool shared-cephfs --capacity-gib 100 \
|
|
30
|
+
[--share proj-b --share proj-c] [--reclaim-policy retain|delete]
|
|
31
|
+
runnpu datavolume update <集群卷名> [--name …] [--share …整体替换]
|
|
32
|
+
runnpu datavolume delete <集群卷名> --yes # 有活跃绑定 409 has_active_bindings;--force 跳过
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`state` 从 creating 收敛到 ready 才能被挂;`cleanup_blocked` / `conflict` 是一等异常态,看 `state_note`。
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# 模板与资产速查
|
|
2
|
+
|
|
3
|
+
模板 = **整张创建表单**的值拷贝;资产 = **单个配置段**的值拷贝。两者都是「值」不是「链接」:
|
|
4
|
+
展开时把值抄进创建请求,之后改模板不影响已建负载。
|
|
5
|
+
|
|
6
|
+
## 模板
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
runnpu template list [--type training-env] [--scope cluster|project] [--project X] [-q 关键词] [--all]
|
|
10
|
+
runnpu template get <名称或ID> # payload 就是创建请求的字段
|
|
11
|
+
runnpu template create --name N --type T --payload @tpl.json [--description D] [--scope project --project X]
|
|
12
|
+
runnpu template update <名称或ID> [--name …] [--payload @tpl.json] # PUT 整体替换,没给的字段用现值兜底
|
|
13
|
+
runnpu template delete <名称或ID> --yes
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- 列表里 **★ 开头是系统预置**:只读,不能改删,只能照抄一份自己的。
|
|
17
|
+
- `--payload` 支持 `@文件` / `-`(stdin)/ 字面 JSON。改现成的最省事:
|
|
18
|
+
`runnpu template get X -o json | jq .payload > tpl.json`,改完 `--payload @tpl.json`。
|
|
19
|
+
- `--type` 创建后不可改(类型是模板身份的一部分)。
|
|
20
|
+
- 权限不足时 `list` 返回**空列表而不是 403**,所以「空」要说成「没有或没权限」。
|
|
21
|
+
|
|
22
|
+
### 用模板创建负载
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
runnpu workload create --project main --name nb6 --from-template <名称或ID> [--xpu 2] [--mount …] --wait
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
合并规则(CLI 保证,出错会明确报错而不是静默):
|
|
29
|
+
|
|
30
|
+
| 情况 | 行为 |
|
|
31
|
+
|---|---|
|
|
32
|
+
| 命令行没写的字段 | **保留模板值**——`--xpu` 的默认值 1 不会覆盖模板里的 4 |
|
|
33
|
+
| 命令行显式写了的字段 | 覆盖模板值。**`--xpu 0`、`--jupyter=false` 这种显式零值也算覆盖** |
|
|
34
|
+
| `--mount` 与模板预设的数据源 | 取**并集**;挂载点重复直接报错(不静默覆盖,否则用户以为模板的盘还在) |
|
|
35
|
+
| `--type` 与模板类型不一致 | 报错。换模板,不要改类型 |
|
|
36
|
+
| 模板里有 CLI 不认识的字段 | 报错并提示升级 CLI——**宁可报错也不丢字段** |
|
|
37
|
+
|
|
38
|
+
模板的 `pending_fields` 是「使用者提交时要补」的字段:**问用户,不要编**。
|
|
39
|
+
|
|
40
|
+
## 资产
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
runnpu asset list [--kind environment|compute|data-source|credential] [--scope …] [-q 关键词]
|
|
44
|
+
runnpu asset get <名称或ID> # 凭证的密文永不回显
|
|
45
|
+
runnpu asset create --kind K --name N [--subtype S] [--payload @a.json] [--secret KEY=VALUE ...]
|
|
46
|
+
runnpu asset update <名称或ID> [--payload @a.json] [--secret KEY=VALUE ...]
|
|
47
|
+
runnpu asset delete <名称或ID> --yes
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- `--secret` 的值支持 `@文件`(推荐:避免密码/私钥进 shell history):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
runnpu asset create --kind credential --subtype ssh-key --name 部署密钥 \
|
|
54
|
+
--payload '{}' --secret 'ssh-privatekey=@~/.ssh/id_ed25519'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- 凭证是唯一会离开 `ready` 的资产——它要在项目命名空间里落成 Secret,那一步可能失败;
|
|
58
|
+
`list` 的状态列会带出 `state_note`。
|
|
59
|
+
- **没有 `data-volume` 大类**:数据卷背后是真实的一块盘,是独立一等资源,走 `runnpu datavolume`。
|
|
60
|
+
- 资产怎么用:environment(镜像 + env)/ compute(池 + 卡数)按段并入创建参数;
|
|
61
|
+
credential / data-source 在挂载里按名引用(如 `--mount s3:/data,bucket=b,credential=<资产名>`)。
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# 训练类负载的平台侧事实(RunNPU / 昇腾)
|
|
2
|
+
|
|
3
|
+
> 训练**方法论**(跑通链路的纪律、看曲线诊断、自动超参研究、长程与并行)不在本文件——
|
|
4
|
+
> 见 `training-research` skill(训练模式会话里可用)。本文件只放**换个平台就作废**的事实:
|
|
5
|
+
> 这个平台怎么传文件、这套镜像有什么炸点、这个集群实测过什么。
|
|
6
|
+
|
|
7
|
+
## 1. 工程进集群(本平台的通道)
|
|
8
|
+
|
|
9
|
+
- **先起一个带 SSH 的训练环境**。SSH 与安全级、与 root **无关**(helper 是注入的原生二进制,
|
|
10
|
+
PID 1 非 root 时保持当前身份;run-npu#595 起 restricted 也支持,旧版 operator 才会以
|
|
11
|
+
`SshRootInitUnavailable` 拒绝)——`restricted` 下照样能开,**不要为了拿 SSH 把负载降到 baseline**。
|
|
12
|
+
SSH 会话身份继承容器主进程,传上去的文件属主与容器内进程一致。
|
|
13
|
+
root 镜像在 restricted 下要 `--run-as-uid`,那是安全级自身的要求,别和 SSH 绑一起。
|
|
14
|
+
- ⚠️ **torch-npu 官方镜像做 training-env 必须带 `--arg sleep --arg infinity`**(平台已知问题:
|
|
15
|
+
该镜像 entrypoint 无命令时空参立退,PID 1 数秒退出 → 容器每 1-2 分钟重启,SSH 只在残存窗口
|
|
16
|
+
时好时坏,极易误判成网络/平台故障)。用 args、**不要用 `--command`**——保留 entrypoint 的
|
|
17
|
+
CANN 环境装配,SSH helper 的 env 快照才有内容。training-job 有真实训练命令,不受此影响。
|
|
18
|
+
- 手工上传路径(无 `training_upload` 工具时,如算力管理模式):连接参数**别自己拼**——
|
|
19
|
+
`runnpu workload endpoints <p>/<n>` 给出拼好的命令,结构化值(脚本用)在
|
|
20
|
+
`runnpu workload get <p>/<n> -o json` 的 `.ssh.{username,host,port}`。登录用户名固定是
|
|
21
|
+
`<负载名>.<命名空间>`(SSHPiper 路由标识;注意命名空间带 `proj-` 前缀,不是项目 slug 本身,
|
|
22
|
+
更不是平台用户名),如 `scp -P 32222 -r ./工程 job1.proj-main@<入口IP>:/workspace/`;
|
|
23
|
+
平台 SFTP 已验证可用。能力边界:exec / 交互 shell / sftp / 本地转发 `-L`/`-D` 都支持,
|
|
24
|
+
**远端监听 `-R` 未实现**。
|
|
25
|
+
- 备选 exec 管道(无 SSH 时):`tar czf - . | runnpu workload exec <p>/<n>` 里接
|
|
26
|
+
`tar xzf - -C /workspace`(stdin 必须以 `exit` 结尾);只传代码,数据集级流量不过控制面。
|
|
27
|
+
- 数据集进数据卷 / 工作盘(`--mount` / `--workspace-gib`),一次落盘、代码单独同步。
|
|
28
|
+
|
|
29
|
+
## 2. 容器内运行(本部署实测)
|
|
30
|
+
|
|
31
|
+
- 依赖安装走平台注入的 pip 镜像(负载 env 里已有);装 opencv 报 libGL 缺失 →
|
|
32
|
+
改 `opencv-python-headless`,不要 apt 补一堆图形库。
|
|
33
|
+
- **pip 装到 `--target /workspace/pylibs`** 并 `export PYTHONPATH=/workspace/pylibs`:
|
|
34
|
+
容器层随重启丢失,/workspace 工作盘持久——否则重启一次就要重装一遍。
|
|
35
|
+
- 本集群到 PyPI 的出口带宽实测只有几十 KiB/s:大依赖预置进数据卷/工作盘。
|
|
36
|
+
- 平台日志通道:`runnpu workload logs <p>/<n> --tail`(监控双信号源里的信号源 B)。
|
|
37
|
+
|
|
38
|
+
## 3. 两个"看起来成功了"的假象(本平台实测踩过,2026-09-02)
|
|
39
|
+
|
|
40
|
+
- **job 秒级 completed + 跟踪平台无 run**:大概率是**覆盖了镜像 entrypoint**
|
|
41
|
+
(`--command bash -c …`)丢掉 CANN 环境装配 → torch_npu 不可用 → 设备探测降级 CPU
|
|
42
|
+
秒跑完、PYTHONPATH 也可能没带上。提交 training-job 时**保留 entrypoint、用 args 传命令**
|
|
43
|
+
(与 training-env 的 sleep infinity 同一个原理);手动按模板 payload 拼 job 时,
|
|
44
|
+
模板里的 `LD_LIBRARY_PATH` 救援 env 必须原样带上。正常 NPU 训练首个 step 要编译算子
|
|
45
|
+
(分钟级),秒级完成先怀疑设备降级,别急着重提。
|
|
46
|
+
- **卡够 ≠ 调度得上**:`CardInsufficientMemory` 事件 + 有空闲卡数,通常是**分卡(vNPU)碎片化**
|
|
47
|
+
——平台外 Pod 按显存切片占卡,凑不出整卡。查逐卡占用(node metrics / overview 逐卡网格),
|
|
48
|
+
必要时先停自己占整卡的 env 给 job 让路(先 job 后 env,反过来 env 会把唯一整卡占住)。
|
|
49
|
+
|
|
50
|
+
## 4. 昇腾特有炸点(详见 troubleshooting.md「镜像兼容性」)
|
|
51
|
+
|
|
52
|
+
- 官方 torch-npu 镜像的 LD 冲突:预置模板已带救援 `LD_LIBRARY_PATH`,自拼镜像时照抄那条 env。
|
|
53
|
+
- CUDA 工程迁移三件套:`import torch, torch_npu` + `from torch_npu.contrib import transfer_to_npu`
|
|
54
|
+
必须在训练框架 import **之前**;AMP 自检会探 CUDA,训练参数要显式关掉(如 ultralytics `amp=False`);
|
|
55
|
+
dataloader `workers` 从小起步。
|
|
56
|
+
- 容器内 `npu-smi` 报 libruntime 错链时的前缀写法也在 troubleshooting.md。
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# 排查决策树与已知集群行为
|
|
2
|
+
|
|
3
|
+
## 决策树
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
runnpu workload get <p>/<n> → status + reason.text 先读完
|
|
7
|
+
│
|
|
8
|
+
├─ rejected → 终态,不会自愈。reason.text 说明原因:
|
|
9
|
+
│ ├─ QuotaGrantNotFound → 项目配额没落到集群(建项目太快 / 集群配额被占满)。
|
|
10
|
+
│ │ runnpu project quota get <slug> 看 cluster 段;配额恢复后**删掉重建**负载
|
|
11
|
+
│ ├─ PersistentVolumeClaimNotFound → pvc 挂载缺 npu.run/project-id 标签(见 data-sources),或真不存在
|
|
12
|
+
│ ├─ QuotaResourceUnauthorized / AcceleratorPoolUnavailable → 池没在项目配额里 / 池发现不可用
|
|
13
|
+
│ └─ 其它 reason 原样转述,别猜
|
|
14
|
+
├─ queued → 配额不够在排队:runnpu project quota get 看 used vs guaranteed(+limit 可借)
|
|
15
|
+
├─ pending / admitted 很久 → runnpu workload events:
|
|
16
|
+
│ ├─ Pulling image… → 大镜像拉取,等;--wait 超时 ≠ 失败
|
|
17
|
+
│ ├─ FilteringFailed / 0 nodes fit → 卡不够或分卡份额放不下
|
|
18
|
+
│ └─ 无事件 → runnpu workload yaml 看 CR conditions
|
|
19
|
+
├─ running 但工具/端口不通 → 下面「入口就绪」
|
|
20
|
+
├─ CrashLoopBackOff → runnpu workload logs:
|
|
21
|
+
│ ├─ symbol lookup error(libdrvdsmi/libruntime)→ 下面「镜像兼容性」
|
|
22
|
+
│ └─ 其它应用错误原样转述
|
|
23
|
+
└─ Terminating 很久 → 下面「删除卡住」
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 镜像兼容性(昇腾镜像 + HAMi 注入)
|
|
27
|
+
|
|
28
|
+
平台用 HAMi 软切时会向**每个进程**注入 preload 库,它要求容器的 `LD_LIBRARY_PATH` 能同时解析到
|
|
29
|
+
CANN `libruntime.so` 和与宿主驱动一致的 dsmi/hal 链。已知结论:
|
|
30
|
+
|
|
31
|
+
- ✅ `vllm-ascend` 镜像(平台预置列表里的):路径顺序兼容,直接用;
|
|
32
|
+
- ❌ **ascendhub 官方 `torch-npu` 镜像**:烘死的 LD 路径与注入冲突,容器里 **bash 都起不来**
|
|
33
|
+
(`symbol lookup error: libdrvdsmi_host.so: drvSetDeviceInfo`,CrashLoopBackOff)。
|
|
34
|
+
**救援配方**(创建时加一个 env,实测 Running + npu-smi + torch_npu 全通):
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
--env "LD_LIBRARY_PATH=/usr/local/Ascend/driver/lib64/driver:/usr/local/Ascend/driver/lib64/common:/usr/local/Ascend/driver/lib64:/usr/local/Ascend/ascend-toolkit/latest/lib64"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
要点:宿主驱动目录在前,镜像 toolkit 目录兜 `libruntime.so`;**只留驱动路径会缺 libruntime.so 照样炸**。
|
|
41
|
+
其它镜像炸在同一错误时,同思路调整(驱动在前 + 该镜像的 CANN lib64 在后)。
|
|
42
|
+
- ❌ busybox/alpine 等非 glibc 镜像与注入不兼容,连通性验证用 `ubuntu:24.04`。
|
|
43
|
+
- 容器内 `npu-smi` 报 `libruntime.so: cannot open` 是另一回事(宿主二进制错链镜像 CANN):
|
|
44
|
+
exec 时前缀 `LD_LIBRARY_PATH=/usr/local/Ascend/driver/lib64:…/driver:…/common npu-smi info` 即可。
|
|
45
|
+
|
|
46
|
+
## 入口就绪的口径
|
|
47
|
+
|
|
48
|
+
- 工具 `url` 只在 ready 且有权限时返回;`internal_url` 是集群内地址(浏览器打不开)。
|
|
49
|
+
- `node_port` 是**分配观测**:不含节点地址、不代表可达、可能在 ready=false 时就有。访问 = `http://<任一节点IP>:<node_port>`。
|
|
50
|
+
- **nodePort 工具的 ready 有已知误报**(run-npu#553:EndpointUnavailable 但实际可达):
|
|
51
|
+
Pod Running 后直接 `curl --max-time 5 http://<节点IP>:<node_port>/` 实测,别等 ready。
|
|
52
|
+
|
|
53
|
+
## 删除卡住(run-npu#549)
|
|
54
|
+
|
|
55
|
+
带**工具 Service / 受管卷 / 项目持久卷**的负载删除后可能长期 Terminating(operator teardown 对已删对象
|
|
56
|
+
的 404 处理缺陷)。表现:`kubectl` 里 RunWorkload 带 deletionTimestamp 不消失、PVC 卡 Terminating。
|
|
57
|
+
|
|
58
|
+
- **不要**反复 delete、不要循环 --force;
|
|
59
|
+
- 如实告知用户是已知上游问题,需等待修复或运维用 finalizer 手段清理(运维操作,不在本 Skill 权限内做);
|
|
60
|
+
- 项目删除因此失败时(`has_running_workloads` / 卡 Terminating)同理。
|
|
61
|
+
|
|
62
|
+
训练任务删除后残留的 Completed Pod(ownerReferences 为空)是另一个已知无害残留(run-npu#551)。
|
|
63
|
+
|
|
64
|
+
## 配额相关
|
|
65
|
+
|
|
66
|
+
- 建项目后配额(QuotaGrant)落集群有延迟且可能被拒(集群卡总量被其它项目占满时):
|
|
67
|
+
建完先 `runnpu project quota get <slug>` 确认 `cluster` 段非空再建负载。
|
|
68
|
+
- `queued` 时算账:单位是卡;分卡按份额折算(25 核 = 0.25 卡);`limit` 是可借增量,借用的部分可被抢占。
|
|
69
|
+
- `runnpu project repair <slug> --action resync` 可触发配额重新物化(管理员)。
|
|
70
|
+
|
|
71
|
+
## 指标与日志
|
|
72
|
+
|
|
73
|
+
- `metrics.available=false` / 值为 null = 数据源未接入(Prometheus 的 NPU 指标、ClickHouse 日志可能没部署),
|
|
74
|
+
说"未接入",不是 0,原因在 `metrics.reason`。
|
|
75
|
+
- `runnpu workload logs` 是容器 stdout;`events` 是 K8s Event + CR 条件(最近 1 小时)。
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# 四类负载:参数矩阵与生命周期
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
| | workspace 开发环境 | training-env 训练环境 | training-job 训练任务 | inference 推理服务 |
|
|
5
|
+
| ---- | ---------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
6
|
+
| 形态 | 常驻交互(Sandbox) | 常驻交互(Sandbox) | 跑完即止(Job / 多机 JobSet) | 常驻服务(Deployment) |
|
|
7
|
+
| 可抢占 | ✅(闲时可被回收) | ❌ | 由优先级决定 | ❌ |
|
|
8
|
+
| 工具 | `--jupyter` `--code-server` `--ssh` | 同左 | 无 | `--port`(对外端口) |
|
|
9
|
+
| 专属参数 | `--idle/max-lifetime`(暂无 CLI flag,走 api) | 同左 | `--nodes` `--completions` `--parallelism` `--backoff-limit` `--active-deadline-seconds` `--ttl-after-finished` | `--port` `--replicas`(探针/伸缩/滚动策略走 `runnpu api`) |
|
|
10
|
+
| 终态 | stopped / failed | 同左 | **completed** / failed | 无自然终态 |
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## 通用参数
|
|
14
|
+
|
|
15
|
+
- `--xpu N`:整卡卡数 0..8(0 = 纯 CPU)。**分卡**:`--xpu-core 1..100` + `--xpu-memory-mib M` 成对给出,
|
|
16
|
+
且 `--xpu` 必须为 1——**只支持单卡切分**,多卡分卡上游不支持(要多张卡就整卡申请,或建多个负载)。
|
|
17
|
+
只给一半会被 422 拒(服务端不猜另一半)。
|
|
18
|
+
- `--security restricted|baseline`:restricted 强制非 root(root 镜像必须给 `--run-as-uid`)。但注意多数昇腾官方镜像(root 运行)仍需要 baseline——那是镜像自身身份的要求,不是 SSH 的。
|
|
19
|
+
- `--pool`:省略取项目第一个池;`runnpu cluster get` 查池名。
|
|
20
|
+
- `--command` / `--arg` 可重复按序拼接:`--command bash --command -c --arg "…"`。
|
|
21
|
+
|
|
22
|
+
## training-job 细节
|
|
23
|
+
|
|
24
|
+
- 单机(`--nodes 1`,默认):`--completions`(目标完成次数)、`--parallelism`(并行 Pod 数)可用;
|
|
25
|
+
- 多机(`--nodes ≥ 2`):**没有** completions/parallelism(规模由节点数决定,传了 422);
|
|
26
|
+
`--backoff-limit` 变成整组重启次数;`--active-deadline-seconds` 创建后不可变。
|
|
27
|
+
- `--ttl-after-finished 0` 表示完成立即删;省略 = 不自动删(完成的负载保留,占记录不占卡)。
|
|
28
|
+
- 进度看 `runnpu workload get`:`progress` 里 `succeeded / failed / retries_used`,多机另有 `ready_replicas`。
|
|
29
|
+
- 已知小残留:任务完成后被删除时,Completed Pod 可能残留在 namespace(上游 run-npu#551,无害)。
|
|
30
|
+
|
|
31
|
+
## inference 细节
|
|
32
|
+
|
|
33
|
+
- `--port [名字:]<端口>[,expose=…][,node-port=N]` 可重复(≤8);名字是 DNS label ≤15(要拼进入口 host),省略为 `http`。
|
|
34
|
+
- `expose`:`clusterIP`(默认,仅集群内)/ `nodePort`(30000..32767,`node-port=` 可指定,否则集群分配)/
|
|
35
|
+
`loadBalancer`(需集群有 LB)/ `externalUrl`(需平台配了入口域名)。
|
|
36
|
+
- 推理端点一律 `access: public`——平台登录态挡不住 API 客户端,**鉴权由服务自身负责**(vLLM 加 `--api-key` 之类)。
|
|
37
|
+
- `--replicas N` 副本数;探针(readiness/liveness)、autoscaling、max_surge/max_unavailable 目前 CLI 不表达,
|
|
38
|
+
需要时 `runnpu api POST /workloads` 直接给 `service` 对象(字段见 api/openapi.yaml 的 ServiceSpecInput)。
|
|
39
|
+
- 状态语义:`running` = Deployment 副本就绪;工具 `ready` 是**入口观测**,nodePort 场景有已知误报
|
|
40
|
+
(run-npu#553:EndpointUnavailable 但端口实际可达)——以实测 `curl <节点IP>:<nodePort>` 为准。
|
|
41
|
+
- 服务视图字段:`runnpu workload get` 的 `service` 段有 replicas / ready_replicas / available_replicas。
|
|
42
|
+
|
|
43
|
+
## 不可修改的字段(想改 = 重建负载)
|
|
44
|
+
|
|
45
|
+
类型、项目、规格(xpu/core/memoryMiB)、数据源整段、SSH 开关、多机节点数、
|
|
46
|
+
batch 的 backoff/deadline、镜像与命令(可改的只有:暂停/恢复、部分运行时参数)。
|
|
47
|
+
用户要"改配置"时先对照这张表,别许诺做不到的事。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# dsh-runnpu bundle patch — HOST-PLANE rows only.
|
|
2
|
+
#
|
|
3
|
+
# What belongs here vs. in an agent preset:
|
|
4
|
+
# - HOST plane (this file): things shared across every session — the RunNPU /
|
|
5
|
+
# SwanLab connectors, the plugin's own HTTP routes the browser half calls,
|
|
6
|
+
# and the client roster row that serves the browser bundle.
|
|
7
|
+
# - AGENT plane (~/.dsh/.agent-presets/{compute,training}/agent.cordis.yml):
|
|
8
|
+
# what one session's agent gets — its tools, persona and skill roots. A
|
|
9
|
+
# preset is what decides whether a session is "compute mode" at all.
|
|
10
|
+
#
|
|
11
|
+
# The two `*/node` rows below own the per-mode HTTP surface. They inject
|
|
12
|
+
# `webServer`, so they activate exactly when the shipped webserver row does.
|
|
13
|
+
|
|
14
|
+
- insert:
|
|
15
|
+
# Shared connectors: ctx.runnpu (REST client for the RunNPU /api/v1 control
|
|
16
|
+
# panel) and ctx.swanlab (thin wrapper over the `swanlab api` CLI). Both are
|
|
17
|
+
# plain-value services read with ctx.get, so a consumer degrades with a
|
|
18
|
+
# stable reason when nothing is configured yet.
|
|
19
|
+
- id: runnpu-connectors
|
|
20
|
+
name: 'dsh-runnpu/shared/connectors'
|
|
21
|
+
config:
|
|
22
|
+
# Server address and token are NOT here: they are entered in the UI and
|
|
23
|
+
# stored through the credential seam. A value here is only a fallback
|
|
24
|
+
# default for a fresh install.
|
|
25
|
+
runnpu:
|
|
26
|
+
server: ''
|
|
27
|
+
tokenEnv: 'RUNNPU_TOKEN'
|
|
28
|
+
swanlab:
|
|
29
|
+
apiHost: 'https://api.swanlab.cn'
|
|
30
|
+
webHost: 'https://swanlab.cn'
|
|
31
|
+
apiKeyEnv: 'SWANLAB_API_KEY'
|
|
32
|
+
cli: 'swanlab'
|
|
33
|
+
|
|
34
|
+
# Compute mode's node half: connection/login routes, cluster reads, and the
|
|
35
|
+
# pinned-chart registry with its refresh loop.
|
|
36
|
+
- id: runnpu-compute-node
|
|
37
|
+
name: 'dsh-runnpu/compute/node'
|
|
38
|
+
config:
|
|
39
|
+
apiPath: '/runnpu-api'
|
|
40
|
+
refreshMs: 30000
|
|
41
|
+
|
|
42
|
+
# Training mode's node half: `.train/` ledger reads, SwanLab metric pulls
|
|
43
|
+
# for the metric strip, and the training-side config routes.
|
|
44
|
+
- id: runnpu-training-node
|
|
45
|
+
name: 'dsh-runnpu/training/node'
|
|
46
|
+
config:
|
|
47
|
+
apiPath: '/training-api'
|
|
48
|
+
refreshMs: 15000
|
|
49
|
+
|
|
50
|
+
# ── periodic reports ────────────────────────────────────────────────
|
|
51
|
+
#
|
|
52
|
+
# Session-local reminders, which is how 定期报告 is delivered: the agent
|
|
53
|
+
# calls `schedule_create` with an interval, and each occurrence comes back
|
|
54
|
+
# as an ordinary follow-up message that prompts it to run `cluster_report`.
|
|
55
|
+
#
|
|
56
|
+
# HOST plane, not the preset, and the reason is mechanical: the plugin
|
|
57
|
+
# observes `agent/created` and installs on ROOT agents published AFTER it
|
|
58
|
+
# loads. A preset row loads with the agent it configures, which is already
|
|
59
|
+
# too late — the tools would never appear. Mounting it here also gives both
|
|
60
|
+
# modes reminders from one row.
|
|
61
|
+
#
|
|
62
|
+
# ⚠️ A session that was already running when this row loaded does NOT have
|
|
63
|
+
# the reminder tools. After adding this, start a NEW session.
|
|
64
|
+
#
|
|
65
|
+
# `time-context` is what lets the model turn "每天早上九点" into an explicit
|
|
66
|
+
# instant: Schedule itself never reads a browser, session or process time
|
|
67
|
+
# zone, and refuses a local time without an explicit IANA zone.
|
|
68
|
+
- id: time-context
|
|
69
|
+
name: '@deepseek-ai/dsh-time-context'
|
|
70
|
+
|
|
71
|
+
- id: schedule
|
|
72
|
+
name: '@deepseek-ai/dsh-schedule'
|
|
73
|
+
|
|
74
|
+
# Client roster row: the client-modules node half scans this entry, reads
|
|
75
|
+
# `dsh.client` + exports["./client"] from package.json, and serves the
|
|
76
|
+
# browser bundle (dsh ≥ 0.1.5: /plugins/??dsh-runnpu/client.js&rev=…). One row per
|
|
77
|
+
# package — the bundle carries BOTH modes' UI and branches on the session's
|
|
78
|
+
# `agentPreset` projection.
|
|
79
|
+
- id: runnpu-modes-ui
|
|
80
|
+
name: 'dsh-runnpu'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var h=["overview.trends","node.metrics","workload.metrics"],k=["utilization","allocation","storage","workloads"],w={cpu:{label:"CPU \u5229\u7528\u7387",unit:"%",rows:[["cpu_pct","CPU \u5229\u7528\u7387"]]},memory:{label:"\u5185\u5B58\u5229\u7528\u7387",unit:"%",rows:[["memory_pct","\u5185\u5B58\u5229\u7528\u7387"]]},net:{label:"\u7F51\u7EDC\u541E\u5410",unit:"B/s",rows:[["net_rx_bps","\u63A5\u6536"],["net_tx_bps","\u53D1\u9001"]]},disk:{label:"\u78C1\u76D8\u541E\u5410",unit:"B/s",rows:[["disk_read_bps","\u8BFB"],["disk_write_bps","\u5199"]]}},_=240;function y(s,t){if(s==="workload.metrics"){let e={};for(let n of t?.groups??[])e[n.key]={label:n.title??n.key,unit:n.unit,rows:n.series??[]};return e}if(s==="node.metrics"){let e={};for(let[n,r]of Object.entries(w)){let u=r.rows.filter(([l])=>Array.isArray(t?.[l])).map(([l,f])=>({key:l,label:f,unit:r.unit,data:t[l]}));u.length>0&&(e[n]={label:r.label,unit:r.unit,rows:u})}return e}let o={};for(let e of k)Array.isArray(t?.[e])&&(o[e]={label:e,unit:t[e][0]?.unit,rows:t[e]});return o}function x(s,t){return Object.keys(y(s,t))}function A(s,t={}){let o=t.query??"overview.trends",e=s?.timestamps;if(!Array.isArray(e))return{series:[],note:"\u8BE5\u67E5\u8BE2\u6CA1\u6709\u65F6\u95F4\u8F74\uFF0C\u65E0\u6CD5\u753B\u6210\u66F2\u7EBF"};let n=y(o,s),r=Object.keys(n);if(r.length===0)return{series:[],note:"\u8FD9\u6B21\u54CD\u5E94\u91CC\u6CA1\u6709\u4EFB\u4F55\u53EF\u7ED8\u5236\u7684\u6307\u6807\u7EC4"};let u=t.group??r[0],l=n[u]??n[r[0]],f=n[u]===void 0,m=e.map(i=>new Date(i).getTime()),b=t.series?.length?new Set(t.series):void 0,d=Math.max(1,Math.ceil(m.length/_)),g=(l.rows??[]).filter(i=>b?b.has(i.key):!0).slice(0,5).map(i=>({key:i.key,name:i.label??i.key,unit:i.unit??l.unit,points:(i.data??[]).map((a,c)=>[m[c],a]).filter((a,c)=>c%d===0).filter(([a])=>Number.isFinite(a)).map(([a,c])=>[a,typeof c=="number"&&Number.isFinite(c)?c:null])})),p=[];return f&&p.push(`\u6CA1\u6709 ${u} \u8FD9\u4E00\u7EC4\uFF0C\u753B\u7684\u662F ${r[0]}\uFF08\u53EF\u7528\uFF1A${r.join(" / ")}\uFF09`),s?.metrics?.available===!1&&p.push(`\u6307\u6807\u6E90\u672A\u63A5\u5165\uFF08${s.metrics.reason??"\u539F\u56E0\u672A\u7ED9\u51FA"}\uFF09\uFF0C\u8FD9\u7EC4\u66F2\u7EBF\u6CA1\u6709\u6570\u636E`),{series:g,group:n[u]?u:r[0],note:p.length>0?p.join("\uFF1B"):void 0}}function O(s){let t=s?.points??[];for(let o=t.length-1;o>=0;o-=1){let e=t[o]?.[1];if(e!=null)return e}}export{h as a,k as b,y as c,x as d,A as e,O as f};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{randomUUID as K}from"node:crypto";import{readFile as H,writeFile as g,mkdir as v,chmod as R,readdir as P,rename as U,rm as _}from"node:fs/promises";import{existsSync as w}from"node:fs";import{homedir as C}from"node:os";import{join as s,resolve as A}from"node:path";var L=".train",x={apiHost:"https://api.swanlab.cn",webHost:"https://swanlab.cn"},S={runnpuToken:"RUNNPU_TOKEN",swanlabApiKey:"SWANLAB_API_KEY"};function N(){let e=process.env.DSH_HOME||s(C(),".dsh");return s(e,"runnpu-modes")}async function f(e){try{return JSON.parse(await H(e,"utf8"))}catch{return}}async function m(e,n,r){await v(s(e,".."),{recursive:!0}),await g(e,`${JSON.stringify(n,void 0,2)}
|
|
2
|
+
`,{encoding:"utf8",mode:r}),r!==void 0&&await R(e,r)}function l(e){return s(A(e),L)}async function I(e){if(!e)return{config:void 0,secrets:void 0,dir:void 0};let n=l(e);return{dir:n,config:await f(s(n,"config.json")),secrets:await f(s(n,"secrets.json"))}}async function D(e){let n=l(e);await v(s(n,"runs"),{recursive:!0});let r=s(n,".gitignore");return w(r)||await g(r,`secrets.json
|
|
3
|
+
`,"utf8"),n}async function G(e,n){let r=await D(e),i={...await f(s(r,"config.json"))??{},...n,updatedAt:new Date().toISOString()};return await m(s(r,"config.json"),i),i}async function J(e,n){let r=await D(e),t=s(r,"secrets.json"),o={...await f(t)??{}};for(let[a,c]of Object.entries(n))c===void 0||c===""?delete o[a]:o[a]=c;return await m(t,o,384),Object.keys(o)}async function h(e,n,r){let t=r?.[n];if(typeof t=="string"&&t!=="")return{value:t,source:"project"};let i=e.get?.("credentials");if(i)try{let a=await i.resolve(n);if(a?.value)return{value:a.value,source:`credentials:${a.source}`}}catch{}let o=process.env[n];if(o)return{value:o,source:"env"}}async function V(e,n,r,t){if(t.scope==="project"){if(!t.cwd)throw new Error("project scope needs a workspace");return await J(t.cwd,{[n]:r}),"project"}let i=e.get?.("credentials");if(!i)throw new Error("credential provider unavailable; use project scope");return await i.set(n,r),"credentials"}async function E(e,n,r){let{config:t,secrets:i,dir:o}=await I(r),a=await f(s(N(),"connections.json"))??{},c=t?.runnpu?.server||a.runnpu?.server||n?.runnpu?.server||process.env.RUNNPU_SERVER||"",p=await h(e,S.runnpuToken,i),u=t?.swanlab?.mode||a.swanlab?.mode||"cloud",d=u==="cloud",j=t?.swanlab?.apiHost||a.swanlab?.apiHost||(d?x.apiHost:""),O=t?.swanlab?.webHost||a.swanlab?.webHost||(d?x.webHost:""),b=await h(e,S.swanlabApiKey,i);return{workspace:r??void 0,trainDir:o,runnpu:{server:c,configured:!!(c&&p),tokenSource:p?.source,token:p?.value},swanlab:{mode:u,apiHost:j,webHost:O,configured:!!(j&&b),apiKeySource:b?.source,apiKey:b?.value}}}async function Y(e,n,r){let t=await E(e,n,r);return{workspace:t.workspace,trainDir:t.trainDir,runnpu:{server:t.runnpu.server,configured:t.runnpu.configured,tokenSource:t.runnpu.tokenSource},swanlab:{mode:t.swanlab.mode,apiHost:t.swanlab.apiHost,webHost:t.swanlab.webHost,configured:t.swanlab.configured,apiKeySource:t.swanlab.apiKeySource}}}async function Q(e,n,r){return await E(e,n,r)}async function X(e){let n=s(N(),"connections.json"),t={...await f(n)??{},...e};return await m(n,t),t}async function Z(e){if(!e)return[];let n=s(l(e),"runs"),r;try{r=await P(n)}catch{return[]}let t=[];for(let i of r.filter(o=>o.endsWith(".json")).sort().reverse()){let o=await f(s(n,i));o&&t.push(o)}return t}function T(e){return`${String(e).padStart(4,"0")}.json`}var k=Object.freeze(["swanlabRun","status","endedAt","swanlabUpload"]),y=new Map;function $(e,n){let r=A(e),i=(y.get(r)??Promise.resolve()).then(n),o=i.catch(()=>{});return y.set(r,o),o.then(()=>{y.get(r)===o&&y.delete(r)}),i}async function F(e,n){await v(s(e,".."),{recursive:!0});let r=`${e}.${K().slice(0,8)}.tmp`;await g(r,`${JSON.stringify(n,void 0,2)}
|
|
4
|
+
`,"utf8");try{await U(r,e)}catch(t){throw await _(r,{force:!0}),t}}async function ee(e,n,r){return await $(e,async()=>{let t=s(l(e),"runs",T(n)),i=r??{},o=Object.keys(i).filter(u=>!k.includes(u)),a=await f(t);if(!a)return{path:t,run:void 0,changed:[],ignored:o};let c={...a},p=[];for(let u of k){if(!(u in i))continue;let d=i[u];d===void 0||d===a[u]||(c[u]=d,p.push(u))}return p.length===0?{path:t,run:a,changed:p,ignored:o}:(await F(t,c),{path:t,run:c,changed:p,ignored:o})})}async function ne(e){if(e)return await f(s(l(e),"metrics.json"))}async function te(e){if(!e)return!1;if(w(l(e))||w(s(e,"train.py")))return!0;for(let n of["requirements.txt","pyproject.toml"])try{let r=await H(s(e,n),"utf8");if(/\b(torch|transformers|deepspeed|swanlab|accelerate|ultralytics)\b/i.test(r))return!0}catch{}for(let n of["configs","config"])if(w(s(e,n,"accelerate.yaml"))||w(s(e,n,"deepspeed.json")))return!0;return!1}export{x as a,S as b,N as c,l as d,I as e,D as f,G as g,J as h,V as i,Y as j,Q as k,X as l,Z as m,ee as n,ne as o,te as p};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0})};export{e as a};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import{randomBytes as m,timingSafeEqual as b}from"node:crypto";var y=m(32).toString("base64url"),N="x-runnpu-nonce",E="nonce",a=0,d;function _(r){let t=r.get("webServer");t&&r.effect(()=>(a+=1,a===1&&(d=t.tapIndex(e=>e.replace("</head>",`<script>globalThis.__RUNNPU_NONCE__=${JSON.stringify(y)}</script></head>`))),()=>{a-=1,!(a>0)&&(d?.(),d=void 0)}),"runnpu: index nonce injection")}function p(r){if(typeof r!="string"||r.length===0)return!1;let t=Buffer.from(r,"utf8"),e=Buffer.from(y,"utf8");return t.byteLength===e.byteLength&&b(t,e)}function u(r,t){let e=r.headers?.[N];if(p(Array.isArray(e)?e[0]:e))return!0;let n=t??new URL(r.url??"/","http://127.0.0.1");return p(n.searchParams.get(E)??"")}function x(r,t,e){let n=JSON.stringify(e);r.writeHead(t,{"content-type":"application/json; charset=utf-8","cache-control":"no-store","content-length":Buffer.byteLength(n)}),r.end(n)}function h(r,t,e,n,o){x(r,t,{error:e,status:t,message:n,details:o})}async function H(r,t=256*1024){let e=[],n=0;for await(let o of r){if(n+=o.length,n>t)throw Object.assign(new Error(`request body exceeds ${t} bytes`),{status:413,code:"body_too_large"});e.push(o)}if(n!==0)try{return JSON.parse(Buffer.concat(e).toString("utf8"))}catch(o){throw Object.assign(new Error(`request body is not valid JSON: ${o.message}`),{status:400,code:"bad_json"})}}function S(r){let t=r.headers.origin;if(!t)return!0;try{let e=new URL(t);return e.hostname==="127.0.0.1"||e.hostname==="localhost"||e.hostname==="[::1]"}catch{return!1}}function J(r){return async(t,e)=>{try{let n=new URL(t.url??"/","http://127.0.0.1"),o=r[t.method??"GET"];if(!o){e.writeHead(405,{allow:Object.keys(r).join(", ")}).end();return}if(t.method!=="GET"&&t.method!=="HEAD"&&!S(t)){h(e,403,"forbidden_origin","cross-origin request refused");return}if(!u(t,n)){h(e,401,"unauthenticated","\u7F3A\u5C11\u6216\u65E0\u6548\u7684\u63D2\u4EF6\u8BF7\u6C42\u4EE4\u724C");return}await o(t,e,n)}catch(n){if(e.headersSent){e.end();return}h(e,n?.status&&Number.isInteger(n.status)?n.status:500,n?.code??"internal_error",String(n?.message??n),n?.details)}}}var f=class{#e=new Set;subscribe(t,e){e.writeHead(200,{"content-type":"text/event-stream","cache-control":"no-cache, no-transform",connection:"keep-alive"}),e.write(`: connected
|
|
2
|
+
|
|
3
|
+
`);let n={res:e};this.#e.add(n);let o=()=>{this.#e.delete(n);try{e.end()}catch{}};return t.on("close",o),o}publish(t,e){let n=`event: ${t}
|
|
4
|
+
data: ${JSON.stringify(e)}
|
|
5
|
+
|
|
6
|
+
`;for(let o of this.#e)try{o.res.write(n)}catch{this.#e.delete(o)}}closeAll(){for(let t of this.#e)try{t.res.end()}catch{}this.#e.clear()}get size(){return this.#e.size}};function v(r,t,e){try{r.write(`event: ${t}
|
|
7
|
+
data: ${JSON.stringify(e)}
|
|
8
|
+
|
|
9
|
+
`)}catch{}}function R({refresh:r,intervalMs:t}){let e=new f;async function n(){await r()}function o(i,c,s){if(i.method!=="GET"){c.writeHead(405,{allow:"GET"}).end();return}if(!u(i)){c.writeHead(401,{"content-type":"application/json"}).end('{"error":"unauthenticated"}');return}if(e.subscribe(i,c),s?.onClose&&i.on("close",s.onClose),!!s?.seed)try{s.seed((l,w)=>v(c,l,w))}catch{}}function g(i,c){i.effect(()=>{let s=!1,l=setInterval(()=>{e.size!==0&&(s||(s=!0,n().catch(()=>{}).finally(()=>{s=!1})))},t);return()=>clearInterval(l)},`${c}: refresh loop`),i.effect(()=>()=>e.closeAll(),`${c}: sse teardown`)}return{hub:e,handler:o,install:g}}export{_ as a,x as b,h as c,H as d,J as e,R as f};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a as $,e as v}from"./chunk-JLZMOSQC.mjs";import{c as R}from"./chunk-JUBIWGKW.mjs";import{readFile as ne,writeFile as ie,mkdir as oe,rename as ae,rm as ue}from"node:fs/promises";import{join as D}from"node:path";import{randomUUID as G}from"node:crypto";var S=["line","area","bar","table"],y={projects:{items:{label:"\u9879\u76EE",at:t=>t?.items},usage:{label:"\u9879\u76EE \xD7 \u8D44\u6E90\u6C60 \u7528\u91CF",at:t=>A(t,"usage",K)},storage:{label:"\u9879\u76EE \xD7 \u5B58\u50A8\u6C60 \u7528\u91CF",at:t=>A(t,"storage",W)},queues:{label:"\u9879\u76EE\u961F\u5217\u51C6\u5165",at:t=>A(t,"queues",z)}},workloads:{items:{label:"\u8D1F\u8F7D",at:t=>t?.items}},nodes:{items:{label:"\u8282\u70B9",at:t=>t?.items}},cluster:{items:{label:"\u96C6\u7FA4\u7EC4\u4EF6",at:t=>t?.items}},overview:{projects:{label:"\u9879\u76EE\u914D\u989D\u4E0E\u7528\u91CF",at:t=>t?.projects},idle:{label:"\u95F2\u7F6E\u5360\u7528",at:t=>t?.idle},queued:{label:"\u6392\u961F\u8D1F\u8F7D",at:t=>t?.queued},nodes:{label:"\u8282\u70B9",at:t=>t?.nodes}}},F=Object.fromEntries(Object.entries(y).map(([t,e])=>[t,Object.entries(e).map(([r,s])=>({key:r,label:s.label}))]));function A(t,e,r){let s=[];for(let n of t?.items??[])for(let a of n?.cluster?.[e]??[])s.push({project:n.slug,project_name:n.name,...r(a)});return s}function K(t){let e=t?.guaranteed??0,r=t?.used??0;return{pool:t?.pool,device_type:t?.device_type,guaranteed:e,used:r,over_guarantee:r-e}}function W(t){let e=t?.limit_gib??0,r=t?.remaining_gib,s=typeof r=="number"&&e>0?e-r:void 0;return{pool:t?.pool,limit_gib:t?.limit_gib,persistent_gib:t?.persistent_gib,runtime_gib:t?.runtime_gib,remaining_gib:r,used_gib:s,used_pct:s===void 0||e<=0?void 0:Math.round(s/e*1e3)/10}}function z(t){return{queue:t?.name,pool:t?.pool,admitted:t?.admitted,reason:t?.reason}}function Q(t,e,r){if(t==="overview"&&e==="idle"){let s=r?.workload;return{workload:typeof s=="string"?s:`${s?.project??"?"}/${s?.name??"?"}`,type:typeof s=="object"?s?.display_type:void 0,idle_cards:r?.idle_cards,idle_hours:r?.idle_seconds===void 0?void 0:Math.round(r.idle_seconds/3600*10)/10,source:r?.source}}return r}var I={"workloads/items":["project","name","status","xpu_count","pods_ready","pods_total","pool"],"overview/queued":["project","name","xpu_count","pool","created_at"],"overview/projects":["slug","quota_cards","used_cards","running","queued"],"overview/idle":["workload","idle_cards","idle_hours","source"],"overview/nodes":["name","status","cards_total","cards_allocated","cards_free"],"nodes/items":["name","status","pool","cards_total","cards_allocated","cards_free","avg_util"],"projects/items":["slug","name","member_count","created_at"],"projects/usage":["project","pool","guaranteed","used","over_guarantee"],"projects/storage":["project","pool","limit_gib","used_gib","used_pct","remaining_gib"],"projects/queues":["project","queue","pool","admitted","reason"],"cluster/items":["name","status","ready","total","version"]},H={"overview/projects":{dimension:"slug",measures:["quota_cards","used_cards"]},"overview/idle":{dimension:"workload",measures:["idle_cards"]},"overview/queued":{dimension:"name",measures:["xpu_count"]},"overview/nodes":{dimension:"name",measures:["cards_allocated","cards_free"]},"workloads/items":{dimension:"name",measures:["xpu_count"]},"nodes/items":{dimension:"name",measures:["cards_allocated","cards_free"]},"projects/items":{dimension:"slug",measures:["member_count"]},"projects/usage":{dimension:"project",measures:["guaranteed","used"]},"projects/storage":{dimension:"project",measures:["used_gib","remaining_gib"]},"projects/queues":{dimension:"project",measures:[]},"cluster/items":{dimension:"name",measures:["ready","total"]}},B={project:"\u9879\u76EE",project_name:"\u9879\u76EE\u540D",slug:"\u9879\u76EE",name:"\u540D\u79F0",workload:"\u8D1F\u8F7D",status:"\u72B6\u6001",phase:"\u9636\u6BB5",pool:"\u8D44\u6E90\u6C60",type:"\u7C7B\u578B",display_type:"\u7C7B\u578B",xpu_count:"\u5361\u6570",quota_cards:"\u914D\u989D(\u5361)",used_cards:"\u5DF2\u7528(\u5361)",running:"\u8FD0\u884C",queued:"\u6392\u961F",idle_cards:"\u95F2\u7F6E(\u5361)",idle_hours:"\u95F2\u7F6E(\u5C0F\u65F6)",source:"\u5224\u5B9A\u6765\u6E90",pods_ready:"\u5C31\u7EEA Pod",pods_total:"\u603B Pod",member_count:"\u6210\u5458\u6570",cards_total:"\u603B\u5361",cards_allocated:"\u5DF2\u5206\u914D",cards_free:"\u7A7A\u95F2",cards_healthy:"\u5065\u5EB7",cards_allocatable:"\u53EF\u5206\u914D",avg_util:"\u5E73\u5747\u5229\u7528\u7387",cpu_cores:"CPU \u6838",ready:"\u5C31\u7EEA",total:"\u603B\u6570",version:"\u7248\u672C",arch:"\u67B6\u6784",os:"\u7CFB\u7EDF",guaranteed:"\u4FDD\u5E95(\u5361)",used:"\u5B9E\u9645\u7528(\u5361)",over_guarantee:"\u8D85\u51FA\u4FDD\u5E95",limit_gib:"\u914D\u989D(GiB)",used_gib:"\u5DF2\u7528(GiB)",used_pct:"\u5DF2\u7528\u6BD4\u4F8B",remaining_gib:"\u5269\u4F59(GiB)",persistent_gib:"\u6301\u4E45(GiB)",runtime_gib:"\u8FD0\u884C\u65F6(GiB)",queue:"\u961F\u5217",admitted:"\u5DF2\u51C6\u5165",reason:"\u539F\u56E0",created_at:"\u521B\u5EFA\u65F6\u95F4",admitted_at:"\u51C6\u5165\u65F6\u95F4",started_at:"\u5F00\u59CB\u65F6\u95F4",last_activity_at:"\u6700\u8FD1\u6D3B\u52A8",updated_at:"\u66F4\u65B0\u65F6\u95F4",internal_ip:"\u5185\u7F51 IP",owner_name:"\u6240\u6709\u8005",image:"\u955C\u50CF",device_type:"\u8BBE\u5907\u578B\u53F7",chip_model:"\u82AF\u7247"},J=t=>t===null||["string","number","boolean"].includes(typeof t);function X(t,e){if(e==null||e==="")return{text:"\u2014",align:"right"};if(typeof e=="boolean")return{text:e?"\u662F":"\u5426",align:"center"};if(typeof e=="number")return t.endsWith("_bytes")?{text:Y(e),align:"right"}:t.endsWith("_pct")||t==="avg_util"?{text:`${Number(e.toPrecision(3))}%`,align:"right"}:{text:String(Number(e.toPrecision(6))),align:"right"};if(t.endsWith("_at")&&/^\d{4}-\d{2}-\d{2}T/.test(e)){let r=new Date(e);if(Number.isFinite(r.getTime())){let s=n=>String(n).padStart(2,"0");return{text:`${s(r.getMonth()+1)}-${s(r.getDate())} ${s(r.getHours())}:${s(r.getMinutes())}`,align:"right"}}}return{text:String(e),align:"left"}}function Y(t){let e=["B","KiB","MiB","GiB","TiB"],r=t,s=0;for(;Math.abs(r)>=1024&&s<e.length-1;)r/=1024,s+=1;return`${Number(r.toPrecision(3))} ${e[s]}`}function T(t,e,r){let s=y[t];if(!s)return;let n=r&&s[r]?r:Object.keys(s)[0],a=s[n]?.at(e);if(Array.isArray(a))return{source:n,rows:a.map(i=>Q(t,n,i))}}function Z(t){let e=[];for(let r of t.slice(0,20))for(let[s,n]of Object.entries(r??{}))J(n)&&!e.includes(s)&&e.push(s);return e}function ee(t,e,r,s){let n=new Set(r),a=Array.isArray(s)?s:[],i=a.filter(c=>!n.has(c)),o=a.filter(c=>n.has(c));if(o.length>0)return{columns:o,dropped:i};let u=I[`${t}/${e}`];if(u){let c=u.filter(m=>n.has(m));if(c.length>0)return{columns:c,dropped:i}}return{columns:r.slice(0,8),dropped:i}}var M=50,N=t=>Number.isFinite(t)?Math.max(1,Math.min(200,t)):M;function te(t,e,r){if(!e)return t;let s=r===!1?1:-1;return[...t].sort((n,a)=>{let i=n?.[e],o=a?.[e];return typeof i=="number"&&typeof o=="number"?(i-o)*s:String(i??"").localeCompare(String(o??""))*s})}function re(t,e,r={}){let s=S.includes(r.kind)?r.kind:"line";if(s==="line"||s==="area"){let d=v(e,{query:t,group:r.group,series:r.series});return{kind:s,series:d.series,group:d.group,note:d.note}}let n=T(t,e,r.source);if(!n){let d=Object.keys(y).join(" / ");return{kind:s,columns:[],rows:[],note:`${t} \u6CA1\u6709\u53EF\u5217\u8868\u5316\u7684\u6570\u636E\uFF08\u652F\u6301\uFF1A${d}\uFF09`}}let{source:a,rows:i}=n;if(i.length===0)return{kind:s,source:a,columns:[],rows:[],note:"\u8FD9\u6B21\u67E5\u8BE2\u6CA1\u6709\u8FD4\u56DE\u4EFB\u4F55\u884C"};let o=N(r.limit),u=te(i,r.sort,r.desc),c=u.slice(0,o),m=u.length>c.length?u.length-c.length:0,f=[];m>0&&f.push(`\u53EA\u663E\u793A\u524D ${c.length} \u884C\uFF0C\u53E6\u6709 ${m} \u884C\u672A\u663E\u793A`);let h=Z(i);if(r.sort&&!h.includes(r.sort)&&f.push(`\u6CA1\u6709 ${r.sort} \u8FD9\u4E00\u5217\uFF0C\u6392\u5E8F\u6CA1\u6709\u751F\u6548\uFF08\u53EF\u7528\uFF1A${h.join(" / ")}\uFF09`),s==="bar"){let d=h.filter(l=>i.slice(0,20).some(w=>typeof w?.[l]=="number")),g=r.dimension&&h.includes(r.dimension)?r.dimension:["slug","name","workload","project"].find(l=>h.includes(l))??h[0],p=Array.isArray(r.measures)?r.measures:[],k=p.filter(l=>d.includes(l)),O=p.filter(l=>!d.includes(l)),j=k.length>0?k.slice(0,5):d.slice(0,2);return j.length===0?{kind:s,source:a,categories:[],series:[],note:"\u8FD9\u7EC4\u6570\u636E\u91CC\u6CA1\u6709\u53EF\u7528\u4F5C\u67F1\u9AD8\u7684\u6570\u503C\u5217"}:(O.length>0&&f.push(`\u6CA1\u6709 ${O.join("/")} \u8FD9\u4E9B\u6570\u503C\u5217\uFF0C\u753B\u7684\u662F ${j.join("/")}\uFF08\u53EF\u7528\uFF1A${d.join(" / ")}\uFF09`),{kind:s,source:a,categories:c.map(l=>String(l?.[g]??"\u2014")),series:j.map(l=>({key:l,name:B[l]??l,data:c.map(w=>typeof w?.[l]=="number"?w[l]:null)})),dimension:g,note:f.length>0?f.join("\uFF1B"):void 0})}let{columns:b,dropped:E}=ee(t,a,h,r.columns);return E.length>0&&f.push(`\u6CA1\u6709 ${E.join("/")} \u8FD9\u4E9B\u5217\uFF0C\u663E\u793A\u7684\u662F ${b.join("/")}\uFF08\u53EF\u7528\uFF1A${h.join(" / ")}\uFF09`),{kind:"table",source:a,columns:b.map(d=>({key:d,label:B[d]??d})),rows:c.map(d=>b.map(g=>{let p=X(g,d?.[g]);return{text:p.text,align:p.align}})),total:i.length,note:f.length>0?f.join("\uFF1B"):void 0}}function se(t){return{kind:"line",...t?.view??{}}}function _(t,e={}){let r=S.includes(e.kind)?e.kind:"line";if(U(r))return{kind:r};let s=y[t],n={kind:r};s&&(n.source=e.source&&s[e.source]?e.source:Object.keys(s)[0]);let a=r==="bar"?H[`${t}/${n.source}`]:void 0,i=e.dimension??a?.dimension;i!==void 0&&(n.dimension=i),e.sort!==void 0&&(n.sort=e.sort);let o=N(e.limit);o!==M&&(n.limit=o),e.desc===!1&&(n.desc=!1);let u=r==="table"?I[`${t}/${n.source}`]:void 0,c=Array.isArray(e.columns)&&e.columns.length>0?e.columns:u;Array.isArray(c)&&c.length>0&&(n.columns=[...c]);let m=Array.isArray(e.measures)&&e.measures.length>0?e.measures:a?.measures;return Array.isArray(m)&&m.length>0&&(n.measures=[...m]),n}var U=t=>t===void 0||t==="line"||t==="area";var L=["1h","6h","24h","7d"],P=16,ce=["kind","source","dimension","sort","desc","limit"];function C(t){if(t?.range===void 0)return t??{};let{range:e,...r}=t;return r}function x(t){let e=Object.entries(t.params??{}).filter(([o])=>o!=="range").filter(([,o])=>o!==void 0).sort(([o],[u])=>o<u?-1:o>u?1:0).map(([o,u])=>`${o}=${String(u)}`).join("&"),r=Array.isArray(t.series)&&t.series.length>0?[...t.series].sort().join(","):"",s=_(t.query,t.view),n=ce.filter(o=>s[o]!==void 0).map(o=>`${o}=${String(s[o])}`).join(","),a=Array.isArray(s.columns)?s.columns.join("+"):"",i=Array.isArray(s.measures)?s.measures.join("+"):"";return`${t.query}?${e}#${r}@${n}|${a}|${i}`}function q(t,e){let r=Object.entries({...t.params??{},range:e}).filter(([s])=>s!=="group").filter(([,s])=>s!==void 0).sort(([s],[n])=>s<n?-1:s>n?1:0).map(([s,n])=>`${s}=${String(n)}`).join("&");return`${t.query}?${r}`}var V=class{#n;#e=[];#s=new Map;#t;#o=new Set;#r="24h";#a=0;#m=Promise.resolve();constructor(e=D(R(),"pins.json")){this.#n=e,this.#t=this.#f()}#u(e){let r=this.#m.then(e);return this.#m=r.catch(()=>{}),r}async#f(){try{let e=JSON.parse(await ne(this.#n,"utf8"));L.includes(e?.range)&&(this.#r=e.range);let r=Array.isArray(e?.pins)?e.pins:[],s=new Set,n=!1;this.#e=r.map(a=>{let i=a;i.params?.range!==void 0&&(n=!0,i={...i,params:C(i.params)});let o=_(i.query,i.view);return JSON.stringify(o)!==JSON.stringify(i.view)&&(n=!0,i={...i,view:o}),i}).filter(a=>{let i=x(a);return s.has(i)?(n=!0,!1):(s.add(i),!0)}),(n||this.#e.length!==r.length)&&await this.#i()}catch{this.#e=[]}}async#i(){await oe(D(this.#n,".."),{recursive:!0});let e=`${JSON.stringify({range:this.#r,pins:this.#e},void 0,2)}
|
|
2
|
+
`,r=`${this.#n}.${G().slice(0,8)}.tmp`;await ie(r,e,"utf8");try{await ae(r,this.#n)}catch(s){throw await ue(r,{force:!0}),s}}async ready(){return await this.#t,this}async list(){return await this.#t,this.#e.map(e=>({...e,latest:this.#s.get(e.id)}))}async range(){return await this.#t,this.#r}async setRange(e,r,s){if(await this.#t,!L.includes(e))throw new Error(`\u4E0D\u652F\u6301\u7684\u65F6\u95F4\u8303\u56F4\uFF1A${e}`);if(!await this.#u(async()=>e===this.#r?!1:(this.#r=e,this.#s.clear(),this.#a+=1,await this.#i(),!0)))return{range:e,results:[]};let a=await this.refreshAll(r,s);return this.#l(),{range:e,results:a}}async findEquivalent(e){await this.#t;let r=x(e);return this.#e.find(s=>x(s)===r)}async add(e,r){return await this.#t,this.#u(async()=>{let s=C(e.params),n=await this.findEquivalent({...e,params:s});if(n!==void 0)return{pin:n,created:!1};if(this.#e.length>=P){let i=new Error(`\u56FA\u5B9A\u56FE\u8868\u5DF2\u8FBE\u4E0A\u9650\uFF08${P} \u5F20\uFF09\uFF0C\u5148\u53D6\u6D88\u56FA\u5B9A\u4E00\u5F20\u518D\u9489\u65B0\u7684`);throw i.code="pin_limit",i.status=409,i}let a={id:G(),title:e.title||"\u672A\u547D\u540D\u56FE\u8868",query:e.query,params:s,series:e.series,view:_(e.query,e.view),workspace:e.workspace,provenance:{source:"runnpu",createdAt:new Date().toISOString(),...r}};return this.#e=[...this.#e,a],await this.#i(),this.#l(),{pin:a,created:!0}})}async remove(e){return await this.#t,this.#u(async()=>{let r=this.#e.length;return this.#e=this.#e.filter(s=>s.id!==e),this.#s.delete(e),this.#e.length===r?!1:(await this.#i(),this.#l(),!0)})}async refresh(e,r,s){let n=await this.#c(e,r,s);return n===void 0?{id:e.id,at:Date.now(),ok:!1,stale:!0,error:"\u65F6\u95F4\u8303\u56F4\u521A\u521A\u5207\u6362\uFF0C\u8FD9\u6B21\u53D6\u6570\u5DF2\u4F5C\u5E9F\uFF0C\u7A0D\u540E\u4F1A\u6309\u65B0\u7A97\u53E3\u91CD\u65B0\u53D6\u6570"}:this.#d(e,n)}async#c(e,r,s){let n=this.#a,a=$.includes(e.query)?{...e.params,range:this.#r}:{...e.params},i;try{i={ok:!0,data:await r.query(e.workspace,e.query,a,s)}}catch(o){i={ok:!1,error:o.message,code:o.code}}return n===this.#a?i:void 0}#d(e,r){let s={id:e.id,at:Date.now(),...r};return this.#s.set(e.id,s),s}async refreshAll(e,r){await this.#t;let s=new Map;for(let a of this.#e){let i=q(a,this.#r),o=s.get(i);o?o.push(a):s.set(i,[a])}return(await Promise.all([...s.values()].map(async a=>{let i=await this.#c(a[0],e,r);return i===void 0?[]:a.map(o=>this.#d(o,i))}))).flat()}async requestCount(){return await this.#t,new Set(this.#e.map(e=>q(e,this.#r))).size}async refreshMissing(e,r){await this.#t;let s=this.#e.filter(i=>this.#s.get(i.id)===void 0);if(s.length===0)return[];let n=new Map;for(let i of s){let o=q(i,this.#r),u=n.get(o);u?u.push(i):n.set(o,[i])}return(await Promise.all([...n.values()].map(async i=>{let o=await this.#c(i[0],e,r);return o===void 0?[]:i.map(u=>this.#d(u,o))}))).flat()}latestOf(e){return this.#s.get(e)}onChange(e){return this.#o.add(e),()=>this.#o.delete(e)}#l(){for(let e of[...this.#o])try{e()}catch{}}};export{S as a,F as b,T as c,re as d,se as e,_ as f,U as g,L as h,P as i,x as j,V as k};
|