Computer

VHTS와 병렬 컴퓨팅: 1. 기초 개념, file split

Novelism 2022. 2. 27. 15:39

저는 컴퓨터 전공자는 아니라 체계적으로 개념을 배운 것은 아니고, 연구에 필요해서 실용적으로 배우다 보니 용어에 오개념이 있을 수도 있음을 양해해주시기를 부탁드립니다. 작업 환경은 linux입니다.

 

신약개발에서 가상탐색 virture high throuput screening (VHTS)이 사용된 것은 아마도 CADD의 역사와 비슷할 것이지만, 최근에는 ultra large scale이라 불릴 정도의 규모... 수천만, 수억, 수십억 이상의 분자를 가상 탐색하는 것이 트렌드입니다.

여기에 필요한 기술 중 하나가 병렬 컴퓨팅입니다.

 

 병렬 컴퓨팅의 필요성은 다음과 같습니다.

 기본적으로 하나의 프로세스는(소프트웨어) 하나의 프로세서를(하드웨어) 사용합니다.

 하지만 하나의 프로세서로 낼 수 있는 성능에는 한계가 있습니다. 만약 하나의 작업을 두 개 이상의 프로세서를 사용할 수 있다면, 계산 속도가 더 빨라질 수 있습니다. 하지만 그냥 하나의 프로세스가 여러 개의 프로세서를 사용하도록 하는 것은 생각보다 쉽지 않습니다. 병렬화를 고려하지 않고 작성한 프로그램을 병렬로 돌릴 경우는 똑같은 계산을 여러 CPU(혹은 코어, 프로세서)가 동일하게 수행하는 경우도 있습니다.

 병렬화는, 동일한 루틴(혹은 함수)으로 서로 다른 파라미터(혹은 변수, 조건)에 대한 계산을 해야 할 경우에 의미가 있습니다.

 

예를 들어 1,000,000 개의 분자 SMILES 데이터가 담긴 파일이 있고, 각 분자 데이터로부터 분자의 특성(분자량, logP 등)을 계산하여 출력하는 코드를 만든다고 생각해봅시다.

 각 계산은 서로 독립되어 있고, 순차적일 필요도 없습니다. 컴퓨터에 사용할 수 있는 core가 10개 있다고 가정합시다.

이것이 무슨 병렬 컴퓨팅이냐라고 말할 정도로 가장 쉬운 방법 중 하나는 (다만 손이 많이 갈 수도 있는) 파일을 10개로 쪼개서 10개를 별개로 실행해주는 것입니다. 리눅스에선 split이라는 명령어가 있어서 간단하게 할 수 있습니다.

 연습:

seq 명령어를 이용해서1부터 10000까지 적힌 파일을 만들어봅시다.

$ seq 1 10000 > seq.txt

split이라는 명령어를 사용하기 전에 도움말을 봅시다.

$ split --help

Usage: split [OPTION]... [FILE [PREFIX]]
Output pieces of FILE to PREFIXaa, PREFIXab, ...;
default size is 1000 lines, and default PREFIX is 'x'.

  -d                      use numeric suffixes starting at 0, not alphabetic
      --numeric-suffixes[=FROM]  same as -d, but allow setting the start value
  -l, --lines=NUMBER      put NUMBER lines/records per output file

다양한 옵션이 있지만, 저는 -d와 -l 두 옵션을 사용하였습니다. -d 옵션은 subfix에 숫자를 사용하는 옵션이고, -l은 파일당 몇 줄을 넣을 것인지 선택하는 옵션입니다. split 다음에 옵션을 적어주고, 파일 이름과 prefix를 적어줍니다. 저는 prefix에 list_를 적어주었습니다. -d 옵션이 없을 경우는 00 01 02 대신 aa ab ac 등이 subfix로 붙습니다.

$ split -d -l 1000 seq.txt list_
$ ls 
list_00  list_01  list_02  list_03  list_04  list_05  list_06  list_07  list_08  list_09  seq.txt

이런 식으로 데이터 리스트를 나눈 후에, 각 파일을 별개로 실행만 해주면 계산을 병렬화할 수 있습니다.

실행해야 할 파일이 a.py이고, 리스트 파일을 파라미터로 받는다면

 bash를 사용해서, 다음과 같은 코드를 사용할 수도 있습니다.

for idx in `seq -w 00 09`
do
    a.py list_$idx &
done

결과 파일을 합치고 싶다면,

cat file1 file2 file3 file4  ... > output_file

로 합칠 수 있습니다.

