Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions axlearn/cloud/gcp/jobset_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,8 @@ def _build_pod(self) -> Nested[Any]:
system = USER_FACING_NAME_TO_SYSTEM_CHARACTERISTICS[self._tpu_type]
annotations, labels, selector, volumes, tolerations = {}, {}, {}, [], []
annotations["axlearn/main-container"] = cfg.name
if cfg.command and ("enable_ml_diagnostics_metrics=True" in cfg.command or "enable_ml_diagnostics_xprof=True" in cfg.command):
labels["managed-mldiagnostics-gke"] = "true"

volumes.append(dict(name="shared-output", emptyDir={}))
if cfg.gcsfuse_mount:
Expand Down Expand Up @@ -1165,6 +1167,7 @@ def _build_main_container(self) -> Nested[Any]:

# These are common across all GPUReplicatedJobs, used for connecting between replicas
env_vars: dict[str, Nested[str]] = {}
env_vars["AXLEARN_JOB_NAME"] = cfg.name
env_vars["DISTRIBUTED_COORDINATOR"] = f"{cfg.name}-{cfg.job_name}-0-0.{cfg.name}:8080"
env_vars["NUM_PROCESSES"] = f"{cfg.accelerator.num_replicas}"

Expand Down
22 changes: 22 additions & 0 deletions axlearn/cloud/gcp/jobset_utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,28 @@ def test_ephemeral_disk_mount_dataclass(self):
self.assertEqual(m.size_gb, 500)
self.assertEqual(m.read_only, False)

def test_mldiagnostics_label(self):
cases = [
("python3 -m trainer --enable_ml_diagnostics_metrics=True", True),
("python3 -m trainer --enable_ml_diagnostics_xprof=True", True),
("python3 -m trainer --enable_ml_diagnostics_metrics=True --enable_ml_diagnostics_xprof=True", True),
("python3 -m trainer --enable_ml_diagnostics_metrics=False", False),
("python3 -m trainer", False),
]
for command, expected in cases:
with self._job_config(bundler_cls=ArtifactRegistryBundler) as (cfg, bundler_cfg):
cfg.set(
command=command,
output_dir="FAKE",
)
job = cfg.instantiate(bundler=bundler_cfg.instantiate())
pod = job._build_pod() # pylint: disable=protected-access
labels = pod["metadata"]["labels"]
if expected:
self.assertEqual(labels.get("managed-mldiagnostics-gke"), "true")
else:
self.assertNotIn("managed-mldiagnostics-gke", labels)


class CompositeReplicatedJobTest(TestCase):
def test_composite_replicated_job(self):
Expand Down
52 changes: 52 additions & 0 deletions axlearn/cloud/gcp/scripts/get_mldiag_urls.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
#
# Extracts profiler session TensorBoard URLs and Cloud Console URL
# for a given ML run as JSON.
#
# Usage:
# ./axlearn/cloud/gcp/scripts/get_mldiag_urls.sh <RUN_NAME> <LOCATION> <PROJECT>

set -euo pipefail

