【问题标题】:Tensorflow slow performances using tf.data.Dataset使用 tf.data.Dataset 的 Tensorflow 性能缓慢
【发布时间】:2020-01-12 14:50:11
【问题描述】:

我在 tensorflow 中实现了一个简单的培训师课程。我正在运行一些实验来检查代码性能,但我无法理解 tf.data.Datasettf.function 背后发生的事情。

下面我将介绍我运行的测试,最后会有一些关于我得到的结果的问题。

配置:Intel i3 cpu,tensorflow-cpu 2.1

class Trainer:
    def __init__(self, model, optimizer, loss):
        self.model = model
        self.loss_function = loss
        self.optimizer = optimizer

    @tf.function
    def train_step(self, inputs, targets):
        with tf.GradientTape() as tape:
            predictions = self.model(inputs)
            loss = self.loss_function(targets, predictions)
        gradients = tape.gradient(loss, self.model.trainable_variables)
        self.optimizer.apply_gradients(zip(gradients, self.model.trainable_variables))
        return loss

    # fit using dataset
    @tf.function
    def fit0(self, dataset, epochs):
        for epoch in tf.range(epochs):
            for input_batch, target_batch in dataset:
                self.train_step(input_batch, target_batch)

    # fit using list of tensors
    @tf.function
    def fit1(self, inputs, targets, epochs):
        for epoch in tf.range(epochs):
            for input_batch, target_batch in zip(inputs, targets):
                self.train_step(input_batch, target_batch)

在以下 train_step 中将始终包裹在 tf.function 中。

fit0fit1 将在使用和不使用 tf.function 的情况下进行测试。

这是我用来运行测试的代码:

input_size = 10000
batch_size = 100
q = input_size // batch_size

# create random inputs (x) and outputs (y)
x = tf.random.normal((input_size, 1), dtype=tf.float32)
y = tf.random.normal((input_size, 1), dtype=tf.float32)

splits = tf.fill([q, ], batch_size)

# create a list of tensors rappresenting batches
x_list = tf.split(x, splits)
y_list = tf.split(y, splits)

# create datasets in the different ways
dataset0 = tf.data.Dataset.from_tensor_slices((x, y)).batch(batch_size)
dataset1 = tf.data.Dataset.from_tensor_slices((tf.stack(x_list), tf.stack(y_list)))

# model definition
model = tf.keras.Sequential([
    tf.keras.layers.Dense(20, activation='tanh', input_shape=(1,)),
    tf.keras.layers.Dense(1, activation='linear')])

# trainer initialization
trainer = Trainer(model=model, optimizer=tf.keras.optimizers.Adam(), loss=tf.keras.losses.MeanSquaredError())

# first run to perform initializations
time0 = time.time()
trainer.fit0(dataset=dataset0, epochs=tf.constant(1, dtype=tf.int32))
time0 = time.time() - time0

time1 = time.time()
trainer.fit0(dataset=dataset1, epochs=tf.constant(1, dtype=tf.int32))
time1 = time.time() - time1

time2 = time.time()
trainer.fit1(inputs=x_list, targets=y_list, epochs=tf.constant(1, dtype=tf.int32))
time2 = time.time() - time2

print("first fit0 with dataset0 took {} seconds".format(time0))
print("first fit0 with dataset1 took {} seconds".format(time1))
print("first fit1 with tensorlist took {} seconds".format(time2))

# measure performances
time0 = time.time()
trainer.fit0(dataset=dataset0, epochs=tf.constant(100, dtype=tf.int32))
time0 = time.time() - time0

time1 = time.time()
trainer.fit0(dataset=dataset1, epochs=tf.constant(100, dtype=tf.int32))
time1 = time.time() - time1

time2 = time.time()
trainer.fit1(inputs=x_list, targets=y_list, epochs=tf.constant(100, dtype=tf.int32))
time2 = time.time() - time2

print("fit0 with dataset0 took {} seconds".format(time0))
print("fit0 with dataset1 took {} seconds".format(time1))
print("fit1 with tensorlist took {} seconds".format(time2))

这里是测试结果:

第一次测试是 100 批,每批 100 个样品。

input_size = 10000
批量大小 = 100

没有@tf.function:
第一次 fit0 与 dataset0 花了 0.9953532218933105 秒
第一次 fit0 与 dataset1 花了 0.07995295524597168 秒
第一次使用 tensorlist 的 fit1 耗时 0.05196571350097656 秒
fit0 和 dataset0 耗时 10.46957802772522 秒
fit0 和 dataset1 花了 7.822799205780029 秒
带有张量列表的 fit1 耗时 4.650130748748779 秒

使用@tf.function:
第一次 fit0 与 dataset0 花了 1.4042332172393799 秒
第一次 fit0 与 dataset1 花了 0.46071624755859375 秒
首次使用 tensorlist 的 fit1 耗时 7.3524699211120605 秒
fit0 和 dataset0 花了 15.077088832855225 秒
fit0 和 dataset1 花了 9.136569738388062 秒
带有张量列表的 fit1 耗时 2.1366817951202393 秒

