SJ_Koding

[LLM] Docker compose를 활용한 sLLM 파인튜닝 및 추론 자동화하기 下편 - Docker compose 본문

LLM

[LLM] Docker compose를 활용한 sLLM 파인튜닝 및 추론 자동화하기 下편 - Docker compose

성지코딩 2024. 11. 18. 10:12

여러분의 소스코드가 담겨있는 Docker Image를 성공적으로 빌드했습니다. ipynb가 아닌 이상 학습을 실행하는 코드와 추론을 진행하는 코드가 별도로 존재하고, 특정 명령을 통해 수행될 것입니다. 

 

 

[LLM] Docker compose를 활용한 sLLM 파인튜닝 및 추론 자동화하기 上편 - Docker Image 빌드

대학생때 부터 AI만 전공해오다보니 백엔드 지식이 턱없이 부족한 것을 깨닫게 해준 프로젝트를 진행해왔습니다.그 중 Docker를 활용하여 LLM파인튜닝 및 추론단계를 자동화 할 수 있도록 만들어

sjkoding.tistory.com

 

LLM파인튜닝 특성상 환경을 분할할 필요가 적습니다. train타입과 inference타입의 환경은 거의 동일하며 소스코드만 차이가 나기 때문에 이 때문에 이미지를 2개로 나누는 것이 어쩌면 공간적으로 비효율적일 수 있기 때문에 하나의 이미지를 활용할 예정입니다. 물론 inference때 train코드가 필요 없겠지만, 일반적으로 큰 문제가 아닙니다.

우선 Docker compose가 무엇인지 간략하게 살펴보겠습니다.

Docker compose는 여러 컨테이너 정의를 docker-compose.yml파일로 정의하며, 이 파일에 담긴 모든 컨테이너의 정의 및 명령을 일괄적으로 빌드할 수 있게 해주는 도구입니다.

저는 trainable한 데이터셋으로 변환해주는 컨테이너, fine-tuning을 진행하는 컨테이너, inference를 진행하는 컨테이너를 빌드할 예정인데, 이를 간편하게 수행할 수 있게 해줍니다.

설정 값으로는 네트워크, 볼륨, 환경 변수 등이 존재하며, 해당 파일로 어디에서나 동일한 환경을 재현할 수 있어 편리합니다. 

빌드 명령은 아래 명령 으로 매우 간편하게 빌드할 수 있습니다:

docker compose up # 최신버전 (2020년 12월 이후, 권장버전)
docker-compose up # 구버전

복잡한 docker run과 다르게 이미 설정값이 docker-compose.yml에 삽입 되어있기 때문입니다. 

컨테이너의 구성

1. dataset_generator: 예를 들어 db나 csv파일로 부터 가져온 데이터를 LLM이 파인튜닝할 수 있는 형태로 재 가공하는 컨테이너
2. finetuner: LLM 모델을 fine-tuning하는 컨테이너 (LoRA or Full)
3. inferrer: 학습된 LLM모델을 테스트하고 기능을 수행하는 컨테이너

위 컨테이너를 작성시키기 위해서는 각 컨테이너마다 필요한 소스코드를 Image 빌드시점에서 함께 빌드되어야합니다. 예를 들어, 여러분이 사용하시는 데이터셋을 여러분의 LLM모델 학습 포맷에 맞춰 재 가공하는 소스파일을 작성하셔야합니다. 저는 이전에 빌드했던 /app/train경로안에 fine-tuning소스와 dataset_generator소스파일이 함께 포함되어 있어 /app/train과 /app/inference 경로 두 개를 사용합니다.

Docker-compose.yml 작성하기

1) Docker volume 설정

이전 포스팅에서 저만의 규칙을 세웠고 해당 내용을 다시 복기하자면 기존 모델파일은 /app/models에 저장될 것이며, 파인튜닝된 LoRA어뎁터, 생성된 데이터셋을 저장하는 경로는 /app/outputs에 저장될 예정이라고 했습니다. 그리고 저는 해당 경로를 local의 디스크를 빌린 volume을 활용하여 저장할 계획입니다. 이를 각각 다음과 같이 정의하겠습니다.

volumes:
  output-volume:
    name: output-volume
    driver: local
  model-volume:
    name: model-volume
    driver: local

두 개의 볼륨을 정의했으며 name: 값을 지정하지 않으면 docker compose를 실행하는 프로젝트 폴더명이 접미사로 붙게됩니다. 만약 docker_project/ 라는 폴더에서 지정하고 해당 볼륨을 name: 값 없이 생성하게 되면docker_project_output-volume 이라는 볼륨 명을 가질 것 입니다. 이는 추후 협업을 진행할 때 프로젝트 명에 따라 파일을 중복하여 저장할 수 있기 때문에 name을 지정하면 동일한 볼륨에서 협업이 가능합니다.

