En mi aplicación de Python, estoy usando Detectron2 para ejecutar la predicción en una imagen y detectar los puntos clave de todos los humanos en la imagen.
Quiero ejecutar la predicción en fotogramas que se transmiten a mi aplicación en vivo (usando aiortc), pero descubrí que el tiempo de predicción es mucho peor porque ahora se ejecuta en un nuevo subproceso (el subproceso principal está ocupado con el servidor).
Ejecutar predicciones en un hilo lleva entre 1,5 y 4 segundos , lo cual es mucho.
Cuando ejecuto las predicciones en el hilo principal (sin la parte de transmisión de video), obtengo tiempos de predicción de menos de un segundo .
Mi pregunta es ¿por qué sucede y cómo puedo solucionarlo? ¿Por qué el rendimiento de la GPU se degrada tan drásticamente cuando se usa desde un hilo nuevo?
Notas:
El código se prueba en Google Colab con GPU Tesla P100 y la transmisión de video se emula leyendo fotogramas de un archivo de video.
Calculo el tiempo que lleva ejecutar la predicción en un marco usando el código en esta pregunta .
Intenté cambiar a multiprocesamiento en su lugar, pero no pude hacerlo funcionar con cuda (intenté tanto import multiprocessing como import torch.multiprocessing con set_stratup_method('spawn') ) simplemente se atasca al llamar al start del proceso.
Código de ejemplo:
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://stackoverflow.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()Al no ver el código completo, aquí hay algunas sugerencias:
Los subprocesos de Python se basan en el GIL , que debe estar bloqueado por todos los enlaces de C que intentan acceder a los objetos de Python. Las bibliotecas informáticas de la GPU suelen utilizar enlaces C y podrían bloquear la GIL de vez en cuando y, por lo tanto, pausar la ejecución del código de Python.
Es una suposición descabellada, pero es posible que la función predictora, que necesita pasar por C y un bloqueo de GIL, se encuentre esperando a los otros subprocesos que están escribiendo los búferes de video. Luego, dependiendo de cómo se desglose el cálculo y cómo Python haga malabarismos con su otro hilo, supongo que el impacto en el rendimiento puede ser visible.
Puedes:
Algunas operaciones están vinculadas a E/S. Por ejemplo, cada llamada cv2.imread da como resultado una sobrecarga de E/S. Puede leer este artículo que dice: "No todos los algoritmos se pueden hacer paralelos y distribuir a todos los núcleos de un procesador; algunos algoritmos son simplemente de un solo subproceso por naturaleza".
Esto significa que el multiprocesamiento para los algoritmos de visión por computadora debe ser global: una sola operación (como imread) no mejorará con el multiproceso. Sin embargo, a veces ganará velocidad al realizar otras operaciones en paralelo porque no están limitadas por E/S ni nada más. En este punto, probablemente verá una aceleración general:
Si ejecuta imread único:
Pero si ejecuta operaciones que pueden ser multiproceso:
(Estas cifras no son reales, son solo para ilustrar lo que quiero decir)
El problema está en: tu hardware, tus bibliotecas o, en las diferencias entre tu código de ejemplo y el código real.
Implementé tu código en una Nvidia Jetson Xavier. Instalé todas las bibliotecas necesarias usando los siguientes comandos:
# 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-pythonDespués de eso, ejecuté su script de ejemplo en un video de ejemplo y recibí el siguiente resultado:
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 msDespués de la primera pasada, el detector tarda constantemente alrededor de 400 ms en ejecutar la detección. Lo que parece correcto para un Supersónico Xavier. No experimento la desaceleración que describiste.
Debo señalar que el Supersónico es una pieza específica de hardware. En este hardware la memoria RAM se comparte entre la CPU y la GPU. Por lo tanto, no tengo que transferir los datos de la CPU a la GPU. Entonces, si su ralentización se debe a la transferencia entre la memoria de la CPU y la GPU, no experimentaré este problema en mi configuración.