第二个是1批100000个样本。

input_size = 100000
批量大小 = 100000

没有@tf.function:
第一次 fit0 与 dataset0 花了 1.1792669296264648 秒
第一次 fit0 与 dataset1 花了 0.027983427047729492 秒
第一次使用 tensorlist 的 fit1 耗时 0.020987749099731445 秒
fit0 和 dataset0 耗时 28.71895956993103 秒
fit0 和 dataset1 花了 2.730872869491577 秒
带有张量列表的 fit1 耗时 2.194814682006836 秒

使用@tf.function:
第一次 fit0 与 dataset0 花了 1.5979444980621338 秒
第一次 fit0 与 dataset1 花了 0.4557182788848877 秒
第一次使用 tensorlist 的 fit1 耗时 0.3708038330078125 秒
fit0 和 dataset0 耗时 36.43854784965515 秒
fit0 和 dataset1 花了 9.819332122802734 秒
带有张量列表的 fit1 耗时 2.1136972904205322 秒

问题:

  1. 为什么 tf.data.Dataset 使用 tf.function 包装时性能最差?
  2. 即使 dataset0 和 dataset1 功能相同。两者之间的底层区别是什么?为什么数据集 1 的性能优于数据集 0?
  3. fit1tf.function 获得了最佳的长期性能。
    • 是否可以使用 tf.data.Dataset 实现相同的性能?
    • 为什么初始化需要这么长时间?
      使用 100 个批次时,第一次运行耗时 7.3524699211120605 秒,这一次随着批次数量的增加而增加。
      我猜是因为签名正在创建一个更大的图,展开不同批次的计算。不过,我看不到任何并行化的机会,因为每个批次都依赖于前一个批次的结果。

