Repository navigation
Expand file tree
/
Copy pathfetch_artifacts.py
More file actions
418 lines (353 loc) · 14.1 KB
/
Copy pathfetch_artifacts.py
File metadata and controls
418 lines (353 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
#!/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 (find run IDs at https://github.com/ROCm/TheRock/actions):
pip install boto3
RUN_ID="<replace-with-run-id>"
python build_tools/fetch_artifacts.py \
--run-id ${RUN_ID} --output-dir ~/.therock/artifacts_${RUN_ID}
Download artifacts for selected individual GPU targets:
python build_tools/fetch_artifacts.py \
--run-id ${RUN_ID} --amdgpu-targets gfx942,gfx1100 \
--output-dir ~/.therock/artifacts_${RUN_ID}
Include/exclude regular expressions can be given to control what is downloaded:
python build_tools/fetch_artifacts.py \
--run-id ${RUN_ID} --output-dir ~/.therock/artifacts_${RUN_ID} \
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:])