PR #25544 opened by Jake (jakefineman)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25544
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25544.patch

Two fixes in the LibTorch backend.

1. dnn_free_model_th() destroys lltask_queue and task_queue with 
ff_queue_destroy(), which frees only the queue nodes. Every task still queued 
at teardown leaks with its input and output frames. With async=1 (the default), 
this happens whenever the graph is freed before all results were fetched, e.g. 
-frames:v 1 on a longer input. The patch pops and frees the queued lltasks and 
tasks after ff_dnn_wait_requests(), as dnn_free_model_ov() does.
2. In the CUDA input path, execute_model_th() calls fill_model_input_th_cuda(), 
which reads `request->lltasks[0]` without popping an lltask or recording it. 
That slot is uninitialized on the first request and NULL after a completed one. 
The input and output tensors are NULL too, so a CUDA input frame crashes. The 
patch does the same request setup as the CPU path: pop the lltask, record it, 
set lltask_count, and allocate the input and output tensors.

**Testing**

macOS arm64, CPU libtorch, TorchScript model `x * 0.5`.

Patch 1, `leaks --atExit`:

| case | master | this PR |
|---|---|---|
| ffmpeg, async=1, `-frames:v 1` on a 2 s source, 6 runs | 3 of 6 runs leak (1 
to 3 TaskItems, each with two frames) | 0 leaks in 6 runs |
| same with batch_size=4, and with `-frames:v 3` | leaks | 0 leaks |
| libavfilter API: push 6 frames with async=0:batch_size=4, free the graph 
without EOF | 2 LastLevelTaskItems leaked | 0 leaks |

On master the CLI leak depends on timing, so a finite number of runs can't 
fully rule it out. The patched binary also ran clean under libgmalloc. Output 
is unchanged: 50-frame framemd5 is identical to master with async=0 and 1, and 
with batch_size=1 and 4.

Patch 2: there is no CUDA here, so this is **untested on CUDA**. What was 
checked:
- dnn_backend_torch.cpp compiles with CONFIG_CUDA=1 and no new warnings. 
ffnvcodec's dynlink_cuda.h stood in for cuda.h.
- A test program that includes the backend with CONFIG_CUDA=1 and passes one 
AV_PIX_FMT_CUDA task to execute_model_th() segfaults on master. With the patch, 
the lltask is popped and recorded and execution reaches the CUDA from_blob(), 
which throws on a CPU-only libtorch.
- With an empty queue, master segfaults and the patch returns EINVAL.
- The non-CUDA build is unchanged: the stripped binaries are byte-identical.

Not tested: a real CUDA run (hwupload_cuda into dnn_processing with 
device=cuda), the CUDA output path, and async=1 with CUDA frames.


From 63a0736927346726ca6a09c3a594babbb3eee30f Mon Sep 17 00:00:00 2001
From: Jake Fineman <[email protected]>
Date: Tue, 27 Oct 2026 15:18:34 -0400
Subject: [PATCH 1/2] avfilter/dnn_backend_torch: free queued tasks in
 dnn_free_model_th()

dnn_free_model_th() waits for the inference requests and destroys them,
but then only calls ff_queue_destroy() on lltask_queue and task_queue.
That frees the queue nodes, not the LastLevelTaskItem and TaskItem
entries they point to, so every task still queued at teardown leaks
together with the input and output frames it owns.

Tasks are still queued whenever the filter is freed before all results
were fetched. With async=1 (the default) frames are executed ahead of
the output, so e.g. -frames:v 1 on a longer input regularly leaves a few
finished tasks in task_queue, each holding two frames. With
batch_size > 1 and async=0, frames waiting for a full batch also leave
their LastLevelTaskItems in lltask_queue when the graph is freed without
EOF.

Pop and free every queued lltask, and free every queued task with its
frames, as dnn_free_model_ov() already does. This is done after
ff_dnn_wait_requests(), so no inference thread uses the tasks anymore.

Signed-off-by: Jake Fineman <[email protected]>
---
 libavfilter/dnn/dnn_backend_torch.cpp | 17 +++++++++++++++--
 1 file changed, 15 insertions(+), 2 deletions(-)

diff --git a/libavfilter/dnn/dnn_backend_torch.cpp 
b/libavfilter/dnn/dnn_backend_torch.cpp
index 59ab0a9c729..e4e1928094d 100644
--- a/libavfilter/dnn/dnn_backend_torch.cpp
+++ b/libavfilter/dnn/dnn_backend_torch.cpp
@@ -139,10 +139,23 @@ static void dnn_free_model_th(DNNModel **model)
         ff_safe_queue_destroy(th_model->request_queue);
     }
 
