Tensorflow conv1d/Keras conv1d奇怪的性能变化



在测量Conv1D层的处理运行时,我得到了一些意想不到的结果,不知道是否有人理解这些结果。在继续之前,我注意到观察结果不仅与Conv1D层有关,而且对于tf.nn.Conv1D函数也可以类似地观察到。

我使用的代码是非常简单的

import os
# silence verbose TF feedback
if 'TF_CPP_MIN_LOG_LEVEL' not in os.environ:
os.environ['TF_CPP_MIN_LOG_LEVEL'] = "3"
import tensorflow as tf
import time
def fun(sigl, cc, bs=10):
oo = tf.ones((bs, sigl, 200), dtype=tf.float32)
start_time = time.time()
ss=cc(oo).numpy()
dur = time.time() - start_time
print(f"size {sigl} time: {dur:.3f} speed {bs*sigl / 1000 / dur:.2f}kHz  su {ss.shape}")

cctf2t = tf.keras.layers.Conv1D(100,10)
for jj in range(2):
print("====")
for ii in range(30):
fun(10000+ii, cctf2t, bs=10)

我希望观察到第一个调用很慢,而其他调用的运行时间大致相似。事实证明,这种行为是完全不同的。假设上面的代码存储在一个名为debug_conv_speed.py的脚本中,我在NVIDIA GeForce GTX 1050 Ti 上得到以下内容

$> ./debug_conv_speed.py 
====
size 10000 time: 0.901 speed 111.01kHz  su (10, 9991, 100)
size 10001 time: 0.202 speed 554.03kHz  su (10, 9992, 100)
...
size 10029 time: 0.178 speed 563.08kHz  su (10, 10020, 100)
====
size 10000 time: 0.049 speed 2027.46kHz  su (10, 9991, 100)
...
size 10029 time: 0.049 speed 2026.87kHz  su (10, 10020, 100)

其中CCD_ 1指示大致相同的结果。正如预期的那样,第一次是慢的,然后对于每个输入长度,我得到大约550kHz的相同速度。但对于重复,我惊讶地发现所有操作的运行速度都快了约4倍,频率为2MHz。

GeForce GTX 1080的结果更为不同。在那里,第一次使用长度时,它的运行频率约为200kHz,对于重复,我发现速度为1.8MHz

响应https://stackoverflow.com/a/71184388/3932675我添加了使用tf.function的代码的第二个变体

import os
# silence verbose TF feedback
if 'TF_CPP_MIN_LOG_LEVEL' not in os.environ:
os.environ['TF_CPP_MIN_LOG_LEVEL'] = "3"
import tensorflow as tf
import time
from functools import partial
print(tf.config.list_physical_devices())
class run_fun(object):
def __init__(self, ll, channels):
self.op = ll
self.channels = channels
@tf.function(input_signature=(tf.TensorSpec(shape=[None,None,None]),),
experimental_relax_shapes=True)
def __call__(self, input):
print("retracing")
return self.op(tf.reshape(input, (tf.shape(input)[0], tf.shape(input)[1], self.channels)))

def run_layer(sigl, ll, bs=10):
oo = tf.random.normal((bs, sigl, 200), dtype=tf.float32)
start_time = time.time()
ss=ll(oo).numpy()
dur = time.time() - start_time
print(f"len {sigl} time: {dur:.3f} speed {bs*sigl / 1000 / dur:.2f}kHz su {ss.shape}")

ww= tf.ones((10, 200, 100))
ll=partial(tf.nn.conv1d, filters=ww, stride=1, padding="VALID", data_format="NWC")
run_ll = run_fun(ll, 200)
for jj in range(2):
print(f"=== run {jj+1} ===")
for ii in range(5):
run_layer(10000+ii, run_ll)
# alternatively for eager mode run
# run_layer(10000+ii, ll)

在谷歌的colab GPU 上运行后的结果

[PhysicalDevice(name='/physical_device:CPU:0', device_type='CPU'), PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')]
=== run 1 ===
retracing
len 10000 time: 10.168 speed 9.83kHz su (10, 9991, 100)
len 10001 time: 0.621 speed 161.09kHz su (10, 9992, 100)
len 10002 time: 0.622 speed 160.80kHz su (10, 9993, 100)
len 10003 time: 0.644 speed 155.38kHz su (10, 9994, 100)
len 10004 time: 0.632 speed 158.18kHz su (10, 9995, 100)
=== run 2 ===
len 10000 time: 0.080 speed 1253.34kHz su (10, 9991, 100)
len 10001 time: 0.053 speed 1898.41kHz su (10, 9992, 100)
len 10002 time: 0.052 speed 1917.43kHz su (10, 9993, 100)
len 10003 time: 0.067 speed 1499.43kHz su (10, 9994, 100)
len 10004 time: 0.095 speed 1058.60kHz su (10, 9995, 100)

这表明,对于给定的tf.function参数,不会发生回溯,性能也显示出相同的差异。

有人知道怎么解释吗?

第一次迭代相对较慢的原因是您正在向cctf2t中输入不同的形状,这会触发计算图的收回。

在第二次迭代以及随后的所有迭代中,您不再遇到新的形状,因此不再有进一步的回溯。

我很确定已经在TensorFlow cudnn的来源中找到了解释,并在这里为遇到同样问题的其他人(尤其是那些投票支持这个问题的人(分享了见解。

cuda支持许多卷积内核,在当前版本的TensorFlow 2.9.0中,这些内核是通过CudnnSupport::GetConvolveRunners获得的此处

https://github.com/tensorflow/tensorflow/blob/21368c687cafdf97fac3dd0eefaed710df0068a2/tensorflow/stream_executor/cuda/cuda_dnn.cc#L4557

然后在各种自动调谐功能中使用

https://github.com/tensorflow/tensorflow/blob/21368c687cafdf97fac3dd0eefaed710df0068a2/tensorflow/core/kernels/conv_ops_gpu.cc#L365

似乎每次遇到由数据形状、过滤器形状和其他参数组成的配置时,cuda驱动程序都会测试所有内核,并保留最有效的内核。对于大多数情况来说,这是一个非常好的优化,尤其是使用恒定批量形状进行训练,或者使用恒定图像大小进行推理。对于可能具有任意长度的音频信号的推断(例如,具有48000Hz采样率、持续时间从1s到20s的音频信号具有近100万个不同的长度(,cuda实现在大部分时间测试所有内核版本。对于任何给定的配置,哪一个内核是最有效的内核,这几乎没有任何好处,因为几乎不会第二次遇到相同的配置。

对于我的用例,我现在使用基于重叠相加的处理,具有固定的信号长度和大约因子4的改进的推断时间。

最新更新