diff --git a/.automation_scripts/pytorch-unit-test-scripts/download_testlogs b/.automation_scripts/pytorch-unit-test-scripts/download_testlogs index 22371a1c9e9a1..4500cdfcdbf3a 100755 --- a/.automation_scripts/pytorch-unit-test-scripts/download_testlogs +++ b/.automation_scripts/pytorch-unit-test-scripts/download_testlogs @@ -332,6 +332,7 @@ def _shorten_unzipped_dirs(): failing job from the parity summary. """ from pathlib import Path + job_ids = [] for d in sorted(Path(".").glob("unzipped-*")): if not d.is_dir(): continue @@ -346,11 +347,39 @@ def _shorten_unzipped_dirs(): job_id_match = re.search(r'_(\d{6,})(?:\.zip)?$', d.name) if job_id_match: short_name += f"_{job_id_match.group(1)}" + job_ids.append(job_id_match.group(1)) if not Path(short_name).exists(): d.rename(short_name) print(f" Renamed {d.name} -> {short_name}") else: print(f" WARNING: {short_name} already exists, keeping {d.name}") + return job_ids + +def _record_shard_run_ids(job_ids, workflow_run_id): + """Persist a job_id -> upstream run_id map ("_wf_run_ids.json") next to the + shard dirs so summarize_xml_testreports.py can build a correct job URL for + each shard. + + The default/distributed/inductor configs resolve to DIFFERENT upstream runs + (e.g. mi350 default+inductor come from a trunk push while distributed comes + from periodic / a trunk fallback), yet they all unzip into the same rocm_xml + folder. The legacy single "_wf_run_id" (last write wins) therefore mislabels + shards from the other runs, producing "busted" job URLs that point at a run + the job never belonged to. Recording the run id per job id fixes that. + """ + if not job_ids: + return + mapping = {} + if os.path.isfile("_wf_run_ids.json"): + try: + with open("_wf_run_ids.json") as f: + mapping = json.load(f) + except Exception: + mapping = {} + for jid in job_ids: + mapping[str(jid)] = str(workflow_run_id) + with open("_wf_run_ids.json", "w") as f: + json.dump(mapping, f) def download_xml_files(workflow_run_id, workflow_run_attempts, prefixes=[], allowed_substrings=None): # Get from S3 artifacts. @@ -410,8 +439,12 @@ def download_xml_files(workflow_run_id, workflow_run_attempts, prefixes=[], allo for path in artifact_paths: unzip(path) - _shorten_unzipped_dirs() + job_ids = _shorten_unzipped_dirs() + # Record which upstream run each shard's job id came from so job URLs are + # built per-shard (different configs come from different runs). + _record_shard_run_ids(job_ids, workflow_run_id) + # Kept for backward compatibility with older summarize layouts. with open("_wf_run_id", "w") as f: f.write(str(workflow_run_id)) @@ -526,7 +559,7 @@ def parse_args(): parser.add_argument('--no_rocm', action='store_true') parser.add_argument('--no_cuda', action='store_true') parser.add_argument('--pr_id', type=int, help='The pull request ID') - parser.add_argument('--arch', type=str, choices=['mi200', 'mi300', 'mi350', 'navi31', 'nightly'], default='mi350', help='ROCm GPU architecture (mi200, mi300, mi350, navi31, or nightly, default: mi350)') + parser.add_argument('--arch', type=str, choices=['mi200', 'mi300', 'mi350', 'navi31', 'preview'], default='mi350', help='ROCm GPU architecture (mi200, mi300, mi350, navi31, or preview, default: mi350)') parser.add_argument('--include_inductor_periodic', action='store_true', help='Also download inductor-periodic benchmark artifacts (into a separate directory, not included in parity CSV)') parser.add_argument('--baseline_sha', type=str, help='Baseline commit SHA to compare against. Downloads the same ROCm workflows for this commit into baseline_xml/.') return parser.parse_args() @@ -615,7 +648,7 @@ def main(): # in, job-name prefixes, shard counts and fallbacks) comes from # parity_job_config.json - see PARITY_CONFIG at the top of this file. global ROCmWorkflowNames - arch = args.arch # 'mi200', 'mi300', 'mi350', 'navi31', or 'nightly' + arch = args.arch # 'mi200', 'mi300', 'mi350', 'navi31', or 'preview' arch_config = PARITY_CONFIG["rocm"][arch] ROCmWorkflowNames, rocm_job_prefix, arch_fallbacks = parity_config_views(arch_config) diff --git a/.automation_scripts/pytorch-unit-test-scripts/parity_job_config.json b/.automation_scripts/pytorch-unit-test-scripts/parity_job_config.json index 97d2de3e16a24..1ad42449dbcb5 100644 --- a/.automation_scripts/pytorch-unit-test-scripts/parity_job_config.json +++ b/.automation_scripts/pytorch-unit-test-scripts/parity_job_config.json @@ -38,7 +38,7 @@ "default": [{ "workflow": "rocm-mi300", "job_prefix": "linux-noble-rocm-py3.12-mi300" }], "distributed": [{ "workflow": "periodic-rocm-mi300", "job_prefix": "linux-noble-rocm-py3.12-mi300" }], "inductor": [{ "workflow": "inductor-rocm-mi300", "job_prefix": "linux-noble-rocm-py3.12-mi300" }], - "shard_counts": { "default": 6, "distributed": 3, "inductor": 2 }, + "shard_counts": { "default": 8, "distributed": 3, "inductor": 2 }, "checkrun_regex": "rocm.*mi300.*/ test [(](default|distributed|inductor)," }, "mi200": { @@ -64,12 +64,12 @@ "shard_counts": { "default": 2, "distributed": 3, "inductor": 2 }, "checkrun_regex": "rocm.*navi31.*/ test [(]default," }, - "nightly": { - "default": [{ "workflow": "rocm-nightly", "job_prefix": "linux-noble-rocm-nightly-py3.12-mi350" }], - "distributed": [{ "workflow": "rocm-nightly", "job_prefix": "linux-noble-rocm-nightly-py3.12-mi350" }], - "inductor": [{ "workflow": "rocm-nightly", "job_prefix": "linux-noble-rocm-nightly-py3.12-mi350" }], - "shard_counts": { "default": 6, "distributed": 3, "inductor": 2 }, - "checkrun_regex": "rocm-nightly.*/ test [(](default|distributed|inductor)," + "preview": { + "default": [{ "workflow": "rocm-preview", "job_prefix": "linux-noble-rocm-preview-py3.12-mi350" }], + "distributed": [{ "workflow": "rocm-preview", "job_prefix": "linux-noble-rocm-preview-py3.12-mi350" }], + "inductor": [{ "workflow": "rocm-preview", "job_prefix": "linux-noble-rocm-preview-py3.12-mi350" }], + "shard_counts": { "default": 8, "distributed": 3, "inductor": 2 }, + "checkrun_regex": "rocm-preview.*/ test [(](default|distributed|inductor)," } } } diff --git a/.automation_scripts/pytorch-unit-test-scripts/summarize_xml_testreports.py b/.automation_scripts/pytorch-unit-test-scripts/summarize_xml_testreports.py index 42d69152e243f..15253ff4da703 100755 --- a/.automation_scripts/pytorch-unit-test-scripts/summarize_xml_testreports.py +++ b/.automation_scripts/pytorch-unit-test-scripts/summarize_xml_testreports.py @@ -2,6 +2,7 @@ import argparse import csv +import json import os import re import pandas as pd @@ -73,11 +74,27 @@ def parse_xml_reports_as_dict(workflow_run_id, workflow_run_attempt, tag, path=" test_config = "" test_cases = {} - # download_testlogs writes the upstream pytorch CI workflow run id - # into "_wf_run_id" alongside the shard dirs. We combine it with each - # shard dir's trailing "_" to form the URL - # https://github.com/pytorch/pytorch/actions/runs//job/ + # download_testlogs records the upstream pytorch CI run id for each shard's + # job id in "_wf_run_ids.json" (job_id -> run_id) alongside the shard dirs, + # so we can build the URL + # https://github.com/pytorch/pytorch/actions/runs//job/ # surfaced as the "Job ID" column in the FAILED TESTS table. + # + # A single run id is NOT enough: the default/distributed/inductor configs + # resolve to DIFFERENT upstream runs (e.g. mi350 default+inductor come from + # a trunk push while distributed comes from periodic/a trunk fallback), so + # pairing every shard's job id with one run id produced links to a run the + # job never belonged to (a "busted" URL that 404s). We look each job id up + # in the per-job map and fall back to the legacy single "_wf_run_id" file + # only when the map has no entry (older downloads / backward compat). + run_id_by_job = {} + run_ids_file = os.path.join(path, "_wf_run_ids.json") + if os.path.isfile(run_ids_file): + try: + with open(run_ids_file) as f: + run_id_by_job = {str(k): str(v) for k, v in json.load(f).items()} + except Exception: + run_id_by_job = {} wf_run_id = "" wf_id_file = os.path.join(path, "_wf_run_id") if os.path.isfile(wf_id_file): @@ -96,9 +113,13 @@ def parse_xml_reports_as_dict(workflow_run_id, workflow_run_attempt, tag, path=" test_config = TestConfigName.inductor.name shard = _extract_shard(dir) jid = re.search(r'_(\d+)$', dir) + job_id = jid.group(1) if jid else "" + # Prefer the run id recorded for this specific job; fall back to the + # single legacy run id so we never regress older download layouts. + shard_run_id = run_id_by_job.get(job_id, wf_run_id) job_url = ( - f"https://github.com/pytorch/pytorch/actions/runs/{wf_run_id}/job/{jid.group(1)}" - if wf_run_id and jid else "" + f"https://github.com/pytorch/pytorch/actions/runs/{shard_run_id}/job/{job_id}" + if shard_run_id and job_id else "" ) for xml_report in Path(new_dir).glob("**/*.xml"): try: diff --git a/.github/workflows/parity.yml b/.github/workflows/parity.yml index 3543f9c5ba15a..0c47168a1062c 100644 --- a/.github/workflows/parity.yml +++ b/.github/workflows/parity.yml @@ -31,7 +31,7 @@ on: required: false type: string arch: - description: 'ROCm architectures, comma or space separated. Options: mi350, mi300, mi200, nightly, navi31. Example: "nightly, mi350" or "mi300"' + description: 'ROCm architectures, comma or space separated. Options: mi350, mi300, mi200, navi31, preview. Example: "preview, mi350" or "mi300"' required: false default: 'mi350, mi300, mi200' type: string @@ -72,7 +72,7 @@ on: type: boolean # summarize_xml_testreports flags set1_name: - description: 'Label for ROCm columns in output CSV. Examples: rocm, nightly, mi300. Default: rocm' + description: 'Label for ROCm columns in output CSV. Examples: rocm, preview, mi300. Default: rocm' required: false default: 'rocm' type: string