Skip to content

Commit fc1210c

Browse files
Gasoonjiameta-codesync[bot]
authored andcommitted
Add the aoti_torch_cuda_masked_select fallback shim (pytorch#24788)
Summary: Pull Request resolved: pytorch#24788 A program that calls torch.masked_select directly could not be lowered with the CUDA backend; export stopped with "missing fallback kernels: aoti_torch_cuda_masked_select" (P2594125604). Mask indexing (x[x > 0]) was unaffected because Inductor reads it as index.Tensor, but tools that trace models into explicit ops emit direct masked_select calls. masked_select(self, mask) is self_b[mask_b] for self and mask broadcast together, so the shim builds stride-0 broadcast views of both and calls the existing aoti_torch_cuda_index_Tensor with the mask as its index. That reuses its CUB mask scan, dtype coverage and CUDA graph refusal, and gives the contiguous 1-D result ATen does. A 0-dim broadcast shape is viewed as 1-D, since a 0-dim mask indexes no dims. Registered in get_supported_fallback_kernels, the wheel's shim symbol check, and the Windows import library (regenerated per aoti_cuda_shims.lib.md with mingw64 dlltool 2.41, which reproduces the checked-in file byte for byte before the change; superset of the old exports, zero timestamps). Reviewed By: shoumikhin Differential Revision: D130366694
1 parent 02e0d3a commit fc1210c

11 files changed

Lines changed: 280 additions & 1 deletion

File tree

‎.ci/scripts/wheel/test_shared_libraries.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
"aoti_torch_index_put_out",
108108
"aoti_torch_cuda_scatter_reduce_two_out",
109109
"aoti_torch_cuda_scatter_src_out",
110+
"aoti_torch_cuda_masked_select",
110111
)
111112

112113
_QUANTIZED_KERNEL_SYMBOLS = (

‎backends/cuda/cuda_backend.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,7 @@ def get_supported_fallback_kernels(cls) -> Dict[str, Any]:
12191219
"aoti_torch_index_put_out": None,
12201220
"aoti_torch_cuda_scatter_reduce_two_out": None,
12211221
"aoti_torch_cuda_scatter_src_out": None,
1222+
"aoti_torch_cuda_masked_select": None,
12221223
}
12231224

12241225
@classmethod
840 Bytes
Binary file not shown.

‎backends/cuda/runtime/shims/index.cu‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,43 @@ Error scatter(
12361236
}
12371237
}
12381238

1239+
// The shape a and b broadcast to, as in ATen.
1240+
Error broadcast_sizes(
1241+
const Tensor& a,
1242+
const Tensor& b,
1243+
std::vector<int64_t>* sizes) {
1244+
const size_t ndim = std::max(a.dim(), b.dim());
1245+
sizes->assign(ndim, 1);
1246+
for (size_t i = 0; i < ndim; ++i) {
1247+
const int64_t size_a =
1248+
i < ndim - a.dim() ? 1 : a.size(i - (ndim - a.dim()));
1249+
const int64_t size_b =
1250+
i < ndim - b.dim() ? 1 : b.size(i - (ndim - b.dim()));
1251+
ET_CHECK_OR_RETURN_ERROR(
1252+
size_a == size_b || size_a == 1 || size_b == 1,
1253+
InvalidArgument,
1254+
"masked_select: self and mask cannot be broadcast together");
1255+
(*sizes)[i] = size_a == 1 ? size_b : size_a;
1256+
}
1257+
return Error::Ok;
1258+
}
1259+
1260+
// A view of tensor broadcast to sizes, with stride 0 along the dims it
1261+
// expands.
1262+
Tensor expanded(const Tensor& tensor, const std::vector<int64_t>& sizes) {
1263+
const size_t lead = sizes.size() - tensor.dim();
1264+
std::vector<int64_t> strides(sizes.size(), 0);
1265+
for (size_t d = 0; d < tensor.dim(); ++d) {
1266+
if (tensor.size(d) != 1) {
1267+
strides[lead + d] = tensor.stride(d);
1268+
}
1269+
}
1270+
return tensor.as_strided(
1271+
executorch::backends::aoti::slim::makeArrayRef(sizes),
1272+
executorch::backends::aoti::slim::makeArrayRef(strides),
1273+
tensor.storage_offset());
1274+
}
1275+
12391276
} // namespace
12401277

12411278
#ifdef __cplusplus
@@ -1512,6 +1549,32 @@ AOTITorchError aoti_torch_cuda_scatter_reduce_two_out(
15121549
include_self != 0);
15131550
}
15141551

