【问题标题】:How to improve data input pipeline performance?如何提高数据输入管道性能?
【发布时间】:2020-01-20 16:07:15
【问题描述】:

我尝试优化我的数据输入管道。 该数据集是一组 450 个 TFRecord 文件,每个文件大小约为 70MB,托管在 GCS 上。 该作业使用 GCP ML Engine 执行。没有 GPU。

这是管道:

def build_dataset(file_pattern):
    return tf.data.Dataset.list_files(
        file_pattern
    ).interleave(
        tf.data.TFRecordDataset,
        num_parallel_calls=tf.data.experimental.AUTOTUNE
    ).shuffle(
        buffer_size=2048
    ).batch(
        batch_size=2048,
        drop_remainder=True,
    ).cache(
    ).repeat(
    ).map(
        map_func=_parse_example_batch,
        num_parallel_calls=tf.data.experimental.AUTOTUNE
    ).prefetch(
        buffer_size=1
    )

使用映射函数:

def _bit_to_float(string_batch: tf.Tensor):
    return tf.reshape(tf.math.floormod(tf.dtypes.cast(tf.bitwise.right_shift(
        tf.expand_dims(tf.io.decode_raw(string_batch, tf.uint8), 2),
        tf.reshape(tf.dtypes.cast(tf.range(7, -1, -1), tf.uint8), (1, 1, 8))
    ), tf.float32), 2), (tf.shape(string_batch)[0], -1))


def _parse_example_batch(example_batch):
    preprocessed_sample_columns = {
        "features": tf.io.VarLenFeature(tf.float32),
        "booleanFeatures": tf.io.FixedLenFeature((), tf.string, ""),
        "label": tf.io.FixedLenFeature((), tf.float32, -1)
    }
    samples = tf.io.parse_example(example_batch, preprocessed_sample_columns)
    dense_float = tf.sparse.to_dense(samples["features"])
    bits_to_float = _bit_to_float(samples["booleanFeatures"])
    return (
        tf.concat([dense_float, bits_to_float], 1),
        tf.reshape(samples["label"], (-1, 1))
    )

我尝试遵循data pipeline tutorial 的最佳实践,并矢量化我的映射函数(按照mrry 的建议)。

使用此设置,虽然数据以高速下载(带宽约为 200MB/s),但 CPU 使用不足 (14%) 且训练速度非常慢(一个 epoch 超过 1 小时)。

我尝试了一些参数配置,更改了interleave() 参数,如num_parallel_callscycle_lengthTFRecordDataset 参数,如num_parallel_calls

最快的配置使用这组参数:

  • interleave.num_parallel_calls: 1
  • interleave.cycle_length: 8
  • TFRecordDataset.num_parallel_calls: 8

有了这个,一个 epoch 只需要大约 20 分钟即可运行。 但是,CPU 使用率仅为 50%,而带宽消耗约为 55MB/s

问题:

  1. 如何优化管道以达到 100% 的 CPU 使用率(以及类似 100MB/s 的带宽消耗)?
  2. 为什么tf.data.experimental.AUTOTUNE 没有找到加速训练的最佳价值?

善良, 亚历克西斯。


编辑

