Skip to content

Commit d3277a4

Browse files
authored
HParams: Create hparams data source to fetch data from the hparams plugin (#6535)
## Motivation for features / changes Today we fetch data from the hparams plugin using the runs data source. The data is then written to both the runs and hparams state. Unfortunately there are a few issues with both this event structure and our current data model. The data model implies a 1:1 mapping between experiment ids and hparam/metric specs. This is not the case when in an experiment view and thus the model will need to be changed. The event structure is inconsistent with our typical redux structure and thus is hard to refactor to work the way we need it to. This is the first PR in a series I am creating to address this issue. Future PRs will: * add a new effects file, action, and hparams state entries for both a `runToHparamsAndMetrics` mapping along with a single `currentSpecs` (name pending). * remove much of the logic from runs data source and update the `getRuns` selector to populate data ## Screenshots of UI changes (or N/A) N/A
1 parent 8616842 commit d3277a4

File tree

12 files changed

+711
-5
lines changed

12 files changed

+711
-5
lines changed

tensorboard/webapp/hparams/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ tf_ts_library(
2222
],
2323
deps = [
2424
"//tensorboard/webapp/runs/data_source",
25+
"//tensorboard/webapp/runs/data_source:backend_types",
2526
],
2627
)
2728

tensorboard/webapp/hparams/_redux/BUILD

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,20 @@ tf_ts_library(
8181
],
8282
)
8383

