多线程降低GPU性能

11

在我的 Python 应用程序中,我使用 Detectron2 在图像上运行预测并检测图像中所有人的关键点。

我想在实时流式传输到我的应用程序中的帧上运行预测(使用 aiortc),但是我发现因为现在在新线程上运行(主线程被服务器占用),所以预测时间要长得多。

在线程上运行预测需要花费 1.5 到 4 秒不等,这太久了。

当在主线程上运行预测(没有视频流部分)时,我可以得到少于一秒的预测时间。

我的问题是为什么会发生这种情况,以及我该如何解决它?为什么从新线程使用 GPU 性能会下降得如此严重?

备注:

  1. 代码在 Google Colab 上测试,使用 Tesla P100 GPU,并通过从视频文件中读取帧来模拟视频流。

  2. 我使用这个问题中的代码计算运行帧预测所需的时间。

我尝试切换到 multiprocessing,但无法使用 cuda(我尝试过 import multiprocessingimport torch.multiprocessing,并使用 set_stratup_method('spawn')),在调用进程上的 start 时会卡住。

示例代码:

from detectron2 import model_zoo
from detectron2.engine import DefaultPredictor
from detectron2.config import get_cfg

import threading
from typing import List
import numpy as np
import timeit
import cv2

# Prepare the configuration file
cfg = get_cfg()
cfg.merge_from_file(model_zoo.get_config_file("COCO-Keypoints/keypoint_rcnn_R_50_FPN_3x.yaml"))
cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = 0.7  # set threshold for this model
cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url("COCO-Keypoints/keypoint_rcnn_R_50_FPN_3x.yaml")

cfg.MODEL.DEVICE = "cuda"
predictor = DefaultPredictor(cfg)


def get_frames(video: cv2.VideoCapture):
    frames = list()
    while True:
        has_frame, frame = video.read()
        if not has_frame:
            break
        frames.append(frame)
    return frames

class CodeTimer:
    # Source: https://dev59.com/tGYq5IYBdhLWcg3weQgy#52749808
    def __init__(self, name=None):
        self.name = " '" + name + "'" if name else ''

    def __enter__(self):
        self.start = timeit.default_timer()

    def __exit__(self, exc_type, exc_value, traceback):
        self.took = (timeit.default_timer() - self.start) * 1000.0
        print('Code block' + self.name + ' took: ' + str(self.took) + ' ms')

video = cv2.VideoCapture('DemoVideo.mp4')
num_frames = round(video.get(cv2.CAP_PROP_FRAME_COUNT))
frames_buffer = list()
predictions = list()

def send_frames():
    # This function emulates the stream, so here we "get" a frame and add it to our buffer
    for frame in get_frames(video):
        frames_buffer.append(frame)
        # Simulate delays between frames
        time.sleep(random.uniform(0.3, 2.1))

def predict_frames():
    predicted_frames = 0  # The number of frames predicted so far
    while predicted_frames < num_frames:  # Stop after we predicted all frames
        buffer_length = len(frames_buffer)
        if buffer_length <= predicted_frames:
            continue  # Wait until we get a new frame

        # Read all the frames from the point we stopped
        for frame in frames_buffer[predicted_frames:]:
            # Measure the prediction time
            with CodeTimer('In stream prediction'):
                predictions.append(predictor(frame))
            predicted_frames += 1


t1 = threading.Thread(target=send_frames)
t1.start()
t2 = threading.Thread(target=predict_frames)
t2.start()
t1.join()
t2.join()

  1. 你是对的,我刚刚注意到我过于简化了我的示例代码,它应该在另一个线程上发送帧。
  2. 我尝试过了,似乎确实已经初始化了,但仍然运行缓慢。
  3. cfg 中,我指定了预测器在 cuda 上运行,并且 DefaultPredictor 将帧移动到 GPU 上。
