Files
TheRock/rocm-systems/shared/kpack/python/rocm_kpack/compression.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

392 lines
13 KiB
Python

"""Compression support for packed kernel archives.
This module provides a pluggable compression interface with map/reduce pattern,
supporting both simple per-kernel compression and complex cross-kernel optimization.
Compression Scheme Design:
- Each compressor has a SCHEME_NAME used in the TOC
- Compressor-specific metadata is stored at the TOC level
- Per-kernel TOC entries reference kernels by ordinal (0..num_kernels-1)
- Runtime initializes compressor from TOC once for efficient random access
"""
from abc import ABC, abstractmethod
from pathlib import Path
import struct
import zstandard as zstd
class CompressionInput:
"""Opaque result from map phase - base class for compressor-specific data.
Subclasses can hold:
- Compressed bytes (simple schemes like ZstdCompressor)
- Raw data + statistics (dictionary training)
- Fingerprints (deduplication)
- Block structure analysis (block sorting)
"""
pass
class Compressor(ABC):
"""Abstract base class for compression strategies with map/reduce pattern.
Design:
- SCHEME_NAME: Identifier stored in TOC compression_scheme field
- prepare_kernel(): Map phase (parallel-safe preprocessing)
- finalize(): Reduce phase (returns blob + TOC metadata)
- from_toc(): Initialize compressor from TOC for reading
- decompress_kernel(): Extract kernel by ordinal
"""
SCHEME_NAME: str = NotImplemented
@abstractmethod
def prepare_kernel(self, kernel_data: bytes, kernel_id: str) -> CompressionInput:
"""Map phase: analyze/preprocess kernel (parallel-safe).
This method is called in parallel for each kernel. Depending on the
compression strategy, it may:
- Compress immediately (simple schemes)
- Extract samples and statistics (dictionary training)
- Compute hashes (deduplication)
- Analyze structure (block sorting)
Args:
kernel_data: Raw kernel bytes
kernel_id: Unique identifier for this kernel (for debugging)
Returns:
CompressionInput subclass instance with prepared data
"""
pass
@abstractmethod
def finalize(
self, inputs: list[tuple[str, CompressionInput]]
) -> tuple[bytes, dict[str, object]]:
"""Reduce phase: compress all kernels with cross-kernel optimization (exclusive).
This method is called once with all prepared inputs. It performs
cross-kernel optimization and writes the final compressed archive blob.
For simple schemes, this just concatenates pre-compressed frames.
For complex schemes, this may build shared dictionaries, deduplicate,
or perform other global optimizations.
Args:
inputs: List of (kernel_id, CompressionInput) tuples from prepare_kernel
Returns:
Tuple of (blob_data, toc_metadata) where:
- blob_data: Opaque compressed blob to write to archive
- toc_metadata: Compression-specific fields to add to TOC
"""
pass
@staticmethod
@abstractmethod
def from_toc(toc_data: dict[str, object], file_path: Path) -> "Compressor":
"""Initialize compressor from TOC metadata for reading.
The runtime calls this once when opening a kpack file, then uses
the returned compressor instance for all decompress_kernel() calls.
Args:
toc_data: The full TOC dictionary from the kpack file
file_path: Path object for reading blob data
Returns:
Compressor instance initialized for reading
"""
pass
@abstractmethod
def decompress_kernel(self, ordinal: int) -> bytes:
"""Runtime: extract and decompress kernel by ordinal.
The compressor was initialized via from_toc() with all necessary
metadata to perform efficient random access.
Args:
ordinal: Kernel index (0..num_kernels-1)
Returns:
Decompressed kernel bytes
"""
pass
class NoOpCompressionInput(CompressionInput):
"""Compression input for uncompressed data."""
def __init__(self, data: bytes):
self.data = data
class NoOpCompressor(Compressor):
"""Passthrough compressor that stores data uncompressed.
TOC structure:
- compression_scheme: "none"
- blobs: [{offset: uint64, size: uint32}, ...] indexed by ordinal
Useful for testing and as a baseline for comparison.
"""
SCHEME_NAME = "none"
def __init__(self):
"""Initialize compressor for writing."""
# For reading mode
self._file_path = None
self._blobs = None
def prepare_kernel(self, kernel_data: bytes, kernel_id: str) -> CompressionInput:
"""Store kernel data without compression."""
return NoOpCompressionInput(data=kernel_data)
def finalize(
self, inputs: list[tuple[str, CompressionInput]]
) -> tuple[bytes, dict[str, object]]:
"""Concatenate all kernel data and build blob metadata.
Returns:
(concatenated_blobs, {"blobs": [{"offset": ..., "size": ...}, ...]})
"""
result = bytearray()
blobs = []
current_offset = 0
for kernel_id, comp_input in inputs:
assert isinstance(comp_input, NoOpCompressionInput)
data = comp_input.data
# Append data
result.extend(data)
# Record blob metadata (offset is relative to start of blob section)
blobs.append({"offset": current_offset, "size": len(data)})
current_offset += len(data)
toc_metadata = {"blobs": blobs}
return bytes(result), toc_metadata
@staticmethod
def from_toc(toc_data: dict[str, object], file_path: Path) -> "NoOpCompressor":
"""Initialize from TOC for reading."""
compressor = NoOpCompressor()
compressor._file_path = file_path
compressor._blobs = toc_data["blobs"]
return compressor
def decompress_kernel(self, ordinal: int) -> bytes:
"""Extract uncompressed kernel by ordinal."""
if self._blobs is None or self._file_path is None:
raise RuntimeError("Compressor not initialized from TOC")
if ordinal < 0 or ordinal >= len(self._blobs):
raise ValueError(
f"Ordinal {ordinal} out of range (0..{len(self._blobs)-1})"
)
blob = self._blobs[ordinal]
offset = blob["offset"] # Absolute file offset (fixed up by PackArchive.write)
size = blob["size"]
# Read from file at absolute offset
with self._file_path.open("rb") as f:
f.seek(offset)
return f.read(size)
class ZstdCompressionInput(CompressionInput):
"""Compression input containing pre-compressed zstd frame."""
def __init__(self, kernel_id: str, compressed_frame: bytes, original_size: int):
self.kernel_id = kernel_id
self.compressed_frame = compressed_frame
self.original_size = original_size
class ZstdCompressor(Compressor):
"""Per-kernel zstd compression (matches current ROCm practice).
TOC structure:
- compression_scheme: "zstd-per-kernel"
- zstd_offset: uint64 (offset to compressed blob)
- zstd_size: uint64 (size of compressed blob)
The compressed blob contains:
- Header: num_kernels (uint32)
- Frame entries: [size (uint32), compressed_frame (variable bytes)] * num_kernels
- Frames are indexed by ordinal in order
This allows O(1) lookup by building an index on first access.
"""
SCHEME_NAME = "zstd-per-kernel"
def __init__(self, compression_level: int = 3):
"""Initialize zstd compressor.
Args:
compression_level: Zstd compression level (1-22, default 3)
3 is the zstd default, good balance of speed/ratio
"""
self.compression_level = compression_level
# For reading mode
self._file_path = None
self._zstd_offset = None
self._zstd_size = None
self._frame_index = (
None # Built on first access: [(offset, size, original_size), ...]
)
self._decompressor = None # Created lazily for reading
def prepare_kernel(self, kernel_data: bytes, kernel_id: str) -> CompressionInput:
"""Compress kernel immediately (work done in parallel).
Creates a fresh compressor instance for thread-safety.
ZstdCompressor objects are not thread-safe, so we create one per call.
"""
compressor = zstd.ZstdCompressor(level=self.compression_level)
compressed = compressor.compress(kernel_data)
return ZstdCompressionInput(
kernel_id=kernel_id,
compressed_frame=compressed,
original_size=len(kernel_data),
)
def finalize(
self, inputs: list[tuple[str, CompressionInput]]
) -> tuple[bytes, dict[str, object]]:
"""Concatenate pre-compressed frames sequentially.
Format:
[Header]
num_kernels: uint32
[Frame entries]
compressed_size: uint32
compressed_frame: variable bytes
(repeated for each kernel in order)
Returns:
(compressed_blob, {"zstd_offset": ..., "zstd_size": ...})
"""
result = bytearray()
# Write header
num_kernels = len(inputs)
result.extend(struct.pack("<I", num_kernels))
# Write frames sequentially
for kernel_id, comp_input in inputs:
assert isinstance(comp_input, ZstdCompressionInput)
frame = comp_input.compressed_frame
# Write frame size then frame data
result.extend(struct.pack("<I", len(frame)))
result.extend(frame)
# TOC metadata will be filled in by PackArchive with actual offset/size
toc_metadata = {
"zstd_offset": 0, # Placeholder, filled by PackArchive
"zstd_size": len(result),
}
return bytes(result), toc_metadata
@staticmethod
def from_toc(toc_data: dict[str, object], file_path: Path) -> "ZstdCompressor":
"""Initialize from TOC for reading."""
compressor = ZstdCompressor()
compressor._file_path = file_path
compressor._zstd_offset = toc_data["zstd_offset"]
compressor._zstd_size = toc_data["zstd_size"]
return compressor
def _build_frame_index(self) -> None:
"""Build frame index for random access (called once on first access)."""
if self._frame_index is not None:
return
# Read compressed blob
with self._file_path.open("rb") as f:
f.seek(self._zstd_offset)
blob_data = f.read(self._zstd_size)
# Parse header
offset = 0
num_kernels = struct.unpack("<I", blob_data[offset : offset + 4])[0]
offset += 4
# Build index of frames
self._frame_index = []
for _ in range(num_kernels):
frame_size = struct.unpack("<I", blob_data[offset : offset + 4])[0]
offset += 4
frame_offset = offset
offset += frame_size
# Store (offset_in_blob, size) for each frame
self._frame_index.append((frame_offset, frame_size))
# Cache blob data for faster access (trade memory for speed)
self._blob_data = blob_data
def decompress_kernel(self, ordinal: int) -> bytes:
"""Extract and decompress kernel by ordinal."""
if self._zstd_offset is None or self._file_path is None:
raise RuntimeError("Compressor not initialized from TOC")
# Build index on first access
self._build_frame_index()
if ordinal < 0 or ordinal >= len(self._frame_index):
raise ValueError(
f"Ordinal {ordinal} out of range (0..{len(self._frame_index)-1})"
)
# Get frame from index
frame_offset, frame_size = self._frame_index[ordinal]
compressed_frame = self._blob_data[frame_offset : frame_offset + frame_size]
# Decompress (create decompressor lazily)
if self._decompressor is None:
self._decompressor = zstd.ZstdDecompressor()
return self._decompressor.decompress(compressed_frame)
# Registry of compression schemes
COMPRESSION_SCHEMES = {
NoOpCompressor.SCHEME_NAME: NoOpCompressor,
ZstdCompressor.SCHEME_NAME: ZstdCompressor,
}
def create_compressor_from_toc(
toc_data: dict[str, object], file_path: Path
) -> Compressor:
"""Factory function to create compressor from TOC metadata.
Args:
toc_data: Full TOC dictionary
file_path: Path to kpack file
Returns:
Initialized compressor instance
Raises:
ValueError: If compression scheme is unknown
"""
scheme = toc_data.get("compression_scheme", "none")
if scheme not in COMPRESSION_SCHEMES:
raise ValueError(f"Unknown compression scheme: {scheme}")
compressor_class = COMPRESSION_SCHEMES[scheme]
return compressor_class.from_toc(toc_data, file_path)