1552+
AOTITorchError aoti_torch_cuda_masked_select(
1553+
Tensor* self,
1554+
Tensor* mask,
1555+
Tensor** ret0) {
1556+
ET_CHECK_OR_RETURN_ERROR(
1557+
self != nullptr && mask != nullptr && ret0 != nullptr,
1558+
InvalidArgument,
1559+
"aoti_torch_cuda_masked_select: null argument");
1560+
ET_CHECK_OR_RETURN_ERROR(
1561+
mask->dtype() == ScalarType::Bool || mask->dtype() == ScalarType::Byte,
1562+
InvalidArgument,
1563+
"aoti_torch_cuda_masked_select: mask must be bool or uint8");
1564+
std::vector<int64_t> sizes;
1565+
ET_CHECK_OK_OR_RETURN_ERROR(broadcast_sizes(*self, *mask, &sizes));
1566+
// A 0-dim mask would cover no dims of the indexed tensor, so index a 1-D
1567+
// view of everything instead.
1568+
if (sizes.empty()) {
1569+
sizes = {1};
1570+
}
1571+
Tensor self_view = expanded(*self, sizes);
1572+
Tensor mask_view = expanded(*mask, sizes);
1573+
Tensor* mask_index = &mask_view;
1574+
Tensor* const* indices[] = {&mask_index};
1575+
return aoti_torch_cuda_index_Tensor(&self_view, indices, 1, ret0);
1576+
}
1577+
15151578
#ifdef __cplusplus
15161579
}
15171580
#endif

‎backends/cuda/runtime/shims/index.h‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,26 @@ AOTI_SHIM_EXPORT AOTITorchError aoti_torch_cuda_scatter_reduce_two_out(
139139
const char* reduce,
140140
int32_t include_self);
141141

142+
/**
143+
* The elements of self where mask is set, with the semantics of
144+
* aten::masked_select: self and mask broadcast together and the selected
145+
* elements come out in row-major order of the broadcast shape.
146+
*
147+
* Inductor calls this fallback since the number of set elements decides the
148+
* output size. Reading that count waits for the stream, so a CUDA graph cannot
149+
* capture this call.
150+
*
151+
* @param self Tensor to select from (CUDA device, up to 16 dims, any dtype
152+
* aoti_torch_cuda_index_Tensor supports)
153+
* @param mask bool or uint8 mask on the CUDA device, broadcastable with self
154+
* @param ret0 Output: new contiguous 1-D tensor with the dtype of self
155+
* @return AOTITorchError
156+
*/
157+
AOTI_SHIM_EXPORT AOTITorchError aoti_torch_cuda_masked_select(
158+
Tensor* self,
159+
Tensor* mask,
160+
Tensor** ret0);
161+
142162
#ifdef __cplusplus
143163
}
144164
#endif

‎backends/cuda/runtime/shims/tests/CMakeLists.txt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ set(CUDA_SHIM_TESTS
5050
)
5151

5252
# CUDA-specific tests requiring GPU kernels
53-
set(CUDA_KERNEL_TESTS test_aoti_torch_cuda__weight_int4pack_mm)
53+
set(CUDA_KERNEL_TESTS test_aoti_torch_cuda__weight_int4pack_mm
54+
test_aoti_torch_cuda_masked_select
55+
)
5456

5557
enable_testing()
5658

‎backends/cuda/runtime/shims/tests/targets.bzl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def define_common_targets(is_fbcode = False):
4747
cuda_shim_cpp_unittest("aoti_torch_new_tensor_handle")
4848
cuda_shim_cpp_unittest("aoti_torch_item_bool")
4949
cuda_shim_cpp_unittest("aoti_torch_assign_tensors_out")
50+
cuda_shim_cpp_unittest("aoti_torch_cuda_masked_select")
5051