driver: local의 의미는 해당 docker compose를 실행하는 호스트 환경의 디스크를 사용한다는 의미입니다. Ubuntu환경 기준으로 /var/lib/docker/volume/~~~ 경로에 저장되게 되며 일반 권한으로 해당 경로를 접근할 수 없어 sudo권한을 이용해야합니다.

 

2) dataset_generator 컨테이너 설정

version: '3'
services:
  dataset_generator:
    image: seongjiko/enssel_llm_env:latest
    container_name: dataset_generator
    command: [ "/bin/bash", "-c", "set -e; python3 /app/train/datasets/trainable_dataset_generator.py;" ]
    volumes:
      - output-volume:/app/outputs

컨테이너명은 dataset_generator로 지정했습니다. image는 이전에 빌드했던 이미지를 사용합니다. command는 해당 컨테이너가 빌드될 때 실행하는 명령들의 집합체입니다. 즉 trainable_dataset_generator.py를 실행한다는 뜻이죠.

이때 output-volume의 /app/outputs경로를 마운트하여 해당 경로에 생성된 데이터셋을 저장합니다. 저장될 경로는 소스코드내에서 직접 /app/outputs로 지정해야합니다. 해당 볼륨에 저장하면 추후에 fine-tuning하는 시점에서 저장된 데이터셋을 같은 볼륨에 접근하여 불러올 수 있게 됩니다.

 

3) llm_finetuner 컨테이너 설정

 #  if test for inferrer
  llm_finetuner:
    image: seongjiko/enssel_llm_env:latest
    container_name: llm_finetuner
    command: |
      /bin/bash -c "
      set -e
      echo 'Starting llm_finetuner...'
      git lfs install
      if [ ! -d /app/models/llama-3.2-3B-Instruct ]; then
        echo 'Cloning repository Llama-3.2-3B-Instruct...'
        sudo git clone https://huggingface.co/Enssel/Llama-3.2-3B-@@@ /app/models/llama-3.2-3B-Instruct
      else
        echo 'Repository Llama-3.2-3B-Instruct already cloned.'
      fi
      if [ ! -d /app/outputs/llama3.2-SFT-lora ]; then
        echo 'Running finetuning.py...'
        sudo git clone https://huggingface.co/@@@/llama3.2-3b-@@@ /app/outputs/llama3.2-SFT-lora
      else
        echo '/app/outputs/llama3.2-SFT-lora already exists. Skipping finetuning.'
      fi
      echo 'llm_finetuner completed.'
      "
    networks:
      - sop-network

    volumes:
      - output-volume:/app/outputs
      - model-volumes:/app/models:rw
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 2
              capabilities: [ gpu ]
    depends_on:
      dataset_generator:
        condition: service_completed_successfully

예시로 llama3.2를 사용합니다.
git lfs install는 LFS(Large File Storage)를 활성화 하여 대형 모델 파일을 다운로드할 준비를 합니다.

그리고 만약 모델이 다운로드된 적이 없다면(git clone을 통해 다운로드 받아진 모델은 model-volumes의 /app/models에 저장하게 되며 if문으로 이미 다운로드가 되어있는지 확인할 수 있습니다.) huggingface에서 모델을 clone해옵니다. 사실 Image를 빌드하는 시점에서 모델을 미리 삽입해도 되나, 그렇게 되면 이미지의 용량이 터무니없이 커지게됩니다. (30~50GB 이상)

그리고 나서 파인튜닝된 모델이 저장되어있지 않은경우(최초로 튜닝을 시도할 경우) 파인튜닝 소스파일을 실행시켜 파인튜닝을 진행하게 됩니다.

이때 output-volume의 /app/outputs경로를 마운트하며 이전에 저장했던 trainable dataset을 사용할 수 있게됩니다. 그러면 선행조건이 dataset_generator 컨테이너가 실행이 되고 난 후에 해당 컨테이너를 실행해야한다는 점인데, 이를 depends_on으로 설정할 수 있습니다.

dataset_generator에 의존하는 컨테이너이며, 실행 조건은 service_completed_successfully 즉, 의존하는 컨테이너가 성공적으로 종료되고 나서 실행한다는 의미입니다. 저는 해당 구조를 통해 자동화를 진행하였습니다. 나중에는 KubeFlow등에 비슷한 원리로 적용할 수 있습니다.

파인튜닝된 모델은 dataset과 마찬가지로 output-volume의 /app/outputs에 저장되게 됩니다.

