dsh-scnet 0.1.0 → 0.2.1

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/README.md CHANGED
@@ -1,26 +1,20 @@
1
1
  <h1 align="center">dsh-scnet</h1>
2
2
 
3
- <p align="center">在 DeepSeek Harness 里操作超算互联网(scnet.cn)及同类国产超算集群:配 SSH、生成 Slurm 作业、探测集群、排查失败,附海光 DCU/DTK 知识。</p>
3
+ <p align="center">在 DeepSeek Harness 里操作超算互联网(scnet.cn)及同类国产超算集群:配 SSH、生成 Slurm 作业、探测/刷新集群、运行计算节点探针、排查失败,附海光 DCU/DTK 知识。</p>
4
4
 
5
5
  ## 这是什么
6
6
 
7
- 一个 DeepSeek Harness 官方 bundle 插件,把 `scnet-hpc` 的 Agent Skill 和 bash 脚本包装成 DSH 可安装包。装好后 DSH 模型在涉及超算集群、Slurm 作业、海光 DCU/DTK 时会加载对应 skill,并可调用工具完成确定性操作。
7
+ 一个 DeepSeek Harness bundle 插件,把 `scnet-hpc` 的 Agent Skill 和 bash 脚本包装成 DSH 可安装包。装好后 DSH 模型在涉及超算集群、Slurm 作业、海光 DCU/DTK 时会加载对应 skill,并可调用工具完成确定性操作。
8
8
 
9
9
  ## 安装
10
10
 
11
- ```sh
12
- dsh plugin --profile web add "github:lql341/dsh-scnet#main"
13
- ```
14
-
15
- 或本地目录:
11
+ 推荐从 npm 安装:
16
12
 
17
13
  ```sh
18
- git clone https://github.com/lql341/dsh-scnet.git
19
- cd dsh-scnet
20
- dsh plugin --profile web add .
14
+ dsh plugin --profile web add dsh-scnet
21
15
  ```
22
16
 
23
- > git 源安装会取源码而非构建产物;本插件入口是纯 JavaScript(无编译步骤),无需 allowBuilds。
17
+ git 源、本地目录安装以及验证步骤见 [INSTALL.md](./INSTALL.md)。
24
18
 
25
19
  ## 能力面
26
20
 
@@ -30,6 +24,14 @@ dsh plugin --profile web add .
30
24
  |---|---|
31
25
  | scnet-hpc | 国产超算使用规范:连接、提交/监控、调试、失败排查、海光 DCU/DTK 资源索引 |
32
26
 
27
+ ### 内置集群
28
+
29
+ | 短名 | 描述 | 加速器 |
30
+ |---|---|---|
31
+ | zzeshell | 超算互联网 · 郑州 | 海光 BW1000 DCU (gfx936) ×8, 64GB/卡 |
32
+ | kseshell | 超算互联网 · 昆山 | 海光 Z100 DCU (gfx906) ×4, 16GB/卡 |
33
+ | wuzhshell | 超算互联网 · 乌镇 | 海光 Z100 DCU (gfx906) ×4, 16GB/卡 |
34
+
33
35
  ### Tools
34
36
 
35
37
  | 工具 | 说明 |
@@ -39,6 +41,13 @@ dsh plugin --profile web add .
39
41
  | scnet_generate_job | 按 profile 生成合规 Slurm 作业脚本 |
40
42
  | scnet_setup_ssh | 配置本地到集群的 SSH 连接 |
41
43
  | scnet_probe_cluster | 探测新集群并生成 profile |
44
+ | scnet_refresh_cluster | 动态刷新已有集群的规则缓存 |
45
+ | scnet_run_compute_probe | 在计算节点运行最小能力探针 |
46
+
47
+ ## 文档
48
+
49
+ - [INSTALL.md](./INSTALL.md) — 安装与验证
50
+ - [TESTING.md](./TESTING.md) — 按风险排序的初步测试步骤
42
51
 
43
52
  ## 结构
44
53
 
@@ -46,10 +55,20 @@ dsh plugin --profile web add .
46
55
  .
47
56
  ├── package.json # dsh.bundle + dsh.skills
48
57
  ├── cordis.patch.yml # bundle 组合层
49
- ├── index.mjs # Cordis 入口,注册 5 个工具
50
- ├── skills/scnet-hpc/ # Agent Skill + clusters/scripts/references
58
+ ├── index.mjs # Cordis 入口,注册 7 个工具
59
+ ├── skills/scnet-hpc/
60
+ │ ├── clusters/
61
+ │ │ ├── zzeshell.conf
62
+ │ │ ├── kseshell.conf
63
+ │ │ └── wuzhshell.conf
64
+ │ ├── scripts/
65
+ │ └── references/
51
66
  ├── sync.sh # 从 canonical 仓库同步
52
- └── README.md
67
+ ├── README.md # 项目说明
68
+ ├── INSTALL.md # 安装与验证
69
+ ├── TESTING.md # 初步测试步骤
70
+ ├── LICENSE
71
+ └── .gitignore
53
72
  ```
54
73
 
55
74
  ## 同步
@@ -62,13 +81,16 @@ dsh plugin --profile web add .
62
81
  ```
63
82
 
64
83
  只改 canonical,本仓库跑 sync 更新;不要把 `skills/scnet-hpc/` 当手工维护目录。
84
+ `sync.sh` 会自动排除 `scripts/install.sh` 和 `clusters/.cache/`,避免把安装脚本或本机动态缓存打进发布包。
65
85
 
66
86
  ## 隐私与安全
67
87
 
68
88
  - 仓库不含私钥、token、用户名、密钥指纹。
69
89
  - `scnet_setup_ssh` 会写入 `~/.ssh/`,调用前确认私钥路径和集群。
70
90
  - `scnet_generate_job` 生成的 `.slurm` 可能含本机用户名,`.gitignore` 已排除。
71
- - 集群主机名/端口是平台公开信息;profile 中连接字段默认留空,由用户本地填写。
91
+ - 集群主机名/端口是平台公开信息,由 canonical 仓库维护。
92
+ - `scnet_refresh_cluster` / `scnet_run_compute_probe` 会 SSH 到远端并可能提交作业;运行前确认集群和副作用。
93
+ - `clusters/.cache/` 是本机动态探测缓存,`.gitignore` 已排除,不应提交。
72
94
 
73
95
  ## License
74
96
 
package/index.mjs CHANGED
@@ -134,6 +134,8 @@ export function apply(ctx) {
134
134
  time: { type: 'string', description: '时长,默认 00:20:00,格式 HH:MM:SS 或 D-HH:MM:SS' },
135
135
  cluster: { type: 'string', description: '集群短名;省略时若只有一个 profile 则自动选择' },
136
136
  remote_user: { type: 'string', description: '远端用户名;与本地不同时填写(日志路径需要真实用户名)' },
137
+ refresh: { type: 'boolean', description: '是否在生成前运行 refresh-cluster.sh 刷新动态规则(默认否)' },
138
+ no_auto: { type: 'boolean', description: '是否忽略 clusters/.cache/*.auto.conf 动态缓存(默认否)' },
137
139
  },
138
140
  output: {
139
141
  schema: { type: 'string' },
@@ -153,18 +155,51 @@ export function apply(ctx) {
153
155
  if (cluster) argv.push('--cluster', cluster)
154
156
  const remoteUser = String(args.remote_user || '').trim()
155
157
  if (remoteUser) argv.push('--user', remoteUser)
158
+ if (args.refresh === true) argv.push('--refresh')
159
+ if (args.no_auto === true) argv.push('--no-auto')
156
160
  argv.push(nameVal)
157
161
  if (accelerators) argv.push(accelerators.value)
158
162
  if (cpus) argv.push(cpus.value)
159
163
  if (args.time && String(args.time).trim()) argv.push(String(args.time).trim())
160
164
 
161
- const res = await runBash(join(SCRIPTS_DIR, 'new-job.sh'), argv, 30000)
165
+ const timeoutMs = args.refresh === true ? 240000 : 30000
166
+ const res = await runBash(join(SCRIPTS_DIR, 'new-job.sh'), argv, timeoutMs)
162
167
  if (res.ok) return res.stdout.trim() || res.stderr.trim()
163
168
  return `生成失败(exit ${res.code}):\n${res.stderr.trim() || res.stdout.trim()}`
164
169
  },
165
170
  }),
166
171
  )
167
172
 
