多线程会降低 GPU 性能

Posted

技术标签:

【中文标题】多线程会降低 GPU 性能【英文标题】:Multithreading degrades GPU performance 【发布时间】:2021-11-19 18:38:58 【问题描述】:

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

我想对实时流式传输到我的应用程序的帧运行预测(使用 aiortc),但我发现预测时间更糟,因为它现在在新线程上运行(主线程被服务器占用)。

在线程上运行预测需要 1.5 到 4 之间的任何时间,这很长。

在主线程上运行预测时(没有视频流部分),我得到的预测时间不到一秒

我的问题是为什么会发生这种情况,我该如何解决?为什么从新线程中使用 GPU 性能会急剧下降?

注意事项:

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

    我使用this question 中的代码计算在帧上运行预测所需的时间。

我尝试切换到多处理,但无法使其与 cuda 一起工作(我尝试了 import multiprocessing 以及 import torch.multiprocessingset_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://***.com/a/52749808/9977758
    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。我不明白你是如何使用线程的,因为看起来你目前有一个线程同时运行检测和get_frames 函数。让一个线程用图像填充缓冲区,而另一个线程处理图像对我来说是有意义的。 2.你能在把它变成一个线程之前检查检测模型是否完全初始化吗?通常检测模型需要更长的时间(几秒钟)来处理第一帧。您可以尝试让模型在初始化后直接处理一个虚拟帧/空法师(在此行之后predictor = DefaultPredictor(cfg))。 3. 能否检查检测模型是否在 GPU 上运行。我没有看到将您的模型或图像移动到 GPU 的代码。也许这是在DefaultPredictor 内完成的。但是我不能确定。 @ThijsRuigrok 1. 你是对的,我刚刚注意到我过度简化了我的示例代码,它假设在另一个线程上发送帧。 2.我试过了,它似乎确实已经初始化但仍然运行缓慢。 3. 在cfg 中,我指定预测器在cuda 上运行,DefaultPredictor 将帧移动到 GPU。 听起来不错。您是否 100% 确定在实际代码中执行线程不会造成任何问题?是否可以共享(部分)真实代码? Tnx 用于更新代码。考虑到线程部分,您的代码似乎合乎逻辑。我注意到您从不清除帧缓冲区。如果视频/图像流较大,这可能会占用大量内存,这可能会减慢您的系统甚至崩溃(当我加载包含 7200 帧的 4 分钟视频时发生在我身上)。 【参考方案1】:

没有看到完整的代码,这里有一些建议:

您可能每次都会遇到启动新线程的开销。因此,请探索线程池的选项,而不是每次都启动新线程。 如果您不将工作负载转移到 GPU - 这意味着它的 CPU 绑定任务和 Python 线程不是该任务的正确工具。对于 CPU 密集型任务,您应该使用 https://docs.python.org/3/library/multiprocessing.html#module-multiprocessing

【讨论】:

1) 我只创建 2 个线程 - 一个用于视频流,一个用于预测 2) 帧缓冲区在 CPU 上,但每一帧都由 predictor 对象移动到 GPU 正如我在问题中所写,由于某种原因,多处理不起作用【参考方案2】:

Python 线程依赖于GIL,它必须被所有试图访问 Python 对象的 C 绑定锁定。 GPU 计算库通常使用 C 绑定,并且可能会不时锁定 GIL,从而暂停 Python 代码执行。

这是一个疯狂的猜测,但有可能需要通过 C 和 GIL 锁的预测函数发现自己正在等待正在写入视频缓冲区的其他线程。然后根据计算的分解方式以及 Python 与其他线程的处理方式,我认为对性能的影响可能会变得明显。

你可以:

通过在同一线程中执行读取和预测来避免多线程。 使用多处理,以便 GIL 不会干扰两个进程 用 C、C++ 等本地语言编写代码...

【讨论】:

