Skip to content

Commit ef5798c

Browse files
committed
improve handling of padding for conv1d
1 parent 70fe998 commit ef5798c

12 files changed

Lines changed: 590 additions & 15 deletions

File tree

lib/Kernel/KernelImplementationTest.cpp

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include <cstdint>
22
#include <functional>
33
#include <map>
4+
#include <string>
45
#include <vector>
56

67
#include "gtest/gtest.h" // from @googletest
@@ -737,6 +738,74 @@ TEST_P(KernelImplementationTest, TestConv1dCwFcwStride2) {
737738
EXPECT_EQ(actualUnpacked, expected);
738739
}
739740

741+
// End-to-end Halevi-Shoup matvec for a padded strided 1-D multichannel conv, in
742+
// the packing production uses when LayoutPropagation folds a zero tensor.pad on
743+
// the width dim into the conv's own `padding` parameter: the data ciphertext is
744+
// packed row-major at the *unpadded* width, and the Toeplitz matrix is built
745+
// with padding = p so that a window reaching into the padding contributes no
746+
// column. `expected` is the convolution of the zero-padded data.
747+
void checkPaddedConv1dCwFcw(int64_t padding, const tensor3d& expected,
748+
bool unroll, bool interchangeRows) {
749+
SCOPED_TRACE("padding = " + std::to_string(padding));
750+
MLIRContext context;
751+
// 1x2x6 input data, 2x2x3 filter, stride 2.
752+
tensor3d data = {{{0, 1, 2, 3, 4, 5}, {6, 7, 8, 9, 10, 11}}};
753+
tensor3d filter = {{{3, 4, 1}, {1, 5, 2}}, {{1, 2, 3}, {2, 2, 2}}};
754+
int64_t stride = 2;
755+
int numSlots = 16;
756+
757+
RankedTensorType dataType =
758+
RankedTensorType::get({1, 2, 6}, mlir::IndexType::get(&context));
759+
RankedTensorType filterType =
760+
RankedTensorType::get({2, 2, 3}, mlir::IndexType::get(&context));
761+
762+
auto dataLayout = getRowMajorLayoutRelation(dataType, numSlots);
763+
std::vector<std::vector<int>> packedData =
764+
evaluateLayout(dataLayout, getDataValueFn3D(data));
765+
766+
auto filterLayout = get1dConvCwFcwFilterDiagonalizedRelation(
767+
filterType, dataType, stride, padding, numSlots, interchangeRows);
768+
ASSERT_TRUE(succeeded(filterLayout));
769+
std::function<int(const std::vector<int64_t>&)> getFilterValueFn =
770+
[&](const std::vector<int64_t>& domainPoint) -> int {
771+
return filter[domainPoint[0]][domainPoint[1]][domainPoint[2]];
772+
};
773+
std::vector<std::vector<int>> packedFilter =
774+
evaluateLayout(filterLayout.value(), getFilterValueFn);
775+
// The matrix shape must be derived the same way the filter was diagonalized,
776+
// i.e. against the unpadded data type with padding = p: implementHaleviShoup
777+
// sizes the squat-diagonal collapse from nextPowerOfTwo of these dims.
778+
auto expandedFilterShape =
779+
get1dConvCwFcwFilterExpandedType(filterType, dataType, stride, padding);
780+
781+
auto dag = implementHaleviShoup(
782+
LiteralValue(packedData[0]), LiteralValue(packedFilter),
783+
expandedFilterShape.getShape(), DagType::intTensor(32, {numSlots}),
784+
/*zeroDiagonals=*/{}, unroll);
785+
auto actual = std::get<std::vector<int>>(evalKernel(dag)[0].get());
786+
787+
int64_t outputWidth = expected[0][0].size();
788+
RankedTensorType outputType = RankedTensorType::get(
789+
{1, 2, outputWidth}, mlir::IndexType::get(&context));
790+
auto resultLayout = get1dConvResultRelation(outputType, stride, /*padding=*/0,
791+
numSlots, interchangeRows);
792+
793+
EXPECT_EQ(
794+
unpackLayoutTo3DTensor<int>(resultLayout, {actual}, {1, 2, outputWidth}),
795+
expected);
796+
}
797+
798+
TEST_P(KernelImplementationTest, TestConv1dCwFcwStride2WithPadding) {
799+
// Same shape family as TestConv1dCwFcwStride2, but with padding != 0
800+
// padding 1 keeps the unpadded and padded column counts in the same
801+
// power-of-two bucket (2*6=12 and 2*8=16 both round to 16); padding 2 does
802+
// not (2*6=12 rounds to 16, 2*10=20 rounds to 32).
803+
checkPaddedConv1dCwFcw(/*padding=*/1, {{{45, 79, 111}, {29, 62, 86}}},
804+
std::get<0>(GetParam()), std::get<1>(GetParam()));
805+
checkPaddedConv1dCwFcw(/*padding=*/2, {{{12, 63, 95, 97}, {12, 50, 74, 56}}},
806+
std::get<0>(GetParam()), std::get<1>(GetParam()));
807+
}
808+
740809
TEST_P(KernelImplementationTest,
741810
TestConv2dNchwFchwStride2InterchangedLargeSlots) {
742811
MLIRContext context;

lib/Transforms/ConvertToCiphertextSemantics/ConvertToCiphertextSemantics.cpp

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,7 +1153,29 @@ struct ConvertLinalgConv1DNcwFcw
11531153
return isPowerOfTwoDims && isConv1dAsMatvec;
11541154
}
11551155

