之前只是直接用的一些API,大概知道哪些会方便一些快一些,但是具体的加速方式没细研究,也没有严格对比过对速度的提升。前一阵子做一些的项目,发现数据集在很大的情况下gpu的利用率很低,训练速度很慢,回想起来以前经常会在nvidia-smi里看到gpu利用率一直周期性跳动的问题。正好最近研究训练时GPU内存占用问题,就顺便系统地研究一下。本来想先看看pytorch的,看到有人提出Estimator+tf.data的解决方案,就先对比研究一下tensorflow的提速trick。
不同的任务,数据量,前处理复杂度,batch size, 不同的硬件,CPU, GPU,集群等,如何在特定条件下,取得最优的训练效率,是一个很有意义的课题。不管是在校的学生还是休闲炼丹爱好者,在抱怨算力太低之前,先把自己手上的这块cpu和gpu的内存和利用率压榨干净,也许能解决不少问题。
本文内容:
关于tf.data,官网上是这么介绍的:除GPU 和 TPU 等硬件加速设备外, 高效的数据输入pipeline也可以很大程度提升模型的能,减少模型的训练时间。
这里的“高效pipeline”简单来讲指的是:GPU要等CPU准备好数据才能跑,能否让CPU提前准备,趁GPU一个batch的数据时,把下一个epoch的数据准备好呢?
CPU GPU 互等 CPU提前准备现在CPU都是多核的,如果数据的读取、预处理1、预处理2等都跑在一个cpu上,那么其他cpu也是空闲的,所以把数据的读取、预处理1、预处理2都分别分配在不同的cpu上,可以最大限度的减少idle的时间。
上述功能可以通过 tf.data.Dataset.prefetch(n) 这个函数实现,这里最重要的是这个n的设置,最好设置成一个training step需要使用的elements个数。源码
构建dataset最常用的方式就是 tf.data.Dataset.from_tensor_slices(),也可直接读取TFrecord,CSV等格式数据,这个文档也都有介绍。
tf.contrib.data.map_and_batch:
如果batch size成百上千的话,并行batch creation可以进一步提高pipline的速度,可以把map和batch混在一起来并行处理,使用上直接把map和batch两个函数替换为:
dataset = dataset.apply(tf.contrib.data.map_and_batch(map_func=parse_fn, batch_size=batch_size))
Parallelize Data Extraction
单机时使用pipline可以取得很好的效果,但是在分布式训练时pipline则可能会成为瓶颈。
本地和远程存储的区别:
除此之外,即使把远端的数据读到了本地,还是需要串并转换和解码(TCP/IP协议),这些都会增加overhead。相比本地存储的数据,如果不进行有效的prefetch分布式训练的overhead会更长。
为了减轻数据导出的overhead,tensorflow提供了tf.contrib.data.parallel_interleave(并行交错) ,The number of datasets to overlap(dataset的重叠) can be specified by the cycle_length argument.
使用时把第一行改为第二行
#dataset = files.interleave(tf.data.TFRecordDataset)
dataset = files.apply(tf.contrib.data.parallel_interleave(tf.data.TFRecordDataset, cycle_length=FLAGS.num_parallel_readers))
下图说明了为 parallel_interleave 转换提供 cycle_length=2 的效果
在把数据传给网络前,我们进行一些预处理,比如map、batch、cache、prefetch、interleave、shuffle等等,这些操作的前后顺序也会影响处理速度。
最佳策略:
具体的实现有的前文已经写过,没提到的可以参考(官方文档的翻译+归纳):
1. Tensorflow高效流水线Pipelinewww.cnblogs.comPS:关于shuffle(),batch(),repect()的不同顺序的含义(函数首字母表示):
整个epoch和每个batch的数据分布的多样性,会影响训练模型的鲁棒性。
使用Iterator来得到数据集dataset类型的数据接口。
Dataset类型提供直接生成迭代器的函数:
注意最后收尾的get_next要放在训练的循环外,只把调用的img,label ==sess.run(dataset) 放在循环内,不然会越跑越慢,原因显而易见,一定要搞清迭代器的逻辑。
为了对比效果,先搭了一个普通读取数据方式的二分类训练流程,因为要探究如何提升低算力下的训练效率,就先在自己电脑上跑。
CPU : AMD Ryzen 5 3600 || GPU: 1660s
数据量为1000train+400valid的数据集,格式为jpeg,在前处理会统一resize到(64,64)。label为0或1,image path和label储存在txt文件中。
为了方便训练,测试,可视化数据集等脚本的调用,尽量把读取数据的代码单独存放。
普通读取方式
class Dataset(object):
def __init__(self, dataset_type):
self.annot_path = './DATA/train.txt' if dataset_type == 'train' else './DATA/test.txt'
self.batch_size = 8 if dataset_type == 'train' else 1 # 2
self.data_aug = True if dataset_type == 'train' else False
self.train_input_size = 64
self.annotations = self.load_annotations(dataset_type)
self.img_num = len(self.annotations)
self.step_per_epoch = int(np.ceil(self.img_num / self.batch_size))
self.step_index_epoch = 0
def load_annotations(self, dataset_type):
with open(self.annot_path, 'r') as f:
txt = f.readlines()
annotations = [line.strip() for line in txt if len(line.strip().split()[1:]) != 0]
np.random.shuffle(annotations)
return annotations
def __iter__(self):
return self
def __next__(self):
with tf.device('/cpu:0'):
batch_image = np.zeros((self.batch_size, self.train_input_size, self.train_input_size, 3))
batch_label = np.zeros((self.batch_size, 1))
i = 0
if self.step_index_epoch < self.step_per_epoch: # in one epoch
while i < self.batch_size: # in one batch
# try:
img_index = self.step_index_epoch * self.batch_size + i
if img_index >= self.img_num: # last batch cannot be rounded up
img_index -= self.img_num
annotation = self.annotations[img_index] # txt ===> ano
image, label = self.parse_annotation(annotation)
batch_image[i, :, :, :] = image # (h,w,c)
batch_label[i,:] = label
i += 1
self.step_index_epoch += 1
return batch_image, batch_label
else: # next epoch
self.step_index_epoch = 0
np.random.shuffle(self.annotations)
raise StopIteration
def parse_annotation(self, annotation): # get image and label from txtfile
line = annotation.split()
image_path = line[0]
label = line[1]
if not os.path.exists(image_path):
print(("%s does not exist ... " % image_path))
image = np.array(cv2.imread(image_path))
image = cv2.resize(image,(self.train_input_size, self.train_input_size))
return image, label
def __len__(self):
return self.step_per_epoch
训练时通过以下代码调用:
for epoch in range(1, self.epoch):
train_epoch_loss, test_epoch_loss = [], []
for train_data in Dataset('train'):
_, train_summary, train_step_loss, train_predict, _, train_step = self.sess.run(
[self.train_op, self.merge_train_op, self.loss, self.predict, self.acc, self.step],
feed_dict = {
self.inputs: train_data[0],
self.labels: train_data[1],
self.training:True })
train_epoch_loss.append(train_step_loss)
for test_data in Dataset('test'):
test_step_loss, test_predict = self.sess.run(
[self.loss, self.predict],
feed_dict = {
self.inputs: test_data[0],
self.labels: test_data[1],
self.training:False })
test_epoch_loss.append(test_step_loss)
train_epoch_loss, test_epoch_loss = np.mean(train_epoch_loss), np.mean(test_epoch_loss)
结果(单位默认为秒):
第1个epoch: train_cost:3.426 // test_cost:1.003
第2,3,4.....个epoch : train_cost: 1.76 // test_cost: 0.83 (后面基本在这个数值附近保持很小的波动)
GPU利用率在30 - 50 %间浮动
CPU:可以看到大概只有6个线程比较忙,其他的虽然也被调动起来了但基本上在摸鱼。
使用td.data读取数据
这里有很多细节,建议还是去看一下tensorflow的官方文档:
tf.data: Build TensorFlow input pipelines | TensorFlow Coretensorflow.google.cn首先读取数据: tf.data.Dataset.from_tensor_slices((image_paths, labels))
新构建的基于tf.data读取方式的dataset.py如下
def Dataset(file_list, batch_size):
def Dataset(file_list, batch_size):
image_paths, labels, img_num = gen_data(file_list)
dataset = tf.data.Dataset.from_tensor_slices((image_paths, labels))
# dataset = tf.data.TFRecordDataset(tf_records_filename)
dataset = dataset.map(_parse_data, num_parallel_calls=6)
dataset = dataset.shuffle(buffer_size=1000)
dataset = dataset.batch(batch_size)
dataset = dataset.repeat(1)
dataset = dataset.prefetch(2)
iterator = dataset.make_one_shot_iterator()
# iterator = dataset.make_initializable_iterator()
next_element = iterator.get_next()
return iterator, next_element, img_num
def _parse_data(image_path, label):
image_size = 64
image_channel = 3
file_contents = tf.read_file(image_path)
image = tf.image.decode_jpeg(file_contents, image_channel)
image = tf.image.resize_images(image, (image_size, image_size), method=0)
# image = tf.image.random_brightness(image, max_delta=60)
# image = tf.image.random_contrast(image, lower=0.2, upper=1.8)
image = tf.cast(image, tf.float32)
image = image / 256.0
label = tf.expand_dims(label,axis=0)
return (image, label)
def gen_data(file_list):
with open(file_list, 'r') as f:
lines = f.readlines()
image_paths, labels = [], []
for line in lines:
line = line.strip().split()
image_path = line[0]
label = line[1]
image_paths.append(image_path)
labels.append(label)
image_paths = np.asarray(image_paths, dtype=np.str)
labels = np.asarray(labels, dtype=np.float32)
return image_paths, labels, len(lines)
训练时通过以下代码调用
train_iterator, train_next_element, len_train = Dataset('./DATA/train.txt', batch_size=self.batch_size_train)
test_iterator, test_next_element, len_test = Dataset('./DATA/test.txt', batch_size=self.batch_size_test)
image_batch_train_np, label_batch_train_np = train_next_element
image_batch_test_np, label_batch_test_np = test_next_element
# self.sess.run(test_iterator.initializer) 该阶段只需要初始化一次
for epoch in range(1, self.epoch):
train_epoch_loss, test_epoch_loss = [], []
# self.sess.run(train_iterator.initializer)#每个epoch初始化一次
for i in range(int(np.ceil(len_train/self.batch_size_train))):
image_batch, label_batch = self.sess.run([image_batch_train_np, label_batch_train_np])
_, train_summary, train_step_loss, train_predict, _, train_step = self.sess.run(
[self.train_op, self.merge_train_op, self.loss, self.predict, self.acc, self.step],
feed_dict = {self.inputs: image_batch,
self.labels: label_batch,
self.training:True })
train_epoch_loss.append(train_step_loss)
for i in range(int(np.ceil(len_test / self.batch_size_test))):
image_batch, label_batch = self.sess.run([image_batch_test_np, label_batch_test_np])
test_step_loss, test_predict = self.sess.run(
[self.loss, self.predict],
feed_dict = {self.inputs: image_batch,
self.labels: label_batch,
self.training:False })
test_epoch_loss.append(test_step_loss)
PS:注释部分为使用iterator = dataset.make_initializable_iterator()的代码,会导致速度变慢约20%。
结果:
第一个epoch依旧会长一些,但比未使用tf.data时还是快了很多:
第1个epoch:train_cost: 3.24916 // test_cost:1.05556
第2,3,4.....个epoch :train_cost:1.22807 // test_cost:0.69816(后面基本在这个数值附近保持很小的波动)
CPU:
GPU:利用率在45-70%之间周期浮动
PS: 理论每个任务的 num_parallel_calls 的设置和 prefetch()的设置都要根据具体情况来设定。上述实验仅证明了tf.data本身的加速效果。然而这两个参数的设定实测发现影响不大,原因是batch,数据量太小,也几乎没有前处理。
此时将image size改为256,GPU利用率稳定在了96%。但两个参数对训练速度影响仍不大:
实际上cpu只在每个epoch的load数据阶段利用率比较大。那么当前处理任务繁重时,是否可以调整cpu和gpu的工作量比例,达到最佳速度呢?
image size改为64(保证显存够),在decode_jpeg后,加入对image做10次翻转的前处理:
不设置num_parallel_calls :
for i in range(10):
image = tf.image.flip_up_down(image)
此时将num_parallel_calls 设置为6:
可以看出在prefetch=120时,根据图中CPU各个线程的利用率曲线跳变幅度,看出cpu的利用率已经比较稳定,不会在每个step都出现相对空闲(周期性突降)。
当cpu和gpu的并行程度较大时,可以发现gpu的利用率不仅不会周期跳变(实际上出现较低利用率时往往说明训练并行度很差, 会有较长时间停留在这个最低利用率上),随着最低利用率的上升,峰值利用率也会稍有降低,维持在一个小范围内波动。(这可能是gpu内部的并行因输入数据时域上的均匀而“节省”了峰值算力)
但要注意在前处理比较复杂的情况下,设置太大的prefetch会导致gpu运行完数据后,因cpu仍在预处理下一批数据而导致的gpu空闲。整体速度可能会不增反降。(prefetch=480时)
上述实验因网路太小,最后得到的最佳prefetch相对较大。实际训练时要根据各部分计算量比例权衡。
上文没有对比使用tf.image的速度提升,当前处理较复杂时可以考虑使用tf.image或者把前处理后的数据保存为TFrecord。
如果你的网络大小和batch size等参数由于任务原因已经固定,同时cpu性能较差,由于数据搬运造成的耗时始终达不到一个和gpu较好的平衡,且内存较大,可以考虑使用cache函数,将 dataset = dataset.cache('./cache.data')加在map函数前既可。
pytorch的框架Dataloader,封装了num_workers和pin_memory两个参数,只需要在原来使用基础上调整两个参数即可。
dataloader = DataLoader(
xxdataset,
batch_size=args.train_batchsize,
shuffle=True,
num_workers=args.workers,
drop_last=False,
pin_memory=False)
num_workers:同理,增加子线程的个数,来分担主线程的数据处理压力,多线程协同处理数据和传输数据,不用放在一个线程里负责所有的预处理和传输任务。线程数的设置太大太小都会降低训练效率。num_wokers尽量设置为偶数。
pin_memory:当服务器或者电脑的内存较大,性能较好的时候,建议打开pin_memory,可以省掉了将数据从CPU传入到缓存RAM里面,再给传输到GPU上的时间。设置为True时则将数据直接映射到GPU的相关内存块上,节省了一些数据传输的时间。
在模型训练过程中,不只要关注GPU的各种性能参数,还需要查看CPU处理的怎么样。。但是对于CPU,不能一味追求超高的占用率。很多情况下CPU占用率很高,但时间主要用于加载和传输数据上。实际上很可能浪费了数倍的加载数据时间,当DataLoader的num_workers=0(default),或者不设置这个参数,会出现这个情况。
在一语义任务上实测发现,num_wokers设置为核心数时达到了最快训练速度,且GPU的利用率也稳定在了86-100%的状态。
未设置pin_memory:
在num_wokers = 6的基础上,打开pin_memory,耗时进一步降低到了3.88s。
torch.multiprocessing
是对 Python 的 multiprocessing
模块的一个封装,并且百分比兼容原始模块,也就是可以采用原始模块中的如 Queue
、Pipe
、Array
等方法。并且为了加快速度,还添加了一个新的方法--share_memory_()
,它允许数据处于一种特殊的状态,可以在不需要拷贝的情况下,任何进程都可以直接使用该数据。
通过该方法,可以共享 Tensors
、模型的参数 parameters
,可以在 CPU 或者 GPU 之间共享它们。
下面展示一个采用多进程训练模型的例子:
# Training a model using multiple processes:
import torch.multiprocessing as mp
def train(model):
for data, labels in data_loader:
optimizer.zero_grad()
loss_fn(model(data), labels).backward()
optimizer.step() # This will update the shared parameters
model = nn.Sequential(nn.Linear(n_in, n_h1),
nn.ReLU(),
nn.Linear(n_h1, n_out))
model.share_memory() # Required for 'fork' method to work
processes = []
for i in range(4): # No. of processes
p = mp.Process(target=train, args=(model,))
p.start()
processes.append(p)
for p in processes:
p.join()
在你已经达到计算资源上限的情况下,你的batch size仍然太小(比如8),然后我们需要模拟一个更大的batch size来进行梯度下降,以提供一个良好的估计。
假设我们想要达到128的batch size大小。我们需要以batch size为8执行16个前向传播和向后传播,然后再执行一次优化步骤。
# clear last step
optimizer.zero_grad()
# 16 accumulated gradient steps
scaled_loss = 0
for accumulated_step_i in range(16):
out = model.forward()
loss = some_loss(out,y)
loss.backward()
scaled_loss += loss.item()
# update weights after 8 steps. effective batch = 8*16
optimizer.step()
# loss is now scaled up by the number of accumulated batches
上述代码在lightning中,全部都给你做好了,只需要设置accumulate_grad_batches=16
:
trainer = Trainer(accumulate_grad_batches=16)
trainer.fit(model)
Lightning是在Pytorch之上的一个封装,它可以自动训练,同时可以完全控制关键的模型组件。
eg:为MNIST定义LightningModel并使用Trainer来训练模型。
from pytorch_lightning import Trainer
model = LightningModule(…)
trainer = Trainer()
trainer.fit(model)
为了记录日志存储loss时,loss仍然包含有整个图的副本。在这种情况下,调用.item()来释放它:
losses.append(loss.item())
Lightning会确保不会保留计算图的副本。
16bit精度是将内存占用减半的惊人技术。大多数模型使用32bit精度数字进行训练。然而,最近的研究发现,16bit模型也可以工作得很好。混合精度意味着对某些内容使用16bit,但将权重等内容保持在32bit。
要在Pytorch中使用16bit精度,请安装NVIDIA的apex库,并对你的模型进行这些更改。
# enable 16-bit on the model and the optimizer
model, optimizers = amp.initialize(model, optimizers, opt_level= O2 )
# when doing .backward, let amp do it so it can scale the loss
with amp.scale_loss(loss, optimizer) as scaled_loss:
scaled_loss.backward()
amp包会处理好大部分事情。如果梯度爆炸或趋向于0,它甚至会缩放loss。
在lightning中,启用16bit并不需要修改模型中的任何内容,也不需要执行我上面所写的操作。设置Trainer(precision=16)
就可以了。
trainer = Trainer(amp_level= O2 , use_amp=False)
trainer.fit(model)
夕小瑶:训练效率低?GPU利用率上不去?快来看看别人家的tricks吧~
【Tensorflow】tf.data API使用方法
1. Tensorflow高效流水线Pipeline
tf.data学习指南(超实用、超详细)_我的博客有点东西-CSDN博客
TensorFlow tf.data 导入数据(tf.data官方教程) * * * * *
深度学习PyTorch,TensorFlow中GPU利用率较低,CPU利用率很低,且模型训练速度很慢的问题总结与分析_是否龍磊磊真的一无所有的博客-CSDN博客
鑫鑫淼淼焱焱:PyTorch系列 | 如何加快你的模型训练速度呢?
9个让PyTorch模型训练提速的技巧!_Evan-yzh的博客-CSDN博客