已合并
fix(npu): 对齐searchsorted接口入参为side=right时的正确语义、补齐相关校验和告警信息 #4934
Margaret_wangrui创建于 5月12日
fix(npu): 对齐searchsorted接口入参为side=right时的正确语义、补齐相关校验和告警信息 #4934
已合并
共 7 个文件变更+487-58
| @@ -4427,25 +4427,38 @@ official: | |||
| 4427 | acl_op: all_version | 4427 | acl_op: all_version |
| 4428 | op_api: all_version | 4428 | op_api: all_version |
| 4429 | gen_opapi: | 4429 | gen_opapi: |
| 4430 | + new_params: | ||
| 4431 | + # Comma chain: pre_check and warn must run before right_eff; single entry avoids key-order hazards. | ||
| 4432 | + right_eff: '(op_plugin::searchsorted_validate_scalar_op(sorted_sequence, self, out_int32, right, side, sorter), op_plugin::warn_if_searchsorted_scalar_inputs_noncontiguous(sorted_sequence, sorter), op_plugin::resolve_searchsorted_effective_right(right, side))' | ||
| 4430 | out: | 4433 | out: |
| 4431 | size: 'c10::SmallVector<int64_t, op_infer::SIZE>{}' | 4434 | size: 'c10::SmallVector<int64_t, op_infer::SIZE>{}' |
| 4432 | dtype: 'out_int32 ? at::kInt : at::kLong' | 4435 | dtype: 'out_int32 ? at::kInt : at::kLong' |
| 4433 | - exec: aclnnSearchSorteds, sorted_sequence, self, out_int32, right, sorter, out | 4436 | + exec: aclnnSearchSorteds, sorted_sequence, self, out_int32, right_eff, sorter, out |
| 4434 | 4437 | ||
| 4435 | - func: searchsorted.Tensor(Tensor sorted_sequence, Tensor self, *, bool out_int32=False, bool right=False, str? side=None, Tensor? sorter=None) -> Tensor | 4438 | - func: searchsorted.Tensor(Tensor sorted_sequence, Tensor self, *, bool out_int32=False, bool right=False, str? side=None, Tensor? sorter=None) -> Tensor |
| 4436 | acl_op: all_version | 4439 | acl_op: all_version |
| 4437 | op_api: all_version | 4440 | op_api: all_version |
| 4438 | gen_opapi: | 4441 | gen_opapi: |
| 4439 | - structured_inherit: searchsorted.Tensor_out | 4442 | + new_params: |
| 4443 | + # Functional: out is not a function arg yet when new_params run — use core check only (incl. sorter size). | ||
| 4444 | + right_eff: '(op_plugin::searchsorted_validate_core_no_output(sorted_sequence, self, out_int32, right, side, sorter), op_plugin::warn_if_searchsorted_inputs_noncontiguous(sorted_sequence, self, sorter), op_plugin::resolve_searchsorted_effective_right(right, side))' | ||
| 4445 | + out: | ||
| 4446 | + size: self | ||
| 4447 | + dtype: 'out_int32 ? at::kInt : at::kLong' | ||
| 4448 | + exec: aclnnSearchSorted, sorted_sequence, self, out_int32, right_eff, sorter, out | ||
| 4440 | 4449 | ||
| 4441 | - func: searchsorted.Tensor_out(Tensor sorted_sequence, Tensor self, *, bool out_int32=False, bool right=False, str? side=None, Tensor? sorter=None, Tensor(a!) out) -> Tensor(a!) | 4450 | - func: searchsorted.Tensor_out(Tensor sorted_sequence, Tensor self, *, bool out_int32=False, bool right=False, str? side=None, Tensor? sorter=None, Tensor(a!) out) -> Tensor(a!) |
| 4442 | acl_op: all_version | 4451 | acl_op: all_version |
| 4443 | op_api: all_version | 4452 | op_api: all_version |
| 4444 | gen_opapi: | 4453 | gen_opapi: |
| 4454 | + new_params: | ||
| 4455 | + # Comma chain: full ATen pre_check (incl. out dtype) before warn and before resolve; ensures PyTorch | ||
| 4456 | + # RuntimeError text before npu_preparation::check_tensor / aclnn ("expected dtype Long..."). | ||
| 4457 | + right_eff: '(op_plugin::searchsorted_validate_tensor_out_op(sorted_sequence, self, out, out_int32, right, side, sorter), op_plugin::warn_if_searchsorted_inputs_noncontiguous(sorted_sequence, self, sorter), op_plugin::resolve_searchsorted_effective_right(right, side))' | ||
| 4445 | out: | 4458 | out: |
| 4446 | size: self | 4459 | size: self |
| 4447 | dtype: 'out_int32 ? at::kInt : at::kLong' | 4460 | dtype: 'out_int32 ? at::kInt : at::kLong' |
| 4448 | - exec: aclnnSearchSorted, sorted_sequence, self, out_int32, right, sorter, out | 4461 | + exec: aclnnSearchSorted, sorted_sequence, self, out_int32, right_eff, sorter, out |
| 4449 | 4462 | ||
| 4450 | - func: sgn(Tensor self) -> Tensor | 4463 | - func: sgn(Tensor self) -> Tensor |
| 4451 | acl_op: all_version | 4464 | acl_op: all_version |
| @@ -15,19 +15,18 @@ | |||
| 15 | 15 | ||
| 16 | 16 | ||
| 17 | 17 | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 18 | 21 | ||
| 19 | namespace acl_op { | 22 | namespace acl_op { |
| 20 | using npu_preparation = at_npu::native::OpPreparation; | 23 | using npu_preparation = at_npu::native::OpPreparation; |
| 21 | using npu_utils = at_npu::native::NpuUtils; | 24 | using npu_utils = at_npu::native::NpuUtils; |
| 22 | 25 | ||
| 23 | namespace { | 26 | namespace { |
| 24 | -at::Tensor& searchsorted_out_nocheck( | 27 | + |
| 25 | - at::Tensor& result, | 28 | +at::Tensor &searchsorted_out_nocheck( |
| 26 | - const at::Tensor& sorted_sequence, | 29 | + at::Tensor &result, const at::Tensor &sorted_sequence, const at::Tensor &self, bool out_int32, bool right) { |
| 27 | - const at::Tensor& self, | ||
| 28 | - bool out_int32, | ||
| 29 | - bool right) | ||
| 30 | -{ | ||
| 31 | at::ScalarType scalar_type = out_int32 ? at::kInt : at::kLong; | 30 | at::ScalarType scalar_type = out_int32 ? at::kInt : at::kLong; |
| 32 | at_npu::native::OpCommand cmd; | 31 | at_npu::native::OpCommand cmd; |
| 33 | cmd.Name("SearchSorted") | 32 | cmd.Name("SearchSorted") |
| @@ -41,60 +40,49 @@ at::Tensor& searchsorted_out_nocheck( | |||
| 41 | } | 40 | } |
| 42 | } // namespace | 41 | } // namespace |
| 43 | 42 | ||
| 44 | -at::Tensor& searchsorted_out( | 43 | +at::Tensor &searchsorted_out(const at::Tensor &sorted_sequence, const at::Tensor &self, bool out_int32, bool right, |
| 45 | - const at::Tensor& sorted_sequence, | 44 | + const c10::optional<c10::string_view> side_opt, const c10::optional<at::Tensor> &sorter_opt, at::Tensor &result) { |
| 46 | - const at::Tensor& self, | 45 | + (void)op_plugin::searchsorted_validate_tensor_out_op( |
| 47 | - bool out_int32, | 46 | + sorted_sequence, self, result, out_int32, right, side_opt, sorter_opt); |
| 48 | - bool right, | 47 | + (void)op_plugin::warn_if_searchsorted_inputs_noncontiguous(sorted_sequence, self, sorter_opt); |
| 49 | - const c10::optional<c10::string_view> side_opt, | ||
| 50 | - const c10::optional<at::Tensor>& sorter_opt, | ||
| 51 | - at::Tensor& result) | ||
| 52 | -{ | ||
| 53 | at::ScalarType scalar_type = out_int32 ? at::kInt : at::kLong; | 48 | at::ScalarType scalar_type = out_int32 ? at::kInt : at::kLong; |
| 54 | npu_preparation::CheckOut( | 49 | npu_preparation::CheckOut( |
| 55 | - {sorted_sequence, self}, | 50 | + {sorted_sequence, self}, result, npu_preparation::get_tensor_npu_format(self), scalar_type, self.sizes()); |
| 56 | - result, | 51 | + const bool right_eff = op_plugin::resolve_searchsorted_effective_right(right, side_opt); |
| 57 | - npu_preparation::get_tensor_npu_format(self), | ||
| 58 | - scalar_type, | ||
| 59 | - self.sizes()); | ||
| 60 | if (!npu_utils::check_match(&result)) { | 52 | if (!npu_utils::check_match(&result)) { |
| 61 | at::Tensor contiguous_result = npu_utils::format_contiguous(result); | 53 | at::Tensor contiguous_result = npu_utils::format_contiguous(result); |
| 62 | - searchsorted_out_nocheck(contiguous_result, sorted_sequence, self, out_int32, right); | 54 | + searchsorted_out_nocheck(contiguous_result, sorted_sequence, self, out_int32, right_eff); |
| 63 | npu_utils::format_fresh_view(result, contiguous_result); | 55 | npu_utils::format_fresh_view(result, contiguous_result); |
| 64 | } else { | 56 | } else { |
| 65 | - searchsorted_out_nocheck(result, sorted_sequence, self, out_int32, right); | 57 | + searchsorted_out_nocheck(result, sorted_sequence, self, out_int32, right_eff); |
| 66 | } | 58 | } |
| 67 | 59 | ||
| 68 | return result; | 60 | return result; |
| 69 | } | 61 | } |
| 70 | 62 | ||
| 71 | -at::Tensor searchsorted( | 63 | +at::Tensor searchsorted(const at::Tensor &sorted_sequence, const at::Tensor &self, bool out_int32, bool right, |
| 72 | - const at::Tensor& sorted_sequence, | 64 | + const c10::optional<c10::string_view> side_opt, const c10::optional<at::Tensor> &sorter_opt) { |
| 73 | - const at::Tensor& self, | ||
| 74 | - bool out_int32, | ||
| 75 | - bool right, | ||
| 76 | - const c10::optional<c10::string_view> side_opt, | ||
| 77 | - const c10::optional<at::Tensor>& sorter_opt) | ||
| 78 | -{ | ||
| 79 | at::ScalarType scalar_type = out_int32 ? at::kInt : at::kLong; | 65 | at::ScalarType scalar_type = out_int32 ? at::kInt : at::kLong; |
| 80 | at::Tensor result = npu_preparation::apply_tensor(self.sizes(), self.options().dtype(scalar_type), self); | 66 | at::Tensor result = npu_preparation::apply_tensor(self.sizes(), self.options().dtype(scalar_type), self); |
| 81 | - searchsorted_out_nocheck(result, sorted_sequence, self, out_int32, right); | 67 | + (void)op_plugin::searchsorted_validate_tensor_out_op( |
| 68 | + sorted_sequence, self, result, out_int32, right, side_opt, sorter_opt); | ||
| 69 | + (void)op_plugin::warn_if_searchsorted_inputs_noncontiguous(sorted_sequence, self, sorter_opt); | ||
| 70 | + const bool right_eff = op_plugin::resolve_searchsorted_effective_right(right, side_opt); | ||
| 71 | + searchsorted_out_nocheck(result, sorted_sequence, self, out_int32, right_eff); | ||
| 82 | return result; | 72 | return result; |
| 83 | } | 73 | } |
| 84 | 74 | ||
| 85 | -at::Tensor searchsorted( | 75 | +at::Tensor searchsorted(const at::Tensor &sorted_sequence, const at::Scalar &self, bool out_int32, bool right, |
| 86 | - const at::Tensor& sorted_sequence, | 76 | + const c10::optional<c10::string_view> side_opt, const c10::optional<at::Tensor> &sorter_opt) { |
| 87 | - const at::Scalar& self, | ||
| 88 | - bool out_int32, | ||
| 89 | - bool right, | ||
| 90 | - const c10::optional<c10::string_view> side_opt, | ||
| 91 | - const c10::optional<at::Tensor>& sorter_opt) | ||
| 92 | -{ | ||
| 93 | at::ScalarType scalar_type = out_int32 ? at::kInt : at::kLong; | 77 | at::ScalarType scalar_type = out_int32 ? at::kInt : at::kLong; |
| 78 | + (void)op_plugin::searchsorted_validate_scalar_op(sorted_sequence, self, out_int32, right, side_opt, sorter_opt); | ||
| 79 | + (void)op_plugin::warn_if_searchsorted_scalar_inputs_noncontiguous(sorted_sequence, sorter_opt); | ||
| 94 | at::Tensor self_op = npu_preparation::copy_scalar_to_device(self, sorted_sequence.scalar_type()); | 80 | at::Tensor self_op = npu_preparation::copy_scalar_to_device(self, sorted_sequence.scalar_type()); |
| 95 | self_op = self_op.unsqueeze(0); | 81 | self_op = self_op.unsqueeze(0); |
| 96 | - at::Tensor result = npu_preparation::apply_tensor({}, sorted_sequence.options().dtype(scalar_type), sorted_sequence); | 82 | + at::Tensor result = |
| 97 | - searchsorted_out_nocheck(result, sorted_sequence, self_op, out_int32, right); | 83 | + npu_preparation::apply_tensor({}, sorted_sequence.options().dtype(scalar_type), sorted_sequence); |
| 84 | + const bool right_eff = op_plugin::resolve_searchsorted_effective_right(right, side_opt); | ||
| 85 | + searchsorted_out_nocheck(result, sorted_sequence, self_op, out_int32, right_eff); | ||
| 98 | return result; | 86 | return result; |
| 99 | } | 87 | } |
| 100 | } // namespace acl_op | 88 | } // namespace acl_op |
| @@ -24,10 +24,13 @@ namespace op_infer { | |||
| 24 | 24 | ||
| 25 | OP_PLUGIN_HIDDEN int64_t npu_gelu_approximate_mode(c10::string_view approximate); | 25 | OP_PLUGIN_HIDDEN int64_t npu_gelu_approximate_mode(c10::string_view approximate); |
| 26 | OP_PLUGIN_HIDDEN std::string npu_gelu_approximate_str(c10::string_view approximate); | 26 | OP_PLUGIN_HIDDEN std::string npu_gelu_approximate_str(c10::string_view approximate); |
| 27 | -OP_PLUGIN_HIDDEN bool npu_add_rms_norm_quant_param_check(c10::optional<at::Tensor> scales2, | 27 | +OP_PLUGIN_HIDDEN bool npu_add_rms_norm_quant_param_check( |
| 28 | - c10::optional<at::Tensor> zero_points2, | 28 | + c10::optional<at::Tensor> scales2, c10::optional<at::Tensor> zero_points2, int64_t axis, bool div_mode); |
| 29 | - int64_t axis, | ||
| 30 | - bool div_mode); | ||
| 31 | 29 | ||
| 32 | } // namespace op_infer | 30 | } // namespace op_infer |
| 31 | + | ||
| 32 | + | ||
| 33 | + | ||
| 34 | + | ||
| 35 | + | ||
| 33 | 36 | ||
| @@ -0,0 +1,29 @@ | |||
| 1 | +// Copyright (c) 2026 Huawei Technologies Co., Ltd | ||
| 2 | +// All rights reserved. | ||
| 3 | +// | ||
| 4 | +// Licensed under the BSD 3-Clause License (the "License"); | ||
| 5 | +// you may not use this file except in compliance with the License. | ||
| 6 | +// You may obtain a copy of the License at | ||
| 7 | +// | ||
| 8 | +// https://opensource.org/licenses/BSD-3-Clause | ||
| 9 | + | ||
| 10 | + | ||
| 11 | + | ||
| 12 | + | ||
| 13 | + | ||
| 14 | + | ||
| 15 | + | ||
| 16 | +namespace op_plugin { | ||
| 17 | + | ||
| 18 | +/// When `side` is set it overrides `right` (see torch.searchsorted(side=...)). | ||
| 19 | +/// Invalid `side` and side/right conflicts are handled in searchsorted_pre_check_npu; call this after validate. | ||
| 20 | +inline bool resolve_searchsorted_effective_right(bool right, const c10::optional<c10::string_view> &side_opt) { | ||
| 21 | + if (!side_opt.has_value()) { | ||
| 22 | + return right; | ||
| 23 | + } | ||
| 24 | + return *side_opt == "right"; | ||
| 25 | +} | ||
| 26 | + | ||
| 27 | +} // namespace op_plugin | ||
| 28 | + | ||
| 29 | + | ||
| @@ -0,0 +1,153 @@ | |||
| 1 | +// Copyright (c) 2026 Huawei Technologies Co., Ltd | ||
| 2 | +// All rights reserved. | ||
| 3 | +// | ||
| 4 | +// Licensed under the BSD 3-Clause License (the "License"); | ||
| 5 | +// you may not use this file except in compliance with the License. | ||
| 6 | +// You may obtain a copy of the License at | ||
| 7 | +// | ||
| 8 | +// https://opensource.org/licenses/BSD-3-Clause | ||
| 9 | +// | ||
| 10 | +// Mirrors aten/src/ATen/native/BucketizationUtils.h searchsorted_pre_check so NPU raises | ||
| 11 | +// the same RuntimeError messages as CPU/CUDA (not CANN parameter errors). | ||
| 12 | + | ||
| 13 | + | ||
| 14 | + | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + | ||
| 25 | + | ||
| 26 | +namespace op_plugin { | ||
| 27 | + | ||
| 28 | +inline bool searchsorted_dims_matched_before_last_dim(const at::Tensor &boundaries, const at::Tensor &input) { | ||
| 29 | + if (boundaries.dim() != input.dim()) { | ||
| 30 | + return false; | ||
| 31 | + } | ||
| 32 | + const auto &dims_bd = boundaries.sizes(); | ||
| 33 | + const auto &dims_in = input.sizes(); | ||
| 34 | + for (int64_t dim = 0; dim + 1 < boundaries.dim(); ++dim) { | ||
| 35 | + if (dims_bd[dim] != dims_in[dim]) { | ||
| 36 | + return false; | ||
| 37 | + } | ||
| 38 | + } | ||
| 39 | + return true; | ||
| 40 | +} | ||
| 41 | + | ||
| 42 | +/// Full searchsorted pre_check for Tensor values; `output` may be null to skip out-dtype checks (scalar path). | ||
| 43 | +inline void searchsorted_pre_check_npu(const at::Tensor &boundaries, const at::Tensor &input, const at::Tensor *output, | ||
| 44 | + bool out_int32, bool right, const c10::optional<c10::string_view> &side_opt, | ||
| 45 | + const c10::optional<at::Tensor> &sorter_opt) { | ||
| 46 | + if (side_opt.has_value()) { | ||
| 47 | + c10::string_view side = *side_opt; | ||
| 48 | + TORCH_CHECK(side == "left" || side == "right", | ||
| 49 | + "torch.searchsorted(): side can only be 'left' or 'right' but got ", std::string(side.data(), side.size())); | ||
| 50 | + | ||
| 51 | + TORCH_CHECK(!right || side == "right", | ||
| 52 | + "torch.searchsorted(): side and right can't be set to opposites, got side of ", | ||
| 53 | + std::string(side.data(), side.size()), " while right was True"); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + TORCH_CHECK(boundaries.device() == input.device(), | ||
| 57 | + "torch.searchsorted(): boundaries and input value tensors should have same device type, but got " | ||
| 58 | + "boundaries tensor device type ", | ||
| 59 | + boundaries.device(), " and input value tensor device type ", input.device()); | ||
| 60 | + | ||
| 61 | + if (sorter_opt.has_value()) { | ||
| 62 | + const at::Tensor &sorter = *sorter_opt; | ||
| 63 | + TORCH_CHECK( | ||
| 64 | + sorter.defined(), "torch.searchsorted(): optional sorter was set but the sorter tensor is undefined"); | ||
| 65 | + | ||
| 66 | + TORCH_CHECK(sorter.device() == boundaries.device(), | ||
| 67 | + "torch.searchsorted(): sorter and boundary tensors should have same device type, but got sorter tensor " | ||
| 68 | + "device type ", | ||
| 69 | + sorter.device(), " and input value tensor device type ", boundaries.device()); | ||
| 70 | + | ||
| 71 | + TORCH_CHECK(sorter.sizes() == boundaries.sizes(), | ||
| 72 | + "torch.searchsorted(): boundary and sorter must have the same size, but got boundary tensor ", | ||
| 73 | + boundaries.sizes(), "and got sorter tensor ", sorter.sizes()); | ||
| 74 | + | ||
| 75 | + TORCH_CHECK(sorter.scalar_type() == at::ScalarType::Long, | ||
| 76 | + "torch.searchsorted(): sorter must be a tensor of long dtype but got dtype ", sorter.scalar_type()); | ||
| 77 | + | ||
| 78 | + if (sorter.numel() > 0) { | ||
| 79 | + auto minmax = sorter.aminmax(); | ||
| 80 | + int64_t vmin = std::get<0>(minmax).item().toLong(); | ||
| 81 | + int64_t vmax = std::get<1>(minmax).item().toLong(); | ||
| 82 | + TORCH_CHECK(vmin >= 0 && vmax < sorter.sizes().back(), "torch.searchsorted(): sorter index out of range"); | ||
| 83 | + } | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + TORCH_CHECK(input.dim() > 0 || (input.dim() == 0 && input.numel() == 1 && boundaries.dim() == 1), | ||
| 87 | + "torch.searchsorted(): input value can be a scalar only when boundaries tensor dimension is 1, but we " | ||
| 88 | + "got boundaries tensor dim(", | ||
| 89 | + boundaries.dim(), ") and input value's dim(", input.dim(), ") numel(", input.numel(), ")"); | ||
| 90 | + | ||
| 91 | + // Python `searchsorted(boundaries, 1)` may bind as a Tensor overload with shape [1] instead of a true | ||
| 92 | + // rank-0 scalar; CPU raises the scalar/boundaries-dim error, not "first N-1 dimensions...". | ||
| 93 | + if (boundaries.dim() != 1 && input.numel() == 1 && input.dim() != boundaries.dim()) { | ||
| 94 | + TORCH_CHECK(false, | ||
| 95 | + "torch.searchsorted(): input value can be a scalar only when boundaries tensor dimension is 1, but we " | ||
| 96 | + "got boundaries tensor dim(", | ||
| 97 | + boundaries.dim(), ") and input value's dim(", input.dim(), ") numel(", input.numel(), ")"); | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + TORCH_CHECK(boundaries.dim() != 0, | ||
| 101 | + "torch.searchsorted(): boundaries tensor should have positive dimension, but got 0 dimension"); | ||
| 102 | + | ||
| 103 | + TORCH_CHECK(boundaries.dim() == 1 || searchsorted_dims_matched_before_last_dim(boundaries, input), | ||
| 104 | + "torch.searchsorted(): boundaries tensor should be 1 dimension or the first N-1 dimensions of boundaries " | ||
| 105 | + "tensor and input value tensor must match, but we got boundaries tensor ", | ||
| 106 | + boundaries.sizes(), " and input value tensor ", input.sizes()); | ||
| 107 | + | ||
| 108 | + if (output != nullptr) { | ||
| 109 | + at::ScalarType output_dtype = output->scalar_type(); | ||
| 110 | + TORCH_CHECK( | ||
| 111 | + (output_dtype == at::ScalarType::Long && !out_int32) || (output_dtype == at::ScalarType::Int && out_int32), | ||
| 112 | + "torch.searchsorted(): output tensor's dtype is wrong, it can only be Int(int32) or Long(int64) " | ||
| 113 | + "depending on whether out_int32 flag is True, but we got output tensor's dtype ", | ||
| 114 | + output_dtype, " and out_int32 flag is ", (out_int32 ? "True" : "False")); | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + if (out_int32) { | ||
| 118 | + TORCH_CHECK(boundaries.sizes().back() < INT_MAX, | ||
| 119 | + "torch.searchsorted(): the size of boundaries' last dimension should be less than ", INT_MAX, | ||
| 120 | + ", but we got ", boundaries.sizes().back()); | ||
| 121 | + } | ||
| 122 | +} | ||
| 123 | + | ||
| 124 | +/// Functional Tensor overload: `out` is created after new_params in generated op_api, so validate everything | ||
| 125 | +/// except output dtype here (matches CPU before aclnn). Tensor_out uses searchsorted_validate_tensor_out_op. | ||
| 126 | +inline int searchsorted_validate_core_no_output(const at::Tensor &sorted_sequence, const at::Tensor &self, | ||
| 127 | + bool out_int32, bool right, const c10::optional<c10::string_view> &side_opt, | ||
| 128 | + const c10::optional<at::Tensor> &sorter_opt) { | ||
| 129 | + searchsorted_pre_check_npu(sorted_sequence, self, nullptr, out_int32, right, side_opt, sorter_opt); | ||
| 130 | + return 0; | ||
| 131 | +} | ||
| 132 | + | ||
| 133 | +/// Tensor_out / same shapes as aclnnSearchSorted. Returns 0 for use in yaml new_params. | ||
| 134 | +inline int searchsorted_validate_tensor_out_op(const at::Tensor &sorted_sequence, const at::Tensor &self, | ||
| 135 | + const at::Tensor &out, bool out_int32, bool right, const c10::optional<c10::string_view> &side_opt, | ||
| 136 | + const c10::optional<at::Tensor> &sorter_opt) { | ||
| 137 | + searchsorted_pre_check_npu(sorted_sequence, self, &out, out_int32, right, side_opt, sorter_opt); | ||
| 138 | + return 0; | ||
| 139 | +} | ||
| 140 | + | ||
| 141 | +/// Scalar value: materialize wrapped-number tensor like aten (no output-dtype check; out is created by kernel). | ||
| 142 | +inline int searchsorted_validate_scalar_op(const at::Tensor &sorted_sequence, const c10::Scalar &self, bool out_int32, | ||
| 143 | + bool right, const c10::optional<c10::string_view> &side_opt, const c10::optional<at::Tensor> &sorter_opt) { | ||
| 144 | + at::Tensor input_t = at::empty({}, sorted_sequence.options()); | ||
| 145 | + input_t.fill_(self); | ||
| 146 | + input_t.unsafeGetTensorImpl()->set_wrapped_number(true); | ||
| 147 | + searchsorted_pre_check_npu(sorted_sequence, input_t, nullptr, out_int32, right, side_opt, sorter_opt); | ||
| 148 | + return 0; | ||
| 149 | +} | ||
| 150 | + | ||
| 151 | +} // namespace op_plugin | ||
| 152 | + | ||
| 153 | + | ||
| @@ -0,0 +1,101 @@ | |||
| 1 | +// Copyright (c) 2026 Huawei Technologies Co., Ltd | ||
| 2 | +// All rights reserved. | ||
| 3 | +// | ||
| 4 | +// Licensed under the BSD 3-Clause License (the "License"); | ||
| 5 | +// you may not use this file except in compliance with the License. | ||
| 6 | +// You may obtain a copy of the License at | ||
| 7 | +// | ||
| 8 | +// https://opensource.org/licenses/BSD-3-Clause | ||
| 9 | +// | ||
| 10 | +// Aligns with aten/src/ATen/native/BucketizationUtils.h (searchsorted_maybe_trim_input_tensors). | ||
| 11 | + | ||
| 12 | + | ||
| 13 | + | ||
| 14 | + | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + | ||
| 19 | +namespace op_plugin { | ||
| 20 | + | ||
| 21 | +/// Row-major contiguous check aligned with dense strided layout (matches TensorImpl contiguous semantics). | ||
| 22 | +/// Some backends may report is_contiguous() true while strides still require an explicit copy for aclnn; this | ||
| 23 | +/// catches permute().to(...) cases that CPU flags in searchsorted_maybe_trim_input_tensors. | ||
| 24 | +inline bool searchsorted_tensor_is_row_major_contiguous(const at::Tensor &t) { | ||
| 25 | + if (!t.defined() || t.numel() == 0) { | ||
| 26 | + return true; | ||
| 27 | + } | ||
| 28 | + if (t.layout() != c10::Layout::Strided) { | ||
| 29 | + return t.is_contiguous(); | ||
| 30 | + } | ||
| 31 | + const int64_t dim = t.dim(); | ||
| 32 | + if (dim == 0) { | ||
| 33 | + return true; | ||
| 34 | + } | ||
| 35 | + int64_t z = 1; | ||
| 36 | + for (int64_t d = dim - 1; d >= 0; --d) { | ||
| 37 | + const int64_t size_d = t.size(d); | ||
| 38 | + if (size_d != 1) { | ||
| 39 | + if (t.stride(d) != z) { | ||
| 40 | + return false; | ||
| 41 | + } | ||
| 42 | + if (size_d == 0) { | ||
| 43 | + return true; | ||
| 44 | + } | ||
| 45 | + } | ||
| 46 | + z *= size_d; | ||
| 47 | + } | ||
| 48 | + return true; | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +/// Tensor, Tensor overload: warn once per process when inputs may need a contiguous copy (matches CPU/CUDA). | ||
| 52 | +inline int warn_if_searchsorted_inputs_noncontiguous( | ||
| 53 | + const at::Tensor &sorted_sequence, const at::Tensor &self, const c10::optional<at::Tensor> &sorter_opt) { | ||
| 54 | + if (!searchsorted_tensor_is_row_major_contiguous(self)) { | ||
| 55 | + TORCH_WARN_ONCE( | ||
| 56 | + "torch.searchsorted(): input value tensor is non-contiguous, this will lower the performance due " | ||
| 57 | + "to extra data copy when converting non-contiguous tensor to contiguous, please use contiguous input value " | ||
| 58 | + "tensor if possible. This message will only appear once per program."); | ||
| 59 | + } | ||
| 60 | + if (!searchsorted_tensor_is_row_major_contiguous(sorted_sequence)) { | ||
| 61 | + TORCH_WARN_ONCE( | ||
| 62 | + "torch.searchsorted(): boundary tensor is non-contiguous, this will lower the performance due " | ||
| 63 | + "to extra data copy when converting non-contiguous tensor to contiguous, please use contiguous boundary " | ||
| 64 | + "tensor if possible. This message will only appear once per program."); | ||
| 65 | + } | ||
| 66 | + if (sorter_opt.has_value()) { | ||
| 67 | + const at::Tensor &st = *sorter_opt; | ||
| 68 | + if (st.defined() && !searchsorted_tensor_is_row_major_contiguous(st)) { | ||
| 69 | + TORCH_WARN_ONCE( | ||
| 70 | + "torch.searchsorted(): sorter tensor is non-contiguous, this will lower the performance due " | ||
| 71 | + "to extra data copy when converting non-contiguous tensor to contiguous, please use contiguous sorter " | ||
| 72 | + "tensor if possible. This message will only appear once per program."); | ||
| 73 | + } | ||
| 74 | + } | ||
| 75 | + return 0; | ||
| 76 | +} | ||
| 77 | + | ||
| 78 | +/// Tensor, Scalar overload: only boundaries / sorter apply (scalar value is materialized separately). | ||
| 79 | +inline int warn_if_searchsorted_scalar_inputs_noncontiguous( | ||
| 80 | + const at::Tensor &sorted_sequence, const c10::optional<at::Tensor> &sorter_opt) { | ||
| 81 | + if (!searchsorted_tensor_is_row_major_contiguous(sorted_sequence)) { | ||
| 82 | + TORCH_WARN_ONCE( | ||
| 83 | + "torch.searchsorted(): boundary tensor is non-contiguous, this will lower the performance due " | ||
| 84 | + "to extra data copy when converting non-contiguous tensor to contiguous, please use contiguous boundary " | ||
| 85 | + "tensor if possible. This message will only appear once per program."); | ||
| 86 | + } | ||
| 87 | + if (sorter_opt.has_value()) { | ||
| 88 | + const at::Tensor &st = *sorter_opt; | ||
| 89 | + if (st.defined() && !searchsorted_tensor_is_row_major_contiguous(st)) { | ||
| 90 | + TORCH_WARN_ONCE( | ||
| 91 | + "torch.searchsorted(): sorter tensor is non-contiguous, this will lower the performance due " | ||
| 92 | + "to extra data copy when converting non-contiguous tensor to contiguous, please use contiguous sorter " | ||
| 93 | + "tensor if possible. This message will only appear once per program."); | ||
| 94 | + } | ||
| 95 | + } | ||
| 96 | + return 0; | ||
| 97 | +} | ||
| 98 | + | ||
| 99 | +} // namespace op_plugin | ||
| 100 | + | ||
| 101 | + | ||
| @@ -1,14 +1,12 @@ | |||
| 1 | +import warnings | ||
| 2 | + | ||
| 1 | import torch | 3 | import torch |
| 2 | import numpy as np | 4 | import numpy as np |
| 3 | - | ||
| 4 | -import torch_npu | ||
| 5 | - | ||
| 6 | from torch_npu.testing.testcase import TestCase, run_tests | 5 | from torch_npu.testing.testcase import TestCase, run_tests |
| 7 | from torch_npu.testing.common_utils import create_common_tensor | 6 | from torch_npu.testing.common_utils import create_common_tensor |
| 8 | 7 | ||
| 9 | 8 | ||
| 10 | class TestSearchsorted(TestCase): | 9 | class TestSearchsorted(TestCase): |
| 11 | - | ||
| 12 | def cpu_sorted_input(self, input1): | 10 | def cpu_sorted_input(self, input1): |
| 13 | input_dim = input1.dim() - 1 | 11 | input_dim = input1.dim() - 1 |
| 14 | input_op, _ = input1.float().sort(input_dim) | 12 | input_op, _ = input1.float().sort(input_dim) |
| @@ -102,11 +100,7 @@ class TestSearchsorted(TestCase): | |||
| 102 | self.assertRtolEqual(cpu_output, npu_output) | 100 | self.assertRtolEqual(cpu_output, npu_output) |
| 103 | 101 | ||
| 104 | def test_searchsorted_scalar_bool(self): | 102 | def test_searchsorted_scalar_bool(self): |
| 105 | - shape_format = [ | 103 | + shape_format = [[[np.float32, 0, [64]], 2.5], [[np.int32, 0, [128]], 2], [[np.int64, 0, [256]], 3]] |
| 106 | - [[np.float32, 0, [64]], 2.5], | ||
| 107 | - [[np.int32, 0, [128]], 2], | ||
| 108 | - [[np.int64, 0, [256]], 3] | ||
| 109 | - ] | ||
| 110 | 104 | ||
| 111 | for item in shape_format: | 105 | for item in shape_format: |
| 112 | cpu_input, npu_input = create_common_tensor(item[0], -10, 10) | 106 | cpu_input, npu_input = create_common_tensor(item[0], -10, 10) |
| @@ -120,6 +114,154 @@ class TestSearchsorted(TestCase): | |||
| 120 | self.assertRtolEqual(cpu_output1, npu_output1) | 114 | self.assertRtolEqual(cpu_output1, npu_output1) |
| 121 | self.assertRtolEqual(cpu_output2, npu_output2) | 115 | self.assertRtolEqual(cpu_output2, npu_output2) |
| 122 | 116 | ||
| 117 | + def test_searchsorted_side_kwarg_aligns_with_cpu(self): | ||
| 118 | + """side='right' / side='left' must match CPU (aten passes side_opt; right alone is not enough). | ||
| 119 | + | ||
| 120 | + Uses float32 (+ NaN) as the main path to avoid stack-specific fp64 flaky; see | ||
| 121 | + ``test_searchsorted_side_kwarg_fp64_nan_aligns_with_cpu`` for fp64 supplement. | ||
| 122 | + """ | ||
| 123 | + boundaries = torch.tensor([0.0, 1.0, 2.0, 3.0], dtype=torch.float32) | ||
| 124 | + values = torch.tensor([1.0, float("nan"), 2.0, float("nan")], dtype=torch.float32) | ||
| 125 | + npu_b = boundaries.npu() | ||
| 126 | + npu_v = values.npu() | ||
| 127 | + | ||
| 128 | + cases = [ | ||
| 129 | + {"side": "left"}, | ||
| 130 | + {"side": "right"}, | ||
| 131 | + {"right": False}, | ||
| 132 | + {"right": True}, | ||
| 133 | + ] | ||
| 134 | + for kwargs in cases: | ||
| 135 | + cpu_out = torch.searchsorted(boundaries, values, **kwargs) | ||
| 136 | + npu_out = torch.searchsorted(npu_b, npu_v, **kwargs) | ||
| 137 | + self.assertEqual( | ||
| 138 | + cpu_out, | ||
| 139 | + npu_out.cpu(), | ||
| 140 | + message=f"searchsorted kwargs={kwargs!r}", | ||
| 141 | + ) | ||
| 142 | + | ||
| 143 | + # Non-NaN sanity: right branch uses strict > for insertion point | ||
| 144 | + b32 = torch.tensor([0.0, 1.0, 2.0, 3.0], dtype=torch.float32) | ||
| 145 | + v32 = torch.tensor([0.25, 1.0, 1.5, 2.5], dtype=torch.float32) | ||
| 146 | + kwargs_r = {"side": "right"} | ||
| 147 | + cpu_r = torch.searchsorted(b32, v32, **kwargs_r) | ||
| 148 | + npu_r = torch.searchsorted(b32.npu(), v32.npu(), **kwargs_r) | ||
| 149 | + self.assertEqual(cpu_r, npu_r.cpu(), message="float32 side=right") | ||
| 150 | + | ||
| 151 | + def test_searchsorted_side_kwarg_fp64_nan_aligns_with_cpu(self): | ||
| 152 | + """Supplemental fp64 + NaN vs CPU for ``side``; keep narrow to limit flaky on stacks without full fp64 parity.""" | ||
| 153 | + boundaries = torch.tensor([0.0, 1.0, 2.0, 3.0], dtype=torch.float64) | ||
| 154 | + values = torch.tensor([1.0, float("nan"), 2.0, float("nan")], dtype=torch.float64) | ||
| 155 | + npu_b = boundaries.npu() | ||
| 156 | + npu_v = values.npu() | ||
| 157 | + for kwargs in ({"side": "left"}, {"side": "right"}): | ||
| 158 | + cpu_out = torch.searchsorted(boundaries, values, **kwargs) | ||
| 159 | + npu_out = torch.searchsorted(npu_b, npu_v, **kwargs) | ||
| 160 | + self.assertEqual(cpu_out, npu_out.cpu(), message=f"fp64 searchsorted kwargs={kwargs!r}") | ||
| 161 | + | ||
| 162 | + def test_searchsorted_pre_check_invalid_side_matches_cpu(self): | ||
| 163 | + """``searchsorted_pre_check_npu`` (SearchsortedValidateUtil): invalid ``side`` must error before aclnn / acl.""" | ||
| 164 | + seq_cpu = torch.tensor([0.0, 1.0, 2.0, 3.0], dtype=torch.float32) | ||
| 165 | + vals_cpu = torch.tensor([0.5, 1.5], dtype=torch.float32) | ||
| 166 | + seq_npu = seq_cpu.npu() | ||
| 167 | + vals_npu = vals_cpu.npu() | ||
| 168 | + pattern = r"side can only be 'left' or 'right'" | ||
| 169 | + with self.assertRaisesRegex(RuntimeError, pattern): | ||
| 170 | + torch.searchsorted(seq_cpu, vals_cpu, side="middle") | ||
| 171 | + with self.assertRaisesRegex(RuntimeError, pattern): | ||
| 172 | + torch.searchsorted(seq_npu, vals_npu, side="middle") | ||
| 173 | + | ||
| 174 | + def test_searchsorted_pre_check_side_right_conflict_matches_cpu(self): | ||
| 175 | + """Explicit ``side='left'`` with ``right=True`` is rejected like CPU.""" | ||
| 176 | + seq_cpu = torch.tensor([0.0, 1.0, 2.0], dtype=torch.float32) | ||
| 177 | + vals_cpu = torch.tensor([0.5], dtype=torch.float32) | ||
| 178 | + seq_npu = seq_cpu.npu() | ||
| 179 | + vals_npu = vals_cpu.npu() | ||
| 180 | + pattern = "side and right can't be set to opposites" | ||
| 181 | + with self.assertRaisesRegex(RuntimeError, pattern): | ||
| 182 | + torch.searchsorted(seq_cpu, vals_cpu, side="left", right=True) | ||
| 183 | + with self.assertRaisesRegex(RuntimeError, pattern): | ||
| 184 | + torch.searchsorted(seq_npu, vals_npu, side="left", right=True) | ||
| 185 | + with self.assertRaisesRegex(RuntimeError, pattern): | ||
| 186 | + torch.searchsorted(seq_npu, 0.5, side="left", right=True) | ||
| 187 | + | ||
| 188 | + def test_searchsorted_pre_check_sorter_dtype_matches_cpu(self): | ||
| 189 | + """Sorter must be long; float dtype must raise ATen-style message (not CANN dtype mismatch).""" | ||
| 190 | + sequence = torch.tensor([0.0, 1.0, 2.0, 3.0], dtype=torch.float32) | ||
| 191 | + values_1d = torch.tensor([1.0, 2.0], dtype=torch.float32) | ||
| 192 | + _, sorted_idx = torch.sort(sequence) | ||
| 193 | + pattern = "sorter must be a tensor of long dtype" | ||
| 194 | + with self.assertRaisesRegex(RuntimeError, pattern): | ||
| 195 | + torch.searchsorted(sequence, values_1d, sorter=sorted_idx.to(torch.float32)) | ||
| 196 | + with self.assertRaisesRegex(RuntimeError, pattern): | ||
| 197 | + torch.searchsorted(sequence.npu(), values_1d.npu(), sorter=sorted_idx.to(torch.float32).npu()) | ||
| 198 | + | ||
| 199 | + def test_searchsorted_pre_check_sorter_shape_mismatch_matches_cpu(self): | ||
| 200 | + seq = torch.arange(5.0, dtype=torch.float32) | ||
| 201 | + vals = torch.tensor([1.0, 2.0], dtype=torch.float32) | ||
| 202 | + sorter = torch.arange(4, dtype=torch.long) | ||
| 203 | + pattern = "boundary and sorter must have the same size" | ||
| 204 | + with self.assertRaisesRegex(RuntimeError, pattern): | ||
| 205 | + torch.searchsorted(seq, vals, sorter=sorter) | ||
| 206 | + with self.assertRaisesRegex(RuntimeError, pattern): | ||
| 207 | + torch.searchsorted(seq.npu(), vals.npu(), sorter=sorter.npu()) | ||
| 208 | + | ||
| 209 | + def test_searchsorted_pre_check_sorter_index_out_of_range_matches_cpu(self): | ||
| 210 | + seq = torch.arange(5.0, dtype=torch.float32) | ||
| 211 | + vals = torch.tensor([1.0], dtype=torch.float32) | ||
| 212 | + sorter = torch.tensor([0, 1, 2, 3, 10], dtype=torch.long) | ||
| 213 | + pattern = "sorter index out of range" | ||
| 214 | + with self.assertRaisesRegex(RuntimeError, pattern): | ||
| 215 | + torch.searchsorted(seq, vals, sorter=sorter) | ||
| 216 | + with self.assertRaisesRegex(RuntimeError, pattern): | ||
| 217 | + torch.searchsorted(seq.npu(), vals.npu(), sorter=sorter.npu()) | ||
| 218 | + | ||
| 219 | + def test_searchsorted_pre_check_device_mismatch(self): | ||
| 220 | + """Cross-device boundaries/values must fail; CPU reports ATen pre_check text, NPU often reports the generic | ||
| 221 | + wrapper_NPU same-device RuntimeError before custom pre_check runs.""" | ||
| 222 | + seq_cpu = torch.tensor([0.0, 1.0, 2.0], dtype=torch.float32) | ||
| 223 | + vals_npu = torch.tensor([0.5], dtype=torch.float32).npu() | ||
| 224 | + pattern = ( | ||
| 225 | + r"boundaries and input value tensors should have same device type" | ||
| 226 | + r"|Expected all tensors to be on the same device" | ||
| 227 | + ) | ||
| 228 | + with self.assertRaisesRegex(RuntimeError, pattern): | ||
| 229 | + torch.searchsorted(seq_cpu, vals_npu) | ||
| 230 | + | ||
| 231 | + def test_searchsorted_pre_check_out_dtype_npu(self): | ||
| 232 | + """Tensor_out validate path: wrong ``out`` dtype vs ``out_int32`` must error before check_tensor / aclnn.""" | ||
| 233 | + seq = torch.tensor([0.0, 1.0, 2.0], dtype=torch.float32).npu() | ||
| 234 | + vals = torch.tensor([0.5], dtype=torch.float32).npu() | ||
| 235 | + out = torch.empty(1, dtype=torch.int32, device="npu") | ||
| 236 | + pattern = "output tensor's dtype is wrong" | ||
| 237 | + with self.assertRaisesRegex(RuntimeError, pattern): | ||
| 238 | + torch.searchsorted(seq, vals, out_int32=False, out=out) | ||
| 239 | + | ||
| 240 | + def test_searchsorted_pre_check_leading_dims_mismatch_matches_cpu(self): | ||
| 241 | + seq = torch.tensor([[0.0, 1.0, 2.0], [0.0, 1.0, 2.0]], dtype=torch.float32) | ||
| 242 | + vals = torch.tensor([0.5, 0.5], dtype=torch.float32) | ||
| 243 | + pattern = "first N-1 dimensions of boundaries" | ||
| 244 | + with self.assertRaisesRegex(RuntimeError, pattern): | ||
| 245 | + torch.searchsorted(seq, vals) | ||
| 246 | + with self.assertRaisesRegex(RuntimeError, pattern): | ||
| 247 | + torch.searchsorted(seq.npu(), vals.npu()) | ||
| 248 | + | ||
| 249 | + def test_searchsorted_noncontiguous_warns(self): | ||
| 250 | + """WarnUtil: non-contiguous boundary / values should emit TORCH_WARN_ONCE-style user warning.""" | ||
| 251 | + # `.t()` on a 2x3 tensor changes shape to 3x2 and breaks batch alignment with 2x2 values; keep [2, 3] x [2, 2] | ||
| 252 | + # while forcing non-contiguous strides (same trick as non-contiguous views without permuting dims wrongly). | ||
| 253 | + seq = torch.tensor([[0.0, 1.0, 2.0], [10.0, 20.0, 30.0]], dtype=torch.float32).npu() | ||
| 254 | + seq = seq.transpose(0, 1).contiguous().transpose(0, 1) | ||
| 255 | + vals = torch.tensor([[0.5, 1.5], [15.0, 25.0]], dtype=torch.float32).npu().t() | ||
| 256 | + with warnings.catch_warnings(record=True) as recorded: | ||
| 257 | + warnings.simplefilter("always") | ||
| 258 | + torch.searchsorted(seq, vals) | ||
| 259 | + msgs = [str(w.message) for w in recorded] | ||
| 260 | + self.assertTrue( | ||
| 261 | + any("non-contiguous" in m for m in msgs), | ||
| 262 | + msg=f"expected non-contiguous warning, got: {msgs}", | ||
| 263 | + ) | ||
| 264 | + | ||
| 123 | 265 | ||
| 124 | if __name__ == "__main__": | 266 | if __name__ == "__main__": |
| 125 | run_tests() | 267 | run_tests() |