Skip to content

Commit 90053d0

Browse files
rsudermanclaude
andauthored
Add AMAX, AVG, NORM1, NORM2, MUL, MUL_NO_ZEROS reduction modes (#325)
Enable the remaining cuDNN reduction modes in ReductionAttr and add the corresponding MLIR schemas to the asm emitter: - NORM1 lowers to abs + sum.dim_IntList. - AMAX lowers to abs + amax. - AVG lowers to mean.dim (float dtypes only — torch.aten.mean.dim is not defined on integer tensors, so the sample skips int32 for AVG). - NORM2 lowers to mul + sum.dim_IntList + sqrt. - MUL lowers directly to torch.prims.prod. - MUL_NO_ZEROS uses aten.ne.Scalar to build an i1 mask, then aten.where.ScalarOther to substitute 1 for zero entries before feeding the result to torch.prims.prod, so zero inputs are excluded from the product. Extend samples/reduction/reduction_ops.cpp to exercise every new mode. Input data is built by a per-mode generateReductionInputData helper so MUL/MUL_NO_ZEROS get a non-trivial pattern (mostly 1s with a 2 and a 3, plus injected zeros for MUL_NO_ZEROS) that stays in range for fp16/int32, and the expected value is computed by the existing reference reduction loop rather than hardcoded. Add lit tests for each new mode under tests/lit/ and register them in tests/CMakeLists.txt. --------- Signed-off-by: Rob Suderman <rob.suderman@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cd233ad commit 90053d0

14 files changed

Lines changed: 906 additions & 45 deletions

include/fusilli/attributes/reduction_attributes.h

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,16 @@ namespace fusilli {
2626

2727
#define FUSILLI_REDUCTION_MODES(OP) \
2828
OP(NOT_SET) \
29-
OP(SUM) \
30-
/* OP(ADD) */ \
31-
/* OP(MUL) */ \
29+
OP(ADD) \
30+
OP(SUM) /* Remove once SUM is deprecated */ \
31+
OP(MUL) \
3232
OP(MIN) \
3333
OP(MAX) \
34-
/* OP(AMAX) */ \
35-
/* OP(AVG) */ \
36-
/* OP(NORM1) */ \
37-
/* OP(NORM2) */ \
38-
/* OP(MUL_NO_ZEROS) */
34+
OP(AMAX) \
35+
OP(AVG) \
36+
OP(NORM1) \
37+
OP(NORM2) \
38+
OP(MUL_NO_ZEROS)
3939

4040
class ReductionAttr : public AttributesCRTP<ReductionAttr> {
4141
public:

include/fusilli/attributes/types.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,24 @@ enum class DataType : uint8_t {
4545
#undef DEFINE_ENUM
4646
};
4747

48+
inline bool isIntegerType(DataType type) {
49+
switch (type) {
50+
case DataType::Uint8:
51+
case DataType::Int4:
52+
case DataType::Int8:
53+
case DataType::Int16:
54+
case DataType::Int32:
55+
case DataType::Int64:
56+
return true;
57+
default:
58+
return false;
59+
}
60+
}
61+
62+
inline bool isIntegralOrBoolType(DataType type) {
63+
return isIntegerType(type) || type == DataType::Boolean;
64+
}
65+
4866
// Map from Fusilli types to MLIR types.
4967
static const std::unordered_map<DataType, std::string> kDataTypeToMlirTypeAsm =
5068
{

include/fusilli/node/reduction_node.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,13 @@ class ReductionNode : public NodeCRTP<ReductionNode> {
119119
ErrorCode::InvalidAttribute,
120120
"Reduction input and output must have the same rank");
121121

122+
FUSILLI_RETURN_ERROR_IF(
123+
reductionAttr.getMode() == ReductionAttr::Mode::AVG &&
124+
(isIntegralOrBoolType(xTensor->getDataType()) ||
125+
isIntegralOrBoolType(yTensor->getDataType())),
126+
ErrorCode::InvalidAttribute,
127+
"Reduction AVG is not supported for integral or boolean tensors");
128+
122129
// Validate reduction dimensions - if Y[i] differs from X[i], Y[i] must be 1
123130
const auto &xDim = xTensor->getDim();
124131
const auto &yDim = yTensor->getDim();

include/fusilli/support/asm_emitter.h

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2155,6 +2155,56 @@ inline std::string ReductionNode::emitNodePreAsm() const {
21552155
{7}
21562156
)";
21572157

2158+
constexpr std::string_view kNorm1Schema = R"(
2159+
{0}
2160+
{1}
2161+
%abs_{2} = torch.aten.abs {4} : {5} -> {5}
2162+
%keepdim_{2} = torch.constant.bool true
2163+
%dtype_{2} = torch.constant.none
2164+
{3}_{2}_perm = torch.aten.sum.dim_IntList %abs_{2}, %reduction_dims_{2}, %keepdim_{2}, %dtype_{2} : {5}, !torch.list<int>, !torch.bool, !torch.none -> {6}
2165+
{7}
2166+
)";
2167+
2168+
constexpr std::string_view kNorm2Schema = R"(
2169+
{0}
2170+
{1}
2171+
%sq_{2} = torch.aten.mul.Tensor {4}, {4} : {5}, {5} -> {5}
2172+
%keepdim_{2} = torch.constant.bool true
2173+
%dtype_{2} = torch.constant.none
2174+
%sumsq_{2} = torch.aten.sum.dim_IntList %sq_{2}, %reduction_dims_{2}, %keepdim_{2}, %dtype_{2} : {5}, !torch.list<int>, !torch.bool, !torch.none -> {6}
2175+
{3}_{2}_perm = torch.aten.sqrt %sumsq_{2} : {6} -> {6}
2176+
{7}
2177+
)";
2178+
2179+
constexpr std::string_view kAbsAmaxSchema = R"(
2180+
{0}
2181+
{1}
2182+
%abs_{2} = torch.aten.abs {4} : {5} -> {5}
2183+
%keepdim_{2} = torch.constant.bool true
2184+
{3}_{2}_perm = torch.aten.amax %abs_{2}, %reduction_dims_{2}, %keepdim_{2} : {5}, !torch.list<int>, !torch.bool -> {6}
2185+
{7}
2186+
)";
2187+
2188+
constexpr std::string_view kMulSchema = R"(
2189+
{0}
2190+
{1}
2191+
%dtype_{2} = torch.constant.none
2192+
{3}_{2}_perm = torch.prims.prod {4}, %reduction_dims_{2}, %dtype_{2} : {5}, !torch.list<int>, !torch.none -> {6}
2193+
{7}
2194+
)";
2195+
2196+
constexpr std::string_view kMulNoZerosSchema = R"(
2197+
{0}
2198+
{1}
2199+
%zero_{2} = torch.constant.int 0
2200+
%mask_{2} = torch.aten.ne.Scalar {4}, %zero_{2} : {5}, !torch.int -> {8}
2201+
%one_{2} = torch.constant.int 1
2202+
%fixed_{2} = torch.aten.where.ScalarOther %mask_{2}, {4}, %one_{2} : {8}, {5}, !torch.int -> {5}
2203+
%dtype_{2} = torch.constant.none
2204+
{3}_{2}_perm = torch.prims.prod %fixed_{2}, %reduction_dims_{2}, %dtype_{2} : {5}, !torch.list<int>, !torch.none -> {6}
2205+
{7}
2206+
)";
2207+
21582208
#define FUSILLI_DECLARE_REDUCTION_EMITTER(MODE, SCHEMA, OPIR) \
21592209
case ReductionAttr::Mode::MODE: { \
21602210
return std::format(SCHEMA, permuteX, /* {0} */ \
@@ -2175,11 +2225,55 @@ inline std::string ReductionNode::emitNodePreAsm() const {
21752225
#define FUSILLI_DECLARE_KEEPDIM_DTYPE_REDUCTION_EMITTER(MODE, OPIR) \
21762226
FUSILLI_DECLARE_REDUCTION_EMITTER(MODE, kKeepdimDtypeReductionSchema, OPIR)
21772227

2228+
#define FUSILLI_DECLARE_CUSTOM_REDUCTION_EMITTER(MODE, SCHEMA) \
2229+
case ReductionAttr::Mode::MODE: { \
2230+
return std::format(SCHEMA, permuteX, /* {0} */ \
2231+
dimListOss.str(), /* {1} */ \
2232+
suffix, /* {2} */ \
2233+
getResultNamesAsm(), /* {3} */ \
2234+
getOperandNamesAsm(), /* {4} */ \
2235+
getOperandTypesAsm(), /* {5} */ \
2236+
getResultTypesAsm(), /* {6} */ \
2237+
permuteY /* {7} */ \
2238+
); \
2239+
}
2240+
21782241
switch (reductionAttr.getMode()) {
2242+
case ReductionAttr::Mode::ADD:
21792243
FUSILLI_DECLARE_KEEPDIM_DTYPE_REDUCTION_EMITTER(SUM,
21802244
torch.aten.sum.dim_IntList)
21812245
FUSILLI_DECLARE_KEEPDIM_REDUCTION_EMITTER(MIN, torch.aten.amin)
21822246
FUSILLI_DECLARE_KEEPDIM_REDUCTION_EMITTER(MAX, torch.aten.amax)
2247+
FUSILLI_DECLARE_KEEPDIM_DTYPE_REDUCTION_EMITTER(AVG, torch.aten.mean.dim)
2248+
FUSILLI_DECLARE_CUSTOM_REDUCTION_EMITTER(NORM1, kNorm1Schema)
2249+
FUSILLI_DECLARE_CUSTOM_REDUCTION_EMITTER(NORM2, kNorm2Schema)
2250+
FUSILLI_DECLARE_CUSTOM_REDUCTION_EMITTER(AMAX, kAbsAmaxSchema)
2251+
FUSILLI_DECLARE_CUSTOM_REDUCTION_EMITTER(MUL, kMulSchema)
2252+
2253+
case ReductionAttr::Mode::MUL_NO_ZEROS: {
2254+
// Build a bool tensor type matching the (logical) shape of X. This is
2255+
// used for the mask produced by `torch.aten.ne.Scalar`.
2256+
std::ostringstream boolTypeOss;
2257+
boolTypeOss << "!torch.vtensor<[";
2258+
const auto &xDims = xT->getDim();
2259+
interleave(
2260+
xDims.begin(), xDims.end(),
2261+
[&](int64_t d) { boolTypeOss << std::to_string(d); },
2262+
[&] { boolTypeOss << ","; });
2263+
boolTypeOss << "],i1>";
2264+
std::string boolType = boolTypeOss.str();
2265+
2266+
return std::format(kMulNoZerosSchema, permuteX, /* {0} */
2267+
dimListOss.str(), /* {1} */
2268+
suffix, /* {2} */
2269+
getResultNamesAsm(), /* {3} */
2270+
getOperandNamesAsm(), /* {4} */
2271+
getOperandTypesAsm(), /* {5} */
2272+
getResultTypesAsm(), /* {6} */
2273+
permuteY, /* {7} */
2274+
boolType /* {8} */
2275+
);
2276+
}
21832277
default:
21842278
assert(false && "Unsupported reduction mode");
21852279
return "";
@@ -2189,6 +2283,7 @@ inline std::string ReductionNode::emitNodePreAsm() const {
21892283
#undef FUSILLI_DECLARE_REDUCTION_EMITTER
21902284
#undef FUSILLI_DECLARE_KEEPDIM_REDUCTION_EMITTER
21912285
#undef FUSILLI_DECLARE_KEEPDIM_DTYPE_REDUCTION_EMITTER
2286+
#undef FUSILLI_DECLARE_CUSTOM_REDUCTION_EMITTER
21922287

21932288
//===----------------------------------------------------------------------===//
21942289
//

samples/reduction/reduction_ops.cpp

Lines changed: 108 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,46 @@
2323

2424
using namespace fusilli;
2525

26+
// Builds the input tensor data for a given reduction mode. The reduction
27+
// in this sample is checked at output index (d0=0, d1=0), which corresponds
28+
// to xData indices [0, 64). Each branch picks values that produce a
29+
// non-trivial, deterministic expected value at that output index while
30+
// staying in range for fp16/int32.
31+
template <typename T>
32+
static std::vector<T> generateReductionInputData(ReductionAttr::Mode mode,
33+
int64_t xSize) {
34+
std::vector<T> xData(xSize);
35+
switch (mode) {
36+
case ReductionAttr::Mode::MUL: {
37+
// Mostly 1s with a 2 and a 3 inside the (d0=0, d1=0) block so the
38+
// product over that block is 6 — non-trivial enough to verify the
39+
// multiplication actually happens (rather than degenerating to 1).
40+
std::fill(xData.begin(), xData.end(), T(1));
41+
xData[10] = T(2);
42+
xData[50] = T(3);
43+
break;
44+
}
45+
case ReductionAttr::Mode::MUL_NO_ZEROS: {
46+
// Same shape as MUL but with zeros sprinkled in. The expected
47+
// product over the (d0=0, d1=0) block is still 6 because zeros
48+
// are excluded from the reduction.
49+
std::fill(xData.begin(), xData.end(), T(1));
50+
xData[10] = T(2);
51+
xData[50] = T(3);
52+
xData[5] = T(0);
53+
xData[20] = T(0);
54+
break;
55+
}
56+
default: {
57+
// Values from -50 to 49.
58+
for (int64_t i = 0; i < xSize; ++i)
59+
xData[i] = static_cast<T>(i % 100 - 50);
60+
break;
61+
}
62+
}
63+
return xData;
64+
}
65+
2666
// Based on parameters, generates a unique name for the graph
2767
static std::string generateName(ReductionAttr::Mode mode, DataType type,
2868
const std::vector<int64_t> &xDim,
@@ -46,9 +86,16 @@ TEST_CASE("Reduction ops", "[reduction][graph]") {
4686

4787
// clang-format off
4888
const auto mode = GENERATE(
89+
ReductionAttr::Mode::ADD,
4990
ReductionAttr::Mode::SUM,
5091
ReductionAttr::Mode::MIN,
51-
ReductionAttr::Mode::MAX);
92+
ReductionAttr::Mode::MAX,
93+
ReductionAttr::Mode::AMAX,
94+
ReductionAttr::Mode::AVG,
95+
ReductionAttr::Mode::NORM1,
96+
ReductionAttr::Mode::NORM2,
97+
ReductionAttr::Mode::MUL,
98+
ReductionAttr::Mode::MUL_NO_ZEROS);
5299
// clang-format on
53100

54101
auto execute = [&]<typename T>(Handle &handle, DataType dt, T initValue) {
@@ -80,11 +127,7 @@ TEST_CASE("Reduction ops", "[reduction][graph]") {
80127
for (auto d : xDims)
81128
xSize *= d;
82129

83-
std::vector<T> xData(xSize);
84-
for (int64_t i = 0; i < xSize; ++i) {
85-
// Values from -50 to 49
86-
xData[i] = static_cast<T>(i % 100 - 50);
87-
}
130+
std::vector<T> xData = generateReductionInputData<T>(mode, xSize);
88131

89132
FUSILLI_REQUIRE_ASSIGN(auto xBuf, allocateBufferOfType(handle, xT, xData));
90133
FUSILLI_REQUIRE_ASSIGN(auto yBuf,
@@ -118,12 +161,14 @@ TEST_CASE("Reduction ops", "[reduction][graph]") {
118161

119162
// Initialize to same value as output buffer
120163
T expectedValue = initValue;
164+
double sumSq = 0.0;
121165

122166
// Perform reduction
123167
for (int64_t d2 = 0; d2 < 8; ++d2) {
124168
for (int64_t d3 = 0; d3 < 8; ++d3) {
125169
int64_t inIdx = ((d0 * 16 + d1) * 8 + d2) * 8 + d3;
126170
switch (mode) {
171+
case ReductionAttr::Mode::ADD:
127172
case ReductionAttr::Mode::SUM:
128173
expectedValue = expectedValue + xData[inIdx];
129174
break;
@@ -133,11 +178,43 @@ TEST_CASE("Reduction ops", "[reduction][graph]") {
133178
case ReductionAttr::Mode::MAX:
134179
expectedValue = std::max(expectedValue, xData[inIdx]);
135180
break;
181+
case ReductionAttr::Mode::NORM1:
182+
expectedValue =
183+
static_cast<T>(static_cast<double>(expectedValue) +
184+
std::abs(static_cast<double>(xData[inIdx])));
185+
break;
186+
case ReductionAttr::Mode::AMAX:
187+
expectedValue = std::max(
188+
expectedValue,
189+
static_cast<T>(std::abs(static_cast<double>(xData[inIdx]))));
190+
break;
191+
case ReductionAttr::Mode::AVG:
192+
expectedValue = expectedValue + xData[inIdx];
193+
break;
194+
case ReductionAttr::Mode::NORM2: {
195+
double v = static_cast<double>(xData[inIdx]);
196+
sumSq += v * v;
197+
break;
198+
}
199+
case ReductionAttr::Mode::MUL:
200+
expectedValue = expectedValue * xData[inIdx];
201+
break;
202+
case ReductionAttr::Mode::MUL_NO_ZEROS:
203+
if (xData[inIdx] != T(0))
204+
expectedValue = expectedValue * xData[inIdx];
205+
break;
136206
default:
137207
break;
138208
}
139209
}
140210
}
211+
if (mode == ReductionAttr::Mode::NORM2)
212+
expectedValue = static_cast<T>(std::sqrt(sumSq));
213+
214+
// Finalize AVG by dividing the accumulated sum by the number of
215+
// reduced elements (8 * 8 = 64). Integer types use integer division.
216+
if (mode == ReductionAttr::Mode::AVG)
217+
expectedValue = expectedValue / T(64);
141218

142219
// Read output buffer
143220
std::vector<T> result;
@@ -146,10 +223,19 @@ TEST_CASE("Reduction ops", "[reduction][graph]") {
146223
// Validate output size and check the first value (d0=0, d1=0)
147224
REQUIRE(result.size() == ySize);
148225
int64_t checkIdx = d0 * 16 + d1;
149-
if constexpr (std::is_floating_point_v<T>)
226+
if constexpr (std::is_floating_point_v<T>) {
150227
REQUIRE(std::abs(result[checkIdx] - expectedValue) < T(0.01));
151-
else
152-
REQUIRE(result[checkIdx] == expectedValue);
228+
} else {
229+
if (mode == ReductionAttr::Mode::NORM2) {
230+
// sqrt may round; allow off-by-one for integer types.
231+
T diff = result[checkIdx] > expectedValue
232+
? result[checkIdx] - expectedValue
233+
: expectedValue - result[checkIdx];
234+
REQUIRE(diff <= T(1));
235+
} else {
236+
REQUIRE(result[checkIdx] == expectedValue);
237+
}
238+
}
153239
};
154240

155241
// Create handle for the target backend.
@@ -158,19 +244,28 @@ TEST_CASE("Reduction ops", "[reduction][graph]") {
158244
// Determine initial value based on reduction mode
159245
auto getInitValue = [&]<typename T>() -> T {
160246
switch (mode) {
161-
case ReductionAttr::Mode::SUM:
162-
return T(0);
163247
case ReductionAttr::Mode::MIN:
164248
return std::numeric_limits<T>::max();
165249
case ReductionAttr::Mode::MAX:
166250
return std::numeric_limits<T>::lowest();
251+
case ReductionAttr::Mode::MUL:
252+
case ReductionAttr::Mode::MUL_NO_ZEROS:
253+
return T(1);
254+
case ReductionAttr::Mode::ADD:
255+
case ReductionAttr::Mode::SUM:
256+
case ReductionAttr::Mode::AMAX:
257+
case ReductionAttr::Mode::AVG:
258+
case ReductionAttr::Mode::NORM1:
259+
case ReductionAttr::Mode::NORM2:
167260
default:
168261
return T(0);
169262
}
170263
};
171264

172-
// int32
173-
execute(handle, DataType::Int32, getInitValue.template operator()<int>());
265+
// torch.aten.mean.dim is not defined on integer tensors, so AVG only
266+
// exercises the floating-point paths.
267+
if (mode != ReductionAttr::Mode::AVG)
268+
execute(handle, DataType::Int32, getInitValue.template operator()<int>());
174269
// fp16
175270
execute(handle, DataType::Half, getInitValue.template operator()<half>());
176271
// fp32

tests/CMakeLists.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,9 +223,15 @@ add_fusilli_lit_tests(
223223
lit/test_sdpa_asm_emitter_gqa_hk_ne_hv.cpp
224224
lit/test_sdpa_asm_emitter_custom_scale.cpp
225225
lit/test_sdpa_asm_emitter_cross_attn.cpp
226-
lit/test_reduction_asm_emitter_sum.cpp
226+
lit/test_reduction_asm_emitter_add.cpp
227227
lit/test_reduction_asm_emitter_min.cpp
228+
lit/test_reduction_asm_emitter_amax.cpp
228229
lit/test_reduction_asm_emitter_max.cpp
230+
lit/test_reduction_asm_emitter_mul.cpp
231+
lit/test_reduction_asm_emitter_mul_no_zeros.cpp
232+
lit/test_reduction_asm_emitter_norm1.cpp
233+
lit/test_reduction_asm_emitter_avg.cpp
234+
lit/test_reduction_asm_emitter_norm2.cpp
229235
DEPS
230236
libfusilli
231237
libutils

0 commit comments

Comments
 (0)