已合并
Align NPU mixed-dtype async host-device copy behavior with CUDA #38858
zzhongmin创建于 6月18日
Align NPU mixed-dtype async host-device copy behavior with CUDA #38858
已合并
共 3 个文件变更+486-12
| @@ -7,10 +7,9 @@ import torch | |||
| 7 | from torch.autograd import Variable | 7 | from torch.autograd import Variable |
| 8 | 8 | ||
| 9 | import torch_npu | 9 | import torch_npu |
| 10 | -from torch_npu.testing.common_utils import freeze_rng_state | 10 | +from torch_npu.testing.common_utils import SupportedDevices, freeze_rng_state |
| 11 | from torch_npu.testing.testcase import run_tests, TestCase | 11 | from torch_npu.testing.testcase import run_tests, TestCase |
| 12 | 12 | ||
| 13 | - | ||
| 14 | class TestNpu(TestCase): | 13 | class TestNpu(TestCase): |
| 15 | FIFTY_MIL_CYCLES = 50000000 | 14 | FIFTY_MIL_CYCLES = 50000000 |
| 16 | 15 | ||
| @@ -370,6 +369,24 @@ class TestNpu(TestCase): | |||
| 370 | ) | 369 | ) |
| 371 | _test_to_non_blocking(src, try_non_blocking, dst) | 370 | _test_to_non_blocking(src, try_non_blocking, dst) |
| 372 | 371 | ||
| 372 | + | ||
| 373 | + def test_to_non_blocking_different_dtype(self): | ||
| 374 | + stream = torch_npu.npu.current_stream() | ||
| 375 | + | ||
| 376 | + def _test_to_non_blocking_different_dtype(src, non_blocking, dst, dtype): | ||
| 377 | + torch_npu.npu.synchronize() | ||
| 378 | + out = src.to(device=dst, dtype=dtype, non_blocking=non_blocking) | ||
| 379 | + stream.synchronize() | ||
| 380 | + self.assertEqual(src.to(dtype=dtype), out) | ||
| 381 | + self.assertTrue(out.is_pinned() == (non_blocking and dst == "cpu")) | ||
| 382 | + | ||
| 383 | + src_cpu = torch.arange(1024, dtype=torch.int32).reshape(128, 8).pin_memory() | ||
| 384 | + src_npu = torch.arange(1024, dtype=torch.int32, device="npu").reshape(128, 8) | ||
| 385 | + | ||
| 386 | + for non_blocking in (True, False): | ||
| 387 | + _test_to_non_blocking_different_dtype(src_cpu, non_blocking, "npu", torch.float32) | ||
| 388 | + _test_to_non_blocking_different_dtype(src_npu, non_blocking, "cpu", torch.float32) | ||
| 389 | + | ||
| 373 | def test_to_cpu_blocking_by_default(self): | 390 | def test_to_cpu_blocking_by_default(self): |
| 374 | src = torch.randn(1000000, device="npu") | 391 | src = torch.randn(1000000, device="npu") |
| 375 | torch_npu.npu.synchronize() | 392 | torch_npu.npu.synchronize() |
| @@ -12,17 +12,19 @@ API 签名:copy_(src, non_blocking=False) -> Tensor | |||
| 12 | | 参数类型 | src 为 Tensor(含标量张量)、与 self dtype 可不同 | 已覆盖 | | 12 | | 参数类型 | src 为 Tensor(含标量张量)、与 self dtype 可不同 | 已覆盖 | |
| 13 | | 传参与不传参 | non_blocking 省略与显式传入 | 已覆盖 | | 13 | | 传参与不传参 | non_blocking 省略与显式传入 | 已覆盖 | |
| 14 | | 等价类/边界值 | 同形、可广播、非连续目标、跨 CPU/NPU | 已覆盖 | | 14 | | 等价类/边界值 | 同形、可广播、非连续目标、跨 CPU/NPU | 已覆盖 | |
| 15 | +| 精度/数值正确性 | mixed-dtype host-device 路径下,同步/异步 copy 结果一致 | 已覆盖 | | ||
| 15 | | 正常传参场景 | NPU 上 copy 后 self 的 shape/dtype 不变;返回 self | 已覆盖 | | 16 | | 正常传参场景 | NPU 上 copy 后 self 的 shape/dtype 不变;返回 self | 已覆盖 | |
| 16 | | 异常传参场景 | 不可广播的 shape | 已覆盖 | | 17 | | 异常传参场景 | 不可广播的 shape | 已覆盖 | |
| 17 | 18 | ||
| 18 | 未覆盖项及原因: | 19 | 未覆盖项及原因: |
| 19 | - 无 | 20 | - 无 |
| 20 | 21 | ||
| 21 | -注意:本测试仅验证功能正确性(调用不报错、tensor 结构属性符合预期), | 22 | +注意:本测试除了验证功能正确性(调用不报错、tensor 结构属性符合预期), |
| 22 | - 不做精度和数值正确性校验。 | 23 | + 也对 mixed-dtype host-device 路径补充了同步/异步 copy 结果一致性校验。 |
| 23 | """ | 24 | """ |
| 24 | import torch | 25 | import torch |
| 25 | import torch_npu # noqa: F401 | 26 | import torch_npu # noqa: F401 |
| 27 | +from torch_npu.testing.common_utils import SupportedDevices | ||
| 26 | 28 | ||
| 27 | try: | 29 | try: |
| 28 | from torch_npu.testing.testcase import TestCase, run_tests | 30 | from torch_npu.testing.testcase import TestCase, run_tests |
| @@ -34,7 +36,6 @@ except ImportError: | |||
| 34 | def run_tests(): | 36 | def run_tests(): |
| 35 | unittest.main(argv=sys.argv) | 37 | unittest.main(argv=sys.argv) |
| 36 | 38 | ||
| 37 | - | ||
| 38 | class TestTensorCopy_(TestCase): | 39 | class TestTensorCopy_(TestCase): |
| 39 | """Functional tests for torch.Tensor.copy_ on NPU.""" | 40 | """Functional tests for torch.Tensor.copy_ on NPU.""" |
| 40 | 41 | ||
| @@ -47,6 +48,118 @@ class TestTensorCopy_(TestCase): | |||
| 47 | f"Expected device 'npu', got '{self.device_name}'", | 48 | f"Expected device 'npu', got '{self.device_name}'", |
| 48 | ) | 49 | ) |
| 49 | self.device = torch.device(self.device_name) | 50 | self.device = torch.device(self.device_name) |
| 51 | + self.dtype_cast_pairs = [ | ||
| 52 | + (torch.int32, torch.float32), | ||
| 53 | + (torch.int64, torch.float32), | ||
| 54 | + (torch.float16, torch.float32), | ||
| 55 | + (torch.float32, torch.float16), | ||
| 56 | + (torch.complex64, torch.complex128), | ||
| 57 | + (torch.complex128, torch.complex64), | ||
| 58 | + ] | ||
| 59 | + self.aclnn_cast_fallback_dtypes = [ | ||
| 60 | + torch.float8_e5m2, | ||
| 61 | + torch.float8_e4m3fn, | ||
| 62 | + ] | ||
| 63 | + self.precision_compare_cases = [ | ||
| 64 | + (torch.bool, torch.float32, [False, True, False, True, True, False, True, False]), | ||
| 65 | + (torch.int8, torch.float16, [-127, -31, -1, 0, 1, 7, 42, 127]), | ||
| 66 | + (torch.int16, torch.float32, [-32768, -1025, -1, 0, 1, 255, 1024, 32767]), | ||
| 67 | + (torch.uint16, torch.float32, [0, 1, 17, 255, 1024, 4096, 32768, 65535]), | ||
| 68 | + (torch.int64, torch.float32, [-(2 ** 20), -12345, -1, 0, 1, 12345, 4096, 2 ** 20]), | ||
| 69 | + (torch.float16, torch.float32, [-2048.0, -7.5, -0.125, 0.0, 0.125, 1.5, 33.25, 2048.0]), | ||
| 70 | + (torch.bfloat16, torch.float32, [-1.0e4, -7.5, -0.125, 0.0, 0.125, 1.5, 33.25, 1.0e4]), | ||
| 71 | + (torch.float32, torch.float16, [-65504.0, -255.5, -0.33325195, 0.0, 0.33325195, 17.625, 255.5, 65504.0]), | ||
| 72 | + (torch.float32, torch.bfloat16, [-1.0e8, -255.5, -0.33325195, 0.0, 0.33325195, 17.625, 255.5, 1.0e8]), | ||
| 73 | + ( | ||
| 74 | + torch.complex64, | ||
| 75 | + torch.complex128, | ||
| 76 | + [ | ||
| 77 | + -7.5 + 1.25j, | ||
| 78 | + -3.25 - 0.5j, | ||
| 79 | + -1.0 + 2.0j, | ||
| 80 | + 0.0 - 1.0j, | ||
| 81 | + 1.5 + 0.75j, | ||
| 82 | + 2.25 - 3.5j, | ||
| 83 | + 5.0 + 4.0j, | ||
| 84 | + 9.75 - 8.5j, | ||
| 85 | + ], | ||
| 86 | + ), | ||
| 87 | + ( | ||
| 88 | + torch.complex128, | ||
| 89 | + torch.complex64, | ||
| 90 | + [ | ||
| 91 | + -12.5 + 7.25j, | ||
| 92 | + -4.0 - 3.0j, | ||
| 93 | + -0.5 + 0.125j, | ||
| 94 | + 0.0 + 0.0j, | ||
| 95 | + 0.5 - 0.125j, | ||
| 96 | + 3.75 + 2.5j, | ||
| 97 | + 17.5 - 9.0j, | ||
| 98 | + 31.25 + 15.5j, | ||
| 99 | + ], | ||
| 100 | + ), | ||
| 101 | + ] | ||
| 102 | + | ||
| 103 | + def _make_host_source(self, dtype, pin_memory=False): | ||
| 104 | + base_dtype = torch.complex128 if dtype.is_complex else torch.float32 | ||
| 105 | + values = ( | ||
| 106 | + [[-7.5 + 1.25j, -3.25 - 0.5j, -1.0 + 2.0j, 0.0 - 1.0j], | ||
| 107 | + [1.5 + 0.75j, 2.25 - 3.5j, 5.0 + 4.0j, 9.75 - 8.5j]] | ||
| 108 | + if dtype.is_complex | ||
| 109 | + else [[-7.5, -3.25, -1.0, 0.0], [1.5, 2.25, 5.0, 9.75]] | ||
| 110 | + ) | ||
| 111 | + src = torch.tensor(values, dtype=base_dtype).to(dtype) | ||
| 112 | + return src.pin_memory() if pin_memory else src | ||
| 113 | + | ||
| 114 | + def _make_device_source(self, dtype): | ||
| 115 | + return self._make_host_source(dtype).to(self.device) | ||
| 116 | + | ||
| 117 | + def _assert_dtype_cast_copy_keeps_async(self, dst, src): | ||
| 118 | + gate_stream = torch_npu.npu.Stream(device=self.device) | ||
| 119 | + copy_stream = torch_npu.npu.Stream(device=self.device) | ||
| 120 | + gate_event = torch_npu.npu.Event() | ||
| 121 | + done_event = torch_npu.npu.Event() | ||
| 122 | + | ||
| 123 | + torch_npu.npu.synchronize() | ||
| 124 | + | ||
| 125 | + # Keep copy_stream pending behind work on gate_stream. A synchronous | ||
| 126 | + # fallback in copy_ would wait for the gate before returning. | ||
| 127 | + gate_a = torch.ones((4096, 4096), device=self.device, dtype=torch.float32) | ||
| 128 | + gate_b = torch.ones((4096, 4096), device=self.device, dtype=torch.float32) | ||
| 129 | + with torch_npu.npu.stream(gate_stream): | ||
| 130 | + gate_c = gate_a @ gate_b | ||
| 131 | + gate_c = gate_c @ gate_b | ||
| 132 | + gate_event.record() | ||
| 133 | + | ||
| 134 | + with torch_npu.npu.stream(copy_stream): | ||
| 135 | + copy_stream.wait_event(gate_event) | ||
| 136 | + ret = dst.copy_(src, non_blocking=True) | ||
| 137 | + done_event.record() | ||
| 138 | + | ||
| 139 | + self.assertIs(ret, dst) | ||
| 140 | + self.assertFalse(done_event.query()) | ||
| 141 | + done_event.synchronize() | ||
| 142 | + | ||
| 143 | + def _assert_copy_matches_cast(self, dst, src): | ||
| 144 | + expected = src.cpu().to(dtype=dst.dtype) | ||
| 145 | + actual = dst.cpu() if dst.device.type == self.device_name else dst | ||
| 146 | + self.assertEqual(actual, expected) | ||
| 147 | + | ||
| 148 | + def _to_cpu_if_needed(self, tensor): | ||
| 149 | + return tensor.cpu() if tensor.device.type == self.device_name else tensor | ||
| 150 | + | ||
| 151 | + def _assert_non_blocking_matches_blocking(self, async_dst, sync_dst, src, async_base=None, sync_base=None): | ||
| 152 | + sync_ret = sync_dst.copy_(src, non_blocking=False) | ||
| 153 | + async_ret = async_dst.copy_(src, non_blocking=True) | ||
| 154 | + | ||
| 155 | + self.assertIs(sync_ret, sync_dst) | ||
| 156 | + self.assertIs(async_ret, async_dst) | ||
| 157 | + | ||
| 158 | + torch_npu.npu.synchronize() | ||
| 159 | + self.assertEqual(self._to_cpu_if_needed(async_dst), self._to_cpu_if_needed(sync_dst)) | ||
| 160 | + | ||
| 161 | + if async_base is not None and sync_base is not None: | ||
| 162 | + self.assertEqual(self._to_cpu_if_needed(async_base), self._to_cpu_if_needed(sync_base)) | ||
| 50 | 163 | ||
| 51 | def test_copy_npu_same_device_same_shape(self): | 164 | def test_copy_npu_same_device_same_shape(self): |
| 52 | dst = torch.empty(3, 4, device=self.device, dtype=torch.float32) | 165 | dst = torch.empty(3, 4, device=self.device, dtype=torch.float32) |
| @@ -87,6 +200,291 @@ class TestTensorCopy_(TestCase): | |||
| 87 | self.assertIs(ret, dst) | 200 | self.assertIs(ret, dst) |
| 88 | self.assertEqual(dst.shape, torch.Size([2, 2])) | 201 | self.assertEqual(dst.shape, torch.Size([2, 2])) |
| 89 | 202 | ||
| 203 | + | ||
| 204 | + def test_copy_npu_from_pinned_cpu_src_dtype_cast_non_blocking(self): | ||
| 205 | + for src_dtype, dst_dtype in self.dtype_cast_pairs: | ||
| 206 | + dst = torch.empty(2, 4, dtype=dst_dtype, device=self.device) | ||
| 207 | + src = self._make_host_source(src_dtype, pin_memory=True) | ||
| 208 | + self._assert_dtype_cast_copy_keeps_async(dst, src) | ||
| 209 | + self._assert_copy_matches_cast(dst, src) | ||
| 210 | + | ||
| 211 | + | ||
| 212 | + def test_copy_pinned_cpu_from_npu_src_dtype_cast_non_blocking(self): | ||
| 213 | + for src_dtype, dst_dtype in self.dtype_cast_pairs: | ||
| 214 | + dst = torch.empty(2, 4, dtype=dst_dtype, pin_memory=True) | ||
| 215 | + src = self._make_device_source(src_dtype) | ||
| 216 | + self._assert_dtype_cast_copy_keeps_async(dst, src) | ||
| 217 | + self.assertTrue(dst.is_pinned()) | ||
| 218 | + self._assert_copy_matches_cast(dst, src) | ||
| 219 | + | ||
| 220 | + | ||
| 221 | + def test_copy_pinned_cpu_from_non_contiguous_npu_src_dtype_cast_non_blocking(self): | ||
| 222 | + src = torch.arange(8, dtype=torch.int32, device=self.device).reshape(4, 2).t() | ||
| 223 | + dst = torch.empty(2, 4, dtype=torch.float32, pin_memory=True) | ||
| 224 | + self.assertFalse(src.is_contiguous()) | ||
| 225 | + self._assert_dtype_cast_copy_keeps_async(dst, src) | ||
| 226 | + self.assertTrue(dst.is_pinned()) | ||
| 227 | + self._assert_copy_matches_cast(dst, src) | ||
| 228 | + | ||
| 229 | + | ||
| 230 | + def test_copy_pinned_cpu_non_contiguous_dst_dtype_cast_preserves_strided_layout(self): | ||
| 231 | + base = torch.full((3, 6), -99.0, dtype=torch.float32).pin_memory() | ||
| 232 | + dst = base[:, 1::2] | ||
| 233 | + src = torch.arange(9, dtype=torch.int32, device=self.device).reshape(3, 3) | ||
| 234 | + expected_base = torch.full((3, 6), -99.0, dtype=torch.float32) | ||
| 235 | + expected_base[:, 1::2] = src.cpu().to(dtype=dst.dtype) | ||
| 236 | + | ||
| 237 | + self.assertFalse(dst.is_contiguous()) | ||
| 238 | + self.assertTrue(dst.is_pinned()) | ||
| 239 | + self.assertNotEqual(dst.storage_offset(), 0) | ||
| 240 | + ret = dst.copy_(src, non_blocking=True) | ||
| 241 | + torch_npu.npu.synchronize() | ||
| 242 | + | ||
| 243 | + self.assertIs(ret, dst) | ||
| 244 | + self.assertEqual(dst, src.cpu().to(dtype=dst.dtype)) | ||
| 245 | + self.assertEqual(base, expected_base) | ||
| 246 | + | ||
| 247 | + | ||
| 248 | + def test_copy_npu_non_contiguous_dst_dtype_cast_non_blocking(self): | ||
| 249 | + base = torch.empty(4, 2, dtype=torch.float32, device=self.device) | ||
| 250 | + dst = base.t() | ||
| 251 | + src = self._make_host_source(torch.int32, pin_memory=True) | ||
| 252 | + self.assertFalse(dst.is_contiguous()) | ||
| 253 | + self._assert_dtype_cast_copy_keeps_async(dst, src) | ||
| 254 | + self._assert_copy_matches_cast(dst, src) | ||
| 255 | + | ||
| 256 | + | ||
| 257 | + def test_copy_npu_non_contiguous_dst_dtype_cast_preserves_strided_layout(self): | ||
| 258 | + base = torch.full((3, 6), -99.0, dtype=torch.float32, device=self.device) | ||
| 259 | + dst = base[:, 1::2] | ||
| 260 | + src = torch.arange(9, dtype=torch.int32).reshape(3, 3).pin_memory() | ||
| 261 | + expected_base = torch.full((3, 6), -99.0, dtype=torch.float32) | ||
| 262 | + expected_base[:, 1::2] = src.to(dtype=dst.dtype) | ||
| 263 | + | ||
| 264 | + self.assertFalse(dst.is_contiguous()) | ||
| 265 | + self.assertNotEqual(dst.storage_offset(), 0) | ||
| 266 | + ret = dst.copy_(src, non_blocking=True) | ||
| 267 | + torch_npu.npu.synchronize() | ||
| 268 | + | ||
| 269 | + self.assertIs(ret, dst) | ||
| 270 | + self.assertEqual(dst.cpu(), src.to(dtype=dst.dtype)) | ||
| 271 | + self.assertEqual(base.cpu(), expected_base) | ||
| 272 | + | ||
| 273 | + | ||
| 274 | + def test_copy_npu_from_pinned_cpu_broadcast_src_dtype_cast_non_blocking(self): | ||
| 275 | + dst = torch.empty(3, 4, dtype=torch.float32, device=self.device) | ||
| 276 | + src = torch.arange(4, dtype=torch.int32).reshape(1, 4).pin_memory() | ||
| 277 | + expected = src.to(dtype=dst.dtype).expand(3, 4) | ||
| 278 | + | ||
| 279 | + ret = dst.copy_(src, non_blocking=True) | ||
| 280 | + torch_npu.npu.synchronize() | ||
| 281 | + | ||
| 282 | + self.assertIs(ret, dst) | ||
| 283 | + self.assertEqual(dst.cpu(), expected) | ||
| 284 | + | ||
| 285 | + | ||
| 286 | + def test_copy_pinned_cpu_from_npu_broadcast_src_dtype_cast_non_blocking(self): | ||
| 287 | + dst = torch.empty(3, 4, dtype=torch.float32, pin_memory=True) | ||
| 288 | + src = torch.arange(4, dtype=torch.int32, device=self.device).reshape(1, 4) | ||
| 289 | + expected = src.cpu().to(dtype=dst.dtype).expand_as(dst) | ||
| 290 | + | ||
| 291 | + ret = dst.copy_(src, non_blocking=True) | ||
| 292 | + torch_npu.npu.synchronize() | ||
| 293 | + | ||
| 294 | + self.assertIs(ret, dst) | ||
| 295 | + self.assertEqual(dst, expected) | ||
| 296 | + | ||
| 297 | + | ||
| 298 | + def test_copy_npu_from_pinned_cpu_aclnn_cast_unsupported_src_dtype_fallback(self): | ||
| 299 | + for src_dtype in self.aclnn_cast_fallback_dtypes: | ||
| 300 | + dst = torch.empty(2, 4, dtype=torch.float32, device=self.device) | ||
| 301 | + src = torch.arange(8, dtype=torch.float32).reshape(2, 4).to(src_dtype).pin_memory() | ||
| 302 | + | ||
| 303 | + ret = dst.copy_(src, non_blocking=True) | ||
| 304 | + torch_npu.npu.synchronize() | ||
| 305 | + | ||
| 306 | + self.assertIs(ret, dst) | ||
| 307 | + self.assertEqual(dst.cpu(), src.to(dtype=dst.dtype)) | ||
| 308 | + | ||
| 309 | + | ||
| 310 | + def test_copy_pinned_cpu_from_npu_aclnn_cast_unsupported_dst_dtype_fallback(self): | ||
| 311 | + for dst_dtype in self.aclnn_cast_fallback_dtypes: | ||
| 312 | + dst = torch.empty(2, 4, dtype=dst_dtype).pin_memory() | ||
| 313 | + src = torch.arange(8, dtype=torch.float32, device=self.device).reshape(2, 4) | ||
| 314 | + | ||
| 315 | + ret = dst.copy_(src, non_blocking=True) | ||
| 316 | + torch_npu.npu.synchronize() | ||
| 317 | + | ||
| 318 | + self.assertIs(ret, dst) | ||
| 319 | + self.assertEqual(dst.dtype, dst_dtype) | ||
| 320 | + self.assertEqual(dst.to(dtype=src.dtype), src.cpu()) | ||
| 321 | + | ||
| 322 | + | ||
| 323 | + def test_copy_npu_from_pinned_cpu_aclnn_cast_unsupported_complex32_src_fallback(self): | ||
| 324 | + dst = torch.empty(2, 4, dtype=torch.complex64, device=self.device) | ||
| 325 | + src = self._make_host_source(torch.complex32, pin_memory=True) | ||
| 326 | + | ||
| 327 | + ret = dst.copy_(src, non_blocking=True) | ||
| 328 | + torch_npu.npu.synchronize() | ||
| 329 | + | ||
| 330 | + self.assertIs(ret, dst) | ||
| 331 | + self.assertEqual(dst.cpu(), src.to(dtype=dst.dtype)) | ||
| 332 | + | ||
| 333 | + | ||
| 334 | + def test_copy_pinned_cpu_from_npu_aclnn_cast_unsupported_complex32_dst_fallback(self): | ||
| 335 | + dst = torch.empty(2, 4, dtype=torch.complex32).pin_memory() | ||
| 336 | + src = self._make_device_source(torch.complex64) | ||
| 337 | + | ||
| 338 | + ret = dst.copy_(src, non_blocking=True) | ||
| 339 | + torch_npu.npu.synchronize() | ||
| 340 | + | ||
| 341 | + self.assertIs(ret, dst) | ||
| 342 | + self.assertTrue(dst.is_pinned()) | ||
| 343 | + self.assertEqual(dst.to(dtype=src.dtype), src.cpu()) | ||
| 344 | + | ||
| 345 | + | ||
| 346 | + def test_copy_npu_from_pinned_cpu_slice_dtype_cast_non_blocking(self): | ||
| 347 | + src_base = torch.arange(9, dtype=torch.int32).pin_memory() | ||
| 348 | + src = src_base[1:].reshape(2, 4) | ||
| 349 | + dst = torch.empty(2, 4, dtype=torch.float32, device=self.device) | ||
| 350 | + self.assertTrue(src.is_pinned()) | ||
| 351 | + self.assertNotEqual(src.data_ptr(), src.untyped_storage().data_ptr()) | ||
| 352 | + self._assert_dtype_cast_copy_keeps_async(dst, src) | ||
| 353 | + self._assert_copy_matches_cast(dst, src) | ||
| 354 | + | ||
| 355 | + | ||
| 356 | + def test_copy_npu_from_pinned_cpu_dtype_cast_non_blocking_matches_blocking(self): | ||
| 357 | + for src_dtype, dst_dtype, values in self.precision_compare_cases: | ||
| 358 | + with self.subTest(direction="h2d", src_dtype=src_dtype, dst_dtype=dst_dtype): | ||
| 359 | + src = torch.tensor(values, dtype=src_dtype).reshape(2, 4).pin_memory() | ||
| 360 | + async_dst = torch.empty(2, 4, dtype=dst_dtype, device=self.device) | ||
| 361 | + sync_dst = torch.empty(2, 4, dtype=dst_dtype, device=self.device) | ||
| 362 | + | ||
| 363 | + self._assert_non_blocking_matches_blocking(async_dst, sync_dst, src) | ||
| 364 | + self._assert_copy_matches_cast(async_dst, src) | ||
| 365 | + | ||
| 366 | + | ||
| 367 | + def test_copy_pinned_cpu_from_npu_dtype_cast_non_blocking_matches_blocking(self): | ||
| 368 | + for src_dtype, dst_dtype, values in self.precision_compare_cases: | ||
| 369 | + with self.subTest(direction="d2h", src_dtype=src_dtype, dst_dtype=dst_dtype): | ||
| 370 | + src = torch.tensor(values, dtype=src_dtype).reshape(2, 4).to(self.device) | ||
| 371 | + async_dst = torch.empty(2, 4, dtype=dst_dtype, pin_memory=True) | ||
| 372 | + sync_dst = torch.empty(2, 4, dtype=dst_dtype, pin_memory=True) | ||
| 373 | + | ||
| 374 | + self._assert_non_blocking_matches_blocking(async_dst, sync_dst, src) | ||
| 375 | + self._assert_copy_matches_cast(async_dst, src) | ||
| 376 | + self.assertTrue(async_dst.is_pinned()) | ||
| 377 | + self.assertTrue(sync_dst.is_pinned()) | ||
| 378 | + | ||
| 379 | + | ||
| 380 | + def test_copy_mixed_dtype_non_blocking_matches_blocking_for_layout_variants(self): | ||
| 381 | + layout_cases = [ | ||
| 382 | + { | ||
| 383 | + "name": "h2d_non_contiguous_dst", | ||
| 384 | + "src_dtype": torch.float32, | ||
| 385 | + "dst_dtype": torch.float16, | ||
| 386 | + "make_src": lambda: torch.tensor( | ||
| 387 | + [-63.5, -7.25, -0.5, 0.0, 0.5, 3.25, 17.75, 63.5, -19.5], | ||
| 388 | + dtype=torch.float32, | ||
| 389 | + ).reshape(3, 3).pin_memory(), | ||
| 390 | + "make_async_dst": lambda: torch.full((3, 6), -99.0, dtype=torch.float16, device=self.device), | ||
| 391 | + "make_sync_dst": lambda: torch.full((3, 6), -99.0, dtype=torch.float16, device=self.device), | ||
| 392 | + "select_dst_view": lambda base: base[:, 1::2], | ||
| 393 | + "expected": lambda src, dst: src.to(dtype=dst.dtype), | ||
| 394 | + }, | ||
| 395 | + { | ||
| 396 | + "name": "d2h_non_contiguous_dst", | ||
| 397 | + "src_dtype": torch.int32, | ||
| 398 | + "dst_dtype": torch.float32, | ||
| 399 | + "make_src": lambda: torch.arange(9, dtype=torch.int32, device=self.device).reshape(3, 3) - 4, | ||
| 400 | + "make_async_dst": lambda: torch.full((3, 6), -99.0, dtype=torch.float32).pin_memory(), | ||
| 401 | + "make_sync_dst": lambda: torch.full((3, 6), -99.0, dtype=torch.float32).pin_memory(), | ||
| 402 | + "select_dst_view": lambda base: base[:, 1::2], | ||
| 403 | + "expected": lambda src, dst: src.cpu().to(dtype=dst.dtype), | ||
| 404 | + }, | ||
| 405 | + { | ||
| 406 | + "name": "h2d_broadcast_src", | ||
| 407 | + "src_dtype": torch.int16, | ||
| 408 | + "dst_dtype": torch.float32, | ||
| 409 | + "make_src": lambda: torch.tensor([-32768, -17, 9, 32767], dtype=torch.int16).reshape(1, 4).pin_memory(), | ||
| 410 | + "make_async_dst": lambda: torch.empty(3, 4, dtype=torch.float32, device=self.device), | ||
| 411 | + "make_sync_dst": lambda: torch.empty(3, 4, dtype=torch.float32, device=self.device), | ||
| 412 | + "select_dst_view": lambda base: base, | ||
| 413 | + "expected": lambda src, dst: src.to(dtype=dst.dtype).expand_as(dst), | ||
| 414 | + }, | ||
| 415 | + { | ||
| 416 | + "name": "d2h_non_contiguous_src", | ||
| 417 | + "src_dtype": torch.float16, | ||
| 418 | + "dst_dtype": torch.float32, | ||
| 419 | + "make_src": lambda: torch.tensor( | ||
| 420 | + [-7.5, -1.25, 0.0, 1.25, 3.5, 7.75, 15.5, 31.0], | ||
| 421 | + dtype=torch.float16, | ||
| 422 | + device=self.device, | ||
| 423 | + ).reshape(4, 2).t(), | ||
| 424 | + "make_async_dst": lambda: torch.empty(2, 4, dtype=torch.float32, pin_memory=True), | ||
| 425 | + "make_sync_dst": lambda: torch.empty(2, 4, dtype=torch.float32, pin_memory=True), | ||
| 426 | + "select_dst_view": lambda base: base, | ||
| 427 | + "expected": lambda src, dst: src.cpu().to(dtype=dst.dtype), | ||
| 428 | + }, | ||
| 429 | + ] | ||
| 430 | + | ||
| 431 | + for case in layout_cases: | ||
| 432 | + with self.subTest(case=case["name"], src_dtype=case["src_dtype"], dst_dtype=case["dst_dtype"]): | ||
| 433 | + src = case["make_src"]() | ||
| 434 | + async_base = case["make_async_dst"]() | ||
| 435 | + sync_base = case["make_sync_dst"]() | ||
| 436 | + async_dst = case["select_dst_view"](async_base) | ||
| 437 | + sync_dst = case["select_dst_view"](sync_base) | ||
| 438 | + | ||
| 439 | + self._assert_non_blocking_matches_blocking( | ||
| 440 | + async_dst, | ||
| 441 | + sync_dst, | ||
| 442 | + src, | ||
| 443 | + async_base=async_base, | ||
| 444 | + sync_base=sync_base, | ||
| 445 | + ) | ||
| 446 | + | ||
| 447 | + self.assertEqual( | ||
| 448 | + self._to_cpu_if_needed(async_dst), | ||
| 449 | + case["expected"](src, async_dst), | ||
| 450 | + ) | ||
| 451 | + | ||
| 452 | + | ||
| 453 | + def test_copy_dtype_cast_non_blocking_temporary_lifetime(self): | ||
| 454 | + h2d_dst = torch.empty(2, 4, dtype=torch.float32, device=self.device) | ||
| 455 | + d2h_dst = torch.empty(2, 4, dtype=torch.float32, pin_memory=True) | ||
| 456 | + expected = None | ||
| 457 | + | ||
| 458 | + for i in range(32): | ||
| 459 | + host_src = self._make_host_source(torch.int32) + i | ||
| 460 | + h2d_src = host_src.pin_memory() | ||
| 461 | + h2d_dst.copy_(h2d_src, non_blocking=True) | ||
| 462 | + d2h_src = host_src.to(self.device) | ||
| 463 | + d2h_dst.copy_(d2h_src, non_blocking=True) | ||
| 464 | + expected = host_src.to(dtype=d2h_dst.dtype) | ||
| 465 | + | ||
| 466 | + torch_npu.npu.synchronize() | ||
| 467 | + self.assertEqual(h2d_dst.cpu(), h2d_src.to(dtype=h2d_dst.dtype)) | ||
| 468 | + self.assertEqual(d2h_dst, expected) | ||
| 469 | + | ||
| 470 | + | ||
| 471 | + def test_copy_npu_from_cpu_src_dtype_cast_blocking(self): | ||
| 472 | + for src_dtype, dst_dtype in self.dtype_cast_pairs: | ||
| 473 | + dst = torch.empty(2, 4, dtype=dst_dtype, device=self.device) | ||
| 474 | + src = self._make_host_source(src_dtype) | ||
| 475 | + ret = dst.copy_(src, non_blocking=False) | ||
| 476 | + self.assertIs(ret, dst) | ||
| 477 | + self._assert_copy_matches_cast(dst, src) | ||
| 478 | + | ||
| 479 | + | ||
| 480 | + def test_copy_cpu_from_npu_src_dtype_cast_blocking(self): | ||
| 481 | + for src_dtype, dst_dtype in self.dtype_cast_pairs: | ||
| 482 | + dst = torch.empty(2, 4, dtype=dst_dtype) | ||
| 483 | + src = self._make_device_source(src_dtype) | ||
| 484 | + ret = dst.copy_(src, non_blocking=False) | ||
| 485 | + self.assertIs(ret, dst) | ||
| 486 | + self._assert_copy_matches_cast(dst, src) | ||
| 487 | + | ||
| 90 | def test_copy_npu_src_int_dtype_cast(self): | 488 | def test_copy_npu_src_int_dtype_cast(self): |
| 91 | dst = torch.empty(2, 2, dtype=torch.float32, device=self.device) | 489 | dst = torch.empty(2, 2, dtype=torch.float32, device=self.device) |
| 92 | src = torch.ones(2, 2, dtype=torch.int32, device=self.device) | 490 | src = torch.ones(2, 2, dtype=torch.int32, device=self.device) |
| @@ -17,10 +17,14 @@ | |||
| 17 | 17 | ||
| 18 | 18 | ||
| 19 | 19 | ||
| 20 | + | ||
| 20 | 21 | ||
| 22 | + | ||
| 21 | 23 | ||
| 24 | + | ||
| 22 | 25 | ||
| 23 | 26 | ||
| 27 | + | ||
| 24 | 28 | ||
| 25 | 29 | ||
| 26 | 30 | ||
| @@ -31,6 +35,36 @@ | |||
| 31 | namespace at_npu { | 35 | namespace at_npu { |
| 32 | namespace native { | 36 | namespace native { |
| 33 | 37 | ||
| 38 | +namespace { | ||
| 39 | + | ||
| 40 | +bool is_aclnn_cast_unsupported_dtype(const at::Tensor& tensor) | ||
| 41 | +{ | ||
| 42 | + // On A2-and-later products, aclnnCast rejects these dtype families. | ||
| 43 | + aclDataType dtype = c10_npu::GetAclDataType(static_cast<int64_t>(tensor.scalar_type())); | ||
| 44 | + return dtype == aclDataType::ACL_COMPLEX32 || | ||
| 45 | + dtype == aclDataType::ACL_HIFLOAT8 || | ||
| 46 | + dtype == aclDataType::ACL_FLOAT8_E5M2 || | ||
| 47 | + dtype == aclDataType::ACL_FLOAT8_E4M3FN || | ||
| 48 | + dtype == aclDataType::ACL_FLOAT4_E2M1 || | ||
| 49 | + dtype == aclDataType::ACL_FLOAT4_E1M2 || | ||
| 50 | + dtype == aclDataType::ACL_INT4; | ||
| 51 | +} | ||
| 52 | + | ||
| 53 | +bool should_fallback_to_cpu_cast(const at::Tensor& dst, const at::Tensor& src) | ||
| 54 | +{ | ||
| 55 | + const auto soc = c10_npu::GetSocVersion(); | ||
| 56 | + const bool is_a2_or_later = | ||
| 57 | + ((soc >= c10_npu::SocVersion::Ascend910B1 && soc < c10_npu::SocVersion::Ascend310B1) || | ||
| 58 | + (soc >= c10_npu::SocVersion::Ascend910_9391)); | ||
| 59 | + if (!is_a2_or_later) { | ||
| 60 | + return false; | ||
| 61 | + } | ||
| 62 | + return is_aclnn_cast_unsupported_dtype(src) || | ||
| 63 | + is_aclnn_cast_unsupported_dtype(dst); | ||
| 64 | +} | ||
| 65 | + | ||
| 66 | +} // namespace | ||
| 67 | + | ||
| 34 | // the format of dst and src is base format now | 68 | // the format of dst and src is base format now |
| 35 | // the dtype of dst and src is same | 69 | // the dtype of dst and src is same |
| 36 | // and src and dst are contiguous | 70 | // and src and dst are contiguous |
| @@ -111,6 +145,14 @@ void copy_d2h_baseformat_dtype_contigous_opapi(at::Tensor& dst, const at::Tensor | |||
| 111 | copy_between_host_and_device_opapi(dst, src, kind, non_blocking); | 145 | copy_between_host_and_device_opapi(dst, src, kind, non_blocking); |
| 112 | } | 146 | } |
| 113 | 147 | ||
| 148 | +void cast_dtype_out_baseformat_opapi(at::Tensor& dst, const at::Tensor& src) | ||
| 149 | +{ | ||
| 150 | + TORCH_INTERNAL_ASSERT(dst.sizes().equals(src.sizes()), OPS_ERROR(ErrCode::VALUE)); | ||
| 151 | + TORCH_INTERNAL_ASSERT(dst.device() == src.device(), OPS_ERROR(ErrCode::VALUE)); | ||
| 152 | + auto dst_scalar_type = dst.scalar_type(); | ||
| 153 | + EXEC_NPU_CMD(aclnnCast, src, dst_scalar_type, dst); | ||
| 154 | +} | ||
| 155 | + | ||
| 114 | // the format of dst and src is baseformat now | 156 | // the format of dst and src is baseformat now |
| 115 | void copy_h2d_baseformat_opapi(at::Tensor& dst, const at::Tensor& src, bool non_blocking, | 157 | void copy_h2d_baseformat_opapi(at::Tensor& dst, const at::Tensor& src, bool non_blocking, |
| 116 | bool dst_must_be_contiguous = false) | 158 | bool dst_must_be_contiguous = false) |
| @@ -124,12 +166,16 @@ void copy_h2d_baseformat_opapi(at::Tensor& dst, const at::Tensor& src, bool non_ | |||
| 124 | return; | 166 | return; |
| 125 | } | 167 | } |
| 126 | 168 | ||
| 127 | - at::Tensor dst_contig = dst_is_contiguous ? dst : at::empty_like(dst, LEGACY_CONTIGUOUS_MEMORY_FORMAT); | 169 | + at::Tensor dst_contig; |
| 128 | at::Tensor src_contig; | 170 | at::Tensor src_contig; |
| 129 | - if (!same_type) { | 171 | + if (!same_type && non_blocking && !should_fallback_to_cpu_cast(dst, src)) { |
| 130 | - src_contig = src.to(dst.dtype()).expand_as(dst).contiguous(); | 172 | + // keep the H2D leg same-dtype, then cast on device. |
| 131 | - } else { | 173 | + dst_contig = at::empty_like(dst, src.dtype(), LEGACY_CONTIGUOUS_MEMORY_FORMAT); |
| 132 | src_contig = src.expand_as(dst).contiguous(); | 174 | src_contig = src.expand_as(dst).contiguous(); |
| 175 | + } else { | ||
| 176 | + dst_contig = dst_is_contiguous ? dst : at::empty_like(dst, LEGACY_CONTIGUOUS_MEMORY_FORMAT); | ||
| 177 | + src_contig = !same_type ? src.to(dst.dtype()).expand_as(dst).contiguous() | ||
| 178 | + : src.expand_as(dst).contiguous(); | ||
| 133 | } | 179 | } |
| 134 | // perform a same-dtype copy on contiguous tensors | 180 | // perform a same-dtype copy on contiguous tensors |
| 135 | TORCH_INTERNAL_ASSERT(dst_contig.sizes().equals(src_contig.sizes()), OPS_ERROR(ErrCode::VALUE)); | 181 | TORCH_INTERNAL_ASSERT(dst_contig.sizes().equals(src_contig.sizes()), OPS_ERROR(ErrCode::VALUE)); |
| @@ -138,7 +184,11 @@ void copy_h2d_baseformat_opapi(at::Tensor& dst, const at::Tensor& src, bool non_ | |||
| 138 | // if necessary, copy back into dst | 184 | // if necessary, copy back into dst |
| 139 | if (!dst_contig.is_same(dst)) { | 185 | if (!dst_contig.is_same(dst)) { |
| 140 | TORCH_INTERNAL_ASSERT(dst_contig.device() == dst.device(), OPS_ERROR(ErrCode::VALUE)); | 186 | TORCH_INTERNAL_ASSERT(dst_contig.device() == dst.device(), OPS_ERROR(ErrCode::VALUE)); |
| 141 | - copy_d2d_baseformat_opapi(dst, dst_contig, non_blocking); | 187 | + if (dst_contig.scalar_type() == dst.scalar_type()) { |
| 188 | + copy_d2d_baseformat_opapi(dst, dst_contig, non_blocking); | ||
| 189 | + } else { | ||
| 190 | + cast_dtype_out_baseformat_opapi(dst, dst_contig); | ||
🔵 Low Priority 在 建议:建议在 ![]() ![]() 原来的语义是,当dst非连续时,将dst_contig转回非连续;修改后,只有判断数据类型相同,才转回非连续;当数据类型不同,且dst是非连续的时候,输出只做了cast操作,缺少转回连续的操作 ![]() ![]() chengpeng25 6月27日 评论: 6月27日 评论: zzhongmin 6月29日 评论: 6月29日 评论: | |||
| 191 | + } | ||
| 142 | } | 192 | } |
| 143 | } | 193 | } |
| 144 | 194 | ||
| @@ -153,8 +203,17 @@ void copy_d2h_baseformat_opapi(at::Tensor& dst, const at::Tensor& src, bool non_ | |||
| 153 | copy_d2h_baseformat_dtype_contigous_opapi(dst, src, non_blocking); | 203 | copy_d2h_baseformat_dtype_contigous_opapi(dst, src, non_blocking); |
| 154 | return; | 204 | return; |
| 155 | } | 205 | } |
| 156 | - at::Tensor dst_contig = (dst_is_contiguous && same_type) ? dst : at::empty_like(dst, src.dtype(), LEGACY_CONTIGUOUS_MEMORY_FORMAT); | 206 | + at::Tensor dst_contig; |
| 157 | - at::Tensor src_contig = src.expand_as(dst).contiguous(); | 207 | + at::Tensor src_contig; |
| 208 | + if (!same_type && non_blocking && !should_fallback_to_cpu_cast(dst, src)) { | ||
| 209 | + // cast on device before the D2H leg. | ||
| 210 | + dst_contig = dst_is_contiguous ? dst : at::empty_like(dst, LEGACY_CONTIGUOUS_MEMORY_FORMAT); | ||
| 211 | + at::Tensor src_cast_input = NpuUtils::check_match(&src) ? src : NpuUtils::format_contiguous(src); | ||
| 212 | + src_contig = custom_ops::_npu_dtype_cast(src_cast_input, dst.scalar_type()).expand_as(dst).contiguous(); | ||
| 213 | + } else { | ||
| 214 | + dst_contig = (dst_is_contiguous && same_type) ? dst : at::empty_like(dst, src.dtype(), LEGACY_CONTIGUOUS_MEMORY_FORMAT); | ||
| 215 | + src_contig = src.expand_as(dst).contiguous(); | ||
| 216 | + } | ||
| 158 | // perform a same-dtype copy on contiguous tensors | 217 | // perform a same-dtype copy on contiguous tensors |
| 159 | TORCH_INTERNAL_ASSERT(dst_contig.sizes().equals(src_contig.sizes()), OPS_ERROR(ErrCode::VALUE)); | 218 | TORCH_INTERNAL_ASSERT(dst_contig.sizes().equals(src_contig.sizes()), OPS_ERROR(ErrCode::VALUE)); |
| 160 | TORCH_INTERNAL_ASSERT(dst_contig.scalar_type() == src_contig.scalar_type(), OPS_ERROR(ErrCode::VALUE)); | 219 | TORCH_INTERNAL_ASSERT(dst_contig.scalar_type() == src_contig.scalar_type(), OPS_ERROR(ErrCode::VALUE)); |


🔵 Low Priority
上一次审查指出:
cast_dtype_out_baseformat_opapi(CopyKernelOpApi.cpp 第116行) 在copy_h2d_baseformat_opapi的非阻塞路径中被调用(第167行cast_dtype_out_baseformat_opapi(dst, dst_contig);),但函数签名不接受bool non_blocking参数。当前 diff 中该函数仍为
void cast_dtype_out_baseformat_opapi(at::Tensor& dst, const at::Tensor& src),未添加non_blocking参数。由于该函数内部通过EXEC_NPU_CMD(aclnnCast, ...)执行 ACL 算子,ACL 算子本身是流序异步的,因此当前调用路径(仅在non_blocking=true时进入)在功能上是正确的,不会导致同步/异步行为错误。但若未来有其他调用方从阻塞路径调用此函数,缺少
non_blocking参数可能导致行为不符合预期。这是一个接口一致性和可维护性的改进建议,非必须修复项。建议:可选改进:为
cast_dtype_out_baseformat_opapi添加bool non_blocking参数以保持接口一致性。即使当前不消费该参数,也能降低未来维护风险。若确定该函数仅用于非阻塞路径,也可在注释中明确说明。