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.
@@ -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