From a7efa37d67b13e2a7da141451174af7161163337 Mon Sep 17 00:00:00 2001 From: ethanwee1 Date: Mon, 6 Jul 2026 17:40:18 +0000 Subject: [PATCH 01/14] [CI] Parity: support sha=latest by resolving the latest green run on main download_testlogs required exactly one of --pr_id or --sha1 and rejected the "neither" case, so dispatching parity with an empty/"latest" sha (the documented "latest green on main" option, e.g. a baseline_sha comparison) failed with "Please provide either pr_id or sha!". Treat an empty --sha1 or the literal "latest" as "resolve latest green run on main": look up the newest successful ROCm default-workflow run on main (download_workflow_run already does this when given no head_sha) and use its head commit as the target sha. The parity.yml side already omits --sha1 for "latest", so no workflow change is needed. Also clarifies the both-provided error message. --- .../download_testlogs | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/.automation_scripts/pytorch-unit-test-scripts/download_testlogs b/.automation_scripts/pytorch-unit-test-scripts/download_testlogs index 22371a1c9e9a1..f390321a1ebe0 100755 --- a/.automation_scripts/pytorch-unit-test-scripts/download_testlogs +++ b/.automation_scripts/pytorch-unit-test-scripts/download_testlogs @@ -654,17 +654,37 @@ def main(): token = os.getenv('GITHUB_TOKEN', '...') global authentication_headers authentication_headers = {'Authorization': f'token {token}'} - if (args.pr_id and args.sha1) or (not args.pr_id and not args.sha1): - error_msg = "Error: Please provide either pr_id or sha!" + status = "success" + # An empty --sha1 (or the literal "latest") means "use the latest green + # run on main" rather than a specific commit. download_workflow_run() + # already resolves that when given no head_sha, so treat these as unset. + sha_is_latest = (not args.sha1) or (str(args.sha1).strip().lower() == "latest") + if args.pr_id and not sha_is_latest: + error_msg = "Error: Please provide either pr_id or sha (not both)!" print(error_msg) sys.exit(1) if args.pr_id: pr_id = args.pr_id - sha = get_latest_commit_sha(pr_id, token) - else: + sha = get_latest_commit_sha(pr_id, token) + elif not sha_is_latest: sha = args.sha1 pr_id = None - status = "success" + else: + # No pr_id and no explicit sha: resolve the latest green ROCm run on + # main and use its head commit as the target sha. + pr_id = None + print("No sha or pr_id given; resolving latest green ROCm run on main...") + latest_wf = download_workflow_run( + created=args.created, + max_pages=args.max_pages, + workflow=ROCmWorkflowNames["default"], + sha=None, + ignore_status=args.ignore_status, + status=status, + error_msg="Error: could not find a latest green ROCm run on main", + ) + sha = latest_wf["head_sha"] + print(f"Resolved 'latest' to sha: {sha}") print(f"sha: {sha}") # When comparing two commits, prefix log filenames with short SHAs From 5d7a6dcef8cd7d58151b184171c9f01b2f08c407 Mon Sep 17 00:00:00 2001 From: ethanwee1 Date: Tue, 7 Jul 2026 01:47:37 +0000 Subject: [PATCH 02/14] [CI] Parity auto-trigger: open up mi300/mi200/navi31 collection Auto-parity was trunk-scoped (mi350 only). mi300/mi200/navi31 run in their own scheduled upstream workflows on their own SHAs at a different cadence, so the trunk-push scan never reached them. - Discover candidate SHAs from both trunk.yml pushes and the scheduled per-arch workflows (fetch_scheduled_commits / fetch_candidate_commits), deduped newest-first; a 40-hex guard drops stray non-row output. - Hold back SHAs newer than the newest scheduled run so a lagging mi300/mi200/navi batch can still join that SHA's report, yielding ONE combined parity report per SHA (parity.yml's matrix already emits a per-arch artifact plus a merged summary) once every arch that ran has finished. When no scheduled runs exist, nothing is held (mi350/trunk behaves exactly as before). - Expand ARCHS_IN to mi350 mi300 mi200 navi31. - Per-config CUDA-baseline gating: a SHA whose CUDA jobs for a test config did not run (e.g. a failed trunk run that never launched CUDA default) has no baseline for that config, so exclude just that config from the dispatch (via parity.yml's exclude_* inputs) instead of emitting a bogus all-MISSED column, and drop its ROCm shards from the completion gate. No parity_job_config.json change: mi300 has no fallback; its default/ distributed/inductor come from rocm-mi300 / periodic-rocm-mi300 / inductor-rocm-mi300, which run together on one scheduled SHA. --- .github/workflows/parity-auto.yml | 123 ++++++++++++++++++++++++++---- 1 file changed, 110 insertions(+), 13 deletions(-) diff --git a/.github/workflows/parity-auto.yml b/.github/workflows/parity-auto.yml index 06eb450f9bdbf..129c0a5e01f5f 100644 --- a/.github/workflows/parity-auto.yml +++ b/.github/workflows/parity-auto.yml @@ -1,10 +1,16 @@ name: Parity Auto Trigger run-name: "Parity auto-trigger ยท pytorch/pytorch main" -# Every 10 min, dispatch parity.yml once per completed upstream trunk.yml push -# whose parity inputs have all finished. Scope is trunk only: the mi350 ROCm -# shards that ride along in trunk.yml vs that run's CUDA shards. Other arches -# have their own periodic workflows - run parity.yml manually for those. +# Every 10 min, dispatch parity.yml once per upstream SHA whose parity inputs +# have all finished. Candidate SHAs come from two sources: completed trunk.yml +# pushes (mi350 rides along there) and the scheduled per-arch workflows for +# mi300/mi200/navi31 (which run on their own SHAs at their own cadence). A SHA +# that carries several arches yields ONE combined parity report (parity.yml's +# matrix emits a per-arch artifact plus a merged summary). +# +# Because the scheduled arches lag trunk, we hold back SHAs newer than the +# newest scheduled run so a late mi300/mi200 batch can still join that SHA's +# report instead of producing a premature trunk-only one. # # Readiness is gated per check-run, not on workflow_run conclusion: one failed # shard flips the parent run to failure while siblings are still going, so we @@ -76,9 +82,11 @@ jobs: MAX_COMMITS: ${{ github.event_name == 'pull_request' && '20' || inputs.max_commits || '200' }} MAX_DISPATCHES: ${{ github.event_name == 'pull_request' && '5' || inputs.max_dispatches || '50' }} MAX_AGE_HOURS: ${{ inputs.max_age_hours || '72' }} - # Auto-parity is trunk-scoped: mi350 is the only ROCm arch that rides - # along in trunk.yml. Other arches have their own periodic workflows. - ARCHS_IN: mi350 + # mi350 rides along in trunk.yml (per push); mi300/mi200/navi31 run in + # their own scheduled workflows on their own SHAs. We scan both sources + # (see fetch_candidate_commits) so a SHA that carries several arches + # yields one combined parity report once every arch's run has finished. + ARCHS_IN: mi350 mi300 mi200 navi31 # Optional manual overrides; blank means "derive from parity_job_config.json". ARCH_JOBNAME_REGEX_OVERRIDE: ${{ inputs.arch_jobname_regex_map || '' }} ARCH_WORKFLOW_REGEX_OVERRIDE: ${{ inputs.arch_workflow_regex_map || '' }} @@ -117,6 +125,15 @@ jobs: | "(^|/)(" + join("|") + ")[.]yml$" )')} CUDA_JOBNAME_REGEX=$(echo "$config_json" | jq -r '.cuda.checkrun_regex') + # Upstream workflow file names for the non-trunk arches (mi350 rides + # trunk; nightly is not auto-scanned). Their scheduled runs land on + # their own SHAs, so fetch_scheduled_commits mines those SHAs as + # extra candidates on top of the trunk pushes. + SCHEDULED_WORKFLOWS=$(echo "$config_json" | jq -r ' + [ .rocm | to_entries[] + | select(.key != "mi350" and .key != "nightly") + | (.value.default[]?, .value.distributed[]?, .value.inductor[]?).workflow ] + | unique | .[]') } print_run_config() { @@ -124,6 +141,7 @@ jobs: "Upstream: $UPSTREAM@$BRANCH" \ "Target ref: $TARGET_REF" \ "Scope archs: $ARCHS" \ + "Scheduled wfs: $(echo "$SCHEDULED_WORKFLOWS" | tr '\n' ' ')" \ "Max trunk runs: $MAX_COMMITS" \ "Max dispatches: $MAX_DISPATCHES" \ "Max age: ${MAX_AGE_HOURS}h" \ @@ -166,6 +184,33 @@ jobs: echo "$commits_json" | jq -r '.[] | "\(.head_sha) \(.created_at)"' } + # Echo " " lines for recent completed runs of the + # non-trunk arch workflows (mi300/mi200/navi31). These arches run on a + # schedule against whatever main HEAD was current then, so their runs + # surface SHAs that the trunk-push scan alone would rank too low to + # reach. 404/stale workflows simply yield nothing. + fetch_scheduled_commits() { + local wf rows + for wf in $SCHEDULED_WORKFLOWS; do + rows=$(gh api \ + "repos/$UPSTREAM/actions/workflows/$wf.yml/runs?branch=$BRANCH&status=completed&per_page=30" \ + --jq '.workflow_runs[] | "\(.head_sha) \(.created_at)"' 2>/dev/null) + [ -n "$rows" ] && printf '%s\n' "$rows" + done + } + + # Merge trunk pushes + the already-fetched scheduled candidates (global + # SCHEDULED_COMMITS) into one newest-first " " list, one + # row per SHA, capped at MAX_COMMITS. Called in a subshell via $(), so it + # only reads globals - main computes NEWEST_SCHEDULED_EPOCH itself. + fetch_candidate_commits() { + # $1 ~ 40-hex guard drops any stray non-row output (e.g. a 404 body + # from a missing scheduled workflow) that slipped past the API calls. + { fetch_trunk_commits; printf '%s\n' "$SCHEDULED_COMMITS"; } \ + | awk 'NF>=2 && $1 ~ /^[0-9a-f]{40}$/' \ + | sort -k2,2r | awk '!seen[$1]++' | head -n "$MAX_COMMITS" + } + # Echo a JSON array of recent auto-parity workflow_dispatch runs in our # repo, used to skip SHAs we already dispatched. Auto runs come from # github-actions[bot] and carry an "autoparity-" run-name prefix; we @@ -261,6 +306,18 @@ jobs: return 1 fi + # Hold back SHAs newer than the newest scheduled arch run: the schedule + # has not reached them yet, so a mi300/mi200/navi batch may still land + # on this SHA. Waiting lets us emit ONE combined report per SHA once + # every arch that will run has finished, rather than a premature + # trunk-only report that a later batch can no longer join. (When no + # scheduled runs exist NEWEST_SCHEDULED_EPOCH == now, so nothing is + # held and mi350/trunk behaves exactly as before.) + if [ "$commit_epoch" -ne 0 ] && [ "$commit_epoch" -gt "$NEWEST_SCHEDULED_EPOCH" ]; then + echo "[$short] $date newer than newest scheduled arch run - waiting for scheduled arches to catch up" + return 0 + fi + if sha_already_dispatched "$sha"; then echo "[$short] parity report already exists for this SHA - skip" return 0 @@ -300,6 +357,31 @@ jobs: return 0 fi + # Per-config CUDA baseline presence. A config whose CUDA test jobs did + # not run on this SHA (e.g. a failed trunk run that never launched CUDA + # default) has no baseline to compare against, so exclude it from the + # dispatch instead of emitting a bogus all-MISSED column. The length + # check above guarantees at least one config still has a baseline. + EXCLUDE_FLAGS="" + local cfg cuda_missing="" + for cfg in default distributed inductor; do + if ! echo "$cuda_check_runs" | jq -e --arg c "$cfg" \ + 'any(.[]; .name | test("[(]" + $c + ","))' >/dev/null; then + EXCLUDE_FLAGS="$EXCLUDE_FLAGS -f exclude_${cfg}=true" + cuda_missing="$cuda_missing $cfg" + fi + done + cuda_missing=$(echo "$cuda_missing" | xargs) + if [ -n "$cuda_missing" ]; then + # Drop the excluded configs' ROCm check-runs from the completion gate + # so a missing-CUDA config never makes us wait on its ROCm shards. + local miss_rx + miss_rx=$(echo "$cuda_missing" | tr ' ' '|') + ROCM_CHECK_RUNS=$(echo "$ROCM_CHECK_RUNS" | jq --arg rx "[(]($miss_rx)," \ + '[.[] | select((.name | test($rx)) | not)]') + echo "[$short] $date no CUDA baseline for: $cuda_missing - excluding from report" + fi + # Gate 1: every check-run the report consumes (ROCm shards for arches # that ran + CUDA tests) must be status=completed - we author the # SHA's report on dispatch, so dispatching early = partial data. @@ -328,22 +410,24 @@ jobs: fi arch_dispatch=$(echo "$READY" | sed 's/ /, /g') - echo "[$short] READY archs: '$(echo "$READY" | tr ' ' ',')' (committed $date; not-run: ${NOT_RUN_NOTES:-none})" - echo "[$short] dispatching for: '$(echo "$READY" | tr ' ' ',')'" + echo "[$short] READY archs: '$(echo "$READY" | tr ' ' ',')' (committed $date; not-run: ${NOT_RUN_NOTES:-none}; excluded-configs: ${cuda_missing:-none})" + echo "[$short] dispatching for: '$(echo "$READY" | tr ' ' ',')'${EXCLUDE_FLAGS:+ (excluding${cuda_missing:+ }$cuda_missing)}" if [ "$DRY_RUN" = "true" ]; then echo "[$short] DRY_RUN=true - not dispatching" else + # $EXCLUDE_FLAGS is intentionally unquoted so its -f pairs word-split. gh workflow run parity.yml \ --repo "$GITHUB_REPOSITORY" \ --ref "$TARGET_REF" \ -f sha="$sha" \ -f arch="$arch_dispatch" \ - -f auto_triggered=true + -f auto_triggered=true \ + $EXCLUDE_FLAGS fi DISPATCHED_COUNT=$((DISPATCHED_COUNT + 1)) - DISPATCHED_SUMMARY="${DISPATCHED_SUMMARY}${short}:${arch_dispatch}"$'\n' + DISPATCHED_SUMMARY="${DISPATCHED_SUMMARY}${short}:${arch_dispatch}${cuda_missing:+ (no-cuda:$(echo "$cuda_missing" | tr ' ' ','))}"$'\n' if [ "$DISPATCHED_COUNT" -ge "$MAX_DISPATCHES" ]; then echo "Reached max dispatches for this scan ($MAX_DISPATCHES); stopping" return 1 @@ -388,10 +472,23 @@ jobs: load_matching_config print_run_config + # Fetch scheduled-arch candidates once (global, read by + # fetch_candidate_commits) and record the newest scheduled run time so + # process_commit can hold back not-yet-reached SHAs. No scheduled runs + # => hold nothing (NEWEST_SCHEDULED_EPOCH = now). + SCHEDULED_COMMITS=$(fetch_scheduled_commits) + local newest + newest=$(printf '%s\n' "$SCHEDULED_COMMITS" | awk 'NF>=2 && $1 ~ /^[0-9a-f]{40}$/{print $2}' | sort -r | head -1) + if [ -n "$newest" ]; then + NEWEST_SCHEDULED_EPOCH=$(date -u -d "$newest" +%s 2>/dev/null || echo "$NOW_EPOCH") + else + NEWEST_SCHEDULED_EPOCH=$NOW_EPOCH + fi + local commits - commits=$(fetch_trunk_commits) + commits=$(fetch_candidate_commits) if [ -z "$commits" ]; then - echo "::warning::No completed trunk.yml push runs returned from $UPSTREAM@$BRANCH" + echo "::warning::No completed trunk/scheduled workflow runs returned from $UPSTREAM@$BRANCH" exit 0 fi From c41847fe7510498e93e3a27c39976f146085595e Mon Sep 17 00:00:00 2001 From: ethanwee1 Date: Tue, 7 Jul 2026 16:16:06 +0000 Subject: [PATCH 03/14] [CI] Parity: derive ROCm shard count from the resolved workflow Shard counts were hardcoded (config value or literals), so when an arch falls back to a different workflow whose sharding differs, the constructed "(config, i, N)" job/artifact keys miss the real jobs. Concretely mi200's default/inductor workflows are dormant upstream and fall back to trunk-rocm-sandbox, which shards default into 10 (config says 6) and inductor into 4 (config says 2) - so every key "DOES NOT EXIST IN JOBS" and the mi200 shard of a combined auto report fails. Add derive_shard_count(), which reads the actual shard total from the resolved run's job names, and use it for ROCm default/distributed/inductor (falling back to the config value only when no matching jobs are found). This unblocks mi200 in the multi-arch auto-trigger. --- .../download_testlogs | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/.automation_scripts/pytorch-unit-test-scripts/download_testlogs b/.automation_scripts/pytorch-unit-test-scripts/download_testlogs index f390321a1ebe0..bb4e4d3396ace 100755 --- a/.automation_scripts/pytorch-unit-test-scripts/download_testlogs +++ b/.automation_scripts/pytorch-unit-test-scripts/download_testlogs @@ -185,6 +185,36 @@ def get_workflow_jobs(wf, all_attempts=False): jobs += page_json["jobs"] return jobs +def derive_shard_count(wf, job_prefix, test_config, fallback): + """Return the shard total upstream actually used for this (job_prefix, + test_config), parsed from job names like + ' / test (, , , ...)' -> . + + Shard counts differ between an arch's primary workflow and its fallback + (e.g. mi200 default is 6 shards in rocm-mi200 but 10 in the + trunk-rocm-sandbox fallback), so we read the real total from the resolved + run instead of assuming the config value - otherwise the constructed + "(default, i, 6)" keys miss the actual "(default, i, 10)" jobs. Falls back + to `fallback` when the run has no matching jobs. + """ + try: + jobs = get_workflow_jobs(wf) + except Exception: + return fallback + pat = re.compile( + re.escape(job_prefix) + r" / \S+ \(" + re.escape(test_config) + r", \d+, (\d+)" + ) + totals = {} + for job in jobs: + m = pat.search(job.get("name", "")) + if m: + total = int(m.group(1)) + totals[total] = totals.get(total, 0) + 1 + if not totals: + return fallback + # Most common total wins (guards against a stray reshaped/duplicate job). + return sorted(totals.items(), key=lambda kv: (-kv[1], -kv[0]))[0][0] + def get_check_runs_for_commit(sha, prefix): """Get check runs for a commit filtered by name prefix. @@ -762,10 +792,7 @@ def main(): # HUD link: https://hud.pytorch.org/hud/pytorch/pytorch/main/1?per_page=50&name_filter=rocm # Make sure "Hide unstable jobs" is unselected, in case ROCm jobs are marked as unstable - if arch == "mi350": - dist_shards = 3 if not periodic_fallback_used else rocm_shards["distributed"] - else: - dist_shards = rocm_shards["distributed"] + dist_shards = derive_shard_count(periodic_wf, dist_job_prefix, "distributed", rocm_shards["distributed"]) print(f"Using final ROCm shard count {dist_shards} for distributed") if not args.artifacts_only: @@ -825,10 +852,7 @@ def main(): # Download logs # If logs aren't found you might want to check the HUD for the correct tags # HUD link: https://hud.pytorch.org/hud/pytorch/pytorch/main/1?per_page=50&name_filter=rocm - if arch == "mi350": - default_shards = 6 if default_fallback_used else rocm_shards["default"] - else: - default_shards = rocm_shards["default"] + default_shards = derive_shard_count(rocm_wf, rocm_job_prefix['default'], "default", rocm_shards["default"]) print(f"Using final ROCm shard count {default_shards} for default") if not args.artifacts_only: @@ -883,12 +907,12 @@ def main(): folder_list = get_or_create_test_folder(inductor_wf_rocm) - inductor_shards = rocm_shards["inductor"] - print(f"Using final ROCm shard count {inductor_shards} for inductor") if inductor_fallback_used and arch in inductor_fallbacks: inductor_job_prefix = inductor_fallbacks[arch][1] else: inductor_job_prefix = rocm_job_prefix['inductor'] + inductor_shards = derive_shard_count(inductor_wf_rocm, inductor_job_prefix, "inductor", rocm_shards["inductor"]) + print(f"Using final ROCm shard count {inductor_shards} for inductor") # Download logs if not args.artifacts_only: From a64959b03b4d4695d0028c5379aee800a9b6099e Mon Sep 17 00:00:00 2001 From: ethanwee1 Date: Wed, 24 Jun 2026 19:47:08 +0000 Subject: [PATCH 04/14] parity summary: count a disagreement only when CUDA PASSED The DISAGREE/AGREE metric counted a ROCm SKIPPED or MISSED test as a disagreement whenever CUDA merely did not SKIP it - which includes the large set of attention-backend parametrization variants CUDA never even enumerates (CUDA MISSED). Those are not real ROCm-vs-CUDA coverage gaps and inflated DISAGREE to ~3% (AGREE ~97%). Count a disagreement only when CUDA actually PASSED the test (s2 == PASSED) in both compute_test_config_stats and compute_overall_stats, and relabel the two line items accordingly ('PASSED on '). This matches the triage spreadsheet's adjusted definition; AGREE% becomes ~99%. --- .../pytorch-unit-test-scripts/generate_summary.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.automation_scripts/pytorch-unit-test-scripts/generate_summary.py b/.automation_scripts/pytorch-unit-test-scripts/generate_summary.py index 5c78317197b43..2ae893cf82333 100644 --- a/.automation_scripts/pytorch-unit-test-scripts/generate_summary.py +++ b/.automation_scripts/pytorch-unit-test-scripts/generate_summary.py @@ -114,10 +114,10 @@ def test_config_stats_keys(s1_name, s2_name, has_set2=True): f'TOTAL {s1}', ] return [ - f'SKIPPED (on {s1_name}, but not on {s2_name})', + f'SKIPPED (on {s1_name}, PASSED on {s2_name})', f'SKIPPED (on {s1_name})', f'SKIPPED (on {s2_name})', - f'MISSED (MISSED on {s1_name}, NOT SKIPPED on {s2_name})', + f'MISSED (on {s1_name}, PASSED on {s2_name})', f'{s1}ONLY (PASSED on {s1}, NOT PASSED on {s2})', s2, s1, @@ -141,15 +141,18 @@ def compute_test_config_stats(rows, s1_col, s2_col, s1_name, s2_name, has_set2=T vals[keys[4]] = sum(1 for r in rows if r[s1_col].strip()) return vals + # Count a ROCm SKIPPED/MISSED test as a disagreement only when CUDA actually + # PASSED it. This excludes parametrization variants CUDA never runs (CUDA + # MISSED) or also skips, which aren't real ROCm-vs-CUDA coverage gaps. s1_skip_not_s2 = sum( 1 for r in rows - if r[s1_col] == 'SKIPPED' and r[s2_col] != 'SKIPPED' + if r[s1_col] == 'SKIPPED' and r[s2_col] == 'PASSED' ) s1_skip = sum(1 for r in rows if r[s1_col] == 'SKIPPED') s2_skip = sum(1 for r in rows if r[s2_col] == 'SKIPPED') s1_miss_not_s2_skip = sum( 1 for r in rows - if r[s1_col] == 'MISSED' and r[s2_col] != 'SKIPPED' + if r[s1_col] == 'MISSED' and r[s2_col] == 'PASSED' ) only_s1 = sum( 1 for r in rows @@ -233,11 +236,11 @@ def safe_float(v): wf_rows = [r for r in rows if r['test_config'] == wf] s1_skip_not_s2 = sum( 1 for r in wf_rows - if r[s1_col] == 'SKIPPED' and r[s2_col] != 'SKIPPED' + if r[s1_col] == 'SKIPPED' and r[s2_col] == 'PASSED' ) s1_miss_not_s2_skip = sum( 1 for r in wf_rows - if r[s1_col] == 'MISSED' and r[s2_col] != 'SKIPPED' + if r[s1_col] == 'MISSED' and r[s2_col] == 'PASSED' ) total_disagree += s1_skip_not_s2 + s1_miss_not_s2_skip total_s2 += sum(1 for r in wf_rows if r[s2_col].strip() and r[s2_col].strip() != 'MISSED') From 78dbc6a6c63ba679ade8cbc26bf967d974c90c82 Mon Sep 17 00:00:00 2001 From: ethanwee1 Date: Wed, 24 Jun 2026 19:07:40 +0000 Subject: [PATCH 05/14] parity summary: add regex-filtered HUD link for the report commit The HUD link added in #3258 was lost when parity.yml was rewritten. Re-add it in generate_summary.py (next to the Commit SHA header) so it lives in the report itself (step summary + CSV artifact) and survives workflow rewrites. The link targets the upstream HUD page for the resolved commit, regex-filtered to the trunk CUDA/inductor/rocm test jobs the report is built from. Parens/pipes are percent-encoded to keep the markdown link valid; normal runs already pass set1_name=rocm/set2_name=cuda. Verified end-to-end against a real mi350 status CSV. --- .../pytorch-unit-test-scripts/generate_summary.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.automation_scripts/pytorch-unit-test-scripts/generate_summary.py b/.automation_scripts/pytorch-unit-test-scripts/generate_summary.py index 2ae893cf82333..50a89674c6c43 100644 --- a/.automation_scripts/pytorch-unit-test-scripts/generate_summary.py +++ b/.automation_scripts/pytorch-unit-test-scripts/generate_summary.py @@ -642,6 +642,18 @@ def build_rows(args, archs, arch_data): if args.sha: out.append(('__header__', f'Commit SHA: {args.sha}')) + # Link straight to the upstream HUD page filtered (regex) to the trunk + # CUDA / inductor / rocm test jobs this report is built from, so a + # reviewer can jump from the summary to the matching CI jobs. Parens and + # pipes are percent-encoded to keep the markdown link valid. + hud_url = ( + f'https://hud.pytorch.org/hud/pytorch/pytorch/{args.sha}/1' + '?per_page=50' + '&name_filter=%28trunk.*cuda%7Cinductor%7Crocm%29.*test.*' + '%28default%7Cdistributed%7Cinductor%29%2C' + '&useRegexFilter=true' + ) + out.append(('__header__', f'HUD: [parity jobs for this commit]({hud_url})')) if args.pr_id: out.append(('__header__', f'PR ID: {args.pr_id}')) From 756a3e4779456976b0dc693ac8f59e147a1fdbca Mon Sep 17 00:00:00 2001 From: ethanwee1 Date: Tue, 23 Jun 2026 18:31:26 +0000 Subject: [PATCH 06/14] parity: carry commit SHA labels through log + running-time parsing Consumer-side support for commit-vs-commit parity (baseline_sha mode): - detect_log_failures.py: preserve the short commit-SHA prefix on log filenames (e.g. 09e0c59b_rocm3.txt) as the platform label, matching the SHA-prefixed filenames download_testlogs produces in baseline mode. - summarize_xml_testreports.py: name the per-test-file running-time CSV columns from the resolved set names (set1/set2) instead of hardcoded rocm_/cuda_, and add per-config test-shard counts. The download_testlogs run-selection side of this work already landed in develop via #3278, so this PR carries only the remaining consumer-side parsing changes. --- .../detect_log_failures.py | 14 +++- .../summarize_xml_testreports.py | 81 ++++++++++++------- 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/.automation_scripts/pytorch-unit-test-scripts/detect_log_failures.py b/.automation_scripts/pytorch-unit-test-scripts/detect_log_failures.py index b563fcf74bc1e..7b4a73a69508b 100755 --- a/.automation_scripts/pytorch-unit-test-scripts/detect_log_failures.py +++ b/.automation_scripts/pytorch-unit-test-scripts/detect_log_failures.py @@ -69,13 +69,23 @@ def classify_log_file(filename): - """Return (platform, test_config, shard_num) from a log filename like rocm3.txt.""" + """Return (platform, test_config, shard_num) from a log filename like rocm3.txt. + + Commit-vs-commit parity prefixes log files with the short commit SHA + (for example, 09e0c59b_rocm3.txt). In that mode the SHA label is the + platform name used by generate_summary.py, so preserve it here. + """ stem = Path(filename).stem + label = None + m = re.match(r"(?P