Skip to content

Commit 3d7eacb

Browse files
committed
feat: Add DAG for validating tpu-info -help command execution in TPU worker pods
1 parent 8f2e872 commit 3d7eacb

1 file changed

Lines changed: 279 additions & 0 deletions

File tree

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
A DAG to validate the `tpu-info -help` command execution inside TPU worker pods.
17+
"""
18+
19+
import datetime
20+
import tempfile
21+
import os
22+
23+
from airflow import models
24+
from airflow.decorators import task
25+
from airflow.utils.trigger_rule import TriggerRule
26+
from airflow.utils.task_group import TaskGroup
27+
28+
from dags import composer_env
29+
from dags.tpu_observability.utils import jobset_util as jobset
30+
from dags.tpu_observability.utils import node_pool_util as node_pool
31+
from dags.tpu_observability.utils import subprocess_util as subprocess
32+
from dags.tpu_observability.utils.jobset_util import JobSet, Workload
33+
from dags.tpu_observability.configs.common import MachineConfigMap, GCS_CONFIG_PATH
34+
35+
36+
def _get_tpu_info_help_output(info: node_pool.Info, pod_name: str) -> str:
37+
"""
38+
Retrieves the raw output of `tpu-info -help` from a specific pod.
39+
"""
40+
with tempfile.NamedTemporaryFile() as temp_config_file:
41+
env = os.environ.copy()
42+
env["KUBECONFIG"] = temp_config_file.name
43+
44+
cmd = " && ".join([
45+
jobset.Command.get_credentials_command(info),
46+
f"kubectl exec {pod_name} -n default -- tpu-info -help",
47+
])
48+
49+
return subprocess.run_exec(cmd, env=env)
50+
51+
52+
@task
53+
def validate_tpu_info_help(info: node_pool.Info, pod_name: str) -> str:
54+
"""
55+
Validates that the `tpu-info -help` output contains all required fields.
56+
"""
57+
output = _get_tpu_info_help_output(info, pod_name)
58+
required_patterns = [
59+
"Display TPU info and metrics.",
60+
"options:",
61+
"-h, --help",
62+
"-v, --version",
63+
"-p, --process",
64+
"--streaming",
65+
"--rate RATE",
66+
"--list_metrics",
67+
]
68+
69+
for pattern in required_patterns:
70+
if pattern not in output:
71+
raise AssertionError(
72+
"Validation failed: Missing expected string "
73+
f"'{pattern}' in tpu-info help.\n"
74+
f"Output received:\n{output}"
75+
)
76+
77+
return output
78+
79+
80+
def _get_tpu_version_output(info: node_pool.Info, pod_name: str) -> str:
81+
"""
82+
Executes the version command in the pod to get libtpu version and accelerator type.
83+
"""
84+
with tempfile.NamedTemporaryFile() as temp_config_file:
85+
env = os.environ.copy()
86+
env["KUBECONFIG"] = temp_config_file.name
87+
cmd = " && ".join([
88+
jobset.Command.get_credentials_command(info),
89+
f"kubectl exec {pod_name} -n default -- tpu-info --version",
90+
])
91+
return subprocess.run_exec(cmd, env=env)
92+
93+
94+
@task
95+
def validate_tpu_info_version(info: node_pool.Info, pod_name: str) -> str:
96+
"""
97+
Validates that `tpu-info --version` returns version and accelerator info.
98+
"""
99+
output = _get_tpu_version_output(info, pod_name)
100+
required_patterns = [
101+
"tpu-info version:",
102+
"libtpu version:",
103+
"accelerator type:",
104+
]
105+
106+
for pattern in required_patterns:
107+
if pattern not in output:
108+
raise AssertionError(
109+
f"Validation failed: Missing expected string '{pattern}' "
110+
f"in tpu-info --version output.\nOutput:\n{output}"
111+
)
112+
113+
return output
114+
115+
116+
def _get_tpu_process_output(info: node_pool.Info, pod_name: str) -> str:
117+
"""
118+
Executes the process command in the pod to get the TPU process table.
119+
"""
120+
with tempfile.NamedTemporaryFile() as temp_config_file:
121+
env = os.environ.copy()
122+
env["KUBECONFIG"] = temp_config_file.name
123+
cmd = " && ".join([
124+
jobset.append_credentials_command(info),
125+
f"kubectl exec {pod_name} -n default -- tpu-info --process",
126+
])
127+
return subprocess.run_exec(cmd, env=env)
128+
129+
130+
@task
131+
def validate_tpu_info_process(info: node_pool.Info, pod_name: str) -> str:
132+
"""
133+
Validates that `tpu-info --process` displays the TPU process table.
134+
"""
135+
output = _get_tpu_process_output(info, pod_name)
136+
required_patterns = [
137+
"TPU Process Info",
138+
"Chip",
139+
"PID",
140+
"Process Name",
141+
"/dev/vfio/",
142+
"python",
143+
]
144+
145+
for pattern in required_patterns:
146+
if pattern not in output:
147+
raise AssertionError(
148+
f"Validation failed: Missing expected string '{pattern}' "
149+
f"in tpu-info --process output.\nOutput:\n{output}"
150+
)
151+
152+
return output
153+
154+
155+
# Keyword arguments are generated dynamically at runtime (pylint does not
156+
# know this signature).
157+
with models.DAG( # pylint: disable=unexpected-keyword-arg
158+
dag_id="tpu_info_help_validation_dags",
159+
start_date=datetime.datetime(2025, 8, 10),
160+
schedule="0 18 * * *" if composer_env.is_prod_env() else None,
161+
catchup=False,
162+
tags=[
163+
"cloud-ml-auto-solutions",
164+
"jobset",
165+
"time-to-recover",
166+
"tpu-observability",
167+
"TPU",
168+
"v6e-16",
169+
],
170+
description=(
171+
"This DAG validates the `tpu-info -help` command "
172+
"execution inside TPU worker pods "
173+
),
174+
doc_md="""
175+
### Description
176+
This DAG validates the `tpu-info -help` command execution inside TPU worker pods
177+
across different TPU versions and machine configurations.
178+
It ensures that the command runs successfully and returns the expected help information.
179+
""",
180+
) as dag:
181+
for machine in MachineConfigMap:
182+
config = machine.value
183+
184+
jobset_config = JobSet(
185+
jobset_name="tpu-info-help-validation-jobset",
186+
namespace="default",
187+
max_restarts=5,
188+
replicated_job_name="tpu-job-slice",
189+
replicas=1,
190+
backoff_limit=0,
191+
completions=4,
192+
parallelism=4,
193+
tpu_accelerator_type="tpu-v6e-slice",
194+
tpu_topology="4x4",
195+
container_name="jax-tpu-worker",
196+
image="asia-northeast1-docker.pkg.dev/cienet-cmcs/"
197+
"yuna-docker/tpu-info:v0.5.1",
198+
tpu_cores_per_pod=4,
199+
)
200+
201+
# Keyword arguments are generated dynamically at runtime (pylint does not
202+
# know this signature).
203+
with TaskGroup( # pylint: disable=unexpected-keyword-arg
204+
group_id=f"v{config.tpu_version.value}"
205+
):
206+
cluster_info = node_pool.build_node_pool_info_from_gcs_yaml.override(
207+
task_id="build_node_pool_info_from_gcs_yaml"
208+
)(
209+
gcs_path=GCS_CONFIG_PATH,
210+
dag_name="tpu_info_help_validation_dags",
211+
is_prod=composer_env.is_prod_env(),
212+
machine_type=config.machine_version.value,
213+
tpu_topology=config.tpu_topology,
214+
)
215+
216+
create_node_pool = node_pool.create(
217+
node_pool=cluster_info,
218+
)
219+
220+
apply_time = jobset.run_workload(
221+
node_pool=cluster_info,
222+
yaml_config=jobset_config.generate_yaml(
223+
workload_script=Workload.JAX_TPU_BENCHMARK
224+
),
225+
namespace=jobset_config.namespace,
226+
)
227+
228+
pod_names = jobset.list_pod_names.override(task_id="list_pod_names")(
229+
node_pool=cluster_info,
230+
namespace=jobset_config.namespace,
231+
)
232+
233+
wait_for_job_start = jobset.wait_for_jobset_started.override(
234+
task_id="wait_for_job_start", timeout=1200
235+
)(cluster_info, pod_name_list=pod_names, job_apply_time=apply_time)
236+
237+
validate_help = validate_tpu_info_help.partial(info=cluster_info).expand(
238+
pod_name=pod_names
239+
)
240+
241+
validate_version = validate_tpu_info_version.partial(
242+
info=cluster_info
243+
).expand(pod_name=pod_names)
244+
245+
validate_process = validate_tpu_info_process.partial(
246+
info=cluster_info
247+
).expand(pod_name=pod_names)
248+
249+
cleanup_workload = jobset.end_workload.override(
250+
task_id="cleanup_workload", trigger_rule=TriggerRule.ALL_DONE
251+
)(
252+
node_pool=cluster_info,
253+
jobset_name=jobset_config.jobset_name,
254+
namespace=jobset_config.namespace,
255+
).as_teardown(
256+
setups=apply_time
257+
)
258+
259+
cleanup_node_pool = node_pool.delete.override(
260+
task_id="cleanup_node_pool", trigger_rule=TriggerRule.ALL_DONE
261+
)(node_pool=cluster_info).as_teardown(
262+
setups=create_node_pool,
263+
)
264+
265+
# Airflow uses >> for task chaining, which is pointless for pylint.
266+
# pylint: disable=pointless-statement
267+
(
268+
cluster_info
269+
>> create_node_pool
270+
>> apply_time
271+
>> pod_names
272+
>> wait_for_job_start
273+
>> validate_help
274+
>> validate_process
275+
>> validate_version
276+
>> cleanup_workload
277+
>> cleanup_node_pool
278+
)
279+
# pylint: enable=pointless-statement

0 commit comments

Comments
 (0)