Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

histogram: implement buckets_v3 #5356

Merged
merged 12 commits into from Oct 7, 2021
Merged

Conversation

yatbear
Copy link
Member

@yatbear yatbear commented Oct 1, 2021

Main changes:

  • For single value input, the histogram output will no longer be reshaped to length 1, instead, there will be {bucket_count} buckets where the first {bucket_count - 1} buckets have zero counts and the last bucket has count {data_size}, since only the last bucket is closed-closed and the other buckets are closed-open.
  • If bucket_count is invalid (<= 0), an empty tensor of shape (0, 3) will be returned.
  • Naming singular is replaced by single_value in v3 to avoid confusion with the mathematical term singularity.

Tests for v3 pb op will be added once histogram_pb_v3 is implemented.

#histogram #tpu

tensorboard/plugins/histogram/summary_v2.py Outdated Show resolved Hide resolved
tensorboard/plugins/histogram/summary_v2.py Outdated Show resolved Hide resolved
tensorboard/plugins/histogram/summary_test.py Outdated Show resolved Hide resolved
tensorboard/plugins/histogram/summary_test.py Outdated Show resolved Hide resolved
tensorboard/plugins/histogram/summary_test.py Outdated Show resolved Hide resolved
tensorboard/plugins/histogram/summary_test.py Outdated Show resolved Hide resolved
@yatbear yatbear requested a review from nfelt October 2, 2021 00:02
tensorboard/plugins/histogram/summary_test.py Outdated Show resolved Hide resolved
tensorboard/plugins/histogram/summary_test.py Show resolved Hide resolved
tensorboard/plugins/histogram/summary_test.py Outdated Show resolved Hide resolved
tensorboard/plugins/histogram/summary_v2.py Outdated Show resolved Hide resolved
tensorboard/plugins/histogram/summary_v2.py Outdated Show resolved Hide resolved
  Use `single_value` here, `singular` has unrelated mathematical meaning:
  https://en.wikipedia.org/wiki/Singularity_(mathematics).
  Since v3 change the outcome for single value input, this enables the
  customization of these test cases.
  More tests for v3 pb ops will be added after histogram_pb_v3 is implemented.
This reverts commit be2213cac544d17a9cec65e9e3b37a4c8a59d5d2.
  TODO: figure out why `test_zero_bucket_count` doesn't work in the
  v3 graph test case (works in v3 op test case).
@@ -263,6 +347,14 @@ def graph_fn():
with writer.as_default():
graph_fn.get_concrete_function()

