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