Каркас Python-пакета `cuframes`: - python/pyproject.toml — scikit-build-core конфиг - python/CMakeLists.txt — pybind11 module через FetchContent - python/src/_native.cpp — module entry, error таксономия, enum mirrors (PixelFormat, SubscriberMode), version - python/cuframes/__init__.py — re-export публичного API - python/tests/test_smoke.py — smoke tests без real subscribe - python/README.md — статус + build instructions - CMakeLists.txt — подключение python/ при BUILD_PYTHON_BINDINGS=ON Реальный subscriber/frame wrapper в следующих коммитах (tasks #198-#202). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -39,3 +39,7 @@ endif()
|
||||
if(BUILD_TOOLS)
|
||||
add_subdirectory(tools/cuframes-rtsp-source)
|
||||
endif()
|
||||
|
||||
if(BUILD_PYTHON_BINDINGS)
|
||||
add_subdirectory(python)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.so
|
||||
.pytest_cache/
|
||||
@@ -0,0 +1,52 @@
|
||||
# Python bindings for cuframes — pybind11 module.
|
||||
#
|
||||
# Buildup: используется как subdirectory из root CMakeLists.txt при
|
||||
# BUILD_PYTHON_BINDINGS=ON, либо standalone через scikit-build-core
|
||||
# (см. pyproject.toml).
|
||||
#
|
||||
# Output: единый shared module `_native.so` который импортируется из
|
||||
# Python package `cuframes` (cuframes/__init__.py re-export'ит публичный API).
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
# pybind11 — header-only + helper functions. FetchContent чтобы не требовать
|
||||
# system install; pinned tag для воспроизводимых билдов.
|
||||
FetchContent_Declare(
|
||||
pybind11
|
||||
GIT_REPOSITORY https://github.com/pybind/pybind11.git
|
||||
GIT_TAG v2.13.6
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(pybind11)
|
||||
|
||||
pybind11_add_module(_native MODULE
|
||||
src/_native.cpp
|
||||
)
|
||||
|
||||
target_include_directories(_native PRIVATE
|
||||
${PROJECT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_link_libraries(_native PRIVATE
|
||||
cuframes # imported target из libcuframes/CMakeLists.txt
|
||||
)
|
||||
|
||||
# Версия модуля соответствует libcuframes (см. cuframes.h)
|
||||
target_compile_definitions(_native PRIVATE
|
||||
CUFRAMES_PY_BINDING_VERSION="${PROJECT_VERSION}"
|
||||
)
|
||||
|
||||
set_target_properties(_native PROPERTIES
|
||||
CXX_STANDARD 17
|
||||
CXX_STANDARD_REQUIRED ON
|
||||
CXX_VISIBILITY_PRESET hidden
|
||||
INTERPROCEDURAL_OPTIMIZATION TRUE
|
||||
)
|
||||
|
||||
# При scikit-build-core билде модуль попадает в wheel рядом с Python-исходниками
|
||||
# пакета. При standalone CMake — устанавливается в site-packages по умолчанию.
|
||||
if(SKBUILD)
|
||||
install(TARGETS _native DESTINATION cuframes)
|
||||
else()
|
||||
install(TARGETS _native LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/cuframes)
|
||||
endif()
|
||||
@@ -0,0 +1,53 @@
|
||||
# cuframes — Python bindings
|
||||
|
||||
Status: **WIP** (Phase 0 skeleton — issue [gx/cuframes#6](http://server:3000/gx/cuframes/issues/6))
|
||||
|
||||
Это пакет Python-обёрток над `libcuframes` (C ABI). Цель — позволить
|
||||
downstream ML/CV пайплайнам (yolo-world-detector, zone-motion, custom
|
||||
скриптам) подписываться на cuframes без CPU round-trip: получать NV12
|
||||
frames прямо как CUDA pointer / `torch.Tensor` (DLPack export, zero-copy).
|
||||
|
||||
## Текущий статус (что уже работает в этом skeleton)
|
||||
|
||||
- Module import: `import cuframes` загружает `_native.so`
|
||||
- Версия: `cuframes.version_string()`, `cuframes.protocol_version()`
|
||||
- Enums: `PixelFormat`, `SubscriberMode`
|
||||
- Иерархия исключений: `CuframesError` + 8 subclasses (publisher gone,
|
||||
frame timeout, device lost, и т. д.)
|
||||
|
||||
## Что в работе (см. tasks #198-#202)
|
||||
|
||||
- [ ] `CuframesSubscriber` + `CuframesFrame` lifecycle
|
||||
- [ ] DLPack export → `torch.from_dlpack`, `cupy.from_dlpack`
|
||||
- [ ] Context manager (`with cuframes.subscribe(key) as sub:`)
|
||||
- [ ] Per-subscriber CUDA stream
|
||||
- [ ] Health/stats properties (`ring_occupancy`, `drop_count`)
|
||||
- [ ] Thread-safety contract документация
|
||||
|
||||
## Build (dev)
|
||||
|
||||
Standalone wheel:
|
||||
|
||||
```bash
|
||||
cd python/
|
||||
pip install -e . --no-build-isolation
|
||||
```
|
||||
|
||||
Через корневой CMake-проект (вместе с libcuframes):
|
||||
|
||||
```bash
|
||||
cmake -B build -DBUILD_PYTHON_BINDINGS=ON
|
||||
cmake --build build -j
|
||||
```
|
||||
|
||||
## Зависимости
|
||||
|
||||
- `libcuframes` ≥ 0.4 (линкуется из соседнего CMake target)
|
||||
- CUDA Toolkit 12+
|
||||
- `pybind11` 2.13+ (берётся через FetchContent при CMake-сборке)
|
||||
- Python 3.10+
|
||||
- Опционально: `torch>=2.4` или `cupy-cuda12x>=13` для DLPack-потребителей
|
||||
|
||||
## Лицензия
|
||||
|
||||
LGPL-2.1+ (как у libcuframes).
|
||||
@@ -0,0 +1,56 @@
|
||||
"""cuframes — zero-copy CUDA frame sharing.
|
||||
|
||||
Python bindings to libcuframes. См. docs/python.md (т.б.д.) для
|
||||
архитектуры, threading контракта и примеров интеграции с PyTorch/CuPy.
|
||||
|
||||
Пример использования (skeleton — реальный subscriber API в работе, см.
|
||||
issue gx/cuframes#6):
|
||||
|
||||
import cuframes
|
||||
|
||||
print(cuframes.version_string()) # "0.4.0"
|
||||
print(cuframes.protocol_version()) # uint32
|
||||
|
||||
# TODO (task #198):
|
||||
# with cuframes.subscribe("cam-parking") as sub:
|
||||
# for frame in sub.frames(timeout_ms=1000):
|
||||
# tensor = torch.from_dlpack(frame)
|
||||
# ...
|
||||
"""
|
||||
|
||||
from ._native import (
|
||||
# Метаданные
|
||||
version_string,
|
||||
protocol_version,
|
||||
# Enums
|
||||
PixelFormat,
|
||||
SubscriberMode,
|
||||
# Error taxonomy
|
||||
CuframesError,
|
||||
CuframesPublisherGone,
|
||||
CuframesFrameTimeout,
|
||||
CuframesDeviceLost,
|
||||
CuframesShmError,
|
||||
CuframesProtocolMismatch,
|
||||
CuframesInvalidArgument,
|
||||
CuframesOutOfMemory,
|
||||
CuframesInternal,
|
||||
)
|
||||
|
||||
__version__ = version_string()
|
||||
|
||||
__all__ = [
|
||||
"version_string",
|
||||
"protocol_version",
|
||||
"PixelFormat",
|
||||
"SubscriberMode",
|
||||
"CuframesError",
|
||||
"CuframesPublisherGone",
|
||||
"CuframesFrameTimeout",
|
||||
"CuframesDeviceLost",
|
||||
"CuframesShmError",
|
||||
"CuframesProtocolMismatch",
|
||||
"CuframesInvalidArgument",
|
||||
"CuframesOutOfMemory",
|
||||
"CuframesInternal",
|
||||
]
|
||||
@@ -0,0 +1,47 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"scikit-build-core>=0.10",
|
||||
"pybind11>=2.13",
|
||||
]
|
||||
build-backend = "scikit_build_core.build"
|
||||
|
||||
[project]
|
||||
name = "cuframes"
|
||||
version = "0.4.0"
|
||||
description = "Python bindings for cuframes — zero-copy CUDA frame sharing"
|
||||
readme = "README.md"
|
||||
license = { text = "LGPL-2.1+" }
|
||||
requires-python = ">=3.10"
|
||||
authors = [{ name = "Evgeny Demchenko", email = "demchenkoev@gmail.com" }]
|
||||
keywords = ["cuda", "video", "ipc", "zero-copy"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Multimedia :: Video",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
torch = ["torch>=2.4"]
|
||||
cupy = ["cupy-cuda12x>=13"]
|
||||
dev = ["pytest>=8", "ruff>=0.6"]
|
||||
|
||||
[tool.scikit-build]
|
||||
cmake.version = ">=3.20"
|
||||
cmake.build-type = "Release"
|
||||
build-dir = "build/{wheel_tag}"
|
||||
wheel.packages = ["cuframes"]
|
||||
# Будем строить только Python модуль; libcuframes собирается отдельно
|
||||
# в основном CMake-проекте и линкуется как imported target.
|
||||
cmake.args = ["-DBUILD_PYTHON_BINDINGS=ON", "-DBUILD_EXAMPLES=OFF", "-DBUILD_TOOLS=OFF"]
|
||||
cmake.source-dir = ".."
|
||||
|
||||
[tool.scikit-build.cmake.define]
|
||||
BUILD_PYTHON_BINDINGS = "ON"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
@@ -0,0 +1,136 @@
|
||||
// cuframes Python bindings — pybind11 entry point.
|
||||
//
|
||||
// Этот файл — skeleton модуля. Полноценные обёртки subscriber/frame появятся
|
||||
// в следующих коммитах (см. issue gx/cuframes#6, tasks #198-#202).
|
||||
//
|
||||
// Контракт thread-safety: см. docs/python.md (т.б.д. в task #202).
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "cuframes/cuframes.h"
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace {
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Error taxonomy — Python exceptions, соответствующие cuframes_error_t.
|
||||
//
|
||||
// Принцип: каждая категория ошибок которая требует разной обработки в
|
||||
// downstream'е (reconnect vs retry vs fatal) → отдельный exception class.
|
||||
// Это решает требование из architect review: «detector должен уметь
|
||||
// reconnect-loop по publisher-gone, не падать».
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
struct CuframesExceptions {
|
||||
py::object base;
|
||||
py::object publisher_gone; // CUFRAMES_ERR_DISCONNECTED, _NOT_FOUND
|
||||
py::object frame_timeout; // CUFRAMES_ERR_TIMEOUT, _WOULD_BLOCK
|
||||
py::object device_lost; // CUFRAMES_ERR_CUDA
|
||||
py::object shm_error; // CUFRAMES_ERR_IO
|
||||
py::object protocol_mismatch; // CUFRAMES_ERR_PROTOCOL
|
||||
py::object invalid_argument; // CUFRAMES_ERR_INVALID_ARG
|
||||
py::object out_of_memory; // CUFRAMES_ERR_OUT_OF_MEMORY
|
||||
py::object internal; // CUFRAMES_ERR_INTERNAL, прочее
|
||||
};
|
||||
|
||||
CuframesExceptions g_exc;
|
||||
|
||||
// Маппинг cuframes_error_t → подходящий Python exception class.
|
||||
py::object exception_for(int err) {
|
||||
switch (err) {
|
||||
case CUFRAMES_ERR_NOT_FOUND:
|
||||
case CUFRAMES_ERR_DISCONNECTED:
|
||||
return g_exc.publisher_gone;
|
||||
case CUFRAMES_ERR_TIMEOUT:
|
||||
case CUFRAMES_ERR_WOULD_BLOCK:
|
||||
return g_exc.frame_timeout;
|
||||
case CUFRAMES_ERR_CUDA:
|
||||
return g_exc.device_lost;
|
||||
case CUFRAMES_ERR_IO:
|
||||
return g_exc.shm_error;
|
||||
case CUFRAMES_ERR_PROTOCOL:
|
||||
return g_exc.protocol_mismatch;
|
||||
case CUFRAMES_ERR_INVALID_ARG:
|
||||
return g_exc.invalid_argument;
|
||||
case CUFRAMES_ERR_OUT_OF_MEMORY:
|
||||
return g_exc.out_of_memory;
|
||||
default:
|
||||
return g_exc.internal;
|
||||
}
|
||||
}
|
||||
|
||||
// Бросает подходящий exception если err != CUFRAMES_OK.
|
||||
void check(int err, const char* operation = nullptr) {
|
||||
if (err == CUFRAMES_OK) return;
|
||||
const char* msg = cuframes_strerror(err);
|
||||
std::string what = operation
|
||||
? std::string(operation) + ": " + msg + " (code=" + std::to_string(err) + ")"
|
||||
: std::string(msg) + " (code=" + std::to_string(err) + ")";
|
||||
PyErr_SetString(exception_for(err).ptr(), what.c_str());
|
||||
throw py::error_already_set();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PYBIND11_MODULE(_native, m) {
|
||||
m.doc() = "cuframes — zero-copy CUDA frame sharing (native bindings)";
|
||||
|
||||
// ── Версия ──────────────────────────────────────────────────────────
|
||||
m.def("version_string", []() {
|
||||
return std::string(cuframes_version_string());
|
||||
}, "Runtime version of libcuframes (MAJOR.MINOR.PATCH).");
|
||||
|
||||
m.def("protocol_version", []() {
|
||||
return static_cast<uint32_t>(cuframes_protocol_version());
|
||||
}, "Wire-protocol version. Subscribers с разной версией не подключатся.");
|
||||
|
||||
m.attr("__binding_version__") = CUFRAMES_PY_BINDING_VERSION;
|
||||
|
||||
// ── Error taxonomy ──────────────────────────────────────────────────
|
||||
// Иерархия:
|
||||
// CuframesError (base)
|
||||
// ├── CuframesPublisherGone
|
||||
// ├── CuframesFrameTimeout
|
||||
// ├── CuframesDeviceLost
|
||||
// ├── CuframesShmError
|
||||
// ├── CuframesProtocolMismatch
|
||||
// ├── CuframesInvalidArgument
|
||||
// ├── CuframesOutOfMemory
|
||||
// └── CuframesInternal
|
||||
//
|
||||
// Downstream code может ловить либо конкретный subtype, либо CuframesError
|
||||
// как catch-all.
|
||||
|
||||
g_exc.base = py::exception<std::runtime_error>(m, "CuframesError").attr("__class__");
|
||||
auto make_subexc = [&m](const char* name) {
|
||||
return py::exception<std::runtime_error>(m, name, g_exc.base.ptr()).attr("__class__");
|
||||
};
|
||||
g_exc.publisher_gone = make_subexc("CuframesPublisherGone");
|
||||
g_exc.frame_timeout = make_subexc("CuframesFrameTimeout");
|
||||
g_exc.device_lost = make_subexc("CuframesDeviceLost");
|
||||
g_exc.shm_error = make_subexc("CuframesShmError");
|
||||
g_exc.protocol_mismatch = make_subexc("CuframesProtocolMismatch");
|
||||
g_exc.invalid_argument = make_subexc("CuframesInvalidArgument");
|
||||
g_exc.out_of_memory = make_subexc("CuframesOutOfMemory");
|
||||
g_exc.internal = make_subexc("CuframesInternal");
|
||||
|
||||
// ── Pixel formats (enum mirror) ─────────────────────────────────────
|
||||
py::enum_<cuframes_format_t>(m, "PixelFormat")
|
||||
.value("NV12", CUFRAMES_FORMAT_NV12)
|
||||
.value("YUV420P", CUFRAMES_FORMAT_YUV420P)
|
||||
.value("RGB", CUFRAMES_FORMAT_RGB)
|
||||
.value("BGR", CUFRAMES_FORMAT_BGR)
|
||||
.value("RGBA", CUFRAMES_FORMAT_RGBA)
|
||||
.value("GRAYSCALE", CUFRAMES_FORMAT_GRAYSCALE);
|
||||
|
||||
py::enum_<cuframes_subscriber_mode_t>(m, "SubscriberMode")
|
||||
.value("NEWEST_ONLY", CUFRAMES_MODE_NEWEST_ONLY)
|
||||
.value("STRICT_ORDER", CUFRAMES_MODE_STRICT_ORDER);
|
||||
|
||||
// TODO(task #198): CuframesSubscriber + CuframesFrame classes.
|
||||
// TODO(task #199): DLPack export.
|
||||
// TODO(task #200): Health/stats properties (потребует расширения C API).
|
||||
// TODO(task #201): Per-subscriber CUDA stream + thread-safety contract.
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Smoke tests для cuframes Python bindings.
|
||||
|
||||
В Phase 0 (skeleton) проверяем что:
|
||||
- модуль импортируется
|
||||
- версия читается
|
||||
- error классы существуют и являются нормальной иерархией
|
||||
|
||||
Subscriber / DLPack тесты появятся в следующих фазах
|
||||
(см. issue gx/cuframes#6, tasks #198+).
|
||||
"""
|
||||
|
||||
import cuframes
|
||||
|
||||
|
||||
def test_version_format():
|
||||
v = cuframes.version_string()
|
||||
assert isinstance(v, str)
|
||||
parts = v.split(".")
|
||||
assert len(parts) >= 3
|
||||
assert all(p.isdigit() for p in parts[:3])
|
||||
|
||||
|
||||
def test_protocol_version_is_uint():
|
||||
pv = cuframes.protocol_version()
|
||||
assert isinstance(pv, int)
|
||||
assert pv >= 0
|
||||
|
||||
|
||||
def test_pixel_format_enum_members():
|
||||
assert cuframes.PixelFormat.NV12.value == 0
|
||||
assert cuframes.PixelFormat.YUV420P.value == 1
|
||||
|
||||
|
||||
def test_subscriber_mode_enum_members():
|
||||
assert cuframes.SubscriberMode.NEWEST_ONLY.value == 0
|
||||
assert cuframes.SubscriberMode.STRICT_ORDER.value == 1
|
||||
|
||||
|
||||
def test_error_hierarchy():
|
||||
"""Все subtype'ы наследуются от CuframesError."""
|
||||
for sub in [
|
||||
cuframes.CuframesPublisherGone,
|
||||
cuframes.CuframesFrameTimeout,
|
||||
cuframes.CuframesDeviceLost,
|
||||
cuframes.CuframesShmError,
|
||||
cuframes.CuframesProtocolMismatch,
|
||||
cuframes.CuframesInvalidArgument,
|
||||
cuframes.CuframesOutOfMemory,
|
||||
cuframes.CuframesInternal,
|
||||
]:
|
||||
assert issubclass(sub, cuframes.CuframesError)
|
||||
Reference in New Issue
Block a user