Skip to content

Commit 9f9b9b3

Browse files
committed
feat: Add validation DAG for tpu-info CLI tool functionality in TPU worker pods
1 parent 170e978 commit 9f9b9b3

1 file changed

Lines changed: 215 additions & 2 deletions

File tree

dags/tpu_observability/tpu_info_format_validation_dags.py

Lines changed: 215 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,21 @@
1313
# limitations under the License.
1414

1515
"""
16+
tpu_info_format_validation_dag:
1617
A DAG orchestrates the process of verifying TensorCore utilization metrics.
17-
1818
This is done by comparing data from Cloud Logging and Cloud Monitoring.
19+
20+
tpu_info_cli_validation_dags:
21+
A DAG to validate the `tpu-info` CLI tool, ensuring help documentation,
22+
version metadata, and process monitoring are functional inside TPU worker pods.
1923
"""
2024

2125
import datetime
2226
import os
2327
import re
2428
import subprocess
2529
import tempfile
26-
from dataclasses import replace
30+
from typing import List
2731

2832
from airflow import models
2933
from airflow.decorators import task
@@ -286,6 +290,70 @@ def validate_latency_table(tpu_info_output: list[tpu_info.Table]):
286290
)
287291

288292

293+
def execute_tpu_info_cli_command(info, pod_name: str, tpu_args: str) -> str:
294+
"""Helper to handle KUBECONFIG and execute kubectl."""
295+
with tempfile.NamedTemporaryFile() as temp_config_file:
296+
env = os.environ.copy()
297+
env["KUBECONFIG"] = temp_config_file.name
298+
299+
cmd = " && ".join([
300+
jobset.Command.get_credentials_command(info),
301+
f"kubectl exec {pod_name} -n default -- {tpu_args}",
302+
])
303+
return subprocess.run_exec(cmd, env=env)
304+
305+
306+
def verify_output_contains_patterns(
307+
output: str, patterns: List[str], context: str
308+
):
309+
"""Helper to verify expected strings in output."""
310+
for pattern in patterns:
311+
if pattern not in output:
312+
raise AssertionError(
313+
f"Validation failed for '{context}': Missing '{pattern}'."
314+
)
315+
316+
317+
@task
318+
def validate_help(info, pod_name: str) -> str:
319+
output = execute_tpu_info_cli_command(info, pod_name, "tpu-info -help")
320+
patterns = [
321+
"Display TPU info and metrics.",
322+
"options:",
323+
"-h, --help",
324+
"-v, --version",
325+
"-p, --process",
326+
"--streaming",
327+
"--rate RATE",
328+
"--list_metrics",
329+
]
330+
verify_output_contains_patterns(output, patterns, "tpu-info -help")
331+
return output
332+
333+
334+
@task
335+
def validate_version(info, pod_name: str) -> str:
336+
output = execute_tpu_info_cli_command(info, pod_name, "tpu-info --version")
337+
patterns = ["tpu-info version:", "libtpu version:", "accelerator type:"]
338+
verify_output_contains_patterns(output, patterns, "tpu-info --version")
339+
return output
340+
341+
342+
@task
343+
def validate_process(info, pod_name: str) -> str:
344+
output = execute_tpu_info_cli_command(info, pod_name, "tpu-info --process")
345+
patterns = [
346+
"TPU Process Info",
347+
"Chip",
348+
"PID",
349+
"Process Name",
350+
"/dev/vfio/",
351+
"python",
352+
]
353+
verify_output_contains_patterns(output, patterns, "tpu-info --process")
354+
return output
355+
356+
289357
# Keyword arguments are generated dynamically at runtime (pylint does not
290358
# know this signature).
291359
with models.DAG( # pylint: disable=unexpected-keyword-arg
@@ -525,3 +593,148 @@ def generate_second_node_pool_name(
525593
clean_up_workload,
526594
cleanup_node_pool,
527595
)
596+
# pylint: enable=pointless-statement
597+
598+
599+
# Keyword arguments are generated dynamically at runtime (pylint does not
600+
# know this signature).
601+
with models.DAG( # pylint: disable=unexpected-keyword-arg
602+
dag_id="tpu_info_cli_validation_dags",
603+
start_date=datetime.datetime(2025, 8, 10),
604+
schedule=None,
605+
catchup=False,
606+
tags=[
607+
"cloud-ml-auto-solutions",
608+
"jobset",
609+
"time-to-recover",
610+
"tpu-observability",
611+
"TPU",
612+
"v6e-16",
613+
],
614+
description=(
615+
"Validates tpu-info CLI tool: help documentation, version metadata, "
616+
"and process monitoring capabilities inside TPU worker pods."
617+
),
618+
doc_md="""
619+
### Description
620+
This DAG performs an end-to-end validation of the `tpu-info` observability tool
621+
within TPU worker pods. It ensures the CLI tool is correctly installed and
622+
functional across different TPU configurations.
623+
624+
### Validation Steps:
625+
1. **Help Menu Validation**: Verifies `tpu-info -help` displays all required
626+
options (streaming, rate, etc.) and specific usage instructions.
627+
2. **Process Table Validation**: Confirms `tpu-info --process` can successfully
628+
map PIDs to TPU chips.
629+
3. **Version Validation**: Ensures `tpu-info --version` correctly reports
630+
the tool version, libtpu version, and accelerator type.
631+
""",
632+
) as dag:
633+
for machine in MachineConfigMap:
634+
config = machine.value
635+
636+
jobset_config = JobSet(
637+
jobset_name="tpu-info-cli-validation-jobset",
638+
namespace="default",
639+
max_restarts=5,
640+
replicated_job_name="tpu-job-slice",
641+
replicas=1,
642+
backoff_limit=0,
643+
completions=4,
644+
parallelism=4,
645+
tpu_accelerator_type="tpu-v6e-slice",
646+
tpu_topology="4x4",
647+
container_name="jax-tpu-worker",
648+
image="asia-northeast1-docker.pkg.dev/cienet-cmcs/"
649+
"yuna-docker/tpu-info:v0.5.1",
650+
tpu_cores_per_pod=4,
651+
)
652+
653+
# Keyword arguments are generated dynamically at runtime (pylint does not
654+
# know this signature).
655+
with TaskGroup( # pylint: disable=unexpected-keyword-arg
656+
group_id=f"v{config.tpu_version.value}"
657+
):
658+
cluster_info = node_pool.build_node_pool_info_from_gcs_yaml.override(
659+
task_id="build_node_pool_info_from_gcs_yaml"
660+
)(
661+
gcs_path=GCS_CONFIG_PATH,
662+
dag_name="tpu_info_cli_validation_dags",
663+
is_prod=composer_env.is_prod_env(),
664+
machine_type=config.machine_version.value,
665+
tpu_topology=config.tpu_topology,
666+
)
667+
668+
create_node_pool = node_pool.create.override(task_id="create_node_pool")(
669+
node_pool=cluster_info,
670+
)
671+
672+
apply_time = jobset.run_workload.override(task_id="run_workload")(
673+
node_pool=cluster_info,
674+
yaml_config=jobset_config.generate_yaml(
675+
workload_script=Workload.JAX_TPU_BENCHMARK
676+
),
677+
namespace=jobset_config.namespace,
678+
)
679+
680+
pod_names = jobset.list_pod_names.override(task_id="list_pod_names")(
681+
node_pool=cluster_info,
682+
namespace=jobset_config.namespace,
683+
)
684+
685+
wait_for_job_start = jobset.wait_for_jobset_started.override(
686+
task_id="wait_for_job_start"
687+
)(cluster_info, pod_name_list=pod_names, job_apply_time=apply_time)
688+
689+
# Keyword arguments are generated dynamically at runtime (pylint does not
690+
# know this signature).
691+
with TaskGroup( # pylint: disable=unexpected-keyword-arg
692+
group_id="verification_group"
693+
) as verification_group:
694+
help_validation = (
695+
validate_help.override(task_id="validate_help")
696+
.partial(info=cluster_info)
697+
.expand(pod_name=pod_names)
698+
)
699+
700+
version_validation = (
701+
validate_version.override(task_id="validate_version")
702+
.partial(info=cluster_info)
703+
.expand(pod_name=pod_names)
704+
)
705+
706+
process_validation = (
707+
validate_process.override(task_id="validate_process")
708+
.partial(info=cluster_info)
709+
.expand(pod_name=pod_names)
710+
)
711+
712+
cleanup_workload = jobset.end_workload.override(
713+
task_id="cleanup_workload", trigger_rule=TriggerRule.ALL_DONE
714+
)(
715+
node_pool=cluster_info,
716+
jobset_name=jobset_config.jobset_name,
717+
namespace=jobset_config.namespace,
718+
).as_teardown(
719+
setups=apply_time
720+
)
721+
722+
cleanup_node_pool = node_pool.delete.override(
723+
task_id="cleanup_node_pool", trigger_rule=TriggerRule.ALL_DONE
724+
)(node_pool=cluster_info).as_teardown(
725+
setups=create_node_pool,
726+
)
727+
728+
# Airflow uses >> for task chaining, which is pointless for pylint.
729+
# pylint: disable=pointless-statement
730+
(
731+
cluster_info
732+
>> create_node_pool
733+
>> apply_time
734+
>> pod_names
735+
>> wait_for_job_start
736+
>> verification_group
737+
>> cleanup_workload
738+
>> cleanup_node_pool
739+
)
740+
# pylint: enable=pointless-statement

0 commit comments

Comments
 (0)