Keras / Tensorflow中的类生成器(继承序列)线程安全吗?

时间:2018-10-22 15:02:34

标签: python tensorflow keras generator

为了更快地训练模型,在CPU上填充/生成批次并在GPU上并行运行模型的训练似乎是一个好习惯。为此,可以使用Python编写一个生成器类,该类继承Sequence类。

以下是文档的链接: https://www.tensorflow.org/api_docs/python/tf/keras/utils/Sequence

文档指出的重要内容是:

  

Sequence是进行多处理的更安全方法。这种结构   保证网络在每个样本上仅训练一次   生成器不是这样的时代。

它给出了一个简单的代码示例,如下所示:

from skimage.io import imread
from skimage.transform import resize
import numpy as np
import math

# Here, `x_set` is list of path to the images
# and `y_set` are the associated classes.

class CIFAR10Sequence(Sequence):

    def __init__(self, x_set, y_set, batch_size):
        self.x, self.y = x_set, y_set
        self.batch_size = batch_size

    def __len__(self):
        return math.ceil(len(self.x) / self.batch_size)

    def __getitem__(self, idx):
        batch_x = self.x[idx * self.batch_size:(idx + 1) *
        self.batch_size]
        batch_y = self.y[idx * self.batch_size:(idx + 1) *
        self.batch_size]

        return np.array([
            resize(imread(file_name), (200, 200))
               for file_name in batch_x]), np.array(batch_y)

在我看来,在模型中理想地要做的是创建此生成器类的实例并将其提供给fit_generator(...)函数。

gen = CIFAR10Sequence(x_set, y_set, batch_size)
# Train the model
model.fit_generator(generator=gen,
                    use_multiprocessing=True,
                    workers=6)

以下是Keras文档的引文:

  

使用keras.utils.Sequence保证顺序并保证   使用时每个历元的每个输入的单一用法   use_multiprocessing=True

在这种形状下,我认为此设置是线程安全的。 问题1):我的假设正确吗?

一个令人困惑的事情是,在Windows 10上参数use_multiprocessing可能未设置为True。似乎只能在Linux上将其设置为True。 (我不知道其他平台上的情况如何。)但是workers参数仍然可以设置为大于0的值。

让我们看一下这两个参数的定义:

  

workers: 整数。使用时最多可启动的进程数   基于进程的线程。如果未指定,则worker将默认为1。   0,将在主线程上执行生成器。

     

use_multiprocessing: 布尔值。如果为True,则使用基于进程的线程。如果   未指定,use_multiprocessing将默认为False。注意   因为此实现依赖于多处理,所以您不应   将不可拾取的参数传递给生成器,因为它们无法传递   轻松进行子进程。

因此,通过使用workers参数,似乎有可能创建多个过程来独立于use_multiprocessing是否为True来加快训练速度。

如果要使用继承Sequence的生成器类(在Windows 10上为 ),则必须将use_multiprocessing设置为False,如下所示:

gen = CIFAR10Sequence(x_set, y_set, batch_size)
# Train the model
model.fit_generator(generator=gen,
                    use_multiprocessing=False,  # CHANGED
                    workers=6)

由于worker = 6,这里仍然有多个进程在运行。

问题2):此设置仍然是线程安全的吗?或者在将use_multiprocessing参数设置为False之后,线程安全特性现在丢失了吗?根据文档,我无法弄清楚。

问题3)仍与此主题相关...当以这种方式进行训练时,CPU生成数据并在GPU上进行训练,如果所训练的模型较浅,由于GPU一直在等待来自CPU的数据,因此GPU的使用率最终非常低,CPU的使用率显着提高。在这种情况下,是否有办法利用一些GPU资源进行数据生成?

2 个答案:

答案 0 :(得分:2)

在看过这篇文章的人中,似乎没有人给出最终答案,因此我想给出对我有用的答案。由于该领域缺乏文档,我的答案可能缺少一些相关细节。请随时添加我在这里没有提及的更多信息。

貌似,在 Windows 中不支持使用Python编写继承Sequence类的生成器类。 (您似乎可以使其在Linux上运行。)要使其运行,您需要设置参数use_multiprocessing=True(使用类方法)。但是如上所述,它在Windows上不起作用,因此您必须将use_multiprocessing设置为False(在Windows上)。 但是,这并不意味着多重处理在Windows上不起作用。即使您设置了use_multiprocessing=False,使用以下设置运行代码时仍可以支持多处理,只需将workers参数设置为任何大于1的值即可。

示例:

history = \
   merged_model.fit_generator(generator=train_generator,
                              steps_per_epoch=trainset_steps_per_epoch,
                              epochs=300,
                              verbose=1,
                              use_multiprocessing=False,
                              workers=3,
                              max_queue_size=4)

在这一点上,让我们再次记住Keras文档:

  

