我应该对所有函数使用@tf.function吗?

33
一份关于@tf.function官方教程这样说:
使用tf.function将程序转换为图形以获得最佳性能并使您的模型可以在任何地方部署。由于AutoGraph的存在,令人惊讶的是,大量Python代码可以直接与tf.function配合使用,但仍然需要注意一些陷阱。
主要的收获和建议是:
- 不要依赖Python副作用,如对象突变或列表附加。 - tf.function最适合TensorFlow操作,而不是NumPy操作或Python基元。 - 如果有疑问,请使用for x in y习语。
它只提到了如何实现带有@tf.function注释的函数,但没有提到何时使用它。
是否有一种启发式方法来决定是否应该至少尝试使用tf.function注释函数?除非我懒得删除副作用或更改某些内容,例如range()-> tf.range(),否则似乎没有理由不这样做。但如果我愿意这样做... 是否有任何理由不使用@tf.function注释所有函数?

1
为什么要添加这些标签?我们同样可以添加 tensorflow0.1tensorflow0.2tensorflow0.3tensorflow0.4tensorflow0.5 等标签,以及每个 这些 tf 模块和类 的标签。此外,为什么不为 Python 的标准模块及其函数和类添加标签呢? - ForceBru
这就是为什么我引入了tensorflow2.x标签的原因,因为有些问题不仅涉及tensorflow2.0,还涉及tensorflow2.x标签。但是为每个库的每个版本添加一个标签是不可行的和不合适的。以Python为例,你不会有python3.4.6... .python3.8.2,而只有python3.x。 - Timbus Calin
2
一方面,tf.function指南中提到:“装饰模块级函数和模块级类的方法,并避免装饰局部函数或方法”。我记得有更明确的措辞,比如“不要装饰每个函数,在高级函数(如训练循环)中使用tf.function”,但我可能记错了(或者可能已经被删除)。另一方面,这个讨论有开发人员的有趣输入,最终似乎可以在任何张量/变量的函数中使用它。 - jdehesa
1
据我所知,@tf.function注释的函数也会将它们自己调用的函数编译成图形。因此,您只需要注释与您描述的一致的模块入口点即可。但是,手动注释调用堆栈较低的函数也不会有任何损失。 - problemofficer - n.f. Monica
@problemofficer 是的,在我链接的 GitHub 问题中有一些关于创建多个中间函数是否会对性能产生轻微影响的讨论,但似乎图形优化器(grappler)可以在需要时“内联”函数,但另一方面,如果另一个非 tf.function 被多次调用,则无法防止图形中的“代码重复”,这就是为什么广泛使用似乎是可取的原因。 - jdehesa
据我所知,我可以建议两个最好的资源,更详细地描述何时使用tf.function https://www.tensorflow.org/tutorials/customization/performance,https://www.tensorflow.org/guide/function。它不能回答您提出的所有问题,但我想为任何新用户发布这两个链接以开始使用@tf.function。希望能有所帮助。 - Vishnuvardhan Janapati
3个回答

36
TLDR: 是否使用tf.function取决于您的功能及其是否在生产或开发中。如果想轻松调试函数,或者它属于AutoGraph或tf.v1代码兼容性的限制范围内,则不要使用tf.function。强烈建议观看有关AutoGraphFunctions, not Sessions 的Inside TensorFlow讲座。
以下我将阐述原因,这些原因均来自Google在网上公布的信息。
通常,使用tf.function装饰器会导致函数被编译为可执行TensorFlow图形的可调用对象。这需要:
  • 通过AutoGraph进行代码转换(包括从已注释函数调用的任何函数)
  • 跟踪并执行生成的图形代码
此处提供了关于设计思路的详细信息。

使用tf.function修饰函数的好处

一般好处

  • 更快的执行速度,特别是对于由许多小操作组成的函数(来源)

对于包含Python代码的函数/使用AutoGraph通过tf.function装饰

如果要使用AutoGraph,则强烈建议使用tf.function而不是直接调用AutoGraph。原因包括:自动控制依赖关系、某些API需要它、更多缓存和异常帮助器(来源)

使用tf.function修饰函数的缺点

一般缺点

  • 如果函数只包含少量昂贵的操作,那么不会有太大的加速效果(来源)

对于包含Python代码的函数/使用AutoGraph通过tf.function装饰

  • 没有异常捕获(应在急切模式下完成;在修饰函数之外)(来源)
  • 调试更加困难
  • 由于隐藏的副作用和TF控制流而受到限制

AutoGraph限制的详细信息在此处可用。

对于使用tf.v1代码的函数

  • 不允许在tf.function中多次创建变量,但随着tf.v1代码的逐步淘汰,这可能会发生改变 (来源)

对于使用tf.v2代码的函数

  • 没有特定的缺点

限制示例

多次创建变量

不允许多次创建变量,例如以下示例中的v

@tf.function
def f(x):
    v = tf.Variable(1)
    return tf.add(x, v)

f(tf.constant(2))

# => ValueError: tf.function-decorated function tried to create variables on non-first call.

在以下代码中,可以通过确保只创建一次self.v来缓解这个问题:
class C(object):
    def __init__(self):
        self.v = None
    @tf.function
    def f(self, x):
        if self.v is None:
            self.v = tf.Variable(1)
        return tf.add(x, self.v)

