使用TensorFlow进行多GPU分布式训练
1、绪论
1.1 使用TensorFlow进行多GPU分布式训练概念
TensorFlow 是一个流行的开源机器学习框架,它支持多GPU分布式训练,允许开发者利用多个GPU并行处理数据和模型参数,从而加速训练过程。多GPU分布式训练在深度学习领域尤其重要,因为它可以极大地提高模型的训练速度和效率。
在使用TensorFlow进行多GPU分布式训练时,通常需要遵循以下步骤:
- 环境准备 :确保你的系统配备了多个GPU,并已正确安装和配置了TensorFlow。此外,你可能还需要安装一些额外的库和工具,如Horovod(一个用于分布式训练的框架)或TensorFlow的内置分布式策略(如
tf.distribute.Strategy
)。 - 定义模型:在TensorFlow中定义你的深度学习模型。这通常涉及到定义模型的层、激活函数、损失函数和优化器等。
- 配置分布式策略 :使用TensorFlow的分布式策略(如
tf.distribute.MirroredStrategy
)来配置你的训练环境。这些策略允许你在多个GPU上复制模型,并自动处理数据分发、梯度计算和模型更新等任务。 - 加载数据:准备你的训练数据,并将其加载到TensorFlow的数据集中。你可能需要使用TensorFlow的数据加载和预处理工具来准备数据。
- 编写训练循环:编写一个训练循环,用于在每个训练步骤中迭代数据、执行前向传播、计算损失、执行反向传播和更新模型权重。在分布式训练中,这个循环将自动分发数据到多个GPU上,并聚合来自各个GPU的梯度来更新模型权重。
- 监控和评估:在训练过程中,使用TensorFlow的监控和评估工具来跟踪模型的性能。这包括计算验证集上的损失和准确度等指标,并在需要时调整模型参数或学习率等超参数。
- 保存和加载模型:在训练完成后,保存你的模型以便将来使用。你可以使用TensorFlow的模型保存和加载功能来保存和加载模型。
通过使用TensorFlow的多GPU分布式训练功能,程序员可以有效地利用多个GPU的计算能力来加速深度学习模型的训练过程,提高模型的训练速度和效率。
1.2 本文讨论的范围
多GPU分布式训练通常有两种方法可以在多个设备之间分配计算:
数据并行,即将单个模型在多个设备或多个机器上进行复制。每个设备或机器处理不同的数据批次,然后它们合并各自的结果。这种设置存在许多变体,它们之间的区别在于不同的模型副本如何合并结果、是否在每个批次后保持同步或是否更松散地耦合等。
模型并行,即单个模型的不同部分在不同的设备上运行,共同处理单个数据批次。这种方法最适合具有自然并行架构的模型,例如具有多个分支的模型。
本文主要讨论数据并行,特别是同步数据并行,其中模型的不同副本在每个处理批次后保持同步。同步性使得模型的收敛行为与单设备训练时看到的相同。
具体来说,本文主要讨论如何使用tf.distribute
API在多个GPU上训练Keras模型,你的代码只需进行最小的修改。这通常适用于单个机器(单主机、多设备训练)上安装的多个GPU(通常为2到16个)。这是研究人员和小规模工业工作流程中最常见的设置。
2、使用TensorFlow进行分布式训练
2.1 系统设置
python
import os
os.environ["KERAS_BACKEND"] = "tensorflow"
import tensorflow as tf
import keras
2.2 开启TensorFlow分布式训练
在单主机,多GPU在这种设置中,程序员有一台机器,上面装有多个GPU(通常为2到16个)。每个设备将运行你的模型的一个副本(称为一个复制品)。为了简单起见,在接下来的内容中,我们将假设我们正在处理8个GPU。
2.2.1 工作原理
在训练的每一步中:
- 当前的数据批次(称为全局批次)被分割成8个不同的子批次(称为局部批次)。例如,如果全局批次有512个样本,那么8个局部批次中的每一个都将有64个样本。
- 8个复制品中的每一个都独立地处理一个局部批次:它们执行前向传播,然后是反向传播,输出模型在局部批次上的损失相对于权重的梯度。
- 来自局部梯度的权重更新会在8个复制品之间高效地合并。由于这是在每个步骤结束时进行的,因此复制品始终保持同步。
在实践中,同步更新模型复制品的权重的过程是在每个单独的权重变量级别上处理的。这是通过一个镜像变量对象来实现的。
2.2.2 使用方法
要使用Keras模型进行单主机、多设备同步训练,程序员可以使用tf.distribute.MirroredStrategy
API。下面是它的工作原理:
- 实例化一个
MirroredStrategy
对象,可选地配置你想要使用的特定设备(默认情况下,该策略将使用所有可用的GPU)。 - 使用策略对象打开一个作用域,并在该作用域内创建所有包含变量的Keras对象。通常,这意味着在分发作用域内创建并编译模型。在某些情况下,对
fit()
的首次调用也可能会创建变量,因此最好也将你的fit()
调用放在作用域中。 - 像往常一样通过
fit()
训练模型。 - 重要的是,我们推荐你在多设备或分布式工作流中使用
tf.data.Dataset
对象来加载数据。
从结构上看,它大致如下:
python
# Create a MirroredStrategy.
strategy = tf.distribute.MirroredStrategy()
print('Number of devices: {}'.format(strategy.num_replicas_in_sync))
# Open a strategy scope.
with strategy.scope():
# Everything that creates variables should be under the strategy scope.
# In general this is only model construction & `compile()`.
model = Model(...)
model.compile(...)
# Train the model on all available devices.
model.fit(train_dataset, validation_data=val_dataset, ...)
# Test the model on all available devices.
model.evaluate(test_dataset)
2.2.3 完整的代码示例
python
def get_compiled_model():
# Make a simple 2-layer densely-connected neural network.
inputs = keras.Input(shape=(784,))
x = keras.layers.Dense(256, activation="relu")(inputs)
x = keras.layers.Dense(256, activation="relu")(x)
outputs = keras.layers.Dense(10)(x)
model = keras.Model(inputs, outputs)
model.compile(
optimizer=keras.optimizers.Adam(),
loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=[keras.metrics.SparseCategoricalAccuracy()],
)
return model
def get_dataset():
batch_size = 32
num_val_samples = 10000
# Return the MNIST dataset in the form of a [`tf.data.Dataset`](https://www.tensorflow.org/api_docs/python/tf/data/Dataset).
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
# Preprocess the data (these are Numpy arrays)
x_train = x_train.reshape(-1, 784).astype("float32") / 255
x_test = x_test.reshape(-1, 784).astype("float32") / 255
y_train = y_train.astype("float32")
y_test = y_test.astype("float32")
# Reserve num_val_samples samples for validation
x_val = x_train[-num_val_samples:]
y_val = y_train[-num_val_samples:]
x_train = x_train[:-num_val_samples]
y_train = y_train[:-num_val_samples]
return (
tf.data.Dataset.from_tensor_slices((x_train, y_train)).batch(batch_size),
tf.data.Dataset.from_tensor_slices((x_val, y_val)).batch(batch_size),
tf.data.Dataset.from_tensor_slices((x_test, y_test)).batch(batch_size),
)
# Create a MirroredStrategy.
strategy = tf.distribute.MirroredStrategy()
print("Number of devices: {}".format(strategy.num_replicas_in_sync))
# Open a strategy scope.
with strategy.scope():
# Everything that creates variables should be under the strategy scope.
# In general this is only model construction & `compile()`.
model = get_compiled_model()
# Train the model on all available devices.
train_dataset, val_dataset, test_dataset = get_dataset()
model.fit(train_dataset, epochs=2, validation_data=val_dataset)
# Test the model on all available devices.
model.evaluate(test_dataset)
2.3 使用回调函数来确保容错性
在使用分布式训练时,程序员应该始终确保有一个从失败中恢复的策略(容错性)。处理这个问题的最简单方法是将ModelCheckpoint
回调传递给fit()
函数,以便定期保存你的模型(例如,每100个批次或每个周期)。然后,程序员就可以从保存的模型中重新启动训练。
下面是一个简单的例子:
python
# Prepare a directory to store all the checkpoints.
checkpoint_dir = "./ckpt"
if not os.path.exists(checkpoint_dir):
os.makedirs(checkpoint_dir)
def make_or_restore_model():
# Either restore the latest model, or create a fresh one
# if there is no checkpoint available.
checkpoints = [checkpoint_dir + "/" + name for name in os.listdir(checkpoint_dir)]
if checkpoints:
latest_checkpoint = max(checkpoints, key=os.path.getctime)
print("Restoring from", latest_checkpoint)
return keras.models.load_model(latest_checkpoint)
print("Creating a new model")
return get_compiled_model()
def run_training(epochs=1):
# Create a MirroredStrategy.
strategy = tf.distribute.MirroredStrategy()
# Open a strategy scope and create/restore the model
with strategy.scope():
model = make_or_restore_model()
callbacks = [
# This callback saves a SavedModel every epoch
# We include the current epoch in the folder name.
keras.callbacks.ModelCheckpoint(
filepath=checkpoint_dir + "/ckpt-{epoch}.keras",
save_freq="epoch",
)
]
model.fit(
train_dataset,
epochs=epochs,
callbacks=callbacks,
validation_data=val_dataset,
verbose=2,
)
# Running the first time creates the model
run_training(epochs=1)
# Calling the same function again will resume from where we left off
run_training(epochs=1)
2.4 tf.data 性能提示
在进行分布式训练时,程序员加载数据的效率往往会变得至关重要。以下是一些提示,以确保程序员的 tf.data 管道尽可能快地运行。
关于数据集批处理的注意事项
在创建数据集时,请确保它是用全局批量大小进行批处理的。例如,如果程序员的8个GPU中的每一个都能够运行一个包含64个样本的批次,那么你应该使用全局批量大小为512。
2.4.1 调用 dataset.cache()
程序员对数据集调用 .cache()
,在数据第一次迭代运行之后,它的数据将被缓存。之后的每一次迭代都将使用缓存的数据。缓存可以位于内存中(默认)或程序员指定的本地文件中。
以下情况下使用缓存可以提高性能:
- 数据在迭代之间不会发生变化
- 从远程分布式文件系统读取数据
- 从本地磁盘读取数据,但数据可以放入内存中,并且工作流程显著受到I/O的限制(例如,读取和解码图像文件)。
2.4.2调用 dataset.prefetch(buffer_size)
在创建数据集之后,几乎总是应该调用 .prefetch(buffer_size)
。这意味着数据管道将与模型异步运行,新的样本将在当前批次样本用于训练模型的同时被预处理并存储在缓冲区中。当前批次结束时,下一个批次将已经在GPU内存中预取。
3、总结
前前面的段落中我们讨论了在使用TensorFlow进行分布式训练时,如何通过优化数据加载管道来提高训练效率。以下是几个关键点:
-
使用
tf.data.Dataset
对象 :在进行多设备或分布式训练时,推荐使用tf.data.Dataset
对象来加载数据。它提供了灵活的数据预处理和增强功能,并且可以与TensorFlow的分布式策略很好地集成。 -
数据批处理:在创建数据集时,确保它使用全局批量大小进行批处理。例如,如果你有多个GPU,每个GPU能够处理一定数量的样本,那么你应该使用这些GPU能够处理的总样本数作为全局批量大小。
-
数据缓存 :通过调用
dataset.cache()
,你可以在第一次迭代数据后将其缓存起来。这样,在后续的迭代中,数据将直接从缓存中读取,从而加速数据加载速度。缓存可以位于内存中(默认),也可以存储在你指定的本地文件中。 -
数据预取 :使用
dataset.prefetch(buffer_size)
可以在模型训练的同时异步地预取和预处理数据。这意味着当模型正在处理当前批次的数据时,下一个批次的数据已经在后台被预取并准备好,从而减少了数据加载和预处理造成的延迟。 -
容错性 :在分布式训练中,确保有从失败中恢复的策略(容错性)是很重要的。你可以使用
ModelCheckpoint
回调来定期保存模型,以便在需要时可以从保存的模型状态恢复训练。
通过结合使用这些技术,程序员可以显著提高分布式训练的效率和性能。