5152
cpp_unittest(
5253
name = "test_op__device_copy",
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#include <cuda_runtime.h>
10+
#include <executorch/backends/aoti/slim/c10/core/DeviceType.h>
11+
#include <executorch/backends/aoti/slim/c10/core/ScalarType.h>
12+
#include <executorch/backends/cuda/runtime/shims/index.h>
13+
#include <executorch/backends/cuda/runtime/shims/memory.h>
14+
#include <executorch/runtime/core/error.h>
15+
#include <executorch/runtime/platform/platform.h>
16+
#include <gtest/gtest.h>
17+
18+
#include <cstdint>
19+
#include <vector>
20+
21+
using executorch::backends::cuda::aoti_torch_cuda_masked_select;
22+
using executorch::backends::cuda::aoti_torch_delete_tensor_object;
23+
using executorch::backends::cuda::aoti_torch_empty_strided;
24+
using executorch::runtime::Error;
25+
using Tensor = executorch::backends::aoti::slim::SlimTensor;
26+
namespace slim_c10 = executorch::backends::aoti::slim::c10;
27+
28+
class AOTITorchCudaMaskedSelectTest : public ::testing::Test {
29+
protected:
30+
void SetUp() override {
31+
et_pal_init();
32+
int device_count = 0;
33+
if (cudaGetDeviceCount(&device_count) != cudaSuccess || device_count == 0) {
34+
GTEST_SKIP() << "CUDA not available, skipping CUDA tests";
35+
}
36+
}
37+
38+
void TearDown() override {
39+
for (Tensor* tensor : tensors_) {
40+
aoti_torch_delete_tensor_object(tensor);
41+
}
42+
}
43+
44+
template <typename T>
45+
Tensor* to_device(
46+
const std::vector<T>& values,
47+
const std::vector<int64_t>& sizes,
48+
slim_c10::ScalarType dtype,
49+
const std::vector<int64_t>& strides = {}) {
50+
Tensor* tensor = nullptr;
51+
EXPECT_EQ(
52+
aoti_torch_empty_strided(
53+
static_cast<int64_t>(sizes.size()),
54+
sizes.data(),
55+
strides.empty() ? nullptr : strides.data(),
56+
static_cast<int32_t>(dtype),
57+
static_cast<int32_t>(slim_c10::DeviceType::CUDA),
58+
0,
59+
&tensor),
60+
Error::Ok);
61+
EXPECT_EQ(
62+
cudaMemcpy(
63+
tensor->data_ptr(),
64+
values.data(),
65+
values.size() * sizeof(T),
66+
cudaMemcpyHostToDevice),
67+
cudaSuccess);
68+
tensors_.push_back(tensor);
69+
return tensor;
70+
}
71+
72+
template <typename T>
73+
std::vector<T> masked_select(Tensor* self, Tensor* mask) {
74+
Tensor* result = nullptr;
75+
EXPECT_EQ(aoti_torch_cuda_masked_select(self, mask, &result), Error::Ok);
76+
if (result == nullptr) {
77+
return {};
78+
}
79+
tensors_.push_back(result);
80+
EXPECT_EQ(result->dim(), 1);
81+
std::vector<T> host(result->numel());
82+
EXPECT_EQ(cudaDeviceSynchronize(), cudaSuccess);
83+
EXPECT_EQ(
84+
cudaMemcpy(
85+
host.data(),
86+
result->data_ptr(),
87+
host.size() * sizeof(T),
88+
cudaMemcpyDeviceToHost),
89+
cudaSuccess);
90+
return host;
91+
}
92+
93+
std::vector<Tensor*> tensors_;
94+
};
95+
96+
TEST_F(AOTITorchCudaMaskedSelectTest, SameShape) {
97+
Tensor* self = to_device<float>(
98+
{1.0f, -2.0f, 3.0f, -4.0f, 5.0f, -6.0f}, {2, 3}, slim_c10::kFloat);
99+
Tensor* mask =
100+
to_device<uint8_t>({1, 0, 1, 0, 1, 0}, {2, 3}, slim_c10::kBool);
101+
102+
const std::vector<float> expected = {1.0f, 3.0f, 5.0f};
103+
EXPECT_EQ(masked_select<float>(self, mask), expected);
104+
}
105+
106+
TEST_F(AOTITorchCudaMaskedSelectTest, MaskBroadcastsOverRows) {
107+
Tensor* self =
108+
to_device<int64_t>({10, 11, 12, 20, 21, 22}, {2, 3}, slim_c10::kLong);
109+
Tensor* mask = to_device<uint8_t>({0, 1, 1}, {3}, slim_c10::kBool);
110+
111+
const std::vector<int64_t> expected = {11, 12, 21, 22};
112+
EXPECT_EQ(masked_select<int64_t>(self, mask), expected);
113+
}
114+
115+
TEST_F(AOTITorchCudaMaskedSelectTest, SelfBroadcastsToMask) {
116+
// self is a column, so each of its values repeats along the mask's rows.
117+
Tensor* self = to_device<int32_t>({7, 8}, {2, 1}, slim_c10::kInt);
118+
Tensor* mask =
119+
to_device<uint8_t>({1, 0, 1, 1, 1, 0}, {2, 3}, slim_c10::kByte);
120+
121+
const std::vector<int32_t> expected = {7, 7, 8, 8};
122+
EXPECT_EQ(masked_select<int32_t>(self, mask), expected);
123+
}
124+
125+
TEST_F(AOTITorchCudaMaskedSelectTest, NonContiguousSelf) {
126+
// Stored column-major, so the logical tensor is {{1, 2}, {3, 4}}.
127+
Tensor* self = to_device<float>(
128+
{1.0f, 3.0f, 2.0f, 4.0f}, {2, 2}, slim_c10::kFloat, {1, 2});
129+
Tensor* mask = to_device<uint8_t>({0, 1, 1, 0}, {2, 2}, slim_c10::kBool);
130+
131+
const std::vector<float> expected = {2.0f, 3.0f};
132+
EXPECT_EQ(masked_select<float>(self, mask), expected);
133+
}
134+
135+
TEST_F(AOTITorchCudaMaskedSelectTest, ZeroDimSelfAndMask) {
136+
Tensor* self = to_device<float>({4.0f}, {}, slim_c10::kFloat);
137+
Tensor* set = to_device<uint8_t>({1}, {}, slim_c10::kBool);
138+
Tensor* unset = to_device<uint8_t>({0}, {}, slim_c10::kBool);
139+
140+
EXPECT_EQ(masked_select<float>(self, set), std::vector<float>{4.0f});
141+
EXPECT_EQ(masked_select<float>(self, unset), std::vector<float>{});
142+
}
143+
144+
TEST_F(AOTITorchCudaMaskedSelectTest, IncompatibleShapesAreRejected) {
145+
Tensor* self = to_device<float>({1.0f, 2.0f, 3.0f}, {3}, slim_c10::kFloat);
146+
Tensor* mask = to_device<uint8_t>({1, 0}, {2}, slim_c10::kBool);
147+
Tensor* result = nullptr;
148+
149+
EXPECT_EQ(
150+
aoti_torch_cuda_masked_select(self, mask, &result),
151+
Error::InvalidArgument);
152+
}

‎backends/cuda/tests/test_cuda_export.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,22 @@ def forward(self, counts: torch.Tensor, idx: torch.Tensor):
986986
torch.use_deterministic_algorithms(deterministic, warn_only=warn_only)
987987
self.assertIn("aoti_torch_cuda_scatter_value_out", str(context.exception))
988988

989+
@unittest.skipIf(
990+
torch.version.hip is not None,
991+
"ROCm does not build the CUDA-only fallback shims",
992+
)
993+
def test_masked_select(self):
994+
"""A direct masked_select call reaches its fallback kernel, unlike a mask
995+
index, which Inductor reads as index.Tensor."""
996+
997+
class MaskedSelectModule(torch.nn.Module):
998+
def forward(self, x: torch.Tensor) -> torch.Tensor:
999+
return torch.masked_select(x, x > 0).sum()
1000+
1001+
self._export_to_cuda_with_lower(
1002+
MaskedSelectModule(), (torch.randn(2, 3, device="cuda"),)
1003+
).to_executorch()
1004+
9891005
@unittest.skipIf(
9901006
torch.version.hip is not None,
9911007
"loads through the Python runtime, which the ROCm build does not include",

‎backends/cuda/tests/test_index_shims.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,3 +367,25 @@ def forward(self, q, k, v, cu_seq):
367367
(big, big, v, torch.tensor([0, 12, 28], dtype=torch.int32)),
368368
]
369369
self._run(Attention(), cases, atol=1e-2, rtol=1e-2)
370+
371+
def test_masked_select(self):
372+
"""A direct masked_select call goes to the shim, unlike a mask index,
373+
which Inductor reads as index.Tensor. The mask broadcasts over rows and a
374+
transposed input checks that the shim reads strides."""
375+
376+
class MaskedSelect(torch.nn.Module):
377+
def forward(self, x, row_mask):
378+
return (
379+
torch.masked_select(x, x > 0),
380+
torch.masked_select(x, row_mask),
381+
torch.masked_select(x.transpose(0, 1), x.transpose(0, 1) < 0),
382+
)
383+
384+
torch.manual_seed(0)
385+
x = torch.randn(4, 3)
386+
cases = [
387+
(x, torch.tensor([True, False, True])),
388+
(torch.zeros(4, 3), torch.zeros(3, dtype=torch.bool)),
389+
(torch.randn(4, 3), torch.ones(3, dtype=torch.bool)),
390+
]
391+
self._run(MaskedSelect(), cases)

0 commit comments

Comments
 (0)