-    if (th_model->lltask_queue)
+    if (th_model->lltask_queue) {
+        while (ff_queue_size(th_model->lltask_queue) != 0) {
+            LastLevelTaskItem *item = (LastLevelTaskItem 
*)ff_queue_pop_front(th_model->lltask_queue);
+            av_freep(&item);
+        }
         ff_queue_destroy(th_model->lltask_queue);
-    if (th_model->task_queue)
+    }
+
+    if (th_model->task_queue) {
+        while (ff_queue_size(th_model->task_queue) != 0) {
+            TaskItem *item = (TaskItem 
*)ff_queue_pop_front(th_model->task_queue);
+            av_frame_free(&item->in_frame);
+            av_frame_free(&item->out_frame);
+            av_freep(&item);
+        }
         ff_queue_destroy(th_model->task_queue);
+    }
 
     if (th_model->jit_model)
         delete th_model->jit_model;
-- 
2.54.0


From 50cd9317050cd7ecf88fb3c35caaabaa48faae55 Mon Sep 17 00:00:00 2001
From: Jake Fineman <[email protected]>
Date: Tue, 27 Oct 2026 15:13:35 -0400
Subject: [PATCH 2/2] avfilter/dnn_backend_torch: set up the request in the
 CUDA input path

fill_model_input_th_cuda() reads request->lltasks[0] and writes through
infer_request->input_tensor, but nothing has stored an lltask in the
request or allocated the tensors by then. fill_model_input_th() does
both for software frames: it pops the lltask off lltask_queue into
request->lltasks, sets lltask_count and allocates input_tensor and
output. The CUDA variant added in 09bf8dab5b skips all of it.

The path is reached by the first AV_PIX_FMT_CUDA frame sent to
dnn_processing with the torch backend in a CONFIG_CUDA build:
execute_model_th() passes the request to fill_model_input_th_cuda(),
where request->lltasks[0] is either the uninitialized slot from
av_malloc_array() or NULL once a previous request has completed, and
input_tensor is NULL, so the frame dereferences an invalid pointer.
Past that, th_start_inference() would fail on the NULL output tensor,
an lltask_count of 0 would make infer_completion_callback() skip the
frame, and the lltask would stay queued.

Pop the lltask, return EINVAL if there is none, record it in the
request and allocate the input and output tensors as the software path
does. Batching is already rejected for CUDA frames by
ff_dnn_zero_copy_supported_cuda(), so one lltask per request is enough.

Signed-off-by: Jake Fineman <[email protected]>
---
 libavfilter/dnn/dnn_backend_torch.cpp | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/libavfilter/dnn/dnn_backend_torch.cpp 
b/libavfilter/dnn/dnn_backend_torch.cpp
index e4e1928094d..a73d0298c0d 100644
--- a/libavfilter/dnn/dnn_backend_torch.cpp
+++ b/libavfilter/dnn/dnn_backend_torch.cpp
@@ -199,10 +199,17 @@ static void cuda_tensor_deleter(void *arg)
 static int fill_model_input_th_cuda(THModel *th_model, THRequestItem *request)
 {
     THInferRequest *infer_request = request->infer_request;
-    LastLevelTaskItem *lltask = request->lltasks[0];
-    TaskItem *task = lltask->task;
-    AVFrame *frame = task->in_frame;
+    LastLevelTaskItem *lltask;
+    TaskItem *task;
+    AVFrame *frame;
 
+    lltask = (LastLevelTaskItem *)ff_queue_pop_front(th_model->lltask_queue);
+    if (!lltask)
+        return AVERROR(EINVAL);
+    request->lltasks[0] = lltask;
+    request->lltask_count = 1;
+    task = lltask->task;
+    frame = task->in_frame;
 
     int height = frame->height;
     int width  = frame->width;
@@ -221,6 +228,8 @@ static int fill_model_input_th_cuda(THModel *th_model, 
THRequestItem *request)
         cuda_tensor_deleter,
         torch::TensorOptions().dtype(torch::kUInt8).device(torch::kCUDA));
 
+    infer_request->input_tensor = new torch::Tensor();
+    infer_request->output = new torch::Tensor();
     /* Convert NHWC uint8 → NCHW float32 in [0, 1] and keep on GPU. */
     *infer_request->input_tensor =
         byte_tensor.to(torch::kFloat32).div(255.0f)
-- 
2.54.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to