Enzyme main
Loading...
Searching...
No Matches
LowerLLVMExtPass.cpp
Go to the documentation of this file.
1//===- LowerLLVMExtPass.cpp - Lower LLVM Ext operations ------------------ //
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 a pass to lower custom ops generated by the LLVM Ext
10// dialect.
11//
12//===----------------------------------------------------------------------===//
13
15#include "Dialect/Ops.h"
16#include "Passes/Passes.h"
17
18#include "mlir/Dialect/Arith/IR/Arith.h"
19#include "mlir/Dialect/GPU/IR/GPUDialect.h"
20#include "mlir/Dialect/LLVMIR/LLVMDialect.h"
21#include "mlir/Dialect/MemRef/IR/MemRef.h"
22
23#include "mlir/IR/Dominance.h"
24#include "mlir/IR/Matchers.h"
25#include "mlir/IR/PatternMatch.h"
26#include "mlir/Interfaces/SideEffectInterfaces.h"
27#include "mlir/Interfaces/ViewLikeInterface.h"
28
29namespace mlir {
30namespace enzyme {
31using namespace mlir::enzyme;
32#define GEN_PASS_DEF_LOWERLLVMEXTPASS
33#include "Passes/Passes.h.inc"
34} // namespace enzyme
35} // namespace mlir
36
37namespace {
38using namespace mlir;
39using namespace enzyme;
40
41/// Device memory is only reachable through the accelerator runtime, so the
42/// host allocator (`malloc`/`free`) can't be used for it. Non-zero memory
43/// spaces are routed through the GPU dialect instead: `gpu.alloc`/`gpu.dealloc`
44/// become the runtime's allocator calls further down the pipeline
45/// (convert-polygeist-to-llvm). Copies don't need that split at all --
46/// `enzymexla.memcpy` covers every memory space, including the host.
47///
48/// Those ops work on memrefs rather than raw pointers, so getting in and out of
49/// them needs the enzymexla view casts. They live outside this repository, so
50/// they are built by name.
51static constexpr StringLiteral kEnzymeXLADialect = "enzymexla";
52static constexpr StringLiteral kPointer2Memref = "enzymexla.pointer2memref";
53static constexpr StringLiteral kMemref2Pointer = "enzymexla.memref2pointer";
54static constexpr StringLiteral kEnzymeXLAMemcpy = "enzymexla.memcpy";
55
56/// Rank-1 byte buffer in `memorySpace`, the shape the enzymexla view casts and
57/// the GPU runtime lowerings expect for an untyped allocation.
58static MemRefType deviceBufferType(OpBuilder &builder, unsigned memorySpace) {
59 return MemRefType::get({ShapedType::kDynamic}, builder.getI8Type(),
60 MemRefLayoutAttrInterface{},
61 builder.getI64IntegerAttr(memorySpace));
62}
63
64static Operation *createByName(OpBuilder &builder, Location loc, StringRef name,
65 ValueRange operands, TypeRange results) {
66 return builder.create(loc, StringAttr::get(builder.getContext(), name),
67 operands, results);
68}
69
70/// Views `ptr` as a byte buffer in the address space its type already names.
71static Value pointerToMemref(OpBuilder &builder, Location loc, Value ptr) {
72 unsigned memorySpace =
73 cast<LLVM::LLVMPointerType>(ptr.getType()).getAddressSpace();
74 return createByName(builder, loc, kPointer2Memref, ValueRange{ptr},
75 TypeRange{deviceBufferType(builder, memorySpace)})
76 ->getResult(0);
77}
78
79/// Inverse of the above: `ptrType` is the pointer being replaced, so the view
80/// cast keeps its address space rather than handing back a host pointer.
81static Value memrefToPointer(OpBuilder &builder, Location loc, Value memref,
82 Type ptrType) {
83 return createByName(builder, loc, kMemref2Pointer, ValueRange{memref},
84 TypeRange{ptrType})
85 ->getResult(0);
86}
87
88/// Emits the error on `op` and returns failure if the enzymexla ops can't be
89/// built here, since without the view casts and the copy there is nothing to
90/// lower to and silently leaving the op behind would fail much later with no
91/// indication of why. A context that allows unregistered dialects can build
92/// them by name regardless, which is how the tests in this repository -- where
93/// enzymexla isn't linked in -- exercise the lowering.
94static LogicalResult checkEnzymeXLAAvailable(Operation *op) {
95 MLIRContext *ctx = op->getContext();
96 if (!ctx->getLoadedDialect(kEnzymeXLADialect) &&
97 !ctx->allowsUnregisteredDialects()) {
98 op->emitError() << "lowering this op requires the '" << kEnzymeXLADialect
99 << "' dialect to be loaded";
100 return failure();
101 }
102 return success();
103}
104
105static LogicalResult lowerAllocDevice(llvm_ext::AllocOp alloc) {
106 if (failed(checkEnzymeXLAAvailable(alloc)))
107 return failure();
108
109 OpBuilder builder(alloc);
110 Location loc = alloc.getLoc();
111
112 Value size = arith::IndexCastOp::create(builder, loc, builder.getIndexType(),
113 alloc.getSize());
114 Value buffer =
115 gpu::AllocOp::create(builder, loc,
116 deviceBufferType(builder, alloc.getMemorySpace()),
117 /*asyncToken=*/(mlir::Type) nullptr,
118 /*asyncDependencies=*/ValueRange{},
119 /*dynamicSizes=*/ValueRange{size},
120 /*symbolOperands=*/ValueRange{})
121 .getMemref();
122
123 alloc.getResult().replaceAllUsesWith(
124 memrefToPointer(builder, loc, buffer, alloc.getResult().getType()));
125 alloc.erase();
126 return success();
127}
128
129static LogicalResult lowerFreeDevice(llvm_ext::FreeOp free) {
130 if (failed(checkEnzymeXLAAvailable(free)))
131 return failure();
132
133 OpBuilder builder(free);
134 Location loc = free.getLoc();
135
136 Value buffer = pointerToMemref(builder, loc, free.getPtr());
137 gpu::DeallocOp::create(builder, loc, /*asyncToken=*/(mlir::Type) nullptr,
138 /*asyncDependencies=*/ValueRange{}, buffer);
139 free.erase();
140 return success();
141}
142
143/// `enzymexla.memcpy` handles host memory as well as device memory, so space 0
144/// isn't special-cased into an `llvm.memcpy` here: the memory space rides along
145/// on the memref views and the actual copy is chosen once, further down the
146/// pipeline.
147static LogicalResult lowerMemcpy(llvm_ext::MemcpyOp memcpy) {
148 if (failed(checkEnzymeXLAAvailable(memcpy)))
149 return failure();
150
151 OpBuilder builder(memcpy);
152 Location loc = memcpy.getLoc();
153
154 // enzymexla.memcpy takes the byte count explicitly, so the (dynamic) extents
155 // of the two views don't have to be recoverable from the IR.
156 Value size = arith::IndexCastOp::create(builder, loc, builder.getIndexType(),
157 memcpy.getSize());
158 Value dst = pointerToMemref(builder, loc, memcpy.getDst());
159 Value src = pointerToMemref(builder, loc, memcpy.getSrc());
160 createByName(builder, loc, kEnzymeXLAMemcpy, ValueRange{dst, src, size},
161 TypeRange{});
162 memcpy.erase();
163 return success();
164}
165
166LogicalResult tryLoweringToAlloca(llvm_ext::AllocOp alloc,
167 uint64_t staticThreshold) {
168 // An alloca is host stack memory; promoting a device allocation to one would
169 // hand back a pointer the device can't touch.
170 if (alloc.getMemorySpace() != 0)
171 return failure();
172
173 llvm::APInt size;
174 if (!matchPattern(alloc.getSize(), m_ConstantInt(&size)))
175 return failure();
176
177 if (size.getZExtValue() > staticThreshold)
178 return failure();
179
180 Operation *free = nullptr;
181 for (auto user : alloc.getResult().getUsers()) {
182 auto oFree = dyn_cast<llvm_ext::FreeOp>(user);
183 if (!oFree)
184 continue;
185
186 if (free)
187 return failure(); // multiple frees
188
189 if (oFree->getBlock() != alloc->getBlock()) // free not in same block
190 return failure();
191
192 free = user;
193
194 if (!alloc->isBeforeInBlock(free))
195 return failure();
196 }
197
198 // No visible free means the pointer escapes -- e.g. it was stored into a
199 // buffer of clone handles and is freed via a load of that buffer. Stack
200 // memory would not survive the escape, and an alloc in a loop would make
201 // every slot the same address.
202 if (!free)
203 return failure();
204
205 OpBuilder builder(alloc);
206 auto alloca = LLVM::AllocaOp::create(builder, alloc.getLoc(),
207 alloc.getResult().getType(),
208 builder.getI8Type(), alloc.getSize());
209 alloc.getResult().replaceAllUsesWith(alloca.getResult());
210
211 LLVM::LifetimeStartOp::create(builder, alloc.getLoc(), alloca.getResult());
212
213 builder.setInsertionPoint(free);
214 LLVM::LifetimeEndOp::create(builder, alloc.getLoc(), alloca.getResult());
215
216 free->erase();
217 alloc->erase();
218
219 return success();
220}
221
222LogicalResult lowerAlloc(llvm_ext::AllocOp alloc, uint64_t staticThreshold) {
223 if (tryLoweringToAlloca(alloc, staticThreshold).succeeded())
224 return success();
225
226 if (alloc.getMemorySpace() != 0)
227 return lowerAllocDevice(alloc);
228
229 SymbolTable symtable(SymbolTable::getNearestSymbolTable(alloc));
230
231 auto mallocFn = symtable.lookup<LLVM::LLVMFuncOp>("malloc");
232 if (!mallocFn) {
233 Block *b = &symtable.getOp()->getRegion(0).front();
234 OpBuilder builder(b, b->begin());
235
236 auto fnType = mlir::LLVM::LLVMFunctionType::get(
237 LLVM::LLVMPointerType::get(alloc.getContext()), builder.getI64Type(),
238 /*isVarArg=*/false);
239
240 mallocFn =
241 LLVM::LLVMFuncOp::create(builder, alloc.getLoc(), "malloc", fnType);
242 }
243
244 OpBuilder builder(alloc);
245 auto mallocCall = LLVM::CallOp::create(builder, alloc.getLoc(), mallocFn,
246 alloc->getOperands());
247 alloc.getResult().replaceAllUsesWith(mallocCall.getResult());
248 alloc.erase();
249 return success();
250}
251
252LogicalResult lowerFree(llvm_ext::FreeOp free) {
253 if (free.getMemorySpace() != 0)
254 return lowerFreeDevice(free);
255
256 SymbolTable symtable(SymbolTable::getNearestSymbolTable(free));
257
258 auto freeFn = symtable.lookup<LLVM::LLVMFuncOp>("free");
259 if (!freeFn) {
260 Block *b = &symtable.getOp()->getRegion(0).front();
261 OpBuilder builder(b, b->begin());
262
263 auto fnType = mlir::LLVM::LLVMFunctionType::get(
264 LLVM::LLVMVoidType::get(free.getContext()),
265 LLVM::LLVMPointerType::get(free.getContext()),
266 /*isVarArg=*/false);
267
268 freeFn = LLVM::LLVMFuncOp::create(builder, free.getLoc(), "free", fnType);
269 }
270
271 OpBuilder builder(free);
272 LLVM::CallOp::create(builder, free.getLoc(), freeFn, free->getOperands());
273
274 free.erase();
275 return success();
276}
277
278/// `llvm_ext.ptr_size_hint` is the only record of how large a raw pointer's
279/// allocation is, and this pass drops it. Reverse-mode AD recovers the extent
280/// of a pointer-derived memref with `memref.dim` (see
281/// MemRefClonableTypeInterface::cloneValue) -- e.g. to size the checkpoint
282/// buffer for a cached grid -- but a `memref.dim` of a value that is really
283/// just a bare pointer has no lowering at all: the size is nowhere in the IR
284/// once the hint is gone. Resolve those dims from the hint while it is still
285/// around.
286static void resolveDimsFromHint(llvm_ext::PtrSizeHintOp psh,
287 DominanceInfo &dom) {
288 Value ptr = psh.getPtr();
289 Value byteSize = psh.getSize();
290
291 for (Operation *user : llvm::make_early_inc_range(ptr.getUsers())) {
292 // A side-effect-free view of the pointer as a rank-1 dynamic memref is a
293 // reinterpret cast, so the pointee's byte size fixes the memref's extent.
294 auto view = dyn_cast<ViewLikeOpInterface>(user);
295 if (!view || view.getViewSource() != ptr || !isMemoryEffectFree(user))
296 continue;
297 auto MT = dyn_cast<MemRefType>(user->getResult(0).getType());
298 if (!MT || MT.getRank() != 1 || !MT.isDynamicDim(0))
299 continue;
300 if (!MT.getElementType().isIntOrFloat())
301 continue;
302 unsigned bits = MT.getElementTypeBitWidth();
303 if (bits == 0 || bits % 8 != 0)
304 continue;
305
306 for (Operation *dimUser :
307 llvm::make_early_inc_range(user->getResult(0).getUsers())) {
308 auto dim = dyn_cast<memref::DimOp>(dimUser);
309 if (!dim || dim.getConstantIndex() != 0)
310 continue;
311 // The hint's size operand has to be available where the dim was.
312 if (!dom.properlyDominates(byteSize, dim))
313 continue;
314
315 OpBuilder builder(dim);
316 Value extent = arith::IndexCastOp::create(
317 builder, dim.getLoc(), builder.getIndexType(), byteSize);
318 if (unsigned bytes = bits / 8; bytes != 1) {
319 Value elemSize =
320 arith::ConstantIndexOp::create(builder, dim.getLoc(), bytes);
321 extent =
322 arith::DivUIOp::create(builder, dim.getLoc(), extent, elemSize);
323 }
324 dim.getResult().replaceAllUsesWith(extent);
325 dim.erase();
326 }
327 }
328}
329
330struct LowerLLVMExtPass
331 : public enzyme::impl::LowerLLVMExtPassBase<LowerLLVMExtPass> {
332 using LowerLLVMExtPassBase::LowerLLVMExtPassBase;
333
334 void runOnOperation() override {
335 Operation *op = getOperation();
336
337 DominanceInfo dom(op);
338 op->walk([&](llvm_ext::PtrSizeHintOp psh) {
339 resolveDimsFromHint(psh, dom);
340 psh.erase();
341 });
342
343 SmallVector<llvm_ext::AllocOp> allocs;
344 op->walk([&](llvm_ext::AllocOp alloc) { allocs.push_back(alloc); });
345
346 for (auto alloc : allocs)
347 if (failed(lowerAlloc(alloc, lowerToAllocaThreshold)))
348 return signalPassFailure();
349
350 // After the allocs, so a memcpy whose alloca-promoted destination went away
351 // still sees a valid pointer.
352 SmallVector<llvm_ext::MemcpyOp> memcpys;
353 op->walk([&](llvm_ext::MemcpyOp memcpy) { memcpys.push_back(memcpy); });
354
355 for (auto memcpy : memcpys)
356 if (failed(lowerMemcpy(memcpy)))
357 return signalPassFailure();
358
359 SmallVector<llvm_ext::FreeOp> frees;
360 op->walk([&](llvm_ext::FreeOp free) { frees.push_back(free); });
361
362 for (auto free : frees)
363 if (failed(lowerFree(free)))
364 return signalPassFailure();
365 }
366};
367
368} // end anonymous namespace