💡
해당 포스트에 설명된 예시 코드는 ml-docker-example에서 확인하실 수 있습니다.

Introduction

딥러닝 및 머신러닝 연구를 위한 환경을 세팅하는 것은 다소 번거로운 작업일 수 있습니다. 운영체제(Ubuntu, Windows..), 프레임워크(tensorflow, pytorch..)에 맞는 GPU환경을 구성하고, 관련 패키지들을 설치 해주어야 합니다. 사용 목적에 따라 여러가지 환경을 동시에 관리해야하는 경우가 빈번하게 생길 수 있고, 각 환경에 대한 버전 관리가 명확히 되지 않는다면 예상치 못한 에러가 발생할 수 있습니다.

Docker는 연구 환경을 하나의 이미지로써 본을 떠 놓으면, 그 이미지를 복제 하여 어떠한 PC에서도 동일한 환경에서 작업을 할 수 있도록 합니다. 이미지는 여러 가지 패키지 뿐 아니라 운영체제 까지도 포함하기 때문에, Window나 Mac PC에서도 Linux 환경을 그대로 사용할 수 있습니다. 이러한 장점 때문에, 2013년 Docker의 등장 이후로 여러 논문 저자들은 Docker를 활용 하여 자신들의 연구 내용을 재현할 수 있도록 오픈 소스를 공개 하기 시작하였습니다. 따라서 원활한 딥러닝 및 머신러닝을 연구를 위해서는 Docker의 유연함을 이해하고 활용하는 것이 이제는 필수 요건이 되었습니다.

저희 옴니어스 연구팀 역시, Docker를 활용해 여러 대의 서버에서 연구원들이 동일한 환경에서 연구를 진행하고, 버전관리를 할 수 있도록 하고 있습니다. 이번 포스트에서는 Docker에 대한 간단한 개념과 구조, 예제를 통해 연구 환경을 구성하는 방법을 소개합니다.

Contents

1.  What is docker ?

  • 이미지와 컨테이너
  • Docker 환경의 기본 구조
  • 옴니어스의 Docker 환경 구조

2.  Practice

  • Docker 설치 및 기본 환경 세팅
  • Dockerfile로 이미지 만들기
  • 이미지 실행 및 연구 환경 접근

1.   What is Docker ?

이미지와 컨테이너

Docker는 크게 이미지와 컨테이너로 구성되어 있습니다. 연구 환경에 필요한 여러 가지 파일들을 이미지로써 패키징을 해 두고, 그 속의 파일을 컨테이너를 통해 실행시킬 수 있습니다. 예를 들어 Docker 이미지내에 운영체제와 프레임 워크, 패키지들을 설치해두고, jupyter notebook이나 vscode-server와 같은 프로세스들을 컨테이너에 담에 특정 포트에 띄워 두면, Docker 환경에 접속했을 때 해당 포트로 접근하여 jupyter notebook을 사용할 수 있습니다. 이미지와 컨테이너의 특징은 다음과 같습니다.

💡 이미지

  1. Dockerfile이라는 파일로 만들어진다.
  2. 이미지는 한 번 만들어 두면, 그 내용이 변하지 않는다.
  3. 여러 개의 컨테이너를 생성할 수 있고, 컨테이너의 상태에 영향을 받지 않는다.
  4. Pull & Push를 통해 버전 관리 및 배포가 가능하다.

💡 컨테이너

  1. 이미지에 읽기 / 쓰기 권한을 부여할 수 있다.
  2. 기본적으로 하나의 프로세스만을 담당한다.
  3. 각각의 컨테이너들은 독립적이다.

Docker 이미지의 가장 큰 장점은 우리가 코드를 작성할 때 Import구문으로 서드파티 패키지를 불러 오듯, 누군가 만들어 놓은 이미지를 상속하고, 필요한 것들을 추가하여 새로운 이미지를 만들어 낼 수 있다는 것입니다. 따라서 Ndivia에서 만들어 놓은 ubuntu20.04의 운영체제, cuda11.3.1, cudnn8이 패키징된 이미지를 그대로 불러와서 마치 Layer를 쌓듯이, 그 위에 우리가 원하는 python, pytorch 버전을 설치할 수 있습니다. 설치 시에는 바탕이 되는 이미지의 컨테이너들을 활용해, 이미지에 추가적인 파일을 읽고 쓰도록 합니다.

