Skip to content

Commit ded4999

Browse files
authored
combine download_profile and xplane_to_metrics (GoogleCloudPlatform#744)
1 parent 3b5d7f9 commit ded4999

2 files changed

Lines changed: 155 additions & 158 deletions

File tree

xlml/apis/task.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -413,15 +413,13 @@ def post_process(self, result_location: Optional[str] = None) -> DAGNode:
413413
)
414414

415415
if self.task_metric_config and self.task_metric_config.profile:
416-
profile_tmp_dir = metric.download_profile(
417-
process_id, self.task_metric_config.profile.file_location
418-
)
419-
self.task_metric_config.profile.metrics = metric.xplane_to_metrics(
420-
profile_tmp_dir
416+
self.task_metric_config.profile.metrics = (
417+
metric.xplane_to_metrics.override(retries=0)(
418+
self.task_metric_config.profile.file_location
419+
)
421420
)
422421
(
423422
process_id
424-
>> profile_tmp_dir
425423
>> self.task_metric_config.profile.metrics
426424
>> post_process_metrics
427425
)

xlml/utils/metric.py

Lines changed: 151 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -347,96 +347,80 @@ def get_gcs_file_location_with_regex(
347347
return f"gs://{bucket_name}/{selected_blob.name}"
348348

349349

350-
def get_gcs_profile_locations(dir_location: str) -> str:
351-
"""
352-
Find first gcs location matching regex `{dir_location}/.*/*xplane.pb`
353-
354-
Returns:
355-
The matched gcs location, or "" if no match
356-
"""
357-
storage_client = storage.Client()
358-
url = urlparse(dir_location)
359-
bucket_name = url.netloc
360-
file_path = url.path.lstrip("/")
361-
file_path_regex = re.compile(file_path + "/.*/*xplane.pb")
362-
matched_blobs = [
363-
b
364-
for b in storage_client.list_blobs(bucket_name, prefix=file_path)
365-
if file_path_regex.match(b.name)
366-
]
367-
if not matched_blobs:
368-
logging.warning(
369-
f"No objects matched supplied regex: {dir_location}/.*/*xplane.pb"
370-
)
371-
return ""
372-
373-
selected_blob = matched_blobs[0]
374-
return f"gs://{bucket_name}/{selected_blob.name}"
375-
376-
377-
@task
378-
def download_profile(
379-
process_id: str, dir_location: str | airflow.XComArg
380-
) -> str:
381-
"""
382-
Find the first file_location matching regex `{dir_location}/.*/*xplane.pb`
383-
Create tmp_dir = f"/tmp/profile-{process_id}"
384-
Download profile to tmp_location = f"{tmp_dir}/gke.xplane.pb"
385-
386-
Returns:
387-
tmp_dir, or "" if no match
388-
"""
389-
if isinstance(dir_location, airflow.XComArg):
390-
dir_location = dir_location.resolve(get_current_context())
391-
file_location = get_gcs_profile_locations(dir_location)
392-
# no match profile
393-
if not file_location:
394-
return ""
395-
print(f"For local download, run: gcloud storage cp {file_location} .")
396-
# unique directory to accomodate intermediate files from profile extraction
397-
tmp_dir = f"/tmp/profile-{process_id}"
398-
os.makedirs(tmp_dir, exist_ok=False)
399-
download_object_from_gcs(file_location, f"{tmp_dir}/gke.xplane.pb")
400-
return tmp_dir
401-
402-
403350
@task.virtualenv(
404351
task_id="process_profile_metrics",
405352
requirements=["tensorboard_plugin_profile==2.19.4"],
406-
system_site_packages=False,
353+
system_site_packages=True,
407354
)
408-
def xplane_to_metrics(input_dir: str) -> dict:
355+
def xplane_to_metrics(dir_location: str | airflow.XComArg) -> dict:
409356
"""
410-
Extract metrics from xplane file `{input_dir}/gke.xplane.pb`.
411-
Remove temporory input_dir generated by `download_profile`.
357+
Find the first profile matching regex `{dir_location}/.*/*xplane.pb`.
358+
Download profile to temporary directory.
359+
Extract metrics from xplane file.
412360
413361
Returns:
414-
A dictionary of profile metrics, or None if input_dir is ""
362+
A dictionary of profile metrics, or None if no match
415363
"""
416364
# pylint: disable=redefined-outer-name, import-outside-toplevel, reimported
417-
from tensorboard_plugin_profile.convert import raw_to_tool_data
418365
import json
419366
import logging
420-
import shutil
367+
import re
368+
import tempfile
369+
from urllib.parse import urlparse
370+
import airflow
371+
from google.cloud import storage
372+
from tensorboard_plugin_profile.convert import raw_to_tool_data
421373
# pylint: enable=redefined-outer-name, import-outside-toplevel, reimported
422374

423-
if not input_dir:
424-
return None
425-
input_path = f"{input_dir}/gke.xplane.pb"
426-
print(f"processing xplane file: {input_path}")
375+
# --- Find and Download Profile ---
376+
# pylint: disable-next=redefined-outer-name
377+
def download_object_from_gcs(
378+
source_location: str, destination_location: str
379+
) -> None:
380+
"""Download object from GCS bucket.
381+
382+
Args:
383+
source_location: The full path of a file in GCS.
384+
destination_location: The local path of the file.
385+
"""
386+
storage_client = storage.Client()
387+
bucket_name = source_location.split("/")[2]
388+
object_name = "/".join(source_location.split("/")[3:])
389+
390+
bucket = storage_client.bucket(bucket_name)
391+
blob = bucket.blob(object_name)
392+
blob.download_to_filename(destination_location)
393+
logging.info(
394+
("Download file from" f" {source_location} to {destination_location}")
395+
)
427396

428-
def cleanup(input_dir):
429-
# remove temporary directory generated by `download_profile`
430-
if input_dir.startswith("/tmp/profile-"):
431-
shutil.rmtree(input_dir)
432-
print(f"remove temporary directory: {input_dir}")
397+
def get_gcs_profile_location(dir_location: str) -> str:
398+
"""
399+
Find first gcs location matching regex `{dir_location}/.*/*xplane.pb`.
400+
401+
Returns:
402+
The matched gcs location, or "" if no match
403+
"""
404+
storage_client = storage.Client()
405+
url = urlparse(dir_location)
406+
bucket_name = url.netloc
407+
file_path = url.path.lstrip("/")
408+
file_path_regex = re.compile(file_path + "/.*/*xplane.pb")
409+
for b in storage_client.list_blobs(bucket_name, prefix=file_path):
410+
if file_path_regex.match(b.name):
411+
return f"gs://{bucket_name}/{b.name}"
412+
logging.warning(
413+
f"No objects matched supplied regex: {dir_location}/.*/*xplane.pb"
414+
)
415+
return ""
433416

417+
# --- Extract Metrics from Profile ---
434418
def round_number(number: float | str, decimal: int) -> float:
435419
if isinstance(number, str):
436420
number = float(number)
437421
return float(f"{number:.{decimal}f}")
438422

439-
def xplane_tool(input_path: str, tool: str, params: dict) -> dict:
423+
def get_tool_data(input_path: str, tool: str, params: dict) -> dict:
440424
data, content_type = raw_to_tool_data.xspace_to_tool_data(
441425
xspace_paths=[input_path],
442426
tool=tool,
@@ -447,96 +431,111 @@ def xplane_tool(input_path: str, tool: str, params: dict) -> dict:
447431
parsed_data = json.loads(data)
448432
return parsed_data
449433

450-
out = {}
451-
# check available tools
452-
available_tools = set(
453-
raw_to_tool_data.xspace_to_tool_names(xspace_paths=[input_path])
454-
)
455-
# 1 overview_page
456-
# generate ALL_HOSTS.op_stats.pb
457-
if "overview_page" not in available_tools:
458-
logging.warning("overview_page: unavailable tool")
459-
else:
434+
def get_tool_metrics(input_path: str) -> dict:
435+
out = {}
436+
# check available tools
437+
available_tools = set(
438+
raw_to_tool_data.xspace_to_tool_names(xspace_paths=[input_path])
439+
)
440+
# 1 overview_page
441+
# generate ALL_HOSTS.op_stats.pb
442+
if "overview_page" not in available_tools:
443+
logging.warning("overview_page: unavailable tool")
444+
else:
445+
try:
446+
overview_page = get_tool_data(
447+
input_path, tool="overview_page", params={}
448+
)
449+
out.update({
450+
"Device Type": overview_page[2]["p"]["device_type"],
451+
"Device Core Count": int(
452+
overview_page[2]["p"]["device_core_count"]
453+
),
454+
"Average Tensor Core Step Time (ms)": float(
455+
overview_page[1]["p"]["steptime_ms_average"]
456+
),
457+
})
458+
except ValueError as e:
459+
logging.warning(e)
460+
461+
# 2 op_profile
462+
if "op_profile" not in available_tools:
463+
logging.warning("op_profile: unavailable tool")
464+
return out
460465
try:
461-
overview_page = xplane_tool(input_path, tool="overview_page", params={})
466+
op_profile = get_tool_data(input_path, tool="op_profile", params={})
462467
out.update({
463-
"device_type": overview_page[2]["p"]["device_type"],
464-
"device_core_count": int(overview_page[2]["p"]["device_core_count"]),
465-
"Average Tensor Core Step Time (ms)": float(
466-
overview_page[1]["p"]["steptime_ms_average"]
468+
"TPU FLOPS Utilization (%): exclude_idle": round_number(
469+
op_profile["byProgramExcludeIdle"]["metrics"]["flops"] * 100, 2
470+
),
471+
"HBM Bandwidth Utilization (%): exclude_idle": round_number(
472+
op_profile["byProgramExcludeIdle"]["metrics"]["bandwidthUtils"][0]
473+
* 100,
474+
2,
467475
),
468476
})
469477
except ValueError as e:
470478
logging.warning(e)
471-
472-
# 2 op_profile
473-
if "op_profile" not in available_tools:
474-
logging.warning("op_profile: unavailable tool")
475-
cleanup(input_dir)
476-
return out
477-
try:
478-
op_profile = xplane_tool(input_path, tool="op_profile", params={})
479-
out.update({
480-
"TPU FLOPS Utilization (%): exclude_idle": round_number(
481-
op_profile["byProgramExcludeIdle"]["metrics"]["flops"] * 100, 2
482-
),
483-
"HBM Bandwidth Utilization (%): exclude_idle": round_number(
484-
op_profile["byProgramExcludeIdle"]["metrics"]["bandwidthUtils"][0]
485-
* 100,
486-
2,
487-
),
488-
})
489-
except ValueError as e:
490-
logging.warning(e)
491-
cleanup(input_dir)
492-
return out
493-
494-
# 3 memory_viewer: jit_train_step
495-
# generate jit*.hlo_proto.pb
496-
if "memory_viewer" not in available_tools:
497-
logging.warning("memory_viewer: unavailable tool")
498-
cleanup(input_dir)
499-
return out
500-
# find jit_train_step from 2
501-
# example: "jit_train_step(4869159985936022652)"
502-
jit_train_step = None
503-
for program in op_profile["byProgramExcludeIdle"]["children"]:
504-
if program["name"].startswith("jit_train_step"):
505-
jit_train_step = program["name"]
506-
break
507-
if jit_train_step is None:
508-
logging.warning("memory_viewer: cannot find jit_train_step in op_profile")
509-
cleanup(input_dir)
479+
return out
480+
481+
# 3 memory_viewer: jit_train_step
482+
# generate jit*.hlo_proto.pb
483+
if "memory_viewer" not in available_tools:
484+
logging.warning("memory_viewer: unavailable tool")
485+
return out
486+
# find jit_train_step from 2
487+
# example: "jit_train_step(4869159985936022652)"
488+
jit_train_step = None
489+
for program in op_profile["byProgramExcludeIdle"]["children"]:
490+
if program["name"].startswith("jit_train_step"):
491+
jit_train_step = program["name"]
492+
break
493+
if jit_train_step is None:
494+
logging.warning("memory_viewer: cannot find jit_train_step in op_profile")
495+
return out
496+
# 3.1 hbm
497+
MEMORY_SPACE_HBM = "0"
498+
try:
499+
memory_viewer_hbm = get_tool_data(
500+
input_path,
501+
"memory_viewer",
502+
params={"host": jit_train_step, "memory_space": MEMORY_SPACE_HBM},
503+
)
504+
out["Peak memory allocation (MiB): jit_train_step, HBM"] = round_number(
505+
memory_viewer_hbm["totalBufferAllocationMib"], 2
506+
)
507+
except ValueError as e:
508+
logging.warning(e)
509+
# 3.2 host
510+
MEMORY_SPACE_HOST = "5"
511+
try:
512+
memory_viewer_host = get_tool_data(
513+
input_path,
514+
"memory_viewer",
515+
params={"host": jit_train_step, "memory_space": MEMORY_SPACE_HOST},
516+
)
517+
out["Peak memory allocation (MiB): jit_train_step, host"] = round_number(
518+
memory_viewer_host["totalBufferAllocationMib"], 2
519+
)
520+
except ValueError as e:
521+
logging.warning(e)
510522
return out
511-
# 3.1 hbm
512-
MEMORY_SPACE_HBM = "0"
513-
try:
514-
memory_viewer_hbm = xplane_tool(
515-
input_path,
516-
"memory_viewer",
517-
params={"host": jit_train_step, "memory_space": MEMORY_SPACE_HBM},
518-
)
519-
out["Peak memory allocation (MiB): jit_train_step, HBM"] = round_number(
520-
memory_viewer_hbm["totalBufferAllocationMib"], 2
521-
)
522-
except ValueError as e:
523-
logging.warning(e)
524-
# 3.2 host
525-
MEMORY_SPACE_HOST = "5"
526-
try:
527-
memory_viewer_host = xplane_tool(
528-
input_path,
529-
"memory_viewer",
530-
params={"host": jit_train_step, "memory_space": MEMORY_SPACE_HOST},
531-
)
532-
out["Peak memory allocation (MiB): jit_train_step, host"] = round_number(
533-
memory_viewer_host["totalBufferAllocationMib"], 2
534-
)
535-
except ValueError as e:
536-
logging.warning(e)
537523

538-
cleanup(input_dir)
539-
return out
524+
# --- Main Logic ---
525+
# Find the first file_location matching regex `{dir_location}/.*/*xplane.pb`
526+
if isinstance(dir_location, airflow.XComArg):
527+
dir_location = dir_location.resolve(get_current_context())
528+
file_location = get_gcs_profile_location(dir_location)
529+
if file_location:
530+
print(f"For local download, run: gcloud storage cp {file_location} .")
531+
# Temporary directory for file download and extraction cache
532+
with tempfile.TemporaryDirectory() as tmp_dir:
533+
input_path = f"{tmp_dir}/gke.xplane.pb"
534+
download_object_from_gcs(file_location, input_path)
535+
# Extract profile
536+
return get_tool_metrics(input_path)
537+
# No match profile
538+
return None
540539

541540

542541
def process_profile(

0 commit comments

Comments
 (0)