84+
tf_ng_module(
85+
name = "hparams_data_source",
86+
srcs = [
87+
"hparams_data_source.ts",
88+
],
89+
deps = [
90+
"//tensorboard/webapp/hparams:types",
91+
"//tensorboard/webapp/webapp_data_source:http_client",
92+
"@npm//@angular/core",
93+
"@npm//@ngrx/store",
94+
"@npm//rxjs",
95+
],
96+
)
97+
8498
tf_ts_library(
8599
name = "testing",
86100
testonly = True,
@@ -98,21 +112,25 @@ tf_ts_library(
98112
name = "_redux_test_lib",
99113
testonly = True,
100114
srcs = [
115+
"hparams_data_source_test.ts",
101116
"hparams_reducers_test.ts",
102117
"hparams_selectors_test.ts",
103118
"hparams_selectors_utils_test.ts",
104119
"utils_test.ts",
105120
],
106121
deps = [
107122
":hparams_actions",
123+
":hparams_data_source",
108124
":hparams_reducers",
109125
":hparams_selectors",
110126
":testing",
111127
":utils",
128+
"//tensorboard/webapp/angular:expect_angular_core_testing",
112129
"//tensorboard/webapp/hparams:types",
113130
"//tensorboard/webapp/runs/actions",
114131
"//tensorboard/webapp/runs/data_source:testing",
115132
"//tensorboard/webapp/runs/store:testing",
133+
"//tensorboard/webapp/webapp_data_source:http_client_testing",
116134
"@npm//@types/jasmine",
117135
],
118136
)
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/* Copyright 2023 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
import {Injectable} from '@angular/core';
16+
import {Observable} from 'rxjs';
17+
import {map} from 'rxjs/operators';
18+
19+
import {
20+
Domain,
21+
DomainType,
22+
BackendListSessionGroupRequest,
23+
BackendHparamsExperimentResponse,
24+
BackendHparamSpec,
25+
DiscreteDomainHparamSpec,
26+
SessionGroup,
27+
HparamAndMetricSpec,
28+
IntervalDomainHparamSpec,
29+
BackendListSessionGroupResponse,
30+
RunStatus,
31+
} from '../types';
32+
import {TBHttpClient} from '../../webapp_data_source/tb_http_client';
33+
34+
const HPARAMS_HTTP_PATH_PREFIX = 'data/plugin/hparams';
35+
36+
function isHparamDiscrete(
37+
hparam: BackendHparamSpec
38+
): hparam is DiscreteDomainHparamSpec {
39+
return Boolean((hparam as DiscreteDomainHparamSpec).domainDiscrete);
40+
}
41+
42+
function isHparamInterval(
43+
hparam: BackendHparamSpec
44+
): hparam is IntervalDomainHparamSpec {
45+
return Boolean((hparam as IntervalDomainHparamSpec).domainInterval);
46+
}
47+
48+
function getHparamDomain(hparam: BackendHparamSpec): Domain {
49+
if (isHparamDiscrete(hparam)) {
50+
return {
51+
type: DomainType.DISCRETE,
52+
values: hparam.domainDiscrete,
53+
};
54+
}
55+
56+
if (isHparamInterval(hparam)) {
57+
return {
58+
...hparam.domainInterval,
59+
type: DomainType.INTERVAL,
60+
};
61+
}
62+
63+
return {
64+
values: [],
65+
type: DomainType.DISCRETE,
66+
};
67+
}
68+
69+
@Injectable()
70+
export class HparamsDataSource {
71+
constructor(private readonly http: TBHttpClient) {}
72+
73+
private getPrefix(experimentIds: string[]) {
74+
return experimentIds.length > 1 ? 'compare' : 'experiment';
75+
}
76+
77+
private formatExperimentIds(experimentIds: string[]) {
78+
if (experimentIds.length === 1) {
79+
return experimentIds[0];
80+
}
81+
82+
// The server does not send back experiment ids. Instead the response is formatted as
83+
// `[AliasNumber] ExperimentAlias/RunName`
84+
// By using the index as the alias we can translate associate the response with an experiment id
85+
// Note: The experiment id itself cannot be the alias because it may contain ':'
86+
return experimentIds.map((eid, index) => `${index}:${eid}`).join(',');
87+
}
88+
89+
fetchExperimentInfo(
90+
experimentIds: string[]
91+
): Observable<HparamAndMetricSpec> {
92+
const formattedExperimentIds = this.formatExperimentIds(experimentIds);
93+
return this.http
94+
.post<BackendHparamsExperimentResponse>(
95+
`/${this.getPrefix(
96+
experimentIds
97+
)}/${formattedExperimentIds}/${HPARAMS_HTTP_PATH_PREFIX}/experiment`,
98+
{experimentName: formattedExperimentIds},
99+
{},
100+
'request'
101+
)
102+
.pipe(
103+
map((response) => {
104+
return {
105+
hparams: response.hparamInfos.map((hparam) => {
106+
const feHparam = {
107+
...hparam,
108+
domain: getHparamDomain(hparam),
109+
};
110+
111+
delete (feHparam as any).domainInterval;
112+
delete (feHparam as any).domainDiscrete;
113+
114+
return feHparam;
115+
}),
116+
metrics: response.metricInfos.map((metric) => ({
117+
...metric,
118+
tag: metric.name.tag,
119+
})),
120+
};
121+
})
122+
);
123+
}
124+
125+
fetchSessionGroups(
126+
experimentIds: string[],
127+
hparamsAndMetricsSpecs: HparamAndMetricSpec
128+
): Observable<SessionGroup[]> {
129+
const formattedExperimentIds = this.formatExperimentIds(experimentIds);
130+
131+
const colParams: BackendListSessionGroupRequest['colParams'] = [];
132+
133+
for (const hparam of hparamsAndMetricsSpecs.hparams) {
134+
colParams.push({hparam: hparam.name});
135+
}
136+
for (const mectric of hparamsAndMetricsSpecs.metrics) {
137+
colParams.push({
138+
metric: mectric.name,
139+
});
140+
}
141+
142+
const listSessionRequestParams: BackendListSessionGroupRequest = {
143+
experimentName: formattedExperimentIds,
144+
allowedStatuses: [
145+
RunStatus.STATUS_FAILURE,
146+
RunStatus.STATUS_RUNNING,
147+
RunStatus.STATUS_SUCCESS,
148+
RunStatus.STATUS_UNKNOWN,
149+
],
150+
colParams,
151+
startIndex: 0,
152+
// arbitrary large number so it does not get clipped.
153+
sliceSize: 1e6,
154+
};
155+
156+
return this.http
157+
.post<BackendListSessionGroupResponse>(
158+
`/${this.getPrefix(
159+
experimentIds
160+
)}/${formattedExperimentIds}/${HPARAMS_HTTP_PATH_PREFIX}/session_groups`,
161+
listSessionRequestParams,
162+
{},
163+
'request'
164+
)
165+
.pipe(
166+
map((response) =>
167+
response.sessionGroups.map((sessionGroup) => {
168+
sessionGroup.sessions = sessionGroup.sessions.map((session) => {
169+
/*
170+
* In single experiment mode the Session.name is equal to the runName.
171+
* In comparison view it is `[AliasNumber] ExperimentAlias/runName`
172+
*
173+
* We store runs as experimentId/runName so it is necessary to prepend the experiment name
174+
* in single experiment view. "In comparison view we pass the indeces of the experimentIds
175+
* as the aliases in the request. That allows us to parse the indeces from the response and
176+
* use them to lookup the correct ids from the experimentIds argument.
177+
*/
178+
if (experimentIds.length > 1) {
179+
const [, ...aliasAndRunName] = session.name.split(' ');
180+
const [experimentIndex, ...runName] = aliasAndRunName
181+
.join(' ')
182+
.split('/');
183+
session.name = [
184+
// This parseInt should not be necessary because JS Arrays DO support indexing by string
185+
experimentIds[parseInt(experimentIndex)],
186+
...runName,
187+
].join('/');
188+
} else {
189+
session.name = [experimentIds[0], session.name].join('/');
190+
}
191+
return session;
192+
});
193+
return sessionGroup;
194+
})
195+
)
196+
);
197+
}
198+
}

0 commit comments

Comments
 (0)