已合并
feat: adapt FRACTAL_NZ D2H/printing and serialization UT for Ascend950 #44315
feat: adapt FRACTAL_NZ D2H/printing and serialization UT for Ascend950 #44315
已合并
wuyouqi1创建于 9 天前
8 个文件变更+254-3
Mtest/npu/test_serialization.py+5-0
@@ -114,6 +114,11 @@ class TestSerialization(TestCase):
114 114 
115 with tempfile.TemporaryDirectory() as tmpdir:115 with tempfile.TemporaryDirectory() as tmpdir:
116 path = os.path.join(tmpdir, "data.pt")116 path = os.path.join(tmpdir, "data.pt")
117+ if x.is_npu:
118+ # legacy save of NPU storage reads an invalid NPUStorageDesc; expect a clear error.
119+ with self.assertRaisesRegex(RuntimeError, "NPUStorageImpl"):
120+ torch.serialization.save(x, path, _use_new_zipfile_serialization=False)
121+ return
117 torch.serialization.save(x, path, _use_new_zipfile_serialization=False)122 torch.serialization.save(x, path, _use_new_zipfile_serialization=False)
118 y = torch.load(123 y = torch.load(
119 path,124 path,
Mtest/npu/test_serialization_format.py+178-0
@@ -1,5 +1,6 @@
1import os1import os
2import tempfile2import tempfile
3+import unittest
3 4 
4import torch5import torch
5from torch.testing._internal.common_utils import TestCase, run_tests6from torch.testing._internal.common_utils import TestCase, run_tests
@@ -14,6 +15,8 @@ import torch_npu
14# to test this case.15# to test this case.
15 16 
16 17 
18+IS_ASCEND950 = torch_npu._C._npu_get_soc_version() >= 260 # Ascend950 = 260
19+ 
17FORMAT_INFO = {20FORMAT_INFO = {
18 "NCHW": 0,21 "NCHW": 0,
19 "NHWC": 1,22 "NHWC": 1,
@@ -23,6 +26,26 @@ FORMAT_INFO = {
23 "FRACTAL_NZ": 29,26 "FRACTAL_NZ": 29,
24 }27 }
25 28 
29+NZ_ROUNDTRIP_DTYPES = [
30+ (torch.float16, "fp16"),
31+ (torch.bfloat16, "bf16"),
32+ (torch.int8, "i8"),
33+ (torch.int32, "i32"),
34+ (torch.int64, "i64"),
35+]
36+D2H_TEST_DTYPES = [torch.float16, torch.bfloat16, torch.int8, torch.int32]
37+ 
38+# Ascend950 materializes a FRACTAL_NZ cast as an NZ_C0 variant (50-54) since C0
39+# variants are output-only; accept the NZ family when checking the cast result.
40+NZ_FORMAT_FAMILY = (
41+ FORMAT_INFO["FRACTAL_NZ"],
42+ torch_npu.Format.FRACTAL_NZ_C0_16,
43+ torch_npu.Format.FRACTAL_NZ_C0_32,
44+ torch_npu.Format.FRACTAL_NZ_C0_2,
45+ torch_npu.Format.FRACTAL_NZ_C0_4,
46+ torch_npu.Format.FRACTAL_NZ_C0_8,
47+)
48+ 
26 49 
27def save_tensor(tensor, path, acl_format):50def save_tensor(tensor, path, acl_format):
28 x = torch_npu.npu_format_cast(tensor.npu(), acl_format)51 x = torch_npu.npu_format_cast(tensor.npu(), acl_format)
@@ -36,6 +59,8 @@ def load_tensor(tensor, path):
36 raise ValueError("load tensor not equal to save tensor.")59 raise ValueError("load tensor not equal to save tensor.")
37 60 
38 61 
62+@unittest.skipIf(IS_ASCEND950,
63+ "Ascend950 uses aclnn-only path; see TestSerializationFormatAscend950")
39class TestSerializationFormat(TestCase):64class TestSerializationFormat(TestCase):
40 def test_save_load_format(self):65 def test_save_load_format(self):
41 with tempfile.TemporaryDirectory() as tmpdir:66 with tempfile.TemporaryDirectory() as tmpdir:
@@ -64,5 +89,158 @@ class TestSerializationFormat(TestCase):
64 self.assertEqual(process_load.exitcode, 0)89 self.assertEqual(process_load.exitcode, 0)
65 90 
66 91 
92+@unittest.skipUnless(IS_ASCEND950, "Ascend950 only")
93+class TestSerializationFormatAscend950(TestCase):
94+ 
95+ def test_save_load_nd_format(self):
96+ with tempfile.TemporaryDirectory() as tmpdir:
97+ path = os.path.join(tmpdir, 'data.pt')
98+ tensor = torch.rand(64, 3, 7, 7)
99+ 
100+ proc = torch.multiprocessing.get_context("spawn").Process
101+ 
102+ process_save = proc(target=save_tensor, name="save",
103+ args=(tensor, path, 2))
104+ process_save.start()
105+ process_save.join()
106+ self.assertEqual(process_save.exitcode, 0)
107+ 
108+ process_load = proc(target=load_tensor, name="load",
109+ args=(tensor, path))
110+ process_load.start()
111+ process_load.join()
112+ self.assertEqual(process_load.exitcode, 0)
113+ 
114+ def test_save_load_nz_round_trip_by_dtype(self):
115+ """Per-dtype FRACTAL_NZ save/load round-trip."""
116+ for dt, name in NZ_ROUNDTRIP_DTYPES:
117+ try:
118+ x = torch.randn(64, 64, dtype=torch.float32).to(dt).npu()
119+ x = torch_npu.npu_format_cast(x, torch_npu.Format.FRACTAL_NZ)
120+ except Exception:
121+ if dt == torch.int64:
122+ continue # i64: not in CANN WEIGHT_DTYPE_SUPPORT_LIST
123+ raise
124+ 
125+ fmt_before = torch_npu.get_npu_format(x)
126+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pt") as f:
127+ path = f.name
128+ try:
129+ torch.save(x, path)
130+ y = torch.load(path)
131+ fmt_after = torch_npu.get_npu_format(y)
132+ 
133+ self.assertEqual(fmt_before, fmt_after,
134+ f"{name}: format {fmt_before} != {fmt_after}")
135+ self.assertTrue(torch.equal(x.cpu(), y.cpu()),
136+ f"{name}: value mismatch")
137+ finally:
138+ os.unlink(path)
139+ 
140+ def test_nz_d2h_and_repr(self):
141+ """D2H, repr, str, print on private-format tensor must not crash."""
142+ for dt in D2H_TEST_DTYPES:
143+ x = torch.randn(64, 64, dtype=torch.float32).to(dt).npu()
144+ x = torch_npu.npu_format_cast(x, torch_npu.Format.FRACTAL_NZ)
145+ 
146+ c = x.cpu()
147+ self.assertEqual(c.device.type, "cpu")
148+ 
149+ self.assertIsInstance(repr(x), str)
150+ print(x)
151+ 
152+ 
153+def cast_copy_tensor(tensor, acl_format):
154+ # Without allow_internal_format the cast silently downgrades internal formats to ND.
155+ if acl_format != FORMAT_INFO["ND"]:
156+ torch_npu.npu.config.allow_internal_format = True
157+ return torch_npu.npu_format_cast(tensor, acl_format)
158+ 
159+ 
160+@unittest.skipIf(IS_ASCEND950,
161+ "Ascend950 copy behavior differs; see TestCopyFormatAscend950")
162+class TestCopyFormat(TestCase):
163+ """A2/A3: copy_ across all FORMAT_INFO formats in H2D/D2H/D2D directions."""
164+ 
165+ def test_copy_formats_h2d_d2h_d2d(self):
166+ for fmt_name, fmt in FORMAT_INFO.items():
167+ src_cpu = torch.randn(2, 3, 7, 7)
168+ 
169+ # h2d: NPU dst (fmt) <- CPU src
170+ dst_h2d = cast_copy_tensor(torch.zeros(2, 3, 7, 7).npu(), fmt)
171+ dst_h2d.copy_(src_cpu)
172+ self.assertTrue(torch.equal(dst_h2d.cpu(), src_cpu),
173+ f"h2d dst={fmt_name}")
174+ 
175+ # d2h: CPU dst <- NPU src (fmt)
176+ src_d2h = cast_copy_tensor(src_cpu.npu(), fmt)
177+ dst_d2h = torch.zeros(2, 3, 7, 7)
178+ dst_d2h.copy_(src_d2h)
179+ self.assertTrue(torch.equal(dst_d2h, src_cpu),
180+ f"d2h src={fmt_name}")
181+ 
182+ # d2d: dst (fmt) <- src (other format)
183+ for other_name, other in FORMAT_INFO.items():
184+ dst_d2d = cast_copy_tensor(torch.zeros(2, 3, 7, 7).npu(), fmt)
185+ fmt_before = torch_npu.get_npu_format(dst_d2d)
186+ src_d2d = cast_copy_tensor(src_cpu.npu(), other)
187+ dst_d2d.copy_(src_d2d)
188+ self.assertTrue(torch.equal(dst_d2d.cpu(), src_cpu),
189+ f"d2d dst={fmt_name} src={other_name}")
190+ self.assertEqual(torch_npu.get_npu_format(dst_d2d), fmt_before,
191+ f"d2d dst={fmt_name} format changed")
192+ 
193+ 
194+@unittest.skipUnless(IS_ASCEND950, "Ascend950 only")
195+class TestCopyFormatAscend950(TestCase):
196+ """Ascend950: internal-format copy_ is only supported device-to-host."""
197+ 
198+ COPY_TEST_DTYPE = torch.float16
199+ 
200+ def test_copy_base_format_h2d_d2h_d2d(self):
201+ src_cpu = torch.randn(8, 8, dtype=self.COPY_TEST_DTYPE)
202+ dst_h2d = torch.zeros(8, 8, dtype=self.COPY_TEST_DTYPE).npu()
203+ dst_h2d.copy_(src_cpu)
204+ self.assertTrue(torch.equal(dst_h2d.cpu(), src_cpu))
205+ 
206+ src_d2h = src_cpu.npu()
207+ dst_d2h = torch.zeros(8, 8, dtype=self.COPY_TEST_DTYPE)
208+ dst_d2h.copy_(src_d2h)
209+ self.assertTrue(torch.equal(dst_d2h, src_cpu))
210+ 
211+ dst_d2d = torch.zeros(8, 8, dtype=self.COPY_TEST_DTYPE).npu()
212+ dst_d2d.copy_(src_d2h)
213+ self.assertTrue(torch.equal(dst_d2d.cpu(), src_cpu))
214+ 
215+ def test_copy_nz_d2h(self):
216+ for dt in D2H_TEST_DTYPES:
217+ src_cpu = torch.randn(8, 8).to(dt)
218+ src_nz = cast_copy_tensor(src_cpu.npu(), FORMAT_INFO["FRACTAL_NZ"])
219+ self.assertIn(torch_npu.get_npu_format(src_nz), NZ_FORMAT_FAMILY,
220+ f"expected FRACTAL_NZ family, got {torch_npu.get_npu_format(src_nz)}")
221+ 
222+ dst_cpu = torch.zeros(8, 8, dtype=dt)
223+ dst_cpu.copy_(src_nz)
224+ self.assertTrue(torch.equal(dst_cpu, src_cpu), f"d2h {dt}")
225+ 
226+ def test_copy_nz_h2d_not_supported(self):
227+ dst_nz = cast_copy_tensor(torch.zeros(8, 8, dtype=self.COPY_TEST_DTYPE).npu(),
228+ FORMAT_INFO["FRACTAL_NZ"])
229+ with self.assertRaisesRegex(RuntimeError, "not supported on Ascend950"):
230+ dst_nz.copy_(torch.randn(8, 8, dtype=self.COPY_TEST_DTYPE))
231+ 
232+ def test_copy_nz_d2d_not_supported(self):
233+ src_nz = cast_copy_tensor(torch.randn(8, 8, dtype=self.COPY_TEST_DTYPE).npu(),
234+ FORMAT_INFO["FRACTAL_NZ"])
235+ dst_nz = cast_copy_tensor(torch.zeros(8, 8, dtype=self.COPY_TEST_DTYPE).npu(),
236+ FORMAT_INFO["FRACTAL_NZ"])
237+ with self.assertRaisesRegex(RuntimeError, "not supported on Ascend950"):
238+ dst_nz.copy_(src_nz)
239+ 
240+ dst_nd = torch.zeros(8, 8, dtype=self.COPY_TEST_DTYPE).npu()
241+ with self.assertRaisesRegex(RuntimeError, "not supported on Ascend950"):
242+ dst_nd.copy_(src_nz)
243+ 
244+ 
67if __name__ == "__main__":245if __name__ == "__main__":
68 run_tests()246 run_tests()
Mtorch_npu/csrc/aten/common/FormatCastKernelNpu.cpp+16-1
@@ -61,6 +61,15 @@ static bool ShouldFallbackNzToNd(const at::Tensor& self, int64_t acl_format)
61 return false;61 return false;
62}62}
63 63 
64+// CANN only accepts FRACTAL_NZ(29) or ND(2) as dstFormat; NZ_C0 variants (50-54)
65+// are output-only (returned as actualFormat). Normalize here for the load path.
66+static bool IsNzC0Variant(int64_t fmt)
67+{
68+ return fmt == ACL_FORMAT_FRACTAL_NZ_C0_16 || fmt == ACL_FORMAT_FRACTAL_NZ_C0_32 ||
69+ fmt == ACL_FORMAT_FRACTAL_NZ_C0_2 || fmt == ACL_FORMAT_FRACTAL_NZ_C0_4 ||
70+ fmt == ACL_FORMAT_FRACTAL_NZ_C0_8;
71+}
72+ 
64std::tuple<bool, int64_t, c10::SmallVector<int64_t, SIZE>> MaybeUseAclnnNpuFormatCast(const at::Tensor& src,73std::tuple<bool, int64_t, c10::SmallVector<int64_t, SIZE>> MaybeUseAclnnNpuFormatCast(const at::Tensor& src,
65 int64_t acl_format, c10::optional<int64_t> customize_dtype, c10::optional<int64_t> input_dtype)74 int64_t acl_format, c10::optional<int64_t> customize_dtype, c10::optional<int64_t> input_dtype)
66{75{
@@ -80,9 +89,15 @@ std::tuple<bool, int64_t, c10::SmallVector<int64_t, SIZE>> MaybeUseAclnnNpuForma
80 c10_npu::GetAclDataType(customize_dtype.value()) : srcAcltype;89 c10_npu::GetAclDataType(customize_dtype.value()) : srcAcltype;
81 TensorWrapper srcWrapper = make_wrapper(src, input_dtype);90 TensorWrapper srcWrapper = make_wrapper(src, input_dtype);
82 if (c10_npu::IsAclnnOnly()) {91 if (c10_npu::IsAclnnOnly()) {
92+ // >=4-byte types (fp32/int32) need FLOAT16 additionalDtype for C0=16.
93+ if (!customize_dtype.has_value() && src.element_size() >= 4) {
94+ customizeAcltype = aclDataType::ACL_FLOAT16;
95+ }
83 if (aclnnNpuFormatCastExist) {96 if (aclnnNpuFormatCastExist) {
97+ // Normalize NZ_C0 -> FRACTAL_NZ(29); C0 variants are output-only.
98+ int64_t cann_dst_format = IsNzC0Variant(acl_format) ? ACL_FORMAT_FRACTAL_NZ : acl_format;
84 auto acl_src = ConvertType(srcWrapper);99 auto acl_src = ConvertType(srcWrapper);
85- auto api_ret = GetFormat(acl_src, acl_format, customizeAcltype, &dstStorageShape,100+ auto api_ret = GetFormat(acl_src, cann_dst_format, customizeAcltype, &dstStorageShape,
86 &dstShapeSize, &dstFormat);101 &dstShapeSize, &dstFormat);
87 Release(acl_src);102 Release(acl_src);
88 NPU_CHECK_ERROR(api_ret, "aclnnNpuFormatCastCalculateSizeAndFormat");103 NPU_CHECK_ERROR(api_ret, "aclnnNpuFormatCastCalculateSizeAndFormat");
Mtorch_npu/csrc/aten/npu_native_functions.yaml+1-0
@@ -16,6 +16,7 @@ supported:
16 - contiguous16 - contiguous
17 - func: copy_17 - func: copy_
18 op_api: True18 op_api: True
19+ internal_format_opapi: True
19 - copy_memory_20 - copy_memory_
20 - empty.memory_format21 - empty.memory_format
21 - empty_like22 - empty_like
Mtorch_npu/csrc/aten/ops/op_api/CopyKernelOpApi.cpp+19-1
@@ -27,6 +27,8 @@
27#include "torch_npu/csrc/custom_dtype/Init.h"27#include "torch_npu/csrc/custom_dtype/Init.h"
28#include "torch_npu/csrc/aten/NPUOpApiNativeFunctions.h"28#include "torch_npu/csrc/aten/NPUOpApiNativeFunctions.h"
29#include "torch_npu/csrc/aten/NPUNativeFunctions.h"29#include "torch_npu/csrc/aten/NPUNativeFunctions.h"
30+#include "torch_npu/csrc/framework/FormatHelper.h"
31+#include "torch_npu/csrc/aten/common/FormatCastHelper.h"
30#include "third_party/op-plugin/op_plugin/utils/op_api_common.h"32#include "third_party/op-plugin/op_plugin/utils/op_api_common.h"
31#ifndef BUILD_LIBTORCH33#ifndef BUILD_LIBTORCH
32#include "torch_npu/csrc/sanitizer/NPUTrace.h"34#include "torch_npu/csrc/sanitizer/NPUTrace.h"
@@ -238,6 +240,19 @@ at::Tensor& NPUNativeOpApiFunctions::copy_(at::Tensor& self, const at::Tensor& s
238 240 
239 auto maybe_outnames = at::namedinference::compute_broadcast_outnames(self, src);241 auto maybe_outnames = at::namedinference::compute_broadcast_outnames(self, src);
240 242 
243+ // aclnnInplaceCopy corrupts internal-format storage: on A2/A3 fall back to
244+ // the native copy_, on Ascend950 only d2h is supported (cast to base first).
245+ const bool self_is_base = FormatHelper::IsOpInputBaseFormat(self);
246+ const bool src_is_base = FormatHelper::IsOpInputBaseFormat(src);
247+ if (!self_is_base || !src_is_base) {
248+ if (!c10_npu::IsAclnnOnly()) {
249+ return NPUNativeFunctions::copy_(self, src, non_blocking);
250+ }
251+ TORCH_CHECK(!torch_npu::utils::is_npu(self),
252+ "The copy_ operator with internal format tensors is not supported on Ascend950, "
253+ "only device-to-host copies are supported", OPS_ERROR(ErrCode::NOT_SUPPORT));
254+ }
255+ 
241 if (torch_npu::utils::is_npu(self)) {256 if (torch_npu::utils::is_npu(self)) {
242 if (torch_npu::utils::is_npu(src)) {257 if (torch_npu::utils::is_npu(src)) {
243 copy_d2d_baseformat_opapi(self, src, non_blocking);258 copy_d2d_baseformat_opapi(self, src, non_blocking);
@@ -254,7 +269,10 @@ at::Tensor& NPUNativeOpApiFunctions::copy_(at::Tensor& self, const at::Tensor& s
254 }269 }
255 } else {270 } else {
256 if (torch_npu::utils::is_npu(src)) {271 if (torch_npu::utils::is_npu(src)) {
257- copy_d2h_baseformat_opapi(self, src, non_blocking);272+ // Ascend950: aclnnInplaceCopy rejects internal format. Cast NZ->ND first.
273+ at::Tensor src_base = FormatHelper::IsBaseFormatType(src)
274+ ? src : FormatCastHelper::ApplyBaseFormatTensorBy(src);
275+ copy_d2h_baseformat_opapi(self, src_base, non_blocking);
258 if (src.is_complex() && src.is_conj()) {276 if (src.is_complex() && src.is_conj()) {
259 self.conj_physical_();277 self.conj_physical_();
260 }278 }
Mtorch_npu/csrc/core/NPUBridge.cpp+8-1
@@ -1,3 +1,5 @@
1+#include <typeinfo>
2+ 
1#include <torch_npu/csrc/core/NPUBridge.h>3#include <torch_npu/csrc/core/NPUBridge.h>
2 4 
3 5 
@@ -19,7 +21,12 @@ NPUStorageImpl *NPUBridge::GetNpuStorageImpl(const at::Tensor &tensor)
19 21 
20NPUStorageDesc &NPUBridge::GetNpuStorageImplDesc(const at::Tensor &tensor)22NPUStorageDesc &NPUBridge::GetNpuStorageImplDesc(const at::Tensor &tensor)
21{23{
22- return static_cast<NPUStorageImpl *>(tensor.storage().unsafeGetStorageImpl())->npu_desc_;24+ // from_blob tensors (legacy serialization _write_file) carry a plain
25+ // c10::StorageImpl; reading npu_desc_ on them is out-of-bounds.
26+ auto *storage_impl = tensor.storage().unsafeGetStorageImpl();
27+ TORCH_CHECK(typeid(*storage_impl) == typeid(NPUStorageImpl),
28+ "The npu storage desc is unavailable: the tensor's storage is not an NPUStorageImpl.");
29+ return static_cast<NPUStorageImpl *>(storage_impl)->npu_desc_;
23}30}
24 31 
25 32 
Mtorch_npu/csrc/core/NPUSerialization.cpp+5-0
@@ -13,6 +13,11 @@ std::unordered_map<std::string, aclFormat> FORMAT_INFO = {
13 { "NCHW", ACL_FORMAT_NCHW },13 { "NCHW", ACL_FORMAT_NCHW },
14 { "NHWC", ACL_FORMAT_NHWC },14 { "NHWC", ACL_FORMAT_NHWC },
15 { "FRACTAL_NZ", ACL_FORMAT_FRACTAL_NZ },15 { "FRACTAL_NZ", ACL_FORMAT_FRACTAL_NZ },
16+ { "FRACTAL_NZ_C0_16", ACL_FORMAT_FRACTAL_NZ_C0_16 },
17+ { "FRACTAL_NZ_C0_32", ACL_FORMAT_FRACTAL_NZ_C0_32 },
18+ { "FRACTAL_NZ_C0_2", ACL_FORMAT_FRACTAL_NZ_C0_2 },
19+ { "FRACTAL_NZ_C0_4", ACL_FORMAT_FRACTAL_NZ_C0_4 },
20+ { "FRACTAL_NZ_C0_8", ACL_FORMAT_FRACTAL_NZ_C0_8 },
16 { "FRACTAL_Z", ACL_FORMAT_FRACTAL_Z },21 { "FRACTAL_Z", ACL_FORMAT_FRACTAL_Z },
17 { "NDHWC", ACL_FORMAT_NDHWC },22 { "NDHWC", ACL_FORMAT_NDHWC },
18 { "NCDHW", ACL_FORMAT_NCDHW },23 { "NCDHW", ACL_FORMAT_NCDHW },
Mtorch_npu/utils/tensor_methods.py+22-0
@@ -81,6 +81,28 @@ def _npu_type(self, dtype=None, non_blocking=False, **kwargs):
81 return self.type_raw(dtype, non_blocking, **kwargs)81 return self.type_raw(dtype, non_blocking, **kwargs)
82 82 
83 83 
84+def _add_repr_patch():
85+ # Private-format tensors (e.g. FRACTAL_NZ) hit internal-format guard in
86+ # _tensor_str cat/stack. Force .cpu() to trigger d2h + format cast first.
87+ _orig_repr = torch.Tensor.__repr__
88+ 
89+ def _npu_private_format_repr(self, *, tensor_contents=None):
90+ if self.device.type == "npu":
91+ try:
92+ is_private_format = (
93+ torch_npu.get_npu_format(self) != int(torch_npu.Format.ND)
94+ )
95+ except Exception:
96+ is_private_format = False
97+ if is_private_format:
98+ with torch.no_grad():
99+ return _orig_repr(self.cpu(), tensor_contents=tensor_contents)
100+ return _orig_repr(self, tensor_contents=tensor_contents)
101+ 
102+ torch.Tensor.__repr__ = _npu_private_format_repr
103+ 
104+ 
84def _add_tensor_methods():105def _add_tensor_methods():
85 torch.Tensor.type_raw = torch.Tensor.type106 torch.Tensor.type_raw = torch.Tensor.type
86 torch.Tensor.type = _npu_type107 torch.Tensor.type = _npu_type
108+ _add_repr_patch()