为什么在使用 python 的 Pool 模块并行运行该过程时比较两个图像需要更长的时间?



我正在开发一个程序,该程序涉及计算大约480对图像(20个目录,每个目录中大约有24个图像)的相似性分数。我使用sentence_transformersPython模块进行图像比较,在我的Windows 11机器上,串行运行时比较两个图像大约需要0.1-0.2秒,但由于某种原因,使用进程Pool并行运行时,这一时间会增加到1.5到3.0秒。所以,要么a),幕后发生了一些我还不知道的事情,要么b)我只是做错了。

以下是图像比较函数的大致结构:

def compare_images(image_one, image_two, clip_model):
start = time()
images = [image_one, image_two]
# clip_model is set to SentenceTransformer('clip-ViT-B-32') elsewhere in the code
encoded_images = clip_model.encode(images, batch_size = 2, convert_to_tensor = True, show_progress_bar = False)
processed_images = util.paraphrase_mining_embeddings(encoded_images)
stop = time()
print("Comparison time: %f" % (stop - start) )
score, image_id1, image_id2 = processed_images[0]
return score

以下是串行版本代码的粗略结构,用于比较每个图像:

def compare_all_images(candidate_image, directory, clip_model):
for dir_entry in os.scandir(directory):
dir_image_path = dir_entry.path
dir_image = Image.open(dir_image_path)
similiarity_score = compare_images(candidate_image, dir_image, clip_model)
# ... code to determine whether this is the maximum score the program has seen...

以下是并行版本的大致结构:

def compare_all_images(candidate_image, directory, clip_model):
pool_results = dict()
pool = Pool()
for dir_entry in os.scandir(directory):
dir_image_path = dir_entry.path
dir_image = Image.open(dir_image_path)
pool_results[dir_image_path] = pool.apply_async(compare_images, args = (candidate_image, dir_image, clip_model)
# Added everything to the pool, close it and wait for everything to finish
pool.close()
pool.join()
# ... remaining code to determine which image has the highest similarity rating

我不确定我可能在哪里犯了错。

有趣的是,我还开发了一个较小的程序来验证我是否做得正确:

def func():
sleep(6)
def main():
pool = Pool()
for i in range(20):
pool.apply_async(func)
pool.close()
start = time()
pool.join()
stop = time()
print("Time: %f" % (stop - start) ) # This gave an average of 12 seconds 
# across multiple runs on my Windows 11 
# machine, on which multiprocessing.cpu_count=12

这是一个试图将事物与句子转换符平行的问题,还是问题出在其他地方?

更新:现在我特别困惑。我现在只将str对象传递给比较函数,并暂时将return 0作为函数中的第一行,以查看是否可以进一步隔离问题。奇怪的是,即使并行函数现在什么都不做,从池关闭到pool.join()完成之间似乎仍要过几秒(通常在5秒左右)。有什么想法吗?

更新2:我又玩了一些游戏,发现一个空池仍然有一些开销。这是我目前正在测试的代码:

# ...
pool = Pool()
pool.close()
start = time()
DebuggingUtilities.debug("empty pool closed, doing a join on the empty pool to see if directory traversal is messing things up")
pool.join()
stop = time()
DebuggingUtilities.debug("Empty pool join time: %f" % (stop - start) )

这给了我一个";空池加入时间";大约5秒。将这个片段移到我的主函数的第一部分仍然会产生相同的结果。也许Pool在Windows上的工作方式不同?在WSL(Ubuntu 20.04)中,同样的代码运行大约0.02秒。那么,是什么导致一个空的Pool在Windows上挂起这么长时间呢?

更新3:我又发现了一个。如果我只有from multiprocessing import Poolfrom time import time导入,那么空池问题就会消失。然而,该程序在多个源文件中使用大量导入语句,这会导致程序在首次启动时挂起一点。我怀疑这是由于某种原因而向下传播到Pool中的。不幸的是,我需要源文件中的所有import语句,所以我不知道如何绕过它(或者为什么导入会影响空池)。

UPDATE 4:所以,很明显是from sentence_transformers import SentenceTransformer行引起了问题(如果没有导入,pool.join()调用发生得相对较快。我认为现在最简单的解决方案是简单地将compare_images函数移动到一个单独的文件中。我将在实现此功能时用更新再次更新此问题。

UPDATE 5:我做了更多的尝试,在Windows上,每当创建Pool时,导入语句都会被执行多次,我觉得这很奇怪。这是我用来验证的代码:

from multiprocessing import Pool
from datetime import datetime
from time import time
from utils import test
print("outside function lol")
def get_time():
now = datetime.now()
return "%02d/%02d/%04d - %02d:%02d:%02d" % (now.month, now.day, now.year, now.hour, now.minute, now.second)

def main():
pool = Pool()
print("Starting pool")
"""
for i in range(4):
print("applying %d to pool %s" % (i, get_time() ) )
pool.apply_async(test, args = (i, ) )
"""
pool.close()
print("Pool closed, waiting for all processes to finish")
start = time()
pool.join()
stop = time()
print("pool done: %f" % (stop - start) )
if __name__ == "__main__":
main()

通过Windows命令提示符运行:

outside function lol
Starting pool
Pool closed, waiting for all processes to finish
outside function lol
outside function lol
outside function lol
outside function lol
outside function lol
outside function lol
outside function lol
outside function lol
outside function lol
outside function lol
outside function lol
outside function lol
pool done: 4.794051

运行WSL:

outside function lol
Starting pool
Pool closed, waiting for all processes to finish
pool done: 0.048856

UPDATE 6:我想我可能有一个变通方法,那就是在一个不直接或间接从sentence_transformers导入任何内容的文件中创建Pool。然后,我将模型和sentence_transformers中所需的任何其他内容作为参数传递给处理Pool并启动所有并行进程的函数。由于sentence_transformers导入似乎是唯一有问题的一个,所以我将把该导入语句包装在if __name__ == "__main__"中,使其只运行一次,这很好,因为我将从中传递我需要的东西作为参数。这是一个相当棘手的解决方案,可能不是其他人认为的";Pythonic";,但我觉得这会奏效的。

更新7:解决方法已成功。我已经设法将空池的池加入时间降到了合理的水平(0.2-0.4秒)。这种方法的缺点是,将整个模型作为参数传递给并行函数肯定会有相当大的开销,这是因为我需要在与导入模型不同的地方创建Pool。不过我很接近。

我做了更多的挖掘,认为我终于发现了问题的根源,这与这里所描述的一切有关。

总之,在Linux系统上,进程从主进程分叉,这意味着当前进程状态被复制(这就是为什么import语句不会多次运行的原因)。在Windows(和macOS)上,进程是派生的,这意味着解释器从";主";文件,从而再次运行所有import语句。所以,我看到的行为不是一个bug,但我需要重新思考我的程序设计来解释这一点。

最新更新