def test_zero_bucket_count(self):
self.skipTest(
"TODO: figure out why this doesn't work in graph test case"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I think the reason this was failing is that in graph mode, when it compiles the TensorFlow code into a graph, all of the branches of the tf.cond()s are inserted as parts of the graph (even if those branches won't be taken when the graph is actually executed). When doing that, TensorFlow does shape inference, described a little bit here: https://www.tensorflow.org/guide/create_op#shape_functions_in_c

Basically, it tries to compute what the resulting shape will be for all of the ops in all of the branches. At the line where we currently do tf.fill([bucket_count - 1], 0), because bucket_count is a constant, it is able to determine at compile time that this will result in tf.fill([-1], 0) and the shape function for tf.fill emits an error, because this would result in an output shape that doesn't make sense.

Hence the error (at least, the one that I was seeing when trying this PR locally):

ValueError: Fill dimensions must be >= 0 for '{{node zero_bucket_count/write_summary/buckets/cond/cond/Fill_1}} = Fill[T=DT_INT32, index_type=DT_INT32](zero_bucket_count/write_summary/buckets/cond/cond/Fill_1/dims, zero_bucket_count/write_summary/buckets/cond/cond/Fill_1/value)' with input shapes: [1], [] and with input tensors computed as partial shapes: input[0] = [?].

In other words, it doesn't actually mean it's picking the wrong branch of the conditional to execute; it's that during graph compilation, it visits all the branches regardless of what will be ultimately executed.

(Aside: you could argue that when bucket_count is a constant 0, we could actually optimize away all the tf.cond ops and all the branches besides the empty case, at which point we wouldn't hit this error. I'm guessing that the compilation's constant-folding isn't quite smart enough to do that. Also, if you change the test to pass bucket_count=tf.Variable(0) and replace the use of max() with tf.math.maximum() as mentioned in the other comment, it will also pass, since now that the bucket count is a variable, it doesn't do as much shape inference and can't raise the false positive error.)

Ultimately, the best way to fix this is to just ensure that all the branches are still creating the correctly shaped tensor of dimensions (bucket_count, 3), even when we know that branch will never be executed. Here's a patch that gets the test to pass:

diff --git i/tensorboard/plugins/histogram/summary_v2.py w/tensorboard/plugins/histogram/summary_v2.py
index bff980492..e1cd01846 100644
--- i/tensorboard/plugins/histogram/summary_v2.py
+++ w/tensorboard/plugins/histogram/summary_v2.py
@@ -425,6 +425,8 @@ def _buckets_v3(data, bucket_count=None):
     with tf.name_scope("buckets"):
         tf.debugging.assert_scalar(bucket_count)
         tf.debugging.assert_type(bucket_count, tf.int32)
+        # Treat a negative bucket count as zero.
+        bucket_count = tf.math.maximum(0, bucket_count)
         data = tf.reshape(data, shape=[-1])  # flatten
         data = tf.cast(data, tf.float64)
         data_size = tf.size(input=data)
@@ -440,7 +442,7 @@ def _buckets_v3(data, bucket_count=None):
             2. If the input data is empty, a tensor of shape (bucket_count, 3)
               of all zero values will be returned.
             """
-            return tf.zeros((max(0, bucket_count), 3), dtype=tf.float64)
+            return tf.zeros((bucket_count, 3), dtype=tf.float64)

         def when_nonempty():
             min_ = tf.reduce_min(input_tensor=data)
@@ -480,11 +482,12 @@ def _buckets_v3(data, bucket_count=None):
                 """When input data contains a single unique value."""
                 # Left and right edges are the same for single value input.
                 edges = tf.fill([bucket_count], max_)
-                # Counts for the first {bucket_count - 1} buckets [v, v) are 0.
-                zero_bucket_counts = tf.fill([bucket_count - 1], 0)
-                # Count for last bucket [v, v] is {data_size}.
+                # Bucket counts are 0 except the last bucket (if bucket_count > 0),
+                # which is `data_size`. Ensure that the resulting counts vector has
+                # length `bucket_count` always, including the bucket_count==0 case.
+                zeroes = tf.fill([bucket_count], 0)
                 bucket_counts = tf.cast(
-                    tf.concat([zero_bucket_counts, [data_size]], 0),
+                    tf.concat([zeroes[:-1], [data_size]], 0)[:bucket_count],
                     dtype=tf.float64,
                 )
                 return tf.transpose(a=tf.stack([edges, edges, bucket_counts]))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for the clear explanation and the patch!

tensorboard/plugins/histogram/summary_test.py Show resolved Hide resolved
2. If the input data is empty, a tensor of shape (bucket_count, 3)
of all zero values will be returned.
"""
return tf.zeros((max(0, bucket_count), 3), dtype=tf.float64)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use tf.math.maximum() here, rather than Python's max(), in order to be compatible with bucket_count values that aren't native Python numbers (e.g. a tf.constant() instead).

Also I'd recommend just adding a conversion up at the top that does bucket_count = tf.math.maximum(0, bucket_count) so we don't have to worry about negative bucket counts in any of the later logic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Thanks!

  Move tf.math.maximum() to the top and avoid compile time shape
  inference that fails the conditional branch that isn't supposed
  to be execute when bucket_count is 0.
@yatbear yatbear requested a review from nfelt October 7, 2021 00:10
Copy link
Collaborator

@nfelt nfelt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM modulo what looks like unintentional additions

def graph_fn():
# Recreate the active scope inside the defun since it won't propagate.
with tf.name_scope(scope):
self.call_histogram_op(*args, **kwargs)

writer = tf2.summary.create_file_writer(self.get_temp_dir())
with writer.as_default():
# print(tf2.autograph.to_code(graph_fn.python_function))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unintentional? Also the autograph=True addition above seems like it shouldn't be necessary? (I believe it's true by default.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. Thanks!

@yatbear yatbear merged commit 42368f5 into tensorflow:master Oct 7, 2021
@yatbear yatbear deleted the buckets_v3 branch October 7, 2021 16:21
yatbear added a commit to yatbear/tensorboard that referenced this pull request Mar 27, 2023
* rename functions to avoid confusion

  Use `single_value` here, `singular` has unrelated mathematical meaning:
  https://en.wikipedia.org/wiki/Singularity_(mathematics).

* add v3 implementation for single value input

* check if bucket_count <= 0 and fix tf ops

* Make SummaryV3OpGraphTest inherit from V2 test case and add test for zero bucket count

* use an alternative (tf.fill) to be consistent

* distinguish zero bucket count case v.s. empty input data case

* add SummaryV3OpGraphTest test case and update tests

* make bucket_count a variable before tf.cond op

  Move tf.math.maximum() to the top and avoid compile time shape
  inference that fails the conditional branch that isn't supposed
  to be execute when bucket_count is 0.
dna2github pushed a commit to dna2fork/tensorboard that referenced this pull request May 1, 2023
* rename functions to avoid confusion

  Use `single_value` here, `singular` has unrelated mathematical meaning:
  https://en.wikipedia.org/wiki/Singularity_(mathematics).

* add v3 implementation for single value input

* check if bucket_count <= 0 and fix tf ops

* Make SummaryV3OpGraphTest inherit from V2 test case and add test for zero bucket count

* use an alternative (tf.fill) to be consistent

* distinguish zero bucket count case v.s. empty input data case

* add SummaryV3OpGraphTest test case and update tests

* make bucket_count a variable before tf.cond op

  Move tf.math.maximum() to the top and avoid compile time shape
  inference that fails the conditional branch that isn't supposed
  to be execute when bucket_count is 0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants