【问题标题】:Why are the parallel tasks always slow at the first time?为什么第一次并行任务总是很慢?
【发布时间】:2018-06-02 01:56:31
【问题描述】:

我有一些分类器,我想对一个样本进行评估。此任务可以并行运行,因为它们彼此独立。这意味着我想并行化它。

我用 python 和 bash 脚本尝试过。问题是,当我第一次运行该程序时,大约需要 30 到 40 秒才能完成。当我连续多次运行该程序时,只需 1s-3s 即可完成。即使我给分类器提供不同的输入,我也会得到不同的结果,所以似乎没有缓存。当我运行一些其他程序然后重新运行该程序时,它又需要 40 秒才能完成。

我还在 htop 中观察到,当程序第一次运行时,CPU 的利用率并不高,但是当我一次又一次地重新运行它时,CPU 就被充分利用了。

有人可以解释一下这种奇怪的行为吗?我怎样才能避免它,即使程序的第一次运行也会很快?

这是python代码:

import time
import os
from fastText import load_model
from joblib import delayed, Parallel, cpu_count
import json

os.system("taskset -p 0xff %d" % os.getpid())

def format_duration(start_time, end_time):
    m, s = divmod(end_time - start_time, 60)
    h, m = divmod(m, 60)
    return "%d:%02d:%02d" % (h, m, s)

def classify(x, classifier_name, path):
    f = load_model(path + os.path.sep + classifier_name)    
    labels, probabilities = f.predict(x, 2)
    if labels[0] == '__label__True':
        return classifier_name
    else:
        return None

if __name__ == '__main__':
    with open('classifier_names.json') as json_data:
        classifiers = json.load(json_data)
    x = "input_text"

    Parallel(n_jobs=cpu_count(), verbose=100, backend='multiprocessing', pre_dispatch='all') \
        (delayed(perform_binary_classification)
         (x, classifier, 'clfs/') for
         classifier in classifiers)

    end_time = time.time()
    print(format_duration(start_time, end_time))

这是 bash 代码:

#!/usr/bin/env bash
N=4
START_TIME=$SECONDS
open_sem(){
    mkfifo pipe-$$
    exec 3<>pipe-$$
    rm pipe-$$
    local i=$1
    for((;i>0;i--)); do
        printf %s 000 >&3
    done
}
run_with_lock(){
    local x
    read -u 3 -n 3 x && ((0==x)) || exit $x
    (
    "$@" 
    printf '%.3d' $? >&3
    )&
}
open_sem $N
for d in classifiers/* ; do
    run_with_lock ~/fastText/fasttext predict "$d" test.txt 
done

ELAPSED_TIME=$(($SECONDS - $START_TIME))
echo time taken $ELAPSED_TIME seconds

已编辑

更大的图景是我正在使用 2 个 API 方法运行烧瓶应用程序。他们每个人都调用并行化分类的函数。当我在做请求时,它的行为方式与下面的这个程序相同。对方法 A 的第一次请求需要很多时间,然后后续请求需要 1 秒。当我切换到方法 B 时,它的行为与方法 A 相同。如果我在方法 A 和方法 B 之间多次切换,例如 A、B、A、B,那么每个请求大约需要 40 秒才能完成。

【问题讨论】:

  • 可能是因为它必须加载库 int 内存和缓存程序等。
  • @WillemVanOnsem 但它应该释放内存对吗?为什么在没有导入库的 bash 脚本中也会观察到类似的行为。 RAM 总是在程序运行后被释放。如果它以某种方式缓存程序,我该如何预先缓存它?
  • 没有程序留在内存中,除非需要内存。它是“免费的”,因为它可以被其他东西占用,但现代操作系统会标记最初存在的内容,因为您可能希望以后重用相同的程序。看是这样的:你执行程序a,程序a(部分)加载到内存中,程序a终止,操作系统将a的内存标记为空闲,你再次执行a,每次你如果它仍在内存中的某处,则加载操作系统看起来的一段程序。
  • 另外,如果你在内存中有一个程序的实例并且你执行另一个实例,则代码部分在实例之间共享(并且不需要再次加载),而每个实例都有自己的自己的数据。当您第一次执行时,您想要执行的“预加载”由操作系统为您完成。
  • 好的。我得到了它。那么为什么 htop 中的核心第一次没有显示出高利用率?然后他们通过连续调用程序来显示它。顺便说一句,我更新了问题并添加了我的问题的更大图景。

标签: python bash parallel-processing joblib


【解决方案1】:

一种方法是修改您的 Python 代码以使用事件循环,始终保持运行状态,并在检测到新作业时并行执行新作业。一种方法是创建一个作业目录,并在有新的待办作业时在该目录中放置一个文件。 python 脚本还应该将已完成的作业移出该目录,以防止多次运行它们。 How to run an function when anything changes in a dir with Python Watchdog?

另一种选择是使用通过管道传输到 python 脚本的 fifo 文件,并向该文件添加新行以用于新作业。 https://www.linuxjournal.com/content/using-named-pipes-fifos-bash

我个人不喜欢在 python 中进行并行化,而更喜欢使用 GNU 并行在 bash 中进行并行化。要做到这一点,我会

  • 使用 bash 和 GNU 并行实现事件循环和作业目录或 fifo 文件作业队列
  • 修改python脚本删除所有并行代码
  • 从标准输入读取每个作业规范
  • 在一个循环中连续处理每一个
  • 管道作业并行,将它们通过管道传送到 ncpu python 进程,每个进程永远运行,等待来自标准输入的下一个作业

例如,类似:

run_jobs.sh:
mkfifo jobs
cat jobs | parallel --pipe --round-robin -n1 ~/fastText/fasttext

queue_jobs.sh:
echo jobspec >> jobs

.py:
for jobspec in sys.stdin:
    ...

这样做的缺点是所有的ncpu python进程都可能有启动慢的问题,但是可以无限期的保持运行,所以问题变得无足轻重,而且代码更简单,更容易调试和维护。

为每个作业规范使用作业目录和文件,而不是使用先进先出作业队列需要稍微多一点的代码,但它也可以更直接地查看哪些作业已排队以及哪些作业已完成。

【讨论】:

    猜你喜欢
    • 2016-05-12
    • 1970-01-01
    • 2023-03-05
    • 2017-08-09
    • 2020-11-11
    • 1970-01-01
    • 1970-01-01
    • 2014-10-31
    • 1970-01-01
    相关资源
    最近更新 更多