173
+ ctx.tools.register(
174
+ defineTool({
175
+ name: 'scnet_refresh_cluster',
176
+ description:
177
+ '动态刷新指定集群的规则缓存(分区、内存、GRES、网络、登录节点缺库等)。默认只做登录节点只读探测;compute=true 会额外提交一个约 10 分钟的计算节点能力探针,会 SSH 到远端并写 clusters/.cache/<短名>.auto.conf。',
178
+ parameters: {
179
+ cluster: { type: 'string', description: '集群短名;省略时若只有一个 profile 则自动选择' },
180
+ compute: { type: 'boolean', description: '是否额外提交计算节点能力探针(默认否)' },
181
+ dry_run: { type: 'boolean', description: '只输出将要写入的缓存,不落盘(默认否)' },
182
+ },
183
+ timeoutMs: 900000,
184
+ output: {
185
+ schema: { type: 'string' },
186
+ render: (_args, value) => textBlock(value),
187
+ },
188
+ async execute(args) {
189
+ const argv = []
190
+ const cluster = String(args.cluster || '').trim()
191
+ if (cluster) argv.push('--cluster', cluster)
192
+ if (args.compute === true) argv.push('--compute')
193
+ if (args.dry_run === true) argv.push('--dry-run')
194
+
195
+ const timeoutMs = args.compute === true ? 900000 : 240000
196
+ const res = await runBash(join(SCRIPTS_DIR, 'refresh-cluster.sh'), argv, timeoutMs)
197
+ if (res.ok) return res.stdout.trim() || res.stderr.trim()
198
+ return `刷新失败(exit ${res.code}):\n${res.stderr.trim() || res.stdout.trim()}`
199
+ },
200
+ }),
201
+ )
202
+
168
203
  ctx.tools.register(
169
204
  defineTool({
170
205
  name: 'scnet_setup_ssh',
@@ -194,6 +229,46 @@ export function apply(ctx) {
194
229
  }),
195
230
  )
196
231
 
232
+ ctx.tools.register(
233
+ defineTool({
234
+ name: 'scnet_run_compute_probe',
235
+ description:
236
+ '在已配置 SSH 的集群计算节点上运行最小能力探针,输出 PROBE_ 开头的结果(加速器架构、FP8、Triton、bitsandbytes、外网等)。会提交一个 1 卡、4 核、10 分钟的小作业并等待完成。',
237
+ parameters: {
238
+ cluster: { type: 'string', required: true, description: '集群短名' },
239
+ accelerators: { type: 'string', description: '加速器数量,默认 1' },
240
+ cpus: { type: 'string', description: 'CPU 核数,默认 4' },
241
+ time: { type: 'string', description: '作业时长,默认 00:10:00' },
242
+ remote_user: { type: 'string', description: '远端用户名;与本地不同时填写' },
243
+ },
244
+ timeoutMs: 900000,
245
+ output: {
246
+ schema: { type: 'string' },
247
+ render: (_args, value) => textBlock(value),
248
+ },
249
+ async execute(args) {
250
+ const cluster = String(args.cluster || '').trim()
251
+ if (!cluster) return '缺少必填参数 cluster。'
252
+
253
+ const accelerators = positiveInt(args.accelerators, 'accelerators')
254
+ if (accelerators && accelerators.error) return accelerators.error
255
+ const cpus = positiveInt(args.cpus, 'cpus')
256
+ if (cpus && cpus.error) return cpus.error
257
+
258
+ const argv = ['--cluster', cluster]
259
+ const remoteUser = String(args.remote_user || '').trim()
260
+ if (remoteUser) argv.push('--user', remoteUser)
261
+ if (cpus) argv.push('--cpus', cpus.value)
262
+ if (args.time && String(args.time).trim()) argv.push('--time', String(args.time).trim())
263
+ if (accelerators) argv.push('--accelerators', accelerators.value)
264
+
265
+ const res = await runBash(join(SCRIPTS_DIR, 'run-compute-probe.sh'), argv, 900000)
266
+ if (res.ok) return res.stdout.trim()
267
+ return `计算节点探针失败(exit ${res.code}):\n${res.stderr.trim() || res.stdout.trim()}`
268
+ },
269
+ }),
270
+ )
271
+
197
272
  ctx.tools.register(
198
273
  defineTool({
199
274
  name: 'scnet_probe_cluster',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-scnet",
3
- "version": "0.1.0",
4
- "description": "DeepSeek Harness bundle plugin for SCNet (scnet.cn) and similar domestic HPC clusters: SSH setup, Slurm job generation, cluster probing, and Hygon DCU/DTK guidance.",
3
+ "version": "0.2.1",
4
+ "description": "DeepSeek Harness bundle plugin for SCNet (scnet.cn) and similar domestic HPC clusters: SSH setup, Slurm job generation, cluster probing/refresh, compute-node capability probes, and Hygon DCU/DTK guidance.",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
7
7
  "exports": {
@@ -9,6 +9,13 @@
9
9
  "./cordis.patch.yml": "./cordis.patch.yml",
10
10
  "./package.json": "./package.json"
11
11
  },
12
+ "dependencies": {
13
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/lql341/dsh-scnet.git"
18
+ },
12
19
  "files": [
13
20
  "index.mjs",
14
21
  "cordis.patch.yml",
@@ -23,7 +30,7 @@
23
30
  ]
24
31
  },
25
32
  "engines": {
26
- "node": ">=20"
33
+ "node": ">=22.19"
27
34
  },
28
35
  "keywords": [
29
36
  "deepseek-harness",
@@ -24,6 +24,8 @@ cat clusters/<集群短名>.conf # 看具体参数
24
24
  | `clusters/_template.conf` | 新增集群的模板 |
25
25
  | `scripts/setup-ssh.sh` | 新电脑上配连接(macOS/Linux,幂等)|
26
26
  | `scripts/probe-cluster.sh` | 探测新集群,自动生成 profile |
27
+ | `scripts/refresh-cluster.sh` | 动态刷新已有集群的规则缓存 |
28
+ | `scripts/run-compute-probe.sh` | 在计算节点运行最小能力探针 |
27
29
  | `scripts/new-job.sh` | 按 profile 生成合规的作业脚本 |
28
30
  | `references/setup.md` | 连接配置详解、平台差异、故障排查 |
29
31
  | `references/environment.md` | 环境栈、装依赖、目录约定、迁移注意 |
@@ -34,12 +36,14 @@ cat clusters/<集群短名>.conf # 看具体参数
34
36
 
35
37
  ## 换机器
36
38
 
37
- 本插件已随 DeepSeek Harness 安装,换机器时用 `dsh plugin --profile web add` 重装即可。
39
+ 本插件已随 DeepSeek Harness 安装,换机器时用 `dsh plugin --profile web add dsh-scnet` 重装即可。
38
40
  配置集群连接:
39
41
 
40
42
  ```bash
41
- ./scripts/setup-ssh.sh <私钥文件> # 配连接(自动读唯一的 profile)
43
+ ./scripts/setup-ssh.sh --cluster <集群短名> <私钥文件> <用户名>
42
44
  ```
45
+
46
+ 只有一个 profile 时可省略 `--cluster`;公开 profile 里的 `KEY_NAME_MARKER` 是占位符,所以需要显式传用户名。
43
47
  ## 新增集群
44
48
 
45
49
  ```bash
@@ -51,6 +55,30 @@ cat clusters/<集群短名>.conf # 看具体参数
51
55
 
52
56
  完整步骤见 `references/adding-cluster.md`。
53
57
 
58
+ ## 动态刷新已有集群规则
59
+
60
+ profile 里的 `SSH_HOST` / `SSH_PORT` 是固定连接参数,应写死在
61
+ `clusters/<集群短名>.conf`。其余容易随平台调整的规则
62
+ (`DEF_MEM_PER_CPU`、`MIN_GRES`、`PARTITION`、`NET_OK`、登录节点缺库等)
63
+ 可以动态探测:
64
+
65
+ ```bash
66
+ ./scripts/refresh-cluster.sh --cluster <集群短名> # 只做登录节点只读探测
67
+ ./scripts/refresh-cluster.sh --cluster <集群短名> --compute # 再提交一个 10 分钟小作业
68
+ ./scripts/refresh-cluster.sh --cluster <集群短名> --dry-run # 只看结果,不写缓存
69
+ ```
70
+
71
+ 结果写到 `clusters/.cache/<集群短名>.auto.conf`,后续脚本会自动加载并覆盖
72
+ profile 中的同名字段。删除该缓存文件即可回退到纯 profile。生成作业时可用
73
+ `./scripts/new-job.sh --refresh ...` 先刷新再生成,或用 `--no-auto` 忽略缓存。
74
+
75
+ 默认只做登录节点探测,不提交作业;`--compute` 才会到计算节点实测加速器架构、
76
+ FP8、Triton、bitsandbytes 和外网状态。
77
+
78
+ 分区节点数 `NODE_COUNT` 是例外:每次加载集群时都会实时查询当前
79
+ `PARTITION` 的 `TotalNodes`,查不到才回退到 cache/profile。需要关闭时设
80
+ `SCNET_HPC_LIVE_NODE_COUNT=no`。
81
+
54
82
  ## 通用硬约束
55
83
 
56
84
  以下几条在国产超算上普遍存在,**具体数值查 profile**。
@@ -222,6 +250,8 @@ Triton 那条影响最大:**任何依赖 Triton kernel 的推理框架都跑
222
250
  | `lib*.so: cannot open shared object file` | 没 `module load`,或在登录节点跑了 |
223
251
  | `Name or service not known` / `offline mode` | 计算节点在访问外网 |
224
252
  | `hipDrvLaunchKernelEx` | Triton 不可用,换纯 PyTorch 路线 |
253
+ | `libgcvm.so.17git: cannot open shared object file` | Triton 在当前 DTK/DCU 栈不可用 |
254
+ | `python3: command not found` | 昆山计算节点需 `module load python/3.8.10` |
225
255
  | `sacct` COMPLETED 但结果不对 | 脚本缺 `exit $rc` |
226
256
 
227
257
  完整手册见 `references/troubleshooting.md`。
@@ -3,10 +3,11 @@
3
3
  # 新增集群:
4
4
  # 1. cp _template.conf <集群短名>.conf
5
5
  # 2. 填下面的值(不确定的用 scripts/probe-cluster.sh 探测)
6
- # 3. scripts/setup-ssh.sh --cluster <集群短名> <私钥文件>
6
+ # 3. scripts/setup-ssh.sh --cluster <集群短名> <私钥文件> [用户名]
7
7
  #
8
- # 只有 CLUSTER_ID / SSH_HOST / SSH_PORT 是必填,其余可留空,
9
- # 留空的项脚本会跳过、文档里显示为「未探测」。
8
+ # CLUSTER_ID / SSH_HOST / SSH_PORT 是必填连接参数,应写死;其余可留空。
9
+ # 留空的项脚本会跳过、文档里显示为「未探测」。旧 profile 漏填 SSH_HOST 时,
10
+ # _common.sh 会临时从本机 ssh config 补全,但新集群仍建议在此显式填写。
10
11
 
11
12
  # ---- 连接(必填)----
12
13
  CLUSTER_ID="" # 短名,同时用作 ssh 别名,如 "zzeshell"
@@ -35,7 +36,7 @@ ACCEL_MEM_GB=""
35
36
  CPUS_PER_NODE=""
36
37
  CPU_MODEL=""
37
38
  NODE_MEM_GB=""
38
- NODE_COUNT=""
39
+ NODE_COUNT="" # 通常留空/作回退;运行时优先实时查 TotalNodes
39
40
  LOGIN_NODES=""
40
41
  HOME_QUOTA_GB=""
41
42
 
@@ -44,6 +45,7 @@ TOOLCHAIN="" # "DTK" / "CUDA" / "ROCm" 等
44
45
  TOOLCHAIN_VERSION=""
45
46
  TOOLCHAIN_PATH=""
46
47
  MODULE_LOADS="" # 空格分隔的 module load 参数
48
+ PYTHON_MODULE="" # 计算节点缺少 python3 时加载的 Python module
47
49
 
48
50
  DEFAULT_VENV="" # 相对家目录的 venv 路径
49
51
 
@@ -1,4 +1,4 @@
1
- # kseshell —— 海光 Z100 DCU 集群(共享模板;连接字段需按平台填写)
1
+ # kseshell —— 海光 Z100 DCU 集群(连接字段已按平台填写)
2
2
  #
3
3
  # SCNet 昆山集群。与郑州(zzeshell / BW1000 / gfx936)的主要差别:
4
4
  # - 加速器是 Z100 (gfx906),单机 4 卡、每卡 16GB(BW1000 是 8 卡 × 64GB)
@@ -9,11 +9,11 @@
9
9
  # ---- 连接 ----
10
10
  CLUSTER_ID="kseshell"
11
11
  CLUSTER_DESC="海光 Z100 DCU(昆山)"
12
- SSH_HOST=""
13
- SSH_PORT=""
12
+ SSH_HOST="cancon.hpccube.com"
13
+ SSH_PORT="65023"
14
14
  HOME_BASE="/public/home"
15
15
 
16
- KEY_NAME_MARKER=""
16
+ KEY_NAME_MARKER="_<CLUSTER_HOST>_"
17
17
  NEEDS_UPDATE_HOSTKEYS_NO="no" # 实测无坏签名告警
18
18
 
19
19
  # ---- 调度器 ----
@@ -46,8 +46,8 @@ ACCEL_TDP_W="300"
46
46
  CPUS_PER_NODE="32"
47
47
  CPU_MODEL="Hygon C86 7185 32-core"
48
48
  NODE_MEM_GB="123"
49
- NODE_COUNT="571" # kshdnormal;CPU 队列 3557 节点
50
- LOGIN_NODES="login07 等(有多台)"
49
+ NODE_COUNT="1368" # 回退值;运行时优先实时查 TotalNodes
50
+ LOGIN_NODES="login01 / login02(示意)"
51
51
  HOME_QUOTA_GB="2950" # /public 文件系统总量,非个人独占
52
52
 
53
53
  # ---- 软件栈 ----
@@ -57,6 +57,7 @@ TOOLCHAIN_PATH=""
57
57
  MODULE_LOADS="compiler/dtk/26.04"
58
58
  # 可用版本很多:22.10 / 23.04 / 23.10 / 24.04.x / 25.04.x / 26.04
59
59
  # 实测 26.04、25.04.4、24.04.3 都能正常识别 4 张 gfx906
60
+ PYTHON_MODULE="python/3.8.10" # 计算节点默认无 python3
60
61
 
61
62
  DEFAULT_VENV="" # 尚未建 venv;系统 python3 是 3.8.5,无 torch
62
63
 
@@ -76,6 +77,7 @@ LOGIN_NODE_MISSING_LIBS="" # 登录节点无 torch(未装),
76
77
  # - 单卡仅 16GB,大模型必须多卡切分或量化
77
78
  # 具体的 torch/Triton 能力待建 venv 后实测,参考 references/adding-cluster.md
78
79
  KNOWN_LIMITATIONS="gfx906 (Vega20) 无 FP8/BF16 硬件支持、无 MFMA Matrix Core;\
80
+ 无 BF16 数据类型支持;计算节点默认无 python3,需 module load python/3.8.10;\
79
81
  单卡显存仅 16GB;torch/Triton 可用性待实测;\
80
82
  CentOS 7.6 / glibc 2.17 环境优先使用 manylinux_2_17 wheel,onnxruntime 通常锁定 1.16.3;\
81
83
  MinerU 3.4.x 需 Python >=3.10,旧版 magika 兼容方案需单独验证 API;\
@@ -0,0 +1,60 @@
1
+ # wuzhshell —— 乌镇超算(wuzh02.hpccube.com)
2
+
3
+ # ---- 连接 ----
4
+ CLUSTER_ID="wuzhshell"
5
+ CLUSTER_DESC="乌镇超算(wuzh02)"
6
+ SSH_HOST="wuzh02.hpccube.com"
7
+ SSH_PORT="65091"
8
+ HOME_BASE="/work/home"
9
+
10
+ KEY_NAME_MARKER="_<CLUSTER_HOST>_"
11
+ NEEDS_UPDATE_HOSTKEYS_NO="no"
12
+
13
+ # ---- 调度器 ----
14
+ SCHEDULER="slurm"
15
+
16
+ # 默认使用 DCU 队列;纯 CPU 作业改用 wzhcnormal。
17
+ PARTITION="wzhdnormal"
18
+
19
+ DEF_MEM_PER_CPU="3634"
20
+ MIN_GRES="dcu:1"
21
+ GRES_TYPE="dcu"
22
+ MAX_WALLTIME="333-08:00:00"
23
+
24
+ # CPU 队列和 DCU 队列。
25
+ PARTITION_CPU="wzhcnormal"
26
+ PARTITION_DCU="wzhdnormal"
27
+
28
+ # ---- 硬件 ----
29
+ ACCEL_NAME="海光 Z100 DCU (Z100SM)"
30
+ ACCEL_ARCH="gfx906"
31
+ ACCEL_PER_NODE="4"
32
+ ACCEL_MEM_GB="16"
33
+ CPUS_PER_NODE="32"
34
+ CPU_MODEL="Hygon C86 7285 32-core Processor"
35
+ NODE_MEM_GB="123"
36
+ NODE_COUNT="434"
37
+ LOGIN_NODES="login01 / login02(示意)"
38
+ HOME_QUOTA_GB="2000"
39
+
40
+ # ---- 软件栈 ----
41
+ TOOLCHAIN="DTK"
42
+ TOOLCHAIN_VERSION="26.04"
43
+ TOOLCHAIN_PATH=""
44
+ MODULE_LOADS="compiler/dtk/26.04"
45
+ PYTHON_MODULE="python/3.8.10"
46
+
47
+ DEFAULT_VENV=""
48
+
49
+ # ---- 网络 ----
50
+ NET_OK="pypi.org pypi.tuna.tsinghua.edu.cn modelscope.cn github.com"
51
+ NET_BLOCKED="huggingface.co"
52
+ PIP_INDEX="https://pypi.tuna.tsinghua.edu.cn/simple"
53
+ COMPUTE_NODE_OFFLINE="yes"
54
+ TMP_SHARED="no"
55
+
56
+ # ---- 已知问题 ----
57
+ LOGIN_NODE_MISSING_LIBS=""
58
+ KNOWN_LIMITATIONS="gfx906 (Vega20) 无 FP8/BF16 硬件支持、无 MFMA Matrix Core;\
59
+ 单卡显存 16GB;计算节点默认无 torch(module load compiler/dtk/26.04 后仍无 torch,需建 venv);\
60
+ 计算节点无外网(PROBE_NET_PYPI=OFFLINE);DCU FP8/Triton/bitsandbytes 待装 torch 后实测"
@@ -1,4 +1,4 @@
1
- # zzeshell —— 海光 DCU 集群(共享模板;连接字段需按平台填写)
1
+ # zzeshell —— 海光 DCU 集群(连接字段已按平台填写)
2
2
  #
3
3
  # 这是一个集群 profile。新增集群时复制本文件改值即可,
4
4
  # 脚本和文档都从这里读配置,不硬编码。
@@ -6,13 +6,13 @@
6
6
  # ---- 连接 ----
7
7
  CLUSTER_ID="zzeshell" # 短名,同时用作 ssh 别名
8
8
  CLUSTER_DESC="海光 BW1000 DCU(郑州)"
9
- SSH_HOST=""
10
- SSH_PORT=""
9
+ SSH_HOST="zzeshell.scnet.cn"
10
+ SSH_PORT="65032"
11
11
  HOME_BASE="/public/home" # 家目录父路径,实际家目录是 $HOME_BASE/$USER
12
12
 
13
13
  # 私钥文件名里的主机标识,用于从文件名推断用户名
14
14
  # 形如 <用户名>_<集群主机>_RsaKeyExpireTime_<日期>.txt
15
- KEY_NAME_MARKER=""
15
+ KEY_NAME_MARKER="_<CLUSTER_HOST>_"
16
16
 
17
17
  # 平台对 SSH 主机密钥轮换扩展返回错误签名,需要 UpdateHostKeys no 抑制告警
18
18
  NEEDS_UPDATE_HOSTKEYS_NO="yes"
@@ -22,7 +22,7 @@ SCHEDULER="slurm"
22
22
  PARTITION="hx1hdnormal"
23
23
 
24
24
  # 每 CPU 默认内存(MB)。内存上限 = cpus-per-task × 此值
25
- DEF_MEM_PER_CPU="3888"
25
+ DEF_MEM_PER_CPU="3800" # 2026-08-16 实测;旧值 3888
26
26
 
27
27
  # QOS 强制的最小 GRES。非空表示每个作业必须申请,纯 CPU 作业会被拒
28
28
  MIN_GRES="dcu:1"
@@ -40,8 +40,8 @@ ACCEL_MEM_GB="64"
40
40
  CPUS_PER_NODE="128"
41
41
  CPU_MODEL="Hygon C86"
42
42
  NODE_MEM_GB="498"
43
- NODE_COUNT="16"
44
- LOGIN_NODES="zz-login01 / zz-login02(有负载均衡)"
43
+ NODE_COUNT="131" # 回退值;运行时优先实时查 TotalNodes
44
+ LOGIN_NODES="login01 / login02(有负载均衡)"
45
45
  HOME_QUOTA_GB="1450" # /public 文件系统总量,非个人独占
46
46
 
47
47
  # ---- 软件栈 ----
@@ -49,6 +49,7 @@ TOOLCHAIN="DTK" # 海光的 ROCm 发行版
49
49
  TOOLCHAIN_VERSION="26.04"
50
50
  TOOLCHAIN_PATH=""
51
51
  MODULE_LOADS="compiler/gcc/9.3.0 compiler/dtk/26.04"
52
+ PYTHON_MODULE="" # 计算节点可用 python3,无需额外加载
52
53
 
53
54
  # 默认 venv 路径(相对家目录)。因人而异,装完可改
54
55
  DEFAULT_VENV="softwares/bitsandbytes/venv"
@@ -63,10 +64,10 @@ TMP_SHARED="no" # /tmp 是否跨节点共享
63
64
 
64
65
  # ---- 已知的平台特有问题 ----
65
66
  # 登录节点缺失的库(导致无法在登录节点 import 深度学习框架)
66
- LOGIN_NODE_MISSING_LIBS="libmsgpackc.so.2"
67
+ LOGIN_NODE_MISSING_LIBS="libgalaxyhip.so.5"
67
68
 
68
69
  # 加速器不支持的能力,写进文档避免重复踩坑
69
- KNOWN_LIMITATIONS="Triton 不可用(libamdhip64.so 缺 hipDrvLaunchKernelEx);\
70
+ KNOWN_LIMITATIONS="Triton 不可用(实测 libgcvm.so.17git 或 libamdhip64.so 缺 hipDrvLaunchKernelEx);\
70
71
  FP8 无硬件计算支持(torch._scaled_mm 拒绝 gfx936);\
71
72
  bitsandbytes 8-bit 不可用(hipBLASLt 无 gfx936 INT8 algo);\
72
73
  纯 PyTorch 软件反量化可作为大模型推理退路,但单序列解码约 0.93–1.12 tok/s;\
@@ -45,6 +45,9 @@ EOF
45
45
 
46
46
  profile 末尾的注释列出了还需确认的。主要是:
47
47
 
48
+ **`SSH_HOST` / `SSH_PORT`** —— 固定连接参数,应写死。本机已配置 SSH 别名时,
49
+ 可用 `ssh -G <别名>` 查出 `hostname` 和 `port` 后填进 profile。
50
+
48
51
  **加速器型号和架构** —— 在计算节点跑:
49
52
 
50
53
  ```bash
@@ -68,9 +71,12 @@ srun -p <分区> --gres=gpu:1 --time=00:05:00 nvidia-smi --query-gpu=name --form
68
71
  删掉第 2 步的临时段,然后:
69
72
 
70
73
  ```bash
71
- ./scripts/setup-ssh.sh --cluster <集群短名> <私钥文件>
74
+ ./scripts/setup-ssh.sh --cluster <集群短名> <私钥文件> <用户名>
72
75
  ```
73
76
 
77
+ 公开 profile 使用占位符时,`<用户名>` 必须显式传入;如果已在本地私有 profile
78
+ 里填了真实 `KEY_NAME_MARKER`,可以省略。
79
+
74
80
  ### 6. 实测加速器能力,写回 profile
75
81
 
76
82
  **这一步别省。** 国产加速器的软件栈往往落后于上游,很多想当然的能力并不存在。
@@ -121,6 +127,10 @@ Triton 尤其值得测:**它不可用会连带否掉一大批推理框架**(
121
127
  vLLM 的 ROCm MoE 后端、transformers 的 finegrained-fp8)。提前知道能避免在框架
122
128
  适配上白费时间。
123
129
 
130
+ 已有集群后续不必重跑完整流程,可用
131
+ `./scripts/refresh-cluster.sh --cluster <集群短名> --compute` 动态刷新调度器、
132
+ 内存、分区、网络和计算节点能力,结果写到 `clusters/.cache/<集群短名>.auto.conf`。
133
+
124
134
  ## profile 字段速查
125
135
 
126
136
  必填三项:`CLUSTER_ID`、`SSH_HOST`、`SSH_PORT`。其余留空则脚本跳过、文档显示为未探测。
@@ -133,7 +143,9 @@ vLLM 的 ROCm MoE 后端、transformers 的 finegrained-fp8)。提前知道能
133
143
  | `GRES_TYPE` | `new-job.sh` | 生成的脚本不含 `--gres` |
134
144
  | `DEF_MEM_PER_CPU` | `new-job.sh` | 生成的脚本不含 `--mem`,需手填 |
135
145
  | `MODULE_LOADS` | `new-job.sh` | 生成的脚本不含 `module load` |
146
+ | `PYTHON_MODULE` | `run-compute-probe.sh` | 计算节点缺 python3 时可能找不到解释器 |
136
147
  | `DEFAULT_VENV` | `new-job.sh` | 生成的脚本不含 venv 激活 |
148
+ | `NODE_COUNT` | 运行时回退 | 实时查 TotalNodes 失败时使用该值 |
137
149
  | `KEY_NAME_MARKER` | `setup-ssh.sh` | 无法从文件名推断用户名,需显式传 |
138
150
  | `NEEDS_UPDATE_HOSTKEYS_NO` | `setup-ssh.sh` | 不写 `UpdateHostKeys no` |
139
151
  | `COMPUTE_NODE_OFFLINE` | `new-job.sh` | 不设 `HF_HUB_OFFLINE` |
@@ -16,6 +16,14 @@ ssh <集群> 'sinfo -p <分区> -o "%n %c %m %G %t"' # 每节点 CPU/内存/GRE
16
16
  ssh <集群> 'sacctmgr show qos where name=<QOS名> format=Name,MinTRES%30,MaxTRES%30 -P'
17
17
  ```
18
18
 
19
+ 也可以用 skill 自带的动态刷新,登录节点只读探测不会提交作业:
20
+
21
+ ```bash
22
+ ./scripts/refresh-cluster.sh --cluster <集群短名> --dry-run
23
+ ```
24
+
25
+ 其中分区节点数 `NODE_COUNT` 默认每次加载集群时实时查询 `TotalNodes`。
26
+
19
27
  节点状态含义:`idle` 空闲、`mix` 部分占用、`alloc` 全占、`drain`/`drng` 不可调度
20
28
  (维护中)。实际可用节点常少于总数。
21
29
 
@@ -56,6 +64,12 @@ source ~/<DEFAULT_VENV>/bin/activate
56
64
  ImportError: libgalaxyhip.so.5: cannot open shared object file
57
65
  ```
58
66
 
67
+ 昆山计算节点默认没有 `python3` 命令,需额外加载:
68
+
69
+ ```bash
70
+ module load python/3.8.10
71
+ ```
72
+
59
73
  ## 厂商预编译的 wheel
60
74
 
61
75
  国产加速器的 torch 等框架需要厂商适配版,文件名带厂商标记。海光的例子:
@@ -87,7 +87,9 @@ ssh -O exit <集群> # 手工断开(网络切换后卡住时用)
87
87
  `<用户名>_<主机>_RsaKeyExpireTime_2026-10-29_12-51-40.txt` 表示 2026-10-29 过期。
88
88
  到期前从控制台重新下载并重跑 setup 脚本。
89
89
 
90
- profile 里的 `KEY_NAME_MARKER` 就是用来从这种文件名里提取用户名的。
90
+ profile 里的 `KEY_NAME_MARKER` 就是用来从这种文件名里提取用户名的;公开
91
+ `scnet-hpc` profile 使用 `_<CLUSTER_HOST>_` 占位符,所以安装时通常需要显式传
92
+ 用户名,或在本地私有 profile 中补齐真实标记。
91
93
 
92
94
  查看当前密钥指纹:
93
95
 
@@ -54,8 +54,9 @@ sbatch: error: QOSMinGRES
54
54
 
55
55
  三种可能:
56
56
 
57
- 1. **在登录节点跑 torch** —— 登录节点缺 `libmsgpackc.so.2`,即使 `module load`
58
- 之后也 import 不了。必须用 `srun`/`sbatch`。
57
+ 1. **在登录节点跑 torch** —— 郑州集群最近实测是缺 `libgalaxyhip.so.5`,旧环境
58
+ 报过 `libmsgpackc.so.2`;即使 `module load` 之后也 import 不了。必须用
59
+ `srun`/`sbatch`。
59
60
  2. **忘了 `module load`** —— venv 激活前必须先
60
61
  `module load compiler/gcc/9.3.0 compiler/dtk/26.04`。
61
62
  3. **顺序错了** —— 先激活 venv 再 module load 也会失败,顺序必须是 module 在前。
@@ -84,6 +85,14 @@ Triton。DTK 26.04 的 HIP 运行时缺上游 ROCm 7 的 API,Triton 的 AMD ba
84
85
  即失败。**放弃这条路**,包括所有依赖 Triton kernel 的框架(SGLang FP8、vLLM ROCm
85
86
  MoE、transformers finegrained-fp8)。
86
87
 
88
+ 郑州集群最近还会先报另一种缺库:
89
+
90
+ ```
91
+ ImportError: libgcvm.so.17git: cannot open shared object file
92
+ ```
93
+
94
+ 本质相同,都是 Triton 在当前 DTK/DCU 软件栈上不可用,不要继续调 Triton kernel。
95
+
87
96
  ### `torch._scaled_mm is only supported on CUDA devices with compute capability >= 9.0 or 8.9, or ROCm MI300+`
88
97
 
89
98
  gfx936 不支持硬件 FP8 计算。需要软件反量化(把 FP8 权重转成 BF16 再算)。
@@ -127,6 +136,24 @@ ROCM_PATH=未设置
127
136
 
128
137
  自检:作业里打印 `echo "ROCM_PATH=${ROCM_PATH:-未设置}"`,未设置就说明没生效。
129
138
 
139
+ ### `python3: command not found`(昆山计算节点)
140
+
141
+ 昆山计算节点 `module purge` 后没有 `python3` 命令,裸 `python` 是 Python 2,
142
+ 会再报:
143
+
144
+ ```
145
+ SyntaxError: Non-ASCII character '\xe5' ... no encoding declared
146
+ ```
147
+
148
+ 修法:作业脚本里显式加载 Python 3 module:
149
+
150
+ ```bash
151
+ module load python/3.8.10
152
+ ```
153
+
154
+ `scripts/run-compute-probe.sh` 会自动从 `module avail` 探测 `python/3.*` 模块,
155
+ 但普通业务作业仍需在 `MODULE_LOADS` 或 `PYTHON_MODULE` 里带上。
156
+
130
157
  ### `TIMEOUT`
131
158
 
132
159
  `--time` 不够。注意大模型加载本身就很慢(DeepSeek V4 加载 154B 权重约 27 分钟),
@@ -16,6 +16,25 @@ list_clusters() {
16
16
  find "$CLUSTER_DIR" -maxdepth 1 -name '*.conf' ! -name '_*' -exec basename {} .conf \; 2>/dev/null | sort
17
17
  }
18
18
 
19
+ refresh_live_node_count() {
20
+ [ "${SCNET_HPC_LIVE_NODE_COUNT:-yes}" != "no" ] || return 0
21
+ [ "${SCHEDULER:-slurm}" = "slurm" ] || return 0
22
+ [ -n "${CLUSTER_ID:-}" ] || return 0
23
+ [ -n "${PARTITION:-}" ] || return 0
24
+
25
+ local live
26
+ live=$(ssh -o BatchMode=yes -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=2 \
27
+ "$CLUSTER_ID" "scontrol show partition ${PARTITION}" 2>/dev/null \
28
+ | tr ' ' '\n' | sed -n 's/^TotalNodes=//p' | head -1)
29
+
30
+ if [[ "$live" =~ ^[0-9]+$ ]] && [ "$live" -gt 0 ]; then
31
+ NODE_COUNT="$live"
32
+ NODE_COUNT_SOURCE="live"
33
+ else
34
+ NODE_COUNT_SOURCE="${NODE_COUNT_SOURCE:-profile/cache}"
35
+ fi
36
+ }
37
+
19
38
  load_cluster() {
20
39
  local want="${1:-}"
21
40
  local available
@@ -45,11 +64,32 @@ $(printf '%s\n' "$available" | sed 's/^/ /')"
45
64
  . "$conf"
46
65
 
47
66
  [ -n "${CLUSTER_ID:-}" ] || die "$conf 里 CLUSTER_ID 为空"
48
- [ -n "${SSH_HOST:-}" ] || die "$conf 里 SSH_HOST 为空"
49
67
  : "${SSH_PORT:=22}"
50
68
  : "${HOME_BASE:=/public/home}"
51
69
 
70
+ # SSH_HOST/SSH_PORT 应以 profile 为准。这里只作为兼容回退:
71
+ # 旧 profile 漏填时,从本机 ~/.ssh/config 动态补全,避免脚本直接失败。
72
+ if [ -z "${SSH_HOST:-}" ]; then
73
+ SSH_HOST="$(ssh -G "$CLUSTER_ID" 2>/dev/null | awk '/^hostname /{print $2; exit}')"
74
+ fi
75
+ if [ -z "${SSH_PORT:-}" ]; then
76
+ SSH_PORT="$(ssh -G "$CLUSTER_ID" 2>/dev/null | awk '/^port /{print $2; exit}')"
77
+ fi
78
+ REMOTE_USER_DETECTED="$(ssh -G "$CLUSTER_ID" 2>/dev/null | awk '/^user /{print $2; exit}')"
79
+ : "${REMOTE_USER_DETECTED:=}"
80
+
81
+ # 用于设置新连接的新集群仍需明确的 SSH_HOST/SSH_PORT;已有本机 ssh 别名时
82
+ # 这里会从 ssh -G 补齐。若仍为空,给一个可操作的错误。
83
+ [ -n "$SSH_HOST" ] || die "$conf 里 SSH_HOST 为空,且 ssh -G ${CLUSTER_ID} 也解析不到 HostName;请先配置 ~/.ssh/config 或补 profile。"
84
+
52
85
  CLUSTER_CONF="$conf"
86
+ CLUSTER_AUTO_CONF="$CLUSTER_DIR/.cache/$want.auto.conf"
87
+ if [ "${SCNET_HPC_USE_AUTO:-yes}" != "no" ] && [ -f "$CLUSTER_AUTO_CONF" ]; then
88
+ # shellcheck disable=SC1090
89
+ . "$CLUSTER_AUTO_CONF"
90
+ fi
91
+
92
+ refresh_live_node_count
53
93
  }
54
94
 
55
95
  # 从参数里摘出 --cluster <名>,剩下的放进 REST[]
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env python3
2
+ """在目标计算节点上做一次最小能力探针。
3
+
4
+ 输出统一使用 PROBE_ 前缀的 KEY=value 行,方便 refresh-cluster.sh 解析。
5
+ 每个检查都独立捕获异常,单项失败不会中断后续检查。
6
+ """
7
+
8
+ import os
9
+ import urllib.request
10
+
11
+
12
+ def emit(key, value):
13
+ print("PROBE_{}={}".format(key, value), flush=True)
14
+
15
+
16
+ def short_error(exc, limit=180):
17
+ text = str(exc).replace("\n", " ")
18
+ return "{}:{}".format(type(exc).__name__, text[:limit])
19
+
20
+
21
+ try:
22
+ import torch
23
+ emit("TORCH_VERSION", torch.__version__)
24
+ cuda_available = bool(torch.cuda.is_available())
25
+ emit("CUDA_AVAILABLE", "1" if cuda_available else "0")
26
+
27
+ if cuda_available:
28
+ count = torch.cuda.device_count()
29
+ emit("DEVICE_COUNT", count)
30
+
31
+ props = torch.cuda.get_device_properties(0)
32
+ emit("DEVICE_NAME", getattr(props, "name", "unknown").replace("\n", " "))
33
+
34
+ arch = getattr(props, "gcnArchName", None)
35
+ if arch is None:
36
+ arch = "{}.{}".format(getattr(props, "major", "?"), getattr(props, "minor", "?"))
37
+ emit("ARCH", arch)
38
+
39
+ total_memory = getattr(props, "total_memory", 0)
40
+ emit("MEM_MIB", int(total_memory // (1024 * 1024)))
41
+
42
+ have_dtypes = []
43
+ for dtype_name in ["float8_e4m3fn", "float8_e5m2", "float8_e8m0fnu", "bfloat16"]:
44
+ if hasattr(torch, dtype_name):
45
+ have_dtypes.append(dtype_name)
46
+ emit("DTYPES", ",".join(have_dtypes))
47
+
48
+ # 硬件 FP8 matmul
49
+ try:
50
+ if hasattr(torch, "float8_e4m3fn") and hasattr(torch, "_scaled_mm"):
51
+ a = torch.randn(64, 128).to(torch.float8_e4m3fn).cuda()
52
+ b = torch.randn(128, 64).to(torch.float8_e4m3fn).cuda().t().contiguous().t()
53
+ torch._scaled_mm(
54
+ a,
55
+ b,
56
+ scale_a=torch.tensor(1.0).cuda(),
57
+ scale_b=torch.tensor(1.0).cuda(),
58
+ out_dtype=torch.bfloat16,
59
+ )
60
+ torch.cuda.synchronize()
61
+ emit("SCALED_MM", "OK")
62
+ else:
63
+ emit("SCALED_MM", "SKIP:float8_e4m3fn_or_scaled_mm_missing")
64
+ except Exception as exc:
65
+ emit("SCALED_MM", "ERR:" + short_error(exc))
66
+
67
+ # Triton 内核
68
+ try:
69
+ import triton
70
+ import triton.language as tl
71
+
72
+ @triton.jit
73
+ def add_kernel(x, y, out, n, BLOCK: tl.constexpr):
74
+ idx = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK)
75
+ mask = idx < n
76
+ tl.store(out + idx, tl.load(x + idx, mask=mask) + tl.load(y + idx, mask=mask), mask=mask)
77
+
78
+ n = 4096
79
+ x = torch.randn(n, device="cuda")
80
+ y = torch.randn(n, device="cuda")
81
+ out = torch.empty_like(x)
82
+ add_kernel[(triton.cdiv(n, 256),)](x, y, out, n, BLOCK=256)
83
+ torch.cuda.synchronize()
84
+ max_err = float((out - (x + y)).abs().max().item())
85
+ emit("TRITON", "OK err={:.3g}".format(max_err))
86
+ except Exception as exc:
87
+ emit("TRITON", "ERR:" + short_error(exc, 240))
88
+
89
+ # bitsandbytes:先看能否 import;是否真正可用仍需业务级测试
90
+ try:
91
+ import bitsandbytes as bnb
92
+ emit("BITSANDBYTES", "IMPORT_OK version={}".format(getattr(bnb, "__version__", "unknown")))
93
+ except Exception as exc:
94
+ emit("BITSANDBYTES", "IMPORT_ERR:" + short_error(exc))
95
+ else:
96
+ emit("DEVICE_COUNT", 0)
97
+ except Exception as exc:
98
+ emit("TORCH_IMPORT", "ERR:" + short_error(exc, 240))
99
+
100
+ # 计算节点外网
101
+ try:
102
+ urllib.request.urlopen("https://pypi.org", timeout=5)
103
+ emit("NET_PYPI", "ONLINE")
104
+ except Exception:
105
+ emit("NET_PYPI", "OFFLINE")
106
+
107
+ # /tmp 是否跨节点共享无法只靠单节点可靠判断,这里只记录文件系统信息
108
+ try:
109
+ emit("TMP_FSTYPE", str(os.statvfs("/tmp").f_fsid))
110
+ except Exception as exc:
111
+ emit("TMP_FSTYPE", "ERR:" + short_error(exc))
112
+ emit("TMP_SHARED", "UNKNOWN")
113
+
114
+ print("PROBE_EXIT=0", flush=True)
@@ -14,11 +14,15 @@ set -- "${REST[@]+${REST[@]}}"
14
14
 
15
15
  # --user <名>:远端用户名与本地不同时指定(日志路径需要真实用户名)
16
16
  REMOTE_USER=""
17
+ REFRESH=no
18
+ USE_AUTO=yes
17
19
  ARGS=()
18
20
  while [ $# -gt 0 ]; do
19
21
  case "$1" in
20
22
  --user) REMOTE_USER="${2:-}"; shift 2 ;;
21
23
  --user=*) REMOTE_USER="${1#*=}"; shift ;;
24
+ --refresh) REFRESH=yes; shift ;;
25
+ --no-auto) USE_AUTO=no; shift ;;
22
26
  *) ARGS+=("$1"); shift ;;