Docker Hub에서는 다양한 Docker 이미지들이 만들어지고, 공유되고 있기 때문에, 큰 부담 없이 Docker환경을 구성할 수 있습니다. 이후에 예제를 통해 이 곳에서 공유되고 있는 nvidia/cuda docker 이미지를 활용하여 새로운 이미지를 빌드해볼 것입니다.

Docker 환경의 기본 구조

보통 머신러닝 / 딥러닝 연구를 위해서는 대용량의 storage및 충분한 메모리와 뛰어난 성능의 GPU를 보유하고 있는 서버를 활용하는 것이 일반적입니다. Docker를 잘 활용한다면, 서로 다른 여러 개의 서버에 동일한 연구 환경을 손쉽게 세팅하고, 로컬에서 Docker 환경에 접속하여 작업을 진행할 수 있습니다.

기본적인 Docker 환경 구조는 생각보다 간단합니다. 먼저 Host가 되는 PC에서 Docker를 설치하고, 운영체제 및 작업 환경에 필요한 패키지를 포함하여 이미지를 만듭니다. 이미지를 실행시키는 컨테이너는 Host PC의 CPU 및 GPU를 공유하게 되며, 저장 공간 또한 공유하게 됩니다. 그 후 이미지에 설치된 ssh를 컨테이너에 담아 띄워두면, local PC에서 ssh를 통해 Docker 환경으로 접속이 가능하게 됩니다.

옴니어스의 Docker 환경 구조

옴니어스 연구팀에서는 기본적으로 nvidia에서 제공하는, nvidia/cuda:11.3.1-cudnn8-devel-ubuntu20.0를 바탕으로, 그 위에 환경에 필요한 여러 가지 패키지들을 설치하여 패키징하고 있습니다. 이미지 내에 설치된 파일들은 컨테이너를 통해 실행시킬 수 있는데, 기본적으로 컨테이너 하나당 하나의 프로세스만을 담당합니다. 옴니어스 연구팀에서는 좀 더 효율적인 컨테이너 관리를 위해, supervisor라는 패키지를 활용합니다.

Supervisor는 쉽게 말해, 동시에 여러 개의 프로세스를 관리할 수 있는 도구 인데, 하나의 컨테이너에 Jupyter notebook, jupyter lab, sshd를 관리하는 프로세스인 supervisor를 띄워 놓으면, 하나의 컨테이에서 세 개의 프로세스를 동시에 실행할 수 있게 됩니다. 각각의 프로세스들은 특정 포트와 연결되어 있어, ssh를 통해 Docker환경으로의 접근을 가능하게 하고, jupyter notebook, jupyter lab를 다른 PC환경에서도 손쉽게 접근이 가능하도록 하고 있습니다. Supervisor에 대한 보다 자세한 사항은 공식 문서에 기술되어 있습니다.

2.   Practice

Dockerfile로 이미지를 만들고 컨테이너로 띄운 다음, ssh를 통해 vscode나 주피터 노트북에 접근하는 전체 과정을 실습해 보겠습니다. 실습 예시는 ubuntu 환경에서 진행하였으며, 전체 코드는 ml-docker-example에서 확인하실 수 있습니다. 코드는 기본적인 Docker 설치가 완료되었음을 가정하고 있습니다.

Docker 설치 및 기본 환경 세팅

우선, 공식 홈페이지로 접근하여, 운영체제에 맞는 Docker 버전을 설치해 줍니다. Ubuntu 사용자라면 커맨드로도 설치가 가능합니다.

💡
NVIDIA Container Toolkit
Nvidia GPU를 docker에서 사용할 수 있도록 해주는 NVIDIA Container Toolkit은 이 곳을 참고하여 설치가 가능합니다.

