HTCondor 사용법

본 문서에서는 HTCondor (HT: High Throughput) 를 사용하는 방법에 대해 다룹니다. HTCondor 가 기구축된 클러스터를 활용하는 사용자 측면에서 다룹니다. HTCondor 는 한개 또는 한개 이상의 컴퓨터로 구성된 클러스터의 작업을 관리하는 프로그램으로서 CERN lxplus, NERSC 등 초대형 클러스터부터 kCMS, koALICE 나 각 연구실의 클러스터에서도 광범위하게 활용되고 있습니다.

다음과 같은 사전 지식이 필요합니다.

  • 컴퓨터 프로그램을 컴파일하여 실행파일을 만들수 있거나, ROOT 를 활용하여 매크로를 실행할 수 있을 것
  • 파일 입출력 (txt, root 파일 무관)
  • 파일 실행 (Unix or Unix-like system 에서 bash 등 shell 을 통한)
  • Shell script 를 실행할 수 있을 것
  • mv, cp
  • 상대 경로, 절대 경로

HTCondor 를 활용하여 프로그램을 병렬작동하기 위해서는 다음 과정을 따릅니다.

  1. 프로그램이 여러개의 작업으로 쪼개어져 작동할 수 있도록 병렬화 합니다.
  2. 프로그램이 각 노드에서 작업될 수 있도록 작업 환경 구축 과정을 구상하고, 쉘 스크립트를 작성합니다.
  3. 필요한 폴더를 생성하고, 이동할 파일과 출력될 파일위치를 파악합니다.
  4. 필요한 파일을 이동하고 쉘 스크립트가 각 작업별로 작동한 후 결과 파일을 가져올 수 있도록 condor_submit 의 submit file 을 작성합니다.

이 과정은 대부분의 작업에 대해 명시적이며 거의 변동되지 않습니다. (병렬컴퓨팅에서는 거의 비슷합니다)

프로그램 병렬화

HTCondor 을 활용하여 작업을 할 구상을 한다면, 가장 먼저 해야할 것은 프로그램을 병렬작업에 맞게 튜닝하는 것입니다. 프로그램을 병렬화하는 것에는 크게 두가지로 나눌 수 있습니다.

이 과정은 아주 간단하게 말해서 (1) 모든 작업에 대해 입력과 출력이 구분될 수 있도록 하고 (2) 그에 맞는 입출력을 할 수 있도록 Program Argument 를 프로그램에 부여하는 것 입니다. 만약 무작위연산을 기반으로 한 연산이 있다면 (몬테카를로연산 등) 랜덤 시드를 외부에서 argument 로 부여할 수 있도록 세팅해야합니다. (시간시드 금지, 중요)

입출력 구분

프로그램을 병렬화할 때에는 어떤 입력에 대해 어떤 출력을 받아내야하는지를 확실히 해야합니다. 프로그램을 일종의 입출력이 있는 함수 로 생각하면 편합니다.

출력값 분리

입력값보다 중요한 것은 출력값을 분리하는 것입니다. HTCondor 를 통한 작업이 종료된 후 출력 파일이 이동될 때 UI 머신의 한 폴더로 모이게 되는데, 만약 출력되는 파일 명이 겹칠 경우 대개 덮어쓰기 됩니다. 그래서, 출력되는 파일 명이 모두 다를 수 있도록 출력될 프로그램의 argument 로 출력될 파일명 또는 파일 번호와 관련된 정보를 받아야 합니다.

출력 파일 구분을 할 수 없는 프로그램의 경우

일반적인 상황은 아닙니다만, 프로그램의 상태에 따라 출력 파일명을 지정할 수 없는 경우가 있습니다. 어쨌든 간에 출력 파일명은 구분되어야 합니다. 이 경우에는, 하나의 실행파일로 구동하는 것이 안되므로 (이후 mv 등으로 파일명을 바꾸어야 하므로) "작업 과정 구상" 을 거쳐야만 합니다. 해당 과정을 참고하세요.

랜덤 시드 부여

몬테카를로 시뮬레이션과 같이 무작위 연산이 포함되어있을 경우 랜덤 시드를 job 에 따라 별도 부여해야합니다. 랜덤 시드가 겹치는 작업이 있을 경우, 제대로된 시뮬레이션이 되지 않을 수 있습니다. 랜덤 시드를 부여하는 방법은 어느 라이브러리의 어느 생성기를 쓰느냐에 따라 다릅니다만, C++ 의 <cstdlib> 를 활용하는 경우 srand, ROOT 의 TRandom을 쓸 경우 TRandom::SetSeed 를 활용하면 됩니다.

