Files
TheRock/rocm-systems/shared/kpack/test_generation/build_test_bundles.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

700 lines
24 KiB
Python
Executable File

#!/usr/bin/env python3
"""Build script for generating test bundled binaries.
Generates various permutations of bundled binaries for testing unbundling
functionality across different platforms and compiler configurations.
Usage:
python build_test_bundles.py [--rocm-path PATH]
"""
import argparse
import os
import platform
import subprocess
import sys
from pathlib import Path
class BundleBuilder:
"""Builder for HIP bundled binary test assets."""
def __init__(self, rocm_path: Path | None = None):
"""Initialize the builder.
Args:
rocm_path: Path to ROCm installation (auto-detected if None)
"""
self.script_dir = Path(__file__).parent
self.kernel_src = self.script_dir / "simple_kernel.hip"
self.host_only_src = self.script_dir / "host_only.cpp"
self.multi_wrapper_src1 = self.script_dir / "multi_wrapper_kernel1.hip"
self.multi_wrapper_src2 = self.script_dir / "multi_wrapper_kernel2.hip"
# Detect platform first (needed by other methods)
system = platform.system().lower()
self.platform = "windows" if system == "windows" else "linux"
# Find ROCm and tools
self.rocm_path = self._find_rocm(rocm_path)
self.hipcc = self._find_hipcc()
self.clang = self._find_clang()
# Detect code object version
self.code_object_version = self._detect_code_object_version()
# Setup output directory
self.output_dir = (
self.script_dir.parent
/ "test_assets"
/ "bundled_binaries"
/ self.platform
/ f"cov{self.code_object_version}"
)
def _find_rocm(self, explicit_path: Path | None) -> Path:
"""Find ROCm installation path.
Args:
explicit_path: Explicit ROCm path if provided
Returns:
Path to ROCm installation
Raises:
RuntimeError: If ROCm cannot be found
"""
if explicit_path:
if explicit_path.exists():
return explicit_path
raise RuntimeError(f"Specified ROCm path does not exist: {explicit_path}")
# Check environment variables
for env_var in ["ROCM_PATH", "HIP_PATH"]:
env_path = os.environ.get(env_var)
if env_path:
path = Path(env_path)
if path.exists():
return path
raise RuntimeError(
"Could not find ROCm installation. "
"Please specify with --rocm-path or set ROCM_PATH/HIP_PATH environment variable."
)
def _find_hipcc(self) -> Path:
"""Find hipcc compiler.
Returns:
Path to hipcc executable
Raises:
RuntimeError: If hipcc cannot be found
"""
# Try in ROCm bin directory
hipcc_name = "hipcc.bat" if self.platform == "windows" else "hipcc"
hipcc = self.rocm_path / "bin" / hipcc_name
if hipcc.exists():
return hipcc
raise RuntimeError(f"Could not find hipcc at {hipcc}")
def _find_clang(self) -> Path:
"""Find clang++ compiler for host-only builds.
Returns:
Path to clang++ executable
Raises:
RuntimeError: If clang++ cannot be found
"""
# Try in ROCm LLVM bin directory
clang_name = "clang++.exe" if self.platform == "windows" else "clang++"
clang = self.rocm_path / "lib" / "llvm" / "bin" / clang_name
if clang.exists():
return clang
# Try in ROCm llvm/bin directory (alternative layout)
clang = self.rocm_path / "llvm" / "bin" / clang_name
if clang.exists():
return clang
raise RuntimeError(
f"Could not find clang++ in ROCm installation at {self.rocm_path}"
)
def _detect_code_object_version(self) -> str:
"""Detect code object version from compiler.
Returns:
Code object version string (e.g., "5", "6")
"""
try:
result = subprocess.run(
[str(self.hipcc), "--version"],
capture_output=True,
text=True,
check=True,
)
# Try to extract code object version from output
for line in result.stdout.splitlines():
if "code object version" in line.lower():
# Extract version number
parts = line.split()
for part in parts:
if part.isdigit():
return part
# Default to 5 if not found
return "5"
except subprocess.CalledProcessError:
return "5"
def _run_hipcc(self, args: list[str], output_file: Path) -> bool:
"""Run hipcc with given arguments.
Args:
args: Arguments to pass to hipcc
output_file: Output file path
Returns:
True if successful, False otherwise
"""
cmd = [str(self.hipcc)] + args
print(f" Running: {' '.join(cmd)}")
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
if output_file.exists():
size_kb = output_file.stat().st_size / 1024
print(f" ✓ Created: {output_file.name} ({size_kb:.1f} KB)")
return True
else:
print(f" ✗ Failed: Output file not created")
return False
except subprocess.CalledProcessError as e:
print(f" ✗ Failed: {e}")
print(f" stdout: {e.stdout}")
print(f" stderr: {e.stderr}")
return False
def _run_clang(self, args: list[str], output_file: Path) -> bool:
"""Run clang++ with given arguments.
Args:
args: Arguments to pass to clang++
output_file: Output file path
Returns:
True if successful, False otherwise
"""
cmd = [str(self.clang)] + args
print(f" Running: {' '.join(cmd)}")
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
if output_file.exists():
size_kb = output_file.stat().st_size / 1024
print(f" ✓ Created: {output_file.name} ({size_kb:.1f} KB)")
return True
else:
print(f" ✗ Failed: Output file not created")
return False
except subprocess.CalledProcessError as e:
print(f" ✗ Failed: {e}")
print(f" stdout: {e.stdout}")
print(f" stderr: {e.stderr}")
return False
def build_exe_compressed(self):
"""Build executable with compressed bundles (if supported)."""
exe_name = "test_kernel_compressed.exe"
print(f"\nBuilding: {exe_name} (gfx1100,gfx1101 with compression)")
output = self.output_dir / exe_name
# Try with compression flag
success = self._run_hipcc(
[
str(self.kernel_src),
"--offload-arch=gfx1100",
"--offload-arch=gfx1101",
"-mllvm",
"--offload-compress",
"-o",
str(output),
"-fno-gpu-rdc",
"-DBUILD_EXECUTABLE",
],
output,
)
if not success:
print(" ⚠ Compression may not be supported, trying without flag")
success = self._run_hipcc(
[
str(self.kernel_src),
"--offload-arch=gfx1100",
"--offload-arch=gfx1101",
"-o",
str(output),
"-fno-gpu-rdc",
"-DBUILD_EXECUTABLE",
],
output,
)
return success
def build_exe_wide_arch(self):
"""Build executable with wide architecture coverage."""
exe_name = "test_kernel_wide.exe"
print(f"\nBuilding: {exe_name} (gfx900,gfx906,gfx908,gfx90a,gfx1100)")
output = self.output_dir / exe_name
return self._run_hipcc(
[
str(self.kernel_src),
"--offload-arch=gfx900",
"--offload-arch=gfx906",
"--offload-arch=gfx908",
"--offload-arch=gfx90a",
"--offload-arch=gfx1100",
"-o",
str(output),
"-fno-gpu-rdc",
"-DBUILD_EXECUTABLE",
],
output,
)
def build_executable_single_arch(self):
"""Build executable with single architecture."""
exe_name = "test_kernel_single.exe"
print(f"\nBuilding: {exe_name} (gfx1100 executable)")
output = self.output_dir / exe_name
return self._run_hipcc(
[
str(self.kernel_src),
"--offload-arch=gfx1100",
"-o",
str(output),
"-fno-gpu-rdc",
"-DBUILD_EXECUTABLE",
],
output,
)
def build_executable_multi_arch(self):
"""Build executable with multiple architectures."""
exe_name = "test_kernel_multi.exe"
print(f"\nBuilding: {exe_name} (gfx1100,gfx1101 executable)")
output = self.output_dir / exe_name
return self._run_hipcc(
[
str(self.kernel_src),
"--offload-arch=gfx1100",
"--offload-arch=gfx1101",
"-o",
str(output),
"-fno-gpu-rdc",
"-DBUILD_EXECUTABLE",
],
output,
)
def build_shared_lib_single_arch(self):
"""Build shared library with single architecture."""
if self.platform == "windows":
lib_name = "test_kernel_single.dll"
else:
lib_name = "libtest_kernel_single.so"
print(f"\nBuilding: {lib_name} (gfx1100 shared library)")
output = self.output_dir / lib_name
args = [
str(self.kernel_src),
"--offload-arch=gfx1100",
"-o",
str(output),
"-fno-gpu-rdc",
"-shared",
]
# -fPIC is required on Linux but unsupported on Windows
if self.platform != "windows":
args.append("-fPIC")
return self._run_hipcc(args, output)
def build_shared_lib_multi_arch(self):
"""Build shared library with multiple architectures."""
if self.platform == "windows":
lib_name = "test_kernel_multi.dll"
else:
lib_name = "libtest_kernel_multi.so"
print(f"\nBuilding: {lib_name} (gfx1100,gfx1101 shared library)")
output = self.output_dir / lib_name
args = [
str(self.kernel_src),
"--offload-arch=gfx1100",
"--offload-arch=gfx1101",
"-o",
str(output),
"-fno-gpu-rdc",
"-shared",
]
# -fPIC is required on Linux but unsupported on Windows
if self.platform != "windows":
args.append("-fPIC")
return self._run_hipcc(args, output)
def build_shared_lib_multi_wrapper(self):
"""Build shared library with multiple fat binary wrappers.
This builds two separate translation units with -fgpu-rdc (relocatable
device code) and links them together. Each TU generates its own
__CudaFatBinaryWrapper in .hipFatBinSegment, resulting in multiple
wrappers in the final binary.
This is common in large projects like RCCL that use RDC builds, and
tests the kpack splitter's ability to transform ALL wrappers.
"""
if self.platform == "windows":
lib_name = "test_multi_wrapper.dll"
else:
lib_name = "libtest_multi_wrapper.so"
print(f"\nBuilding: {lib_name} (RDC build with 2 wrappers)")
# Create temp directory for intermediate object files
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
obj1 = tmpdir / "kernel1.o"
obj2 = tmpdir / "kernel2.o"
output = self.output_dir / lib_name
# Compile first TU with -fgpu-rdc
print(f" Compiling kernel1 with -fgpu-rdc...")
cmd1 = [
str(self.hipcc),
"-fgpu-rdc",
"-c",
str(self.multi_wrapper_src1),
"--offload-arch=gfx1100",
"-o",
str(obj1),
]
# -fPIC is required on Linux but unsupported on Windows
if self.platform != "windows":
cmd1.insert(-2, "-fPIC")
try:
subprocess.run(cmd1, check=True, capture_output=True, text=True)
except subprocess.CalledProcessError as e:
print(f" ✗ Failed to compile kernel1: {e.stderr}")
return False
# Compile second TU with -fgpu-rdc
print(f" Compiling kernel2 with -fgpu-rdc...")
cmd2 = [
str(self.hipcc),
"-fgpu-rdc",
"-c",
str(self.multi_wrapper_src2),
"--offload-arch=gfx1100",
"-o",
str(obj2),
]
# -fPIC is required on Linux but unsupported on Windows
if self.platform != "windows":
cmd2.insert(-2, "-fPIC")
try:
subprocess.run(cmd2, check=True, capture_output=True, text=True)
except subprocess.CalledProcessError as e:
print(f" ✗ Failed to compile kernel2: {e.stderr}")
return False
# Link with -fgpu-rdc
print(f" Linking with -fgpu-rdc...")
return self._run_hipcc(
[
"-fgpu-rdc",
str(obj1),
str(obj2),
"-shared",
"-o",
str(output),
],
output,
)
def build_shared_lib_multi_wrapper_with_debug(self):
"""Build multi-arch RDC shared library with DWARF debug info.
Same as build_shared_lib_multi_wrapper_multi_arch but compiled with -g,
which adds non-allocatable .debug_* sections to the ELF. This exercises
the section-header reordering fix: when kpack adds .rocm_kpack_ref
(SHF_ALLOC) after .debug_* sections already exist, the alloc section
ends up after non-alloc sections in the SHT. Tools like dh_dwz abort
with "Allocatable section after non-allocatable ones" unless the headers
are reordered (and the file content is spliced) before writing.
Linux only — debug info layout is ELF-specific.
"""
if self.platform == "windows":
print("\nSkipping: libtest_multi_wrapper_with_debug.so (Linux only)")
return True
lib_name = "libtest_multi_wrapper_with_debug.so"
print(f"\nBuilding: {lib_name} (RDC build, gfx1100+gfx1101, 2 TUs, -g)")
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
obj1 = tmpdir / "kernel1.o"
obj2 = tmpdir / "kernel2.o"
output = self.output_dir / lib_name
for src, obj, label in [
(self.multi_wrapper_src1, obj1, "kernel1"),
(self.multi_wrapper_src2, obj2, "kernel2"),
]:
print(f" Compiling {label} with -fgpu-rdc -g (gfx1100, gfx1101)...")
cmd = [
str(self.hipcc),
"-fgpu-rdc",
"-fPIC",
"-g",
"-c",
str(src),
"--offload-arch=gfx1100",
"--offload-arch=gfx1101",
"-o",
str(obj),
]
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
except subprocess.CalledProcessError as e:
print(f" ✗ Failed to compile {label}: {e.stderr}")
return False
print(" Linking with -fgpu-rdc...")
return self._run_hipcc(
[
"-fgpu-rdc",
str(obj1),
str(obj2),
"-shared",
"-o",
str(output),
],
output,
)
def build_host_only_executable(self):
"""Build host-only executable (no GPU device code)."""
exe_name = "host_only.exe"
print(f"\nBuilding: {exe_name} (host-only executable, no GPU code)")
output = self.output_dir / exe_name
return self._run_clang(
[
str(self.host_only_src),
"-o",
str(output),
"-DBUILD_EXECUTABLE",
],
output,
)
def build_host_only_shared_lib(self):
"""Build host-only shared library (no GPU device code)."""
if self.platform == "windows":
lib_name = "host_only.dll"
else:
lib_name = "libhost_only.so"
print(f"\nBuilding: {lib_name} (host-only shared library, no GPU code)")
output = self.output_dir / lib_name
args = [
str(self.host_only_src),
"-o",
str(output),
"-shared",
]
# -fPIC is required on Linux but unsupported on Windows
if self.platform != "windows":
args.append("-fPIC")
return self._run_clang(args, output)
def generate_manifest(self):
"""Generate manifest file documenting the test assets."""
import datetime
manifest_path = self.output_dir / "MANIFEST.txt"
# Get compiler version
try:
result = subprocess.run(
[str(self.hipcc), "--version"],
capture_output=True,
text=True,
check=True,
)
compiler_version = result.stdout.splitlines()[0]
except (subprocess.CalledProcessError, OSError):
compiler_version = "Unknown"
lib_prefix = "" if self.platform == "windows" else "lib"
lib_ext = ".dll" if self.platform == "windows" else ".so"
content = f"""Test Bundled Binaries Generated: {datetime.datetime.now()}
ROCm Path: {self.rocm_path}
Code Object Version: {self.code_object_version}
Platform: {self.platform}
Compiler: {compiler_version}
Bundled Executables (with GPU device code):
- test_kernel_single.exe: Single architecture (gfx1100)
- test_kernel_multi.exe: Multiple architectures (gfx1100, gfx1101)
- test_kernel_compressed.exe: Multiple architectures with compression (if supported)
- test_kernel_wide.exe: Wide architecture coverage (gfx900,906,908,90a,1100)
Bundled Shared Libraries (with GPU device code):
- {lib_prefix}test_kernel_single{lib_ext}: Single architecture (gfx1100)
- {lib_prefix}test_kernel_multi{lib_ext}: Multiple architectures (gfx1100, gfx1101)
- {lib_prefix}test_multi_wrapper{lib_ext}: RDC build with 2 __CudaFatBinaryWrapper structs (gfx1100)
- libtest_multi_wrapper_with_debug.so: RDC build, 2 TUs x gfx1100+gfx1101, compiled with -g (Linux only)
Tests ELF section reordering: .rocm_kpack_ref (SHF_ALLOC) must precede .debug_* (non-ALLOC).
Host-Only Binaries (NO GPU device code, for negative testing):
- host_only.exe: Host-only executable
- {lib_prefix}host_only{lib_ext}: Host-only shared library
Bundled binaries contain HIP kernels (vectorAdd, scalarMultiply) with .hip_fatbin sections.
Host-only binaries contain only CPU code with no .hip_fatbin sections.
Sources:
- test_generation/simple_kernel.hip (GPU kernels)
- test_generation/multi_wrapper_kernel1.hip (RDC kernel 1)
- test_generation/multi_wrapper_kernel2.hip (RDC kernel 2)
- test_generation/host_only.cpp (host-only code)
Build Script: test_generation/build_test_bundles.py
These assets are used to test unbundling functionality across:
- Executables and shared libraries
- Uncompressed vs compressed code objects
- Single vs multiple architectures
- Different code object versions
- ELF (Linux) vs PE/COFF (Windows) formats
- Positive tests (bundled binaries) vs negative tests (host-only binaries)
"""
manifest_path.write_text(content)
print(f"\n✓ Manifest created: {manifest_path}")
def build_all(self):
"""Build all test bundles."""
print("=" * 70)
print("Building Test Bundled Binaries")
print("=" * 70)
print(f"ROCm Path: {self.rocm_path}")
print(f"Compiler: {self.hipcc}")
print(f"Code Object Version: {self.code_object_version}")
print(f"Platform: {self.platform}")
print(f"Output Directory: {self.output_dir}")
# Create output directory
self.output_dir.mkdir(parents=True, exist_ok=True)
# Build all variants
results = {
# Bundled executables
"test_kernel_single (exe)": self.build_executable_single_arch(),
"test_kernel_multi (exe)": self.build_executable_multi_arch(),
"test_kernel_compressed (exe)": self.build_exe_compressed(),
"test_kernel_wide (exe)": self.build_exe_wide_arch(),
# Bundled shared libraries
"test_kernel_single (so/dll)": self.build_shared_lib_single_arch(),
"test_kernel_multi (so/dll)": self.build_shared_lib_multi_arch(),
# Multi-wrapper shared library (RDC build, single arch)
"test_multi_wrapper (so/dll)": self.build_shared_lib_multi_wrapper(),
# Multi-arch RDC build with debug info — tests ELF section reordering
"test_multi_wrapper_with_debug (so)": self.build_shared_lib_multi_wrapper_with_debug(),
# Host-only binaries (for negative testing)
"host_only (exe)": self.build_host_only_executable(),
"host_only (so/dll)": self.build_host_only_shared_lib(),
}
# Generate manifest
self.generate_manifest()
# Summary
print("\n" + "=" * 70)
print("Build Summary")
print("=" * 70)
for name, success in results.items():
status = "✓ SUCCESS" if success else "✗ FAILED"
print(f"{status}: {name}")
# List all generated files
print(f"\nGenerated files in {self.output_dir}:")
# List all binary files (executables, libraries)
patterns = ["*.exe", "*.so", "*.dll", "test_kernel_*", "libtest_kernel_*"]
seen_files = set()
for pattern in patterns:
for file in sorted(self.output_dir.glob(pattern)):
if file.name != "MANIFEST.txt" and file not in seen_files:
size_kb = file.stat().st_size / 1024
print(f" {file.name:40} {size_kb:8.1f} KB")
seen_files.add(file)
total_success = sum(results.values())
total_builds = len(results)
print(f"\nTotal: {total_success}/{total_builds} successful")
return all(results.values())
def main():
parser = argparse.ArgumentParser(
description="Build test bundled binaries for rocm-kpack testing"
)
parser.add_argument(
"--rocm-path",
type=Path,
help="Path to ROCm installation (auto-detected if not specified)",
)
args = parser.parse_args()
try:
builder = BundleBuilder(rocm_path=args.rocm_path)
success = builder.build_all()
sys.exit(0 if success else 1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
# Configure Windows console for UTF-8 before any output
import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "python"))
from rocm_kpack.platform_utils import configure_windows_console
configure_windows_console()
main()