CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
417 lines
14 KiB
Python
417 lines
14 KiB
Python
#!/usr/bin/env python
|
|
# Copyright Advanced Micro Devices, Inc.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
|
|
"""Fetches artifacts from S3.
|
|
|
|
The install_rocm_from_artifacts.py script builds on top of this script to both
|
|
download artifacts then unpack them into a usable install directory.
|
|
|
|
Example usage (using https://github.com/ROCm/TheRock/actions/runs/15685736080):
|
|
pip install boto3
|
|
python build_tools/fetch_artifacts.py \
|
|
--run-id 15685736080 --artifact-group gfx110X-all --output-dir ~/.therock/artifacts_15685736080
|
|
|
|
Download all artifacts for all GPU architectures:
|
|
python build_tools/fetch_artifacts.py \
|
|
--run-id 15685736080 --output-dir ~/.therock/artifacts_15685736080
|
|
|
|
Include/exclude regular expressions can be given to control what is downloaded:
|
|
python build_tools/fetch_artifacts.py \
|
|
--run-id 15685736080 --artifact-group gfx110X-all --output-dir ~/.therock/artifacts_15685736080 \
|
|
amd-llvm base 'core-(hip|runtime)' sysdeps \
|
|
--exclude _dbg_
|
|
|
|
This will process artifacts that match any of the include patterns and do not
|
|
match any of the exclude patterns.
|
|
|
|
Note this module will respect:
|
|
AWS_ACCESS_KEY_ID
|
|
AWS_SECRET_ACCESS_KEY
|
|
AWS_SESSION_TOKEN
|
|
if and only if all are specified in the environment to connect with S3.
|
|
If unspecified, we will create an anonymous boto file that can only acccess public artifacts.
|
|
"""
|
|
|
|
import argparse
|
|
import concurrent.futures
|
|
from pathlib import Path
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import sys
|
|
|
|
from _therock_utils.archive_util import open_archive_for_read
|
|
from _therock_utils.artifact_backend import ArtifactBackend, S3Backend
|
|
from _therock_utils.os_util import rmtree_with_retry
|
|
from _therock_utils.artifacts import (
|
|
ArtifactName,
|
|
ArtifactPopulator,
|
|
)
|
|
from _therock_utils.workflow_outputs import WorkflowOutputRoot
|
|
from artifact_manager import DownloadRequest, download_artifact
|
|
|
|
|
|
# TODO(geomin12): switch out logging library
|
|
def log(*args, **kwargs):
|
|
print(*args, **kwargs)
|
|
sys.stdout.flush()
|
|
|
|
|
|
def _get_base_arch(target: str) -> str:
|
|
"""Strip xnack/other suffixes: 'gfx942:xnack+' -> 'gfx942'."""
|
|
if not target:
|
|
return ""
|
|
base = target.split(":")[0]
|
|
return base if base else target
|
|
|
|
|
|
def _matches_target(artifact_target: str, requested_targets: set[str]) -> bool:
|
|
"""Match if the artifact's base arch equals any requested target's base arch."""
|
|
if not artifact_target:
|
|
return False
|
|
requested_bases = {_get_base_arch(t) for t in requested_targets}
|
|
return _get_base_arch(artifact_target) in requested_bases
|
|
|
|
|
|
def list_artifacts_for_group(
|
|
backend: ArtifactBackend,
|
|
artifact_group: str | None,
|
|
amdgpu_targets: list[str] | None = None,
|
|
) -> set[str]:
|
|
"""Lists artifacts from backend, filtered by artifact_group and/or individual targets.
|
|
|
|
Inclusive matching: accepts both family-named archives (mono-arch pipeline)
|
|
and individual-target archives (split/kpack pipeline). Whichever naming
|
|
convention is present in the bucket will be matched.
|
|
|
|
Base architecture matching: requesting a base arch (e.g., "gfx942") will
|
|
also match variants with suffixes (e.g., "gfx942:xnack+", "gfx942:xnack-").
|
|
|
|
Args:
|
|
backend: ArtifactBackend instance configured for the target run
|
|
artifact_group: GPU family to filter by (e.g., "gfx94X-dcgpu"). If None
|
|
and amdgpu_targets is also empty, downloads all artifacts.
|
|
amdgpu_targets: Individual GPU targets to also match (e.g., ["gfx942"]).
|
|
|
|
Returns:
|
|
Set of artifact filenames matching any of the target families or "generic".
|
|
If both artifact_group and amdgpu_targets are None/empty, returns all artifacts.
|
|
"""
|
|
log(f"Retrieving artifacts from '{backend.base_uri}'")
|
|
|
|
# Get all artifacts from backend
|
|
all_artifacts = backend.list_artifacts()
|
|
|
|
# If no filtering criteria specified, return all artifacts
|
|
if not artifact_group and not amdgpu_targets:
|
|
log("No artifact group or targets specified - downloading all artifacts")
|
|
return all_artifacts
|
|
|
|
# Build inclusive set of target families to match
|
|
targets_to_match: set[str] = {"generic"}
|
|
if artifact_group:
|
|
targets_to_match.add(artifact_group)
|
|
if amdgpu_targets:
|
|
targets_to_match.update(amdgpu_targets)
|
|
|
|
log(f"Matching artifact target families: {sorted(targets_to_match)}")
|
|
|
|
# Use structured ArtifactName parsing for reliable matching
|
|
data = set()
|
|
for filename in all_artifacts:
|
|
an = ArtifactName.from_filename(filename)
|
|
if an and _matches_target(an.target_family, targets_to_match):
|
|
data.add(filename)
|
|
|
|
if not data:
|
|
log(
|
|
f"Found no artifacts matching {sorted(targets_to_match)} "
|
|
f"at '{backend.base_uri}'"
|
|
)
|
|
return data
|
|
|
|
|
|
def filter_artifacts(
|
|
artifacts: set[str], includes: list[str], excludes: list[str]
|
|
) -> set[str]:
|
|
"""Filters artifacts based on include and exclude regex lists"""
|
|
|
|
def _should_include(artifact_name: str) -> bool:
|
|
if not includes and not excludes:
|
|
return True
|
|
|
|
# If includes, then one include must match.
|
|
if includes:
|
|
for include in includes:
|
|
pattern = re.compile(include)
|
|
if pattern.search(artifact_name):
|
|
break
|
|
else:
|
|
return False
|
|
|
|
# If excludes, then no excludes must match.
|
|
if excludes:
|
|
for exclude in excludes:
|
|
pattern = re.compile(exclude)
|
|
if pattern.search(artifact_name):
|
|
return False
|
|
|
|
# Included and not excluded.
|
|
return True
|
|
|
|
return {a for a in artifacts if _should_include(a)}
|
|
|
|
|
|
def get_postprocess_mode(args) -> str | None:
|
|
"""Returns 'extract', 'flatten' or None (default is 'extract')."""
|
|
if args.flatten:
|
|
return "flatten"
|
|
if args.no_extract:
|
|
return None
|
|
return "extract"
|
|
|
|
|
|
def extract_artifact(
|
|
archive_path: Path, *, delete_archive: bool, postprocess_mode: str
|
|
):
|
|
"""Extracts and postprocesses an artifact from an archive file in-place.
|
|
|
|
Args:
|
|
archive_path: Path to the archive (e.g. `amd-llvm_lib_generic.tar.xz`)
|
|
delete_archive: True to delete the archive after extraction
|
|
postprocess_mode: Either 'flatten' or 'extract'
|
|
* 'flatten' merges artifacts into a single "dist/" directory
|
|
* 'extract' puts each artifact in a dir (e.g. `amd-llvm_lib_generic/`)
|
|
"""
|
|
# Get (for example) 'amd-llvm_lib_generic' from '/path/to/amd-llvm_lib_generic.tar.xz'
|
|
# We can't just use .stem since that only removes the last extension.
|
|
# 1. .name gets us 'amd-llvm_lib_generic.tar.xz'
|
|
# 2. .partition('.') gets (before, sep, after), discard all but 'before'
|
|
archive_file = archive_path
|
|
artifact_name, *_ = archive_file.name.partition(".")
|
|
|
|
if postprocess_mode == "extract":
|
|
output_dir = archive_file.parent / artifact_name
|
|
if output_dir.exists():
|
|
rmtree_with_retry(output_dir)
|
|
with open_archive_for_read(archive_file) as tf:
|
|
log(f"++ Extracting '{archive_file.name}' to '{artifact_name}'")
|
|
tf.extractall(archive_file.parent / artifact_name, filter="tar")
|
|
elif postprocess_mode == "flatten":
|
|
output_dir = archive_file.parent
|
|
log(f"++ Flattening '{archive_file.name}' to '{artifact_name}'")
|
|
flattener = ArtifactPopulator(
|
|
output_path=output_dir, verbose=True, flatten=True
|
|
)
|
|
flattener(archive_file)
|
|
else:
|
|
raise AssertionError(f"Unhandled postprocess_mode = {postprocess_mode}")
|
|
|
|
if delete_archive:
|
|
archive_file.unlink()
|
|
|
|
|
|
def run(args):
|
|
run_github_repo = args.run_github_repo
|
|
run_id = args.run_id
|
|
artifact_group = args.artifact_group
|
|
output_dir = args.output_dir
|
|
|
|
output_root = WorkflowOutputRoot.from_workflow_run(
|
|
run_id=run_id,
|
|
platform=args.platform,
|
|
github_repository=run_github_repo,
|
|
lookup_workflow_run=True,
|
|
)
|
|
backend = S3Backend(output_root=output_root)
|
|
|
|
# Parse individual GPU targets (comma-separated string to list).
|
|
amdgpu_targets = (
|
|
[t.strip() for t in args.amdgpu_targets.split(",") if t.strip()]
|
|
if args.amdgpu_targets
|
|
else []
|
|
)
|
|
|
|
# Lookup which artifacts exist in the bucket.
|
|
# Note: this currently does not check that all requested artifacts
|
|
# (via include patterns) do exist, so this may silently fail to fetch
|
|
# expected files.
|
|
available_artifacts = list_artifacts_for_group(
|
|
backend=backend,
|
|
artifact_group=artifact_group,
|
|
amdgpu_targets=amdgpu_targets,
|
|
)
|
|
if not available_artifacts:
|
|
log(f"No matching artifacts for {run_id} exist. Exiting...")
|
|
sys.exit(1)
|
|
|
|
# Include/exclude filtering.
|
|
filtered_artifacts = filter_artifacts(
|
|
available_artifacts, args.include, args.exclude
|
|
)
|
|
if not filtered_artifacts:
|
|
log(f"Filtering artifacts for {run_id} resulted in an empty set. Exiting...")
|
|
sys.exit(1)
|
|
|
|
download_requests = [
|
|
DownloadRequest(
|
|
artifact_key=artifact,
|
|
dest_path=output_dir / artifact,
|
|
backend=backend,
|
|
)
|
|
for artifact in sorted(filtered_artifacts)
|
|
]
|
|
|
|
download_summary = "\n ".join(
|
|
[f"{req.backend.base_uri}/{req.artifact_key}" for req in download_requests]
|
|
)
|
|
log(f"\nFiltered artifacts to download:\n {download_summary}\n")
|
|
|
|
if args.dry_run:
|
|
log("Skipping downloads since --dry-run was set")
|
|
return
|
|
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Download and extract in parallel.
|
|
with concurrent.futures.ThreadPoolExecutor(
|
|
max_workers=args.download_concurrency
|
|
) as download_executor:
|
|
download_futures = [
|
|
download_executor.submit(download_artifact, req)
|
|
for req in download_requests
|
|
]
|
|
|
|
postprocess_mode = get_postprocess_mode(args)
|
|
if not postprocess_mode:
|
|
# No postprocessing to do, wait on downloads then return.
|
|
[f.result() for f in download_futures]
|
|
return
|
|
|
|
with concurrent.futures.ThreadPoolExecutor(
|
|
max_workers=args.extract_concurrency
|
|
) as extract_executor:
|
|
extract_futures: list[concurrent.futures.Future] = []
|
|
for download_future in concurrent.futures.as_completed(download_futures):
|
|
# download_artifact returns Optional[Path] - None on failure
|
|
download_result = download_future.result(timeout=60)
|
|
if download_result is None:
|
|
continue
|
|
extract_futures.append(
|
|
extract_executor.submit(
|
|
extract_artifact,
|
|
download_result,
|
|
delete_archive=args.delete_after_extract,
|
|
postprocess_mode=postprocess_mode,
|
|
)
|
|
)
|
|
|
|
[f.result() for f in extract_futures]
|
|
|
|
|
|
def main(argv):
|
|
parser = argparse.ArgumentParser(prog="fetch_artifacts")
|
|
|
|
filter_group = parser.add_argument_group("Artifact filtering")
|
|
filter_group.add_argument(
|
|
"include",
|
|
nargs="*",
|
|
help="Regular expression patterns of artifacts to include: "
|
|
"if supplied one pattern must match for an artifact to be included",
|
|
)
|
|
filter_group.add_argument(
|
|
"--exclude",
|
|
nargs="*",
|
|
help="Regular expression patterns of artifacts to exclude",
|
|
)
|
|
filter_group.add_argument(
|
|
"--platform",
|
|
type=str,
|
|
choices=["linux", "windows"],
|
|
default=platform.system().lower(),
|
|
help="Platform to download artifacts for (matches artifact folder name suffixes in S3)",
|
|
)
|
|
filter_group.add_argument(
|
|
"--artifact-group",
|
|
type=str,
|
|
help="Artifact group to fetch (e.g., 'gfx110X-all'). If omitted along with --amdgpu-targets, downloads all artifacts.",
|
|
)
|
|
filter_group.add_argument(
|
|
"--amdgpu-targets",
|
|
type=str,
|
|
default="",
|
|
help="Comma-separated individual GPU targets for fetching split artifacts (e.g. 'gfx942')",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--download-concurrency",
|
|
type=int,
|
|
default=10,
|
|
help="Number of concurrent download jobs to execute at once",
|
|
)
|
|
parser.add_argument(
|
|
"--extract-concurrency",
|
|
type=int,
|
|
help="Number of extract jobs to execute at once (defaults to python VM defaults for CPU tasks)",
|
|
)
|
|
parser.add_argument(
|
|
"--run-github-repo",
|
|
type=str,
|
|
help="GitHub repository for --run-id in 'owner/repo' format (e.g. 'ROCm/TheRock'). Defaults to GITHUB_REPOSITORY env var or 'ROCm/TheRock'",
|
|
)
|
|
parser.add_argument(
|
|
"--run-id",
|
|
type=str,
|
|
required=True,
|
|
help="GitHub run ID to retrieve artifacts from",
|
|
)
|
|
parser.add_argument(
|
|
"--output-dir",
|
|
type=Path,
|
|
default="build/artifacts",
|
|
help="Output path for fetched artifacts, defaults to `build/artifacts/` as in source builds",
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
default=False,
|
|
help="If set, will only log which artifacts would be fetched without downloading or extracting",
|
|
action=argparse.BooleanOptionalAction,
|
|
)
|
|
|
|
postprocess_group = parser.add_argument_group("Postprocessing")
|
|
postprocess_p = postprocess_group.add_mutually_exclusive_group()
|
|
postprocess_p.add_argument(
|
|
"--no-extract",
|
|
default=False,
|
|
action="store_true",
|
|
help="Do no extraction or flattening",
|
|
)
|
|
postprocess_p.add_argument(
|
|
"--extract",
|
|
default=False,
|
|
action="store_true",
|
|
help="Extract files after fetching them",
|
|
)
|
|
postprocess_p.add_argument(
|
|
"--flatten",
|
|
default=False,
|
|
action="store_true",
|
|
help="Flattens artifacts after fetching them",
|
|
)
|
|
postprocess_group.add_argument(
|
|
"--delete-after-extract",
|
|
default=True,
|
|
action=argparse.BooleanOptionalAction,
|
|
help="Delete archive files after extraction",
|
|
)
|
|
|
|
args = parser.parse_args(argv)
|
|
|
|
run(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main(sys.argv[1:])
|