Skip to content

POC: hipFile Python API using Cython bindings#248

Closed
riley-dixon wants to merge 28 commits intodevelopfrom
rildixon/python-bindings-poc
Closed

POC: hipFile Python API using Cython bindings#248
riley-dixon wants to merge 28 commits intodevelopfrom
rildixon/python-bindings-poc

Conversation

@riley-dixon
Copy link
Copy Markdown
Collaborator

@riley-dixon riley-dixon commented Apr 2, 2026

A proof of concept to see if we can create Python bindings for hipFile. This is still in its early stages and will likely change substantially between now and what could be called a "beta" release. The generated Cython bindings should be considered temporary as we work with another team for a long-term solution, but they at least give a base to build off of for the higher-level Python API.

Addresses #201

Test Plan

Can we load the C hipFile library from Python and run select functions?
Can we run IO?

Test Result

It works :)

AIHIPFILE-140

@riley-dixon riley-dixon self-assigned this Apr 2, 2026
# Internal helpers
# ---------------------------------------------------------------------------

cdef inline tuple _err(hipFileError_t e):
Copy link
Copy Markdown
Collaborator

@kurtmcmillan kurtmcmillan Apr 2, 2026

Choose a reason for hiding this comment

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

Should the python wrappers return an err or throw an exception? I think it is more idiomatic to throw exceptions in python.

... Or will a higher level wrapper do the throwing?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The python wrappers can raise an exception, however in our case I think we want to leave that for the higher-level Python API to raise.

HIP Python returns a tuple of varying size depending on the function but does raise an exception in certain instances. A non-exhaustive list of examples are invalid inputs from Python, or an error value being returned from an external library (e.g. malloc()).

By aligning this way, we can also allow third parties who just want to use the C-like interface without the exceptions.

@riley-dixon riley-dixon force-pushed the rildixon/python-bindings-poc branch 3 times, most recently from a7a7fce to ce61640 Compare April 8, 2026 17:49
Also move _hipfile driver functions out of public __init__.py
This exception will be thrown by the Python hipFile module
rather than returning an error struct.

The hipFile C error macros are unused since they aren't very
Pythonic in the first place. If the error is a hipFile error
(even if it is a wrapped HIP error), we raise HipFileException.
If it is a different error, a different Exception will be raised.

We also won't be exposing the error struct/tuple to Python users,
though it can be accessed via the HipFileException when raised.
This module will contain the functions related to querying
driver/library configuration/properties in a Python friendly
manner.
A fairly rudimentary interface for opening/closing files with hipFile.
A simple wrapper. Still need to figure out how exactly a
hipFile Buffer object should look like and how it is tied
to the underlying device memory.
We need some GPU memory allocated for registered a buffer
with hipFile.
This isn't perfect, but it provides a starting point to at least
running IO.
Not meant to be permanent. Just a quick and dirty script to verify
running IO works.
@riley-dixon riley-dixon force-pushed the rildixon/python-bindings-poc branch from ed96c41 to e4951f7 Compare April 8, 2026 19:55
Also adds limited typing hint for this method only.
This import is only run if TYPE_CHECKING is defined.

At runtime, if the ctypes package is not found, no error is raised
since __future__.annotations is imported which treats the hint as a
string literal and does not try to evaluate the name.
hipFileOpError and hipFileFileHandleType are exposed to the Python
layer to provide the user direct access to these enum values.
A proper fix will come next where the C enum values are namespace
qualified so that enum values exposed to Python do not need to have
their names modified.
This puts the C hipFile symbols being imported by Cython into a
"_c" namespace to avoid a namespace conflict when trying to set
a Python variable/function with the same name.
The C functions hipFileRead()/hipFileWrite() report error conditions
through a negative return value. If this return value is -1 or -5011,
it indicates additional error information needs to be fetched as it
cannot be included in a single return value.

However, this error information needs to be fetched prior to control
returning to Python. Otherwise there is no guarantee that these error
values are still valid for the error that occurred.
Cleans up what is imported in __init__.py to not pollute
the namespace when a user imports 'hipfile'.
Originally these functions were given different names when
the initial Cython code was generated. In part, this was to
avoid namespace conflicts. Now that the C functions are imported
into their own "_c" namespace, we can reuse the same function
names without conflict.

Reusing the names improves consistency of what Cython function
calls which C function.
@riley-dixon riley-dixon force-pushed the rildixon/python-bindings-poc branch from e4951f7 to a337f12 Compare April 8, 2026 20:19
@riley-dixon riley-dixon marked this pull request as ready for review April 8, 2026 20:26
Copilot AI review requested due to automatic review settings April 8, 2026 20:26
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a proof-of-concept Python package (hipfile) that exposes a low-level hipFile API via generated Cython bindings, plus a thin higher-level Python wrapper layer and build integration.

