Skip to content

Commit 59db823

Browse files
natarenchanglan
authored andcommitted
Add command rewrite support for LWS based jobs
GitOrigin-RevId: b2d4da4
1 parent 80a17f2 commit 59db823

4 files changed

Lines changed: 69 additions & 10 deletions

File tree

axlearn/cloud/gcp/jobset_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@ def define_flags(cls, fv):
190190

191191
def __init__(self, cfg: Config, *, bundler: Bundler):
192192
super().__init__(cfg)
193+
self._user_command_patcher = (
194+
cfg.user_command_patcher.instantiate() if cfg.user_command_patcher else None
195+
)
193196
self._bundler = bundler
194197

195198
def __call__(self) -> Sequence[Nested[Any]]:
@@ -446,9 +449,6 @@ def from_flags(cls, fv: flags.FlagValues, **kwargs) -> Config:
446449

447450
def __init__(self, cfg: Config, *, bundler: Bundler):
448451
super().__init__(cfg, bundler=bundler)
449-
self._user_command_patcher = (
450-
cfg.user_command_patcher.instantiate() if cfg.user_command_patcher else None
451-
)
452452
cfg: TPUJobBuilder.Config = self.config
453453
if cfg.output_dir is None:
454454
raise ValueError("cfg.output_dir is required.")

axlearn/cloud/gcp/lws_utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from absl import flags
88

99
from axlearn.cloud.common.bundler import Bundler
10+
from axlearn.cloud.common.user_command_patcher import UserCommandPatcher
1011
from axlearn.cloud.common.utils import FlagConfigurable
1112
from axlearn.cloud.gcp.config import gcp_settings
1213
from axlearn.cloud.gcp.jobset_utils import TPUJobBuilder
@@ -40,6 +41,7 @@ class Config(FlagConfigurable.Config):
4041
service_account: Optional[str] = None
4142
output_dir: Optional[str] = None
4243
image_id: Optional[str] = None
44+
user_command_patcher: Optional[UserCommandPatcher.Config] = None
4345

4446
@classmethod
4547
def define_flags(cls, fv):
@@ -79,6 +81,9 @@ def from_flags(cls, fv: flags.FlagValues, **kwargs):
7981
def __init__(self, cfg: Config, *, bundler: Bundler):
8082
super().__init__(cfg)
8183
self._bundler = bundler
84+
self._user_command_patcher = (
85+
cfg.user_command_patcher.instantiate() if cfg.user_command_patcher else None
86+
)
8287

