已合并
test(jit): add ScriptModule API alignment test cases [v2.10.0] #37635
test(jit): add ScriptModule API alignment test cases [v2.10.0] #37635
已合并
TensorLake创建于 6月4日
1 个文件变更+609-0
Atest/jit/test_script_module.py+609-0
@@ -0,0 +1,609 @@
1+"""
2+Add validation cases for torch.jit.ScriptModule APIs on NPU:
3+1. PyTorch community lacks sufficient direct API validations for
4+ ScriptModule instance methods, so this file is added.
5+2. This file validates 19 torch.jit.ScriptModule APIs using
6+ torch.jit.script() as the canonical creation method:
7+ train, eval, zero_grad, float, double, to, type,
8+ state_dict, save, extra_repr, requires_grad_, to_empty,
9+ xpu, get_buffer, set_submodule, register_module,
10+ register_parameter, share_memory, set_extra_state
11+ (extendable).
12+"""
13+ 
14+import io
15+import os
16+import re
17+import tempfile
18+ 
19+import torch
20+import torch.nn as nn
21+from torch.testing._internal.common_utils import run_tests, TestCase
22+ 
23+ 
24+device_type = acc.type if (acc := torch.accelerator.current_accelerator()) else "cpu"
25+ 
26+ 
27+# ---------------------------------------------------------------------------
28+# Module-level model builders (required by torch.jit.script() source access)
29+# ---------------------------------------------------------------------------
30+ 
31+def _make_linear():
32+ class M(nn.Module):
33+ def __init__(self):
34+ super().__init__()
35+ self.linear = nn.Linear(2, 2)
36+ 
37+ def forward(self, x):
38+ return self.linear(x)
39+ 
40+ return torch.jit.script(M().to(device_type))
41+ 
42+ 
43+def _make_with_buffer():
44+ class M(nn.Module):
45+ def __init__(self):
46+ super().__init__()
47+ self.linear = nn.Linear(2, 2)
48+ self.register_buffer("buf", torch.ones(2, 2))
49+ 
50+ def forward(self, x):
51+ return self.linear(x) + self.buf
52+ 
53+ return torch.jit.script(M().to(device_type))
54+ 
55+ 
56+def _make_nested():
57+ class Sub(nn.Module):
58+ def __init__(self):
59+ super().__init__()
60+ self.linear = nn.Linear(2, 2)
61+ 
62+ def forward(self, x):
63+ return self.linear(x)
64+ 
65+ class Outer(nn.Module):
66+ def __init__(self):
67+ super().__init__()
68+ self.sub = Sub()
69+ 
70+ def forward(self, x):
71+ return self.sub(x)
72+ 
73+ return torch.jit.script(Outer().to(device_type))
74+ 
75+ 
76+def _make_cpu_linear():
77+ class M(nn.Module):
78+ def __init__(self):
79+ super().__init__()
80+ self.linear = nn.Linear(2, 2)
81+ 
82+ def forward(self, x):
83+ return self.linear(x)
84+ 
85+ return torch.jit.script(M())
86+ 
87+ 
88+def _make_nested_with_buffer():
89+ class Sub(nn.Module):
90+ def __init__(self):
91+ super().__init__()
92+ self.register_buffer("buf", torch.ones(2, 2))
93+ 
94+ def forward(self, x):
95+ return x + self.buf
96+ 
97+ class Outer(nn.Module):
98+ def __init__(self):
99+ super().__init__()
100+ self.sub = Sub()
101+ 
102+ def forward(self, x):
103+ return self.sub(x)
104+ 
105+ return torch.jit.script(Outer().to(device_type))
106+ 
107+ 
108+# ===================================================================
109+# Test Classes
110+# ===================================================================
111+ 
112+ 
113+class TestScriptModuleTrainEval(TestCase):
114+ 
115+ def test_train_default_is_training(self):
116+ sm = _make_linear()
117+ self.assertTrue(sm.training)
118+ 
119+ def test_train_set_true_explicit(self):
120+ sm = _make_linear()
121+ sm.train(True)
122+ self.assertTrue(sm.training)
123+ 
124+ def test_train_set_false(self):
125+ sm = _make_linear()
126+ sm.train(False)
127+ self.assertFalse(sm.training)
128+ 
129+ def test_eval_sets_training_false(self):
130+ sm = _make_linear()
131+ sm.eval()
132+ self.assertFalse(sm.training)
133+ 
134+ def test_train_returns_self(self):
135+ sm = _make_linear()
136+ result = sm.train()
137+ self.assertIs(result, sm)
138+ 
139+ def test_eval_returns_self(self):
140+ sm = _make_linear()
141+ result = sm.eval()
142+ self.assertIs(result, sm)
143+ 
144+ def test_train_eval_roundtrip(self):
145+ sm = _make_linear()
146+ sm.train()
147+ self.assertTrue(sm.training)
148+ sm.eval()
149+ self.assertFalse(sm.training)
150+ sm.train(True)
151+ self.assertTrue(sm.training)
152+ 
153+ def test_train_on_npu(self):
154+ sm = _make_linear()
155+ sm.train()
156+ self.assertTrue(sm.training)
157+ self.assertEqual(sm.linear.weight.device.type, device_type)
158+ 
159+ def test_eval_on_npu(self):
160+ sm = _make_linear()
161+ sm.eval()
162+ self.assertFalse(sm.training)
163+ self.assertEqual(sm.linear.weight.device.type, device_type)
164+ 
165+ def test_train_propagates_to_submodules(self):
166+ sm = _make_nested()
167+ sm.train()
168+ self.assertTrue(sm.sub.training)
169+ 
170+ def test_eval_propagates_to_submodules(self):
171+ sm = _make_nested()
172+ sm.eval()
173+ self.assertFalse(sm.sub.training)
174+ 
175+ 
176+class TestScriptModuleZeroGrad(TestCase):
177+ 
178+ def test_zero_grad_no_error(self):
179+ sm = _make_linear()
180+ sm.zero_grad()
181+ 
182+ def test_zero_grad_clears_grads(self):
183+ sm = _make_linear()
184+ x = torch.randn(2, 2, requires_grad=True).to(device_type)
185+ out = sm(x)
186+ out.sum().backward()
187+ self.assertIsNotNone(sm.linear.weight.grad)
188+ sm.zero_grad()
189+ self.assertIsNone(sm.linear.weight.grad)
190+ 
191+ def test_zero_grad_set_to_none(self):
192+ sm = _make_linear()
193+ x = torch.randn(2, 2, requires_grad=True).to(device_type)
194+ out = sm(x)
195+ out.sum().backward()
196+ self.assertIsNotNone(sm.linear.weight.grad)
197+ sm.zero_grad(set_to_none=True)
198+ self.assertIsNone(sm.linear.weight.grad)
199+ 
200+ def test_zero_grad_set_to_none_false(self):
201+ sm = _make_linear()
202+ x = torch.randn(2, 2, requires_grad=True).to(device_type)
203+ out = sm(x)
204+ out.sum().backward()
205+ old_grad = sm.linear.weight.grad
206+ self.assertIsNotNone(old_grad)
207+ sm.zero_grad(set_to_none=False)
208+ self.assertIsNotNone(sm.linear.weight.grad)
209+ self.assertEqual(sm.linear.weight.grad,
210+ torch.zeros_like(old_grad))
211+ 
212+ def test_zero_grad_backward_chain_on_npu(self):
213+ sm = _make_linear()
214+ x = torch.randn(2, 2, requires_grad=True).to(device_type)
215+ out = sm(x)
216+ out.sum().backward()
217+ self.assertIsNotNone(sm.linear.weight.grad)
218+ sm.zero_grad()
219+ self.assertIsNone(sm.linear.weight.grad)
220+ out2 = sm(x)
221+ out2.sum().backward()
222+ self.assertIsNotNone(sm.linear.weight.grad)
223+ 
224+ 
225+class TestScriptModuleTo(TestCase):
226+ 
227+ def test_to_dtype(self):
228+ sm = _make_linear()
229+ sm.to(torch.float64)
230+ self.assertIn(sm.linear.weight.dtype,
231+ (torch.float64, torch.float32))
232+ 
233+ def test_to_device(self):
234+ sm = _make_linear()
235+ sm.to(device_type)
236+ self.assertEqual(sm.linear.weight.device.type, device_type)
237+ 
238+ def test_to_returns_self(self):
239+ sm = _make_linear()
240+ result = sm.to(torch.float32)
241+ self.assertIsInstance(result, torch.jit.ScriptModule)
242+ 
243+ def test_to_device_and_dtype(self):
244+ sm = _make_linear()
245+ sm.to(device_type, torch.float64)
246+ self.assertEqual(sm.linear.weight.device.type, device_type)
247+ self.assertIn(sm.linear.weight.dtype,
248+ (torch.float64, torch.float32))
249+ 
250+ def test_to_dtype_keyword(self):
251+ sm = _make_linear()
252+ sm.to(dtype=torch.float64)
253+ self.assertIn(sm.linear.weight.dtype,
254+ (torch.float64, torch.float32))
255+ 
256+ def test_to_string_device(self):
257+ sm = _make_linear()
258+ sm.to(str(torch.device(device_type)))
259+ self.assertEqual(sm.linear.weight.device.type, device_type)
260+ 
261+ def test_to_no_args_returns_self(self):
262+ sm = _make_linear()
263+ result = sm.to()
264+ self.assertIsInstance(result, torch.jit.ScriptModule)
265+ 
266+ def test_to_propagates_to_submodules(self):
267+ sm = _make_nested()
268+ sm.to(dtype=torch.float64)
269+ self.assertIn(sm.sub.linear.weight.dtype,
270+ (torch.float64, torch.float32))
271+ 
272+ def test_to_npu_and_dtype(self):
273+ sm = _make_linear()
274+ sm.to(device_type, dtype=torch.float64)
275+ self.assertEqual(sm.linear.weight.device.type, device_type)
276+ self.assertIn(sm.linear.weight.dtype,
277+ (torch.float64, torch.float32))
278+ 
279+ 
280+class TestScriptModuleFloatDouble(TestCase):
281+ 
282+ def test_float_returns_self(self):
283+ sm = _make_linear()
284+ result = sm.float()
285+ self.assertIsInstance(result, torch.jit.ScriptModule)
286+ 
287+ def test_float_converts_params(self):
288+ sm = _make_linear()
289+ sm.float()
290+ self.assertEqual(sm.linear.weight.dtype, torch.float32)
291+ 
292+ def test_float_on_npu(self):
293+ sm = _make_linear()
294+ sm.float()
295+ self.assertEqual(sm.linear.weight.dtype, torch.float32)
296+ self.assertEqual(sm.linear.weight.device.type, device_type)
297+ 
298+ def test_float_propagates_to_submodules(self):
299+ sm = _make_nested()
300+ sm.float()
301+ self.assertEqual(sm.sub.linear.weight.dtype, torch.float32)
302+ 
303+ def test_double_returns_self(self):
304+ sm = _make_linear()
305+ result = sm.double()
306+ self.assertIsInstance(result, torch.jit.ScriptModule)
307+ 
308+ def test_double_converts_params(self):
309+ sm = _make_linear()
310+ sm.double()
311+ self.assertIn(sm.linear.weight.dtype,
312+ (torch.float64, torch.float32))
313+ 
314+ def test_double_on_npu_fallback_to_float32(self):
315+ sm = _make_linear()
316+ sm.double()
317+ # NPU does not support float64; double() falls back to float32
318+ self.assertEqual(sm.linear.weight.dtype, torch.float32)
319+ 
320+ 
321+class TestScriptModuleType(TestCase):
322+ 
323+ def test_type_float32(self):
324+ sm = _make_linear()
325+ sm.type(torch.float32)
326+ self.assertEqual(sm.linear.weight.dtype, torch.float32)
327+ 
328+ def test_type_float64(self):
329+ sm = _make_linear()
330+ sm.type(torch.float64)
331+ self.assertIn(sm.linear.weight.dtype,
332+ (torch.float64, torch.float32))
333+ 
334+ def test_type_on_npu(self):
335+ sm = _make_linear()
336+ sm.type(torch.float64)
337+ self.assertEqual(sm.linear.weight.device.type, device_type)
338+ self.assertIn(sm.linear.weight.dtype,
339+ (torch.float64, torch.float32))
340+ 
341+ def test_type_int32_raises(self):
342+ sm = _make_linear()
343+ with self.assertRaisesRegex(
344+ RuntimeError, r"must be floating point"):
345+ sm.type(torch.int32)
346+ 
347+ 
348+class TestScriptModuleStateDict(TestCase):
349+ 
350+ def test_state_dict_contains_params(self):
351+ sm = _make_linear()
352+ sd = sm.state_dict()
353+ self.assertIn("linear.weight", sd)
354+ self.assertIn("linear.bias", sd)
355+ 
356+ def test_state_dict_contains_buffers(self):
357+ sm = _make_with_buffer()
358+ sd = sm.state_dict()
359+ self.assertIn("buf", sd)
360+ 
361+ def test_state_dict_values_match(self):
362+ sm = _make_linear()
363+ sd = sm.state_dict()
364+ self.assertEqual(sd["linear.weight"], sm.linear.weight)
365+ self.assertEqual(sd["linear.bias"], sm.linear.bias)
366+ 
367+ def test_state_dict_on_npu(self):
368+ sm = _make_linear()
369+ sd = sm.state_dict()
370+ self.assertEqual(sd["linear.weight"].device.type, device_type)
371+ 
372+ def test_state_dict_with_prefix(self):
373+ sm = _make_linear()
374+ sd = sm.state_dict(prefix="mymodel.")
375+ self.assertIn("mymodel.linear.weight", sd)
376+ self.assertIn("mymodel.linear.bias", sd)
377+ 
378+ def test_state_dict_with_destination(self):
379+ sm = _make_linear()
380+ dest = {"existing": torch.tensor(0)}
381+ result = sm.state_dict(destination=dest, prefix="mod.")
382+ self.assertIs(result, dest)
383+ self.assertIn("existing", result)
384+ self.assertIn("mod.linear.weight", result)
385+ 
386+ def test_state_dict_keep_vars(self):
387+ sm = _make_linear()
388+ sd = sm.state_dict(keep_vars=True)
389+ self.assertIsInstance(sd["linear.weight"], nn.Parameter)
390+ self.assertTrue(sd["linear.weight"].requires_grad)
391+ 
392+ 
393+class TestScriptModuleSave(TestCase):
394+ 
395+ def test_save_and_load(self):
396+ sm = _make_linear()
397+ with tempfile.NamedTemporaryFile(suffix=".pt", delete=False) as f:
398+ path = f.name
399+ try:
400+ sm.save(path)
401+ loaded = torch.jit.load(path)
402+ x = torch.randn(2, 2).to(device_type)
403+ self.assertEqual(sm(x), loaded(x))
404+ finally:
405+ if os.path.exists(path):
406+ os.remove(path)
407+ 
408+ def test_save_preserves_output(self):
409+ sm = _make_linear()
410+ with tempfile.NamedTemporaryFile(suffix=".pt", delete=False) as f:
411+ path = f.name
412+ try:
413+ with torch.no_grad():
414+ sm.linear.weight.fill_(1.0)
415+ sm.linear.bias.fill_(2.0)
416+ sm.save(path)
417+ loaded = torch.jit.load(path)
418+ self.assertEqual(loaded.linear.weight, torch.ones(2, 2))
419+ self.assertEqual(loaded.linear.bias, torch.ones(2) * 2)
420+ finally:
421+ if os.path.exists(path):
422+ os.remove(path)
423+ 
424+ def test_save_returns_none(self):
425+ sm = _make_linear()
426+ with tempfile.NamedTemporaryFile(suffix=".pt", delete=False) as f:
427+ path = f.name
428+ try:
429+ ret = sm.save(path)
430+ self.assertIsNone(ret)
431+ finally:
432+ if os.path.exists(path):
433+ os.remove(path)
434+ 
435+ def test_save_on_npu(self):
436+ sm = _make_linear()
437+ with tempfile.NamedTemporaryFile(suffix=".pt", delete=False) as f:
438+ path = f.name
439+ try:
440+ sm.save(path)
441+ loaded = torch.jit.load(path)
442+ x = torch.randn(2, 2).to(device_type)
443+ self.assertEqual(sm(x), loaded(x))
444+ finally:
445+ if os.path.exists(path):
446+ os.remove(path)
447+ 
448+ def test_save_with_extra_files(self):
449+ sm = _make_linear()
450+ extra = {"meta.json": '{"version": 1}', "readme.txt": "hello"}
451+ with tempfile.NamedTemporaryFile(suffix=".pt", delete=False) as f:
452+ path = f.name
453+ try:
454+ sm.save(path, _extra_files=extra)
455+ self.assertTrue(os.path.exists(path))
456+ self.assertGreater(os.path.getsize(path), 0)
457+ finally:
458+ if os.path.exists(path):
459+ os.remove(path)
460+ 
461+ def test_save_to_buffer(self):
462+ sm = _make_linear()
463+ buf = sm.save_to_buffer()
464+ self.assertIsInstance(buf, bytes)
465+ loaded = torch.jit.load(io.BytesIO(buf))
466+ x = torch.randn(2, 2).to(device_type)
467+ self.assertEqual(sm(x), loaded(x))
468+ 
469+ 
470+class TestScriptModuleExtraRepr(TestCase):
471+ 
472+ def test_extra_repr_returns_str(self):
473+ sm = _make_linear()
474+ result = sm.extra_repr()
475+ self.assertIsInstance(result, str)
476+ 
477+ def test_extra_repr_contains_original_name(self):
478+ sm = _make_linear()
479+ result = sm.extra_repr()
480+ match = re.search(r"original_name=(\S+)", result)
481+ if match:
482+ self.assertIsInstance(match.group(1), str)
483+ 
484+ def test_extra_repr_on_npu(self):
485+ sm = _make_linear()
486+ result = sm.extra_repr()
487+ self.assertIsInstance(result, str)
488+ 
489+ 
490+class TestScriptModuleShareMemory(TestCase):
491+ """share_memory behavior differs by device:
492+ CPU: works, makes storage shared.
493+ GPU/CUDA: no-op (per torch.Tensor.share_memory_ docstring).
494+ NPU: torch-npu intercepts with RuntimeError.
495+ Tests document actual NPU behavior and CPU baseline."""
496+ 
497+ def test_share_memory_cpu_returns_self(self):
498+ sm = _make_cpu_linear()
499+ result = sm.share_memory()
500+ self.assertIs(result, sm)
501+ 
502+ def test_share_memory_cpu_makes_shared(self):
503+ sm = _make_cpu_linear()
504+ sm.share_memory()
505+ self.assertTrue(sm.linear.weight.untyped_storage().is_shared())
506+ 
507+ def test_share_memory_cpu_idempotent(self):
508+ sm = _make_cpu_linear()
509+ sm.share_memory()
510+ sm.share_memory()
511+ self.assertTrue(sm.linear.weight.untyped_storage().is_shared())
512+ 
513+ def test_share_memory_on_npu_raises(self):
514+ sm = _make_linear()
515+ with self.assertRaisesRegex(
516+ RuntimeError, r"share_memory.*not supported in npu"):
517+ sm.share_memory()
518+ 
519+ 
520+class TestScriptModuleMetadata(TestCase):
521+ """register_module/register_parameter on NPU:
522+ torch-npu intercepts with RuntimeError.
523+ On CPU they also raise RuntimeError (PyTorch limitation:
524+ "Cannot re-assign modules" / "Can't add a new parameter
525+ after ScriptModule construction")."""
526+ 
527+ def test_register_module_raises_on_npu(self):
528+ sm = _make_linear()
529+ sub = nn.Linear(2, 2).to(device_type)
530+ with self.assertRaisesRegex(
531+ RuntimeError, r"register_module.*not supported in npu"):
532+ sm.register_module("new_sub", sub)
533+ 
534+ def test_register_parameter_raises_on_npu(self):
535+ sm = _make_linear()
536+ param = nn.Parameter(torch.randn(2, 2)).to(device_type)
537+ with self.assertRaisesRegex(
538+ RuntimeError, r"register_parameter.*not supported in npu"):
539+ sm.register_parameter("new_param", param)
540+ 
541+ def test_set_submodule_raises(self):
542+ sm = _make_linear()
543+ new_sub = nn.Linear(2, 2).to(device_type)
544+ with self.assertRaisesRegex(
545+ RuntimeError, r"not supported on ScriptModules"):
546+ sm.set_submodule("linear", new_sub)
547+ 
548+ def test_set_submodule_nested_raises(self):
549+ sm = _make_nested()
550+ new_sub = nn.Linear(2, 2).to(device_type)
551+ with self.assertRaisesRegex(
552+ RuntimeError, r"not supported on ScriptModules"):
553+ sm.set_submodule("sub.linear", new_sub)
554+ 
555+ def test_get_buffer_unsupported(self):
556+ sm = _make_with_buffer()
557+ with self.assertRaisesRegex(
558+ RuntimeError,
559+ r"get_buffer is not supported on ScriptModules"):
560+ sm.get_buffer("buf")
561+ 
562+ def test_get_buffer_unsupported_on_nested(self):
563+ sm = _make_nested_with_buffer()
564+ with self.assertRaisesRegex(
565+ RuntimeError,
566+ r"get_buffer is not supported on ScriptModules"):
567+ sm.get_buffer("sub.buf")
568+ 
569+ def test_get_buffer_unsupported_nonexistent(self):
570+ sm = _make_linear()
571+ with self.assertRaisesRegex(
572+ RuntimeError,
573+ r"get_buffer is not supported on ScriptModules"):
574+ sm.get_buffer("nonexistent")
575+ 
576+ 
577+class TestScriptModuleUnsupported(TestCase):
578+ """APIs that raise errors by PyTorch design, not torch-npu."""
579+ 
580+ def test_requires_grad_unsupported(self):
581+ sm = _make_linear()
582+ with self.assertRaisesRegex(
583+ RuntimeError,
584+ r"requires_grad_ is not supported on ScriptModules"):
585+ sm.requires_grad_(True)
586+ 
587+ def test_to_empty_unsupported(self):
588+ sm = _make_linear()
589+ with self.assertRaisesRegex(
590+ RuntimeError,
591+ r"to_empty is not supported on ScriptModules"):
592+ sm.to_empty(device=device_type)
593+ 
594+ def test_xpu_unsupported(self):
595+ sm = _make_linear()
596+ with self.assertRaisesRegex(
597+ RuntimeError,
598+ r"xpu is not supported on ScriptModules"):
599+ sm.xpu()
600+ 
601+ def test_set_extra_state_raises(self):
602+ sm = _make_linear()
603+ with self.assertRaisesRegex(
604+ RuntimeError, r"should never be called"):
605+ sm.set_extra_state({"version": 1})
606+ 
607+ 
608+if __name__ == "__main__":
609+ run_tests()