diff --git a/.gitignore b/.gitignore index 5274fea0..27dd914e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,9 @@ build/ # Ignore .cache directory generated by clangd .cache/ + +# Python +__pycache__/ +*.pyc +.pytest_cache/ +*.egg-info/ diff --git a/python/README.md b/python/README.md new file mode 100644 index 00000000..f51c500b --- /dev/null +++ b/python/README.md @@ -0,0 +1,213 @@ +# hipfile – Python bindings for AMD hipFile + +Python `ctypes`-based bindings for [AMD hipFile](https://github.com/ROCm/hipFile), +the ROCm equivalent of NVIDIA's cuFile, enabling **GPU-direct storage** – data +movement directly between NVMe/filesystem storage and GPU memory, bypassing CPU +staging buffers. + +> **Status:** Early-stage community bindings, tracking +> [ROCm/hipFile#201](https://github.com/ROCm/hipFile/issues/201). + +--- + +## Requirements + +- Linux (x86_64 or aarch64) +- ROCm installed (tested with ROCm 6.x) +- hipFile library built and installed from [ROCm/hipFile](https://github.com/ROCm/hipFile) +- Python 3.8+ + +--- + +## Installation + +```bash +# From source +git clone https://github.com/ROCm/hipFile.git +cd hipFile/python +pip install -e . +``` + +Make sure `libhipfile.so` is on your `LD_LIBRARY_PATH`: + +```bash +export LD_LIBRARY_PATH=/opt/rocm/lib:$LD_LIBRARY_PATH +``` + +--- + +## Quick start + +```python +import os +import hipfile +import ctypes + +# --- Open the driver --- +with hipfile.CuFileDriver(): + + # Open a file with O_DIRECT for best performance + fd = os.open("data.bin", os.O_RDONLY | os.O_DIRECT) + try: + byte_size = 4 * 1024 * 1024 # 4 MB + + # Allocate GPU memory (example using PyTorch) + import torch + tensor = torch.empty(1024 * 1024, dtype=torch.float32, device="cuda") + gpu_ptr = tensor.data_ptr() + + # Register the GPU buffer, then do the I/O + from hipfile.bindings import hipFileBufRegister, hipFileBufDeregister + + hipFileBufRegister(ctypes.c_void_p(gpu_ptr), byte_size, 0) + try: + with hipfile.CuFile("data.bin", "r") as f: + n = f.read(ctypes.c_void_p(gpu_ptr), byte_size, file_offset=0) + print(f"Read {n} bytes directly into GPU memory") + finally: + hipFileBufDeregister(ctypes.c_void_p(gpu_ptr)) + + finally: + os.close(fd) +``` + +--- + +## API overview + +### Driver lifecycle + +```python +hipfile.hipFileDriverOpen() # initialise the hipFile driver +hipfile.hipFileDriverClose() # tear down + +props = hipfile.hipFileDriverGetProperties() # returns hipFileDriverProps_t +print(props.major_version, props.minor_version) + +hipfile.hipFileDriverSetMaxDirectIOSize(128) # KB +hipfile.hipFileDriverSetMaxCacheSize(512) # KB +hipfile.hipFileDriverSetMaxPinnedMemSize(256) # KB +``` + +### Context managers + +```python +from hipfile.bindings import hipFileBufRegister, hipFileBufDeregister + +with hipfile.CuFileDriver(): # open / close driver + # Register GPU buffer + hipFileBufRegister(ctypes.c_void_p(ptr), size, 0) + try: + with hipfile.CuFile("data.bin", "r+") as f: # open / close file + f.read(ptr, count=size, file_offset=0) + f.write(ptr, count=size, file_offset=0) + finally: + hipFileBufDeregister(ctypes.c_void_p(ptr)) +``` + +### Buffer registration + +```python +from hipfile.bindings import hipFileBufRegister, hipFileBufDeregister + +hipFileBufRegister(ctypes.c_void_p(gpu_ptr), size, 0) +hipFileBufDeregister(ctypes.c_void_p(gpu_ptr)) +``` + +### Error handling + +```python +try: + hipfile.hipFileDriverOpen() +except hipfile.HipFileError as e: + print(f"HipFile error occurred") +``` + +--- + +## Running the tests + +The test suite uses mocks and runs without real hardware: + +```bash +pip install pytest +pytest tests/ -v +``` + +For LMCache integration testing: + +```bash +python test_lmcache_integration.py +``` + +--- + +## PyTorch example + +```bash +python examples/pytorch_example.py --create --count 1048576 +``` + +--- + +## LMCache Integration + +hipFile Python bindings are designed to be a drop-in replacement for NVIDIA's cuFile in applications like LMCache: + +```python +# Works with both cuFile and hipFile +try: + import cufile as gds_lib +except ImportError: + import hipfile as gds_lib + +# Same API for both +with gds_lib.CuFileDriver(): + from gds_lib.bindings import hipFileBufRegister, hipFileBufDeregister + hipFileBufRegister(ctypes.c_void_p(tensor.data_ptr()), tensor.nbytes, 0) + # ... perform GDS operations ... +``` + +--- + +## Building & Publishing + +```bash +# Build sdist and wheel +uv build + +# Validate the built artifacts +uvx twine check dist/* + +# Publish to PyPI +uv publish +``` + +--- + +## How it works + +hipFile provides a C API for GPU-direct I/O on AMD ROCm hardware. These Python +bindings use `ctypes` to call `libhipfile.so` directly, with no C compilation +needed. The binding layer: + +1. Loads `libhipfile.so` at import time (lazy, configurable via `HIPFILE_LIB_PATH`). +2. Declares `argtypes` / `restype` for each API function. +3. Wraps the C types in Pythonic classes with context-manager support. +4. Translates error status codes to `HipFileError` exceptions. +5. Provides cuFile-compatible API for easy migration. + +--- + +## Known Limitations + +- `RegisteredBuffer` context manager is not yet implemented +- Some advanced cuFile features may not yet have hipFile equivalents +- Error reporting is less detailed than NVIDIA's cuFile + +--- + +## Contributing + +PRs welcome! The main tracking issue for official bindings is +[ROCm/hipFile#201](https://github.com/ROCm/hipFile/issues/201). diff --git a/python/examples/pytorch_example.py b/python/examples/pytorch_example.py new file mode 100644 index 00000000..dc7a7c4d --- /dev/null +++ b/python/examples/pytorch_example.py @@ -0,0 +1,91 @@ +""" +examples/pytorch_example.py +---------------------------- +Demonstrates loading a tensor from disk directly into GPU memory using +hipFile + PyTorch on an AMD ROCm system. + +Requirements: + - AMD GPU with ROCm installed + - hipFile library (see https://github.com/ROCm/hipFile) + - PyTorch with ROCm support (pip install torch --index-url https://download.pytorch.org/whl/rocm6.1) + - This package: pip install hipfile (or python -m pip install -e .) + +Usage:: + + python pytorch_example.py --file /path/to/float32_tensor.bin --count 1048576 +""" + +import argparse +import ctypes +import os +import struct +import time + +import hipfile + + +def create_test_file(path: str, n_floats: int = 1024 * 1024) -> None: + """Write n_floats random float32 values to a file for testing.""" + import random + data = struct.pack(f"{n_floats}f", *[random.random() for _ in range(n_floats)]) + with open(path, "wb") as f: + f.write(data) + print(f"Created test file: {path} ({len(data)} bytes)") + + +def load_tensor_gpu_direct(filepath: str, n_floats: int): + """Load float32 data from file directly into a GPU tensor via hipFile.""" + try: + import torch + except ImportError: + print("PyTorch not available – skipping GPU demo.") + return + + if not torch.cuda.is_available(): + print("No GPU available (torch.cuda.is_available() = False).") + return + + dtype = torch.float32 + byte_size = n_floats * dtype.itemsize + + # 1. Allocate device tensor + tensor = torch.empty(n_floats, dtype=dtype, device="cuda") + dev_ptr = tensor.data_ptr() # raw integer GPU pointer + + # 2. Initialise hipFile driver and register the GPU buffer + from hipfile.bindings import hipFileBufRegister, hipFileBufDeregister + + hipfile.CuFileDriver() + hipFileBufRegister(ctypes.c_void_p(dev_ptr), byte_size, 0) + try: + with hipfile.CuFile(filepath, "r", use_direct_io=True) as hf: + t0 = time.perf_counter() + n_read = hf.read(dev_ptr, byte_size, file_offset=0) + elapsed = time.perf_counter() - t0 + finally: + hipFileBufDeregister(ctypes.c_void_p(dev_ptr)) + + bw_gb = (n_read / elapsed) / 1e9 + print(f"GPU-direct read: {n_read} bytes in {elapsed*1000:.2f} ms " + f"({bw_gb:.2f} GB/s)") + print(f"Tensor first 5 values: {tensor[:5].tolist()}") + + +def main(): + parser = argparse.ArgumentParser(description="hipFile + PyTorch example") + parser.add_argument("--file", default="/tmp/test_hipfile.bin", + help="Path to float32 binary file") + parser.add_argument("--count", type=int, default=1024 * 1024, + help="Number of float32 values") + parser.add_argument("--create", action="store_true", + help="Create a test file before reading") + args = parser.parse_args() + + if args.create or not os.path.exists(args.file): + create_test_file(args.file, args.count) + + load_tensor_gpu_direct(args.file, args.count) + + +if __name__ == "__main__": + main() diff --git a/python/hipfile/__init__.py b/python/hipfile/__init__.py new file mode 100644 index 00000000..a7e35ecf --- /dev/null +++ b/python/hipfile/__init__.py @@ -0,0 +1,58 @@ +""" +hipfile – Python bindings for AMD hipFile (GPU-direct storage). + +Drop-in replacement for the cufile Python package. +Mirrors cufile/__init__.py structure with hip* naming. + +Quick start:: + + import hipfile + + with hipfile.CuFile("data.bin", "r+") as f: + f.write(ctypes.c_void_p(gpu_ptr), size, file_offset=0) +""" + +# High-level (mirrors cufile.cufile exports) +from .hipfile import ( + CuFile, + CuFileDriver, +) + +# Low-level convenience functions (mirrors cufile.bindings exports) +from .bindings import ( + HipFileError, + hipFileDriverOpen, + hipFileDriverClose, + hipFileHandleRegister, + hipFileHandleDeregister, + hipFileBufRegister, + hipFileBufDeregister, + hipFileRead, + hipFileWrite, + hipFileHandle_t, + hipFileStatus, + hipFileDescr, + DescrUnion, +) + +__version__ = "0.1.0" + +__all__ = [ + # High-level + "CuFile", + "CuFileDriver", + "HipFileError", + # Low-level + "hipFileDriverOpen", + "hipFileDriverClose", + "hipFileHandleRegister", + "hipFileHandleDeregister", + "hipFileBufRegister", + "hipFileBufDeregister", + "hipFileRead", + "hipFileWrite", + "hipFileHandle_t", + "hipFileStatus", + "hipFileDescr", + "DescrUnion", +] diff --git a/python/hipfile/bindings.py b/python/hipfile/bindings.py new file mode 100644 index 00000000..256371b8 --- /dev/null +++ b/python/hipfile/bindings.py @@ -0,0 +1,140 @@ +""" +Low-level ctypes bindings for libhipfile.so + +Structured as a direct mirror of cufile/bindings.py so that the two modules +are interchangeable. Variable and function names are prefixed hip/Hip instead +of cu/Cu, but signatures, field layouts, and calling conventions are identical. + +Reference: /usr/local/lib/python3.12/dist-packages/cufile/bindings.py +""" + +import ctypes + +libhipfile = ctypes.CDLL("libhipfile.so") + + +# --------------------------------------------------------------------------- +# Structs (mirror cufile exactly, renamed hip*) +# --------------------------------------------------------------------------- + +class hipFileStatus(ctypes.Structure): + """Maps to CUfileError / hipFileStatus_t.""" + _fields_ = [ + ("err", ctypes.c_int), # hipFile error code + ("cu_err", ctypes.c_int), # underlying HIP runtime error + ] + + +hipFileHandle_t = ctypes.c_void_p # opaque handle (mirrors CUfileHandle_t) + + +class DescrUnion(ctypes.Union): + """Union holding either a file descriptor or an opaque handle.""" + _fields_ = [ + ("fd", ctypes.c_int), + ("handle", ctypes.c_void_p), + ] + + +class hipFileDescr(ctypes.Structure): + """File descriptor passed to hipFileHandleRegister (mirrors CUfileDescr).""" + _fields_ = [ + ("type", ctypes.c_int), # handle type: 1 = opaque fd + ("handle", DescrUnion), + ("fs_ops", ctypes.c_void_p), # reserved / filesystem ops ptr + ] + + +# --------------------------------------------------------------------------- +# Function signatures +# --------------------------------------------------------------------------- + +libhipfile.hipFileDriverOpen.restype = hipFileStatus +libhipfile.hipFileDriverClose.restype = hipFileStatus +libhipfile.hipFileHandleRegister.restype = hipFileStatus +libhipfile.hipFileBufRegister.restype = hipFileStatus +libhipfile.hipFileBufDeregister.restype = hipFileStatus +libhipfile.hipFileRead.restype = ctypes.c_ssize_t +libhipfile.hipFileWrite.restype = ctypes.c_ssize_t +libhipfile.hipFileHandleRegister.argtypes = [ctypes.POINTER(hipFileHandle_t), ctypes.POINTER(hipFileDescr)] +libhipfile.hipFileHandleDeregister.argtypes = [hipFileHandle_t] +libhipfile.hipFileBufRegister.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int] +libhipfile.hipFileBufDeregister.argtypes = [ctypes.c_void_p] +libhipfile.hipFileRead.argtypes = [hipFileHandle_t, ctypes.c_void_p, ctypes.c_size_t, + ctypes.c_longlong, ctypes.c_longlong] +libhipfile.hipFileWrite.argtypes = [hipFileHandle_t, ctypes.c_void_p, ctypes.c_size_t, + ctypes.c_longlong, ctypes.c_longlong] + +# convenience + + +class HipFileError(Exception): + """Exception raised for hipFile errors.""" + pass + + +def _ck(status: hipFileStatus, name: str) -> None: + if status.err != 0: + raise HipFileError( + f"{name} failed (hipFile err={status.err}, hip_err={status.cu_err})" + ) + + +def hipFileDriverOpen() -> None: + _ck(libhipfile.hipFileDriverOpen(), "hipFileDriverOpen") + + +def hipFileDriverClose() -> None: + _ck(libhipfile.hipFileDriverClose(), "hipFileDriverClose") + + +def hipFileHandleRegister(fd: int) -> hipFileHandle_t: + descr = hipFileDescr() + descr.type = 1 # opaque fd (matches cufile type=1) + descr.handle = DescrUnion(fd=fd) + handle = hipFileHandle_t() + _ck( + libhipfile.hipFileHandleRegister( + ctypes.byref(handle), ctypes.byref(descr) + ), + "hipFileHandleRegister", + ) + return handle + + +def hipFileHandleDeregister(handle: hipFileHandle_t) -> None: + libhipfile.hipFileHandleDeregister(handle) + + +def hipFileBufRegister(buf: ctypes.c_void_p, size: int, flags: int) -> None: + _ck(libhipfile.hipFileBufRegister(buf, size, flags), "hipFileBufRegister") + + +def hipFileBufDeregister(buf: ctypes.c_void_p) -> None: + _ck(libhipfile.hipFileBufDeregister(buf), "hipFileBufDeregister") + + +def hipFileRead( + handle: hipFileHandle_t, + buf: ctypes.c_void_p, + size: int, + file_offset: int, + dev_offset: int, +) -> int: + ret = libhipfile.hipFileRead(handle, buf, size, file_offset, dev_offset) + if ret < 0: + raise HipFileError(f"hipFileRead failed (error code {ret})") + return ret + + +def hipFileWrite( + handle: hipFileHandle_t, + buf: ctypes.c_void_p, + size: int, + file_offset: int, + dev_offset: int, +) -> int: + ret = libhipfile.hipFileWrite(handle, buf, size, file_offset, dev_offset) + if ret < 0: + raise HipFileError(f"hipFileWrite failed (error code {ret})") + return ret diff --git a/python/hipfile/hipfile.py b/python/hipfile/hipfile.py new file mode 100644 index 00000000..97a21715 --- /dev/null +++ b/python/hipfile/hipfile.py @@ -0,0 +1,156 @@ +""" +hipfile.py – High-level interface to AMD hipFile (GPU-direct storage). + +Mirrors cufile/cufile.py exactly, with hip* naming. +The CuFile class is a drop-in replacement for cufile.CuFile. +""" + +import ctypes +import os + +from .bindings import ( + hipFileDriverOpen, + hipFileDriverClose, + hipFileHandleRegister, + hipFileHandleDeregister, + hipFileRead, + hipFileWrite, +) + + +# --------------------------------------------------------------------------- +# CuFileDriver singleton (mirrors cufile.CuFileDriver exactly) +# --------------------------------------------------------------------------- + +def _singleton(cls): + _instances = {} + def wrapper(*args, **kwargs): + if cls not in _instances: + _instances[cls] = cls(*args, **kwargs) + return _instances[cls] + return wrapper + +@_singleton +class CuFileDriver: + def __init__(self): + hipFileDriverOpen() + + def __del__(self): + hipFileDriverClose() + + +# --------------------------------------------------------------------------- +# Mode helper (mirrors cufile._os_mode exactly) +# --------------------------------------------------------------------------- + +def _os_mode(mode: str) -> int: + modes = { + "r": os.O_RDONLY, + "r+": os.O_RDWR, + "w": os.O_CREAT | os.O_WRONLY | os.O_TRUNC, + "w+": os.O_CREAT | os.O_RDWR | os.O_TRUNC, + "a": os.O_CREAT | os.O_WRONLY | os.O_APPEND, + "a+": os.O_CREAT | os.O_RDWR | os.O_APPEND, + } + if mode not in modes: + raise ValueError( + f"Unsupported mode {mode!r}. Supported: {list(modes)}" + ) + return modes[mode] + + +# --------------------------------------------------------------------------- +# CuFile (mirrors cufile.CuFile exactly) +# --------------------------------------------------------------------------- + +class CuFile: + """ + Main class for hipFile file operations. + Drop-in replacement for cufile.CuFile. + """ + + def __init__(self, path: str, mode: str = "r", use_direct_io: bool = False): + self._driver = CuFileDriver() + self._path = path + self._mode = mode + self._os_mode = _os_mode(mode) + if use_direct_io: + try: + self._os_mode |= os.O_DIRECT + except AttributeError: + pass + self._handle = None + self._hip_file_handle = None + + def __del__(self): + try: + if getattr(self, "_handle", None) is not None: + self.close() + except Exception: + pass + + @property + def is_open(self) -> bool: + return self._handle is not None + + def open(self): + """Opens the file and registers the handle.""" + if self.is_open: + return + self._handle = os.open(self._path, self._os_mode, 0o644) + self._hip_file_handle = hipFileHandleRegister(self._handle) + + def close(self): + """Deregisters the handle and closes the file.""" + if not self.is_open: + return + hipFileHandleDeregister(self._hip_file_handle) + os.close(self._handle) + self._handle = None + self._hip_file_handle = None + + def __enter__(self): + self.open() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def read( + self, + dest: ctypes.c_void_p, + size: int, + file_offset: int = 0, + dev_offset: int = 0, + ) -> int: + """Read from the file.""" + if not self.is_open: + raise IOError("File is not open.") + return hipFileRead( + self._hip_file_handle, dest, size, file_offset, dev_offset + ) + + def write( + self, + src: ctypes.c_void_p, + size: int, + file_offset: int = 0, + dev_offset: int = 0, + ) -> int: + """Write to the file.""" + if not self.is_open: + raise IOError("File is not open.") + return hipFileWrite( + self._hip_file_handle, src, size, file_offset, dev_offset + ) + + def get_handle(self): + """Get the raw file descriptor.""" + return self._handle + + def __repr__(self) -> str: + return ( + f"" + ) + diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 00000000..12aad587 --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,55 @@ +[build-system] +requires = ["setuptools>=61", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "hipfile" +version = "0.1.0" +description = "Python bindings for AMD hipFile – GPU-direct storage on ROCm" +readme = "README.md" +license = "MIT" +requires-python = ">=3.8" +keywords = ["amd", "rocm", "hip", "gpu", "storage", "io", "gpudirect"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering", + "Topic :: System :: Hardware", +] + +# Runtime: pure ctypes, no mandatory third-party deps +dependencies = [] + +[project.optional-dependencies] +# Install AMD ROCm Python packages if available +rocm = [ + "amdsmi", +] +# PyTorch integration +torch = [ + "torch", +] +dev = [ + "pytest", + "pytest-cov", +] + +[project.urls] +Homepage = "https://github.com/ROCm/hipFile" +"Bug Reports" = "https://github.com/ROCm/hipFile/issues" +"Bindings Issue" = "https://github.com/ROCm/hipFile/issues/201" + +[tool.setuptools.packages.find] +where = ["."] +include = ["hipfile*"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/python/tests/conftest.py b/python/tests/conftest.py new file mode 100644 index 00000000..02341aaa --- /dev/null +++ b/python/tests/conftest.py @@ -0,0 +1,28 @@ +""" +conftest.py – Patch ctypes.CDLL so hipfile can be imported without libhipfile.so. + +This mirrors the situation in cufile-python where libcufile.so is loaded at +module level. On machines without the real library (CI, dev laptops) we +intercept the CDLL call and return a MagicMock. +""" + +import ctypes +import sys +from unittest.mock import MagicMock + +_real_CDLL = ctypes.CDLL + + +def _mock_CDLL(name, *args, **kwargs): + if name == "libhipfile.so": + return MagicMock() + return _real_CDLL(name, *args, **kwargs) + + +# Patch before any test (or fixture) imports hipfile +ctypes.CDLL = _mock_CDLL + +# Remove hipfile modules so they reimport with the patched CDLL +for mod in list(sys.modules): + if mod.startswith("hipfile"): + del sys.modules[mod] diff --git a/python/tests/test_hipfile.py b/python/tests/test_hipfile.py new file mode 100644 index 00000000..0644f7b2 --- /dev/null +++ b/python/tests/test_hipfile.py @@ -0,0 +1,271 @@ +""" +Tests for hipfile Python bindings. +Run without real AMD hardware using mocks. +""" + +import ctypes +import os +import tempfile +import unittest +from unittest.mock import MagicMock, patch, call + + +# --------------------------------------------------------------------------- +# Mock library factory +# --------------------------------------------------------------------------- + +def _make_mock_lib(): + lib = MagicMock() + from hipfile.bindings import hipFileStatus + + ok = hipFileStatus(); ok.err = 0; ok.cu_err = 0 + + lib.hipFileDriverOpen.return_value = ok + lib.hipFileDriverClose.return_value = ok + lib.hipFileHandleRegister.return_value = ok + lib.hipFileHandleDeregister.return_value = None + lib.hipFileBufRegister.return_value = ok + lib.hipFileBufDeregister.return_value = ok + lib.hipFileRead.return_value = 1024 + lib.hipFileWrite.return_value = 1024 + return lib + + +def _patch_lib(mock_lib): + """Patch bindings.libhipfile so all convenience functions use the mock.""" + return patch("hipfile.bindings.libhipfile", mock_lib) + + +# --------------------------------------------------------------------------- +# Low-level bindings tests +# --------------------------------------------------------------------------- + +class TestBindings(unittest.TestCase): + + def setUp(self): + self.lib = _make_mock_lib() + self.p = _patch_lib(self.lib) + self.p.start() + + def tearDown(self): + self.p.stop() + + def test_driver_open(self): + import hipfile + hipfile.hipFileDriverOpen() + self.lib.hipFileDriverOpen.assert_called_once() + + def test_driver_close(self): + import hipfile + hipfile.hipFileDriverClose() + self.lib.hipFileDriverClose.assert_called_once() + + def test_driver_open_error_raises(self): + from hipfile.bindings import hipFileStatus, HipFileError, _ck + bad = hipFileStatus(); bad.err = 7; bad.cu_err = 0 + with self.assertRaises(HipFileError) as cm: + _ck(bad, "hipFileDriverOpen") + self.assertIn("hipFileDriverOpen", str(cm.exception)) + self.assertIn("7", str(cm.exception)) + + def test_handle_register(self): + import hipfile + handle = hipfile.hipFileHandleRegister(3) + self.lib.hipFileHandleRegister.assert_called_once() + + def test_handle_deregister(self): + import hipfile + hipfile.hipFileHandleDeregister(ctypes.c_void_p(0)) + self.lib.hipFileHandleDeregister.assert_called_once() + + def test_buf_register(self): + import hipfile + hipfile.hipFileBufRegister(ctypes.c_void_p(0xDEAD), 4096, 0) + self.lib.hipFileBufRegister.assert_called_once() + + def test_buf_deregister(self): + import hipfile + hipfile.hipFileBufDeregister(ctypes.c_void_p(0xDEAD)) + self.lib.hipFileBufDeregister.assert_called_once() + + def test_read(self): + import hipfile + n = hipfile.hipFileRead(ctypes.c_void_p(0), ctypes.c_void_p(0x1), 1024, 0, 0) + self.assertEqual(n, 1024) + + def test_write(self): + import hipfile + n = hipfile.hipFileWrite(ctypes.c_void_p(0), ctypes.c_void_p(0x1), 1024, 0, 0) + self.assertEqual(n, 1024) + + +# --------------------------------------------------------------------------- +# CuFileDriver singleton +# --------------------------------------------------------------------------- + +def _reset_singleton(): + """Clear the _singleton closure's _instances dict for CuFileDriver.""" + from hipfile import hipfile as _mod + # _singleton wraps CuFileDriver; the _instances dict is in the closure + _mod.CuFileDriver.__closure__[0].cell_contents.clear() + + +class TestCuFileDriver(unittest.TestCase): + + def setUp(self): + self.lib = _make_mock_lib() + self.p = _patch_lib(self.lib) + self.p.start() + _reset_singleton() + + def tearDown(self): + _reset_singleton() + self.p.stop() + + def test_driver_opens_on_init(self): + import hipfile + hipfile.CuFileDriver() + self.lib.hipFileDriverOpen.assert_called_once() + + def test_singleton_returns_same_instance(self): + import hipfile + a = hipfile.CuFileDriver() + b = hipfile.CuFileDriver() + self.assertIs(a, b) # same object — that's the singleton contract + + +# --------------------------------------------------------------------------- +# CuFile +# --------------------------------------------------------------------------- + +class TestCuFile(unittest.TestCase): + + def setUp(self): + self.lib = _make_mock_lib() + self.p = _patch_lib(self.lib) + self.p.start() + # Suppress CuFileDriver singleton state between tests + self.p2 = patch("hipfile.hipfile.CuFileDriver", return_value=MagicMock()) + self.p2.start() + + self.tmp = tempfile.NamedTemporaryFile(delete=False) + self.tmp.write(b"\x00" * 4096) + self.tmp.close() + + def tearDown(self): + self.p.stop() + self.p2.stop() + os.unlink(self.tmp.name) + + def test_lazy_open(self): + """__init__ must NOT open the file.""" + import hipfile + f = hipfile.CuFile(self.tmp.name, "r") + self.assertFalse(f.is_open) + + def test_open_and_close(self): + import hipfile + f = hipfile.CuFile(self.tmp.name, "r") + f.open() + self.assertTrue(f.is_open) + self.lib.hipFileHandleRegister.assert_called_once() + f.close() + self.assertFalse(f.is_open) + self.lib.hipFileHandleDeregister.assert_called_once() + + def test_context_manager(self): + import hipfile + with hipfile.CuFile(self.tmp.name, "r+") as f: + self.assertTrue(f.is_open) + self.assertFalse(f.is_open) + + def test_double_open_is_noop(self): + import hipfile + with hipfile.CuFile(self.tmp.name, "r") as f: + f.open() + self.lib.hipFileHandleRegister.assert_called_once() + + def test_double_close_is_noop(self): + import hipfile + with hipfile.CuFile(self.tmp.name, "r") as f: + pass + f.close() + self.lib.hipFileHandleDeregister.assert_called_once() + + def test_read_requires_open(self): + import hipfile + f = hipfile.CuFile(self.tmp.name, "r") + with self.assertRaises(IOError): + f.read(ctypes.c_void_p(0xABCD), 1024) + + def test_write_requires_open(self): + import hipfile + f = hipfile.CuFile(self.tmp.name, "r+") + with self.assertRaises(IOError): + f.write(ctypes.c_void_p(0xABCD), 1024) + + def test_read_returns_int(self): + import hipfile + with hipfile.CuFile(self.tmp.name, "r") as f: + n = f.read(ctypes.c_void_p(0xABCD), 1024, file_offset=0) + self.assertEqual(n, 1024) + + def test_write_returns_int(self): + import hipfile + with hipfile.CuFile(self.tmp.name, "r+") as f: + n = f.write(ctypes.c_void_p(0xABCD), 1024, file_offset=0) + self.assertEqual(n, 1024) + + def test_dev_offset_passed_through(self): + import hipfile + with hipfile.CuFile(self.tmp.name, "r") as f: + f.read(ctypes.c_void_p(0x1), 512, file_offset=128, dev_offset=64) + # third positional arg to hipFileRead is size=512, then file_offset=128, dev_offset=64 + args = self.lib.hipFileRead.call_args[0] + self.assertEqual(args[2], 512) # size + self.assertEqual(args[3], 128) # file_offset + self.assertEqual(args[4], 64) # dev_offset + + def test_get_handle_when_open(self): + import hipfile + with hipfile.CuFile(self.tmp.name, "r") as f: + self.assertIsInstance(f.get_handle(), int) + + def test_get_handle_when_closed(self): + import hipfile + f = hipfile.CuFile(self.tmp.name, "r") + self.assertIsNone(f.get_handle()) + + def test_use_direct_io(self): + import hipfile + f = hipfile.CuFile(self.tmp.name, "r", use_direct_io=True) + f.open() + f.close() + + def test_bad_mode_raises(self): + import hipfile + with self.assertRaises(ValueError): + hipfile.CuFile(self.tmp.name, "z") + + def test_repr(self): + import hipfile + f = hipfile.CuFile(self.tmp.name, "r") + r = repr(f) + self.assertIn("CuFile", r) + self.assertIn("open=False", r) + + +# --------------------------------------------------------------------------- +# Public API surface +# --------------------------------------------------------------------------- + +class TestPublicAPI(unittest.TestCase): + + def test_all_exports_present(self): + import hipfile + for name in hipfile.__all__: + self.assertTrue(hasattr(hipfile, name), f"Missing export: {name}") + + +if __name__ == "__main__": + unittest.main() diff --git a/python/tests/test_lmcache_integration.py b/python/tests/test_lmcache_integration.py new file mode 100644 index 00000000..21fd6496 --- /dev/null +++ b/python/tests/test_lmcache_integration.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +""" +Test hipfile bindings integration with LMCache pattern. +This verifies that the hipFile bindings can be used in the same way as cuFile. +""" + +import ctypes +import tempfile +import os + +try: + import hipfile + print("✓ hipfile imported successfully") +except ImportError as e: + print(f"✗ Failed to import hipfile: {e}") + exit(1) + +def test_buffer_registration(): + """Test buffer registration similar to LMCache usage.""" + print("\n=== Testing Buffer Registration ===") + + # Test that the required functions exist + required_functions = [ + 'hipFileBufRegister', 'hipFileBufDeregister', + 'hipFileDriverOpen', 'hipFileDriverClose', + ] + + for func_name in required_functions: + if hasattr(hipfile, func_name): + print(f"✓ {func_name} available") + else: + print(f"✗ {func_name} missing") + return False + + return True + +def test_context_managers(): + """Test context manager patterns.""" + print("\n=== Testing Context Managers ===") + + # Check if context managers are available + if hasattr(hipfile, 'CuFileDriver'): + print("✓ CuFileDriver context manager available") + else: + print("✗ CuFileDriver context manager missing") + return False + + if hasattr(hipfile, 'CuFile'): + print("✓ CuFile context manager available") + else: + print("✗ CuFile context manager missing") + return False + + # RegisteredBuffer is not implemented yet, so we'll skip it + print("- RegisteredBuffer context manager not implemented (optional)") + + return True + +def test_error_handling(): + """Test error handling.""" + print("\n=== Testing Error Handling ===") + + if hasattr(hipfile, 'HipFileError'): + print("✓ HipFileError exception class available") + else: + print("✗ HipFileError exception class missing") + return False + + return True + +def test_constants(): + """Test that required constants are available.""" + print("\n=== Testing Constants ===") + + # Check for version + if hasattr(hipfile, '__version__'): + print(f"✓ hipfile version: {hipfile.__version__}") + else: + print("✗ version information missing") + return False + + # Check if we can import from bindings + try: + from hipfile.bindings import hipFileHandle_t + print("✓ hipFileHandle_t type available") + except ImportError: + print("✗ hipFileHandle_t type missing") + return False + + return True + +def main(): + """Run all integration tests.""" + print("=== hipfile Python Bindings Integration Test ===") + print(f"hipfile version: {getattr(hipfile, '__version__', 'unknown')}") + + tests = [ + test_buffer_registration, + test_context_managers, + test_error_handling, + test_constants + ] + + passed = 0 + total = len(tests) + + for test in tests: + try: + if test(): + passed += 1 + except Exception as e: + print(f"✗ Test {test.__name__} failed with exception: {e}") + + print(f"\n=== Results: {passed}/{total} tests passed ===") + + if passed == total: + print("🎉 All integration tests passed! hipfile bindings are ready for LMCache.") + return True + else: + print("❌ Some tests failed. Check the output above.") + return False + +if __name__ == "__main__": + success = main() + exit(0 if success else 1)