| # Copyright 2023 The IREE Authors | |
| # | |
| # Licensed under the Apache License v2.0 with LLVM Exceptions. | |
| # See https://llvm.org/LICENSE.txt for license information. | |
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| import array | |
| import gc | |
| import logging | |
| import os | |
| import numpy as np | |
| from pathlib import Path | |
| import tempfile | |
| import unittest | |
| import iree.runtime as rt | |
| def _float_constant(val: float) -> array.array: | |
| return array.array("f", [val]) | |
| def index_entry_as_array(entry, like_array): | |
| return np.asarray(entry.file_view).view(like_array.dtype).reshape(like_array.shape) | |
| class ParameterApiTest(unittest.TestCase): | |
| def testCreateArchiveFile(self): | |
| splat_index = rt.ParameterIndex() | |
| splat_index.add_splat("weight", _float_constant(2.0), 30 * 20 * 4) | |
| splat_index.add_splat("bias", _float_constant(1.0), 30 * 4) | |
| with tempfile.TemporaryDirectory() as td: | |
| file_path = Path(td) / "archive.irpa" | |
| target_index = splat_index.create_archive_file(str(file_path)) | |
| print(target_index) | |
| self.assertTrue(file_path.exists()) | |
| self.assertGreater(file_path.stat().st_size, 0) | |
| def testArchiveFileRoundtrip(self): | |
| orig_array = np.asarray([[1], [2], [3]], dtype=np.int64) | |
| def verify_archive(file_path: Path): | |
| # Load and verify. | |
| index = rt.ParameterIndex() | |
| # For this test, disable mmap to make temp file management on | |
| # windows a bit better. | |
| index.load(str(file_path), mmap=False) | |
| self.assertEqual(len(index), 3) | |
| # Note that the happy path of most properties are verified via | |
| # the repr (as they are called internal to that). | |
| entries = dict(index.items()) | |
| self.assertEqual( | |
| repr(entries["weight"]), | |
| "<ParameterIndexEntry 'weight' splat b'\\x02':600>", | |
| ) | |
| self.assertEqual( | |
| repr(entries["bias"]), | |
| "<ParameterIndexEntry 'bias' splat b' ':30>", | |
| ) | |
| self.assertRegex( | |
| repr(entries["array"]), | |
| r"<ParameterIndexEntry 'array' FileHandle<host_allocation\(.*\)>:384:24", | |
| ) | |
| # Verify some non-happy paths. | |
| with self.assertRaisesRegex(ValueError, "Entry is not file storage based"): | |
| entries["weight"].file_storage | |
| with self.assertRaisesRegex(ValueError, "Entry is not splat"): | |
| entries["array"].splat_pattern | |
| # Verify that the repr of the index itself is sensical. | |
| index_repr = repr(index) | |
| self.assertIn("Parameter scope <global> (3 entries", index_repr) | |
| # Get the array contents and verify against original. | |
| array_view = entries["array"].file_view | |
| self.assertEqual(len(array_view), 24) | |
| array_back = np.asarray(array_view).view(np.int64).reshape(orig_array.shape) | |
| np.testing.assert_array_equal(array_back, orig_array) | |
| with tempfile.TemporaryDirectory() as td: | |
| file_path = Path(td) / "archive.irpa" | |
| rt.save_archive_file( | |
| { | |
| "weight": rt.SplatValue(np.int8(2), [30, 20]), | |
| "bias": rt.SplatValue(array.array("b", [32]), 30), | |
| "array": orig_array, | |
| }, | |
| file_path, | |
| ) | |
| self.assertTrue(file_path.exists()) | |
| self.assertGreater(file_path.stat().st_size, 0) | |
| # Open / verify in its own scope and collect prior to tearing | |
| # down the temp dir. | |
| verify_archive(file_path) | |
| gc.collect() | |
| def testArchiveFileRoundtripByFD(self): | |
| orig_array = np.asarray([[1], [2], [3]], dtype=np.int64) | |
| def verify_archive_from_fd(file_path: Path): | |
| f = open(file_path, "rb") | |
| self.assertIsNotNone(f) | |
| handle = rt.FileHandle.wrap_fd(f.fileno(), True, False) | |
| # Load and verify. | |
| index = rt.ParameterIndex() | |
| # For this test, disable mmap to make temp file management on | |
| # windows a bit better. | |
| index.load_from_file_handle(handle, "irpa") | |
| self.assertEqual(len(index), 3) | |
| # Note that the happy path of most properties are verified via | |
| # the repr (as they are called internal to that). | |
| entries = dict(index.items()) | |
| self.assertEqual( | |
| repr(entries["weight"]), | |
| "<ParameterIndexEntry 'weight' splat b'\\x02':600>", | |
| ) | |
| self.assertEqual( | |
| repr(entries["bias"]), | |
| "<ParameterIndexEntry 'bias' splat b' ':30>", | |
| ) | |
| self.assertRegex( | |
| repr(entries["array"]), | |
| r"<ParameterIndexEntry 'array' FileHandle<fd\(.*\)>", | |
| ) | |
| # Verify some non-happy paths. | |
| with self.assertRaisesRegex(ValueError, "Entry is not file storage based"): | |
| entries["weight"].file_storage | |
| with self.assertRaisesRegex(ValueError, "Entry is not splat"): | |
| entries["array"].splat_pattern | |
| # Verify that the repr of the index itself is sensical. | |
| index_repr = repr(index) | |
| self.assertIn("Parameter scope <global> (3 entries", index_repr) | |
| f.close() | |
| with tempfile.TemporaryDirectory() as td: | |
| file_path = Path(td) / "archive.irpa" | |
| rt.save_archive_file( | |
| { | |
| "weight": rt.SplatValue(np.int8(2), [30, 20]), | |
| "bias": rt.SplatValue(array.array("b", [32]), 30), | |
| "array": orig_array, | |
| }, | |
| file_path, | |
| ) | |
| self.assertTrue(file_path.exists()) | |
| self.assertGreater(file_path.stat().st_size, 0) | |
| # Open / verify in its own scope and collect prior to tearing | |
| # down the temp dir. | |
| verify_archive_from_fd(file_path) | |
| gc.collect() | |
| def testArchiveFileAsync(self): | |
| with tempfile.TemporaryDirectory() as td: | |
| path = Path(td) / "archive.irpa" | |
| expected = np.arange(24, dtype=np.int64) | |
| rt.save_archive_file({"array": expected}, path) | |
| index = rt.ParameterIndex() | |
| index.load(str(path), mode="file_async") | |
| entry = dict(index.items())["array"] | |
| handle, offset = entry.file_storage | |
| self.assertTrue(handle.is_fd) | |
| self.assertTrue(handle.is_async) | |
| self.assertFalse(handle.is_host_allocation) | |
| self.assertEqual(entry.length, expected.nbytes) | |
| self.assertGreaterEqual(offset, 0) | |
| fd = handle.fd | |
| del entry, index | |
| gc.collect() | |
| # The Python handle keeps the descriptor alive after the index dies. | |
| self.assertEqual(os.fstat(fd).st_size, path.stat().st_size) | |
| del handle | |
| gc.collect() | |
| with self.assertRaises(OSError): | |
| os.fstat(fd) | |
| def testArchiveLegacyMmapModes(self): | |
| def verify(path, expected, *args, **kwargs): | |
| index = rt.ParameterIndex() | |
| index.load(str(path), *args, **kwargs) | |
| entry = dict(index.items())["array"] | |
| handle, _ = entry.file_storage | |
| self.assertTrue(handle.is_host_allocation) | |
| self.assertFalse(handle.is_async) | |
| np.testing.assert_array_equal( | |
| index_entry_as_array(entry, expected), expected | |
| ) | |
| with tempfile.TemporaryDirectory() as td: | |
| path = Path(td) / "archive.irpa" | |
| expected = np.arange(24, dtype=np.int64) | |
| rt.save_archive_file({"array": expected}, path) | |
| verify(path, expected) | |
| for mmap in (True, False): | |
| with self.subTest(mmap=mmap): | |
| verify(path, expected, mmap=mmap) | |
| verify(path, expected, "irpa", True, False, mmap) | |
| gc.collect() | |
| def testArchiveInvalidMode(self): | |
| index = rt.ParameterIndex() | |
| with self.assertRaisesRegex(ValueError, "unsupported parameter file mode"): | |
| index.load("missing.irpa", mode="invalid") | |
| for mmap in (True, False): | |
| with self.subTest(mmap=mmap): | |
| with self.assertRaisesRegex(ValueError, "mode and mmap"): | |
| index.load("missing.irpa", mode="file_async", mmap=mmap) | |
| self.assertEqual(len(index), 0) | |
| def testArchiveAsyncInvalidAccess(self): | |
| index = rt.ParameterIndex() | |
| for readable, writable in ((False, False), (False, True), (True, True)): | |
| with self.subTest(readable=readable, writable=writable): | |
| with self.assertRaisesRegex(ValueError, "requires read-only access"): | |
| index.load( | |
| "missing.irpa", | |
| mode="file_async", | |
| readable=readable, | |
| writable=writable, | |
| ) | |
| self.assertEqual(len(index), 0) | |
| def testParameterIndexEntryFromToNumpy(self): | |
| array = np.array([[1, 2], [3, 4]], dtype=np.int32) | |
| index = rt.ParameterIndex() | |
| key = "key" | |
| rt.parameter_index_add_numpy_ndarray(index, key, array) | |
| assert index.items()[0][0] == key | |
| index_entry_as_array = rt.parameter_index_entry_as_numpy_ndarray( | |
| index.items()[0][1] | |
| ) | |
| np.testing.assert_equal(index_entry_as_array, array) | |
| def testParameterIndexEntryFromToNumpyZeroDims(self): | |
| array = np.array(1234, dtype=np.int32) | |
| index = rt.ParameterIndex() | |
| key = "key" | |
| rt.parameter_index_add_numpy_ndarray(index, key, array) | |
| assert index.items()[0][0] == key | |
| index_entry_as_array = rt.parameter_index_entry_as_numpy_ndarray( | |
| index.items()[0][1] | |
| ) | |
| np.testing.assert_equal(index_entry_as_array, array) | |
| def testParameterIndexEntryFromIreeTurbine(self): | |
| """Verify that we are able to load a tensor from IRPA generated with IREE | |
| Turbine. | |
| We want to maintain backward compatibility with existing IRPA files.""" | |
| index = rt.ParameterIndex() | |
| irpa_path = str( | |
| Path(__file__).resolve().parent | |
| / "testdata" | |
| / "tensor_saved_with_iree_turbine.irpa" | |
| ) | |
| index.load(irpa_path) | |
| items = index.items() | |
| assert len(items) == 1 | |
| key, entry = items[0] | |
| assert key == "the_torch_tensor" | |
| index_entry_as_array = rt.parameter_index_entry_as_numpy_ndarray(entry) | |
| expected_array = np.array([1, 2, 3, 4], dtype=np.uint8) | |
| np.testing.assert_array_equal(index_entry_as_array, expected_array, strict=True) | |
| def testFileHandleWrap(self): | |
| fh = rt.FileHandle.wrap_memory(b"foobar") | |
| view = fh.host_allocation | |
| del fh | |
| self.assertEqual(bytes(view), b"foobar") | |
| def testFileHandleWrap(self): | |
| fh = rt.FileHandle.wrap_memory(b"foobar") | |
| view = fh.host_allocation | |
| del fh | |
| self.assertEqual(bytes(view), b"foobar") | |
| def testParameterIndexAddFromFile(self): | |
| splat_index = rt.ParameterIndex() | |
| fh = rt.FileHandle.wrap_memory(b"foobar") | |
| splat_index.add_from_file_handle("data", fh, length=3, offset=3) | |
| def testSplatTooBig(self): | |
| splat_index = rt.ParameterIndex() | |
| with self.assertRaises(ValueError): | |
| splat_index.add_splat( | |
| "weight", array.array("f", [1.0, 2.0, 3.0, 4.0, 5.0]), 30 * 20 * 4 | |
| ) | |
| def testGguf(self): | |
| index = rt.ParameterIndex() | |
| index.load( | |
| str( | |
| Path(__file__).resolve().parent | |
| / "testdata" | |
| / "parameter_weight_bias_1.gguf" | |
| ) | |
| ) | |
| expected_weight = np.zeros([30, 20], dtype=np.float32) + 2.0 | |
| expected_bias = np.zeros([30], dtype=np.float32) + 1.0 | |
| entries = dict(index.items()) | |
| weight = index_entry_as_array(entries["weight"], expected_weight) | |
| bias = index_entry_as_array(entries["bias"], expected_bias) | |
| np.testing.assert_array_equal(weight, expected_weight) | |
| np.testing.assert_array_equal(bias, expected_bias) | |
| def testSafetensors(self): | |
| index = rt.ParameterIndex() | |
| index.load( | |
| str( | |
| Path(__file__).resolve().parent | |
| / "testdata" | |
| / "parameter_weight_bias_1.safetensors" | |
| ) | |
| ) | |
| expected_weight = np.zeros([30, 20], dtype=np.float32) + 2.0 | |
| expected_bias = np.zeros([30], dtype=np.float32) + 1.0 | |
| entries = dict(index.items()) | |
| weight = index_entry_as_array(entries["weight"], expected_weight) | |
| bias = index_entry_as_array(entries["bias"], expected_bias) | |
| np.testing.assert_array_equal(weight, expected_weight) | |
| np.testing.assert_array_equal(bias, expected_bias) | |
| if __name__ == "__main__": | |
| logging.basicConfig(level=logging.DEBUG) | |
| unittest.main() |