已合并
fix(npu): 对齐searchsorted接口入参为side=right时的正确语义、补齐相关校验和告警信息 #4934
Margaret_wangrui创建于 5月12日
fix(npu): 对齐searchsorted接口入参为side=right时的正确语义、补齐相关校验和告警信息 #4934
已合并
Margaret_wangrui创建于 5月12日
7 个文件变更+487-58
@@ -4427,25 +4427,38 @@ official:
4427 acl_op: all_version4427 acl_op: all_version
4428 op_api: all_version4428 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, out4436+ 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) -> Tensor4438 - 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_version4439 acl_op: all_version
4437 op_api: all_version4440 op_api: all_version
4438 gen_opapi:4441 gen_opapi:
4439- structured_inherit: searchsorted.Tensor_out4442+ 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_version4451 acl_op: all_version
4443 op_api: all_version4452 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: self4459 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, out4461+ exec: aclnnSearchSorted, sorted_sequence, self, out_int32, right_eff, sorter, out
4449 4462 
4450 - func: sgn(Tensor self) -> Tensor4463 - func: sgn(Tensor self) -> Tensor
4451 acl_op: all_version4464 acl_op: all_version
@@ -15,19 +15,18 @@
15 15 
16#include "op_plugin/AclOpsInterface.h"16#include "op_plugin/AclOpsInterface.h"
17#include "op_plugin/utils/OpAdapter.h"17#include "op_plugin/utils/OpAdapter.h"
18+#include "op_plugin/utils/SearchsortedSideUtil.h"
19+#include "op_plugin/utils/SearchsortedValidateUtil.h"
20+#include "op_plugin/utils/SearchsortedWarnUtil.h"
18 21 
19namespace acl_op {22namespace acl_op {
20using npu_preparation = at_npu::native::OpPreparation;23using npu_preparation = at_npu::native::OpPreparation;
21using npu_utils = at_npu::native::NpuUtils;24using npu_utils = at_npu::native::NpuUtils;
22 25 
23namespace {26namespace {
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} // namespace41} // 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_op88} // namespace acl_op
@@ -24,10 +24,13 @@ namespace op_infer {
24 24 
25OP_PLUGIN_HIDDEN int64_t npu_gelu_approximate_mode(c10::string_view approximate);25OP_PLUGIN_HIDDEN int64_t npu_gelu_approximate_mode(c10::string_view approximate);
26OP_PLUGIN_HIDDEN std::string npu_gelu_approximate_str(c10::string_view approximate);26OP_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_infer30} // namespace op_infer
31+ 
32+#include "op_plugin/utils/SearchsortedSideUtil.h"
33+#include "op_plugin/utils/SearchsortedValidateUtil.h"
34+#include "op_plugin/utils/SearchsortedWarnUtil.h"
35+ 
33#endif // OP_PLUGIN_UTILS_KERNEL_NPU_NEW_PARAMS36#endif // OP_PLUGIN_UTILS_KERNEL_NPU_NEW_PARAMS
@@ -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+#ifndef OP_PLUGIN_UTILS_SEARCHSORTED_SIDE_UTIL_H_
11+#define OP_PLUGIN_UTILS_SEARCHSORTED_SIDE_UTIL_H_
12+ 
13+#include <c10/util/Optional.h>
14+#include <c10/util/string_view.h>
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+#endif // OP_PLUGIN_UTILS_SEARCHSORTED_SIDE_UTIL_H_
@@ -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+#ifndef OP_PLUGIN_UTILS_SEARCHSORTED_VALIDATE_UTIL_H_
14+#define OP_PLUGIN_UTILS_SEARCHSORTED_VALIDATE_UTIL_H_
15+ 
16+#include <climits>
17+#include <string>
18+#include <tuple>
19+ 
20+#include <ATen/core/Tensor.h>
21+#include <c10/core/Scalar.h>
22+#include <c10/util/Exception.h>
23+#include <c10/util/Optional.h>
24+#include <c10/util/string_view.h>
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+#endif // OP_PLUGIN_UTILS_SEARCHSORTED_VALIDATE_UTIL_H_
@@ -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+#ifndef OP_PLUGIN_UTILS_SEARCHSORTED_WARN_UTIL_H_
13+#define OP_PLUGIN_UTILS_SEARCHSORTED_WARN_UTIL_H_
14+ 
15+#include <ATen/core/Tensor.h>
16+#include <c10/util/Exception.h>
17+#include <c10/util/Optional.h>
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+#endif // OP_PLUGIN_UTILS_SEARCHSORTED_WARN_UTIL_H_
@@ -1,14 +1,12 @@
1+import warnings
2+ 
1import torch3import torch
2import numpy as np4import numpy as np
3- 
4-import torch_npu
5- 
6from torch_npu.testing.testcase import TestCase, run_tests5from torch_npu.testing.testcase import TestCase, run_tests
7from torch_npu.testing.common_utils import create_common_tensor6from torch_npu.testing.common_utils import create_common_tensor
8 7 
9 8 
10class TestSearchsorted(TestCase):9class TestSearchsorted(TestCase):
11- 
12 def cpu_sorted_input(self, input1):10 def cpu_sorted_input(self, input1):
13 input_dim = input1.dim() - 111 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 
124if __name__ == "__main__":266if __name__ == "__main__":
125 run_tests()267 run_tests()