| # Copyright 2022 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 copy | |
| import pickle | |
| import gc | |
| import numpy as np | |
| import unittest | |
| import iree.runtime | |
| class DeviceHalTest(unittest.TestCase): | |
| def setUp(self): | |
| super().setUp() | |
| self.device = iree.runtime.get_device("local-task") | |
| self.allocator = self.device.allocator | |
| # Make sure device setup maintains proper references. | |
| gc.collect() | |
| def testGcShutdownFiasco(self): | |
| init_ary = np.zeros([3, 4], dtype=np.int32) + 2 | |
| ary = iree.runtime.asdevicearray(self.device, init_ary) | |
| # Drop all references to backing objects in reverse order to try to | |
| # trigger heap use-after-free on bad shutdown order. | |
| self.allocator = None | |
| gc.collect() | |
| self.device = None | |
| gc.collect() | |
| # Now drop the ary and make sure nothing crashes (which would indicate | |
| # a reference counting problem of some kind): The array should retain | |
| # everything that it needs to stay live. | |
| ary = None | |
| gc.collect() | |
| def testMetadataAttributes(self): | |
| init_ary = np.zeros([3, 4], dtype=np.int32) + 2 | |
| ary = iree.runtime.asdevicearray(self.device, init_ary) | |
| self.assertEqual([3, 4], ary.shape) | |
| self.assertEqual(np.int32, ary.dtype) | |
| def testExplicitHostTransfer(self): | |
| init_ary = np.zeros([3, 4], dtype=np.int32) + 2 | |
| ary = iree.runtime.asdevicearray(self.device, init_ary) | |
| self.assertEqual(repr(ary), "<IREE DeviceArray: shape=[3, 4], dtype=int32>") | |
| self.assertFalse(ary.is_host_accessible) | |
| # Explicit transfer. | |
| cp = ary.to_host() | |
| np.testing.assert_array_equal(cp, init_ary) | |
| self.assertTrue(ary.is_host_accessible) | |
| def testCudaDeviceLocalAsDeviceArrayMemoryType(self): | |
| try: | |
| device = iree.runtime.get_device("cuda") | |
| except Exception as e: | |
| self.skipTest(f"CUDA device unavailable: {e}") | |
| init_ary = np.zeros([3, 4], dtype=np.float32) + 2 | |
| ary = iree.runtime.asdevicearray( | |
| device, | |
| init_ary, | |
| memory_type=iree.runtime.MemoryType.DEVICE_LOCAL, | |
| allowed_usage=iree.runtime.BufferUsage.DEFAULT, | |
| ) | |
| memory_type = ary._buffer_view.get_buffer().memory_type() | |
| self.assertTrue(memory_type & int(iree.runtime.MemoryType.DEVICE_LOCAL)) | |
| self.assertFalse(memory_type & int(iree.runtime.MemoryType.HOST_LOCAL)) | |
| cp = ary.to_host() | |
| np.testing.assert_array_equal(cp, init_ary) | |
| def testOverrideDtype(self): | |
| init_ary = np.zeros([3, 4], dtype=np.int32) + 2 | |
| buffer_view = self.allocator.allocate_buffer_copy( | |
| memory_type=iree.runtime.MemoryType.DEVICE_LOCAL, | |
| allowed_usage=iree.runtime.BufferUsage.DEFAULT, | |
| device=self.device, | |
| buffer=init_ary, | |
| element_type=iree.runtime.HalElementType.SINT_32, | |
| ) | |
| ary = iree.runtime.DeviceArray( | |
| self.device, buffer_view, override_dtype=np.float32 | |
| ) | |
| # Explicit transfer. | |
| cp = ary.to_host() | |
| self.assertEqual(cp.dtype, np.float32) | |
| np.testing.assert_array_equal(cp, init_ary.astype(np.float32)) | |
| self.assertTrue(ary.is_host_accessible) | |
| def testIllegalImplicitHostTransfer(self): | |
| init_ary = np.zeros([3, 4], dtype=np.int32) + 2 | |
| ary = iree.runtime.asdevicearray(self.device, init_ary) | |
| # Implicit transfer. | |
| with self.assertRaises(ValueError): | |
| _ = np.asarray(ary) | |
| def testIllegalImplicitHostTransferForUfunc(self): | |
| init_ary = np.zeros([3, 4], dtype=np.int32) + 2 | |
| ary = iree.runtime.asdevicearray(self.device, init_ary) | |
| with self.assertRaises(ValueError): | |
| _ = ary + 1 | |
| with self.assertRaises(ValueError): | |
| _ = np.add(ary, 1) | |
| def testImplicitHostArithmetic(self): | |
| init_ary = np.zeros([3, 4], dtype=np.int32) + 2 | |
| ary = iree.runtime.asdevicearray( | |
| self.device, init_ary, implicit_host_transfer=True | |
| ) | |
| sum = ary + init_ary | |
| np.testing.assert_array_equal(sum, init_ary + 2) | |
| self.assertTrue(ary.is_host_accessible) | |
| def testArrayFunctions(self): | |
| init_ary = np.zeros([3, 4], dtype=np.float32) + 2 | |
| ary = iree.runtime.asdevicearray( | |
| self.device, init_ary, implicit_host_transfer=True | |
| ) | |
| f = np.isfinite(ary) | |
| self.assertTrue(f.all()) | |
| def testIteration(self): | |
| init_ary = np.array([0, 1, 2, 3, 4, 5]) | |
| ary = iree.runtime.asdevicearray( | |
| self.device, init_ary, implicit_host_transfer=True | |
| ) | |
| for index, value in enumerate(ary): | |
| self.assertEqual(index, value) | |
| def testSubscriptable(self): | |
| init_ary = np.array([0, 1, 2, 3, 4, 5]) | |
| ary = iree.runtime.asdevicearray( | |
| self.device, init_ary, implicit_host_transfer=True | |
| ) | |
| for index in range(0, 6): | |
| value = ary[index] | |
| self.assertEqual(index, value) | |
| def testReshape(self): | |
| init_ary = np.zeros([3, 4], dtype=np.float32) + 2 | |
| ary = iree.runtime.asdevicearray( | |
| self.device, init_ary, implicit_host_transfer=True | |
| ) | |
| reshaped = ary.reshape((4, 3)) | |
| self.assertEqual((4, 3), reshaped.shape) | |
| np_reshaped = np.reshape(ary, (2, 2, 3)) | |
| self.assertEqual((2, 2, 3), np_reshaped.shape) | |
| def testDeepcopy(self): | |
| init_ary = np.zeros([3, 4], dtype=np.float32) + 2 | |
| orig_ary = iree.runtime.asdevicearray( | |
| self.device, init_ary, implicit_host_transfer=True | |
| ) | |
| copy_ary = copy.deepcopy(orig_ary) | |
| self.assertIsNot(orig_ary, copy_ary) | |
| np.testing.assert_array_equal(orig_ary, copy_ary) | |
| def testPickle(self): | |
| init_ary = np.arange(3 * 4, dtype=np.float32).reshape([3, 4]) | |
| orig_ary = iree.runtime.asdevicearray( | |
| self.device, init_ary, implicit_host_transfer=True | |
| ) | |
| serialized_ary = pickle.dumps(orig_ary) | |
| copy_ary = pickle.loads(serialized_ary) | |
| self.assertIsNot(orig_ary, copy_ary) | |
| np.testing.assert_array_equal(orig_ary, copy_ary) | |
| def testAsType(self): | |
| init_ary = np.zeros([3, 4], dtype=np.int32) + 2 | |
| orig_ary = iree.runtime.asdevicearray( | |
| self.device, init_ary, implicit_host_transfer=True | |
| ) | |
| # Same dtype, no copy. | |
| i32_nocopy = orig_ary.astype(np.int32, copy=False) | |
| self.assertIs(orig_ary, i32_nocopy) | |
| # Same dtype, copy. | |
| i32_nocopy = orig_ary.astype(np.int32) | |
| self.assertIsNot(orig_ary, i32_nocopy) | |
| np.testing.assert_array_equal(orig_ary, i32_nocopy) | |
| # Different dtype, copy. | |
| f32_copy = orig_ary.astype(np.float32) | |
| self.assertIsNot(orig_ary, f32_copy) | |
| self.assertEqual(f32_copy.dtype, np.float32) | |
| np.testing.assert_array_equal(orig_ary.astype(np.float32), f32_copy) | |
| def testBool(self): | |
| init_ary = np.zeros([3, 4], dtype=np.bool_) | |
| init_ary[1] = True # Set some non-zero value. | |
| ary = iree.runtime.asdevicearray(self.device, init_ary) | |
| self.assertEqual(repr(ary), "<IREE DeviceArray: shape=[3, 4], dtype=bool>") | |
| np.testing.assert_array_equal(ary.to_host(), init_ary) | |
| def testBFloat16(self): | |
| dtype = np.dtype("bfloat16") | |
| init_ary = np.arange(12, dtype=dtype).reshape(3, 4) | |
| ary = iree.runtime.asdevicearray(self.device, init_ary) | |
| self.assertEqual(ary.dtype, init_ary.dtype) | |
| result = ary.to_host() | |
| self.assertEqual(result.dtype, init_ary.dtype) | |
| np.testing.assert_array_equal(result, init_ary) | |
| def testFloat8ElementTypes(self): | |
| float8_cases = [ | |
| (iree.runtime.HalElementType.FLOAT_8_E5M2, "float8_e5m2"), | |
| (iree.runtime.HalElementType.FLOAT_8_E4M3_FN, "float8_e4m3fn"), | |
| ( | |
| iree.runtime.HalElementType.FLOAT_8_E5M2_FNUZ, | |
| "float8_e5m2fnuz", | |
| ), | |
| ( | |
| iree.runtime.HalElementType.FLOAT_8_E4M3_FNUZ, | |
| "float8_e4m3fnuz", | |
| ), | |
| (iree.runtime.HalElementType.FLOAT_8_E8M0_FNU, "float8_e8m0fnu"), | |
| ] | |
| for hal_element_type, dtype_name in float8_cases: | |
| with self.subTest(dtype=dtype_name): | |
| init_ary = np.arange(12, dtype=np.dtype(dtype_name)).reshape(3, 4) | |
| ary = iree.runtime.asdevicearray(self.device, init_ary) | |
| self.assertEqual(ary.dtype, init_ary.dtype) | |
| result = ary.to_host() | |
| self.assertEqual(result.dtype, init_ary.dtype) | |
| np.testing.assert_array_equal( | |
| result.view(np.uint8), init_ary.view(np.uint8) | |
| ) | |
| def testCopyToHostImportPath(self): | |
| """_copy_to_host import path must preserve values on local-task.""" | |
| init_ary = np.arange(12, dtype=np.float32).reshape(3, 4) | |
| ary = iree.runtime.asdevicearray(self.device, init_ary) | |
| # Call _copy_to_host directly (bypasses _is_mappable check). | |
| result = ary._copy_to_host() | |
| np.testing.assert_array_equal(result, init_ary) | |
| def testCopyToHostStagingPath(self): | |
| """_copy_to_host staging fallback must preserve values.""" | |
| init_ary = np.arange(12, dtype=np.float32).reshape(3, 4) | |
| ary = iree.runtime.asdevicearray(self.device, init_ary) | |
| # Patch out import support to force the staging path. | |
| from unittest.mock import patch | |
| orig = self.device.allocator.query_buffer_compatibility | |
| def no_import(self_alloc, *args, **kwargs): | |
| return orig(*args, **kwargs) & ~int( | |
| iree.runtime.BufferCompatibility.IMPORTABLE | |
| ) | |
| with patch.object( | |
| type(self.device.allocator), "query_buffer_compatibility", no_import | |
| ): | |
| result = ary._copy_to_host() | |
| np.testing.assert_array_equal(result, init_ary) | |
| if __name__ == "__main__": | |
| unittest.main() |