mlflow-optimization 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. mlflow_optimization/__init__.py +50 -0
  2. mlflow_optimization/_bootstrap.py +129 -0
  3. mlflow_optimization/common/diagnostics.py +19 -0
  4. mlflow_optimization/common/patching.py +82 -0
  5. mlflow_optimization/features/annotations/annotate.py +97 -0
  6. mlflow_optimization/features/annotations/types.py +22 -0
  7. mlflow_optimization/features/artifacts/policy.py +16 -0
  8. mlflow_optimization/features/artifacts/ports.py +17 -0
  9. mlflow_optimization/features/autolog/ports.py +49 -0
  10. mlflow_optimization/features/autolog/service.py +147 -0
  11. mlflow_optimization/features/autolog/types.py +31 -0
  12. mlflow_optimization/features/metadata/collect.py +52 -0
  13. mlflow_optimization/features/metadata/types.py +42 -0
  14. mlflow_optimization/features/metrics/normalize.py +121 -0
  15. mlflow_optimization/features/metrics/types.py +54 -0
  16. mlflow_optimization/features/outcomes/classify.py +53 -0
  17. mlflow_optimization/features/outcomes/types.py +40 -0
  18. mlflow_optimization/features/runs/ports.py +30 -0
  19. mlflow_optimization/features/runs/record.py +231 -0
  20. mlflow_optimization/features/runs/types.py +43 -0
  21. mlflow_optimization/integrations/cp_sat/__init__.py +5 -0
  22. mlflow_optimization/integrations/cp_sat/adapter.py +464 -0
  23. mlflow_optimization/integrations/gurobipy/__init__.py +5 -0
  24. mlflow_optimization/integrations/gurobipy/adapter.py +474 -0
  25. mlflow_optimization/integrations/mathopt/__init__.py +1 -0
  26. mlflow_optimization/integrations/mathopt/adapter.py +939 -0
  27. mlflow_optimization/integrations/mlflow/artifacts.py +44 -0
  28. mlflow_optimization/integrations/mlflow/runs.py +480 -0
  29. mlflow_optimization/integrations/pyomo/__init__.py +5 -0
  30. mlflow_optimization/integrations/pyomo/adapter.py +1499 -0
  31. mlflow_optimization/integrations/pyscipopt/__init__.py +5 -0
  32. mlflow_optimization/integrations/pyscipopt/adapter.py +657 -0
  33. mlflow_optimization/integrations/routing/__init__.py +5 -0
  34. mlflow_optimization/integrations/routing/adapter.py +429 -0
  35. mlflow_optimization/py.typed +0 -0
  36. mlflow_optimization-0.1.0.dist-info/METADATA +52 -0
  37. mlflow_optimization-0.1.0.dist-info/RECORD +40 -0
  38. mlflow_optimization-0.1.0.dist-info/WHEEL +4 -0
  39. mlflow_optimization-0.1.0.dist-info/licenses/LICENSE-APACHE +176 -0
  40. mlflow_optimization-0.1.0.dist-info/licenses/LICENSE-MIT +21 -0
