|
13 | 13 | # limitations under the License. |
14 | 14 |
|
15 | 15 | """ |
| 16 | +tpu_info_format_validation_dag: |
16 | 17 | A DAG orchestrates the process of verifying TensorCore utilization metrics. |
17 | | -
|
18 | 18 | 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. |
19 | 23 | """ |
20 | 24 |
|
21 | 25 | import datetime |
22 | 26 | import os |
23 | 27 | import re |
24 | 28 | import subprocess |
25 | 29 | import tempfile |
26 | | -from dataclasses import replace |
| 30 | +from typing import List |
27 | 31 |
|
28 | 32 | from airflow import models |
29 | 33 | from airflow.decorators import task |
@@ -286,6 +290,70 @@ def validate_latency_table(tpu_info_output: list[tpu_info.Table]): |
286 | 290 | ) |
287 | 291 |
|
288 | 292 |
|
| 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 | + |
289 | 357 | # Keyword arguments are generated dynamically at runtime (pylint does not |
290 | 358 | # know this signature). |
291 | 359 | with models.DAG( # pylint: disable=unexpected-keyword-arg |
@@ -525,3 +593,148 @@ def generate_second_node_pool_name( |
525 | 593 | clean_up_workload, |
526 | 594 | cleanup_node_pool, |
527 | 595 | ) |
| 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