Skip to content

Deadlock when two threads construct vali.PyDecoder concurrently (VALI 4.8.8 regression, GB10/aarch64) #398

Description

@huangyingw

Summary

On a NVIDIA GB10 (DGX Spark, aarch64) box, Jasna v0.10.0 hangs during warmup — no error, no output, just a permanent stall — after the VALI dependency moved from 4.8.7 to 4.8.8.

The hang is a deadlock when two threads construct vali.PyDecoder concurrently. Jasna creates two NvidiaVideoReader instances from different threads (jasna/pipeline_threads.py:77 in the decode/detect worker and :369 in the blend/encode worker); when both enter the vali.PyDecoder(...) constructor at the same time, they lock up in the C++ layer.

Pinning VALI back to 4.8.7 makes the hang disappear, so this looks like a regression in the VALI fork rather than in Jasna itself. I could not reproduce it on x86 — it may be timing-specific to GB10/aarch64.

Environment

Jasna v0.10.0 (source build)
VALI 4.8.8 (f4a67f86) — 4.8.7 (56e01a00) is fine
GPU / platform NVIDIA GB10 (DGX Spark), aarch64
Driver / CUDA 580.173.02 / CUDA 13.0
Python / PyAV 3.13 / 18.0.0 (built from PyAV main, linked against self-built FFmpeg 8.0)
OS Ubuntu 24.04, kernel 6.17.0-1029-nvidia

Symptoms

Warmup starts and then never finishes. Observed with py-spy dump:

  • both the decode/detect and the blend/encode thread sit on the vali.PyDecoder(...) constructor line for 20s+, not moving
  • all 66 threads sleeping
  • CPU ~1.5%, GPU 0%, no network activity

So it is a genuine deadlock, not slow work.

Evidence that it is VALI 4.8.8, not Jasna

  1. Backend swapJASNA_DECODE_BACKEND=pyav-hw with otherwise identical arguments completes 100% of the time (77.3 fps). Only the VALI path hangs.
  2. Version bisect — VALI 4.8.7 never showed this. The two-reader structure in Jasna has not changed since v0.9.1, so the variable is the VALI bump.
  3. Likely culprit — the only new locking between 4.8.7 and 4.8.8 is in d14a25b "fix(decode): retry pending packet", which adds std::unique_lock lock{m_mutex} in has_pending() in src/TC/src/TaskDecodeFrame.cpp. My guess is that this new mutex can be taken while another decoder is still initialising, and crosses with an existing lock during concurrent construction. I have not verified that interleaving in a debugger, so please treat the exact mechanism as a hypothesis — the reproducible facts are the three points above.

(The other 4.8.8 commit, 3ad0d54 "tolerate corrupt packets during decode", adds DecodeSingleSurfaceAsyncDetailed and does not introduce locks.)

Workaround currently in use

Serialising decoder construction with a module-level lock is enough — construction is only milliseconds, so serialising it costs nothing, and decoding afterwards is unaffected:

_VALI_DECODER_INIT_LOCK = threading.Lock()

with _VALI_DECODER_INIT_LOCK:
    self.decoder = vali.PyDecoder(
        file, {}, gpu_id=device.index or 0, stream=self.stream.cuda_stream
    )

With this in place the VALI path completes reliably at 80.7 fps — faster than the pyav-hw fallback (77.3 fps), which is why keeping VALI is worthwhile.

Happy to run any additional diagnostics on the GB10 box (gdb/py-spy traces, patched VALI builds) if that helps narrow it down.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions