1 # Action: Tracing and Debugging MLIR-based Compilers
5 See also the [slides](https://mlir.llvm.org/OpenMeetings/2023-02-23-Actions.pdf)
6 and the [recording](https://youtu.be/ayQSyekVa3c) from the MLIR Open Meeting
7 where this feature was demoed.
11 `Action` are means to encapsulate any transformation of any granularity in a way
12 that can be intercepted by the framework for debugging or tracing purposes,
13 including skipping a transformation programmatically (think about "compiler
14 fuel" or "debug counters" in LLVM). As such, "executing a pass" is an Action, so
15 is "try to apply one canonicalization pattern", or "tile this loop".
17 In MLIR, passes and patterns are the main abstractions to encapsulate general IR
18 transformations. The primary way of observing transformations along the way is
19 to enable “debug printing” of the IR (e.g. -mlir-print-ir-after-all to print
20 after each pass execution). On top of this, finer grain tracing may be available
21 with -debug which enables more detailed logs from the transformations
22 themselves. However, this method has some scaling issues: it is limited to a
23 single stream of text that can be gigantic and requires tedious crawling through
24 this log a posteriori. Iterating through multiple runs of collecting such logs
25 and analyzing it can be very time consuming and often not very practical beyond
28 The `Action` framework doesn't make any assumptions about how the higher level
29 driver is controlling the execution, it merely provides a framework for
30 connecting the two together. A high level overview of the workflow surrounding
31 `Action` execution is shown below:
33 - Compiler developer defines an `Action` class, that is representing the
34 transformation or utility that they are developing.
35 - Depending on the needs, the developer identifies single unit of
36 transformations, and dispatch them to the `MLIRContext` for execution.
37 - An external entity registers an "action handler" with the action manager, and
38 provides the logic surrounding the transformation execution.
40 The exact definition of an `external entity` is left opaque, to allow for more
43 ## Wrapping a Transformation in an Action
45 There are two parts for getting started with enabling tracing through Action in
46 existing or new code: 1) defining an actual `Action` class, and 2) encapsulating
47 the transformation in a lambda function.
49 There are no constraints on the granularity of an “action”, it can be as simple
50 as “perform this fold” and as complex as “run this pass pipeline”. An action is
51 comprised of the following:
54 /// A custom Action can be defined minimally by deriving from
55 /// `tracing::ActionImpl`.
56 class MyCustomAction : public tracing::ActionImpl<MyCustomAction> {
58 using Base = tracing::ActionImpl<MyCustomAction>;
59 /// Actions are initialized with an array of IRUnit (that is either Operation,
60 /// Block, or Region) that provide context for the IR affected by a transformation.
61 MyCustomAction(ArrayRef<IRUnit> irUnits)
63 /// This tag should uniquely identify this action, it can be matched for filtering
64 /// during processing.
65 static constexpr StringLiteral tag = "unique-tag-for-my-action";
66 static constexpr StringLiteral desc =
67 "This action will encapsulate a some very specific transformation";
71 Any transformation can then be dispatched with this `Action` through the
75 context->executeAction<ApplyPatternAction>(
77 rewriter.setInsertionPoint(op);
81 /*IRUnits=*/{op, region});
84 An action can also carry arbitrary payload, for example we can extend the
85 `MyCustomAction` class above with the following member:
88 /// A custom Action can be defined minimally by deriving from
89 /// `tracing::ActionImpl`. It can have any members!
90 class MyCustomAction : public tracing::ActionImpl<MyCustomAction> {
92 using Base = tracing::ActionImpl<MyCustomAction>;
93 /// Actions are initialized with an array of IRUnit (that is either Operation,
94 /// Block, or Region) that provide context for the IR affected by a transformation.
95 /// Other constructor arguments can also be required here.
96 MyCustomAction(ArrayRef<IRUnit> irUnits, int count, PaddingStyle padding)
97 : Base(irUnits), count(count), padding(padding) {}
98 /// This tag should uniquely identify this action, it can be matched for filtering
99 /// during processing.
100 static constexpr StringLiteral tag = "unique-tag-for-my-action";
101 static constexpr StringLiteral desc =
102 "This action will encapsulate a some very specific transformation";
103 /// Extra members can be carried by the Action
105 PaddingStyle padding;
109 These new members must then be passed as arguments when dispatching an `Action`:
112 context->executeAction<ApplyPatternAction>(
114 rewriter.setInsertionPoint(op);
118 /*IRUnits=*/{op, region},
120 /*padding=*/padding);
123 ## Intercepting Actions
125 When a transformation is executed through an `Action`, it can be directly
126 intercepted via a handler that can be set on the `MLIRContext`:
129 /// Signatures for the action handler that can be registered with the context.
131 std::function<void(function_ref<void()>, const tracing::Action &)>;
133 /// Register a handler for handling actions that are dispatched through this
134 /// context. A nullptr handler can be set to disable a previously set handler.
135 void registerActionHandler(HandlerTy handler);
138 This handler takes two arguments: the first on is the transformation wrapped in
139 a callback, and the second is a reference to the associated action object. The
140 handler has full control of the execution, as such it can also decide to return
141 without executing the callback, skipping the transformation entirely!
143 ## MLIR-provided Handlers
145 MLIR provides some predefined action handlers for immediate use that are
146 believed to be useful for most projects built with MLIR.
150 When debugging a compiler issue,
151 ["bisection"](<https://en.wikipedia.org/wiki/Bisection_(software_engineering)>)
152 is a useful technique for locating the root cause of the issue. `Debug Counters`
153 enable using this technique for debug actions by attaching a counter value to a
154 specific action and enabling/disabling execution of this action based on the
155 value of the counter. The counter controls the execution of the action with a
156 "skip" and "count" value. The "skip" value is used to skip a certain number of
157 initial executions of a debug action. The "count" value is used to prevent a
158 debug action from executing after it has executed for a set number of times (not
159 including any executions that have been skipped). If the "skip" value is
160 negative, the action will always execute. If the "count" value is negative, the
161 action will always execute after the "skip" value has been reached. For example,
162 a counter for a debug action with `skip=47` and `count=2`, would skip the first
163 47 executions, then execute twice, and finally prevent any further executions.
164 With a bit of tooling, the values to use for the counter can be automatically
165 selected; allowing for finding the exact execution of a debug action that
166 potentially causes the bug being investigated.
168 Note: The DebugCounter action handler does not support multi-threaded execution,
169 and should only be used in MLIRContexts where multi-threading is disabled (e.g.
170 via `-mlir-disable-threading`).
172 #### CommandLine Configuration
174 The `DebugCounter` handler provides several that allow for configuring counters.
175 The main option is `mlir-debug-counter`, which accepts a comma separated list of
176 `<count-name>=<counter-value>`. A `<counter-name>` is the debug action tag to
177 attach the counter, suffixed with either `-skip` or `-count`. A `-skip` suffix
178 will set the "skip" value of the counter. A `-count` suffix will set the "count"
179 value of the counter. The `<counter-value>` component is a numeric value to use
180 for the counter. An example is shown below using `MyCustomAction` defined above:
183 $ mlir-opt foo.mlir -mlir-debug-counter=unique-tag-for-my-action-skip=47,unique-tag-for-my-action-count=2
186 The above configuration would skip the first 47 executions of
187 `ApplyPatternAction`, then execute twice, and finally prevent any further
190 Note: Each counter currently only has one `skip` and one `count` value, meaning
191 that sequences of `skip`/`count` will not be chained.
193 The `mlir-print-debug-counter` option may be used to print out debug counter
194 information after all counters have been accumulated. The information is printed
195 in the following format:
198 DebugCounter counters:
199 <action-tag> : {<current-count>,<skip>,<count>}
202 For example, using the options above we can see how many times an action is
206 $ mlir-opt foo.mlir -mlir-debug-counter=unique-tag-for-my-action-skip=-1 -mlir-print-debug-counter --pass-pipeline="builtin.module(func.func(my-pass))" --mlir-disable-threading
208 DebugCounter counters:
209 unique-tag-for-my-action : {370,-1,-1}
214 The `ExecutionContext` is a component that provides facility to unify the kind
215 of functionalities that most compiler debuggers tool would need, exposed in a
218 ![IMG](/actions/ActionTracing_ExecutionContext.png)
220 The `ExecutionContext` is itself registered as a handler with the MLIRContext
221 and tracks all executed actions, keeping a per-thread stack of action execution.
222 It acts as a middleware that handles the flow of action execution while allowing
223 injection and control from a debugger.
225 - Multiple `Observers` can be registered with the `ExecutionContext`. When an
226 action is dispatched for execution, it is passed to each of the `Observers`
227 before and after executing the transformation.
228 - Multiple `BreakpointManager` can be registered with the `ExecutionContext`.
229 When an action is dispatched for execution, it is passed to each of the
230 registered `BreakpointManager` until one matches the action and return a valid
231 `Breakpoint` object. In this case, the "callback" set by the client on the
232 `ExecutionContext` is invoked, otherwise the transformation is directly
235 `using CallbackTy = function_ref<Control(const ActionActiveStack *)>;` can be
236 registered with the `ExecutionContext`, it is invoked when a `BreakPoint` is
237 hit by an `Action`. The returned value of type `Control` is an enum
238 instructing the `ExecutionContext` of how to proceed next:
240 /// Enum that allows the client of the context to control the execution of the
242 /// - Apply: The action is executed.
243 /// - Skip: The action is skipped.
244 /// - Step: The action is executed and the execution is paused before the next
245 /// action, including for nested actions encountered before the
246 /// current action finishes.
247 /// - Next: The action is executed and the execution is paused after the
248 /// current action finishes before the next action.
249 /// - Finish: The action is executed and the execution is paused only when we
250 /// reach the parent/enclosing operation. If there are no enclosing
251 /// operation, the execution continues without stopping.
252 enum Control { Apply = 1, Skip = 2, Step = 3, Next = 4, Finish = 5 };
254 Since the callback actually controls the execution, there can be only one
255 registered at any given time.
257 #### Debugger ExecutionContext Hook
259 MLIR provides a callback for the `ExecutionContext` that implements a small
260 runtime suitable for debuggers like `gdb` or `lldb` to interactively control the
261 execution. It can be setup with
262 `mlir::setupDebuggerExecutionContextHook(executionContext);` or using `mlir-opt`
263 with the `--mlir-enable-debugger-hook` flag. This runtime exposes a set of C API
264 function that can be called from a debugger to:
266 - set breakpoints matching either action tags, or the `FileLineCol` locations of
267 the IR associated with the action.
268 - set the `Control` flag to be returned to the `ExecutionContext`.
269 - control a "cursor" allowing to navigate through the IR and inspect it from the
270 IR context associated with the action.
272 The implementation of this runtime can serve as an example for other
273 implementation of programmatic control of the execution.
275 #### Logging Observer
277 One observer is provided that allows to log action execution on a provided
278 stream. It can be exercised with `mlir-opt` using `--log-actions-to=<filename>`,
279 and optionally filtering the output with
280 `--log-mlir-actions-filter=<FileLineCol>`. This observer is not thread-safe at