Changes:

  • Introduces a hipfile Python package with Cython-backed wrappers for driver lifecycle, file handles, buffer registration, and sync I/O.
  • Adds a scikit-build-core/CMake-based build to compile and install the _hipfile extension module linked against libhipfile.
  • Adds initial developer docs and a quick manual test script.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
python/README.md Build/install instructions for the POC bindings
python/pyproject.toml Python packaging metadata and scikit-build configuration
python/CMakeLists.txt CMake build for generating/compiling the Cython extension and linking libhipfile
python/main.py Manual IO smoke-test script for the bindings
python/hipfile/init.py Public package exports and version wiring
python/hipfile/_hipfile.pyx Cython wrappers for hipFile C API + enum re-exports
python/hipfile/_chipfile.pxd C declarations for hipFile/hip runtime used by Cython
python/hipfile/{buffer,driver,file,properties,enums,error}.py Thin Python API layer over the Cython module
python/hipfile/hipMalloc.py Temporary ctypes-based HIP allocation helpers for the POC
.gitignore Ignore venv and Python bytecode artifacts

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +179 to +185
cdef _c.hipFileHandle_t fh = NULL
cdef _c.hipFileDescr_t descr
memset(&descr, 0, sizeof(descr))
descr.type = <_c.hipFileFileHandleType_t>handle_type
descr.fd = fd
cdef _c.hipFileError_t e = _c.hipFileHandleRegister(&fh, &descr)
return (<uintptr_t>fh, _err(e))
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

hipFileHandleRegister() always writes descr.fd = fd and leaves descr.hFile/fs_ops unset. Per hipfile.h, hipFileHandleTypeOpaqueWin32 expects handle.hFile to be set and hipFileHandleTypeUserspaceFS expects fs_ops to be non-NULL; with the current wrapper those handle types can’t work correctly. Either (a) implement handle-type-specific population of hipFileDescr_t (including a way to pass a Win32 HANDLE / fs_ops from Python) or (b) reject unsupported handle types at the Python API boundary.

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +19
default_handle_type = None
if os.name == "posix":
default_handle_type = FileHandleType.OPAQUE_FD
elif os.name == "nt":
default_handle_type = FileHandleType.OPAQUE_WIN32

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

On Windows (os.name == "nt") the default handle_type is set to OPAQUE_WIN32, but open() uses os.open() which returns a POSIX-style file descriptor, not a Win32 HANDLE. Given the current Cython wrapper also populates hipFileDescr_t.handle.fd, this combination will fail on Windows. Consider defaulting to OPAQUE_FD only on POSIX and raising a clear NotImplementedError (or adding a separate Windows-specific open path using a real HANDLE).

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 9, 2026 16:58
@riley-dixon riley-dixon force-pushed the rildixon/python-bindings-poc branch 2 times, most recently from fa4abc1 to 035d62e Compare April 9, 2026 17:00
@riley-dixon riley-dixon force-pushed the rildixon/python-bindings-poc branch 3 times, most recently from 7babc5b to 198c522 Compare April 9, 2026 17:02
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

self._fd = os.open(self._path, self._flags, self._mode)
handle, err = hipFileHandleRegister(self._fd, self._handle_type)
if err[0] != 0:
os.close(self._fd)
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

In the error path, os.close(self._fd) is called but self._fd is left set to a now-closed descriptor. This can cause close()/__del__ to double-close and can also prevent changing handle_type later because _fd is non-None. Reset _fd to None (and consider calling self.close() in a try/finally) before raising.

Suggested change
os.close(self._fd)
try:
os.close(self._fd)
finally:
self._fd = None

Copilot uses AI. Check for mistakes.
self.handle_type = handle_type

def __del__(self):
self.close()
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

__del__ calls close() directly. Since close() can raise (e.g., if _fd is stale/invalid or at interpreter shutdown), exceptions from __del__ will be ignored/printed and can mask real issues. Make __del__ best-effort (catch and suppress/log) and rely on the context manager for deterministic cleanup errors.

Suggested change
self.close()
try:
self.close()
except Exception:
pass

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +58
if self._handle is not None:
raise RuntimeError("Cannot modify handle_type while FileHandle is open")
if _handle_type not in FileHandleType:
raise ValueError(f"'{_handle_type}' is not a member of enum FileHandleType")
if _handle_type == FileHandleType.OPAQUE_WIN32:
raise NotImplementedError(f"FileHandle does not currently support Win32 Handles")
self._handle_type = _handle_type

Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

if _handle_type not in FileHandleType can accept unrelated IntEnum values with the same underlying int (e.g., OpError.SUCCESS likely equals FileHandleType.OPAQUE_FD), leaving _handle_type set to the wrong enum type. Consider coercing/validating via FileHandleType(_handle_type) and storing the resulting FileHandleType instance.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +55
buffer_ptr = buffer.value # pylint: disable=C0103 # False Positive
print(f"Buffer located at: {buffer_ptr} | {hex(buffer_ptr)}")

