Skip to content

Commit 57078b4

Browse files
Move the sdpa_bitwise_mask_gen test onto the OSS-exported path (#23157)
Summary: `generic/operators/op_sdpa_bitwise_mask_gen.cpp` exports to GitHub, but `generic/operators/tests/fb/` does not, so the test that pins the kernel's contract was invisible to every OSS consumer of it. The diff above this one changes the exported kernel's float comparison; without this move it would export with nothing guarding it. `generic/operators/tests/` (non-`fb`) is the exported sibling, and `backends/cadence/CMakeLists.txt` already reaches for `${TARGET_DIR}/operators/tests/CMakeLists.txt` for whichever backend is selected, guarded by `EXISTS`, so adding that file is the supported way to opt a backend in. `TARGET_DIR` defaults to `generic`. The new `CMakeLists.txt` mirrors `hifi/operators/tests/CMakeLists.txt`, naming its target `cadence_generic_op_tests` to match the `cadence_${TARGET_DIR}_op_tests` pattern `test-cadence-xtensa.sh` derives. This is a relocation plus one formatting change. The test file moves unmodified except for a single clang-format line break at the `float_expected` initialisation: `tests/fb/` does not export, so the file had never been seen by OSS `lintrunner`, and putting it on the exported path subjects it to the CLANGFORMAT check for the first time. The BUCK target is the same rule verbatim, just in `tests/BUCK` instead of `tests/fb/BUCK`. The ISS benchmark stays in `tests/fb/` -- it calls `xthal_get_ccount()` and pins `DLA_V130_1p0v2`, so it cannot build outside Meta. Caveat for a follow-up, deliberately not done here: OSS CI does not yet reach this target. `build-cadence-runner.yml` runs `_xtensa_test.yml` with `backend: hifi4` only, and `test-cadence-xtensa.sh` maps `CADENCE_OPT_FLAG` to `hifi`/`vision`/`fusion_g3` and errors on anything else, so there is no path that selects `generic`. Wiring that up means changing CI behaviour and picking an Xtensa core for the generic build, which does not belong in a relocation. Differential Revision: D121265174 Pull Request resolved: #23157
1 parent 6d3c1c7 commit 57078b4

3 files changed

Lines changed: 317 additions & 0 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
load("@fbsource//tools/build_defs:platform_defs.bzl", "CXX")
2+
load("@fbsource//tools/target_determinator/macros:ci.bzl", "ci")
3+
load("@fbsource//xplat/on_device_ai/Assistant/Jarvis/build:cxx_wrapper.bzl", "jarvis_wrapper")
4+
5+
oncall("executorch")
6+
7+
jarvis_wrapper.cxx_test(
8+
name = "test_op_sdpa_bitwise_mask_gen",
9+
srcs = [
10+
"test_op_sdpa_bitwise_mask_gen.cpp",
11+
],
12+
compatible_backends = ["generic"],
13+
labels = [ci.linux(ci.mode("fbsource//arvr/mode/platform010/dev"))],
14+
platforms = CXX,
15+
visibility = [
16+
"fbsource//xplat/executorch/backends/cadence/...",
17+
"fbcode//executorch/backends/cadence/...",
18+
],
19+
deps = [
20+
"fbsource//xplat/executorch/backends/cadence/generic/operators:op_sdpa_bitwise_mask_gen",
21+
"fbsource//xplat/executorch/kernels/test:gtest_utils",
22+
"fbsource//xplat/executorch/runtime/core/exec_aten/testing_util:tensor_util",
23+
],
24+
)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
#
7+
# gtest op-level tests for the Cadence generic (reference) backend,
8+
# cross-compiled for Xtensa and run on the Instruction Set Simulator (xt-run).
9+
# Each test calls the kernel directly (impl::generic::native::<op>) and checks
10+
# with EXPECT_TENSOR_EQ, mirroring the BUCK tests. Built only when
11+
# EXECUTORCH_BUILD_CADENCE_OP_TESTS is ON; see .ci/scripts/test-cadence-xtensa.sh.
12+
13+
cmake_minimum_required(VERSION 3.19)
14+
15+
# Tame the vendored googletest build on the Xtensa clang toolchain. It hardcodes
16+
# -Werror -Wconversion on its own sources, which clang-10 trips (e.g. an int ->
17+
# signed char narrowing in gtest-port.h); -Wno-error keeps those non-fatal. A
18+
# ccache launcher also preprocesses then recompiles the .i, where the -I dirs
19+
# are unused and clang warns; -Qunused-arguments silences that. These land after
20+
# googletest's own flags, so they win, and apply to this whole subtree.
21+
add_compile_options(-Qunused-arguments -Wno-error)
22+
23+
# Let files say "include <executorch/path/to/header.h>".
24+
set(_common_include_directories
25+
${EXECUTORCH_ROOT}/.. ${EXECUTORCH_ROOT}/runtime/core/portable_type/c10
26+
)
27+
28+
# Build googletest FOR the Xtensa target. find_package(GTest) would resolve the
29+
# HOST gtest, which cannot link Xtensa objects, so add the source tree instead.
30+
# The cadence toolchain uses -stdlib=libc++ (exceptions/RTTI/iostream
31+
# available); the ISS is single-threaded, so disable pthreads.
32+
if(NOT TARGET gtest)
33+
set(gtest_disable_pthreads
34+
ON
35+
CACHE BOOL "" FORCE
36+
)
37+
set(INSTALL_GTEST
38+
OFF
39+
CACHE BOOL "" FORCE
40+
)
41+
add_subdirectory(
42+
${EXECUTORCH_ROOT}/third-party/googletest
43+
${CMAKE_CURRENT_BINARY_DIR}/googletest
44+
)
45+
endif()
46+
47+
# Op-level gtests, one source per operator. Each calls its kernel directly and
48+
# checks with EXPECT_TENSOR_EQ, mirroring the BUCK tests.
49+
add_executable(
50+
cadence_generic_op_tests
51+
test_op_sdpa_bitwise_mask_gen.cpp
52+
${EXECUTORCH_ROOT}/runtime/core/exec_aten/testing_util/tensor_util.cpp
53+
)
54+
target_compile_definitions(cadence_generic_op_tests PRIVATE GTEST_HAS_PTHREAD=0)
55+
target_include_directories(
56+
cadence_generic_op_tests PRIVATE ${_common_include_directories}
57+
${CMAKE_BINARY_DIR}
58+
)
59+
60+
# Direct-call: link the libs that define the kernels under test. custom_ops has
61+
# the cadence-specific ops; aten_ops_cadence has the aten-compliant ones;
62+
# cadence_kernels is the shared kernel layer; executorch provides the platform
63+
# layer the tests' runtime_init() needs. No cadence_ops_lib registry is needed
64+
# for direct-call tests.
65+
target_link_libraries(
66+
cadence_generic_op_tests
67+
PRIVATE gtest
68+
gtest_main
69+
gmock
70+
executorch
71+
custom_ops
72+
aten_ops_cadence
73+
cadence_kernels
74+
)
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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_mask_gen.h>
10+
11+
#include <executorch/kernels/test/TestUtil.h>
12+
#include <executorch/runtime/core/exec_aten/exec_aten.h>
13+
#include <executorch/runtime/core/exec_aten/testing_util/tensor_factory.h>
14+
#include <executorch/runtime/core/exec_aten/testing_util/tensor_util.h>
15+
#include <gtest/gtest.h>
16+
17+
#include <cstdint>
18+
#include <vector>
19+
20+
namespace impl {
21+
namespace generic {
22+
namespace native {
23+
namespace {
24+
25+
using ::executorch::aten::ScalarType;
26+
using ::executorch::aten::Tensor;
27+
using ::executorch::runtime::testing::TensorFactory;
28+
29+
// Transcription of the packing loop as it was written before the data pointers
30+
// were hoisted out of it. Tests compare the kernel against this so that any
31+
// rewrite of the loop body has to stay byte-identical.
32+
template <typename T, typename MaskedPred>
33+
std::vector<uint8_t> reference_pack(
34+
const std::vector<T>& in,
35+
MaskedPred is_masked) {
36+
std::vector<uint8_t> packed(in.size() / 8);
37+
for (size_t i = 0, out_index = 0; i < in.size(); i += 8, out_index++) {
38+
uint8_t packed_mask = 0;
39+
for (size_t j = 0; j < 8; j++) {
40+
packed_mask |= static_cast<uint8_t>(is_masked(in[i + j])) << j;
41+
}
42+
packed[out_index] = packed_mask;
43+
}
44+
return packed;
45+
}
46+
47+
// A bool mask uses True = keep, so the kernel inverts it.
48+
std::vector<uint8_t> reference_pack_bool(const std::vector<uint8_t>& in) {
49+
return reference_pack(in, [](uint8_t v) { return !v; });
50+
}
51+
52+
std::vector<uint8_t> reference_pack_float(
53+
const std::vector<float>& in,
54+
double threshold) {
55+
return reference_pack(in, [threshold](float v) { return v < threshold; });
56+
}
57+
58+
class GenericSdpaBitwiseMaskGenTest : public OperatorTest {
59+
protected:
60+
Tensor&
61+
sdpa_bitwise_mask_gen_out(const Tensor& mask, double threshold, Tensor& out) {
62+
return impl::generic::native::sdpa_bitwise_mask_gen_out(
63+
context_, mask, threshold, out);
64+
}
65+
};
66+
67+
// Pins the bit order and polarity with hand-computed bytes: element j of each
68+
// group of 8 lands in bit j, and a False (blocked) input sets that bit.
69+
TEST_F(GenericSdpaBitwiseMaskGenTest, BoolMaskPacksBlockedAsSetBitLsbFirst) {
70+
TensorFactory<ScalarType::Bool> tf_bool;
71+
TensorFactory<ScalarType::Byte> tf_uint8;
72+
73+
// clang-format off
74+
Tensor mask = tf_bool.make(
75+
{4, 8},
76+
{
77+
// All kept -> no bits set.
78+
1, 1, 1, 1, 1, 1, 1, 1,
79+
// All blocked -> every bit set.
80+
0, 0, 0, 0, 0, 0, 0, 0,
81+
// Only element 0 blocked -> bit 0.
82+
0, 1, 1, 1, 1, 1, 1, 1,
83+
// Only element 3 blocked -> bit 3.
84+
1, 1, 1, 0, 1, 1, 1, 1,
85+
});
86+
// clang-format on
87+
88+
Tensor out = tf_uint8.zeros({4, 1});
89+
Tensor expected = tf_uint8.make({4, 1}, {0x00, 0xFF, 0x01, 0x08});
90+
91+
sdpa_bitwise_mask_gen_out(mask, 0.0, out);
92+
93+
EXPECT_TENSOR_EQ(out, expected);
94+
}
95+
96+
// Values equal to the threshold are kept; only strictly smaller ones are
97+
// masked.
98+
TEST_F(GenericSdpaBitwiseMaskGenTest, FloatMaskThresholdIsStrictlyLess) {
99+
TensorFactory<ScalarType::Float> tf_float;
100+
TensorFactory<ScalarType::Byte> tf_uint8;
101+
102+
const double threshold = -1.5;
103+
104+
// clang-format off
105+
Tensor mask = tf_float.make(
106+
{3, 8},
107+
{
108+
// Exactly at the threshold, and above it -> kept.
109+
-1.5, -1.5, -1.5, -1.5, 0.0, 1.0, 100.0, -1.4999,
110+
// Below the threshold -> masked.
111+
-1.6, -2.0, -100.0, -1.50001, -3.0, -4.0, -5.0, -6.0,
112+
// Alternating, starting with a masked element.
113+
-2.0, 0.0, -2.0, 0.0, -2.0, 0.0, -2.0, 0.0,
114+
});
115+
// clang-format on
116+
117+
Tensor out = tf_uint8.zeros({3, 1});
118+
Tensor expected = tf_uint8.make({3, 1}, {0x00, 0xFF, 0x55});
119+
120+
sdpa_bitwise_mask_gen_out(mask, threshold, out);
121+
122+
EXPECT_TENSOR_EQ(out, expected);
123+
}
124+
125+
TEST_F(GenericSdpaBitwiseMaskGenTest, BoolMaskMatchesReferencePacking) {
126+
TensorFactory<ScalarType::Bool> tf_bool;
127+
TensorFactory<ScalarType::Byte> tf_uint8;
128+
129+
constexpr int kRows = 7;
130+
constexpr int kCols = 64;
131+
132+
// Causal-style mask: row r keeps the first 8*(r+1) columns.
133+
std::vector<uint8_t> in(kRows * kCols);
134+
for (int r = 0; r < kRows; r++) {
135+
for (int c = 0; c < kCols; c++) {
136+
in[r * kCols + c] = static_cast<uint8_t>(c < 8 * (r + 1));
137+
}
138+
}
139+
140+
Tensor mask = tf_bool.make({kRows, kCols}, in);
141+
Tensor out = tf_uint8.zeros({kRows, kCols / 8});
142+
Tensor expected = tf_uint8.make({kRows, kCols / 8}, reference_pack_bool(in));
143+
144+
sdpa_bitwise_mask_gen_out(mask, 0.0, out);
145+
146+
EXPECT_TENSOR_EQ(out, expected);
147+
}
148+
149+
TEST_F(GenericSdpaBitwiseMaskGenTest, FloatMaskMatchesReferencePacking) {
150+
TensorFactory<ScalarType::Float> tf_float;
151+
TensorFactory<ScalarType::Byte> tf_uint8;
152+
153+
constexpr int kRows = 7;
154+
constexpr int kCols = 64;
155+
const double threshold = 0.0;
156+
157+
// Irregular sign pattern so every bit position gets exercised both ways.
158+
std::vector<float> in(kRows * kCols);
159+
for (size_t i = 0; i < in.size(); i++) {
160+
in[i] = static_cast<float>((i * 37) % 71) * 0.25f - 8.0f;
161+
}
162+
163+
Tensor mask = tf_float.make({kRows, kCols}, in);
164+
Tensor out = tf_uint8.zeros({kRows, kCols / 8});
165+
Tensor expected =
166+
tf_uint8.make({kRows, kCols / 8}, reference_pack_float(in, threshold));
167+
168+
sdpa_bitwise_mask_gen_out(mask, threshold, out);
169+
170+
EXPECT_TENSOR_EQ(out, expected);
171+
}
172+
173+
// A mask the size of a real attention mask, so the packing loop runs over many
174+
// output bytes rather than a handful.
175+
TEST_F(GenericSdpaBitwiseMaskGenTest, LargeMaskMatchesReferencePacking) {
176+
TensorFactory<ScalarType::Bool> tf_bool;
177+
TensorFactory<ScalarType::Float> tf_float;
178+
TensorFactory<ScalarType::Byte> tf_uint8;
179+
180+
constexpr int kSeq = 256;
181+
constexpr int kKv = 256;
182+
const double threshold = 0.0;
183+
184+
std::vector<uint8_t> bool_in(kSeq * kKv);
185+
std::vector<float> float_in(kSeq * kKv);
186+
for (int r = 0; r < kSeq; r++) {
187+
for (int c = 0; c < kKv; c++) {
188+
const bool keep = c <= r;
189+
bool_in[r * kKv + c] = static_cast<uint8_t>(keep);
190+
float_in[r * kKv + c] = keep ? 0.0f : -1e30f;
191+
}
192+
}
193+
194+
Tensor bool_mask = tf_bool.make({kSeq, kKv}, bool_in);
195+
Tensor bool_out = tf_uint8.zeros({kSeq, kKv / 8});
196+
Tensor bool_expected =
197+
tf_uint8.make({kSeq, kKv / 8}, reference_pack_bool(bool_in));
198+
199+
sdpa_bitwise_mask_gen_out(bool_mask, threshold, bool_out);
200+
201+
EXPECT_TENSOR_EQ(bool_out, bool_expected);
202+
203+
Tensor float_mask = tf_float.make({kSeq, kKv}, float_in);
204+
Tensor float_out = tf_uint8.zeros({kSeq, kKv / 8});
205+
Tensor float_expected =
206+
tf_uint8.make({kSeq, kKv / 8}, reference_pack_float(float_in, threshold));
207+
208+
sdpa_bitwise_mask_gen_out(float_mask, threshold, float_out);
209+
210+
EXPECT_TENSOR_EQ(float_out, float_expected);
211+
212+
// Both dtypes describe the same mask, so they must pack to the same bytes.
213+
EXPECT_TENSOR_EQ(bool_out, float_out);
214+
}
215+
216+
} // namespace
217+
} // namespace native
218+
} // namespace generic
219+
} // namespace impl

0 commit comments

Comments
 (0)