经过更多的实验,我得出了以下解决方案。

  1. 如果num_parallel_calls 大于0,则删除interleave 已由TFRecordDataset 处理的步骤。
  2. 将映射函数更新为只执行parse_exampledecode_raw,返回一个元组`((, ), ())
  3. cachemap 之后
  4. _bit_to_float 函数作为模型的一个组件移动

最后是数据管道代码:

def build_dataset(file_pattern):
    return tf.data.TFRecordDataset(
        tf.data.Dataset.list_files(file_pattern),
        num_parallel_reads=multiprocessing.cpu_count(),
        buffer_size=70*1000*1000
    ).shuffle(
        buffer_size=2048
    ).map(
        map_func=split,
        num_parallel_calls=tf.data.experimental.AUTOTUNE
    ).batch(
        batch_size=2048,
        drop_remainder=True,
    ).cache(
    ).repeat(
    ).prefetch(
        buffer_size=32
    )


def split(example):
    preprocessed_sample_columns = {
        "features": tf.io.VarLenFeature(tf.float32),
        "booleanFeatures": tf.io.FixedLenFeature((), tf.string, ""),
        "label": tf.io.FixedLenFeature((), tf.float32, -1)
    }
    samples = tf.io.parse_single_example(example, preprocessed_sample_columns)
    dense_float = tf.sparse.to_dense(samples["features"])
    bits_to_float = tf.io.decode_raw(samples["booleanFeatures"], tf.uint8)
    return (
        (dense_float, bits_to_float),
        tf.reshape(samples["label"], (1,))
    )


def build_model(input_shape):
    feature = keras.Input(shape=(N,))
    bool_feature = keras.Input(shape=(M,), dtype="uint8")
    one_hot = dataset._bit_to_float(bool_feature)
    dense_input = tf.reshape(
        keras.backend.concatenate([feature, one_hot], 1),
        input_shape)
    output = actual_model(dense_input)

    model = keras.Model([feature, bool_feature], output)
    return model

def _bit_to_float(string_batch: tf.Tensor):
    return tf.dtypes.cast(tf.reshape(
        tf.bitwise.bitwise_and(
            tf.bitwise.right_shift(
                tf.expand_dims(string_batch, 2),
                tf.reshape(
                    tf.dtypes.cast(tf.range(7, -1, -1), tf.uint8),
                    (1, 1, 8)
                ),
            ),
            tf.constant(0x01, dtype=tf.uint8)
        ),
        (tf.shape(string_batch)[0], -1)
    ), tf.float32)

感谢所有这些优化:

  • 带宽消耗约为 90MB/s
  • CPU 使用率约为 20%
  • 第一个 epoch 花费 20 分钟
  • 连续的 epoch 每个花费 5 分钟

所以这似乎是一个很好的第一次设置。但是 CPU 和 BW 仍然没有被过度使用,所以仍然欢迎任何建议!


编辑二

所以,经过一些基准测试后,我发现了我认为我们最好的输入管道:

def build_dataset(file_pattern):
    tf.data.Dataset.list_files(
        file_pattern
    ).interleave(
        TFRecordDataset,
        cycle_length=tf.data.experimental.AUTOTUNE,
        num_parallel_calls=tf.data.experimental.AUTOTUNE
    ).shuffle(
        2048
    ).batch(
        batch_size=64,
        drop_remainder=True,
    ).map(
        map_func=parse_examples_batch,
        num_parallel_calls=tf.data.experimental.AUTOTUNE
    ).cache(
    ).prefetch(
        tf.data.experimental.AUTOTUNE
    )

def parse_examples_batch(examples):
    preprocessed_sample_columns = {
        "features": tf.io.FixedLenSequenceFeature((), tf.float32, allow_missing=True),
        "booleanFeatures": tf.io.FixedLenFeature((), tf.string, ""),
        "label": tf.io.FixedLenFeature((), tf.float32, -1)
    }
    samples = tf.io.parse_example(examples, preprocessed_sample_columns)
    bits_to_float = tf.io.decode_raw(samples["booleanFeatures"], tf.uint8)
    return (
        (samples['features'], bits_to_float),
        tf.expand_dims(samples["label"], 1)
    )

那么,有什么新鲜事:

  • 根据这个GitHub issueTFRecordDataset交错是遗留的,所以interleave功能更好。
  • batchmap 之前是一个好习惯(vectorizing your function),减少映射函数的调用次数。
  • 不再需要repeat。从 TF2.0 开始,Keras 模型 API 支持数据集 API,可以使用缓存(见SO post
  • VarLenFeature 切换到FixedLenSequenceFeature,删除对tf.sparse.to_dense 的无用调用。

希望这会有所帮助。建议仍然欢迎。

【问题讨论】:

  • 感谢您不仅提出了正确的问题,而且还提供了答案。如果可以的话,我会加两个。 :) 编辑:实际上,我只是做了一些 - 我赞成你提到这个的另一个答案。 :)
  • @InnocentBystander 不客气 ^^ 感谢投票,他们也给了我一些徽章!

标签: python python-3.x tensorflow tensorflow-datasets tensorflow2.0


【解决方案1】:

为了社区的利益,在回答部分提及@AlexisBRENON 的解决方案和重要意见。

以下是重要观察:

  1. 根据这个GitHub issueTFRecordDataset interleaving 是一个遗留的,所以interleave 功能更好。
  2. map 之前batch 是一个好习惯(vectorizing your function)并减少调用映射函数的次数。
  3. 不再需要repeat。从 TF2.0 开始,Keras 模型 API 支持数据集 API,可以使用缓存(见SO post
  4. VarLenFeature 切换到FixedLenSequenceFeature,删除对tf.sparse.to_dense 的无用调用。

下面提到了与上述观察结果一致的具有改进性能的管道代码:

def build_dataset(file_pattern):
    tf.data.Dataset.list_files(
        file_pattern
    ).interleave(
        TFRecordDataset,
        cycle_length=tf.data.experimental.AUTOTUNE,
        num_parallel_calls=tf.data.experimental.AUTOTUNE
    ).shuffle(
        2048
    ).batch(
        batch_size=64,
        drop_remainder=True,
    ).map(
        map_func=parse_examples_batch,
        num_parallel_calls=tf.data.experimental.AUTOTUNE
    ).cache(
    ).prefetch(
        tf.data.experimental.AUTOTUNE
    )

def parse_examples_batch(examples):
    preprocessed_sample_columns = {
        "features": tf.io.FixedLenSequenceFeature((), tf.float32, allow_missing=True),
        "booleanFeatures": tf.io.FixedLenFeature((), tf.string, ""),
        "label": tf.io.FixedLenFeature((), tf.float32, -1)
    }
    samples = tf.io.parse_example(examples, preprocessed_sample_columns)
    bits_to_float = tf.io.decode_raw(samples["booleanFeatures"], tf.uint8)
    return (
        (samples['features'], bits_to_float),
        tf.expand_dims(samples["label"], 1)
    )

【讨论】:

    【解决方案2】:

    我还有一个建议要补充:

    根据interleave()的文档,可以作为第一个参数 使用映射函数。

    这意味着,一个人可以写:

     dataset = tf.data.Dataset.list_files(file_pattern)
     dataset = dataset.interleave(lambda x:
        tf.data.TFRecordDataset(x).map(parse_fn, num_parallel_calls=AUTOTUNE),
        cycle_length=tf.data.experimental.AUTOTUNE,
        num_parallel_calls=tf.data.experimental.AUTOTUNE
        )
    

    据我了解,这会将解析函数映射到每个分片,然后将结果交错。这样就可以避免以后使用dataset.map(...)

    【讨论】:

    • 我最后没有做很多实验。但我认为您的解决方案不会带来太大的改进。我认为interleave 负责处理 IO 阻塞行为(不需要 CPU),而map 大部分时间都是 CPU 密集型的(因此不能大量并行化)。所以,我认为你的解决方案大致相当于interleave().map()。为了确保这一点,请随意尝试thisthis
    猜你喜欢
    • 2017-06-18
    • 2019-04-24
    • 1970-01-01
    • 2019-07-22
    • 2016-12-25
    • 1970-01-01
    • 2011-05-24
    • 2011-01-01
    • 2011-03-30
    相关资源
    最近更新 更多