[TensorFlow]Batch Normalization在单机多卡上的实现
前记
最近感觉Batch Normalization在实践上还是很有搞头的,首先在单机TensorFlow里完整实现BN演算法就有很多坑(具体可以看我的文章Michael:[论文阅读]Batch Normalization: Accelerating Deep Netwo,里面也有我对BN的理解),然后在单机多卡上的实现也有些坑,主要是moving mean和moving variance在单机多卡上是怎么实现更新的(分散式的原理类似於单机多卡)。
首先做一些实验来看看BN的运行原理吧,代码已上传至testBNInMultiGPU.py,我改了一些bug后可以完美运行并在实际项目中运行成功的代码在validSyncBN.py。
单机单卡上的BN实验
代码为testBNInSingleGPU()。
结果:
outputArray = [0. 0. 0.]
After normalization:
moving mean = [0.1 0.1 0.1]
moving variance = [0.9 0.9 0.9]
首先看输出全为0表示BN起到了归一化的作用。
之后可以看moving mean和moving variance的值,参考Michael:TensorFlow源码解读之Batch Normalization,得知TF中moving mean和moving variance的更新方式都是momentum,即 。
因为moving mean初始化为0,moving variance初始化为1,momentum设置为0.9,代入可得:
moving mean:
moving variance:
和实验是完美符合的。
单机多卡上的运行实验
这里选取了2块卡进行实验,代码为testBNInMultiGPU()。
结果:
outputArray = [0. 0. 0.]
After normalization:
moving mean = [0.2 0.2 0.2]
moving variance = [0.79999995 0.79999995 0.79999995]
由于是两块卡,结合Michael:TensorFlow源码解读之Batch Normalization的分析,再根据实验结果,我推测更新公式是这样的:
统计每张卡上moving mean和moving variance的变化量,即 ,将各卡的变化量求和得到总变化量,进而得到更新的结果:
,这里 表示GPU的数量。
因为moving mean初始化为0,moving variance初始化为1,momentum设置为0.9,代入可得:
moving mean:
moving variance:
和实验是完美符合的。
另一种更新的策略是,每张卡前向执行一次,更新一次moving mean和moving variance,那么:
moving mean:
第一次:
第二次:
这与实验结果是不符合的。
这里改变一下输入的值(一个卡上是1,一个卡上是2),以验证我的想法是不是对的:
for item in range(numberGPU):
feedDict[tf.get_default_graph().get_tensor_by_name("tower_%s/data:0" % item)]
= np.ones((3))*(item+1)
结果:
outputArray = [0. 0. 0.]
After normalization:
moving mean = [0.3 0.3 0.3]
moving variance = [0.79999995 0.79999995 0.79999995]
因为moving mean初始化为0,moving variance初始化为1,momentum设置为0.9,代入可得:
moving mean:
moving variance:
和实验是完美符合的。
影响
对于公式 ,如果各个卡上 的分布是一致的,那么很清晰就得到:
这个实际是会加速moving mean和moving variance的,所以可设置新的moment为: 。
第二个影响就是对训练了,从上述的实验可以猜测BN的训练过程:
在每张卡上前向推导,得到BN的moving mean和moving variance的改变数,同时,反向推导,得到 和 在每张卡上的梯度。
然后,使用tensorflow 多GPU编程 完全指南中描述的方法,得到 和 的平均梯度。
所以,TensorFlow的官方方法是没有考虑多GPU编程的,下一个步骤好好搞搞这个吧,目测比较难。
目前找到的一些比较好的资源:
batch normalization的multi-GPU版本该怎么实现?
里面有几个比较好的解决方案:
1.jianlong-yuan/syncbn-tensorflow重写了TensorFlow的官方方法,可以做个实验验证一下。
2.旷视科技:CVPR 2018 | 旷视科技物体检测冠军论文——大型Mini-Batch检测器MegDet
tensorpack/tensorpack里面有该论文的代码实现。
关于第一种解决方案的实验论证与理论解释
jianlong-yuan/syncbn-tensorflow是源代码,我稍微改了改代码,并添加了我的一些注释,代码为sync_batch_norm()。
测试代码为testBNInMultiGPU3()。
输出为:
outputTuple = [array([-0.998006, -0.998006, -0.998006], dtype=float32), array([0.9980061, 0.9980061, 0.9980061], dtype=float32),
[array([0.15, 0.15, 0.15], dtype=float32), array([0.92499995, 0.92499995, 0.92499995], dtype=float32)]]
After normalization:
moving mean = [0.15 0.15 0.15]
moving variance = [0.92499995 0.92499995 0.92499995]
outputTuple一共4项,前两项为2个GPU的输出量,一个为-1,一个为1,后两项分别是moving mean和moving variance的输出。
这里先就数据的观点来验证结果是否正确。
GPU_0的数据是3个1,GPU_1的数据是3个2,参考batch normalization accelerating deep network training by reducing internal covariate shift演算法1,有:
对于GPU_0的数据,有:
对于GPU_1的数据,有:
这说明输出是符合的。
下面看moving mean和moving variance的值。
moving mean:moving variance:
也是符合的,说明程序还是很鲁棒的。
这里来分析程序的运行原理,大部分我都用注释说明了,这里讲里面的两个知识点吧,一个是mean和variance是怎么计算得到的,一个是moving mean和moving variance是怎么更新的。
首先说下mean和variance是怎么计算得到的,是以下的这段代码:
# avarage moving_mean and moving_var in multi GPUs
shared_name = tf.get_variable_scope().name
batch_mean = tf.reduce_mean(inputs, axis=axis)
batch_mean = gen_nccl_ops.nccl_all_reduce(
input=batch_mean,
reduction=sum,
num_devices=num_dev,
shared_name=shared_name + _NCCL_mean) * (1.0 / num_dev)
batch_mean_square = tf.reduce_mean(tf.square(inputs), axis=axis)
batch_mean_square = gen_nccl_ops.nccl_all_reduce(
input=batch_mean_square,
reduction=sum,
num_devices=num_dev,
shared_name=shared_name + _NCCL_mean_square) * (1.0 / num_dev)
mean = batch_mean
var = batch_mean_square - tf.square(batch_mean)
假设有N块卡,每个卡上有 个训练样本,且令 ,那么有 ,其中, 是整个的样本数。
这句代码:
batch_mean = tf.reduce_mean(inputs, axis=axis)
其用数学表达式可以表示成:
这句代码:
batch_mean = gen_nccl_ops.nccl_all_reduce(
input=batch_mean,
reduction=sum,
num_devices=num_dev,
shared_name=shared_name + _NCCL_mean) * (1.0 / num_dev)
其会在各卡对相应的变数进行操作,这里进行的操作是求和,最后除N即可,用数学表达式可以表达成:
,因为有 ,那么有:
这个表达式就与论文里的表达式一致了,以下用 代替 。
这句代码:
batch_mean_square = tf.reduce_mean(tf.square(inputs), axis=axis)
其用数学表达式可以表示成:
这句代码:
batch_mean_square = gen_nccl_ops.nccl_all_reduce(
input=batch_mean_square,
reduction=sum,
num_devices=num_dev,
shared_name=shared_name + _NCCL_mean_square) * (1.0 / num_dev)
其用数学表达式可以表示成:
这句代码:
var = batch_mean_square - tf.square(batch_mean)
其用数学表达式可以表示成:
这里利用的一个trick是 ,那么
另外一个值得注意的代码就是这个控制条件:
if int(outputs.device[-1]) == 0:
因为每个GPU上的moving mean和moving variance都一致了,所以没必要每个都更新,更新GPU:0的即可。
综上,看起来这个代码还是不错的,待我在实际项目中再验证验证吧。
在实际项目中的实验论证
说实话我是很想在实验中加入进去的,可惜的是,在我本地跑没有问题,但是在伺服器上跑最终依然有问题,所以最后可能得放弃这个了。
最开始碰到的问题在于Python 2中的对str的解释,报错为:
Traceback (most recent call last):
File "/home/dao/anaconda2/envs/python36/lib/python3.6/site-packages/tensorflow/python/framework/ops.py", line 2349, in get_attr
c_api.TF_OperationGetAttrValueProto(self._c_op, name, buf)
tensorflow.python.framework.errors_impl.InvalidArgumentError: Operation tower_0/lstm2fc/batch_normalization_0/NcclAllReduce_1 has no attr named _XlaCompile.
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/dao/anaconda2/envs/python36/lib/python3.6/site-packages/tensorflow/python/ops/gradients_impl.py", line 391, in _MaybeCompile
xla_compile = op.get_attr("_XlaCompile")
File "/home/dao/anaconda2/envs/python36/lib/python3.6/site-packages/tensorflow/python/framework/ops.py", line 2353, in get_attr
raise ValueError(str(e))
ValueError: Operation tower_0/lstm2fc/batch_normalization_0/NcclAllReduce_1 has no attr named _XlaCompile.
...
在搜索解决方案的时候我碰巧发现本文所写的第二种解决方案(即旷视论文)与第一种解决方案异曲同工(没兴趣去了解代码的根源了)。
第二种解决方案的代码在tensorpack/tensorpack/blob/master/tensorpack/models/batch_norm.py,首先给了我一些关于TF支持nccl的解释:
assert six.PY2 or TF_version >= (1, 10),
"Cross-GPU BatchNorm is only supported in TF>=1.10 ."
"Upgrade TF or apply this patch manually: https://github.com/tensorflow/tensorflow/pull/20360"
链接的issue是Fix gradient of nccl_ops by ppwwyyxx · Pull Request #20360 · tensorflow/tensorflow,个人猜测1.10以下的版本支持Python 2.7的居多,我用Python 3.6,所以报错了。
升级到了1.10.0的版本,发现代码这段加上也是很不错的:
if TF_version <= (1, 12):
try:
from tensorflow.contrib.nccl.python.ops.nccl_ops import _validate_and_load_nccl_so
except Exception:
pass
else:
_validate_and_load_nccl_so()
from tensorflow.contrib.nccl.ops import gen_nccl_ops
else:
from tensorflow.python.ops import gen_nccl_ops
弄好之后又报错:
tensorflow.python.framework.errors_impl.NotFoundError: libnccl.so.2
参考文章tensorflow1.12 多GPU协同训练报错tensorflow.python.framework.errors_impl.NotFoundError: libnccl.so.2发现需要安装nccl库,Nvidia官方的安装指导在Deep Learning SDK Documentation。
好歹不报错了,但是程序似乎不运行了,也查不出原因,所以决定暂时放弃这种做法了。
后续更新:排除bug
终于搞通了,发现是代码的bug,Tensorflow的条件控制没有if else这么简单,必须写到计算图里面去。
这会导致一个什么结果呢?在推理阶段,不需要更新moving mean和moving variance,所以代码会直接推理得到结果:
outputs, _, _ = tf.nn.fused_batch_norm(inputs, gamma, beta, mean=moving_mean, variance=moving_var, epsilon=epsilon, is_training=False)
但是呢,这是一个else语句,计算图是没法把Python里的判断条件放到计算图里去的,如果列印计算图,会发现此时根本就没有直接推理的结果:
print ([n.name for n in tf.get_default_graph().as_graph_def().node if "fused_batch_norm" in n.name])
使用tf.cond()写好了判断函数后,还发现一个问题,在我真实的项目环境中是可以使用的,但是我在写的测试代码中却会报这样的错:
"operation has been marked as not fetchable"
参考Queue operation in conditional execution context fails with "operation has been marked as not fetchable" · Issue #4094 · tensorflow/tensorflow,控制条件不能有太多控制语句,我发现True分支里写了moving mean和moving variance的更新语句,这个会在分支里引入过多的输入,我猜测TensorFlow对此做了限定,在实验中我不在sess.run()中求tf.get_collection(tf.GraphKeys.UPDATE_OPS)的值也是可以不会报错的,综上,我认为错误在于更新语句,也做了一些改进,详细代码见validSyncBN.py。
【已完结】
推荐阅读: