已合并
fix: 修复 clamp_npu_output_size 空张量短路导致 broadcast shape 计算错误 #5687
wangqi_ai创建于 14 天前
fix: 修复 clamp_npu_output_size 空张量短路导致 broadcast shape 计算错误 #5687
已合并
共 2 个文件变更+54-9
| @@ -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 | |||
| 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()); |
| 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()); |
| @@ -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 | ||
| 124 | if __name__ == "__main__": | 176 | if __name__ == "__main__": |


删除上方原有的 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 维场景下同样成立,未引入新问题。