TensorFlow sess.run()能否释放Python的GIL?并行训练性能异常求解
我想要在TensorFlow会话里并行运行多个train_op,之前了解到sess.run()可以释放Python的GIL(全局解释器锁),但实际测试下来似乎没生效——我有8个可用GPU,当num_threads设为4时耗时24秒,设为8时反而增加到54秒,看起来反而受GIL限制了。
以下是我的测试代码:
from threading import Thread import tensorflow as tf import time num_threads = 8 a = [] for i in range(num_threads): with tf.device('/cpu:0'): a.append(tf.get_variable(name='a_%d'%i, shape=[5000, 50, 5, 5, 5, 5], initializer=tf.truncated_normal_initializer())) b = [] for i in range(num_threads): with tf.device('/cpu:0'): b.append(tf.get_variable(name='b_%d'%i, shape=[5000, 50, 5, 5, 5, 5], initializer=tf.truncated_normal_initializer())) train_ops = [] for i in range(num_threads): with tf.device('gpu:%d'%i): loss = tf.multiply(a[i], b[i], name='loss_%d'%i) train_ops.append(tf.train.GradientDescentOptimizer(0.01).minimize(loss)) sess = tf.Session() sess.run(tf.initialize_all_variables()) def train_function(train_op): for i in range(20): sess.run(train_op) train_threads = [] for train_op in train_ops: train_threads.append(Thread(target=train_function, args=(train_op,))) start = time.time() for t in train_threads: t.start() for t in train_threads: t.join() end = time.time() print('elapsed time is:', end-start)
我的核心问题是:是不是我的实现哪里出错了?如果这种方式没法释放GIL,那正确的释放方式是什么?
另外,我知道分布式TensorFlow用gRPC能释放GIL,但gRPC的开销比C pthread这类多线程大,我希望线程间能通信且开销尽可能小。如果没有办法用Python释放GIL,能不能写C++扩展实现多线程?或者有没有其他无GIL的语言可以替代?
问题分析与解决方案
先给你梳理下当前代码的核心问题,以及对应的优化方向:
1. 你的性能瓶颈不是GIL,是跨设备数据拷贝开销
你把所有变量a、b都放在了/cpu:0上,然后每个train_op绑定到不同GPU。这意味着每次GPU执行计算时,都要从CPU内存拷贝超大张量([5000,50,5,5,5,5]的规模不小),当线程数增加到8时,CPU到多个GPU的数据拷贝会形成严重的资源竞争,反而拖慢整体速度——这才是你看到耗时翻倍的主要原因,而非GIL限制。
2. 优化第一步:修正设备分配,让变量和计算同处GPU
把变量直接分配到对应的GPU上,避免跨设备拷贝:
a = [] for i in range(num_threads): with tf.device(f'/gpu:{i}'): # 变量直接放到对应GPU a.append(tf.get_variable(name=f'a_{i}', shape=[5000, 50, 5, 5, 5, 5], initializer=tf.truncated_normal_initializer())) b = [] for i in range(num_threads): with tf.device(f'/gpu:{i}'): b.append(tf.get_variable(name=f'b_{i}', shape=[5000, 50, 5, 5, 5, 5], initializer=tf.truncated_normal_initializer()))
这样每个GPU的计算都使用本地变量,彻底消除跨设备拷贝的开销,同时TensorFlow在执行底层C++计算时会自动释放GIL,让多线程并行真正生效。
3. 更高效的方案:用TensorFlow原生多GPU并行机制,绕开Python线程
Python Thread本身受GIL限制,即使sess.run()会释放GIL,线程调度的开销也不可忽视。TensorFlow提供了原生的多GPU并行策略,比如tf.distribute.MirroredStrategy(针对单机器多GPU场景),它在C++ runtime层面实现并行,完全避开Python GIL,效率更高:
import tensorflow as tf import time num_threads = 8 # 指定要使用的GPU设备 strategy = tf.distribute.MirroredStrategy(devices=[f'/gpu:{i}' for i in range(num_threads)]) with strategy.scope(): # 在策略作用域内定义模型和变量,会自动复制到每个GPU def create_train_op(): a = tf.get_variable(name='a', shape=[5000, 50, 5, 5, 5, 5], initializer=tf.truncated_normal_initializer()) b = tf.get_variable(name='b', shape=[5000, 50, 5, 5, 5, 5], initializer=tf.truncated_normal_initializer()) loss = tf.multiply(a, b) return tf.train.GradientDescentOptimizer(0.01).minimize(loss) train_op = create_train_op() sess = tf.Session(strategy=strategy) sess.run(tf.initialize_all_variables()) start = time.time() for _ in range(20): sess.run(train_op) end = time.time() print('elapsed time is:', end-start)
MirroredStrategy会自动处理变量复制、并行计算、梯度同步,不需要手动管理线程,性能比Python Thread方案好很多。
4. 关于GIL释放的补充说明
如果一定要用Python Thread,要确保每个线程内的sess.run()执行的是纯粹的TensorFlow操作,尽量减少Python端的逻辑(比如避免在train_function里加过多Python循环或计算)。另外,旧版本的tf.Session是线程安全的,可以被多个线程同时调用,但还是推荐用TensorFlow原生的并行机制。
5. C++扩展或无GIL语言的选项
如果以上方案都不能满足你的需求,确实可以通过C扩展直接调用TensorFlow的C API,完全避开Python GIL——TensorFlow提供了完整的C++ API,你可以在C++中创建会话、定义计算图、实现多线程并行,没有GIL的限制。另外,Go语言也有TensorFlow的绑定,Go没有GIL,也能实现高效的多线程并行。
内容的提问来源于stack exchange,提问作者Luochao Wang

