|
27 | 27 |
|
28 | 28 | from airflow.decorators import task |
29 | 29 | from airflow.exceptions import AirflowFailException |
| 30 | +from airflow.operators.python import get_current_context |
| 31 | +from google.cloud.monitoring_v3 import types |
| 32 | +import kubernetes |
30 | 33 |
|
31 | 34 | from dags.tpu_observability.utils import subprocess_util as subprocess |
32 | 35 | from dags.tpu_observability.utils.gcp_util import query_time_series |
@@ -708,79 +711,141 @@ def list_instance_ids_by_pod_names( |
708 | 711 | return instance_ids |
709 | 712 |
|
710 | 713 |
|
711 | | -@task.sensor(poke_interval=60, timeout=3600, mode="poke") |
712 | | -def wait_for_tpu_active_chips( |
713 | | - node_pool: node_pool_info, |
714 | | - instance_id: str, |
715 | | - job_apply_time: TimeUtil, |
716 | | -) -> bool: |
| 714 | +@task |
| 715 | +def get_pod_creation_timestamps( |
| 716 | + node_pool: node_pool_info, namespace: str, jobset_name: str |
| 717 | +) -> dict[str, str]: |
717 | 718 | """ |
718 | | - Polls the TPU active_chips metric for a specific GCE instance. |
| 719 | + Retrieves and returns the creation timestamps for all pods in a JobSet. |
719 | 720 |
|
720 | | - This sensor waits for the monitoring system to report the number of active |
721 | | - TPU chips. Note that there is often a few minutes of latency between |
722 | | - the TPU VM starting and the metric appearing in Cloud Monitoring. |
| 721 | + This is useful for measuring provisioning latency (the time between |
| 722 | + applying the YAML and the Kubernetes API creating the Pod objects). |
723 | 723 |
|
724 | 724 | Args: |
725 | | - node_pool: Configuration object with project and cluster details. |
726 | | - instance_id: The specific GCE Instance ID to monitor. |
727 | | - job_apply_time: The time when the job was applied. |
| 725 | + node_pool: Configuration object with cluster details. |
| 726 | + namespace: The Kubernetes namespace to query. |
| 727 | + jobset_name: The name of the JobSet to filter pods. |
728 | 728 |
|
729 | 729 | Returns: |
730 | | - True if the active_chips metric is found, False otherwise. |
| 730 | + A dictionary mapping pod_name to its creationTimestamp (ISO 8601 string). |
731 | 731 | """ |
732 | | - now = datetime.datetime.now() |
| 732 | + with tempfile.NamedTemporaryFile() as temp_config_file: |
| 733 | + env = os.environ.copy() |
| 734 | + env["KUBECONFIG"] = temp_config_file.name |
| 735 | + |
| 736 | + # Query returns: pod_name <tab> creationTimestamp |
| 737 | + get_timestamp_cmd = ( |
| 738 | + f"kubectl get pods -n {namespace} " |
| 739 | + f"--kubeconfig={temp_config_file.name} " |
| 740 | + f"-l jobset.sigs.k8s.io/jobset-name={jobset_name} " |
| 741 | + '-o jsonpath=\'{range .items[*]}{.metadata.name}{"\\t"}{.metadata.creationTimestamp}{"\\n"}{end}\'' |
| 742 | + ) |
| 743 | + |
| 744 | + cmd = " && ".join([ |
| 745 | + Command.get_credentials_command(node_pool), |
| 746 | + get_timestamp_cmd, |
| 747 | + ]) |
| 748 | + |
| 749 | + stdout = subprocess.run_exec(cmd, env=env) |
733 | 750 |
|
| 751 | + if not stdout or not stdout.strip(): |
| 752 | + logging.info(f"No pods found for JobSet {jobset_name} yet.") |
| 753 | + return {} |
| 754 | + |
| 755 | + # Parse output into a dictionary { "pod_name": "timestamp" } |
| 756 | + timestamps = {} |
| 757 | + for line in stdout.strip().split("\n"): |
| 758 | + if "\t" in line: |
| 759 | + name, ts = line.split("\t") |
| 760 | + timestamps[name.strip()] = ts.strip() |
| 761 | + |
| 762 | + logging.info(f"Retrieved Pod Creation Timestamps: {timestamps}") |
| 763 | + return timestamps |
| 764 | + |
| 765 | + |
| 766 | +@task.sensor(poke_interval=60, timeout=3600, mode="poke") |
| 767 | +def wait_for_tpu_scheduled_chips( |
| 768 | + node_pool: node_pool_info, |
| 769 | + instance_id: str, |
| 770 | + job_apply_time: TimeUtil, |
| 771 | +) -> bool: |
| 772 | + now = datetime.datetime.now() |
734 | 773 | time_series = query_time_series( |
735 | 774 | project_id=node_pool.project_id, |
736 | 775 | filter_str=( |
737 | | - 'metric.type="compute.googleapis.com/instance/tpu/active_chips" ' |
| 776 | + 'metric.type="compute.googleapis.com/instance/tpu/scheduled_chips" ' |
738 | 777 | f'resource.labels.instance_id="{instance_id}"' |
739 | 778 | ), |
740 | 779 | start_time=job_apply_time, |
741 | 780 | end_time=TimeUtil.from_datetime(now), |
742 | 781 | ) |
743 | | - logging.info( |
744 | | - "TPU Active Chips Time series for %s: %s", instance_id, time_series |
745 | | - ) |
746 | 782 |
|
747 | | - return len(time_series) > 0 |
| 783 | + if len(time_series) > 0: |
| 784 | + # Capture the timestamp of the first metric point |
| 785 | + first_ts = str(time_series[0].points[0].interval.start_time) |
| 786 | + # Push to XCom so the latency task can read it |
| 787 | + ti = get_current_context()["ti"] |
| 788 | + ti.xcom_push(key="scheduled_ts", value=first_ts) |
| 789 | + return True |
| 790 | + return False |
| 791 | + |
| 792 | + |
| 793 | +@task |
| 794 | +def calculate_latency( |
| 795 | + instance_id: str, pod_timestamps: dict, scheduled_ts: str |
| 796 | +): |
| 797 | + """Calculates time delta between Pod Creation and TPU Scheduling.""" |
| 798 | + from dateutil import parser |
| 799 | + |
| 800 | + t_scheduled = parser.isoparse(scheduled_ts) |
| 801 | + # Match the first available pod timestamp from the JobSet |
| 802 | + first_pod_ts = list(pod_timestamps.values())[0] |
| 803 | + t_pod = parser.isoparse(first_pod_ts) |
| 804 | + |
| 805 | + diff_seconds = (t_scheduled - t_pod).total_seconds() |
| 806 | + logging.info(f"Instance {instance_id} Latency: {diff_seconds}s") |
| 807 | + |
| 808 | + return { |
| 809 | + "instance_id": instance_id, |
| 810 | + "latency_seconds": diff_seconds, |
| 811 | + "latency_minutes": round(diff_seconds / 60, 2), |
| 812 | + } |
748 | 813 |
|
749 | 814 |
|
750 | 815 | @task.sensor(poke_interval=60, timeout=3600, mode="poke") |
751 | | -def wait_for_tpu_scheduled_chips( |
| 816 | +def wait_for_tpu_active_chips( |
752 | 817 | node_pool: node_pool_info, |
753 | 818 | instance_id: str, |
754 | 819 | job_apply_time: TimeUtil, |
755 | 820 | ) -> bool: |
756 | 821 | """ |
757 | | - Polls the TPU scheduled_chips metric for a specific GCE instance. |
| 822 | + Polls the TPU active_chips metric for a specific GCE instance. |
758 | 823 |
|
759 | | - This metric indicates the number of TPU chips scheduled by the system for |
760 | | - a given instance. It is often the first indicator that a TPU resource |
761 | | - has been successfully allocated to a VM. |
| 824 | + This sensor waits for the monitoring system to report the number of active |
| 825 | + TPU chips. Note that there is often a few minutes of latency between |
| 826 | + the TPU VM starting and the metric appearing in Cloud Monitoring. |
762 | 827 |
|
763 | 828 | Args: |
764 | 829 | node_pool: Configuration object with project and cluster details. |
765 | 830 | instance_id: The specific GCE Instance ID to monitor. |
766 | 831 | job_apply_time: The time when the job was applied. |
767 | 832 |
|
768 | 833 | Returns: |
769 | | - True if the scheduled_chips metric is found, False otherwise. |
| 834 | + True if the active_chips metric is found, False otherwise. |
770 | 835 | """ |
771 | 836 | now = datetime.datetime.now() |
772 | 837 |
|
773 | 838 | time_series = query_time_series( |
774 | 839 | project_id=node_pool.project_id, |
775 | 840 | filter_str=( |
776 | | - 'metric.type="compute.googleapis.com/instance/tpu/scheduled_chips" ' |
| 841 | + 'metric.type="compute.googleapis.com/instance/tpu/active_chips" ' |
777 | 842 | f'resource.labels.instance_id="{instance_id}"' |
778 | 843 | ), |
779 | 844 | start_time=job_apply_time, |
780 | 845 | end_time=TimeUtil.from_datetime(now), |
781 | 846 | ) |
782 | 847 | logging.info( |
783 | | - "TPU Scheduled Chips Time series for %s: %s", instance_id, time_series |
| 848 | + "TPU Active Chips Time series for %s: %s", instance_id, time_series |
784 | 849 | ) |
785 | 850 |
|
786 | 851 | return len(time_series) > 0 |
|
0 commit comments