NVIDIA / VideoProcessingFramework

Set of Python bindings to C++ libraries which provides full HW acceleration for video decoding, encoding and GPU-accelerated color space and pixel format conversions
Apache License 2.0
1.3k stars 231 forks source link

how to avoid race condition when using PySurfaceConverter in multiple processes? #554

Open niujiabenbeng opened 9 months ago

niujiabenbeng commented 9 months ago

Race conditions occur when using PySurfaceConverter in multiple processes. #506 suggests cloning the output surface, which reduces the problem a lot but does not solve it.

As the following code shows, we convert the same surface twice, but the results are not equal when using multiple processing.

#! /usr/bin/env python
# coding: utf-8

# pylint: disable=all

import multiprocessing
import numpy as np
import PyNvCodec as nvc
import PytorchNvCodec as pnvc

class NvColorConverter:
    "Color converter using PySurfaceConverter."

    def __init__(self, width, height, gpuid=0):
        # yapf: disable
        self.width, self.height = width, height
        self.context = nvc.ColorspaceConversionContext(
            nvc.ColorSpace.BT_601, nvc.ColorRange.MPEG)
        self.to_yuv = nvc.PySurfaceConverter(
            width, height, nvc.PixelFormat.NV12,
            nvc.PixelFormat.YUV420, gpuid)
        self.to_rgb = nvc.PySurfaceConverter(
            width, height, nvc.PixelFormat.YUV420,
            nvc.PixelFormat.RGB, gpuid)
        self.to_planar = nvc.PySurfaceConverter(
            width, height, nvc.PixelFormat.RGB,
            nvc.PixelFormat.RGB_PLANAR, gpuid)
        self.downloader = nvc.PySurfaceDownloader(
            width, height, nvc.PixelFormat.RGB_PLANAR, gpuid)
        # yapf: enable

    def convert_color(self, surface):
        surface = self.to_yuv.Execute(surface, self.context)
        surface = self.to_rgb.Execute(surface, self.context)
        surface = self.to_planar.Execute(surface, self.context)
        # We clone the surface as suggested.
        surface = surface.Clone()
        frame = np.ndarray(shape=(0, ), dtype=np.uint8)
        self.downloader.DownloadSingleSurface(surface, frame)
        return frame

# arg is a placeholder
def test_decode_video(arg):
    path, gpuid = "./test.mp4", 0
    dec = nvc.PyNvDecoder(path, gpuid)
    converter = NvColorConverter(dec.Width(), dec.Height(), gpuid)
    for i in range(dec.Numframes()):
        surface = dec.DecodeSingleSurface()
        if surface.Empty(): break
        # We use the same converter to convert the same surface twice,
        # When processes = 1, both arrays have the same value,
        # When processes > 1, two arrays are not equal.
        array1 = converter.convert_color(surface)
        array2 = converter.convert_color(surface)
        if not np.array_equal(array1, array2):
            print("frame not match")

def main():
    with multiprocessing.Pool(processes=8) as pool:
        pool.map(test_decode_video, [None] * 16)

if __name__ == "__main__":
    main()
RomanArzumanyan commented 8 months ago

Hi @niujiabenbeng

Apologies for big delay in response. Please consider checking out https://github.com/RomanArzumanyan/VALI repository. It's a VPF spin-off which is actively developed and maintained. It has compatible API and package naming.

Just clone your issue there. Thanks!