- SagiZiv
1
听起来不错。你能百分之百确定线程在实际代码中的实现没有引起任何问题吗?是否可以分享(部分)真实代码? - Thijs Ruigrok
1
感谢您更新代码。考虑到线程部分,您的代码似乎很合理。我注意到您从未清除帧缓冲区。在大型视频/图像流的情况下,这可能会占用大量RAM,从而减慢系统甚至导致崩溃(当我加载由7200帧组成的4分钟视频时就发生了这种情况)。 - Thijs Ruigrok
@ThijsRuigrok 谢谢,看起来 AsyncPredictor 在演示包中,所以导入不是那么容易。我正在检查当我安装 detectron 时是否有那个包。 - SagiZiv
@ThijsRuigrok 嗯,要么我使用方法不对,要么这就是为什么它只在演示中出现的原因,我的Colab会话崩溃并显示错误Your session crashed after using all available RAM。它似乎一直在加载和加载东西而没有打印任何内容。我还尝试在10帧后停止预测而不是整个视频,但得到了相同的结果... - SagiZiv
显示剩余6条评论
4个回答

1
问题可能出在:您的硬件、您的库文件,或者是实际代码与示例代码之间的差异。
我在 Nvidia Jetson Xavier 上实现了您的代码。我使用以下命令安装了所有必需的库文件:
# first create your virtual env
virtualenv -p python3 detectron_gpu
source detectron_gpu/bin/activate

#torch for jetson
wget https://nvidia.box.com/shared/static/p57jwntv436lfrd78inwl7iml6p13fzh.whl -O torch-1.8.0-cp36-cp36m-linux_aarch64.whl
sudo apt-get install python3-pip libopenblas-base libopenmpi-dev 
pip3 install Cython
pip3 install numpy torch-1.8.0-cp36-cp36m-linux_aarch64.whl

# torchvision
pip install 'git+https://github.com/pytorch/vision.git@v0.9.0'

# detectron
python -m pip install 'git+https://github.com/facebookresearch/detectron2.git'

# ipython bindings (optional)
pip install ipykernel cloudpickle 

# opencv
pip install opencv-python

之后我在一个示例视频上运行了你的脚本,并收到了以下输出:

Code block 'In stream prediction' took: 2932.241764000537 ms
Code block 'In stream prediction' took: 409.69691300051636 ms
Code block 'In stream prediction' took: 410.03823099981673 ms
Code block 'In stream prediction' took: 409.4023269999525 ms

在第一次检测后,探测器始终需要大约400毫秒才能运行检测。这对于Jetson Xavier来说似乎是正确的。我没有遇到您描述的减速问题。
我必须指出,Jetson是一种特定的硬件。在这个硬件中,RAM内存是CPU和GPU共享的。因此,我不必将数据从CPU传输到GPU。因此,如果您的减速是由CPU和GPU内存之间的传输引起的,我在我的设置中不会遇到此问题。

这很有趣... 我在Colab ProAWS EC2 T4 GPU实例上运行了这个示例代码,并获得了大约800到1200毫秒的时间,因此真正的代码可能会增加减速,但与在主线程上运行预测(没有其他线程)相比仍然要慢得多,平均结果为400毫秒。非常感谢您的帮助。 - SagiZiv
我已经在一个新问题上添加了代码,可以重现在此处看到的缓慢情况。https://stackoverflow.com/questions/70967366/multithreading-slower-on-detectron2-inferencing-with-cv2-videocapture - Austin Ulfers

1
Python线程依赖于GIL,所有试图访问Python对象的C绑定必须锁定它。GPU计算库通常使用C绑定,可能会不时地锁定GIL,从而暂停Python代码执行。
这只是一个猜测,但是可能预测函数需要通过C和GIL锁定,发现自己正在等待写入视频缓冲区的其他线程。然后根据计算如何分解以及Python如何与其他线程交互,我认为对性能的影响可能会变得明显。
您可以:
  • 通过在同一个线程中执行读取和预测来避免多线程。
  • 使用多进程,使GIL不会干扰两个进程之间的操作
  • 使用本地语言编写,例如C、C++...