@@ -0,0 +1,44 @@
1
+ """MLflow implementation of the artifact storage."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+
7
+ import mlflow
8
+ from mlflow.tracking import MlflowClient
9
+
10
+ from mlflow_optimization.features.artifacts.ports import ArtifactStoreError
11
+
12
+
13
+ class MlflowArtifactStore:
14
+ """Run artifacts stored through the MLflow tracking client."""
15
+
16
+ def log_artifact(
17
+ self: MlflowArtifactStore,
18
+ run_id: str,
19
+ local_path: str,
20
+ artifact_path: str | None,
21
+ ) -> None:
22
+ tracking_uri: str = _artifact_store_sdk_call(mlflow.get_tracking_uri)
23
+ tracking_client: MlflowClient = _artifact_store_sdk_call(
24
+ lambda: MlflowClient(tracking_uri=tracking_uri)
25
+ )
26
+ _artifact_store_sdk_call(
27
+ lambda: tracking_client.log_artifact(
28
+ run_id=run_id,
29
+ local_path=local_path,
30
+ artifact_path=artifact_path,
31
+ )
32
+ )
33
+
34
+
35
+ def _artifact_store_sdk_call[ReturnType](
36
+ mlflow_call: Callable[[], ReturnType],
37
+ ) -> ReturnType:
38
+ """An MLflow SDK call converted to an artifact-store error."""
39
+ try:
40
+ return mlflow_call()
41
+ except Exception as error:
42
+ raise ArtifactStoreError(
43
+ f"Failed while logging an artifact: {type(error).__name__}: {error}"
44
+ ) from error
@@ -0,0 +1,480 @@
1
+ """MLflow implementation of the run storage."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import zlib
7
+ from collections.abc import Callable, Mapping
8
+ from functools import partial
9
+ from typing import Any
10
+
11
+ import mlflow
12
+ from mlflow.entities import Metric, Param, Run, RunTag
13
+ from mlflow.protos.service_pb2 import LogBatch
14
+ from mlflow.tracking import MlflowClient
15
+ from mlflow.tracking.context.registry import resolve_tags
16
+ from mlflow.tracking.fluent import ActiveRun, _get_experiment_id
17
+ from mlflow.utils.mlflow_tags import MLFLOW_PARENT_RUN_ID, MLFLOW_RUN_NAME
18
+ from mlflow.utils.proto_json_utils import message_to_json
19
+ from mlflow.utils.validation import (
20
+ MAX_BATCH_LOG_REQUEST_SIZE,
21
+ MAX_ENTITY_KEY_LENGTH,
22
+ MAX_METRICS_PER_BATCH,
23
+ MAX_PARAM_VAL_LENGTH,
24
+ MAX_PARAMS_TAGS_PER_BATCH,
25
+ MAX_TAG_VAL_LENGTH,
26
+ path_not_unique,
27
+ validate_param_and_metric_name,
28
+ )
29
+
30
+ from mlflow_optimization.common.diagnostics import warn
31
+ from mlflow_optimization.features.runs.ports import RunStoreError
32
+ from mlflow_optimization.features.runs.types import RunReference, SolveRecord
33
+
34
+ _ENCODED_KEY_PREFIX: str = "_mlflow_key_"
35
+ _BATCH_LOG_REQUEST_SIZE_SAFETY_MARGIN: int = MAX_BATCH_LOG_REQUEST_SIZE // 10
36
+ _MAX_BATCH_LOG_REQUEST_SIZE_BUDGET: int = (
37
+ MAX_BATCH_LOG_REQUEST_SIZE - _BATCH_LOG_REQUEST_SIZE_SAFETY_MARGIN
38
+ )
39
+ _SOLVE_IDENTITY_TAG_KEYS: tuple[str, ...] = (
40
+ "optimization.solve_id",
41
+ "optimization.solve_index",
42
+ "optimization.error",
43
+ )
44
+
45
+
46
+ class MlflowRunStore:
47
+ """Solve runs stored through the MLflow tracking client."""
48
+
49
+ def active_run(self: MlflowRunStore) -> RunReference | None:
50
+ active_mlflow_run: ActiveRun | None = _run_store_sdk_call(
51
+ "reading the active run",
52
+ mlflow.active_run,
53
+ )
54
+ if active_mlflow_run is None:
55
+ return None
56
+ active_run_id: str = _run_store_sdk_call(
57
+ "reading the active run",
58
+ lambda: active_mlflow_run.info.run_id,
59
+ )
60
+ active_experiment_id: str = _run_store_sdk_call(
61
+ "reading the active run",
62
+ lambda: active_mlflow_run.info.experiment_id,
63
+ )
64
+ return RunReference(
65
+ run_id=active_run_id,
66
+ experiment_id=active_experiment_id,
67
+ )
68
+
69
+ def save(
70
+ self: MlflowRunStore, record: SolveRecord, parent: RunReference | None
71
+ ) -> RunReference:
72
+ tracking_uri: str = _run_store_sdk_call(
73
+ "saving the run", mlflow.get_tracking_uri
74
+ )
75
+ tracking_client: MlflowClient = _run_store_sdk_call(
76
+ "saving the run",
77
+ lambda: MlflowClient(tracking_uri=tracking_uri),
78
+ )
79
+ experiment_id: str
80
+ if parent is None:
81
+ resolved_experiment_id: str | None = _run_store_sdk_call(
82
+ "saving the run",
83
+ _get_experiment_id,
84
+ )
85
+ if resolved_experiment_id is None:
86
+ raise RunStoreError(
87
+ "Failed while saving the run: MLflow did not resolve an experiment ID."
88
+ )
89
+ experiment_id = resolved_experiment_id
90
+ else:
91
+ experiment_id = parent.experiment_id
92
+
93
+ run_name: str = _truncate_tag_value(record.name)
94
+ resolved_tag_values: dict[str, Any] = dict(record.tags)
95
+ if parent is not None:
96
+ resolved_tag_values[MLFLOW_PARENT_RUN_ID] = parent.run_id
97
+ if run_name:
98
+ resolved_tag_values[MLFLOW_RUN_NAME] = run_name
99
+ context_tag_values: dict[str, Any] = _run_store_sdk_call(
100
+ "saving the run",
101
+ lambda: resolve_tags(resolved_tag_values),
102
+ )
103
+
104
+ creation_tag_values: dict[str, str] = {}
105
+ for identity_tag_key in _SOLVE_IDENTITY_TAG_KEYS:
106
+ if identity_tag_key in context_tag_values:
107
+ creation_tag_values[identity_tag_key] = _truncate_tag_value(
108
+ str(context_tag_values[identity_tag_key])
109
+ )
110
+ if parent is not None:
111
+ creation_tag_values[MLFLOW_PARENT_RUN_ID] = parent.run_id
112
+
113
+ created_run: Run = _run_store_sdk_call(
114
+ "saving the run",
115
+ lambda: tracking_client.create_run(
116
+ experiment_id=experiment_id,
117
+ start_time=record.start_time_ms,
118
+ tags=creation_tag_values,
119
+ run_name=run_name,
120
+ ),
121
+ )
122
+ created_run_id: str = _run_store_sdk_call(
123
+ "saving the run",
124
+ lambda: created_run.info.run_id,
125
+ )
126
+
127
+ try:
128
+ metric_entities: list[Metric] = []
129
+ for metric_key, metric_value in record.metrics.items():
130
+ encoded_metric_key: str | None = _encode_key(
131
+ metric_key, "saving the run"
132
+ )
133
+ if encoded_metric_key is None:
134
+ continue
135
+ metric_entities.append(
136
+ _run_store_sdk_call(
137
+ "saving the run",
138
+ partial(
139
+ Metric,
140
+ key=encoded_metric_key,
141
+ value=metric_value,
142
+ timestamp=record.end_time_ms,
143
+ step=0,
144
+ ),
145
+ )
146
+ )
147
+
148
+ parameter_entities: list[Param] = []
149
+ for parameter_key, parameter_value in record.params.items():
150
+ encoded_parameter_key: str | None = _encode_key(
151
+ parameter_key,
152
+ "saving the run",
153
+ )
154
+ if encoded_parameter_key is None:
155
+ continue
156
+ parameter_entities.append(
157
+ _run_store_sdk_call(
158
+ "saving the run",
159
+ partial(
160
+ Param,
161
+ key=encoded_parameter_key,
162
+ value=parameter_value[:MAX_PARAM_VAL_LENGTH],
163
+ ),
164
+ )
165
+ )
166
+
167
+ self._log_metric_batches(tracking_client, created_run_id, metric_entities)
168
+ self._log_parameter_batches(
169
+ tracking_client,
170
+ created_run_id,
171
+ parameter_entities,
172
+ )
173
+ self._log_tag_batches(
174
+ tracking_client,
175
+ created_run_id,
176
+ _make_tag_entities(context_tag_values, "saving the run"),
177
+ "saving the run",
178
+ )
179
+ finally:
180
+ _run_store_sdk_call(
181
+ "saving the run",
182
+ lambda: tracking_client.set_terminated(
183
+ run_id=created_run_id,
184
+ status=record.status.value,
185
+ end_time=record.end_time_ms,
186
+ ),
187
+ )
188
+ created_experiment_id: str = _run_store_sdk_call(
189
+ "saving the run",
190
+ lambda: created_run.info.experiment_id,
191
+ )
192
+ return RunReference(
193
+ run_id=created_run_id,
194
+ experiment_id=created_experiment_id,
195
+ )
196
+
197
+ def log_metric(
198
+ self: MlflowRunStore, run: RunReference, key: str, value: float
199
+ ) -> None:
200
+ tracking_uri: str = _run_store_sdk_call(
201
+ "logging a metric", mlflow.get_tracking_uri
202
+ )
203
+ tracking_client: MlflowClient = _run_store_sdk_call(
204
+ "logging a metric",
205
+ lambda: MlflowClient(tracking_uri=tracking_uri),
206
+ )
207
+ encoded_key: str | None = _encode_key(key, "logging a metric")
208
+ if encoded_key is None:
209
+ return
210
+ _run_store_sdk_call(
211
+ "logging a metric",
212
+ lambda: tracking_client.log_metric(
213
+ run_id=run.run_id,
214
+ key=encoded_key,
215
+ value=value,
216
+ synchronous=True,
217
+ ),
218
+ )
219
+
220
+ def set_tags(
221
+ self: MlflowRunStore, run: RunReference, tags: Mapping[str, str]
222
+ ) -> None:
223
+ tracking_uri: str = _run_store_sdk_call("setting tags", mlflow.get_tracking_uri)
224
+ tracking_client: MlflowClient = _run_store_sdk_call(
225
+ "setting tags",
226
+ lambda: MlflowClient(tracking_uri=tracking_uri),
227
+ )
228
+ self._log_tag_batches(
229
+ tracking_client,
230
+ run.run_id,
231
+ _make_tag_entities(tags, "setting tags"),
232
+ "setting tags",
233
+ )
234
+
235
+ def _log_metric_batches(
236
+ self: MlflowRunStore,
237
+ tracking_client: MlflowClient,
238
+ run_id: str,
239
+ metric_entities: list[Metric],
240
+ ) -> None:
241
+ current_batch: list[Metric] = []
242
+ for metric_entity in metric_entities:
243
+ candidate_batch: list[Metric] = [*current_batch, metric_entity]
244
+ if current_batch and (
245
+ len(current_batch) >= MAX_METRICS_PER_BATCH
246
+ or _run_store_sdk_call(
247
+ "saving the run",
248
+ partial(
249
+ _serialized_batch_request_size,
250
+ run_id,
251
+ candidate_batch,
252
+ [],
253
+ [],
254
+ ),
255
+ )
256
+ > _MAX_BATCH_LOG_REQUEST_SIZE_BUDGET
257
+ ):
258
+ _run_store_sdk_call(
259
+ "saving the run",
260
+ partial(
261
+ tracking_client.log_batch,
262
+ run_id=run_id,
263
+ metrics=current_batch,
264
+ synchronous=True,
265
+ ),
266
+ )
267
+ current_batch = []
268
+ current_batch.append(metric_entity)
269
+ if current_batch:
270
+ _run_store_sdk_call(
271
+ "saving the run",
272
+ partial(
273
+ tracking_client.log_batch,
274
+ run_id=run_id,
275
+ metrics=current_batch,
276
+ synchronous=True,
277
+ ),
278
+ )
279
+
280
+ def _log_parameter_batches(
281
+ self: MlflowRunStore,
282
+ tracking_client: MlflowClient,
283
+ run_id: str,
284
+ parameter_entities: list[Param],
285
+ ) -> None:
286
+ current_batch: list[Param] = []
287
+ for parameter_entity in parameter_entities:
288
+ candidate_batch: list[Param] = [*current_batch, parameter_entity]
289
+ if current_batch and (
290
+ len(current_batch) >= MAX_PARAMS_TAGS_PER_BATCH
291
+ or _run_store_sdk_call(
292
+ "saving the run",
293
+ partial(
294
+ _serialized_batch_request_size,
295
+ run_id,
296
+ [],
297
+ candidate_batch,
298
+ [],
299
+ ),
300
+ )
301
+ > _MAX_BATCH_LOG_REQUEST_SIZE_BUDGET
302
+ ):
303
+ _run_store_sdk_call(
304
+ "saving the run",
305
+ partial(
306
+ tracking_client.log_batch,
307
+ run_id=run_id,
308
+ params=current_batch,
309
+ synchronous=True,
310
+ ),
311
+ )
312
+ current_batch = []
313
+ current_batch.append(parameter_entity)
314
+ if current_batch:
315
+ _run_store_sdk_call(
316
+ "saving the run",
317
+ partial(
318
+ tracking_client.log_batch,
319
+ run_id=run_id,
320
+ params=current_batch,
321
+ synchronous=True,
322
+ ),
323
+ )
324
+
325
+ def _log_tag_batches(
326
+ self: MlflowRunStore,
327
+ tracking_client: MlflowClient,
328
+ run_id: str,
329
+ tag_entities: list[RunTag],
330
+ operation: str,
331
+ ) -> None:
332
+ current_batch: list[RunTag] = []
333
+ for tag_entity in tag_entities:
334
+ candidate_batch: list[RunTag] = [*current_batch, tag_entity]
335
+ if current_batch and (
336
+ len(current_batch) >= MAX_PARAMS_TAGS_PER_BATCH
337
+ or _run_store_sdk_call(
338
+ operation,
339
+ partial(
340
+ _serialized_batch_request_size,
341
+ run_id,
342
+ [],
343
+ [],
344
+ candidate_batch,
345
+ ),
346
+ )
347
+ > _MAX_BATCH_LOG_REQUEST_SIZE_BUDGET
348
+ ):
349
+ _run_store_sdk_call(
350
+ operation,
351
+ partial(
352
+ tracking_client.log_batch,
353
+ run_id=run_id,
354
+ tags=current_batch,
355
+ synchronous=True,
356
+ ),
357
+ )
358
+ current_batch = []
359
+ current_batch.append(tag_entity)
360
+ if current_batch:
361
+ _run_store_sdk_call(
362
+ operation,
363
+ partial(
364
+ tracking_client.log_batch,
365
+ run_id=run_id,
366
+ tags=current_batch,
367
+ synchronous=True,
368
+ ),
369
+ )
370
+
371
+
372
+ def _encode_key(key: str, operation: str = "saving the run") -> str | None:
373
+ valid_key: bool = bool(
374
+ _run_store_sdk_call(
375
+ operation,
376
+ lambda: validate_param_and_metric_name(key),
377
+ )
378
+ )
379
+ unique_path: bool = not _run_store_sdk_call(
380
+ operation,
381
+ lambda: path_not_unique(key),
382
+ )
383
+ if (
384
+ valid_key
385
+ and unique_path
386
+ and len(key) <= MAX_ENTITY_KEY_LENGTH
387
+ and not key.startswith(_ENCODED_KEY_PREFIX)
388
+ ):
389
+ return key
390
+
391
+ encoded_characters: list[str] = []
392
+ for character in key:
393
+ if character == "_":
394
+ encoded_characters.append("__")
395
+ elif character == "/":
396
+ encoded_characters.append("_x2F_")
397
+ elif _run_store_sdk_call(
398
+ operation,
399
+ partial(validate_param_and_metric_name, character),
400
+ ):
401
+ encoded_characters.append(character)
402
+ else:
403
+ encoded_characters.append(f"_x{ord(character):X}_")
404
+
405
+ escaped_key: str = _ENCODED_KEY_PREFIX + "e_" + "".join(encoded_characters)
406
+ if len(escaped_key) <= MAX_ENTITY_KEY_LENGTH:
407
+ return escaped_key
408
+
409
+ key_bytes: bytes = key.encode("utf-8", errors="surrogatepass")
410
+ compressed_key_bytes: bytes = zlib.compress(key_bytes, level=9)
411
+ base64_key_bytes: bytes = base64.urlsafe_b64encode(compressed_key_bytes)
412
+ compressed_key: str = (
413
+ _ENCODED_KEY_PREFIX + "z_" + base64_key_bytes.decode("ascii").rstrip("=")
414
+ )
415
+ if len(compressed_key) > MAX_ENTITY_KEY_LENGTH:
416
+ warn(f"Skipping MLflow key {key!r} because it exceeds the tracker key limit.")
417
+ return None
418
+ return compressed_key
419
+
420
+
421
+ def _truncate_tag_value(value: str) -> str:
422
+ return value[:MAX_TAG_VAL_LENGTH]
423
+
424
+
425
+ def _make_tag_entities(tag_values: Mapping[str, Any], operation: str) -> list[RunTag]:
426
+ tag_entities: list[RunTag] = []
427
+ for tag_key, tag_value in tag_values.items():
428
+ encoded_tag_key: str | None = _encode_key(tag_key, operation)
429
+ if encoded_tag_key is None:
430
+ continue
431
+ truncated_tag_value: str = _truncate_tag_value(str(tag_value))
432
+ tag_entities.append(
433
+ _run_store_sdk_call(
434
+ operation,
435
+ partial(
436
+ RunTag,
437
+ key=encoded_tag_key,
438
+ value=truncated_tag_value,
439
+ ),
440
+ )
441
+ )
442
+ return tag_entities
443
+
444
+
445
+ def _serialized_batch_request_size(
446
+ run_id: str,
447
+ metric_entities: list[Metric],
448
+ parameter_entities: list[Param],
449
+ tag_entities: list[RunTag],
450
+ ) -> int:
451
+ metric_protocol_buffers: list[Any] = []
452
+ for metric_entity in metric_entities:
453
+ metric_protocol_buffers.append(metric_entity.to_proto())
454
+ parameter_protocol_buffers: list[Any] = []
455
+ for parameter_entity in parameter_entities:
456
+ parameter_protocol_buffers.append(parameter_entity.to_proto())
457
+ tag_protocol_buffers: list[Any] = []
458
+ for tag_entity in tag_entities:
459
+ tag_protocol_buffers.append(tag_entity.to_proto())
460
+ batch_message: LogBatch = LogBatch(
461
+ run_id=run_id,
462
+ metrics=metric_protocol_buffers,
463
+ params=parameter_protocol_buffers,
464
+ tags=tag_protocol_buffers,
465
+ )
466
+ serialized_request: str = message_to_json(batch_message)
467
+ return len(serialized_request.encode("utf-8"))
468
+
469
+
470
+ def _run_store_sdk_call[ReturnType](
471
+ operation: str,
472
+ mlflow_call: Callable[[], ReturnType],
473
+ ) -> ReturnType:
474
+ """An MLflow SDK call converted to a run-store error."""
475
+ try:
476
+ return mlflow_call()
477
+ except Exception as error:
478
+ raise RunStoreError(
479
+ f"Failed while {operation}: {type(error).__name__}: {error}"
480
+ ) from error
@@ -0,0 +1,5 @@
1
+ """Pyomo solver integration."""
2
+
3
+ from .adapter import PyomoAdapter
4
+
5
+ __all__ = ["PyomoAdapter"]