23
27
  esac
24
28
  done
@@ -31,7 +35,7 @@ TIME="${4:-00:20:00}"
31
35
 
32
36
  if [ -z "$NAME" ]; then
33
37
  cat >&2 <<EOF
34
- 用法: $0 [--cluster <集群短名>] [--user <远端用户名>] <作业名> [加速器数] [cpu数] [时长]
38
+ 用法: $0 [--cluster <集群短名>] [--user <远端用户名>] [--refresh|--no-auto] <作业名> [加速器数] [cpu数] [时长]
35
39
 
36
40
  例子:
37
41
  $0 probe # 1 卡, 8 核, 20 分钟(探针)
@@ -42,6 +46,11 @@ EOF
42
46
  exit 1
43
47
  fi
44
48
 
49
+ if [ "$REFRESH" = yes ]; then
50
+ "$REPO_ROOT/scripts/refresh-cluster.sh" --cluster "${CLUSTER:-}"
51
+ fi
52
+
53
+ export SCNET_HPC_USE_AUTO="$USE_AUTO"
45
54
  load_cluster "$CLUSTER"
46
55
 
47
56
  [ "${SCHEDULER:-slurm}" = "slurm" ] \
@@ -69,7 +78,7 @@ fi
69
78
  # 远端用户名。#SBATCH 是注释行,Slurm 不做变量展开 —— 日志路径里写 $USER
