Files
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

379 lines
13 KiB
Python

"""Artifact scanner for walking directory trees and categorizing ROCm artifacts.
This module provides infrastructure for scanning ROCm installation trees and
identifying different categories of artifacts:
- Opaque files (to be copied verbatim)
- Bundled binaries (fat binaries with multi-arch device code)
- Kernel databases (library-specific kernel collections)
"""
import subprocess
from abc import ABC, abstractmethod
from concurrent.futures import Executor, as_completed
from dataclasses import dataclass
from pathlib import Path
from typing import Iterator
from rocm_kpack.binutils import BundledBinary, Toolchain
@dataclass(frozen=True)
class ArtifactPath:
"""Represents a path relative to a scan root.
This dual representation allows consumers to work with relative paths
(for materializing altered copies) while still having access to the
absolute path for reading the original files.
Attributes:
root_dir: The root directory being scanned
relative_path: Path relative to root_dir
"""
root_dir: Path
relative_path: Path
@property
def absolute_path(self) -> Path:
"""Compute the absolute path by joining root_dir and relative_path."""
return self.root_dir / self.relative_path
class KernelArtifact:
"""Represents a single kernel artifact within a database.
Attributes:
relative_path: Path relative to the database root
gfx_target: GPU architecture (e.g., 'gfx1100', 'gfx1201')
artifact_type: Type of artifact ('code_object', 'metadata', 'hsaco', etc.)
"""
def __init__(self, relative_path: Path, gfx_target: str, artifact_type: str):
self.relative_path = relative_path
self.gfx_target = gfx_target
self.artifact_type = artifact_type
def __repr__(self):
return (
f"KernelArtifact(relative_path={self.relative_path}, "
f"gfx_target={self.gfx_target}, artifact_type={self.artifact_type})"
)
class KernelDatabase(ABC):
"""Base class for different kernel database formats.
A kernel database represents a collection of GPU kernels organized by
architecture. Different ROCm libraries use different formats (paired files,
SQLite databases, etc.).
"""
def __init__(self, artifact_path: ArtifactPath):
self.artifact_path = artifact_path
@property
def root_dir(self) -> Path:
"""Scan root directory."""
return self.artifact_path.root_dir
@property
def relative_path(self) -> Path:
"""Path relative to scan root."""
return self.artifact_path.relative_path
@property
def absolute_path(self) -> Path:
"""Absolute path to database root."""
return self.artifact_path.absolute_path
@abstractmethod
def get_architectures(self) -> list[str]:
"""Get list of gfx targets (e.g., ['gfx1100', 'gfx1101'])."""
@abstractmethod
def get_kernel_artifacts(self) -> Iterator[KernelArtifact]:
"""Iterate over kernel artifacts (paths relative to database root)."""
class ArtifactVisitor(ABC):
"""Visitor interface for processing discovered artifacts.
Implement this interface to handle different categories of artifacts
discovered during tree scanning.
"""
def visit_opaque_file(self, artifact_path: ArtifactPath) -> None:
"""Called for files that should be copied verbatim.
Args:
artifact_path: Contains root_dir and relative_path
"""
pass
def visit_bundled_binary(
self, artifact_path: ArtifactPath, bundled_binary: BundledBinary
) -> None:
"""Called for fat binaries with bundled single-arch hsaco files.
Args:
artifact_path: Location of the bundled binary
bundled_binary: Parsed binary object
"""
pass
def visit_kernel_database(
self, artifact_path: ArtifactPath, database: KernelDatabase
) -> None:
"""Called for recognized kernel database structures.
Args:
artifact_path: Root path of the database (e.g., lib/hipblaslt/library)
database: Parsed database object
"""
pass
class DatabaseRecognizer(ABC):
"""Plugin interface for recognizing kernel database formats.
Implement this to add support for new kernel database formats.
"""
@abstractmethod
def can_recognize(self, artifact_path: ArtifactPath) -> bool:
"""Quick check if this path might be a database of this type.
This should be a fast heuristic check (e.g., checking file extensions
or directory names) to avoid expensive parsing operations.
Args:
artifact_path: Path to check
Returns:
True if this recognizer might handle this path
"""
@abstractmethod
def recognize(self, artifact_path: ArtifactPath) -> KernelDatabase | None:
"""Parse and return the database, or None if not recognized.
This is called after can_recognize returns True and should perform
more thorough validation and parsing.
Args:
artifact_path: Path to parse
Returns:
KernelDatabase instance if recognized, None otherwise
"""
class RecognizerRegistry:
"""Registry of database recognizers.
Maintains a list of recognizers and tries them in order until one succeeds.
"""
def __init__(self):
self.recognizers: list[DatabaseRecognizer] = []
def register(self, recognizer: DatabaseRecognizer) -> None:
"""Register a new recognizer plugin.
Recognizers are tried in registration order.
Args:
recognizer: The recognizer to register
"""
self.recognizers.append(recognizer)
def try_recognize(self, artifact_path: ArtifactPath) -> KernelDatabase | None:
"""Try all recognizers until one succeeds.
Args:
artifact_path: Path to attempt recognition on
Returns:
KernelDatabase instance if recognized, None otherwise
"""
for recognizer in self.recognizers:
if recognizer.can_recognize(artifact_path):
result = recognizer.recognize(artifact_path)
if result:
return result
return None
class ArtifactScanner:
"""Scans a directory tree and categorizes artifacts.
Uses a registry of recognizers to identify kernel databases, and falls back
to treating files as opaque or bundled binaries.
Supports parallel scanning when provided with an executor.
"""
def __init__(
self,
recognizer_registry: RecognizerRegistry,
toolchain: Toolchain | None = None,
executor: Executor | None = None,
):
"""Initialize the scanner.
Args:
recognizer_registry: Registry of database recognizers
toolchain: Toolchain for bundled binary operations (optional)
executor: Executor for parallel scanning (optional, default: sequential)
"""
self.registry = recognizer_registry
self.toolchain = toolchain
self.executor = executor
# Track relative paths of visited databases to avoid double-visiting
self._visited_database_paths: set[Path] = set()
def scan_tree(self, root_dir: Path, visitor: ArtifactVisitor) -> None:
"""Walk the tree and invoke visitor callbacks.
If an executor was provided, processes paths in parallel.
Otherwise, processes sequentially.
Paths are streamed incrementally - processing begins immediately
as paths are discovered, rather than collecting all paths upfront.
Args:
root_dir: Root directory to scan (stored as root for all ArtifactPaths)
visitor: Visitor to receive callbacks (must be thread-safe if using executor)
"""
if self.executor is None:
# Sequential processing: process as we walk
for abs_path in self._walk_tree(root_dir):
relative_path = abs_path.relative_to(root_dir)
artifact_path = ArtifactPath(root_dir, relative_path)
self._process_path(artifact_path, visitor)
else:
# Parallel processing: submit to executor as we walk
futures = []
for abs_path in self._walk_tree(root_dir):
relative_path = abs_path.relative_to(root_dir)
artifact_path = ArtifactPath(root_dir, relative_path)
future = self.executor.submit(
self._process_path, artifact_path, visitor
)
futures.append(future)
# Wait for all to complete (will raise exceptions if any occurred)
for future in as_completed(futures):
future.result() # Propagate exceptions
def _walk_tree(self, root_dir: Path) -> Iterator[Path]:
"""Walk the directory tree, yielding all paths.
Args:
root_dir: Root directory to walk
Yields:
Absolute paths to all files and directories
"""
# Use sorted for deterministic ordering in tests
for path in sorted(root_dir.rglob("*")):
yield path
def _process_path(
self, artifact_path: ArtifactPath, visitor: ArtifactVisitor
) -> None:
"""Process a single path and invoke appropriate visitor callback.
Args:
artifact_path: Path to process
visitor: Visitor to invoke
"""
# Skip if already visited as part of a database
if any(
artifact_path.relative_path.is_relative_to(db_rel_path)
for db_rel_path in self._visited_database_paths
):
return
# Try database recognition (directories only)
if artifact_path.absolute_path.is_dir():
database = self.registry.try_recognize(artifact_path)
if database:
visitor.visit_kernel_database(artifact_path, database)
self._visited_database_paths.add(artifact_path.relative_path)
return
# Try bundled binary detection
if (
artifact_path.absolute_path.is_file()
and self.toolchain
and self._is_bundled_binary(artifact_path)
):
bb = BundledBinary(artifact_path.absolute_path, toolchain=self.toolchain)
visitor.visit_bundled_binary(artifact_path, bb)
return
# Default: opaque file
if artifact_path.absolute_path.is_file():
visitor.visit_opaque_file(artifact_path)
def _is_bundled_binary(self, artifact_path: ArtifactPath) -> bool:
"""Check if a file is a bundled binary with device code.
Detects ELF binaries (executables and shared libraries) that contain
bundled GPU device code in .hip_fatbin sections.
TODO: Add support for Windows PE/COFF binaries. This currently only
handles ELF binaries using readelf. For COFF binaries, we'll need
to use a different approach (e.g., llvm-objdump or parse PE headers).
The repackaging tooling will run on Linux but needs to process both
Linux ELF and Windows COFF binaries.
Args:
artifact_path: Path to check
Returns:
True if this is a bundled binary with .hip_fatbin section
"""
file_path = artifact_path.absolute_path
# Skip symlinks - they just point to the actual versioned .so files
# We want to process the actual file, not the symlink
if file_path.is_symlink():
return False
# Use file content introspection instead of extension checking.
# Check ELF magic bytes to determine if this is an ELF binary.
# This is more robust than extension checking and handles edge cases
# like renamed files or non-standard extensions.
#
# ELF magic: First 4 bytes are \x7fELF
try:
with open(file_path, "rb") as f:
magic = f.read(4)
if magic != b"\x7fELF":
return False
except (OSError, IOError):
# Can't read file, skip
return False
# Check for .hip_fatbin ELF section using readelf
# This only works for ELF binaries (Linux)
try:
result = subprocess.run(
["readelf", "-S", str(file_path)],
capture_output=True,
text=True,
check=False, # Don't raise on non-zero exit
)
# readelf succeeds for ELF files, fails for non-ELF
if result.returncode == 0:
return ".hip_fatbin" in result.stdout
except FileNotFoundError:
# readelf not available - fall through to return False
pass
# For non-ELF files (Windows COFF) or if readelf fails, return False for now
# TODO: Implement COFF detection when we have Windows test assets
return False