Skip to content

Commit 6146f99

Browse files
[torchlib] [ci] Fix failing external data tests for Windows (#1834)
Fix failing external data tests for Windows due to file permission errors --------- Co-authored-by: Justin Chu <[email protected]>
1 parent 6071f8d commit 6146f99

File tree

4 files changed

+52
-2
lines changed

4 files changed

+52
-2
lines changed

onnxscript/ir/_core.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,13 @@ def tobytes(self) -> bytes:
671671
length = self._length or self.nbytes
672672
return self.raw[offset : offset + length]
673673

674+
def release(self) -> None:
675+
"""Delete all references to the memory buffer and close the memory-mapped file."""
676+
self._array = None
677+
if self.raw is not None:
678+
self.raw.close()
679+
self.raw = None
680+
674681
@property
675682
def metadata_props(self) -> dict[str, str]:
676683
if self._metadata_props is None:

onnxscript/ir/_core_test.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,26 @@ def test_initialize(self):
244244
# Ensure repeated reads are consistent
245245
np.testing.assert_equal(tensor, self.data)
246246

247+
def test_release_does_not_invalidate_tensor(self):
248+
external_tensor = self.model.graph.initializer[0]
249+
external_info = onnx.external_data_helper.ExternalDataInfo(external_tensor)
250+
tensor = _core.ExternalTensor(
251+
external_info.location,
252+
offset=external_info.offset,
253+
length=external_info.length,
254+
dtype=ir.DataType.FLOAT,
255+
base_dir=self.base_path,
256+
name="input",
257+
shape=_core.Shape(external_tensor.dims),
258+
)
259+
self.assertEqual(tensor.dtype, ir.DataType.FLOAT)
260+
self.assertEqual(tensor.tobytes(), self.data.tobytes())
261+
# Release tensor
262+
tensor.release()
263+
self.assertEqual(tensor.raw, None)
264+
# Tensor can be re-loaded after release
265+
self.assertEqual(tensor.tobytes(), self.data.tobytes())
266+
247267
def test_initialize_with_relative_path(self):
248268
external_tensor = self.model.graph.initializer[0]
249269
external_info = onnx.external_data_helper.ExternalDataInfo(external_tensor)

onnxscript/ir/_external_data.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def _load_external_data_file(
100100
if os.path.samefile(tensor.path, os.path.join(base_path, relative_path)):
101101
# Copy the data as the .numpy() call references data from a file whose data is eventually modified
102102
tensor_data = external_tensor.numpy().copy()
103+
external_tensor.release()
103104
tensor = _core.Tensor(
104105
tensor_data, name=external_tensor.name, dtype=external_tensor.dtype
105106
)
@@ -165,6 +166,8 @@ def _save_external_data(
165166
current_offset = tensor_info.offset
166167
assert tensor is not None
167168
raw_data = tensor.tobytes()
169+
if isinstance(tensor, _core.ExternalTensor):
170+
tensor.release()
168171
# Pad file to required offset if needed
169172
file_size = data_file.tell()
170173
if current_offset > file_size:
@@ -223,6 +226,7 @@ def convert_tensors_to_external(
223226
path = os.path.join(base_path, relative_path)
224227
# Check if file path is valid, and create subsequent subdirectories within the path if they don't exist
225228
os.makedirs(os.path.dirname(path), exist_ok=True)
229+
tmp_file_created = False
226230
# Check if file exists. Load pre-existing external data if it does.
227231
if os.path.exists(path):
228232
# Check if any tensor in the model is using the destination file
@@ -241,6 +245,7 @@ def convert_tensors_to_external(
241245
os.makedirs(tmp_path, exist_ok=True)
242246
# If exisiting external tensors are not loaded to memory, copy the external data to a temporary location
243247
os.rename(path, os.path.join(tmp_path, relative_path))
248+
tmp_file_created = True
244249
for tensor in tensors:
245250
if (
246251
isinstance(tensor, _core.ExternalTensor)
@@ -270,6 +275,12 @@ def convert_tensors_to_external(
270275
external_tensors[i]
271276
for i in sorted(range(len(external_tensors)), key=lambda i: sorted_indices[i])
272277
]
278+
279+
# Clean-up temporary file if it is created
280+
tmp_path = os.path.join(base_path, "tmp", relative_path)
281+
if os.path.exists(tmp_path) and tmp_file_created:
282+
os.remove(tmp_path)
283+
273284
return external_tensors
274285

275286

onnxscript/ir/_external_data_test.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
33
import os
4+
import sys
45
import tempfile
56
import typing
67
import unittest
@@ -115,7 +116,10 @@ class OffloadExternalTensorTest(unittest.TestCase):
115116

116117
def setUp(self):
117118
# File paths
118-
self.temp_dir = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with
119+
if sys.version_info[:2] >= (3, 10):
120+
self.temp_dir = tempfile.TemporaryDirectory(ignore_cleanup_errors=True) # pylint: disable=consider-using-with
121+
else:
122+
self.temp_dir = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with
119123
self.external_data_name = "external_tensors.bin"
120124
self.base_path = self.temp_dir.name
121125
self.ext_data_1 = "external_data_1.bin"
@@ -136,7 +140,15 @@ def setUp(self):
136140
self.model_with_mixed_external_data = self._model_with_mixed_external_data()
137141

138142
def tearDown(self) -> None:
139-
self.temp_dir.cleanup()
143+
# Handle exceptions for windows and python versions < 3.10
144+
try:
145+
self.temp_dir.cleanup()
146+
except PermissionError as e:
147+
print(f"PermissionError: {e}")
148+
except FileNotFoundError as e:
149+
print(f"FileNotFoundError: {e}")
150+
except Exception as e: # pylint: disable=broad-exception-caught
151+
print(f"An unexpected error occurred: {e}")
140152

141153
def _simple_model(self) -> ir.Model:
142154
tensor1 = ir.Tensor(

0 commit comments

Comments
 (0)