설치 후 설치가 성공적으로 이루어졌는지 확인해보기 위해 docker images, docker ps 명령어로 이미지와 컨테이너(프로세스) 목록을 확인해 봅니다. 처음 Docker를 설치하게 되면 이미지와 컨테이너 목록에 아무것도 존재하지 않습니다. 우선, 작성할 이미지의 바탕이 될 이미지를 설치해 줍니다. git과 마찬가지로, pull 명령어를 통해 docker hub에서 공유되고 있는 여러 가지 이미지들을 가져올 수 있습니다.

docker pull nvidia/cuda:11.3.1-cudnn8-devel-ubuntu20.04

이후 docker images를 통해 확인해보면, nvidia/cuda repository에 cuda:11.3.1-cudnn8-devel-ubuntu20.04의 태그를 가진 이미지가 저장되었습니다. 이미지는 기본적으로 repository:tag의 형태로 관리됩니다.

Dockerfile로 이미지 만들기

Dockerfile은 이미지를 만들기 위한 파일입니다. Dockerfile은 여러 개의 명령어로 이루어질 수 있는데, Docker 공식 문서에 각 명령어에 대해 자세히 설명되어 있습니다.

위 코드는 ml-docker-example에 작성된 Dockerfile의 일부 입니다. Dockerfile의 역할을 간단히 요약하면, FROM을 통해 미리 받아 두었던 이미지를 가져오고, ENV, RUN, COPY, EXPOSE등의 명령어를 통해 가져온 이미지에 추가적으로 다양한 라이브러리를 설치하여 다시 패키징하는 것입니다.

코드를 자세히 들어다보면, ENVRUN같은 Dockerfile의 명령어들은 Linux 환경의 bash 명령어를 수행하기 위함임을 알 수 있습니다. 따라서 nvidia/cuda repository의 이미지를 컨테이너로 띄워놓고 접속 한 뒤, 같은 bash 명령어를 명시적으로 수행해도 결과 자체는 같게 됩니다. 그러나 컨테이너를 내렸다가 다시 띄우게 되면 설치되었던 내용들은 모두 사라지기 때문에, 매번 다시 설치할 필요 없도록 새로운 이미지로 패키징을 해두는 것입니다.

위의 샘플 코드가 연구 환경 구축을 위해 Docker를 활용하는, 가장 중요한 이유가 되는 부분입니다. python 및 pytorch와 같은 프레임워크의 버전을 CUDA 버전에 맞게 세팅해두고, 이에 맞는 여러 가지 패키지들을 설치해줍니다. 이미지 단위로 버전을 관리한다면, 일괄적으로 여러 대의 서버에 같은 환경의 업데이트를 손 쉽게 할 수 있습니다. 추가적으로, ml-docker-example에는 Dockerfile.opencv라는 opencv의 설치만을 위한 Dockerfile이 있습니다. 해당 이미지를 먼저 빌드 한 후, Dockerfile에서 COPY 명령어를 이용해 빌드 해 둔 이미지에 설치된 파일을 옮겨올 수 있습니다.

위는 jupyter notebook과, jupyter lab, ssh 등을 일괄적으로 관리할 수있도록 supervisor의 configuration파일을 세팅하는 부분입니다. 마지막의 CMD 명령어를 통해 이미지를 생성할 때, 하나의 컨테이너로 supervisord 폴더의 start.sh를 실행하여 결과적으로 supervisor 프로세스를 실행할 수 있도록 해주었습니다.

CMD 명령어 이전에 EXPOSE 명령어로 jupyter notebook과 같은 연구 환경에 접속하기 위한 포트를 미리 개방해 두었습니다. 예제 코드의 docker-environment/supervisord/run-jupyter-lab.sh 과 같은 실행파일을 살펴보면, jupyter notebook은 8800번, jupyter lab은 8801번의 내부포트를 사용하도록 해두었으므로 해당 포트를 열어두어야 외부포트로 접근이 가능하게 됩니다.

Dockerfile의 작성이 완료 되었다면,

docker build -f Dockerfile.opencv -t repository_name:tag .

