1 //===- MemRefToSPIRV.cpp - MemRef to SPIR-V Patterns ----------------------===// 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 // 9 // This file implements patterns to convert MemRef dialect to SPIR-V dialect. 10 // 11 //===----------------------------------------------------------------------===// 12 13 #include "mlir/Dialect/MemRef/IR/MemRef.h" 14 #include "mlir/Dialect/SPIRV/IR/SPIRVDialect.h" 15 #include "mlir/Dialect/SPIRV/IR/SPIRVOps.h" 16 #include "mlir/Dialect/SPIRV/Transforms/SPIRVConversion.h" 17 #include "llvm/Support/Debug.h" 18 19 #define DEBUG_TYPE "memref-to-spirv-pattern" 20 21 using namespace mlir; 22 23 //===----------------------------------------------------------------------===// 24 // Utility functions 25 //===----------------------------------------------------------------------===// 26 27 /// Returns the offset of the value in `targetBits` representation. 28 /// 29 /// `srcIdx` is an index into a 1-D array with each element having `sourceBits`. 30 /// It's assumed to be non-negative. 31 /// 32 /// When accessing an element in the array treating as having elements of 33 /// `targetBits`, multiple values are loaded in the same time. The method 34 /// returns the offset where the `srcIdx` locates in the value. For example, if 35 /// `sourceBits` equals to 8 and `targetBits` equals to 32, the x-th element is 36 /// located at (x % 4) * 8. Because there are four elements in one i32, and one 37 /// element has 8 bits. 38 static Value getOffsetForBitwidth(Location loc, Value srcIdx, int sourceBits, 39 int targetBits, OpBuilder &builder) { 40 assert(targetBits % sourceBits == 0); 41 IntegerType targetType = builder.getIntegerType(targetBits); 42 IntegerAttr idxAttr = 43 builder.getIntegerAttr(targetType, targetBits / sourceBits); 44 auto idx = builder.create<spirv::ConstantOp>(loc, targetType, idxAttr); 45 IntegerAttr srcBitsAttr = builder.getIntegerAttr(targetType, sourceBits); 46 auto srcBitsValue = 47 builder.create<spirv::ConstantOp>(loc, targetType, srcBitsAttr); 48 auto m = builder.create<spirv::UModOp>(loc, srcIdx, idx); 49 return builder.create<spirv::IMulOp>(loc, targetType, m, srcBitsValue); 50 } 51 52 /// Returns an adjusted spirv::AccessChainOp. Based on the 53 /// extension/capabilities, certain integer bitwidths `sourceBits` might not be 54 /// supported. During conversion if a memref of an unsupported type is used, 55 /// load/stores to this memref need to be modified to use a supported higher 56 /// bitwidth `targetBits` and extracting the required bits. For an accessing a 57 /// 1D array (spv.array or spv.rt_array), the last index is modified to load the 58 /// bits needed. The extraction of the actual bits needed are handled 59 /// separately. Note that this only works for a 1-D tensor. 60 static Value adjustAccessChainForBitwidth(SPIRVTypeConverter &typeConverter, 61 spirv::AccessChainOp op, 62 int sourceBits, int targetBits, 63 OpBuilder &builder) { 64 assert(targetBits % sourceBits == 0); 65 const auto loc = op.getLoc(); 66 IntegerType targetType = builder.getIntegerType(targetBits); 67 IntegerAttr attr = 68 builder.getIntegerAttr(targetType, targetBits / sourceBits); 69 auto idx = builder.create<spirv::ConstantOp>(loc, targetType, attr); 70 auto lastDim = op->getOperand(op.getNumOperands() - 1); 71 auto indices = llvm::to_vector<4>(op.indices()); 72 // There are two elements if this is a 1-D tensor. 73 assert(indices.size() == 2); 74 indices.back() = builder.create<spirv::SDivOp>(loc, lastDim, idx); 75 Type t = typeConverter.convertType(op.component_ptr().getType()); 76 return builder.create<spirv::AccessChainOp>(loc, t, op.base_ptr(), indices); 77 } 78 79 /// Returns the shifted `targetBits`-bit value with the given offset. 80 static Value shiftValue(Location loc, Value value, Value offset, Value mask, 81 int targetBits, OpBuilder &builder) { 82 Type targetType = builder.getIntegerType(targetBits); 83 Value result = builder.create<spirv::BitwiseAndOp>(loc, value, mask); 84 return builder.create<spirv::ShiftLeftLogicalOp>(loc, targetType, result, 85 offset); 86 } 87 88 /// Returns true if the allocations of type `t` can be lowered to SPIR-V. 89 static bool isAllocationSupported(MemRefType t) { 90 // Currently only support workgroup local memory allocations with static 91 // shape and int or float or vector of int or float element type. 92 if (!(t.hasStaticShape() && 93 SPIRVTypeConverter::getMemorySpaceForStorageClass( 94 spirv::StorageClass::Workgroup) == t.getMemorySpaceAsInt())) 95 return false; 96 Type elementType = t.getElementType(); 97 if (auto vecType = elementType.dyn_cast<VectorType>()) 98 elementType = vecType.getElementType(); 99 return elementType.isIntOrFloat(); 100 } 101 102 /// Returns the scope to use for atomic operations use for emulating store 103 /// operations of unsupported integer bitwidths, based on the memref 104 /// type. Returns None on failure. 105 static Optional<spirv::Scope> getAtomicOpScope(MemRefType t) { 106 Optional<spirv::StorageClass> storageClass = 107 SPIRVTypeConverter::getStorageClassForMemorySpace( 108 t.getMemorySpaceAsInt()); 109 if (!storageClass) 110 return {}; 111 switch (*storageClass) { 112 case spirv::StorageClass::StorageBuffer: 113 return spirv::Scope::Device; 114 case spirv::StorageClass::Workgroup: 115 return spirv::Scope::Workgroup; 116 default: { 117 } 118 } 119 return {}; 120 } 121 122 //===----------------------------------------------------------------------===// 123 // Operation conversion 124 //===----------------------------------------------------------------------===// 125 126 // Note that DRR cannot be used for the patterns in this file: we may need to 127 // convert type along the way, which requires ConversionPattern. DRR generates 128 // normal RewritePattern. 129 130 namespace { 131 132 /// Converts an allocation operation to SPIR-V. Currently only supports lowering 133 /// to Workgroup memory when the size is constant. Note that this pattern needs 134 /// to be applied in a pass that runs at least at spv.module scope since it wil 135 /// ladd global variables into the spv.module. 136 class AllocOpPattern final : public OpConversionPattern<memref::AllocOp> { 137 public: 138 using OpConversionPattern<memref::AllocOp>::OpConversionPattern; 139 140 LogicalResult 141 matchAndRewrite(memref::AllocOp operation, ArrayRef<Value> operands, 142 ConversionPatternRewriter &rewriter) const override; 143 }; 144 145 /// Removed a deallocation if it is a supported allocation. Currently only 146 /// removes deallocation if the memory space is workgroup memory. 147 class DeallocOpPattern final : public OpConversionPattern<memref::DeallocOp> { 148 public: 149 using OpConversionPattern<memref::DeallocOp>::OpConversionPattern; 150 151 LogicalResult 152 matchAndRewrite(memref::DeallocOp operation, ArrayRef<Value> operands, 153 ConversionPatternRewriter &rewriter) const override; 154 }; 155 156 /// Converts memref.load to spv.Load. 157 class IntLoadOpPattern final : public OpConversionPattern<memref::LoadOp> { 158 public: 159 using OpConversionPattern<memref::LoadOp>::OpConversionPattern; 160 161 LogicalResult 162 matchAndRewrite(memref::LoadOp loadOp, ArrayRef<Value> operands, 163 ConversionPatternRewriter &rewriter) const override; 164 }; 165 166 /// Converts memref.load to spv.Load. 167 class LoadOpPattern final : public OpConversionPattern<memref::LoadOp> { 168 public: 169 using OpConversionPattern<memref::LoadOp>::OpConversionPattern; 170 171 LogicalResult 172 matchAndRewrite(memref::LoadOp loadOp, ArrayRef<Value> operands, 173 ConversionPatternRewriter &rewriter) const override; 174 }; 175 176 /// Converts memref.store to spv.Store on integers. 177 class IntStoreOpPattern final : public OpConversionPattern<memref::StoreOp> { 178 public: 179 using OpConversionPattern<memref::StoreOp>::OpConversionPattern; 180 181 LogicalResult 182 matchAndRewrite(memref::StoreOp storeOp, ArrayRef<Value> operands, 183 ConversionPatternRewriter &rewriter) const override; 184 }; 185 186 /// Converts memref.store to spv.Store. 187 class StoreOpPattern final : public OpConversionPattern<memref::StoreOp> { 188 public: 189 using OpConversionPattern<memref::StoreOp>::OpConversionPattern; 190 191 LogicalResult 192 matchAndRewrite(memref::StoreOp storeOp, ArrayRef<Value> operands, 193 ConversionPatternRewriter &rewriter) const override; 194 }; 195 196 } // namespace 197 198 //===----------------------------------------------------------------------===// 199 // AllocOp 200 //===----------------------------------------------------------------------===// 201 202 LogicalResult 203 AllocOpPattern::matchAndRewrite(memref::AllocOp operation, 204 ArrayRef<Value> operands, 205 ConversionPatternRewriter &rewriter) const { 206 MemRefType allocType = operation.getType(); 207 if (!isAllocationSupported(allocType)) 208 return operation.emitError("unhandled allocation type"); 209 210 // Get the SPIR-V type for the allocation. 211 Type spirvType = getTypeConverter()->convertType(allocType); 212 213 // Insert spv.GlobalVariable for this allocation. 214 Operation *parent = 215 SymbolTable::getNearestSymbolTable(operation->getParentOp()); 216 if (!parent) 217 return failure(); 218 Location loc = operation.getLoc(); 219 spirv::GlobalVariableOp varOp; 220 { 221 OpBuilder::InsertionGuard guard(rewriter); 222 Block &entryBlock = *parent->getRegion(0).begin(); 223 rewriter.setInsertionPointToStart(&entryBlock); 224 auto varOps = entryBlock.getOps<spirv::GlobalVariableOp>(); 225 std::string varName = 226 std::string("__workgroup_mem__") + 227 std::to_string(std::distance(varOps.begin(), varOps.end())); 228 varOp = rewriter.create<spirv::GlobalVariableOp>(loc, spirvType, varName, 229 /*initializer=*/nullptr); 230 } 231 232 // Get pointer to global variable at the current scope. 233 rewriter.replaceOpWithNewOp<spirv::AddressOfOp>(operation, varOp); 234 return success(); 235 } 236 237 //===----------------------------------------------------------------------===// 238 // DeallocOp 239 //===----------------------------------------------------------------------===// 240 241 LogicalResult 242 DeallocOpPattern::matchAndRewrite(memref::DeallocOp operation, 243 ArrayRef<Value> operands, 244 ConversionPatternRewriter &rewriter) const { 245 MemRefType deallocType = operation.memref().getType().cast<MemRefType>(); 246 if (!isAllocationSupported(deallocType)) 247 return operation.emitError("unhandled deallocation type"); 248 rewriter.eraseOp(operation); 249 return success(); 250 } 251 252 //===----------------------------------------------------------------------===// 253 // LoadOp 254 //===----------------------------------------------------------------------===// 255 256 LogicalResult 257 IntLoadOpPattern::matchAndRewrite(memref::LoadOp loadOp, 258 ArrayRef<Value> operands, 259 ConversionPatternRewriter &rewriter) const { 260 memref::LoadOpAdaptor loadOperands(operands); 261 auto loc = loadOp.getLoc(); 262 auto memrefType = loadOp.memref().getType().cast<MemRefType>(); 263 if (!memrefType.getElementType().isSignlessInteger()) 264 return failure(); 265 266 auto &typeConverter = *getTypeConverter<SPIRVTypeConverter>(); 267 spirv::AccessChainOp accessChainOp = 268 spirv::getElementPtr(typeConverter, memrefType, loadOperands.memref(), 269 loadOperands.indices(), loc, rewriter); 270 271 int srcBits = memrefType.getElementType().getIntOrFloatBitWidth(); 272 bool isBool = srcBits == 1; 273 if (isBool) 274 srcBits = typeConverter.getOptions().boolNumBits; 275 Type pointeeType = typeConverter.convertType(memrefType) 276 .cast<spirv::PointerType>() 277 .getPointeeType(); 278 Type structElemType = pointeeType.cast<spirv::StructType>().getElementType(0); 279 Type dstType; 280 if (auto arrayType = structElemType.dyn_cast<spirv::ArrayType>()) 281 dstType = arrayType.getElementType(); 282 else 283 dstType = structElemType.cast<spirv::RuntimeArrayType>().getElementType(); 284 285 int dstBits = dstType.getIntOrFloatBitWidth(); 286 assert(dstBits % srcBits == 0); 287 288 // If the rewrited load op has the same bit width, use the loading value 289 // directly. 290 if (srcBits == dstBits) { 291 rewriter.replaceOpWithNewOp<spirv::LoadOp>(loadOp, 292 accessChainOp.getResult()); 293 return success(); 294 } 295 296 // Assume that getElementPtr() works linearizely. If it's a scalar, the method 297 // still returns a linearized accessing. If the accessing is not linearized, 298 // there will be offset issues. 299 assert(accessChainOp.indices().size() == 2); 300 Value adjustedPtr = adjustAccessChainForBitwidth(typeConverter, accessChainOp, 301 srcBits, dstBits, rewriter); 302 Value spvLoadOp = rewriter.create<spirv::LoadOp>( 303 loc, dstType, adjustedPtr, 304 loadOp->getAttrOfType<spirv::MemoryAccessAttr>( 305 spirv::attributeName<spirv::MemoryAccess>()), 306 loadOp->getAttrOfType<IntegerAttr>("alignment")); 307 308 // Shift the bits to the rightmost. 309 // ____XXXX________ -> ____________XXXX 310 Value lastDim = accessChainOp->getOperand(accessChainOp.getNumOperands() - 1); 311 Value offset = getOffsetForBitwidth(loc, lastDim, srcBits, dstBits, rewriter); 312 Value result = rewriter.create<spirv::ShiftRightArithmeticOp>( 313 loc, spvLoadOp.getType(), spvLoadOp, offset); 314 315 // Apply the mask to extract corresponding bits. 316 Value mask = rewriter.create<spirv::ConstantOp>( 317 loc, dstType, rewriter.getIntegerAttr(dstType, (1 << srcBits) - 1)); 318 result = rewriter.create<spirv::BitwiseAndOp>(loc, dstType, result, mask); 319 320 // Apply sign extension on the loading value unconditionally. The signedness 321 // semantic is carried in the operator itself, we relies other pattern to 322 // handle the casting. 323 IntegerAttr shiftValueAttr = 324 rewriter.getIntegerAttr(dstType, dstBits - srcBits); 325 Value shiftValue = 326 rewriter.create<spirv::ConstantOp>(loc, dstType, shiftValueAttr); 327 result = rewriter.create<spirv::ShiftLeftLogicalOp>(loc, dstType, result, 328 shiftValue); 329 result = rewriter.create<spirv::ShiftRightArithmeticOp>(loc, dstType, result, 330 shiftValue); 331 332 if (isBool) { 333 dstType = typeConverter.convertType(loadOp.getType()); 334 mask = spirv::ConstantOp::getOne(result.getType(), loc, rewriter); 335 Value isOne = rewriter.create<spirv::IEqualOp>(loc, result, mask); 336 Value zero = spirv::ConstantOp::getZero(dstType, loc, rewriter); 337 Value one = spirv::ConstantOp::getOne(dstType, loc, rewriter); 338 result = rewriter.create<spirv::SelectOp>(loc, dstType, isOne, one, zero); 339 } else if (result.getType().getIntOrFloatBitWidth() != 340 static_cast<unsigned>(dstBits)) { 341 result = rewriter.create<spirv::SConvertOp>(loc, dstType, result); 342 } 343 rewriter.replaceOp(loadOp, result); 344 345 assert(accessChainOp.use_empty()); 346 rewriter.eraseOp(accessChainOp); 347 348 return success(); 349 } 350 351 LogicalResult 352 LoadOpPattern::matchAndRewrite(memref::LoadOp loadOp, ArrayRef<Value> operands, 353 ConversionPatternRewriter &rewriter) const { 354 memref::LoadOpAdaptor loadOperands(operands); 355 auto memrefType = loadOp.memref().getType().cast<MemRefType>(); 356 if (memrefType.getElementType().isSignlessInteger()) 357 return failure(); 358 auto loadPtr = spirv::getElementPtr( 359 *getTypeConverter<SPIRVTypeConverter>(), memrefType, 360 loadOperands.memref(), loadOperands.indices(), loadOp.getLoc(), rewriter); 361 rewriter.replaceOpWithNewOp<spirv::LoadOp>(loadOp, loadPtr); 362 return success(); 363 } 364 365 LogicalResult 366 IntStoreOpPattern::matchAndRewrite(memref::StoreOp storeOp, 367 ArrayRef<Value> operands, 368 ConversionPatternRewriter &rewriter) const { 369 memref::StoreOpAdaptor storeOperands(operands); 370 auto memrefType = storeOp.memref().getType().cast<MemRefType>(); 371 if (!memrefType.getElementType().isSignlessInteger()) 372 return failure(); 373 374 auto loc = storeOp.getLoc(); 375 auto &typeConverter = *getTypeConverter<SPIRVTypeConverter>(); 376 spirv::AccessChainOp accessChainOp = 377 spirv::getElementPtr(typeConverter, memrefType, storeOperands.memref(), 378 storeOperands.indices(), loc, rewriter); 379 int srcBits = memrefType.getElementType().getIntOrFloatBitWidth(); 380 381 bool isBool = srcBits == 1; 382 if (isBool) 383 srcBits = typeConverter.getOptions().boolNumBits; 384 Type pointeeType = typeConverter.convertType(memrefType) 385 .cast<spirv::PointerType>() 386 .getPointeeType(); 387 Type structElemType = pointeeType.cast<spirv::StructType>().getElementType(0); 388 Type dstType; 389 if (auto arrayType = structElemType.dyn_cast<spirv::ArrayType>()) 390 dstType = arrayType.getElementType(); 391 else 392 dstType = structElemType.cast<spirv::RuntimeArrayType>().getElementType(); 393 394 int dstBits = dstType.getIntOrFloatBitWidth(); 395 assert(dstBits % srcBits == 0); 396 397 if (srcBits == dstBits) { 398 rewriter.replaceOpWithNewOp<spirv::StoreOp>( 399 storeOp, accessChainOp.getResult(), storeOperands.value()); 400 return success(); 401 } 402 403 // Since there are multi threads in the processing, the emulation will be done 404 // with atomic operations. E.g., if the storing value is i8, rewrite the 405 // StoreOp to 406 // 1) load a 32-bit integer 407 // 2) clear 8 bits in the loading value 408 // 3) store 32-bit value back 409 // 4) load a 32-bit integer 410 // 5) modify 8 bits in the loading value 411 // 6) store 32-bit value back 412 // The step 1 to step 3 are done by AtomicAnd as one atomic step, and the step 413 // 4 to step 6 are done by AtomicOr as another atomic step. 414 assert(accessChainOp.indices().size() == 2); 415 Value lastDim = accessChainOp->getOperand(accessChainOp.getNumOperands() - 1); 416 Value offset = getOffsetForBitwidth(loc, lastDim, srcBits, dstBits, rewriter); 417 418 // Create a mask to clear the destination. E.g., if it is the second i8 in 419 // i32, 0xFFFF00FF is created. 420 Value mask = rewriter.create<spirv::ConstantOp>( 421 loc, dstType, rewriter.getIntegerAttr(dstType, (1 << srcBits) - 1)); 422 Value clearBitsMask = 423 rewriter.create<spirv::ShiftLeftLogicalOp>(loc, dstType, mask, offset); 424 clearBitsMask = rewriter.create<spirv::NotOp>(loc, dstType, clearBitsMask); 425 426 Value storeVal = storeOperands.value(); 427 if (isBool) { 428 Value zero = spirv::ConstantOp::getZero(dstType, loc, rewriter); 429 Value one = spirv::ConstantOp::getOne(dstType, loc, rewriter); 430 storeVal = 431 rewriter.create<spirv::SelectOp>(loc, dstType, storeVal, one, zero); 432 } 433 storeVal = shiftValue(loc, storeVal, offset, mask, dstBits, rewriter); 434 Value adjustedPtr = adjustAccessChainForBitwidth(typeConverter, accessChainOp, 435 srcBits, dstBits, rewriter); 436 Optional<spirv::Scope> scope = getAtomicOpScope(memrefType); 437 if (!scope) 438 return failure(); 439 Value result = rewriter.create<spirv::AtomicAndOp>( 440 loc, dstType, adjustedPtr, *scope, spirv::MemorySemantics::AcquireRelease, 441 clearBitsMask); 442 result = rewriter.create<spirv::AtomicOrOp>( 443 loc, dstType, adjustedPtr, *scope, spirv::MemorySemantics::AcquireRelease, 444 storeVal); 445 446 // The AtomicOrOp has no side effect. Since it is already inserted, we can 447 // just remove the original StoreOp. Note that rewriter.replaceOp() 448 // doesn't work because it only accepts that the numbers of result are the 449 // same. 450 rewriter.eraseOp(storeOp); 451 452 assert(accessChainOp.use_empty()); 453 rewriter.eraseOp(accessChainOp); 454 455 return success(); 456 } 457 458 LogicalResult 459 StoreOpPattern::matchAndRewrite(memref::StoreOp storeOp, 460 ArrayRef<Value> operands, 461 ConversionPatternRewriter &rewriter) const { 462 memref::StoreOpAdaptor storeOperands(operands); 463 auto memrefType = storeOp.memref().getType().cast<MemRefType>(); 464 if (memrefType.getElementType().isSignlessInteger()) 465 return failure(); 466 auto storePtr = 467 spirv::getElementPtr(*getTypeConverter<SPIRVTypeConverter>(), memrefType, 468 storeOperands.memref(), storeOperands.indices(), 469 storeOp.getLoc(), rewriter); 470 rewriter.replaceOpWithNewOp<spirv::StoreOp>(storeOp, storePtr, 471 storeOperands.value()); 472 return success(); 473 } 474 475 //===----------------------------------------------------------------------===// 476 // Pattern population 477 //===----------------------------------------------------------------------===// 478 479 namespace mlir { 480 void populateMemRefToSPIRVPatterns(SPIRVTypeConverter &typeConverter, 481 RewritePatternSet &patterns) { 482 patterns.add<AllocOpPattern, DeallocOpPattern, IntLoadOpPattern, 483 IntStoreOpPattern, LoadOpPattern, StoreOpPattern>( 484 typeConverter, patterns.getContext()); 485 } 486 } // namespace mlir 487