Repository navigation
data: implement gRPC data provider for scalars #4356
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
Changes from all commits
3d9e07e
17358a5
9939eba
6632225
4c576ce
fc765da
b0dda21
b5e14db
eb4597c
e138214
8054686
d67379a
8e90194
ae9c1b0
6034782
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| # Copyright 2020 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. | ||
| # ============================================================================== | ||
| """A data provider that talks to a gRPC server.""" | ||
|
|
||
| import contextlib | ||
|
|
||
| import grpc | ||
|
|
||
| from tensorboard.util import timing | ||
| from tensorboard import errors | ||
| from tensorboard.data import provider | ||
| from tensorboard.data.proto import data_provider_pb2 | ||
| from tensorboard.data.proto import data_provider_pb2_grpc | ||
|
|
||
|
|
||
| def make_stub(channel): | ||
| """Wraps a gRPC channel with a service stub.""" | ||
| return data_provider_pb2_grpc.TensorBoardDataProviderStub(channel) | ||
|
|
||
|
|
||
| class GrpcDataProvider(provider.DataProvider): | ||
| """Data provider that talks over gRPC.""" | ||
|
|
||
| def __init__(self, addr, stub): | ||
| """Initializes a GrpcDataProvider. | ||
|
|
||
| Args: | ||
| addr: String address of the remote peer. Used cosmetically for | ||
| data location. | ||
| stub: `data_provider_pb2_grpc.TensorBoardDataProviderStub` | ||
| value. See `make_stub` to construct one from a channel. | ||
| """ | ||
| self._addr = addr | ||
| self._stub = stub | ||
|
|
||
| def data_location(self, ctx, *, experiment_id): | ||
| return "grpc://%s" % (self._addr,) | ||
|
|
||
| def list_plugins(self, ctx, *, experiment_id): | ||
| req = data_provider_pb2.ListPluginsRequest() | ||
| req.experiment_id = experiment_id | ||
| with _translate_grpc_error(): | ||
| res = self._stub.ListPlugins(req) | ||
| return [p.name for p in res.plugins] | ||
|
|
||
| def list_runs(self, ctx, *, experiment_id): | ||
| req = data_provider_pb2.ListRunsRequest() | ||
| req.experiment_id = experiment_id | ||
| with _translate_grpc_error(): | ||
| res = self._stub.ListRuns(req) | ||
| return [ | ||
| provider.Run( | ||
| run_id=run.name, run_name=run.name, start_time=run.start_time, | ||
| ) | ||
| for run in res.runs | ||
| ] | ||
|
|
||
| @timing.log_latency | ||
| def list_scalars( | ||
| self, ctx, *, experiment_id, plugin_name, run_tag_filter=None | ||
| ): | ||
| with timing.log_latency("build request"): | ||
| req = data_provider_pb2.ListScalarsRequest() | ||
| req.experiment_id = experiment_id | ||
| req.plugin_filter.plugin_name = plugin_name | ||
| _populate_rtf(run_tag_filter, req.run_tag_filter) | ||
| with timing.log_latency("_stub.ListScalars"): | ||
| with _translate_grpc_error(): | ||
| res = self._stub.ListScalars(req) | ||
| with timing.log_latency("build result"): | ||
| result = {} | ||
| for run_entry in res.runs: | ||
| tags = {} | ||
| result[run_entry.run_name] = tags | ||
| for tag_entry in run_entry.tags: | ||
| ts = tag_entry.metadata | ||
| tags[tag_entry.tag_name] = provider.ScalarTimeSeries( | ||
| max_step=ts.max_step, | ||
| max_wall_time=ts.max_wall_time, | ||
| plugin_content=ts.summary_metadata.plugin_data.content, | ||
| description=ts.summary_metadata.summary_description, | ||
| display_name=ts.summary_metadata.display_name, | ||
| ) | ||
| return result | ||
|
|
||
| @timing.log_latency | ||
| def read_scalars( | ||
| self, | ||
| ctx, | ||
| *, | ||
| experiment_id, | ||
| plugin_name, | ||
| downsample=None, | ||
| run_tag_filter=None | ||
| ): | ||
| with timing.log_latency("build request"): | ||
| req = data_provider_pb2.ReadScalarsRequest() | ||
| req.experiment_id = experiment_id | ||
| req.plugin_filter.plugin_name = plugin_name | ||
| _populate_rtf(run_tag_filter, req.run_tag_filter) | ||
| req.downsample.num_points = downsample | ||
| with timing.log_latency("_stub.ReadScalars"): | ||
| with _translate_grpc_error(): | ||
| res = self._stub.ReadScalars(req) | ||
| with timing.log_latency("build result"): | ||
| result = {} | ||
| for run_entry in res.runs: | ||
| tags = {} | ||
| result[run_entry.run_name] = tags | ||
| for tag_entry in run_entry.tags: | ||
| series = [] | ||
| tags[tag_entry.tag_name] = series | ||
| d = tag_entry.data | ||
| for (step, wt, value) in zip(d.step, d.wall_time, d.value): | ||
| pt = provider.ScalarDatum( | ||
| step=step, wall_time=wt, value=value, | ||
| ) | ||
| series.append(pt) | ||
| return result | ||
|
|
||
|
|
||
| @contextlib.contextmanager | ||
| def _translate_grpc_error(): | ||
| try: | ||
| yield | ||
| except grpc.RpcError as e: | ||
| if e.code() == grpc.StatusCode.INVALID_ARGUMENT: | ||
| raise errors.InvalidArgumentError(e.details()) | ||
| if e.code() == grpc.StatusCode.NOT_FOUND: | ||
| raise errors.NotFoundError(e.details()) | ||
| if e.code() == grpc.StatusCode.PERMISSION_DENIED: | ||
| raise errors.PermissionDeniedError(e.details()) | ||
| raise | ||
|
|
||
|
|
||
| def _populate_rtf(run_tag_filter, rtf_proto): | ||
| """Copies `run_tag_filter` into `rtf_proto`.""" | ||
| if run_tag_filter is None: | ||
| return | ||
| if run_tag_filter.runs is not None: | ||
| rtf_proto.runs.names[:] = sorted(run_tag_filter.runs) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does traditional TB also sort the runs/tags, or is this sorting logic new?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Traditional TensorBoard doesn’t send an RPC at all here, so there’s no |
||
| if run_tag_filter.tags is not None: | ||
| rtf_proto.tags.names[:] = sorted(run_tag_filter.tags) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are
run_ids a new concept introduced in this grpc design, which is supposed to be unique "across all experiments"?If the plan is to support globally unique run ids in a world with multiple experiments (e.g. upgrade
run_id=run.nameintorun_id=get_run_id(exp_id, run.name)or something), it might be worth adding a comment saying that this is intended to change in the future.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, on the contrary: the gRPC design does not have run IDs, because we
do not yet know what the right semantics are. It is the Python data
provider API that (optimistically) has run IDs, which are a bit of a
wart: they’re returned from
list_runs, but ignored, and not used inany of the other methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha, I also see that
MultiplexerDataProvideralso uses the run_name as the run_id.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. (And same for TensorBoard.dev, fwiw.)