로 이미지를 빌드할 수 있고, 완료 후 역시 docker images 로 확인이 가능합니다. ml-docker-example에서는 두 개의 Dockerfile을 한 번에 설치할 수 있도록 Makefile을 활용하였습니다. Makefile이 존재하는 docker-environment 폴더 내에서 make all 을 통해 설치할 수 있습니다.

이미지 실행 및 연구 환경 접근

빌드 된 이미지에 접근하기 위해서는, docker run 으로 이미지를 실행시켜줘야 합니다. 자주 사용 되는 옵션 들은 다음과 같습니다.

  • -it : shell interactive mode.
  • --rm : 컨테이너가 종료되면 자동으로 삭제되도록 함.
  • -d : detach mode. 백그라운드에서 컨테이너가 실행되도록 함.
  • -restart : 컨테이너 안의 프로세스가 종료되더라도 다시 시작하도록 설정함.
  • -p : Host와 컨테이너 사이의 포트포워딩 설정.
  • -v : Host와 컨테이너 사이의 볼륨 마운트 설정.
💡
-v option
Docker의 컨테이너는 종료되는 순간, 변경 사항을 모두 잃게 됩니다. 따라서 변경 사항을 유지할 수 있도록, Host와 컨테이너 사이의 volume을 마운트하여, 컨테이너가 Host의 volume으로 부터 파일을 읽고 쓸 수 있도록 하는 것이 일반적입니다.

docker run —-rm -it image_repository bash 로 interactive 하게 이미지 환경에 접근할 수 있으며, docker run -it —-rm -p external_port:internal_port image_repository 로, -p옵션을 통해 포트포워딩을 지정하여 실행시킬 수 있습니다. 또한 테스트용이 아닌, background에서 이미지가 계속 실행되도록 하기 위해서는 -it —rm 옵션 대신 -d 옵션을 사용할 수 있습니다. 옵션에 대한 보다 자세한 사항은 공식 문서에서 확인할 수 있습니다.

예를 들어, docker run -it —rm -p 21501:8800 -p 21502:8801 res-env/sample 로 이미지를 실행시키면, 앞서 supervisor configuration 파일에 명시해 주었던 cron, jupyter-lab, jupyter-notebook, sshd의 프로세스가 실행되는 것을 확인할 수 있고, local PC에서 Host 서버의 21501번 외부포트를 통해 8800번의 jupyter notebook으로 접근할 수 있게 됩니다.

jupyter Notebook 환경 접속 예시(host_server_ip_address:21501로 접근)
jupyter Lab 환경 접속 예시(server_ip_address:21502로 접근)

마지막으로, ssh를 통해 원격으로 Docker 컨테이너에 접속해 보겠습니다. ssh 접근을 위해서는 Host에서 private key, public key를 생성해주고, 같은 public key를 Docker 이미지안에 등록해두어야 합니다.

ml-docker-example의 makefile은 ssh key를 생성하고, 이미지를 빌드할 때 생성해 둔 public키를 authorized_keys로써 복사하여 옮겨오는 것을 포함하고 있습니다(Dockerfile 122 line 참조). 따라서, make all로 이미지 빌드 후 make dry-run으로 컨테이너를 띄운 다음,
ssh -i private_key_location -p 25000 omnious@host_server_ip_address
로 ssh 접근이 가능합니다. private key만 가지고 있다면, 어떤 local PC에서도 Docker 컨테이너로 접속할 수 있게 됩니다. 앞선 jupyer notebook, jupyter lab과 마찬가지로, 예제에서는 dry-run을 실행할 때, ssh를 담당하는 22번 컨테이너 내부포트를 Host의 25000번 외부포트로 지정해주었기 때문에 위와 같이 접속하였습니다. 또한 dry-run에서는 -v 옵션을 통해 workspace 내부의 sample.py를 컨테이너에 마운트 해주었는데, ssh로 접근하여 해당 파일이 존재하는지 확인해볼 수 있습니다.

ssh 접속 예시

마치며

지금 까지 옴니어스 연구팀의 Docker 환경 구축에 대해 알아보았습니다. 실습 과정에서 추가적으로 궁금하신 점이 있다면 언제든지 옴니어스 연구팀(team.research@omnious.com)으로 연락주세요! 감사합니다.