google-cloud-mldiagnostics 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.
- google_cloud_mldiagnostics/__init__.py +13 -0
- google_cloud_mldiagnostics/api/__init__.py +7 -0
- google_cloud_mldiagnostics/api/metrics.py +38 -0
- google_cloud_mldiagnostics/api/mlrun.py +71 -0
- google_cloud_mldiagnostics/api/test_metrics.py +32 -0
- google_cloud_mldiagnostics/api/test_mlrun.py +97 -0
- google_cloud_mldiagnostics/clients/__init__.py +0 -0
- google_cloud_mldiagnostics/clients/control_plane_client.py +127 -0
- google_cloud_mldiagnostics/clients/logging_client.py +142 -0
- google_cloud_mldiagnostics/clients/test_control_plane_client.py +255 -0
- google_cloud_mldiagnostics/clients/test_logging_client.py +220 -0
- google_cloud_mldiagnostics/core/__init__.py +3 -0
- google_cloud_mldiagnostics/core/create_mlrun.py +131 -0
- google_cloud_mldiagnostics/core/global_manager.py +246 -0
- google_cloud_mldiagnostics/core/metrics.py +231 -0
- google_cloud_mldiagnostics/core/test_create_mlrun.py +235 -0
- google_cloud_mldiagnostics/core/test_global_manager.py +653 -0
- google_cloud_mldiagnostics/core/test_metrics.py +386 -0
- google_cloud_mldiagnostics/core/test_xprof.py +151 -0
- google_cloud_mldiagnostics/core/xprof.py +150 -0
- google_cloud_mldiagnostics/custom_types/__init__.py +0 -0
- google_cloud_mldiagnostics/custom_types/exceptions.py +29 -0
- google_cloud_mldiagnostics/custom_types/metric_types.py +32 -0
- google_cloud_mldiagnostics/custom_types/mlrun_types.py +79 -0
- google_cloud_mldiagnostics/utils/__init__.py +0 -0
- google_cloud_mldiagnostics/utils/config_utils.py +79 -0
- google_cloud_mldiagnostics/utils/gcp.py +72 -0
- google_cloud_mldiagnostics/utils/host_utils.py +161 -0
- google_cloud_mldiagnostics/utils/jax_utils/jax_config.py +43 -0
- google_cloud_mldiagnostics/utils/jax_utils/test_jax_config.py +72 -0
- google_cloud_mldiagnostics/utils/libtpu_utils/libtpu_metric.py +81 -0
- google_cloud_mldiagnostics/utils/libtpu_utils/libtpu_metric_test.py +131 -0
- google_cloud_mldiagnostics/utils/metric_utils.py +41 -0
- google_cloud_mldiagnostics/utils/metric_utils_test.py +71 -0
- google_cloud_mldiagnostics/utils/test_config_utils.py +170 -0
- google_cloud_mldiagnostics/utils/test_gcp.py +224 -0
- google_cloud_mldiagnostics/utils/test_host_utils.py +174 -0
- google_cloud_mldiagnostics-0.1.0.dist-info/METADATA +18 -0
- google_cloud_mldiagnostics-0.1.0.dist-info/RECORD +42 -0
- google_cloud_mldiagnostics-0.1.0.dist-info/WHEEL +5 -0
- google_cloud_mldiagnostics-0.1.0.dist-info/licenses/LICENSE +201 -0
- google_cloud_mldiagnostics-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Init module for ml diagnostics."""
|
|
2
|
+
|
|
3
|
+
# Import the upfront API for users
|
|
4
|
+
from . import api
|
|
5
|
+
from . import core
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
machinelearning_run = api.machinelearning_run
|
|
12
|
+
metrics = api.metrics
|
|
13
|
+
xprof = core.xprof.Xprof
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""API for interacting with the diagnostics library."""
|
|
2
|
+
|
|
3
|
+
from google_cloud_mldiagnostics.src.google_cloud_mldiagnostics.api import metrics
|
|
4
|
+
from google_cloud_mldiagnostics.src.google_cloud_mldiagnostics.api import mlrun
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
machinelearning_run = mlrun.machinelearning_run
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Module for recording metrics."""
|
|
2
|
+
|
|
3
|
+
from google_cloud_mldiagnostics.src.google_cloud_mldiagnostics.core import metrics
|
|
4
|
+
from google_cloud_mldiagnostics.src.google_cloud_mldiagnostics.custom_types import metric_types
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_metrics_recorder = metrics.metrics_recorder
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def record(
|
|
11
|
+
metric_name: metric_types.MetricType or str,
|
|
12
|
+
value: int | float,
|
|
13
|
+
step: int | None = None,
|
|
14
|
+
labels: dict[str, str] | None = None,
|
|
15
|
+
record_on_all_hosts: bool = False,
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Record a single metric value using the active run.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
metric_name: Name of metric to record.
|
|
21
|
+
value: Metric value.
|
|
22
|
+
step: Optional step number (auto-incremented if not provided).
|
|
23
|
+
labels: Optional additional labels.
|
|
24
|
+
record_on_all_hosts: Whether to record metrics on all hosts.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
RecordingError: If no active run or recording fails.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
metrics.record(MetricType.TF_FLOPS, per_device_tf_flops)
|
|
31
|
+
metrics.record(MetricType.LEARNING_RATE, learning_rate)
|
|
32
|
+
metrics.record(MetricType.LEARNING_RATE, learning_rate, step=1)
|
|
33
|
+
"""
|
|
34
|
+
is_enum = isinstance(metric_name, metric_types.MetricType)
|
|
35
|
+
metric_name = metric_name.value if is_enum else metric_name
|
|
36
|
+
_metrics_recorder.record(
|
|
37
|
+
metric_name, value, step, labels, record_on_all_hosts
|
|
38
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Module for creating and managing ML runs."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from google_cloud_mldiagnostics.src.google_cloud_mldiagnostics.core import create_mlrun
|
|
5
|
+
from google_cloud_mldiagnostics.src.google_cloud_mldiagnostics.custom_types import exceptions
|
|
6
|
+
from google_cloud_mldiagnostics.src.google_cloud_mldiagnostics.custom_types import mlrun_types
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Main SDK function - this is the primary interface users will import
|
|
10
|
+
def machinelearning_run(
|
|
11
|
+
run_group: str,
|
|
12
|
+
name: str | None = None,
|
|
13
|
+
configs: dict[str, Any] | None = None,
|
|
14
|
+
gcs_path: str | None = None,
|
|
15
|
+
gcp_project: str | None = None,
|
|
16
|
+
gcp_region: str | None = None,
|
|
17
|
+
metrics_record_interval_sec: float = 10.0,
|
|
18
|
+
) -> mlrun_types.MLRun:
|
|
19
|
+
"""Create a new machine learning run.
|
|
20
|
+
|
|
21
|
+
This is the main entry point for the SDK that users will call to create ML
|
|
22
|
+
runs.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
run_group: The run set this run belongs to
|
|
26
|
+
name: The name of the run (optional), system will generate a unique ID if
|
|
27
|
+
not provided by users.
|
|
28
|
+
configs: dict of configuration parameters
|
|
29
|
+
gcs_path: GCS path for storing run artifacts
|
|
30
|
+
gcp_project: The GCP project ID
|
|
31
|
+
gcp_region: The GCP region
|
|
32
|
+
metrics_record_interval_sec: The interval in seconds
|
|
33
|
+
for recording system metrics backend (tpu duty cycle,
|
|
34
|
+
tpu tensorcore utilization, hbm utilization,
|
|
35
|
+
host cpu utilization, host memory utilization).
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
MLRun: A new ML run instance
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
from google_cloud_mldiagnostics import machinelearning_run
|
|
42
|
+
|
|
43
|
+
my_run = machinelearning_run(
|
|
44
|
+
run_group="training_set_v1",
|
|
45
|
+
name="experiment_1",
|
|
46
|
+
configs={"epochs": 100, "batch_size": 32},
|
|
47
|
+
gcs_path="gs://my-bucket/experiments"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Update configs using dict methods
|
|
51
|
+
my_run.configs.update({"epochs": 300, "optimizer": "adam"})
|
|
52
|
+
|
|
53
|
+
# Update configs using attribute notation
|
|
54
|
+
my_run.configs.batch_size = 64
|
|
55
|
+
"""
|
|
56
|
+
if not run_group:
|
|
57
|
+
raise exceptions.MLRunConfigurationError(
|
|
58
|
+
"run_group is required and must be provided. The run_group parameter"
|
|
59
|
+
" helps identify and organize your ML workloads. Please provide a"
|
|
60
|
+
" meaningful run_group name (e.g., 'training_v1',"
|
|
61
|
+
" 'experiment_batch_1')."
|
|
62
|
+
)
|
|
63
|
+
return create_mlrun.initialize_mlrun(
|
|
64
|
+
run_group=run_group,
|
|
65
|
+
name=name,
|
|
66
|
+
configs=configs,
|
|
67
|
+
gcs_path=gcs_path,
|
|
68
|
+
gcp_project=gcp_project,
|
|
69
|
+
gcp_region=gcp_region,
|
|
70
|
+
metrics_record_interval_sec=metrics_record_interval_sec,
|
|
71
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Unit tests for the API metrics module."""
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
from unittest import mock
|
|
5
|
+
|
|
6
|
+
from google_cloud_mldiagnostics.src.google_cloud_mldiagnostics.api import metrics
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestMetricsAPI(unittest.TestCase):
|
|
10
|
+
"""Test cases for the metrics API module."""
|
|
11
|
+
|
|
12
|
+
@mock.patch(
|
|
13
|
+
"google_cloud_mldiagnostics.src.google_cloud_mldiagnostics.api."
|
|
14
|
+
"metrics._metrics_recorder"
|
|
15
|
+
)
|
|
16
|
+
def test_record_function_minimal_parameters(self, mock_recorder):
|
|
17
|
+
"""Test record function with minimal parameters."""
|
|
18
|
+
# Setup
|
|
19
|
+
mock_metric_type = mock.Mock()
|
|
20
|
+
mock_metric_type.value = "test_metric"
|
|
21
|
+
|
|
22
|
+
# Call the API function
|
|
23
|
+
metrics.record(metric_name=mock_metric_type, value=42.5)
|
|
24
|
+
|
|
25
|
+
# Verify delegation to the underlying recorder
|
|
26
|
+
mock_recorder.record.assert_called_once_with(
|
|
27
|
+
mock_metric_type, 42.5, None, None, False
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
unittest.main()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Unit tests for the machinelearning_run module."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import unittest
|
|
5
|
+
from unittest import mock
|
|
6
|
+
|
|
7
|
+
from google_cloud_mldiagnostics.src.google_cloud_mldiagnostics.api import mlrun as api_mlrun
|
|
8
|
+
from google_cloud_mldiagnostics.src.google_cloud_mldiagnostics.custom_types import exceptions
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestMachineLearningRun(unittest.TestCase):
|
|
12
|
+
"""Test cases for the machinelearning_run function."""
|
|
13
|
+
|
|
14
|
+
def setUp(self):
|
|
15
|
+
"""Set up test fixtures before each test method."""
|
|
16
|
+
super().setUp()
|
|
17
|
+
self.valid_run_group = "test_run_group"
|
|
18
|
+
self.valid_name = "test_run_name"
|
|
19
|
+
self.valid_configs = {"epochs": 100, "batch_size": 32}
|
|
20
|
+
self.valid_gcs_path = "gs://test-bucket/path"
|
|
21
|
+
self.mock_create_mlrun = mock.patch(
|
|
22
|
+
"google_cloud_mldiagnostics.src.google_cloud_mldiagnostics.core.create_mlrun.initialize_mlrun"
|
|
23
|
+
).start()
|
|
24
|
+
|
|
25
|
+
def tearDown(self):
|
|
26
|
+
"""Clean up after each test method."""
|
|
27
|
+
mock.patch.stopall()
|
|
28
|
+
super().tearDown()
|
|
29
|
+
|
|
30
|
+
def test_machinelearning_run_with_all_parameters(self):
|
|
31
|
+
"""Test creating ML run with all parameters provided."""
|
|
32
|
+
api_mlrun.machinelearning_run(
|
|
33
|
+
run_group=self.valid_run_group,
|
|
34
|
+
name=self.valid_name,
|
|
35
|
+
configs=self.valid_configs,
|
|
36
|
+
gcs_path=self.valid_gcs_path
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
self.mock_create_mlrun.assert_called_once_with(
|
|
40
|
+
run_group=self.valid_run_group,
|
|
41
|
+
name=self.valid_name,
|
|
42
|
+
configs=self.valid_configs,
|
|
43
|
+
gcs_path=self.valid_gcs_path,
|
|
44
|
+
gcp_project=None,
|
|
45
|
+
gcp_region=None,
|
|
46
|
+
metrics_record_interval_sec=10.0,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def test_machinelearning_run_none_run_group_raises_error(self):
|
|
50
|
+
"""Test that None run_group raises MLRunConfigurationError."""
|
|
51
|
+
with self.assertRaises(exceptions.MLRunConfigurationError) as context:
|
|
52
|
+
api_mlrun.machinelearning_run(run_group=None)
|
|
53
|
+
|
|
54
|
+
self.assertIn(
|
|
55
|
+
"run_group is required and must be provided. The run_group parameter"
|
|
56
|
+
" helps identify and organize your ML workloads. Please provide a"
|
|
57
|
+
" meaningful run_group name (e.g., 'training_v1',"
|
|
58
|
+
" 'experiment_batch_1').",
|
|
59
|
+
str(context.exception),
|
|
60
|
+
)
|
|
61
|
+
self.assertIn("identify and organize", str(context.exception))
|
|
62
|
+
|
|
63
|
+
def test_yaml_json_workflow_configs(self):
|
|
64
|
+
"""Test ML run creation with configs loaded from a JSON representation of YAML."""
|
|
65
|
+
self.valid_run_group = "test_run_group"
|
|
66
|
+
self.valid_name = "test_run_name"
|
|
67
|
+
self.valid_configs = {"epochs": 100, "batch_size": 32}
|
|
68
|
+
self.valid_gcs_path = "gs://test-bucket/path"
|
|
69
|
+
|
|
70
|
+
# Simulate YAML data that was loaded and converted to JSON
|
|
71
|
+
yaml_data = {
|
|
72
|
+
"epochs": 200,
|
|
73
|
+
"batch_size": 64,
|
|
74
|
+
"learning_rate": 0.001,
|
|
75
|
+
"model_type": "transformer"
|
|
76
|
+
}
|
|
77
|
+
json_data = json.dumps(yaml_data)
|
|
78
|
+
|
|
79
|
+
api_mlrun.machinelearning_run(
|
|
80
|
+
run_group=self.valid_run_group,
|
|
81
|
+
name=self.valid_name,
|
|
82
|
+
configs=json.loads(json_data), # Parse JSON back to dict for this test
|
|
83
|
+
gcs_path=self.valid_gcs_path
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
self.mock_create_mlrun.assert_called_once_with(
|
|
87
|
+
run_group=self.valid_run_group,
|
|
88
|
+
name=self.valid_name,
|
|
89
|
+
configs=yaml_data, # Verify that the YAML data was correctly converted
|
|
90
|
+
gcs_path=self.valid_gcs_path,
|
|
91
|
+
gcp_project=None,
|
|
92
|
+
gcp_region=None,
|
|
93
|
+
metrics_record_interval_sec=10.0,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
unittest.main()
|
|
File without changes
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Client for sending requests to Diagon Control Plane."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
import google.auth
|
|
7
|
+
from google.auth.transport import requests as google_auth_requests
|
|
8
|
+
from google_cloud_mldiagnostics.src.google_cloud_mldiagnostics.utils import host_utils
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ControlPlaneClient:
|
|
13
|
+
"""Client for communicating with Google Cloud Hypercompute Cluster ML Run service."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
project_id: str = "supercomputer-testing",
|
|
18
|
+
location: str = "us-central1",
|
|
19
|
+
base_url: str = "https://autopush-hypercomputecluster.sandbox.googleapis.com/v1alpha",
|
|
20
|
+
):
|
|
21
|
+
"""Initializes a new ControlPlaneClient.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
project_id: Google Cloud project ID
|
|
25
|
+
location: Google Cloud location/region
|
|
26
|
+
base_url: Base URL for the API endpoint
|
|
27
|
+
"""
|
|
28
|
+
self.project_id = project_id
|
|
29
|
+
self.location = location
|
|
30
|
+
self.base_url = base_url
|
|
31
|
+
self.ml_runs_url = f"{base_url}/projects/{project_id}/locations/{location}/machineLearningRuns"
|
|
32
|
+
|
|
33
|
+
# Initialize Google Cloud credentials
|
|
34
|
+
self.credentials, _ = google.auth.default()
|
|
35
|
+
|
|
36
|
+
def _get_access_token(self) -> str:
|
|
37
|
+
"""Get Google Cloud access token for authentication."""
|
|
38
|
+
if not self.credentials.valid:
|
|
39
|
+
self.credentials.refresh(google_auth_requests.Request())
|
|
40
|
+
|
|
41
|
+
return self.credentials.token
|
|
42
|
+
|
|
43
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
44
|
+
"""Get HTTP headers with authentication."""
|
|
45
|
+
return {
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
"Authorization": f"Bearer {self._get_access_token()}",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
def create_ml_run(
|
|
51
|
+
self,
|
|
52
|
+
name: str,
|
|
53
|
+
display_name: str,
|
|
54
|
+
run_phase: str,
|
|
55
|
+
configs: Optional[Dict[str, Any]] = None,
|
|
56
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
57
|
+
metrics: Optional[Dict[str, str]] = None,
|
|
58
|
+
artifacts: Optional[Dict[str, str]] = None,
|
|
59
|
+
run_group: Optional[str] = None,
|
|
60
|
+
labels: Optional[Dict[str, str]] = None,
|
|
61
|
+
) -> Dict[str, Any]:
|
|
62
|
+
"""Create a new ML run using the Google Cloud API.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
name: Name of the run
|
|
66
|
+
display_name: Display name for the run
|
|
67
|
+
run_phase: Phase of the run (ACTIVE, COMPLETE, FAILED)
|
|
68
|
+
configs: Configuration settings (userConfigs, softwareConfigs,
|
|
69
|
+
hardwareConfigs)
|
|
70
|
+
tools: List of tools to enable (e.g., XProf, NSys)
|
|
71
|
+
metrics: Metrics for the run (e.g., avgStep, avgLatency)
|
|
72
|
+
artifacts: Artifacts configuration (e.g., gcsPath)
|
|
73
|
+
run_group: Run group grouping identifier
|
|
74
|
+
labels: Custom labels for the run
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Response from the API as a dictionary
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
requests.exceptions.RequestException: If the HTTP request fails
|
|
81
|
+
"""
|
|
82
|
+
payload = {"displayName": display_name, "name": name}
|
|
83
|
+
|
|
84
|
+
if configs:
|
|
85
|
+
payload["configs"] = configs
|
|
86
|
+
|
|
87
|
+
if metrics:
|
|
88
|
+
payload["metrics"] = metrics
|
|
89
|
+
|
|
90
|
+
if artifacts:
|
|
91
|
+
payload["artifacts"] = artifacts
|
|
92
|
+
|
|
93
|
+
if run_group:
|
|
94
|
+
payload["runSet"] = run_group
|
|
95
|
+
|
|
96
|
+
if labels:
|
|
97
|
+
payload["labels"] = labels
|
|
98
|
+
|
|
99
|
+
if run_phase:
|
|
100
|
+
payload["runPhase"] = run_phase
|
|
101
|
+
|
|
102
|
+
if tools:
|
|
103
|
+
payload["tools"] = tools
|
|
104
|
+
|
|
105
|
+
# Sanitize the name for machineLearningRunId
|
|
106
|
+
sanitized_name = host_utils.sanitize_identifier(name)
|
|
107
|
+
|
|
108
|
+
params = {"machine_learning_run_id": sanitized_name}
|
|
109
|
+
|
|
110
|
+
response = requests.post(
|
|
111
|
+
self.ml_runs_url,
|
|
112
|
+
headers=self._get_headers(),
|
|
113
|
+
params=params,
|
|
114
|
+
json=payload,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Raise an exception for HTTP error status codes
|
|
118
|
+
response.raise_for_status()
|
|
119
|
+
|
|
120
|
+
return response.json()
|
|
121
|
+
|
|
122
|
+
def update_ml_run(
|
|
123
|
+
self
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Update an existing ML run using the Google Cloud API."""
|
|
126
|
+
del self
|
|
127
|
+
pass
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Client for writing metrics directly to Google Cloud Logging."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
from google.auth import credentials
|
|
9
|
+
from google.cloud import logging as cloud_logging
|
|
10
|
+
from google.cloud.logging_v2 import resource
|
|
11
|
+
from google_cloud_mldiagnostics.src.google_cloud_mldiagnostics.custom_types import exceptions
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LoggingClient:
|
|
18
|
+
"""Client for writing metrics directly to Google Cloud Logging."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
project_id: str,
|
|
23
|
+
log_name: str = "ml_diagnostics_metric",
|
|
24
|
+
credentials_path: Optional[str] = None,
|
|
25
|
+
user_credentials: Optional[credentials.Credentials] = None,
|
|
26
|
+
):
|
|
27
|
+
"""Initialize the logging client.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
project_id: GCP project ID
|
|
31
|
+
log_name: Name of the log to write to
|
|
32
|
+
credentials_path: Optional path to service account credentials
|
|
33
|
+
user_credentials: Optional explicit credentials object.
|
|
34
|
+
"""
|
|
35
|
+
self.project_id = project_id
|
|
36
|
+
self.log_name = log_name
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
if user_credentials:
|
|
40
|
+
# Use explicit credentials if provided
|
|
41
|
+
self.client = cloud_logging.Client(
|
|
42
|
+
project=project_id, credentials=user_credentials
|
|
43
|
+
)
|
|
44
|
+
elif credentials_path:
|
|
45
|
+
# Fall back to credentials file
|
|
46
|
+
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = credentials_path
|
|
47
|
+
self.client = cloud_logging.Client(project=project_id)
|
|
48
|
+
else:
|
|
49
|
+
# Use default credentials
|
|
50
|
+
self.client = cloud_logging.Client(project=project_id)
|
|
51
|
+
|
|
52
|
+
# Get the logger for this log name
|
|
53
|
+
self.logger = self.client.logger(log_name)
|
|
54
|
+
|
|
55
|
+
except Exception as e:
|
|
56
|
+
raise exceptions.MLDiagnosticError(
|
|
57
|
+
f"Failed to initialize logging client: {e}"
|
|
58
|
+
) from e
|
|
59
|
+
|
|
60
|
+
def write_metric(
|
|
61
|
+
self,
|
|
62
|
+
metric_name: str,
|
|
63
|
+
value: float | int | List[float | int],
|
|
64
|
+
run_id: str,
|
|
65
|
+
location: str,
|
|
66
|
+
step: Optional[int] = None,
|
|
67
|
+
labels: Optional[dict[str, str]] = None,
|
|
68
|
+
):
|
|
69
|
+
"""Write a single metric point to Cloud Logging.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
metric_name: Name of the metric
|
|
73
|
+
value: Metric value. Can be a single int or float, or a list of ints
|
|
74
|
+
and floats.
|
|
75
|
+
run_id: ML run identifier
|
|
76
|
+
location: ML run region
|
|
77
|
+
step: Optional step number
|
|
78
|
+
labels: Optional additional labels
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
True if the metric was written successfully, False otherwise.
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
# Create timestamp
|
|
85
|
+
current_time = datetime.datetime.now(datetime.timezone.utc)
|
|
86
|
+
|
|
87
|
+
# Define the resource for the log entry
|
|
88
|
+
metric_resource = resource.Resource(
|
|
89
|
+
type="generic_node",
|
|
90
|
+
labels={
|
|
91
|
+
"project_id": self.project_id,
|
|
92
|
+
"location": location,
|
|
93
|
+
"namespace": metric_name,
|
|
94
|
+
"node_id": run_id,
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Build the log payload structure
|
|
99
|
+
if isinstance(value, (int, float)):
|
|
100
|
+
value = [value]
|
|
101
|
+
payload = {"values": value}
|
|
102
|
+
|
|
103
|
+
# Add optional fields
|
|
104
|
+
if step is not None:
|
|
105
|
+
payload["step_index"] = step
|
|
106
|
+
if labels:
|
|
107
|
+
payload.update(labels)
|
|
108
|
+
|
|
109
|
+
# Write to Cloud Logging
|
|
110
|
+
self.logger.log_struct(
|
|
111
|
+
payload,
|
|
112
|
+
severity="INFO",
|
|
113
|
+
timestamp=current_time,
|
|
114
|
+
resource=metric_resource,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
logger.info("Successfully written metric to log: %s", metric_name)
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
raise exceptions.MLDiagnosticError(
|
|
121
|
+
f"Failed to write to Cloud Logging: {e}"
|
|
122
|
+
) from e
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class NoOpLoggingClient(LoggingClient):
|
|
126
|
+
"""A logging client that performs no operations."""
|
|
127
|
+
|
|
128
|
+
def __init__(self): # pylint: disable=super-init-not-called
|
|
129
|
+
"""Initializes the NoOp client. Does nothing."""
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
def write_metric(
|
|
133
|
+
self,
|
|
134
|
+
metric_name: str,
|
|
135
|
+
value: float | int | List[float | int],
|
|
136
|
+
run_id: str,
|
|
137
|
+
location: str,
|
|
138
|
+
step: Optional[int] = None,
|
|
139
|
+
labels: Optional[dict[str, str]] = None,
|
|
140
|
+
):
|
|
141
|
+
"""This is a no-op and does not write any metrics."""
|
|
142
|
+
pass
|