if [[ $# -ne 3 ]]; then
echo "Usage: $0 <RUN_NAME> <LOCATION> <PROJECT>" >&2
echo "Example: $0 rapatchi-fuji-golden-mldiag-run us-central1 rapatchiexperiment-b6dk7n" >&2
exit 1
fi

RUN="$1"
LOCATION="$2"
PROJECT="$3"

# Resolve display name to resource ID if needed
RUN_ID=$(gcloud alpha mldiagnostics machine-learning-run list \
--location="${LOCATION}" \
--project="${PROJECT}" \
--filter="displayName:${RUN} OR name:${RUN} OR workloadDetails.gke.id:${RUN}" \
--format="value(name)" \
--limit=1 \
--quiet)

if [[ -z "${RUN_ID}" ]]; then
RUN_ID="${RUN}"
fi

HEX_ID="${RUN_ID##*/}"
CONSOLE_URL="https://console.cloud.google.com/cluster-director/diagnostics/details/${LOCATION}/${HEX_ID}?project=${PROJECT}"

# Fetch profiler sessions as JSON and format with jq
gcloud alpha mldiagnostics profiler-session list \
--machine-learning-run="${RUN_ID}" \
--location="${LOCATION}" \
--project="${PROJECT}" \
--filter="dashboardUri:*" \
--format=json \
--quiet | jq \
--arg run "${RUN}" \
--arg console "${CONSOLE_URL}" \
'{
($run): {
console_url: $console,
profiler_sessions: [ .[] | { ((.name | split("/") | last)): .dashboardUri } ]
}
}'
1 change: 1 addition & 0 deletions axlearn/cloud/gcp/tpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
def get_default_env(*, tpu_type: str, num_tpu_slices: int, job_name: str) -> dict[str, Any]:
"""Gets the default environment for TPU pods."""
return dict(
AXLEARN_JOB_NAME=job_name,
# Use a large refresh to mitigate DNS timeout issues until tf>2.12 upgrade.
GCS_RESOLVE_REFRESH_SECS=600,
TPU_TYPE=tpu_type,
Expand Down
22 changes: 22 additions & 0 deletions axlearn/common/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,7 @@ py_library(
":input_base",
":metrics",
":module",
":managed_mldiagnostics",
":summary_writer",
":utils",
"@axlearn_pip//absl_py",
Expand Down Expand Up @@ -1205,6 +1206,7 @@ py_library(
":file_system",
":input_base",
":learner",
":managed_mldiagnostics",
":measurement",
":module",
":optimizer_base",
Expand Down Expand Up @@ -2129,6 +2131,7 @@ py_library(
":config",
":evaler",
":file_system",
":managed_mldiagnostics",
":inference_output",
":input_base",
":layers",
Expand Down Expand Up @@ -3475,3 +3478,22 @@ py_test(
"@axlearn_pip//numpy",
],
)

py_library(
name = "managed_mldiagnostics",
srcs = ["managed_mldiagnostics.py"],
visibility = ["//visibility:public"],
deps = [
"@axlearn_pip//absl_py",
],
)

py_test(
name = "managed_mldiagnostics_test",
srcs = ["managed_mldiagnostics_test.py"],
deps = [
":managed_mldiagnostics",
"@axlearn_pip//absl_py",
],
)

66 changes: 48 additions & 18 deletions axlearn/common/evaler.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@

from axlearn.common import flax_struct, input_base, summary_writer, utils
from axlearn.common.base_model import BaseModel
from axlearn.common.managed_mldiagnostics import (
MLDiagnosticsConfig,
is_ml_diagnostics_metrics_enabled,
is_ml_diagnostics_xprof_enabled,
)
from axlearn.common.config import (
REQUIRED,
InstantiableConfig,
Expand Down Expand Up @@ -582,6 +587,8 @@ class Config(Module.Config):
metric_calculator: BaseMetricCalculator.Config = ModelSummaryAccumulator.default_config()
# If not None, writes input batches and `metric_calculator` forward outputs.
output_writer: Optional[BaseOutputWriter.Config] = None
# Configuration for ML Diagnostics.
ml_diagnostics: Optional[MLDiagnosticsConfig] = None

def __init__(
self,
Expand Down Expand Up @@ -609,12 +616,28 @@ def __init__(
model=model,
model_param_partition_specs=model_param_partition_specs,
)
self._add_child("summary_writer", cfg.summary_writer)
if cfg.output_writer is not None:
self._add_child("output_writer", cfg.output_writer)

self._trace_steps = set()
self._eval_policy: EvalPolicy = cfg.eval_policy.instantiate()
self._enable_ml_diagnostics_metrics: bool = is_ml_diagnostics_metrics_enabled(
cfg.ml_diagnostics
)
self._enable_ml_diagnostics_xprof: bool = is_ml_diagnostics_xprof_enabled(
cfg.ml_diagnostics
)
if self._enable_ml_diagnostics_xprof:
from axlearn.common.managed_mldiagnostics import ManagedMLDiagnostics
try:
ManagedMLDiagnostics(cfg.ml_diagnostics)
except Exception: # pylint: disable=broad-except
pass

writer_cfg = cfg.summary_writer
if self._enable_ml_diagnostics_metrics:
from axlearn.common.summary_writer import inject_mldiagnostics_writer
writer_cfg = inject_mldiagnostics_writer(writer_cfg, cfg.ml_diagnostics)
self._add_child("summary_writer", writer_cfg)
if cfg.output_writer is not None:
self._add_child("output_writer", cfg.output_writer)

def eval_step(
self,
Expand Down Expand Up @@ -691,25 +714,32 @@ def eval_step(
"output was None at the end of a trace, not expected."
)
jax.tree.map(lambda x: x.block_until_ready(), forward_outputs)
jax.profiler.stop_trace()
if self._enable_ml_diagnostics_xprof:
from axlearn.common.managed_mldiagnostics import ManagedMLDiagnostics
ManagedMLDiagnostics().stop_xprof()
else:
jax.profiler.stop_trace()
self.vlog(2, "Stopped profiler tracing for evaler %s.", cfg.name)
stop_trace_iter = None
self._trace_steps.add(step)

if batch_ix in cfg.trace_at_iters and len(self._trace_steps) <= 3:
try:
jax.profiler.start_trace(self.summary_writer.config.dir)
except RuntimeError as e:
if "Only one profile may be run at a time." in str(e):
# https://github.com/google/jax/blob/260f1d8b/jax/_src/profiler.py#L110-L111
# No functionality is currently exposed to check this robustly.
raise RuntimeError(
"Nesting evaler profiling within a higher "
"level profile session is not currently supported. "
) from e
# Else profiler is already running.
finally:
stop_trace_iter = batch_ix + 1 # We only look at one batch.
if self._enable_ml_diagnostics_xprof:
from axlearn.common.managed_mldiagnostics import ManagedMLDiagnostics
ManagedMLDiagnostics().start_xprof()
else:
try:
jax.profiler.start_trace(self.summary_writer.config.dir)
except RuntimeError as e:
if "Only one profile may be run at a time." in str(e):
# https://github.com/google/jax/blob/260f1d8b/jax/_src/profiler.py#L110-L111
# No functionality is currently exposed to check this robustly.
raise RuntimeError(
"Nesting evaler profiling within a higher "
"level profile session is not currently supported. "
) from e
# Else profiler is already running.
stop_trace_iter = batch_ix + 1 # We only look at one batch.
self.vlog(2, "Start profiling evaler %s", cfg.name)

with jax.profiler.StepTraceAnnotation(cfg.name, step_num=step):
Expand Down
57 changes: 57 additions & 0 deletions axlearn/common/evaler_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,63 @@ def fn(*, step, train_summaries) -> bool:
)
self.assertIsNotNone(summaries)

def test_ml_diagnostics_xprof_tracing(self):
from unittest import mock
from axlearn.common.managed_mldiagnostics import ManagedMLDiagnostics, MLDiagnosticsConfig
ManagedMLDiagnostics._instance = None

mesh = jax.sharding.Mesh(mesh_utils.create_device_mesh((1, 1)), ("data", "model"))
with mesh:
# Create model state.
model_cfg = DummyModel.default_config()
model = model_cfg.instantiate(parent=None)
model_param_partition_specs = jax.tree.map(
lambda spec: spec.mesh_axes, model.create_parameter_specs_recursively()
)
model_state = pjit(
model.initialize_parameters_recursively,
in_shardings=(None,),
out_shardings=model_param_partition_specs,
)(jax.random.PRNGKey(0))

ManagedMLDiagnostics._instance = None

mock_xprof_module = mock.MagicMock()
mock_xprof_class = mock_xprof_module.xprof
mock_xprof_inst = mock_xprof_class.return_value

with mock.patch.dict("sys.modules", {"google_cloud_mldiagnostics": mock_xprof_module}), mock.patch.dict(os.environ, {"AXLEARN_JOB_NAME": "test_run"}):
with tempfile.TemporaryDirectory() as temp_dir:
evaler = (
SpmdEvaler.default_config()
.set(
input=DummyInput.default_config().set(
total_num_batches=3, batch_size=4
),
name="spmd_evaler",
summary_writer=SummaryWriter.default_config().set(dir=temp_dir),
metric_calculator=DummyMetricCalculator.default_config(),
ml_diagnostics=MLDiagnosticsConfig(
enable_xprof=True,
region="us-central1",
gcs_path="gs://test/profiles",
),
trace_at_iters=[1],
)
.instantiate(
parent=None,
model=model,
model_param_partition_specs=model_param_partition_specs,
)
)
self.assertTrue(evaler._enable_ml_diagnostics_xprof)

prng_key = jax.device_put(jax.random.PRNGKey(789), jax.NamedSharding(mesh, P()))
evaler.eval_step(1, prng_key=prng_key, model_params=model_state, force_run=True)

mock_xprof_inst.start.assert_called_once()
mock_xprof_inst.stop.assert_called_once()


class ModelSummaryAccumulatorTest(absltest.TestCase):
def test_accumulated_summaries_match(self):
Expand Down
22 changes: 22 additions & 0 deletions axlearn/common/launch_trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from axlearn.common import file_system as fs
from axlearn.common import measurement
from axlearn.common.config import TrainerConfigFn, get_named_trainer_config
from axlearn.common.managed_mldiagnostics import MLDiagnosticsConfig
from axlearn.common.trainer import SpmdTrainer, select_mesh_config
from axlearn.common.utils import MeshShape, get_data_dir, infer_mesh_shape

Expand Down Expand Up @@ -113,6 +114,21 @@
None,
"The mesh selector string. See `SpmdTrainer.Config.mesh_rules` for details.",
)
flags.DEFINE_bool(
"enable_ml_diagnostics_xprof",
False,
"Whether to enable Google Cloud ML Diagnostics automated profiler (xprof) capture.",
)
flags.DEFINE_bool(
"enable_ml_diagnostics_metrics",
False,
"Whether to enable Google Cloud ML Diagnostics metrics collection.",
)
flags.DEFINE_string(
"ml_diagnostics_region",
None,
"Google Cloud region for ML Diagnostics.",
)

FLAGS = flags.FLAGS

Expand Down Expand Up @@ -170,6 +186,12 @@ def get_trainer_config(
)
if trainer_config.log_every_n_steps is None:
trainer_config.log_every_n_steps = flag_values.trainer_log_every_n_steps
trainer_config.ml_diagnostics = MLDiagnosticsConfig(
enable_xprof=flag_values.enable_ml_diagnostics_xprof,
enable_metrics=flag_values.enable_ml_diagnostics_metrics,
region=flag_values.ml_diagnostics_region,
gcs_path=f"{trainer_config.dir}/profiles",
)
for eval_cfg in trainer_config.evalers.values():
eval_cfg.trace_at_iters = [int(el) for el in flag_values.eval_trace_at_iters]
if flag_values.device_monitor == "tpu":
Expand Down
Loading