일반적인 프로그램에서 랜덤 시드를 무작위 지정할 때, 프로그램이 구동되는 시간과 관련된 시드를 쓰는 경우가 많습니다만, HTCondor 병렬 연산에서는 그래서는 안됩니다.[1] HTCondor 를 통해 생성한 작업은 언제 작동할 지 알 수 없으며, 그에 따라 랜덤 시드가 작업마다 겹칠 가능성이 있습니다. 그렇기 때문에, 랜덤 시드는 외부에서 겹치지 않도록 부여하는 것이 맞습니다. 프로그램의 argument 를 통해 넣도록 세팅하면 됩니다. 어떻게 개개 프로그램마다 다른 시드값을 넣을 수 있는지는 condor_submit 에서 다룹니다.

입력값 부여

위 사항을 고려하여, 프로그램에 필요한 입력값을 명확히 합니다. 이후, Argument 를 부여하는 과정으로 이동합니다. 보통, 다음과 같은 사항이 입력값으로 생각됩니다.

  • 프로그램에 입력되어야 하는 숫자나 문자
    • 랜덤 시드 포함
  • 입출력 파일과 관련된 문자나 숫자
    • 입출력 파일명 등

Argument 부여

위의 입출력 조정 작업을 거친 후, 각 프로그램에 Argument 를 넣을 수 있도록 작업합니다. 대부분의 executable 을 만들 수 있는 프로그램은 argument 를 입력할 수 있도록 방법을 지원합니다. 본 문서에서는 C/C++, python 의 경우만 다룹니다.

C/C++

int main(int argc, char** argv) {  }

