Extract the distributions dashboard to a plugin

This continues the great plugin migration. The distributions plugin was
similar to the histograms plugin, but it also purported to allow CSV
download like the scalars plugin. However, the existing implementation
of this was flawed, and would always yield a 500 on current prod [1]
(unless there were actually no data). This indicates that no one is
actually using it---probably because there isn't a relevant button on
the frontend, anyway!---so I just removed it.

This also changes most frontend occurrences of "compressedHistograms"
to "distributions" while we're at it.

[1]: Due to the reference `value.rank_in_bps` in the handler
`_serve_compressed_histograms`; this field does not exist and throws an
`AttributeError`.

PiperOrigin-RevId: 157787156
This commit is contained in:
William Chargin 2017-06-01 17:37:28 -07:00 committed by TensorFlower Gardener
parent 23cdf96b85
commit 7d7a403096
13 changed files with 277 additions and 80 deletions

View File

@ -380,6 +380,7 @@ filegroup(
"//tensorflow/tensorboard/java/org/tensorflow/tensorboard/vulcanize:all_files",
"//tensorflow/tensorboard/plugins:all_files",
"//tensorflow/tensorboard/plugins/audio:all_files",
"//tensorflow/tensorboard/plugins/distributions:all_files",
"//tensorflow/tensorboard/plugins/histograms:all_files",
"//tensorflow/tensorboard/plugins/images:all_files",
"//tensorflow/tensorboard/plugins/projector:all_files",

View File

@ -230,6 +230,7 @@ add_python_module("tensorflow/tensorboard/backend")
add_python_module("tensorflow/tensorboard/backend/event_processing")
add_python_module("tensorflow/tensorboard/plugins")
add_python_module("tensorflow/tensorboard/plugins/audio")
add_python_module("tensorflow/tensorboard/plugins/distributions")
add_python_module("tensorflow/tensorboard/plugins/histograms")
add_python_module("tensorflow/tensorboard/plugins/images")
add_python_module("tensorflow/tensorboard/plugins/projector")

View File

@ -14,6 +14,7 @@ py_binary(
"//tensorflow/tensorboard/backend:application",
"//tensorflow/tensorboard/backend/event_processing:event_file_inspector",
"//tensorflow/tensorboard/plugins/audio:audio_plugin",
"//tensorflow/tensorboard/plugins/distributions:distributions_plugin",
"//tensorflow/tensorboard/plugins/histograms:histograms_plugin",
"//tensorflow/tensorboard/plugins/images:images_plugin",
"//tensorflow/tensorboard/plugins/projector:projector_plugin",

View File

@ -22,15 +22,12 @@ from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import csv
import os
import re
import threading
import time
import six
from six import StringIO
from six.moves import xrange # pylint: disable=redefined-builtin
from six.moves.urllib import parse as urlparse
import tensorflow as tf
from werkzeug import wrappers
@ -59,6 +56,7 @@ DEFAULT_SIZE_GUIDANCE = {
# /data/runs entirely.
_MIGRATED_DATA_KEYS = frozenset((
'audio',
'distributions',
'histograms',
'images',
'scalars',
@ -69,7 +67,6 @@ LOGDIR_ROUTE = '/logdir'
RUNS_ROUTE = '/runs'
PLUGIN_PREFIX = '/plugin'
PLUGINS_LISTING_ROUTE = '/plugins_listing'
COMPRESSED_HISTOGRAMS_ROUTE = '/' + event_accumulator.COMPRESSED_HISTOGRAMS
GRAPH_ROUTE = '/' + event_accumulator.GRAPH
RUN_METADATA_ROUTE = '/' + event_accumulator.RUN_METADATA
TAB_ROUTES = ['', '/events', '/images', '/audio', '/graphs', '/histograms']
@ -80,16 +77,6 @@ TAB_ROUTES = ['', '/events', '/images', '/audio', '/graphs', '/histograms']
_VALID_PLUGIN_RE = re.compile(r'^[A-Za-z0-9_.-]+$')
class _OutputFormat(object):
"""An enum used to list the valid output formats for API calls.
Not all API calls support all formats (for example, only scalars and
compressed histograms support CSV).
"""
JSON = 'json'
CSV = 'csv'
def standard_tensorboard_wsgi(
logdir,
purge_orphaned_data,
@ -159,8 +146,6 @@ class TensorBoardWSGIApp(object):
reload_multiplexer(self._multiplexer, path_to_run)
self.data_applications = {
DATA_PREFIX + COMPRESSED_HISTOGRAMS_ROUTE:
self._serve_compressed_histograms,
DATA_PREFIX + GRAPH_ROUTE:
self._serve_graph,
DATA_PREFIX + LOGDIR_ROUTE:
@ -278,35 +263,6 @@ class TensorBoardWSGIApp(object):
return http_util.Respond(
request, str(run_metadata), 'text/x-protobuf') # pbtxt
@wrappers.Request.application
def _serve_compressed_histograms(self, request):
"""Given a tag and single run, return an array of compressed histograms."""
tag = request.args.get('tag')
run = request.args.get('run')
compressed_histograms = self._multiplexer.CompressedHistograms(run, tag)
if request.args.get('format') == _OutputFormat.CSV:
string_io = StringIO()
writer = csv.writer(string_io)
# Build the headers; we have two columns for timing and two columns for
# each compressed histogram bucket.
headers = ['Wall time', 'Step']
if compressed_histograms:
bucket_count = len(compressed_histograms[0].compressed_histogram_values)
for i in xrange(bucket_count):
headers += ['Edge %d basis points' % i, 'Edge %d value' % i]
writer.writerow(headers)
for compressed_histogram in compressed_histograms:
row = [compressed_histogram.wall_time, compressed_histogram.step]
for value in compressed_histogram.compressed_histogram_values:
row += [value.rank_in_bps, value.value]
writer.writerow(row)
return http_util.Respond(request, string_io.getvalue(), 'text/csv')
else:
return http_util.Respond(
request, compressed_histograms, 'application/json')
@wrappers.Request.application
def _serve_plugins_listing(self, request):
"""Serves an object mapping plugin name to whether it is enabled.

View File

@ -167,7 +167,6 @@ class TensorboardServerTest(tf.test.TestCase):
run_json,
{
'run1': {
'compressedHistograms': ['histogram'],
# if only_use_meta_graph, the graph is from the metagraph
'graph': True,
'meta_graph': self._only_use_meta_graph,
@ -265,13 +264,8 @@ class TensorboardServerTest(tf.test.TestCase):
"""Generates the test data directory.
The test data has a single run named run1 which contains:
- a histogram [1]
- a graph definition
[1]: Histograms no longer appear in `/runs`, but compressed
histograms do, and they use the same test data. Thus, histograms are
still here for now.
Returns:
temp_dir: The directory the test data is generated under.
"""
@ -281,14 +275,6 @@ class TensorboardServerTest(tf.test.TestCase):
os.makedirs(run1_path)
writer = tf.summary.FileWriter(run1_path)
histogram_value = tf.HistogramProto(
min=0,
max=2,
num=3,
sum=6,
sum_squares=5,
bucket_limit=[0, 1, 2],
bucket=[1, 1, 1])
# Add a simple graph event.
graph_def = tf.GraphDef()
node1 = graph_def.node.add()
@ -309,13 +295,6 @@ class TensorboardServerTest(tf.test.TestCase):
device_stats = run_metadata.step_stats.dev_stats.add()
device_stats.device = 'test device'
writer.add_run_metadata(run_metadata, 'test run')
writer.add_event(
tf.Event(
wall_time=0,
step=0,
summary=tf.Summary(value=[
tf.Summary.Value(tag='histogram', histo=histogram_value),
])))
writer.flush()
writer.close()

View File

@ -72,7 +72,7 @@ SUMMARY_TYPES = {
## The tagTypes below are just arbitrary strings chosen to pass the type
## information of the tag from the backend to the frontend
COMPRESSED_HISTOGRAMS = 'compressedHistograms'
COMPRESSED_HISTOGRAMS = 'distributions'
HISTOGRAMS = 'histograms'
IMAGES = 'images'
AUDIO = 'audio'

View File

@ -193,8 +193,9 @@ export class Backend {
* Return a promise showing the Run-to-Tag mapping for compressedHistogram
* data.
*/
public compressedHistogramRuns(): Promise<RunToTag> {
return this.runs().then((x) => _.mapValues(x, 'compressedHistograms'));
public compressedHistogramTags(): Promise<RunToTag> {
return this.requestManager.request(
this.router.pluginRoute('distributions', '/tags'));
}
/**
@ -343,7 +344,8 @@ export class Backend {
*/
private compressedHistogram(tag: string, run: string):
Promise<Array<Datum&CompressedHistogramTuple>> {
const url = this.router.compressedHistograms(tag, run);
const url = (this.router.pluginRunTagRoute(
'distributions', '/distributions')(tag, run));
let p: Promise<TupleData<CompressedHistogramTuple>[]>;
p = this.requestManager.request(url);
return p.then(map(detupler((x) => x)));

View File

@ -21,7 +21,6 @@ export interface Router {
logdir: () => string;
runs: () => string;
isDemoMode: () => boolean;
compressedHistograms: RunTagUrlFn;
graph:
(run: string, limit_attr_size?: number,
large_attrs_key?: string) => string;
@ -88,7 +87,6 @@ export function router(dataDir = 'data', demoMode = false): Router {
runs: () => dataDir + '/runs' + (demoMode ? '.json' : ''),
isDemoMode: () => demoMode,
graph: graphUrl,
compressedHistograms: standardRoute('compressedHistograms'),
runMetadata: standardRoute('run_metadata', '.pbtxt'),
healthPills: () => dataDir + '/plugin/debugger/health_pills',
textRuns: () => dataDir + '/plugin/text/runs' + (demoMode ? '.json' : ''),

View File

@ -55,13 +55,11 @@ all of the data available from the TensorBoard server. Here is an example:
{
"train_run": {
"compressedHistograms": ["foo_histogram", "bar_histogram"],
"graph": true,
"firstEventTimestamp": 123456.789
"run_metadata": ["forward prop", "inference"]
},
"eval": {
"compressedHistograms": ["foo_histogram", "bar_histogram"],
"graph": false,
"run_metadata": []
}
@ -81,6 +79,7 @@ and will not appear in the output from this route:
- `audio`
- `images`
- `scalars`
- `compressedHistograms`, moved to `distributions`
- `histograms`
## `/data/plugin/scalars/tags`
@ -160,7 +159,21 @@ Annotated Example: (note - real data is higher precision)
]
]
## '/data/compressedHistograms?run=foo&tag=bar'
## `/data/plugin/distributions/tags`
Returns a dictionary mapping from `run_name` (quoted string) to arrays of
`tag_name` (quoted string), where each array contains the names of all
distribution tags present in the corresponding run. Here is an example:
{
"train_run": ["foo_histogram", "bar_histogram"],
"eval": ["foo_histogram", "bar_histogram"]
}
Note that runs without any distribution tags are included as keys with
value the empty array.
## `/data/plugin/distributions/distributions?run=foo&tag=bar`
Returns an array of event_accumulator.CompressedHistogramEvents ([wall_time,
step, CompressedHistogramValues]) for the given run and tag.
@ -180,8 +193,8 @@ Annotated Example: (note - real data is higher precision)
[
1441154832.580509, # wall_time
5, # step
[ [0, -3.67], # CompressedHistogramValue for 0th percentile
[2500, -4.19], # CompressedHistogramValue for 25th percentile
[ [0, -3.67], # CompressedHistogramValue for 0th percentile
[2500, -4.19], # CompressedHistogramValue for 25th percentile
[5000, 6.29],
[7500, 1.64],
[10000, 3.67]

View File

@ -0,0 +1,50 @@
# Description:
# TensorBoard plugin for distributions
package(default_visibility = ["//tensorflow:internal"])
licenses(["notice"]) # Apache 2.0
exports_files(["LICENSE"])
load("//tensorflow:tensorflow.bzl", "py_test")
## Distributions Plugin ##
py_library(
name = "distributions_plugin",
srcs = ["distributions_plugin.py"],
srcs_version = "PY2AND3",
visibility = [
"//tensorflow:internal",
],
deps = [
"//tensorflow/tensorboard/backend:http_util",
"//tensorflow/tensorboard/backend/event_processing:event_accumulator",
"//tensorflow/tensorboard/plugins:base_plugin",
"@org_pocoo_werkzeug//:werkzeug",
"@six_archive//:six",
],
)
py_test(
name = "distributions_plugin_test",
size = "small",
srcs = ["distributions_plugin_test.py"],
main = "distributions_plugin_test.py",
srcs_version = "PY2AND3",
deps = [
":distributions_plugin",
"//tensorflow:tensorflow_py",
"//tensorflow/tensorboard/backend:application",
"//tensorflow/tensorboard/backend/event_processing:event_accumulator",
"//tensorflow/tensorboard/backend/event_processing:event_multiplexer",
"@org_pocoo_werkzeug//:werkzeug",
"@six_archive//:six",
],
)
filegroup(
name = "all_files",
srcs = glob(["**"]),
visibility = ["//tensorflow:__pkg__"],
)

View File

@ -0,0 +1,69 @@
# 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.
# ==============================================================================
"""The TensorBoard Distributions (a.k.a. compressed histograms) plugin."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from werkzeug import wrappers
from tensorflow.tensorboard.backend import http_util
from tensorflow.tensorboard.backend.event_processing import event_accumulator
from tensorflow.tensorboard.plugins import base_plugin
_PLUGIN_PREFIX_ROUTE = event_accumulator.COMPRESSED_HISTOGRAMS
class DistributionsPlugin(base_plugin.TBPlugin):
"""Distributions Plugin for TensorBoard."""
plugin_name = _PLUGIN_PREFIX_ROUTE
def get_plugin_apps(self, multiplexer, unused_logdir):
self._multiplexer = multiplexer
return {
'/distributions': self.distributions_route,
'/tags': self.tags_route,
}
def is_active(self):
"""This plugin is active iff any run has at least one relevant tag."""
return any(self.index_impl().values())
def index_impl(self):
return {
run_name: run_data[event_accumulator.COMPRESSED_HISTOGRAMS]
for (run_name, run_data) in self._multiplexer.Runs().items()
if event_accumulator.COMPRESSED_HISTOGRAMS in run_data
}
def distributions_impl(self, tag, run):
"""Result of the form `(body, mime_type)`."""
values = self._multiplexer.CompressedHistograms(run, tag)
return (values, 'application/json')
@wrappers.Request.application
def tags_route(self, request):
index = self.index_impl()
return http_util.Respond(request, index, 'application/json')
@wrappers.Request.application
def distributions_route(self, request):
"""Given a tag and single run, return array of compressed histograms."""
tag = request.args.get('tag')
run = request.args.get('run')
(body, mime_type) = self.distributions_impl(tag, run)
return http_util.Respond(request, body, mime_type)

View File

@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
# 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.
# ==============================================================================
"""Integration tests for the Distributions Plugin."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import os.path
from six.moves import xrange # pylint: disable=redefined-builtin
import tensorflow as tf
from tensorflow.tensorboard.backend.event_processing import event_accumulator
from tensorflow.tensorboard.backend.event_processing import event_multiplexer
from tensorflow.tensorboard.plugins.distributions import distributions_plugin
class DistributionsPluginTest(tf.test.TestCase):
_STEPS = 99
_DISTRIBUTION_TAG = 'my-favorite-distribution'
_SCALAR_TAG = 'my-boring-scalars'
_RUN_WITH_DISTRIBUTION = '_RUN_WITH_DISTRIBUTION'
_RUN_WITH_SCALARS = '_RUN_WITH_SCALARS'
def set_up_with_runs(self, run_names):
self.logdir = self.get_temp_dir()
for run_name in run_names:
self.generate_run(run_name)
multiplexer = event_multiplexer.EventMultiplexer(size_guidance={
# don't truncate my test data, please
event_accumulator.COMPRESSED_HISTOGRAMS:
self._STEPS,
})
multiplexer.AddRunsFromDirectory(self.logdir)
multiplexer.Reload()
self.plugin = distributions_plugin.DistributionsPlugin()
self.apps = self.plugin.get_plugin_apps(multiplexer, None)
def generate_run(self, run_name):
if run_name == self._RUN_WITH_DISTRIBUTION:
(use_distributions, use_scalars) = (True, False)
elif run_name == self._RUN_WITH_SCALARS:
(use_distributions, use_scalars) = (False, True)
else:
assert False, 'Invalid run name: %r' % run_name
tf.reset_default_graph()
sess = tf.Session()
placeholder = tf.placeholder(tf.float32, shape=[3])
if use_distributions:
tf.summary.histogram(self._DISTRIBUTION_TAG, placeholder)
if use_scalars:
tf.summary.scalar(self._SCALAR_TAG, tf.reduce_mean(placeholder))
summ = tf.summary.merge_all()
subdir = os.path.join(self.logdir, run_name)
writer = tf.summary.FileWriter(subdir)
writer.add_graph(sess.graph)
for step in xrange(self._STEPS):
feed_dict = {placeholder: [1 + step, 2 + step, 3 + step]}
s = sess.run(summ, feed_dict=feed_dict)
writer.add_summary(s, global_step=step)
writer.close()
def test_index(self):
self.set_up_with_runs([self._RUN_WITH_DISTRIBUTION,
self._RUN_WITH_SCALARS])
self.assertEqual({
self._RUN_WITH_DISTRIBUTION: [self._DISTRIBUTION_TAG],
self._RUN_WITH_SCALARS: [],
}, self.plugin.index_impl())
def _test_distributions_json(self, run_name, should_have_distributions):
self.set_up_with_runs([self._RUN_WITH_DISTRIBUTION,
self._RUN_WITH_SCALARS])
if should_have_distributions:
(data, mime_type) = self.plugin.distributions_impl(
self._DISTRIBUTION_TAG, run_name)
self.assertEqual('application/json', mime_type)
self.assertEqual(len(data), self._STEPS)
for i in xrange(self._STEPS):
self.assertEqual(i, data[i].step)
else:
with self.assertRaises(KeyError):
self.plugin.distributions_impl(
self._DISTRIBUTION_TAG, run_name)
def test_distributions_json_with_scalars(self):
self._test_distributions_json(self._RUN_WITH_DISTRIBUTION, True)
def test_distributions_json_with_histogram(self):
self._test_distributions_json(self._RUN_WITH_SCALARS, False)
def test_active_with_distribution(self):
self.set_up_with_runs([self._RUN_WITH_DISTRIBUTION])
self.assertTrue(self.plugin.is_active())
def test_active_with_scalars(self):
self.set_up_with_runs([self._RUN_WITH_SCALARS])
self.assertFalse(self.plugin.is_active())
def test_active_with_both(self):
self.set_up_with_runs([self._RUN_WITH_DISTRIBUTION,
self._RUN_WITH_SCALARS])
self.assertTrue(self.plugin.is_active())
if __name__ == '__main__':
tf.test.main()

View File

@ -33,6 +33,7 @@ from werkzeug import serving
from tensorflow.tensorboard.backend import application
from tensorflow.tensorboard.backend.event_processing import event_file_inspector as efi
from tensorflow.tensorboard.plugins.audio import audio_plugin
from tensorflow.tensorboard.plugins.distributions import distributions_plugin
from tensorflow.tensorboard.plugins.histograms import histograms_plugin
from tensorflow.tensorboard.plugins.images import images_plugin
from tensorflow.tensorboard.plugins.projector import projector_plugin
@ -204,10 +205,11 @@ def main(unused_argv=None):
return 0
else:
plugins = [
audio_plugin.AudioPlugin(),
histograms_plugin.HistogramsPlugin(),
images_plugin.ImagesPlugin(),
scalars_plugin.ScalarsPlugin(),
images_plugin.ImagesPlugin(),
audio_plugin.AudioPlugin(),
distributions_plugin.DistributionsPlugin(),
histograms_plugin.HistogramsPlugin(),
projector_plugin.ProjectorPlugin(),
text_plugin.TextPlugin(),
]