생각할 수 있는 가장 간단한 형태의 병렬 프로그래밍입니다.

여기서 제가 a.py를 백그라운드로 (&) 로 작업했습니다.

 이것은 여러 subprocess를 생성하는 방법입니다. for 문만큼 자식 프로세서가 생겨납니다.

 위에서 말씀드린 것처럼 기본적으로는 하나의 프로세스가 하나의 프로세서를 사용합니다.

 정확한 개념은 아니지만 프로세서라는 것은 cpu 혹은 core, thread라고 생각하시면 이해하기 쉬우실 것입니다.

 여러 프로세서를 사용하고 싶다면, 사용 중인 프로세서에서 자식 프로세서를(subprocess) 여러 개 만들어서 실행하는 것이 한 가지 방법입니다.

python에서도 자식 프로세서를 만드 형태의 병렬화를 많이 사용합니다.

예시로는 multiprocessing module이 있습니다.

 MPI라고 하는 방법에서는 하나의 프로세서가 자식 프로세서를 실행하는 대신 여러 프로세서가 동시에 실행됩니다.

 둘 다 메모리를 공유하지 않는 방식의 병렬 컴퓨팅입니다.

python multiprocessing module은 한 컴퓨터 내에서만 사용 가능합니다. 왜냐하면 다른 컴퓨터에 자식 프로세서를 만들 수 없기 때문입니다. 반면에 MPI는 여러 컴퓨터 사이에서도 사용할 수 있습니다.

 

이와는 다르게  openmp처럼 여러 프로세서가 하나의 프로세스에서 동작하는 경우도 있습니다. (대신 thread라는 방법을 사용하는 듯합니다.) 이 방식의 제일 큰 장점은 여러 프로세서들이 메모리를 공유할 수 있다는 것입니다.

 MPI (massage passing interface)는 이름이 설명하는 것처럼 서로 다른 프로세스끼리 메시지를 교환해야 합니다. 하나의 메모리를 공유한다면, 이런 과정이 필요하지 않습니다. 메모리를 단점으로 메모리가 프로세스의 수만큼 필요하다는 것이 있습니다.

설계를 잘못한다면 메모리 비공유 방식의 병렬 컴퓨팅을 사용할 때, 메모리가 하드웨어 상의 메모리를 넘어버릴 수도 있으니 주의해야 합니다.

 openmp는 C, C++, fortran에서 지원됩니다.

 python에서는 openmp를 코딩할 수는 없지만, 대신 c나 fortran으로 만들어진 라이브러리들을 가져오는 경우는 python에서도 openmp를 활용하게 됩니다. 대표적인 사례가 numpy입니다.

 아마도 python에서 numpy를 사용하다 보면 CPU 사용률이 100% 를 넘어가는 것을 볼 수 있을 것입니다.

 이것은 numpy가 병렬화 되어있기 때문입니다.

numpy가 몇 개의 thread를 사용할지는 아래 명령어로 제어할 수 있습니다.

export MKL_NUM_THREADS=1
export NUMEXPR_NUM_THREADS=1
export OMP_NUM_THREADS=1

 0일 때는 모든 cpu를 사용하는 것이고, 직접 사용할 숫자를 넣어줄 수도 있습니다.

 서로 다른 병렬화 방법을 동시에 여러 개 사용하는 경우 비효율적일 수 있어서 저는 다른 병렬 툴을 사용할 때는 1을 사용합니다.

 

openmp의 장점을 잘 설명할 수 있는 예제는 matrix 곱입니다.

C = A B

A는 100*10의 matrix, B는 10*2 maxrix , C는 100*2의 matrix라고 생각해봅시다.
이것을 element notation을 사용하면

C[i][k] = sum_[j] A[i][j] B[j][k]

입니다.

c로 작성하면 다음과 같습니다.

 #pragma omp parallel shared(a,b,c) private(i,j,k)
 for(i=1;i<=100;i++)
    for(k=1;k<=2;k++)
    	c[i][k] = 0
 		for(j=1;j<=10;j++)
        	c[i][k]+=a[i][j]*b[j][k];

 