c = C()
print(c.f(tf.constant(2)))

# => tf.Tensor(3, shape=(), dtype=int32)

AutoGraph无法捕获的隐藏副作用

在这个例子中对于self.a的更改是无法隐藏的,因为跨函数分析还没有完成(参见来源),从而导致错误。

class C(object):
    def change_state(self):
        self.a += 1

    @tf.function
    def f(self):
        self.a = tf.constant(0)
        if tf.constant(True):
            self.change_state() # Mutation of self.a is hidden
        tf.print(self.a)

x = C()
x.f()

# => InaccessibleTensorError: The tensor 'Tensor("add:0", shape=(), dtype=int32)' cannot be accessed here: it is defined in another function or code block. Use return values, explicit Python locals or TensorFlow collections to access it. Defined in: FuncGraph(name=cond_true_5, id=5477800528); accessed from: FuncGraph(name=f, id=5476093776).

明显的变化不是问题:

class C(object):
    @tf.function
    def f(self):
        self.a = tf.constant(0)
        if tf.constant(True):
            self.a += 1 # Mutation of self.a is in plain sight
        tf.print(self.a)

x = C()
x.f()

# => 1

TF控制流限制示例

由于需要为TF控制流定义else的值,因此此if语句会导致错误:

@tf.function
def f(a, b):
    if tf.greater(a, b):
        return tf.constant(1)

# If a <= b would return None
x = f(tf.constant(3), tf.constant(2))   

# => ValueError: A value must also be returned from the else branch. If a value is returned from one branch of a conditional a value must be returned from all branches.

2
这是一个很好的总结。值得注意的是,当从急切模式调用时,tf.function在第一次调用后会有大约200微秒(加减)的开销。不过,从另一个tf.function调用tf.function是可以的。因此,您应该尽可能地包装更多的计算。如果没有限制,您应该包装整个程序。 - Dan Moldovan
这个回答太长了,而且我认为它并没有真正回答我的问题,只是给了我一些我自己已经找到的零散信息。另外,说我不应该在生产中使用@tf.function,只能在开发阶段使用,这并不是一个可行的解决方案。首先,在机器学习(至少在研究中),开发阶段的训练也会创建最终产品(训练模型)。其次,装饰器是一个重大的改变。我不能只把它们放在“开发后”并确信代码的行为相同。这意味着我必须在它们已经存在的情况下进行开发和测试。 - problemofficer - n.f. Monica
1
@problemofficer 对于造成的困惑我感到抱歉。在我的回答中,当谈到生产时,我认为训练(使用大型数据集)是其中的一部分。在我的研究中,我会使用玩具数据集以急切模式开发/调试函数,然后根据需要添加 tf.function - prouast

4

tf.function在创建和使用计算图方面非常有用,应该在训练和部署中使用,但大多数函数都不需要它。

假设我们正在构建一个特殊的层,该层将是更大模型的一部分。我们不希望在构造该层的函数上方添加tf.function装饰器,因为它只是该层外观的定义。

另一方面,假设我们要使用某些函数进行预测或继续训练。我们希望在这些函数上使用tf.function装饰器,因为我们实际上正在使用计算图来获得某些值。

一个很好的例子是构建编码器-解码器模型。 不要将装饰器放置在创建编码器、解码器或任何层的函数周围,因为那只是它将要执行的定义。 在“train”或“predict”方法周围放置装饰器,因为它们实际上将使用计算图进行计算。


1
但是副作用或例如 tf.range() 呢?据我所知,这些无法自动转换。因此,我需要从一开始就考虑使用自动图形编写我的自定义层。因此,我不能只装饰调用(预测)函数。 - problemofficer - n.f. Monica
我很感激这个评论。但是,如果enc/dec函数以及它们上面的训练步骤都有@tf.function装饰器,那会怎样呢?我正在尝试弄清楚这对图形的影响。它肯定仍然有效。 - kylejmcintyre

1
根据我的理解和文档,使用tf.function主要是为了加速您的代码,因为被tf.function包装的代码将被转换为图形,因此有一些优化的空间(例如操作剪枝、折叠等),这些优化可能在同样的代码急切运行时不会执行。
然而,也有一些情况下使用tf.function可能会产生额外的开销或者并不能带来明显的加速。其中一个值得注意的情况是当包装的函数很小,在您的代码中只使用了几次,因此调用图可能的开销相对较大。另一个情况是当大部分计算已经在加速设备(例如GPU、TPU)上完成,因此通过图形计算获得的加速可能并不显著。
还有一个文档中的章节讨论了各种场景下的加速效果,在这个章节的开头提到了上述两种情况。
将一个使用张量的函数包装在tf.function中并不会自动加速你的代码。对于在单台机器上调用几次的小型函数来说,调用图或图片段的开销可能会支配运行时间。此外,如果大部分计算已经发生在加速器上,例如堆叠GPU密集卷积,那么图形加速不会很大。

对于复杂的计算,图形可以提供显著的加速。这是因为图形减少了Python到设备之间的通信并执行一些加速操作。

但是归根结底,如果适用于您的工作流程,我认为确定对于您特定的用例和环境最好的方法是在使用急切模式(即不使用tf.function)执行代码时对其进行分析与在使用图形模式(即广泛使用tf.function)执行代码时进行分析。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接