Enzyme main
Loading...
Searching...
No Matches
DataFlowActivityAnalysis.cpp
Go to the documentation of this file.
1//===- DataFlowActivityAnalysis.h - Implementation of Activity Analysis ---===//
2//
3// Enzyme Project
4//
5// Part of the Enzyme Project, under the Apache License v2.0 with LLVM
6// Exceptions. See https://llvm.org/LICENSE.txt for license information.
7// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
8//
9// If using this code in an academic setting, please cite the following:
10// @incollection{enzymeNeurips,
11// title = {Instead of Rewriting Foreign Code for Machine Learning,
12// Automatically Synthesize Fast Gradients},
13// author = {Moses, William S. and Churavy, Valentin},
14// booktitle = {Advances in Neural Information Processing Systems 33},
15// year = {2020},
16// note = {To appear in},
17// }
18//
19//===----------------------------------------------------------------------===//
20//
21// This file contains the implementation of Activity Analysis -- an AD-specific
22// analysis that deduces if a given instruction or value can impact the
23// calculation of a derivative. This file formulates activity analysis within
24// a dataflow framework.
25//
26//===----------------------------------------------------------------------===//
29#include "Dialect/Ops.h"
32
33#include "mlir/Analysis/DataFlow/ConstantPropagationAnalysis.h"
34#include "mlir/Analysis/DataFlow/DeadCodeAnalysis.h"
35
36// TODO: Don't depend on specific dialects
37#include "mlir/Dialect/Affine/IR/AffineOps.h"
38#include "mlir/Dialect/LLVMIR/LLVMDialect.h"
39#include "mlir/Dialect/Linalg/IR/Linalg.h"
40#include "mlir/Dialect/MemRef/IR/MemRef.h"
41
42#include "mlir/Analysis/AliasAnalysis/LocalAliasAnalysis.h"
43
44using namespace mlir;
45using namespace mlir::dataflow;
47
48raw_ostream &operator<<(raw_ostream &os, const enzyme::ValueActivity &val) {
49 val.print(os);
50 return os;
51}
52
53raw_ostream &operator<<(raw_ostream &os, const CallControlFlowAction &action) {
54 switch (action) {
55 case CallControlFlowAction::EnterCallee:
56 os << "EnterCallee";
57 break;
58 case CallControlFlowAction::ExitCallee:
59 os << "ExitCallee";
60 break;
61 case CallControlFlowAction::ExternalCallee:
62 os << "ExternalCallee";
63 break;
64 }
65 return os;
66}
67
68//===----------------------------------------------------------------------===//
69// ValueActivity
70//===----------------------------------------------------------------------===//
71raw_ostream &enzyme::ValueActivity::operator<<(raw_ostream &os) const {
72 print(os);
73 return os;
74}
75
76void enzyme::ValueActivity::print(llvm::raw_ostream &os) const {
77 switch (value) {
79 os << "ActiveVal";
80 break;
82 os << "Constant";
83 break;
85 os << "Unknown";
86 break;
87 }
88}
89
90//===----------------------------------------------------------------------===//
91// BackwardValueActivity
92//===----------------------------------------------------------------------===//
93ChangeResult
95 const auto *rhs =
96 reinterpret_cast<const enzyme::BackwardValueActivity *>(&other);
97 return meet(rhs->getValue());
98}
99
101 auto met = enzyme::ValueActivity::merge(getValue(), other);
102 if (getValue() == met) {
103 return ChangeResult::NoChange;
104 }
105
106 value = met;
107 return ChangeResult::Change;
108}
109
110void enzyme::BackwardValueActivity::print(raw_ostream &os) const {
111 value.print(os);
112}
113
114//===----------------------------------------------------------------------===//
115// SparseForwardActivityAnalysis
116//===----------------------------------------------------------------------===//
117
118/// In general, we don't know anything about entry operands.
121 // llvm::errs() << "sparse forward setting to entry state\n";
122 propagateIfChanged(lattice, lattice->join(enzyme::ValueActivity()));
123}
124
126 Operation *op, ArrayRef<const enzyme::ForwardValueActivity *> operands,
127 ArrayRef<enzyme::ForwardValueActivity *> results) {
128 if (op->hasTrait<OpTrait::ConstantLike>())
129 return success();
130
131 // Bail out if this op affects memory.
132 if (!isPure(op))
133 return success();
134
135 transfer(op, operands, results);
136
137 return success();
138}
139
141 CallOpInterface call,
142 ArrayRef<const enzyme::ForwardValueActivity *> operands,
143 ArrayRef<enzyme::ForwardValueActivity *> results) {
144 transfer(call, operands, results);
145}
146
148 Operation *op, ArrayRef<const enzyme::ForwardValueActivity *> operands,
149 ArrayRef<enzyme::ForwardValueActivity *> results) {
150 // For value-based AA, assume any active argument leads to an active
151 // result.
152 enzyme::ValueActivity joinedResult;
153 for (const enzyme::ForwardValueActivity *operand : operands)
154 joinedResult =
155 enzyme::ValueActivity::merge(joinedResult, operand->getValue());
156
157 // Only mark results as active data if the type can carry perturbations and
158 // has value semantics
159 for (enzyme::ForwardValueActivity *result : results) {
160 if (joinedResult.isActiveVal())
161 propagateIfChanged(
162 result, result->join(
163 isa<FloatType, ComplexType>(result->getAnchor().getType())
164 ? joinedResult
166 else
167 propagateIfChanged(result, result->join(joinedResult));
168 }
169}
170
171//===----------------------------------------------------------------------===//
172// SparseBackwardActivityAnalysis
173//===----------------------------------------------------------------------===//
174
176 Operation *op, ArrayRef<enzyme::BackwardValueActivity *> operands,
177 ArrayRef<const enzyme::BackwardValueActivity *> results) {
178 // Propagate all operands to all results
179 for (auto operand : operands)
180 for (auto result : results)
181 meet(operand, *result);
182}
183
185 Operation *op, ArrayRef<enzyme::BackwardValueActivity *> operands,
186 ArrayRef<const enzyme::BackwardValueActivity *> results) {
187 // Bail out if the op propagates memory
188 if (!isPure(op)) {
189 return success();
190 }
191
192 transfer(op, operands, results);
193 return success();
194}
195
197 CallOpInterface call, ArrayRef<enzyme::BackwardValueActivity *> operands,
198 ArrayRef<const enzyme::BackwardValueActivity *> results) {
199 transfer(call, operands, results);
200}
201
202// When applying a transfer function to a store from memory, we need to know
203// what value is being stored.
204std::optional<Value> getStored(Operation *op) {
205 if (auto storeOp = dyn_cast<LLVM::StoreOp>(op)) {
206 return storeOp.getValue();
207 } else if (auto memsetOp = dyn_cast<LLVM::MemsetOp>(op)) {
208 return memsetOp.getVal();
209 } else if (auto storeOp = dyn_cast<memref::StoreOp>(op)) {
210 return storeOp.getValue();
211 } else if (auto storeOp = dyn_cast<affine::AffineStoreOp>(op)) {
212 return storeOp.getValue();
213 } else if (auto pushOp = dyn_cast<enzyme::PushOp>(op)) {
214 return pushOp.getValue();
215 }
216 return std::nullopt;
217}
218
219// TODO consider making this an interface ourselves
220std::optional<Value> getCopySource(Operation *op) {
221 if (isa<LLVM::MemcpyOp, LLVM::MemcpyInlineOp, LLVM::MemmoveOp>(op)) {
222 return op->getOperand(1);
223 }
224 return std::nullopt;
225}
226
227/// The dense analyses operate using a pointer's "canonical allocation", the
228/// Value corresponding to its allocation.
229/// The callback may receive null allocation when the class alias set is
230/// unknown.
231/// If the classes are undefined, the callback will not be called at all.
232void forEachAliasedAlloc(const AliasClassLattice *ptrAliasClass,
233 function_ref<void(DistinctAttr)> forEachFn) {
234 (void)ptrAliasClass->getAliasClassesObject().foreachElement(
235 [&](DistinctAttr alloc, enzyme::AliasClassSet::State state) {
236 if (state != enzyme::AliasClassSet::State::Undefined)
237 forEachFn(alloc);
238 return ChangeResult::NoChange;
239 });
240}
241
242void traverseCallGraph(FunctionOpInterface root,
243 SymbolTableCollection *symbolTable,
244 function_ref<void(FunctionOpInterface)> processFunc) {
245 std::deque<FunctionOpInterface> frontier{root};
246 DenseSet<FunctionOpInterface> visited{root};
247
248 while (!frontier.empty()) {
249 FunctionOpInterface curr = frontier.front();
250 frontier.pop_front();
251 processFunc(curr);
252
253 curr.walk([&](CallOpInterface call) {
254 auto neighbor = dyn_cast_if_present<FunctionOpInterface>(
255 call.resolveCallableInTable(symbolTable));
256 if (neighbor && !visited.contains(neighbor)) {
257 frontier.push_back(neighbor);
258 visited.insert(neighbor);
259 }
260 });
261 }
262}
263
264//===----------------------------------------------------------------------===//
265// MemoryActivityState
266//===----------------------------------------------------------------------===//
268 if (!activeIn && !activeOut)
269 return ChangeResult::NoChange;
270 activeIn = false;
271 activeOut = false;
272 return ChangeResult::Change;
273}
274
275ChangeResult
277 if (*this == other) {
278 return ChangeResult::NoChange;
279 }
280
281 activeIn |= other.activeIn;
282 activeOut |= other.activeOut;
283 return ChangeResult::Change;
284}
285
286//===----------------------------------------------------------------------===//
287// MemoryActivity
288//===----------------------------------------------------------------------===//
289
291 if (activityStates.empty())
292 return otherMemoryActivity.reset();
293 activityStates.clear();
294 return otherMemoryActivity.reset();
295}
296
297bool enzyme::MemoryActivity::hasActiveData(DistinctAttr aliasClass) const {
298 if (!aliasClass)
299 return otherMemoryActivity.activeIn;
300 auto it = activityStates.find(aliasClass);
301 if (it != activityStates.end())
302 return it->getSecond().activeIn;
303 return otherMemoryActivity.activeIn;
304}
305
306bool enzyme::MemoryActivity::activeDataFlowsOut(DistinctAttr aliasClass) const {
307 if (!aliasClass)
308 return otherMemoryActivity.activeOut;
309
310 auto it = activityStates.find(aliasClass);
311 if (it != activityStates.end())
312 return it->getSecond().activeOut;
313 return otherMemoryActivity.activeOut;
314}
315
316/// Set the internal activity state. Accepts null attribute to indicate "other
317/// classes".
318ChangeResult enzyme::MemoryActivity::setActiveIn(DistinctAttr aliasClass) {
319 if (!aliasClass)
320 return setActiveIn();
321
322 auto &state = activityStates[aliasClass];
323 ChangeResult result =
324 state.activeIn ? ChangeResult::NoChange : ChangeResult::Change;
325 state.activeIn = true;
326 return result;
327}
329 if (otherMemoryActivity.activeIn && activityStates.empty())
330 return ChangeResult::NoChange;
331 otherMemoryActivity.activeIn = true;
332 activityStates.clear();
333 return ChangeResult::Change;
334}
335ChangeResult enzyme::MemoryActivity::setActiveOut(DistinctAttr aliasClass) {
336 if (!aliasClass)
337 return setActiveOut();
338
339 auto &state = activityStates[aliasClass];
340 ChangeResult result =
341 state.activeOut ? ChangeResult::NoChange : ChangeResult::Change;
342 state.activeOut = true;
343 return result;
344}
346 if (otherMemoryActivity.activeOut && activityStates.empty())
347 return ChangeResult::NoChange;
348 otherMemoryActivity.activeOut = true;
349 activityStates.clear();
350 return ChangeResult::Change;
351}
352
353void enzyme::MemoryActivity::print(raw_ostream &os) const {
354 if (activityStates.empty()) {
355 os << "<memory activity state was empty>"
356 << "\n";
357 }
358 for (const auto &[value, state] : activityStates) {
359 os << value << ": in " << state.activeIn << " out " << state.activeOut
360 << "\n";
361 }
362 os << "other classes: in " << otherMemoryActivity.activeIn << " out "
363 << otherMemoryActivity.activeOut << "\n";
364}
365
366raw_ostream &enzyme::MemoryActivity::operator<<(raw_ostream &os) const {
367 print(os);
368 return os;
369}
370
371ChangeResult
373
374 const auto &rhs = static_cast<const MemoryActivity &>(lattice);
375 ChangeResult result = ChangeResult::NoChange;
376 DenseSet<DistinctAttr> known;
377 auto lhsRange = llvm::make_first_range(activityStates);
378 auto rhsRange = llvm::make_first_range(rhs.activityStates);
379 known.insert(lhsRange.begin(), lhsRange.end());
380 known.insert(rhsRange.begin(), rhsRange.end());
381
382 MemoryActivityState updatedOther(otherMemoryActivity);
383 result |= updatedOther.merge(rhs.otherMemoryActivity);
384 DenseMap<DistinctAttr, MemoryActivityState> updated;
385 for (DistinctAttr d : known) {
386 auto lhsIt = activityStates.find(d);
387 auto rhsIt = rhs.activityStates.find(d);
388 bool isKnownInLHS = lhsIt != activityStates.end();
389 bool isKnownInRHS = rhsIt != rhs.activityStates.end();
390 const MemoryActivityState *lhsActivity =
391 isKnownInLHS ? &lhsIt->getSecond() : &otherMemoryActivity;
392 const MemoryActivityState *rhsActivity =
393 isKnownInRHS ? &rhsIt->getSecond() : &rhs.otherMemoryActivity;
394 MemoryActivityState updatedActivity(*lhsActivity);
395 (void)updatedActivity.merge(*rhsActivity);
396 if ((lhsIt != activityStates.end() &&
397 updatedActivity != lhsIt->getSecond()) ||
398 (lhsIt == activityStates.end() &&
399 updatedActivity != otherMemoryActivity)) {
400 result |= ChangeResult::Change;
401 }
402 if (updatedActivity != updatedOther)
403 updated.try_emplace(d, updatedActivity);
404 }
405
406 std::swap(updated, activityStates);
407 return otherMemoryActivity.merge(rhs.otherMemoryActivity) | result;
408}
409
410//===----------------------------------------------------------------------===//
411// DenseForwardActivityAnalysis
412//===----------------------------------------------------------------------===//
413
415 Operation *op, const ForwardMemoryActivity &before,
416 ForwardMemoryActivity *after) {
417 join(after, before);
418 ChangeResult result = ChangeResult::NoChange;
419
420 // TODO If we know this is inactive by definition
421 // if (auto ifaceOp = dyn_cast<enzyme::ActivityOpInterface>(op)) {
422 // if (ifaceOp.isInactive()) {
423 // propagateIfChanged(after, result);
424 // return;
425 // }
426 // }
427
428 auto memory = dyn_cast<MemoryEffectOpInterface>(op);
429 // If we can't reason about the memory effects, then conservatively assume
430 // we can't deduce anything about activity via side-effects.
431 if (!memory)
432 return success();
433
434 SmallVector<MemoryEffects::EffectInstance> effects;
435 memory.getEffects(effects);
436
437 for (const auto &effect : effects) {
438 Value value = effect.getValue();
439
440 // If we see an effect on anything other than a value, assume we can't
441 // deduce anything about the activity.
442 if (!value)
443 return success();
444
445 // In forward-flow, a value is active if loaded from a memory resource
446 // that has previously been actively stored to.
447 if (isa<MemoryEffects::Read>(effect.getEffect())) {
448 auto *ptrAliasClass =
449 getOrCreateFor<AliasClassLattice>(getProgramPointAfter(op), value);
450 forEachAliasedAlloc(ptrAliasClass, [&](DistinctAttr alloc) {
451 if (before.hasActiveData(alloc)) {
452 for (OpResult opResult : op->getResults()) {
453 // Mark the result as (forward) active
454 // TODO: We might need type analysis here
455 // Structs and tensors also have value semantics
456 if (isa<FloatType, ComplexType>(opResult.getType())) {
457 auto *valueState =
458 getOrCreate<enzyme::ForwardValueActivity>(opResult);
459 propagateIfChanged(
460 valueState,
461 valueState->join(enzyme::ValueActivity::getActiveVal()));
462 }
463 }
464
465 if (auto linalgOp = dyn_cast<linalg::LinalgOp>(op)) {
466 // propagate from input to block argument
467 for (OpOperand *inputOperand : linalgOp.getDpsInputOperands()) {
468 if (inputOperand->get() == value) {
469 auto *valueState = getOrCreate<enzyme::ForwardValueActivity>(
470 linalgOp.getMatchingBlockArgument(inputOperand));
471 propagateIfChanged(
472 valueState,
473 valueState->join(enzyme::ValueActivity::getActiveVal()));
474 }
475 }
476 }
477 }
478 });
479 }
480
481 if (isa<MemoryEffects::Write>(effect.getEffect())) {
482 std::optional<Value> stored = getStored(op);
483 if (stored.has_value()) {
484 auto *valueState = getOrCreateFor<enzyme::ForwardValueActivity>(
485 getProgramPointAfter(op), *stored);
486 if (valueState->getValue().isActiveVal()) {
487 auto *ptrAliasClass = getOrCreateFor<AliasClassLattice>(
488 getProgramPointAfter(op), value);
489 forEachAliasedAlloc(ptrAliasClass, [&](DistinctAttr alloc) {
490 // Mark the pointer as having been actively stored into
491 result |= after->setActiveIn(alloc);
492 });
493 }
494 } else if (auto copySource = getCopySource(op)) {
495 auto *srcAliasClass = getOrCreateFor<AliasClassLattice>(
496 getProgramPointAfter(op), *copySource);
497 forEachAliasedAlloc(srcAliasClass, [&](DistinctAttr srcAlloc) {
498 if (before.hasActiveData(srcAlloc)) {
499 auto *destAliasClass = getOrCreateFor<AliasClassLattice>(
500 getProgramPointAfter(op), value);
501 forEachAliasedAlloc(destAliasClass, [&](DistinctAttr destAlloc) {
502 result |= after->setActiveIn(destAlloc);
503 });
504 }
505 });
506 } else if (auto linalgOp = dyn_cast<linalg::LinalgOp>(op)) {
507 // linalg.yield stores to the corresponding value.
508 for (OpOperand &dpsInit : linalgOp.getDpsInitsMutable()) {
509 if (dpsInit.get() == value) {
510 int64_t resultIndex =
511 dpsInit.getOperandNumber() - linalgOp.getNumDpsInputs();
512 Value yieldOperand =
513 linalgOp.getBlock()->getTerminator()->getOperand(resultIndex);
514 auto *valueState = getOrCreateFor<enzyme::ForwardValueActivity>(
515 getProgramPointAfter(op), yieldOperand);
516 if (valueState->getValue().isActiveVal()) {
517 auto *ptrAliasClass = getOrCreateFor<AliasClassLattice>(
518 getProgramPointAfter(op), value);
519 forEachAliasedAlloc(ptrAliasClass, [&](DistinctAttr alloc) {
520 result |= after->setActiveIn(alloc);
521 });
522 }
523 }
524 }
525 }
526 }
527 }
528 propagateIfChanged(after, result);
529 return success();
530}
531
533 ForwardMemoryActivity *lattice) {
534 if (auto pp = dyn_cast_if_present<ProgramPoint *>(lattice->getAnchor()))
535 if (Block *block = pp->getBlock();
536 block && block == entryBlock && pp->isBlockStart()) {
537 for (const auto &[arg, activity] :
538 llvm::zip(block->getArguments(), argumentActivity)) {
539 if (activity != enzyme::Activity::enzyme_dup &&
540 activity != enzyme::Activity::enzyme_dupnoneed)
541 continue;
542 auto *argAliasClasses = getOrCreateFor<AliasClassLattice>(
543 getProgramPointBefore(block), arg);
544 ChangeResult changed =
545 argAliasClasses->getAliasClassesObject().foreachElement(
546 [lattice](DistinctAttr argAliasClass,
548 if (state == enzyme::AliasClassSet::State::Undefined)
549 return ChangeResult::NoChange;
550 return lattice->setActiveIn(argAliasClass);
551 });
552 propagateIfChanged(lattice, changed);
553 }
554 }
555}
556
557//===----------------------------------------------------------------------===//
558// DenseBackwardActivityAnalysis
559//===----------------------------------------------------------------------===//
560
562 Operation *op, const BackwardMemoryActivity &after,
563 BackwardMemoryActivity *before) {
564
565 // TODO: If we know this is inactive by definition
566 // if (auto ifaceOp = dyn_cast<enzyme::ActivityOpInterface>(op)) {
567 // if (ifaceOp.isInactive()) {
568 // return;
569 // }
570 // }
571
572 // Initialize the return activity of arguments.
573 if (mlir::enzyme::isFunctionReturnLike(op) && op->getParentOp() == parentOp) {
574 for (const auto &[arg, argActivity] : llvm::zip(
575 parentOp->getRegions().front().getArguments(), argumentActivity)) {
576 if (argActivity != enzyme::Activity::enzyme_dup &&
577 argActivity != enzyme::Activity::enzyme_dupnoneed) {
578 continue;
579 }
580 auto *argAliasClasses =
581 getOrCreateFor<AliasClassLattice>(getProgramPointBefore(op), arg);
582 ChangeResult changed =
583 argAliasClasses->getAliasClassesObject().foreachElement(
584 [before](DistinctAttr argAliasClass,
586 if (state == enzyme::AliasClassSet::State::Undefined)
587 return ChangeResult::NoChange;
588 return before->setActiveOut(argAliasClass);
589 });
590 propagateIfChanged(before, changed);
591 }
592
593 assert(op->getNumOperands() == returnActivity.size() &&
594 "the number of returned arguments must match the number of return "
595 "activity entries");
596
597 // Initialize the return activity of the operands
598 for (const auto &[operand, operandActivity] :
599 llvm::zip(op->getOperands(), returnActivity)) {
600 if (operandActivity != enzyme::Activity::enzyme_dup &&
601 operandActivity != enzyme::Activity::enzyme_dupnoneed)
602 continue;
603
604 auto *retAliasClasses =
605 getOrCreateFor<AliasClassLattice>(getProgramPointBefore(op), operand);
606 ChangeResult changed =
607 retAliasClasses->getAliasClassesObject().foreachElement(
608 [before](DistinctAttr retAliasClass,
610 if (state == enzyme::AliasClassSet::State::Undefined)
611 return ChangeResult::NoChange;
612 return before->setActiveOut(retAliasClass);
613 });
614 propagateIfChanged(before, changed);
615 }
616 }
617
618 meet(before, after);
619 ChangeResult result = ChangeResult::NoChange;
620 auto memory = dyn_cast<MemoryEffectOpInterface>(op);
621 // If we can't reason about the memory effects, then conservatively assume
622 // we can't deduce anything about activity via side-effects.
623 if (!memory)
624 return success();
625
626 SmallVector<MemoryEffects::EffectInstance> effects;
627 memory.getEffects(effects);
628
629 for (const auto &effect : effects) {
630 Value value = effect.getValue();
631
632 // If we see an effect on anything other than a value, assume we can't
633 // deduce anything about the activity.
634 if (!value)
635 return success();
636
637 // In backward-flow, a value is active if stored into a memory resource
638 // that has subsequently been actively loaded from.
639 if (isa<MemoryEffects::Read>(effect.getEffect())) {
640 for (Value opResult : op->getResults()) {
641 auto *valueState = getOrCreateFor<enzyme::BackwardValueActivity>(
642 getProgramPointBefore(op), opResult);
643 if (valueState->getValue().isActiveVal()) {
644 auto *ptrAliasClass = getOrCreateFor<AliasClassLattice>(
645 getProgramPointBefore(op), value);
646 forEachAliasedAlloc(ptrAliasClass, [&](DistinctAttr alloc) {
647 result |= before->setActiveOut(alloc);
648 });
649 }
650 }
651 }
652 if (isa<MemoryEffects::Write>(effect.getEffect())) {
653 auto *ptrAliasClass =
654 getOrCreateFor<AliasClassLattice>(getProgramPointBefore(op), value);
655 std::optional<Value> stored = getStored(op);
656 std::optional<Value> copySource = getCopySource(op);
657 forEachAliasedAlloc(ptrAliasClass, [&](DistinctAttr alloc) {
658 if (stored.has_value() && after.activeDataFlowsOut(alloc)) {
659 if (isa<FloatType, ComplexType>(stored->getType())) {
660 auto *valueState =
661 getOrCreate<enzyme::BackwardValueActivity>(*stored);
662 propagateIfChanged(
663 valueState,
664 valueState->meet(enzyme::ValueActivity::getActiveVal()));
665 }
666 } else if (copySource.has_value() && after.activeDataFlowsOut(alloc)) {
667 auto *srcAliasClass = getOrCreateFor<AliasClassLattice>(
668 getProgramPointBefore(op), *copySource);
669 forEachAliasedAlloc(srcAliasClass, [&](DistinctAttr srcAlloc) {
670 result |= before->setActiveOut(srcAlloc);
671 });
672 } else if (auto linalgOp = dyn_cast<linalg::LinalgOp>(op)) {
673 if (after.activeDataFlowsOut(alloc)) {
674 for (OpOperand &dpsInit : linalgOp.getDpsInitsMutable()) {
675 if (dpsInit.get() == value) {
676 int64_t resultIndex =
677 dpsInit.getOperandNumber() - linalgOp.getNumDpsInputs();
678 Value yieldOperand =
679 linalgOp.getBlock()->getTerminator()->getOperand(
680 resultIndex);
681 auto *valueState =
682 getOrCreate<enzyme::BackwardValueActivity>(yieldOperand);
683 propagateIfChanged(
684 valueState,
685 valueState->meet(enzyme::ValueActivity::getActiveVal()));
686 }
687 }
688 }
689 }
690 });
691 }
692 }
693 propagateIfChanged(before, result);
694 return success();
695}
696
697void printActivityAnalysisResults(DataFlowSolver &solver,
698 FunctionOpInterface callee,
699 const SmallPtrSet<Operation *, 2> &returnOps,
700 SymbolTableCollection *symbolTable,
701 bool verbose, bool annotate) {
702 auto isActiveData = [&](Value value) {
703 auto fva = solver.lookupState<enzyme::ForwardValueActivity>(value);
704 auto bva = solver.lookupState<enzyme::BackwardValueActivity>(value);
705 bool forwardActive = fva && fva->getValue().isActiveVal();
706 bool backwardActive = bva && bva->getValue().isActiveVal();
707 return forwardActive && backwardActive;
708 };
709
710 auto isConstantValue = [&](Value value) {
711 // TODO: integers/vectors that might be pointers
712 if (isa<LLVM::LLVMPointerType, MemRefType>(value.getType())) {
713 assert(returnOps.size() == 1);
714 auto *fma = solver.lookupState<enzyme::ForwardMemoryActivity>(
715 solver.getProgramPointAfter(*returnOps.begin()));
716 auto *bma = solver.lookupState<enzyme::BackwardMemoryActivity>(
717 solver.getProgramPointBefore(
718 &callee.getFunctionBody().front().front()));
719
720 const enzyme::PointsToSets *pointsToSets =
721 solver.lookupState<enzyme::PointsToSets>(
722 solver.getProgramPointAfter(*returnOps.begin()));
723 auto *aliasClassLattice = solver.lookupState<AliasClassLattice>(value);
724 // Traverse the points-to sets in a simple BFS
725 std::deque<DistinctAttr> frontier;
726 DenseSet<DistinctAttr> visited;
727 auto scheduleVisit = [&](const enzyme::AliasClassSet &aliasClasses) {
728 (void)aliasClasses.foreachElement(
729 [&](DistinctAttr neighbor, enzyme::AliasClassSet::State state) {
730 assert(neighbor &&
731 "unhandled undefined/unknown case before visit");
732 if (!visited.contains(neighbor)) {
733 visited.insert(neighbor);
734 frontier.push_back(neighbor);
735 }
736 return ChangeResult::NoChange;
737 });
738 };
739
740 // If this triggers, investigate why the alias classes weren't computed.
741 // If they weren't computed legitimately, treat the value as
742 // conservatively non-constant or change the return type to be tri-state.
743 assert(!aliasClassLattice->isUndefined() &&
744 "didn't compute alias classes");
745
746 if (aliasClassLattice->isUnknown()) {
747 // Pointers of unknown class may point to active data.
748 // TODO: is this overly conservative? Should we rather check
749 // if listed classes may point to non-constants?
750 return false;
751 } else {
752 scheduleVisit(aliasClassLattice->getAliasClassesObject());
753 }
754 while (!frontier.empty()) {
755 DistinctAttr aliasClass = frontier.front();
756 frontier.pop_front();
757
758 // It's an active pointer if active data flows in from the forward
759 // direction and out from the backward direction.
760 if (fma->hasActiveData(aliasClass) &&
761 bma->activeDataFlowsOut(aliasClass))
762 return false;
763
764 // If this triggers, investigate why points-to sets couldn't be
765 // computed. Treat conservatively as "unknown" if necessary.
766 assert(!pointsToSets->getPointsTo(aliasClass).isUndefined() &&
767 "couldn't compute points-to sets");
768
769 // Pointers to unknown classes may (transitively) point to active data.
770 if (pointsToSets->getPointsTo(aliasClass).isUnknown())
771 return false;
772
773 scheduleVisit(pointsToSets->getPointsTo(aliasClass));
774 }
775 // Otherwise, it's constant
776 return true;
777 }
778
779 return !isActiveData(value);
780 };
781
782 std::function<bool(Operation *)> isConstantInstruction = [&](Operation *op) {
783 if (isPure(op)) {
784 // If an operation doesn't have side effects, the only way it can
785 // propagate active data is through its results.
786 return llvm::none_of(op->getResults(), isActiveData);
787 }
788 // We need a special case because stores of active pointers don't fit the
789 // definition but are active instructions
790 if (auto storeOp = dyn_cast<LLVM::StoreOp>(op)) {
791 if (!isConstantValue(storeOp.getValue())) {
792 return false;
793 }
794 } else if (auto callOp = dyn_cast<CallOpInterface>(op)) {
795 // TODO: Should traverse bottom-up for performance (or cache
796 // intermediate results)
797 auto callable = cast<CallableOpInterface>(callOp.resolveCallable());
798 if (callable.getCallableRegion()) {
799 // If any of the instructions in the body are active instructions, the
800 // function is active.
801 WalkResult result = callable->walk([&](Operation *op) {
802 if (!isConstantInstruction(op)) {
803 return WalkResult::interrupt();
804 }
805 return WalkResult::advance();
806 });
807 return !result.wasInterrupted();
808 } else {
809 // fall back to seeing if any operand or result is active data
810 }
811 }
812 return llvm::none_of(op->getOperands(), isActiveData) &&
813 llvm::none_of(op->getResults(), isActiveData);
814 };
815
816 llvm::errs() << FlatSymbolRefAttr::get(callee) << ":\n";
817 for (BlockArgument arg : callee.getArguments()) {
818 if (Attribute tagAttr =
819 callee.getArgAttr(arg.getArgNumber(), "enzyme.tag")) {
820 llvm::errs() << " " << tagAttr << ": "
821 << (isConstantValue(arg) ? "Constant" : "Active") << "\n";
822 }
823 }
824
825 if (annotate) {
826 MLIRContext *ctx = callee.getContext();
827 traverseCallGraph(callee, symbolTable, [&](FunctionOpInterface func) {
828 func.walk([&](Operation *op) {
829 if (op == func) {
830 SmallVector<bool> argICVs(func.getNumArguments());
831 llvm::transform(func.getArguments(), argICVs.begin(),
832 isConstantValue);
833 func->setAttr("enzyme.icv", DenseBoolArrayAttr::get(ctx, argICVs));
834 return;
835 }
836
837 op->setAttr("enzyme.ici",
838 BoolAttr::get(ctx, isConstantInstruction(op)));
839
840 bool icv;
841 if (op->getNumResults() == 0) {
842 icv = true;
843 } else if (op->getNumResults() == 1) {
844 icv = isConstantValue(op->getResult(0));
845 } else {
846 op->emitWarning(
847 "annotating icv for op that produces multiple results");
848 icv = false;
849 }
850 op->setAttr("enzyme.icv", BoolAttr::get(ctx, icv));
851 });
852 });
853 }
854 callee.walk([&](Operation *op) {
855 if (op->hasAttr("tag")) {
856 llvm::errs() << " " << op->getAttr("tag") << ": ";
857 for (OpResult opResult : op->getResults()) {
858 llvm::errs() << (isConstantValue(opResult) ? "Constant" : "Active")
859 << "\n";
860 }
861 }
862 if (verbose) {
863 // Annotate each op's results with its value activity states
864 for (OpResult result : op->getResults()) {
865 auto forwardValueActivity =
866 solver.lookupState<enzyme::ForwardValueActivity>(result);
867 if (forwardValueActivity) {
868 std::string dest, key{"fva"};
869 llvm::raw_string_ostream os(dest);
870 if (op->getNumResults() != 1)
871 key += result.getResultNumber();
872 forwardValueActivity->getValue().print(os);
873 op->setAttr(key, StringAttr::get(op->getContext(), dest));
874 }
875
876 auto backwardValueActivity =
877 solver.lookupState<enzyme::BackwardValueActivity>(result);
878 if (backwardValueActivity) {
879 std::string dest, key{"bva"};
880 llvm::raw_string_ostream os(dest);
881 if (op->getNumResults() != 1)
882 key += result.getResultNumber();
883 backwardValueActivity->getValue().print(os);
884 op->setAttr(key, StringAttr::get(op->getContext(), dest));
885 }
886 }
887 }
888 });
889
890 if (verbose) {
891 // Annotate function attributes
892 for (BlockArgument arg : callee.getArguments()) {
893 auto backwardValueActivity =
894 solver.lookupState<enzyme::BackwardValueActivity>(arg);
895 if (backwardValueActivity) {
896 std::string dest;
897 llvm::raw_string_ostream os(dest);
898 backwardValueActivity->getValue().print(os);
899 callee.setArgAttr(arg.getArgNumber(), "enzyme.bva",
900 StringAttr::get(callee->getContext(), dest));
901 }
902 }
903
904 for (Operation *returnOp : returnOps) {
905 auto *state = solver.lookupState<enzyme::ForwardMemoryActivity>(
906 solver.getProgramPointAfter(returnOp));
907 if (state)
908 llvm::errs() << "forward end state:\n" << *state << "\n";
909 else
910 llvm::errs() << "state was null\n";
911 }
912
913 auto startState = solver.lookupState<enzyme::BackwardMemoryActivity>(
914 solver.getProgramPointAfter(&callee.getFunctionBody().front().front()));
915 if (startState)
916 llvm::errs() << "backwards end state:\n" << *startState << "\n";
917 else
918 llvm::errs() << "backwards end state was null\n";
919 }
920}
921
923 FunctionOpInterface callee, ArrayRef<enzyme::Activity> argumentActivity,
924 ArrayRef<enzyme::Activity> returnActivity, bool print, bool verbose,
925 bool annotate) {
926 SymbolTableCollection symbolTable;
927 DataFlowSolver solver;
928
929 solver.load<enzyme::PointsToPointerAnalysis>();
930 solver.load<enzyme::AliasAnalysis>(callee.getContext());
931 solver.load<SparseForwardActivityAnalysis>();
932 solver.load<DenseForwardActivityAnalysis>(&callee.getFunctionBody().front(),
933 argumentActivity);
934 solver.load<SparseBackwardActivityAnalysis>(symbolTable);
935 solver.load<DenseBackwardActivityAnalysis>(symbolTable, callee,
936 argumentActivity, returnActivity);
937
938 // Required for the dataflow framework to traverse region-based control flow
939 solver.load<DeadCodeAnalysis>();
940 solver.load<SparseConstantPropagation>();
941
942 // Initialize the argument states based on the given activity annotations.
943 for (const auto &[arg, activity] :
944 llvm::zip(callee.getArguments(), argumentActivity)) {
945 // enzyme_dup, dupnoneed are initialized within the dense forward/backward
946 // analyses, enzyme_const is the default.
947 if (activity == enzyme::Activity::enzyme_active) {
948 auto *argLattice =
949 solver.getOrCreateState<enzyme::ForwardValueActivity>(arg);
950 (void)argLattice->join(ValueActivity::getActiveVal());
951 }
952 }
953
954 // Detect function returns as direct children of the FunctionOpInterface
955 // that have the ReturnLike trait.
956 SmallPtrSet<Operation *, 2> returnOps;
957 for (Operation &op : callee.getFunctionBody().getOps()) {
959 returnOps.insert(&op);
960 for (Value operand : op.getOperands()) {
961 auto *returnLattice =
962 solver.getOrCreateState<enzyme::BackwardValueActivity>(operand);
963 // Very basic type inference of the type
964 if (isa<FloatType, ComplexType>(operand.getType())) {
965 (void)returnLattice->meet(ValueActivity::getActiveVal());
966 }
967 }
968 }
969 }
970
971 if (failed(solver.initializeAndRun(callee->getParentOfType<ModuleOp>()))) {
972 assert(false && "dataflow analysis failed\n");
973 }
974
975 if (print) {
976 printActivityAnalysisResults(solver, callee, returnOps, &symbolTable,
977 verbose, annotate);
978 }
979}
std::optional< Value > getCopySource(Operation *op)
void printActivityAnalysisResults(DataFlowSolver &solver, FunctionOpInterface callee, const SmallPtrSet< Operation *, 2 > &returnOps, SymbolTableCollection *symbolTable, bool verbose, bool annotate)
void forEachAliasedAlloc(const AliasClassLattice *ptrAliasClass, function_ref< void(DistinctAttr)> forEachFn)
The dense analyses operate using a pointer's "canonical allocation", the Value corresponding to its a...
raw_ostream & operator<<(raw_ostream &os, const enzyme::ValueActivity &val)
void traverseCallGraph(FunctionOpInterface root, SymbolTableCollection *symbolTable, function_ref< void(FunctionOpInterface)> processFunc)
std::optional< Value > getStored(Operation *op)
This analysis implements interprocedural alias analysis.
ChangeResult meet(const AbstractSparseLattice &other) override
void print(raw_ostream &os) const override
LogicalResult visitOperation(Operation *op, const BackwardMemoryActivity &after, BackwardMemoryActivity *before) override
LogicalResult visitOperation(Operation *op, const ForwardMemoryActivity &before, ForwardMemoryActivity *after) override
void setToEntryState(ForwardMemoryActivity *lattice) override
Initialize the entry block with the supplied argument activities.
void print(raw_ostream &os) const override
bool activeDataFlowsOut(DistinctAttr aliasClass) const
ChangeResult reset()
Clear all modifications.
ChangeResult merge(const AbstractDenseLattice &lattice)
ChangeResult setActiveIn(DistinctAttr aliasClass)
Set the internal activity state.
ChangeResult setActiveOut(DistinctAttr aliasClass)
raw_ostream & operator<<(raw_ostream &os) const
bool hasActiveData(DistinctAttr aliasClass) const
void visitExternalCall(CallOpInterface call, ArrayRef< enzyme::BackwardValueActivity * > operands, ArrayRef< const enzyme::BackwardValueActivity * > results) override
void transfer(Operation *op, ArrayRef< enzyme::BackwardValueActivity * > operands, ArrayRef< const enzyme::BackwardValueActivity * > results)
LogicalResult visitOperation(Operation *op, ArrayRef< enzyme::BackwardValueActivity * > operands, ArrayRef< const enzyme::BackwardValueActivity * > results) override
Sparse activity analysis reasons about activity by traversing forward down the def-use chains startin...
void transfer(Operation *op, ArrayRef< const enzyme::ForwardValueActivity * > operands, ArrayRef< enzyme::ForwardValueActivity * > results)
void setToEntryState(enzyme::ForwardValueActivity *lattice) override
In general, we don't know anything about entry operands.
void visitExternalCall(CallOpInterface call, ArrayRef< const enzyme::ForwardValueActivity * > operands, ArrayRef< enzyme::ForwardValueActivity * > results) override
LogicalResult visitOperation(Operation *op, ArrayRef< const enzyme::ForwardValueActivity * > operands, ArrayRef< enzyme::ForwardValueActivity * > results) override
void print(raw_ostream &os) const
static ValueActivity merge(const ValueActivity &lhs, const ValueActivity &rhs)
const ActivityKind & getValue() const
Get the known activity state.
raw_ostream & operator<<(raw_ostream &os) const
bool isFunctionReturnLike(Operation *op)
void runDataFlowActivityAnalysis(FunctionOpInterface callee, ArrayRef< enzyme::Activity > argumentActivity, ArrayRef< enzyme::Activity > returnActivity, bool print=false, bool verbose=false, bool annotate=false)
This needs to keep track of three things:
bool activeIn
Whether active data has stored into this memory location.
ChangeResult merge(const MemoryActivityState &other)
bool activeOut
Whether active data was loaded out of this memory location.