1156-
void haleviShoupKernel(
1156+
// Rebuild the operand shape LayoutPropagation diagonalized the filter
1157+
// against, honoring any zero tensor.pad it folded into the conv's `padding`
1158+
// parameter.
1159+
FailureOr<RankedTensorType> expandedFilterShape(
1160+
linalg::Conv1DNcwFcwOp op) const {
1161+
auto filterType = cast<RankedTensorType>(op.getInputs()[1].getType());
1162+
auto dataType = cast<RankedTensorType>(op.getInputs()[0].getType());
1163+
int64_t stride =
1164+
llvm::to_vector(op.getStrides().getValues<int64_t>()).front();
1165+
1166+
int64_t padding = getConvFoldedPadding(op);
1167+
std::optional<ConvMatrixOperand> matrixOperand =
1168+
foldConvWidthPadding(dataType, padding);
1169+
if (!matrixOperand) {
1170+
return op.emitError()
1171+
<< kConvFoldedPaddingAttrName << " of " << padding
1172+
<< " does not fit this conv's data operand " << dataType;
1173+
}
1174+
return get1dConvCwFcwFilterExpandedType(filterType, matrixOperand->dataType,
1175+
stride, matrixOperand->padding);
1176+
}
1177+
1178+
LogicalResult haleviShoupKernel(
11571179
linalg::Conv1DNcwFcwOp op, OpAdaptor adaptor,
11581180
ContextAwareConversionPatternRewriter& rewriter) const {
11591181
LLVM_DEBUG(
@@ -1168,13 +1190,8 @@ struct ConvertLinalgConv1DNcwFcw
11681190
cast<TypedValue<RankedTensorType>>(adaptor.getInputs()[1]);
11691191
SSAValue matrixLeaf(matrix);
11701192

1171-
// The original matrix shape is the shape of the expanded filter before
1172-
// diagonalization.
1173-
RankedTensorType expandedMatrixType = get1dConvCwFcwFilterExpandedType(
1174-
cast<RankedTensorType>(op.getInputs()[1].getType()),
1175-
cast<RankedTensorType>(op.getInputs()[0].getType()),
1176-
llvm::to_vector(op.getStrides().getValues<int64_t>()).front(),
1177-
/*padding=*/0);
1193+
FailureOr<RankedTensorType> expandedMatrixType = expandedFilterShape(op);
1194+
if (failed(expandedMatrixType)) return failure();
11781195
// Collect any zero diagonals of the filter matrix.
11791196
LayoutAttr filterLayout = getLayoutAttr(adaptor.getInputs()[1]);
11801197
auto filterRelation = filterLayout.getIntegerRelation();
@@ -1193,7 +1210,7 @@ struct ConvertLinalgConv1DNcwFcw
11931210
data.getType().getShape().back());
11941211
std::shared_ptr<ArithmeticDagNode<SSAValue>> implementedKernel =
11951212
implementHaleviShoup(vectorLeaf, matrixLeaf,
1196-
expandedMatrixType.getShape(), dagType,
1213+
expandedMatrixType->getShape(), dagType,
11971214
zeroDiagonals,
11981215
/*unroll=*/unrollKernels);
11991216

@@ -1207,6 +1224,7 @@ struct ConvertLinalgConv1DNcwFcw
12071224
// Add the initial accumulator value.
12081225
Value result = adaptor.getOutputs()[0];
12091226
addBiasAndReplace(rewriter, op, finalOutput, result, layoutAttr);
1227+
return success();
12101228
}
12111229

12121230
LogicalResult matchAndRewrite(
@@ -1223,8 +1241,7 @@ struct ConvertLinalgConv1DNcwFcw
12231241
}
12241242

12251243
if (supportsExpandedHaleviShoup(op, adaptor)) {
1226-
haleviShoupKernel(op, adaptor, rewriter);
1227-
return success();
1244+
return haleviShoupKernel(op, adaptor, rewriter);
12281245
}
12291246

12301247
return op.emitError() << "unsupported layout for 1d conv";
@@ -1300,6 +1317,10 @@ struct ConvertLinalgConv2DNchwFchw
13001317

13011318
// The original matrix shape is the shape of the expanded filter before
13021319
// diagonalization.
1320+
// NOTE: `padding=0` is only correct because nothing folds a `tensor.pad`
1321+
// into a 2-D conv's padding parameter. If that changes, go through
1322+
// foldConvWidthPadding the way ConvertLinalgConv1DNcwFcw does; see
1323+
// ConvMatrixOperand.
13031324
RankedTensorType expandedMatrixType = get2dConvChwFchwFilterExpandedType(
13041325
cast<RankedTensorType>(op.getInputs()[1].getType()), dataType,
13051326
/*padding=*/0, llvm::to_vector(op.getStrides().getValues<int64_t>()));

lib/Transforms/LayoutPropagation/LayoutPropagation.cpp

Lines changed: 145 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
#include "mlir/include/mlir/IR/BuiltinAttributes.h" // from @llvm-project
5050
#include "mlir/include/mlir/IR/BuiltinTypes.h" // from @llvm-project
5151
#include "mlir/include/mlir/IR/Diagnostics.h" // from @llvm-project
52+
#include "mlir/include/mlir/IR/Matchers.h" // from @llvm-project
5253
#include "mlir/include/mlir/IR/Operation.h" // from @llvm-project
5354
#include "mlir/include/mlir/IR/PatternMatch.h" // from @llvm-project
5455
#include "mlir/include/mlir/IR/Types.h" // from @llvm-project
@@ -134,9 +135,95 @@ std::pair<Value, LayoutAttr> convertToLayout(
134135
return std::make_pair(toReplace, layoutAttr);
135136
}
136137

138+
// The outcome of folding a zero `tensor.pad` on the width dim into a 1-D
139+
// conv's own `padding` parameter.
140+
struct FoldedConvPadding {
141+
// What the Toeplitz matrix must be built against.
142+
ConvMatrixOperand matrixOperand;
143+
// The layout the padded operand must already carry for the fold to be valid.
144+
IntegerRelation targetRelation;
145+
};
146+
147+
// Try to fold a zero `tensor.pad` on the width dim of a conv's `data` operand
148+
// into the conv's own `padding` parameter.
149+
//
150+
// Returns nullopt when the pattern does not apply, in which case the caller
151+
// keeps the unfolded path. `dataType` must be rank 3 with N=1.
152+
std::optional<FoldedConvPadding> tryFoldPadIntoConvPadding(
153+
Value data, RankedTensorType dataType, LayoutAttr dataLayout,
154+
int64_t ciphertextSize) {
155+
assert(dataType.getRank() == 3 && dataType.getDimSize(0) == 1 &&
156+
"expected a rank-3 N=1 conv data operand");
157+
158+
// DropUnitDims rewrites a rank-3 pad into
159+
// collapse_shape -> tensor.pad (rank 2) -> expand_shape, so peel any
160+
// reshape/cast chain to find the pad.
161+
Value cursor = data;
162+
tensor::PadOp padOp;
163+
while (Operation* def = cursor.getDefiningOp()) {
164+
if (auto p = dyn_cast<tensor::PadOp>(def)) {
165+
padOp = p;
166+
break;
167+
}
168+
if (isa<tensor::ExpandShapeOp, tensor::CollapseShapeOp, tensor::CastOp>(
169+
def)) {
170+
cursor = def->getOperand(0);
171+
continue;
172+
}
173+
break;
174+
}
175+
if (!padOp) return std::nullopt;
176+
177+
Value padValue = padOp.getConstantPaddingValue();
178+
bool zeroPad = padValue && (matchPattern(padValue, m_AnyZeroFloat()) ||
179+
matchPattern(padValue, m_Zero()));
180+
if (!zeroPad) return std::nullopt;
181+
182+
// Only a symmetric pad on the trailing (width) dim is expressible as the
183+
// conv's `padding` parameter, and only when the bounds are static. The pad
184+
// may be rank 2 (unit batch dim dropped) or rank 3.
185+
if (!padOp.getLow().empty() || !padOp.getHigh().empty()) return std::nullopt;
186+
ArrayRef<int64_t> low = padOp.getStaticLow();
187+
ArrayRef<int64_t> high = padOp.getStaticHigh();
188+
bool widthOnly = !low.empty() && low.size() == high.size() &&
189+
low.back() == high.back() && low.back() > 0;
190+
for (size_t i = 0; widthOnly && i + 1 < low.size(); ++i) {
191+
widthOnly &= low[i] == 0 && high[i] == 0;
192+
}
193+
if (!widthOnly) return std::nullopt;
194+
195+
// The reshape chain above means the pad's width dim is not guaranteed to be
196+
// this conv operand's width dim, so validate before building a type from it.
197+
int64_t p = low.back();
198+
std::optional<ConvMatrixOperand> matrixOperand =
199+
foldConvWidthPadding(dataType, p);
200+
if (!matrixOperand) return std::nullopt;
201+
202+
// The layout we expect on the padded value: the unpadded row-major layout
203+
// with the width index shifted by `p`. If the actual layout is anything else
204+
// (a conversion intervened, a non-row-major producer, reshapes that did not
205+
// cancel) do not fold, rather than silently mis-indexing the matrix.
206+
IntegerRelation expected =
207+
getRowMajorLayoutRelation(matrixOperand->dataType, ciphertextSize);
208+
expected = shiftVar(
209+
expected, expected.getVarKindOffset(presburger::VarKind::Domain) + 2, p);
210+
if (!dataLayout.getIntegerRelation().isEqual(expected)) {
211+
LLVM_DEBUG(llvm::dbgs()
212+
<< "conv_1d found a pad of " << p
213+
<< " but the operand layout does not match the shifted "
214+
"unpadded row-major layout; not folding\n");
215+
return std::nullopt;
216+
}
217+
218+
LLVM_DEBUG(llvm::dbgs() << "conv_1d folding tensor.pad of " << p
219+
<< " into the conv padding parameter\n");
220+
return FoldedConvPadding{*matrixOperand, expected};
221+
}
222+
137223
// Return a copy of the kernel info associated with the value and update the
138224
// result shape to the new result shape. If the value does not have a kernel
139225
// info, return an empty Attribute.
226+
140227
Attribute cloneKernelInfoWithResultShape(Value value,
141228
ArrayRef<int64_t> resultShape) {
142229
auto kernelInfo = findAttributeAssociatedWith(value, kKernelInfoAttrName);
@@ -179,6 +266,7 @@ struct LayoutPropagation : impl::LayoutPropagationBase<LayoutPropagation> {
179266
LogicalResult visitOperation(tensor::InsertOp op);
180267
LogicalResult visitOperation(tensor::InsertSliceOp op);
181268
LogicalResult visitOperation(tensor::ExtractSliceOp op);
269+
LogicalResult visitOperation(tensor::PadOp op);
182270

183271
// Determine if the operation arguments have compatible layouts for the
184272
// given op. If the check fails, the CompatibilityResult::compatible field
@@ -353,8 +441,8 @@ LogicalResult LayoutPropagation::visitOperation(Operation* op) {
353441
.Case<affine::AffineForOp>([&](auto op) { return visitOperation(op); })
354442
// tensor ops
355443
.Case<tensor::ExtractOp, tensor::InsertOp, tensor::InsertSliceOp,
356-
tensor::ExtractSliceOp, CollapseShapeOp, ExpandShapeOp>(
357-
[&](auto op) { return visitOperation(op); })
444+
tensor::ExtractSliceOp, tensor::PadOp, CollapseShapeOp,
445+
ExpandShapeOp>([&](auto op) { return visitOperation(op); })
358446
// AddI, AddF, mgmt.* all pass the layout through unchanged.
359447
.Default([&](Operation* op) {
360448
passLayoutThroughOp(op);
@@ -907,8 +995,15 @@ LogicalResult LayoutPropagation::visitOperation(Conv1DNcwFcwOp op) {
907995
}
908996

909997
LayoutAttr dataLayout = getComposedLayoutAttr(data);
998+
999+
ConvMatrixOperand matrixOperand{dataType};
9101000
IntegerRelation targetDataRelation =
9111001
getRowMajorLayoutRelation(dataType, minSlotCount);
1002+
if (auto folded =
1003+
tryFoldPadIntoConvPadding(data, dataType, dataLayout, minSlotCount)) {
1004+
matrixOperand = folded->matrixOperand;
1005+
targetDataRelation = folded->targetRelation;
1006+
}
9121007

9131008
if (!isRelationEqual(dataLayout.getIntegerRelation(), targetDataRelation)) {
9141009
LLVM_DEBUG(llvm::dbgs() << "conv_1d data input is not row major, "
@@ -923,8 +1018,8 @@ LogicalResult LayoutPropagation::visitOperation(Conv1DNcwFcwOp op) {
9231018
// into a larger matrix and then diagonalizing.
9241019
LayoutAttr filterLayout = getComposedLayoutAttr(filter);
9251020
auto convRelation = get1dConvCwFcwFilterDiagonalizedRelation(
926-
filterType, dataType, stride, /*padding=*/0, minSlotCount,
927-
/*interchangeRows=*/interchangeRows);
1021+
filterType, matrixOperand.dataType, stride, matrixOperand.padding,
1022+
minSlotCount, /*interchangeRows=*/interchangeRows);
9281023
if (failed(convRelation)) {
9291024
return failure();
9301025
}
@@ -956,6 +1051,10 @@ LogicalResult LayoutPropagation::visitOperation(Conv1DNcwFcwOp op) {
9561051
Attribute kernelInfoAttr =
9571052
cloneKernelInfoWithResultShape(data, outputType.getShape());
9581053

1054+
// Record what the filter was diagonalized against, so that
1055+
// ConvertToCiphertextSemantics rebuilds the same expanded matrix shape.
1056+
setConvFoldedPadding(op, matrixOperand.padding);
1057+
9591058
assignedLayouts.insert({result, resultLayoutAttr});
9601059
setResultLayoutAttr(op, kernelInfoAttr);
9611060
auto kernelAttr =
@@ -1395,6 +1494,48 @@ LogicalResult LayoutPropagation::visitOperation(tensor::InsertSliceOp op) {
13951494
return success();
13961495
}
13971496

1497+
LogicalResult LayoutPropagation::visitOperation(tensor::PadOp op) {
1498+
// A zero-pad does not move any data: result[i + low] = source[i], so the
1499+
// result layout is the source layout with each domain index shifted by the
1500+
// low padding. Pad positions stay unmapped in the relation; unmapped points
1501+
// are zero-filled when a layout is materialized, which matches the
1502+
// zero-fill pad body. (The default layout passthrough would instead map
1503+
// result[i] to source[i]'s slots — off by `low`, shifting every downstream
1504+
// consumer's reads by one stride per pad.)
1505+
if (!assignedLayouts.contains(op.getSource())) {
1506+
return op->emitError() << "Source tensor has no assigned layout";
1507+
}
1508+
Value padValue = op.getConstantPaddingValue();
1509+
if (!padValue || !(matchPattern(padValue, m_AnyZeroFloat()) ||
1510+
matchPattern(padValue, m_Zero()))) {
1511+
return op->emitError()
1512+
<< "layout propagation only supports zero-padding tensor.pad";
1513+
}
1514+
if (!op.getLow().empty() || !op.getHigh().empty()) {
1515+
return op->emitError()
1516+
<< "layout propagation requires static tensor.pad bounds";
1517+
}
1518+
1519+
IntegerRelation padRelation =
1520+
getComposedLayoutAttr(op.getSource()).getIntegerRelation();
1521+
auto domainVarOffset =
1522+
padRelation.getVarKindOffset(presburger::VarKind::Domain);
1523+
for (auto [dim, low] : llvm::enumerate(op.getStaticLow())) {
1524+
if (low != 0) {
1525+
padRelation = shiftVar(padRelation, domainVarOffset + dim, low);
1526+
}
1527+
}
1528+
1529+
LayoutAttr outputLayout =
1530+
LayoutAttr::getFromIntegerRelation(op.getContext(), padRelation);
1531+
Attribute kernelInfoAttr = cloneKernelInfoWithResultShape(
1532+
op.getSource(), op.getResultType().getShape());
1533+
assignedLayouts.insert({op.getResult(), outputLayout});
1534+
debugAssignLayout(op.getResult(), outputLayout);
1535+
setResultLayoutAttr(op, kernelInfoAttr);
1536+
return success();
1537+
}
1538+
13981539
LogicalResult LayoutPropagation::visitOperation(tensor::ExtractSliceOp op) {
13991540
// Assign the induced layout from extracting a slice from the source tensor.
14001541
if (!assignedLayouts.contains(op.getSource())) {

0 commit comments

Comments
 (0)