Skip to content

Commit f735d6d

Browse files
committed
feat: Add pod creation timestamp retrieval and latency calculation to TPU chips validation DAG
1 parent 497e273 commit f735d6d

2 files changed

Lines changed: 108 additions & 28 deletions

File tree

dags/tpu_observability/chips_capacity_validation_dag.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@
129129
jobset_name=jobset_config.jobset_name,
130130
)
131131

132+
pod_ts = jobset.get_pod_creation_timestamps.override(
133+
task_id="get_pod_creation_timestamps"
134+
)(
135+
node_pool=cluster_info,
136+
namespace=jobset_config.namespace,
137+
jobset_name=jobset_config.jobset_name,
138+
)
139+
132140
# Keyword arguments are generated dynamically at runtime (pylint does not
133141
# know this signature).
134142
with TaskGroup( # pylint: disable=unexpected-keyword-arg
@@ -144,6 +152,12 @@
144152
)
145153
)
146154

155+
latency_report = jobset.calculate_latency.expand(
156+
instance_id=instance_ids,
157+
pod_timestamps=pod_ts,
158+
scheduled_ts=scheduled_chips.output,
159+
)
160+
147161
tpu_active_chips = (
148162
jobset.wait_for_tpu_active_chips.override(
149163
task_id="wait_for_tpu_active_chips"
@@ -198,6 +212,7 @@
198212
>> apply_time
199213
>> pod_names
200214
>> wait_for_job_start
215+
>> pod_ts
201216
>> instance_ids
202217
>> verification_group
203218
>> clean_up_workload

dags/tpu_observability/utils/jobset_util.py

Lines changed: 93 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727

2828
from airflow.decorators import task
2929
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
3033

3134
from dags.tpu_observability.utils import subprocess_util as subprocess
3235
from dags.tpu_observability.utils.gcp_util import query_time_series
@@ -708,79 +711,141 @@ def list_instance_ids_by_pod_names(
708711
return instance_ids
709712

710713

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]:
717718
"""
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.
719720
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).
723723
724724
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.
728728
729729
Returns:
730-
True if the active_chips metric is found, False otherwise.
730+
A dictionary mapping pod_name to its creationTimestamp (ISO 8601 string).
731731
"""
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)
733750

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()
734773
time_series = query_time_series(
735774
project_id=node_pool.project_id,
736775
filter_str=(
737-
'metric.type="compute.googleapis.com/instance/tpu/active_chips" '
776+
'metric.type="compute.googleapis.com/instance/tpu/scheduled_chips" '
738777
f'resource.labels.instance_id="{instance_id}"'
739778
),
740779
start_time=job_apply_time,
741780
end_time=TimeUtil.from_datetime(now),
742781
)
743-
logging.info(
744-
"TPU Active Chips Time series for %s: %s", instance_id, time_series
745-
)
746782

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+
}
748813

749814

750815
@task.sensor(poke_interval=60, timeout=3600, mode="poke")
751-
def wait_for_tpu_scheduled_chips(
816+
def wait_for_tpu_active_chips(
752817
node_pool: node_pool_info,
753818
instance_id: str,
754819
job_apply_time: TimeUtil,
755820
) -> bool:
756821
"""
757-
Polls the TPU scheduled_chips metric for a specific GCE instance.
822+
Polls the TPU active_chips metric for a specific GCE instance.
758823
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.
762827
763828
Args:
764829
node_pool: Configuration object with project and cluster details.
765830
instance_id: The specific GCE Instance ID to monitor.
766831
job_apply_time: The time when the job was applied.
767832
768833
Returns:
769-
True if the scheduled_chips metric is found, False otherwise.
834+
True if the active_chips metric is found, False otherwise.
770835
"""
771836
now = datetime.datetime.now()
772837

773838
time_series = query_time_series(
774839
project_id=node_pool.project_id,
775840
filter_str=(
776-
'metric.type="compute.googleapis.com/instance/tpu/scheduled_chips" '
841+
'metric.type="compute.googleapis.com/instance/tpu/active_chips" '
777842
f'resource.labels.instance_id="{instance_id}"'
778843
),
779844
start_time=job_apply_time,
780845
end_time=TimeUtil.from_datetime(now),
781846
)
782847
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
784849
)
785850

786851
return len(time_series) > 0

0 commit comments

Comments
 (0)