有趣...有没有办法克服它?我尝试使用进程而不是线程,但程序只是因为未知原因停止响应。 多进程解决方案似乎合法,但我无法解释为什么它不适合您。另一种方法是从主线程执行所有操作,但您的帧速率将取决于预测器的性能。例如get_frames 可能会在其循环缓冲区已满时丢弃未读帧,从而使您的系统跳过帧。最后一种选择:不要用 Python 编写代码,而是用本地语言编写代码。 这个答案感觉不够准确,足以产生误导。 Python 确实使用常规的操作系统级线程,它不模拟它们。 GIL 的目的是保护对 Python 对象的修改 - 编译代码(“C 绑定”),尤其是 GPU 代码通常不会这样做,因此 持有吉尔。即使 GIL 存在争议,切换时间也大约为 0.005 秒,这在两个线程之间应该是相当的——这比在问题中观察到的减速要少得多。 在主线程上运行它的有趣想法,但我让服务器本身在该线程上运行(这是我第一次构建这样的应用程序,如果它非常规,很抱歉)。更改编程语言意味着我们不能使用我们现在正在使用的 python 库并处理我们迄今为止在 python 中所做的事情 -我无法避免多线程,因为帧总是来自另一个线程,我不想向这个线程添加代码,这可能会减慢它的速度并使它错过一些帧。 - 尝试多处理它只是冻结,我没有得到应用程序的响应。 - 用另一种语言编写的代码可能更好,但这需要我更改大量代码并找到一个等效的库来进行预测。【参考方案3】:

某些操作受 I/O 限制。例如,每个 cv2.imread 调用都会导致 I/O 开销。您可以阅读此article,其中说:“并非所有算法都可以并行化并分布到处理器的所有内核 - 有些算法本质上只是单线程。”

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

如果你运行单个 imread:

非多线程:5 毫秒 = 读取成本 多线程:7 毫秒 = 多线程成本 + 读取成本

但是如果你运行的操作可以是多线程的:

非多线程:5 毫秒 + 10 毫秒 = 读取成本 + 操作成本 多线程:2ms + 5 ms + 5 ms = 多线程成本 + imread 成本 + 并行操作成本

(这些数字不是真实的,它们只是为了说明我的意思)

【讨论】:

我正在使用 CV2 读取视频文件作为示例,因为我无法确定视频流部分。在真实的代码中,我没有视频文件 我知道,我刚刚编辑了消息。我的帖子只是为了解释一下为什么您的程序可能会因多线程而变慢。您的外部库中有大量可以不并行的函数或操作。 imread 函数也是一个示例,还有其他函数(如 imread)可能导致 I/O 开销。不幸的是,似乎很难定义哪些 我不明白这如何适用于问题中显示的场景。你能澄清一下吗?执行 I/O 绑定操作(即读取帧)和计算绑定操作(即图像识别)正是问题场景已经执行的操作。因此,这个答案似乎表明多线程应该更快 不,我的回答只是表明,如果您只执行不可并行化的操作,那么多线程而不是单线程的程序会更慢。但是,如果在您的代码中使用其他可并行化的操作,您将在增加线程数时全局获得时间,但如果您的操作不可并行化,则不一定【参考方案4】:

问题在于:您的硬件、您的库,或者您的示例代码和真实代码之间的差异。

我在 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 instance with T4 GPU 上都运行了这个示例代码,得到了大约 800 到 1200 毫秒的时间,所以真正的代码可能会增加慢-down,但与在主线程(没有任何其他线程)上运行预测相比仍然慢得多,结果平均为 400 毫秒。非常感谢您的帮助

以上是关于多线程会降低 GPU 性能的主要内容,如果未能解决你的问题,请参考以下文章

高性能异步爬虫

05.java多线程问题

Faiss使用多线程出现的性能问题

多线程计算平台的性能模型

多线程PPT

Python第四周之多线程和多进程之线程加锁五车争霸赛