嵌套 tf.function 非常慢

Nested tf.function is horribly slow

在一个用 tf.function 修饰的函数中,我尝试调用另一个用 tf.function 修饰的函数。结果慢得可怕。

那是因为我不应该在函数中使用 python 本机类型吗?

测试:

import numpy as np
import tensorflow as tf


@tf.function
def loop(x, y):
    for i in range(1000):
        x.assign_add(y)
    return x


@tf.function
def loop2(x, y):
    for i in range(1000):
        loop(x, y)
    return x


def main():
    print("TensorFlow version: {}".format(tf.__version__))
    print("Eager execution: {}".format(tf.executing_eagerly()))

    x = tf.Variable(initial_value=0, dtype=np.float32)
    y = tf.Variable(initial_value=1, dtype=np.float32)

    # print(loop2(x, y))  # horribly slow

    for i in range(1000):  # faster
        loop(x, y)


main()

您应该阅读您链接的答案中引用的文章 part 3

在第 3 部分中,您可以看到问题不仅出现在使用 Python 本机类型时,而且出现在使用 Python 构造(如 for)时 Python 类型而不是 tf.Tensor 对象。

特别是,当在 range 上循环而不是在 tf.range 上循环时,您正在构建一个巨大的图表,因为您重复了 1000 次主体循环(您重新展开循环。

如果将 range 替换为 tf.range,一切都会变得更快。

证明。

您的代码(带有时间测量值和 100 而不是 1000):

import numpy as np
import tensorflow as tf
from time import time

@tf.function
def loop(x, y):
    for i in range(100):
        x.assign_add(y)
    return x


@tf.function
def loop2(x, y):
    for i in range(100):
        loop(x, y)
    return x


def main():
    print("TensorFlow version: {}".format(tf.__version__))
    print("Eager execution: {}".format(tf.executing_eagerly()))

    x = tf.Variable(initial_value=0, dtype=np.float32)
    y = tf.Variable(initial_value=1, dtype=np.float32)
    print("one")
    start = time()
    print(loop2(x, y))  # horribly slow
    print("end: ", time() - start)
    print("second: ")
    start = time()
    for i in range(100):  # faster
        loop(x, y)
    print("end: ", time() - start)


main()

输出:

TensorFlow version: 2.0.0-beta0
Eager execution: True
one
tf.Tensor(10000.0, shape=(), dtype=float32)
end:  86.44128751754761
second: 
end:  0.08476066589355469

仅使用 TensorFlow 方法更新代码:

@tf.function
def loop__(x, y):
    for i in tf.range(100):
        x.assign_add(y)
    return x


@tf.function
def loop2__(x, y):
    for i in tf.range(100):
        loop__(x, y)
    return x


def main():
    print("TensorFlow version: {}".format(tf.__version__))
    print("Eager execution: {}".format(tf.executing_eagerly()))

    x = tf.Variable(initial_value=0, dtype=np.float32)
    y = tf.Variable(initial_value=1, dtype=np.float32)
    print("one")
    start = time()
    print(loop2__(x, y))  # horribly slow
    print("end: ", time() - start)
    print("second: ")
    start = time()
    for i in tf.range(100):  # faster
        loop__(x, y)
    print("end: ", time() - start)


main()

输出:

TensorFlow version: 2.0.0-beta0
Eager execution: True
one
tf.Tensor(10000.0, shape=(), dtype=float32)
end:  0.4946322441101074
second: 
end:  0.24096465110778809

关于@tf.function的几点注意事项(外行):

  1. @tf.function 构建它修饰的函数的可调用图
  2. 该图是通过使用作为函数签名的键来引用的。如果函数的输入是张量,则此签名为 TensorSpec;如果函数的输入不是张量,则此签名为具有参数实际值的元组
  3. 每次调用图形时,都会在所有可用的 'callable graphs' 中检查密钥,如果找到匹配项,则使用 'already built callable graph'。如果不是,则将函数转换为可调用图,然后调用它。构建图形称为通过文档跟踪功能。现在,您可以了解为什么每次调用带有 python natives 的函数都会创建一个新图形。输入的特定组合根本不作为键存在,而在张量的情况下,Tesnsorspec 键对于具有相同形状和 dtype
  4. 的每个张量都是相同的
  5. 如果在函数内部使用 python 可迭代对象,那么 while 'tracing the function' 将展开循环以创建一个巨大的图形。如果使用像 tf.range 这样的 tensorflow 等价物,那么 tensorflow 知道如何在不展开的情况下处理这个问题。这种展开在函数第一次为 运行 时会产生开销,但是展开的循环总是比循环本身更快。因此,您会注意到的行为是这样的:With python iterable,与 tensorflow equivalnet (tf.range) 相反,第一个函数 运行 非常慢,因此图表created 将在加速器上消耗更多内存,但在所有后续 运行s 上明显更快,因为具有 python 可迭代的图形使用展开循环 .

演示:

与tf.range

@tf.function
def loop__(x, y):
    for i in tf.range(10000):
        x.assign_add(y)
    return x


@tf.function
def loop2__(x, y):
    for i in tf.range(100):
        loop__(x, y)
    return x

x = tf.Variable(initial_value=0, dtype=np.float32)
y = tf.Variable(initial_value=1, dtype=np.float32)

start = time()
print(loop2__(x, y))
print("first run with tf.range", time() - start)
start = time()
print(loop2__(x, y))
print("second run with tf.range", time() - start)

output:
tf.Tensor(1000000.0, shape=(), dtype=float32)
first run with tf.range 10.322974920272827
tf.Tensor(2000000.0, shape=(), dtype=float32)
second run with tf.range 11.379822969436646

python 范围:

@tf.function
def loop__(x, y):
    for i in range(10000):
        x.assign_add(y)
    return x


@tf.function
def loop2__(x, y):
    for i in tf.range(100):
        loop__(x, y)
    return x

x = tf.Variable(initial_value=0, dtype=np.float32)
y = tf.Variable(initial_value=1, dtype=np.float32)

start = time()
print(loop2__(x, y))
print("first run with python range", time() - start)
start = time()
print(loop2__(x, y))
print("second run with python range", time() - start)

output (with loads of warnings about inefficient graph unrolling):
tf.Tensor(1000000.0, shape=(), dtype=float32)
first run with python range 51.13001751899719
tf.Tensor(2000000.0, shape=(), dtype=float32)
second run with python range 1.1093688011169434