Skip to content

Commit 255c7b5

Browse files
authored
Merge branch 'apple:main' into jax_0.9.1_dev
2 parents b3ecea8 + 9056b6f commit 255c7b5

46 files changed

Lines changed: 1756 additions & 343 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

axlearn/audio/frontend_utils.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,11 @@
1515
import jax
1616
import jax.numpy as jnp
1717
import numpy as np
18-
from jax._src.mesh import thread_resources
19-
from jax.experimental.shard_map import shard_map
2018
from jax.sharding import PartitionSpec
2119
from numpy.typing import ArrayLike
2220

2321
from axlearn.common import ein_ops
24-
from axlearn.common.utils import Tensor
22+
from axlearn.common.utils import Tensor, get_current_abstract_or_physical_mesh
2523

2624

2725
class WindowType(enum.Enum):
@@ -402,9 +400,9 @@ def sharded_fft(n: int, partition_spec: PartitionSpec) -> Callable[[Tensor], Ten
402400
Returns:
403401
A callable that computes FFT.
404402
"""
405-
return shard_map(
403+
return jax.shard_map(
406404
lambda x: jnp.fft.rfft(cast_for_rfft(x), n=n),
407-
mesh=thread_resources.env.physical_mesh,
405+
mesh=get_current_abstract_or_physical_mesh(),
408406
in_specs=partition_spec,
409407
out_specs=partition_spec,
410408
)

axlearn/cloud/common/bastion.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
except ModuleNotFoundError:
111111
logging.warning("tensorflow_io is not installed -- tf_io may not work with s3://")
112112

113+
from axlearn.cloud.common import metrics
113114
from axlearn.cloud.common.cleaner import Cleaner
114115
from axlearn.cloud.common.event_queue import BaseQueueClient, Event
115116
from axlearn.cloud.common.quota import QuotaFn
@@ -904,10 +905,13 @@ def check_leaves(x, y):
904905

905906
def _wait_and_close_proc(self, proc: _PipedProcess, kill: bool = False):
906907
"""Cleans up the process/fds and upload logs to gs."""
908+
pid = proc.popen.pid
907909
if kill:
908910
send_signal(proc.popen, sig=signal.SIGKILL)
909911
# Note: proc should already be polled and completed, so wait is nonblocking.
910912
proc.popen.wait()
913+
# Cleanup the local prometheus DB files when the process is terminated
914+
_catch_with_error_log(metrics.cleanup, pid)
911915
proc.fd.close()
912916
# Upload outputs to log dir.
913917
_catch_with_error_log(
@@ -958,15 +962,16 @@ def _sync_jobs(self):
958962
959963
We use these jobspecs to update the local self._active_jobs.
960964
"""
961-
active_jobs, jobs_with_user_states, jobs_with_failed_validation = download_job_batch(
962-
spec_dir=self._active_dir,
963-
state_dir=self._state_dir,
964-
user_state_dir=self._user_state_dir,
965-
verbose=True,
966-
remove_invalid_user_states=True,
967-
quota=self._quota,
968-
validator=self._validator,
969-
)
965+
with metrics.BASTION_DOWNLOAD_JOB_ALL_SECONDS.time():
966+
active_jobs, jobs_with_user_states, jobs_with_failed_validation = download_job_batch(
967+
spec_dir=self._active_dir,
968+
state_dir=self._state_dir,
969+
user_state_dir=self._user_state_dir,
970+
verbose=True,
971+
remove_invalid_user_states=True,
972+
quota=self._quota,
973+
validator=self._validator,
974+
)
970975
self._jobs_with_user_states = jobs_with_user_states
971976
# Iterate over unique job names.
972977
# pylint: disable-next=use-sequence-for-iteration

axlearn/cloud/common/metrics.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
"""Prometheus metrics for Bastion runners."""
2+
3+
import logging
4+
import threading
5+
from http.server import BaseHTTPRequestHandler, HTTPServer
6+
7+
from prometheus_client import CollectorRegistry, Gauge, Histogram, generate_latest, multiprocess
8+
9+
BASTION_DOWNLOAD_JOB_ALL_SECONDS = Histogram(
10+
"bastion_download_job_all_seconds",
11+
"Time in seconds consumed in each scheduling loop to download all the job specs",
12+
[],
13+
buckets=(1, 5, 10, 30, 60, 120, 300, 600, 1800, 3600), # 1s to 1hr
14+
)
15+
16+
JOB_TIME_TO_RUNNING_SECONDS = Histogram(
17+
"bastion_job_time_to_running_seconds",
18+
"Time in seconds from runner starts a job until until it reaches Running state",
19+
["job_name"],
20+
buckets=(1, 5, 10, 30, 60, 120, 300, 600, 1800, 3600), # 1s to 1hr
21+
)
22+
23+
JOB_RUN_LATENCY_GAUGE = Gauge(
24+
"bastion_job_run_latency_seconds",
25+
"Time in seconds from runner starts a job until until it reaches Running state",
26+
["job_name"],
27+
)
28+
29+
JOB_WAIT_FOR_BUILD_SECONDS = Gauge(
30+
"bastion_job_wait_for_build_seconds",
31+
"time in seconds until the job is built successfully",
32+
["job_name"],
33+
)
34+
35+
JOB_SUSPENDED_DURATION_SECONDS = Histogram(
36+
"bastion_job_suspended_duration_seconds",
37+
"Time in seconds the jobset stayed in SUSPENDED state",
38+
["job_name"],
39+
buckets=(1, 5, 10, 30, 60, 120, 300, 600, 1800, 3600), # 1s to 1hr
40+
)
41+
42+
43+
class _MetricsHandler(BaseHTTPRequestHandler):
44+
"""HTTP handler that serves aggregated metrics from multiprocess mode."""
45+
46+
def do_GET(self): # pylint: disable=invalid-name
47+
if self.path == "/metrics":
48+
registry = CollectorRegistry()
49+
multiprocess.MultiProcessCollector(registry)
50+
metrics = generate_latest(registry)
51+
52+
self.send_response(200)
53+
self.send_header("Content-Type", "text/plain; charset=utf-8")
54+
self.end_headers()
55+
self.wfile.write(metrics)
56+
else:
57+
self.send_error(404)
58+
59+
def log_message(self, format, *args): # pylint: disable=redefined-builtin
60+
# Suppress default logging
61+
pass
62+
63+
64+
class MetricsServer:
65+
"""Singleton class to manage Prometheus metrics server.
66+
67+
Supports multiprocess mode where metrics from parent and child processes
68+
are aggregated and exposed via a single endpoint.
69+
"""
70+
71+
_instance = None
72+
_lock = threading.Lock()
73+
_server_started = False
74+
_multiprocess_dir = None
75+
76+
def __new__(cls):
77+
if cls._instance is None:
78+
with cls._lock:
79+
if cls._instance is None:
80+
cls._instance = super().__new__(cls)
81+
return cls._instance
82+
83+
@classmethod
84+
def start(cls, port: int = 8000):
85+
"""Start Prometheus metrics HTTP server on the specified port.
86+
87+
Args:
88+
port: Port number to start the metrics server on. Defaults to 8000.
89+
use_multiprocess: If True, use multiprocess mode which aggregates metrics
90+
from child processes. Defaults to False for backward compatibility.
91+
"""
92+
with cls._lock:
93+
if not cls._server_started:
94+
try:
95+
# Use custom handler for multiprocess aggregation
96+
server = HTTPServer(("", port), _MetricsHandler)
97+
thread = threading.Thread(target=server.serve_forever, daemon=True)
98+
thread.start()
99+
logging.info(
100+
"Prometheus metrics server (multiprocess mode) started on port %s", port
101+
)
102+
cls._server_started = True
103+
except OSError as e:
104+
# Port might already be in use
105+
logging.warning("Failed to start metrics server on port %s: %s", port, e)
106+
107+
@classmethod
108+
def is_started(cls) -> bool:
109+
"""Check if the metrics server has been started."""
110+
return cls._server_started
111+
112+
@classmethod
113+
def get_multiprocess_dir(cls) -> str:
114+
"""Get the multiprocess directory path if configured."""
115+
return cls._multiprocess_dir
116+
117+
118+
def record_job_time_to_running(job_name: str, time_seconds: float):
119+
"""Record the time it took for a job to reach RUNNING state.
120+
121+
Args:
122+
job_name: Name of the job
123+
time_seconds: Time in seconds from first seen until RUNNING state
124+
"""
125+
JOB_TIME_TO_RUNNING_SECONDS.labels(job_name=job_name).observe(time_seconds)
126+
logging.info("Metrics: job %s took %.2f seconds to reach RUNNING state", job_name, time_seconds)
127+
128+
129+
def record_job_run_latency(job_name: str, time_seconds: float):
130+
JOB_RUN_LATENCY_GAUGE.labels(job_name).set(time_seconds)
131+
132+
133+
def record_job_wait_for_build(job_name: str, time_seconds: float):
134+
"""Record the time it took to wait for the cloud build to finish
135+
136+
This does not necessarily mean the whole cloud build time for a job,
137+
as the build starts when the client submits the job. This only measures
138+
how long the runner waits for the build to complete before launching
139+
the job.
140+
"""
141+
JOB_WAIT_FOR_BUILD_SECONDS.labels(job_name).set(time_seconds)
142+
143+
144+
def record_job_suspended_duration(job_name: str, time_seconds: float):
145+
"""Record the time a job spent in SUSPENDED state.
146+
147+
Args:
148+
job_name: Name of the job
149+
time_seconds: Time in seconds the job was in SUSPENDED state
150+
"""
151+
JOB_SUSPENDED_DURATION_SECONDS.labels(job_name=job_name).observe(time_seconds)
152+
logging.info("Metrics: job %s was in SUSPENDED state for %.2f seconds", job_name, time_seconds)
153+
154+
155+
def cleanup(pid=None):
156+
if pid:
157+
logging.info("Cleaning up prometheus metrics DB file for %s", pid)
158+
multiprocess.mark_process_dead(pid)

0 commit comments

Comments
 (0)