Optimizing Docker Images and Conda Environments in SageMaker Training
Tags: docker, devops, aws, beginners
Published: 2024-01-31 22:09:29

## Introduction
Previously, I set up a Conda environment inside a Docker container for training in Amazon SageMaker. To align the environments between the debugging environment (EC2/Deep Learning AMI) and the training environment (SageMaker), I exported the `environment.yml` configuration file from the debugging environment and used it to build the Docker image for the training environment.

Choosing a SageMaker Training specific base image for Docker resulted in a large, approximately 11 GB image, due to duplication of libraries in the base image and `environment.yml`. At that time, I continued using this container image due to the urgency of running the training sessions. However, the long load times for the image and anticipated difficulties in future version upgrades of key libraries (like `transformers` and `torch`) led me to reorganize the `environment.yml` and `Dockerfile`.

This should be helpful for those wanting to use Conda environments built in a debug environment in SageMaker but are unsure about Docker image construction.

## Implementation
Here are the final versions of the `Dockerfile` and `environment.yml`:

```Dockerfile
# Using Amazon SageMaker's PyTorch GPU training base image
FROM 763104351884.dkr.ecr.ap-northeast-1.amazonaws.com/pytorch-training:1.12.1-gpu-py38-cu113-ubuntu20.04-sagemaker

# Setting the path for Conda (as it's included in the base image, installation is unnecessary)
ENV PATH /opt/conda/bin:$PATH

# Initializing Conda environment
RUN conda init bash

# Copying the environment.yml file to the container
COPY environment.yml /root/environment.yml

# Creating the environment from environment.yml
RUN conda env create -f /root/environment.yml

# Automatically activating the Conda environment
RUN echo "conda activate environment" >> ~/.bashrc

# Starting bash in the environment when the container launches
CMD [ "/bin/bash" ]
```

```yml
name: environment
channels:
  - anaconda
  - pytorch
  - huggingface
  - conda-forge
dependencies:
  - python=3.8.13
  - pandas=1.4.3
  - scikit-learn=1.1.1
  - transformers=4.26.0
  - numpy=1.23.1
  - libgcc
  - imbalanced-learn=0.10.1
  - pip:
      - wandb
      - python-dotenv
```

By using SageMaker's PyTorch GPU training base image and avoiding duplicate installations in the Conda environment, I reduced the image size to about 8 GB, a 3 GB reduction from the previous image.

## Supplement 1: Building the Image and Pushing to ECR
Here's a supplementary guide on building the Dockerfile and pushing it to a private ECR repository.

Before building the image using `deep-learning-containers`, it's necessary to specify the region and ECR registry, obtain an authentication token, and authenticate the Docker client.

```bash
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 763104351884.dkr.ecr.ap-northeast-1.amazonaws.com
```
With this, the image can be built.

```bash
$ docker build -t <Account ID>.dkr.ecr.ap-northeast-1.amazonaws.com/<Image Name>:<Tag Name> .
```
※For ARM-based Macs (M1~3), use the following command to build:

```bash
$ docker buildx build --platform linux/amd64 -f Dockerfile -t <Account ID>.dkr.ecr.ap-northeast-1.amazonaws.com/<Image Name>:<Tag Name> .
```

Similarly, authenticate with your own account's ECR registry before pushing the image.

```bash
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <Account ID>.dkr.ecr.ap-northeast-1.amazonaws.com
```
Now, you can push the image.

```bash
$ docker push <Account ID>.dkr.ecr.ap-northeast-1.amazonaws.com/<Image Name>:<Tag Name>
```

## Supplement 2: Setting Up the Entry Point Script for SageMaker
When setting up SageMaker Training jobs, use a Python script like the following. However, this setup alone doesn't automatically switch to the Conda environment, requiring additional steps.

```py
import sagemaker
from sagemaker.estimator import Estimator

session = sagemaker.Session()
role = sagemaker.get_execution_role()

estimator = Estimator(
    image_uri="<URL of the container image you created>",
    role=role,
    instance_type="ml.g4dn.2xlarge",
    instance_count=1,
    base_job_name="pre-training",
    output_path="s3://<bucket name>/sagemaker/output_data/pre_training",
    code_location="s3://<bucket name>/sagemaker/output_data/pre_training",
    sagemaker_session=session,
    entry_point="pre-training.sh",
    dependencies=["tabformer"],
    hyperparameters={
        "mlm": True,
        "do_train": True,
        "field_hs": 64,
        "output_dir": "/opt/ml/model/",
        "data_root": "/opt/ml/input/data/input_data/",
        "data_fname": "<file name>"
    }
)
estimator.fit({
    "input_data": "s3://<bucket name>/sagemaker/input_data/<file name>.csv"
})
```

Therefore, the shell script specified in `entry_point` must switch to the Conda environment.

```sh
#!/bin/bash

# conda activate
source /opt/conda/etc/profile.d/conda.sh
conda activate environment

# pre-training
python main.py
```

## Supplement 3: A Failed Attempt
I also present an example of a `Dockerfile` and `environment.yml` aimed at further reducing image size. This example assumes a debug environment using a standard Ubuntu AMI on an EC2 instance.

First, in the `Dockerfile`, the base image is `ubuntu`. Since this base image doesn't include `conda`, Miniconda was installed using wget (specifying py38). Additionally, `build-essential` and `sagemaker-training` were installed separately.

※The absence of `sagemaker-training` can result in errors. This package is essential for SageMaker Training.

```Dockerfile
# Using Ubuntu 22.04 LTS as the base image
FROM ubuntu:22.04

# Installing necessary packages
RUN apt-get update && \
    apt-get install -y wget bzip2 ca-certificates build-essential && \
    rm -rf /var/lib/apt/lists/*

# Downloading and installing Miniconda
RUN wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-py38_23.11.0-2-Linux-x86_64.sh -O ~/miniconda.sh && \
    /bin/bash ~/miniconda.sh -b -p /opt/conda && \
    rm ~/miniconda.sh && \
    /opt/conda/bin/conda clean --all --yes

# Setting the Conda path
ENV PATH /opt/conda/bin:$PATH

# Initializing Conda environment
RUN conda init bash

# Copying the environment.yml file to the container
COPY environment.yml /root/environment.yml

# Creating the environment from environment.yml
RUN conda env create -f /root/environment.yml

# Automatically activating the Conda environment
RUN echo "conda activate environment" >> ~/.bashrc

# Installing the sagemaker-training package
RUN pip install sagemaker-training

# Starting bash in the 'tabformer-opt-v2' environment when the container launches
CMD [ "/bin/bash" ]
```

In the `environment.yml`, dependencies not included in the build target are specified. As a result, this file has more dependencies than the adopted `environment.yml`.

```yml
name: environment
channels:
  - anaconda
  - pytorch
  - huggingface
  - conda-forge
dependencies:
  - python=3.8.13
  - pip>=21.0
  - pytorch=1.12.1
  - torchaudio=0.12.1
  - torchvision=0.13.1
  - pandas=1.4.3
  - scikit-learn=1.1.1
  - transformers=4.26.0
  - numpy
  - libgcc
  - cudatoolkit=11.6
  - imbalanced-learn=0.10.1
  - pip:
      - transformers==4.26.0
      - wandb
      - python-dotenv
```

With this setup, the image size was reduced to about 3 GB, and training could commence in both the debug and SageMaker environments. However, the training was unexpectedly running on CPU instead of GPU.

Therefore, it wasn't feasible for actual use. This experience highlighted the importance of building `environment.yml` based on the libraries included in SageMaker's PyTorch GPU training base image.