This file documents the infrastructure for Debug Actions
. This is a DEBUG only API that allows for external entities to control various aspects of compiler execution. This is conceptually similar to something like DebugCounters
in LLVM, but at a lower level. This framework doesn't make any assumptions about how the higher level driver is controlling the execution, it merely provides a framework for connecting the two together. A high level overview of the workflow surrounding debug actions is shown below:
action
that is taken by the a pass, transformation, utility that they are developing.action manager
that will provide an answer as to what behavior the action should take.action handler
with the action manager, and provides the logic to resolve queries on actions.The exact definition of an external entity
is left opaque, to allow for more interesting handlers. The set of possible action queries is detailed in the action manager
section below.
A debug action
is essentially a marker for a type of action that may be performed within the compiler. There are no constraints on the granularity of an “action”, it can be as simple as “perform this fold” and as complex as “run this pass pipeline”. An action is comprised of the following:
Tag:
Description:
Parameter Types:
Below is an example action that may be provided by the pattern rewriter framework to control the application of rewrite patterns.
/// A debug action that allows for controlling the application of patterns. /// A new action type can be defined by inheriting from `DebugAction`. /// * The Tag is specified via a static `StringRef getTag()` method. /// * The Description is specified via a static `StringRef getDescription()` /// method. /// * `DebugAction` is a CRTP class, so the first template parameter is the /// action type class itself. /// * The parameters for the action are provided via additional template /// parameters when inheriting from `DebugAction`. struct ApplyPatternAction : public DebugAction<ApplyPatternAction, Operation *, const Pattern &> { static StringRef getTag() { return "apply-pattern"; } static StringRef getDescription() { return "Control the application of rewrite patterns"; } };
The DebugActionManager
orchestrates the various different queries relating to debug actions, and is accessible via the MLIRContext
. These queries allow for external entities to control various aspects of the compiler via action handlers. When resolving a query for an action, the result from the most recently registered handler is used.
TODO: It may be interesting to support merging results from multiple action handlers, but this is left for future work when driven by a real use case.
The set of available queries are shown below:
class DebugActionManager { public: /// Returns true if the given action type should be executed, false otherwise. /// `Params` correspond to any action specific parameters that may be used to /// guide the decision. template <typename ActionType, typename... Params> bool shouldExecute(Params &&... params); };
Building on the example from the previous section, an example usage of the shouldExecute
query is shown below:
/// A debug action that allows for controlling the application of patterns. struct ApplyPatternAction : public DebugAction<ApplyPatternAction, Operation *, const Pattern &> { static StringRef getTag() { return "apply-pattern"; } static StringRef getDescription() { return "Control the application of rewrite patterns"; } }; // ... bool shouldApplyPattern(Operation *currentOp, const Pattern ¤tPattern) { MLIRContext *context = currentOp->getContext(); DebugActionManager &manager = context->getDebugActionManager(); // Query the action manager to see if `currentPattern` should be applied to // `currentOp`. return manager.shouldExecute<ApplyPatternAction>(currentOp, currentPattern); }
A debug action handler provides the internal implementation for the various action related queries within the DebugActionManager
. Action handlers allow for external entities to control and inject external information into the compiler. Handlers can be registered with the DebugActionManager
using registerActionHandler
. There are two types of handlers; action-specific handlers and generic handlers.
Action specific handlers handle a specific debug action type, with the parameters to its query methods mapping 1-1 to the parameter types of the action type. An action specific handler can be defined by inheriting from the handler base class defined at ActionType::Handler
where ActionType
is the specific action that should be handled. An example using our running pattern example is shown below:
struct MyPatternHandler : public ApplyPatternAction::Handler { /// A variant of `shouldExecute` shown in the `DebugActionManager` class /// above. /// This method returns a FailureOr<bool>, where failure signifies that the /// action was not handled (allowing for other handlers to process it), or the /// boolean true/false signifying if the action should execute or not. FailureOr<bool> shouldExecute(Operation *op, const RewritePattern &pattern) final; };
A generic handler allows for handling queries on any action type. These types of handlers are useful for implementing general functionality that doesn’t necessarily need to interpret the exact action parameters, or can rely on an external interpreter (such as the user). As these handlers are generic, they take a set of opaque parameters that try to map the context of the action type in a generic way. A generic handler can be defined by inheriting from DebugActionManager::GenericHandler
. An example is shown below:
struct MyPatternHandler : public DebugActionManager::GenericHandler { /// The return type of this method is the same as the action-specific handler. /// The arguments to this method map the concepts of an action type in an /// opaque way. The arguments are provided in such a way so that the context /// of the action is still somewhat user readable, or at least loggable as /// such. /// - actionTag: The tag specified by the action type. /// - actionDesc: The description specified by the action type. virtual FailureOr<bool> shouldExecute(StringRef actionTag, StringRef actionDesc); };
MLIR provides several common debug action handlers for immediate use that have proven useful in general.
When debugging a compiler issue, “bisection” is a useful technique for locating the root cause of the issue. Debug Counters
enable using this technique for debug actions by attaching a counter value to a specific debug action and enabling/disabling execution of this action based on the value of the counter. The counter controls the execution of the action with a “skip” and “count” value. The “skip” value is used to skip a certain number of initial executions of a debug action. The “count” value is used to prevent a debug action from executing after it has executed for a set number of times (not including any executions that have been skipped). If the “skip” value is negative, the action will always execute. If the “count” value is negative, the action will always execute after the “skip” value has been reached. For example, a counter for a debug action with skip=47
and count=2
, would skip the first 47 executions, then execute twice, and finally prevent any further executions. With a bit of tooling, the values to use for the counter can be automatically selected; allowing for finding the exact execution of a debug action that potentially causes the bug being investigated.
Note: The DebugCounter action handler does not support multi-threaded execution, and should only be used in MLIRContexts where multi-threading is disabled (e.g. via -mlir-disable-threading
).
The DebugCounter
handler provides several that allow for configuring counters. The main option is mlir-debug-counter
, which accepts a comma separated list of <count-name>=<counter-value>
. A <counter-name>
is the debug action tag to attach the counter, suffixed with either -skip
or -count
. A -skip
suffix will set the “skip” value of the counter. A -count
suffix will set the “count” value of the counter. The <counter-value>
component is a numeric value to use for the counter. An example is shown below using ApplyPatternAction
defined above:
$ mlir-opt foo.mlir -mlir-debug-counter=apply-pattern-skip=47,apply-pattern-count=2
The above configuration would skip the first 47 executions of ApplyPatternAction
, then execute twice, and finally prevent any further executions.
Note: Each counter currently only has one skip
and one count
value, meaning that sequences of skip
/count
will not be chained.
The mlir-print-debug-counter
option may be used to print out debug counter information after all counters have been accumulated. The information is printed in the following format:
DebugCounter counters: <action-tag> : {<current-count>,<skip>,<count>}
For example, using the options above we can see how many times an action is executed:
$ mlir-opt foo.mlir -mlir-debug-counter=apply-pattern-skip=-1 -mlir-print-debug-counter DebugCounter counters: apply-pattern : {370,-1,-1}