【问题讨论】:

    标签: python tensorflow optimization


    【解决方案1】:

    我得到了很好的性能改进,代码和结果如下所示。
    不过,我只能回答部分问题,尤其是第二个问题仍然悬而未决。

    配置:Intel i3 cpu,tensorflow-cpu 2.1
    下面是函数 fit0 的改进代码,Trainer 类的其余部分保持不变:

    # fit using dataset
    @tf.function
    def fit0(self, dataset, epochs, batches, unroll=1):
        tf.assert_equal(tf.is_tensor(unroll), False, "unroll must be a python variable.")
        tf.assert_equal(tf.math.floormod(batches, unroll), tf.constant(0), "unroll must be a divisor of batches.")
    
        entries = epochs * batches / unroll
        it = iter(dataset)
    
        for entry in tf.range(entries):
            # this loop gets unrolled if unroll
            # is python variable, not a tensor.
            for _ in range(unroll):
                input_batch, target_batch = next(it)
                self.train_step(input_batch, target_batch)
    

    这是我用来运行测试的代码:

    input_size = 100000
    batch_size = 100
    num_epochs = 100
    num_unroll = 5
    
    num_batches = input_size // batch_size
    
    # create random inputs (x) and outputs (y)
    x = tf.random.normal((input_size, 1), dtype=tf.float32)
    y = tf.random.normal((input_size, 1), dtype=tf.float32)
    
    splits = tf.fill([num_batches, ], batch_size)
    x_list, y_list = tf.split(x, splits), tf.split(y, splits)
    
    # create dataset
    dataset0 = tf.data.Dataset.from_tensor_slices((x, y)).batch(batch_size).cache().prefetch(1).repeat(num_epochs)
    dataset1 = tf.data.Dataset.from_tensor_slices((tf.stack(x_list), tf.stack(y_list))).cache().prefetch(1).repeat(num_epochs)
    
    # model definition
    model = tf.keras.Sequential([
        tf.keras.layers.Dense(20, activation='tanh', input_shape=(1,)),
        tf.keras.layers.Dense(1, activation='linear')])
    
    # trainer initialization
    trainer = Trainer(model=model, optimizer=tf.keras.optimizers.Adam(), loss=tf.keras.losses.MeanSquaredError())
    
    # first run to perform initializations
    time0 = time.perf_counter()
    trainer.fit0(
        dataset=dataset0,
        epochs=tf.constant(1, dtype=tf.int32),
        batches=tf.constant(num_batches, dtype=tf.int32),
        unroll=num_unroll)
    time0 = time.perf_counter() - time0
    
    time1 = time.perf_counter()
    trainer.fit0(
        dataset=dataset1,
        epochs=tf.constant(1, dtype=tf.int32),
        batches=tf.constant(num_batches, dtype=tf.int32),
        unroll=num_unroll)
    time1 = time.perf_counter() - time1
    
    time2 = time.perf_counter()
    trainer.fit1(inputs=x_list, targets=y_list, epochs=tf.constant(1, dtype=tf.int32))
    time2 = time.perf_counter() - time2
    
    print("first fit0 with dataset0 took {} seconds".format(time0))
    print("first fit0 with dataset1 took {} seconds".format(time1))
    print("first fit1 with tensorlist took {} seconds".format(time2))
    
    # measure performances
    time0 = time.perf_counter()
    trainer.fit0(
        dataset=dataset0,
        epochs=tf.constant(num_epochs, dtype=tf.int32),
        batches=tf.constant(num_batches, dtype=tf.int32),
        unroll=num_unroll)
    time0 = time.perf_counter() - time0
    
    time1 = time.perf_counter()
    trainer.fit0(
        dataset=dataset1,
        epochs=tf.constant(num_epochs, dtype=tf.int32),
        batches=tf.constant(num_batches, dtype=tf.int32),
        unroll=num_unroll)
    time1 = time.perf_counter() - time1
    
    time2 = time.perf_counter()
    trainer.fit1(inputs=x_list, targets=y_list, epochs=tf.constant(num_epochs, dtype=tf.int32))
    time2 = time.perf_counter() - time2
    
    print("fit0 with dataset0 took {} seconds".format(time0))
    print("fit0 with dataset1 took {} seconds".format(time1))
    print("fit1 with tensorlist took {} seconds".format(time2))
    
    1. 为什么 tf.data.Dataset 在使用 tf.function 包装时性能最差?

    我不知道到底发生了什么,但可以通过替换来解决:

    dataset0 = tf.data.Dataset.from_tensor_slices((x, y)).batch(batch_size)
    dataset1 = tf.data.Dataset.from_tensor_slices((tf.stack(x_list), tf.stack(y_list)))
    

    使用这个新数据集,其中还包括纪元以及使用缓存和预取。

    dataset0 = tf.data.Dataset.from_tensor_slices((x, y)).batch(batch_size).cache().prefetch(1).repeat(num_epochs)
    dataset1 = tf.data.Dataset.from_tensor_slices((tf.stack(x_list), tf.stack(y_list))).cache().prefetch(1).repeat(num_epochs)
    

    更多信息可以在here找到。

    我测试了 fit0fit1 有和没有 tf.function,但是通过这些更改,我总是使用 获得更好的性能tf.function,因此只会显示后者。

    使用的 input_size 是原来的 10 倍。这里是测试结果:

    第一次测试是 1000 批,每批 100 个样品。
    请注意,与 num_unroll = 1 相比,num_unroll = 5 提高了性能。设置 num_unroll > 5 不会提供任何进一步的改进。

    input_size = 100000
    批量大小 = 100
    num_epochs = 100
    num_unroll = 5

    第一次 fit0 与 dataset0 耗时 2.2224882999999993 秒
    第一次 fit0 与 dataset1 花了 0.804360700000001 秒
    首次使用 tensorlist 的 fit1 耗时 88.2123332 秒
    fit0 和 dataset0 耗时 35.27911590000001 秒
    fit0 和 dataset1 花了 20.370243099999982 秒
    带有张量列表的 fit1 耗时 23.66727979999999 秒

    第二个是1批1000000个样本。

    input_size = 100000
    批量大小 = 100000

    input_size = 1000000
    批量大小 = 1000000
    num_epochs = 100
    num_unroll = 1

    第一次 fit0 与 dataset0 花了 4.3616363 秒
    第一次 fit0 与 dataset1 花了 0.7977632000000003 秒
    第一次使用 tensorlist 的 fit1 耗时 0.7329889000000005 秒
    fit0 和 dataset0 耗时 21.131495899999997 秒
    fit0 和 dataset1 花了 19.915148600000002 秒
    带有张量列表的 fit1 耗时 19.817472700000003 秒

    以上结果可以回答问题:

    1. fit1tf.function 获得了最佳的长期性能。
      • 是否可以使用 tf.data.Dataset 实现相同的性能?
      • 为什么初始化需要这么长时间?
        当使用 100 个批次时,第一次运行需要 7.3524699211120605 秒,这 通过增加批次数量来增加时间。我猜是 因为签名正在创建一个更大的图,展开 不同批次的计算。我看不到任何机会 但是并行化,因为每个批次都取决于结果 上一个。

    通过检查 TensorBoard 上的图形结构,很容易看出在 fit1 函数上使用 autograph 会通过完全展开循环创建一个非常大的图形. 这提供了更好的性能,但创建图形的时间很长,而且很可能会过度使用内存,这使得它无法用于更复杂的问题。
    但是,如上所示,使用 tf.data.Dataset 可以实现相同的性能,只需要几个展开的循环和随之而来的图大小的改进。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-12-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-14
      • 2015-01-27
      • 2021-06-04
      • 2016-11-09
      相关资源
      最近更新 更多