DebugCounters allow for selectively enabling the execution of a debug action based upon a "counter". This counter is comprised of two components that are used in the control of execution of an action, a "skip" value and a "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). 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. This is effectively the same as the DebugCounter infrastructure in LLVM, but using the DebugAction infrastructure in MLIR. We can't simply reuse the DebugCounter support already present in LLVM due to its heavy reliance on global constructors (which are not allowed in MLIR). The DebugAction infrastructure already nicely supports the debug counter use case, and promotes the separation of policy and mechanism design philosophy. Differential Revision: https://reviews.llvm.org/D96395
10 KiB
Debug Actions
[TOC]
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:
- Compiler developer defines an
action
that is taken by the a pass, transformation, utility that they are developing. - Depending on the needs, the developer dispatches various queries, pertaining
to this action, to an
action manager
that will provide an answer as to what behavior the action should take. - An external entity registers an
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.
Debug Action
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:
- A unique string identifier, similar to a command line flag or DEBUG_TYPE.
-
Description:
- A short description of what the action represents.
-
Parameter Types:
- The types of values that are passed to queries related to this action, to help guide decisions.
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.
/// * The parameters for the action are provided via template parameters when
/// inheriting from `DebugAction`.
struct ApplyPatternAction
: public DebugAction<Operation *, const Pattern &> {
static StringRef getTag() { return "apply-pattern"; }
static StringRef getDescription() {
return "Control the application of rewrite patterns";
}
};
Debug Action Manager
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<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);
}
Debug Action Handler
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
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;
};
Generic Handlers
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);
};
Common Action Handlers
MLIR provides several common debug action handlers for immediate use that have proven useful in general.
DebugCounter
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
).
CommandLine Configuration
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}