Skip to content

Commit 0d81d77

Browse files
committed
Rotom: axis roll endpoints and per-piece roll semantics
A roll endpoint is now either a piece -- a dims-list position, the original Rotom reading -- or a whole tensor axis, spelled 'axis N' and stored as -(axis+1) in the flat rolls array. An axis endpoint is legal only when the axis is packed as more than one piece; the piece spelling is canonical for an unsplit axis, where the two coincide. A piece FROM rewrites only its own mixed-radix digit -- the original per-piece semantics, now materialized correctly on split axes (no borrow crosses digits). An axis FROM rewrites the whole axis index modulo its full extent, each piece then taking its digit of the rolled index: the borrow across digits is what diagonal packings over a split axis need and no combination of piece rolls can express. A BY piece of a split axis shifts by that piece's digit of the axis's current (possibly already-rolled) expression. Rolls always shift by exactly the partner index: a roll is a packing fact whose alignment partner is another layout's roll, endpoint for endpoint. Kernel-schedule shifts -- e.g. the baby-step/giant-step giant pre-rotation, which shifts by a MULTIPLE of a digit -- are deliberately not layout vocabulary: the emitter accepts them as a plan-level PreRotation composed into the materialized relation (the packed bytes are pre-rotated; the layout stays unit-step and alignable endpoint for endpoint), and the kernel plan that wants the packing carries them as a named encoding. The BSGS diagonal packing is then an ordinary layout plus its plan encoding: #rotom.layout<n = 16, rolls = [(axis 1, 2)], dims = [[1:4:4], [1:4:1] | [0:16:1]]> where the roll diagonalizes the whole split k against i (ciphertext (g, b) holds the digits of (k - i) mod 16) and the plan's pre-rotation shifts i by 4g, giant digit read from the rolled k.
1 parent c98f428 commit 0d81d77

8 files changed

Lines changed: 637 additions & 110 deletions

File tree

lib/Dialect/Rotom/IR/RotomAttributes.cpp

Lines changed: 146 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -227,25 +227,44 @@ static ParseResult parseLayoutDims(AsmParser& parser,
227227
}
228228
}
229229

