1# Action: Tracing and Debugging MLIR-based Compilers 2 3[TOC] 4 5See also the [slides](https://mlir.llvm.org/OpenMeetings/2023-02-23-Actions.pdf) 6and the [recording](https://youtu.be/ayQSyekVa3c) from the MLIR Open Meeting 7where this feature was demoed. 8 9## Overview 10 11`Action` are means to encapsulate any transformation of any granularity in a way 12that can be intercepted by the framework for debugging or tracing purposes, 13including skipping a transformation programmatically (think about "compiler 14fuel" or "debug counters" in LLVM). As such, "executing a pass" is an Action, so 15is "try to apply one canonicalization pattern", or "tile this loop". 16 17In MLIR, passes and patterns are the main abstractions to encapsulate general IR 18transformations. The primary way of observing transformations along the way is 19to enable “debug printing” of the IR (e.g. -mlir-print-ir-after-all to print 20after each pass execution). On top of this, finer grain tracing may be available 21with -debug which enables more detailed logs from the transformations 22themselves. However, this method has some scaling issues: it is limited to a 23single stream of text that can be gigantic and requires tedious crawling through 24this log a posteriori. Iterating through multiple runs of collecting such logs 25and analyzing it can be very time consuming and often not very practical beyond 26small input programs. 27 28The `Action` framework doesn't make any assumptions about how the higher level 29driver is controlling the execution, it merely provides a framework for 30connecting the two together. A high level overview of the workflow surrounding 31`Action` execution is shown below: 32 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. 39 40The exact definition of an `external entity` is left opaque, to allow for more 41interesting handlers. 42 43## Wrapping a Transformation in an Action 44 45There are two parts for getting started with enabling tracing through Action in 46existing or new code: 1) defining an actual `Action` class, and 2) encapsulating 47the transformation in a lambda function. 48 49There are no constraints on the granularity of an “action”, it can be as simple 50as “perform this fold” and as complex as “run this pass pipeline”. An action is 51comprised of the following: 52 53```c++ 54/// A custom Action can be defined minimally by deriving from 55/// `tracing::ActionImpl`. 56class MyCustomAction : public tracing::ActionImpl<MyCustomAction> { 57public: 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) 62 : Base(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"; 68}; 69``` 70 71Any transformation can then be dispatched with this `Action` through the 72`MLIRContext`: 73 74```c++ 75context->executeAction<ApplyPatternAction>( 76 [&]() { 77 rewriter.setInsertionPoint(op); 78 79 ... 80 }, 81 /*IRUnits=*/{op, region}); 82``` 83 84An action can also carry arbitrary payload, for example we can extend the 85`MyCustomAction` class above with the following member: 86 87```c++ 88/// A custom Action can be defined minimally by deriving from 89/// `tracing::ActionImpl`. It can have any members! 90class MyCustomAction : public tracing::ActionImpl<MyCustomAction> { 91public: 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 104 int count; 105 PaddingStyle padding; 106}; 107``` 108 109These new members must then be passed as arguments when dispatching an `Action`: 110 111```c++ 112context->executeAction<ApplyPatternAction>( 113 [&]() { 114 rewriter.setInsertionPoint(op); 115 116 ... 117 }, 118 /*IRUnits=*/{op, region}, 119 /*count=*/count, 120 /*padding=*/padding); 121``` 122 123## Intercepting Actions 124 125When a transformation is executed through an `Action`, it can be directly 126intercepted via a handler that can be set on the `MLIRContext`: 127 128```c++ 129 /// Signatures for the action handler that can be registered with the context. 130 using HandlerTy = 131 std::function<void(function_ref<void()>, const tracing::Action &)>; 132 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); 136``` 137 138This handler takes two arguments: the first on is the transformation wrapped in 139a callback, and the second is a reference to the associated action object. The 140handler has full control of the execution, as such it can also decide to return 141without executing the callback, skipping the transformation entirely! 142 143## MLIR-provided Handlers 144 145MLIR provides some predefined action handlers for immediate use that are 146believed to be useful for most projects built with MLIR. 147 148### Debug Counters 149 150When debugging a compiler issue, 151["bisection"](<https://en.wikipedia.org/wiki/Bisection_(software_engineering)>) 152is a useful technique for locating the root cause of the issue. `Debug Counters` 153enable using this technique for debug actions by attaching a counter value to a 154specific action and enabling/disabling execution of this action based on the 155value 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 157initial executions of a debug action. The "count" value is used to prevent a 158debug action from executing after it has executed for a set number of times (not 159including any executions that have been skipped). If the "skip" value is 160negative, the action will always execute. If the "count" value is negative, the 161action will always execute after the "skip" value has been reached. For example, 162a counter for a debug action with `skip=47` and `count=2`, would skip the first 16347 executions, then execute twice, and finally prevent any further executions. 164With a bit of tooling, the values to use for the counter can be automatically 165selected; allowing for finding the exact execution of a debug action that 166potentially causes the bug being investigated. 167 168Note: The DebugCounter action handler does not support multi-threaded execution, 169and should only be used in MLIRContexts where multi-threading is disabled (e.g. 170via `-mlir-disable-threading`). 171 172#### CommandLine Configuration 173 174The `DebugCounter` handler provides several that allow for configuring counters. 175The 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 177attach the counter, suffixed with either `-skip` or `-count`. A `-skip` suffix 178will set the "skip" value of the counter. A `-count` suffix will set the "count" 179value of the counter. The `<counter-value>` component is a numeric value to use 180for the counter. An example is shown below using `MyCustomAction` defined above: 181 182```shell 183$ mlir-opt foo.mlir -mlir-debug-counter=unique-tag-for-my-action-skip=47,unique-tag-for-my-action-count=2 184``` 185 186The above configuration would skip the first 47 executions of 187`ApplyPatternAction`, then execute twice, and finally prevent any further 188executions. 189 190Note: Each counter currently only has one `skip` and one `count` value, meaning 191that sequences of `skip`/`count` will not be chained. 192 193The `mlir-print-debug-counter` option may be used to print out debug counter 194information after all counters have been accumulated. The information is printed 195in the following format: 196 197```shell 198DebugCounter counters: 199<action-tag> : {<current-count>,<skip>,<count>} 200``` 201 202For example, using the options above we can see how many times an action is 203executed: 204 205```shell 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 207 208DebugCounter counters: 209unique-tag-for-my-action : {370,-1,-1} 210``` 211 212### ExecutionContext 213 214The `ExecutionContext` is a component that provides facility to unify the kind 215of functionalities that most compiler debuggers tool would need, exposed in a 216composable way. 217 218 219 220The `ExecutionContext` is itself registered as a handler with the MLIRContext 221and tracks all executed actions, keeping a per-thread stack of action execution. 222It acts as a middleware that handles the flow of action execution while allowing 223injection and control from a debugger. 224 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 233 executed. 234- A single callback: 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: 239 ```c++ 240 /// Enum that allows the client of the context to control the execution of the 241 /// action. 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 }; 253 ``` 254 Since the callback actually controls the execution, there can be only one 255 registered at any given time. 256 257#### Debugger ExecutionContext Hook 258 259MLIR provides a callback for the `ExecutionContext` that implements a small 260runtime suitable for debuggers like `gdb` or `lldb` to interactively control the 261execution. It can be setup with 262`mlir::setupDebuggerExecutionContextHook(executionContext);` or using `mlir-opt` 263with the `--mlir-enable-debugger-hook` flag. This runtime exposes a set of C API 264function that can be called from a debugger to: 265 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. 271 272The implementation of this runtime can serve as an example for other 273implementation of programmatic control of the execution. 274 275#### Logging Observer 276 277One observer is provided that allows to log action execution on a provided 278stream. It can be exercised with `mlir-opt` using `--log-actions-to=<filename>`, 279and optionally filtering the output with 280`--log-mlir-actions-filter=<FileLineCol>`. This observer is not thread-safe at 281the moment. 282