1661 lines
57 KiB
Python
1661 lines
57 KiB
Python
# Copyright 2016 The TensorFlow Authors. All Rights Reserved.
|
|
#
|
|
# 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.
|
|
# ==============================================================================
|
|
"""CLI Backend for the Analyzer Part of the Debugger.
|
|
|
|
The analyzer performs post hoc analysis of dumped intermediate tensors and
|
|
graph structure information from debugged Session.run() calls.
|
|
"""
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
|
|
import argparse
|
|
import copy
|
|
import re
|
|
|
|
from six.moves import xrange # pylint: disable=redefined-builtin
|
|
|
|
from tensorflow.python.debug.cli import cli_config
|
|
from tensorflow.python.debug.cli import cli_shared
|
|
from tensorflow.python.debug.cli import command_parser
|
|
from tensorflow.python.debug.cli import debugger_cli_common
|
|
from tensorflow.python.debug.cli import evaluator
|
|
from tensorflow.python.debug.cli import ui_factory
|
|
from tensorflow.python.debug.lib import debug_graphs
|
|
from tensorflow.python.debug.lib import source_utils
|
|
|
|
RL = debugger_cli_common.RichLine
|
|
|
|
# String constants for the depth-dependent hanging indent at the beginning
|
|
# of each line.
|
|
HANG_UNFINISHED = "| " # Used for unfinished recursion depths.
|
|
HANG_FINISHED = " "
|
|
HANG_SUFFIX = "|- "
|
|
|
|
# String constant for displaying depth and op type.
|
|
DEPTH_TEMPLATE = "(%d) "
|
|
OP_TYPE_TEMPLATE = "[%s] "
|
|
|
|
# String constants for control inputs/outputs, etc.
|
|
CTRL_LABEL = "(Ctrl) "
|
|
ELLIPSIS = "..."
|
|
|
|
SORT_TENSORS_BY_TIMESTAMP = "timestamp"
|
|
SORT_TENSORS_BY_DUMP_SIZE = "dump_size"
|
|
SORT_TENSORS_BY_OP_TYPE = "op_type"
|
|
SORT_TENSORS_BY_TENSOR_NAME = "tensor_name"
|
|
|
|
|
|
def _add_main_menu(output,
|
|
node_name=None,
|
|
enable_list_tensors=True,
|
|
enable_node_info=True,
|
|
enable_print_tensor=True,
|
|
enable_list_inputs=True,
|
|
enable_list_outputs=True):
|
|
"""Generate main menu for the screen output from a command.
|
|
|
|
Args:
|
|
output: (debugger_cli_common.RichTextLines) the output object to modify.
|
|
node_name: (str or None) name of the node involved (if any). If None,
|
|
the menu items node_info, list_inputs and list_outputs will be
|
|
automatically disabled, overriding the values of arguments
|
|
enable_node_info, enable_list_inputs and enable_list_outputs.
|
|
enable_list_tensors: (bool) whether the list_tensor menu item will be
|
|
enabled.
|
|
enable_node_info: (bool) whether the node_info item will be enabled.
|
|
enable_print_tensor: (bool) whether the print_tensor item will be enabled.
|
|
enable_list_inputs: (bool) whether the item list_inputs will be enabled.
|
|
enable_list_outputs: (bool) whether the item list_outputs will be enabled.
|
|
"""
|
|
|
|
menu = debugger_cli_common.Menu()
|
|
|
|
menu.append(
|
|
debugger_cli_common.MenuItem(
|
|
"list_tensors", "list_tensors", enabled=enable_list_tensors))
|
|
|
|
if node_name:
|
|
menu.append(
|
|
debugger_cli_common.MenuItem(
|
|
"node_info",
|
|
"node_info -a -d -t %s" % node_name,
|
|
enabled=enable_node_info))
|
|
menu.append(
|
|
debugger_cli_common.MenuItem(
|
|
"print_tensor",
|
|
"print_tensor %s" % node_name,
|
|
enabled=enable_print_tensor))
|
|
menu.append(
|
|
debugger_cli_common.MenuItem(
|
|
"list_inputs",
|
|
"list_inputs -c -r %s" % node_name,
|
|
enabled=enable_list_inputs))
|
|
menu.append(
|
|
debugger_cli_common.MenuItem(
|
|
"list_outputs",
|
|
"list_outputs -c -r %s" % node_name,
|
|
enabled=enable_list_outputs))
|
|
else:
|
|
menu.append(
|
|
debugger_cli_common.MenuItem(
|
|
"node_info", None, enabled=False))
|
|
menu.append(
|
|
debugger_cli_common.MenuItem("print_tensor", None, enabled=False))
|
|
menu.append(
|
|
debugger_cli_common.MenuItem("list_inputs", None, enabled=False))
|
|
menu.append(
|
|
debugger_cli_common.MenuItem("list_outputs", None, enabled=False))
|
|
|
|
menu.append(
|
|
debugger_cli_common.MenuItem("run_info", "run_info"))
|
|
menu.append(
|
|
debugger_cli_common.MenuItem("help", "help"))
|
|
|
|
output.annotations[debugger_cli_common.MAIN_MENU_KEY] = menu
|
|
|
|
|
|
class DebugAnalyzer(object):
|
|
"""Analyzer for debug data from dump directories."""
|
|
|
|
_TIMESTAMP_COLUMN_HEAD = "t (ms)"
|
|
_DUMP_SIZE_COLUMN_HEAD = "Size (B)"
|
|
_OP_TYPE_COLUMN_HEAD = "Op type"
|
|
_TENSOR_NAME_COLUMN_HEAD = "Tensor name"
|
|
|
|
# Op types to be omitted when generating descriptions of graph structure.
|
|
_GRAPH_STRUCT_OP_TYPE_DENYLIST = ("_Send", "_Recv", "_HostSend", "_HostRecv",
|
|
"_Retval")
|
|
|
|
def __init__(self, debug_dump, config):
|
|
"""DebugAnalyzer constructor.
|
|
|
|
Args:
|
|
debug_dump: A DebugDumpDir object.
|
|
config: A `cli_config.CLIConfig` object that carries user-facing
|
|
configurations.
|
|
"""
|
|
|
|
self._debug_dump = debug_dump
|
|
self._evaluator = evaluator.ExpressionEvaluator(self._debug_dump)
|
|
|
|
# Initialize tensor filters state.
|
|
self._tensor_filters = {}
|
|
|
|
self._build_argument_parsers(config)
|
|
config.set_callback("graph_recursion_depth",
|
|
self._build_argument_parsers)
|
|
|
|
# TODO(cais): Implement list_nodes.
|
|
|
|
def _build_argument_parsers(self, config):
|
|
"""Build argument parsers for DebugAnalayzer.
|
|
|
|
Args:
|
|
config: A `cli_config.CLIConfig` object.
|
|
|
|
Returns:
|
|
A dict mapping command handler name to `ArgumentParser` instance.
|
|
"""
|
|
# Argument parsers for command handlers.
|
|
self._arg_parsers = {}
|
|
|
|
# Parser for list_tensors.
|
|
ap = argparse.ArgumentParser(
|
|
description="List dumped intermediate tensors.",
|
|
usage=argparse.SUPPRESS)
|
|
ap.add_argument(
|
|
"-f",
|
|
"--tensor_filter",
|
|
dest="tensor_filter",
|
|
type=str,
|
|
default="",
|
|
help="List only Tensors passing the filter of the specified name")
|
|
ap.add_argument(
|
|
"-fenn",
|
|
"--filter_exclude_node_names",
|
|
dest="filter_exclude_node_names",
|
|
type=str,
|
|
default="",
|
|
help="When applying the tensor filter, exclude node with names "
|
|
"matching the regular expression. Applicable only if --tensor_filter "
|
|
"or -f is used.")
|
|
ap.add_argument(
|
|
"-n",
|
|
"--node_name_filter",
|
|
dest="node_name_filter",
|
|
type=str,
|
|
default="",
|
|
help="filter node name by regex.")
|
|
ap.add_argument(
|
|
"-t",
|
|
"--op_type_filter",
|
|
dest="op_type_filter",
|
|
type=str,
|
|
default="",
|
|
help="filter op type by regex.")
|
|
ap.add_argument(
|
|
"-s",
|
|
"--sort_by",
|
|
dest="sort_by",
|
|
type=str,
|
|
default=SORT_TENSORS_BY_TIMESTAMP,
|
|
help=("the field to sort the data by: (%s | %s | %s | %s)" %
|
|
(SORT_TENSORS_BY_TIMESTAMP, SORT_TENSORS_BY_DUMP_SIZE,
|
|
SORT_TENSORS_BY_OP_TYPE, SORT_TENSORS_BY_TENSOR_NAME)))
|
|
ap.add_argument(
|
|
"-r",
|
|
"--reverse",
|
|
dest="reverse",
|
|
action="store_true",
|
|
help="sort the data in reverse (descending) order")
|
|
self._arg_parsers["list_tensors"] = ap
|
|
|
|
# Parser for node_info.
|
|
ap = argparse.ArgumentParser(
|
|
description="Show information about a node.", usage=argparse.SUPPRESS)
|
|
ap.add_argument(
|
|
"node_name",
|
|
type=str,
|
|
help="Name of the node or an associated tensor, e.g., "
|
|
"hidden1/Wx_plus_b/MatMul, hidden1/Wx_plus_b/MatMul:0")
|
|
ap.add_argument(
|
|
"-a",
|
|
"--attributes",
|
|
dest="attributes",
|
|
action="store_true",
|
|
help="Also list attributes of the node.")
|
|
ap.add_argument(
|
|
"-d",
|
|
"--dumps",
|
|
dest="dumps",
|
|
action="store_true",
|
|
help="Also list dumps available from the node.")
|
|
ap.add_argument(
|
|
"-t",
|
|
"--traceback",
|
|
dest="traceback",
|
|
action="store_true",
|
|
help="Also include the traceback of the node's creation "
|
|
"(if available in Python).")
|
|
self._arg_parsers["node_info"] = ap
|
|
|
|
# Parser for list_inputs.
|
|
ap = argparse.ArgumentParser(
|
|
description="Show inputs to a node.", usage=argparse.SUPPRESS)
|
|
ap.add_argument(
|
|
"node_name",
|
|
type=str,
|
|
help="Name of the node or an output tensor from the node, e.g., "
|
|
"hidden1/Wx_plus_b/MatMul, hidden1/Wx_plus_b/MatMul:0")
|
|
ap.add_argument(
|
|
"-c", "--control", action="store_true", help="Include control inputs.")
|
|
ap.add_argument(
|
|
"-d",
|
|
"--depth",
|
|
dest="depth",
|
|
type=int,
|
|
default=config.get("graph_recursion_depth"),
|
|
help="Maximum depth of recursion used when showing the input tree.")
|
|
ap.add_argument(
|
|
"-r",
|
|
"--recursive",
|
|
dest="recursive",
|
|
action="store_true",
|
|
help="Show inputs to the node recursively, i.e., the input tree.")
|
|
ap.add_argument(
|
|
"-t",
|
|
"--op_type",
|
|
action="store_true",
|
|
help="Show op types of input nodes.")
|
|
self._arg_parsers["list_inputs"] = ap
|
|
|
|
# Parser for list_outputs.
|
|
ap = argparse.ArgumentParser(
|
|
description="Show the nodes that receive the outputs of given node.",
|
|
usage=argparse.SUPPRESS)
|
|
ap.add_argument(
|
|
"node_name",
|
|
type=str,
|
|
help="Name of the node or an output tensor from the node, e.g., "
|
|
"hidden1/Wx_plus_b/MatMul, hidden1/Wx_plus_b/MatMul:0")
|
|
ap.add_argument(
|
|
"-c", "--control", action="store_true", help="Include control inputs.")
|
|
ap.add_argument(
|
|
"-d",
|
|
"--depth",
|
|
dest="depth",
|
|
type=int,
|
|
default=config.get("graph_recursion_depth"),
|
|
help="Maximum depth of recursion used when showing the output tree.")
|
|
ap.add_argument(
|
|
"-r",
|
|
"--recursive",
|
|
dest="recursive",
|
|
action="store_true",
|
|
help="Show recipients of the node recursively, i.e., the output "
|
|
"tree.")
|
|
ap.add_argument(
|
|
"-t",
|
|
"--op_type",
|
|
action="store_true",
|
|
help="Show op types of recipient nodes.")
|
|
self._arg_parsers["list_outputs"] = ap
|
|
|
|
# Parser for print_tensor.
|
|
self._arg_parsers["print_tensor"] = (
|
|
command_parser.get_print_tensor_argparser(
|
|
"Print the value of a dumped tensor."))
|
|
|
|
# Parser for print_source.
|
|
ap = argparse.ArgumentParser(
|
|
description="Print a Python source file with overlaid debug "
|
|
"information, including the nodes (ops) or Tensors created at the "
|
|
"source lines.",
|
|
usage=argparse.SUPPRESS)
|
|
ap.add_argument(
|
|
"source_file_path",
|
|
type=str,
|
|
help="Path to the source file.")
|
|
ap.add_argument(
|
|
"-t",
|
|
"--tensors",
|
|
dest="tensors",
|
|
action="store_true",
|
|
help="Label lines with dumped Tensors, instead of ops.")
|
|
ap.add_argument(
|
|
"-m",
|
|
"--max_elements_per_line",
|
|
type=int,
|
|
default=10,
|
|
help="Maximum number of elements (ops or Tensors) to show per source "
|
|
"line.")
|
|
ap.add_argument(
|
|
"-b",
|
|
"--line_begin",
|
|
type=int,
|
|
default=1,
|
|
help="Print source beginning at line number (1-based.)")
|
|
self._arg_parsers["print_source"] = ap
|
|
|
|
# Parser for list_source.
|
|
ap = argparse.ArgumentParser(
|
|
description="List source files responsible for constructing nodes and "
|
|
"tensors present in the run().",
|
|
usage=argparse.SUPPRESS)
|
|
ap.add_argument(
|
|
"-p",
|
|
"--path_filter",
|
|
type=str,
|
|
default="",
|
|
help="Regular expression filter for file path.")
|
|
ap.add_argument(
|
|
"-n",
|
|
"--node_name_filter",
|
|
type=str,
|
|
default="",
|
|
help="Regular expression filter for node name.")
|
|
self._arg_parsers["list_source"] = ap
|
|
|
|
# Parser for eval.
|
|
ap = argparse.ArgumentParser(
|
|
description="""Evaluate an arbitrary expression. Can use tensor values
|
|
from the current debug dump. The debug tensor names should be enclosed
|
|
in pairs of backticks. Expressions with spaces should be enclosed in
|
|
a pair of double quotes or a pair of single quotes. By default, numpy
|
|
is imported as np and can be used in the expressions. E.g.,
|
|
1) eval np.argmax(`Softmax:0`),
|
|
2) eval 'np.sum(`Softmax:0`, axis=1)',
|
|
3) eval "np.matmul((`output/Identity:0`/`Softmax:0`).T, `Softmax:0`)".
|
|
""",
|
|
usage=argparse.SUPPRESS)
|
|
ap.add_argument(
|
|
"expression",
|
|
type=str,
|
|
help="""Expression to be evaluated.
|
|
1) in the simplest case, use <node_name>:<output_slot>, e.g.,
|
|
hidden_0/MatMul:0.
|
|
|
|
2) if the default debug op "DebugIdentity" is to be overridden, use
|
|
<node_name>:<output_slot>:<debug_op>, e.g.,
|
|
hidden_0/MatMul:0:DebugNumericSummary.
|
|
|
|
3) if the tensor of the same name exists on more than one device, use
|
|
<device_name>:<node_name>:<output_slot>[:<debug_op>], e.g.,
|
|
/job:worker/replica:0/task:0/gpu:0:hidden_0/MatMul:0
|
|
/job:worker/replica:0/task:2/cpu:0:hidden_0/MatMul:0:DebugNanCount.
|
|
|
|
4) if the tensor is executed multiple times in a given `Session.run`
|
|
call, specify the execution index with a 0-based integer enclose in a
|
|
pair of brackets at the end, e.g.,
|
|
RNN/tanh:0[0]
|
|
/job:worker/replica:0/task:0/gpu:0:RNN/tanh:0[0].""")
|
|
ap.add_argument(
|
|
"-a",
|
|
"--all",
|
|
dest="print_all",
|
|
action="store_true",
|
|
help="Print the tensor in its entirety, i.e., do not use ellipses "
|
|
"(may be slow for large results).")
|
|
ap.add_argument(
|
|
"-w",
|
|
"--write_path",
|
|
default="",
|
|
help="Path of the numpy file to write the evaluation result to, "
|
|
"using numpy.save()")
|
|
self._arg_parsers["eval"] = ap
|
|
|
|
def add_tensor_filter(self, filter_name, filter_callable):
|
|
"""Add a tensor filter.
|
|
|
|
A tensor filter is a named callable of the signature:
|
|
filter_callable(dump_datum, tensor),
|
|
|
|
wherein dump_datum is an instance of debug_data.DebugTensorDatum carrying
|
|
metadata about the dumped tensor, including tensor name, timestamps, etc.
|
|
tensor is the value of the dumped tensor as an numpy.ndarray object.
|
|
The return value of the function is a bool.
|
|
This is the same signature as the input argument to
|
|
debug_data.DebugDumpDir.find().
|
|
|
|
Args:
|
|
filter_name: (str) name of the filter. Cannot be empty.
|
|
filter_callable: (callable) a filter function of the signature described
|
|
as above.
|
|
|
|
Raises:
|
|
ValueError: If filter_name is an empty str.
|
|
TypeError: If filter_name is not a str.
|
|
Or if filter_callable is not callable.
|
|
"""
|
|
|
|
if not isinstance(filter_name, str):
|
|
raise TypeError("Input argument filter_name is expected to be str, "
|
|
"but is not.")
|
|
|
|
# Check that filter_name is not an empty str.
|
|
if not filter_name:
|
|
raise ValueError("Input argument filter_name cannot be empty.")
|
|
|
|
# Check that filter_callable is callable.
|
|
if not callable(filter_callable):
|
|
raise TypeError(
|
|
"Input argument filter_callable is expected to be callable, "
|
|
"but is not.")
|
|
|
|
self._tensor_filters[filter_name] = filter_callable
|
|
|
|
def get_tensor_filter(self, filter_name):
|
|
"""Retrieve filter function by name.
|
|
|
|
Args:
|
|
filter_name: Name of the filter set during add_tensor_filter() call.
|
|
|
|
Returns:
|
|
The callable associated with the filter name.
|
|
|
|
Raises:
|
|
ValueError: If there is no tensor filter of the specified filter name.
|
|
"""
|
|
|
|
if filter_name not in self._tensor_filters:
|
|
raise ValueError("There is no tensor filter named \"%s\"" % filter_name)
|
|
|
|
return self._tensor_filters[filter_name]
|
|
|
|
def get_help(self, handler_name):
|
|
return self._arg_parsers[handler_name].format_help()
|
|
|
|
def list_tensors(self, args, screen_info=None):
|
|
"""Command handler for list_tensors.
|
|
|
|
List tensors dumped during debugged Session.run() call.
|
|
|
|
Args:
|
|
args: Command-line arguments, excluding the command prefix, as a list of
|
|
str.
|
|
screen_info: Optional dict input containing screen information such as
|
|
cols.
|
|
|
|
Returns:
|
|
Output text lines as a RichTextLines object.
|
|
|
|
Raises:
|
|
ValueError: If `--filter_exclude_node_names` is used without `-f` or
|
|
`--tensor_filter` being used.
|
|
"""
|
|
|
|
# TODO(cais): Add annotations of substrings for dumped tensor names, to
|
|
# facilitate on-screen highlighting/selection of node names.
|
|
_ = screen_info
|
|
|
|
parsed = self._arg_parsers["list_tensors"].parse_args(args)
|
|
|
|
output = []
|
|
|
|
filter_strs = []
|
|
if parsed.op_type_filter:
|
|
op_type_regex = re.compile(parsed.op_type_filter)
|
|
filter_strs.append("Op type regex filter: \"%s\"" % parsed.op_type_filter)
|
|
else:
|
|
op_type_regex = None
|
|
|
|
if parsed.node_name_filter:
|
|
node_name_regex = re.compile(parsed.node_name_filter)
|
|
filter_strs.append("Node name regex filter: \"%s\"" %
|
|
parsed.node_name_filter)
|
|
else:
|
|
node_name_regex = None
|
|
|
|
output = debugger_cli_common.RichTextLines(filter_strs)
|
|
output.append("")
|
|
|
|
if parsed.tensor_filter:
|
|
try:
|
|
filter_callable = self.get_tensor_filter(parsed.tensor_filter)
|
|
except ValueError:
|
|
output = cli_shared.error("There is no tensor filter named \"%s\"." %
|
|
parsed.tensor_filter)
|
|
_add_main_menu(output, node_name=None, enable_list_tensors=False)
|
|
return output
|
|
|
|
data_to_show = self._debug_dump.find(
|
|
filter_callable,
|
|
exclude_node_names=parsed.filter_exclude_node_names)
|
|
else:
|
|
if parsed.filter_exclude_node_names:
|
|
raise ValueError(
|
|
"The flag --filter_exclude_node_names is valid only when "
|
|
"the flag -f or --tensor_filter is used.")
|
|
|
|
data_to_show = self._debug_dump.dumped_tensor_data
|
|
|
|
# TODO(cais): Implement filter by lambda on tensor value.
|
|
|
|
max_timestamp_width, max_dump_size_width, max_op_type_width = (
|
|
self._measure_tensor_list_column_widths(data_to_show))
|
|
|
|
# Sort the data.
|
|
data_to_show = self._sort_dump_data_by(
|
|
data_to_show, parsed.sort_by, parsed.reverse)
|
|
|
|
output.extend(
|
|
self._tensor_list_column_heads(parsed, max_timestamp_width,
|
|
max_dump_size_width, max_op_type_width))
|
|
|
|
dump_count = 0
|
|
for dump in data_to_show:
|
|
if node_name_regex and not node_name_regex.match(dump.node_name):
|
|
continue
|
|
|
|
if op_type_regex:
|
|
op_type = self._debug_dump.node_op_type(dump.node_name)
|
|
if not op_type_regex.match(op_type):
|
|
continue
|
|
|
|
rel_time = (dump.timestamp - self._debug_dump.t0) / 1000.0
|
|
dump_size_str = cli_shared.bytes_to_readable_str(dump.dump_size_bytes)
|
|
dumped_tensor_name = "%s:%d" % (dump.node_name, dump.output_slot)
|
|
op_type = self._debug_dump.node_op_type(dump.node_name)
|
|
|
|
line = "[%.3f]" % rel_time
|
|
line += " " * (max_timestamp_width - len(line))
|
|
line += dump_size_str
|
|
line += " " * (max_timestamp_width + max_dump_size_width - len(line))
|
|
line += op_type
|
|
line += " " * (max_timestamp_width + max_dump_size_width +
|
|
max_op_type_width - len(line))
|
|
line += dumped_tensor_name
|
|
|
|
output.append(
|
|
line,
|
|
font_attr_segs=[(
|
|
len(line) - len(dumped_tensor_name), len(line),
|
|
debugger_cli_common.MenuItem("", "pt %s" % dumped_tensor_name))])
|
|
dump_count += 1
|
|
|
|
if parsed.tensor_filter:
|
|
output.prepend([
|
|
"%d dumped tensor(s) passing filter \"%s\":" %
|
|
(dump_count, parsed.tensor_filter)
|
|
])
|
|
else:
|
|
output.prepend(["%d dumped tensor(s):" % dump_count])
|
|
|
|
_add_main_menu(output, node_name=None, enable_list_tensors=False)
|
|
return output
|
|
|
|
def _measure_tensor_list_column_widths(self, data):
|
|
"""Determine the maximum widths of the timestamp and op-type column.
|
|
|
|
This method assumes that data is sorted in the default order, i.e.,
|
|
by ascending timestamps.
|
|
|
|
Args:
|
|
data: (list of DebugTensorDaum) the data based on which the maximum
|
|
column widths will be determined.
|
|
|
|
Returns:
|
|
(int) maximum width of the timestamp column. 0 if data is empty.
|
|
(int) maximum width of the dump size column. 0 if data is empty.
|
|
(int) maximum width of the op type column. 0 if data is empty.
|
|
"""
|
|
|
|
max_timestamp_width = 0
|
|
if data:
|
|
max_rel_time_ms = (data[-1].timestamp - self._debug_dump.t0) / 1000.0
|
|
max_timestamp_width = len("[%.3f] " % max_rel_time_ms) + 1
|
|
max_timestamp_width = max(max_timestamp_width,
|
|
len(self._TIMESTAMP_COLUMN_HEAD) + 1)
|
|
|
|
max_dump_size_width = 0
|
|
for dump in data:
|
|
dump_size_str = cli_shared.bytes_to_readable_str(dump.dump_size_bytes)
|
|
if len(dump_size_str) + 1 > max_dump_size_width:
|
|
max_dump_size_width = len(dump_size_str) + 1
|
|
max_dump_size_width = max(max_dump_size_width,
|
|
len(self._DUMP_SIZE_COLUMN_HEAD) + 1)
|
|
|
|
max_op_type_width = 0
|
|
for dump in data:
|
|
op_type = self._debug_dump.node_op_type(dump.node_name)
|
|
if len(op_type) + 1 > max_op_type_width:
|
|
max_op_type_width = len(op_type) + 1
|
|
max_op_type_width = max(max_op_type_width,
|
|
len(self._OP_TYPE_COLUMN_HEAD) + 1)
|
|
|
|
return max_timestamp_width, max_dump_size_width, max_op_type_width
|
|
|
|
def _sort_dump_data_by(self, data, sort_by, reverse):
|
|
"""Sort a list of DebugTensorDatum in specified order.
|
|
|
|
Args:
|
|
data: (list of DebugTensorDatum) the data to be sorted.
|
|
sort_by: The field to sort data by.
|
|
reverse: (bool) Whether to use reversed (descending) order.
|
|
|
|
Returns:
|
|
(list of DebugTensorDatum) in sorted order.
|
|
|
|
Raises:
|
|
ValueError: given an invalid value of sort_by.
|
|
"""
|
|
|
|
if sort_by == SORT_TENSORS_BY_TIMESTAMP:
|
|
return sorted(
|
|
data,
|
|
reverse=reverse,
|
|
key=lambda x: x.timestamp)
|
|
elif sort_by == SORT_TENSORS_BY_DUMP_SIZE:
|
|
return sorted(data, reverse=reverse, key=lambda x: x.dump_size_bytes)
|
|
elif sort_by == SORT_TENSORS_BY_OP_TYPE:
|
|
return sorted(
|
|
data,
|
|
reverse=reverse,
|
|
key=lambda x: self._debug_dump.node_op_type(x.node_name))
|
|
elif sort_by == SORT_TENSORS_BY_TENSOR_NAME:
|
|
return sorted(
|
|
data,
|
|
reverse=reverse,
|
|
key=lambda x: "%s:%d" % (x.node_name, x.output_slot))
|
|
else:
|
|
raise ValueError("Unsupported key to sort tensors by: %s" % sort_by)
|
|
|
|
def _tensor_list_column_heads(self, parsed, max_timestamp_width,
|
|
max_dump_size_width, max_op_type_width):
|
|
"""Generate a line containing the column heads of the tensor list.
|
|
|
|
Args:
|
|
parsed: Parsed arguments (by argparse) of the list_tensors command.
|
|
max_timestamp_width: (int) maximum width of the timestamp column.
|
|
max_dump_size_width: (int) maximum width of the dump size column.
|
|
max_op_type_width: (int) maximum width of the op type column.
|
|
|
|
Returns:
|
|
A RichTextLines object.
|
|
"""
|
|
|
|
base_command = "list_tensors"
|
|
if parsed.tensor_filter:
|
|
base_command += " -f %s" % parsed.tensor_filter
|
|
if parsed.op_type_filter:
|
|
base_command += " -t %s" % parsed.op_type_filter
|
|
if parsed.node_name_filter:
|
|
base_command += " -n %s" % parsed.node_name_filter
|
|
|
|
attr_segs = {0: []}
|
|
row = self._TIMESTAMP_COLUMN_HEAD
|
|
command = "%s -s %s" % (base_command, SORT_TENSORS_BY_TIMESTAMP)
|
|
if parsed.sort_by == SORT_TENSORS_BY_TIMESTAMP and not parsed.reverse:
|
|
command += " -r"
|
|
attr_segs[0].append(
|
|
(0, len(row), [debugger_cli_common.MenuItem(None, command), "bold"]))
|
|
row += " " * (max_timestamp_width - len(row))
|
|
|
|
prev_len = len(row)
|
|
row += self._DUMP_SIZE_COLUMN_HEAD
|
|
command = "%s -s %s" % (base_command, SORT_TENSORS_BY_DUMP_SIZE)
|
|
if parsed.sort_by == SORT_TENSORS_BY_DUMP_SIZE and not parsed.reverse:
|
|
command += " -r"
|
|
attr_segs[0].append((prev_len, len(row),
|
|
[debugger_cli_common.MenuItem(None, command), "bold"]))
|
|
row += " " * (max_dump_size_width + max_timestamp_width - len(row))
|
|
|
|
prev_len = len(row)
|
|
row += self._OP_TYPE_COLUMN_HEAD
|
|
command = "%s -s %s" % (base_command, SORT_TENSORS_BY_OP_TYPE)
|
|
if parsed.sort_by == SORT_TENSORS_BY_OP_TYPE and not parsed.reverse:
|
|
command += " -r"
|
|
attr_segs[0].append((prev_len, len(row),
|
|
[debugger_cli_common.MenuItem(None, command), "bold"]))
|
|
row += " " * (
|
|
max_op_type_width + max_dump_size_width + max_timestamp_width - len(row)
|
|
)
|
|
|
|
prev_len = len(row)
|
|
row += self._TENSOR_NAME_COLUMN_HEAD
|
|
command = "%s -s %s" % (base_command, SORT_TENSORS_BY_TENSOR_NAME)
|
|
if parsed.sort_by == SORT_TENSORS_BY_TENSOR_NAME and not parsed.reverse:
|
|
command += " -r"
|
|
attr_segs[0].append((prev_len, len(row),
|
|
[debugger_cli_common.MenuItem("", command), "bold"]))
|
|
row += " " * (
|
|
max_op_type_width + max_dump_size_width + max_timestamp_width - len(row)
|
|
)
|
|
|
|
return debugger_cli_common.RichTextLines([row], font_attr_segs=attr_segs)
|
|
|
|
def node_info(self, args, screen_info=None):
|
|
"""Command handler for node_info.
|
|
|
|
Query information about a given node.
|
|
|
|
Args:
|
|
args: Command-line arguments, excluding the command prefix, as a list of
|
|
str.
|
|
screen_info: Optional dict input containing screen information such as
|
|
cols.
|
|
|
|
Returns:
|
|
Output text lines as a RichTextLines object.
|
|
"""
|
|
|
|
# TODO(cais): Add annotation of substrings for node names, to facilitate
|
|
# on-screen highlighting/selection of node names.
|
|
_ = screen_info
|
|
|
|
parsed = self._arg_parsers["node_info"].parse_args(args)
|
|
|
|
# Get a node name, regardless of whether the input is a node name (without
|
|
# output slot attached) or a tensor name (with output slot attached).
|
|
node_name, unused_slot = debug_graphs.parse_node_or_tensor_name(
|
|
parsed.node_name)
|
|
|
|
if not self._debug_dump.node_exists(node_name):
|
|
output = cli_shared.error(
|
|
"There is no node named \"%s\" in the partition graphs" % node_name)
|
|
_add_main_menu(
|
|
output,
|
|
node_name=None,
|
|
enable_list_tensors=True,
|
|
enable_node_info=False,
|
|
enable_list_inputs=False,
|
|
enable_list_outputs=False)
|
|
return output
|
|
|
|
# TODO(cais): Provide UI glossary feature to explain to users what the
|
|
# term "partition graph" means and how it is related to TF graph objects
|
|
# in Python. The information can be along the line of:
|
|
# "A tensorflow graph defined in Python is stripped of unused ops
|
|
# according to the feeds and fetches and divided into a number of
|
|
# partition graphs that may be distributed among multiple devices and
|
|
# hosts. The partition graphs are what's actually executed by the C++
|
|
# runtime during a run() call."
|
|
|
|
lines = ["Node %s" % node_name]
|
|
font_attr_segs = {
|
|
0: [(len(lines[-1]) - len(node_name), len(lines[-1]), "bold")]
|
|
}
|
|
lines.append("")
|
|
lines.append(" Op: %s" % self._debug_dump.node_op_type(node_name))
|
|
lines.append(" Device: %s" % self._debug_dump.node_device(node_name))
|
|
output = debugger_cli_common.RichTextLines(
|
|
lines, font_attr_segs=font_attr_segs)
|
|
|
|
# List node inputs (non-control and control).
|
|
inputs = self._exclude_denylisted_ops(
|
|
self._debug_dump.node_inputs(node_name))
|
|
ctrl_inputs = self._exclude_denylisted_ops(
|
|
self._debug_dump.node_inputs(node_name, is_control=True))
|
|
output.extend(self._format_neighbors("input", inputs, ctrl_inputs))
|
|
|
|
# List node output recipients (non-control and control).
|
|
recs = self._exclude_denylisted_ops(
|
|
self._debug_dump.node_recipients(node_name))
|
|
ctrl_recs = self._exclude_denylisted_ops(
|
|
self._debug_dump.node_recipients(node_name, is_control=True))
|
|
output.extend(self._format_neighbors("recipient", recs, ctrl_recs))
|
|
|
|
# Optional: List attributes of the node.
|
|
if parsed.attributes:
|
|
output.extend(self._list_node_attributes(node_name))
|
|
|
|
# Optional: List dumps available from the node.
|
|
if parsed.dumps:
|
|
output.extend(self._list_node_dumps(node_name))
|
|
|
|
if parsed.traceback:
|
|
output.extend(self._render_node_traceback(node_name))
|
|
|
|
_add_main_menu(output, node_name=node_name, enable_node_info=False)
|
|
return output
|
|
|
|
def _exclude_denylisted_ops(self, node_names):
|
|
"""Exclude all nodes whose op types are in _GRAPH_STRUCT_OP_TYPE_DENYLIST.
|
|
|
|
Args:
|
|
node_names: An iterable of node or graph element names.
|
|
|
|
Returns:
|
|
A list of node names that are not denylisted.
|
|
"""
|
|
return [
|
|
node_name for node_name in node_names
|
|
if self._debug_dump.node_op_type(debug_graphs.get_node_name(node_name))
|
|
not in self._GRAPH_STRUCT_OP_TYPE_DENYLIST
|
|
]
|
|
|
|
def _render_node_traceback(self, node_name):
|
|
"""Render traceback of a node's creation in Python, if available.
|
|
|
|
Args:
|
|
node_name: (str) name of the node.
|
|
|
|
Returns:
|
|
A RichTextLines object containing the stack trace of the node's
|
|
construction.
|
|
"""
|
|
|
|
lines = [RL(""), RL(""), RL("Traceback of node construction:", "bold")]
|
|
|
|
try:
|
|
node_stack = self._debug_dump.node_traceback(node_name)
|
|
for depth, (file_path, line, function_name, text) in enumerate(
|
|
node_stack):
|
|
lines.append("%d: %s" % (depth, file_path))
|
|
|
|
attribute = debugger_cli_common.MenuItem(
|
|
"", "ps %s -b %d" % (file_path, line)) if text else None
|
|
line_number_line = RL(" ")
|
|
line_number_line += RL("Line: %d" % line, attribute)
|
|
lines.append(line_number_line)
|
|
|
|
lines.append(" Function: %s" % function_name)
|
|
lines.append(" Text: " + (("\"%s\"" % text) if text else "None"))
|
|
lines.append("")
|
|
except KeyError:
|
|
lines.append("(Node unavailable in the loaded Python graph)")
|
|
except LookupError:
|
|
lines.append("(Unavailable because no Python graph has been loaded)")
|
|
|
|
return debugger_cli_common.rich_text_lines_from_rich_line_list(lines)
|
|
|
|
def list_inputs(self, args, screen_info=None):
|
|
"""Command handler for inputs.
|
|
|
|
Show inputs to a given node.
|
|
|
|
Args:
|
|
args: Command-line arguments, excluding the command prefix, as a list of
|
|
str.
|
|
screen_info: Optional dict input containing screen information such as
|
|
cols.
|
|
|
|
Returns:
|
|
Output text lines as a RichTextLines object.
|
|
"""
|
|
|
|
# Screen info not currently used by this handler. Include this line to
|
|
# mute pylint.
|
|
_ = screen_info
|
|
# TODO(cais): Use screen info to format the output lines more prettily,
|
|
# e.g., hanging indent of long node names.
|
|
|
|
parsed = self._arg_parsers["list_inputs"].parse_args(args)
|
|
|
|
output = self._list_inputs_or_outputs(
|
|
parsed.recursive,
|
|
parsed.node_name,
|
|
parsed.depth,
|
|
parsed.control,
|
|
parsed.op_type,
|
|
do_outputs=False)
|
|
|
|
node_name = debug_graphs.get_node_name(parsed.node_name)
|
|
_add_main_menu(output, node_name=node_name, enable_list_inputs=False)
|
|
|
|
return output
|
|
|
|
def print_tensor(self, args, screen_info=None):
|
|
"""Command handler for print_tensor.
|
|
|
|
Print value of a given dumped tensor.
|
|
|
|
Args:
|
|
args: Command-line arguments, excluding the command prefix, as a list of
|
|
str.
|
|
screen_info: Optional dict input containing screen information such as
|
|
cols.
|
|
|
|
Returns:
|
|
Output text lines as a RichTextLines object.
|
|
"""
|
|
|
|
parsed = self._arg_parsers["print_tensor"].parse_args(args)
|
|
|
|
np_printoptions = cli_shared.numpy_printoptions_from_screen_info(
|
|
screen_info)
|
|
|
|
# Determine if any range-highlighting is required.
|
|
highlight_options = cli_shared.parse_ranges_highlight(parsed.ranges)
|
|
|
|
tensor_name, tensor_slicing = (
|
|
command_parser.parse_tensor_name_with_slicing(parsed.tensor_name))
|
|
|
|
node_name, output_slot = debug_graphs.parse_node_or_tensor_name(tensor_name)
|
|
if (self._debug_dump.loaded_partition_graphs() and
|
|
not self._debug_dump.node_exists(node_name)):
|
|
output = cli_shared.error(
|
|
"Node \"%s\" does not exist in partition graphs" % node_name)
|
|
_add_main_menu(
|
|
output,
|
|
node_name=None,
|
|
enable_list_tensors=True,
|
|
enable_print_tensor=False)
|
|
return output
|
|
|
|
watch_keys = self._debug_dump.debug_watch_keys(node_name)
|
|
if output_slot is None:
|
|
output_slots = set()
|
|
for watch_key in watch_keys:
|
|
output_slots.add(int(watch_key.split(":")[1]))
|
|
|
|
if len(output_slots) == 1:
|
|
# There is only one dumped tensor from this node, so there is no
|
|
# ambiguity. Proceed to show the only dumped tensor.
|
|
output_slot = list(output_slots)[0]
|
|
else:
|
|
# There are more than one dumped tensors from this node. Indicate as
|
|
# such.
|
|
# TODO(cais): Provide an output screen with command links for
|
|
# convenience.
|
|
lines = [
|
|
"Node \"%s\" generated debug dumps from %s output slots:" %
|
|
(node_name, len(output_slots)),
|
|
"Please specify the output slot: %s:x." % node_name
|
|
]
|
|
output = debugger_cli_common.RichTextLines(lines)
|
|
_add_main_menu(
|
|
output,
|
|
node_name=node_name,
|
|
enable_list_tensors=True,
|
|
enable_print_tensor=False)
|
|
return output
|
|
|
|
# Find debug dump data that match the tensor name (node name + output
|
|
# slot).
|
|
matching_data = []
|
|
for watch_key in watch_keys:
|
|
debug_tensor_data = self._debug_dump.watch_key_to_data(watch_key)
|
|
for datum in debug_tensor_data:
|
|
if datum.output_slot == output_slot:
|
|
matching_data.append(datum)
|
|
|
|
if not matching_data:
|
|
# No dump for this tensor.
|
|
output = cli_shared.error("Tensor \"%s\" did not generate any dumps." %
|
|
parsed.tensor_name)
|
|
elif len(matching_data) == 1:
|
|
# There is only one dump for this tensor.
|
|
if parsed.number <= 0:
|
|
output = cli_shared.format_tensor(
|
|
matching_data[0].get_tensor(),
|
|
matching_data[0].watch_key,
|
|
np_printoptions,
|
|
print_all=parsed.print_all,
|
|
tensor_slicing=tensor_slicing,
|
|
highlight_options=highlight_options,
|
|
include_numeric_summary=parsed.numeric_summary,
|
|
write_path=parsed.write_path)
|
|
else:
|
|
output = cli_shared.error(
|
|
"Invalid number (%d) for tensor %s, which generated one dump." %
|
|
(parsed.number, parsed.tensor_name))
|
|
|
|
_add_main_menu(output, node_name=node_name, enable_print_tensor=False)
|
|
else:
|
|
# There are more than one dumps for this tensor.
|
|
if parsed.number < 0:
|
|
lines = [
|
|
"Tensor \"%s\" generated %d dumps:" % (parsed.tensor_name,
|
|
len(matching_data))
|
|
]
|
|
font_attr_segs = {}
|
|
|
|
for i, datum in enumerate(matching_data):
|
|
rel_time = (datum.timestamp - self._debug_dump.t0) / 1000.0
|
|
lines.append("#%d [%.3f ms] %s" % (i, rel_time, datum.watch_key))
|
|
command = "print_tensor %s -n %d" % (parsed.tensor_name, i)
|
|
font_attr_segs[len(lines) - 1] = [(
|
|
len(lines[-1]) - len(datum.watch_key), len(lines[-1]),
|
|
debugger_cli_common.MenuItem(None, command))]
|
|
|
|
lines.append("")
|
|
lines.append(
|
|
"You can use the -n (--number) flag to specify which dump to "
|
|
"print.")
|
|
lines.append("For example:")
|
|
lines.append(" print_tensor %s -n 0" % parsed.tensor_name)
|
|
|
|
output = debugger_cli_common.RichTextLines(
|
|
lines, font_attr_segs=font_attr_segs)
|
|
elif parsed.number >= len(matching_data):
|
|
output = cli_shared.error(
|
|
"Specified number (%d) exceeds the number of available dumps "
|
|
"(%d) for tensor %s" %
|
|
(parsed.number, len(matching_data), parsed.tensor_name))
|
|
else:
|
|
output = cli_shared.format_tensor(
|
|
matching_data[parsed.number].get_tensor(),
|
|
matching_data[parsed.number].watch_key + " (dump #%d)" %
|
|
parsed.number,
|
|
np_printoptions,
|
|
print_all=parsed.print_all,
|
|
tensor_slicing=tensor_slicing,
|
|
highlight_options=highlight_options,
|
|
write_path=parsed.write_path)
|
|
_add_main_menu(output, node_name=node_name, enable_print_tensor=False)
|
|
|
|
return output
|
|
|
|
def list_outputs(self, args, screen_info=None):
|
|
"""Command handler for inputs.
|
|
|
|
Show inputs to a given node.
|
|
|
|
Args:
|
|
args: Command-line arguments, excluding the command prefix, as a list of
|
|
str.
|
|
screen_info: Optional dict input containing screen information such as
|
|
cols.
|
|
|
|
Returns:
|
|
Output text lines as a RichTextLines object.
|
|
"""
|
|
|
|
# Screen info not currently used by this handler. Include this line to
|
|
# mute pylint.
|
|
_ = screen_info
|
|
# TODO(cais): Use screen info to format the output lines more prettily,
|
|
# e.g., hanging indent of long node names.
|
|
|
|
parsed = self._arg_parsers["list_outputs"].parse_args(args)
|
|
|
|
output = self._list_inputs_or_outputs(
|
|
parsed.recursive,
|
|
parsed.node_name,
|
|
parsed.depth,
|
|
parsed.control,
|
|
parsed.op_type,
|
|
do_outputs=True)
|
|
|
|
node_name = debug_graphs.get_node_name(parsed.node_name)
|
|
_add_main_menu(output, node_name=node_name, enable_list_outputs=False)
|
|
|
|
return output
|
|
|
|
def evaluate_expression(self, args, screen_info=None):
|
|
parsed = self._arg_parsers["eval"].parse_args(args)
|
|
|
|
eval_res = self._evaluator.evaluate(parsed.expression)
|
|
|
|
np_printoptions = cli_shared.numpy_printoptions_from_screen_info(
|
|
screen_info)
|
|
return cli_shared.format_tensor(
|
|
eval_res,
|
|
"from eval of expression '%s'" % parsed.expression,
|
|
np_printoptions,
|
|
print_all=parsed.print_all,
|
|
include_numeric_summary=True,
|
|
write_path=parsed.write_path)
|
|
|
|
def _reconstruct_print_source_command(self,
|
|
parsed,
|
|
line_begin,
|
|
max_elements_per_line_increase=0):
|
|
return "ps %s %s -b %d -m %d" % (
|
|
parsed.source_file_path, "-t" if parsed.tensors else "", line_begin,
|
|
parsed.max_elements_per_line + max_elements_per_line_increase)
|
|
|
|
def print_source(self, args, screen_info=None):
|
|
"""Print the content of a source file."""
|
|
del screen_info # Unused.
|
|
|
|
parsed = self._arg_parsers["print_source"].parse_args(args)
|
|
|
|
source_annotation = source_utils.annotate_source(
|
|
self._debug_dump,
|
|
parsed.source_file_path,
|
|
do_dumped_tensors=parsed.tensors)
|
|
|
|
source_lines, line_num_width = source_utils.load_source(
|
|
parsed.source_file_path)
|
|
|
|
labeled_source_lines = []
|
|
actual_initial_scroll_target = 0
|
|
for i, line in enumerate(source_lines):
|
|
annotated_line = RL("L%d" % (i + 1), cli_shared.COLOR_YELLOW)
|
|
annotated_line += " " * (line_num_width - len(annotated_line))
|
|
annotated_line += line
|
|
labeled_source_lines.append(annotated_line)
|
|
|
|
if i + 1 == parsed.line_begin:
|
|
actual_initial_scroll_target = len(labeled_source_lines) - 1
|
|
|
|
if i + 1 in source_annotation:
|
|
sorted_elements = sorted(source_annotation[i + 1])
|
|
for k, element in enumerate(sorted_elements):
|
|
if k >= parsed.max_elements_per_line:
|
|
omitted_info_line = RL(" (... Omitted %d of %d %s ...) " % (
|
|
len(sorted_elements) - parsed.max_elements_per_line,
|
|
len(sorted_elements),
|
|
"tensor(s)" if parsed.tensors else "op(s)"))
|
|
omitted_info_line += RL(
|
|
"+5",
|
|
debugger_cli_common.MenuItem(
|
|
None,
|
|
self._reconstruct_print_source_command(
|
|
parsed, i + 1, max_elements_per_line_increase=5)))
|
|
labeled_source_lines.append(omitted_info_line)
|
|
break
|
|
|
|
label = RL(" " * 4)
|
|
if self._debug_dump.debug_watch_keys(
|
|
debug_graphs.get_node_name(element)):
|
|
attribute = debugger_cli_common.MenuItem("", "pt %s" % element)
|
|
else:
|
|
attribute = cli_shared.COLOR_BLUE
|
|
|
|
label += RL(element, attribute)
|
|
labeled_source_lines.append(label)
|
|
|
|
output = debugger_cli_common.rich_text_lines_from_rich_line_list(
|
|
labeled_source_lines,
|
|
annotations={debugger_cli_common.INIT_SCROLL_POS_KEY:
|
|
actual_initial_scroll_target})
|
|
_add_main_menu(output, node_name=None)
|
|
return output
|
|
|
|
def _make_source_table(self, source_list, is_tf_py_library):
|
|
"""Make a table summarizing the source files that create nodes and tensors.
|
|
|
|
Args:
|
|
source_list: List of source files and related information as a list of
|
|
tuples (file_path, is_tf_library, num_nodes, num_tensors, num_dumps,
|
|
first_line).
|
|
is_tf_py_library: (`bool`) whether this table is for files that belong
|
|
to the TensorFlow Python library.
|
|
|
|
Returns:
|
|
The table as a `debugger_cli_common.RichTextLines` object.
|
|
"""
|
|
path_head = "Source file path"
|
|
num_nodes_head = "#(nodes)"
|
|
num_tensors_head = "#(tensors)"
|
|
num_dumps_head = "#(tensor dumps)"
|
|
|
|
if is_tf_py_library:
|
|
# Use color to mark files that are guessed to belong to TensorFlow Python
|
|
# library.
|
|
color = cli_shared.COLOR_GRAY
|
|
lines = [RL("TensorFlow Python library file(s):", color)]
|
|
else:
|
|
color = cli_shared.COLOR_WHITE
|
|
lines = [RL("File(s) outside TensorFlow Python library:", color)]
|
|
|
|
if not source_list:
|
|
lines.append(RL("[No files.]"))
|
|
lines.append(RL())
|
|
return debugger_cli_common.rich_text_lines_from_rich_line_list(lines)
|
|
|
|
path_column_width = max(
|
|
max(len(item[0]) for item in source_list), len(path_head)) + 1
|
|
num_nodes_column_width = max(
|
|
max(len(str(item[2])) for item in source_list),
|
|
len(num_nodes_head)) + 1
|
|
num_tensors_column_width = max(
|
|
max(len(str(item[3])) for item in source_list),
|
|
len(num_tensors_head)) + 1
|
|
|
|
head = RL(path_head + " " * (path_column_width - len(path_head)), color)
|
|
head += RL(num_nodes_head + " " * (
|
|
num_nodes_column_width - len(num_nodes_head)), color)
|
|
head += RL(num_tensors_head + " " * (
|
|
num_tensors_column_width - len(num_tensors_head)), color)
|
|
head += RL(num_dumps_head, color)
|
|
|
|
lines.append(head)
|
|
|
|
for (file_path, _, num_nodes, num_tensors, num_dumps,
|
|
first_line_num) in source_list:
|
|
path_attributes = [color]
|
|
if source_utils.is_extension_uncompiled_python_source(file_path):
|
|
path_attributes.append(
|
|
debugger_cli_common.MenuItem(None, "ps %s -b %d" %
|
|
(file_path, first_line_num)))
|
|
|
|
line = RL(file_path, path_attributes)
|
|
line += " " * (path_column_width - len(line))
|
|
line += RL(
|
|
str(num_nodes) + " " * (num_nodes_column_width - len(str(num_nodes))),
|
|
color)
|
|
line += RL(
|
|
str(num_tensors) + " " *
|
|
(num_tensors_column_width - len(str(num_tensors))), color)
|
|
line += RL(str(num_dumps), color)
|
|
lines.append(line)
|
|
lines.append(RL())
|
|
|
|
return debugger_cli_common.rich_text_lines_from_rich_line_list(lines)
|
|
|
|
def list_source(self, args, screen_info=None):
|
|
"""List Python source files that constructed nodes and tensors."""
|
|
del screen_info # Unused.
|
|
|
|
parsed = self._arg_parsers["list_source"].parse_args(args)
|
|
source_list = source_utils.list_source_files_against_dump(
|
|
self._debug_dump,
|
|
path_regex_allowlist=parsed.path_filter,
|
|
node_name_regex_allowlist=parsed.node_name_filter)
|
|
|
|
top_lines = [
|
|
RL("List of source files that created nodes in this run", "bold")]
|
|
if parsed.path_filter:
|
|
top_lines.append(
|
|
RL("File path regex filter: \"%s\"" % parsed.path_filter))
|
|
if parsed.node_name_filter:
|
|
top_lines.append(
|
|
RL("Node name regex filter: \"%s\"" % parsed.node_name_filter))
|
|
top_lines.append(RL())
|
|
output = debugger_cli_common.rich_text_lines_from_rich_line_list(top_lines)
|
|
if not source_list:
|
|
output.append("[No source file information.]")
|
|
return output
|
|
|
|
output.extend(self._make_source_table(
|
|
[item for item in source_list if not item[1]], False))
|
|
output.extend(self._make_source_table(
|
|
[item for item in source_list if item[1]], True))
|
|
_add_main_menu(output, node_name=None)
|
|
return output
|
|
|
|
def _list_inputs_or_outputs(self,
|
|
recursive,
|
|
node_name,
|
|
depth,
|
|
control,
|
|
op_type,
|
|
do_outputs=False):
|
|
"""Helper function used by list_inputs and list_outputs.
|
|
|
|
Format a list of lines to display the inputs or output recipients of a
|
|
given node.
|
|
|
|
Args:
|
|
recursive: Whether the listing is to be done recursively, as a boolean.
|
|
node_name: The name of the node in question, as a str.
|
|
depth: Maximum recursion depth, applies only if recursive == True, as an
|
|
int.
|
|
control: Whether control inputs or control recipients are included, as a
|
|
boolean.
|
|
op_type: Whether the op types of the nodes are to be included, as a
|
|
boolean.
|
|
do_outputs: Whether recipients, instead of input nodes are to be
|
|
listed, as a boolean.
|
|
|
|
Returns:
|
|
Input or recipient tree formatted as a RichTextLines object.
|
|
"""
|
|
|
|
if do_outputs:
|
|
tracker = self._debug_dump.node_recipients
|
|
type_str = "Recipients of"
|
|
short_type_str = "recipients"
|
|
else:
|
|
tracker = self._debug_dump.node_inputs
|
|
type_str = "Inputs to"
|
|
short_type_str = "inputs"
|
|
|
|
lines = []
|
|
font_attr_segs = {}
|
|
|
|
# Check if this is a tensor name, instead of a node name.
|
|
node_name, _ = debug_graphs.parse_node_or_tensor_name(node_name)
|
|
|
|
# Check if node exists.
|
|
if not self._debug_dump.node_exists(node_name):
|
|
return cli_shared.error(
|
|
"There is no node named \"%s\" in the partition graphs" % node_name)
|
|
|
|
if recursive:
|
|
max_depth = depth
|
|
else:
|
|
max_depth = 1
|
|
|
|
if control:
|
|
include_ctrls_str = ", control %s included" % short_type_str
|
|
else:
|
|
include_ctrls_str = ""
|
|
|
|
line = "%s node \"%s\"" % (type_str, node_name)
|
|
font_attr_segs[0] = [(len(line) - 1 - len(node_name), len(line) - 1, "bold")
|
|
]
|
|
lines.append(line + " (Depth limit = %d%s):" % (max_depth, include_ctrls_str
|
|
))
|
|
|
|
command_template = "lo -c -r %s" if do_outputs else "li -c -r %s"
|
|
self._dfs_from_node(
|
|
lines,
|
|
font_attr_segs,
|
|
node_name,
|
|
tracker,
|
|
max_depth,
|
|
1, [],
|
|
control,
|
|
op_type,
|
|
command_template=command_template)
|
|
|
|
# Include legend.
|
|
lines.append("")
|
|
lines.append("Legend:")
|
|
lines.append(" (d): recursion depth = d.")
|
|
|
|
if control:
|
|
lines.append(" (Ctrl): Control input.")
|
|
if op_type:
|
|
lines.append(" [Op]: Input node has op type Op.")
|
|
|
|
# TODO(cais): Consider appending ":0" at the end of 1st outputs of nodes.
|
|
|
|
return debugger_cli_common.RichTextLines(
|
|
lines, font_attr_segs=font_attr_segs)
|
|
|
|
def _dfs_from_node(self,
|
|
lines,
|
|
attr_segs,
|
|
node_name,
|
|
tracker,
|
|
max_depth,
|
|
depth,
|
|
unfinished,
|
|
include_control=False,
|
|
show_op_type=False,
|
|
command_template=None):
|
|
"""Perform depth-first search (DFS) traversal of a node's input tree.
|
|
|
|
It recursively tracks the inputs (or output recipients) of the node called
|
|
node_name, and append these inputs (or output recipients) to a list of text
|
|
lines (lines) with proper indentation that reflects the recursion depth,
|
|
together with some formatting attributes (to attr_segs). The formatting
|
|
attributes can include command shortcuts, for example.
|
|
|
|
Args:
|
|
lines: Text lines to append to, as a list of str.
|
|
attr_segs: (dict) Attribute segments dictionary to append to.
|
|
node_name: Name of the node, as a str. This arg is updated during the
|
|
recursion.
|
|
tracker: A callable that takes one str as the node name input and
|
|
returns a list of str as the inputs/outputs.
|
|
This makes it this function general enough to be used with both
|
|
node-input and node-output tracking.
|
|
max_depth: Maximum recursion depth, as an int.
|
|
depth: Current recursion depth. This arg is updated during the
|
|
recursion.
|
|
unfinished: A stack of unfinished recursion depths, as a list of int.
|
|
include_control: Whether control dependencies are to be included as
|
|
inputs (and marked as such).
|
|
show_op_type: Whether op type of the input nodes are to be displayed
|
|
alongside the nodes' names.
|
|
command_template: (str) Template for command shortcut of the node names.
|
|
"""
|
|
|
|
# Make a shallow copy of the list because it may be extended later.
|
|
all_inputs = self._exclude_denylisted_ops(
|
|
copy.copy(tracker(node_name, is_control=False)))
|
|
is_ctrl = [False] * len(all_inputs)
|
|
if include_control:
|
|
# Sort control inputs or recipients in alphabetical order of the node
|
|
# names.
|
|
ctrl_inputs = self._exclude_denylisted_ops(
|
|
sorted(tracker(node_name, is_control=True)))
|
|
all_inputs.extend(ctrl_inputs)
|
|
is_ctrl.extend([True] * len(ctrl_inputs))
|
|
|
|
if not all_inputs:
|
|
if depth == 1:
|
|
lines.append(" [None]")
|
|
|
|
return
|
|
|
|
unfinished.append(depth)
|
|
|
|
# Create depth-dependent hanging indent for the line.
|
|
hang = ""
|
|
for k in xrange(depth):
|
|
if k < depth - 1:
|
|
if k + 1 in unfinished:
|
|
hang += HANG_UNFINISHED
|
|
else:
|
|
hang += HANG_FINISHED
|
|
else:
|
|
hang += HANG_SUFFIX
|
|
|
|
if all_inputs and depth > max_depth:
|
|
lines.append(hang + ELLIPSIS)
|
|
unfinished.pop()
|
|
return
|
|
|
|
hang += DEPTH_TEMPLATE % depth
|
|
|
|
for i, inp in enumerate(all_inputs):
|
|
op_type = self._debug_dump.node_op_type(debug_graphs.get_node_name(inp))
|
|
if op_type in self._GRAPH_STRUCT_OP_TYPE_DENYLIST:
|
|
continue
|
|
|
|
if is_ctrl[i]:
|
|
ctrl_str = CTRL_LABEL
|
|
else:
|
|
ctrl_str = ""
|
|
|
|
op_type_str = ""
|
|
if show_op_type:
|
|
op_type_str = OP_TYPE_TEMPLATE % op_type
|
|
|
|
if i == len(all_inputs) - 1:
|
|
unfinished.pop()
|
|
|
|
line = hang + ctrl_str + op_type_str + inp
|
|
lines.append(line)
|
|
if command_template:
|
|
attr_segs[len(lines) - 1] = [(
|
|
len(line) - len(inp), len(line),
|
|
debugger_cli_common.MenuItem(None, command_template % inp))]
|
|
|
|
# Recursive call.
|
|
# The input's/output's name can be a tensor name, in the case of node
|
|
# with >1 output slots.
|
|
inp_node_name, _ = debug_graphs.parse_node_or_tensor_name(inp)
|
|
self._dfs_from_node(
|
|
lines,
|
|
attr_segs,
|
|
inp_node_name,
|
|
tracker,
|
|
max_depth,
|
|
depth + 1,
|
|
unfinished,
|
|
include_control=include_control,
|
|
show_op_type=show_op_type,
|
|
command_template=command_template)
|
|
|
|
def _format_neighbors(self, neighbor_type, non_ctrls, ctrls):
|
|
"""List neighbors (inputs or recipients) of a node.
|
|
|
|
Args:
|
|
neighbor_type: ("input" | "recipient")
|
|
non_ctrls: Non-control neighbor node names, as a list of str.
|
|
ctrls: Control neighbor node names, as a list of str.
|
|
|
|
Returns:
|
|
A RichTextLines object.
|
|
"""
|
|
|
|
# TODO(cais): Return RichTextLines instead, to allow annotation of node
|
|
# names.
|
|
lines = []
|
|
font_attr_segs = {}
|
|
|
|
lines.append("")
|
|
lines.append(" %d %s(s) + %d control %s(s):" %
|
|
(len(non_ctrls), neighbor_type, len(ctrls), neighbor_type))
|
|
lines.append(" %d %s(s):" % (len(non_ctrls), neighbor_type))
|
|
for non_ctrl in non_ctrls:
|
|
line = " [%s] %s" % (self._debug_dump.node_op_type(non_ctrl),
|
|
non_ctrl)
|
|
lines.append(line)
|
|
font_attr_segs[len(lines) - 1] = [(
|
|
len(line) - len(non_ctrl), len(line),
|
|
debugger_cli_common.MenuItem(None, "ni -a -d -t %s" % non_ctrl))]
|
|
|
|
if ctrls:
|
|
lines.append("")
|
|
lines.append(" %d control %s(s):" % (len(ctrls), neighbor_type))
|
|
for ctrl in ctrls:
|
|
line = " [%s] %s" % (self._debug_dump.node_op_type(ctrl), ctrl)
|
|
lines.append(line)
|
|
font_attr_segs[len(lines) - 1] = [(
|
|
len(line) - len(ctrl), len(line),
|
|
debugger_cli_common.MenuItem(None, "ni -a -d -t %s" % ctrl))]
|
|
|
|
return debugger_cli_common.RichTextLines(
|
|
lines, font_attr_segs=font_attr_segs)
|
|
|
|
def _list_node_attributes(self, node_name):
|
|
"""List neighbors (inputs or recipients) of a node.
|
|
|
|
Args:
|
|
node_name: Name of the node of which the attributes are to be listed.
|
|
|
|
Returns:
|
|
A RichTextLines object.
|
|
"""
|
|
|
|
lines = []
|
|
lines.append("")
|
|
lines.append("Node attributes:")
|
|
|
|
attrs = self._debug_dump.node_attributes(node_name)
|
|
for attr_key in attrs:
|
|
lines.append(" %s:" % attr_key)
|
|
attr_val_str = repr(attrs[attr_key]).strip().replace("\n", " ")
|
|
lines.append(" %s" % attr_val_str)
|
|
lines.append("")
|
|
|
|
return debugger_cli_common.RichTextLines(lines)
|
|
|
|
def _list_node_dumps(self, node_name):
|
|
"""List dumped tensor data from a node.
|
|
|
|
Args:
|
|
node_name: Name of the node of which the attributes are to be listed.
|
|
|
|
Returns:
|
|
A RichTextLines object.
|
|
"""
|
|
|
|
lines = []
|
|
font_attr_segs = {}
|
|
|
|
watch_keys = self._debug_dump.debug_watch_keys(node_name)
|
|
|
|
dump_count = 0
|
|
for watch_key in watch_keys:
|
|
debug_tensor_data = self._debug_dump.watch_key_to_data(watch_key)
|
|
for datum in debug_tensor_data:
|
|
line = " Slot %d @ %s @ %.3f ms" % (
|
|
datum.output_slot, datum.debug_op,
|
|
(datum.timestamp - self._debug_dump.t0) / 1000.0)
|
|
lines.append(line)
|
|
command = "pt %s:%d -n %d" % (node_name, datum.output_slot, dump_count)
|
|
font_attr_segs[len(lines) - 1] = [(
|
|
2, len(line), debugger_cli_common.MenuItem(None, command))]
|
|
dump_count += 1
|
|
|
|
output = debugger_cli_common.RichTextLines(
|
|
lines, font_attr_segs=font_attr_segs)
|
|
output_with_header = debugger_cli_common.RichTextLines(
|
|
["%d dumped tensor(s):" % dump_count, ""])
|
|
output_with_header.extend(output)
|
|
return output_with_header
|
|
|
|
|
|
def create_analyzer_ui(debug_dump,
|
|
tensor_filters=None,
|
|
ui_type="curses",
|
|
on_ui_exit=None,
|
|
config=None):
|
|
"""Create an instance of CursesUI based on a DebugDumpDir object.
|
|
|
|
Args:
|
|
debug_dump: (debug_data.DebugDumpDir) The debug dump to use.
|
|
tensor_filters: (dict) A dict mapping tensor filter name (str) to tensor
|
|
filter (Callable).
|
|
ui_type: (str) requested UI type, e.g., "curses", "readline".
|
|
on_ui_exit: (`Callable`) the callback to be called when the UI exits.
|
|
config: A `cli_config.CLIConfig` object.
|
|
|
|
Returns:
|
|
(base_ui.BaseUI) A BaseUI subtype object with a set of standard analyzer
|
|
commands and tab-completions registered.
|
|
"""
|
|
if config is None:
|
|
config = cli_config.CLIConfig()
|
|
|
|
analyzer = DebugAnalyzer(debug_dump, config=config)
|
|
if tensor_filters:
|
|
for tensor_filter_name in tensor_filters:
|
|
analyzer.add_tensor_filter(
|
|
tensor_filter_name, tensor_filters[tensor_filter_name])
|
|
|
|
cli = ui_factory.get_ui(ui_type, on_ui_exit=on_ui_exit, config=config)
|
|
cli.register_command_handler(
|
|
"list_tensors",
|
|
analyzer.list_tensors,
|
|
analyzer.get_help("list_tensors"),
|
|
prefix_aliases=["lt"])
|
|
cli.register_command_handler(
|
|
"node_info",
|
|
analyzer.node_info,
|
|
analyzer.get_help("node_info"),
|
|
prefix_aliases=["ni"])
|
|
cli.register_command_handler(
|
|
"list_inputs",
|
|
analyzer.list_inputs,
|
|
analyzer.get_help("list_inputs"),
|
|
prefix_aliases=["li"])
|
|
cli.register_command_handler(
|
|
"list_outputs",
|
|
analyzer.list_outputs,
|
|
analyzer.get_help("list_outputs"),
|
|
prefix_aliases=["lo"])
|
|
cli.register_command_handler(
|
|
"print_tensor",
|
|
analyzer.print_tensor,
|
|
analyzer.get_help("print_tensor"),
|
|
prefix_aliases=["pt"])
|
|
cli.register_command_handler(
|
|
"print_source",
|
|
analyzer.print_source,
|
|
analyzer.get_help("print_source"),
|
|
prefix_aliases=["ps"])
|
|
cli.register_command_handler(
|
|
"list_source",
|
|
analyzer.list_source,
|
|
analyzer.get_help("list_source"),
|
|
prefix_aliases=["ls"])
|
|
cli.register_command_handler(
|
|
"eval",
|
|
analyzer.evaluate_expression,
|
|
analyzer.get_help("eval"),
|
|
prefix_aliases=["ev"])
|
|
|
|
dumped_tensor_names = []
|
|
for datum in debug_dump.dumped_tensor_data:
|
|
dumped_tensor_names.append("%s:%d" % (datum.node_name, datum.output_slot))
|
|
|
|
# Tab completions for command "print_tensors".
|
|
cli.register_tab_comp_context(["print_tensor", "pt"], dumped_tensor_names)
|
|
|
|
return cli
|