위에서 설명한 대로 메모리를 공유하면 프로세스 간의 데이터 통신을 할 필요가 없지만, 메모리를 공유하지 못하면 프로세스 간의 데이터 통신이 필요합니다. 2번 프로세스가 1번 프로세스의 값을 참고하고 싶다면, 1번 프로세스가 2번 프로세스에 특정 메모리 값을 전달해야 한다는 의미입니다. 그런데, 꼭 메모만으로 통신할 필요는 없습니다.

 인간 세상에서 일어나는 일을 생각해봅시다. 두 사람이 서로 정보를 공유하기 위해선 서로 만나서 대화를 하거나, 혹은 편지를 보내야 합니다. 편지를 보낸다면 그것을 배달하는 존재가 있어야 하고, 우편물 보관함이 있어야 합니다. 이런 개념은 MPI에서도 그대로 적용됩니다.

 그런데, 이런 (중개를 거치거나 거치지 않거나) 1:1 통신 이외에도 다른 방법도 있습니다. 공공 게시판이 있고, 제가 거기에 무언가를 적으면 다른 사람이 볼 수 있습니다. 만약 특정 수신인에게 알리고 싶다면, 그 사람에게 보라는 메시지를 추가로 적을 수도 있습니다. (보안에는 취약하겠지만) 공공게시판을 이용한 개인 간 통신도 가능합니다.

 하드디스크가 게시판이라고 생각한다면, 하드디스크를 공유하는 프로세스끼리는 하드디스크의 파일을 이용해서 정보를 전달할 수도 있습니다. 메모리는 각 프로세스가 독점적으로 사용하는 것이지만, 하드디스크는 독점적으로 사용하지 않는다는 차이가 있습니다. 클러스터 환경에서는 NFS나 lustre를 이용해서 노드 간 저장소를(스토리지, HDD 혹은 SSD) 공유합니다.

 이 방식이 때때로 MPI 보다 편리할 수도 있습니다. 특히 이종간 데이터 전송이 더 쉽다는 장점이 있습니다. 그리고, MPI는 처음부터 통신을 위한 common world로 묶여야 하는데, 이후 시점에서 이것을 변경하는 것이 어렵습니다. 만약 특정 프로세스에서 에러가 발생할 경우 전체를 중단시켜야 할 수도 있습니다.

 반면에 하드디스크 공유 방식은 해당 파일에 접속만 가능하다면, 누구든지 통신에 참여할 수 있고, 특정 프로세스에서 에러가 발생할지라도 그 프로세서만 죽이고 다시 가동할 수도 있고, 수시로 프로세스의 수를 변경할 수도 있습니다.

 

 제가 사용하는 스토리지를 이용한 병렬화 방법을 (virtual flow (https://pubmed.ncbi.nlm.nih.gov/32152607/ 를 참고해서 python으로 코드를 작성했습니다.) 인간의 작업으로 예를 들겠습니다.

갑과 을이 있다고 합시다. (보통 병렬 컴퓨팅 설명할 때 master, slave 같은 용어를 쓰긴 하는데, 이런 용어가 차별적인 표현이라고 쓰지 말자는 운동도 있습니다. 하지만 적당한 용어가 생각이 안 나서 한국에서 흔히 쓰는 갑 을이라는 용어를 사용하겠습니다.)

 

 갑은 큰 일을 하나 가져와서, 그 일을 세세하게 나누는 일을 합니다.

 그 일들을 쪼개서 게시판에다가 할 일 목록 (todo)를 작성합니다. 그리고 가끔씩 완료 목록 (done)을 체크해서 일이 전부 종료되었는지 확인합니다.

 그러면 을은 todo 목록에서 일이 있을 경우, 그 일을 가져가고, 대신 todo 목록에서 그 일을 지우고 진행 중 목록 (current)로 보냅니다. 그리고 열심히 일을 한 후 일이 종료되면 결과물을 current에서 done으로 옮깁니다. 그리고 todo에 일이 있을 경우 그 일을 다시 가져다 수행합니다. todo의 일이 전부 사라질 때까지 이 작업을 반복합니다.

 갑이 done 목록을 확인해서 일이 전부 종료되었다면 이 결과들을 취합하고 전체 과정은 종료됩니다.

 

 이런 과정에서 갑과 을은 서로 얼굴 한번 본 적 없고, 직접 메시지를 보낸 적조차 없습니다.

 을이 5명에서 출발해서 10명이 되었다가 3명이 되었어도, 전체 프로세스의 진행에는 큰 문제가 없습니다.(종료시간에 차이가 생기겠지만...)

 세세한 문제 중에, 할 일 목록에 2명이 동시에 접근해선 안된다 같은 것이 있는데, 그건 file lock이라는 것을 이용해서 해결할 수 있습니다.

 

 다음에 기회가 되면 각 내용을 좀 더 자세히 적어보도록 하겠습니다.