dsh-scnet 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 +75 -0
- package/cordis.patch.yml +3 -0
- package/index.mjs +221 -0
- package/package.json +39 -0
- package/skills/scnet-hpc/SKILL.md +235 -0
- package/skills/scnet-hpc/clusters/_template.conf +59 -0
- package/skills/scnet-hpc/clusters/kseshell.conf +88 -0
- package/skills/scnet-hpc/clusters/zzeshell.conf +73 -0
- package/skills/scnet-hpc/references/adding-cluster.md +144 -0
- package/skills/scnet-hpc/references/environment.md +162 -0
- package/skills/scnet-hpc/references/hygon-dcu-development.md +74 -0
- package/skills/scnet-hpc/references/setup.md +136 -0
- package/skills/scnet-hpc/references/software-compatibility.md +49 -0
- package/skills/scnet-hpc/references/troubleshooting.md +205 -0
- package/skills/scnet-hpc/scripts/_common.sh +66 -0
- package/skills/scnet-hpc/scripts/new-job.sh +146 -0
- package/skills/scnet-hpc/scripts/probe-cluster.sh +156 -0
- package/skills/scnet-hpc/scripts/setup-ssh.sh +146 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
## 国产超算软件兼容性排查
|
|
2
|
+
|
|
3
|
+
### 验证顺序
|
|
4
|
+
|
|
5
|
+
1. **系统 ABI**:确认发行版、glibc、CPU 架构和节点类型;检查 wheel 的 `manylinux` 标签是否高于目标 glibc。
|
|
6
|
+
2. **工具链**:记录 GCC/Clang、DTK、HIP、CMake、MPI、Python 和框架版本;登录节点可用不等于计算节点可用。
|
|
7
|
+
3. **最小探针**:在目标计算节点验证 import、设备枚举、最小 kernel、一个真实功能和退出码。
|
|
8
|
+
4. **依赖路径**:区分 pip/conda 解析失败、运行时动态库失败、框架 kernel 不支持和模型/数据下载失败。
|
|
9
|
+
5. **端到端结果**:记录成功条件、性能、显存/内存、失败日志和未验证范围。
|
|
10
|
+
|
|
11
|
+
### 已验证的典型边界
|
|
12
|
+
|
|
13
|
+
- **gfx936 / BW1000**:Triton、硬件 FP8 `torch._scaled_mm`、部分 bitsandbytes INT8 路径可能不可用;纯 PyTorch 软件反量化可作为推理兜底,但需要单独做性能和精度验证。
|
|
14
|
+
- **gfx906 / Z100**:CentOS 7.6 的 glibc 2.17 会限制 manylinux wheel 版本;安装 Python 软件时优先选 manylinux2014 / manylinux_2_17 兼容包。MinerU 场景中,Python 3.10 + onnxruntime 1.16.3 是已验证方向,magika 版本/API 需要随源码适配。
|
|
15
|
+
- **GROMACS HIP**:先验证 CMake 最低版本、DTK 的 `lib`/`lib64` CMake 路径、目标架构、FFTW 预置和 SIMD;CUDA 兼容层与原生 HIP 的性能不可直接互相推断。
|
|
16
|
+
|
|
17
|
+
### 作业验证模板
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
set -o pipefail
|
|
21
|
+
python3 -c 'import torch; print(torch.__version__); print(torch.cuda.is_available())'
|
|
22
|
+
python3 -c 'import <target_package>; print("import ok")'
|
|
23
|
+
<minimal_function_test>
|
|
24
|
+
rc=$?
|
|
25
|
+
echo "test_exit_code=$rc"
|
|
26
|
+
exit "$rc"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
不要只看 `sacct` 的 `COMPLETED`。脚本必须显式传递最后一个测试命令的退出码,并同时读取 `.out/.err`。
|
|
30
|
+
|
|
31
|
+
### 公开报告模板
|
|
32
|
+
|
|
33
|
+
标题采用“项目 + 加速器/平台 + 编译与性能/兼容性测试报告”。正文建议按以下顺序:
|
|
34
|
+
|
|
35
|
+
1. 环境信息
|
|
36
|
+
2. 编译过程与主要难题
|
|
37
|
+
3. 最终方案与验证结果
|
|
38
|
+
4. 测试数据与资源占用
|
|
39
|
+
5. 已知限制与注意事项
|
|
40
|
+
6. 经验总结与通用建议
|
|
41
|
+
7. 附录:快速复现命令
|
|
42
|
+
|
|
43
|
+
发布前扫描:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
rg -n -i 'ssh|password|token|secret|/home/|/public/|/Users/|@[A-Za-z0-9.-]+|192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|job [0-9]+|作业编号|节点名' report.html
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
将命中项替换为占位符或概括描述;保留版本、架构、错误类型、性能数据和验证边界。
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# 排查作业失败
|
|
2
|
+
|
|
3
|
+
## 第一原则:不要相信 `sacct` 的 State
|
|
4
|
+
|
|
5
|
+
bash 脚本里 Python 崩了之后,bash 继续执行后面的行,作业以**最后一条命令**的退出码
|
|
6
|
+
结束。真实案例:
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
$ sacct -j <JOB_ID> --format=JobID,JobName,State,ExitCode,Elapsed
|
|
10
|
+
<JOB_ID> | example-job | COMPLETED | 0:0 | 00:34:32
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
看着完美,实际上 Python 在第 3 分钟就抛了 `ValueError`,日志末尾还打印了脚本里
|
|
14
|
+
写的 `===== COMPLETE =====`。
|
|
15
|
+
|
|
16
|
+
**排查顺序永远是:读日志 → 看 State,不是反过来。**
|
|
17
|
+
|
|
18
|
+
预防:脚本末尾写
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
rc=$?
|
|
22
|
+
echo "### python exit=$rc"
|
|
23
|
+
exit $rc
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 日志被进度条淹没时
|
|
27
|
+
|
|
28
|
+
`transformers` 加载权重的进度条写到 stderr,会刷出几万字符:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
grep -v "Loading weights" job_123.err | tail -50
|
|
32
|
+
# 或者只看 Python traceback
|
|
33
|
+
grep -A30 "Traceback" job_123.err | head -60
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 按报错分类
|
|
37
|
+
|
|
38
|
+
### `QOSMinGRES`
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
sbatch: error: min tres(gres/dcu) request 0 exceeds per-job max tres limit 1
|
|
42
|
+
for qos <QOS名>
|
|
43
|
+
sbatch: error: QOSMinGRES
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
忘了 `--gres=dcu:N`。这个分区**强制**每个作业至少 1 张 DCU,纯 CPU 作业也要写。
|
|
47
|
+
|
|
48
|
+
### `too much memory was requested relative to the number of CPUs`
|
|
49
|
+
|
|
50
|
+
违反 `--mem ≤ cpus-per-task × DEF_MEM_PER_CPU`(值见 profile)。
|
|
51
|
+
要么降 `--mem`,要么加 `--cpus-per-task`。
|
|
52
|
+
|
|
53
|
+
### `libmsgpackc.so.2` / `libgalaxyhip.so.5: cannot open shared object file`
|
|
54
|
+
|
|
55
|
+
三种可能:
|
|
56
|
+
|
|
57
|
+
1. **在登录节点跑 torch** —— 登录节点缺 `libmsgpackc.so.2`,即使 `module load`
|
|
58
|
+
之后也 import 不了。必须用 `srun`/`sbatch`。
|
|
59
|
+
2. **忘了 `module load`** —— venv 激活前必须先
|
|
60
|
+
`module load compiler/gcc/9.3.0 compiler/dtk/26.04`。
|
|
61
|
+
3. **顺序错了** —— 先激活 venv 再 module load 也会失败,顺序必须是 module 在前。
|
|
62
|
+
|
|
63
|
+
### `Name or service not known` / `Hugging Face Hub is in offline mode`
|
|
64
|
+
|
|
65
|
+
计算节点在访问外网。典型的是 `transformers` 的 kernel 下载机制:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
ValueError: Version 4 of 'kernels-community/finegrained-fp8' is not available
|
|
69
|
+
in the local cache and Hugging Face Hub is in offline mode
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
或者更慢的版本(httpx 超时后才报):
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
httpcore.ConnectError: [Errno -2] Name or service not known
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
脚本里加 `export HF_HUB_OFFLINE=1` 让它立刻失败而不是慢慢超时。但根本问题是**这个
|
|
79
|
+
库需要运行时下载**,得找不依赖下载的替代路径。
|
|
80
|
+
|
|
81
|
+
### `cannot get address for 'hipDrvLaunchKernelEx' from libamdhip64.so`
|
|
82
|
+
|
|
83
|
+
Triton。DTK 26.04 的 HIP 运行时缺上游 ROCm 7 的 API,Triton 的 AMD backend 加载
|
|
84
|
+
即失败。**放弃这条路**,包括所有依赖 Triton kernel 的框架(SGLang FP8、vLLM ROCm
|
|
85
|
+
MoE、transformers finegrained-fp8)。
|
|
86
|
+
|
|
87
|
+
### `torch._scaled_mm is only supported on CUDA devices with compute capability >= 9.0 or 8.9, or ROCm MI300+`
|
|
88
|
+
|
|
89
|
+
gfx936 不支持硬件 FP8 计算。需要软件反量化(把 FP8 权重转成 BF16 再算)。
|
|
90
|
+
|
|
91
|
+
### 退出码 53,且**完全没有日志文件**
|
|
92
|
+
|
|
93
|
+
`#SBATCH` 是注释行,**Slurm 不做变量展开**。日志路径里写 `$USER` 会被当成字面量
|
|
94
|
+
目录名,作业启动就失败:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
$ sacct -j <ID> --format=State,ExitCode
|
|
98
|
+
FAILED|0:53
|
|
99
|
+
$ cat ~/scripts/logs/myjob_<ID>.log
|
|
100
|
+
cat: No such file or directory ← 连日志都没有,因为写不进去
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
两个原因都会导致这个:
|
|
104
|
+
|
|
105
|
+
1. `--output` / `--error` 路径里有未展开的变量(`$USER`、`$HOME`)
|
|
106
|
+
2. **日志目录不存在** —— Slurm 不会自动创建
|
|
107
|
+
|
|
108
|
+
修法:`#SBATCH` 里写完整字面量路径,提交前 `mkdir -p <日志目录>`。
|
|
109
|
+
`scripts/new-job.sh` 会展开好用户名并提示建目录。
|
|
110
|
+
|
|
111
|
+
### `module load` 静默失败:作业返回 0 但 `ROCM_PATH` 没设置
|
|
112
|
+
|
|
113
|
+
某些集群的 module 函数只在 **login shell** 里定义。用普通 `#!/bin/bash` 时
|
|
114
|
+
`module load` 什么都不做,也不报错:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
ROCM_PATH=未设置
|
|
118
|
+
--- 架构 --- ← rocminfo 无输出
|
|
119
|
+
--- 显存 --- ← rocm-smi 无输出
|
|
120
|
+
### python exit=0 ← 作业「成功」了
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
这比直接报错更坑,因为退出码是 0。实测于昆山集群。
|
|
124
|
+
|
|
125
|
+
修法:shebang 用 `#!/bin/bash -l`,或在 heredoc 内包一层 `bash -l`。
|
|
126
|
+
`scripts/new-job.sh` 生成的脚本已经用 `bash -l`。
|
|
127
|
+
|
|
128
|
+
自检:作业里打印 `echo "ROCM_PATH=${ROCM_PATH:-未设置}"`,未设置就说明没生效。
|
|
129
|
+
|
|
130
|
+
### `TIMEOUT`
|
|
131
|
+
|
|
132
|
+
`--time` 不够。注意大模型加载本身就很慢(DeepSeek V4 加载 154B 权重约 27 分钟),
|
|
133
|
+
`--time` 要留足加载 + 计算的总时间。
|
|
134
|
+
|
|
135
|
+
### OOM(进程被杀,日志突然截断)
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
sacct -j <ID> --format=JobID,State,MaxRSS,ReqMem -P
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
注意 `MaxRSS` **只出现在 `.batch` 子步骤那一行**,主作业行是空的:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
JobID|MaxRSS|ReqMem
|
|
145
|
+
<JOB_ID>||115G ← 主行没有 MaxRSS
|
|
146
|
+
<JOB_ID>.batch|120586316K| ← 实际峰值在这里
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`MaxRSS` 接近 `cpus × DEF_MEM_PER_CPU` 就是 CPU 内存不够,加 `--cpus-per-task`。
|
|
150
|
+
上例申请了 `--mem=115gb` 而实际用到 120GB —— 因为 cgroup 按
|
|
151
|
+
`32 × 3888MB = 121.5GB` 给(该集群 DEF_MEM_PER_CPU=3888),`--mem` 只是请求量。
|
|
152
|
+
|
|
153
|
+
GPU OOM 会有明确的 `torch.cuda.OutOfMemoryError`,看每卡分配:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
for i in range(torch.cuda.device_count()):
|
|
157
|
+
print(i, torch.cuda.memory_allocated(i) / 1024**3, "GB")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 作业一直 `PD (Priority)` 不动
|
|
161
|
+
|
|
162
|
+
排队等资源,正常现象。看预计启动时间:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
squeue -j <ID> --start
|
|
166
|
+
sinfo -p <分区> -o "%n %t" # 有几台 idle
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`drain`/`drng` 状态的节点不参与调度,忙时可能等很久。
|
|
170
|
+
|
|
171
|
+
## 调试策略
|
|
172
|
+
|
|
173
|
+
**验证要便宜。** 大模型加载动辄半小时,用大作业试错代价太高。分层验证:
|
|
174
|
+
|
|
175
|
+
1. **纯逻辑**(不需要 GPU/torch)→ 本地 Python 直接跑
|
|
176
|
+
2. **需要 torch/DCU 但数据量小** → `srun` 交互式,约 5 秒拿到节点
|
|
177
|
+
3. **小规模真实数据** → 2 分钟的 `sbatch` 探针作业
|
|
178
|
+
4. **完整模型** → 最后才做
|
|
179
|
+
|
|
180
|
+
真实教训:DeepSeek V4 的反量化实现,先写 16 项单元测试(2 分钟作业)验证数值正确,
|
|
181
|
+
再跑完整模型。如果跳过第 3 步直接跑,每轮试错 30 分钟。
|
|
182
|
+
|
|
183
|
+
**探针作业模板**:只读 metadata 不加载权重,几秒就出结果:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from safetensors import safe_open
|
|
187
|
+
with safe_open(path, framework="pt", device="cpu") as f:
|
|
188
|
+
sl = f.get_slice(key)
|
|
189
|
+
print(key, sl.get_shape(), sl.get_dtype()) # 不读实际数据
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## 检查作业真正在干什么
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# 作业跑在哪个节点
|
|
196
|
+
squeue -j <ID> -o "%N"
|
|
197
|
+
|
|
198
|
+
# 登到那个节点看进程(作业运行期间可以)
|
|
199
|
+
ssh <节点名> 'top -b -n1 -u $USER | head -20'
|
|
200
|
+
|
|
201
|
+
# GPU 占用
|
|
202
|
+
srun --jobid=<ID> --overlap rocm-smi
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`--overlap` 让新的 srun 步骤共享已有作业的资源分配,不用重新排队。
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 加载集群 profile。被其他脚本 source。
|
|
3
|
+
#
|
|
4
|
+
# . "$(dirname "$0")/_common.sh"
|
|
5
|
+
# load_cluster "$CLUSTER" # 空则自动选择
|
|
6
|
+
#
|
|
7
|
+
# 自动选择规则:clusters/ 下只有一个非模板 profile 时用它,否则要求显式指定。
|
|
8
|
+
|
|
9
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
10
|
+
CLUSTER_DIR="$REPO_ROOT/clusters"
|
|
11
|
+
|
|
12
|
+
die() { printf '错误: %s\n' "$1" >&2; exit 1; }
|
|
13
|
+
info() { printf ' %s\n' "$1"; }
|
|
14
|
+
|
|
15
|
+
list_clusters() {
|
|
16
|
+
find "$CLUSTER_DIR" -maxdepth 1 -name '*.conf' ! -name '_*' -exec basename {} .conf \; 2>/dev/null | sort
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
load_cluster() {
|
|
20
|
+
local want="${1:-}"
|
|
21
|
+
local available
|
|
22
|
+
available=$(list_clusters)
|
|
23
|
+
|
|
24
|
+
[ -n "$available" ] || die "clusters/ 下没有可用的 profile。
|
|
25
|
+
从模板新建一个:
|
|
26
|
+
cp $CLUSTER_DIR/_template.conf $CLUSTER_DIR/<集群短名>.conf"
|
|
27
|
+
|
|
28
|
+
if [ -z "$want" ]; then
|
|
29
|
+
local count
|
|
30
|
+
count=$(printf '%s\n' "$available" | grep -c .)
|
|
31
|
+
if [ "$count" -eq 1 ]; then
|
|
32
|
+
want="$available"
|
|
33
|
+
else
|
|
34
|
+
die "有多个集群 profile,请用 --cluster <名> 指定:
|
|
35
|
+
$(printf '%s\n' "$available" | sed 's/^/ /')"
|
|
36
|
+
fi
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
local conf="$CLUSTER_DIR/$want.conf"
|
|
40
|
+
[ -f "$conf" ] || die "找不到 profile: $conf
|
|
41
|
+
可用的:
|
|
42
|
+
$(printf '%s\n' "$available" | sed 's/^/ /')"
|
|
43
|
+
|
|
44
|
+
# shellcheck disable=SC1090
|
|
45
|
+
. "$conf"
|
|
46
|
+
|
|
47
|
+
[ -n "${CLUSTER_ID:-}" ] || die "$conf 里 CLUSTER_ID 为空"
|
|
48
|
+
[ -n "${SSH_HOST:-}" ] || die "$conf 里 SSH_HOST 为空"
|
|
49
|
+
: "${SSH_PORT:=22}"
|
|
50
|
+
: "${HOME_BASE:=/public/home}"
|
|
51
|
+
|
|
52
|
+
CLUSTER_CONF="$conf"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# 从参数里摘出 --cluster <名>,剩下的放进 REST[]
|
|
56
|
+
parse_cluster_arg() {
|
|
57
|
+
CLUSTER=""
|
|
58
|
+
REST=()
|
|
59
|
+
while [ $# -gt 0 ]; do
|
|
60
|
+
case "$1" in
|
|
61
|
+
--cluster) CLUSTER="${2:-}"; shift 2 ;;
|
|
62
|
+
--cluster=*) CLUSTER="${1#*=}"; shift ;;
|
|
63
|
+
*) REST+=("$1"); shift ;;
|
|
64
|
+
esac
|
|
65
|
+
done
|
|
66
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 生成一个符合目标集群所有约束的作业脚本骨架。
|
|
3
|
+
#
|
|
4
|
+
# ./new-job.sh [--cluster <集群短名>] <作业名> [加速器数] [cpu数] [时长]
|
|
5
|
+
#
|
|
6
|
+
# 内存按 cpus × DEF_MEM_PER_CPU 自动取安全值,避免手算出错。
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh"
|
|
11
|
+
|
|
12
|
+
parse_cluster_arg "$@"
|
|
13
|
+
set -- "${REST[@]+${REST[@]}}"
|
|
14
|
+
|
|
15
|
+
# --user <名>:远端用户名与本地不同时指定(日志路径需要真实用户名)
|
|
16
|
+
REMOTE_USER=""
|
|
17
|
+
ARGS=()
|
|
18
|
+
while [ $# -gt 0 ]; do
|
|
19
|
+
case "$1" in
|
|
20
|
+
--user) REMOTE_USER="${2:-}"; shift 2 ;;
|
|
21
|
+
--user=*) REMOTE_USER="${1#*=}"; shift ;;
|
|
22
|
+
*) ARGS+=("$1"); shift ;;
|
|
23
|
+
esac
|
|
24
|
+
done
|
|
25
|
+
set -- "${ARGS[@]+${ARGS[@]}}"
|
|
26
|
+
|
|
27
|
+
NAME="${1:-}"
|
|
28
|
+
ACCEL="${2:-1}"
|
|
29
|
+
CPUS="${3:-8}"
|
|
30
|
+
TIME="${4:-00:20:00}"
|
|
31
|
+
|
|
32
|
+
if [ -z "$NAME" ]; then
|
|
33
|
+
cat >&2 <<EOF
|
|
34
|
+
用法: $0 [--cluster <集群短名>] [--user <远端用户名>] <作业名> [加速器数] [cpu数] [时长]
|
|
35
|
+
|
|
36
|
+
例子:
|
|
37
|
+
$0 probe # 1 卡, 8 核, 20 分钟(探针)
|
|
38
|
+
$0 infer 8 32 01:30:00 # 8 卡, 32 核, 1.5 小时(大模型)
|
|
39
|
+
|
|
40
|
+
可用集群: $(list_clusters | tr '\n' ' ')
|
|
41
|
+
EOF
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
load_cluster "$CLUSTER"
|
|
46
|
+
|
|
47
|
+
[ "${SCHEDULER:-slurm}" = "slurm" ] \
|
|
48
|
+
|| die "暂只支持 slurm,${CLUSTER_ID} 用的是 ${SCHEDULER}"
|
|
49
|
+
|
|
50
|
+
# 内存上限 = cpus × DEF_MEM_PER_CPU,留 1GB 余量避免边界抖动
|
|
51
|
+
if [ -n "${DEF_MEM_PER_CPU:-}" ]; then
|
|
52
|
+
MEM_MB=$(( CPUS * DEF_MEM_PER_CPU ))
|
|
53
|
+
MEM_GB=$(( MEM_MB / 1024 ))
|
|
54
|
+
[ "$MEM_GB" -gt 1 ] && MEM_GB=$(( MEM_GB - 1 ))
|
|
55
|
+
MEM_LINE="#SBATCH --mem=${MEM_GB}gb"
|
|
56
|
+
MEM_NOTE="${MEM_GB}gb(上限 $(( MEM_MB / 1024 )).$(( MEM_MB % 1024 * 100 / 1024 ))gb)"
|
|
57
|
+
else
|
|
58
|
+
MEM_LINE="# DEF_MEM_PER_CPU 未在 profile 中设置,未生成 --mem"
|
|
59
|
+
MEM_NOTE="未设置"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# QOS 强制申请加速器时必须写 --gres
|
|
63
|
+
GRES_LINE=""
|
|
64
|
+
if [ -n "${GRES_TYPE:-}" ]; then
|
|
65
|
+
GRES_LINE="#SBATCH --gres=${GRES_TYPE}:${ACCEL}"
|
|
66
|
+
[ -n "${MIN_GRES:-}" ] && GRES_LINE="$GRES_LINE # 必须:QOS 要求最少 ${MIN_GRES}"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# 远端用户名。#SBATCH 是注释行,Slurm 不做变量展开 —— 日志路径里写 $USER
|
|
70
|
+
# 会被当成字面量目录名,作业以 exit 53 失败且不产生日志。所以这里必须展开成
|
|
71
|
+
# 真实用户名。默认取本地用户名,不同时用 --user 覆盖。
|
|
72
|
+
REMOTE_USER="${REMOTE_USER:-$(whoami)}"
|
|
73
|
+
HOME_DIR="${HOME_BASE}/${REMOTE_USER}"
|
|
74
|
+
|
|
75
|
+
# 脚本体(非 #SBATCH 行)里可以正常用 $USER
|
|
76
|
+
MODULES=""
|
|
77
|
+
[ -n "${MODULE_LOADS:-}" ] && MODULES="module load ${MODULE_LOADS}"
|
|
78
|
+
VENV_LINE=""
|
|
79
|
+
[ -n "${DEFAULT_VENV:-}" ] && VENV_LINE="source ${HOME_DIR}/${DEFAULT_VENV}/bin/activate"
|
|
80
|
+
|
|
81
|
+
OFFLINE_LINES=""
|
|
82
|
+
if [ "${COMPUTE_NODE_OFFLINE:-yes}" = "yes" ]; then
|
|
83
|
+
OFFLINE_LINES="# 计算节点无外网:让 Hub 请求立刻失败,而不是等 httpx 超时
|
|
84
|
+
export HF_HUB_OFFLINE=1
|
|
85
|
+
export TRANSFORMERS_OFFLINE=1"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
OUT="${NAME}.slurm"
|
|
89
|
+
[ -e "$OUT" ] && die "$OUT 已存在"
|
|
90
|
+
|
|
91
|
+
cat > "$OUT" <<EOF
|
|
92
|
+
#!/bin/bash -l
|
|
93
|
+
# 目标集群: ${CLUSTER_ID} (${CLUSTER_DESC:-$SSH_HOST})
|
|
94
|
+
#
|
|
95
|
+
# 注意 shebang 是 "bash -l"(login shell)。有些集群的 module 函数只在 login
|
|
96
|
+
# shell 里定义,用普通 "#!/bin/bash" 会让 module load 静默失败 —— 作业照样
|
|
97
|
+
# 返回 0,但 ROCM_PATH 不设置、rocminfo/rocm-smi 无输出。实测于昆山集群。
|
|
98
|
+
#SBATCH --job-name=${NAME}
|
|
99
|
+
#SBATCH --partition=${PARTITION}
|
|
100
|
+
${GRES_LINE}
|
|
101
|
+
#SBATCH --nodes=1
|
|
102
|
+
#SBATCH --ntasks-per-node=1
|
|
103
|
+
#SBATCH --cpus-per-task=${CPUS}
|
|
104
|
+
${MEM_LINE}
|
|
105
|
+
#SBATCH --time=${TIME}
|
|
106
|
+
#SBATCH --output=${HOME_DIR}/scripts/logs/${NAME}_%j.log
|
|
107
|
+
#SBATCH --error=${HOME_DIR}/scripts/logs/${NAME}_%j.err
|
|
108
|
+
|
|
109
|
+
module purge
|
|
110
|
+
${MODULES}
|
|
111
|
+
${VENV_LINE}
|
|
112
|
+
|
|
113
|
+
${OFFLINE_LINES}
|
|
114
|
+
|
|
115
|
+
echo "### node=\$(hostname) jobid=\$SLURM_JOB_ID"
|
|
116
|
+
date +"%Y-%m-%d %H:%M:%S"
|
|
117
|
+
|
|
118
|
+
python3 - <<'PYEOF'
|
|
119
|
+
import torch
|
|
120
|
+
print("torch", torch.__version__, "|", torch.cuda.get_device_name(0),
|
|
121
|
+
"| devices", torch.cuda.device_count())
|
|
122
|
+
|
|
123
|
+
# TODO: 在这里写你的代码
|
|
124
|
+
PYEOF
|
|
125
|
+
|
|
126
|
+
rc=\$?
|
|
127
|
+
echo "### python exit=\$rc"
|
|
128
|
+
date +"%Y-%m-%d %H:%M:%S"
|
|
129
|
+
# 必须传播退出码,否则 python 崩了 sacct 仍显示 COMPLETED(假成功)
|
|
130
|
+
exit \$rc
|
|
131
|
+
EOF
|
|
132
|
+
|
|
133
|
+
chmod +x "$OUT"
|
|
134
|
+
|
|
135
|
+
# 清掉 profile 缺项造成的空行
|
|
136
|
+
sed -i.bak '/^$/N;/^\n$/D' "$OUT" 2>/dev/null && rm -f "$OUT.bak"
|
|
137
|
+
|
|
138
|
+
echo "已生成 $OUT"
|
|
139
|
+
echo " 集群=${CLUSTER_ID} ${GRES_TYPE:-加速器}=${ACCEL} CPU=${CPUS} 内存=${MEM_NOTE} 时长=${TIME}"
|
|
140
|
+
echo " 日志目录=${HOME_DIR}/scripts/logs(用户名 ${REMOTE_USER},不对请加 --user)"
|
|
141
|
+
echo
|
|
142
|
+
echo "上传并提交:"
|
|
143
|
+
echo " scp $OUT ${CLUSTER_ID}:${HOME_DIR}/scripts/"
|
|
144
|
+
echo " ssh ${CLUSTER_ID} 'mkdir -p ${HOME_DIR}/scripts/logs && sbatch --test-only ${HOME_DIR}/scripts/$OUT'"
|
|
145
|
+
echo
|
|
146
|
+
echo "注意:logs 目录必须先存在,否则作业会以 exit 53 失败且不产生任何日志。"
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 探测一个已能 SSH 登录的集群,输出可直接保存为 profile 的内容。
|
|
3
|
+
#
|
|
4
|
+
# ./probe-cluster.sh <ssh别名或host> [集群短名] > ../clusters/<集群短名>.conf
|
|
5
|
+
#
|
|
6
|
+
# 新增集群时先手工能 ssh 上去,再跑这个脚本自动填大部分字段。
|
|
7
|
+
# 探测不到的项留空并注释说明,不猜。
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
TARGET="${1:-}"
|
|
12
|
+
NAME="${2:-$TARGET}"
|
|
13
|
+
|
|
14
|
+
[ -n "$TARGET" ] || { echo "用法: $0 <ssh别名或host> [集群短名]" >&2; exit 1; }
|
|
15
|
+
|
|
16
|
+
r() { ssh -o BatchMode=yes -o ConnectTimeout=15 "$TARGET" "$1" 2>/dev/null || true; }
|
|
17
|
+
|
|
18
|
+
echo "==> 探测 $TARGET ..." >&2
|
|
19
|
+
|
|
20
|
+
# 先确认能连上,否则后面全是空值
|
|
21
|
+
probe_ok=$(r 'echo yes')
|
|
22
|
+
[ "$probe_ok" = "yes" ] || { echo "错误: 无法通过 ssh 连接 $TARGET" >&2; exit 1; }
|
|
23
|
+
|
|
24
|
+
USER_REMOTE=$(r 'whoami')
|
|
25
|
+
LOGIN_HOST=$(r 'hostname')
|
|
26
|
+
HOME_REAL=$(r 'echo $HOME')
|
|
27
|
+
HOME_BASE_V=$(dirname "$HOME_REAL")
|
|
28
|
+
|
|
29
|
+
# ---- 调度器 ----
|
|
30
|
+
SCHED="unknown"
|
|
31
|
+
r 'command -v sbatch' >/dev/null && [ -n "$(r 'command -v sbatch')" ] && SCHED="slurm"
|
|
32
|
+
[ "$SCHED" = "unknown" ] && [ -n "$(r 'command -v qsub')" ] && SCHED="pbs"
|
|
33
|
+
|
|
34
|
+
PART=""; DEFMEM=""; MAXWALL=""; MINGRES=""; GRESTYPE=""
|
|
35
|
+
NODECNT=""; CPUS_N=""; NODEMEM=""
|
|
36
|
+
|
|
37
|
+
if [ "$SCHED" = "slurm" ]; then
|
|
38
|
+
# 取第一个 up 状态的分区
|
|
39
|
+
PART=$(r "sinfo -h -o '%P %a' | awk '\$2==\"up\"{print \$1; exit}'" | tr -d '*')
|
|
40
|
+
|
|
41
|
+
if [ -n "$PART" ]; then
|
|
42
|
+
SCON=$(r "scontrol show partition $PART")
|
|
43
|
+
DEFMEM=$(printf '%s' "$SCON" | tr ' ' '\n' | sed -n 's/^DefMemPerCPU=//p' | head -1)
|
|
44
|
+
MAXWALL=$(printf '%s' "$SCON" | tr ' ' '\n' | sed -n 's/^MaxTime=//p' | head -1)
|
|
45
|
+
NODECNT=$(printf '%s' "$SCON" | tr ' ' '\n' | sed -n 's/^TotalNodes=//p' | head -1)
|
|
46
|
+
|
|
47
|
+
# QOS 的 MinTRES(如 gres/dcu=1)—— 决定是否强制申请加速器
|
|
48
|
+
QOSN=$(printf '%s' "$SCON" | tr ' ' '\n' | sed -n 's/^QoS=//p' | head -1)
|
|
49
|
+
if [ -n "$QOSN" ] && [ "$QOSN" != "N/A" ]; then
|
|
50
|
+
MINTRES=$(r "sacctmgr -nP show qos where name=$QOSN format=MinTRES")
|
|
51
|
+
# gres/dcu=1 -> dcu:1
|
|
52
|
+
MINGRES=$(printf '%s' "$MINTRES" | sed -n 's|.*gres/\([a-z]*\)=\([0-9]*\).*|\1:\2|p')
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# 节点规格取第一台
|
|
56
|
+
NODEINFO=$(r "sinfo -h -p $PART -o '%c|%m|%G' | head -1")
|
|
57
|
+
CPUS_N=$(printf '%s' "$NODEINFO" | cut -d'|' -f1)
|
|
58
|
+
NODEMEM=$(printf '%s' "$NODEINFO" | cut -d'|' -f2 | awk '{printf "%d", $1/1024}')
|
|
59
|
+
# gres 形如 dcu:Hygon:8(S:0-7) 或 gpu:a100:4
|
|
60
|
+
# 先砍掉括号里的 socket 映射,再取最后一段数字,否则会抓到 "0-7"
|
|
61
|
+
GRESRAW=$(printf '%s' "$NODEINFO" | cut -d'|' -f3)
|
|
62
|
+
GRESBASE=${GRESRAW%%(*}
|
|
63
|
+
GRESTYPE=$(printf '%s' "$GRESBASE" | cut -d: -f1)
|
|
64
|
+
ACCEL_N=$(printf '%s' "$GRESBASE" | awk -F: '{print $NF}' | tr -dc '0-9')
|
|
65
|
+
fi
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
CPU_MODEL_V=$(r "lscpu | sed -n 's/^Model name:[[:space:]]*//p' | head -1")
|
|
69
|
+
|
|
70
|
+
# ---- 网络(登录节点)----
|
|
71
|
+
# 超时给足 15s 并重试:某些站点首包慢,单次探测会误判成不可达
|
|
72
|
+
net_ok=""; net_bad=""
|
|
73
|
+
for h in pypi.org pypi.tuna.tsinghua.edu.cn modelscope.cn huggingface.co github.com; do
|
|
74
|
+
reachable="no"
|
|
75
|
+
for _ in 1 2; do
|
|
76
|
+
code=$(r "timeout 15 curl -sI https://$h -o /dev/null -w '%{http_code}'")
|
|
77
|
+
case "$code" in 2*|3*) reachable="yes"; break ;; esac
|
|
78
|
+
done
|
|
79
|
+
if [ "$reachable" = "yes" ]; then
|
|
80
|
+
net_ok="$net_ok $h"
|
|
81
|
+
else
|
|
82
|
+
net_bad="$net_bad $h"
|
|
83
|
+
fi
|
|
84
|
+
done
|
|
85
|
+
|
|
86
|
+
# ---- 登录节点能否 import torch ----
|
|
87
|
+
torch_login=$(r 'python3 -c "import torch" 2>&1 | tail -1')
|
|
88
|
+
missing_lib=$(printf '%s' "$torch_login" | sed -n 's/^ImportError: \([^:]*\.so[^:]*\).*/\1/p')
|
|
89
|
+
|
|
90
|
+
QUOTA=$(r "df -BG $HOME_REAL 2>/dev/null | awk 'NR==2{gsub(/[^0-9]/,\"\",\$2); print \$2}'")
|
|
91
|
+
|
|
92
|
+
echo "==> 完成,输出 profile" >&2
|
|
93
|
+
|
|
94
|
+
cat <<EOF
|
|
95
|
+
# ${NAME} —— 自动探测于 $(date +%Y-%m-%d)
|
|
96
|
+
# 探测源: ssh ${TARGET} (登录节点 ${LOGIN_HOST})
|
|
97
|
+
# 留空的项表示未能自动探测,请手工确认后填写。
|
|
98
|
+
|
|
99
|
+
# ---- 连接 ----
|
|
100
|
+
CLUSTER_ID="${NAME}"
|
|
101
|
+
CLUSTER_DESC=""
|
|
102
|
+
SSH_HOST="$(ssh -G "$TARGET" 2>/dev/null | awk '/^hostname /{print $2}')"
|
|
103
|
+
SSH_PORT="$(ssh -G "$TARGET" 2>/dev/null | awk '/^port /{print $2}')"
|
|
104
|
+
HOME_BASE="${HOME_BASE_V}"
|
|
105
|
+
|
|
106
|
+
KEY_NAME_MARKER=""
|
|
107
|
+
NEEDS_UPDATE_HOSTKEYS_NO="no"
|
|
108
|
+
|
|
109
|
+
# ---- 调度器 ----
|
|
110
|
+
SCHEDULER="${SCHED}"
|
|
111
|
+
PARTITION="${PART}"
|
|
112
|
+
DEF_MEM_PER_CPU="${DEFMEM}"
|
|
113
|
+
MIN_GRES="${MINGRES}"
|
|
114
|
+
GRES_TYPE="${GRESTYPE}"
|
|
115
|
+
MAX_WALLTIME="${MAXWALL}"
|
|
116
|
+
|
|
117
|
+
# ---- 硬件 ----
|
|
118
|
+
ACCEL_NAME=""
|
|
119
|
+
ACCEL_ARCH=""
|
|
120
|
+
ACCEL_PER_NODE="${ACCEL_N:-}"
|
|
121
|
+
ACCEL_MEM_GB=""
|
|
122
|
+
CPUS_PER_NODE="${CPUS_N}"
|
|
123
|
+
CPU_MODEL="${CPU_MODEL_V}"
|
|
124
|
+
NODE_MEM_GB="${NODEMEM}"
|
|
125
|
+
NODE_COUNT="${NODECNT}"
|
|
126
|
+
LOGIN_NODES="${LOGIN_HOST}"
|
|
127
|
+
HOME_QUOTA_GB="${QUOTA}"
|
|
128
|
+
|
|
129
|
+
# ---- 软件栈 ----
|
|
130
|
+
TOOLCHAIN=""
|
|
131
|
+
TOOLCHAIN_VERSION=""
|
|
132
|
+
TOOLCHAIN_PATH=""
|
|
133
|
+
MODULE_LOADS=""
|
|
134
|
+
DEFAULT_VENV=""
|
|
135
|
+
|
|
136
|
+
# ---- 网络 ----
|
|
137
|
+
NET_OK="$(echo "$net_ok" | sed 's/^ //')"
|
|
138
|
+
NET_BLOCKED="$(echo "$net_bad" | sed 's/^ //')"
|
|
139
|
+
PIP_INDEX="https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
140
|
+
COMPUTE_NODE_OFFLINE="yes"
|
|
141
|
+
TMP_SHARED="no"
|
|
142
|
+
|
|
143
|
+
# ---- 已知问题 ----
|
|
144
|
+
LOGIN_NODE_MISSING_LIBS="${missing_lib}"
|
|
145
|
+
KNOWN_LIMITATIONS=""
|
|
146
|
+
|
|
147
|
+
# 探测时登录节点 import torch 的结果(供参考):
|
|
148
|
+
# ${torch_login:-(无输出,可能已可用)}
|
|
149
|
+
#
|
|
150
|
+
# 还需手工确认的:
|
|
151
|
+
# - ACCEL_NAME / ACCEL_ARCH:在计算节点跑
|
|
152
|
+
# srun -p ${PART} ${MINGRES:+--gres=$MINGRES} --time=00:05:00 rocminfo | grep gfx
|
|
153
|
+
# 或 nvidia-smi --query-gpu=name --format=csv
|
|
154
|
+
# - MODULE_LOADS:module avail 里挑编译器和加速器 SDK
|
|
155
|
+
# - KNOWN_LIMITATIONS:跑一次能力探针(见 references/troubleshooting.md)
|
|
156
|
+
EOF
|