使用keras.utils.Sequence保证顺序并保证   使用时每个时期每个输入的单一使用   use_multiprocessing = True。

据我了解,如果为use_multiprocessing=False,则生成器不再是线程安全的,这使得编写继承Sequence generator类变得困难。

要解决此问题,我自己编写了一个生成器,并手动将其设为线程安全。这是伪代码示例:

import tensorflow as tf
import threading

class threadsafe_iter:
    """Takes an iterator/generator and makes it thread-safe by
    serializing call to the `next` method of given iterator/generator.
    """
    def __init__(self, it):
        self.it = it
        self.lock = threading.Lock()

    def __iter__(self):
        return self

    def __next__(self): # Py3
        return next(self.it)

    #def next(self):     # Python2 only
    #    with self.lock:
    #        return self.it.next()

def threadsafe_generator(f):
    """A decorator that takes a generator function and makes it thread-safe.
    """
    def g(*a, **kw):
        return threadsafe_iter(f(*a, **kw))
    return g


@threadsafe_generator
def generate_data(tfrecord_file_path_list, ...):

    dataset = tf.data.TFRecordDataset(tfrecord_file_path_list)

    # example proto decode
    def _parse_function(example_proto):
      ...
      return batch_data

    # Parse the record into tensors.
    dataset = dataset.map(_parse_function)  

    dataset = dataset.shuffle(buffer_size=100000)

    # Repeat the input indefinitly
    dataset = dataset.repeat()  

    # Generate batches
    dataset = dataset.batch(batch_size)

    # Create an initializable iterator
    iterator = dataset.make_initializable_iterator()

    # Get batch data
    batch_data = iterator.get_next()

    iterator_init_op = iterator.make_initializer(dataset)

    with tf.Session() as sess:

        sess.run(iterator_init_op)

        while True:            
            try:
                batch_data = sess.run(batch_data)
            except tf.errors.OutOfRangeError:
                break
            yield batch_data

好吧,可以这样进行讨论是否真的很优雅,但是似乎运行得很好。

总结:

  • 如果在Windows上编写程序,请将use_multiprocessing设置为False
  • (据我所知,到目前为止),在Windows上编写代码时,不支持编写继承Sequence的生成器类。 (我猜这是一个Tensorflow / Keras问题)。
  • 要解决此问题,请编写一个普通的生成器,使生成器线程安全,并将workers设置为大于1的数字。

重要说明::在此设置中,生成器在CPU上运行,培训在GPU上完成。我可以观察到的一个问题是,如果您正在训练的模型足够浅,则GPU的利用率仍然很低,而CPU利用率却很高。如果模型较浅并且数据集足够小,那么将所有数据存储在内存中并在GPU上运行所有数据是一个不错的选择。它应该大大加快培训速度。如果出于任何原因想要同时使用CPU和GPU,我的建议是尝试使用Tensorflow的tf.data API,该API可显着加快数据预处理和批处理的速度。如果生成器仅使用Python编写,则GPU会一直等待数据以继续训练。可以说有关Tensorflow / Keras文档的所有内容,但这确实是高效的代码!

任何对API有更全面的了解的人,请看这篇文章,如果我误解了任何东西或者API已更新甚至在Windows上也解决了问题,请随时在这里纠正我。

答案 1 :(得分:1)

我有一个建议的“改进的”解决方案,可能会让其他人感兴趣。请注意,这是基于我对Tensorflow 1.15的经验(我尚未使用版本2)。

TL; DR

在Windows上安装wsl版本2,在此处在Linux环境(例如Ubuntu)中安装Tensorflow,然后将use_multiprocessing设置为True以将其保存到工作。

注意: 仅适用于Windows 10版本1903,内部版本18362或更高版本的Windows Subshel​​l for Linux(WSL)2。 请确保升级Windows Update中的Windows版本以使其正常工作。

请参见Install Tensorflow-GPU on WSL2

长回答

对于multitaskingmultithreading(即parallelismconcurrency),我们必须考虑两个操作:

  • forking =父进程创建其自身(子级)的副本,该副本具有其使用的所有内存段的精确副本
  • spawning =父进程创建了一个不共享其内存的全新子进程,并且父进程必须等待子进程完成才能继续

Linux支持forking,但Windows不支持。 Windows仅支持spawning

使用use_multiprocessing=True时Windows挂起的原因是因为Python threading模块将spawn用于Windows。因此,父进程将永远等待子进程完成,因为父进程无法将其内存转移给子进程,因此子进程不知道该怎么做。

答案2: 它不是threadsafe在Windows上,如果您曾经尝试使用数据生成器或序列,则可能看到这样的错误

ValueError: Using a generator with use_multiprocessing=True is not supported on Windows 
(no marshalling of generators across process boundaries). Instead, use single 
thread/process or multithreading.

