1919import tempfile
2020import os
2121import random
22+ import time
2223from typing import List
2324
2425from airflow import models
3132from dags .tpu_observability .utils import jobset_util as jobset
3233from dags .tpu_observability .utils import node_pool_util as node_pool
3334from dags .tpu_observability .utils import subprocess_util as subprocess
34- from dags .tpu_observability .utils .jobset_util import JobSet
35- from dags .tpu_observability .configs .common import MachineConfigMap , GCS_CONFIG_PATH
35+ from dags .tpu_observability .utils .jobset_util import Workload
36+ from dags .tpu_observability .configs .common import (
37+ MachineConfigMap ,
38+ GCS_CONFIG_PATH ,
39+ GCS_JOBSET_CONFIG_PATH ,
40+ )
41+ from dags .common .scheduling_helper .scheduling_helper import SchedulingHelper , get_dag_timeout
42+
43+ DAG_ID = "jobset_ttr_pod_oom"
44+ DAGRUN_TIMEOUT = get_dag_timeout (DAG_ID )
45+ SCHEDULE = SchedulingHelper .arrange_schedule_time (DAG_ID )
3646
3747
3848@task
39- def trigger_oom_failure (info , pod_name : str ):
49+ def trigger_oom_failure (info , pod_name : str , namespace : str ):
4050 """
41- Triggers an OOM (Out-of-Memory) event on a specified TPU pod.
51+ Injects an OOM fault by running a memory-intensive loop inside a pod.
4252
43- This task connects to the specified pod via kubectl exec and runs a
44- memory-intensive Python script (oomkill.py). The script is designed
45- to exhaust the container's memory limit, forcing a SIGKILL (Exit Code 137).
46- A connection error is expected and caught when the pod is killed.
53+ This task waits for the target pod to stabilize, then executes a Python
54+ one-liner via 'kubectl exec' that rapidly allocates memory. The script
55+ uses bytearray allocation combined with an immediate write operation
56+ (a[-1][0] = 1) to ensure physical RAM is committed by the OS, effectively
57+ triggering a SIGKILL from the OOM Killer (Exit Code 137).
4758
4859 Args:
49- info: Node pool and cluster information.
50- pod_name (str): The name of the target pod to be OOMKilled.
60+ info: An object containing cluster and node pool credentials/metadata.
61+ pod_name: The name of the target TPU worker pod to inject the fault.
62+ namespace: The Kubernetes namespace where the pod is running.
63+
64+ Raises:
65+ Exception: If the subprocess execution fails for reasons other than
66+ the expected connection loss during an OOM event.
5167 """
68+
69+ wait_time = 60
70+ print (
71+ f"Waiting { wait_time } s for Pod { pod_name } to stabilize before OOM Test..."
72+ )
73+ time .sleep (wait_time )
74+
75+ python_logic = (
76+ "import time\n "
77+ "a = []\n "
78+ "print('Starting memory stress test...')\n "
79+ "while True:\n "
80+ " a.append(bytearray(1024**3))\n "
81+ " a[-1][0] = 1\n "
82+ " time.sleep(0.01)"
83+ )
84+
5285 with tempfile .NamedTemporaryFile () as temp_config_file :
5386 env = os .environ .copy ()
5487 env ["KUBECONFIG" ] = temp_config_file .name
5588
56- cmd = " && " .join ([
57- jobset .Command .get_credentials_command (info ),
58- f"kubectl exec { pod_name } -n default -- python3 oomkill.py" ,
59- ])
89+ credentials_cmd = jobset .Command .get_credentials_command (info )
90+ exec_cmd = (
91+ f'kubectl exec { pod_name } -n { namespace } -- python3 -c "{ python_logic } "'
92+ )
93+
94+ full_command = f"{ credentials_cmd } && { exec_cmd } "
6095
6196 try :
62- print (f"Executing OOM script in { pod_name } ..." )
63- subprocess .run_exec (cmd , env = env )
64- except RuntimeError as e :
65- print (f"Expectation: Connection closed due to OOMKilled. Info: { e } " )
97+ print (f"Blasting { pod_name } memory now..." )
98+ subprocess .run_exec (full_command , env = env )
99+ except Exception as e :
100+ if "137" in str (e ):
101+ print (f"Success: Pod { pod_name } was OOMKilled (Exit Code 137)." )
102+ else :
103+ print (f"Connection lost (Possible OOM): { e } " )
66104
67105
68106@task
@@ -88,9 +126,10 @@ def pick_random_pod(active_pods: List[str]) -> str:
88126
89127
90128with models .DAG (
91- dag_id = "jobset_ttr_pod_oom" ,
129+ dag_id = DAG_ID ,
92130 start_date = datetime .datetime (2026 , 1 , 15 ),
93- schedule = "0 18 * * *" if composer_env .is_prod_env () else None ,
131+ schedule = SCHEDULE if composer_env .is_prod_env () else None ,
132+ dagrun_timeout = DAGRUN_TIMEOUT ,
94133 catchup = False ,
95134 tags = [
96135 "cloud-ml-auto-solutions" ,
@@ -130,103 +169,82 @@ def pick_random_pod(active_pods: List[str]) -> str:
130169 for machine in MachineConfigMap :
131170 config = machine .value
132171
133- jobset_config = JobSet (
134- jobset_name = "jobset-ttr-pod-oom-v6e-workload" ,
135- namespace = "default" ,
136- max_restarts = 5 ,
137- replicated_job_name = "tpu-job-slice" ,
138- replicas = 1 ,
139- backoff_limit = 0 ,
140- completions = 4 ,
141- parallelism = 4 ,
142- tpu_accelerator_type = "tpu-v6e-slice" ,
143- tpu_topology = "4x4" ,
144- container_name = "jax-tpu-worker" ,
145- image = "us-docker.pkg.dev/cienet-cmcs/emma-tpu-test-repo/oom-test:v1" ,
146- tpu_cores_per_pod = 4 ,
147- )
148- raw_yaml = jobset_config .generate_yaml (workload_script = "sleep infinity" )
149- custom_yaml = raw_yaml .replace (
150- "google.com/tpu: 4" ,
151- 'google.com/tpu: 4\n memory: "4Gi"' ,
152- )
153- # Keyword arguments are generated dynamically at runtime (pylint does not
154- # know this signature).
155- with TaskGroup ( # pylint: disable=unexpected-keyword-arg
156- group_id = f"v{ config .tpu_version .value } "
157- ):
158- cluster_info = node_pool .build_node_pool_info_from_gcs_yaml .override (
159- task_id = "build_node_pool_info_from_gcs_yaml"
160- )(
161- gcs_path = GCS_CONFIG_PATH ,
162- dag_name = "jobset_ttr_pod_oom" ,
163- is_prod = composer_env .is_prod_env (),
164- machine_type = config .machine_version .value ,
165- tpu_topology = config .tpu_topology ,
166- )
167-
168- create_node_pool = node_pool .create .override (task_id = "create_node_pool" )(
169- node_pool = cluster_info ,
170- )
171-
172- start_workload = jobset .run_workload .override (task_id = "start_workload" )(
173- node_pool = cluster_info ,
174- yaml_config = custom_yaml ,
175- namespace = jobset_config .namespace ,
176- )
177-
178- ensure_all_pods_running = jobset .wait_for_all_pods_running .override (
179- task_id = "ensure_all_pods_running"
180- )(
181- num_pods = (jobset_config .replicas * jobset_config .parallelism ),
182- node_pool = cluster_info ,
183- )
184-
185- found_pods = jobset .list_pod_names .override (task_id = "list_pod_names" )(
186- node_pool = cluster_info ,
187- namespace = jobset_config .namespace ,
188- )
189-
190- select_random_pod = pick_random_pod .override (task_id = "select_random_pod" )(
191- active_pods = found_pods
192- )
193-
194- trigger_oom_killed = trigger_oom_failure .override (
195- task_id = "trigger_oom_killed"
196- )(info = cluster_info , pod_name = select_random_pod )
197-
198- wait_for_metric_upload = jobset .wait_for_jobset_ttr_to_be_found .override (
199- task_id = "wait_for_jobset_ttr_to_be_found"
200- )(
201- node_pool = cluster_info ,
202- jobset_name = jobset_config .jobset_name ,
203- )
204-
205- cleanup_workload = jobset .end_workload .override (
206- task_id = "cleanup_workload" , trigger_rule = TriggerRule .ALL_DONE
207- )(
208- node_pool = cluster_info ,
209- jobset_name = jobset_config .jobset_name ,
210- namespace = jobset_config .namespace ,
211- ).as_teardown (
212- setups = start_workload
213- )
214-
215- cleanup_node_pool = node_pool .delete .override (
216- task_id = "cleanup_node_pool" , trigger_rule = TriggerRule .ALL_DONE
217- )(node_pool = cluster_info ).as_teardown (
218- setups = create_node_pool ,
219- )
220-
221- chain (
222- cluster_info ,
223- create_node_pool ,
224- start_workload ,
225- ensure_all_pods_running ,
226- found_pods ,
227- select_random_pod ,
228- trigger_oom_killed ,
229- wait_for_metric_upload ,
230- cleanup_workload ,
231- cleanup_node_pool ,
232- )
172+ # Keyword arguments are generated dynamically at runtime (pylint does not
173+ # know this signature).
174+ with TaskGroup ( # pylint: disable=unexpected-keyword-arg
175+ group_id = f"v{ config .tpu_version .value } "
176+ ):
177+ selector = jobset .generate_node_pool_selector ("jobset-ttr-pod-oom" )
178+
179+ jobset_config = jobset .build_jobset_from_gcs_yaml (
180+ gcs_path = GCS_JOBSET_CONFIG_PATH ,
181+ dag_name = DAG_ID ,
182+ node_pool_selector = selector ,
183+ )
184+
185+ cluster_info = node_pool .build_node_pool_info_from_gcs_yaml .override (
186+ task_id = "build_node_pool_info_from_gcs_yaml"
187+ )(
188+ gcs_path = GCS_CONFIG_PATH ,
189+ dag_name = DAG_ID ,
190+ is_prod = composer_env .is_prod_env (),
191+ machine_type = config .machine_version .value ,
192+ tpu_topology = config .tpu_topology ,
193+ node_pool_selector = selector ,
194+ )
195+
196+ create_node_pool = node_pool .create .override (task_id = "create_node_pool" )(
197+ node_pool = cluster_info ,
198+ )
199+
200+ start_workload = jobset .run_workload .override (task_id = "start_workload" )(
201+ node_pool = cluster_info ,
202+ jobset_config = jobset_config ,
203+ workload_type = Workload .JAX_TPU_BENCHMARK ,
204+ )
205+
206+ ensure_all_pods_running = jobset .wait_for_all_pods_running .override (
207+ task_id = "ensure_all_pods_running"
208+ )(
209+ node_pool = cluster_info ,
210+ jobset_config = jobset_config ,
211+ )
212+
213+ select_random_pod = pick_random_pod .override (task_id = "select_random_pod" )(
214+ active_pods = ensure_all_pods_running
215+ )
216+
217+ trigger_oom_killed = trigger_oom_failure .override (
218+ task_id = "trigger_oom_killed"
219+ )(info = cluster_info , pod_name = select_random_pod , namespace = "default" )
220+
221+ wait_for_metric_upload = jobset .wait_for_jobset_ttr_to_be_found .override (
222+ task_id = "wait_for_jobset_ttr_to_be_found"
223+ )(
224+ node_pool = cluster_info ,
225+ jobset_config = jobset_config ,
226+ )
227+
228+ cleanup_workload = jobset .end_workload .override (
229+ task_id = "cleanup_workload" , trigger_rule = TriggerRule .ALL_DONE
230+ )(node_pool = cluster_info , jobset_config = jobset_config ).as_teardown (
231+ setups = start_workload
232+ )
233+
234+ cleanup_node_pool = node_pool .delete .override (
235+ task_id = "cleanup_node_pool" , trigger_rule = TriggerRule .ALL_DONE
236+ )(node_pool = cluster_info ).as_teardown (
237+ setups = create_node_pool ,
238+ )
239+
240+ chain (
241+ cluster_info ,
242+ create_node_pool ,
243+ start_workload ,
244+ ensure_all_pods_running ,
245+ select_random_pod ,
246+ trigger_oom_killed ,
247+ wait_for_metric_upload ,
248+ cleanup_workload ,
249+ cleanup_node_pool ,
250+ )
0 commit comments