Skip to content

Commit e682d64

Browse files
sdimitroyanok
authored andcommitted
Allow open()/close() without context manager
File registration/deregistration can become a bottleneck. Opening cuFile handles without a context manager allows us to re-use handles and register them once.
1 parent 4afe7b2 commit e682d64

File tree

3 files changed

+97
-21
lines changed

3 files changed

+97
-21
lines changed

cufile/cufile.py

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,37 +49,59 @@ def __init__(self, path: str, mode: str = "r", use_direct_io: bool = False):
4949
if use_direct_io:
5050
self._os_mode |= os.O_DIRECT
5151

52-
def __enter__(self):
52+
self._handle = None
53+
self._cu_file_handle = None
54+
55+
def __del__(self):
5356
"""
54-
Context manager entry.
57+
Destructor to ensure the file is closed.
5558
"""
59+
if self.is_open:
60+
self.close()
61+
62+
@property
63+
def is_open(self) -> bool:
64+
return self._handle is not None
65+
66+
def open(self):
67+
"""Opens the file and registers the handle."""
68+
if self.is_open:
69+
return
5670
self._handle = os.open(self._path, self._os_mode)
5771
self._cu_file_handle = cuFileHandleRegister(self._handle)
58-
return self
5972

60-
def __exit__(self, exc_type, exc_val, exc_tb):
61-
"""
62-
Context manager exit.
63-
"""
73+
def close(self):
74+
"""Deregisters the handle and closes the file."""
75+
if not self.is_open:
76+
return
6477
cuFileHandleDeregister(self._cu_file_handle)
6578
os.close(self._handle)
79+
self._handle = None
80+
self._cu_file_handle = None
81+
82+
def __enter__(self):
83+
"""Context manager entry."""
84+
self.open()
85+
return self
86+
87+
def __exit__(self, exc_type, exc_val, exc_tb):
88+
"""Context manager exit."""
89+
self.close()
6690

6791
def read(self, dest: ctypes.c_void_p, size: int, file_offset: int = 0, dev_offset: int = 0):
68-
"""
69-
Read from the file.
70-
"""
92+
"""Read from the file."""
93+
if not self.is_open:
94+
raise IOError("File is not open.")
7195
return cuFileRead(self._cu_file_handle, dest, size, file_offset, dev_offset)
7296

7397
def write(self, src: ctypes.c_void_p, size: int, file_offset: int = 0, dev_offset: int = 0):
74-
"""
75-
Write to the file.
76-
"""
98+
"""Write to the file."""
99+
if not self.is_open:
100+
raise IOError("File is not open.")
77101
return cuFileWrite(self._cu_file_handle, src, size, file_offset, dev_offset)
78102

79103
def get_handle(self):
80-
"""
81-
Get the file handle.
82-
"""
104+
"""Get the file handle."""
83105
return self._handle
84106

85-
107+

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ pytest>=7.0.0
22
pytest-cov>=4.0.0
33
black>=22.0.0
44
flake8>=6.0.0
5-
mypy>=1.0.0
5+
mypy>=1.0.0
6+
cuda-bindings>=12.0.0

tests/test_cufile.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
assert err == 0, f"cuInit failed: {err}"
2121
err, device = cuda.cuDeviceGet(CUDA_DEVICE)
2222
assert err == 0, f"cuDeviceGet failed: {err}"
23-
err, context = cuda.cuCtxCreate(0, device)
23+
params = cuda.CUctxCreateParams()
24+
assert params is not None
25+
err, context = cuda.cuCtxCreate(params, 0, device)
2426
assert err == 0, f"cuCtxCreate failed: {err}"
2527
err, dptr_w = cuda.cuMemAlloc(BUF_SIZE)
2628
assert err == 0, f"cuMemAlloc failed: {err}"
@@ -41,9 +43,8 @@ def test_cufile_context_manager():
4143
with CuFile(file_path, "w") as cufile:
4244
assert isinstance(cufile, CuFile)
4345

44-
def test_cufile_read_write():
46+
def test_cufile_read_write_with_context_manager():
4547
"""Test that CuFile can read and write to a file."""
46-
4748
begin = time.perf_counter()
4849
with CuFile(file_path, "w") as cufile:
4950
begin_write = time.perf_counter()
@@ -69,3 +70,55 @@ def test_cufile_read_write():
6970
host_buf = (ctypes.c_ubyte * BUF_SIZE).from_address(hptr)
7071
for i in range(BUF_SIZE):
7172
assert host_buf[i] == PATTERN_BYTE
73+
74+
def test_cufile_read_write():
75+
"""Test that CuFile can read and write to a file."""
76+
77+
begin = time.perf_counter()
78+
cufile = CuFile(file_path, "w")
79+
cufile.open()
80+
begin_write = time.perf_counter()
81+
ret = cufile.write(ctypes.c_void_p(int(dptr_w)), BUF_SIZE)
82+
write_time = time.perf_counter() - begin_write
83+
assert ret == BUF_SIZE
84+
cufile.close()
85+
dt = time.perf_counter() - begin
86+
print(f"WRITE (w/o open/register) {ret/1024/1024:.2f}MB in {write_time*1e3:.2f}ms ({ret/write_time/1024/1024/1024:.2f}GB/s)")
87+
print(f"FULL WRITE {ret/1024/1024:.2f}MB in {dt*1e3:.2f}ms ({ret/dt/1024/1024/1024:.2f}GB/s)")
88+
89+
begin = time.perf_counter()
90+
cufile = CuFile(file_path, "r")
91+
cufile.open()
92+
begin_read = time.perf_counter()
93+
ret = cufile.read(ctypes.c_void_p(int(dptr_r)), BUF_SIZE)
94+
read_time = time.perf_counter() - begin_read
95+
assert ret == BUF_SIZE
96+
cufile.close()
97+
dt = time.perf_counter() - begin
98+
print(f"READ (w/o open/register) {ret/1024/1024:.2f}MB in {read_time*1e3:.2f}ms ({ret/read_time/1024/1024/1024:.2f}GB/s)")
99+
print(f"FULL READ {ret/1024/1024:.2f}MB in {dt*1e3:.2f}ms ({ret/dt/1024/1024/1024:.2f}GB/s)")
100+
101+
err, = cuda.cuMemcpyDtoH(hptr, dptr_r, BUF_SIZE)
102+
assert err == 0, f"cuMemcpyDtoH failed: {err}"
103+
host_buf = (ctypes.c_ubyte * BUF_SIZE).from_address(hptr)
104+
for i in range(BUF_SIZE):
105+
assert host_buf[i] == PATTERN_BYTE
106+
107+
def test_read_write_without_open():
108+
cufile = CuFile(file_path, "r")
109+
try:
110+
cufile.read(ctypes.c_void_p(int(dptr_r)), BUF_SIZE)
111+
raise AssertionError("Expected an exception but none was raised")
112+
except IOError as e:
113+
assert "File is not open." in str(e)
114+
finally:
115+
cufile.close()
116+
117+
cufile = CuFile(file_path, "w")
118+
try:
119+
cufile.write(ctypes.c_void_p(int(dptr_w)), BUF_SIZE)
120+
raise AssertionError("Expected an exception but none was raised")
121+
except IOError as e:
122+
assert "File is not open." in str(e)
123+
finally:
124+
cufile.close()

0 commit comments

Comments
 (0)