marshalling的意思是“将对象的内存表示形式转换为适合传输的数据格式”。错误是说,与使用fork的Linux不同,use_multiprocessing=True在Windows上不起作用,因为它使用spawn`并且无法将其数据传输到子线程。

此时,您可能会问自己:

“等等……Python全局解释器锁(GIL)怎么样?”。如果Python一次只允许一个线程运行,为什么它甚至具有threading模块,为什么?我们在Tensorflow中关心吗?!”

答案在于CPU-bound tasksI/O-bound tasks之间的区别:

  • CPU-bound tasks =等待数据处理的那些人
  • I/O-bound tasks =等待其他进程的输入或输出(即数据传输)的那些人

在编程中,当我们说两个任务是concurrent时,是指它们可以在重叠的时间内启动,运行和完成。当我们说它们是parallel时,意味着它们实际上是在同时运行。

因此,GIL阻止线程并行运行 ,但不能并行运行 。这对Tensorflow很重要的原因是因为 concurrency 都是关于I / O操作(数据传输)的。 Tensorflow中良好的数据流管道应尽量设置为concurrent,这样在与CPU,GPU和/或RAM之间来回传输数据时,就不会有延迟时间,并且训练可以更快地完成。 (而不是让线程坐等直到它从其他地方获取数据,我们可以让它执行图像预处理或其他方法直到数据返回。)


重要的方面GIL是用Python制作的,因为Python中的所有内容都是对象。 (这就是为什么您可以使用“愚蠢/魔术”方法来做“怪异”事情的原因,例如(5).__add__(3)获得8 注意::在上文中,55.,因此float周围需要加上括号,因此我们需要使用括号来利用运算顺序。 Python通过计算对单个对象的所有引用来处理内存和垃圾回收。当计数变为0时,Python删除对象。如果两个线程试图同时访问同一对象,或者一个线程比另一个线程完成得更快,则可以得到race condition,对象将被“随机”删除。我们可以在每个线程上放置一个lock,但是我们将无法阻止deadlocks。 Guido认为parallel线程执行的丢失(以及我自己,尽管这当然可以争论)是很小的损失,因为我们仍然保持I / O并发操作,并且任务仍然可以在parallel中运行在不同的CPU内核(即multiprocessing)上运行它们。因此,这是(原因之一)Python同时具有threadingmultiprocessing模块。

现在返回threadsafe。在执行concurrent / parallel任务时,您必须注意其他事项。两个大的是:

  1. race conditions-每次运行程序时,运算所花的时间并不完全相同(为什么timeit会在多次运行中求平均值)。由于线程会根据运行在不同的时间完成,因此每次运行都会得到不同的结果。

  2. deadlock-如果两个线程试图同时访问相同的内存,则会出现错误。为防止这种情况,我们在线程中添加了lockmutex(互斥),以防止其他线程在运行时访问同一内存。但是,如果两个线程需要访问相同的内存而被锁定,并且每个线程都依赖于另一个线程才能执行,则程序将挂起。

之所以提出这一点,是因为Tensorflow需要能够pickle Python对象以使代码运行更快。 (pickling将对象和数据转换为字节码,就像将整个程序的源代码转换为Windows上的exe一样)。 Tensorflow Iterator.__init__()方法锁定线程并包含一个threading.Lock()

def __init__(self, n, batch_size, shuffle, seed):
    ...
    self.lock = threading.Lock()
    ...

问题是Python无法在Windows上pickle线程锁定对象(即Windows无法marshall线程锁定到child线程)。

如果您尝试使用生成器并将其传递给fit_generator,则会收到错误消息(请参见GitHub Issue #10842

TypeError: can't pickle _thread.lock objects

因此,尽管use_multiprocessing=True在Linux上是线程安全的,但在Windows上却不是。

解决方案::2020年6月左右,Microsoft推出了适用于Linux的Windows Subshel​​l版本2(wsl)。这很重要,因为它启用了GPU硬件加速。版本1“仅”是Windows NT和Linux之间的驱动程序,而wsl现在实际上是内核。因此,您现在可以在Windows上安装Linux,在命令提示符下打开bash shell,(最重要的是)访问硬件。因此,现在可以在tensorflow-gpu上安装wsl。此外,您现在可以使用fork

**因此,我建议

  1. 在Windows上安装wsl版本2并添加所需的Linux环境
  2. 在此处tensorflow-gpu Linux环境中的虚拟环境中安装wsl
  3. 重试use_multiprocessing=True看看是否有效。**

注意:我尚未测试此功能以验证其是否有效,但是据我所知,我相信它应该起作用。

此后,回答问题3 应该很简单,只需调整并行性和并行性即可,我建议使用TensorflowDev 2018 Summit视频Training Performance: A user’s guide to converge faster来了解如何做到这一点。