已合并
fix: 修复 clamp_npu_output_size 空张量短路导致 broadcast shape 计算错误 #5687
fix: 修复 clamp_npu_output_size 空张量短路导致 broadcast shape 计算错误 #5687
已合并
wangqi_ai创建于 14 天前
2 个文件变更+54-9
Mop_plugin/utils/KernelNpuOutputSize.cpp+0-7
@@ -1792,13 +1792,6 @@ c10::SmallVector<int64_t, SIZE> clamp_npu_output_size(
1792 min.has_value() || max.has_value(),1792 min.has_value() || max.has_value(),
1793 "torch.clamp: At least one of 'min' or 'max' must not be None",1793 "torch.clamp: At least one of 'min' or 'max' must not be None",
1794 OPS_ERROR(ErrCode::PARAM));1794 OPS_ERROR(ErrCode::PARAM));
1795- if (self.numel() == 0) {
1796- c10::SmallVector<int64_t, SIZE> empty_sizes;
1797- for (int64_t i = 0; i < self.dim(); ++i) {
1798- empty_sizes.push_back(self.size(i));
1799- }
1800- return empty_sizes;
1801- }
1802 if (min.has_value() && max.has_value()) {1795 if (min.has_value() && max.has_value()) {
W
Wwangqi_ai14 天前

删除上方原有的 self.numel()==0 空张量短路分支是正确的。broadcast_ops_npu_output_size 内部走 at::infer_size,已具备 0 维与空张量的处理能力:对 self=(0,)、min=(3,1) 这类需要 left-pad 及维度扩展的场景会正确返回 (3,0);对 self=(0,)、min=(3,) 这类不可 broadcast 的输入会抛出异常而非静默放行,与 PyTorch 上游 torch.clamp.Tensor 的语义一致。min 与 max 同时存在时采用的两步 broadcast broadcast(broadcast(self,min),max) 满足结合律,结果与一次性三方 broadcast 等价,0 维场景下同样成立,未引入新问题。

likedislike
wangqi_ai
11 天前 评论:
1803 auto brc_shape_min = broadcast_ops_npu_output_size(self.sizes(), min.value().sizes());1796 auto brc_shape_min = broadcast_ops_npu_output_size(self.sizes(), min.value().sizes());
W
Wwangqi_ai14 天前

建议补充回归测试。现有 test/test_base_ops/test_clamp.py 中 TestClamp 整体被 @unittest.skip 跳过,且 test_clamp_tensor 的 shape 用例均未覆盖 self.numel()==0 且需 broadcast 的场景,本 PR 修复的正确性目前仅靠 PR 描述中的手工验证表保证。建议在该用例中补入描述里列出的几组场景(如 self=(0,)、min=(3,1) 期望 (3,0);self=(0,)、min=(3,) 期望抛 broadcast 异常),避免后续误改回短路分支而无人发现。

likedislike
wangqi_ai
11 天前 评论:
1804 return broadcast_ops_npu_output_size(brc_shape_min, max.value().sizes());1797 return broadcast_ops_npu_output_size(brc_shape_min, max.value().sizes());
Mtest/test_base_ops/test_clamp.py+54-2
@@ -107,7 +107,7 @@ class TestClamp(TestCase):
107 npu_out_output = self.npu_op_exec_out(input_npu, min_npu, max_npu, out_npu)107 npu_out_output = self.npu_op_exec_out(input_npu, min_npu, max_npu, out_npu)
108 self.assertRtolEqual(cpu_out_output, npu_out_output)108 self.assertRtolEqual(cpu_out_output, npu_out_output)
109 else:109 else:
110- with self.asserctRaises(RuntimeError) as cpu_err:110+ with self.assertRaises(RuntimeError) as cpu_err:
111 self.cpu_inp_op_exec(input_cpu, min_cpu, max_cpu)111 self.cpu_inp_op_exec(input_cpu, min_cpu, max_cpu)
112 self.assertTrue("can't be cast to the desired output" in str(cpu_err.exception))112 self.assertTrue("can't be cast to the desired output" in str(cpu_err.exception))
113 with self.assertRaises(RuntimeError) as npu_err:113 with self.assertRaises(RuntimeError) as npu_err:
@@ -118,7 +118,59 @@ class TestClamp(TestCase):
118 self.assertTrue("can't be cast to the desired output" in str(cpu_err.exception))118 self.assertTrue("can't be cast to the desired output" in str(cpu_err.exception))
119 with self.assertRaises(RuntimeError) as npu_err:119 with self.assertRaises(RuntimeError) as npu_err:
120 self.npu_op_exec_out(input_npu, min_npu, max_npu, out_npu)120 self.npu_op_exec_out(input_npu, min_npu, max_npu, out_npu)
121- self.assertTrue("an't be cast to the desired output" in str(npu_err.exception))121+ self.assertTrue("can't be cast to the desired output" in str(npu_err.exception))
122+ 
123+ def test_clamp_tensor_empty_broadcast(self):
124+ # Regression: clamp_npu_output_size must not short-circuit empty self.
125+ # Broadcast with min/max must still apply (expand dims / reject invalid).
126+ # case 1: self.ndim < min.ndim, broadcast should expand dims -> (3, 0)
127+ input_cpu = torch.tensor([], dtype=torch.float32).reshape(1, 0)
128+ input_npu = input_cpu.npu()
129+ min_cpu = torch.tensor([[0.0]] * 3, dtype=torch.float32)
130+ min_npu = min_cpu.npu()
131+ max_cpu = torch.tensor([[1.0]] * 3, dtype=torch.float32)
132+ max_npu = max_cpu.npu()
133+ cpu_output = self.cpu_op_exec(input_cpu, min_cpu, max_cpu)
134+ npu_output = self.npu_op_exec(input_npu, min_npu, max_npu)
135+ self.assertEqual(tuple(cpu_output.shape), (3, 0))
136+ self.assertEqual(tuple(npu_output.shape), (3, 0))
137+ self.assertEqual(npu_output.size, 0)
138+ 
139+ # case 2: self.ndim == min.ndim, same dim needs expand -> (3, 0)
140+ input_cpu = torch.tensor([], dtype=torch.float32).reshape(1, 0)
141+ input_npu = input_cpu.npu()
142+ min_cpu = torch.arange(3, dtype=torch.float32).reshape(3, 1)
143+ min_npu = min_cpu.npu()
144+ max_cpu = torch.full((3, 1), 1.0, dtype=torch.float32)
145+ max_npu = max_cpu.npu()
146+ cpu_output = self.cpu_op_exec(input_cpu, min_cpu, max_cpu)
147+ npu_output = self.npu_op_exec(input_npu, min_npu, max_npu)
148+ self.assertEqual(tuple(cpu_output.shape), (3, 0))
149+ self.assertEqual(tuple(npu_output.shape), (3, 0))
150+ 
151+ # case 3: non-broadcastable empty input should raise (not silently pass)
152+ input_cpu = torch.tensor([], dtype=torch.float32)
153+ input_npu = input_cpu.npu()
154+ min_cpu = torch.arange(3, dtype=torch.float32)
155+ min_npu = min_cpu.npu()
156+ max_cpu = torch.full((3,), 1.0, dtype=torch.float32)
157+ max_npu = max_cpu.npu()
158+ with self.assertRaises(RuntimeError):
159+ self.cpu_op_exec(input_cpu, min_cpu, max_cpu)
160+ with self.assertRaises(RuntimeError):
161+ self.npu_op_exec(input_npu, min_npu, max_npu)
162+ 
163+ # case 4: scalar min/max with empty self -> (0,) (unchanged, sanity)
164+ input_cpu = torch.tensor([], dtype=torch.float32)
165+ input_npu = input_cpu.npu()
166+ min_cpu = torch.tensor(0.0, dtype=torch.float32)
167+ min_npu = min_cpu.npu()
168+ max_cpu = torch.tensor(1.0, dtype=torch.float32)
169+ max_npu = max_cpu.npu()
170+ cpu_output = self.cpu_op_exec(input_cpu, min_cpu, max_cpu)
171+ npu_output = self.npu_op_exec(input_npu, min_npu, max_npu)
172+ self.assertEqual(tuple(cpu_output.shape), (0,))
173+ self.assertEqual(tuple(npu_output.shape), (0,))
122 174 
123 175 
124if __name__ == "__main__":176if __name__ == "__main__":