-c, -v, --version 등 위치 기반이 아닌 문자옵션 기반 구성은 getopt, getopt_long, getopt_long_only 검색하여 활용 (한국어 자료 많음, #include <unistd.h>)

python

import sys
# argc can be called via sys.argc 
# argv can be called via sys.argv

python 도 getopt 활용 가능 (import getopt, 활용방법은 별도 검색)

예시

C++ 예시 코드는 다음과 같습니다. (https://github.com/Isaac-Kwon/apv/) getopt_long 을 활용하였습니다. 본 코드에는 입력 및 출력파일, 입력 숫자가 적용되어 있습니다. 실제 작업인 Analysis 함수는 첨부되지 않았습니다.

#include "iostream"
#include "string"
#include "TTree.h"

#include "stdlib.h"
#include "getopt.h"

struct Arguments{
    std::string ifilename = "";
    std::string ofilename = "";
    int   startn    = 0;
    int   endn      = -1;
    short threshold = 0;

    bool        b_ifilename = false;
    bool        b_ofilename = false;
    bool        b_startn    = false;
    bool        b_endn      = false;
    bool        b_threshold = false;

    Arguments(): ifilename(""),ofilename(""),startn(0), endn(-1), threshold(100),
                 b_ifilename(false), b_ofilename(false), b_startn(false), b_endn(false), b_threshold(false){;}

};

int main(int argc, char** argv){
  char c; // option
  Arguments args;

  static struct option long_options[] =
    {
      {"in"         , required_argument, 0, 'i'},
      {"out"        , required_argument, 0, 'o'},
      {"start"      , required_argument, 0, 's'},
      {"end"        , required_argument, 0, 'e'},
      {"threshold"  , required_argument, 0, 't'},
      {"help"       , no_argument,       0, 'h'},
      {0, 0, 0, 0}
    };

    int option_index = 0;
    while( (c = getopt_long(argc, argv, "i:o:s:e:t:h", long_options, &option_index)) != -1){
        switch(c){
            case 'i':
                args.ifilename  = std::string(optarg);
                args.b_ifilename = true;
                break;
            case 'o':
                args.ofilename   = std::string(optarg);
                args.b_ofilename = true;
                break;
            case 's':
                args.startn   = std::stoi(optarg);
                args.b_startn = true;
                break;
            case 'e':
                args.endn   = std::stoi(optarg);
                args.b_endn = true;
                break;
            case 't':
                args.threshold   = std::stoi(optarg);
                args.b_threshold = true;
                break;
            case 'h':
            std::cout<<"Single Run with Stert/End Entry"<<std::endl;
            std::cout<<"Available Options" <<std::endl;
            std::cout<<"-i, --in   \t input file(txt)    \t [REQUIRED]" <<std::endl;
            std::cout<<"-o, --out  \t output file(root)  \t [REQUIRED]" <<std::endl;
            std::cout<<"-s, --start\t start entry number \t [default=0]" <<std::endl;
            std::cout<<"-s, --end  \t end entry number   \t [default=all]" <<std::endl;
            std::cout<<"-t, --threshold\t threshold      \t [default=100]" <<std::endl;
            std::cout<<"-h, --help     \t this message" <<std::endl;
                return 0;
            case '?':
                printf("Unknown flag : %c", optopt);
                return 1;
        }
    }

    if (!(args.b_ifilename && args.b_ofilename)){
        std::cout<<"Input file name and output file name are necessary!"<<std::endl;
        return 1;
    }

    
    return Analysis(args.ifilename, args.ofilename, args.startn, args.endn, args.threshold);;
}

작업 과정 구상

각 노드의 운영체제와 접근할 수 있는 스토리지(라이브러리)에 따라 작업 과정 구상이 다릅니다. 작업 과정을 구상하지 않고 바로 condor_submit 을 하는 과정으로 넘어갈 수도 있습니다. (작업 과정에 대한 구상이 필요없는 경우가 더 많습니다.)

본 과정은 하나의 실행파일로 작업을 정의할 수 없는 경우, (여러 과정의 실행이 필요한 경우) 그를 하나의 실행 파일을 실행하는 것으로 묶는 과정으로, 보통 Shell Script 하나를 만듦으로써 끝나게 됩니다. 마지막의 "하나의 실행파일" 은 python 스크립트가 될 수도 있습니다만, 본 과정에서는 Shell Script 로 다룹니다.

작업과정 구상을 하는 경우

작업 과정을 구상하는 경우는 다음과 같습니다.

  • 작업환경 구성을 해야하는 경우
  • 한개의 실행파일(executable)로 작업이 끝나지 않는 경우
  • 프로그램이 자주 액세스하는 파일을 먼저 노드에 복사하는 경우

작업 환경 구성을 해야만 하는 경우

다음과 같은 경우 개별 노드에서 작업환경을 구성해야하며, 보통 새로 프로그램을 빌드해야만 합니다. 보통 잘 구성된 클러스터는, 이럴 일이 잘 없습니다.

  • UI 기기(condor_submit을 하는 클러스터의 머신, 일반적으로 1차접속단)의 OS 가 개별 노드와 다른 경우
  • 프로그램에서 호출하는 라이브러리의 경로를 각 노드에서 호출할 수 없는 경우 (동일한 스토리지 시스템이 마운트되지 않은 경우)

위 상황 이외에도 작업환경을 노드별로 구축해야하는 경우가 발생할 수 있으나, 이는 이를 아는 것은 매우 경험적인 부분입니다. (비명시적일 경우가 더 많습니다)

한개의 실행파일로 작업이 끝나지 않는 경우

여러 실행파일의 연속적 실행으로 작업을 해야하는 경우가 있습니다. 이 경우 또한 작업과정 구상을 따로 해야합니다.

파일을 노드에 복사하는 경우

일반적으로, 공통마운트 스토리지가 있어서 이에 액세스하면 되지만 네트워크 스토리지는 인트라넷 속도를 지속적으로 소모하여 전체 노드의 속도를 줄일 수 있습니다. 최초 1회 노드로 복사하여 내부에서 접근함으로서 이를 방지할 수 있습니다.

Argument Pass-Through

#!/bin/bash

출력 파일 구분

구동하는 파일이나, 입력 파일을 노드에 복사한 경우 그 파일을 삭제하여야만 합니다. (그렇지 않으면 output file 로서 UI 머신에 카피됩니다.) 또는, 출력 파일명이 개별 작업별로 구별되지 않는 경우 그 파일명을 구별하기 위한 이름바꾸기를 해야합니다. 파일 삭제가 필요한 경우 rm -rf 를 쓰면 되고, 파일명 변경이 필요한 경우 mv 를 하면 됩니다. 다만, 클러스터 노드의 폴더 측면에서 생각해야만 합니다. 상대 경로로 작업하는 것으로 생각하면 됩니다.

condor_submit

HTCondor 을 통한 작업을 진행하기 위해서는 condor_submit 를 통해 필요 사항을 전달해야합니다. 일반적으로는, condor_submit 에 맞는 submit description file 을 만들어 진행합니다. htcondor python api 를 통해 submit 하는 경우도 있습니다만, 일반적이지는 않습니다.

사전 작업

  • 출력 폴더 생성 및 위치 파악
    • Log (Condor 작업 구동 로그), Output(Condor 작업 stdout 스트림), Error(Condor 작업 stderr 스트림)
    • output_destination (출력된 파일이 어디로 갈 지 경로)
  • 구동할 실행파일 파악 (Executable)

병렬화에 따른 queue 와 argument 구성

숫자로 병렬화한 경우

이름으로 병렬화한 경우

ROOT 병렬화

이외에 필요할 수 있는 설정

getenv

stream_output

stream_error

accounting_group

예시

작업 시행

주로 사용하는 작업 시행 중의 condor 명령어
역할 사용 예시
condor_q 작업 진행 현황 파악 (작업이 진행중이거나, 진행예정인 것) $ condor_q

$ condor_q -all

$ condor_q username

condor_history 작업 진행 사항 파악 (작업이 완료되었거나, 취소, 제거된 것) $ condor_history -limit 100 username
condor_hold 특정 작업의 진행을 정지하고 대기 (hold) $ condor_hold username

$ condor_hold 12112

$ condor_hold 12112.12

condor_release 대기중(hold)인 특정 작업을 재시작 $ condor_hold username

$ condor_hold 12112

$ condor_hold 12112.12

condor_rm 특정 작업을 대기열(condor_q)에서 제거 $ condor_hold username

$ condor_hold 12112

$ condor_hold 12112.12

출력 파일 병합

자동화

HTCondor 에 필요한 여러 작업을 한번에 할 수 있도록, 다음과 같이 하나의 shell script 로 자동화할 수 있습니다.

#! /bin/bash

# USER DEFINED AREA
WORKDIR=$(date +%y%m%d%H%M%S)
ExeDir="/absolute/path/to/directory/executable/contained/"
Executable="myexe.exe"
NJOBQUUE=2000

#########################################
# Make Directories

mkdir -p $WORKDIR/log $WORKDIR/err $WORKDIR/out $WORKDIR/output

#########################################
# Make Merge Script
cat <<EOF > $WORKDIR/merge
#!/bin/bash
echo "MERGING FILES IN " $(pwd)/$WORKDIR/output/*.root
hadd merge.root $(pwd)/$WORKDIR/output/*.root
EOF
chmod 755 $WORKDIR/merge
chmod +x  $WORKDIR/merge

#########################################
# make condor.script

cat <<EOF > condor.script
Universe = vanilla
Executable = ${ExeDir}${Executable}

Log = ${WORKDIR}/log/\$(Cluster)_\$(ProcId).log
Output = ${WORKDIR}/out/\$(Cluster)_\$(ProcId).log
Error = ${WORKDIR}/err/\$(Cluster)_\$(ProcId).log

should_transfer_files = YES
when_to_transfer_output = ON_EXIT_OR_EVICT

JobSuccessExitCode = 0
on_exit_remove     = (ExitBySignal == False) && (ExitCode == 0)
max_retries        = 32
requirements       = Machine =!= LastRemoteHost

# Passing ProcID as argument -s for random seeding, -o for naming individial output file.
arguments= -s \$(ProcID) -o ./result_\$(ProcId).root
output_destination=file://${PWD}/${WORKDIR}/output/
getenv = true

# Watch out to use stream; It needs I/O throughput
# stream_output = true #default false
# stream_error  = true #default false

Queue $NJOBQUUE
EOF

#########################################
# Queue randseed from $INPUT_FILE
cp condor.script ${WORKDIR}/condor.script
condor_submit condor.script

주의 사항

예시

참고: https://gist.github.com/Isaac-Kwon/c062ba4abee8bebf2f64f95ea52e2068

  1. 테스트 목적의 프로그램에서도, 시드는 일반적으로 바꾸지 않습니다. 여러 조건에 대해 프로그램을 테스트할 때 특정 조건에서 일어나는 버그의 경우 시드가 변동되지 않아야 버그 및 디버깅 조건을 파악할 수 있습니다.