很有趣… 这个问题有解决的办法吗?我试过使用进程代替线程,但程序出现了未知原因停止响应的情况。 - SagiZiv
多进程的解决方案看起来很合理,但我无法确定为什么它对你不起作用。另一种选择是从主线程中完成所有操作,但你的帧率将取决于预测器的性能。例如,当循环缓冲区已满时,“get_frames”可能会丢弃未读帧,导致系统跳帧。最后一个选择:不要使用Python编写此代码,而是使用本地语言。 - Victor Paléologue
1
这个答案感觉不太准确,可能会误导人。Python确实使用常规的操作系统级线程,而不是模拟它们。GIL的目的是保护对Python对象的修改 - 编译代码(“C绑定”)和特别是GPU代码通常不会这样做,因此不会持有GIL。即使GIL受到争用,切换的时间也在0.005秒左右,这应该在两个线程之间非常平均 - 这比问题中观察到的减速要少得多。 - MisterMiyagi
有趣的想法在主线程上运行它,但是我的服务器本身正在该线程上运行(这是我第一次构建这样的应用程序,所以如果不常规请见谅)。 更改编程语言意味着我们无法使用当前正在使用的Python库,并且需要放弃我们迄今为止在Python中所做的工作。 - SagiZiv
感谢@MisterMiyagi提供的详细信息。我认为你说得对,大多数GPU操作不需要锁定GIL,但由于缺乏细节,我们不能排除它。我正在修复我的观点,这是我从官方文档中“基于Java线程模型”的错误解读中得出的。 - Victor Paléologue
1
我无法避免多线程,因为帧始终来自另一个线程,而且我不想在该线程中添加可能会减慢其速度并使其错过一些帧的代码。 尝试使用多进程,但它只是冻结了,应用程序没有响应。 另一种语言的代码可能更好,但这需要我改变很多代码并找到一个等效的库来进行预测。 - SagiZiv

-1

看不到完整的代码,这里有几个建议:

  • 你可能会遇到每次启动新线程的开销问题。所以尝试使用线程池选项,而不是每次都启动新线程。
  • 如果你没有将工作负载移到GPU上——这意味着它是CPU绑定的任务,Python线程不是处理该任务的正确工具。对于CPU密集型任务,你应该使用https://docs.python.org/3/library/multiprocessing.html#module-multiprocessing

1
  1. 我只创建了两个线程——一个用于视频流,另一个用于预测。
  2. 帧缓冲区在CPU上,但每一帧都会被predictor对象移动到GPU上。
- SagiZiv
正如我在问题中所写的那样,由于某种原因,多进程无法工作。 - SagiZiv

-2

一些操作是I/O绑定的。例如,每个cv2.imread调用都会导致I/O开销。您可以阅读这篇文章,其中提到:“并非所有算法都可以并行和分布到处理器的所有核心上 - 有些算法只是单线程的自然状态。”

这意味着计算机视觉算法的多进程必须是全局的:单个操作(如imread)不会因为多线程而得到改善。但是,通过执行其他操作并行处理,您有时会获得速度提升,因为它们不受I/O或其他任何限制。此时,您可能会看到整体加速:

如果您运行单个imread:

  • 非多线程:5毫秒= imread成本
  • 多线程:7毫秒=多线程成本+ imread成本

但是,如果您运行可以进行多线程处理的操作:

非多线程:5毫秒+10毫秒=imread成本+操作成本 多线程:2毫秒+5毫秒+5毫秒=多线程成本+imread成本+并行操作成本
(这些数字不是真实的,它们只是为了说明我的意思)

我正在使用CV2读取一个视频文件,这只是一个示例,因为我不能确定视频流部分。在实际代码中,我没有视频文件。 - SagiZiv
我知道,我刚刚编辑了这条消息。我的帖子只是为了解释一下为什么你的程序在多线程情况下可能会变慢。你的外部库中有很多函数或操作可能不支持并行处理。imread函数也是一个例子,还有其他像imread这样的函数,可能会导致I/O开销。不幸的是,似乎很难定义哪些函数是如此。 - Pommepomme
我不明白这与问题中所展示的情景有何关联。你能否请说明一下?在进行I/O绑定操作,即读取帧,以及计算绑定操作,即图像识别上,这正是问题场景所要求的。因此,这个答案似乎暗示它应该在多线程下更快。 - MisterMiyagi
不,我的回答只是建议如果你只执行不能并行的操作,那么使用多线程会比单线程更慢。但是,如果在你的代码中使用了其他可并行化的操作,随着线程数量的增加,你将全局获得时间,但如果你的操作不能并行化,则不一定会如此。 - Pommepomme

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接