8388
def __call__(self) -> Sequence[Nested[Any]]:
8489
"""Builds LeaderWorkerTemplate for the LWS API.

axlearn/cloud/gcp/pathways_utils.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@
33
"""Utilities for building Pathways Jobset specs."""
44

55
import copy
6+
import io
67
import logging
78
import os
89
from typing import Any, Optional, Sequence, Union
910

1011
from absl import flags
1112

12-
from axlearn.cloud.common.bastion import BASTION_JOB_VERSION_ENV_VAR
13+
from axlearn.cloud.common.bastion import (
14+
_BASTION_SERIALIZED_JOBSPEC_ENV_VAR,
15+
BASTION_JOB_VERSION_ENV_VAR,
16+
ValidationError,
17+
deserialize_jobspec,
18+
)
1319
from axlearn.cloud.common.bundler import Bundler
1420
from axlearn.cloud.common.utils import parse_kv_flags
1521
from axlearn.cloud.gcp.jobset_utils import (
@@ -985,6 +991,8 @@ def __init__(self, cfg: Config, *, bundler):
985991

986992
self._bundler = bundler
987993
self._inner: TPULeaderWorkerTemplate = cfg.inner.instantiate(bundler=self._bundler)
994+
self._inner._user_command_patcher = self._user_command_patcher
995+
988996
self._tpu_type = infer_tpu_type(cfg.inner.accelerator.instance_type)
989997
if self._tpu_type not in USER_FACING_NAME_TO_SYSTEM_CHARACTERISTICS:
990998
raise NotImplementedError(f"Missing system characteristics for {self._tpu_type}")
@@ -1102,10 +1110,23 @@ def _build_head_container(self) -> dict:
11021110
"requests": {"cpu": cpu_req, "memory": mem_req},
11031111
"limits": {"cpu": cpu_req, "memory": mem_req},
11041112
}
1113+
command = inner_cfg.command
1114+
1115+
if self._user_command_patcher is not None:
1116+
user_id = None
1117+
job_spec = os.environ.get(_BASTION_SERIALIZED_JOBSPEC_ENV_VAR)
1118+
if job_spec:
1119+
try:
1120+
spec = deserialize_jobspec(io.StringIO(job_spec))
1121+
user_id = spec.metadata.user_id
1122+
except ValidationError:
1123+
logging.debug("Failed to deserialize job spec for user command patching.")
1124+
command = self._user_command_patcher.patch(command, user_id=user_id)
1125+
11051126
container = dict(
11061127
name=inner_cfg.name,
11071128
image=inner_cfg.image_id or self._bundler.id(inner_cfg.name),
1108-
command=["bash", "-c", inner_cfg.command],
1129+
command=["bash", "-c", command],
11091130
env=[
11101131
{
11111132
"name": "XCLOUD_ENVIRONMENT",

axlearn/cloud/gcp/pathways_utils_test.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ def test_v6e_default_options_split_megascale_and_xla(self):
5757
self.assertEqual(len(megascale_options) + len(xla_options), len(default_options))
5858

5959

60+
class MockUserCommandPatcher(jobset_utils.UserCommandPatcher):
61+
def patch(self, command: str, **kwargs: dict) -> str:
62+
return f"./prefix_command.sh; {command}"
63+
64+
6065
class PathwaysReplicatedJobTest(TestCase):
6166
"""Tests PathwaysReplicatedJob."""
6267

@@ -408,18 +413,23 @@ def _job_config(self, bundler_cls: type[Bundler], num_replicas: int, **kwargs):
408413
bundler_cfg = bundler_cls.from_spec([], fv=fv).set(image="test-image")
409414
yield cfg, bundler_cfg
410415

411-
@parameterized.parameters([1, 2])
412-
def test_replicated_job(self, num_replicas):
416+
@parameterized.product(
417+
num_replicas=[1, 2], user_command_patcher=[None, MockUserCommandPatcher.default_config()]
418+
)
419+
def test_replicated_job(self, num_replicas, user_command_patcher):
413420
with (self._job_config(CloudBuildBundler, num_replicas) as (cfg, bundler_cfg),):
421+
command = "test_command"
414422
cfg.inner.set(
415423
project="test-project",
416424
name="test",
417-
command="test_command",
425+
command=command,
418426
output_dir="FAKE",
419-
).instantiate(bundler=bundler_cfg.instantiate())
427+
)
428+
if user_command_patcher:
429+
cfg.inner.set(user_command_patcher=user_command_patcher)
430+
cfg.instantiate(bundler=bundler_cfg.instantiate())
420431

421432
builder = cfg.instantiate(bundler=bundler_cfg.instantiate())
422-
423433
replicated_jobs = builder()
424434

425435
self.assertEqual(len(replicated_jobs), num_replicas + 1)
@@ -449,6 +459,29 @@ def test_replicated_job(self, num_replicas):
449459
elif replicated_job_name.startswith("pwwk"):
450460
self.assertEqual(replicated_job["replicas"], 1)
451461

462+
command_was_patched = False
463+
464+
if replicated_job_name.startswith("pwhd"):
465+
containers = (
466+
job_spec.get("spec", {})
467+
.get("template", {})
468+
.get("spec", {})
469+
.get("containers", [])
470+
)
471+
if containers and len(containers) > 0:
472+
command_parts = containers[0].get("command", [])
473+
if command_parts and len(command_parts) > 0:
474+
the_command = command_parts[-1]
475+
if user_command_patcher:
476+
self.assertEqual(f"./prefix_command.sh; {command}", the_command)
477+
command_was_patched = True
478+
else:
479+
self.assertEqual(command, the_command)
480+
self.assertEqual(
481+
user_command_patcher is not None and replicated_job_name.startswith("pwhd"),
482+
command_was_patched,
483+
)
484+
452485
def test_validate_head_name(self):
453486
with self._job_config(CloudBuildBundler, 2) as (cfg, bundler_cfg):
454487
cfg.inner.set(

0 commit comments

Comments
 (0)