qsub 반복문의 문제
계산 디렉터리가 여러 개 있으면 가장 먼저 떠오르는 방법은 반복문으로 하나씩 제출하는 것이다.
for d in */; do
(cd "$d" && qsub ../run.pbs)
done
열 개 정도면 문제없다. 수백 개가 되면 작업 번호가 수백 개 생겨 관리가 어렵고, 스케줄러에 부담을 주며, 많은 클러스터가 사용자당 대기 작업 수에 상한을 둔다. 작업 배열은 같은 스크립트를 번호만 바꿔 여러 번 실행하는 작업 하나다. 제출은 한 번, 작업 번호도 하나이고 각 실행(서브작업)은 자기 번호를 환경 변수로 받는다.
PBS Pro 작업 배열
#PBS -J 1-6을 넣으면 서브작업 여섯 개가 생기고, 각각 $PBS_ARRAY_INDEX에 1부터 6까지를 받는다.
자원 요청(select, walltime)은 서브작업 하나 기준이다.
#!/bin/bash
#PBS -N dft-grid
#PBS -l select=1:ncpus=4:mpiprocs=4:mem=8gb
#PBS -l walltime=04:00:00
#PBS -j oe
#PBS -J 1-6
cd "$PBS_O_WORKDIR" || exit 1
module load openmpi/4.1.6
ORCA="$HOME/opt/orca_6_1_0/orca"
NAME=$(sed -n "${PBS_ARRAY_INDEX}p" list.txt)
cd "$NAME" || exit 1
"$ORCA" "$NAME.inp" > "$NAME.out" 2>&1
작업 번호는 1234[].서버처럼 대괄호가 붙는다. qstat -u $USER는 배열 전체를 한 줄로 보여 주고,
서브작업 하나하나를 보려면 qstat -t를 쓴다. 서브작업 로그는 따로따로 생긴다.
qstat -t -u $USER # 서브작업까지 표시
qdel "1234[]" # 배열 전체 취소
qdel "1234[3]" # 3번 서브작업만 취소
대괄호는 셸의 glob 문자라 따옴표로 감싼다. PBS Pro는 범위에 서브작업이 두 개 이상이어야 하므로 -J 1-1은 거부된다.
-J 1-20:2처럼 간격을 줄 수도 있다.
Slurm 작업 배열
Slurm은 --array=1-20이고 번호는 $SLURM_ARRAY_TASK_ID에 들어온다. %5를 붙이면
동시에 도는 서브작업을 다섯 개로 제한한다. 공용 클러스터에서 큰 배열을 올릴 때는 이 제한을 거는 편이 예의다.
로그 이름의 %A는 배열 작업 번호, %a는 서브작업 번호다.
#!/bin/bash
#SBATCH --job-name=dft-grid
#SBATCH --nodes=1
#SBATCH --ntasks=4
#SBATCH --mem=8G
#SBATCH --time=04:00:00
#SBATCH --array=1-6%3
#SBATCH --output=dft-grid.%A_%a.log
module load openmpi/4.1.6
ORCA="$HOME/opt/orca_6_1_0/orca"
mapfile -t names < list.txt
NAME=${names[$((SLURM_ARRAY_TASK_ID - 1))]}
cd "$NAME" || exit 1
"$ORCA" "$NAME.inp" > "$NAME.out" 2>&1
squeue에서는 1234_[4-6%3]처럼 대기 중인 범위와 1234_1처럼 실행 중인 서브작업이 따로 보인다.
scancel 1234는 전체, scancel 1234_5는 하나만 취소한다. PBS Pro에 이런 동시 실행 제한이 있는지는
버전과 사이트 설정에 따라 다르니 클러스터 문서를 확인한다.
번호를 입력으로 바꾸기
서브작업이 받는 것은 숫자뿐이다. 숫자를 계산 이름으로 바꾸는 가장 단순한 방법은 이름을 한 줄에 하나씩 적은 목록 파일이다. 위 두 스크립트가 쓴 방법은 두 가지다.
$ cat list.txt
B3LYP_def2-SVP
B3LYP_def2-TZVP
PBE0_def2-SVP
PBE0_def2-TZVP
TPSS_def2-SVP
TPSS_def2-TZVP
$ IDX=4; sed -n "${IDX}p" list.txt
PBE0_def2-TZVP
$ mapfile -t names < list.txt; echo "${#names[@]} jobs, #4 is ${names[3]}"
6 jobs, #4 is PBE0_def2-TZVP
sed -n "4p"는 넷째 줄만 출력한다. mapfile -t는 파일을 줄 단위로 bash 배열에 읽는데,
배열은 0부터 세므로 1을 빼야 한다. 배열 크기는 목록 줄 수와 맞춘다. 제출할 때 -J나 --array를 명령줄로 주면
스크립트를 고치지 않아도 된다. 명령줄 옵션이 스크립트 안의 지시어보다 우선한다.
N=$(wc -l < list.txt)
qsub -J 1-"$N" array.pbs # PBS Pro
sbatch --array=1-"$N"%5 array.sh # Slurm
파라미터 스윕 입력 만들기
범함수 세 개와 기저 두 개의 모든 조합으로 물 분자 최적화 입력을 만든다. 조합마다 디렉터리를 하나씩 만들고
목록 파일도 같이 쓴다. 입력 파일은 here-doc(<<EOF … EOF)으로 쓴다.
#!/bin/bash
# make_inputs.sh: one directory and ORCA input per functional/basis pair
set -euo pipefail
functionals=(B3LYP PBE0 TPSS)
basis_sets=(def2-SVP def2-TZVP)
coords=$(tail -n +3 water.xyz)
: > list.txt
for f in "${functionals[@]}"; do
for b in "${basis_sets[@]}"; do
name="${f}_${b}"
mkdir -p "$name"
cat > "$name/$name.inp" <<EOF
! $f D4 $b Opt
%pal nprocs 4 end
%maxcore 1500
* xyz 0 1
$coords
*
EOF
echo "$name" >> list.txt
done
done
$ bash make_inputs.sh
$ ls
B3LYP_def2-SVP
B3LYP_def2-TZVP
PBE0_def2-SVP
PBE0_def2-TZVP
TPSS_def2-SVP
TPSS_def2-TZVP
list.txt
make_inputs.sh
water.xyz
$ cat PBE0_def2-TZVP/PBE0_def2-TZVP.inp
! PBE0 D4 def2-TZVP Opt
%pal nprocs 4 end
%maxcore 1500
* xyz 0 1
O 0.000000 0.000000 0.119262
H 0.000000 0.763239 -0.477047
H 0.000000 -0.763239 -0.477047
*
: > list.txt는 목록 파일을 빈 파일로 만든다. 다시 실행해도 이름이 중복해서 쌓이지 않는다.
좌표는 xyz 파일에서 읽어 넣었으므로 입력 파일이 다른 파일을 참조하지 않는다. scratch로 입력만 복사해 돌려도 문제가 없다.
LAMMPS 온도 시리즈
LAMMPS 입력은 ${T} 같은 자기 변수를 쓰는데, bash도 같은 문법으로 변수를 치환한다. here-doc의 끝 표시를
'EOF'처럼 따옴표로 감싸면 bash가 본문을 건드리지 않는다.
cat > in.nvt <<'EOF'
units lj
atom_style atomic
lattice fcc 0.8442
region box block 0 10 0 10 0 10
create_box 1 box
create_atoms 1 box
mass 1 1.0
velocity all create ${T} 87287 loop geom
pair_style lj/cut 2.5
pair_coeff 1 1 1.0 1.0 2.5
fix 1 all nvt temp ${T} ${T} 0.5
thermo 100
run 5000
EOF
따옴표가 없으면 bash가 ${T}를 자기 변수로 바꿔 버린다. T가 정해져 있지 않으면 빈 문자열이 된다.
fix 1 all nvt temp 0.5
온도 값은 LAMMPS의 -var 옵션으로 넘긴다. 입력 파일은 하나만 두고 서브작업마다 디렉터리와 온도만 바꾼다.
# temps.txt: 0.8, 1.0, 1.2, 1.4 한 줄에 하나
T=$(sed -n "${SLURM_ARRAY_TASK_ID}p" temps.txt)
mkdir -p "T$T" && cd "T$T" || exit 1
lmp -var T "$T" -in ../in.nvt -log log.lammps
결과 모으기
계산마다 디렉터리가 하나씩이면 결과를 모으는 스크립트도 단순해진다. 디렉터리를 돌며 정상 종료를 확인하고 에너지를 CSV로 모으는 방법은 06장에 있다.
실패한 것만 다시 제출
배열 중 몇 개가 walltime이나 수렴 문제로 끝나지 못했다면, 목록 파일에서 실패한 이름만 골라 새 목록을 만들고 그것으로 다시 제출한다. 출력 파일이 아예 없는 경우(시작도 못 한 서브작업)도 실패로 센다.
#!/bin/bash
# find_failed.sh: names from list.txt whose ORCA output is missing or unfinished
while read -r name; do
out="$name/$name.out"
if [[ ! -f $out ]] || ! grep -q 'ORCA TERMINATED NORMALLY' "$out"; then
echo "$name"
fi
done < list.txt > failed.txt
$ bash find_failed.sh
$ cat failed.txt
PBE0_def2-SVP
TPSS_def2-SVP
$ grep -L "ORCA TERMINATED NORMALLY" */*.out
PBE0_def2-SVP/PBE0_def2-SVP.out
이 예에서 PBE0_def2-SVP는 출력이 중간에 끊겼고 TPSS_def2-SVP는 출력이 없다.
grep -L(맞는 줄이 없는 파일)은 앞의 것만 찾는다. 파일이 없는 경우는 grep이 볼 수 없기 때문이다.
다시 제출하려면 배열 스크립트가 목록 파일 이름을 변수로 받게 고친다. LIST가 없으면 list.txt를 쓴다.
LIST=${LIST:-list.txt}
IDX=${PBS_ARRAY_INDEX:-1}
NAME=$(sed -n "${IDX}p" "$LIST")
[[ -n $NAME ]] || { echo "no line $IDX in $LIST"; exit 1; }
N=$(wc -l < failed.txt)
qsub -J 1-"$N" -v LIST=failed.txt array.pbs # PBS Pro, N >= 2
sbatch --array=1-"$N" --export=ALL,LIST=failed.txt array.sh # Slurm
실패한 것이 하나뿐이면 PBS Pro에서는 -J 없이 qsub -v LIST=failed.txt array.pbs로 일반 작업으로 제출한다.
PBS_ARRAY_INDEX가 없으니 IDX가 1이 된다. 이때 스크립트 안에 #PBS -J 줄이 남아 있으면 다시 배열이 되므로,
배열 범위는 스크립트에 적지 않고 늘 명령줄로 주는 편이 편하다.
다시 돌리기 전에 실패 원인을 먼저 본다. walltime이 모자랐다면 늘리고, SCF가 수렴하지 않았다면 입력을 고쳐야 같은 실패를 되풀이하지 않는다.
작은 계산 여러 개를 작업 하나에서
xtb 단일점처럼 몇 초면 끝나는 계산은 하나마다 작업을 만들면 대기 시간이 계산 시간보다 길다. 코어 여러 개를 받은 작업 하나 안에서
xargs -P나 GNU parallel로 동시에 돌린다. 먼저 xyz 파일 하나를 처리하는 스크립트를 만든다.
#!/bin/bash
# run_one.sh: xtb single point for one xyz file, in its own directory
set -euo pipefail
xyz=$(realpath "$1")
name=$(basename "$xyz" .xyz)
mkdir -p "sp/$name"
cd "sp/$name"
xtb "$xyz" > sp.out 2>&1
awk -v n="$name" '/TOTAL ENERGY/ {print n, $4}' sp.out
$ export OMP_NUM_THREADS=1
$ printf '%s\n' mols/*.xyz | xargs -P 4 -n 1 ./run_one.sh
ammonia -4.426093057513
methanol -8.225938944071
methane -4.175074573917
water -5.070222286727
$ parallel -j 4 --keep-order ./run_one.sh ::: mols/*.xyz
ammonia -4.426093057513
methane -4.175074573917
methanol -8.225938944071
water -5.070222286727
xargs -P 4 -n 1은 입력 한 줄씩 받아 최대 네 개를 동시에 실행한다. 출력은 끝난 순서대로 나오므로 매번 달라질 수 있다.
GNU parallel의 --keep-order는 입력 순서대로 출력한다. 계산마다 디렉터리를 따로 쓰는 것은
xtb가 결과 파일을 고정된 이름으로 쓰기 때문이다. 같은 디렉터리에서 동시에 돌리면 서로 덮어쓴다.
동시 실행 수 × 계산당 스레드 수가 요청한 코어 수를 넘지 않게 한다. 작업 스크립트에서는 -j "$NCPUS"와
OMP_NUM_THREADS=1을 함께 쓴다. 로그인 노드에서 이런 병렬 실행을 돌리면 다른 사용자가 쓰는 노드를 막게 되니
반드시 작업 안에서 돌린다.