a few other changes. Changes: - Some improvements to convolution by using 32-bit indices by @benoitsteiner. Not all calls converted yet. Also some improvements to pooling as well by @benoitsteiner. - Improvements to sparse matmul CPU implementation by Ashish - Some fixes to warnings by @vrv - Doc fixes to padding by @Yangqing - Some improvements to Tensor wrappers by Eider - Speed up of matrix inverse on CPU by Rasmus - Add an example of doing image inference from a pre-trained model by @petewarden. - fixed formula in mnist example by nodir - Updates to event accumulator by Cassandra - Slight changes to tensor c api by @mrry - Handling of strings in listdiff by Phil - Fix negative fraction-of-queue-full stats by Frank - Type-checking improvement to importer by Yaroslav - logdir recursive search for Tensorboard by @danmane - Session.run() checks for empty graph by Manoj Base CL: 108013706
733 lines
26 KiB
TypeScript
733 lines
26 KiB
TypeScript
/// <reference path="graph.ts" />
|
|
/// <reference path="template.ts" />
|
|
|
|
/**
|
|
* Package for the Graph Hierarchy for TensorFlow graph.
|
|
*/
|
|
module tf.graph.hierarchy {
|
|
|
|
const LOG_PREFIX_MSG = "Graph hierarchy: ";
|
|
|
|
/**
|
|
* Class used as output for getPredecessors and getSuccessors methods
|
|
*/
|
|
export interface Edges {
|
|
control: string[];
|
|
regular: string[];
|
|
}
|
|
|
|
export interface Hierarchy {
|
|
root: Metanode;
|
|
templates: {[templateId: string]: string[]};
|
|
/** List of all device names */
|
|
devices: string[];
|
|
getNodeMap(): {[nodeName: string]: GroupNode|OpNode};
|
|
node(name: string): GroupNode|OpNode;
|
|
setNode(name: string, node: GroupNode|OpNode): void;
|
|
getBridgegraph(nodeName: string): graphlib.Graph<GroupNode|OpNode, Metaedge>;
|
|
getPredecessors(nodeName: string): Edges;
|
|
getSuccessors(nodeName: string): Edges;
|
|
getTopologicalOrdering(nodeName: string): { [childName: string]: number };
|
|
getTemplateIndex(): (string) => number;
|
|
}
|
|
|
|
/**
|
|
* Class for the Graph Hierarchy for TensorFlow graph.
|
|
*/
|
|
class HierarchyImpl implements Hierarchy {
|
|
root: Metanode;
|
|
templates: {[templateId: string]: string[]};
|
|
private index: {[nodeName: string]: GroupNode|OpNode};
|
|
devices: string[];
|
|
orderings: { [nodeName: string]: { [childName: string]: number } };
|
|
|
|
constructor() {
|
|
this.root = createMetanode(ROOT_NAME, {compound: true});
|
|
this.templates = null;
|
|
this.devices = null;
|
|
/**
|
|
* @type {Object} Dictionary object that maps node name to the node
|
|
* (could be op-node, metanode, or series-node)
|
|
*/
|
|
this.index = {};
|
|
this.index[ROOT_NAME] = this.root;
|
|
this.orderings = {};
|
|
}
|
|
|
|
getNodeMap(): {[nodeName: string]: GroupNode|OpNode} {
|
|
return this.index;
|
|
}
|
|
|
|
node(name: string): GroupNode|OpNode {
|
|
return this.index[name];
|
|
}
|
|
|
|
setNode(name: string, node: GroupNode|OpNode): void {
|
|
this.index[name] = node;
|
|
}
|
|
|
|
/**
|
|
* Given the name of a node in this hierarchy, get its bridgegraph, creating
|
|
* it on the fly if necessary. If the node is not a GroupNode, then this
|
|
* method returns null. If the provided name does not map to a node in the
|
|
* hierarchy, an error will be thrown.
|
|
*/
|
|
getBridgegraph(nodeName: string): graphlib.Graph<GroupNode|OpNode, Metaedge> {
|
|
let node = this.index[nodeName];
|
|
if (!node) {
|
|
throw Error("Could not find node in hierarchy: " + nodeName);
|
|
}
|
|
if (!("metagraph" in node)) {
|
|
return null;
|
|
}
|
|
let groupNode = <GroupNode> node;
|
|
if (groupNode.bridgegraph) {
|
|
return groupNode.bridgegraph;
|
|
}
|
|
let bridgegraph = groupNode.bridgegraph =
|
|
createGraph<GroupNode|OpNode, Metaedge>(
|
|
"BRIDGEGRAPH", GraphType.BRIDGE);
|
|
if (!node.parentNode || !("metagraph" in node.parentNode)) {
|
|
return bridgegraph;
|
|
}
|
|
|
|
let parentNode = <GroupNode>node.parentNode;
|
|
let parentMetagraph = parentNode.metagraph;
|
|
let parentBridgegraph = this.getBridgegraph(parentNode.name);
|
|
|
|
// For each of the parent node's two Metaedge containing graphs, process
|
|
// each Metaedge involving this node.
|
|
_.each([parentMetagraph, parentBridgegraph], parentGraph => {
|
|
_(parentGraph.edges())
|
|
.filter(e => e.v === nodeName || e.w === nodeName)
|
|
.each(parentEdgeObj => {
|
|
|
|
let inbound = parentEdgeObj.w === nodeName;
|
|
let parentMetaedge = parentGraph.edge(parentEdgeObj);
|
|
|
|
// The parent's Metaedge represents some number of underlying
|
|
// BaseEdges from the original full graph. For each of those, we need
|
|
// to determine which immediate child is involved and make sure
|
|
// there's a Metaedge in the bridgegraph that covers it.
|
|
_.each(parentMetaedge.baseEdgeList, baseEdge => {
|
|
|
|
// Based on the direction, figure out which is the descendant node
|
|
// and which is the "other" node (sibling of parent or ancestor).
|
|
let [descendantName, otherName] =
|
|
inbound ?
|
|
[baseEdge.w, parentEdgeObj.v] :
|
|
[baseEdge.v, parentEdgeObj.w];
|
|
|
|
// Determine the immediate child containing this descendant node.
|
|
let childName = this.getChildName(nodeName, descendantName);
|
|
|
|
// Look for an existing Metaedge in the bridgegraph (or create a
|
|
// new one) that covers the relationship between child and other.
|
|
let bridgeEdgeObj = <graphlib.EdgeObject> {
|
|
v: inbound ? otherName : childName,
|
|
w: inbound ? childName : otherName,
|
|
};
|
|
let bridgeMetaedge = bridgegraph.edge(bridgeEdgeObj);
|
|
if (!bridgeMetaedge) {
|
|
bridgeMetaedge = createMetaedge(bridgeEdgeObj.v, bridgeEdgeObj.w);
|
|
bridgeMetaedge.inbound = inbound;
|
|
bridgegraph.setEdge(bridgeEdgeObj.v, bridgeEdgeObj.w,
|
|
bridgeMetaedge);
|
|
}
|
|
|
|
// Copy the BaseEdge from the parent's Metaedge into this
|
|
// bridgegraph Metaedge.
|
|
bridgeMetaedge.addBaseEdge(baseEdge);
|
|
});
|
|
})
|
|
.value(); // force lodash chain execution.
|
|
});
|
|
|
|
return bridgegraph;
|
|
}
|
|
|
|
/**
|
|
* Utility function for determining the name of the immediate child under a
|
|
* node for a given descendant path. If the descendant corresponds to no
|
|
* immediate child, an error is thrown.
|
|
*/
|
|
getChildName(nodeName: string, descendantName: string): string {
|
|
// Walk up the hierarchy from the descendant to find the child.
|
|
let currentNode: Node = this.index[descendantName];
|
|
while (currentNode) {
|
|
if (currentNode.parentNode && currentNode.parentNode.name === nodeName) {
|
|
return currentNode.name;
|
|
}
|
|
currentNode = currentNode.parentNode;
|
|
}
|
|
throw Error("Could not find immediate child for descendant: " +
|
|
descendantName);
|
|
};
|
|
|
|
/**
|
|
* Given the name of a node, return the names of its predecssors.
|
|
* For an OpNode, this will contain the targets from the underlying BaseEdges.
|
|
* For a GroupNode, this will contain the targets truncated to siblings of
|
|
* the shared ancestor.
|
|
*
|
|
* For example, consider an original non-control BaseEdge A/B/C->Z/Y/X. Their
|
|
* shared ancestor is the ROOT node. A and Z are the highest siblings. Here
|
|
* are the results of calling getPredecessors():
|
|
*
|
|
* - getPredecessors("Z/Y/X") === {regular: ["A/B/C"], control: []};
|
|
* - getPredecessors("Z/Y") === {regular: ["A"], control: []};
|
|
* - getPredecessors("Z") === {regular: ["A"], control: []};
|
|
*
|
|
* The reason getPredecessors("Z/Y") returns ["A"] (and not ["A/B"] as you
|
|
* might intuitively expect) is because it's not clear how far down the
|
|
* other end of the hierarchy to traverse in the general case.
|
|
*
|
|
* Continuing this example, say there was another BaseEdge A/K->Z/Y/W. When
|
|
* we look at Z/Y's predecessors, the best we can say is ["A"] without getting
|
|
* into the details of which of of Z/Y's descendant nodes have predecessors to
|
|
* which of A's descendants.
|
|
*
|
|
* On the other hand, for an OpNode it's clear what the final predecssors
|
|
* ought to be. There is no ambiguity.
|
|
*/
|
|
getPredecessors(nodeName: string): Edges {
|
|
let node = this.index[nodeName];
|
|
if (!node) {
|
|
throw Error("Could not find node with name: " + nodeName);
|
|
}
|
|
|
|
let predecessors = this.getOneWayEdges(node, true);
|
|
|
|
// Add embedded predecessors, such as constants.
|
|
if (!node.isGroupNode) {
|
|
_.each((<OpNode>node).inEmbeddings, embeddedNode => {
|
|
predecessors.regular.push(embeddedNode.name);
|
|
});
|
|
}
|
|
return predecessors;
|
|
}
|
|
|
|
/**
|
|
* Given the name of a node, return an array of the names of its successors.
|
|
* For an OpNode, this will contain the targets from the underlying BaseEdges.
|
|
* For a GroupNode, this will contain the targets truncated to sibling of
|
|
* the shared ancestor.
|
|
*
|
|
* This is the inverse of getPredecessors(). See that method's documentation
|
|
* for an in-depth example.
|
|
*/
|
|
getSuccessors(nodeName: string): Edges {
|
|
let node = this.index[nodeName];
|
|
if (!node) {
|
|
throw Error("Could not find node with name: " + nodeName);
|
|
}
|
|
|
|
let successors = this.getOneWayEdges(node, false);
|
|
|
|
// Add embedded successors, such as summaries.
|
|
if (!node.isGroupNode) {
|
|
_.each((<OpNode>node).outEmbeddings, embeddedNode => {
|
|
successors.regular.push(embeddedNode.name);
|
|
});
|
|
}
|
|
return successors;
|
|
}
|
|
|
|
/** Helper method for getPredeccessors and getSuccessors */
|
|
getOneWayEdges(node: GroupNode|OpNode, inEdges: boolean) {
|
|
let edges = { control: [], regular: [] };
|
|
// A node with no parent cannot have any edges.
|
|
if (!node.parentNode) {
|
|
return edges;
|
|
}
|
|
if (node.parentNode.isGroupNode) {
|
|
let parentNode = <GroupNode>node.parentNode;
|
|
let metagraph = parentNode.metagraph;
|
|
let bridgegraph = this.getBridgegraph(parentNode.name);
|
|
findEdgeTargetsInGraph(metagraph, node, inEdges, edges);
|
|
findEdgeTargetsInGraph(bridgegraph, node, inEdges, edges);
|
|
}
|
|
return edges;
|
|
}
|
|
|
|
/**
|
|
* For a given GroupNode, get or calculate an object which describes a
|
|
* topological ordering of child nodes within that GroupNode's metagraph.
|
|
*
|
|
* This ordering is used when rendering bridge control edges which are
|
|
* sometimes backwards relative to the dataflow.
|
|
*
|
|
* For example, say we have a graph with two edges A->B and A->C, and we're
|
|
* interested in the ordering under ROOT. In this case, any of the following
|
|
* would be legitimate return values:
|
|
*
|
|
* - { "A": 0, "B": 1, "C": 2 } -- most likely
|
|
* - { "A": 0, "B": 2, "C": 1 } -- less likely
|
|
* - { "A": 12, "B": 100, "C": 99 } -- unlikely, but still OK
|
|
*
|
|
* The algorithm does not guarantee that all numbers from 0-N (where N is
|
|
* the number of nodes) appear exactly once. Rather it guarantees that if
|
|
* there is a path between two nodes, the earlier one will have a lower
|
|
* number in the ordering hash.
|
|
*
|
|
* When generating the ordering, we ignore control Metaedges (those which
|
|
* represent only BaseEdges that have isControlDependency set to true).
|
|
*
|
|
* If there is no node with the specified name, an error is thrown. If the
|
|
* node with the specified name is not a group node, null is returned.
|
|
*/
|
|
getTopologicalOrdering(nodeName: string): { [childName: string]: number } {
|
|
let node = this.index[nodeName];
|
|
if (!node) {
|
|
throw Error("Could not find node with name: " + nodeName);
|
|
}
|
|
if (!node.isGroupNode) {
|
|
return null;
|
|
}
|
|
if (nodeName in this.orderings) {
|
|
return this.orderings[nodeName];
|
|
}
|
|
|
|
// Mapping of a child node names to lists of their successors.
|
|
let successors: { [childName: string]: string[] } = {};
|
|
|
|
// Set of node names which have appeared as a destination.
|
|
let destinations: { [childName: string]: boolean } = {};
|
|
|
|
let metagraph = (<GroupNode> node).metagraph;
|
|
_.each(metagraph.edges(), (e: graphlib.EdgeObject) => {
|
|
if (!metagraph.edge(e).numRegularEdges) {
|
|
return; // Skip control edges.
|
|
}
|
|
|
|
// Keep track of successors and destinations.
|
|
if (!(e.v in successors)) {
|
|
successors[e.v] = [];
|
|
}
|
|
successors[e.v].push(e.w);
|
|
destinations[e.w] = true;
|
|
});
|
|
|
|
// Seed the queue with true sources (those that are not destinations).
|
|
let queue: string[] =
|
|
_.difference(_.keys(successors), _.keys(destinations));
|
|
|
|
// Produce an ordering by traversing the graph breadth first.
|
|
let ordering = this.orderings[nodeName] = {};
|
|
let index = 0;
|
|
while (queue.length) {
|
|
let childName = queue.shift();
|
|
ordering[childName] = index++;
|
|
_.each(successors[childName], succName => queue.push(succName));
|
|
delete successors[childName]; // Prevent cycles from infinite looping.
|
|
}
|
|
return ordering;
|
|
}
|
|
|
|
/**
|
|
* Returns a d3 Ordinal function that can be used to look up the index of
|
|
* a node based on its template id.
|
|
*/
|
|
getTemplateIndex(): (string) => number {
|
|
let templateNames = d3.keys(this.templates);
|
|
let templateIndex = d3.scale.ordinal()
|
|
.domain(templateNames)
|
|
.range(d3.range(0, templateNames.length));
|
|
return (templateId: string) => <number>templateIndex(templateId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal utility function - given a graph (should be either a metagraph or a
|
|
* bridgegraph) and a node which is known to be in that graph, determine
|
|
* the other ends of edges that involve that node in the direction specified
|
|
* by whether it's inbound.
|
|
*
|
|
* For example if you wanted to find the predecessors of a node, you'd call
|
|
* this method for the parent's metagraph and bridgegraph, specifying inbound
|
|
* as true (look at the source of inbound edges to the specified node).
|
|
*
|
|
* Discovered target names are appended to the targets array.
|
|
*/
|
|
function findEdgeTargetsInGraph(
|
|
graph: graphlib.Graph<GroupNode|OpNode, Metaedge>,
|
|
node: Node, inbound: boolean, targets: Edges): void {
|
|
_.each(<Metaedge[]> graph.edges(), e => {
|
|
let [selfName, otherName] = inbound ? [e.w, e.v] : [e.v, e.w];
|
|
if (selfName === node.name) {
|
|
if (node.isGroupNode) {
|
|
let targetList = graph.edge(e).numRegularEdges
|
|
? targets.regular : targets.control;
|
|
targetList.push(otherName);
|
|
} else {
|
|
_.each(graph.edge(e).baseEdgeList, baseEdge => {
|
|
let targetList = baseEdge.isControlDependency
|
|
? targets.control : targets.regular;
|
|
targetList.push(inbound ? baseEdge.v : baseEdge.w);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
export interface HierarchyParams {
|
|
verifyTemplate: boolean;
|
|
seriesNodeMinSize: number;
|
|
}
|
|
|
|
/**
|
|
* @param graph The raw graph.
|
|
* @param params Parameters used when building a hierarchy.
|
|
*/
|
|
export function build(graph: tf.graph.SlimGraph, params: HierarchyParams,
|
|
tracker: ProgressTracker): Promise<Hierarchy|void> {
|
|
let h = new HierarchyImpl();
|
|
let seriesNames: { [name: string]: string } = {};
|
|
return runAsyncTask("Adding nodes", 20, () => {
|
|
// Get all the possible device names.
|
|
let deviceNames = {};
|
|
_.each(graph.nodes, (node, nodeName) => {
|
|
if (node.device != null) {
|
|
deviceNames[node.device] = true;
|
|
}
|
|
});
|
|
h.devices = _.keys(deviceNames);
|
|
addNodes(h, graph);
|
|
}, tracker)
|
|
.then(() => {
|
|
return runAsyncTask("Detect series", 20, () => {
|
|
if (params.seriesNodeMinSize > 0) {
|
|
groupSeries(h.root, h, seriesNames, params.seriesNodeMinSize);
|
|
}
|
|
}, tracker);
|
|
})
|
|
.then(() => {
|
|
return runAsyncTask("Adding edges", 30, () => {
|
|
addEdges(h, graph, seriesNames);
|
|
}, tracker);
|
|
})
|
|
.then(() => {
|
|
return runAsyncTask("Finding similar subgraphs", 30, () => {
|
|
h.templates = template.detect(h, params.verifyTemplate);
|
|
}, tracker);
|
|
})
|
|
.then(() => {
|
|
return h;
|
|
}).catch(function(reason) {
|
|
throw new Error("Failure creating graph hierarchy");
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates the metanodes in the hierarchical graph and assigns parent-child
|
|
* relationship between them.
|
|
*/
|
|
function addNodes(h: Hierarchy, graph: SlimGraph) {
|
|
_.each(graph.nodes, (node, nodeName) => {
|
|
let path = getHierarchicalPath(node.name);
|
|
let parent: Metanode = h.root;
|
|
|
|
parent.depth = Math.max(path.length, parent.depth);
|
|
|
|
// Create parent metanodes for each depth. For example if the node name
|
|
// is 'a/b/c', then create metanodes 'a' and 'a/b', where 'a/b' is a child
|
|
// of a.
|
|
for (let i = 0; i < path.length; i++) {
|
|
parent.depth = Math.max(parent.depth, path.length - i);
|
|
parent.cardinality += node.cardinality;
|
|
parent.opHistogram[node.op] = (parent.opHistogram[node.op] || 0) + 1;
|
|
if (node.stats) {
|
|
parent.stats.combine(node.stats);
|
|
}
|
|
if (node.device != null) {
|
|
parent.deviceHistogram[node.device] =
|
|
(parent.deviceHistogram[node.device] || 0) + 1;
|
|
}
|
|
if (i === path.length - 1) { break; }
|
|
let name = path[i];
|
|
let child = <Metanode>h.node(name);
|
|
if (!child) {
|
|
child = createMetanode(name);
|
|
child.parentNode = parent;
|
|
h.setNode(name, child);
|
|
parent.metagraph.setNode(name, child);
|
|
}
|
|
parent = child;
|
|
}
|
|
// Assuming node name is 'a/b/c', assign the OpNode as a child of the metanode 'a/b'.
|
|
h.setNode(node.name, node);
|
|
node.parentNode = parent;
|
|
parent.metagraph.setNode(node.name, node);
|
|
|
|
// Add each of the in-embeddings and out-embeddings in the hierarchy.
|
|
_.each(node.inEmbeddings, function(embedding) {
|
|
h.setNode(embedding.name, embedding);
|
|
embedding.parentNode = node;
|
|
});
|
|
_.each(node.outEmbeddings, function(embedding) {
|
|
h.setNode(embedding.name, embedding);
|
|
embedding.parentNode = node;
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* For each metanode in the hierarchical graph, this method adds:
|
|
* the edges in the metagraph. These are edges between nodes
|
|
* that share the same parent.
|
|
*/
|
|
function addEdges(h: Hierarchy, graph: SlimGraph,
|
|
seriesNames: { [name: string]: string }) {
|
|
|
|
let nodeIndex = h.getNodeMap();
|
|
|
|
// Ancestor paths for the source and destination nodes of an edge. These are
|
|
// reused for each edge rather than allocating new ones. It's about 10% faster
|
|
// than allocating new ones on each pass through the loop.
|
|
let sourcePath: string[] = [];
|
|
let destPath: string[] = [];
|
|
|
|
// Insert the ancestor path for a node into the provided array, including the
|
|
// node itself. Return the index of the last node inserted (always ROOT).
|
|
let getPath = (node: Node, path: string[]): number => {
|
|
let i = 0;
|
|
while (node) {
|
|
path[i++] = node.name;
|
|
node = node.parentNode;
|
|
}
|
|
return i - 1;
|
|
};
|
|
|
|
_.each(graph.edges, baseEdge => {
|
|
|
|
// Get the hierarchical paths for the source and destination of the edge.
|
|
let sourceAncestorIndex = getPath(graph.nodes[baseEdge.v], sourcePath);
|
|
let destAncestorIndex = getPath(graph.nodes[baseEdge.w], destPath);
|
|
|
|
// Find the lowest shared ancestor between source and dest by looking for
|
|
// the highest nodes that differ between their ancestor paths.
|
|
while (sourcePath[sourceAncestorIndex] === destPath[destAncestorIndex]) {
|
|
sourceAncestorIndex--;
|
|
destAncestorIndex--;
|
|
if (sourceAncestorIndex < 0 || destAncestorIndex < 0) {
|
|
// This would only occur if the two nodes were the same (a cycle in the
|
|
// graph), or if one endpoint was a strict ancestor of the other. The
|
|
// latter shouldn't happen because we rename nodes which are both
|
|
// metanodes and op nodes. E.g. "A/B" becomes "A/B/(B)".
|
|
throw Error("No difference found between ancestor paths.");
|
|
}
|
|
}
|
|
|
|
let sharedAncestorNode =
|
|
<GroupNode>nodeIndex[sourcePath[sourceAncestorIndex + 1]];
|
|
let sourceAncestorName = sourcePath[sourceAncestorIndex];
|
|
let destAncestorName = destPath[destAncestorIndex];
|
|
|
|
// Find or create the Metaedge which should contain this BaseEdge inside
|
|
// the shared ancestor.
|
|
let metaedge =
|
|
sharedAncestorNode.metagraph.edge(sourceAncestorName, destAncestorName);
|
|
if (!metaedge) {
|
|
metaedge = createMetaedge(sourceAncestorName, destAncestorName);
|
|
sharedAncestorNode.metagraph
|
|
.setEdge(sourceAncestorName, destAncestorName, metaedge);
|
|
}
|
|
if (!sharedAncestorNode.hasNonControlEdges &&
|
|
!baseEdge.isControlDependency) {
|
|
sharedAncestorNode.hasNonControlEdges = true;
|
|
}
|
|
metaedge.addBaseEdge(baseEdge);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
/**
|
|
* Using the hierarchy template information, detect series in the provided
|
|
* metanode. For each detected series, create a new SeriesNode
|
|
* and remove series members from the metanode's metagraph and move them to
|
|
* the new series node's metagraph.
|
|
*
|
|
* @param metanode
|
|
* @param hierarchy
|
|
* @param threshold If the series has this many nodes or more, then group them
|
|
* into a series.
|
|
* @return A dictionary from node name to series node name that contains the node
|
|
*/
|
|
function groupSeries(metanode: Metanode, hierarchy: Hierarchy,
|
|
seriesNames: { [name: string]: string }, threshold: number) {
|
|
let metagraph = metanode.metagraph;
|
|
_.each(metagraph.nodes(), n => {
|
|
let child = metagraph.node(n);
|
|
if (child.type === tf.graph.NodeType.META) {
|
|
groupSeries(<Metanode>child, hierarchy, seriesNames, threshold);
|
|
}
|
|
});
|
|
|
|
let clusters = clusterNodes(metagraph);
|
|
let seriesDict = detectSeries(clusters, metagraph);
|
|
|
|
// Add each series node to the graph and add its grouped children to its own
|
|
// metagraph.
|
|
_.each(seriesDict, function(seriesNode: SeriesNode, seriesName: string) {
|
|
let nodeMemberNames = seriesNode.metagraph.nodes();
|
|
if (nodeMemberNames.length < threshold) {
|
|
return;
|
|
}
|
|
let firstMember = seriesNode.metagraph.node(nodeMemberNames[0]);
|
|
let seriesType = firstMember.type;
|
|
|
|
hierarchy.setNode(seriesName, seriesNode); // add to the index
|
|
metagraph.setNode(seriesName, seriesNode);
|
|
_.each(nodeMemberNames, n => {
|
|
let child = <OpNode> metagraph.node(n);
|
|
seriesNode.metagraph.setNode(n, child);
|
|
seriesNode.parentNode = child.parentNode;
|
|
seriesNode.cardinality++;
|
|
if (child.device != null) {
|
|
seriesNode.deviceHistogram[child.device] =
|
|
(seriesNode.deviceHistogram[child.device] || 0) + 1;
|
|
}
|
|
child.parentNode = seriesNode;
|
|
seriesNames[n] = seriesName;
|
|
|
|
if (child.stats) {
|
|
seriesNode.stats.combine(child.stats);
|
|
}
|
|
|
|
// Remove now-grouped node from its original parent's metagraph.
|
|
metagraph.removeNode(n);
|
|
});
|
|
});
|
|
};
|
|
|
|
/** cluster op-nodes with similar op */
|
|
function clusterNodes(metagraph: graphlib.Graph<GroupNode|OpNode, Metaedge>):
|
|
{[clusterId: string]: string[]} {
|
|
let result: {[clusterId: string]: string[]} = {};
|
|
return _.reduce(metagraph.nodes(), function(clusters: {[clusterId: string]: string[]}, n: string) {
|
|
let child = metagraph.node(n);
|
|
if (child.type === NodeType.META) {
|
|
// skip metanodes
|
|
return clusters;
|
|
}
|
|
let template = (<OpNode>child).op;
|
|
if (template) {
|
|
clusters[template] = clusters[template] || [];
|
|
clusters[template].push(child.name);
|
|
}
|
|
return clusters;
|
|
}, result);
|
|
}
|
|
|
|
/**
|
|
* For each cluster of op-nodes based op type, try to detect groupings.
|
|
* Infer series name using by trying to find pattern "<number>" in the node
|
|
* name.
|
|
*
|
|
* @param clusters Dictionary output from clusterNodes().
|
|
* @param metagraph
|
|
* @return A dictionary from series name => seriesNode
|
|
*/
|
|
function detectSeries(clusters: {[clusterId: string]: string[]},
|
|
metagraph: graphlib.Graph<GroupNode|OpNode, Metaedge>):
|
|
{[seriesName: string]: SeriesNode} {
|
|
let seriesDict: {[seriesName: string]: SeriesNode} = {};
|
|
_.each(clusters, function(members, clusterId: string) {
|
|
if (members.length <= 1) { return; } // isolated clusters can't make series
|
|
|
|
/** @type {Object} A dictionary mapping seriesName to seriesInfoArray,
|
|
* which is an array that contains objects with name, id, prefix, suffix,
|
|
* and parent properties.
|
|
*/
|
|
let candidatesDict: {[seriesName: string]: SeriesNode[]} = {};
|
|
|
|
// Group all nodes that have the same name, with the exception of a
|
|
// number at the end of the name after an underscore, which is allowed to
|
|
// vary.
|
|
_.each(members, function(name: string) {
|
|
let isGroup = name.charAt(name.length - 1) === "*";
|
|
let namepath = name.split("/");
|
|
let leaf = namepath[namepath.length - 1];
|
|
let parent = namepath.slice(0, namepath.length - 1).join("/");
|
|
let matches = leaf.match(/^(\D*)_(\d+)$/);
|
|
|
|
let prefix;
|
|
let id;
|
|
let suffix = "";
|
|
if (matches) { // if found "<number>" in the name, assign id.
|
|
prefix = matches[1]; // the front non-numeric characters
|
|
id = matches[2]; // the digits
|
|
} else { // for node without "_<number>", make them zero-th items.
|
|
prefix = isGroup ? leaf.substr(0, leaf.length - 1) : leaf;
|
|
if (prefix.charAt(prefix.length - 1) !== "_") {
|
|
prefix += "_";
|
|
}
|
|
id = 0;
|
|
suffix = isGroup ? "*" : "";
|
|
}
|
|
let seriesName = getSeriesNodeName(prefix, suffix, parent);
|
|
candidatesDict[seriesName] = candidatesDict[seriesName] || [];
|
|
let seriesNode = createSeriesNode(prefix, suffix, parent, +id, name);
|
|
candidatesDict[seriesName].push(seriesNode);
|
|
});
|
|
|
|
// In each group of nodes, group nodes in bunches that have monotonically
|
|
// increasing numbers in their names. Each of these bunches is a series.
|
|
_.each(candidatesDict, function(seriesInfoArray: SeriesNode[], seriesName) {
|
|
if (seriesInfoArray.length < 2) {
|
|
return;
|
|
}
|
|
seriesInfoArray.sort(function(a, b) {
|
|
return (+a.clusterId) - (+b.clusterId);
|
|
});
|
|
|
|
// Loop through the nodes sorted by its detected series number, grouping
|
|
// all nodes with monotonically-increasing series numbers.
|
|
let seriesNodes = [seriesInfoArray[0]];
|
|
for (let index = 1; index < seriesInfoArray.length; index++) {
|
|
let nextNode = seriesInfoArray[index];
|
|
if (nextNode.clusterId === seriesNodes[seriesNodes.length - 1].clusterId + 1) {
|
|
seriesNodes.push(nextNode);
|
|
continue;
|
|
}
|
|
addSeriesToDict(seriesNodes, seriesDict, +clusterId, metagraph);
|
|
seriesNodes = [nextNode];
|
|
}
|
|
addSeriesToDict(seriesNodes, seriesDict, +clusterId, metagraph);
|
|
});
|
|
});
|
|
return seriesDict;
|
|
}
|
|
|
|
/**
|
|
* Add a series to the provided dictionary mapping series names to series.
|
|
*
|
|
* @param seriesNodes the nodes in the series. Contains
|
|
* name, id, prefix, suffix and parent properties of the node.
|
|
* @param seriesDict the dictionary of series
|
|
* @param clusterId ID of the template of the nodes of the series
|
|
* @param metagraph
|
|
*/
|
|
function addSeriesToDict(seriesNodes: SeriesNode[],
|
|
seriesDict: {[seriesName: string] : SeriesNode},
|
|
clusterId: number,
|
|
metagraph: graphlib.Graph<GroupNode|OpNode, Metaedge>) {
|
|
if (seriesNodes.length > 1) {
|
|
let curSeriesName = getSeriesNodeName(
|
|
seriesNodes[0].prefix, seriesNodes[0].suffix,
|
|
seriesNodes[0].parent, seriesNodes[0].clusterId,
|
|
seriesNodes[seriesNodes.length - 1].clusterId);
|
|
let curSeriesNode = createSeriesNode(seriesNodes[0].prefix,
|
|
seriesNodes[0].suffix, seriesNodes[0].parent, clusterId,
|
|
curSeriesName);
|
|
_.each(seriesNodes, function(node) {
|
|
curSeriesNode.ids.push(node.clusterId);
|
|
curSeriesNode.metagraph.setNode(node.name, metagraph.node(node.name));
|
|
});
|
|
seriesDict[curSeriesName] = curSeriesNode;
|
|
}
|
|
}
|
|
|
|
} // close module tf.graph.hierarchy
|