또한 deploy.resources.reservations.devices를 통해 NVIDIA GPU 2개를 할당하여, GPU 리소스를 활용한 고성능 학습이 가능하도록 설정했습니다.

4) llm_inferrer 컨테이너 설정

llm_inferrer:
    image: seongjiko/enssel_llm_env:latest
    container_name: llm_inferrer
    command: |
      /bin/bash -c "
      set -e
      echo 'Starting llm_inferrer...'
      cd /app/inference
      uvicorn main:app --host 0.0.0.0 --port 8811
      "
    networks:
      - sop-network
    volumes:
      - output-volume:/app/outputs
      - model-volumes:/app/models:rw
    ports:
      - "8811:8811"
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 2
              capabilities: [ gpu ]
    depends_on:
      llm_finetuner:
        condition: service_completed_successfully

마지막으로 추론 컨테이너입니다. command를 보고 눈치를 채신분도 계시겠지만, 개인적으로는 FastAPI를 통해 추론 시스템을 개발했습니다(uvicorn명령).  

이제는 대충 감이 오실텐데 지금까지 저장된 model과 outputs들을 모두 불러와서 단순히 추론 로직을 실행하는 구문입니다. 

이 역시 llm_finetuner가 정상적으로 종료되는 것을 대기하고 있습니다. 데이터셋 생성 - 파인튜닝 - 추론이 순차적으로 자동화 되게 됩니다.

 

전체코드

version: '3'
services:
  dataset_generator:
    image: seongjiko/enssel_llm_env:latest
    container_name: dataset_generator
    command: [ "/bin/bash", "-c", "set -e; echo 'Starting dataset generation...'; echo 'Generating dataset...'; python3 /app/train/datasets/trainable_dataset_generator.py; echo 'Dataset generation completed.'" ]
    volumes:
      - output-volume:/app/outputs


  llm_finetuner:
    image: seongjiko/enssel_llm_env:latest
    container_name: llm_finetuner
    command: |
      /bin/bash -c "
      set -e
      echo 'Starting llm_finetuner...'
      git lfs install
      if [ ! -d /app/models/llama-3.2-3B-Instruct ]; then
        echo 'Cloning repository Llama-3.2-3B-Instruct...'
        sudo git clone https://huggingface.co/@@@/Llama-3.2-3B-Instruct_@@@ /app/models/llama-3.2-3B-Instruct
      else
        echo 'Repository Llama-3.2-3B-Instruct already cloned.'
      fi
      if [ ! -d /app/outputs/llama3.2-SFT-lora ]; then
        echo 'Running finetuning.py...'
        sudo git clone https://huggingface.co/@@@/llama3.2@@@ /app/outputs/llama3.2-SFT-lora
      else
        echo '/app/outputs/llama3.2-SFT-lora already exists. Skipping finetuning.'
      fi
      echo 'llm_finetuner completed.'
      "
    networks:
      - sop-network

    volumes:
      - output-volume:/app/outputs
      - model-volumes:/app/models:rw
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 2
              capabilities: [ gpu ]
    depends_on:
      dataset_generator:
        condition: service_completed_successfully

  llm_inferrer:
    image: seongjiko/enssel_llm_env:latest
    container_name: llm_inferrer
    command: |
      /bin/bash -c "
      set -e
      ls /app/outputs
      echo 'Starting llm_inferrer...'
      cd /app/inference
      uvicorn main:app --host 0.0.0.0 --port 8811
      "
    networks:
      - sop-network
    volumes:
      - output-volume:/app/outputs
      - model-volumes:/app/models:rw
    ports:
      - "8811:8811"
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 2
              capabilities: [ gpu ]
    depends_on:
      llm_finetuner:
        condition: service_completed_successfully

networks:
  sop-network:
    driver: bridge
    name: sop-network

volumes:
  output-volume:
    name: sop-output-volume
    driver: local
  model-volumes:
    name: sop-model-volumes
    driver: local

 

 


이번 글에서는 Docker Compose를 활용해 LLM의 데이터셋 생성, 파인튜닝, 추론 과정을 자동화하는 방법을 다뤘습니다. 세 단계로 나뉜 컨테이너 구성(dataset_generator, llm_finetuner, llm_inferrer)을 통해 데이터 파이프라인을 체계적으로 관리하고, GPU 리소스를 효율적으로 활용하며, 설정 파일을 통해 환경 재현성을 확보할 수 있었습니다.

이러한 자동화 구조는 개발 및 운영의 효율성을 극대화하며, 향후 KubeFlow와 같은 더 큰 확장성 있는 플랫폼으로도 자연스럽게 연결될 수 있습니다.

감사합니다.