Skip to content

Commit 1dc85b7

Browse files
dvrogozhNicolasHug
andauthored
Support out-of-tree plugins autoloading (#1151)
Co-authored-by: Nicolas Hug <contact@nicolas-hug.com> Signed-off-by: Dmitry Rogozhkin <dmitry.v.rogozhkin@intel.com>
1 parent 04a1e34 commit 1dc85b7

6 files changed

Lines changed: 96 additions & 6 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
build/
22
dist/
33
src/TorchCodec.egg-info/
4+
test/plugin/torchcodec_test_plugin.egg-info/
45
*/**/__pycache__
56
*/__pycache__
67
*/*.pyc

README.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,6 @@ Intel GPUs (XPU) support requires a stand-alone plugin for TorchCodec:
170170
pip install torchcodec-xpu --extra-index-url=https://download.pytorch.org/whl/xpu
171171
```
172172

173-
Upon installation, the plugin needs to be imported:
174-
175-
```python
176-
import torchcodec_xpu
177-
```
178-
179173
For any XPU-related support, please refer to
180174
https://github.com/intel/torchlib-xpu.
181175

src/torchcodec/__init__.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# This source code is licensed under the BSD-style license found in the
55
# LICENSE file in the root directory of this source tree.
66

7+
import os
78
from pathlib import Path
89

910
# Note: usort wants to put Frame and FrameBatch after decoders and samplers,
@@ -25,3 +26,25 @@
2526
# Similarly, these are exposed for downstream builds that use torchcodec as a
2627
# dependency.
2728
from ._core import core_library_path, ffmpeg_major_version # usort:skip
29+
30+
31+
# Leverage the Python plugin mechanism to load out-of-the-tree device extensions.
32+
# See https://github.com/pytorch/pytorch/pull/127074 that enabled the same
33+
# plugin support in torch.
34+
# This block should be kept at the end to ensure all the other functions in this
35+
# module that may be accessed by an autoloaded backend are defined.
36+
_TORCHCODEC_DEVICE_BACKEND_AUTOLOAD_VAR_NAME = "TORCHCODEC_DEVICE_BACKEND_AUTOLOAD"
37+
if os.getenv(_TORCHCODEC_DEVICE_BACKEND_AUTOLOAD_VAR_NAME, "1") == "1":
38+
from importlib.metadata import entry_points
39+
40+
_backend_extensions = entry_points(group="torchcodec.backends")
41+
42+
for _backend_extension in _backend_extensions:
43+
try:
44+
_entrypoint = _backend_extension.load()
45+
_entrypoint()
46+
except Exception as _err:
47+
raise RuntimeError(
48+
f"Failed to load the backend extension: {_backend_extension.name}. "
49+
f"You can disable extension auto-loading with {_TORCHCODEC_DEVICE_BACKEND_AUTOLOAD_VAR_NAME}=0."
50+
) from _err
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""
2+
This is a device backend extension used for testing.
3+
"""
4+
5+
import os
6+
7+
8+
def _autoload():
9+
os.environ["IS_DUMMY_PLUGIN_LOADED"] = "1"

test/plugin/pyproject.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[build-system]
2+
requires = ["setuptools>=61.0"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "torchcodec-test-plugin"
7+
description = "Test extension for torchcodec"
8+
requires-python = ">=3.8"
9+
dynamic = ["version"]
10+
11+
[project.entry-points.'torchcodec.backends']
12+
device_backend = 'dummy_plugin:_autoload'

test/plugin/test_plugins.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import os
8+
import subprocess
9+
import sys
10+
from pathlib import Path
11+
12+
import pytest
13+
14+
15+
@pytest.mark.parametrize("enable_autoload", [True, False])
16+
def test_plugin_autoload(tmp_path, enable_autoload):
17+
plugin_dir = Path(__file__).parent
18+
install_dir = str(tmp_path / "install")
19+
20+
# Install the dummy plugin into a temp directory. We use --target so that
21+
# the .dist-info metadata and the plugin package are installed flat into a
22+
# separate directory to avoid polluting the current env's site-packages.
23+
subprocess.run(
24+
[sys.executable, "-m", "pip", "install", "--target", install_dir, plugin_dir],
25+
check=True,
26+
)
27+
28+
env = {
29+
**os.environ,
30+
"PYTHONPATH": os.pathsep.join([install_dir, os.environ.get("PYTHONPATH", "")]),
31+
"TORCHCODEC_DEVICE_BACKEND_AUTOLOAD": str(int(enable_autoload)),
32+
}
33+
34+
result = subprocess.run(
35+
[
36+
sys.executable,
37+
"-c",
38+
"""
39+
import os
40+
import torchcodec
41+
42+
loaded = os.environ.get("IS_DUMMY_PLUGIN_LOADED") == "1"
43+
autoload = os.environ.get("TORCHCODEC_DEVICE_BACKEND_AUTOLOAD") == "1"
44+
assert loaded == autoload, f"{loaded=} {autoload=}"
45+
""",
46+
],
47+
env=env,
48+
capture_output=True,
49+
text=True,
50+
)
51+
assert result.returncode == 0, result.stderr

0 commit comments

Comments
 (0)