with Driver() as hipfile_driver:
print(f"Driver Use Count After: {hipfile_driver.use_count()}")
with Buffer.from_ctypes_void_p(buffer, size, 0) as registered_buffer:
with FileHandle(
input_path,
os.O_RDWR | os.O_DIRECT | os.O_CREAT,
handle_type=FileHandleType.OPAQUE_FD,
) as fh_input:
with FileHandle(
output_path, os.O_RDWR | os.O_DIRECT | os.O_CREAT | os.O_TRUNC
) as fh_output:
print(f"Transferring {size} bytes...")
bytes_read = fh_input.read(registered_buffer, size, 0, 0)
print(f"Bytes Read: {bytes_read}")
bytes_written = fh_output.write(registered_buffer, size, 0, 0)
print(f"Bytes Written: {bytes_written}")

hipFree(buffer)

Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

The GPU allocation is freed only after the I/O block succeeds. If Driver(), Buffer.register(), FileHandle open/read/write, etc. raises, hipFree(buffer) is skipped and the device allocation leaks. Wrap the allocation in a try/finally (or a small context manager) so hipFree always runs.

Suggested change
buffer_ptr = buffer.value # pylint: disable=C0103 # False Positive
print(f"Buffer located at: {buffer_ptr} | {hex(buffer_ptr)}")
with Driver() as hipfile_driver:
print(f"Driver Use Count After: {hipfile_driver.use_count()}")
with Buffer.from_ctypes_void_p(buffer, size, 0) as registered_buffer:
with FileHandle(
input_path,
os.O_RDWR | os.O_DIRECT | os.O_CREAT,
handle_type=FileHandleType.OPAQUE_FD,
) as fh_input:
with FileHandle(
output_path, os.O_RDWR | os.O_DIRECT | os.O_CREAT | os.O_TRUNC
) as fh_output:
print(f"Transferring {size} bytes...")
bytes_read = fh_input.read(registered_buffer, size, 0, 0)
print(f"Bytes Read: {bytes_read}")
bytes_written = fh_output.write(registered_buffer, size, 0, 0)
print(f"Bytes Written: {bytes_written}")
hipFree(buffer)
try:
buffer_ptr = buffer.value # pylint: disable=C0103 # False Positive
print(f"Buffer located at: {buffer_ptr} | {hex(buffer_ptr)}")
with Driver() as hipfile_driver:
print(f"Driver Use Count After: {hipfile_driver.use_count()}")
with Buffer.from_ctypes_void_p(buffer, size, 0) as registered_buffer:
with FileHandle(
input_path,
os.O_RDWR | os.O_DIRECT | os.O_CREAT,
handle_type=FileHandleType.OPAQUE_FD,
) as fh_input:
with FileHandle(
output_path, os.O_RDWR | os.O_DIRECT | os.O_CREAT | os.O_TRUNC
) as fh_output:
print(f"Transferring {size} bytes...")
bytes_read = fh_input.read(registered_buffer, size, 0, 0)
print(f"Bytes Read: {bytes_read}")
bytes_written = fh_output.write(registered_buffer, size, 0, 0)
print(f"Bytes Written: {bytes_written}")
finally:
hipFree(buffer)

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +11
from libc.stdint cimport int64_t, uint64_t
from posix.types cimport off_t

Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

This .pxd cimports off_t from posix.types, which will fail to build on Windows even though other parts of the Python layer have win32 branches. If bindings are intended to be Linux-only for now, consider adding an explicit platform guard/error (or documenting Linux-only in packaging metadata); otherwise switch to a portable off_t definition/import for non-POSIX builds.

Copilot uses AI. Check for mistakes.
Removes supporting Win32 from FileHandle. FileHandle was designed
to manage opening & closing its own file reference. Python does
not provide a public API to open a file which returns a Win32 handle.
This leads to a contrived process of opening a file, getting a file
descriptor, and converting it to a Win32 handle. To be clear,
os.open() on Windows provides a POSIX FD.

FileHandle does not currently support a user providing an already
open file resource, which at this time seems to be the only
plausible use case of a Win32 handle being used.

For the sake of simplicity and getting out an initial release, we
will just error if the OPAQUE_WIN32 handle type is set.

(This however will leave Win32 support in the Cython layer as that
is a trivial switch that gets passed into the C library.)
@riley-dixon riley-dixon force-pushed the rildixon/python-bindings-poc branch from 198c522 to 0a38456 Compare April 9, 2026 17:11
@ammallya
Copy link
Copy Markdown
Collaborator

ammallya commented Apr 9, 2026

Imported to ROCm/rocm-systems pull 4865

@ammallya ammallya closed this Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants