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
833 lines
34 KiB
Python
833 lines
34 KiB
Python
#!/usr/bin/env python3
|
|
|
|
"""
|
|
Artifact splitter for separating host and device code in TheRock artifacts.
|
|
|
|
This module provides functionality to split TheRock build artifacts into:
|
|
- Generic artifacts: Host-only binaries with kpack manifest references
|
|
- Architecture-specific artifacts: Device code (kpack files and kernel databases)
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import List, Tuple, Dict, Optional, Set
|
|
from collections import defaultdict
|
|
from dataclasses import dataclass
|
|
|
|
from rocm_kpack.artifact_utils import (
|
|
extract_architecture_from_target,
|
|
is_fat_binary,
|
|
read_artifact_manifest,
|
|
scan_directory,
|
|
write_artifact_manifest,
|
|
)
|
|
from rocm_kpack.binutils import BundledBinary, Toolchain
|
|
from rocm_kpack.database_handlers import DatabaseHandler
|
|
from rocm_kpack.kpack import PackedKernelArchive
|
|
from rocm_kpack.compression import ZstdCompressor
|
|
from rocm_kpack.kpack_transform import kpack_offload_binary, NotFatBinaryError
|
|
|
|
|
|
def strip_target_features(target: str) -> str:
|
|
"""Strip GPU target feature flags (e.g. 'gfx942:sramecc+:xnack-' -> 'gfx942')."""
|
|
colon_pos = target.find(":")
|
|
return target[:colon_pos] if colon_pos >= 0 else target
|
|
|
|
|
|
# Known LLVM AMDGPU target features that ride on a base arch, in either the
|
|
# colon form (gfx942:xnack+) or the Tensile kernel-filename hyphen form
|
|
# (gfx90a-xnack-). Normalizing the hyphen form to colon lets one colon-split
|
|
# drop every feature variant regardless of how many features are present.
|
|
_HYPHEN_FEATURE_RE = re.compile(r"-(xnack|sramecc)")
|
|
|
|
|
|
def base_arch(arch: str) -> str:
|
|
"""Strip known feature suffixes in colon or hyphen form (e.g. 'gfx90a-xnack-' -> 'gfx90a')."""
|
|
return strip_target_features(_HYPHEN_FEATURE_RE.sub(r":\1", arch))
|
|
|
|
|
|
@dataclass
|
|
class ExtractedKernel:
|
|
"""Represents a kernel extracted from a fat binary."""
|
|
|
|
target_name: str # Target identifier from bundler (e.g., "hip-amdgcn-amd-amdhsa-gfx1100")
|
|
kernel_data: bytes # The actual kernel binary data
|
|
source_binary_relpath: str # Path to the original fat binary relative to prefix
|
|
source_prefix: str # The prefix this kernel came from (e.g., "math-libs/BLAS/rocBLAS/stage")
|
|
architecture: str # Architecture (e.g., "gfx1100")
|
|
|
|
|
|
@dataclass
|
|
class KpackInfo:
|
|
"""Information about a created kpack file."""
|
|
|
|
kpack_path: Path # Path to the kpack file
|
|
size: int # Size of the kpack file in bytes
|
|
kernel_count: int # Number of kernels in the kpack
|
|
|
|
|
|
class FileClassificationVisitor:
|
|
"""Visitor that accumulates file classification results during tree scanning."""
|
|
|
|
def __init__(
|
|
self,
|
|
toolchain: Toolchain,
|
|
database_handlers: Optional[List[DatabaseHandler]] = None,
|
|
verbose: bool = False,
|
|
gpu_targets: Optional[Set[str]] = None,
|
|
):
|
|
"""
|
|
Initialize the classification visitor.
|
|
|
|
Args:
|
|
toolchain: Toolchain instance for binary operations
|
|
database_handlers: List of database handler instances
|
|
verbose: Enable verbose output
|
|
gpu_targets: If set, only produce per-arch artifacts for these GPU targets
|
|
"""
|
|
self.toolchain = toolchain
|
|
self.database_handlers = database_handlers or []
|
|
self.verbose = verbose
|
|
self.gpu_targets = gpu_targets
|
|
|
|
# Accumulated results
|
|
self.fat_binaries: List[Path] = []
|
|
self.database_files_by_arch: Dict[
|
|
str, List[Tuple[Path, DatabaseHandler]]
|
|
] = defaultdict(list)
|
|
self.exclude_from_generic: Set[Path] = set()
|
|
|
|
def visit_file(self, file_path: Path, prefix_path: Path) -> None:
|
|
"""
|
|
Visit a file and classify it.
|
|
|
|
Args:
|
|
file_path: Path to the file
|
|
prefix_path: Root of the prefix for relative path computation
|
|
"""
|
|
if not file_path.is_file():
|
|
return
|
|
|
|
# Check database handlers first — some files (e.g. MIOpen CK per-arch
|
|
# .so files) are also fat binaries, but compiled per-arch. Matching
|
|
# them here prevents them from entering the kpack processing path.
|
|
for handler in self.database_handlers:
|
|
arch = handler.detect(file_path, prefix_path)
|
|
if arch:
|
|
# detect() returns the Tensile hyphen form ('gfx90a-xnack-');
|
|
# collapse onto the bare base arch used by gpu_targets and the
|
|
# shard key so xnack variants are not dropped (ROCM-25535).
|
|
arch = base_arch(arch)
|
|
if self.gpu_targets is not None and arch not in self.gpu_targets:
|
|
if self.verbose:
|
|
print(
|
|
f" Skipping {handler.name()} file for {arch} "
|
|
f"(not in gpu_targets): {file_path.relative_to(prefix_path)}"
|
|
)
|
|
# Exclude from generic (per-arch content doesn't belong
|
|
# there) but don't add to database_files_by_arch so no
|
|
# per-arch artifact is created. The correct arch job will
|
|
# produce this file. The return also prevents the file
|
|
# from falling through to the fat binary / kpack path.
|
|
self.exclude_from_generic.add(file_path)
|
|
return
|
|
self.database_files_by_arch[arch].append((file_path, handler))
|
|
self.exclude_from_generic.add(file_path)
|
|
if self.verbose:
|
|
print(
|
|
f" Found {handler.name()} database file for {arch}: {file_path.relative_to(prefix_path)}"
|
|
)
|
|
return # First matching handler wins
|
|
|
|
# Check if it's a fat binary
|
|
if is_fat_binary(file_path, self.toolchain):
|
|
self.fat_binaries.append(file_path)
|
|
if self.verbose:
|
|
print(f" Found fat binary: {file_path.relative_to(prefix_path)}")
|
|
|
|
def get_statistics(self) -> str:
|
|
"""Get a summary of classification results."""
|
|
total_db_files = sum(
|
|
len(files) for files in self.database_files_by_arch.values()
|
|
)
|
|
return (
|
|
f" Total: {len(self.fat_binaries)} fat binaries, "
|
|
f"{total_db_files} database files, "
|
|
f"{len(self.exclude_from_generic)} files to exclude from generic"
|
|
)
|
|
|
|
|
|
class GenericCopyVisitor:
|
|
"""Visitor that copies files to generic artifact, excluding marked files."""
|
|
|
|
def __init__(
|
|
self,
|
|
exclude_files: Set[Path],
|
|
source_prefix: Path,
|
|
dest_prefix: Path,
|
|
verbose: bool = False,
|
|
):
|
|
"""
|
|
Initialize the copy visitor.
|
|
|
|
Args:
|
|
exclude_files: Set of files to exclude from copying
|
|
source_prefix: Source prefix directory
|
|
dest_prefix: Destination prefix directory
|
|
verbose: Enable verbose output
|
|
"""
|
|
self.exclude_files = exclude_files
|
|
self.source_prefix = source_prefix
|
|
self.dest_prefix = dest_prefix
|
|
self.verbose = verbose
|
|
self.copied_count = 0
|
|
self.excluded_count = 0
|
|
|
|
def visit_file(self, file_path: Path) -> None:
|
|
"""
|
|
Visit a file and copy it if not excluded.
|
|
|
|
Args:
|
|
file_path: Path to the file
|
|
"""
|
|
if file_path in self.exclude_files:
|
|
self.excluded_count += 1
|
|
if self.verbose:
|
|
rel_path = file_path.relative_to(self.source_prefix)
|
|
print(f" Excluding: {rel_path}")
|
|
return
|
|
|
|
# Copy the file preserving structure
|
|
rel_path = file_path.relative_to(self.source_prefix)
|
|
dest_path = self.dest_prefix / rel_path
|
|
|
|
# Create parent directories
|
|
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Handle symlinks vs regular files
|
|
if file_path.is_symlink():
|
|
# Preserve symlink (don't follow)
|
|
link_target = os.readlink(file_path)
|
|
if dest_path.exists() or dest_path.is_symlink():
|
|
dest_path.unlink()
|
|
os.symlink(link_target, dest_path)
|
|
else:
|
|
# Copy regular file
|
|
shutil.copy2(file_path, dest_path)
|
|
self.copied_count += 1
|
|
|
|
def get_statistics(self) -> str:
|
|
"""Get a summary of copy results."""
|
|
return (
|
|
f" Copied {self.copied_count} files, excluded {self.excluded_count} files"
|
|
)
|
|
|
|
|
|
class ArtifactSplitter:
|
|
"""Splits TheRock artifacts into generic and architecture-specific components."""
|
|
|
|
def __init__(
|
|
self,
|
|
artifact_prefix: str,
|
|
toolchain: Toolchain,
|
|
database_handlers: Optional[List[DatabaseHandler]] = None,
|
|
verbose: bool = False,
|
|
gpu_targets: Optional[List[str]] = None,
|
|
):
|
|
"""
|
|
Initialize the artifact splitter.
|
|
|
|
Args:
|
|
artifact_prefix: Artifact prefix name_component (e.g., 'hip_lib', 'blas_lib')
|
|
toolchain: Toolchain instance for binary operations
|
|
database_handlers: Optional list of DatabaseHandler instances for kernel databases
|
|
verbose: Enable verbose output
|
|
gpu_targets: If set, only create per-arch artifacts for these GPU targets.
|
|
Target feature flags are stripped (e.g. 'gfx942:sramecc+:xnack-' -> 'gfx942').
|
|
"""
|
|
self.artifact_prefix = artifact_prefix
|
|
self.toolchain = toolchain
|
|
self.database_handlers = database_handlers or []
|
|
self.verbose = verbose
|
|
self.gpu_targets: Optional[Set[str]] = (
|
|
{base_arch(t) for t in gpu_targets} if gpu_targets else None
|
|
)
|
|
|
|
def compute_kpack_search_pattern(self, binary_path: Path, prefix_root: Path) -> str:
|
|
"""
|
|
Compute the @GFXARCH@ search pattern from a binary to its kpack files.
|
|
|
|
The pattern uses @GFXARCH@ as a placeholder that the runtime expands
|
|
with the requested GPU architecture at load time. This eliminates the
|
|
need for .kpm manifest files that enumerate architectures at build time.
|
|
|
|
Args:
|
|
binary_path: Path to the binary
|
|
prefix_root: Root of the prefix (where .kpack/ directory will be)
|
|
|
|
Returns:
|
|
Relative path pattern like .kpack/{artifact_prefix}_@GFXARCH@.kpack
|
|
"""
|
|
# Get the relative path from prefix root to binary
|
|
rel_path = binary_path.relative_to(prefix_root)
|
|
|
|
# Count directory levels (excluding the binary file itself)
|
|
depth = len(rel_path.parts) - 1
|
|
|
|
# Build the relative path to .kpack directory with @GFXARCH@ pattern
|
|
kpack_pattern = f".kpack/{self.artifact_prefix}_@GFXARCH@.kpack"
|
|
if depth == 0:
|
|
# Binary is at prefix root
|
|
search_pattern = kpack_pattern
|
|
else:
|
|
# Binary is in subdirectories
|
|
up_path = "/".join([".."] * depth)
|
|
search_pattern = f"{up_path}/{kpack_pattern}"
|
|
|
|
if self.verbose:
|
|
print(f" Binary at: {rel_path}")
|
|
print(f" Search pattern: {search_pattern}")
|
|
|
|
return search_pattern
|
|
|
|
def scan_prefix(
|
|
self, prefix_path: Path, visitor: FileClassificationVisitor
|
|
) -> None:
|
|
"""
|
|
Scan a prefix directory and classify files using the visitor.
|
|
|
|
Args:
|
|
prefix_path: Path to the prefix directory
|
|
visitor: Visitor to accumulate classification results
|
|
"""
|
|
if self.verbose:
|
|
print(f"Scanning prefix: {prefix_path}")
|
|
|
|
# Walk through all files in the prefix using robust directory traversal
|
|
for file_path, direntry in scan_directory(prefix_path):
|
|
if direntry.is_file(follow_symlinks=False):
|
|
visitor.visit_file(file_path, prefix_path)
|
|
|
|
if self.verbose:
|
|
print(visitor.get_statistics())
|
|
|
|
def copy_generic_artifact(
|
|
self, prefix_path: Path, dest_prefix: Path, exclude_files: Set[Path]
|
|
) -> None:
|
|
"""
|
|
Copy files to generic artifact, excluding marked files.
|
|
|
|
Args:
|
|
prefix_path: Source prefix directory
|
|
dest_prefix: Destination prefix directory
|
|
exclude_files: Set of files to exclude from copying
|
|
"""
|
|
if self.verbose:
|
|
print(f" Creating generic artifact (excluding {len(exclude_files)} files)")
|
|
|
|
copy_visitor = GenericCopyVisitor(
|
|
exclude_files, prefix_path, dest_prefix, self.verbose
|
|
)
|
|
|
|
# Walk through all files and copy non-excluded ones using robust traversal
|
|
for file_path, direntry in scan_directory(prefix_path):
|
|
# Handle both regular files and symlinks
|
|
if direntry.is_file(follow_symlinks=False) or direntry.is_symlink():
|
|
copy_visitor.visit_file(file_path)
|
|
|
|
if self.verbose:
|
|
print(copy_visitor.get_statistics())
|
|
|
|
def process_fat_binaries(
|
|
self, fat_binaries: List[Path], prefix: str, prefix_path: Path
|
|
) -> Dict[str, List[ExtractedKernel]]:
|
|
"""
|
|
Process fat binaries to extract device code.
|
|
|
|
Args:
|
|
fat_binaries: List of fat binary paths
|
|
prefix: The prefix string (from artifact_manifest.txt)
|
|
prefix_path: The actual prefix directory path
|
|
|
|
Returns:
|
|
Dictionary of architecture to list of ExtractedKernel objects
|
|
"""
|
|
kernels_by_arch: Dict[str, List[ExtractedKernel]] = defaultdict(list)
|
|
|
|
for binary_path in fat_binaries:
|
|
if self.verbose:
|
|
print(f"Processing fat binary: {binary_path.relative_to(prefix_path)}")
|
|
|
|
# Create a BundledBinary instance with our toolchain
|
|
binary = BundledBinary(binary_path, toolchain=self.toolchain)
|
|
|
|
# Track code object index per (binary, arch) for multi-TU support
|
|
# Each TU in a -fgpu-rdc binary gets a unique index
|
|
code_object_index: Dict[str, int] = defaultdict(int)
|
|
|
|
# Extract kernels using context manager
|
|
with binary.unbundle() as unbundled:
|
|
# Process each unbundled target
|
|
for target_name, file_name in unbundled.target_list:
|
|
# Extract architecture from target name (e.g., "hip-amdgcn-amd-amdhsa-gfx1100")
|
|
arch = extract_architecture_from_target(target_name)
|
|
if arch:
|
|
if self.gpu_targets is not None:
|
|
bare_arch = strip_target_features(arch)
|
|
if bare_arch not in self.gpu_targets:
|
|
code_object_index[arch] += 1
|
|
if self.verbose:
|
|
print(
|
|
f" Skipping kernel for {arch}: "
|
|
f"{file_name} (not in gpu_targets)"
|
|
)
|
|
continue
|
|
|
|
kernel_path = unbundled.dest_dir / file_name
|
|
# Read kernel data while the file still exists
|
|
kernel_data = kernel_path.read_bytes()
|
|
|
|
# Build source_binary_relpath with index suffix
|
|
# TOC always uses indexed format: "lib/foo.so#0", "lib/foo.so#1"
|
|
# This matches the co_index in wrapper's reserved1 field
|
|
base_relpath = binary_path.relative_to(prefix_path).as_posix()
|
|
index = code_object_index[arch]
|
|
code_object_index[arch] += 1
|
|
indexed_relpath = f"{base_relpath}#{index}"
|
|
|
|
# Create ExtractedKernel object
|
|
extracted_kernel = ExtractedKernel(
|
|
target_name=target_name,
|
|
kernel_data=kernel_data,
|
|
source_binary_relpath=indexed_relpath,
|
|
source_prefix=prefix,
|
|
architecture=arch,
|
|
)
|
|
|
|
kernels_by_arch[arch].append(extracted_kernel)
|
|
if self.verbose:
|
|
print(
|
|
f" Extracted kernel for {arch}: {file_name} -> {indexed_relpath} ({len(kernel_data)} bytes)"
|
|
)
|
|
|
|
return kernels_by_arch
|
|
|
|
def create_kpack_files(
|
|
self, all_kernels_by_arch: Dict[str, List[ExtractedKernel]], output_dir: Path
|
|
) -> Dict[str, KpackInfo]:
|
|
"""
|
|
Create kpack files from all extracted kernels in architecture-specific artifacts.
|
|
Creates a single kpack file per architecture, placed in the first prefix's .kpack directory.
|
|
|
|
The kpack file is placed in the original prefix directory structure (not a synthetic
|
|
"kpack/stage" prefix) so that when generic and arch-specific artifacts are overlaid
|
|
during bootstrap, the .kpack directory merges correctly with the .kpm manifest.
|
|
|
|
Args:
|
|
all_kernels_by_arch: Dictionary mapping architecture to list of ALL ExtractedKernel objects from all prefixes
|
|
output_dir: Output directory for artifacts
|
|
|
|
Returns:
|
|
Dict mapping architecture to KpackInfo
|
|
"""
|
|
if not all_kernels_by_arch:
|
|
return {}
|
|
|
|
kpack_info_by_arch = {}
|
|
|
|
if self.verbose:
|
|
print(
|
|
f"\nCreating kpack files for {len(all_kernels_by_arch)} architectures"
|
|
)
|
|
|
|
# Process each architecture
|
|
for arch, kernel_list in all_kernels_by_arch.items():
|
|
if not kernel_list:
|
|
continue
|
|
|
|
# Group kernels by prefix
|
|
kernels_by_prefix: Dict[str, List[ExtractedKernel]] = defaultdict(list)
|
|
for kernel in kernel_list:
|
|
kernels_by_prefix[kernel.source_prefix].append(kernel)
|
|
|
|
if self.verbose:
|
|
print(
|
|
f" Creating kpack for {arch} with {len(kernel_list)} kernels total"
|
|
)
|
|
for prefix, kernels in kernels_by_prefix.items():
|
|
print(f" - {len(kernels)} kernels from {prefix}")
|
|
|
|
# Create architecture-specific artifact directory
|
|
arch_artifact_name = f"{self.artifact_prefix}_{arch}"
|
|
arch_artifact_dir = output_dir / arch_artifact_name
|
|
|
|
# Use the first prefix as the location for the kpack file.
|
|
# This preserves the original directory structure so that when
|
|
# generic and arch-specific artifacts are overlaid, the .kpack/
|
|
# directory containing both .kpm (generic) and .kpack (arch) merge.
|
|
first_prefix = sorted(kernels_by_prefix.keys())[0]
|
|
kpack_prefix_dir = arch_artifact_dir / first_prefix
|
|
|
|
# Create .kpack directory (hidden)
|
|
kpack_dir = kpack_prefix_dir / ".kpack"
|
|
kpack_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create kpack archive - single file for all kernels from all prefixes
|
|
kpack_file = kpack_dir / f"{self.artifact_prefix}_{arch}.kpack"
|
|
|
|
# Create PackedKernelArchive instance
|
|
# Use the specific architecture directly, no family grouping
|
|
archive = PackedKernelArchive(
|
|
group_name=self.artifact_prefix,
|
|
gfx_arch_family=arch, # Use specific arch, not family
|
|
gfx_arches=[arch],
|
|
compressor=ZstdCompressor(),
|
|
)
|
|
|
|
# Add kernels to archive
|
|
for kernel in kernel_list:
|
|
# Build the full relative path including the source prefix
|
|
# This allows the runtime to find the kernel for the right binary
|
|
relative_path = f"{kernel.source_prefix}/{kernel.source_binary_relpath}"
|
|
|
|
# Prepare and add kernel
|
|
prepared = archive.prepare_kernel(
|
|
relative_path=relative_path,
|
|
gfx_arch=kernel.architecture,
|
|
hsaco_data=kernel.kernel_data,
|
|
metadata={
|
|
"target": kernel.target_name,
|
|
"source_prefix": kernel.source_prefix,
|
|
},
|
|
)
|
|
archive.add_kernel(prepared)
|
|
|
|
# Finalize and write archive
|
|
archive.finalize_archive()
|
|
archive.write(kpack_file)
|
|
|
|
# Validate kpack file was created successfully
|
|
if not kpack_file.exists():
|
|
raise RuntimeError(f"Failed to create kpack file: {kpack_file}")
|
|
|
|
kpack_size = kpack_file.stat().st_size
|
|
if kpack_size == 0:
|
|
raise RuntimeError(f"Kpack file is empty: {kpack_file}")
|
|
|
|
kernel_count = len(kernel_list)
|
|
kpack_info_by_arch[arch] = KpackInfo(
|
|
kpack_path=kpack_file, size=kpack_size, kernel_count=kernel_count
|
|
)
|
|
|
|
if self.verbose:
|
|
print(f" Written: {kpack_file.relative_to(kpack_prefix_dir)}")
|
|
print(f" Size: {kpack_size} bytes")
|
|
|
|
# Update or create artifact manifest for this architecture artifact
|
|
# Include all prefixes that contributed kernels
|
|
manifest_path = arch_artifact_dir / "artifact_manifest.txt"
|
|
existing_prefixes = []
|
|
if manifest_path.exists():
|
|
with open(manifest_path, "r") as f:
|
|
existing_prefixes = [line.strip() for line in f if line.strip()]
|
|
|
|
# Add all prefixes that had kernels
|
|
for prefix in kernels_by_prefix.keys():
|
|
if prefix not in existing_prefixes:
|
|
existing_prefixes.append(prefix)
|
|
|
|
write_artifact_manifest(arch_artifact_dir, sorted(existing_prefixes))
|
|
|
|
return kpack_info_by_arch
|
|
|
|
def inject_kpack_references(
|
|
self,
|
|
fat_binaries_by_prefix: Dict[str, List[Path]],
|
|
generic_artifact_dir: Path,
|
|
kpack_info_by_arch: Dict[str, KpackInfo],
|
|
):
|
|
"""
|
|
Inject kpack search patterns into fat binaries and strip device code.
|
|
|
|
Embeds @GFXARCH@ patterns in each binary's .rocm_kpack_ref section.
|
|
The runtime expands @GFXARCH@ with the requested GPU architecture at
|
|
load time, eliminating the need for .kpm manifest files.
|
|
|
|
Args:
|
|
fat_binaries_by_prefix: Dictionary mapping prefix to list of fat binary paths
|
|
generic_artifact_dir: Path to generic artifact directory
|
|
kpack_info_by_arch: Dict mapping arch to KpackInfo
|
|
"""
|
|
if self.verbose:
|
|
print("\nInjecting kpack search patterns and stripping device code")
|
|
|
|
for prefix, binary_paths in fat_binaries_by_prefix.items():
|
|
prefix_dir = generic_artifact_dir / prefix
|
|
|
|
# Inject kpack search pattern in each binary
|
|
for binary_path in binary_paths:
|
|
# Compute @GFXARCH@ search pattern from binary to .kpack directory
|
|
search_pattern = self.compute_kpack_search_pattern(
|
|
binary_path, prefix_dir
|
|
)
|
|
|
|
if self.verbose:
|
|
print(
|
|
f" Processing {binary_path.relative_to(generic_artifact_dir)}"
|
|
)
|
|
print(f" Search pattern: {search_pattern}")
|
|
|
|
# Create temporary output file
|
|
temp_output = binary_path.with_suffix(binary_path.suffix + ".kpacked")
|
|
|
|
# Record original size for validation
|
|
original_size = binary_path.stat().st_size
|
|
|
|
try:
|
|
# Compute kernel_name to match TOC key format (without index):
|
|
# "{prefix}/{binary_relpath}" e.g. "rocrand/lib/librocrand.so.1.1"
|
|
# Runtime appends "#N" based on wrapper's reserved1 (co_index)
|
|
binary_relpath = binary_path.relative_to(prefix_dir).as_posix()
|
|
kernel_name = f"{prefix}/{binary_relpath}"
|
|
|
|
# Add kpack search pattern and transform binary in one pass
|
|
# (adds .rocm_kpack_ref, maps to PT_LOAD, rewrites magic, zero-pages .hip_fatbin)
|
|
kpack_offload_binary(
|
|
input_path=binary_path,
|
|
output_path=temp_output,
|
|
kpack_search_paths=[search_pattern],
|
|
kernel_name=kernel_name,
|
|
verbose=self.verbose,
|
|
)
|
|
|
|
# Remove original first to break hard links, then move temp
|
|
# (mode already preserved by kpack_offload_binary)
|
|
binary_path.unlink()
|
|
shutil.move(temp_output, binary_path)
|
|
|
|
# Validate output was created
|
|
if not binary_path.exists():
|
|
raise RuntimeError(
|
|
f"Binary disappeared after stripping: {binary_path}"
|
|
)
|
|
|
|
new_size = binary_path.stat().st_size
|
|
# Note: Size may grow for small .hip_fatbin sections where
|
|
# structural overhead (padding, PHDR relocation) exceeds
|
|
# zero-page savings. This is acceptable for small binaries.
|
|
if self.verbose:
|
|
size_delta = new_size - original_size
|
|
if size_delta < 0:
|
|
print(
|
|
f" Device code stripped, new size: {new_size} bytes "
|
|
f"(saved {-size_delta} bytes)"
|
|
)
|
|
else:
|
|
print(
|
|
f" Processed, new size: {new_size} bytes "
|
|
f"(+{size_delta} bytes overhead)"
|
|
)
|
|
|
|
except NotFatBinaryError:
|
|
# Binary has no device code - skip silently
|
|
if self.verbose:
|
|
print(f" Skipping (no device code)")
|
|
continue
|
|
|
|
finally:
|
|
# Always clean up temp file
|
|
if temp_output.exists():
|
|
temp_output.unlink()
|
|
|
|
def process_database_files(
|
|
self,
|
|
database_files_by_arch: Dict[str, List[Tuple[Path, DatabaseHandler]]],
|
|
prefix: str,
|
|
prefix_path: Path,
|
|
output_dir: Path,
|
|
):
|
|
"""
|
|
Move kernel database files to architecture-specific artifacts.
|
|
|
|
Args:
|
|
database_files_by_arch: Dictionary of (file_path, handler) tuples by architecture
|
|
prefix: The prefix string (from artifact_manifest.txt)
|
|
prefix_path: The actual prefix directory path
|
|
output_dir: Output directory for artifacts
|
|
"""
|
|
for arch, file_handler_pairs in database_files_by_arch.items():
|
|
# Create architecture-specific artifact directory
|
|
arch_artifact_name = f"{self.artifact_prefix}_{arch}"
|
|
arch_artifact_dir = output_dir / arch_artifact_name
|
|
arch_prefix_dir = arch_artifact_dir / prefix
|
|
|
|
if self.verbose:
|
|
print(
|
|
f" Moving {len(file_handler_pairs)} database files to {arch_artifact_name}"
|
|
)
|
|
|
|
for file_path, handler in file_handler_pairs:
|
|
# Compute destination path preserving structure
|
|
rel_path = file_path.relative_to(prefix_path)
|
|
dest_path = arch_prefix_dir / rel_path
|
|
|
|
# Create parent directories
|
|
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Copy the file (will move after generic is created)
|
|
if self.verbose:
|
|
print(f" Moving: {rel_path}")
|
|
shutil.copy2(file_path, dest_path)
|
|
|
|
# Update or create artifact manifest for this architecture artifact
|
|
manifest_path = arch_artifact_dir / "artifact_manifest.txt"
|
|
existing_prefixes = []
|
|
if manifest_path.exists():
|
|
with open(manifest_path, "r") as f:
|
|
existing_prefixes = [line.strip() for line in f if line.strip()]
|
|
|
|
# Add current prefix if not already present
|
|
if prefix not in existing_prefixes:
|
|
existing_prefixes.append(prefix)
|
|
write_artifact_manifest(arch_artifact_dir, existing_prefixes)
|
|
|
|
def split(self, input_dir: Path, output_dir: Path):
|
|
"""
|
|
Split an artifact directory into generic and architecture-specific components.
|
|
|
|
Args:
|
|
input_dir: Input artifact directory
|
|
output_dir: Output directory for split artifacts
|
|
"""
|
|
input_dir = Path(input_dir)
|
|
output_dir = Path(output_dir)
|
|
|
|
if not input_dir.exists():
|
|
raise FileNotFoundError(f"Input directory does not exist: {input_dir}")
|
|
|
|
# Create output directory
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Read artifact manifest
|
|
prefixes = read_artifact_manifest(input_dir)
|
|
if self.verbose:
|
|
print(f"Found {len(prefixes)} prefixes in artifact manifest:")
|
|
for prefix in prefixes:
|
|
print(f" - {prefix}")
|
|
|
|
# Accumulate all kernels from all prefixes
|
|
all_kernels_by_arch: Dict[str, List[ExtractedKernel]] = defaultdict(list)
|
|
|
|
# Track fat binaries by prefix for later processing
|
|
fat_binaries_by_prefix: Dict[str, List[Path]] = {}
|
|
|
|
# Track prefixes that were actually processed (for manifest)
|
|
processed_prefixes: List[str] = []
|
|
|
|
# Process each prefix
|
|
for prefix in prefixes:
|
|
prefix_path = input_dir / prefix
|
|
|
|
if not prefix_path.exists():
|
|
# Skip empty prefixes (directories that had no files may not be created in artifact)
|
|
if self.verbose:
|
|
print(f"\nSkipping prefix (directory does not exist): {prefix}")
|
|
continue
|
|
|
|
if self.verbose:
|
|
print(f"\nProcessing prefix: {prefix}")
|
|
|
|
# Phase 1: Classify files using visitor
|
|
classifier = FileClassificationVisitor(
|
|
self.toolchain,
|
|
self.database_handlers,
|
|
self.verbose,
|
|
gpu_targets=self.gpu_targets,
|
|
)
|
|
self.scan_prefix(prefix_path, classifier)
|
|
|
|
# Phase 2: Process database files (move to arch-specific artifacts)
|
|
if self.database_handlers and classifier.database_files_by_arch:
|
|
self.process_database_files(
|
|
classifier.database_files_by_arch, prefix, prefix_path, output_dir
|
|
)
|
|
|
|
# Phase 3: Create generic artifact (excluding database files)
|
|
generic_artifact_name = f"{self.artifact_prefix}_generic"
|
|
generic_artifact_dir = output_dir / generic_artifact_name
|
|
generic_prefix_dir = generic_artifact_dir / prefix
|
|
|
|
if self.verbose:
|
|
print(f"Creating generic artifact: {generic_artifact_name}")
|
|
|
|
# Copy files excluding those marked for exclusion
|
|
self.copy_generic_artifact(
|
|
prefix_path, generic_prefix_dir, classifier.exclude_from_generic
|
|
)
|
|
|
|
# Track this prefix for the manifest
|
|
processed_prefixes.append(prefix)
|
|
|
|
# Phase 4: Process fat binaries and accumulate kernels
|
|
if classifier.fat_binaries:
|
|
kernels_by_arch = self.process_fat_binaries(
|
|
classifier.fat_binaries, prefix, prefix_path
|
|
)
|
|
|
|
# Accumulate kernels from this prefix
|
|
for arch, kernels in kernels_by_arch.items():
|
|
all_kernels_by_arch[arch].extend(kernels)
|
|
|
|
# Track fat binaries in the generic artifact for later processing
|
|
fat_binaries_in_generic = []
|
|
for binary_path in classifier.fat_binaries:
|
|
generic_binary_path = generic_prefix_dir / binary_path.relative_to(
|
|
prefix_path
|
|
)
|
|
if generic_binary_path.exists():
|
|
fat_binaries_in_generic.append(generic_binary_path)
|
|
|
|
if fat_binaries_in_generic:
|
|
fat_binaries_by_prefix[prefix] = fat_binaries_in_generic
|
|
|
|
# Phase 5: Create kpack files from all accumulated kernels
|
|
kpack_info_by_arch = {}
|
|
if all_kernels_by_arch:
|
|
kpack_info_by_arch = self.create_kpack_files(
|
|
all_kernels_by_arch, output_dir
|
|
)
|
|
|
|
if (
|
|
fat_binaries_by_prefix
|
|
and self.gpu_targets is not None
|
|
and not kpack_info_by_arch
|
|
):
|
|
target_list = ", ".join(sorted(self.gpu_targets))
|
|
fat_binary_count = sum(
|
|
len(paths) for paths in fat_binaries_by_prefix.values()
|
|
)
|
|
raise RuntimeError(
|
|
f"Found {fat_binary_count} fat binaries, but no device code objects "
|
|
f"matched --gpu-targets ({target_list}). Refusing to emit an "
|
|
"untransformed generic artifact."
|
|
)
|
|
|
|
# Phase 6: Inject kpack references and strip device code from fat binaries
|
|
if fat_binaries_by_prefix and kpack_info_by_arch:
|
|
generic_artifact_dir = output_dir / f"{self.artifact_prefix}_generic"
|
|
self.inject_kpack_references(
|
|
fat_binaries_by_prefix, generic_artifact_dir, kpack_info_by_arch
|
|
)
|
|
|
|
# Phase 7: Write artifact manifest for generic artifact with all prefixes
|
|
# Always write manifest, even if empty (ensures downstream tools find it)
|
|
generic_artifact_dir = output_dir / f"{self.artifact_prefix}_generic"
|
|
if not generic_artifact_dir.exists():
|
|
generic_artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
write_artifact_manifest(generic_artifact_dir, processed_prefixes)
|
|
|
|
if self.verbose:
|
|
print(f"\nSplitting complete. Output in: {output_dir}")
|