blob: 03f9627b9c5c8fe55a1b1dffdf9578cf50c2c29e [file]
// Copyright 2022 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//===------------------- DumpDispatchGraph.cpp ----------------------------===//
//
// Generate a graphviz graph for dispatches
//
//===----------------------------------------------------------------------===//
#include <utility>
#include "iree/compiler/Dialect/Flow/IR/FlowOps.h"
#include "iree/compiler/Dialect/Flow/Transforms/Passes.h"
#include "iree/compiler/Dialect/Util/IR/UtilDialect.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/Format.h"
#include "llvm/Support/GraphWriter.h"
#include "llvm/Support/ToolOutputFile.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/Linalg/IR/Linalg.h"
#include "mlir/IR/AsmState.h"
#include "mlir/IR/Block.h"
#include "mlir/IR/BuiltinTypes.h"
#include "mlir/IR/Operation.h"
#include "mlir/Interfaces/FunctionInterfaces.h"
#include "mlir/Support/FileUtilities.h"
#include "mlir/Support/IndentedOstream.h"
namespace mlir::iree_compiler::IREE::Flow {
#define GEN_PASS_DEF_DUMPDISPATCHGRAPHPASS
#include "iree/compiler/Dialect/Flow/Transforms/Passes.h.inc"
namespace {
static const StringRef kLineStyleControlFlow = "dashed";
static const StringRef kLineStyleDataFlow = "solid";
static const StringRef kShapeNode = "box";
static const StringRef kShapeBox = "box";
static const StringRef kShapeNone = "plain";
static const StringRef kShapeEllipse = "ellipse";
static StringRef getShape(Operation *op) {
if (isa<DispatchOp>(op))
return kShapeBox;
return kShapeEllipse;
}
/// Return the size limits for eliding large attributes.
static int64_t getLargeAttributeSizeLimit() {
// Use the default from the printer flags if possible.
if (std::optional<int64_t> limit =
OpPrintingFlags().getLargeElementsAttrLimit())
return *limit;
return 16;
}
/// Return all values printed onto a stream as a string.
static std::string strFromOs(function_ref<void(raw_ostream &)> func) {
std::string buf;
llvm::raw_string_ostream os(buf);
func(os);
return os.str();
}
/// Escape special characters such as '\n' and quotation marks.
static std::string escapeString(std::string str) {
return strFromOs([&](raw_ostream &os) {
for (unsigned char c : str) {
switch (c) {
case '\\':
os << '\\' << '\\';
break;
case '\t':
os << '\\' << 't';
break;
case '\n':
os << '\\' << 'n';
break;
case '"':
os << '\\' << '"';
break;
case '\r': // translate "carriage return" as "\l"
os << '\\' << 'l';
break;
default:
if (llvm::isPrint(c)) {
os << c;
break;
}
// Always use a full 3-character octal escape.
os << '\\';
os << char('0' + ((c >> 6) & 7));
os << char('0' + ((c >> 3) & 7));
os << char('0' + ((c >> 0) & 7));
}
}
});
}
/// Put quotation marks around a given string.
static std::string quoteString(const std::string &str) {
return "\"" + str + "\"";
}
using AttributeMap = llvm::StringMap<std::string>;
/// This struct represents a node in the DOT language. Each node has an
/// identifier and an optional identifier for the cluster (subgraph) that
/// contains the node.
/// Note: In the DOT language, edges can be drawn only from nodes to nodes, but
/// not between clusters. However, edges can be clipped to the boundary of a
/// cluster with `lhead` and `ltail` attributes. Therefore, when creating a new
/// cluster, an invisible "anchor" node is created.
struct Node {
public:
Node(int id = 0, std::optional<int> clusterId = std::nullopt)
: id(id), clusterId(clusterId) {}
int id;
std::optional<int> clusterId;
};
class GraphPrinter {
public:
GraphPrinter(raw_ostream &os, unsigned maxLabelLen, bool printAttrs,
bool printControlFlowEdges, bool printDataFlowEdges,
bool printResultTypes)
: os(os), maxLabelLen(maxLabelLen), printAttrs(printAttrs),
printControlFlowEdges(printControlFlowEdges),
printDataFlowEdges(printDataFlowEdges),
printResultTypes(printResultTypes) {}
void emitFunctions(ModuleOp module) {
auto funcOps = module.getOps<mlir::FunctionOpInterface>();
if (funcOps.empty())
return;
emitGraph([&]() {
for (auto funcOp : funcOps)
processOperation(funcOp);
emitAllEdgeStmts();
});
}
private:
/// Create a CFG graph for a region. Used in `Region::viewGraph`.
void emitRegionCFG(Region &region) {
printControlFlowEdges = true;
printDataFlowEdges = false;
emitGraph([&]() { processRegion(region); });
}
/// Emit all edges. This function should be called after all nodes have been
/// emitted.
void emitAllEdgeStmts() {
for (const std::string &edge : edges)
os << edge << ";\n";
edges.clear();
}
/// Emit a cluster (subgraph). The specified builder generates the body of the
/// cluster. Return the anchor node of the cluster.
Node emitClusterStmt(function_ref<void()> builder, std::string label = "") {
int clusterId = ++counter;
os << "subgraph cluster_" << clusterId << " {\n";
os.indent();
// Emit invisible anchor node from/to which arrows can be drawn.
Node anchorNode = emitNodeStmt(" ", kShapeNone);
os << attrStmt("label", quoteString(escapeString(std::move(label))))
<< ";\n";
builder();
os.unindent();
os << "}\n";
return Node(anchorNode.id, clusterId);
}
/// Generate an attribute statement.
std::string attrStmt(const Twine &key, const Twine &value) {
return (key + " = " + value).str();
}
/// Emit an attribute list.
void emitAttrList(raw_ostream &os, const AttributeMap &map) {
os << "[";
interleaveComma(map, os, [&](const auto &it) {
os << this->attrStmt(it.getKey(), it.getValue());
});
os << "]";
}
// Print an MLIR attribute to `os`. Large attributes are truncated.
void emitMlirAttr(raw_ostream &os, Attribute attr) {
// A value used to elide large container attribute.
int64_t largeAttrLimit = getLargeAttributeSizeLimit();
// Always emit splat attributes.
if (llvm::isa<SplatElementsAttr>(attr)) {
attr.print(os);
return;
}
// Elide "big" elements attributes.
auto elements = llvm::dyn_cast<ElementsAttr>(attr);
if (elements && elements.getNumElements() > largeAttrLimit) {
auto type = cast<ShapedType>(elements.getType());
os << std::string(type.getRank(), '[') << "..."
<< std::string(type.getRank(), ']') << " : " << type;
return;
}
auto array = llvm::dyn_cast<ArrayAttr>(attr);
if (array && static_cast<int64_t>(array.size()) > largeAttrLimit) {
os << "[...]";
return;
}
// Print all other attributes.
std::string buf;
llvm::raw_string_ostream ss(buf);
attr.print(ss);
os << truncateString(ss.str());
}
/// Append an edge to the list of edges.
/// Note: Edges are written to the output stream via `emitAllEdgeStmts`.
void emitEdgeStmt(Node n1, Node n2, std::string label, StringRef style) {
AttributeMap attrs;
attrs["style"] = style.str();
// Do not label edges that start/end at a cluster boundary. Such edges are
// clipped at the boundary, but labels are not. This can lead to labels
// floating around without any edge next to them.
if (!n1.clusterId && !n2.clusterId)
attrs["label"] = quoteString(escapeString(std::move(label)));
// Use `ltail` and `lhead` to draw edges between clusters.
if (n1.clusterId)
attrs["ltail"] = "cluster_" + std::to_string(*n1.clusterId);
if (n2.clusterId)
attrs["lhead"] = "cluster_" + std::to_string(*n2.clusterId);
edges.push_back(strFromOs([&](raw_ostream &os) {
os << llvm::format("v%i -> v%i ", n1.id, n2.id);
emitAttrList(os, attrs);
}));
}
/// Emit a graph. The specified builder generates the body of the graph.
void emitGraph(function_ref<void()> builder) {
os << "digraph G {\n";
os.indent();
// Edges between clusters are allowed only in compound mode.
os << attrStmt("compound", "true") << ";\n";
builder();
os.unindent();
os << "}\n";
}
/// Emit a node statement.
Node emitNodeStmt(Operation *op) {
int nodeId = ++counter;
AttributeMap attrs;
auto label = getLabel(op);
auto shape = getShape(op);
attrs["label"] = quoteString(escapeString(std::move(label)));
attrs["shape"] = shape.str();
os << llvm::format("v%i ", nodeId);
emitAttrList(os, attrs);
os << ";\n";
return Node(nodeId);
}
Node emitNodeStmt(std::string label, StringRef shape = kShapeNode) {
int nodeId = ++counter;
AttributeMap attrs;
attrs["label"] = quoteString(escapeString(std::move(label)));
attrs["shape"] = shape.str();
os << llvm::format("v%i ", nodeId);
emitAttrList(os, attrs);
os << ";\n";
return Node(nodeId);
}
void printResults(raw_ostream &os, Operation *op, AsmState &state) {
for (auto result : op->getResults()) {
result.printAsOperand(os, state);
}
}
void printResultsAndName(raw_ostream &os, Operation *op, AsmState &state) {
printResults(os, op, state);
os << " = " << op->getName();
}
void printDispatchTensorLoad(raw_ostream &os, DispatchTensorLoadOp op,
AsmState &state) {
printResultsAndName(os, op.getOperation(), state);
os << " ";
op.getSource().printAsOperand(os, state);
os << " -> " << op.getResult().getType();
os << "\r";
}
void printDispatchTensorStore(raw_ostream &os, DispatchTensorStoreOp op,
AsmState &state) {
os << op->getName() << " ";
op.getValue().printAsOperand(os, state);
os << ", ";
op.getTarget().printAsOperand(os, state);
os << "\r";
}
void printGeneric(raw_ostream &os, linalg::GenericOp op, AsmState &state) {
printLinalgInsOuts(os, op, state);
for (Operation &operation : *op.getBlock()) {
os.indent(8);
annotateOperation(os, &operation, state);
}
}
template <typename T>
void printLinalgInsOuts(raw_ostream &os, T op, AsmState &state) {
printResultsAndName(os, op.getOperation(), state);
os << "[";
llvm::interleaveComma(op.getIteratorTypesArray(), os);
os << "] (";
printOperands(os, op.getInputs(), state);
os << ") -> (";
printOperands(os, op.getOutputs(), state);
os << ")\r";
}
void annotateOperation(raw_ostream &os, Operation *op, AsmState &state) {
if (isa<arith::ConstantOp>(op))
return;
if (op->hasTrait<OpTrait::ReturnLike>() &&
isa<mlir::FunctionOpInterface>(op->getParentOp()))
return;
if (auto load = dyn_cast<DispatchTensorLoadOp>(op)) {
printDispatchTensorLoad(os, load, state);
return;
}
if (auto store = dyn_cast<DispatchTensorStoreOp>(op)) {
printDispatchTensorStore(os, store, state);
return;
}
if (auto generic = dyn_cast<linalg::GenericOp>(op)) {
printGeneric(os, generic, state);
return;
}
if (auto linalgOp = dyn_cast<linalg::MatmulOp>(op)) {
printLinalgInsOuts(os, linalgOp, state);
return;
}
if (auto linalgOp = dyn_cast<linalg::BatchMatmulOp>(op)) {
printLinalgInsOuts(os, linalgOp, state);
return;
}
os << *op << "\r";
}
void printDispatchBody(raw_ostream &os, DispatchOp &dispatchOp) {
// Find the entry point function from the dispatch entry point symbol
// attribute.
auto entryPoint = *dispatchOp.getEntryPointRefs().begin();
auto executableOp = cast<ExecutableOp>(SymbolTable::lookupNearestSymbolFrom(
dispatchOp, entryPoint.getRootReference()));
if (!executableOp)
return;
auto calleeNameAttr = entryPoint.getLeafReference();
auto innerModule = executableOp.getInnerModule();
if (!innerModule)
return;
auto funcOps = innerModule.getOps<mlir::FunctionOpInterface>();
auto funcIt = llvm::find_if(
funcOps, [&](auto op) { return op.getNameAttr() == calleeNameAttr; });
if (funcIt == funcOps.end())
return;
auto callee = *funcIt;
AsmState state(callee);
// Iterate the operations of the function body and print important
// operation.
for (auto &block : callee.getBlocks()) {
for (auto &op : block.getOperations()) {
annotateOperation(os, &op, state);
}
}
}
void printOperands(raw_ostream &os, ::mlir::Operation::operand_range operands,
AsmState &state) {
auto numOperands = operands.size();
for (auto it : llvm::enumerate(operands)) {
auto operand = it.value();
auto op = operand.getDefiningOp();
if (op && isScalarConstantOp(op)) {
auto ty = operand.getType();
if (llvm::isa<IntegerType>(ty)) {
os << cast<arith::ConstantIntOp>(op).value();
} else if (llvm::isa<FloatType>(ty)) {
cast<arith::ConstantFloatOp>(op).value().print(os);
} else {
os << cast<arith::ConstantIndexOp>(op).value();
}
} else {
operand.printAsOperand(os, state);
}
if (it.index() != numOperands - 1) {
os << ", ";
}
}
}
/// Generate a label for an operation.
std::string getLabel(Operation *op) {
return strFromOs([&](raw_ostream &os) {
if (op->getNumRegions() == 0) {
auto funcOp = op->getParentOfType<mlir::FunctionOpInterface>();
AsmState state(funcOp);
printResults(os, op, state);
os << " = " << op->getName();
if (auto dispatch = dyn_cast<DispatchOp>(op)) {
// print workload
os << "[";
printOperands(os, dispatch.getWorkload(), state);
os << "]\n";
// Print entry function name, if there is only one entry function,
// then the name space and the entry function names are the same,
// and we can just print the function name to save space.
auto entryPoint = *dispatch.getEntryPointRefs().begin();
auto rootName = entryPoint.getRootReference();
auto leafName = entryPoint.getLeafReference();
if (rootName == leafName) {
os << leafName;
} else {
os << entryPoint; // print the full name
}
// print entry function args
os << "(";
printOperands(os, dispatch.getArguments(), state);
os << ")\n";
printDispatchBody(os, dispatch);
} else {
os << "\n";
}
} else {
os << op->getName() << "\n";
}
if (printResultTypes) {
std::string buf;
llvm::raw_string_ostream ss(buf);
interleave(op->getResultTypes(), ss, "\n");
os << ss.str();
}
});
}
/// Generate a label for a block argument.
std::string getLabel(BlockArgument arg) {
return "arg" + std::to_string(arg.getArgNumber());
}
/// Process a block. Emit a cluster and one node per block argument and
/// operation inside the cluster.
void processBlock(Block &block) {
emitClusterStmt([&]() {
for (BlockArgument &blockArg : block.getArguments())
valueToNode[blockArg] = emitNodeStmt(getLabel(blockArg));
// Emit a node for each operation.
std::optional<Node> prevNode;
for (Operation &op : block) {
Node nextNode = processOperation(&op);
if (printControlFlowEdges && prevNode)
emitEdgeStmt(*prevNode, nextNode, /*label=*/"",
kLineStyleControlFlow);
prevNode = nextNode;
}
});
}
bool isScalarConstantOp(Operation *op) {
if (auto constOp = dyn_cast<mlir::arith::ConstantOp>(op))
if (constOp.getResult().getType().isIntOrIndexOrFloat())
return true;
return false;
}
/// Process an operation. If the operation has regions, emit a cluster.
/// Otherwise, emit a node.
Node processOperation(Operation *op) {
Node node;
// Do not handle some noisy Operations.
if (isa<arith::ConstantOp>(op) || isa<Util::GlobalLoadOpInterface>(op)) {
return node;
}
if (op->getNumRegions() == 1) {
// do not generate a cluster when there is one region.
processRegion(op->getRegion(0));
} else if (op->getNumRegions() > 1) {
// Emit cluster for op with regions.
node = emitClusterStmt(
[&]() {
for (Region &region : op->getRegions())
processRegion(region);
},
getLabel(op));
} else {
node = emitNodeStmt(op);
}
// Insert data flow edges originating from each operand.
if (printDataFlowEdges) {
unsigned numOperands = op->getNumOperands();
for (unsigned i = 0; i < numOperands; i++) {
auto operand = op->getOperand(i);
// a constant operand is not going to be available in the map.
if (valueToNode.count(operand)) {
emitEdgeStmt(valueToNode[op->getOperand(i)], node,
/*label=*/numOperands == 1 ? "" : std::to_string(i),
kLineStyleDataFlow);
}
}
}
for (Value result : op->getResults())
valueToNode[result] = node;
return node;
}
/// Process a region.
void processRegion(Region &region) {
for (Block &block : region.getBlocks())
processBlock(block);
}
/// Truncate long strings.
std::string truncateString(std::string str) {
if (str.length() <= maxLabelLen)
return str;
return str.substr(0, maxLabelLen) + "...";
}
/// Output stream to write DOT file to.
raw_indented_ostream os;
/// A list of edges. For simplicity, should be emitted after all nodes were
/// emitted.
std::vector<std::string> edges;
/// Mapping of SSA values to Graphviz nodes/clusters.
DenseMap<Value, Node> valueToNode;
/// Counter for generating unique node/subgraph identifiers.
int counter = 0;
/// Pass options.
unsigned maxLabelLen = 20;
bool printAttrs = true;
bool printControlFlowEdges = false;
bool printDataFlowEdges = true;
bool printResultTypes = true;
};
/// This pass generates a Graphviz dataflow visualization of an MLIR operation.
/// Note: See https://www.graphviz.org/doc/info/lang.html for more information
/// about the Graphviz DOT language.
class DumpDispatchGraphPass
: public IREE::Flow::impl::DumpDispatchGraphPassBase<
DumpDispatchGraphPass> {
public:
using IREE::Flow::impl::DumpDispatchGraphPassBase<
DumpDispatchGraphPass>::DumpDispatchGraphPassBase;
void runOnOperation() override {
auto modOp = dyn_cast<ModuleOp>(getOperation());
if (!modOp)
return;
// Open the output file we'll be streaming to.
// Since we are processing the entire module at once we overwrite the file.
std::string errorMessage;
auto file = openOutputFile(outputFile, &errorMessage);
if (!file) {
llvm::errs() << errorMessage << "\n";
return signalPassFailure();
}
GraphPrinter printer(file->os(), maxLabelLen, printAttrs,
printControlFlowEdges, printDataFlowEdges,
printResultTypes);
printer.emitFunctions(modOp);
file->keep();
}
private:
};
} // namespace
} // namespace mlir::iree_compiler::IREE::Flow