Skip to content

[pyapi, tests] Fix async inference queue in pythons API#36554

Open
praasz wants to merge 19 commits into
openvinotoolkit:masterfrom
praasz:bugfix/pytest-test-async-infer-request
Open

[pyapi, tests] Fix async inference queue in pythons API#36554
praasz wants to merge 19 commits into
openvinotoolkit:masterfrom
praasz:bugfix/pytest-test-async-infer-request

Conversation

@praasz

@praasz praasz commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Details:

  • Bug AsyncInferQueue.start_async(share_inputs=True use-after-free (introduced July 2023, PR [PyOV] Memory flow control with share_inputs and share_outputs #18275)

    • Root cause: When share inputs the data dispatch create ov::Tensor object that wrap the caller's numpy buffer via raw pointer, no python reference is retained inside the C++ tensor. The reference was being stored on a temporary wrapper, a Pyhon object with no other owner that is garbage-collected as soon as data dispatch returns.
    • Fix: Store _inputs_data on the AsyncInferQueue instance itself, keyed by idle-handle index, before super().start_async() is called. The entry is replaced only when the handle is next reused — which can only happen after the request's callback has fired and returned the handle to the idle queue — guaranteeing the numpy buffer outlives the inference.
  • Bug: Three unbounded blocking calls in the test helpers:

    • samples_common_test_class.py had no timeout, now set 10s for TCP connection and 30s idle between chunks
    • samples_common_test_class.py— download lock loop could spin forever on Windows.
    • common_utils.pyshell() had no execution timeout, now set default for 1200s (20 min)

Tickets:

AI Assistance:

  • AI assistance used: yes
  • Debug and reproduce

@praasz praasz added this to the 2026.3 milestone Jun 24, 2026
@praasz praasz requested review from akladiev, almilosz and wkobielx June 24, 2026 15:06
@praasz praasz requested review from a team as code owners June 24, 2026 15:06
@github-actions github-actions Bot added category: Python API OpenVINO Python bindings category: samples OpenVINO Runtime Samples labels Jun 24, 2026
@praasz praasz requested a review from Copilot June 24, 2026 15:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a Python API lifetime bug in AsyncInferQueue.start_async(..., share_inputs=True) that could lead to use-after-free of NumPy-backed input buffers during async inference, and it adds/updates tests plus safeguards against unbounded blocking in sample test utilities.

Changes:

  • Fix AsyncInferQueue.start_async() to retain Python references to shared input data for the lifetime of the async request.
  • Add regression tests for share_inputs=True use-after-free scenarios (including a platform-independent lifetime test).
  • Add timeouts / bounded waits to sample test helpers to prevent indefinite hangs.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
tests/samples_tests/smoke_tests/common/samples_common_test_class.py Adds request timeouts and adjusts download lock handling to avoid indefinite blocking.
tests/samples_tests/smoke_tests/common/common_utils.py Adds a timeout to the shell() helper to bound command execution time.
src/bindings/python/tests/test_runtime/test_async_infer_request.py Adds regression tests for async queue shared-input lifetime/data-integrity.
src/bindings/python/src/openvino/_ov_api.py Fixes shared-input lifetime by storing per-handle references on the queue instance.

Comment on lines 66 to 70
for _ in range(9999): # Give up after about 3 hours
lock_acquired = False
with contextlib.suppress(FileExistsError, PermissionError):
with lock_path.open('bx'):
if not file_path.exists():
Comment on lines +571 to +575
if share_inputs:
if not hasattr(self, "_inputs_data"):
self._inputs_data = {}
self._inputs_data[request_id] = getattr(request, "_inputs_data", None)
super().start_async(dispatched, userdata)
Comment thread tests/samples_tests/smoke_tests/common/common_utils.py Outdated
del img
gc.collect()
# Allocate a same-sized array with negative values.
# On CPython this lands at the exact same address the allocator just freed (100% reproducible).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this behavior documented?

# relu(0.0) = 0.0 ≠ EXPECTED — if inference reads this the callback will NOT signal the event.
poison = np.zeros(shape, dtype=np.float32) # noqa: F841 – must stay alive

event_was_set = correct_output_seen.wait(timeout=hang_timeout)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's assume for a moment this test runs without fix.
How setting hang_timeout on correct_output_seen prevents hang of the whole test suite? Is there a possibility that the whole test suite hangs before python interpreter gets to line 467?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it should hang to this line?
all calls are non blocking

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what this test checks.
It simulates a hang but does not prevent the test from hanging for other reasons

praasz added 3 commits June 24, 2026 18:15
The Copilot-suggested fix (pop _inputs_data[handle] when share_inputs=False)
introduces a use-after-free:

  update_tensor (non-shared path) copies new data INTO the existing ov::Tensor
  buffer via tensor.data[:] = inputs[:].  That buffer IS the old numpy array's
  memory; releasing the reference before or after the copy leaves the tensor
  with a dangling pointer, causing a segfault when inference reads the tensor.

The old array must remain alive until the NEXT share_inputs=True call creates
a fresh zero-copy tensor and overwrites _inputs_data[handle].

New tests verify the correct ownership semantics:
- _inputs_data not created when share_inputs always False
- False->True establishes ownership
- True->True releases old, holds new
- True->False->True: buffer kept alive during non-shared reuse, released only on
  next shared call that replaces the tensor
- Multi-handle: _inputs_data[A] and _inputs_data[B] are independent
- Many consecutive shared cycles: each overwrite releases the previous array

@olpipi olpipi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@praasz praasz requested a review from a team as a code owner June 26, 2026 07:45
@github-actions github-actions Bot added category: CI OpenVINO public CI github_actions Pull requests that update GitHub Actions code labels Jun 26, 2026
@praasz praasz requested a review from a team as a code owner July 1, 2026 12:51
@github-actions github-actions Bot added the category: AUTO OpenVINO AUTO device selection plugin label Jul 1, 2026
@praasz praasz removed this from the 2026.3 milestone Jul 9, 2026
praasz added 3 commits July 10, 2026 09:58
… deadlock

When AsyncInferQueue is destroyed, m_requests.clear() triggers a deep
C++ destructor chain (InferRequest -> AUTO plugin CompiledModel ->
AutoSchedule -> executor thread joins). If any of those threads attempt
to acquire the GIL (e.g., via py::gil_scoped_acquire in callbacks),
a deadlock occurs because the destructor runs with the GIL held.

Fix: release the GIL in ~AsyncInferQueue() before clearing requests.
The custom deleters in wrap_pyobject_to_sp and wrap_pyfunction will
safely re-acquire the GIL when needed to destroy Python objects.
The remaining member variables (m_user_ids, m_errors) are destroyed
after the dtor body exits and the GIL is re-acquired, so they remain safe.

Also add diagnostic logging in AutoSchedule::~AutoSchedule() to help
identify compile-context model reset ordering in the destructor chain.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

category: AUTO OpenVINO AUTO device selection plugin category: CI OpenVINO public CI category: Python API OpenVINO Python bindings category: samples OpenVINO Runtime Samples do not merge do_not_review github_actions Pull requests that update GitHub Actions code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants