#include "mlir/Dialect/Affine/IR/AffineOps.h" #include "mlir/Dialect/Arith/IR/Arith.h" #include "mlir/Dialect/SCF/IR/SCF.h" #include "mlir/Dialect/Tensor/IR/Tensor.h" #include "mlir/IR/AffineExpr.h" #include "mlir/IR/Block.h" #include "mlir/IR/BuiltinTypeInterfaces.h" #include "mlir/IR/Diagnostics.h" #include "mlir/IR/Matchers.h" #include "mlir/IR/OpDefinition.h" #include "mlir/IR/TypeUtilities.h" #include "mlir/Support/LLVM.h" #include "llvm/ADT/DenseSet.h" #include "llvm/Support/LogicalResult.h" #include "src/Accelerators/PIM/Common/IR/AffineUtils.hpp" #include "src/Accelerators/PIM/Common/IR/ConstantUtils.hpp" #include "src/Accelerators/PIM/Common/PimCommon.hpp" #include "src/Accelerators/PIM/Compiler/PimCompilerOptions.hpp" #include "src/Accelerators/PIM/Conversion/ONNXToSpatial/CompileTime.hpp" #include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp" #include "src/Accelerators/PIM/Dialect/Spatial/SpatialOps.hpp" using namespace mlir; namespace onnx_mlir { namespace spatial { namespace { static FailureOr> getWeightShapeForWeightedOp(Value weight) { auto shapedType = dyn_cast(weight.getType()); if (!shapedType) return failure(); return shapedType.getShape(); } template static bool isBatchOutputArgument(ComputeBatchOpTy batchOp, Value value) { if (batchOp.getNumResults() == 0) return false; auto blockArg = dyn_cast(value); if (!blockArg || blockArg.getOwner() != &batchOp.getBody().front()) return false; unsigned argNumber = blockArg.getArgNumber(); auto firstOutputArg = batchOp.getOutputArgument(0); if (!firstOutputArg) return false; unsigned firstOutputArgNumber = firstOutputArg->getArgNumber(); return argNumber >= firstOutputArgNumber && argNumber < firstOutputArgNumber + batchOp.getNumResults(); } template static LogicalResult verifyStaticWeights(ComputeOpTy computeOp, StringRef kind) { for (Value weight : computeOp.getWeights()) if (!isCompileTimeComputable(weight)) return computeOp.emitOpError() << kind << " weights must be statically computed from constants"; return success(); } static bool isStaticScfForInductionVar(Value value) { auto blockArg = dyn_cast(value); if (!blockArg) return false; auto loop = dyn_cast_or_null(blockArg.getOwner()->getParentOp()); if (!loop || loop.getInductionVar() != value) return false; std::optional lowerBound = matchConstantIndexValue(loop.getLowerBound()); std::optional upperBound = matchConstantIndexValue(loop.getUpperBound()); std::optional step = matchConstantIndexValue(loop.getStep()); return lowerBound && upperBound && step && *step > 0 && *upperBound >= *lowerBound; } static bool isStaticIndexExpr(Value value) { if (matchConstantIndexValue(value)) return true; auto affineApply = value.getDefiningOp(); if (affineApply) { if (!isSingleResultSymbolFreeAffineMap(affineApply.getAffineMap())) return false; return llvm::all_of(affineApply.getMapOperands(), isStaticIndexExpr); } if (auto addOp = value.getDefiningOp()) return isStaticIndexExpr(addOp.getLhs()) && isStaticIndexExpr(addOp.getRhs()); if (auto mulOp = value.getDefiningOp()) return isStaticIndexExpr(mulOp.getLhs()) && isStaticIndexExpr(mulOp.getRhs()); return false; } static bool isSupportedLaneOffsetExpr(Value value, BlockArgument laneArg) { if (value == laneArg || isStaticIndexExpr(value) || isStaticScfForInductionVar(value)) return true; auto affineApply = value.getDefiningOp(); if (affineApply) { if (!isSingleResultSymbolFreeAffineMap(affineApply.getAffineMap())) return false; if (!llvm::all_of(affineApply.getMapOperands(), [&](Value operand) { return isSupportedLaneOffsetExpr(operand, laneArg); })) { return false; } return isDimAndConstantAffineExpr(affineApply.getAffineMap().getResult(0)); } auto extractOp = value.getDefiningOp(); if (extractOp) { auto constantTensor = extractOp.getTensor().getDefiningOp(); auto denseAttr = constantTensor ? dyn_cast(constantTensor.getValue()) : nullptr; if (!denseAttr || denseAttr.getType().getRank() != 1 || extractOp.getIndices().size() != 1) return false; return isSupportedLaneOffsetExpr(extractOp.getIndices().front(), laneArg); } auto addOp = value.getDefiningOp(); if (addOp) return (isSupportedLaneOffsetExpr(addOp.getLhs(), laneArg) && isStaticIndexExpr(addOp.getRhs())) || (isSupportedLaneOffsetExpr(addOp.getRhs(), laneArg) && isStaticIndexExpr(addOp.getLhs())); auto mulOp = value.getDefiningOp(); if (!mulOp) return false; return (isSupportedLaneOffsetExpr(mulOp.getLhs(), laneArg) && isStaticIndexExpr(mulOp.getRhs())) || (isSupportedLaneOffsetExpr(mulOp.getRhs(), laneArg) && isStaticIndexExpr(mulOp.getLhs())); } static LogicalResult verifyStaticUnitStrideExtractSliceOp(tensor::ExtractSliceOp sliceOp, BlockArgument laneArg, StringRef kind) { auto sourceType = dyn_cast(sliceOp.getSource().getType()); auto resultType = dyn_cast(sliceOp.getResult().getType()); if (!sourceType || !resultType || !sourceType.hasStaticShape() || !resultType.hasStaticShape()) return sliceOp.emitOpError() << kind << " requires static ranked tensor types"; if (!sliceOp.hasUnitStride()) return sliceOp.emitOpError() << kind << " requires unit strides"; for (int64_t size : sliceOp.getStaticSizes()) if (ShapedType::isDynamic(size)) return sliceOp.emitOpError() << kind << " requires static slice sizes"; auto offsets = sliceOp.getOffsets(); for (Value offset : offsets) if (!isSupportedLaneOffsetExpr(offset, laneArg)) return sliceOp.emitOpError() << kind << " requires simple lane-dependent offsets"; return success(); } static LogicalResult verifyStaticUnitStrideParallelInsertSliceOp(tensor::ParallelInsertSliceOp sliceOp, BlockArgument laneArg, StringRef kind) { RankedTensorType sourceType = sliceOp.getSourceType(); RankedTensorType destType = sliceOp.getDestType(); if (!sourceType.hasStaticShape() || !destType.hasStaticShape()) return sliceOp.emitOpError() << kind << " requires static ranked tensor types"; if (!sliceOp.hasUnitStride()) return sliceOp.emitOpError() << kind << " requires unit strides"; for (int64_t size : sliceOp.getStaticSizes()) if (ShapedType::isDynamic(size)) return sliceOp.emitOpError() << kind << " requires static slice sizes"; auto offsets = sliceOp.getOffsets(); for (Value offset : offsets) if (!isSupportedLaneOffsetExpr(offset, laneArg)) return sliceOp.emitOpError() << kind << " requires simple lane-dependent offsets"; return success(); } static Region* getParentRegion(Value value) { if (auto blockArg = dyn_cast(value)) return blockArg.getOwner()->getParent(); if (Operation* definingOp = value.getDefiningOp()) return definingOp->getParentRegion(); return nullptr; } static bool isDefinedInsideRegion(Value value, Region& region) { Region* parentRegion = getParentRegion(value); return parentRegion && (®ion == parentRegion || region.isAncestor(parentRegion)); } static bool isConstantExternalValue(Value value) { Operation* definingOp = value.getDefiningOp(); return definingOp && definingOp->hasTrait(); } static bool isRecordedDeferredCommunicationSource(Operation* op, Value value) { auto transfer = dyn_cast(op); return transfer && llvm::is_contained(transfer.getSources(), value); } static LogicalResult verifyOnlyConstantExternalValues(Operation* ownerOp, Region& region, StringRef kind) { bool hasFailure = false; region.walk([&](Operation* op) { for (OpOperand& operand : op->getOpOperands()) { Value value = operand.get(); if (isDefinedInsideRegion(value, region) || isConstantExternalValue(value) || isRecordedDeferredCommunicationSource(op, value)) continue; InFlightDiagnostic diagnostic = ownerOp->emitOpError() << kind << " body may not capture external values"; diagnostic.attachNote(op->getLoc()) << "owner='" << ownerOp->getName() << "' nestedOp='" << op->getName() << "' operand#" << operand.getOperandNumber() << " type=" << value.getType() << " category=" << (isa(value.getType()) ? "tensor" : (value.getType().isIndex() ? "index" : "scalar")); if (Operation* definingOp = value.getDefiningOp()) diagnostic.attachNote(definingOp->getLoc()) << "defining op is '" << definingOp->getName() << "'"; else if (auto blockArg = dyn_cast(value)) diagnostic.attachNote(blockArg.getOwner()->getParentOp()->getLoc()) << "value is block argument #" << blockArg.getArgNumber() << " of '" << blockArg.getOwner()->getParentOp()->getName() << "'"; hasFailure = true; } }); return success(!hasFailure); } static LogicalResult verifyYieldTypes(Operation* op, Region& region, TypeRange resultTypes, StringRef kind) { if (region.empty()) return op->emitOpError() << kind << " requires a body block"; Block& block = region.front(); auto yield = dyn_cast_or_null(block.getTerminator()); if (!yield) return op->emitOpError() << kind << " body must terminate with spat.yield"; if (yield.getOutputs().size() != resultTypes.size()) return op->emitOpError() << kind << " yield operand count must match result count"; for (auto [yieldType, resultType] : llvm::zip(yield.getOutputs().getTypes(), resultTypes)) if (yieldType != resultType) return op->emitOpError() << kind << " yield operand types must match result types"; return success(); } static LogicalResult verifyRegionArguments(Operation* op, Region& region, ValueRange operands, StringRef kind) { if (region.empty()) return op->emitOpError() << kind << " requires a body block"; Block& block = region.front(); if (block.getNumArguments() != operands.size()) return op->emitOpError() << kind << " body argument count must match operand count"; for (auto [arg, operand] : llvm::zip(block.getArguments(), operands)) if (arg.getType() != operand.getType()) return op->emitOpError() << kind << " body argument types must match operand types"; return success(); } template static LogicalResult verifyBatchBody(ComputeBatchOpTy batchOp, Block& block, bool verifyLaneSliceOffsets = true) { if (batchOp.getNumResults() == 0) { auto yieldOp = dyn_cast_or_null(block.getTerminator()); if (!yieldOp) return batchOp.emitError("resultless compute_batch body must terminate with spat.yield"); if (yieldOp.getNumOperands() != 0) return batchOp.emitError("resultless compute_batch body yield must be empty"); } else if (!isa_and_nonnull(block.getTerminator())) { return batchOp.emitError("resultful compute_batch body must terminate with spat.in_parallel"); } auto laneArg = batchOp.getLaneArgument(); if (!laneArg) return batchOp.emitError("compute_batch body must have a lane block argument"); if (verifyLaneSliceOffsets) for (auto& bodyOp : block) { if (auto extractSlice = dyn_cast(&bodyOp)) if (failed(verifyStaticUnitStrideExtractSliceOp(extractSlice, *laneArg, "tensor.extract_slice"))) return failure(); } return success(); } } // namespace LogicalResult SpatVMMOp::verify() { auto matrixShapeOpt = getWeightShapeForWeightedOp(getWeight()); if (failed(matrixShapeOpt)) return emitError("weight must be a shaped value"); auto matrixShape = *matrixShapeOpt; auto vectorShape = getInput().getType().getShape(); auto outputShape = getOutput().getType().getShape(); if (matrixShape.size() != 2 || vectorShape.size() != 2 || outputShape.size() != 2) return emitError("matrix, vector and output must have rank 2"); int64_t N = matrixShape[0]; int64_t M = matrixShape[1]; if (N <= 0 || M <= 0) return emitError("matrix shape must be (N, M) with N > 0 and M > 0"); int64_t vector1 = vectorShape[0]; int64_t vectorN = vectorShape[1]; if (vectorN != N || vector1 != 1) return emitError("vector shape must be (1, N)"); int64_t output1 = outputShape[0]; int64_t outputM = outputShape[1]; if (outputM != M || output1 != 1) return emitError("output shape must be (1, M)"); return success(); } LogicalResult SpatVVDMulOp::verify() { auto lhsType = dyn_cast(getLhs().getType()); auto rhsType = dyn_cast(getRhs().getType()); auto outputType = dyn_cast(getOutput().getType()); if (!lhsType || !rhsType || !outputType) return emitError("lhs, rhs, and output must be shaped values"); if (!lhsType.hasRank() || !rhsType.hasRank() || !outputType.hasRank()) return emitError("lhs, rhs, and output must have ranked types"); ArrayRef lhsShape = lhsType.getShape(); ArrayRef rhsShape = rhsType.getShape(); ArrayRef outputShape = outputType.getShape(); if (lhsShape.size() != 2 || rhsShape.size() != 2 || outputShape.size() != 2) return emitError("lhs, rhs, and output must have rank 2"); if (lhsType.getElementType() != rhsType.getElementType() || lhsType.getElementType() != outputType.getElementType()) return emitError("lhs, rhs, and output must have the same element type"); if (lhsShape != rhsShape) return emitError("lhs and rhs vector shapes must match"); if (lhsShape[0] != 1 || lhsShape[1] <= 0) return emitError("lhs and rhs vector shape must be (1, N) with N > 0"); if (outputShape[0] != 1 || outputShape[1] != 1) return emitError("output shape must be (1, 1)"); return success(); } LogicalResult SpatVAddOp::verify() { if (failed(OpTrait::impl::verifyAtLeastNOperands(*this, 2))) return failure(); return OpTrait::impl::verifySameOperandsAndResultType(*this); } LogicalResult SpatVSubOp::verify() { if (failed(OpTrait::impl::verifyAtLeastNOperands(*this, 2))) return failure(); return OpTrait::impl::verifySameOperandsAndResultType(*this); } LogicalResult SpatVMaxOp::verify() { if (failed(OpTrait::impl::verifyAtLeastNOperands(*this, 2))) return failure(); return OpTrait::impl::verifySameOperandsAndResultType(*this); } LogicalResult SpatExtractRowsOp::verify() { auto inputType = dyn_cast(getInput().getType()); if (!inputType || !inputType.hasRank() || inputType.getRank() != 2) return emitError("input must be a rank-2 shaped type"); int64_t numRows = inputType.getShape()[0]; int64_t numCols = inputType.getShape()[1]; Type elementType = inputType.getElementType(); if (numRows >= 0 && static_cast(getNumResults()) != numRows) return emitError("number of outputs must match the number of input rows"); for (Type output : getResultTypes()) { auto outputType = dyn_cast(output); if (!outputType || !outputType.hasRank() || outputType.getRank() != 2) return emitError("outputs must all be rank-2 shaped types"); if (outputType.getElementType() != elementType) return emitError("output element types must match input element type"); auto outputShape = outputType.getShape(); if (outputShape[0] != 1) return emitError("each output must have exactly one row"); if (numCols >= 0 && outputShape[1] != numCols) return emitError("output column count must match input column count"); } return success(); } LogicalResult SpatConcatOp::verify() { if (getInputs().empty()) return emitError("requires at least one input"); auto outputType = dyn_cast(getOutput().getType()); if (!outputType || !outputType.hasRank()) return emitError("output must be a ranked shaped type"); int64_t axis = getAxis(); int64_t rank = outputType.getRank(); if (axis < 0 || axis >= rank) return emitError("axis must be within the output rank"); int64_t concatenatedDimSize = 0; bool concatenatedDimDynamic = false; Type outputElementType = outputType.getElementType(); for (Value input : getInputs()) { auto inputType = dyn_cast(input.getType()); if (!inputType || !inputType.hasRank()) return emitError("inputs must be ranked shaped types"); if (inputType.getRank() != rank) return emitError("all inputs must have the same rank as the output"); if (inputType.getElementType() != outputElementType) return emitError("all inputs must have the same element type as the output"); for (int64_t dim = 0; dim < rank; ++dim) { if (dim == axis) continue; int64_t inputDim = inputType.getDimSize(dim); int64_t outputDim = outputType.getDimSize(dim); if (!ShapedType::isDynamic(inputDim) && !ShapedType::isDynamic(outputDim) && inputDim != outputDim) return emitError("non-concatenated dimensions must match the output shape"); } int64_t inputConcatDim = inputType.getDimSize(axis); if (ShapedType::isDynamic(inputConcatDim)) { concatenatedDimDynamic = true; continue; } concatenatedDimSize += inputConcatDim; } int64_t outputConcatDim = outputType.getDimSize(axis); if (!concatenatedDimDynamic && !ShapedType::isDynamic(outputConcatDim) && concatenatedDimSize != outputConcatDim) return emitError("output concatenated dimension must equal the sum of input sizes"); return success(); } static bool isKnownLogicalLayout(StringRef layout) { return layout == "nchw"; } static bool isKnownPhysicalLayout(StringRef layout) { return layout == "dense_nchw" || layout == "nchw_row_strip" || layout == "fragmented"; } static LogicalResult verifyPlanTensorTypes(Operation* op, Value input, Value output, StringRef kind) { auto inputType = dyn_cast(input.getType()); auto outputType = dyn_cast(output.getType()); if (!inputType || !outputType) return op->emitOpError() << kind << " requires ranked tensor input and output types"; if (inputType.getElementType() != outputType.getElementType()) return op->emitOpError() << kind << " requires matching input/output element types"; return success(); } LogicalResult SpatConv2DPlanOp::verify() { auto inputType = dyn_cast(getInput().getType()); auto weightType = dyn_cast(getWeight().getType()); auto outputType = dyn_cast(getOutput().getType()); if (!inputType || !weightType || !outputType) return emitError("requires ranked tensor input, weight, and output"); if (inputType.getRank() != 4 || weightType.getRank() != 4 || outputType.getRank() != 4) return emitError("requires rank-4 input, weight, and output tensors"); if (!isKnownLogicalLayout(getLogicalLayout())) return emitError("requires a known logical layout"); if (getPads().size() != 4) return emitError("requires exactly four pad values"); if (getStrides().size() != 2) return emitError("requires exactly two stride values"); if (getDilations().size() != 2) return emitError("requires exactly two dilation values"); if (getGroup() < 1) return emitError("requires group >= 1"); if (inputType.getElementType() != weightType.getElementType() || inputType.getElementType() != outputType.getElementType()) { return emitError("requires matching input, weight, and output element types"); } if (getBias()) { auto biasType = dyn_cast(getBias().getType()); if (!biasType) return emitError("requires ranked tensor bias type"); if (biasType.getElementType() != outputType.getElementType()) return emitError("requires bias element type to match output element type"); } return success(); } LogicalResult SpatReluPlanOp::verify() { if (failed(verifyPlanTensorTypes(getOperation(), getInput(), getOutput(), "spat.relu_plan"))) return failure(); if (!isKnownLogicalLayout(getLogicalLayout())) return emitError("requires a known logical layout"); return success(); } LogicalResult SpatBiasAddPlanOp::verify() { if (failed(verifyPlanTensorTypes(getOperation(), getInput(), getOutput(), "spat.bias_add_plan"))) return failure(); if (!isKnownLogicalLayout(getLogicalLayout())) return emitError("requires a known logical layout"); auto inputType = dyn_cast(getInput().getType()); auto biasType = dyn_cast(getBias().getType()); auto outputType = dyn_cast(getOutput().getType()); if (!inputType || !biasType || !outputType) return emitError("requires ranked tensor input, bias, and output"); if (!inputType.hasStaticShape() || !biasType.hasStaticShape() || !outputType.hasStaticShape()) return emitError("requires static tensor input, bias, and output"); if (inputType != outputType) return emitError("requires matching input and output tensor types"); if (outputType.getRank() != 4) return emitError("requires rank-4 input/output tensors"); if (getLogicalLayout() != "nchw") return emitError("requires logical layout \"nchw\""); if (biasType.getElementType() != outputType.getElementType()) return emitError("requires bias element type to match the output element type"); ArrayRef biasShape = biasType.getShape(); const int64_t channels = outputType.getDimSize(1); const bool supported = biasShape.empty() || (biasShape.size() == 1 && biasShape[0] == channels) || (biasShape.size() == 2 && biasShape[0] == 1 && biasShape[1] == channels) || (biasShape.size() == 4 && biasShape[0] == 1 && biasShape[1] == channels && biasShape[2] == 1 && biasShape[3] == 1); if (!supported) return emitError("requires scalar or per-channel bias broadcastable over NCHW"); return success(); } LogicalResult SpatBlueprintOp::verify() { auto modeAttr = getModeAttr(); bool isFragmentAssembly = modeAttr && modeAttr.getValue() == "fragment_assembly"; if (!isFragmentAssembly && failed(verifyPlanTensorTypes(getOperation(), getInput(), getOutput(), "spat.blueprint"))) return failure(); if (!isKnownLogicalLayout(getLogicalLayout())) return emitError("requires a known logical layout"); if (!isKnownPhysicalLayout(getPhysicalLayout())) return emitError("requires a known physical layout"); auto logicalType = dyn_cast(getOutput().getType()); if (!logicalType) return emitError("requires ranked tensor output"); auto offsets = getFragmentOffsets(); auto sizes = getFragmentSizes(); if (offsets.size() != sizes.size()) return emitError("fragment offset and size arrays must have the same length"); int64_t rank = logicalType.getRank(); if (offsets.empty()) return success(); if (rank <= 0 || offsets.size() % rank != 0) return emitError("fragment metadata must be a whole number of rank-sized fragments"); auto verifyBoundsOnly = [&](ArrayRef strideValues) -> LogicalResult { ArrayRef shape = logicalType.getShape(); for (int64_t index = 0; index < static_cast(offsets.size()); ++index) { int64_t dim = index % rank; int64_t offset = offsets[index]; int64_t size = sizes[index]; int64_t stride = strideValues.empty() ? 1 : strideValues[index]; if (offset < 0 || size < 0 || stride < 0) return emitError("fragment offsets, sizes, and strides must be non-negative"); int64_t logicalDim = shape[dim]; if (!ShapedType::isDynamic(logicalDim) && offset + size > logicalDim) return emitError("fragment bounds must stay within the logical tensor shape"); if (stride != 1) return emitError("fragment assembly currently requires unit strides"); } return success(); }; if (!isFragmentAssembly) { if (failed(verifyBoundsOnly({}))) return failure(); if (!getFragments().empty()) return emitError("legacy blueprint does not accept extra fragment operands"); if (getFragmentSourceOffsetsAttr() || getFragmentStridesAttr() || getConflictPolicyAttr() || getCoveragePolicyAttr()) return emitError("legacy blueprint does not accept fragment assembly attributes"); return success(); } auto stridesAttr = getFragmentStridesAttr(); auto operandIndicesAttr = getFragmentOperandIndicesAttr(); auto sourceSlotsAttr = getFragmentSourceSlotsAttr(); auto sourceOffsetsAttr = getFragmentSourceOffsetsAttr(); if (!operandIndicesAttr) return emitError("fragment assembly blueprint requires fragment operand indices"); if (!sourceSlotsAttr) return emitError("fragment assembly blueprint requires physical fragment source slots"); if (!sourceOffsetsAttr) return emitError("fragment assembly blueprint requires fragment source offsets"); if (!stridesAttr) return emitError("fragment assembly blueprint requires fragment strides"); ArrayRef operandIndices = operandIndicesAttr.asArrayRef(); ArrayRef sourceSlots = sourceSlotsAttr.asArrayRef(); ArrayRef sourceOffsets = sourceOffsetsAttr.asArrayRef(); ArrayRef strides = stridesAttr.asArrayRef(); if (strides.size() != offsets.size()) return emitError("fragment stride and offset arrays must have the same length"); if (sourceOffsets.size() != operandIndices.size()) return emitError("fragment source offset count must match fragment operand index count"); if (sourceSlots.size() != operandIndices.size()) return emitError("fragment source slot count must match fragment operand index count"); if (!getConflictPolicyAttr() || !getCoveragePolicyAttr()) return emitError("fragment assembly blueprint requires conflict and coverage policies"); if (getConflictPolicy() != "disjoint") return emitError("fragment assembly blueprint currently supports only conflict_policy=\"disjoint\""); if (getCoveragePolicy() != "complete" && getCoveragePolicy() != "partial") return emitError("fragment assembly blueprint coverage_policy must be \"complete\" or \"partial\""); SmallVector operands; operands.push_back(getInput()); llvm::append_range(operands, getFragments()); int64_t operandCount = static_cast(operands.size()); int64_t fragmentCount = static_cast(operandIndices.size()); if (operandCount == 0) return emitError("fragment assembly blueprint requires at least one operand"); if (static_cast(offsets.size()) != fragmentCount * rank) return emitError("fragment assembly metadata count must match operand count * result rank"); if (failed(verifyBoundsOnly(strides))) return failure(); SmallVector, SmallVector>, 8> slices; slices.reserve(static_cast(fragmentCount)); SmallVector fragmentCountsByOperand(static_cast(operandCount), 0); auto expandFlatElementIndex = [](int64_t flatIndex, ArrayRef shape) { SmallVector indices(shape.size(), 0); for (int64_t dim = static_cast(shape.size()) - 1; dim >= 0; --dim) { indices[dim] = flatIndex % shape[dim]; flatIndex /= shape[dim]; } return indices; }; for (int64_t fragmentIndex = 0; fragmentIndex < fragmentCount; ++fragmentIndex) { int64_t operandIndex = operandIndices[fragmentIndex]; if (operandIndex < 0 || operandIndex >= operandCount) return emitError("fragment assembly operand index is out of range"); if (sourceSlots[fragmentIndex] < 0) return emitError("fragment assembly physical source slot must be nonnegative"); if (sourceOffsets[fragmentIndex] < 0) return emitError("fragment assembly source offsets must be nonnegative"); auto operandType = dyn_cast(operands[operandIndex].getType()); if (!operandType || !operandType.hasStaticShape()) return emitError("fragment assembly blueprint requires static ranked tensor operands"); if (operandType.getRank() != rank + 1) return emitError("fragment assembly physical operand must have one leading source-slot dimension"); if (sourceSlots[fragmentIndex] >= operandType.getDimSize(0)) return emitError("fragment assembly physical source slot is out of range"); auto fragmentType = RankedTensorType::get(operandType.getShape().drop_front(), operandType.getElementType(), operandType.getEncoding()); SmallVector fragmentOffsets; SmallVector fragmentSizes; fragmentOffsets.reserve(rank); fragmentSizes.reserve(rank); for (int64_t dim = 0; dim < rank; ++dim) { int64_t flatIndex = fragmentIndex * rank + dim; fragmentOffsets.push_back(offsets[flatIndex]); fragmentSizes.push_back(sizes[flatIndex]); } ++fragmentCountsByOperand[static_cast(operandIndex)]; int64_t fragmentElements = 1; for (int64_t dim = 0; dim < rank; ++dim) fragmentElements *= fragmentSizes[dim]; if (sourceOffsets[fragmentIndex] + fragmentElements > fragmentType.getNumElements()) return emitError("fragment assembly source offset exceeds the selected physical fragment bounds"); SmallVector sourceSliceOffsets = expandFlatElementIndex(sourceOffsets[fragmentIndex], fragmentType.getShape()); for (int64_t dim = 0; dim < rank; ++dim) if (sourceSliceOffsets[dim] + fragmentSizes[dim] > fragmentType.getDimSize(dim)) return emitError("fragment assembly source offset must describe a valid unit-stride slice"); for (const auto& [existingOffsets, existingSizes] : slices) { bool overlaps = true; for (int64_t dim = 0; dim < rank; ++dim) { int64_t begin = fragmentOffsets[dim]; int64_t end = begin + fragmentSizes[dim]; int64_t existingBegin = existingOffsets[dim]; int64_t existingEnd = existingBegin + existingSizes[dim]; if (end <= existingBegin || existingEnd <= begin) { overlaps = false; break; } } if (overlaps) return emitError("fragment assembly blueprint requires disjoint static slices"); } slices.push_back({std::move(fragmentOffsets), std::move(fragmentSizes)}); } for (int64_t operandIndex = 0; operandIndex < operandCount; ++operandIndex) { if (fragmentCountsByOperand[static_cast(operandIndex)] == 0) return emitError("fragment assembly blueprint requires every operand to contribute at least one fragment"); } if (getCoveragePolicy() == "complete") { int64_t covered = 0; int64_t logicalElements = 1; for (int64_t dimSize : logicalType.getShape()) { if (ShapedType::isDynamic(dimSize)) return emitError("fragment assembly complete coverage requires static result shape"); logicalElements *= dimSize; } for (const auto& [ignoredOffsets, fragmentSizes] : slices) { int64_t fragmentElements = 1; for (int64_t dimSize : fragmentSizes) fragmentElements *= dimSize; covered += fragmentElements; } if (covered != logicalElements) return emitError("fragment assembly complete coverage must cover the whole result exactly"); } return success(); } LogicalResult SpatMaterializeLayoutOp::verify() { if (failed(verifyPlanTensorTypes(getOperation(), getInput(), getOutput(), "spat.materialize_layout"))) return failure(); if (!isKnownLogicalLayout(getLogicalLayout())) return emitError("requires a known logical layout"); if (!isKnownPhysicalLayout(getSourcePhysicalLayout())) return emitError("requires a known source physical layout"); if (!isKnownPhysicalLayout(getTargetPhysicalLayout())) return emitError("requires a known target physical layout"); return success(); } LogicalResult verifyComputeResultsUses(Operation* op) { if (!isAnySpatialComputeLike(op)) return op->emitError("verifyComputeResultUses: op is not a Spatial compute-like operation"); if (!llvm::all_of(op->getResults(), [](Value result) { return llvm::all_of(result.getUsers(), [result](Operation* op) { if (isRecordedDeferredCommunicationSource(op, result)) return true; return !isAnySpatialComputeLike(op->getParentOp()); }); })) { return op->emitError("compute result used directly inside another Spatial compute body"); } return success(); } template LogicalResult verifyComputeLikeOp(ComputeOpTy compute, StringRef opName) { unsigned expectedArgCount = compute.getWeights().size() + compute.getInputs().size(); bool isScheduled = isa(compute.getOperation()); if (compute.getBody().empty()) return compute.emitOpError("compute body must have at least one block"); SmallVector yieldedTypes; for (Block& block : compute.getBody()) { if ((!isScheduled && block.getNumArguments() != expectedArgCount) || (isScheduled && block.getNumArguments() < expectedArgCount)) return compute.emitOpError("compute body must have weight and input block arguments"); for (auto [weightIndex, weight] : llvm::enumerate(compute.getWeights())) if (block.getArgument(weightIndex).getType() != weight.getType()) return compute.emitOpError("compute weight block argument types must match weight operand types exactly"); for (auto [inputIndex, input] : llvm::enumerate(compute.getInputs())) if (block.getArgument(compute.getWeights().size() + inputIndex).getType() != input.getType()) return compute.emitOpError("compute input block argument types must match input operand types exactly"); Operation* terminator = block.getTerminator(); if (auto yieldOp = dyn_cast_or_null(terminator)) { auto realized = compute->template getAttrOfType("scheduled.realized"); if (isScheduled && (!realized || !realized.getValue() || !compute.getBody().hasOneBlock())) return compute.emitOpError("scheduled compute blocks must terminate with spat.block_yield"); llvm::append_range(yieldedTypes, yieldOp->getOperandTypes()); continue; } auto blockYield = dyn_cast_or_null(terminator); if (!blockYield || !isScheduled) return compute.emitOpError("ComputeOp must have a single yield operation"); if (blockYield->getNumSuccessors() == 0) llvm::append_range(yieldedTypes, blockYield->getOperandTypes()); } auto resultTypes = compute.getResultTypes(); if (resultTypes.size() != yieldedTypes.size()) return compute.emitOpError("ComputeOp must have same number of results as yielded operands"); for (auto it : llvm::reverse(llvm::zip(resultTypes, yieldedTypes))) { auto resultType = std::get<0>(it); auto yieldType = std::get<1>(it); if (resultType != yieldType || failed(verifyCompatibleShape(resultType, yieldType))) return compute.emitOpError("ComputeOp output must be of the same type as yieldOp operand"); if (auto resultRankedType = dyn_cast(resultType)) { if (auto yieldRankedType = dyn_cast(yieldType)) { if (resultRankedType.getEncoding() != yieldRankedType.getEncoding()) return compute.emitOpError("ComputeOp output has an encoding while yieldOp operand does not have one"); } else { return compute.emitOpError("ComputeOp output must have the same encoding as yieldOp operand"); } } else if (dyn_cast(yieldType)) { return compute.emitOpError("ComputeOp output must not have an encoding if yieldOp operand has one"); } } if (compute.getBody().hasOneBlock()) for (unsigned inputIndex = 0; inputIndex < compute.getInputs().size(); ++inputIndex) if (auto inputArg = compute.getInputArgument(inputIndex); !inputArg || inputArg->use_empty()) return compute.emitOpError("ComputeOp block argument is not used"); if (failed(verifyStaticWeights(compute, opName))) return failure(); if (failed(verifyOnlyConstantExternalValues(compute.getOperation(), compute.getBody(), opName))) return failure(); if (failed(verifyComputeResultsUses(compute.getOperation()))) return failure(); return success(); } LogicalResult SpatGraphCompute::verify() { return verifyComputeLikeOp(*this, "spat.graph_compute"); } LogicalResult SpatScheduledCompute::verify() { return verifyComputeLikeOp(*this, "spat.scheduled_compute"); } LogicalResult SpatBlockYieldOp::verify() { if (getOperation()->getNumSuccessors() > 1) return emitOpError("may target at most one next scheduled block"); Operation* parent = getOperation()->getParentOp(); if (!isa_and_nonnull(parent)) return emitOpError("expected spat.scheduled_compute parent"); if (getOperation()->getNumSuccessors() == 1) { Block* next = getOperation()->getSuccessor(0); if (getOperation()->getNumOperands() != next->getNumArguments()) return emitOpError("successor operand count must match next block argument count"); for (auto [operand, argument] : llvm::zip(getOperation()->getOperands(), next->getArguments())) if (operand.getType() != argument.getType()) return emitOpError("successor operand types must match next block argument types"); } return success(); } LogicalResult SpatDeferredCommunicationOp::verify() { if (getSources().empty()) return emitOpError("requires at least one source"); static constexpr StringLiteral staleAttributes[] = { "exchangeId", "logicalProducer", "logicalConsumer", "sourceClass", "targetClass", "sourceCore", "targetCore", "sourceLane", "targetLane", "transferKind", "resultIndex", "projectedTransfer", "hostOutputOwner", "source_cpus", "source_classes", "source_lane_ranges", "target_cpus", "target_classes", "target_lane_ranges", "batched", "source_operand_for_scheduled_lane", "multi_source_payload"}; for (StringLiteral name : staleAttributes) if (getOperation()->hasAttr(name)) return emitOpError() << "does not accept stale routing attribute '" << name << "'; source selection and shaping belong in the body and routing is derived in Phase 2"; if (failed(verifyRegionArguments(getOperation(), getBody(), getSources(), "spat.deferred_communication"))) return failure(); return verifyYieldTypes(getOperation(), getBody(), getOperation()->getResultTypes(), "spat.deferred_communication"); } template LogicalResult verifyComputeBatchLikeOp(ComputeBatchOpTy batch, StringRef opName) { int32_t count = batch.getLaneCount(); if (count <= 0) return batch.emitOpError("laneCount must be positive"); auto laneCountSz = static_cast(count); if (auto coreIdAttr = batch->getAttr(kCoreIdsAttrName)) { auto coreIdsAttr = dyn_cast(coreIdAttr); if (!coreIdsAttr) return batch.emitOpError("compute_batch coreIds attribute must be a dense i32 array"); if (coreIdsAttr.size() != static_cast(laneCountSz)) return batch.emitOpError("compute_batch coreIds array length must match laneCount"); if (llvm::any_of(coreIdsAttr.asArrayRef(), [](int32_t coreId) { return coreId < 0; })) return batch.emitOpError("compute_batch coreIds values must be non-negative"); DenseSet seenCoreIds; for (int32_t coreId : coreIdsAttr.asArrayRef()) if (!seenCoreIds.insert(coreId).second) return batch.emitOpError("compute_batch coreIds values must be unique"); } if (batch.getBody().empty()) return batch.emitOpError("compute_batch body must have at least one block"); unsigned expectedArgCount = 1 + batch.getWeights().size() + batch.getInputs().size() + batch.getNumResults(); bool verifyLaneSliceOffsets = !isa(batch.getOperation()); for (Block& block : batch.getBody()) { if (block.getNumArguments() == 0) return batch.emitOpError("compute_batch body must have exactly one lane block argument"); if (block.getNumArguments() != expectedArgCount) return batch.emitOpError( "compute_batch body block arguments must match lane, weight, input, and output operands/results"); if (!block.getArgument(0).getType().isIndex()) return batch.emitOpError("compute_batch first block argument must have index type"); for (auto [weightIndex, weight] : llvm::enumerate(batch.getWeights())) if (block.getArgument(1 + weightIndex).getType() != weight.getType()) return batch.emitOpError("compute_batch weight block argument types must match weight operand types exactly"); for (auto [inputIndex, input] : llvm::enumerate(batch.getInputs())) if (block.getArgument(1 + batch.getWeights().size() + inputIndex).getType() != input.getType()) return batch.emitOpError("compute_batch input block argument types must match input operand types exactly"); for (auto [resultIndex, resultType] : llvm::enumerate(batch.getResultTypes())) if (block.getArgument(1 + batch.getWeights().size() + batch.getInputs().size() + resultIndex).getType() != resultType) return batch.emitOpError("compute_batch output block argument types must match result types exactly"); if (failed(verifyBatchBody(batch, block, verifyLaneSliceOffsets))) return failure(); } if (failed(verifyComputeResultsUses(batch.getOperation()))) return failure(); if (failed(verifyStaticWeights(batch, opName))) return failure(); if (failed(verifyOnlyConstantExternalValues(batch.getOperation(), batch.getBody(), opName))) return failure(); return success(); } LogicalResult SpatGraphComputeBatch::verify() { return verifyComputeBatchLikeOp(*this, "spat.graph_compute_batch"); } LogicalResult SpatScheduledComputeBatch::verify() { return verifyComputeBatchLikeOp(*this, "spat.scheduled_compute_batch"); } LogicalResult SpatInParallelOp::verify() { Operation* parent = getOperation()->getParentOp(); if (!isAnySpatialComputeBatchLike(parent)) return emitOpError("expected spat.graph_compute_batch or spat.scheduled_compute_batch parent"); if (parent->getNumResults() == 0) return emitOpError("requires a resultful spat.compute_batch parent"); std::optional laneArg; if (auto graphBatch = dyn_cast(parent)) laneArg = graphBatch.getLaneArgument(); else laneArg = cast(parent).getLaneArgument(); if (!laneArg) return emitOpError("expected compute_batch lane block argument"); for (Operation& op : getRegion().front().getOperations()) { auto insertSliceOp = dyn_cast(&op); if (!insertSliceOp) return emitOpError("expected only tensor.parallel_insert_slice ops"); if (failed(verifyStaticUnitStrideParallelInsertSliceOp(insertSliceOp, *laneArg, "tensor.parallel_insert_slice"))) return failure(); MutableOperandRange destinations = insertSliceOp.getUpdatedDestinations(); for (OpOperand& destination : destinations) if ((isa(parent) && !isBatchOutputArgument(cast(parent), destination.get())) || (isa(parent) && !isBatchOutputArgument(cast(parent), destination.get()))) return op.emitOpError("may only insert into a compute_batch output block argument"); } return success(); } } // namespace spatial } // namespace onnx_mlir