230+
// One endpoint of a roll pair: a bare non-negative integer is a dims-list
231+
// position (one piece); `axis N` names the whole tensor axis N.
232+
static ParseResult parseRollEndpoint(AsmParser& parser, int64_t& encoded) {
233+
const bool isAxis = succeeded(parser.parseOptionalKeyword("axis"));
234+
int64_t value;
235+
if (parser.parseInteger(value)) return failure();
236+
if (value < 0) {
237+
return parser.emitError(parser.getNameLoc())
238+
<< (isAxis ? "an axis roll endpoint must name a non-negative "
239+
"tensor axis"
240+
: "a piece roll endpoint must be a non-negative dims "
241+
"position (spell a whole-axis endpoint as `axis N`)");
242+
}
243+
encoded = encodeRollEndpoint({isAxis, value});
244+
return success();
245+
}
246+
230247
static ParseResult parseLayoutRolls(AsmParser& parser,
231248
SmallVector<int64_t>& rolls) {
232249
if (parser.parseLSquare()) return failure();
233250
if (succeeded(parser.parseOptionalRSquare())) return success();
234251

252+
// Each entry is one complete roll pair, either a `(from, to)` tuple or two
253+
// bare endpoints `from, to`.
235254
while (true) {
255+
int64_t from;
256+
int64_t to;
236257
if (succeeded(parser.parseOptionalLParen())) {
237-
int64_t from;
238-
int64_t to;
239-
if (parser.parseInteger(from) || parser.parseComma() ||
240-
parser.parseInteger(to) || parser.parseRParen())
258+
if (failed(parseRollEndpoint(parser, from)) || parser.parseComma() ||
259+
failed(parseRollEndpoint(parser, to)) || parser.parseRParen())
241260
return failure();
242-
rolls.push_back(from);
243-
rolls.push_back(to);
244261
} else {
245-
int64_t value;
246-
if (parser.parseInteger(value)) return failure();
247-
rolls.push_back(value);
262+
if (failed(parseRollEndpoint(parser, from)) || parser.parseComma() ||
263+
failed(parseRollEndpoint(parser, to)))
264+
return failure();
248265
}
266+
rolls.push_back(from);
267+
rolls.push_back(to);
249268

250269
if (succeeded(parser.parseOptionalComma())) continue;
251270
return parser.parseRSquare();
@@ -255,48 +274,109 @@ static ParseResult parseLayoutRolls(AsmParser& parser,
255274
static LogicalResult verifyLayoutRolls(
256275
ArrayAttr dims, DenseI64ArrayAttr rolls,
257276
function_ref<InFlightDiagnostic()> emitError) {
258-
if (!rolls) return success();
277+
const bool noRolls = !rolls || rolls.empty();
278+
if (noRolls) return success();
259279
ArrayRef<int64_t> r = rolls.asArrayRef();
260-
if (r.empty()) return success();
261280
if (r.size() % 2 != 0) {
262-
return emitError() << "rolls must contain an even number of integers "
263-
"(pairs of dim indices)";
281+
return emitError() << "rolls must contain an even number of endpoints "
282+
"(pairs)";
264283
}
265284

285+
// Traversal pieces of a tensor axis: their count decides whether an `axis`
286+
// endpoint is legal (split axes only -- the piece spelling is canonical
287+
// when the axis is one piece), and their extent product is the modulus a
288+
// whole-axis rewrite reduces by.
289+
auto piecesOfAxis = [&](int64_t axis) {
290+
std::pair<int64_t, int64_t> countAndExtent{0, 1};
291+
for (Attribute a : dims) {
292+
auto d = dyn_cast<DimAttr>(a);
293+
if (d && !d.isGap() && !d.isReplicate() && d.getDim() == axis) {
294+
++countAndExtent.first;
295+
countAndExtent.second *= d.getSize();
296+
}
297+
}
298+
return countAndExtent;
299+
};
300+
266301
for (size_t i = 0; i < r.size(); i += 2) {
267-
const int64_t ti = r[i];
268-
const int64_t tj = r[i + 1];
269-
if (ti == tj) {
270-
return emitError() << "each roll must use two distinct dim indices";
302+
const RollEndpoint from = decodeRollEndpoint(r[i]);
303+
const RollEndpoint by = decodeRollEndpoint(r[i + 1]);
304+
305+
// Resolve each endpoint: the piece it names (null for axis endpoints)
306+
// and the tensor axis it reads or rewrites (sentinel for gap/replication
307+
// pieces).
308+
DimAttr fromPiece;
309+
DimAttr byPiece;
310+
int64_t fromAxis = 0;
311+
int64_t byAxis = 0;
312+
auto checkEndpoint = [&](const RollEndpoint& e, DimAttr& piece,
313+
int64_t& axis) -> LogicalResult {
314+
if (e.isAxis) {
315+
auto [count, extent] = piecesOfAxis(e.index);
316+
(void)extent;
317+
if (count == 0) {
318+
return emitError() << "an axis roll endpoint must name a tensor "
319+
"axis present in dims";
320+
}
321+
if (count == 1) {
322+
return emitError() << "an axis roll endpoint requires a split "
323+
"axis; spell an unsplit axis's endpoint as "
324+
"its piece position";
325+
}
326+
axis = e.index;
327+
return success();
328+
}
329+
if (e.index >= static_cast<int64_t>(dims.size())) {
330+
return emitError() << "roll piece endpoint out of bounds for dims "
331+
"list";
332+
}
333+
piece = dyn_cast<DimAttr>(dims[e.index]);
334+
if (!piece) {
335+
return emitError() << "roll endpoints must refer to #rotom.dim "
336+
"entries";
337+
}
338+
axis = piece.getDim();
339+
return success();
340+
};
341+
if (failed(checkEndpoint(from, fromPiece, fromAxis)) ||
342+
failed(checkEndpoint(by, byPiece, byAxis))) {
343+
return failure();
271344
}
272-
if (ti < 0 || tj < 0 || ti >= static_cast<int64_t>(dims.size()) ||
273-
tj >= static_cast<int64_t>(dims.size())) {
274-
return emitError() << "roll dim index out of bounds for dims list";
345+
346+
// The extents need not match: a roll rewrites the from index to
347+
// (idx - shift) mod extent(from), well-defined for any partner extent (a
348+
// smaller partner covers a prefix of the rotations, a larger one wraps).
349+
// FROM is the index expression being rewritten, so it must be a
350+
// traversal piece or a whole (traversal) axis. The by endpoint may be
351+
// any kind: rolling by a replication or gap piece shifts by that piece's
352+
// block index, so each block holds a distinct cyclic rotation of the
353+
// rolled index -- the layout materializes every rotation and alignment
354+
// becomes block selection. (A rolled-by gap thus claims its blocks,
355+
// unlike a plain gap.)
356+
if (!from.isAxis && (fromPiece.isGap() || fromPiece.isReplicate())) {
357+
return emitError() << "the rolled dim must be a traversal dim (dim >= "
358+
"0)";
275359
}
276-
auto di = dyn_cast<DimAttr>(dims[ti]);
277-
auto dj = dyn_cast<DimAttr>(dims[tj]);
278-
if (!di || !dj) {
279-
return emitError() << "roll indices must refer to #rotom.dim entries";
360+
// A roll may not shift an index by itself. Piece endpoints must be
361+
// distinct positions (two pieces of one axis are distinct digits); an
362+
// axis endpoint overlaps every endpoint on the same axis, because a
363+
// whole-axis rewrite touches all of its digits.
364+
if (!from.isAxis && !by.isAxis && from.index == by.index) {
365+
return emitError() << "each roll must use two distinct endpoints";
280366
}
281-
// The extents need not match: roll(i, j) rewrites dims[i]'s index to
282-
// (i_i - i_j) mod size(dims[i]), well-defined for any partner extent (a
283-
// smaller partner covers a prefix of the rotations, a larger one wraps).
284-
// The rolled (from) dim must be a traversal dim -- it is the index
285-
// expression being rewritten. The roll-by (second) dim may be any kind:
286-
// rolling by a replication or gap dim shifts by that dim's block index,
287-
// so each block holds a distinct cyclic rotation of the rolled dim -- the
288-
// layout materializes every rotation and alignment becomes block
289-
// selection. (A rolled-by gap thus claims its blocks, unlike a plain gap.)
290-
if (di.isGap() || di.isReplicate()) {
291-
return emitError() << "the rolled dim must be a traversal dim (dim >= 0)";
367+
if ((from.isAxis || by.isAxis) && fromAxis == byAxis) {
368+
return emitError() << "a roll may not shift an axis by one of its own "
369+
"pieces";
292370
}
293371
// A rolled-by GAP claims one ciphertext block per gap index, each holding
294-
// a distinct rotation of the rolled dim. If the gap is larger than the
295-
// rolled dim's extent the rotations repeat (period = the from extent),
296-
// claiming blocks the conversion/kernel accounting was never audited for.
372+
// a distinct rotation of the rolled index. If the gap is larger than the
373+
// rolled extent the rotations repeat (period = the from extent), claiming
374+
// blocks the conversion/kernel accounting was never audited for.
297375
// (Replication partners of larger extent are intended -- replicate-then-
298376
// roll -- so only gaps are bounded.)
299-
if (dj.isGap() && dj.getSize() > di.getSize()) {
377+
const int64_t fromExtent =
378+
from.isAxis ? piecesOfAxis(fromAxis).second : fromPiece.getSize();
379+
if (byPiece && byPiece.isGap() && byPiece.getSize() > fromExtent) {
300380
return emitError() << "a rolled-by gap dim must not exceed the rolled "
301381
"dim's extent";
302382
}
@@ -345,10 +425,19 @@ void LayoutAttr::print(AsmPrinter& printer) const {
345425
DenseI64ArrayAttr rolls = getRolls();
346426
if (rolls && !rolls.asArrayRef().empty()) {
347427
ArrayRef<int64_t> values = rolls.asArrayRef();
428+
auto printEndpoint = [&](int64_t encoded) {
429+
const RollEndpoint e = decodeRollEndpoint(encoded);
430+
if (e.isAxis) printer << "axis ";
431+
printer << e.index;
432+
};
348433
printer << ", rolls = [";
349434
for (size_t i = 0; i < values.size(); i += 2) {
350435
if (i != 0) printer << ", ";
351-
printer << "(" << values[i] << ", " << values[i + 1] << ")";
436+
printer << "(";
437+
printEndpoint(values[i]);
438+
printer << ", ";
439+
printEndpoint(values[i + 1]);
440+
printer << ")";
352441
}
353442
printer << "]";
354443
}
@@ -436,6 +525,18 @@ Attribute LayoutAttr::parse(AsmParser& parser, Type type) {
436525
ArrayAttr::get(context, dims), n, DenseI64ArrayAttr::get(context, rolls));
437526
}
438527

528+
SmallVector<RollSpec> getRollSpecs(LayoutAttr layout) {
529+
SmallVector<RollSpec> specs;
530+
DenseI64ArrayAttr rolls = layout.getRolls();
531+
if (!rolls) return specs;
532+
ArrayRef<int64_t> r = rolls.asArrayRef();
533+
534+
for (size_t i = 0; i + 1 < r.size(); i += 2) {
535+
specs.push_back({decodeRollEndpoint(r[i]), decodeRollEndpoint(r[i + 1])});
536+
}
537+
return specs;
538+
}
539+
439540
FailureOr<LayoutData> preprocessLayoutAttr(LayoutAttr layout) {
440541
return preprocessLayoutData(layout.getDims(), layout.getN(),
441542
layout.getContext());
@@ -452,7 +553,9 @@ LogicalResult LayoutAttr::verify(function_ref<InFlightDiagnostic()> emitError,
452553
return emitError() << "`dims` must be an array of `#rotom.dim<...>`";
453554
}
454555

455-
if (failed(verifyLayoutRolls(dims, rolls, emitError))) return failure();
556+
if (failed(verifyLayoutRolls(dims, rolls, emitError))) {
557+
return failure();
558+
}
456559

457560
SmallVector<DimAttr> dimVec;
458561
dimVec.reserve(dims.size());
@@ -508,7 +611,8 @@ void canonicalizeLayoutDims(MLIRContext* ctx, SmallVector<DimAttr>& dims,
508611
if (fill <= 1) return;
509612
dims.insert(dims.begin() + ctLen,
510613
DimAttr::get(ctx, /*dim=*/-2, fill, /*stride=*/1));
511-
// Roll endpoints at or past the insertion shift right.
614+
// Piece endpoints at or past the insertion shift right; axis endpoints
615+
// (encoded negative) name axes and do not move.
512616
for (int64_t& encoded : rolls) {
513617
if (encoded >= static_cast<int64_t>(ctLen)) ++encoded;
514618
}

lib/Dialect/Rotom/IR/RotomAttributes.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,38 @@
1515

1616
namespace mlir::heir::rotom {
1717

18+
// One endpoint of a roll: either one piece of the layout (a position in its
19+
// dims list) or a whole tensor axis (spelled `axis N`; legal only when the
20+
// axis is packed as more than one piece).
21+
struct RollEndpoint {
22+
bool isAxis;
23+
int64_t index; // Dims-list position, or the tensor axis id when isAxis.
24+
bool operator==(const RollEndpoint& other) const {
25+
return isAxis == other.isAxis && index == other.index;
26+
}
27+
};
28+
29+
// Endpoint encoding in the flat rolls storage: a piece endpoint is its
30+
// non-negative dims-list position, an axis endpoint is -(axis + 1).
31+
inline int64_t encodeRollEndpoint(RollEndpoint e) {
32+
return e.isAxis ? -(e.index + 1) : e.index;
33+
}
34+
inline RollEndpoint decodeRollEndpoint(int64_t encoded) {
35+
return encoded < 0 ? RollEndpoint{true, -encoded - 1}
36+
: RollEndpoint{false, encoded};
37+
}
38+
39+
// One roll of a layout: FROM's index is rewritten to
40+
// (idx_from - shift(by)) mod extent(from), where a piece FROM rewrites only
41+
// its own mixed-radix digit and an axis FROM rewrites the whole axis index.
42+
struct RollSpec {
43+
RollEndpoint from;
44+
RollEndpoint by;
45+
};
46+
47+
// The layout's rolls with both endpoints decoded.
48+
llvm::SmallVector<RollSpec> getRollSpecs(LayoutAttr layout);
49+
1850
enum class LayoutPieceKind { Traversal, Replication, Gap };
1951

2052
struct LayoutPiece {

lib/Dialect/Rotom/IR/RotomAttributes.td

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,23 @@ def Rotom_LayoutAttr : Rotom_Attr<"Layout", "layout"> {
5555
with an explicit gap piece (e.g. `[G:4:1]`).
5656
See [Section 4.2 of the Rotom paper](https://eprint.iacr.org/2025/1319.pdf).
5757

58-
Optional **rolls** encode a `roll(i,j)` metadata object: each pair `(i, j)`
59-
indexes into the `dims` array (the flattened `ct_dims + slot_dims` list) and
60-
rewrites `dims[i]`'s index to `(idx_i - idx_j) mod size(dims[i])`. The two
61-
extents need not match: the shift reduces modulo the rolled dim's extent, so
62-
a smaller partner covers a prefix of the rotations and a larger one wraps.
58+
Optional **rolls** encode `roll(from, by)` metadata objects, applied left
59+
to right. Each endpoint is either a *piece* -- a position in the `dims`
60+
list, spelled as a bare integer -- or a whole tensor *axis*, spelled
61+
`axis N` (legal only when axis `N` is packed as more than one piece; the
62+
piece spelling is canonical for an unsplit axis, where the two coincide).
63+
64+
A piece FROM rewrites that piece's own mixed-radix digit in place,
65+
`digit(from) <- (digit(from) - shift(by)) mod extent(from)`, leaving
66+
the axis's other digits untouched. An `axis` FROM rewrites the whole axis
67+
index modulo its full extent, and each piece then takes its digit of the
68+
rolled index -- the shift borrows across digits, which no combination of
69+
piece rolls can express. The shift is the by endpoint's index: a
70+
traversal piece's digit (its whole index when the axis is unsplit), a
71+
whole axis's index via `axis`, a replication piece's replica index, or a
72+
gap piece's block index. The two extents need not match: the shift
73+
reduces modulo the rolled extent, so a smaller partner covers a prefix of
74+
the rotations and a larger one wraps.
6375
}];
6476

6577
let parameters = (ins

0 commit comments

Comments
 (0)