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

612 lines
23 KiB
Python

"""Tests for kpack_transform ELF operations."""
import struct
import subprocess
from pathlib import Path
import pytest
from rocm_kpack.elf import (
ElfSurgery,
kpack_offload_binary,
add_kpack_ref_section,
read_kpack_ref_marker,
rewrite_hipfatbin_magic,
verify_no_fatbin_relocations,
NotFatBinaryError,
HIPF_MAGIC,
HIPK_MAGIC,
WRAPPER_SIZE,
)
class TestKpackOffloadBinary:
"""Tests for kpack_offload_binary."""
def test_kpack_fat_binary(self, tmp_path: Path, test_assets_dir: Path):
"""Test kpacking a fat binary with .hip_fatbin section."""
input_binary = (
test_assets_dir / "bundled_binaries/linux/cov5/test_kernel_multi.exe"
)
output_binary = tmp_path / "test_kernel_multi_kpacked.exe"
# Get original size
original_size = input_binary.stat().st_size
# Kpack with marker section in single pass
result = kpack_offload_binary(
input_path=input_binary,
output_path=output_binary,
kpack_search_paths=["test.kpack"],
kernel_name="test_kernel",
verbose=True,
)
# Verify output exists
assert output_binary.exists()
# Device code is removed (.hip_fatbin -> NOBITS, verified below). On-disk
# size is not asserted to shrink: executables get their PHDR table
# normalized to p_vaddr == p_offset for old-kernel (EL8 4.18) direct exec,
# and on a tiny fully-zero-paged fixture that overhead can exceed the
# savings. Real binaries (host image >> stub .hip_fatbin) are unaffected.
new_size = output_binary.stat().st_size
assert result["original_size"] == original_size
assert result["new_size"] == new_size
# Verify output is a valid ELF file
readelf_result = subprocess.run(
["readelf", "-h", output_binary], capture_output=True, text=True
)
assert readelf_result.returncode == 0
assert "ELF" in readelf_result.stdout
# Verify .hip_fatbin section is marked as NOBITS
sections_result = subprocess.run(
["readelf", "-S", output_binary], capture_output=True, text=True
)
assert sections_result.returncode == 0
# The section should now be NOBITS type
if ".hip_fatbin" in sections_result.stdout:
assert "NOBITS" in sections_result.stdout
def test_kpack_shared_library(self, tmp_path: Path, test_assets_dir: Path):
"""Test kpacking a shared library with .hip_fatbin section."""
input_library = (
test_assets_dir / "bundled_binaries/linux/cov5/libtest_kernel_single.so"
)
output_library = tmp_path / "libtest_kernel_single_kpacked.so"
original_size = input_library.stat().st_size
result = kpack_offload_binary(
input_path=input_library,
output_path=output_library,
kpack_search_paths=["../kpacks/test.kpack"],
kernel_name="lib/libtest_kernel_single.so",
)
assert output_library.exists()
# Note: Small .hip_fatbin sections may not reduce size due to overhead
# The important thing is the operation completes and produces valid output
assert result["new_size"] > 0
# Verify it's a valid shared library
readelf_result = subprocess.run(
["readelf", "-h", output_library], capture_output=True, text=True
)
assert readelf_result.returncode == 0
assert "DYN" in readelf_result.stdout # Shared library
def test_kpack_ref_marker_in_output(self, tmp_path: Path, test_assets_dir: Path):
"""Verify .rocm_kpack_ref section is present in output."""
input_binary = (
test_assets_dir / "bundled_binaries/linux/cov5/test_kernel_single.exe"
)
output_binary = tmp_path / "output.exe"
kpack_offload_binary(
input_path=input_binary,
output_path=output_binary,
kpack_search_paths=["test.kpack", "fallback.kpack"],
kernel_name="my_kernel",
)
# Read back and verify marker section
surgery = ElfSurgery.load(output_binary)
section = surgery.find_section(".rocm_kpack_ref")
assert section is not None
# Verify marker has correct content
import msgpack
content = surgery.get_section_content(section)
marker = msgpack.unpackb(content, raw=False)
assert marker["kpack_search_paths"] == ["test.kpack", "fallback.kpack"]
assert marker["kernel_name"] == "my_kernel"
class TestAddKpackRefSection:
"""Tests for add_kpack_ref_section."""
def test_add_section(self, tmp_path: Path, test_assets_dir: Path):
"""Test adding .rocm_kpack_ref section."""
input_binary = (
test_assets_dir / "bundled_binaries/linux/cov5/test_kernel_single.exe"
)
surgery = ElfSurgery.load(input_binary)
# Add section
section = add_kpack_ref_section(
surgery,
kpack_search_paths=["kernels.kpack"],
kernel_name="test_kernel",
)
# Verify section was added
assert section.name == ".rocm_kpack_ref"
# Read back marker
import msgpack
content = surgery.get_section_content(section)
marker = msgpack.unpackb(content, raw=False)
assert marker["kpack_search_paths"] == ["kernels.kpack"]
assert marker["kernel_name"] == "test_kernel"
class TestRewriteHipfatbinMagic:
"""Tests for rewrite_hipfatbin_magic."""
def test_rewrite_magic(self, tmp_path: Path, test_assets_dir: Path):
"""Test HIPF→HIPK magic rewrite."""
input_binary = (
test_assets_dir / "bundled_binaries/linux/cov5/test_kernel_single.exe"
)
surgery = ElfSurgery.load(input_binary)
# Check if .hipFatBinSegment exists
segment = surgery.find_section(".hipFatBinSegment")
if segment is None:
pytest.skip("No .hipFatBinSegment in test binary")
# Read original magic
segment_offset = surgery.vaddr_to_file_offset(segment.header.sh_addr)
import struct
original_magic = struct.unpack_from("<I", surgery.data, segment_offset)[0]
assert original_magic == HIPF_MAGIC
# Rewrite magic
count = rewrite_hipfatbin_magic(surgery, verbose=True)
assert count > 0
# Verify magic was rewritten
new_magic = struct.unpack_from("<I", surgery.data, segment_offset)[0]
assert new_magic == HIPK_MAGIC
def test_wrapper_index_in_reserved1(self, test_assets_dir: Path):
"""Test that wrapper index is written to reserved1 field (offset +16).
For multi-TU support, CLR reads the bundle index from reserved1 and
passes it to kpack_load_code_object as co_index. This index is used
to look up the correct kernel in the kpack archive TOC.
"""
input_binary = (
test_assets_dir / "bundled_binaries/linux/cov5/test_kernel_single.exe"
)
surgery = ElfSurgery.load(input_binary)
segment = surgery.find_section(".hipFatBinSegment")
if segment is None:
pytest.skip("No .hipFatBinSegment in test binary")
segment_offset = surgery.vaddr_to_file_offset(segment.header.sh_addr)
num_wrappers = segment.header.sh_size // WRAPPER_SIZE
# Rewrite magic (this also writes wrapper indices)
count = rewrite_hipfatbin_magic(surgery)
assert count > 0
# Verify each wrapper has correct index in reserved1 (offset +16)
for i in range(num_wrappers):
wrapper_offset = segment_offset + i * WRAPPER_SIZE
reserved1 = struct.unpack_from("<I", surgery.data, wrapper_offset + 16)[0]
assert reserved1 == i, (
f"Wrapper {i} reserved1 field should be {i}, got {reserved1}. "
"This index is used by CLR as co_index for kpack lookup."
)
class TestSmallHipFatbinHandling:
"""Tests for handling small .hip_fatbin sections."""
def test_small_section_does_not_fail(
self, small_hip_fatbin_binary: Path, tmp_path: Path
):
"""Verify small .hip_fatbin sections don't cause failure."""
output_binary = tmp_path / "output.exe"
# Should complete without error
result = kpack_offload_binary(
input_path=small_hip_fatbin_binary,
output_path=output_binary,
kpack_search_paths=["test.kpack"],
kernel_name="test_kernel",
)
assert output_binary.exists()
# May not reduce size for small sections
assert result["new_size"] > 0
def test_marginal_section_completes(
self, marginal_hip_fatbin_binary: Path, tmp_path: Path
):
"""Verify marginal .hip_fatbin sections complete successfully."""
output_binary = tmp_path / "output.exe"
result = kpack_offload_binary(
input_path=marginal_hip_fatbin_binary,
output_path=output_binary,
kpack_search_paths=["test.kpack"],
kernel_name="test_kernel",
)
assert output_binary.exists()
# Size may increase due to overhead, but operation should complete
assert result["new_size"] > 0
class TestKpackRefMarker:
"""Tests for add_kpack_ref_section and read_kpack_ref_marker."""
def test_roundtrip(self, tmp_path: Path, test_assets_dir: Path):
"""Test adding and reading back a kpack ref marker."""
source_binary = (
test_assets_dir / "bundled_binaries/linux/cov5/test_kernel_multi.exe"
)
marked_binary = tmp_path / "marked_binary.exe"
kpack_paths = [".kpack/blas-gfx100X.kpack", ".kpack/torch-gfx100X.kpack"]
kernel_name = "bin/test_kernel_multi.exe"
surgery = ElfSurgery.load(source_binary)
add_kpack_ref_section(
surgery,
kpack_search_paths=kpack_paths,
kernel_name=kernel_name,
)
surgery.save(marked_binary)
assert marked_binary.exists()
assert marked_binary.stat().st_size > 0
marker_data = read_kpack_ref_marker(marked_binary)
assert marker_data is not None
assert marker_data["kpack_search_paths"] == kpack_paths
assert marker_data["kernel_name"] == kernel_name
def test_with_host_only_binary(self, tmp_path: Path, test_assets_dir: Path):
"""Test adding marker to host-only binary (no .hip_fatbin section)."""
source_binary = test_assets_dir / "bundled_binaries/linux/cov5/host_only.exe"
marked_binary = tmp_path / "marked_host_only.exe"
surgery = ElfSurgery.load(source_binary)
add_kpack_ref_section(
surgery,
kpack_search_paths=[".kpack/runtime-gfx1100.kpack"],
kernel_name="bin/host_only.exe",
)
surgery.save(marked_binary)
marker_data = read_kpack_ref_marker(marked_binary)
assert marker_data is not None
assert marker_data["kpack_search_paths"] == [".kpack/runtime-gfx1100.kpack"]
assert marker_data["kernel_name"] == "bin/host_only.exe"
def test_with_shared_library(self, tmp_path: Path, test_assets_dir: Path):
"""Test adding marker to shared library."""
source_binary = (
test_assets_dir / "bundled_binaries/linux/cov5/libtest_kernel_single.so"
)
marked_binary = tmp_path / "marked_library.so"
surgery = ElfSurgery.load(source_binary)
add_kpack_ref_section(
surgery,
kpack_search_paths=[".kpack/hipblas-gfx1100.kpack"],
kernel_name="lib/libtest_kernel_single.so",
)
surgery.save(marked_binary)
marker_data = read_kpack_ref_marker(marked_binary)
assert marker_data is not None
assert marker_data["kernel_name"] == "lib/libtest_kernel_single.so"
def test_multiple_search_paths(self, tmp_path: Path, test_assets_dir: Path):
"""Test marker with multiple kpack search paths."""
source_binary = (
test_assets_dir / "bundled_binaries/linux/cov5/test_kernel_multi.exe"
)
marked_binary = tmp_path / "multi_search.exe"
kpack_paths = [
".kpack/blas-gfx1100.kpack",
".kpack/blas-gfx100X.kpack",
".kpack/blas-gfx90a.kpack",
]
surgery = ElfSurgery.load(source_binary)
add_kpack_ref_section(
surgery,
kpack_search_paths=kpack_paths,
kernel_name="bin/hipcc",
)
surgery.save(marked_binary)
marker_data = read_kpack_ref_marker(marked_binary)
assert marker_data is not None
assert len(marker_data["kpack_search_paths"]) == 3
assert marker_data["kpack_search_paths"] == kpack_paths
def test_no_section_returns_none(self, test_assets_dir: Path):
"""Test reading marker from binary without .rocm_kpack_ref section."""
source_binary = (
test_assets_dir / "bundled_binaries/linux/cov5/test_kernel_multi.exe"
)
marker_data = read_kpack_ref_marker(source_binary)
assert marker_data is None
def test_preserves_existing_sections(self, tmp_path: Path, test_assets_dir: Path):
"""Test that adding marker preserves existing sections like .hip_fatbin."""
source_binary = (
test_assets_dir / "bundled_binaries/linux/cov5/test_kernel_multi.exe"
)
marked_binary = tmp_path / "preserve_sections.exe"
# Verify source has .hip_fatbin section
result = subprocess.run(
["readelf", "-S", str(source_binary)],
capture_output=True,
text=True,
check=True,
)
assert ".hip_fatbin" in result.stdout
surgery = ElfSurgery.load(source_binary)
add_kpack_ref_section(
surgery,
kpack_search_paths=[".kpack/test.kpack"],
kernel_name="bin/test",
)
surgery.save(marked_binary)
# Verify marked binary still has .hip_fatbin section
result = subprocess.run(
["readelf", "-S", str(marked_binary)],
capture_output=True,
text=True,
check=True,
)
assert ".hip_fatbin" in result.stdout
assert ".rocm_kpack_ref" in result.stdout
def test_special_characters_in_paths(self, tmp_path: Path, test_assets_dir: Path):
"""Test marker with special characters in paths."""
source_binary = (
test_assets_dir / "bundled_binaries/linux/cov5/test_kernel_multi.exe"
)
marked_binary = tmp_path / "special_chars.exe"
kpack_paths = [
".kpack/my-lib_v1.0.kpack",
"../shared/.kpack/common-gfx100X.kpack",
]
surgery = ElfSurgery.load(source_binary)
add_kpack_ref_section(
surgery,
kpack_search_paths=kpack_paths,
kernel_name="bin/my-binary_v2.exe",
)
surgery.save(marked_binary)
marker_data = read_kpack_ref_marker(marked_binary)
assert marker_data is not None
assert marker_data["kpack_search_paths"] == kpack_paths
assert marker_data["kernel_name"] == "bin/my-binary_v2.exe"
class TestDebugInfoSectionOrdering:
"""Regression tests for the ELF section reordering fix.
libtest_multi_wrapper_with_debug.so is compiled with -g, so the ELF
contains non-allocatable .debug_* sections alongside the HIP fat binary
sections. When kpack adds .rocm_kpack_ref (SHF_ALLOC) it must appear
before .debug_* in both the section header table and in file offsets.
Tools like dh_dwz (DWARF debuginfo compressor used in Debian packaging)
abort with "Allocatable section after non-allocatable ones" if an ALLOC
section follows a non-ALLOC section in the SHT. The fix:
1. Reorders the SHT so all ALLOC sections precede non-ALLOC ones.
2. Splices the .rocm_kpack_ref content before the first non-ALLOC
file offset so the physical layout matches the SHT order.
"""
_BINARY = "bundled_binaries/linux/cov5/libtest_multi_wrapper_with_debug.so"
@pytest.fixture
def debug_binary(self, test_assets_dir: Path) -> Path:
path = test_assets_dir / self._BINARY
if not path.exists():
pytest.skip(
f"{self._BINARY} not found — run "
"test_generation/build_test_bundles.py with ROCm on Linux"
)
return path
def test_binary_has_debug_sections(self, debug_binary: Path):
"""Confirm the test asset actually contains .debug_* sections."""
result = subprocess.run(
["readelf", "-S", str(debug_binary)], capture_output=True, text=True
)
assert result.returncode == 0
assert ".debug_info" in result.stdout, (
"Expected .debug_info in debug binary — was it compiled without -g?"
)
def test_alloc_sections_precede_non_alloc_after_transform(
self, debug_binary: Path, tmp_path: Path
):
"""After kpack transform, all SHF_ALLOC sections must precede non-ALLOC.
Verifies the section header reordering fix: .rocm_kpack_ref (ALLOC)
must appear before .debug_* (non-ALLOC) in the SHT. If not, dh_dwz
aborts during Debian package builds.
"""
output = tmp_path / "libtest_multi_wrapper_with_debug_kpacked.so"
kpack_offload_binary(
input_path=debug_binary,
output_path=output,
kpack_search_paths=["../.kpack/blas_lib_@GFXARCH@.kpack"],
kernel_name="lib/libtest_multi_wrapper_with_debug.so",
)
assert output.exists()
# Precise check via ElfSurgery: walk sections in SHT order and verify
# no ALLOC section appears after a non-ALLOC section with file content.
surgery = ElfSurgery.load(output)
SHT_NULL = 0
SHT_NOBITS = 8
seen_non_alloc_with_content = False
for section in surgery.iter_sections():
shdr = section.header
if shdr.sh_type == SHT_NULL:
continue
has_content = shdr.sh_type != SHT_NOBITS and shdr.sh_size > 0
if not shdr.is_alloc and has_content:
seen_non_alloc_with_content = True
if shdr.is_alloc and seen_non_alloc_with_content:
pytest.fail(
f"SHF_ALLOC section '{section.name}' appears after non-ALLOC "
"sections in the section header table. dh_dwz will abort with "
"'Allocatable section after non-allocatable ones'."
)
def test_rocm_kpack_ref_file_offset_before_debug(
self, debug_binary: Path, tmp_path: Path
):
"""The .rocm_kpack_ref content must be at a lower file offset than .debug_info.
The ELF section-reordering fix also splices the .rocm_kpack_ref content
before the first non-ALLOC section in the file. This ensures both the
SHT order and physical file layout are correct.
"""
output = tmp_path / "libtest_debug_offset_check.so"
kpack_offload_binary(
input_path=debug_binary,
output_path=output,
kpack_search_paths=["../.kpack/blas_lib_@GFXARCH@.kpack"],
kernel_name="lib/libtest_multi_wrapper_with_debug.so",
)
surgery = ElfSurgery.load(output)
SHT_NOBITS = 8
kpack_ref = surgery.find_section(".rocm_kpack_ref")
assert kpack_ref is not None, ".rocm_kpack_ref section must exist after transform"
debug_info = surgery.find_section(".debug_info")
assert debug_info is not None, ".debug_info must exist in debug binary"
assert kpack_ref.header.sh_offset < debug_info.header.sh_offset, (
f".rocm_kpack_ref file offset (0x{kpack_ref.header.sh_offset:x}) "
f"must be less than .debug_info offset (0x{debug_info.header.sh_offset:x}). "
"The splice fix must insert the ALLOC section before non-ALLOC content."
)
class TestNotFatBinaryError:
"""Tests for NotFatBinaryError handling."""
def test_raises_on_host_only_binary(self, tmp_path: Path, test_assets_dir: Path):
"""Test that kpack_offload_binary raises NotFatBinaryError for host-only binaries."""
# host_only.exe has no .hip_fatbin section
host_only = test_assets_dir / "bundled_binaries/linux/cov5/host_only.exe"
output = tmp_path / "output.exe"
with pytest.raises(NotFatBinaryError) as exc_info:
kpack_offload_binary(
input_path=host_only,
output_path=output,
kpack_search_paths=["test.kpack"],
kernel_name="test",
)
# Verify exception message is informative
assert "no .hip_fatbin section" in str(exc_info.value)
assert "host_only.exe" in str(exc_info.value)
# Verify output was NOT created (no modifications made)
assert not output.exists()
def test_raises_on_shared_library_without_fatbin(
self, tmp_path: Path, test_assets_dir: Path
):
"""Test that kpack_offload_binary raises NotFatBinaryError for host-only libraries."""
host_only_lib = test_assets_dir / "bundled_binaries/linux/cov5/libhost_only.so"
output = tmp_path / "output.so"
with pytest.raises(NotFatBinaryError):
kpack_offload_binary(
input_path=host_only_lib,
output_path=output,
kpack_search_paths=["test.kpack"],
kernel_name="test",
)
# Verify output was NOT created
assert not output.exists()
def test_no_error_without_kpack_search_paths(
self, tmp_path: Path, test_assets_dir: Path
):
"""Test that NotFatBinaryError is NOT raised when kpack_search_paths is None.
When kpack_search_paths is None, the function assumes the binary already
has a .rocm_kpack_ref section. The check is only for new marker additions.
"""
# First add a marker to a host-only binary
host_only = test_assets_dir / "bundled_binaries/linux/cov5/host_only.exe"
marked = tmp_path / "marked.exe"
surgery = ElfSurgery.load(host_only)
add_kpack_ref_section(
surgery,
kpack_search_paths=["test.kpack"],
kernel_name="test",
)
surgery.save(marked)
# Now try to process it without kpack_search_paths
# This would typically be done to re-process an already-marked binary
output = tmp_path / "output.exe"
# Should NOT raise NotFatBinaryError - it processes binaries with existing markers
# Note: This will fail at a later stage (no .hip_fatbin to zero-page) but
# importantly it doesn't raise NotFatBinaryError
kpack_offload_binary(
input_path=marked,
output_path=output,
kpack_search_paths=None,
kernel_name=None,
)