Files
TheRock/rocm-systems/shared/kpack/python/rocm_kpack/artifact_collector.py
T
derek.holloway 7502bf64a4
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
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

364 lines
13 KiB
Python

"""
Artifact collection for the recombination phase.
This module discovers and organizes split artifacts from multiple map phase
shards, preparing them for recombination into package groups.
"""
from dataclasses import dataclass
from pathlib import Path
from typing import NamedTuple
from rocm_kpack.artifact_utils import read_artifact_manifest
@dataclass
class CollectedArtifact:
"""Represents a collected artifact directory."""
path: Path
shard_name: str
component_name: str
architecture: str | None # None for generic artifacts
prefixes: list[str]
@property
def is_generic(self) -> bool:
"""Check if this is a generic artifact."""
return self.architecture is None
@property
def is_architecture_specific(self) -> bool:
"""Check if this is an architecture-specific artifact."""
return self.architecture is not None
class ArtifactNameInfo(NamedTuple):
"""Parsed artifact directory name."""
component_name: str
architecture: str | None # None for generic artifacts
@dataclass
class AvailabilityResult:
"""Result of architecture availability check."""
available: list[str]
missing: list[str]
class ArtifactCollector:
"""
Collects and organizes split artifacts from multiple map phase shards.
This class discovers generic and architecture-specific artifacts across
multiple build shards and prepares them for recombination.
"""
def __init__(self, shards_dir: Path, primary_shard: str, verbose: bool = False):
"""
Initialize artifact collector.
Args:
shards_dir: Directory containing shard subdirectories from map phase
primary_shard: Name of shard to use for generic artifacts
verbose: Enable verbose output
"""
self.shards_dir = shards_dir
self.primary_shard = primary_shard
self.verbose = verbose
# Shard -> component -> artifact
self.shards: dict[str, dict[str, CollectedArtifact]] = {}
# Component -> arch -> artifact (across all shards)
self.arch_artifacts: dict[str, dict[str, CollectedArtifact]] = {}
# Component -> generic artifact (from primary shard only)
self.generic_artifacts: dict[str, CollectedArtifact] = {}
def collect(self) -> None:
"""
Discover and collect all artifacts from shards.
Raises:
FileNotFoundError: If shards directory doesn't exist
ValueError: If artifact structure is invalid or primary shard missing
"""
if not self.shards_dir.exists():
raise FileNotFoundError(
f"Shards directory does not exist: {self.shards_dir}"
)
if not self.shards_dir.is_dir():
raise ValueError(f"Shards path is not a directory: {self.shards_dir}")
if self.verbose:
print(f"Collecting artifacts from shards in: {self.shards_dir}")
print(f"Primary shard for generic artifacts: {self.primary_shard}")
# Discover shards
shard_dirs = [d for d in self.shards_dir.iterdir() if d.is_dir()]
if not shard_dirs:
raise ValueError(f"No shard directories found in {self.shards_dir}")
# Check that primary shard exists
primary_shard_dir = self.shards_dir / self.primary_shard
if not primary_shard_dir.exists():
raise ValueError(
f"Primary shard '{self.primary_shard}' not found in {self.shards_dir}"
)
if self.verbose:
print(f"\nFound {len(shard_dirs)} shards:")
for shard_dir in sorted(shard_dirs):
marker = " (primary)" if shard_dir.name == self.primary_shard else ""
print(f" - {shard_dir.name}{marker}")
print()
# Scan each shard for artifacts
for shard_dir in sorted(shard_dirs):
self._scan_shard(shard_dir)
# Validate that primary shard has generic artifacts
if not self.generic_artifacts:
raise ValueError(
f"No generic artifacts found in primary shard '{self.primary_shard}'"
)
if self.verbose:
print(f"\nCollection summary:")
print(
f" Generic artifacts (from {self.primary_shard}): {len(self.generic_artifacts)}"
)
total_arch_artifacts = sum(
len(archs) for archs in self.arch_artifacts.values()
)
print(
f" Architecture-specific artifacts (all shards): {total_arch_artifacts}"
)
def _scan_shard(self, shard_dir: Path) -> None:
"""
Scan a shard directory for artifacts.
Args:
shard_dir: Shard directory to scan
"""
shard_name = shard_dir.name
if self.verbose:
print(f"Scanning shard: {shard_name}")
if shard_name not in self.shards:
self.shards[shard_name] = {}
# Scan for artifact directories in this shard
for artifact_dir in shard_dir.iterdir():
if not artifact_dir.is_dir():
continue
# Check if it has artifact_manifest.txt
manifest_file = artifact_dir / "artifact_manifest.txt"
if not manifest_file.exists():
if self.verbose:
print(f" Skipping {artifact_dir.name}: no artifact_manifest.txt")
continue
# Parse artifact name to extract component and architecture
artifact_info = self._parse_artifact_name(artifact_dir.name)
if artifact_info is None:
if self.verbose:
print(
f" Skipping {artifact_dir.name}: invalid artifact name format"
)
continue
component_name = artifact_info.component_name
architecture = artifact_info.architecture
# Read artifact manifest
try:
prefixes = read_artifact_manifest(artifact_dir)
except FileNotFoundError as e:
raise ValueError(
f"Artifact manifest not found in {artifact_dir}"
) from e
except OSError as e:
raise ValueError(
f"Cannot read manifest from {artifact_dir}: {e}"
) from e
# Create collected artifact
artifact = CollectedArtifact(
path=artifact_dir,
shard_name=shard_name,
component_name=component_name,
architecture=architecture,
prefixes=prefixes,
)
# Store in shard collection
artifact_key = f"{component_name}_{architecture or 'generic'}"
self.shards[shard_name][artifact_key] = artifact
# Store appropriately based on type
if artifact.is_generic:
# Generic artifacts: only from primary shard
if shard_name == self.primary_shard:
if component_name in self.generic_artifacts:
raise ValueError(
f"Duplicate generic artifact for component '{component_name}' "
f"in primary shard '{self.primary_shard}'"
)
self.generic_artifacts[component_name] = artifact
if self.verbose:
print(f" Found generic artifact: {component_name}")
else:
if self.verbose:
print(
f" Skipping generic artifact {component_name} (not from primary shard)"
)
else:
# Architecture-specific artifacts: from any shard
if component_name not in self.arch_artifacts:
self.arch_artifacts[component_name] = {}
if architecture in self.arch_artifacts[component_name]:
# Multiple shards have same arch - use first one found, warn if different
existing = self.arch_artifacts[component_name][architecture]
if self.verbose:
print(
f" Duplicate {architecture} artifact for '{component_name}': "
f"using {existing.shard_name}, ignoring {shard_name}"
)
else:
self.arch_artifacts[component_name][architecture] = artifact
if self.verbose:
print(
f" Found arch-specific artifact: {component_name} ({architecture})"
)
def _parse_artifact_name(self, name: str) -> ArtifactNameInfo | None:
"""
Parse artifact directory name to extract component and architecture.
Expected formats:
- Generic: {component}_generic
- Architecture-specific: {component}_gfx{arch}
Args:
name: Artifact directory name
Returns:
ArtifactNameInfo with component_name and architecture (None for generic),
or None if name doesn't match expected format
"""
# Check for generic suffix
if name.endswith("_generic"):
component_name = name[: -len("_generic")]
return ArtifactNameInfo(component_name=component_name, architecture=None)
# Check for architecture suffix (gfxXXXX)
parts = name.rsplit("_", 1)
if len(parts) == 2:
component_name, potential_arch = parts
if potential_arch.startswith("gfx"):
return ArtifactNameInfo(
component_name=component_name, architecture=potential_arch
)
return None
def get_generic_artifact(self, component_name: str) -> CollectedArtifact | None:
"""
Get generic artifact for a component (from primary shard).
Args:
component_name: Component name
Returns:
CollectedArtifact or None if not found
"""
return self.generic_artifacts.get(component_name)
def get_arch_artifact(
self, component_name: str, architecture: str
) -> CollectedArtifact | None:
"""
Get architecture-specific artifact for a component (from any shard).
Args:
component_name: Component name
architecture: Architecture (e.g., "gfx1100")
Returns:
CollectedArtifact or None if not found
"""
if component_name not in self.arch_artifacts:
return None
return self.arch_artifacts[component_name].get(architecture)
def get_available_architectures(self, component_name: str) -> list[str]:
"""
Get list of available architectures for a component (across all shards).
Args:
component_name: Component name
Returns:
List of architecture names (e.g., ["gfx1100", "gfx1101"])
"""
if component_name not in self.arch_artifacts:
return []
return sorted(self.arch_artifacts[component_name].keys())
def get_component_names(self) -> set[str]:
"""
Get all component names found in artifacts.
Returns:
Set of component names
"""
components = set(self.generic_artifacts.keys())
components.update(self.arch_artifacts.keys())
return components
def validate_availability(
self,
component_name: str,
required_architectures: list[str],
require_generic: bool = True,
) -> AvailabilityResult:
"""
Validate that required artifacts are available.
Args:
component_name: Component name
required_architectures: List of required architectures
require_generic: Whether generic artifact is required
Returns:
AvailabilityResult with available and missing architectures
Raises:
ValueError: If generic artifact is required but missing
"""
# Check generic artifact
if require_generic and component_name not in self.generic_artifacts:
raise ValueError(
f"Generic artifact not found for component '{component_name}' "
f"in primary shard '{self.primary_shard}'"
)
# Check architecture-specific artifacts
available = self.get_available_architectures(component_name)
missing = [arch for arch in required_architectures if arch not in available]
return AvailabilityResult(
available=[arch for arch in required_architectures if arch in available],
missing=missing,
)