已合并
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
已合并
zzhongmin创建于 6月18日
3 个文件变更+486-12
Mtest/npu/test_npu.py+19-2
@@ -7,10 +7,9 @@ import torch
7from torch.autograd import Variable7from torch.autograd import Variable
8 8 
9import torch_npu9import torch_npu
10-from torch_npu.testing.common_utils import freeze_rng_state10+from torch_npu.testing.common_utils import SupportedDevices, freeze_rng_state
11from torch_npu.testing.testcase import run_tests, TestCase11from torch_npu.testing.testcase import run_tests, TestCase
12 12 
13- 
14class TestNpu(TestCase):13class TestNpu(TestCase):
15 FIFTY_MIL_CYCLES = 5000000014 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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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()
Mtest/test_copy_.py+401-3
@@ -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"""
24import torch25import torch
25import torch_npu # noqa: F40126import torch_npu # noqa: F401
27+from torch_npu.testing.common_utils import SupportedDevices
26 28 
27try:29try:
28 from torch_npu.testing.testcase import TestCase, run_tests30 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- 
38class TestTensorCopy_(TestCase):39class 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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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+ @SupportedDevices(['Ascend910B', 'Ascend910_93', 'Ascend950'])
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)
Mtorch_npu/csrc/aten/ops/op_api/CopyKernelOpApi.cpp+66-7
@@ -17,10 +17,14 @@
17 17 
18#include "torch_npu/csrc/core/npu/NPUGuard.h"18#include "torch_npu/csrc/core/npu/NPUGuard.h"
19#include "torch_npu/csrc/core/npu/NPUPeerToPeerAccess.h"19#include "torch_npu/csrc/core/npu/NPUPeerToPeerAccess.h"
20+#include "torch_npu/csrc/core/npu/NpuVariables.h"
20#include "torch_npu/csrc/framework/utils/CalcuOpUtil.h"21#include "torch_npu/csrc/framework/utils/CalcuOpUtil.h"
22+#include "torch_npu/csrc/framework/utils/NpuUtils.h"
21#include "torch_npu/csrc/framework/contiguous/ContiguousOpt.h"23#include "torch_npu/csrc/framework/contiguous/ContiguousOpt.h"
24+#include "torch_npu/csrc/aten/CustomFunctions.h"
22#include "torch_npu/csrc/aten/common/InnerNpuNativeFunction.h"25#include "torch_npu/csrc/aten/common/InnerNpuNativeFunction.h"
23#include "torch_npu/csrc/core/npu/CachingHostAllocator.h"26#include "torch_npu/csrc/core/npu/CachingHostAllocator.h"
27+#include "torch_npu/csrc/custom_dtype/Init.h"
24#include "torch_npu/csrc/aten/NPUOpApiNativeFunctions.h"28#include "torch_npu/csrc/aten/NPUOpApiNativeFunctions.h"
25#include "torch_npu/csrc/aten/NPUNativeFunctions.h"29#include "torch_npu/csrc/aten/NPUNativeFunctions.h"
26#include "third_party/op-plugin/op_plugin/utils/op_api_common.h"30#include "third_party/op-plugin/op_plugin/utils/op_api_common.h"
@@ -31,6 +35,36 @@
31namespace at_npu {35namespace at_npu {
32namespace native {36namespace 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 now68// the format of dst and src is base format now
35// the dtype of dst and src is same69// the dtype of dst and src is same
36// and src and dst are contiguous70// 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+}
atomgit-bot
atomgit-botatomgit-bot6月23日

🔵 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 参数以保持接口一致性。即使当前不消费该参数,也能降低未来维护风险。若确定该函数仅用于非阻塞路径,也可在注释中明确说明。

likedislike
155+ 
114// the format of dst and src is baseformat now156// the format of dst and src is baseformat now
115void copy_h2d_baseformat_opapi(at::Tensor& dst, const at::Tensor& src, bool non_blocking,157void 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 tensors180 // 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 dst184 // 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);
atomgit-botC
atomgit-botatomgit-bot6月18日

🔵 Low Priority

copy_h2d_baseformat_opapi(CopyKernelOpApi.cpp 第165行),当 dst_contigdst 的 dtype 不同时,调用 cast_dtype_out_baseformat_opapi(dst, dst_contig) 来执行 device 端 dtype 转换。但该函数签名不接收 non_blocking 参数。与之对应,Non-OpApi 版本(CopyKernel.cpp 第212行)在同样场景下调用 copy_d2d(dst, dst_contig, non_blocking),将 non_blocking 传递给了下级函数。

建议:建议在 cast_dtype_out_baseformat_opapi 函数签名中增加 bool non_blocking 参数(即使当前不消费它),以保持与非 OpApi 路径的接口一致性,降低未来维护风险。

likedislike
Cchengpeng256月27日

原来的语义是,当dst非连续时,将dst_contig转回非连续;修改后,只有判断数据类型相同,才转回非连续;当数据类型不同,且dst是非连续的时候,输出只做了cast操作,缺少转回连续的操作

likedislike
chengpeng25
6月27日 评论:
zzhongmin
zzhongmin
6月29日 评论:
zzhongmin
zzhongmin
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 tensors217 // 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));