

本文属于机器翻译版本。若本译文内容与英语原文存在差异，则一律以英文原文为准。

# 扩展预构建容器
<a name="prebuilt-containers-extend"></a>

如果预构建的 SageMaker AI 容器不能满足您的所有要求，则可以扩展现有映像以满足您的需求。即使您的环境或框架直接支持，您也可能需要添加其他功能或以不同的方式配置容器环境。通过扩展预构建的映像，您可以利用随附的深度学习库和设置，而无需从头开始创建映像。您可以扩展容器以添加库、修改设置和安装其他依赖项。

以下教程展示了如何扩展预先构建的 SageMaker 映像并将其发布到 Amazon ECR。

**Topics**
+ [扩展预构建容器的要求](#prebuilt-containers-extend-required)
+ [扩展 SageMaker AI 容器以运行 Python 脚本](#prebuilt-containers-extend-tutorial)

## 扩展预构建容器的要求
<a name="prebuilt-containers-extend-required"></a>

要扩展预先构建的 SageMaker 镜像，你需要在 Dockerfile 中设置以下环境变量。有关带有 SageMaker AI 容器的环境变量的更多信息，请参阅 T [SageMaker raining Toolkit GitHub 存储库](https://github.com/aws/sagemaker-training-toolkit/blob/master/ENVIRONMENT_VARIABLES.md)。
+ `SAGEMAKER_SUBMIT_DIRECTORY`：容器中的目录，用于训练的 Python 脚本在此目录中。
+ `SAGEMAKER_PROGRAM`：应该调用并用作训练入口点的 Python 脚本。

您还可以通过在 Dockerfile 中包含以下内容来安装其他库：

```
RUN pip install <library>
```

以下教程演示了如何使用这些环境变量。

## 扩展 SageMaker AI 容器以运行 Python 脚本
<a name="prebuilt-containers-extend-tutorial"></a>

在本教程中，您将学习如何使用使用 CIFAR-10 数据集的 Python 文件扩展 SageMaker AI PyTorch 容器。通过扩展 SageMaker AI PyTorch 容器，您可以利用与 SageMaker AI 配合使用的现有训练解决方案。本教程扩展了训练映像，不过还可以采取相同的步骤来扩展推理映像。有关可用映像的完整列表，请参阅[可用的深度学习容器映像](https://github.com/aws/deep-learning-containers/blob/master/available_images.md)。

要使用 SageMaker AI 容器运行自己的训练模型，请通过 SageMaker Notebook 实例构建 Docker 容器。

### 步骤 1：创建 SageMaker 笔记本实例
<a name="extend-step1"></a>

1. 打开 A [SageMaker I 控制台](https://console.aws.amazon.com/sagemaker/)。

1. 在左侧导航窗格中，依次选择**笔记本**、**笔记本实例**和**创建笔记本实例**。

1. 在**创建笔记本实例**页面上提供以下信息：

   1. 对于 **Notebook instance name (笔记本实例名称)**，输入 **RunScriptNotebookInstance**。

   1. 对于**笔记本实例类型**，选择 **ml.t2.medium**。

   1. 展开**权限和加密**部分，执行以下操作：

      1. 对于 **IAM 角色**，选择**创建新角色**。

      1. 在**创建 IAM 角色**页面上，选择**特定的 S3 存储桶**，指定一个名为 **sagemaker-run-script** 的 S3 存储桶，然后选择**创建角色**。

         SageMaker AI 创建了一个名为的 IAM 角色`AmazonSageMaker-ExecutionRole-YYYYMMDDTHHmmSS`，例如`AmazonSageMaker-ExecutionRole-20190429T110788`。请注意，执行角色命名约定使用创建角色的日期和时间，由 `T` 分隔。

   1. 对于**根访问**，选择**已启用**。

   1. 选择**创建笔记本实例**。

1. 在**笔记本实例**页面上，**状态**是**待处理**。Amazon A SageMaker I 可能需要几分钟才能启动机器学习计算实例（在本例中为启动笔记本实例）并向其连接 ML 存储卷。笔记本实例有一个预配置的 Jupyter 笔记本服务器和一组 Anaconda 库。有关更多信息，请参阅 [ CreateNotebookInstance](https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_CreateNotebookInstance.html)。

   

1. 在**权限和加密**部分，复制 **IAM 角色 ARN 编号**，然后将其粘贴到记事本文件中以临时保存。稍后，您将使用此 IAM 角色 ARN 编号，在笔记本实例中配置本地训练估算器。**The IAM role ARN number (IAM 角色 ARN 编号)** 如下所示：`'arn:aws:iam::111122223333:role/service-role/AmazonSageMaker-ExecutionRole-20190429T110788'`

1. 笔记本实例的状态更改为后 **InService**，选择**打开 JupyterLab**。

### 步骤 2：创建并上传 Dockerfile 和 Python 训练脚本
<a name="extend-step2"></a>

1.  JupyterLab 打开后，在您的主目录中创建一个新文件夹 JupyterLab。在左上角，选择**新建文件夹**图标，然后输入文件夹名称 `docker_test_folder`。

1.  在 `docker_test_folder` 目录中创建 `Dockerfile` 文本文件。

   1. 选择左上角的**新启动程序**图标 (\$1)。

   1. 在右窗格的**其他**部分下，选择**文本文件**。

   1.  将以下 `Dockerfile` 示例代码粘贴到您的文本文件中。

      ```
      # SageMaker PyTorch image
      FROM 763104351884.dkr.ecr.us-east-1.amazonaws.com/pytorch-training:1.5.1-cpu-py36-ubuntu16.04
      
      ENV PATH="/opt/ml/code:${PATH}"
      
      # this environment variable is used by the SageMaker PyTorch container to determine our user code directory.
      ENV SAGEMAKER_SUBMIT_DIRECTORY /opt/ml/code
      
      # /opt/ml and all subdirectories are utilized by SageMaker, use the /code subdirectory to store your user code.
      COPY cifar10.py /opt/ml/code/cifar10.py
      
      # Defines cifar10.py as script entrypoint 
      ENV SAGEMAKER_PROGRAM cifar10.py
      ```

      Dockerfile 脚本将执行以下任务：
      + `FROM 763104351884.dkr.ecr.us-east-1.amazonaws.com/pytorch-training:1.5.1-cpu-py36-ubuntu16.04`— 下载 A SageMaker I PyTorch 基础镜像。您可以将其替换为要用于构建容器的任何 SageMaker AI 基础镜像。
      + `ENV SAGEMAKER_SUBMIT_DIRECTORY /opt/ml/code` –将 `/opt/ml/code` 设置为训练脚本目录。
      + `COPY cifar10.py /opt/ml/code/cifar10.py`— 将脚本复制到 SageMaker AI 预期的容器内位置。该脚本必须位于此文件夹中。
      + `ENV SAGEMAKER_PROGRAM cifar10.py` –将您的 `cifar10.py` 训练脚本设置为入口点脚本。

   1.  在左侧目录导航窗格中，文本文件可能会自动命名为 `untitled.txt`。要重命名文件，请右键单击该文件，然后选择**重命名**，将文件重命名为没有 `.txt` 扩展名的 `Dockerfile`，然后按 `Ctrl+s` 或 `Command+s` 以保存文件。

1. 在 `docker_test_folder` 中创建或上传训练脚本 `cifar10.py`。在本练习中，您可以使用以下示例脚本。

   ```
   import ast
   import argparse
   import logging
   
   import os
   
   import torch
   import torch.distributed as dist
   import torch.nn as nn
   import torch.nn.parallel
   import torch.optim
   import torch.utils.data
   import torch.utils.data.distributed
   import torchvision
   import torchvision.models
   import torchvision.transforms as transforms
   import torch.nn.functional as F
   
   logger=logging.getLogger(__name__)
   logger.setLevel(logging.DEBUG)
   
   classes=('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
   
   
   # https://github.com/pytorch/tutorials/blob/master/beginner_source/blitz/cifar10_tutorial.py#L118
   class Net(nn.Module):
       def __init__(self):
           super(Net, self).__init__()
           self.conv1=nn.Conv2d(3, 6, 5)
           self.pool=nn.MaxPool2d(2, 2)
           self.conv2=nn.Conv2d(6, 16, 5)
           self.fc1=nn.Linear(16 * 5 * 5, 120)
           self.fc2=nn.Linear(120, 84)
           self.fc3=nn.Linear(84, 10)
   
       def forward(self, x):
           x=self.pool(F.relu(self.conv1(x)))
           x=self.pool(F.relu(self.conv2(x)))
           x=x.view(-1, 16 * 5 * 5)
           x=F.relu(self.fc1(x))
           x=F.relu(self.fc2(x))
           x=self.fc3(x)
           return x
   
   
   def _train(args):
       is_distributed=len(args.hosts) > 1 and args.dist_backend is not None
       logger.debug("Distributed training - {}".format(is_distributed))
   
       if is_distributed:
           # Initialize the distributed environment.
           world_size=len(args.hosts)
           os.environ['WORLD_SIZE']=str(world_size)
           host_rank=args.hosts.index(args.current_host)
           dist.init_process_group(backend=args.dist_backend, rank=host_rank, world_size=world_size)
           logger.info(
               'Initialized the distributed environment: \'{}\' backend on {} nodes. '.format(
                   args.dist_backend,
                   dist.get_world_size()) + 'Current host rank is {}. Using cuda: {}. Number of gpus: {}'.format(
                   dist.get_rank(), torch.cuda.is_available(), args.num_gpus))
   
       device='cuda' if torch.cuda.is_available() else 'cpu'
       logger.info("Device Type: {}".format(device))
   
       logger.info("Loading Cifar10 dataset")
       transform=transforms.Compose(
           [transforms.ToTensor(),
            transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
   
       trainset=torchvision.datasets.CIFAR10(root=args.data_dir, train=True,
                                               download=False, transform=transform)
       train_loader=torch.utils.data.DataLoader(trainset, batch_size=args.batch_size,
                                                  shuffle=True, num_workers=args.workers)
   
       testset=torchvision.datasets.CIFAR10(root=args.data_dir, train=False,
                                              download=False, transform=transform)
       test_loader=torch.utils.data.DataLoader(testset, batch_size=args.batch_size,
                                                 shuffle=False, num_workers=args.workers)
   
       logger.info("Model loaded")
       model=Net()
   
       if torch.cuda.device_count() > 1:
           logger.info("Gpu count: {}".format(torch.cuda.device_count()))
           model=nn.DataParallel(model)
   
       model=model.to(device)
   
       criterion=nn.CrossEntropyLoss().to(device)
       optimizer=torch.optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum)
   
       for epoch in range(0, args.epochs):
           running_loss=0.0
           for i, data in enumerate(train_loader):
               # get the inputs
               inputs, labels=data
               inputs, labels=inputs.to(device), labels.to(device)
   
               # zero the parameter gradients
               optimizer.zero_grad()
   
               # forward + backward + optimize
               outputs=model(inputs)
               loss=criterion(outputs, labels)
               loss.backward()
               optimizer.step()
   
               # print statistics
               running_loss += loss.item()
               if i % 2000 == 1999:  # print every 2000 mini-batches
                   print('[%d, %5d] loss: %.3f' %
                         (epoch + 1, i + 1, running_loss / 2000))
                   running_loss=0.0
       print('Finished Training')
       return _save_model(model, args.model_dir)
   
   
   def _save_model(model, model_dir):
       logger.info("Saving the model.")
       path=os.path.join(model_dir, 'model.pth')
       # recommended way from http://pytorch.org/docs/master/notes/serialization.html
       torch.save(model.cpu().state_dict(), path)
   
   
   def model_fn(model_dir):
       logger.info('model_fn')
       device="cuda" if torch.cuda.is_available() else "cpu"
       model=Net()
       if torch.cuda.device_count() > 1:
           logger.info("Gpu count: {}".format(torch.cuda.device_count()))
           model=nn.DataParallel(model)
   
       with open(os.path.join(model_dir, 'model.pth'), 'rb') as f:
           model.load_state_dict(torch.load(f))
       return model.to(device)
   
   
   if __name__ == '__main__':
       parser=argparse.ArgumentParser()
   
       parser.add_argument('--workers', type=int, default=2, metavar='W',
                           help='number of data loading workers (default: 2)')
       parser.add_argument('--epochs', type=int, default=2, metavar='E',
                           help='number of total epochs to run (default: 2)')
       parser.add_argument('--batch-size', type=int, default=4, metavar='BS',
                           help='batch size (default: 4)')
       parser.add_argument('--lr', type=float, default=0.001, metavar='LR',
                           help='initial learning rate (default: 0.001)')
       parser.add_argument('--momentum', type=float, default=0.9, metavar='M', help='momentum (default: 0.9)')
       parser.add_argument('--dist-backend', type=str, default='gloo', help='distributed backend (default: gloo)')
   
       # The parameters below retrieve their default values from SageMaker environment variables, which are
       # instantiated by the SageMaker containers framework.
       # https://github.com/aws/sagemaker-containers#how-a-script-is-executed-inside-the-container
       parser.add_argument('--hosts', type=str, default=ast.literal_eval(os.environ['SM_HOSTS']))
       parser.add_argument('--current-host', type=str, default=os.environ['SM_CURRENT_HOST'])
       parser.add_argument('--model-dir', type=str, default=os.environ['SM_MODEL_DIR'])
       parser.add_argument('--data-dir', type=str, default=os.environ['SM_CHANNEL_TRAINING'])
       parser.add_argument('--num-gpus', type=int, default=os.environ['SM_NUM_GPUS'])
   
       _train(parser.parse_args())
   ```

### 步骤 3：构建容器
<a name="extend-step3"></a>

1. 在 JupyterLab 主目录中，打开 Jupyter 笔记本。要打开新笔记本，请选择**新启动**图标，然后在**笔记本**部分选择 **conda\$1pytorch\$1p39**。

1. 在第一个笔记本单元格中运行以下命令，以便更改到 `docker_test_folder` 目录：

   ```
   % cd ~/SageMaker/docker_test_folder
   ```

   这会将您返回到当前目录，如下所示。

   ```
   ! pwd
   ```

   `output: /home/ec2-user/SageMaker/docker_test_folder`

1. 登录 Docker 以访问基本容器：

   ```
   ! aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 763104351884.dkr.ecr.us-east-1.amazonaws.com
   ```

1. 要构建 Docker 容器，请运行以下 Docker 构建命令，包括末尾句点后的空格：

   ```
   ! docker build -t pytorch-extended-container-test .
   ```

   Docker 构建命令必须从您创建的 Docker 目录运行，在此例中为 `docker_test_folder`。
**注意**  
如果您收到以下错误消息，提示 Docker 找不到 Dockerfile，请确保 Dockerfile 的名称正确并已保存到目录中。  

   ```
   unable to prepare context: unable to evaluate symlinks in Dockerfile path: 
   lstat /home/ec2-user/SageMaker/docker/Dockerfile: no such file or directory
   ```
请记住，`docker` 在当前目录中查找名为 `Dockerfile` 的文件，没有任何扩展名。如果您将其改为其他名称，可以使用 `-f` 标记手动传入文件名。例如，如果您将 Dockerfile 命名为 `Dockerfile-text.txt`，请运行以下命令：  

   ```
   ! docker build -t tf-custom-container-test -f Dockerfile-text.txt .
   ```

### 步骤 4：测试容器
<a name="extend-step4"></a>

1. 要在笔记本实例中本地测试容器，请打开 Jupyter 笔记本。选择**新启动程序**，然后在 **`conda_pytorch_p39`** 框架中选择**笔记本**。其余的代码片段必须从 Jupyter 笔记本实例中运行。

1. 下载 CIFAR-10 数据集。

   ```
   import torch
   import torchvision
   import torchvision.transforms as transforms
   
   def _get_transform():
       return transforms.Compose(
           [transforms.ToTensor(),
            transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
   
   
   def get_train_data_loader(data_dir='/tmp/pytorch/cifar-10-data'):
       transform=_get_transform()
       trainset=torchvision.datasets.CIFAR10(root=data_dir, train=True,
                                               download=True, transform=transform)
       return torch.utils.data.DataLoader(trainset, batch_size=4,
                                          shuffle=True, num_workers=2)
   
   
   def get_test_data_loader(data_dir='/tmp/pytorch/cifar-10-data'):
       transform=_get_transform()
       testset=torchvision.datasets.CIFAR10(root=data_dir, train=False,
                                              download=True, transform=transform)
       return torch.utils.data.DataLoader(testset, batch_size=4,
                                          shuffle=False, num_workers=2)
   
   trainloader=get_train_data_loader('/tmp/pytorch-example/cifar-10-data')
   testloader=get_test_data_loader('/tmp/pytorch-example/cifar-10-data')
   ```

1. 将 `role` 设置为用于创建 Jupyter 笔记本的角色。这用于配置您的 SageMaker AI 估算器。

   ```
   from sagemaker import get_execution_role
   
   role=get_execution_role()
   ```

1. 将以下示例脚本粘贴到笔记本代码单元中，以使用扩展容器配置 A SageMaker I Estimator。

   ```
   from sagemaker.estimator import Estimator
   
   hyperparameters={'epochs': 1}
   
   estimator=Estimator(
       image_uri='pytorch-extended-container-test',
       role=role,
       instance_count=1,
       instance_type='local',
       hyperparameters=hyperparameters
   )
   
   estimator.fit('file:///tmp/pytorch-example/cifar-10-data')
   ```

1. 运行代码单元。该测试输出训练环境配置、用于环境变量的值、数据源以及训练期间获得的损失和准确率。

### 步骤 5：将容器推送至 Amazon Elastic Container Registry (Amazon ECR)
<a name="extend-step5"></a>

1. 成功运行本地模式测试后，您可以将 Docker 容器推送到 [Amazon ECR](https://docs.aws.amazon.com/AmazonECR/latest/userguide/what-is-ecr.html) 并用它来运行训练作业。

   在笔记本单元格中运行以下命令行。

   ```
   %%sh
   
   # Specify an algorithm name
   algorithm_name=pytorch-extended-container-test
   
   account=$(aws sts get-caller-identity --query Account --output text)
   
   # Get the region defined in the current configuration (default to us-west-2 if none defined)
   region=$(aws configure get region)
   
   fullname="${account}.dkr.ecr.${region}.amazonaws.com/${algorithm_name}:latest"
   
   # If the repository doesn't exist in ECR, create it.
   
   aws ecr describe-repositories --repository-names "${algorithm_name}" > /dev/null 2>&1
   if [ $? -ne 0 ]
   then
   aws ecr create-repository --repository-name "${algorithm_name}" > /dev/null
   fi
   
   # Log into Docker
   aws ecr get-login-password --region ${region}|docker login --username AWS --password-stdin ${fullname}
   
   # Build the docker image locally with the image name and then push it to ECR
   # with the full name.
   
   docker build -t ${algorithm_name} .
   docker tag ${algorithm_name} ${fullname}
   
   docker push ${fullname}
   ```

1. 推送容器后，您可以从 A SageMaker I 环境中的任何位置调用 Amazon ECR 镜像。在下一个笔记本单元格中运行以下代码示例。

   如果您想在 SageMaker Studio 中使用此训练容器来使用其可视化功能，也可以在 Studio 笔记本单元中运行以下代码来调用训练容器的 Amazon ECR 映像。

   ```
   import boto3
   
   client=boto3.client('sts')
   account=client.get_caller_identity()['Account']
   
   my_session=boto3.session.Session()
   region=my_session.region_name
   
   algorithm_name="pytorch-extended-container-test"
   ecr_image='{}.dkr.ecr.{}.amazonaws.com/{}:latest'.format(account, region, algorithm_name)
   
   ecr_image
   # This should return something like
   # 12-digits-of-your-account.dkr.ecr.us-east-2.amazonaws.com/tf-2.2-test:latest
   ```

1. 使用从上一步中`ecr_image`检索到的来配置 A SageMaker I 估算器对象。以下代码示例配置了 A SageMaker I PyTorch 估算器。

   ```
   import sagemaker
   
   from sagemaker import get_execution_role
   from sagemaker.estimator import Estimator
   
   estimator=Estimator(
       image_uri=ecr_image,
       role=get_execution_role(),
       base_job_name='pytorch-extended-container-test',
       instance_count=1,
       instance_type='ml.p2.xlarge'
   )
   
   # start training
   estimator.fit()
   
   # deploy the trained model
   predictor=estimator.deploy(1, instance_type)
   ```

### 步骤 6：清理资源
<a name="extend-step6"></a>

**在完成“开始使用”示例后清理资源**

1. 打开 [SageMaker AI 控制台](https://console.aws.amazon.com/sagemaker/)，选择笔记本实例 **RunScriptNotebookInstance**，选择**操作**，然后选择**停止**。停止实例可能需要几分钟时间。

1. 在实例**状态**更改为**已停止**之后，选择**操作**，选择**删除**，然后在对话框中选择**删除**。删除实例可能需要几分钟时间。删除后，笔记本实例将从表中消失。

1. 打开 [Amazon S3 控制台](https://console.aws.amazon.com/s3/)并删除为存储模型构件和训练数据集创建的存储桶。

1. 打开 [IAM 控制台](https://console.aws.amazon.com/iam/)，然后删除 IAM 角色。如果您已创建权限策略，也可以将其删除。
**注意**  
 Docker 容器在运行后会自动关闭。您不需要删除该容器。