当今组织在 AI 和数据管理方面面临的最大挑战之一是获得可靠的基础设施和计算资源。英特尔 Tiber 开发人员云专为需要概念验证、实验、模型训练和服务部署环境的工程师而构建。与其他难以接近且复杂的云不同,英特尔 Tiber 开发人员云简单易用。该平台对于开发各种类型模型的 AI/ML 工程师特别有价值。使用英特尔的云,AI/ML 工程师可以轻松获取计算和存储,以运行训练和推理工作负载,以及部署应用程序和服务。英特尔选择 MinIO 作为其云的对象存储,因为它带来了简单性、可扩展性、性能以及与云和 AI 生态系统的原生集成。我们很高兴成为首选的对象存储,并很高兴制作此“操作方法”帖子,以加速您采用此平台。我将展示如何使用英特尔 Gaudi AI 加速器训练模型,以及如何在英特尔 Tiber 开发人员云中设置和使用 MinIO(对象存储)。让我们开始吧。本文的完整代码演示可以在这里找到。
创建账户并启动实例
英特尔的云文档包含设置帐户和获取 AI/ML 实验、优化和部署资源的分步指南。本文中提供的代码假定您已完成以下指南。
开始使用 - 本指南将指导您完成创建和登录账户的过程。它还向您展示如何使用云的 Jupyter 服务器。Jupyter Lab 的优点在于您不需要 SSH 密钥即可启动和使用它。
SSH 密钥 - 要创建计算实例,您需要将 SSH 公钥上传到您的账户。本指南介绍如何根据云的规范创建密钥并将其上传到您的账户。请务必将公钥和私钥保存在安全的地方。您将需要私钥才能通过 SSH 连接到计算实例。本指南还介绍了用于连接到实例的 SSH 命令。
Manage Instance (管理实例) – 本指南介绍如何在完成实验后选择、启动和关闭计算节点。
Object Storage (对象存储) – 本指南将指导您在账户中创建存储桶。
拥有帐户后,您将可以访问硬件、软件和服务选项,如下所示。在本文中,我们将使用计算实例和对象存储。
为 MinIO 编程访问做好准备
下面的屏幕截图显示了用于创建存储桶的对话框。请注意,您的存储桶名称以唯一标识符为前缀。这是必要的,因为 MinIO 是 Intel 云的平台服务,它支持云区域中的所有帐户。因此,唯一标识符可防止与其他账户发生名称冲突。
输入存储桶的名称,并根据需要启用版本控制。单击 Create (创建) 按钮后,您应该会看到您的新存储桶,如下所示。本文将使用 MNIST 数据集并以编程方式访问它以训练模型。因此,访问我们的新存储桶需要终端节点、访问密钥和私有密钥。
要查看终端节点地址,请单击新存储桶,然后选择 Details (详细信息) 选项卡,如下图所示。复制此对话框中显示的私有终端节点,因为在设置 MinIO SDK 配置文件时需要它。
由于您需要以编程方式访问存储桶,因此请创建一个委托人并将访问密钥和私有密钥与其关联。为此,请单击 Principles 选项卡。所有现有原则都会显示出来。
接下来,单击 Manage Principles and permissions 按钮。这将打开一个对话框,您可以在其中编辑上面显示的原则。
单击 Create principle 按钮。您现在应该看到用于创建原则的对话框(请参阅下文)。为新原则选择所需的权限,然后单击 Create 按钮。
创建主体后,转到 Manage Principals and Permissions 页面,然后单击新创建的原则。
单击原则后,您应该会看到如下所示的对话框。
您可以在此处创建访问密钥和密钥,以使用 MinIO SDK(或任何其他 S3 投诉库)访问存储桶。单击 Generate password 按钮创建密钥,如下所示。立即将它们复制到配置文件中,因为您无法再次显示它们。
本文的代码示例使用的配置文件是用于设置环境变量的 .env 文件。如下所示,将您的私有终端节点、访问密钥、私有密钥和存储桶名称放入此文件中。
MINIO_URL=s3-phx04-5.tenantiglb.us-region-2.cloud.intel.com:9000
MINIO_ACCESS_KEY={Put access key here.}
MINIO_SECRET_KEY={Put secret key here.}
MINIO_SECURE=false
BUCKET_NAME={Put full bucket name here.}
现在,为训练数据创建计算实例和存储桶后,您就可以编写一些代码了。让我们将 MNIST 数据集上传到我们的新存储桶。
将数据上传到 MinIO
torchvision 软件包使检索 MNIST 数据集中的图像变得容易。下面的函数使用此包下载一组压缩的文件,提取图像,并将其发送到 MinIO。这如下面的代码示例所示。为简洁起见,省略了一些支持功能。完整的代码可以在本文的代码下载中的 data_utlities.py 模块中找到。
import PIL.Image
from dotenv import load_dotenv
from minio import Minio
from minio.error import S3Error
import numpy as np
import PIL
import torch
from torchvision import datasets, transformsdef load_mnist_to_minio(bucket_name: str) -> Tuple[int,int]:''' Download and load the training and test samples.'''logger = create_logger()train = datasets.MNIST('./mnistdata/', download=True, train=True)test = datasets.MNIST('./mnistdata/', download=True, train=False)train_count = 0for sample in train:random_uuid = uuid.uuid4()object_name = f'/train/{sample[1]}/{random_uuid}.jpeg'put_image_to_minio(bucket_name, object_name, sample[0])train_count += 1if train_count % 100 == 0:logger.info(f'{train_count} training objects added to {bucket_name}.')test_count = 0for sample in test:random_uuid = uuid.uuid4()object_name = f'/test/{sample[1]}/{random_uuid}.jpeg'put_image_to_minio(bucket_name, object_name, sample[0])test_count += 1if test_count % 100 == 0:logger.info(f'{test_count} testing objects added to {bucket_name}.')return train_count, test_countdef put_image_to_minio(bucket_name: str, object_name: str, image: PIL.Image.Image) -> None:'''Puts an image byte stream to MinIO.'''logger = create_logger()url, access_key, secret_key, secure = get_minio_credentials()try:# Create client with access and secret keyclient = Minio(url, # host.docker.internalaccess_key, secret_key,secure=secure)image_byte_array = image_to_byte_stream(image)content_type = 'application/octet-stream'response = client.put_object(bucket_name, object_name, image_byte_array,-1, content_type, part_size = 1024*1024*5)except S3Error as s3_err:logger.error(f'S3 Error occurred: {s3_err}.')raise s3_errexcept Exception as err:logger.error(f'Error occurred: {err}.')raise err
现在,您的数据集已加载到存储桶中,让我们看看如何访问它以训练模型。
从 Data Loader 使用 MinIO
在模型训练管道中,有两个位置可以从持久存储加载数据。如果您的数据完全适合内存,则可以在调用训练函数之前,在训练管道开始时将所有内容加载到内存中。(我们将在下一节中创建此训练函数。如果您的数据集足够小,可以完全放入内存中,则此方法有效。如果您以这种方式加载数据,您的训练函数将是计算绑定的,因为它不必进行任何 IO 调用即可从对象存储中获取数据。但是,如果您的数据集太大而无法放入内存,则每次将新批次样本发送到模型进行训练时,都需要检索数据。这将产生 IO 绑定的训练函数。由于我们想演示使用 Gaudi 加速器进行模型训练的好处,因此我们将创建一个计算绑定的训练函数。用于创建自定义 Dataset 并将其加载到 Dataloader 中的 Pytorch 代码如下所示。请注意,所有数据都加载到 ImageDatasetFull 类的构造函数中。所有 MNIST 图像都从 MinIO 中检索,并存储在从此类创建的对象的属性中。这一切都发生在首次创建对象时。如果我们想在每次将一批数据发送到我们的模型进行训练时加载图像,那么我们需要只使用对象名称列表创建此对象,并将实际图像的加载移动到 getitem() 函数。
class ImageDatasetFull(Dataset):def __init__(self, bucket_name: str, X, y, transform=None):self.bucket_name = bucket_nameself.y = yself.transform = transformraw_images = du.get_images_from_minio(bucket_name, X)images = torch.stack([transform(x) for x in raw_images], dim=0)self.X = imagesdef __len__(self):return len(self.y)def __getitem__(self, index):return self.X[index], self.y[index]
def create_mnist_training_loader(bucket_name: str, loader_type:str, batch_size:int, smoke_test_size: float=0) -> Tuple[Any]:# Start of load time.start_time = time.perf_counter()# Define a transform to normalize the datatransform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.5,), (0.5,))])# Get a list of objects and split them according to train and test. X_train, y_train, _, _ = du.get_mnist_lists(bucket_name)if smoke_test_size > 0:train_size = int(smoke_test_size*len(X_train))X_train = X_train[0:train_size]y_train = y_train[0:train_size]train_dataset = ImageDatasetFull(bucket_name, X_train, y_train, transform=transform)train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=1)return train_loader, (time.perf_counter()-start_time)
现在我们已经将数据加载到内存中,让我们看看如何使用 Gaudi2 和英特尔 Developer Cloud 来训练模型。
使用 PyTorch 的 Intel Gaudi 加速器
PyTorch HPU 软件包支持 Intel 的多架构处理 (HPU) 实用程序。通常,HPU 允许开发人员编写针对 Intel 硬件范围优化的 PyTorch 应用程序,例如 CPU、Gaudi 加速器、GPU 以及 Intel 可能构建的任何未来加速器。HPU 抽象使用 PyTorch 提供的通用接口。因此,开发人员可以编写在不同硬件加速器之间动态切换的代码,而无需进行大量重构。这篇文章将使用它来检测 Gaudi 并将张量移动到 Gaudi 的内存中。使用任何类型的加速器时,一种常见的编码模式是首先检查 GPU 或 AI 加速器(如果存在),然后将模型、训练集、验证集和测试集移动到处理器的内存中。这通常是在训练模型的函数中完成的。下面的函数来自本文的代码下载。(这个函数就是我之前提到的训练函数。突出显示的代码显示了如何检查是否存在 Intel 加速器并将模型和训练集移动到设备。要使此检查在 Gaudi 中正常工作,您需要以下导入。您不会直接使用此模块,但需要导入它。
将 habana_frameworks.torch.core 导入为 htcore
请注意,从训练集中移动保存特征和标签的张量是在批处理循环中完成的。PyTorch 数据加载器没有用于将整个数据集移动到目标设备的“to”函数。这是有充分理由的:大型数据集会很快耗尽处理器的内存。对于内存比新 GPU 少的旧 GPU 尤其如此。最佳做法是,在模型需要进行训练之前,仅将当前训练批次所需的张量移动到加速器。
def train_model(model: nn.Module, loader: DataLoader, training_parameters: Dict[str, Any]) -> List[float]:logger = du.create_logger()device = torch.device('hpu' if torch.hpu.is_available() else 'cpu')model.to(device)logger.info(f'Model created on device {device}')loss_func = nn.NLLLoss()optimizer = optim.SGD(model.parameters(), lr=training_parameters['lr'], momentum=training_parameters['momentum'])# Epoch loop.compute_time_by_epoch = []for epoch in range(training_parameters['epochs']):total_loss = 0batch_count = 0total_epoch_compute_time = 0# Batch loop.for images, labels in loader:# Start of compute time for the batch.start = time.perf_counter()# Move to the specified device.# shape = [32, 1, 28, 28]images, labels = images.to(device), labels.to(device)# Flatten MNIST images into a 784 long vector.# shape = [32, 784]images = images.view(images.shape[0], -1)# Training passoptimizer.zero_grad()output = model(images)loss = loss_func(output, labels)loss.backward()optimizer.step()# Loss calculations total_loss += loss.item()batch_count +=1# Track compute timetotal_epoch_compute_time += time.perf_counter() - startcompute_time_by_epoch.append(total_epoch_compute_time)logger.info(f'Epoch {epoch+1} - Training loss: {total_loss/batch_count}.')return compute_time_by_epoch
现在,我们已经知道如何将模型和张量从数据加载器移动到 Gaudi,并有办法从对象存储中获取数据,让我们把所有东西放在一起并运行几个实验。
把它们放在一起
下面的函数将所有内容整合在一起。它将创建我们的数据加载器并将它们传递给我们的 train_model 函数。请注意,所有内容都经过检测,以便从我们的代码中获取性能指标。运行此函数后,我们将看到 IO 时间与计算时间的关系。我们还将能够仅使用 CPU,然后再次使用 Gaudi 运行相同的代码。
def setup_local_training(training_parameters: Dict[str, Any], loader_type: str):logger = du.create_logger()device = torch.device('hpu' if torch.hpu.is_available() else 'cpu')logger.info(f'PyTorch Version: {torch.__version__}')logger.info(f'Using device: {device}')#train_data, test_data, load_time_sec = ru.get_ray_dataset(training_parameters)train_loader, load_time_sec = tu.create_mnist_training_loader(training_parameters['bucket_name'], loader_type, training_parameters['batch_size'])logger.info(f'Data Loader Creation Time (in seconds) = {load_time_sec}')# Train the model and log training metrics.model = tu.MNISTModel(training_parameters['input_size'], training_parameters['hidden_sizes'], training_parameters['output_size'])start_time = time.perf_counter()compute_time_by_epoch = train_model(model, train_loader, training_parameters)training_time_sec = time.perf_counter() - start_timecompute_time_sec = 0for epoch_time in compute_time_by_epoch:compute_time_sec += epoch_timelogger.info(f'Compute Time (in seconds) = {compute_time_sec}')logger.info(f'I/O Time (in seconds) = {training_time_sec - compute_time_sec}')logger.info(f'Total Training Time (in seconds) = {training_time_sec}')test_loader, load_time_sec = tu.create_mnist_testing_loader(training_parameters['bucket_name'], loader_type, training_parameters['batch_size'])tu.test_model_local(model, test_loader, training_parameters)
下面的代码段将调用此函数并传入相应的超参数。
# Hyperparameterstraining_parameters = {'batch_size': 32,'bucket_name': BUCKET_NAME,'epochs': 3,'hidden_sizes': [1024, 1024, 1024, 1024],'input_size': 784,'lr': 0.025,'momentum': 0.5,'output_size': 10,'smoke_test_size': 0,'use_gpu': False,}setup_local_training(training_parameters, 'full')
使用 use_gpu = False 在我们的计算实例上运行上面的 setup 函数会产生以下输出。
INFO | Data Set Size: 60000 samples.INFO | Using device: cpuINFO | Model moved to device: cpuINFO | Epoch 1 - Training loss: 0.4534 - Compute time: 15.3998 IO time: 7.3033.INFO | Epoch 2 - Training loss: 0.1475 - Compute time: 14.7520 IO time: 7.4030.INFO | Epoch 3 - Training loss: 0.1046 - Compute time: 15.1024 IO time: 7.6316.INFO | Compute Time (in seconds) = 45.2544INFO | I/O Time (in seconds) = 22.3410INFO | Total Training Time (in seconds) = 67.5955INFO | Total Experiment Time (in seconds) = 67.6832
使用 Gaudi 运行相同的代码会导致输出显示我们的计算时间显著减少。
INFO | Data Set Size: 60000 samples.INFO | Using device: hpuINFO | Model moved to device: hpuINFO | Epoch 1 - Training loss: 0.4521 - Compute time: 3.5001 IO time: 8.0007.INFO | Epoch 2 - Training loss: 0.1481 - Compute time: 3.4549 IO time: 7.8584.INFO | Epoch 3 - Training loss: 0.1035 - Compute time: 3.8153 IO time: 8.0103.INFO | Compute Time (in seconds) = 10.7705INFO | I/O Time (in seconds) = 29.7047INFO | Total Training Time (in seconds) = 40.4752INFO | Total Experiment Time (in seconds) = 40.5629
上述结果特别有趣,因为快速加速器可以将计算受限的训练工作负载(计算耗时最长)转变为 IO 受限的训练工作负载(数据访问耗时最长)。证明快速加速器必须与快速网络和快速存储携手使用。
总结
在本文中,我展示了如何设置英特尔 Tiber Developer Cloud 进行机器学习实验。这需要创建一个帐户、设置计算实例、创建 MinIO 存储桶和设置 SSH 密钥。创建资源后,我演示了如何编写一些函数来上传和检索数据。我还讨论了可以放入内存的小型数据集和无法放入内存的大型数据集的数据加载注意事项。使用 Intel 的 Gaudi 加速器非常简单,开发人员将识别 PyTorch 中 hpu 包的接口。我展示了检测 Gaudi 并将张量移动到它的基本代码。在这篇文章的最后,我同时使用 CPU 和 Gaudi 加速器训练了一个实际模型。这两个实验展示了 Gaudi 的性能提升,并为使用快速存储和带有快速加速器的快速网络提供了理由。