-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathmodel_manager.py
More file actions
294 lines (267 loc) · 12.2 KB
/
Copy pathmodel_manager.py
File metadata and controls
294 lines (267 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
import asyncio
from datetime import datetime
from pathlib import Path
import tempfile
import os
import yaml
import re
import mlflow
from sfapi_client import AsyncClient
from sfapi_client.compute import Machine
from trame.widgets import vuetify3 as vuetify
from utils import timer, load_config_dict, create_date_filter
from error_manager import add_error
from sfapi_manager import monitor_sfapi_job
from state_manager import state
model_type_tag_dict = {
"Gaussian Process": "GP",
"Neural Network (single)": "NN",
"Neural Network (ensemble)": "ensemble_NN",
}
def enable_amsc_x_api_key(config_dict):
"""
MLflow authentication helper for the AmSC MLflow server.
Standard MLflow does not automatically inject custom headers like 'X-Api-Key'.
This patches the http_request function to ensure every request to the server
includes the AmSC API key.
See https://gitlab.com/amsc2/ai-services/model-services/intro-to-mlflow-pytorch for more details.
"""
import mlflow.utils.rest_utils as rest_utils
mlflow_cfg = config_dict.get("mlflow") or {}
api_key_env = mlflow_cfg.get("api_key_env")
if not api_key_env:
title = "Unable to enable AmSC X-Api-Key authentication"
msg = "MLFlow configuration is missing 'mlflow.api_key_env'"
add_error(title, msg)
print(msg)
return
api_key = os.environ.get(api_key_env)
if not api_key:
title = "Unable to enable AmSC X-Api-Key authentication"
msg = f"Environment variable '{api_key_env}' in 'mlflow.api_key_env' is not set"
add_error(title, msg)
print(msg)
return
_orig = rest_utils.http_request
def patched(host_creds, endpoint, method, *args, **kwargs):
if "headers" in kwargs and kwargs["headers"] is not None:
h = dict(kwargs["headers"])
h["X-Api-Key"] = api_key
kwargs["headers"] = h
else:
h = dict(kwargs.get("extra_headers") or {})
h["X-Api-Key"] = api_key
kwargs["extra_headers"] = h
return _orig(host_creds, endpoint, method, *args, **kwargs)
rest_utils.http_request = patched
class ModelManager:
def __init__(self, config_dict, model_type_tag):
print("Initializing model manager...")
self.__model = None
self.__is_neural_network = False
self.__is_gaussian_process = False
self.__is_neural_network_ensemble = False
self.__model_type_tag = model_type_tag
if "mlflow" not in config_dict or not config_dict["mlflow"].get("tracking_uri"):
print(
f"No mlflow.tracking_uri in configuration file for {config_dict['experiment']}; cannot load model from MLflow."
)
return
mlflow.set_tracking_uri(config_dict["mlflow"]["tracking_uri"])
# When using the AmSC MLflow: inject the X-Api-Key into the requests to authenticate with the MLflow server
# (See https://gitlab.com/amsc2/ai-services/model-services/intro-to-mlflow-pytorch)
if (
config_dict["mlflow"]["tracking_uri"]
== "https://mlflow.american-science-cloud.org"
):
enable_amsc_x_api_key(config_dict)
experiment = config_dict["experiment"]
model_name = f"{experiment}_{model_type_tag}"
try:
# Download model from MLflow server
self.__model = (
mlflow.pyfunc.load_model(f"models:/{model_name}/latest")
.unwrap_python_model()
.model
)
if model_type_tag == "NN":
self.__is_neural_network = True
elif model_type_tag == "ensemble_NN":
self.__is_neural_network_ensemble = True
elif model_type_tag == "GP":
self.__is_gaussian_process = True
else:
raise ValueError(f"Unsupported model type: {model_type_tag}")
except Exception as e:
title = f"Unable to load model {model_type_tag}"
msg = f"Error occurred when loading model from MLflow: {e}"
add_error(title, msg)
print(msg)
def avail(self):
print("Checking model availability...")
model_avail = True if self.__model is not None else False
return model_avail
@property
def is_neural_network(self):
return self.__is_neural_network
@property
def is_gaussian_process(self):
return self.__is_gaussian_process
@property
def is_neural_network_ensemble(self):
return self.__is_neural_network_ensemble
@timer
def evaluate(self, parameters, output):
print("Evaluating model...")
if self.__model is not None:
# evaluate model
output_dict = self.__model.evaluate(parameters)
if self.__is_neural_network:
# compute mean and mean error
mean = output_dict[output]
mean_error = 0.0 # trick to collapse error range when lower/upper bounds are not predicted
elif self.__is_gaussian_process or self.__is_neural_network_ensemble:
# compute mean, standard deviation and mean error
# (call detach method to detach gradients from tensors)
mean = output_dict[output].mean.detach()
std_dev = output_dict[output].variance.sqrt().detach()
mean_error = 2.0 * std_dev
else:
raise ValueError(f"Unsupported model type: {self.__model_type_tag}")
# compute lower/upper bounds for error range
lower = mean - mean_error
upper = mean + mean_error
# convert to Python float if tensor has only one element
# because Trame state variables must be serializable
if mean.numel() == 1:
mean = float(mean)
return (mean, lower, upper)
def get_output_transformers(self):
print("Getting output transformers...")
if self.__model is not None:
return self.__model.output_transformers
async def training_kernel(self):
try:
# create an authenticated client
async with AsyncClient(
client_id=state.sfapi_client_id, secret=state.sfapi_key
) as client:
perlmutter = await client.compute(Machine.perlmutter)
# upload the configuration file to NERSC
config_dict = load_config_dict(state.experiment)
config_dict["simulation_calibration"] = state.simulation_calibration
# add date range filter to the configuration dictionary
date_filter = create_date_filter(state.experiment_date_range)
config_dict["date_filter"] = date_filter
# define the target path on NERSC
target_path = "/global/cfs/cdirs/m558/superfacility/model_training"
[target_path] = await perlmutter.ls(target_path, directory=True)
with tempfile.TemporaryDirectory() as temp_dir:
temp_file_path = Path(temp_dir) / "config.yaml"
with open(temp_file_path, "w") as temp_file:
yaml.dump(config_dict, temp_file)
temp_file.flush()
with open(temp_file_path, "rb") as temp_file:
print("Uploading config file to NERSC")
temp_file.filename = "config.yaml"
await target_path.upload(temp_file)
# set the path of the script used to submit the training job on NERSC
training_script = None
# multiple locations supported, to make development easier
# container (production): script is in cwd
# development, starting the gui app from dashboard/: script is in ../ml/
# development, starting the gui app from the repo root dir: script is in ml/
script_locations = [Path.cwd(), Path.cwd() / "../ml", Path.cwd() / "ml"]
for script_dir in script_locations:
script_path = script_dir / "training_pm.sbatch"
if os.path.exists(script_path):
with open(script_path, "r") as file:
training_script = file.read()
break
if training_script is None:
raise RuntimeError("Could not find training_pm.sbatch")
# replace the --model argument in the python command with the current model type from the state
training_script = re.sub(
pattern=r"--model \$\{model\}",
repl=rf"--model {model_type_tag_dict[state.model_type]}",
string=training_script,
)
# submit the training job through the Superfacility API
sfapi_job = await perlmutter.submit_job(training_script)
state.model_training_status = "Submitted"
state.flush()
# print some logs
print(f"Training job submitted (job ID: {sfapi_job.jobid})")
return await monitor_sfapi_job(sfapi_job, "model_training_status")
except Exception as e:
title = "Unable to complete training kernel"
msg = f"Error occurred when executing training kernel: {e}"
add_error(title, msg)
print(msg)
async def training_async(self):
try:
print("Training model...")
state.model_training = True
state.model_training_status = "Submitting"
state.flush()
if await self.training_kernel():
state.model_training_time = datetime.now().strftime("%Y-%m-%d %H:%M")
state.flush()
print(f"Finished training model at {state.model_training_time}")
else:
print("Unable to complete training job.")
# flush state and enable button
state.model_training = False
state.flush()
except Exception as e:
title = "Unable to train model"
msg = f"Error occurred when training model: {e}"
add_error(title, msg)
print(msg)
def training_trigger(self):
try:
# schedule asynchronous job
asyncio.create_task(self.training_async())
except Exception as e:
title = "Unable to train model"
msg = f"Error occurred when training model: {e}"
add_error(title, msg)
print(msg)
def panel(self):
print("Setting model card...")
# list of available model types
model_type_list = [
"Gaussian Process",
"Neural Network (single)",
"Neural Network (ensemble)",
]
with vuetify.VExpansionPanels(v_model=("expand_panel_control_model", 0)):
with vuetify.VExpansionPanel(
title="Control: Models",
style="font-size: 20px; font-weight: 500;",
):
with vuetify.VExpansionPanelText():
with vuetify.VRow():
with vuetify.VCol():
vuetify.VSelect(
v_model=("model_type",),
label="Model type",
items=(model_type_list,),
dense=True,
)
with vuetify.VCol():
vuetify.VTextField(
v_model_number=("model_training_status",),
label="Training status",
readonly=True,
)
with vuetify.VRow():
with vuetify.VCol():
vuetify.VBtn(
"Train",
click=self.training_trigger,
disabled=(
"model_training || perlmutter_status != 'active'",
),
style="text-transform: none",
)