70
79
  # 会被当成字面量目录名,作业以 exit 53 失败且不产生日志。所以这里必须展开成
71
80
  # 真实用户名。默认取本地用户名,不同时用 --user 覆盖。
72
- REMOTE_USER="${REMOTE_USER:-$(whoami)}"
81
+ REMOTE_USER="${REMOTE_USER:-${REMOTE_USER_DETECTED:-$(whoami)}}"
73
82
  HOME_DIR="${HOME_BASE}/${REMOTE_USER}"
74
83
 
75
84
  # 脚本体(非 #SBATCH 行)里可以正常用 $USER
@@ -137,6 +146,7 @@ sed -i.bak '/^$/N;/^\n$/D' "$OUT" 2>/dev/null && rm -f "$OUT.bak"
137
146
 
138
147
  echo "已生成 $OUT"
139
148
  echo " 集群=${CLUSTER_ID} ${GRES_TYPE:-加速器}=${ACCEL} CPU=${CPUS} 内存=${MEM_NOTE} 时长=${TIME}"
149
+ echo " 分区=${PARTITION:-未设置} 节点数=${NODE_COUNT:-未探测}(来源:${NODE_COUNT_SOURCE:-profile/cache})"
140
150
  echo " 日志目录=${HOME_DIR}/scripts/logs(用户名 ${REMOTE_USER},不对请加 --user)"
