import unittest
from unittest import mock
import threading
import time
import torch
from torch.nn.parallel import scatter_gather
from torch.testing._internal.common_utils import (
IS_JETSON,
IS_REMOTE_GPU,
IS_SANDCASTLE,
NoTest,
TEST_PRIVATEUSE1,
TestCase,
instantiate_parametrized_tests,
run_tests,
skipCUDANonDefaultStreamIf,
skipIfRocm,
)
import torch_npu
import torch_npu._inductor
import torch_npu.testing
from torch_npu.testing.common_utils import get_cycles_per_ms
from torch_npu._inductor.shape_handling import unified_copy
import torch_npu._inductor.shape_handling as shape_handling_module
torch._dynamo.config.cache_size_limit = 128
if not torch.npu.is_available():
raise unittest.SkipTest("NPU is not available")
device = "npu"
def model_fn(A, B):
return A + B
shape_options = {
"enable_shape_handling": True,
"shape_handling_configs": [
{
"type": "BATCHSIZE",
"dimensions": 0,
"value": 0.0,
"gears": [],
"min_size": 1,
"max_size": 1024,
"policy": "TIMES",
},
{
"type": "SEQLEN",
"dimensions": [1, 1],
"value": 0.0,
"gears": [],
"min_size": 1,
"max_size": 1024,
"policy": "TIMES",
}
]
}
class TestShapeHandling(TestCase):
def test_init_no_input(self):
"""Constructing NPUShapeHandling with no arguments should succeed (delay_init path)."""
shape_handling = torch_npu._inductor.NPUShapeHandling()
self.assertNotEqual(shape_handling, None)
def test_init_with_empty_conifg(self):
"""Empty config list should not crash — falls through to delay_init."""
configs = []
shape_handling = torch_npu._inductor.NPUShapeHandling(configs)
self.assertNotEqual(shape_handling, None)
def test_init_with_gears(self):
"""Explicit gears for two dimension types should be accepted."""
configs = [
{"type": "BATCHSIZE", "gears": [16, 32, 64]},
{"type": "SEQLEN", "gears": [16, 32, 64]},
]
shape_handling = torch_npu._inductor.NPUShapeHandling(configs)
self.assertNotEqual(shape_handling, None)
def test_init_with_policy(self):
"""TIMES policy with min/max should auto-generate gears."""
configs = [
{"type": "BATCHSIZE", "min_size": 2, "max_size": 8, "policy": "TIMES"},
{"type": "SEQLEN", "min_size": 2, "max_size": 8, "policy": "TIMES"},
]
shape_handling = torch_npu._inductor.NPUShapeHandling(configs)
self.assertNotEqual(shape_handling, None)
def test_normalize_configs_edge_cases(self):
"""_normalize_configs handles empty input, dedup, float→int, TIMES expansion."""
from torch_npu.utils._adaptive_gears import AdaptiveGearRuntime
from unittest.mock import MagicMock
runtime = MagicMock(spec=AdaptiveGearRuntime)
runtime._expand_policy_gears = lambda config: AdaptiveGearRuntime._expand_policy_gears(runtime, config)
self.assertEqual(AdaptiveGearRuntime._normalize_configs(runtime, []), [])
self.assertEqual(AdaptiveGearRuntime._normalize_configs(runtime, None), [])
config = {"type": "BATCHSIZE", "gears": [64, 16, 32, 16]}
result = AdaptiveGearRuntime._normalize_configs(runtime, [config])
self.assertEqual(result[0]["gears"], [16, 32, 64])
config = {"type": "BATCHSIZE", "gears": [16.0, 32.0]}
result = AdaptiveGearRuntime._normalize_configs(runtime, [config])
self.assertEqual(result[0]["gears"], [16, 32])
self.assertIsInstance(result[0]["gears"][0], int)
config = {"type": "BATCHSIZE", "min_size": 4, "max_size": 4}
result = AdaptiveGearRuntime._normalize_configs(runtime, [config])
self.assertEqual(result[0]["gears"], [4])
config = {"type": "BATCHSIZE", "min_size": 3, "max_size": 100}
result = AdaptiveGearRuntime._normalize_configs(runtime, [config])
self.assertEqual(result[0]["gears"][0], 3)
self.assertEqual(result[0]["gears"][-1], 100)
for g in result[0]["gears"][1:-1]:
self.assertEqual(g & (g - 1), 0, f"{g} is not a power of 2")
config = {"type": "BATCHSIZE", "min_size": 8, "max_size": 128}
result = AdaptiveGearRuntime._normalize_configs(runtime, [config])
self.assertEqual(result[0]["gears"], [8, 16, 32, 64, 128])
def test_transform_no_operation(self):
configs = [
{
"type": "BATCHSIZE",
"gears": [32],
"dimensions": 0
},
{
"type": "SEQLEN",
"gears": [128],
"dimensions": [1]
}
]
shape_handling = torch_npu._inductor.NPUShapeHandling(configs)
input_tensor = torch.randn(32, 128)
outputs = shape_handling.transform([input_tensor])
self.assertEqual(outputs[0][0].shape, input_tensor.shape)
def test_transform_padding(self):
configs = [
{
"type": "BATCHSIZE",
"min_size": 16,
"max_size": 256,
"policy": "TIMES",
"dimensions": 0
},
{
"type": "SEQLEN",
"min_size": 16,
"max_size": 256,
"policy": "TIMES",
"dimensions": [1]
}
]
shape_handling = torch_npu._inductor.NPUShapeHandling(configs)
input_tensor = torch.randn(48, 96)
outputs = shape_handling.transform([input_tensor])
self.assertEqual(outputs[0][0].shape, (64, 128))
def test_transform_padding_with_indices(self):
configs = [
{
"type": "BATCHSIZE",
"min_size": 16,
"max_size": 256,
"policy": "TIMES",
"dimensions": 0,
"indices": [0]
},
{
"type": "SEQLEN",
"min_size": 16,
"max_size": 256,
"policy": "TIMES",
"dimensions": [1],
"indices": [0]
}
]
shape_handling = torch_npu._inductor.NPUShapeHandling(configs)
input_tensor1 = torch.randn(48, 96)
input_tensor2 = torch.randn(48, 96)
outputs = shape_handling.transform([input_tensor1, input_tensor2])
self.assertEqual(outputs[0][0].shape, (64, 128))
self.assertEqual(outputs[0][1].shape, (48, 96))
def test_transform_split(self):
"""测试分割操作"""
configs = [
{
"type": "BATCHSIZE",
"gears": [64, 128],
"dimensions": 0
},
{
"type": "SEQLEN",
"gears": [64, 128],
"dimensions": [1]
}
]
shape_handling = torch_npu._inductor.NPUShapeHandling(configs)
input_tensor = torch.randn(200, 96)
outputs = shape_handling.transform([input_tensor])
self.assertEqual(len(outputs), 2)
self.assertEqual(outputs[0][0].shape, (128, 128))
self.assertEqual(outputs[1][0].shape, (128, 128))
def test_recover_padding(self):
"""测试恢复填充的张量"""
configs = [
{
"type": "BATCHSIZE",
"min_size": 16,
"max_size": 256,
"policy": "TIMES",
"dimensions": 0,
"indices": [0]
},
{
"type": "SEQLEN",
"min_size": 16,
"max_size": 256,
"policy": "TIMES",
"dimensions": [1],
"indices": [0]
}
]
shape_handling = torch_npu._inductor.NPUShapeHandling(configs)
orig_tensor = torch.randn(48, 128)
padded_group = shape_handling.transform([orig_tensor])
recovered = shape_handling.recover(padded_group)
self.assertEqual(recovered[0].shape, orig_tensor.shape)
self.assertTrue(torch.allclose(recovered[0], orig_tensor))
def test_recover_split(self):
"""测试恢复分割的张量组"""
configs = [
{
"type": "BATCHSIZE",
"min_size": 16,
"max_size": 128,
"policy": "TIMES",
"dimensions": 0,
"indices": [0]
},
{
"type": "SEQLEN",
"min_size": 16,
"max_size": 256,
"policy": "TIMES",
"dimensions": [1],
"indices": [0]
}
]
shape_handling = torch_npu._inductor.NPUShapeHandling(configs)
orig_tensor = torch.randn(200, 128)
split_groups = shape_handling.transform([orig_tensor])
recovered = shape_handling.recover(split_groups)
self.assertEqual(recovered[0].shape, orig_tensor.shape)
self.assertTrue(torch.allclose(recovered[0], orig_tensor))
def test_invalid_dim(self):
"""测试无效维度异常"""
configs = [
{
"type": "BATCHSIZE",
"min_size": 16,
"max_size": 128,
"policy": "TIMES",
"dimensions": 3,
"indices": [0]
}
]
shape_handling = torch_npu._inductor.NPUShapeHandling(configs)
input_tensor = torch.randn(32, 128)
with self.assertRaises(RuntimeError):
shape_handling.transform([input_tensor])
def test_register_custom_strategy(self):
"""测试注册自定义策略"""
class CustomBsShapeOp(torch_npu._C._BSShapeOpStrategy):
def __init__(self):
super().__init__()
self.transform_called = False
self.recover_called = False
def Transform(self, inputs, outputs):
self.transform_called = True
if inputs:
outputs.append([tensor.clone() for tensor in inputs])
def Recover(self, inputs, outputs):
self.recover_called = True
if inputs and inputs[0]:
outputs.append(inputs[0][0].clone())
class CustomSeqShapeOp(torch_npu._C._SeqShapeOpStrategy):
def __init__(self):
super().__init__()
self.transform_called = False
self.recover_called = False
def Transform(self, inputs, outputs):
self.transform_called = True
if inputs:
outputs.append([tensor.clone() for tensor in inputs])
configs = [
{
"type": "BATCHSIZE",
"min_size": 16,
"max_size": 128,
"policy": "TIMES",
"dimensions": 0,
"indices": [0]
},
{
"type": "SEQLEN",
"min_size": 16,
"max_size": 256,
"policy": "TIMES",
"dimensions": [1],
"indices": [0]
}
]
shape_handling = torch_npu._inductor.NPUShapeHandling(configs)
custom_bs_strategy = CustomBsShapeOp()
custom_seq_strategy = CustomSeqShapeOp()
shape_handling.register_batch_size_strategy(custom_bs_strategy)
shape_handling.register_sequence_strategy(custom_seq_strategy)
input_tensor = torch.randn(32, 128)
shape_handling.transform([input_tensor])
self.assertTrue(custom_bs_strategy.transform_called)
self.assertTrue(custom_seq_strategy.transform_called)
class TestShapeHandlingBranchCoverage(TestCase):
def test_validate_configs_error_branches(self):
shape_handling = torch_npu._inductor.NPUShapeHandling()
with self.assertRaises(ValueError):
shape_handling._validate_configs([{}])
with self.assertRaises(ValueError):
shape_handling._validate_configs([{"type": 1}])
with self.assertRaises(ValueError):
shape_handling._validate_configs([{"type": "UNKNOWN"}])
with self.assertRaises(ValueError):
shape_handling._validate_configs([{"type": "BATCHSIZE", "dimensions": "0"}])
with self.assertRaises(ValueError):
shape_handling._validate_configs([{"type": "BATCHSIZE", "dimensions": [0, "1"]}])
with self.assertRaises(ValueError):
shape_handling._validate_configs([{"type": "BATCHSIZE", "min_size": 1.5}])
with self.assertRaises(ValueError):
shape_handling._validate_configs([{"type": "BATCHSIZE", "value": "bad"}])
with self.assertRaises(ValueError):
shape_handling._validate_configs([{"type": "BATCHSIZE", "policy": 1}])
with self.assertRaises(ValueError):
shape_handling._validate_configs([{"type": "BATCHSIZE", "policy": "BAD"}])
with self.assertRaises(ValueError):
shape_handling._validate_configs([{"type": "BATCHSIZE"}, {"type": "BATCHSIZE"}])
with self.assertRaises(ValueError):
shape_handling._validate_configs(
[{"type": "BATCHSIZE"}, {"type": "SEQLEN"}, {"type": "BATCHSIZE"}]
)
def test_validate_adaptive_configs(self):
"""Test adaptive config validation: unknown keys, range errors, type compatibility."""
shape_handling = torch_npu._inductor.NPUShapeHandling()
with self.assertRaises(ValueError):
shape_handling._validate_adaptive_configs({"unknown_key": 1})
with self.assertRaises(ValueError):
shape_handling._validate_adaptive_configs({"weight_hit": 1.5})
with self.assertRaises(ValueError):
shape_handling._validate_adaptive_configs({"pad_add_threshold": -0.1})
with self.assertRaises(ValueError):
shape_handling._validate_adaptive_configs({"window_seconds": -1})
shape_handling._validate_adaptive_configs({
"window_seconds": 0,
"recent_use_protect_seconds": 0,
"update_interval_seconds": 0,
})
with self.assertRaises(ValueError):
shape_handling._validate_adaptive_configs({"min_samples_per_gear": 0})
shape_handling._validate_adaptive_configs({
"weight_hit": 1,
"window_seconds": 300,
})
def test_construct_indices_branches(self):
shape_handling = torch_npu._inductor.NPUShapeHandling()
tensors = [torch.randn(2, 3), torch.randn(2)]
bs_indices = shape_handling._construct_indices(tensors, [], "BATCHSIZE")
self.assertEqual(bs_indices, [0, 1])
seq_indices_default = shape_handling._construct_indices(tensors, [], "SEQLEN")
self.assertEqual(seq_indices_default, [0])
seq_indices_dim0 = shape_handling._construct_indices(tensors, [0], "SEQLEN")
self.assertEqual(seq_indices_dim0, [0, 1])
def test_delay_initialize_builds_missing_indices(self):
configs = [
{
"type": "BATCHSIZE",
"dimensions": [0],
"indices": [],
"gears": [8]
},
{
"type": "SEQLEN",
"dimensions": [1],
"indices": [0],
"gears": [8]
}
]
shape_handling = torch_npu._inductor.NPUShapeHandling(configs)
self.assertTrue(shape_handling.delay_init)
shape_handling.delay_initialize([torch.randn(4, 5)])
self.assertEqual(shape_handling.configs[0]["indices"], [0])
self.assertEqual(shape_handling.configs[1]["indices"], [0])
self.assertFalse(shape_handling.delay_init)
def test_flatten_and_unflatten_with_and_without_tensor(self):
shape_handling = torch_npu._inductor.NPUShapeHandling()
tensors, indices, leaves, spec = shape_handling.flatten_to_tensors({"a": 1, "b": "x"})
self.assertEqual(list(tensors), [])
self.assertEqual(list(indices), [])
self.assertEqual(len(leaves), 2)
self.assertIsNotNone(spec)
structure = ((torch.tensor([1.0]), "k"), {"v": torch.tensor([2.0])})
tensors, indices, leaves, spec = shape_handling.flatten_to_tensors(structure)
rebuilt = shape_handling.unflatten_from_tensors(
[torch.tensor([10.0]), torch.tensor([20.0])], indices, leaves, spec
)
self.assertEqual(rebuilt[0][1], "k")
self.assertTrue(torch.equal(rebuilt[0][0], torch.tensor([10.0])))
self.assertTrue(torch.equal(rebuilt[1]["v"], torch.tensor([20.0])))
def test_transform_hook_default_and_custom_paths(self):
shape_handling = torch_npu._inductor.NPUShapeHandling()
input_a = torch.tensor([1.0])
input_b = torch.tensor([2.0])
transform_res = [
[torch.tensor([3.0]), torch.tensor([4.0])],
[torch.tensor([5.0]), torch.tensor([6.0])]
]
with mock.patch.object(
torch_npu._inductor.NPUShapeHandling, "transform", return_value=transform_res
):
args_list, kwargs_list = shape_handling.transform_hook(input_a, extra=input_b)
self.assertEqual(len(args_list), 2)
self.assertEqual(len(kwargs_list), 2)
self.assertTrue(torch.equal(args_list[0][0], torch.tensor([3.0])))
self.assertTrue(torch.equal(kwargs_list[1]["extra"], torch.tensor([6.0])))
recorded = {}
def pre_fn(*args, **kwargs):
recorded["pre"] = (args, kwargs)
return [args[0]]
def post_fn(outputs):
recorded["post"] = outputs
return [["ok"]], [{"done": True}]
shape_handling_custom = torch_npu._inductor.NPUShapeHandling(
transform_pre_fn=pre_fn,
transform_post_fn=post_fn,
)
with mock.patch.object(
torch_npu._inductor.NPUShapeHandling, "transform", return_value=[[torch.tensor([9.0])]]
):
out_args, out_kwargs = shape_handling_custom.transform_hook(torch.tensor([7.0]))
self.assertIn("pre", recorded)
self.assertIn("post", recorded)
self.assertEqual(out_args, [["ok"]])
self.assertEqual(out_kwargs, [{"done": True}])
shape_handling_none = torch_npu._inductor.NPUShapeHandling(transform_post_fn=lambda _: None)
with mock.patch.object(
torch_npu._inductor.NPUShapeHandling, "transform", return_value=[[torch.tensor([1.0])]]
):
self.assertIsNone(shape_handling_none.transform_hook(torch.tensor([1.0])))
def test_recover_hook_default_and_custom_paths(self):
shape_handling = torch_npu._inductor.NPUShapeHandling()
groups = [
((torch.tensor([1.0]), "x"), {"y": torch.tensor([2.0])})
]
with mock.patch.object(
torch_npu._inductor.NPUShapeHandling,
"recover",
side_effect=lambda tensor_groups: list(tensor_groups[0]),
):
outputs = shape_handling.recover_hook(groups)
self.assertEqual(outputs[0][1], "x")
self.assertTrue(torch.equal(outputs[0][0], torch.tensor([1.0])))
self.assertTrue(torch.equal(outputs[1]["y"], torch.tensor([2.0])))
custom = torch_npu._inductor.NPUShapeHandling(
recover_pre_fn=lambda groups: [[torch.tensor([5.0])]],
recover_post_fn=lambda recover_res: {"result": recover_res},
)
with mock.patch.object(
torch_npu._inductor.NPUShapeHandling, "recover", return_value=[torch.tensor([8.0])]
):
outputs = custom.recover_hook(groups)
self.assertEqual(list(outputs.keys()), ["result"])
self.assertTrue(torch.equal(outputs["result"][0], torch.tensor([8.0])))
def test_get_shape_safe_and_patch_shape_handling(self):
shape_handling = torch_npu._inductor.NPUShapeHandling()
shape_info = shape_handling.get_shape_safe((torch.randn(2, 3), [torch.randn(1)]))
self.assertEqual(shape_info[0], [2, 3])
self.assertEqual(shape_info[1][0], [1])
self.assertIs(shape_handling.get_shape_safe(1), int)
if hasattr(shape_handling_module.patch_shape_handling, "_is_patched"):
delattr(shape_handling_module.patch_shape_handling, "_is_patched")
called = []
with mock.patch.object(shape_handling_module, "patch_dynamo_context", side_effect=lambda: called.append(1)):
shape_handling_module.patch_shape_handling()
shape_handling_module.patch_shape_handling()
self.assertEqual(len(called), 1)
if hasattr(shape_handling_module.patch_shape_handling, "_is_patched"):
delattr(shape_handling_module.patch_shape_handling, "_is_patched")
def test_transform_metadata_collection(self):
configs = [
{
"type": "BATCHSIZE",
"gears": [64],
"dimensions": 0,
"indices": [0],
},
{
"type": "SEQLEN",
"gears": [128],
"dimensions": [1],
"indices": [0],
},
]
shape_handling = torch_npu._inductor.NPUShapeHandling(configs)
with mock.patch.object(
torch_npu._inductor.NPUShapeHandling,
"transform",
return_value=[[torch.randn(64, 128)], [torch.randn(64, 128)]],
):
_, metadata = shape_handling._transform_with_metadata(torch.randn(96, 96))
self.assertEqual(len(metadata), 2)
self.assertEqual(metadata[0]["raw_gear_values"], [[96], [96]])
self.assertEqual(metadata[0]["mapped_gear_values"], [[64], [128]])
self.assertGreater(metadata[0]["pad_ratios"][1][0], 0.0)
self.assertGreater(metadata[0]["split_ratios"][0][0], 0.0)
def test_transform_metadata_collection_degrades_on_failure(self):
"""Metadata collection failure should not affect transform output."""
configs = [{"type": "BATCHSIZE", "gears": [32], "dimensions": 0, "indices": [0]}]
shape_handling = torch_npu._inductor.NPUShapeHandling(configs)
with mock.patch.object(
torch_npu._inductor.NPUShapeHandling, "transform",
return_value=[[torch.randn(32, 8)]],
):
with mock.patch(
"torch_npu.utils._adaptive_gears.collect_transform_metadata",
side_effect=RuntimeError("boom"),
):
outputs, metadata = shape_handling._transform_with_metadata(torch.randn(16, 8))
self.assertEqual(len(outputs[0]), 1)
self.assertEqual(metadata, [])
class TestAsyncWorkerAndConcurrency(TestCase):
"""测试异步Worker和并发控制机制"""
def test_async_worker_creation_and_execution(self):
"""测试后台Worker线程异步执行 run_once"""
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [16, 32], "dimensions": 0, "indices": [0]},
],
adaptive_configs={
"update_interval_seconds": 0.1,
"min_samples_per_gear": 1,
"min_gear_count_per_type": 1,
},
)
manager = shape_handling.adaptive_manager
self.assertIsNotNone(manager)
self.assertIsNotNone(manager.worker)
self.assertTrue(manager._worker_thread.is_alive())
run_event = threading.Event()
run_thread_id = None
def mock_run_once(ts):
nonlocal run_thread_id
run_thread_id = threading.current_thread().ident
run_event.set()
with mock.patch.object(manager.worker, "run_once", side_effect=mock_run_once):
self.assertTrue(run_event.wait(timeout=5.0))
self.assertIsNotNone(run_thread_id)
self.assertNotEqual(run_thread_id, threading.current_thread().ident)
manager.shutdown()
def test_update_loop_survives_exception(self):
"""Worker thread should continue after run_once raises."""
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [16, 32], "dimensions": 0, "indices": [0]},
],
adaptive_configs={
"update_interval_seconds": 0.1,
"min_samples_per_gear": 1,
"min_gear_count_per_type": 1,
},
)
manager = shape_handling.adaptive_manager
call_count = 0
run_event = threading.Event()
def mock_run_once(ts):
nonlocal call_count
call_count += 1
if call_count == 1:
raise RuntimeError("simulated failure")
run_event.set()
with mock.patch.object(manager.worker, "run_once", side_effect=mock_run_once):
self.assertTrue(run_event.wait(timeout=5.0))
self.assertGreaterEqual(call_count, 2)
manager.shutdown()
def test_snapshot_isolation_after_update(self):
"""Snapshot taken before an update must still reflect old gear set (clone-on-read)."""
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [16, 32], "dimensions": 0, "indices": [0]},
],
adaptive_configs={
"recent_use_protect_seconds": 1.0,
"min_samples_per_gear": 1,
"min_gear_count_per_type": 1,
},
)
manager = shape_handling.adaptive_manager
snapshot1 = manager.get_snapshot()
original_gears = snapshot1.active_gears["BATCHSIZE"].copy()
manager.record_event([[20]], [[32]], [[0.375]], [[0.0]], 100.0)
manager.worker.run_once(150.0)
self.assertEqual(snapshot1.active_gears["BATCHSIZE"], original_gears)
manager.shutdown()
def test_high_concurrent_gear_update_scenarios(self):
"""测试高并发场景下的gear更新"""
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [16, 32, 64], "dimensions": 0, "indices": [0]},
],
adaptive_configs={
"recent_use_protect_seconds": 1.0,
"min_samples_per_gear": 1,
"add_min_samples": 1,
"min_gear_count_per_type": 2,
"recent_use_protect_seconds": 0.0,
"replace_loss_threshold": 0.20,
},
)
manager = shape_handling.adaptive_manager
def run_update(timestamp):
manager.record_event([[16], [64]], [[32], [64]], [[0.5], [0.0]], [[0.0], [0.0]], 100.0 + timestamp)
manager.worker.run_once(200.0 + timestamp)
threads = []
for i in range(5):
thread = threading.Thread(target=run_update, args=(i,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
latest_snapshot = manager.get_snapshot()
self.assertIsNotNone(latest_snapshot)
self.assertEqual(sorted(latest_snapshot.active_gears["BATCHSIZE"]), [32, 64])
manager.shutdown()
class TestAdaptiveShapeHandling(TestCase):
def test_npu_shape_handling_creates_snapshot_handler(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [16, 32], "dimensions": 0, "indices": [0]},
{"type": "SEQLEN", "gears": [64, 128], "dimensions": [1], "indices": [0]},
],
adaptive_configs={"recent_use_protect_seconds": 1.0},
)
manager = shape_handling.adaptive_manager
snapshot = manager.get_snapshot()
self.assertEqual(snapshot.active_gears["BATCHSIZE"], [16, 32])
self.assertEqual(snapshot.active_gears["SEQLEN"], [64, 128])
self.assertIsNotNone(snapshot.shape_handling)
def test_npu_shape_handling_records_event_and_builds_stats(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [16, 32], "dimensions": 0, "indices": [0]},
{"type": "SEQLEN", "gears": [64, 128], "dimensions": [1], "indices": [0]},
],
adaptive_configs={"window_seconds": 300.0},
)
manager = shape_handling.adaptive_manager
snapshot = manager.get_snapshot()
manager.record_event(
raw_gear_values=[[20], [96]],
mapped_gear_values=[[32], [128]],
pad_ratios=[[0.375], [0.25]],
split_ratios=[[0.0], [0.0]],
event_ts=100.0,
)
stats = manager.build_stats_snapshot(100.0)
self.assertIn("BATCHSIZE:32", stats)
self.assertIn("SEQLEN:128", stats)
self.assertEqual(stats["BATCHSIZE:32"]["sample_count"], 1)
self.assertEqual(stats["BATCHSIZE:32"]["pad_sample_count"], 1)
self.assertEqual(stats["BATCHSIZE:32"]["split_sample_count"], 0)
self.assertAlmostEqual(stats["BATCHSIZE:32"]["avg_pad_ratio"], 0.375)
self.assertAlmostEqual(stats["SEQLEN:128"]["avg_pad_ratio"], 0.25)
def test_npu_shape_handling_records_cleanup_keys_per_gear(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [16, 32], "dimensions": 0, "indices": [0]},
{"type": "SEQLEN", "gears": [64, 128], "dimensions": [1], "indices": [0]},
],
adaptive_configs={"window_seconds": 300.0},
)
manager = shape_handling.adaptive_manager
snapshot = manager.get_snapshot()
fake_key = 42
manager.record_event(
raw_gear_values=[[20], [96]],
mapped_gear_values=[[32], [128]],
pad_ratios=[[0.375], [0.25]],
split_ratios=[[0.0], [0.0]],
event_ts=100.0,
cleanup_key=fake_key,
)
self.assertEqual(manager._states["BATCHSIZE:32"].cleanup_keys, {fake_key})
self.assertEqual(manager._states["SEQLEN:128"].cleanup_keys, {fake_key})
self.assertIsInstance(next(iter(manager._states["BATCHSIZE:32"].cleanup_keys)), int)
def test_npu_shape_handling_commit_update_uses_recorded_cleanup_keys(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [16, 32], "dimensions": 0, "indices": [0]},
],
adaptive_configs={"window_seconds": 300.0},
)
manager = shape_handling.adaptive_manager
snapshot = manager.get_snapshot()
fake_key = 42
manager.record_event(
raw_gear_values=[[20]],
mapped_gear_values=[[32]],
pad_ratios=[[0.375]],
split_ratios=[[0.0]],
event_ts=100.0,
cleanup_key=fake_key,
)
_, removed_keys = manager.commit_update(
configs=[{"type": "BATCHSIZE", "gears": [16], "dimensions": 0, "indices": [0], "policy": "CUSTOM"}],
removed_gears=["BATCHSIZE:32"],
now_ts=101.0,
)
self.assertEqual(removed_keys, [fake_key])
self.assertIsInstance(removed_keys[0], int)
self.assertNotIn("BATCHSIZE:32", manager._states)
def test_npu_shape_handling_builds_separate_pad_and_split_sample_counts(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [16, 32, 64], "dimensions": 0, "indices": [0]},
],
adaptive_configs={"window_seconds": 300.0},
)
manager = shape_handling.adaptive_manager
snapshot = manager.get_snapshot()
manager.record_event([[20]], [[32]], [[0.375]], [[0.0]], 100.0)
manager.record_event([[80]], [[64]], [[0.0]], [[0.20]], 101.0)
stats = manager.build_stats_snapshot(101.0)
self.assertEqual(stats["BATCHSIZE:32"]["pad_sample_count"], 1)
self.assertEqual(stats["BATCHSIZE:32"]["split_sample_count"], 0)
self.assertEqual(stats["BATCHSIZE:64"]["pad_sample_count"], 0)
self.assertEqual(stats["BATCHSIZE:64"]["split_sample_count"], 1)
def test_npu_shape_handling_builds_two_dim_stats_per_dimension(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [16, 32], "dimensions": 0, "indices": [0]},
{"type": "SEQLEN", "gears": [64, 128], "dimensions": [1], "indices": [0]},
],
adaptive_configs={"window_seconds": 300.0},
)
manager = shape_handling.adaptive_manager
snapshot = manager.get_snapshot()
manager.record_event([[20], [96]], [[32], [128]], [[0.375], [0.25]], [[0.0], [0.0]], 100.0)
manager.record_event([[24], [112]], [[32], [128]], [[0.25], [0.125]], [[0.0], [0.0]], 101.0)
stats = manager.build_stats_snapshot(101.0)
self.assertEqual(stats["BATCHSIZE:32"]["sample_count"], 2)
self.assertEqual(stats["BATCHSIZE:32"]["raw_samples"], [20, 24])
self.assertEqual(stats["SEQLEN:128"]["sample_count"], 2)
self.assertEqual(stats["SEQLEN:128"]["raw_samples"], [96, 112])
def test_npu_shape_handling_recent_use_protect_skips_recent_hit_gear(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [8, 16, 32], "dimensions": 0, "indices": [0]},
],
adaptive_configs={
"recent_use_protect_seconds": 60.0,
"min_gear_count_per_type": 1,
},
)
manager = shape_handling.adaptive_manager
snapshot = manager.get_snapshot()
manager.record_event([[20]], [[16]], [[0.375]], [[0.0]], 100.0)
stats = manager.build_stats_snapshot(120.0)
breakdowns = manager.scorer.build_score_breakdown(manager.get_snapshot(), stats)
candidates = manager.worker.build_eviction_candidates(
breakdowns,
manager.get_snapshot(),
stats,
120.0,
)
self.assertEqual(candidates, {"BATCHSIZE": "BATCHSIZE:8"})
def test_npu_shape_handling_eviction_protection_skips_gear(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [8, 16, 32], "dimensions": 0, "indices": [0]},
],
adaptive_configs={
"recent_use_protect_seconds": 300.0,
"min_gear_count_per_type": 1,
},
)
manager = shape_handling.adaptive_manager
snapshot = manager.get_snapshot()
manager.record_event([[20]], [[32]], [[0.375]], [[0.0]], 0.0)
manager.protect_gear_from_eviction("BATCHSIZE:16", 500.0)
stats = manager.build_stats_snapshot(500.0)
breakdowns = manager.scorer.build_score_breakdown(manager.get_snapshot(), stats)
candidates = manager.worker.build_eviction_candidates(
breakdowns,
manager.get_snapshot(),
stats,
500.0,
)
self.assertEqual(candidates, {"BATCHSIZE": "BATCHSIZE:8"})
def test_npu_shape_handling_zero_usage_gears_are_eviction_candidates(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [16, 32], "dimensions": 0, "indices": [0]},
],
adaptive_configs={
"recent_use_protect_seconds": 0.0,
"min_gear_count_per_type": 1,
},
)
manager = shape_handling.adaptive_manager
snapshot = manager.get_snapshot()
manager.record_event([[30]], [[32]], [[0.0625]], [[0.0]], 100.0)
stats = manager.build_stats_snapshot(120.0)
breakdowns = manager.scorer.build_score_breakdown(manager.get_snapshot(), stats)
candidates = manager.worker.build_eviction_candidates(
breakdowns,
manager.get_snapshot(),
stats,
120.0,
)
self.assertEqual(candidates, {"BATCHSIZE": "BATCHSIZE:16"})
def test_npu_shape_handling_update_adds_new_gear(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [16, 32], "dimensions": 0, "indices": [0]},
{"type": "SEQLEN", "gears": [64], "dimensions": [1], "indices": [0]},
],
adaptive_configs={
"recent_use_protect_seconds": 1.0,
"min_samples_per_gear": 2,
"add_min_samples": 2,
"pad_add_threshold": 0.20,
"recent_use_protect_seconds": 0.0,
},
)
manager = shape_handling.adaptive_manager
snapshot = manager.get_snapshot()
manager.record_event([[20], [96]], [[32], [64]], [[0.375], [0.50]], [[0.0], [0.0]], 100.0)
manager.record_event([[20], [96]], [[32], [64]], [[0.375], [0.50]], [[0.0], [0.0]], 101.0)
manager.worker.run_once(150.0)
latest_snapshot = manager.get_snapshot()
self.assertIn(96, latest_snapshot.active_gears["SEQLEN"])
def test_npu_shape_handling_new_zero_usage_gear_respects_recent_create_protection(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [16, 32, 64], "min_size": 1, "dimensions": 0, "indices": [0]},
],
adaptive_configs={
"recent_use_protect_seconds": 1.0,
"add_min_samples": 2,
"pad_add_threshold": 0.20,
"recent_use_protect_seconds": 60.0,
"min_gear_count_per_type": 1,
},
)
manager = shape_handling.adaptive_manager
snapshot = manager.get_snapshot()
manager.record_event([[20]], [[32]], [[0.375]], [[0.0]], 10.0)
manager.record_event([[20]], [[32]], [[0.375]], [[0.0]], 11.0)
manager.worker.run_once(100.0)
latest_snapshot = manager.get_snapshot()
self.assertIn(20, latest_snapshot.active_gears["BATCHSIZE"])
stats = manager.build_stats_snapshot(120.0)
breakdowns = manager.scorer.build_score_breakdown(latest_snapshot, stats)
candidates = manager.worker.build_eviction_candidates(
breakdowns, latest_snapshot, stats, 120.0,
)
self.assertEqual(stats["BATCHSIZE:20"]["sample_count"], 0)
self.assertEqual(stats["BATCHSIZE:20"]["created_ts"], 100.0)
self.assertEqual(candidates, {"BATCHSIZE": "BATCHSIZE:32"})
def test_npu_shape_handling_update_adds_split_driven_gear(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [64], "dimensions": 0, "indices": [0]},
],
adaptive_configs={
"recent_use_protect_seconds": 1.0,
"add_min_samples": 2,
"pad_add_threshold": 0.90,
"split_add_threshold": 0.10,
"recent_use_protect_seconds": 0.0,
},
)
manager = shape_handling.adaptive_manager
snapshot = manager.get_snapshot()
manager.record_event([[96]], [[64]], [[0.0]], [[0.34]], 100.0)
manager.record_event([[96]], [[64]], [[0.0]], [[0.34]], 101.0)
manager.worker.run_once(150.0)
latest_snapshot = manager.get_snapshot()
self.assertIn(96, latest_snapshot.active_gears["BATCHSIZE"])
def test_npu_shape_handling_two_dim_addition_candidates_are_built_per_dimension(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [8], "dimensions": 0, "indices": [0]},
{"type": "SEQLEN", "gears": [32], "dimensions": [1], "indices": [0]},
],
adaptive_configs={
"recent_use_protect_seconds": 1.0,
"add_min_samples": 2,
"split_add_threshold": 0.10,
"recent_use_protect_seconds": 0.0,
},
)
manager = shape_handling.adaptive_manager
snapshot = manager.get_snapshot()
manager.record_event([[16], [192]], [[8], [32]], [[0.0], [0.0]], [[0.50], [0.83]], 100.0)
manager.record_event([[16], [192]], [[8], [32]], [[0.0], [0.0]], [[0.50], [0.83]], 101.0)
manager.record_event([[48], [64]], [[8], [32]], [[0.0], [0.0]], [[0.83], [0.50]], 102.0)
manager.record_event([[48], [64]], [[8], [32]], [[0.0], [0.0]], [[0.83], [0.50]], 103.0)
stats = manager.build_stats_snapshot(103.0)
candidates = manager.worker.build_addition_candidates(
manager.get_snapshot(),
stats,
)
result = {shape_type: value for _, _, shape_type, value in candidates}
self.assertEqual(result, {"BATCHSIZE": 32, "SEQLEN": 128})
def test_npu_shape_handling_update_evicts_low_value_gear(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [8, 16, 32], "dimensions": 0, "indices": [0]},
{"type": "SEQLEN", "gears": [64], "dimensions": [1], "indices": [0]},
],
adaptive_configs={
"recent_use_protect_seconds": 1.0,
"min_samples_per_gear": 1,
"min_gear_count_per_type": 2,
"recent_use_protect_seconds": 0.0,
"replace_loss_threshold": 1.0,
},
)
manager = shape_handling.adaptive_manager
snapshot = manager.get_snapshot()
manager.record_event([[8], [64]], [[8], [64]], [[0.0], [0.0]], [[0.0], [0.0]], 10.0)
manager.record_event([[8], [64]], [[8], [64]], [[0.0], [0.0]], [[0.0], [0.0]], 11.0)
manager.record_event([[10], [64]], [[16], [64]], [[0.375], [0.0]], [[0.0], [0.0]], 12.0)
manager.record_event([[32], [64]], [[32], [64]], [[0.0], [0.0]], [[0.0], [0.0]], 13.0)
manager.worker.run_once(20.0)
latest_snapshot = manager.get_snapshot()
self.assertNotIn(16, latest_snapshot.active_gears["BATCHSIZE"])
self.assertIn(8, latest_snapshot.active_gears["BATCHSIZE"])
self.assertIn(32, latest_snapshot.active_gears["BATCHSIZE"])
def test_npu_shape_handling_two_dim_evicts_per_dimension(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [8, 16, 32], "dimensions": 0, "indices": [0]},
{"type": "SEQLEN", "gears": [32, 64, 128], "dimensions": [1], "indices": [0]},
],
adaptive_configs={
"recent_use_protect_seconds": 1.0,
"min_samples_per_gear": 1,
"min_gear_count_per_type": 2,
"add_min_samples": 10,
"recent_use_protect_seconds": 0.0,
"replace_loss_threshold": 1.0,
},
)
manager = shape_handling.adaptive_manager
snapshot = manager.get_snapshot()
manager.record_event([[8], [64]], [[8], [64]], [[0.0], [0.0]], [[0.0], [0.0]], 18.0)
manager.record_event([[10], [96]], [[16], [128]], [[0.375], [0.25]], [[0.0], [0.0]], 19.0)
manager.record_event([[32], [64]], [[32], [64]], [[0.0], [0.0]], [[0.0], [0.0]], 19.0)
manager.worker.run_once(20.0)
latest_snapshot = manager.get_snapshot()
self.assertEqual(latest_snapshot.active_gears["BATCHSIZE"], [8, 32])
self.assertEqual(latest_snapshot.active_gears["SEQLEN"], [64, 128])
def test_npu_shape_handling_resource_pressure_triggers_update(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [16, 32], "dimensions": 0, "indices": [0]},
],
adaptive_configs={
"recent_use_protect_seconds": 300.0,
"device_memory_usage_threshold_ratio": 0.80,
},
)
manager = shape_handling.adaptive_manager
with mock.patch("torch_npu.utils._adaptive_gears.torch.npu.mem_get_info", return_value=(20, 100)):
budget = manager.build_resource_budget()
self.assertTrue(budget["device_memory_usage_high"])
with mock.patch("torch_npu.utils._adaptive_gears.torch.npu.mem_get_info", return_value=(70, 100)):
budget = manager.build_resource_budget()
self.assertFalse(budget["device_memory_usage_high"])
def test_npu_shape_handling_resource_budget_uses_device_ratio(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [16, 32], "dimensions": 0, "indices": [0]},
],
adaptive_configs={
"device_memory_usage_threshold_ratio": 0.60,
},
)
manager = shape_handling.adaptive_manager
with mock.patch("torch_npu.utils._adaptive_gears.torch.npu.mem_get_info", return_value=(40, 100)):
budget = manager.build_resource_budget()
self.assertAlmostEqual(budget["device_memory_usage_ratio"], 0.60)
self.assertEqual(budget["device_memory_usage_threshold_ratio"], 0.60)
self.assertTrue(budget["device_memory_usage_high"])
def test_npu_shape_handling_device_pressure_blocks_direct_add(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [64], "dimensions": 0, "indices": [0]},
],
adaptive_configs={
"recent_use_protect_seconds": 1.0,
"add_min_samples": 2,
"pad_add_threshold": 0.20,
"device_memory_usage_threshold_ratio": 0.50,
"min_gear_count_per_type": 1,
"recent_use_protect_seconds": 0.0,
},
)
manager = shape_handling.adaptive_manager
snapshot = manager.get_snapshot()
manager.record_event([[40]], [[64]], [[0.375]], [[0.0]], 100.0)
manager.record_event([[40]], [[64]], [[0.375]], [[0.0]], 101.0)
with mock.patch("torch_npu.utils._adaptive_gears.torch.npu.mem_get_info", return_value=(40, 100)):
manager.worker.run_once(150.0)
latest_snapshot = manager.get_snapshot()
self.assertEqual(latest_snapshot.active_gears["BATCHSIZE"], [64])
def test_npu_shape_handling_max_gear_guard_blocks_unsafe_delete(self):
shape_handling = torch_npu._inductor.NPUShapeHandling(
configs=[
{"type": "BATCHSIZE", "gears": [32, 64], "dimensions": 0, "indices": [0]},
],
adaptive_configs={
"recent_use_protect_seconds": 0.0,
"replace_loss_threshold": 1.0,
"min_gear_count_per_type": 1,
},
)
manager = shape_handling.adaptive_manager
snapshot = manager.get_snapshot()
manager.record_event([[40]], [[64]], [[0.375]], [[0.0]], 100.0)
stats = manager.build_stats_snapshot(100.0)
breakdowns = manager.scorer.build_score_breakdown(manager.get_snapshot(), stats)
candidates = manager.worker.build_eviction_candidates(
breakdowns,
manager.get_snapshot(),
stats,
100.0,
)
self.assertNotIn("BATCHSIZE:64", candidates.values())
class TestDynamicShapeCompile(TestCase):
def test_npu_dynamic_shape_reuse_with_no_bucket(self):
compiled_fn = torch.compile(
model_fn,
backend='inductor',
dynamic=False,
)
test_shapes = [(3, 20), (4, 20), (5, 20), (6, 20)]
if hasattr(torch._inductor.metrics.generated_kernel_count, 'reset'):
torch._inductor.metrics.generated_kernel_count.reset()
else:
torch._inductor.metrics.generated_kernel_count = 0
for shape in test_shapes:
A = torch.randn(shape, device=device)
B = torch.randn(shape, device=device)
out = compiled_fn(A, B)
self.assertTrue(torch.allclose(out, A + B))
compile_count = torch._inductor.metrics.generated_kernel_count
print(f"\n[结果] Inductor 编译次数 (generated_kernel_count): {compile_count}")
self.assertEqual(compile_count, 4)
def test_npu_dynamic_shape_reuse_with_bucket(self):
compiled_fn = torch.compile(
model_fn,
backend='inductor',
dynamic=False,
options=shape_options
)
test_shapes = [(3, 32), (4, 32), (5, 32), (6, 32)]
if hasattr(torch._inductor.metrics.generated_kernel_count, 'reset'):
torch._inductor.metrics.generated_kernel_count.reset()
else:
torch._inductor.metrics.generated_kernel_count = 0
for shape in test_shapes:
A = torch.randn(shape, device=device)
B = torch.randn(shape, device=device)
out = compiled_fn(A, B)
self.assertTrue(torch.allclose(out, A + B))
compile_count = torch._inductor.metrics.generated_kernel_count
print(f"\n[结果] Inductor 编译次数 (generated_kernel_count): {compile_count}")
self.assertEqual(compile_count, 2)
def test_npu_dynamic_shape_reuse_with_symbolic_shape(self):
compiled_fn = torch.compile(
model_fn,
backend='inductor',
dynamic=True,
)
test_shapes = [(3, 32), (4, 32), (5, 32), (6, 32)]
if hasattr(torch._inductor.metrics.generated_kernel_count, 'reset'):
torch._inductor.metrics.generated_kernel_count.reset()
else:
torch._inductor.metrics.generated_kernel_count = 0
for shape in test_shapes:
A = torch.randn(shape, device=device)
B = torch.randn(shape, device=device)
out = compiled_fn(A, B)
self.assertTrue(torch.allclose(out, A + B))
compile_count = torch._inductor.metrics.generated_kernel_count
print(f"\n[结果] Inductor 编译次数 (generated_kernel_count): {compile_count}")
self.assertEqual(compile_count, 1)
def test_npu_dynamic_shape_reuse_with_symbolic_shape_and_bucket(self):
compiled_fn = torch.compile(
model_fn,
backend='inductor',
dynamic=True,
options=shape_options
)
test_shapes = [(3, 32), (4, 32), (5, 32), (6, 32)]
if hasattr(torch._inductor.metrics.generated_kernel_count, 'reset'):
torch._inductor.metrics.generated_kernel_count.reset()
else:
torch._inductor.metrics.generated_kernel_count = 0
for shape in test_shapes:
A = torch.randn(shape, device=device)
B = torch.randn(shape, device=device)
out = compiled_fn(A, B)
self.assertTrue(torch.allclose(out, A + B))
compile_count = torch._inductor.metrics.generated_kernel_count
print(f"\n[结果] Inductor 编译次数 (generated_kernel_count): {compile_count}")
self.assertEqual(compile_count, 1)
def test_npu_invalid_shape_options_key_error(self):
invalid_shape_options = {
"enable_shape_handling": True,
"shape_handling_configs": [
{
"type": "BATCHSIZE",
"min_size": 1024,
"max_size": 1,
"policy": "TIMES",
}
]
}
try:
compiled_fn = torch.compile(
model_fn,
backend='inductor',
dynamic=False,
options=invalid_shape_options
)
A = torch.randn((2, 32), device=device)
B = torch.randn((2, 32), device=device)
with self.assertRaises((ValueError, RuntimeError, TypeError)) as cm:
compiled_fn(A, B)
print(f"\n[成功捕获异常]: {cm.exception}")
except Exception as e:
print(f"\n[编译阶段直接报错]: {e}")
self.assertIsInstance(e, (ValueError, RuntimeError, TypeError))
def test_npu_malformed_option_type(self):
malformed_options = {
"shape_handling_configs": "this should be a list, not a string"
}
with self.assertRaises(Exception):
compiled_fn = torch.compile(model_fn, options=malformed_options)
compiled_fn(torch.randn(2, device="npu"), torch.randn(2, device="npu"))
def test_npu_shape_options_with_handling_disabled(self):
shape_options_off = {
"enable_shape_handling": False,
"shape_handling_configs": [
{
"type": "BATCHSIZE",
"min_size": 1,
"max_size": 1024,
"policy": "TIMES",
}
]
}
try:
compiled_fn = torch.compile(
model_fn,
backend='inductor',
dynamic=False,
options=shape_options_off
)
A = torch.randn((2, 32), device=device)
B = torch.randn((2, 32), device=device)
compiled_fn(A, B)
except Exception as e:
self.fail(f"torch.compile raised {type(e).__name__} unexpectedly: {e}")
def test_npu_shape_handling_whit_mutil_compile(self):
try:
compiled_fn = torch.compile(
model_fn,
backend='inductor',
dynamic=False,
options=shape_options
)
compiled_fn(torch.randn((1025, 32), device=device), torch.randn((1025, 32), device=device))
compiled_fn(torch.randn((2048, 32), device=device), torch.randn((2048, 32), device=device))
compiled_fn(torch.randn((10240, 32), device=device), torch.randn((10240, 32), device=device))
except Exception as e:
self.fail(f"torch.compile raised {type(e).__name__} unexpectedly: ")
class TestUnifiedCopy(TestCase):
def test_none_and_simple_types(self):
"""Test unified_copy with None, list, dict, and tuple."""
self.assertIsNone(unified_copy(None))
lst = [1, 2, 3]
copied_lst = unified_copy(lst)
self.assertEqual(lst, copied_lst)
self.assertIsNot(lst, copied_lst)
d = {"a": 1, "b": [2, 3]}
copied_d = unified_copy(d)
self.assertEqual(d, copied_d)
self.assertIsNot(d, copied_d)
t = (1, [2, 3])
copied_t = unified_copy(t)
self.assertEqual(t, copied_t)
self.assertIsInstance(copied_t, tuple)
def test_nested_structure_with_tensor(self):
"""Test nested structure containing NPU tensor."""
original = {
"data": [torch.tensor([1.0, 2.0]), 42],
"meta": {"shape": (2,)}
}
copied = unified_copy(original)
self.assertEqual(original["meta"], copied["meta"])
self.assertTrue(torch.equal(original["data"][0], copied["data"][0]))
self.assertEqual(original["data"][1], copied["data"][1])
original["data"][0][0] = 888.0
self.assertNotEqual(copied["data"][0][0].item(), 888.0)
def test_deepcopy_failure_returns_original(self):
class NonCopyable:
def __deepcopy__(self, memo):
raise TypeError("deepcopy not supported")
obj = NonCopyable()
copied = unified_copy(obj)
self.assertIs(copied, obj)
_ST_ADAPTIVE_CONFIGS = {
"window_seconds": 300.0,
"recent_use_protect_seconds": 0,
"pad_add_threshold": 0.20,
"split_add_threshold": 0.20,
"add_min_samples": 2,
"min_samples_per_gear": 1,
"min_gear_count_per_type": 2,
"replace_loss_threshold": 1.0,
"update_interval_seconds": 9999.0,
}
_ST_DAEMON_ADAPTIVE_CONFIGS = dict(_ST_ADAPTIVE_CONFIGS)
_ST_DAEMON_ADAPTIVE_CONFIGS["update_interval_seconds"] = 0.1
def _make_adaptive_options(shape_configs, adaptive_configs=None):
opts = {
"enable_shape_handling": True,
"shape_handling_configs": shape_configs,
}
if adaptive_configs is not None:
opts["shape_handling_dict"] = {"adaptive_gears": adaptive_configs}
return opts
def _run_model(compiled_fn, shape):
A = torch.randn(shape, device=device)
B = torch.randn(shape, device=device)
out = compiled_fn(A, B)
return out, A, B
class TestAdaptiveGearsCompileST(TestCase):
def setUp(self):
torch._dynamo.reset()
self._captured_managers = []
self._original_sh_init = shape_handling_module.NPUShapeHandling.__init__
test_self = self
def capturing_init(sh_self, *args, **kwargs):
test_self._original_sh_init(sh_self, *args, **kwargs)
if sh_self.adaptive_manager is not None:
test_self._captured_managers.append(sh_self.adaptive_manager)
shape_handling_module.NPUShapeHandling.__init__ = capturing_init
def tearDown(self):
shape_handling_module.NPUShapeHandling.__init__ = self._original_sh_init
for mgr in self._captured_managers:
try:
mgr.shutdown()
except Exception:
pass
self._captured_managers.clear()
torch._dynamo.reset()
def _get_manager(self):
self.assertTrue(
len(self._captured_managers) > 0,
"No AdaptiveGearRuntime was captured — torch.compile did not create one",
)
return self._captured_managers[-1]
def _stop_daemon(self, manager):
manager._shutdown_event.set()
manager._worker_thread.join(timeout=2.0)
def test_adaptive_gears_basic_compile_and_compute(self):
shape_configs = [
{"type": "BATCHSIZE", "gears": [16, 32], "dimensions": 0, "indices": [0, 1]},
]
options = _make_adaptive_options(shape_configs, _ST_ADAPTIVE_CONFIGS)
compiled_fn = torch.compile(
model_fn,
backend="inductor",
dynamic=False,
options=options,
)
for shape in [(8, 32), (24, 32), (10, 32)]:
out, A, B = _run_model(compiled_fn, shape)
self.assertTrue(
torch.allclose(out, A + B),
f"Output mismatch for shape {shape}",
)
manager = self._get_manager()
self.assertTrue(
manager._worker_thread.is_alive(),
"Daemon worker thread should be alive",
)
def test_adaptive_gears_event_recording_and_stats(self):
shape_configs = [
{"type": "BATCHSIZE", "gears": [16, 32], "dimensions": 0, "indices": [0, 1]},
]
options = _make_adaptive_options(shape_configs, _ST_ADAPTIVE_CONFIGS)
compiled_fn = torch.compile(
model_fn,
backend="inductor",
dynamic=False,
options=options,
)
manager = self._get_manager()
for _ in range(3):
out, A, B = _run_model(compiled_fn, (24, 32))
self.assertTrue(torch.allclose(out, A + B))
stats = manager.build_stats_snapshot(time.time())
self.assertIn("BATCHSIZE:32", stats)
self.assertEqual(
stats["BATCHSIZE:32"]["sample_count"], 6,
"Exactly 6 samples should be recorded for gear 32 (3 calls × 2 tensors)",
)
self.assertEqual(
stats["BATCHSIZE:32"]["pad_sample_count"], 6,
"All 6 events should be classified as padding",
)
self.assertAlmostEqual(
stats["BATCHSIZE:32"]["avg_pad_ratio"], 0.25, places=2,
msg="avg_pad_ratio should be (32-24)/32 = 0.25",
)
self.assertEqual(
stats["BATCHSIZE:32"]["raw_samples"], [24, 24, 24, 24, 24, 24],
"raw_samples should have 6 entries (3 calls × 2 tensors)",
)
def test_adaptive_gears_gear_eviction_synchronous(self):
shape_configs = [
{"type": "BATCHSIZE", "gears": [16, 32, 64], "dimensions": 0, "indices": [0, 1]},
]
options = _make_adaptive_options(shape_configs, _ST_ADAPTIVE_CONFIGS)
compiled_fn = torch.compile(
model_fn,
backend="inductor",
dynamic=False,
options=options,
)
manager = self._get_manager()
self._stop_daemon(manager)
for _ in range(5):
out, A, B = _run_model(compiled_fn, (24, 32))
self.assertTrue(torch.allclose(out, A + B))
manager.worker.run_once(time.time())
snapshot_after = manager.get_snapshot()
gears_after = sorted(snapshot_after.active_gears["BATCHSIZE"])
self.assertEqual(
gears_after, [24, 32, 64],
f"Expected [24, 32, 64] after eviction+addition, got {gears_after}",
)
out, A, B = _run_model(compiled_fn, (8, 32))
self.assertTrue(
torch.allclose(out, A + B),
"Computation should be correct after gear eviction",
)
def test_adaptive_gears_gear_addition_synchronous(self):
shape_configs = [
{"type": "BATCHSIZE", "gears": [64], "dimensions": 0, "indices": [0, 1]},
]
add_configs = dict(_ST_ADAPTIVE_CONFIGS)
add_configs["min_gear_count_per_type"] = 1
options = _make_adaptive_options(shape_configs, add_configs)
compiled_fn = torch.compile(
model_fn,
backend="inductor",
dynamic=False,
options=options,
)
manager = self._get_manager()
self._stop_daemon(manager)
for _ in range(3):
out, A, B = _run_model(compiled_fn, (20, 32))
self.assertTrue(torch.allclose(out, A + B))
manager.worker.run_once(time.time())
snapshot_after = manager.get_snapshot()
gears_after = snapshot_after.active_gears["BATCHSIZE"]
self.assertEqual(
len(gears_after), 2,
f"A new gear should be added, got {gears_after}",
)
self.assertIn(
20, gears_after,
f"New gear 20 (median of raw samples) should be added, got {gears_after}",
)
out, A, B = _run_model(compiled_fn, (20, 32))
self.assertTrue(
torch.allclose(out, A + B),
"Computation should be correct after gear addition",
)
def test_adaptive_gears_graph_cleanup_after_eviction(self):
from torch_npu.npu._graph_resource_pool import GraphResourcePool
shape_configs = [
{"type": "BATCHSIZE", "gears": [16, 32, 64], "dimensions": 0, "indices": [0, 1]},
]
options = _make_adaptive_options(shape_configs, _ST_ADAPTIVE_CONFIGS)
options["triton.cudagraphs"] = True
options["triton.cudagraph_trees"] = True
compiled_fn = torch.compile(
model_fn,
backend="inductor",
dynamic=False,
options=options,
)
manager = self._get_manager()
self._stop_daemon(manager)
for shape in [(8, 32), (24, 32), (48, 32)]:
for _ in range(2):
out, A, B = _run_model(compiled_fn, shape)
self.assertTrue(torch.allclose(out, A + B))
device_index = torch.npu.current_device()
pool = GraphResourcePool.get_pool(device_index)
self.assertGreater(
pool.entry_count, 0,
f"Pool should have entries after compilation, got {pool.entry_count}",
)
manager.worker.run_once(time.time())
snapshot_after = manager.get_snapshot()
gears_after = sorted(snapshot_after.active_gears["BATCHSIZE"])
self.assertEqual(
gears_after, [8, 24, 32, 48, 64],
f"Expected [8, 24, 32, 48, 64] after eviction+additions, got {gears_after}",
)
out, A, B = _run_model(compiled_fn, (24, 32))
self.assertTrue(
torch.allclose(out, A + B),
"Computation should be correct after graph cleanup",
)
def test_adaptive_gears_daemon_thread_triggers_update(self):
shape_configs = [
{"type": "BATCHSIZE", "gears": [16, 32, 64], "dimensions": 0, "indices": [0, 1]},
]
options = _make_adaptive_options(shape_configs, _ST_DAEMON_ADAPTIVE_CONFIGS)
compiled_fn = torch.compile(
model_fn,
backend="inductor",
dynamic=False,
options=options,
)
manager = self._get_manager()
original_gears = sorted(manager.get_snapshot().active_gears["BATCHSIZE"])
for _ in range(5):
out, A, B = _run_model(compiled_fn, (24, 32))
self.assertTrue(torch.allclose(out, A + B))
self.assertTrue(
manager._worker_thread.is_alive(),
"Daemon thread should be alive",
)
deadline = time.time() + 5.0
current_gears = original_gears
while time.time() < deadline:
current_gears = sorted(manager.get_snapshot().active_gears["BATCHSIZE"])
if current_gears != original_gears:
break
time.sleep(0.1)
self.assertNotEqual(
current_gears, original_gears,
f"Daemon thread should trigger gear update within 5s. "
f"Original: {original_gears}, Current: {current_gears}",
)
out, A, B = _run_model(compiled_fn, (24, 32))
self.assertTrue(
torch.allclose(out, A + B),
"Computation should be correct after daemon-triggered update",
)
instantiate_parametrized_tests(TestShapeHandling)
instantiate_parametrized_tests(TestShapeHandlingBranchCoverage)
instantiate_parametrized_tests(TestAsyncWorkerAndConcurrency)
instantiate_parametrized_tests(TestAdaptiveShapeHandling)
instantiate_parametrized_tests(TestUnifiedCopy)
instantiate_parametrized_tests(TestDynamicShapeCompile)
instantiate_parametrized_tests(TestAdaptiveGearsCompileST)
if __name__ == '__main__':
run_tests()