262 lines
8.6 KiB
Python
Executable File
262 lines
8.6 KiB
Python
Executable File
#!/usr/bin/python3
|
|
|
|
# Copyright 2019, The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Spec Visualizer
|
|
|
|
Visualize python spec file for test generator.
|
|
Invoked by ml/nn/runtime/test/specs/visualize_spec.sh;
|
|
See that script for details on how this script is used.
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from string import Template
|
|
|
|
# Stuff from test generator
|
|
import test_generator as tg
|
|
from test_generator import ActivationConverter
|
|
from test_generator import BoolScalar
|
|
from test_generator import Configuration
|
|
from test_generator import DataTypeConverter
|
|
from test_generator import DataLayoutConverter
|
|
from test_generator import Example
|
|
from test_generator import Float16Scalar
|
|
from test_generator import Float32Scalar
|
|
from test_generator import Float32Vector
|
|
from test_generator import GetJointStr
|
|
from test_generator import IgnoredOutput
|
|
from test_generator import Input
|
|
from test_generator import Int32Scalar
|
|
from test_generator import Int32Vector
|
|
from test_generator import Internal
|
|
from test_generator import Model
|
|
from test_generator import Operand
|
|
from test_generator import Output
|
|
from test_generator import Parameter
|
|
from test_generator import RelaxedModeConverter
|
|
from test_generator import SymmPerChannelQuantParams
|
|
|
|
|
|
TEMPLATE_FILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), "spec_viz_template.html")
|
|
global_graphs = dict()
|
|
|
|
|
|
def FormatArray(data, is_scalar=False):
|
|
if is_scalar:
|
|
assert len(data) == 1
|
|
return str(data[0])
|
|
else:
|
|
return "[%s]" % (", ".join(str(i) for i in data))
|
|
|
|
|
|
def FormatDict(data):
|
|
return "<br/>".join("<b>%s:</b> %s"%(k.capitalize(), v) for k, v in data.items())
|
|
|
|
|
|
def GetOperandInfo(op):
|
|
op_info = {"lifetime": op.lifetime, "type": op.type.type}
|
|
|
|
if not op.type.IsScalar():
|
|
op_info["dimensions"] = FormatArray(op.type.dimensions)
|
|
|
|
if op.type.scale != 0:
|
|
op_info["scale"] = op.type.scale
|
|
op_info["zero point"] = op.type.zeroPoint
|
|
if op.type.type == "TENSOR_QUANT8_SYMM_PER_CHANNEL":
|
|
op_info["scale"] = FormatArray(op.type.extraParams.scales)
|
|
op_info["channel dim"] = op.type.extraParams.channelDim
|
|
|
|
return op_info
|
|
|
|
|
|
def FormatOperand(op):
|
|
# All keys and values in op_info will appear in the tooltip. We only display the operand data
|
|
# if the length is less than 10. This should be convenient enough for most parameters.
|
|
op_info = GetOperandInfo(op)
|
|
if isinstance(op, Parameter) and len(op.value) <= 10:
|
|
op_info["data"] = FormatArray(op.value, op.type.IsScalar())
|
|
|
|
template = "<span class='tooltip'><span class='tooltipcontent'>{tooltip_content}</span><a href=\"{inpage_link}\">{op_name}</a></span>"
|
|
return template.format(
|
|
op_name=str(op),
|
|
tooltip_content=FormatDict(op_info),
|
|
inpage_link="#details-operands-%d" % (op.model_index),
|
|
)
|
|
|
|
|
|
def GetSubgraph(example):
|
|
"""Produces the nodes and edges information for d3 visualization."""
|
|
|
|
node_index_map = {}
|
|
topological_order = []
|
|
|
|
def AddToTopologicalOrder(op):
|
|
if op not in node_index_map:
|
|
node_index_map[op] = len(topological_order)
|
|
topological_order.append(op)
|
|
|
|
# Get the topological order, both operands and operations are treated the same.
|
|
# Given that the example.model.operations is already topologically sorted, here we simply
|
|
# iterate through and insert inputs and outputs.
|
|
for op in example.model.operations:
|
|
for i in op.ins:
|
|
AddToTopologicalOrder(i)
|
|
AddToTopologicalOrder(op)
|
|
for o in op.outs:
|
|
AddToTopologicalOrder(o)
|
|
|
|
# Assign layers to the nodes.
|
|
layers = {}
|
|
for node in topological_order:
|
|
layers[node] = max([layers[i] for i in node.ins], default=-1) + 1
|
|
for node in reversed(topological_order):
|
|
layers[node] = min([layers[o] for o in node.outs], default=layers[node]+1) - 1
|
|
num_layers = max(layers.values()) + 1
|
|
|
|
# Assign coordinates to the nodes. Nodes are equally spaced.
|
|
CoordX = lambda index: (index + 0.5) * 200 # 200px spacing horizontally
|
|
CoordY = lambda index: (index + 0.5) * 100 # 100px spacing vertically
|
|
coords = {}
|
|
layer_cnt = [0] * num_layers
|
|
for node in topological_order:
|
|
coords[node] = (CoordX(layer_cnt[layers[node]]), CoordY(layers[node]))
|
|
layer_cnt[layers[node]] += 1
|
|
|
|
# Create edges and nodes dictionaries for d3 visualization.
|
|
OpName = lambda idx: "operation%d" % idx
|
|
edges = []
|
|
nodes = []
|
|
for ind, op in enumerate(example.model.operations):
|
|
for tensor in op.ins:
|
|
edges.append({
|
|
"source": str(tensor),
|
|
"target": OpName(ind)
|
|
})
|
|
for tensor in op.outs:
|
|
edges.append({
|
|
"target": str(tensor),
|
|
"source": OpName(ind)
|
|
})
|
|
nodes.append({
|
|
"index": ind,
|
|
"id": OpName(ind),
|
|
"name": op.optype,
|
|
"group": 2,
|
|
"x": coords[op][0],
|
|
"y": coords[op][1],
|
|
})
|
|
|
|
for ind, op in enumerate(example.model.operands):
|
|
nodes.append({
|
|
"index": ind,
|
|
"id": str(op),
|
|
"name": str(op),
|
|
"group": 1,
|
|
"x": coords[op][0],
|
|
"y": coords[op][1],
|
|
})
|
|
|
|
return {"nodes": nodes, "edges": edges}
|
|
|
|
|
|
# The following Get**Info methods will each return a list of dictionaries,
|
|
# whose content will appear in the tables and sidebar views.
|
|
def GetConfigurationsInfo(example):
|
|
return [{
|
|
"relaxed": str(example.model.isRelaxed),
|
|
"use shared memory": str(tg.Configuration.useSHM()),
|
|
"expect failure": str(example.expectFailure),
|
|
}]
|
|
|
|
|
|
def GetOperandsInfo(example):
|
|
ret = []
|
|
for index, op in enumerate(example.model.operands):
|
|
ret.append({
|
|
"index": index,
|
|
"name": str(op),
|
|
"group": "operand"
|
|
})
|
|
ret[-1].update(GetOperandInfo(op))
|
|
if isinstance(op, (Parameter, Input, Output)):
|
|
ret[-1]["data"] = FormatArray(op.value, op.type.IsScalar())
|
|
return ret
|
|
|
|
|
|
def GetOperationsInfo(example):
|
|
return [{
|
|
"index": index,
|
|
"name": op.optype,
|
|
"group": "operation",
|
|
"opcode": op.optype,
|
|
"inputs": ", ".join(FormatOperand(i) for i in op.ins),
|
|
"outputs": ", ".join(FormatOperand(o) for o in op.outs),
|
|
} for index,op in enumerate(example.model.operations)]
|
|
|
|
|
|
# TODO: Remove the unused fd from the parameter.
|
|
def ProcessExample(example, fd):
|
|
"""Process an example and save the information into the global dictionary global_graphs."""
|
|
|
|
global global_graphs
|
|
print(" Processing variation %s" % example.testName)
|
|
global_graphs[str(example.testName)] = {
|
|
"subgraph": GetSubgraph(example),
|
|
"details": {
|
|
"configurations": GetConfigurationsInfo(example),
|
|
"operands": GetOperandsInfo(example),
|
|
"operations": GetOperationsInfo(example)
|
|
}
|
|
}
|
|
|
|
|
|
def DumpHtml(spec_file, out_file):
|
|
"""Dump the final HTML file by replacing entries from a template file."""
|
|
|
|
with open(TEMPLATE_FILE, "r") as template_fd:
|
|
html_template = template_fd.read()
|
|
|
|
with open(out_file, "w") as out_fd:
|
|
out_fd.write(Template(html_template).substitute(
|
|
spec_name=os.path.basename(spec_file),
|
|
graph_dump=json.dumps(global_graphs),
|
|
))
|
|
|
|
|
|
def ParseCmdLine():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("spec", help="the spec file")
|
|
parser.add_argument("-o", "--out", help="the output html path", default="out.html")
|
|
args = parser.parse_args()
|
|
tg.FileNames.InitializeFileLists(args.spec, "-")
|
|
tg.FileNames.NextFile()
|
|
return os.path.abspath(args.spec), os.path.abspath(args.out)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
spec_file, out_file = ParseCmdLine()
|
|
print("Visualizing from spec: %s" % spec_file)
|
|
exec(open(spec_file, "r").read())
|
|
Example.DumpAllExamples(DumpExample=ProcessExample, example_fd=0)
|
|
DumpHtml(spec_file, out_file)
|
|
print("Output HTML file: %s" % out_file)
|
|
|