Skip to content

Commit 00f0654

Browse files
Generate ring masks directly as packed bits (#24089)
Summary: Dense ring masks require an avoidable allocation and packing pass. Add `sdpa_bitwise_ring_mask_gen` schemas, meta/reference implementations and a Cadence C++ out kernel that writes packed `uint8` bits directly from token positions, sliding window and cache capacity. Keep the existing Python regressions and add seven public direct-call C++ unit tests for packed byte order and polarity, ring wrap, all windows on small caches, realistic cache sizes, `int32`/`int64` positions including `INT64_MAX`, empty inputs, invalid shapes/dtypes/windows/capacities and rejection of negative positions before writing. Wire the tests into the existing Buck and Cadence CMake test targets. This commit contains only ExecuTorch sources and synchronized mirrors; Jarvis registration remains in D126973553. AI assistance: Codex separated the history and added the public C++ unit tests and build wiring. The production kernel is unchanged by this test addition. Reviewed By: mvartani-meta Differential Revision: D126734014 Pull Request resolved: #24089
1 parent 3bfbe82 commit 00f0654

9 files changed

Lines changed: 471 additions & 0 deletions

‎backends/cadence/aot/ops_registrations.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,13 @@ def register_fake(
540540
"sdpa_bitwise_causal_mask_gen.out(Tensor positions, int key_length, *, Tensor(a!) out) -> Tensor(a!)"
541541
)
542542

543+
lib.define(
544+
"sdpa_bitwise_ring_mask_gen(Tensor positions, int sliding_window, int cache_length) -> Tensor"
545+
)
546+
lib.define(
547+
"sdpa_bitwise_ring_mask_gen.out(Tensor positions, int sliding_window, int cache_length, *, Tensor(a!) out) -> Tensor(a!)"
548+
)
549+
543550
# Load/store with iDMA. These only exist before memory planning.
544551
# Post memory planning, we check that outputs/inputs for the load/store are in
545552
# DTCM and replace idma_load/idma_store with idma_copy.
@@ -3410,6 +3417,26 @@ def sdpa_bitwise_causal_mask_gen_meta(
34103417
return positions.new_empty((positions.numel(), key_length // 8), dtype=torch.uint8)
34113418

34123419

3420+
@register_fake("cadence::sdpa_bitwise_ring_mask_gen")
3421+
def sdpa_bitwise_ring_mask_gen_meta(
3422+
positions: torch.Tensor, sliding_window: int, cache_length: int
3423+
) -> torch.Tensor:
3424+
torch._check(
3425+
positions.dtype in (torch.int32, torch.int64),
3426+
lambda: "expected int32 or int64 positions",
3427+
)
3428+
torch._check(positions.dim() == 1, lambda: "positions must be one-dimensional")
3429+
torch._check(cache_length > 0, lambda: "cache_length must be positive")
3430+
torch._check(cache_length % 8 == 0, lambda: "cache_length must be a multiple of 8")
3431+
torch._check(
3432+
0 < sliding_window <= cache_length,
3433+
lambda: "sliding_window must be positive and at most cache_length",
3434+
)
3435+
return positions.new_empty(
3436+
(positions.numel(), cache_length // 8), dtype=torch.uint8
3437+
)
3438+
3439+
34133440
@register_fake("cadence::quantized_w8a32_linear")
34143441
def quantized_w8a32_linear_meta(
34153442
src: torch.Tensor,

‎backends/cadence/aot/ref_implementations.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3089,6 +3089,30 @@ def sdpa_bitwise_causal_mask_gen(
30893089
).to(torch.uint8)
30903090

30913091

3092+
@impl_tracked(m, "sdpa_bitwise_ring_mask_gen")
3093+
def sdpa_bitwise_ring_mask_gen(
3094+
positions: torch.Tensor, sliding_window: int, cache_length: int
3095+
) -> torch.Tensor:
3096+
"""Generate an LSB-first ring mask; set bits represent masked cache slots."""
3097+
assert positions.dtype in (torch.int32, torch.int64)
3098+
assert positions.dim() == 1
3099+
assert cache_length > 0 and cache_length % 8 == 0
3100+
assert 0 < sliding_window <= cache_length
3101+
assert bool(torch.all(positions >= 0))
3102+
absolute = positions.to(torch.int64).unsqueeze(1)
3103+
slots = torch.arange(
3104+
0, cache_length, 8, dtype=torch.int64, device=positions.device
3105+
).unsqueeze(0)
3106+
packed = positions.new_zeros(
3107+
(positions.numel(), cache_length // 8), dtype=torch.uint8
3108+
)
3109+
for bit in range(8):
3110+
distance = (absolute - (slots + bit)) % cache_length
3111+
masked = (distance >= sliding_window) | (distance > absolute)
3112+
packed |= masked.to(torch.uint8) << bit
3113+
return packed
3114+
3115+
30923116
@impl_tracked(m, "slice_scatter_")
30933117
def slice_scatter_impl(
30943118
self: torch.Tensor,

‎backends/cadence/aot/tests/test_ref_implementations.py‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,67 @@
2121

2222

2323
class TestRefImplementations(unittest.TestCase):
24+
def test_sdpa_bitwise_ring_mask_matches_visible_slots(self) -> None:
25+
for dtype in (torch.int32, torch.int64):
26+
for capacity in (8, 16, 24, 2048, 4096):
27+
positions = torch.tensor(
28+
[0, 1, 7, 8, capacity - 1, capacity, 3 * capacity + 5],
29+
dtype=dtype,
30+
)
31+
for window in (1, min(9, capacity), capacity):
32+
with self.subTest(dtype=dtype, capacity=capacity, window=window):
33+
expected = torch.full(
34+
(positions.numel(), capacity // 8), 255, dtype=torch.uint8
35+
)
36+
for row, position in enumerate(positions.tolist()):
37+
for key in range(
38+
max(0, position - window + 1), position + 1
39+
):
40+
slot = key % capacity
41+
expected[row, slot // 8] &= 255 ^ (1 << (slot % 8))
42+
actual = torch.ops.cadence.sdpa_bitwise_ring_mask_gen.default(
43+
positions, window, capacity
44+
)
45+
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
46+
meta = torch.ops.cadence.sdpa_bitwise_ring_mask_gen.default(
47+
positions.to("meta"), window, capacity
48+
)
49+
self.assertEqual(meta.shape, expected.shape)
50+
self.assertEqual(meta.dtype, torch.uint8)
51+
52+
def test_sdpa_bitwise_ring_mask_empty_and_large_positions(self) -> None:
53+
empty = torch.ops.cadence.sdpa_bitwise_ring_mask_gen.default(
54+
torch.empty(0, dtype=torch.int64), 4, 16
55+
)
56+
self.assertEqual(tuple(empty.shape), (0, 2))
57+
actual = torch.ops.cadence.sdpa_bitwise_ring_mask_gen.default(
58+
torch.tensor([torch.iinfo(torch.int64).max]), 4, 16
59+
)
60+
torch.testing.assert_close(
61+
actual, torch.tensor([[255, 15]], dtype=torch.uint8), rtol=0, atol=0
62+
)
63+
64+
def test_sdpa_bitwise_ring_mask_rejects_invalid_inputs(self) -> None:
65+
for device in ("cpu", "meta"):
66+
cases = [
67+
(torch.zeros(1, dtype=torch.float32, device=device), 4, 16),
68+
(torch.zeros((1, 1), dtype=torch.int64, device=device), 4, 16),
69+
(torch.zeros(1, dtype=torch.int64, device=device), 0, 16),
70+
(torch.zeros(1, dtype=torch.int64, device=device), 17, 16),
71+
(torch.zeros(1, dtype=torch.int64, device=device), 4, 0),
72+
(torch.zeros(1, dtype=torch.int64, device=device), 4, 15),
73+
]
74+
for positions, window, capacity in cases:
75+
with self.subTest(device=device, window=window, capacity=capacity):
76+
with self.assertRaises((AssertionError, RuntimeError)):
77+
torch.ops.cadence.sdpa_bitwise_ring_mask_gen.default(
78+
positions, window, capacity
79+
)
80+
with self.assertRaises(AssertionError):
81+
torch.ops.cadence.sdpa_bitwise_ring_mask_gen.default(
82+
torch.tensor([-1], dtype=torch.int64), 4, 16
83+
)
84+
2485
@expand(
2586
[
2687
("basic_int8", 0.42, -1.0, 2.0, -7, 7, torch.int8, 0),
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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 <executorch/backends/cadence/generic/operators/op_sdpa_bitwise_ring_mask_gen.h>
10+
11+
#include <cstddef>
12+
#include <cstdint>
13+
#include <cstring>
14+
15+
#include <executorch/runtime/kernel/kernel_includes.h>
16+
17+
namespace impl {
18+
namespace generic {
19+
namespace native {
20+
namespace {
21+
22+
void clear_range(uint8_t* out, int64_t start, int64_t end) {
23+
if (start == end) {
24+
return;
25+
}
26+
const int64_t first_byte = start / 8;
27+
const int64_t last_byte = (end - 1) / 8;
28+
if (first_byte == last_byte) {
29+
const uint32_t visible = ((1u << (end - start)) - 1) << (start % 8);
30+
out[first_byte] &= static_cast<uint8_t>(~visible);
31+
return;
32+
}
33+
out[first_byte] &= static_cast<uint8_t>((1u << (start % 8)) - 1);
34+
std::memset(
35+
out + first_byte + 1,
36+
0,
37+
static_cast<std::size_t>(last_byte - first_byte - 1));
38+
out[last_byte] &= static_cast<uint8_t>(0xFFu << ((end - 1) % 8 + 1));
39+
}
40+
41+
template <typename Position>
42+
bool generate_mask(
43+
const Position* positions,
44+
int64_t num_positions,
45+
int64_t sliding_window,
46+
int64_t cache_length,
47+
uint8_t* out) {
48+
for (int64_t row = 0; row < num_positions; ++row) {
49+
if (positions[row] < 0) {
50+
return false;
51+
}
52+
}
53+
const int64_t packed_width = cache_length / 8;
54+
for (int64_t row = 0; row < num_positions; ++row) {
55+
uint8_t* row_out = out + row * packed_width;
56+
std::memset(row_out, 0xFF, static_cast<std::size_t>(packed_width));
57+
const int64_t position = positions[row];
58+
const int64_t visible =
59+
position >= sliding_window - 1 ? sliding_window : position + 1;
60+
const int64_t end = position % cache_length + 1;
61+
const int64_t start = end - visible;
62+
if (start < 0) {
63+
clear_range(row_out, cache_length + start, cache_length);
64+
clear_range(row_out, 0, end);
65+
} else {
66+
clear_range(row_out, start, end);
67+
}
68+
}
69+
return true;
70+
}
71+
72+
} // namespace
73+
74+
::executorch::aten::Tensor& sdpa_bitwise_ring_mask_gen_out(
75+
::executorch::runtime::KernelRuntimeContext& ctx,
76+
const ::executorch::aten::Tensor& positions,
77+
int64_t sliding_window,
78+
int64_t cache_length,
79+
::executorch::aten::Tensor& out) {
80+
using ::executorch::aten::ScalarType;
81+
ET_KERNEL_CHECK(ctx, positions.dim() == 1, InvalidArgument, out);
82+
ET_KERNEL_CHECK(
83+
ctx, cache_length > 0 && cache_length % 8 == 0, InvalidArgument, out);
84+
ET_KERNEL_CHECK(
85+
ctx,
86+
sliding_window > 0 && sliding_window <= cache_length,
87+
InvalidArgument,
88+
out);
89+
ET_KERNEL_CHECK(
90+
ctx,
91+
positions.dtype() == ScalarType::Int ||
92+
positions.dtype() == ScalarType::Long,
93+
InvalidArgument,
94+
out);
95+
ET_KERNEL_CHECK(ctx, out.dtype() == ScalarType::Byte, InvalidArgument, out);
96+
ET_KERNEL_CHECK(
97+
ctx,
98+
out.dim() == 2 && out.size(0) == positions.numel() &&
99+
out.size(1) == cache_length / 8,
100+
InvalidArgument,
101+
out);
102+
103+
const bool valid = positions.dtype() == ScalarType::Long
104+
? generate_mask(
105+
positions.const_data_ptr<int64_t>(),
106+
positions.numel(),
107+
sliding_window,
108+
cache_length,
109+
out.mutable_data_ptr<uint8_t>())
110+
: generate_mask(
111+
positions.const_data_ptr<int32_t>(),
112+
positions.numel(),
113+
sliding_window,
114+
cache_length,
115+
out.mutable_data_ptr<uint8_t>());
116+
ET_KERNEL_CHECK(ctx, valid, InvalidArgument, out);
117+
return out;
118+
}
119+
120+
} // namespace native
121+
} // namespace generic
122+
} // namespace impl
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
#pragma once
10+
11+
#include <cstdint>
12+
13+
#include <executorch/runtime/core/exec_aten/exec_aten.h>
14+
#include <executorch/runtime/kernel/kernel_runtime_context.h>
15+
16+
namespace impl {
17+
namespace generic {
18+
namespace native {
19+
20+
::executorch::aten::Tensor& sdpa_bitwise_ring_mask_gen_out(
21+
::executorch::runtime::KernelRuntimeContext& ctx,
22+
const ::executorch::aten::Tensor& positions,
23+
int64_t sliding_window,
24+
int64_t cache_length,
25+
::executorch::aten::Tensor& out);
26+
27+
} // namespace native
28+
} // namespace generic
29+
} // namespace impl

‎backends/cadence/generic/operators/targets.bzl‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,19 @@ def define_common_targets():
520520
visibility = ["PUBLIC"],
521521
)
522522

523+
runtime.cxx_library(
524+
name = "op_sdpa_bitwise_ring_mask_gen",
525+
srcs = ["op_sdpa_bitwise_ring_mask_gen.cpp"],
526+
exported_headers = ["op_sdpa_bitwise_ring_mask_gen.h"],
527+
platforms = CXX,
528+
deps = [
529+
"//executorch/runtime/kernel:kernel_includes",
530+
"//executorch/runtime/core/exec_aten:lib",
531+
"//executorch/runtime/kernel:kernel_runtime_context",
532+
],
533+
visibility = ["PUBLIC"],
534+
)
535+
523536
runtime.cxx_library(
524537
name = "op_sdpa_bitwise_mask_gen",
525538
srcs = ["op_sdpa_bitwise_mask_gen.cpp"],

‎backends/cadence/generic/operators/tests/BUCK‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,22 @@ jarvis_wrapper.cxx_test(
4141
"fbsource//xplat/executorch/runtime/core/exec_aten/testing_util:tensor_util",
4242
],
4343
)
44+
45+
jarvis_wrapper.cxx_test(
46+
name = "test_op_sdpa_bitwise_ring_mask_gen",
47+
srcs = [
48+
"test_op_sdpa_bitwise_ring_mask_gen.cpp",
49+
],
50+
compatible_backends = ["generic"],
51+
labels = [ci.linux(ci.mode("fbsource//arvr/mode/platform010/dev"))],
52+
platforms = CXX,
53+
visibility = [
54+
"fbsource//xplat/executorch/backends/cadence/...",
55+
"fbcode//executorch/backends/cadence/...",
56+
],
57+
deps = [
58+
"fbsource//xplat/executorch/backends/cadence/generic/operators:op_sdpa_bitwise_ring_mask_gen",
59+
"fbsource//xplat/executorch/kernels/test:gtest_utils",
60+
"fbsource//xplat/executorch/runtime/core/exec_aten/testing_util:tensor_util",
61+
],
62+
)

‎backends/cadence/generic/operators/tests/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ endif()
4949
add_executable(
5050
cadence_generic_op_tests
5151
test_op_quantized_layer_norm.cpp test_op_sdpa_bitwise_mask_gen.cpp
52+
test_op_sdpa_bitwise_ring_mask_gen.cpp
5253
${EXECUTORCH_ROOT}/runtime/core/exec_aten/testing_util/tensor_util.cpp
5354
)
5455
target_compile_definitions(cadence_generic_op_tests PRIVATE GTEST_HAS_PTHREAD=0)

0 commit comments

Comments
 (0)