141
151
  echo
142
152
  echo "上传并提交:"
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env bash
2
+ # 动态刷新 clusters/.cache/<集群>.auto.conf。
3
+ #
4
+ # 用法:
5
+ # ./refresh-cluster.sh [--cluster <集群短名>] [--compute] [--dry-run]
6
+ #
7
+ # 默认只做登录节点上的只读探测;--compute 会提交一个 1 卡、4 核、10 分钟的小作业,
8
+ # 到计算节点实测加速器、Triton、FP8、外网等能力。生成的缓存会覆盖 profile 的同名字段。
9
+
10
+ set -euo pipefail
11
+
12
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
13
+
14
+ parse_cluster_arg "$@"
15
+ set -- "${REST[@]+${REST[@]}}"
16
+
17
+ COMPUTE=no
18
+ DRY_RUN=no
19
+
20
+ while [ $# -gt 0 ]; do
21
+ case "$1" in
22
+ --compute|-c) COMPUTE=yes; shift ;;
23
+ --dry-run|-n) DRY_RUN=yes; shift ;;
24
+ -h|--help) sed -n '2,14p' "$0" | sed 's/^# \?//'; exit 0 ;;
25
+ *) die "未知参数: $1" ;;
26
+ esac
27
+ done
28
+
29
+ load_cluster "$CLUSTER"
30
+
31
+ r() {
32
+ ssh -o BatchMode=yes -o ConnectTimeout=15 -o ServerAliveInterval=15 -o ServerAliveCountMax=3 \
33
+ "$CLUSTER_ID" "$1" 2>/dev/null || true
34
+ }
35
+
36
+ probe_value() {
37
+ local key="$1"
38
+ printf '%s\n' "${COMPUTE_OUT:-}" | awk -F= -v k="$key" '$1 == k { sub(/^[^=]+=/,""); print; exit }'
39
+ }
40
+
41
+ add_known_limit() {
42
+ local item="$1"
43
+ [ -n "$item" ] || return 0
44
+ case "$item" in
45
+ *Triton*)
46
+ if printf '%s' "$KNOWN_LIMITS_NEW" | grep -q 'Triton'; then return 0; fi
47
+ ;;
48
+ *FP8*)
49
+ if printf '%s' "$KNOWN_LIMITS_NEW" | grep -q 'FP8'; then return 0; fi
50
+ ;;
51
+ *BF16*)
52
+ if printf '%s' "$KNOWN_LIMITS_NEW" | grep -q 'BF16'; then return 0; fi
53
+ ;;
54
+ *bitsandbytes*)
55
+ if printf '%s' "$KNOWN_LIMITS_NEW" | grep -q 'bitsandbytes'; then return 0; fi
56
+ ;;
57
+ esac
58
+ KNOWN_LIMITS_NEW="${KNOWN_LIMITS_NEW:+${KNOWN_LIMITS_NEW};}${item}"
59
+ }
60
+
61
+ info "==> 动态探测 ${CLUSTER_ID} (${CLUSTER_DESC:-$SSH_HOST})"
62
+ probe_ok=$(r 'echo yes')
63
+ [ "$probe_ok" = "yes" ] || die "无法通过 ssh 连接 ${CLUSTER_ID}"
64
+
65
+ # ---- 连接和登录节点基础信息 ----
66
+ DETECTED_HOST="$(ssh -G "$CLUSTER_ID" 2>/dev/null | awk '/^hostname /{print $2; exit}')"
67
+ DETECTED_PORT="$(ssh -G "$CLUSTER_ID" 2>/dev/null | awk '/^port /{print $2; exit}')"
68
+ DETECTED_USER="$(ssh -G "$CLUSTER_ID" 2>/dev/null | awk '/^user /{print $2; exit}')"
69
+ LOGIN_HOST=$(r 'hostname')
70
+ REMOTE_HOME=$(r 'echo $HOME')
71
+ HOME_BASE_NEW=$(dirname "$REMOTE_HOME")
72
+ CPU_MODEL_NEW=$(r "lscpu | sed -n 's/^Model name:[[:space:]]*//p' | head -1")
73
+ QUOTA_NEW=$(r "df -BG $REMOTE_HOME 2>/dev/null | awk 'NR==2{gsub(/[^0-9]/,\"\",\$2); print \$2}'")
74
+
75
+ # ---- 调度器 ----
76
+ SCHED_NEW="unknown"
77
+ if [ -n "$(r 'command -v sbatch')" ]; then
78
+ SCHED_NEW="slurm"
79
+ elif [ -n "$(r 'command -v qsub')" ]; then
80
+ SCHED_NEW="pbs"
81
+ fi
82
+
83
+ PART_NEW=""
84
+ DEFMEM_NEW=""
85
+ MAXWALL_NEW=""
86
+ MINGRES_NEW=""
87
+ GRESTYPE_NEW=""
88
+ NODECNT_NEW=""
89
+ CPUS_NEW=""
90
+ NODEMEM_NEW=""
91
+ ACCEL_NEW=""
92
+
93
+ if [ "$SCHED_NEW" = "slurm" ]; then
94
+ # 优先沿用 profile 现有分区,只在它已经不再 up 时才改选第一个 up 分区。
95
+ if [ -n "${PARTITION:-}" ]; then
96
+ PART_STATE=$(r "sinfo -h -p ${PARTITION} -o '%a' | head -1")
97
+ [ "$PART_STATE" = "up" ] && PART_NEW="$PARTITION"
98
+ fi
99
+ if [ -z "$PART_NEW" ]; then
100
+ PART_NEW=$(r "sinfo -h -o '%P %a' | awk '\$2==\"up\"{print \$1; exit}'" | tr -d '*')
101
+ fi
102
+
103
+ if [ -n "$PART_NEW" ]; then
104
+ SCON=$(r "scontrol show partition $PART_NEW")
105
+ DEFMEM_NEW=$(printf '%s' "$SCON" | tr ' ' '\n' | sed -n 's/^DefMemPerCPU=//p' | head -1)
106
+ MAXWALL_NEW=$(printf '%s' "$SCON" | tr ' ' '\n' | sed -n 's/^MaxTime=//p' | head -1)
107
+ NODECNT_NEW=$(printf '%s' "$SCON" | tr ' ' '\n' | sed -n 's/^TotalNodes=//p' | head -1)
108
+
109
+ QOS_NEW=$(printf '%s' "$SCON" | tr ' ' '\n' | sed -n 's/^QoS=//p' | head -1)
110
+ if [ -n "$QOS_NEW" ] && [ "$QOS_NEW" != "N/A" ]; then
111
+ MINTRES=$(r "sacctmgr -nP show qos where name=$QOS_NEW format=MinTRES")
112
+ MINGRES_NEW=$(printf '%s' "$MINTRES" | sed -n 's|.*gres/\([a-z]*\)=\([0-9]*\).*|\1:\2|p')
113
+ fi
114
+
115
+ NODEINFO=$(r "sinfo -h -p $PART_NEW -o '%c|%m|%G' | head -1")
116
+ CPUS_NEW=$(printf '%s' "$NODEINFO" | cut -d'|' -f1)
117
+ NODEMEM_NEW=$(printf '%s' "$NODEINFO" | cut -d'|' -f2 | awk '{printf "%d", $1/1024}')
118
+ GRESRAW=$(printf '%s' "$NODEINFO" | cut -d'|' -f3)
119
+ GRESBASE=${GRESRAW%%(*}
120
+ GRESTYPE_NEW=$(printf '%s' "$GRESBASE" | cut -d: -f1)
121
+ ACCEL_NEW=$(printf '%s' "$GRESBASE" | awk -F: '{print $NF}' | tr -dc '0-9')
122
+ fi
123
+ fi
124
+
125
+ # ---- 登录节点网络 ----
126
+ NET_OK_NEW=""
127
+ NET_BLOCKED_NEW=""
128
+ for h in pypi.org pypi.tuna.tsinghua.edu.cn modelscope.cn huggingface.co github.com; do
129
+ reachable=no
130
+ for _ in 1 2; do
131
+ code=$(r "timeout 15 curl -sI https://$h -o /dev/null -w '%{http_code}'")
132
+ case "$code" in
133
+ 2*|3*) reachable=yes; break ;;
134
+ esac
135
+ done
136
+ if [ "$reachable" = yes ]; then
137
+ NET_OK_NEW="${NET_OK_NEW:+$NET_OK_NEW }$h"
138
+ else
139
+ NET_BLOCKED_NEW="${NET_BLOCKED_NEW:+$NET_BLOCKED_NEW }$h"
140
+ fi
141
+ done
142
+
143
+ # ---- 登录节点 torch 缺库 ----
144
+ torch_login=$(r 'python3 -c "import torch" 2>&1 | tail -1')
145
+ MISSING_LIB_NEW=$(printf '%s' "$torch_login" | sed -n 's/^ImportError: \([^:]*\.so[^:]*\).*/\1/p')
146
+
147
+ # ---- module 候选(只记录,不自动覆盖 MODULE_LOADS)----
148
+ MODULE_CANDIDATES_NEW=$(r "bash -lc 'module avail 2>&1' | awk '/compiler\\/(dtk|rocm)/{print}' | sort -u | head -30")
149
+
150
+ # ---- 计算节点能力(可选)----
151
+ COMPUTE_OUT=""
152
+ ACCEL_NAME_NEW=""
153
+ ACCEL_ARCH_NEW=""
154
+ ACCEL_MEM_NEW=""
155
+ COMPUTE_OFFLINE_NEW=""
156
+ KNOWN_LIMITS_NEW=""
157
+
158
+ if [ "$COMPUTE" = yes ] && [ "$DRY_RUN" = no ]; then
159
+ info "==> 提交计算节点小探针(约 1 卡、4 核、10 分钟)"
160
+ COMPUTE_OUT="$( { "$REPO_ROOT/scripts/run-compute-probe.sh" --cluster "$CLUSTER_ID"; } 2>&1 )" || true
161
+ if ! printf '%s\n' "$COMPUTE_OUT" | grep -q '^PROBE_'; then
162
+ info "计算节点探针未产生有效 PROBE 输出:"
163
+ printf '%s\n' "$COMPUTE_OUT" | sed 's/^/ /' >&2
164
+ COMPUTE_OUT=""
165
+ fi
166
+ fi
167
+
168
+ if [ "$COMPUTE" = yes ] && [ -n "$COMPUTE_OUT" ]; then
169
+ CUDA_AVAILABLE=$(probe_value PROBE_CUDA_AVAILABLE)
170
+ ACCEL_NAME_NEW=$(probe_value PROBE_DEVICE_NAME)
171
+ case "$ACCEL_NAME_NEW" in
172
+ BW|BW1000) ACCEL_NAME_NEW="海光 BW1000 DCU" ;;
173
+ esac
174
+ ACCEL_ARCH_NEW=$(probe_value PROBE_ARCH)
175
+
176
+ MEM_MIB=$(probe_value PROBE_MEM_MIB)
177
+ if [[ "$MEM_MIB" =~ ^[0-9]+$ ]]; then
178
+ ACCEL_MEM_NEW=$(( (MEM_MIB + 1023) / 1024 ))
179
+ fi
180
+
181
+ case "$(probe_value PROBE_NET_PYPI)" in
182
+ OFFLINE) COMPUTE_OFFLINE_NEW=yes ;;
183
+ ONLINE) COMPUTE_OFFLINE_NEW=no ;;
184
+ esac
185
+
186
+ if [ "$CUDA_AVAILABLE" = "0" ]; then
187
+ KNOWN_LIMITS_NEW="torch.cuda 不可用"
188
+ fi
189
+
190
+ DTYPES=$(probe_value PROBE_DTYPES)
191
+ case ",$DTYPES," in
192
+ *,bfloat16,*) ;;
193
+ *) KNOWN_LIMITS_NEW="${KNOWN_LIMITS_NEW:+${KNOWN_LIMITS_NEW};}无 BF16 数据类型支持" ;;
194
+ esac
195
+
196
+ SCALED=$(probe_value PROBE_SCALED_MM)
197
+ case "$SCALED" in
198
+ ERR:*) KNOWN_LIMITS_NEW="${KNOWN_LIMITS_NEW:+${KNOWN_LIMITS_NEW};}硬件 FP8 torch._scaled_mm 不可用(${SCALED})" ;;
199
+ esac
200
+
201
+ TRITON=$(probe_value PROBE_TRITON)
202
+ case "$TRITON" in
203
+ ERR:*) KNOWN_LIMITS_NEW="${KNOWN_LIMITS_NEW:+${KNOWN_LIMITS_NEW};}Triton 不可用(${TRITON})" ;;
204
+ esac
205
+
206
+ BNB=$(probe_value PROBE_BITSANDBYTES)
207
+ case "$BNB" in
208
+ IMPORT_ERR:*) KNOWN_LIMITS_NEW="${KNOWN_LIMITS_NEW:+${KNOWN_LIMITS_NEW};}bitsandbytes 导入失败(${BNB})" ;;
209
+ esac
210
+
211
+ # 保留 profile 里已有的人工经验,动态结果放前面。
212
+ if [ -n "${KNOWN_LIMITATIONS:-}" ]; then
213
+ OLD_IFS="$IFS"
214
+ IFS=';'
215
+ for item in $KNOWN_LIMITATIONS; do
216
+ add_known_limit "$item"
217
+ done
218
+ IFS="$OLD_IFS"
219
+ fi
220
+ elif [ "$COMPUTE" = yes ]; then
221
+ info "计算节点探针没有返回结果,保留 profile 中的加速器和限制信息。"
222
+ fi
223
+
224
+ build_cache() {
225
+ echo "# 由 scripts/refresh-cluster.sh 自动生成于 $(date '+%Y-%m-%d %H:%M:%S')"
226
+ echo "# 登录节点: ${LOGIN_HOST:-未探测}"
227
+ echo "# 这是动态探测缓存,会覆盖 profile 中的同名字段;删除本文件即可回退到纯 profile。"
228
+ printf 'SSH_HOST=%q\n' "${DETECTED_HOST:-$SSH_HOST}"
229
+ printf 'SSH_PORT=%q\n' "${DETECTED_PORT:-$SSH_PORT}"
230
+ printf 'REMOTE_USER_DETECTED=%q\n' "${DETECTED_USER:-${REMOTE_USER_DETECTED:-}}"
231
+ printf 'HOME_BASE=%q\n' "${HOME_BASE_NEW:-$HOME_BASE}"
232
+ printf 'SCHEDULER=%q\n' "${SCHED_NEW:-${SCHEDULER:-}}"
233
+ printf 'PARTITION=%q\n' "${PART_NEW:-${PARTITION:-}}"
234
+ printf 'DEF_MEM_PER_CPU=%q\n' "${DEFMEM_NEW:-${DEF_MEM_PER_CPU:-}}"
235
+ printf 'MIN_GRES=%q\n' "${MINGRES_NEW:-${MIN_GRES:-}}"
236
+ printf 'GRES_TYPE=%q\n' "${GRESTYPE_NEW:-${GRES_TYPE:-}}"
237
+ printf 'MAX_WALLTIME=%q\n' "${MAXWALL_NEW:-${MAX_WALLTIME:-}}"
238
+ printf 'CPUS_PER_NODE=%q\n' "${CPUS_NEW:-${CPUS_PER_NODE:-}}"
239
+ printf 'NODE_MEM_GB=%q\n' "${NODEMEM_NEW:-${NODE_MEM_GB:-}}"
240
+ printf 'NODE_COUNT=%q\n' "${NODECNT_NEW:-${NODE_COUNT:-}}"
241
+ printf 'ACCEL_PER_NODE=%q\n' "${ACCEL_NEW:-${ACCEL_PER_NODE:-}}"
242
+ printf 'CPU_MODEL=%q\n' "${CPU_MODEL_NEW:-${CPU_MODEL:-}}"
243
+ printf 'LOGIN_NODES=%q\n' "${LOGIN_HOST:-${LOGIN_NODES:-}}"
244
+ printf 'HOME_QUOTA_GB=%q\n' "${QUOTA_NEW:-${HOME_QUOTA_GB:-}}"
245
+ printf 'NET_OK=%q\n' "${NET_OK_NEW:-${NET_OK:-}}"
246
+ printf 'NET_BLOCKED=%q\n' "${NET_BLOCKED_NEW:-${NET_BLOCKED:-}}"
247
+ printf 'LOGIN_NODE_MISSING_LIBS=%q\n' "${MISSING_LIB_NEW:-${LOGIN_NODE_MISSING_LIBS:-}}"
248
+ printf 'MODULE_CANDIDATES=%q\n' "${MODULE_CANDIDATES_NEW:-}"
249
+
250
+ if [ "$COMPUTE" = yes ]; then
251
+ printf 'ACCEL_NAME=%q\n' "${ACCEL_NAME_NEW:-${ACCEL_NAME:-}}"
252
+ printf 'ACCEL_ARCH=%q\n' "${ACCEL_ARCH_NEW:-${ACCEL_ARCH:-}}"
253
+ printf 'ACCEL_MEM_GB=%q\n' "${ACCEL_MEM_NEW:-${ACCEL_MEM_GB:-}}"
254
+ printf 'COMPUTE_NODE_OFFLINE=%q\n' "${COMPUTE_OFFLINE_NEW:-${COMPUTE_NODE_OFFLINE:-}}"
255
+ printf 'KNOWN_LIMITATIONS=%q\n' "${KNOWN_LIMITS_NEW:-${KNOWN_LIMITATIONS:-}}"
256
+ else
257
+ printf 'COMPUTE_NODE_OFFLINE=%q\n' "${COMPUTE_NODE_OFFLINE:-}"
258
+ printf 'KNOWN_LIMITATIONS=%q\n' "${KNOWN_LIMITATIONS:-}"
259
+ fi
260
+ }
261
+
262
+ mkdir -p "$CLUSTER_DIR/.cache"
263
+ if [ "$DRY_RUN" = yes ]; then
264
+ info "==> dry-run,不写缓存"
265
+ build_cache
266
+ else
267
+ AUTO_TMP="$CLUSTER_AUTO_CONF.tmp.$$"
268
+ build_cache > "$AUTO_TMP"
269
+ mv "$AUTO_TMP" "$CLUSTER_AUTO_CONF"
270
+ info "==> 已写入 $CLUSTER_AUTO_CONF"
271
+
272
+ if [ "$COMPUTE" = yes ] && [ -n "$COMPUTE_OUT" ]; then
273
+ PROBE_LOG="$CLUSTER_DIR/.cache/${CLUSTER_ID}.probe.log"
274
+ printf '%s\n' "$COMPUTE_OUT" > "$PROBE_LOG"
275
+ info "==> 原始探针输出已保存 $PROBE_LOG"
276
+ fi
277
+ fi
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env bash
2
+ # 在目标集群计算节点运行一次最小能力探针,输出 PROBE_ 开头的解析结果。
3
+ #
4
+ # 用法:
5
+ # ./run-compute-probe.sh [--cluster <集群短名>] [--cpus 4] [--time 00:10:00] [--accelerators 1]
6
+
7
+ set -euo pipefail
8
+
9
+ . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
10
+
11
+ parse_cluster_arg "$@"
12
+ set -- "${REST[@]+${REST[@]}}"
13
+
14
+ CPUS=4
15
+ TIME=00:10:00
16
+ ACCEL=1
17
+ REMOTE_USER=""
18
+ PYTHON_BIN=""
19
+
20
+ while [ $# -gt 0 ]; do
21
+ case "$1" in
22
+ --cpus) CPUS="${2:-$CPUS}"; shift 2 ;;
23
+ --cpus=*) CPUS="${1#*=}"; shift ;;
24
+ --time) TIME="${2:-$TIME}"; shift 2 ;;
25
+ --time=*) TIME="${1#*=}"; shift ;;
26
+ --accelerators) ACCEL="${2:-$ACCEL}"; shift 2 ;;
27
+ --accelerators=*) ACCEL="${1#*=}"; shift ;;
28
+ --user) REMOTE_USER="${2:-}"; shift 2 ;;
29
+ --user=*) REMOTE_USER="${1#*=}"; shift ;;
30
+ --python) PYTHON_BIN="${2:-}"; shift 2 ;;
31
+ --python=*) PYTHON_BIN="${1#*=}"; shift ;;
32
+ -h|--help) sed -n '2,10p' "$0" | sed 's/^# \?//'; exit 0 ;;
33
+ *) die "未知参数: $1" ;;
34
+ esac
35
+ done
36
+
37
+ [ -n "${CLUSTER:-}" ] || die "请用 --cluster <集群短名> 指定集群。"
38
+ load_cluster "$CLUSTER"
39
+
40
+ [ "${SCHEDULER:-slurm}" = "slurm" ] || die "${CLUSTER_ID} 不是 Slurm 调度器,暂不支持自动计算节点探针。"
41
+ [ -n "${PARTITION:-}" ] || die "${CLUSTER_ID} 的 PARTITION 为空,无法提交计算节点探针。"
42
+
43
+ REMOTE_USER="${REMOTE_USER:-${REMOTE_USER_DETECTED:-$(whoami)}}"
44
+ HOME_DIR="${HOME_BASE}/${REMOTE_USER}"
45
+ PROBE_BASE="${HOME_DIR}/scnet-hpc-probe"
46
+
47
+ ssh_cmd() { ssh -o BatchMode=yes -o ConnectTimeout=20 -o ServerAliveInterval=15 -o ServerAliveCountMax=3 "$@"; }
48
+ scp_cmd() { scp -o BatchMode=yes -o ConnectTimeout=20 "$@"; }
49
+
50
+ PYTHON_MODULE_LINE=""
51
+ if [ -z "${PYTHON_MODULE:-}" ]; then
52
+ PYTHON_MODULE_DETECTED="$(ssh_cmd "$CLUSTER_ID" "bash -lc 'module avail 2>&1'" 2>/dev/null \
53
+ | awk '/^(python|apps\/python)\/3\./{print $1; exit}')"
54
+ [ -n "$PYTHON_MODULE_DETECTED" ] && PYTHON_MODULE="$PYTHON_MODULE_DETECTED"
55
+ fi
56
+ [ -n "${PYTHON_MODULE:-}" ] && PYTHON_MODULE_LINE="module load ${PYTHON_MODULE}"
57
+
58
+ MEM_LINE=""
59
+ if [ -n "${DEF_MEM_PER_CPU:-}" ]; then
60
+ MEM_MB=$(( CPUS * DEF_MEM_PER_CPU ))
61
+ MEM_GB=$(( MEM_MB / 1024 ))
62
+ [ "$MEM_GB" -gt 1 ] && MEM_GB=$(( MEM_GB - 1 ))
63
+ MEM_LINE="#SBATCH --mem=${MEM_GB}gb"
64
+ fi
65
+
66
+ GRES_LINE=""
67
+ if [ -n "${GRES_TYPE:-}" ]; then
68
+ GRES_LINE="#SBATCH --gres=${GRES_TYPE}:${ACCEL}"
69
+ fi
70
+
71
+ MODULE_LINE=""
72
+ [ -n "${MODULE_LOADS:-}" ] && MODULE_LINE="module load ${MODULE_LOADS}"
73
+
74
+ VENV_LINE=""
75
+ [ -n "${DEFAULT_VENV:-}" ] && VENV_LINE="source ${HOME_DIR}/${DEFAULT_VENV}/bin/activate"
76
+
77
+ TMPDIR_LOCAL=$(mktemp -d)
78
+ trap 'rm -rf "$TMPDIR_LOCAL"' EXIT
79
+ cp "$REPO_ROOT/scripts/compute-probe.py" "$TMPDIR_LOCAL/compute-probe.py"
80
+
81
+ cat > "$TMPDIR_LOCAL/probe.slurm" <<EOF
82
+ #!/bin/bash -l
83
+ #SBATCH --job-name=scnet_hpc_probe
84
+ #SBATCH --partition=${PARTITION}
85
+ #SBATCH --nodes=1
86
+ #SBATCH --ntasks-per-node=1
87
+ #SBATCH --cpus-per-task=${CPUS}
88
+ ${GRES_LINE}
89
+ ${MEM_LINE}
90
+ #SBATCH --time=${TIME}
91
+ #SBATCH --output=${HOME_DIR}/scripts/logs/scnet-hpc-probe_%j.out
92
+ #SBATCH --error=${HOME_DIR}/scripts/logs/scnet-hpc-probe_%j.err
93
+
94
+ module purge
95
+ ${MODULE_LINE}
96
+ ${PYTHON_MODULE_LINE}
97
+ ${VENV_LINE}
98
+ export HF_HUB_OFFLINE=1
99
+ export TRANSFORMERS_OFFLINE=1
100
+
101
+ # 部分集群 module purge 后没有 python3 这个命令,按候选名兜底找 Python。
102
+ PYTHON_BIN="${PYTHON_BIN}"
103
+ if [ -z "\$PYTHON_BIN" ]; then
104
+ for candidate in python3 python3.8 python3.9 python3.10 python3.11; do
105
+ if command -v "\$candidate" >/dev/null 2>&1; then
106
+ PYTHON_BIN="\$candidate"
107
+ break
108
+ fi
109
+ done
110
+ fi
111
+ [ -n "\$PYTHON_BIN" ] || { echo "PYTHON_NOT_FOUND" >&2; exit 127; }
112
+ "\$PYTHON_BIN" ${PROBE_BASE}.py
113
+ rc=\$?
114
+ echo "PROBE_EXIT=\$rc"
115
+ exit \$rc
116
+ EOF
117
+
118
+ ssh_cmd "$CLUSTER_ID" "mkdir -p ${HOME_DIR}/scripts/logs"
119
+ scp_cmd -q "$TMPDIR_LOCAL/compute-probe.py" "${CLUSTER_ID}:${PROBE_BASE}.py"
120
+ scp_cmd -q "$TMPDIR_LOCAL/probe.slurm" "${CLUSTER_ID}:${PROBE_BASE}.slurm"
121
+
122
+ JOBID=$(ssh_cmd "$CLUSTER_ID" "cd ${HOME_DIR} && sbatch --parsable --wait ${PROBE_BASE}.slurm" | tail -1)
123
+ OUT=$(ssh_cmd "$CLUSTER_ID" "cat ${HOME_DIR}/scripts/logs/scnet-hpc-probe_${JOBID}.out" 2>/dev/null || true)
124
+ ERR=$(ssh_cmd "$CLUSTER_ID" "cat ${HOME_DIR}/scripts/logs/scnet-hpc-probe_${JOBID}.err" 2>/dev/null || true)
125
+
126
+ if [ -n "$OUT" ]; then
127
+ printf '%s\n' "$OUT"
128
+ else
129
+ printf '%s\n' "$ERR" >&2
130
+ fi
131
+
132
+ ssh_cmd "$CLUSTER_ID" "rm -f ${PROBE_BASE}.py ${PROBE_BASE}.slurm ${HOME_DIR}/scripts/logs/scnet-hpc-probe_${JOBID}.out ${HOME_DIR}/scripts/logs/scnet-hpc-probe_${JOBID}.err"
@@ -28,7 +28,9 @@ EOF
28
28
  exit 1
29
29
  fi
30
30
 
31
+ SCNET_HPC_LIVE_NODE_COUNT=no
31
32
  load_cluster "$CLUSTER"
33
+ unset SCNET_HPC_LIVE_NODE_COUNT
32
34
 
33
35
  [ -f "$KEY_SRC" ] || die "找不到私钥文件: $KEY_SRC"
34
36
  grep -q "BEGIN.*PRIVATE KEY" "$KEY_SRC" \