Add a frontend for viewing the first ops that exhibit bad values (NaN, +/- Inf).

This helps the user identify problematic ops. Also moved the debugger data logic within tf-graph-info into a new tf-graph-debugger-data-card component.

PiperOrigin-RevId: 158208679
This commit is contained in:
A. Unique TensorFlower 2017-06-06 16:41:24 -07:00 committed by TensorFlower Gardener
parent 7f5384dccf
commit 0ea0bf5aae
11 changed files with 789 additions and 251 deletions

View File

@ -356,6 +356,8 @@ filegroup(
"//tensorflow/tensorboard/components/tf_graph_controls/demo:all_files",
"//tensorflow/tensorboard/components/tf_graph_dashboard:all_files",
"//tensorflow/tensorboard/components/tf_graph_dashboard/demo:all_files",
"//tensorflow/tensorboard/components/tf_graph_debugger_data_card:all_files",
"//tensorflow/tensorboard/components/tf_graph_debugger_data_card/demo:all_files",
"//tensorflow/tensorboard/components/tf_graph_info:all_files",
"//tensorflow/tensorboard/components/tf_graph_info/demo:all_files",
"//tensorflow/tensorboard/components/tf_graph_loader:all_files",

View File

@ -138,7 +138,7 @@ paper-progress {
render-hierarchy="{{renderHierarchy}}"
devices-for-stats="[[devicesForStats]]"
stats="[[stats]]"
selected-node="{{_selectedNode}}"
selected-node="{{selectedNode}}"
highlighted-node="{{_highlightedNode}}"
color-by="[[colorBy]]"
color-by-params="{{colorByParams}}"
@ -153,13 +153,14 @@ paper-progress {
graph-hierarchy="[[graphHierarchy]]"
render-hierarchy="[[renderHierarchy]]"
graph="[[graph]]"
selected-node="{{_selectedNode}}"
selected-node="{{selectedNode}}"
selected-node-include="{{_selectedNodeInclude}}"
highlighted-node="{{_highlightedNode}}"
color-by="[[colorBy]]"
color-by-params="[[colorByParams]]"
debugger-data-enabled="[[debuggerDataEnabled]]"
are-health-pills-loading="[[areHealthPillsLoading]]"
debugger-numeric-alerts="[[debuggerNumericAlerts]]"
node-names-to-health-pills="[[nodeNamesToHealthPills]]"
all-steps-mode-enabled="{{allStepsModeEnabled}}"
specific-health-pill-step="{{specificHealthPillStep}}"
@ -198,6 +199,12 @@ Polymer({
debuggerDataEnabled: Boolean,
// Whether health pills are currently being loaded.
areHealthPillsLoading: Boolean,
// An array of alerts (in chronological order) provided by debugging libraries on when bad
// values (NaN, +/- Inf) appear.
debuggerNumericAlerts: {
type: Array,
notify: true,
},
// A mapping between node name to the tf.graph.scene.HealthPill to render.
nodeNamesToHealthPills: Object,
// Whether the user can request health pills for individual steps from the server. This can be
@ -217,7 +224,10 @@ Polymer({
// The step of health pills to show throughout the graph.
healthPillStepIndex: Number,
// Private API: Data routing between child components.
_selectedNode: String,
selectedNode: {
type: String,
notify: true,
},
// The enum value of the include property of the selected node.
_selectedNodeInclude: Number,
_highlightedNode: String
@ -226,7 +236,7 @@ Polymer({
'node-toggle-extract': '_nodeToggleExtract'
},
observers: [
'_updateNodeInclude(_selectedNode)'
'_updateNodeInclude(selectedNode)'
],
/** True if the progress is not complete yet (< 100 %). */
_isNotComplete: function(progress) {
@ -248,7 +258,7 @@ Polymer({
node ? node.include : tf.graph.InclusionType.UNSPECIFIED);
},
_nodeToggleExtract: function() {
this._updateNodeInclude(this._selectedNode);
this._updateNodeInclude(this.selectedNode);
}
});
</script>

View File

@ -1630,4 +1630,44 @@ function extractHighDegrees(renderNode: RenderGroupNodeInfo) {
}
});
}
/**
* Expands nodes in the graph until the desired node is visible.
*
* @param scene The scene polymer component.
* @param renderHierarchy The render hierarchy.
* @param tensorName The name of a tensor.
* @return A string that is the name of the node representing the given tensor.
* Note that the original tensor name might differ from this returned node
* name. Specifically, for instance, the tensor name usually ends with an
* output slot index (such as :0), while the node name lacks that suffix.
*/
export function expandUntilNodeIsShown(
scene, renderHierarchy, tensorName: string) {
const splitTensorName = tensorName.split('/');
// Graph names do not take into account the output slot. Strip it.
const lastNodeNameMatch =
splitTensorName[splitTensorName.length - 1].match(/(.*):\d+/);
if (lastNodeNameMatch.length === 2) {
splitTensorName[splitTensorName.length - 1] = lastNodeNameMatch[1];
}
let nodeName = splitTensorName[0];
let renderNode = renderHierarchy.getRenderNodeByName(nodeName);
for (let i = 1; i < splitTensorName.length; i++) {
// Op nodes are not expandable.
if (renderNode.node.type === tf.graph.NodeType.OP) {
break;
}
renderHierarchy.buildSubhierarchy(nodeName);
renderNode.expanded = true;
scene.setNodeExpanded(renderNode);
nodeName += '/' + splitTensorName[i];
renderNode = renderHierarchy.getRenderNodeByName(nodeName);
}
return renderNode.node.name;
}
} // close module tf.graph.render

View File

@ -288,4 +288,29 @@ module tf.graph.util {
return _.object(queryParams);
}
/**
* Given a timestamp in microseconds, return a human-friendly string denoting
* how long ago the timestamp was.
*/
export function computeHumanFriendlyTime(timeInMicroseconds: number) {
var timeDifferenceInMs =
+(new Date()) - +(new Date(timeInMicroseconds / 1e3));
if (timeDifferenceInMs < 30000) {
return 'just now';
} else if (timeDifferenceInMs < 60000) {
return Math.floor(timeDifferenceInMs / 1000) + ' seconds ago';
} else if (timeDifferenceInMs < 120000) {
return 'a minute ago';
} else if (timeDifferenceInMs < 3600000) {
return Math.floor(timeDifferenceInMs / 60000) + ' minutes ago';
} else if (Math.floor(timeDifferenceInMs / 3600000) == 1) {
return 'an hour ago';
} else if (timeDifferenceInMs < 86400000) {
return Math.floor(timeDifferenceInMs / 3600000) + ' hours ago';
} else if (timeDifferenceInMs < 172800000) {
return 'yesterday';
}
return Math.floor(timeDifferenceInMs / 86400000) + ' days ago';
}
}

View File

@ -78,11 +78,13 @@ out-hierarchy-params="{{_hierarchyParams}}"
progress="[[_progress]]"
debugger-data-enabled="[[debuggerDataEnabled]]"
are-health-pills-loading="[[_areHealthPillsLoading]]"
debugger-numeric-alerts="[[_debuggerNumericAlerts]]"
node-names-to-health-pills="[[_nodeNamesToHealthPills]]"
all-steps-mode-enabled="{{allStepsModeEnabled}}"
specific-health-pill-step="{{specificHealthPillStep}}"
health-pill-step-index="[[_healthPillStepIndex]]"
render-hierarchy="{{_renderHierarchy}}"
selected-node="{{_selectedNode}}"
stats="[[_stats]]"
></tf-graph-board>
</div>
@ -128,12 +130,20 @@ Polymer({
allStepsModeEnabled: Boolean,
specificHealthPillStep: {type: Number, value: 0},
healthPillsToggledOn: {type: Boolean, value: true, observer: '_healthPillsToggledOnChanged'},
_selectedNode: Object,
_isAttached: Boolean,
// Whether this dashboard is initialized. This dashboard should only be initialized once.
_initialized: Boolean,
// Whether health pills are currently being loaded, in which case we may want to say show a
// spinner.
_areHealthPillsLoading: Boolean,
// An array of alerts (in chronological order) provided by debugging libraries on when bad
// values (NaN, +/- Inf) appear.
_debuggerNumericAlerts: {
type: Array,
value: [],
notify: true,
},
// Maps the names of nodes to an array of health pills (HealthPillDatums).
_nodeNamesToHealthPills: {
type: Object,
@ -158,7 +168,7 @@ Polymer({
'node-toggle-expand': '_handleNodeToggleExpand',
},
observers: [
'_maybeFetchHealthPills(allStepsModeEnabled, specificHealthPillStep)',
'_maybeFetchHealthPills(allStepsModeEnabled, specificHealthPillStep, _selectedNode)',
'_maybeInitializeDashboard(backend, _isAttached)',
],
attached: function() {
@ -212,7 +222,7 @@ Polymer({
},
_requestHealthPills: function() {
this.set('_areHealthPillsLoading', true);
const requestId = ++this._healthPillRequestId;
var requestId = ++this._healthPillRequestId;
if (this._healthPillStepRequestTimerId !== null) {
// A request for health pills is already scheduled to be initiated. Clear it, and schedule a
@ -243,9 +253,17 @@ Polymer({
return;
}
const specificStep = this.allStepsModeEnabled ? this.specificHealthPillStep : undefined;
this.backend.healthPills(this._renderHierarchy.getNamesOfRenderedOps(), specificStep).then(
var specificStep = this.allStepsModeEnabled ? this.specificHealthPillStep : undefined;
var healthPillsPromise = this.backend.healthPills(
this._renderHierarchy.getNamesOfRenderedOps(), specificStep);
var alertsPromise = this.backend.debuggerNumericsAlerts();
Promise.all([healthPillsPromise, alertsPromise]).then(
function(result) {
var healthPillsResult = result[0];
var alertsResult = result[1];
if (!this.healthPillsToggledOn) {
// The user has opted to hide health pills via the toggle button.
return;
@ -259,12 +277,13 @@ Polymer({
// Set the index for which step to show for the health pills. By default, show the last step.
// A precondition we assume (that Tensorboard's reservoir sampling guarantees) is that all
// node names should be mapped to the same number of steps.
for (let nodeName in result) {
this.set('_healthPillStepIndex', result[nodeName].length - 1);
for (var nodeName in healthPillsResult) {
this.set('_healthPillStepIndex', healthPillsResult[nodeName].length - 1);
break;
}
this.set('_nodeNamesToHealthPills', result);
this.set('_debuggerNumericAlerts', alertsResult);
this.set('_nodeNamesToHealthPills', healthPillsResult);
this.set('_areHealthPillsLoading', false);
this.set('_healthPillStepRequestTimerId', null);
}.bind(this));

View File

@ -0,0 +1,44 @@
package(default_visibility = ["//tensorflow:internal"])
load("//tensorflow/tensorboard:defs.bzl", "tensorboard_webcomponent_library")
load("//tensorflow/tensorboard:web.bzl", "ts_web_library")
licenses(["notice"]) # Apache 2.0
ts_web_library(
name = "tf_graph_debugger_data_card",
srcs = [
"tf-graph-debugger-data-card.html",
],
path = "/tf-graph-debugger-data-card",
deps = [
"//tensorflow/tensorboard/components/tf_dashboard_common",
"//tensorflow/tensorboard/components/tf_graph_common",
"//tensorflow/tensorboard/components/tf_imports:polymer",
"@org_polymer_paper_slider",
"@org_polymer_paper_spinner",
],
)
tensorboard_webcomponent_library(
name = "legacy",
srcs = [":tf_graph_debugger_data_card"],
destdir = "tf-graph-debugger-data-card",
deps = [
"//tensorflow/tensorboard/components/tf_dashboard_common:legacy",
"//tensorflow/tensorboard/components/tf_graph_common:legacy",
"//third_party/javascript/polymer/v1/iron-collapse:lib",
"//third_party/javascript/polymer/v1/iron-list:lib",
"//third_party/javascript/polymer/v1/paper-icon-button:lib",
"//third_party/javascript/polymer/v1/paper-item:lib",
"//third_party/javascript/polymer/v1/paper-slider:lib",
"//third_party/javascript/polymer/v1/paper-spinner:lib",
"//third_party/javascript/polymer/v1/polymer:lib",
],
)
filegroup(
name = "all_files",
srcs = glob(["**"]),
tags = ["notsan"],
)

View File

@ -0,0 +1,24 @@
package(default_visibility = ["//tensorflow:internal"])
load("//tensorflow/tensorboard:web.bzl", "ts_web_library")
licenses(["notice"]) # Apache 2.0
# bazel run //third_party/tensorflow/tensorboard/components/tf_graph_debugger_data_card/demo
ts_web_library(
name = "demo",
srcs = ["index.html"] + glob(["data/**"]),
path = "/tf-graph-debugger-data-card/demo",
deps = [
"//tensorflow/tensorboard/components/tf_graph_debugger_data_card",
"//tensorflow/tensorboard/components/tf_imports:webcomponentsjs",
"@org_polymer_iron_demo_helpers",
"@org_polymer_paper_styles",
],
)
filegroup(
name = "all_files",
srcs = glob(["**"]),
tags = ["notsan"],
)

View File

@ -0,0 +1,36 @@
<!doctype html>
<!--
@license
Copyright 2017 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.
-->
<script src="../../webcomponentsjs/webcomponents-lite.min.js"></script>
<link rel="import" href="../tf-graph-debugger-data-card.html">
<link rel="import" href="../../iron-demo-helpers/demo-snippet.html">
<title>TF Graph Info Demo</title>
<style>
#demo-container {
border: 2px solid #808080;
width: 1000px;
height: 600px;
}
</style>
<demo-snippet>
<template>
<div id='demo-container'>
<!-- This simple demo starts up a page with a health pill legend. -->
<tf-graph-debugger-data-card></tf-graph-debugger-data-card>
</div>
</template>
</demo-snippet>

View File

@ -0,0 +1,560 @@
<!--
@license
Copyright 2017 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.
-->
<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../paper-slider/paper-slider.html">
<link rel="import" href="../paper-spinner/paper-spinner-lite.html">
<link rel="import" href="../tf-graph-common/tf-graph-common.html">
<dom-module id="tf-graph-debugger-data-card">
<template>
<style>
:host {
font-size: 12px;
margin: 0;
padding: 0;
display: block;
}
h2 {
padding: 0;
text-align: center;
margin: 0;
}
.health-pill-legend {
padding: 15px;
}
.health-pill-legend h2 {
text-align: left;
}
.health-pill-entry {
margin: 10px 10px 10px 0;
}
.health-pill-entry .color-preview {
width: 26px;
height: 26px;
border-radius: 3px;
display: inline-block;
margin: 0 10px 0 0;
}
.health-pill-entry .color-label, .health-pill-entry .tensor-count {
color: #777;
display: inline-block;
height: 26px;
font-size: 22px;
line-height: 26px;
vertical-align: top;
}
.health-pill-entry .tensor-count {
float: right;
}
#health-pill-step-slider {
width: 100%;
margin: 0 0 0 -15px;
/* 31 comes from adding a padding of 15px from both sides of the paper-slider, subtracting
* 1px so that the slider width aligns with the image (the last slider marker takes up 1px),
* and adding 2px to account for a border of 1px on both sides of the image. 30 - 1 + 2.
* Apparently, the paper-slider lacks a mixin for those padding values. */
width: calc(100% + 31px);
}
#health-pills-loading-spinner {
width: 20px;
height: 20px;
vertical-align: top;
}
#health-pill-step-number-input {
text-align: center;
vertical-align: top;
}
#numeric-alerts-table-container {
max-height: 400px;
overflow-x: hidden;
overflow-y: auto;
}
#numeric-alerts-table {
text-align: left;
}
#numeric-alerts-table td {
vertical-align: top;
}
#numeric-alerts-table .first-offense-td {
display: inline-block;
}
.first-offense-td {
width: 80px;
}
.tensor-device-td {
max-width: 140px;
word-wrap : break-word;
}
.tensor-section-within-table {
color: #266236;
cursor: pointer;
opacity: 0.8;
text-decoration: underline;
}
.tensor-section-within-table:hover {
opacity: 1;
}
.device-section-within-table {
color: #666;
}
.mini-health-pill {
width: 130px;
}
.mini-health-pill > div {
height: 100%;
width: 60px;
border-radius: 3px;
}
#event-counts-th {
padding: 0 0 0 10px;
}
.negative-inf-mini-health-pill-section {
background: rgb(255, 141, 0);
width: 20px;
}
.positive-inf-mini-health-pill-section {
background: rgb(0, 62, 212);
width: 20px;
}
.nan-mini-health-pill-section {
background: rgb(204, 47, 44);
width: 20px;
}
.negative-inf-mini-health-pill-section,
.positive-inf-mini-health-pill-section,
.nan-mini-health-pill-section {
color: #fff;
display: inline-block;
height: 100%;
line-height: 20px;
margin: 0 0 0 10px;
text-align: center;
}
.no-numeric-alerts-notification {
margin: 0;
}
</style>
<paper-material elevation="1" class="card health-pill-legend">
<div class="title">
Enable all (not just sampled) steps. Requires slow disk read.
</div>
<paper-toggle-button id="enableAllStepsModeToggle" checked="{{allStepsModeEnabled}}">
</paper-toggle-button>
<h2>
Step of Health Pills:
<template is="dom-if" if="[[allStepsModeEnabled]]">
<input type="number"
id="health-pill-step-number-input"
min="0"
max="[[_biggestStepEverSeen]]"
value="{{specificHealthPillStep::input}}">
</template>
<template is="dom-if" if="[[!allStepsModeEnabled]]">
[[_currentStepDisplayValue]]
</template>
<paper-spinner-lite active
hidden$=[[!areHealthPillsLoading]]
id="health-pills-loading-spinner"></paper-spinner-lite>
</h2>
<template is="dom-if" if="[[allStepsModeEnabled]]">
<paper-slider
id="health-pill-step-slider"
immediate-value="{{specificHealthPillStep}}"
max="[[_biggestStepEverSeen]]"
snaps
step="1"
value="{{specificHealthPillStep}}"></paper-slider>
</template>
<template is="dom-if" if="[[!allStepsModeEnabled]]">
<template is="dom-if" if="[[_maxStepIndex]]">
<paper-slider
id="health-pill-step-slider"
immediate-value="{{healthPillStepIndex}}"
max="[[_maxStepIndex]]"
snaps
step="1"
value="{{healthPillStepIndex}}"></paper-slider>
</template>
</template>
<h2>
Health Pill
<template is="dom-if" if="[[healthPillValuesForSelectedNode]]">
Counts for Selected Node
</template>
<template is="dom-if" if="[[!healthPillValuesForSelectedNode]]">
Legend
</template>
</h2>
<template is="dom-repeat" items="[[healthPillEntries]]">
<div class="health-pill-entry">
<div class="color-preview" style="background:[[item.background_color]]"></div>
<div class="color-label">[[item.label]]</div>
<div class="tensor-count">
[[_computeTensorCountString(healthPillValuesForSelectedNode, index)]]
</div>
</div>
</template>
<div hidden$="[[!_hasDebuggerNumericAlerts(debuggerNumericAlerts)]]">
<h2 id="numeric-alerts-header">Numeric Alerts</h2>
<p>
Alerts are sorted from top to bottom by increasing timestamp.
</p>
<div id='numeric-alerts-table-container'>
<table id="numeric-alerts-table">
<thead>
<tr>
<th>First Offense</th>
<th>Tensor (Device)</th>
<th id='event-counts-th'>Event Counts</th>
</tr>
</thead>
<tbody id="numeric-alerts-body">
</tbody>
</table>
</div>
</div>
<template is="dom-if" if="[[!_hasDebuggerNumericAlerts(debuggerNumericAlerts)]]">
<p class="no-numeric-alerts-notification">
No numeric alerts so far. That is likely good. Alerts indicate the presence of NaN
or (+/-) Infinity values, which may be concerning.
</p>
</template>
</paper-material>
</template>
<script>
"use strict";
(function() {
Polymer({
is: 'tf-graph-debugger-data-card',
properties: {
renderHierarchy: Object,
debuggerNumericAlerts: {
type: Array,
notify: true,
},
nodeNamesToHealthPills: Object,
healthPillStepIndex: {
type: Number,
notify: true,
},
// Only relevant if we are in all steps mode, in which case the user may want to view health
// pills for a specific step.
specificHealthPillStep: {
type: Number,
value: 0,
notify: true,
},
// Two-ways
selectedNode: {
type: String,
notify: true
},
highlightedNode: {
type: String,
notify: true
},
// The enum value of the include property of the selected node.
selectedNodeInclude: {
type: Number,
notify: true
},
// Whether health pills are currently being loaded, in which case we show a spinner (and the
// current health pills shown might be out of date).
areHealthPillsLoading: Boolean,
healthPillEntries: {
type: Array,
value: tf.graph.scene.healthPillEntries,
readOnly: true,
},
healthPillValuesForSelectedNode: {
type: Array,
computed: '_computeHealthPillForNode(nodeNamesToHealthPills, healthPillStepIndex, selectedNode, allStepsModeEnabled, areHealthPillsLoading)',
},
// When all-steps mode is enabled, the user can request health pills for any step. In this
// mode, Tensorboard makes a request every time the user drags the slider to a different step.
allStepsModeEnabled: {
type: Boolean,
notify: true,
},
// The biggest step value ever seen. Used to determine what steps of health pills to let the
// user fetch in all steps mode.
_biggestStepEverSeen: {
type: Number,
computed: '_computeBiggestStepEverSeen(nodeNamesToHealthPills)',
},
_maxStepIndex: {
type: Number,
computed: '_computeMaxStepIndex(nodeNamesToHealthPills)',
},
_currentStepDisplayValue: {
type: String,
computed: '_computeCurrentStepDisplayValue(nodeNamesToHealthPills, healthPillStepIndex, allStepsModeEnabled, specificHealthPillStep, areHealthPillsLoading)',
},
},
observers: [
'_updateAlertsList(debuggerNumericAlerts)',
],
ready: function() {
var mainContainer = document.getElementById('mainContainer');
var scrollbarContainer = document.querySelector('tf-dashboard-layout .scrollbar');
if (mainContainer && scrollbarContainer) {
// If this component is being used inside of TensorBoard's dashboard layout, it may easily
// cause the dashboard layout element to overflow, giving the user 2 scroll bars. Prevent
// that by hiding whatever content overflows - the user will have to expand the viewport to
// use this debugging card.
mainContainer.style.overflow = 'hidden';
scrollbarContainer.style.overflow = 'hidden';
}
},
_healthPillsAvailable: function(debuggerDataEnabled, nodeNamesToHealthPills) {
// So long as there is a mapping (even if empty) from node name to health pills, show the
// legend and slider. We do that because, even if no health pills exist at the current step,
// the user may desire to change steps, and the slider must show for the user to do that.
return debuggerDataEnabled && nodeNamesToHealthPills;
},
_computeTensorCountString: function(healthPillValuesForSelectedNode, valueIndex) {
if (!healthPillValuesForSelectedNode) {
// No health pill data is available.
return '';
}
return healthPillValuesForSelectedNode[valueIndex].toFixed(0);
},
_computeHealthPillForNode: function(
nodeNamesToHealthPills, healthPillStepIndex, selectedNode, allStepsModeEnabled, areHealthPillsLoading) {
if (areHealthPillsLoading) {
// Health pills are loading. Do not render data that is out of date.
return null;
}
if (!selectedNode) {
// No node is selected.
return null;
}
const healthPills = nodeNamesToHealthPills[selectedNode];
if (!healthPills) {
// This node lacks a health pill.
return null;
}
// If all steps mode is enabled, we use the first health pill in the list because the JSON
// response from the server is a mapping between node name and a list of 1 health pill.
const healthPill = healthPills[allStepsModeEnabled ? 0 : healthPillStepIndex];
if (!healthPill) {
// This node lacks a health pill at the current step.
return null;
}
// The health pill count values start at 2. Each health pill contains 6 values.
return healthPill.value.slice(2, 8);
},
_computeCurrentStepDisplayValue: function(
nodeNamesToHealthPills,
healthPillStepIndex,
allStepsModeEnabled,
specificHealthPillStep,
areHealthPillsLoading) {
if (allStepsModeEnabled) {
// The user seeks health pills for specific step from the server.
return specificHealthPillStep.toFixed(0);
}
if (areHealthPillsLoading) {
// The current step is undefined.
return 0;
}
for (let nodeName in nodeNamesToHealthPills) {
// All nodes have the same number of steps stored, so only examine 1 node. We cannot
// directly index into the nodeNamesToHealthPills object because we do not have a key.
// If all steps mode is enabled, we only have 1 step to show.
return nodeNamesToHealthPills[nodeName][healthPillStepIndex].step.toFixed(0);
}
// The current step could not be computed.
return 0;
},
_computeBiggestStepEverSeen: function(nodeNamesToHealthPills) {
for (let nodeName in nodeNamesToHealthPills) {
// All nodes have the same number of steps stored, so only examine 1 node.
// The index is 1 less than the count. Tensorboard backend logic guarantees that the length
// of the array will be greater than 1.
var healthPills = nodeNamesToHealthPills[nodeName];
return Math.max(this._biggestStepEverSeen, healthPills[healthPills.length - 1].step);
}
// No steps seen so far. Default to 0.
return this._biggestStepEverSeen || 0;
},
_computeMaxStepIndex: function(nodeNamesToHealthPills) {
for (let nodeName in nodeNamesToHealthPills) {
// All nodes have the same number of steps stored, so only examine 1 node.
// The index is 1 less than the count. Tensorboard backend logic guarantees that the length
// of the array will be greater than 1.
return nodeNamesToHealthPills[nodeName].length - 1;
}
// Return a falsy value. The slider should be hidden.
return 0;
},
_hasDebuggerNumericAlerts: function(debuggerNumericAlerts) {
return debuggerNumericAlerts && debuggerNumericAlerts.length;
},
_updateAlertsList: function(debuggerNumericAlerts) {
var alertBody = this.$$('#numeric-alerts-body');
if (!alertBody) {
return;
}
alertBody.innerHTML = '';
for (var i = 0; i < debuggerNumericAlerts.length; i++) {
var alert = debuggerNumericAlerts[i];
var tableRow = document.createElement('tr');
var timestampTd = document.createElement('td');
timestampTd.innerHTML = tf.graph.util.computeHumanFriendlyTime(alert.first_timestamp);
timestampTd.classList.add('first-offense-td');
tableRow.appendChild(timestampTd);
var tensorDeviceTd = document.createElement('td');
tensorDeviceTd.classList.add('tensor-device-td')
var tensorSection = document.createElement('div');
tensorSection.classList.add('tensor-section-within-table');
tensorSection.innerHTML = alert.tensor_name;
this._addOpExpansionListener(tensorSection, alert.tensor_name);
tensorDeviceTd.appendChild(tensorSection);
var deviceSection = document.createElement('div');
deviceSection.classList.add('device-section-within-table');
deviceSection.innerHTML = '(' + alert.device_name + ')';
tensorDeviceTd.appendChild(deviceSection);
tableRow.appendChild(tensorDeviceTd);
var miniHealthPill = document.createElement('div');
miniHealthPill.classList.add('mini-health-pill');
var miniHealthPillTd = document.createElement('td');
miniHealthPillTd.classList.add('mini-health-pill-td');
miniHealthPillTd.appendChild(miniHealthPill);
tableRow.appendChild(miniHealthPillTd);
if (alert.neg_inf_event_count) {
var negativeInfCountSection = document.createElement('div');
negativeInfCountSection.classList.add('negative-inf-mini-health-pill-section');
negativeInfCountSection.innerHTML = alert.neg_inf_event_count;
negativeInfCountSection.setAttribute(
'title', alert.neg_inf_event_count + ' events with -∞')
miniHealthPill.appendChild(negativeInfCountSection);
}
if (alert.pos_inf_event_count) {
var positiveInfCountSection = document.createElement('div');
positiveInfCountSection.classList.add('positive-inf-mini-health-pill-section');
positiveInfCountSection.innerHTML = alert.pos_inf_event_count;
positiveInfCountSection.setAttribute(
'title', alert.pos_inf_event_count + ' events with +∞')
miniHealthPill.appendChild(positiveInfCountSection);
}
if (alert.nan_event_count) {
var nanCountSection = document.createElement('div');
nanCountSection.classList.add('nan-mini-health-pill-section');
nanCountSection.innerHTML = alert.nan_event_count;
nanCountSection.setAttribute(
'title', alert.nan_event_count + ' events with NaN')
miniHealthPill.appendChild(nanCountSection);
}
Polymer.dom(alertBody).appendChild(tableRow);
}
},
// Adds a listener to an element, so that when that element is clicked, the tensor with
// tensorName expands.
_addOpExpansionListener: function(clickableElement, tensorName) {
clickableElement.addEventListener('click', () => {
// When the user clicks on a tensor name, expand all nodes until the user can see the
// associated node.
var nameOfNodeToSelect = tf.graph.render.expandUntilNodeIsShown(
document.getElementById('scene'), this.renderHierarchy, tensorName);
// Store the current scroll of the graph info card. Node selection alters that scroll, and
// we restore the scroll later.
var previousScrollFromBottom;
var graphInfoCard = document.querySelector('tf-graph-info#graph-info');
if (graphInfoCard) {
previousScrollFromBottom = graphInfoCard.scrollHeight - graphInfoCard.scrollTop;
}
// Update the selected node within graph logic.
var previousSelectedNode = this.selectedNode;
this.set('selectedNode', nameOfNodeToSelect);
// Scroll the graph info card back down if necessary so that user can see the alerts section
// again. Selecting the node causes the info card to scroll to the top, which may mean the
// user no longer sees the list of alerts.
var scrollToOriginalLocation = () => {
graphInfoCard.scrollTop = graphInfoCard.scrollHeight - previousScrollFromBottom;
};
if (graphInfoCard) {
// This component is used within an info card. Restore the original scroll.
if (previousSelectedNode) {
// The card for the selected node has already opened. Immediately restore the scroll.
scrollToOriginalLocation();
} else {
// Give some time for the DOM of the info card to be created before scrolling down.
window.setTimeout(scrollToOriginalLocation, 20);
}
}
});
},
});
})();
</script>
</dom-module>

View File

@ -17,6 +17,7 @@ ts_web_library(
deps = [
"//tensorflow/tensorboard/components/tf_dashboard_common",
"//tensorflow/tensorboard/components/tf_graph_common",
"//tensorflow/tensorboard/components/tf_graph_debugger_data_card",
"//tensorflow/tensorboard/components/tf_imports:polymer",
"@org_polymer_iron_collapse",
"@org_polymer_iron_list",
@ -34,6 +35,7 @@ tensorboard_webcomponent_library(
deps = [
"//tensorflow/tensorboard/components/tf_dashboard_common:legacy",
"//tensorflow/tensorboard/components/tf_graph_common:legacy",
"//tensorflow/tensorboard/components/tf_graph_debugger_data_card:legacy",
"//third_party/javascript/polymer/v1/iron-collapse:lib",
"//third_party/javascript/polymer/v1/iron-list:lib",
"//third_party/javascript/polymer/v1/paper-icon-button:lib",

View File

@ -19,6 +19,7 @@ limitations under the License.
<link rel="import" href="../paper-slider/paper-slider.html">
<link rel="import" href="../paper-spinner/paper-spinner-lite.html">
<link rel="import" href="../tf-graph-common/tf-graph-common.html">
<link rel="import" href="../tf-graph-debugger-data-card/tf-graph-debugger-data-card.html">
<link rel="import" href="tf-node-info.html">
<dom-module id="tf-graph-info">
@ -29,6 +30,9 @@ limitations under the License.
margin: 0;
padding: 0;
display: block;
max-height: 650px;
overflow-x: hidden;
overflow-y: auto;
}
h2 {
@ -36,60 +40,6 @@ h2 {
text-align: center;
margin: 0;
}
.health-pill-legend {
padding: 15px;
}
.health-pill-legend h2 {
text-align: left;
}
.health-pill-entry {
margin: 10px 10px 10px 0;
}
.health-pill-entry .color-preview {
width: 26px;
height: 26px;
border-radius: 3px;
display: inline-block;
margin: 0 10px 0 0;
}
.health-pill-entry .color-label, .health-pill-entry .tensor-count {
color: #777;
display: inline-block;
height: 26px;
font-size: 22px;
line-height: 26px;
vertical-align: top;
}
.health-pill-entry .tensor-count {
float: right;
}
#health-pill-step-slider {
width: 100%;
margin: 0 0 0 -15px;
/* 31 comes from adding a padding of 15px from both sides of the paper-slider, subtracting
* 1px so that the slider width aligns with the image (the last slider marker takes up 1px),
* and adding 2px to account for a border of 1px on both sides of the image. 30 - 1 + 2.
* Apparently, the paper-slider lacks a mixin for those padding values. */
width: calc(100% + 31px);
}
#health-pills-loading-spinner {
width: 20px;
height: 20px;
vertical-align: top;
}
#health-pill-step-number-input {
text-align: center;
vertical-align: top;
}
</style>
<template is="dom-if" if="{{selectedNode}}">
<paper-material elevation="1" class="card">
@ -104,68 +54,17 @@ h2 {
</paper-material>
</template>
<template is="dom-if" if="[[_healthPillsAvailable(debuggerDataEnabled, nodeNamesToHealthPills)]]">
<paper-material elevation="1" class="card health-pill-legend">
<div class="title">
Enable all (not just sampled) steps. Requires slow disk read.
</div>
<paper-toggle-button id="enableAllStepsModeToggle" checked="{{allStepsModeEnabled}}">
</paper-toggle-button>
<h2>
Step of Health Pills:
<template is="dom-if" if="[[allStepsModeEnabled]]">
<input type="number"
id="health-pill-step-number-input"
min="0"
max="[[_biggestStepEverSeen]]"
value="{{specificHealthPillStep::input}}">
</template>
<template is="dom-if" if="[[!allStepsModeEnabled]]">
[[_currentStepDisplayValue]]
</template>
<paper-spinner-lite active
hidden$=[[!areHealthPillsLoading]]
id="health-pills-loading-spinner"></paper-spinner-lite>
</h2>
<template is="dom-if" if="[[allStepsModeEnabled]]">
<paper-slider
id="health-pill-step-slider"
immediate-value="{{specificHealthPillStep}}"
max="[[_biggestStepEverSeen]]"
snaps
step="1"
value="{{specificHealthPillStep}}"></paper-slider>
</template>
<template is="dom-if" if="[[!allStepsModeEnabled]]">
<template is="dom-if" if="[[_maxStepIndex]]">
<paper-slider
id="health-pill-step-slider"
immediate-value="{{healthPillStepIndex}}"
max="[[_maxStepIndex]]"
snaps
step="1"
value="{{healthPillStepIndex}}"></paper-slider>
</template>
</template>
<h2>
Health Pill
<template is="dom-if" if="[[healthPillValuesForSelectedNode]]">
Counts for Selected Node
</template>
<template is="dom-if" if="[[!healthPillValuesForSelectedNode]]">
Legend
</template>
</h2>
<template is="dom-repeat" items="[[healthPillEntries]]">
<div class="health-pill-entry">
<div class="color-preview" style="background:[[item.background_color]]"></div>
<div class="color-label">[[item.label]]</div>
<div class="tensor-count">
[[_computeTensorCountString(healthPillValuesForSelectedNode, index)]]
</div>
</div>
</template>
</paper-material>
<tf-graph-debugger-data-card render-hierarchy="[[renderHierarchy]]"
debugger-numeric-alerts="[[debuggerNumericAlerts]]"
node-names-to-health-pills="[[nodeNamesToHealthPills]]"
render-hierarchy="[[renderHierarchy]]"
selected-node="{{selectedNode}}"
highlighted-node="{{highlightedNode}}"
are-health-pills-loading="[[areHealthPillsLoading]]"
all-steps-mode-enabled="{{allStepsModeEnabled}}"
specific-health-pill-step="{{specificHealthPillStep}}"
health-pill-step-index="{{healthPillStepIndex}}">
</tf-graph-debugger-data-card>
</template>
</template>
<script>
@ -185,13 +84,6 @@ h2 {
type: Number,
notify: true,
},
// Only relevant if we are in all steps mode, in which case the user may want to view health
// pills for a specific step.
specificHealthPillStep: {
type: Number,
value: 0,
notify: true,
},
colorBy: String,
// Two-ways
selectedNode: {
@ -211,36 +103,6 @@ h2 {
debuggerDataEnabled: Boolean,
// Whether health pills are currently being loaded, in which case we show a spinner (and the
// current health pills shown might be out of date).
areHealthPillsLoading: Boolean,
healthPillEntries: {
type: Array,
value: tf.graph.scene.healthPillEntries,
readOnly: true,
},
healthPillValuesForSelectedNode: {
type: Array,
computed: '_computeHealthPillForNode(nodeNamesToHealthPills, healthPillStepIndex, selectedNode, allStepsModeEnabled, areHealthPillsLoading)',
},
// When all-steps mode is enabled, the user can request health pills for any step. In this
// mode, Tensorboard makes a request every time the user drags the slider to a different step.
allStepsModeEnabled: {
type: Boolean,
notify: true,
},
// The biggest step value ever seen. Used to determine what steps of health pills to let the
// user fetch in all steps mode.
_biggestStepEverSeen: {
type: Number,
computed: '_computeBiggestStepEverSeen(nodeNamesToHealthPills)',
},
_maxStepIndex: {
type: Number,
computed: '_computeMaxStepIndex(nodeNamesToHealthPills)',
},
_currentStepDisplayValue: {
type: String,
computed: '_computeCurrentStepDisplayValue(nodeNamesToHealthPills, healthPillStepIndex, allStepsModeEnabled, specificHealthPillStep, areHealthPillsLoading)',
},
},
listeners: {
'node-list-item-click': '_nodeListItemClicked',
@ -262,92 +124,6 @@ h2 {
// the user may desire to change steps, and the slider must show for the user to do that.
return debuggerDataEnabled && nodeNamesToHealthPills;
},
_computeTensorCountString: function(healthPillValuesForSelectedNode, valueIndex) {
if (!healthPillValuesForSelectedNode) {
// No health pill data is available.
return '';
}
return healthPillValuesForSelectedNode[valueIndex].toFixed(0);
},
_computeHealthPillForNode: function(
nodeNamesToHealthPills, healthPillStepIndex, selectedNode, allStepsModeEnabled, areHealthPillsLoading) {
if (areHealthPillsLoading) {
// Health pills are loading. Do not render data that is out of date.
return null;
}
if (!selectedNode) {
// No node is selected.
return null;
}
const healthPills = nodeNamesToHealthPills[selectedNode];
if (!healthPills) {
// This node lacks a health pill.
return null;
}
// If all steps mode is enabled, we use the first health pill in the list because the JSON
// response from the server is a mapping between node name and a list of 1 health pill.
const healthPill = healthPills[allStepsModeEnabled ? 0 : healthPillStepIndex];
if (!healthPill) {
// This node lacks a health pill at the current step.
return null;
}
// The health pill count values start at 2. Each health pill contains 6 values.
return healthPill.value.slice(2, 8);
},
_computeCurrentStepDisplayValue: function(
nodeNamesToHealthPills,
healthPillStepIndex,
allStepsModeEnabled,
specificHealthPillStep,
areHealthPillsLoading) {
if (allStepsModeEnabled) {
// The user seeks health pills for specific step from the server.
return specificHealthPillStep.toFixed(0);
}
if (areHealthPillsLoading) {
// The current step is undefined.
return 0;
}
for (let nodeName in nodeNamesToHealthPills) {
// All nodes have the same number of steps stored, so only examine 1 node. We cannot
// directly index into the nodeNamesToHealthPills object because we do not have a key.
// If all steps mode is enabled, we only have 1 step to show.
return nodeNamesToHealthPills[nodeName][healthPillStepIndex].step.toFixed(0);
}
// The current step could not be computed.
return 0;
},
_computeBiggestStepEverSeen: function(nodeNamesToHealthPills) {
for (let nodeName in nodeNamesToHealthPills) {
// All nodes have the same number of steps stored, so only examine 1 node.
// The index is 1 less than the count. Tensorboard backend logic guarantees that the length
// of the array will be greater than 1.
var healthPills = nodeNamesToHealthPills[nodeName];
return Math.max(this._biggestStepEverSeen, healthPills[healthPills.length - 1].step);
}
// No steps seen so far. Default to 0.
return this._biggestStepEverSeen || 0;
},
_computeMaxStepIndex: function(nodeNamesToHealthPills) {
for (let nodeName in nodeNamesToHealthPills) {
// All nodes have the same number of steps stored, so only examine 1 node.
// The index is 1 less than the count. Tensorboard backend logic guarantees that the length
// of the array will be greater than 1.
return nodeNamesToHealthPills[nodeName].length - 1;
}
// Return a falsy value. The slider should be hidden.
return 0;
},
});
})();
</script>