MLIR 24.0.0git
XeGPUOps.cpp
Go to the documentation of this file.
1//===- XeGPUOps.cpp - MLIR XeGPU ops implementation -------------*- C++ -*-===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8
14#include "mlir/IR/Builders.h"
17
18#include "llvm/Support/Debug.h"
19
20#include <utility>
21
22#define DEBUG_TYPE "xegpu"
23
24using namespace mlir;
25using namespace mlir::xegpu;
26
27template <typename T>
28static std::string makeString(const T &array, bool breakline = false) {
29 std::string buf;
30 buf.clear();
31 llvm::raw_string_ostream os(buf);
32 os << "[";
33 for (size_t i = 1; i < array.size(); i++) {
34 os << array[i - 1] << ", ";
35 if (breakline)
36 os << "\n\t\t";
37 }
38 os << array.back() << "]";
39 return buf;
40}
41
44 if (auto ty = llvm::dyn_cast<ShapedType>(type))
45 shape = SmallVector<int64_t>(ty.getShape());
46 else
47 shape.push_back(1);
48 return shape;
49}
50
51static bool isReadHintOrNone(const CachePolicyAttr &attr) {
52 if (!attr)
53 return true;
54 auto kind = attr.getValue();
55 return kind == CachePolicy::CACHED || kind == CachePolicy::UNCACHED ||
56 kind == CachePolicy::STREAMING || kind == CachePolicy::READ_INVALIDATE;
57}
58
59static bool isWriteHintOrNone(const CachePolicyAttr &attr) {
60 if (!attr)
61 return true;
62 auto kind = attr.getValue();
63 return kind == CachePolicy::CACHED || kind == CachePolicy::UNCACHED ||
64 kind == CachePolicy::WRITE_BACK || kind == CachePolicy::WRITE_THROUGH;
65}
66
67static LogicalResult
69 VectorType valueTy,
71
72 auto maskVecTy = dyn_cast<VectorType>(maskTy);
73 auto offsetsVecTy = dyn_cast<VectorType>(offsetsTy);
74
75 // The offsets and mask describe the same lane grid: either both are scalar
76 // (single lane) or both are vectors carrying one offset and one mask bit per
77 // lane, so they must have the same shape.
78 if (static_cast<bool>(maskVecTy) != static_cast<bool>(offsetsVecTy))
79 return emitError() << "Expecting offsets and mask to both be scalar or "
80 "both be vectors.";
81 if (maskVecTy && getShapeOf(maskTy) != getShapeOf(offsetsTy))
82 return emitError() << "Expecting offsets and mask to have the same shape.";
83
84 // Scalar payload (e.g. `index, i1 -> f16`): the offsets and mask must be
85 // scalar as well.
86 if (!valueTy) {
87 if (maskVecTy || offsetsVecTy)
88 return emitError() << "Expecting scalar mask and offsets.";
89 return success();
90 }
91
92 // One offset and one mask bit per element, so the value has the same shape as
93 // the mask and the offsets. A scalar and a size-1 vector both stand for a
94 // single element, so they are interchangeable.
95 int64_t maskSize = maskVecTy ? maskVecTy.getNumElements() : 1;
96 if (valueTy.getNumElements() != maskSize ||
97 (maskVecTy && getShapeOf(valueTy) != getShapeOf(maskTy)))
98 return emitError() << "Value shape must match mask shape.";
99 return success();
100}
101
102// Validates the `contiguity` attribute against the op's offsets type: the
103// innermost offsets dimension is contiguous in runs of `size`, so `size` must
104// be >= 2 and must divide that dimension.
105static LogicalResult
106isValidContiguity(std::optional<uint64_t> contiguity, Type offsetsTy,
108 if (!contiguity)
109 return success();
110 auto offsetsVecTy = dyn_cast<VectorType>(offsetsTy);
111 if (!offsetsVecTy)
112 return emitError() << "contiguity requires vector offsets (one per lane).";
113 int64_t size = static_cast<int64_t>(*contiguity);
114 int64_t inner = offsetsVecTy.getShape().back();
115 if (size < 2)
116 return emitError() << "contiguity = " << size << " (must be >= 2)";
117 if (inner % size != 0)
118 return emitError() << "contiguity = " << size
119 << " (must divide the innermost offsets dim " << inner
120 << ")";
121 return success();
122}
123
124LogicalResult
125IsValidMatrixOpParams(VectorType dataTy, MemDescType mdescTy,
126 UnitAttr subgroup_block_io, DistributeLayoutAttr layout,
128
129 if (!dataTy) {
130 if (subgroup_block_io)
131 return emitError() << "subgroup_block_io "
132 "are only allowed when result is a VectorType.";
133 else
134 return success();
135 }
136
137 ArrayRef<int64_t> dataShape = dataTy.getShape();
138 ArrayRef<int64_t> mdescShape = mdescTy.getShape();
139
140 SmallVector<int64_t> blockShape = mdescTy.getBlockShape();
141 ArrayAttr strideAttr = mdescTy.getStrideAttr();
142 SmallVector<int64_t> strides;
143 for (Attribute attr : strideAttr.getValue()) {
144 strides.push_back(cast<IntegerAttr>(attr).getInt());
145 }
146 if (subgroup_block_io && layout) {
147 auto laneData = layout.getEffectiveLaneDataAsInt();
148 auto laneLayout = layout.getEffectiveLaneLayoutAsInt();
149 if (!laneData.empty()) {
150 bool isLaneDataContiguous =
151 std::all_of(laneData.begin(), std::prev(laneData.end()),
152 [](int x) { return x == 1; });
153 if (!isLaneDataContiguous)
154 return emitError() << "With subgroup_block_io, accessed data must be "
155 "contiguous and coalesced.";
156 for (size_t i = 0; i < laneData.size(); ++i) {
157 if (laneLayout[i] != blockShape[i])
158 return emitError() << "With subgroup_block_io, the block shape must "
159 "match the lane layout.";
160 if (laneLayout[i] != 1 && strides[i] != 1)
161 return emitError() << "With subgroup_block_io, the distributed "
162 "dimensions must be contiguous.";
163 }
164 }
165 }
166
167 if (layout && !layout.isDistributable(
168 SmallVector<int64_t>(dataShape.begin(), dataShape.end())))
169 return emitError() << "Value shape is not distributable with the layout";
170
171 if (dataShape.size() == mdescShape.size()) {
172 if (llvm::any_of(llvm::zip_equal(dataShape, mdescShape),
173 [](auto p) { return std::get<0>(p) > std::get<1>(p); }))
174 return emitError() << "data shape must not exceed mem_desc shape.";
175 }
176 // if the subgroup_block_io attribute is set, mdescTy must have block
177 // attribute
178 if (subgroup_block_io && !blockShape.size())
179 return emitError() << "mem_desc must have block attribute when "
180 "subgroup_block_io is set.";
181 return success();
182}
183
184//===----------------------------------------------------------------------===//
185// XeGPU_CreateMemDescOp
186//===----------------------------------------------------------------------===//
187LogicalResult CreateMemDescOp::verify() {
188 auto srcTy = getSource().getType();
190 return emitOpError("source memref must be contiguous.");
191 return success();
192}
193
194//===----------------------------------------------------------------------===//
195// XeGPU_CreateNdDescOp
196//===----------------------------------------------------------------------===//
197
198void CreateNdDescOp::build(OpBuilder &builder, OperationState &state,
199 Type tdesc, TypedValue<MemRefType> source) {
200 build(builder, state, tdesc, source, ValueRange({}) /* empty dynamic shape */,
201 ValueRange({}) /* empty dynamic strides */,
202 DenseI64ArrayAttr({}) /* empty const shape*/,
203 DenseI64ArrayAttr({}) /* empty const strides*/);
204}
205
206void CreateNdDescOp::build(OpBuilder &builder, OperationState &state,
207 Type tdesc, Value source,
210 Type srcTy = source.getType();
211 assert((isa<IntegerType, MemRefType>(srcTy)) &&
212 "Source has to be either int or memref.");
213
214 llvm::SmallVector<Value> dynamicShape;
215 llvm::SmallVector<Value> dynamicStrides;
216
217 llvm::SmallVector<int64_t> staticShape;
218 llvm::SmallVector<int64_t> staticStrides;
219
220 dispatchIndexOpFoldResults(shape, dynamicShape, staticShape);
221 dispatchIndexOpFoldResults(strides, dynamicStrides, staticStrides);
222
223 auto staticShapeAttr = builder.getDenseI64ArrayAttr(staticShape);
224 auto staticStridesAttr = builder.getDenseI64ArrayAttr(staticStrides);
225
226 if (auto memrefTy = dyn_cast<MemRefType>(srcTy)) {
227 auto memrefShape = memrefTy.getShape();
228 auto [memrefStrides, _] = memrefTy.getStridesAndOffset();
229
230 // if shape and strides are from Memref, we don't need attributes for them
231 // to keep the IR print clean (only do so for full-static case, otherwise
232 // printer would fail trying to print empty array-attr).
233 if (staticShape == memrefShape && staticStrides == memrefStrides &&
234 dynamicShape.empty() && dynamicStrides.empty()) {
235 staticShapeAttr = DenseI64ArrayAttr();
236 staticStridesAttr = DenseI64ArrayAttr();
237 }
238 }
239
240 build(builder, state, tdesc, source, dynamicShape, dynamicStrides,
241 staticShapeAttr, staticStridesAttr);
242}
243
244LogicalResult CreateNdDescOp::verify() {
245 auto srcMemrefTy = dyn_cast<MemRefType>(getSourceType());
246 size_t rank = srcMemrefTy ? srcMemrefTy.getRank() : getMixedSizes().size();
247 bool invalidElemTy = false;
248
249 // Memory space of created TensorDesc should match with the source.
250 // Both source and TensorDesc are considered for global memory by default,
251 // if the memory scope attr is not specified. If source is an integer,
252 // it is considered as ptr to global memory.
253 auto srcMemorySpace = getSourceMemorySpace();
254 auto tdescMemorySpace = static_cast<unsigned>(getType().getMemorySpace());
255 if (srcMemorySpace != tdescMemorySpace)
256 return emitOpError("Memory space mismatch.")
257 << " Source: " << srcMemorySpace
258 << ", TensorDesc: " << tdescMemorySpace;
259
260 // check source type matches the rank if it is a memref.
261 // It also should have the same ElementType as TensorDesc.
262 if (auto memrefTy = dyn_cast<MemRefType>(getSourceType()))
263 invalidElemTy |= memrefTy.getElementType() != getElementType();
264
265 bool hasExplicitShapeStrides =
266 !getShape().empty() || !getStrides().empty() ||
267 (getConstShapeAttr() && !getConstShapeAttr().empty()) ||
268 (getConstStridesAttr() && !getConstStridesAttr().empty());
269
270 if (llvm::isa<IntegerType>(getSourceType())) {
271 // strides and shape must present for integer source.
272 if (getMixedStrides().empty() || getMixedSizes().empty())
273 return emitOpError("expecting strides and shape to be present for "
274 "integer source.");
275 if (getMixedSizes().size() != getMixedStrides().size())
276 return emitOpError("Expecting the rank of shape and strides to match.");
277 } else if (srcMemrefTy && hasExplicitShapeStrides) {
278 return emitOpError("shape and strides should not be specified for a memref "
279 "source; they are inferred from the memref.");
280 }
281
282 // check result TensorDesc rank
283 if (getType().getRank() > (int64_t)rank)
284 return emitOpError("Expecting the TensorDesc rank is not greater than the "
285 "ranks of shape, strides or the memref source.");
286
287 if (invalidElemTy)
288 return emitOpError("TensorDesc should have the same element "
289 "type with the source if it is a memref.\n");
290
291 return success();
292}
293
294//===----------------------------------------------------------------------===//
295// XeGPU_PrefetchNdOp
296//===----------------------------------------------------------------------===//
297
298void PrefetchNdOp::build(OpBuilder &builder, OperationState &state,
299 Value tensorDesc, ArrayRef<OpFoldResult> offsets,
300 xegpu::CachePolicyAttr l1_hint,
301 xegpu::CachePolicyAttr l2_hint,
302 xegpu::CachePolicyAttr l3_hint,
303 xegpu::DistributeLayoutAttr layout) {
304 SmallVector<Value> dynamicOffsets;
305 SmallVector<int64_t> staticOffsets;
306 dispatchIndexOpFoldResults(offsets, dynamicOffsets, staticOffsets);
307
308 auto staticOffsetsAttr = builder.getDenseI64ArrayAttr(staticOffsets);
309
310 build(builder, state, tensorDesc, dynamicOffsets, staticOffsetsAttr, l1_hint,
311 l2_hint, l3_hint, /*anchor_layout=*/layout);
312}
313
314LogicalResult PrefetchNdOp::verify() {
315 auto tdescTy = getTensorDescType();
316
317 if (!isReadHintOrNone(getL1HintAttr()))
318 return emitOpError("invalid l1_hint: ") << getL1HintAttr();
319
320 if (!isReadHintOrNone(getL2HintAttr()))
321 return emitOpError("invalid l2_hint: ") << getL2HintAttr();
322
323 if (!isReadHintOrNone(getL3HintAttr()))
324 return emitOpError("invalid l3_hint: ") << getL3HintAttr();
325
326 int64_t tDescRank = tdescTy.getRank();
327 int64_t offsetSize = getMixedOffsets().size();
328 if (offsetSize != tDescRank)
329 return emitOpError(
330 "Mismatched ranks between offsets and tensor descriptor");
331
332 if (auto layout = getAnchorLayout()) {
333 if (!layout.isDistributable(getShapeOf(tdescTy)))
334 return emitOpError(
335 "TensorDesc shape is not distributable with the layout");
336 }
337
338 return success();
339}
340
341//===----------------------------------------------------------------------===//
342// XeGPU_LoadNdOp
343//===----------------------------------------------------------------------===//
344
345void LoadNdOp::build(OpBuilder &builder, OperationState &state, Type retType,
346 Value tensorDesc, ArrayRef<OpFoldResult> offsets,
347 UnitAttr packed, DenseI64ArrayAttr transpose,
348 xegpu::CachePolicyAttr l1_hint,
349 xegpu::CachePolicyAttr l2_hint,
350 xegpu::CachePolicyAttr l3_hint,
351 xegpu::DistributeLayoutAttr layout) {
352 SmallVector<Value> dynamicOffsets;
353 SmallVector<int64_t> staticOffsets;
354 dispatchIndexOpFoldResults(offsets, dynamicOffsets, staticOffsets);
355
356 auto staticOffsetsAttr = builder.getDenseI64ArrayAttr(staticOffsets);
357
358 build(builder, state, retType, tensorDesc, dynamicOffsets, staticOffsetsAttr,
359 packed, transpose, l1_hint, l2_hint, l3_hint,
360 /*anchor_layout=*/layout);
361}
362
363LogicalResult LoadNdOp::verify() {
364 auto tdescTy = getTensorDescType();
365 auto valueTy = getType();
366
367 if (!valueTy)
368 return emitOpError("Invalid result, it should be a VectorType.\n");
369
370 if (!isReadHintOrNone(getL1HintAttr()))
371 return emitOpError("invalid l1_hint: ") << getL1HintAttr();
372
373 if (!isReadHintOrNone(getL2HintAttr()))
374 return emitOpError("invalid l2_hint: ") << getL2HintAttr();
375
376 if (!isReadHintOrNone(getL3HintAttr()))
377 return emitOpError("invalid l3_hint: ") << getL3HintAttr();
378
379 int tdescElems = tdescTy.getNumElements() * tdescTy.getArrayLength();
380 int valueElems = valueTy.getNumElements();
381
382 // If the result vector is 1D and has less elements than the tensor
383 // descriptor, it is supposed to be a SIMT op. The layout attribute in
384 // tensor_desc is not needed.
385 if (valueElems < tdescElems && valueTy.getRank() == 1) {
386 // SIMT mode doesn't need LayoutAttr.
387 if (tdescTy.getLayoutAttr())
388 return emitOpError()
389 << "TensorDesc doesn't need LayoutAttr for SIMT code";
390
391 // For SIMT code, the load is evenly distributed across all lanes in a
392 // subgroup. Since subgroup size is arch dependent, we only check even
393 // distribution here.
394 if (tdescElems % valueElems)
395 return emitOpError()
396 << "Result shape " << makeString(getShapeOf(valueTy))
397 << " is not a valid distribution for tensor descriptor "
398 << tdescTy;
399
400 return success();
401 }
402
403 // Check SIMD mode.
404 auto tdescShape = getShapeOf(tdescTy);
405 auto valueShape = getShapeOf(valueTy);
406
407 if (getTranspose()) {
408 auto trans = getTranspose().value();
409 // Make sure the transpose value is valid, and apply it
410 if (llvm::all_of(trans, [&](size_t s) { return s < tdescShape.size(); }))
411 tdescShape = applyPermutation(tdescShape, trans);
412 else
413 mlir::emitWarning(getLoc()) << "Invalid transpose attr. It is ignored.";
414 }
415
416 if (getPacked()) {
417 if (tdescTy.getRank() == 2) {
418 const int axis = 0;
419 auto vnni_factor = valueShape.back();
420 tdescShape[axis] /= vnni_factor;
421 tdescShape.push_back(vnni_factor);
422 } else {
423 mlir::emitWarning(getLoc())
424 << "Invalid Packed Attr. It is ignored (available for 2D "
425 "TensorDesc only).";
426 }
427 }
428
429 // Handle array_length. Two result shape conventions are accepted:
430 // * 3D shape: leading array_length dimension prepended, e.g. descriptor
431 // 16x16 with array_length=2 -> [2, 16, 16].
432 // * Stacked 2D shape: array blocks stacked along the non-FCD (first)
433 // dimension, e.g. descriptor 16x16 with array_length=2 -> [32, 16].
434 auto array_len = tdescTy.getArrayLength();
435 SmallVector<int64_t> stacked2DShape(tdescShape);
436 SmallVector<int64_t> threeDShape(tdescShape);
437 if (array_len > 1 && !tdescShape.empty()) {
438 stacked2DShape[0] *= array_len;
439 threeDShape.insert(threeDShape.begin(), array_len);
440 }
441
442 if (valueShape != stacked2DShape && valueShape != threeDShape)
443 return emitOpError() << "Result shape " << makeString(valueShape)
444 << " is not consistent with tensor descriptor "
445 << tdescTy;
446
447 int64_t tDescRank = tdescTy.getRank();
448 int64_t offsetSize = getMixedOffsets().size();
449 if (offsetSize != tDescRank)
450 return emitOpError(
451 "Mismatched ranks between offsets and tensor descriptor");
452
453 if (auto layout = getAnchorLayout()) {
454 if (!layout.isDistributable(getShapeOf(tdescTy)))
455 return emitOpError(
456 "TensorDesc shape is not distributable with the layout");
457 }
458
459 return success();
460}
461
462//===----------------------------------------------------------------------===//
463// XeGPU_StoreNdOp
464//===----------------------------------------------------------------------===//
465
466void StoreNdOp::build(OpBuilder &builder, OperationState &state, Value value,
467 Value tensorDesc, ArrayRef<OpFoldResult> offsets,
468 xegpu::CachePolicyAttr l1_hint,
469 xegpu::CachePolicyAttr l2_hint,
470 xegpu::CachePolicyAttr l3_hint,
471 xegpu::DistributeLayoutAttr layout) {
472 SmallVector<Value> dynamicOffsets;
473 SmallVector<int64_t> staticOffsets;
474 dispatchIndexOpFoldResults(offsets, dynamicOffsets, staticOffsets);
475
476 auto staticOffsetsAttr = builder.getDenseI64ArrayAttr(staticOffsets);
477
478 build(builder, state, value, tensorDesc, dynamicOffsets, staticOffsetsAttr,
479 l1_hint, l2_hint, l3_hint, /*anchor_layout=*/layout);
480}
481
482LogicalResult StoreNdOp::verify() {
483 auto dstTy = getTensorDescType(); // Tile
484 auto valTy = getValueType(); // Vector
485
486 if (!valTy)
487 return emitOpError("Expecting a VectorType result.\n");
488
489 if (!isWriteHintOrNone(getL1HintAttr()))
490 return emitOpError("invalid l1_hint: ") << getL1HintAttr();
491
492 if (!isWriteHintOrNone(getL2HintAttr()))
493 return emitOpError("invalid l2_hint: ") << getL2HintAttr();
494
495 if (!isWriteHintOrNone(getL3HintAttr()))
496 return emitOpError("invalid l3_hint: ") << getL3HintAttr();
497
498 auto array_len = dstTy.getArrayLength();
499 if (array_len > 1)
500 return emitOpError("array length is not supported by store_nd.\n");
501
502 auto tdescElems = dstTy.getNumElements();
503 auto valueElems = valTy.getNumElements();
504
505 // Similar to LoadNdOp, if the value vector is 1D and has less elements than
506 // the tensor descriptor, it is supposed to be a SIMT op. The layout attribute
507 // in tensor_desc is not needed.
508 if (valTy.getRank() == 1 && valueElems < tdescElems) {
509 // SIMT mode doesn't need LayoutAttr.
510 if (dstTy.getLayoutAttr())
511 return emitOpError()
512 << "TensorDesc doesn't need LayoutAttr for SIMT code";
513
514 if (tdescElems % valueElems)
515 return emitOpError()
516 << "Value shape " << makeString(getShapeOf(valTy))
517 << " is not a valid distribution for tensor descriptor " << dstTy;
518
519 return success();
520 }
521
522 // SIMD code should have the same shape as the tensor descriptor.
523 auto tdescShape = getShapeOf(dstTy);
524 auto valueShape = getShapeOf(valTy);
525 if (tdescShape != valueShape)
526 return emitOpError() << "Value shape " << makeString(valueShape)
527 << " is not consistent with tensor descriptor "
528 << dstTy;
529
530 int64_t tDescRank = dstTy.getRank();
531 int64_t offsetSize = getMixedOffsets().size();
532 if (offsetSize != tDescRank)
533 return emitOpError(
534 "Mismatched ranks between offsets and tensor descriptor");
535
536 if (auto layout = getAnchorLayout()) {
537 if (!layout.isDistributable(std::move(tdescShape)))
538 return emitOpError(
539 "TensorDesc shape is not distributable with the layout");
540 }
541
542 return success();
543}
544
545//===----------------------------------------------------------------------===//
546// XeGPU_PrefetchOp
547//===----------------------------------------------------------------------===//
548LogicalResult PrefetchOp::verify() {
549 if (!isReadHintOrNone(getL1HintAttr()))
550 return emitOpError("invalid l1_hint: ") << getL1HintAttr();
551
552 if (!isReadHintOrNone(getL2HintAttr()))
553 return emitOpError("invalid l2_hint: ") << getL2HintAttr();
554
555 if (!isReadHintOrNone(getL3HintAttr()))
556 return emitOpError("invalid l3_hint: ") << getL3HintAttr();
557
558 auto srcTy = getSourceType();
559 if (srcTy.isInteger() && !getOffsetAlignByteAttr())
560 return emitOpError("offset_align_byte is required with integer source.");
561
562 if (getOffsetAlignByteAttr() && !srcTy.isInteger())
563 return emitOpError("offset_align_byte only allowed with integer source.");
564
565 if (auto layout = getAnchorLayout()) {
566 // get the offset operand and its shape
567 auto offsetsTy = getOffsets().getType();
568 if (llvm::isa<VectorType>(offsetsTy) &&
569 !layout.isDistributable(getShapeOf(offsetsTy)))
570 return emitOpError("offset shape is not distributable with the layout");
571 }
572
573 return success();
574}
575
576//===----------------------------------------------------------------------===//
577// XeGPU_LoadGatherOp
578//===----------------------------------------------------------------------===//
579LogicalResult LoadGatherOp::verify() {
580 auto maskTy = getMaskType();
581 auto valueTy = getValueType();
582
583 if (!isReadHintOrNone(getL1HintAttr()))
584 return emitOpError("invalid l1_hint: ") << getL1HintAttr();
585
586 if (!isReadHintOrNone(getL2HintAttr()))
587 return emitOpError("invalid l2_hint: ") << getL2HintAttr();
588
589 if (!isReadHintOrNone(getL3HintAttr()))
590 return emitOpError("invalid l3_hint: ") << getL3HintAttr();
591
592 auto srcTy = getSourceType();
593 auto memTy = dyn_cast<MemRefType>(srcTy);
594
595 if (memTy && (getElementType() != memTy.getElementType()))
596 return emitError() << "Value should have the same element type as MemRef.";
597
598 if (auto layout = getAnchorLayout()) {
599 if (!layout.isDistributable(getShapeOf(valueTy)))
600 return emitOpError("Value shape is not distributable with the layout");
601 }
602
603 auto offsetsTy = getOffsets().getType();
604 if (failed(isValidContiguity(getContiguity(), offsetsTy,
605 [&]() { return emitOpError(); })))
606 return failure();
607 return isValidGatherScatterBufferParams(offsetsTy, maskTy, valueTy,
608 [&]() { return emitOpError(); });
609}
610
611void LoadGatherOp::build(OpBuilder &builder, OperationState &state,
612 Type valueType, Value source,
613 ArrayRef<OpFoldResult> offsets, Value mask,
614 xegpu::CachePolicyAttr l1_hint,
615 xegpu::CachePolicyAttr l2_hint,
616 xegpu::CachePolicyAttr l3_hint) {
617 auto loc = source.getLoc();
618 int64_t size = static_cast<int64_t>(offsets.size());
619 auto type = VectorType::get(size, builder.getIndexType());
620 auto values = getValueOrCreateConstantIndexOp(builder, loc, offsets);
621 auto offset = vector::FromElementsOp::create(builder, loc, type, values);
622
623 build(builder, state, valueType, source, offset, mask, l1_hint, l2_hint,
624 l3_hint, /*anchor_layout=*/nullptr,
625 /*contiguity=*/nullptr);
626}
627
628void LoadGatherOp::build(OpBuilder &builder, OperationState &state,
629 Type valueType, Value source,
630 ArrayRef<OpFoldResult> offsets, Value mask,
631 xegpu::CachePolicyAttr l1_hint,
632 xegpu::CachePolicyAttr l2_hint,
633 xegpu::CachePolicyAttr l3_hint,
634 DistributeLayoutAttr layout) {
635 auto loc = source.getLoc();
636 int64_t size = static_cast<int64_t>(offsets.size());
637 auto type = VectorType::get(size, builder.getIndexType());
638 auto values = getValueOrCreateConstantIndexOp(builder, loc, offsets);
639 auto offset = vector::FromElementsOp::create(builder, loc, type, values);
640
641 build(builder, state, valueType, source, offset, mask, l1_hint, l2_hint,
642 l3_hint, layout, /*contiguity=*/nullptr);
643}
644
645//===----------------------------------------------------------------------===//
646// XeGPU_StoreScatterOp
647//===----------------------------------------------------------------------===//
648LogicalResult StoreScatterOp::verify() {
649 auto maskTy = getMaskType();
650 auto valueTy = getValueType();
651
652 if (!isWriteHintOrNone(getL1HintAttr()))
653 return emitOpError("invalid l1_hint: ") << getL1HintAttr();
654
655 if (!isWriteHintOrNone(getL2HintAttr()))
656 return emitOpError("invalid l2_hint: ") << getL2HintAttr();
657
658 if (!isWriteHintOrNone(getL3HintAttr()))
659 return emitOpError("invalid l3_hint: ") << getL3HintAttr();
660
661 auto destTy = getDestType();
662 auto memTy = dyn_cast<MemRefType>(destTy);
663
664 if (memTy && (getElementType() != memTy.getElementType()))
665 return emitError() << "Value should have the same element type as MemRef.";
666
667 if (auto layout = getAnchorLayout()) {
668 if (!layout.isDistributable(getShapeOf(valueTy)))
669 return emitOpError("Value shape is not distributable with the layout");
670 }
671
672 auto offsetsTy = getOffsets().getType();
673 if (failed(isValidContiguity(getContiguity(), offsetsTy,
674 [&]() { return emitOpError(); })))
675 return failure();
676 return isValidGatherScatterBufferParams(offsetsTy, maskTy, valueTy,
677 [&]() { return emitOpError(); });
678}
679
680void StoreScatterOp::build(OpBuilder &builder, OperationState &state,
681 Value value, Value dest,
682 ArrayRef<OpFoldResult> offsets, Value mask,
683 xegpu::CachePolicyAttr l1_hint,
684 xegpu::CachePolicyAttr l2_hint,
685 xegpu::CachePolicyAttr l3_hint) {
686 auto loc = dest.getLoc();
687 int64_t size = static_cast<int64_t>(offsets.size());
688 auto type = VectorType::get(size, builder.getIndexType());
689 auto values = getValueOrCreateConstantIndexOp(builder, loc, offsets);
690 auto offset = vector::FromElementsOp::create(builder, loc, type, values);
691
692 // Call the correct builder overload that does not expect result types.
693 build(builder, state, value, dest, offset, mask, l1_hint, l2_hint, l3_hint,
694 /*anchor_layout=*/nullptr, /*contiguity=*/nullptr);
695}
696
697void StoreScatterOp::build(OpBuilder &builder, OperationState &state,
698 Value value, Value dest,
699 ArrayRef<OpFoldResult> offsets, Value mask,
700 xegpu::CachePolicyAttr l1_hint,
701 xegpu::CachePolicyAttr l2_hint,
702 xegpu::CachePolicyAttr l3_hint,
703 DistributeLayoutAttr layout) {
704 auto loc = dest.getLoc();
705 int64_t size = static_cast<int64_t>(offsets.size());
706 auto type = VectorType::get(size, builder.getIndexType());
707 auto values = getValueOrCreateConstantIndexOp(builder, loc, offsets);
708 auto offset = vector::FromElementsOp::create(builder, loc, type, values);
709
710 // Call the correct builder overload that does not expect result types.
711 build(builder, state, value, dest, offset, mask, l1_hint, l2_hint, l3_hint,
712 layout, /*contiguity=*/nullptr);
713}
714
715//===----------------------------------------------------------------------===//
716// DPAS Common Verification Helpers
717//===----------------------------------------------------------------------===//
718
719// Helper to verify layout distributability for a value
720static LogicalResult
722 std::optional<DistributeLayoutAttr> layout,
723 ArrayRef<int64_t> shape, StringRef operandName) {
724 if (layout && !layout->isDistributable(
725 SmallVector<int64_t>(shape.begin(), shape.end())))
726 return op->emitOpError(operandName)
727 << " shape is not distributable with the layout";
728 return success();
729}
730
731// Helper to verify M, N, K dimensions match between A, B, and result matrices
732static LogicalResult verifyDpasDimensions(Operation *op,
733 ArrayRef<int64_t> aShape,
734 ArrayRef<int64_t> bShape,
735 ArrayRef<int64_t> resShape) {
736
737 auto aRank = aShape.size();
738 auto bRank = bShape.size();
739 auto resRank = resShape.size();
740 if (aRank == 1 && bRank == 1 && resRank == 1)
741 return success();
742
743 // A must be at least 2D, B must be 2D or 3D (innermost dims), result at
744 // least 2D.
745 if (aRank < 2)
746 return op->emitOpError("A operand must be at least a 2D vector.");
747 if (bRank < 2)
748 return op->emitOpError("B operand must be at least a 2D vector.");
749 if (resRank < 2)
750 return op->emitOpError("Result must be at least a 2D vector.");
751
752 // FIXME: B may have one extra trailing dim for VNNI packing
753 // (B[batch..., K/vnni, N, vnni]). We plan to drop VNNI packing support, so
754 // rather than properly verifying the packed dimensions, we simply accept
755 // the packed form here and skip the detailed verification. This branch
756 // should be removed once VNNI packing support is dropped.
757 if (bRank == aRank + 1)
758 return success();
759
760 // All operands have the same rank. They share the same batch dimensions,
761 // with the last two dims being the core matmul dims: A[batch..., M, K],
762 // B[batch..., K, N], result[batch..., M, N].
763 if (aRank != bRank || aRank != resRank)
764 return op->emitOpError("Rank mismatch among A, B, and result.");
765
766 int64_t batchRank = aRank - 2;
767
768 // Verify batch dimensions match.
769 for (int64_t i = 0; i < batchRank; ++i) {
770 if (aShape[i] != resShape[i])
771 return op->emitOpError("Batch dimension mismatch at dim ")
772 << i << ": A has " << aShape[i] << " but result has "
773 << resShape[i] << ".";
774 if (aShape[i] != bShape[i])
775 return op->emitOpError("Batch dimension mismatch at dim ")
776 << i << ": A has " << aShape[i] << " but B has " << bShape[i]
777 << ".";
778 }
779
780 // Core matmul dimensions (last two dims of each operand).
781 int64_t aM = aShape[batchRank];
782 int64_t aK = aShape[batchRank + 1];
783 int64_t bK = bShape[batchRank];
784 int64_t bN = bShape[batchRank + 1];
785 int64_t resM = resShape[batchRank];
786 int64_t resN = resShape[batchRank + 1];
787
788 // Verify K dimension match between A and B
789 if (bK != aK)
790 return op->emitOpError("K-dimension mismatch: A has K=")
791 << aK << " but B has K=" << bK << ".";
792
793 // Verify M dimension match between A and result
794 if (aM != resM)
795 return op->emitOpError("M-dimension mismatch: A has M=")
796 << aM << " but result has M=" << resM << ".";
797
798 // Verify N dimension match between B and result
799 if (bN != resN)
800 return op->emitOpError("N-dimension mismatch: B has N=")
801 << bN << " but result has N=" << resN << ".";
802
803 return success();
804}
805
806// Helper to verify accumulator matches result type
807static LogicalResult verifyDpasAccumulator(Operation *op, Type accType,
808 Type resultType) {
809 if (accType != resultType)
810 return op->emitOpError("Accumulator type must match result type.");
811 return success();
812}
813
814//===----------------------------------------------------------------------===//
815// XeGPU_DpasOp
816//===----------------------------------------------------------------------===//
817LogicalResult DpasOp::verify() {
818 auto lhsShape = getLhsType().getShape();
819 auto rhsShape = getRhsType().getShape();
820 auto resShape = getResultType().getShape();
821
822 // Verify layout distributability
823 if (failed(
824 verifyLayoutDistributable(*this, getLayoutCd(), resShape, "Result")))
825 return failure();
826 if (failed(verifyLayoutDistributable(*this, getLayoutA(), lhsShape, "A")))
827 return failure();
828 if (failed(verifyLayoutDistributable(*this, getLayoutB(), rhsShape, "B")))
829 return failure();
830
831 // Verify accumulator if present
832 if (getAcc() &&
833 failed(verifyDpasAccumulator(*this, getAcc().getType(), getResultType())))
834 return failure();
835
836 return verifyDpasDimensions(*this, lhsShape, rhsShape, resShape);
837}
838
839//===----------------------------------------------------------------------===//
840// XeGPU_ConvertLayoutOp
841//===----------------------------------------------------------------------===//
842LogicalResult ConvertLayoutOp::verify() {
843 auto resLayout = getTargetLayout();
844 if (!resLayout)
845 return emitOpError("expected target layout.");
846 auto srcLayout = getEffectiveInputLayout();
847
848 // both input and target layouts should be WgLayout or SgLayout at the same
849 // time.
850 if ((!srcLayout.isForWorkgroup() || !resLayout.isForWorkgroup()) &&
851 (!srcLayout.isForSubgroup() || !resLayout.isForSubgroup()))
852 return emitOpError("expected input layout and target layout be WgLayout or "
853 "SgLayout at the same time.");
854
855 Type srcType = getSource().getType();
856 if (llvm::isa<VectorType>(srcType)) {
857 SmallVector<int64_t> shape(llvm::cast<VectorType>(srcType).getShape());
858 if (!srcLayout.isDistributable(shape))
859 return emitOpError(
860 "invalid input layout, data cannot be evenly distributed.");
861
862 if (!resLayout.isDistributable(std::move(shape)))
863 return emitOpError(
864 "invalid target layout, data cannot be evenly distributed.");
865 }
866 return mlir::success();
867}
868
869//===----------------------------------------------------------------------===//
870// XeGPU_LoadMatrixOp
871//===----------------------------------------------------------------------===//
872void LoadMatrixOp::build(OpBuilder &builder, OperationState &state, Type res,
875 DistributeLayoutAttr layout) {
876 llvm::SmallVector<Value> dynamicOffsets;
877 llvm::SmallVector<int64_t> staticOffsets;
878 dispatchIndexOpFoldResults(offsets, dynamicOffsets, staticOffsets);
879 auto staticOffsetsAttr = builder.getDenseI64ArrayAttr(staticOffsets);
880 // Call the generated builder with all parameters (including optional ones as
881 // nullptr/empty)
882 build(builder, state, res, memDesc, dynamicOffsets, staticOffsetsAttr,
883 /*subgroup_block_io=*/nullptr, layout);
884}
885
886LogicalResult LoadMatrixOp::verify() {
887
888 auto resTy = dyn_cast<VectorType>(getRes().getType());
889 UnitAttr subgroup_block_io = getSubgroupBlockIoAttr();
890 MemDescType mdescTy = getMemDesc().getType();
891
892 return IsValidMatrixOpParams(resTy, mdescTy, subgroup_block_io,
893 getLayoutAttr(), [&]() { return emitError(); });
894}
895
896//===----------------------------------------------------------------------===//
897// XeGPU_StoreMatrixOp
898//===----------------------------------------------------------------------===//
899void StoreMatrixOp::build(OpBuilder &builder, OperationState &state, Value data,
902 DistributeLayoutAttr layout) {
903 llvm::SmallVector<Value> dynamicOffsets;
904 llvm::SmallVector<int64_t> staticOffsets;
905 dispatchIndexOpFoldResults(offsets, dynamicOffsets, staticOffsets);
906 auto staticOffsetsAttr = builder.getDenseI64ArrayAttr(staticOffsets);
907 build(builder, state, data, memDesc, dynamicOffsets, staticOffsetsAttr,
908 /*subgroup_block_io=*/nullptr, layout);
909}
910
911LogicalResult StoreMatrixOp::verify() {
912
913 auto dataTy = dyn_cast<VectorType>(getData().getType());
914 UnitAttr subgroup_block_io = getSubgroupBlockIoAttr();
915 MemDescType mdescTy = getMemDesc().getType();
916 return IsValidMatrixOpParams(dataTy, mdescTy, subgroup_block_io,
917 getLayoutAttr(), [&]() { return emitError(); });
918}
919
920//===----------------------------------------------------------------------===//
921// XeGPU_TruncfOp
922//===----------------------------------------------------------------------===//
923
924LogicalResult TruncfOp::verify() {
925 auto sourceVecType = dyn_cast<VectorType>(getSource().getType());
926 auto resultVecType = dyn_cast<VectorType>(getResult().getType());
927
928 if (sourceVecType.getElementTypeBitWidth() <=
929 resultVecType.getElementTypeBitWidth())
930 return emitOpError("input type must be wider than result type.");
931
932 return success();
933}
934
935//===----------------------------------------------------------------------===//
936// XeGPU_LaneShuffleOp
937//===----------------------------------------------------------------------===//
938
939LogicalResult LaneShuffleOp::verify() {
940 // With a single element per lane there is nothing to re-distribute, so the
941 // operation would be a no-op.
942 if (getSourceType().getNumElements() < 2)
943 return emitOpError("requires a source vector with at least 2 elements.");
944
945 return success();
946}
947
948OpFoldResult LaneShuffleOp::fold(FoldAdaptor adaptor) {
949 // The two modes are exact inverses, so a pack feeding an unpack (or vice
950 // versa) restores the original fragments.
951 auto producer = getSource().getDefiningOp<LaneShuffleOp>();
952 if (producer && producer.getMode() != getMode())
953 return producer.getSource();
954
955 return {};
956}
957
958//===----------------------------------------------------------------------===//
959// XeGPU_DpasMxOp
960//===----------------------------------------------------------------------===//
961
962LogicalResult DpasMxOp::verify() {
963 auto aShape = getAType().getShape();
964 auto bShape = getBType().getShape();
965 auto resShape = getResultType().getShape();
966
967 // Verify layout distributability for A, B, and result
968 if (failed(
969 verifyLayoutDistributable(*this, getLayoutCd(), resShape, "Result")))
970 return failure();
971 if (failed(verifyLayoutDistributable(*this, getLayoutA(), aShape, "A")))
972 return failure();
973 if (failed(verifyLayoutDistributable(*this, getLayoutB(), bShape, "B")))
974 return failure();
975
976 // Verify accumulator if present
977 if (getAcc() &&
978 failed(verifyDpasAccumulator(*this, getAcc().getType(), getResultType())))
979 return failure();
980
981 // Verify M, N, K dimensions
982 if (failed(verifyDpasDimensions(*this, aShape, bShape, resShape)))
983 return failure();
984
985 // Determine batch rank from A operand.
986 int64_t aBatchRank = aShape.size() - 2;
987
988 // Validate scale_a if present
989 if (getScaleA()) {
990 auto scaleAVecType = dyn_cast<VectorType>(getScaleAType());
991 // Only validate if scale is a vector (scalars are always valid)
992 if (scaleAVecType && scaleAVecType.getRank() > 1) {
993 auto scaleAShape = scaleAVecType.getShape();
994
995 if (scaleAVecType.getRank() < 2)
996 return emitOpError("Scale A must be at least a 2D vector when not a "
997 "scalar.");
998
999 // Verify layout distributability for scale_a
1000 if (failed(verifyLayoutDistributable(*this, getLayoutAScale(),
1001 scaleAShape, "ScaleA")))
1002 return failure();
1003
1004 // Validate M dimension: scale_a's M must match A's M (last-1 dim)
1005 if (scaleAShape[scaleAShape.size() - 2] != aShape[aBatchRank])
1006 return emitOpError("Scale A M dimension [")
1007 << scaleAShape[scaleAShape.size() - 2]
1008 << "] must match A M dimension [" << aShape[aBatchRank] << "].";
1009 }
1010 }
1011
1012 // Validate scale_b if present
1013 if (getScaleB()) {
1014 auto scaleBVecType = dyn_cast<VectorType>(getScaleBType());
1015 // Only validate if scale is a vector (scalars are always valid)
1016 if (scaleBVecType && scaleBVecType.getRank() > 1) {
1017 auto scaleBShape = scaleBVecType.getShape();
1018
1019 if (scaleBVecType.getRank() < 2)
1020 return emitOpError("Scale B must be at least a 2D vector when not a "
1021 "scalar.");
1022
1023 // Verify layout distributability for scale_b
1024 if (failed(verifyLayoutDistributable(*this, getLayoutBScale(),
1025 scaleBShape, "ScaleB")))
1026 return failure();
1027
1028 // Validate N dimension: scale_b's N (last dim) must match B's N (last
1029 // dim)
1030 if (scaleBShape.back() != bShape.back())
1031 return emitOpError("Scale B N dimension [")
1032 << scaleBShape.back() << "] must match B N dimension ["
1033 << bShape.back() << "].";
1034 }
1035 }
1036
1037 // Validate scale K dimension compatibility if both scales are present and
1038 // vectors
1039 if (getScaleA() && getScaleB()) {
1040 auto scaleAVecType = dyn_cast<VectorType>(getScaleAType());
1041 auto scaleBVecType = dyn_cast<VectorType>(getScaleBType());
1042
1043 if (scaleAVecType && scaleBVecType && scaleAVecType.getRank() > 1 &&
1044 scaleBVecType.getRank() > 1) {
1045 auto scaleAShape = scaleAVecType.getShape();
1046 auto scaleBShape = scaleBVecType.getShape();
1047
1048 // Validate scale K dimension compatibility: scale_a's last dim must
1049 // match scale_b's second-to-last dim
1050 if (scaleAShape.back() != scaleBShape[scaleBShape.size() - 2])
1051 return emitOpError("Scale K dimension mismatch: scale_a has K=")
1052 << scaleAShape.back()
1053 << " but scale_b has K=" << scaleBShape[scaleBShape.size() - 2]
1054 << ".";
1055 }
1056 }
1057
1058 return success();
1059}
1060
1061namespace mlir {
1062#include <mlir/Dialect/XeGPU/IR/XeGPUAttrInterface.cpp.inc>
1063} // namespace mlir
1064#include <mlir/Dialect/XeGPU/IR/XeGPUEnums.cpp.inc>
1065#define GET_OP_CLASSES
1066#include <mlir/Dialect/XeGPU/IR/XeGPU.cpp.inc>
return success()
static int64_t getNumElements(Type t)
Compute the total number of elements in the given type, also taking into account nested types.
ArrayAttr()
static Type getValueType(Attribute attr)
Definition SPIRVOps.cpp:835
static Type getElementType(Type type, ArrayRef< int32_t > indices, function_ref< InFlightDiagnostic(StringRef)> emitErrorFn)
Walks the given type hierarchy with the given indices, potentially down to component granularity,...
Definition SPIRVOps.cpp:229
static ArrayRef< int64_t > getShape(Type type)
Returns the shape of the given type.
Definition Traits.cpp:117
static SmallVector< int64_t > getShapeOf(Type type)
Definition XeGPUOps.cpp:42
static std::string makeString(const T &array, bool breakline=false)
Definition XeGPUOps.cpp:28
static LogicalResult verifyDpasAccumulator(Operation *op, Type accType, Type resultType)
Definition XeGPUOps.cpp:807
LogicalResult IsValidMatrixOpParams(VectorType dataTy, MemDescType mdescTy, UnitAttr subgroup_block_io, DistributeLayoutAttr layout, function_ref< InFlightDiagnostic()> emitError)
Definition XeGPUOps.cpp:125
static bool isWriteHintOrNone(const CachePolicyAttr &attr)
Definition XeGPUOps.cpp:59
static bool isReadHintOrNone(const CachePolicyAttr &attr)
Definition XeGPUOps.cpp:51
static LogicalResult isValidContiguity(std::optional< uint64_t > contiguity, Type offsetsTy, function_ref< InFlightDiagnostic()> emitError)
Definition XeGPUOps.cpp:106
static LogicalResult isValidGatherScatterBufferParams(Type offsetsTy, Type maskTy, VectorType valueTy, function_ref< InFlightDiagnostic()> emitError)
Definition XeGPUOps.cpp:68
static LogicalResult verifyDpasDimensions(Operation *op, ArrayRef< int64_t > aShape, ArrayRef< int64_t > bShape, ArrayRef< int64_t > resShape)
Definition XeGPUOps.cpp:732
static LogicalResult verifyLayoutDistributable(Operation *op, std::optional< DistributeLayoutAttr > layout, ArrayRef< int64_t > shape, StringRef operandName)
Definition XeGPUOps.cpp:721
Attributes are known-constant values of operations.
Definition Attributes.h:25
DenseI64ArrayAttr getDenseI64ArrayAttr(ArrayRef< int64_t > values)
Definition Builders.cpp:175
IndexType getIndexType()
Definition Builders.cpp:59
This class represents a diagnostic that is inflight and set to be reported.
This class helps build Operations.
Definition Builders.h:210
This class represents a single result from folding an operation.
Operation is the basic unit of execution within MLIR.
Definition Operation.h:87
InFlightDiagnostic emitOpError(const Twine &message={})
Emit an error with the op name prefixed, like "'dim' op " which is convenient for verifiers.
Instances of the Type class are uniqued, have an immutable identifier and an optional mutable compone...
Definition Types.h:74
bool isInteger() const
Return true if this is an integer type (with the specified width).
Definition Types.cpp:58
This class represents an instance of an SSA value in the MLIR system, representing a computable value...
Definition Value.h:96
Type getType() const
Return the type of this value.
Definition Value.h:105
Location getLoc() const
Return the location of this value.
Definition Value.cpp:24
bool isStaticShapeAndContiguousRowMajor(MemRefType type)
Returns true, if the memref type has static shapes and represents a contiguous chunk of memory.
SmallVector< OpFoldResult > getMixedSizes(OpBuilder &builder, Location loc, Value value)
Return the dimensions of the given memref value.
Definition MemRefOps.cpp:80
detail::InFlightRemark failed(Location loc, RemarkOpts opts)
Report an optimization remark that failed.
Definition Remarks.h:734
Include the generated interface declarations.
InFlightDiagnostic emitWarning(Location loc)
Utility method to emit a warning message using this location.
detail::DenseArrayAttrImpl< int64_t > DenseI64ArrayAttr
Type getType(OpFoldResult ofr)
Returns the int type of the integer in ofr.
Definition Utils.cpp:311
SmallVector< T > applyPermutation(ArrayRef< T > input, ArrayRef< int64_t > permutation)
InFlightDiagnostic emitError(Location loc)
Utility method to emit an error message using this location.
std::conditional_t< std::is_same_v< Ty, mlir::Type >, mlir::Value, detail::TypedValue< Ty > > TypedValue
If Ty is mlir::Type this will select Value instead of having a wrapper around it.
Definition Value.h:494
void dispatchIndexOpFoldResults(ArrayRef< OpFoldResult > ofrs, SmallVectorImpl< Value > &dynamicVec, SmallVectorImpl< int64_t > &staticVec)
Helper function to dispatch multiple OpFoldResults according to the behavior of dispatchIndexOpFoldRe...
Value getValueOrCreateConstantIndexOp(OpBuilder &b, Location loc, OpFoldResult ofr)
Converts an OpFoldResult to a Value.
Definition Utils.cpp:114
llvm::function_ref< Fn > function_ref
Definition LLVM.h:147
This represents an operation in an abstracted form, suitable for use with the builder APIs.