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

425 lines
19 KiB
Python

# Copyright (c) 2026 Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""Cross-ISA instruction analysis for shared execute() deduplication.
The ``CrossIsaAnalyzer`` compares instructions across AMDGPU ISAs
and classifies each into one of three categories:
- **universal** — identical encoding fields and semantics on all ISAs where
the instruction appears. The execute() body can be emitted once as a
shared template.
- **family_shared** — identical within a family (e.g., all CDNA or all RDNA)
but differs across families. A family-scoped shared template is emitted.
- **isa_exclusive** — present on only one ISA, or semantically/structurally
unique. The execute() body is emitted per-ISA as today.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from amdisa.fieldless_policy import operand_participates
if TYPE_CHECKING:
from amdisa.gpuisa import InstEncoding, Instruction, IsaSpec, MicrocodeField
from amdisa.semantics import InstructionSemantics, SemanticsSpec
@dataclass
class SharedInstInfo:
"""Metadata for a shared instruction."""
mnemonic: str
encoding_name: str
field_layout: tuple[tuple[str, int], ...] # ((field_name, bit_cnt), ...)
semantic_class: str
operation: str | None
data_type: str | None
isa_names: list[str]
@dataclass
class SharedInstructionPlan:
"""Result of cross-ISA analysis. Consumed by the codegen.
Key schemes differ across the three dicts:
- ``universal``: keyed by mnemonic alone. Safe because an instruction
can only be universal if ALL entries across ALL ISAs have identical
field layout, semantics, and operands — which implies a single
encoding. Multi-encoding mnemonics (e.g., v_mov_b32 in VOP1 + VOP3)
always have differing field layouts, so they never reach universal;
they go through the grouping path into ``family_shared`` instead.
- ``family_shared``: keyed by family name → ``(mnemonic, encoding_name)``.
The composite key prevents collisions between different encodings of
the same mnemonic (e.g., VOP1 vs VOP3 variants).
- ``isa_exclusive``: keyed by ISA name → set of mnemonics. Encoding
information is not tracked because the per-ISA codegen processes each
encoding file independently and doesn't need it here.
"""
universal: dict[str, SharedInstInfo] = field(default_factory=dict)
family_shared: dict[str, dict[tuple[str, str], SharedInstInfo]] = field(
default_factory=dict
)
isa_exclusive: dict[str, set[str]] = field(default_factory=dict)
@property
def total_universal(self) -> int:
return len(self.universal)
@property
def total_family_shared(self) -> int:
return sum(len(v) for v in self.family_shared.values())
@property
def total_exclusive(self) -> int:
return sum(len(v) for v in self.isa_exclusive.values())
def _field_signature(enc: InstEncoding) -> tuple[tuple[str, int], ...]:
"""Return a canonical tuple of (field_name, bit_count) for an encoding.
Padding fields (``pad_*``) and the format identifier (``encoding``) are
excluded — they are never referenced by execute() bodies, and their
layout varies across XML spec versions without affecting semantics.
"""
return tuple(
(f.name, f.bit_cnt)
for f in sorted(enc.ucode_fields, key=lambda f: f.bit_offset)
if not f.name.startswith('pad_') and f.name != 'encoding'
)
def _sem_key(sem: InstructionSemantics | None) -> tuple[str, str | None, str | None]:
"""Return a hashable semantic identity for comparison."""
if sem is None:
return ('nop', None, None)
return (sem.semantic_class, sem.operation, sem.data_type)
#: Canonical signature size for the inline literal (``OPR_SIMM32``). The literal
#: is always a fixed 32-bit encoding word; specs record its declared size as the
#: datatype width it feeds (16/32) and do so inconsistently across ISAs. Named so
#: the use site can't be "cleaned up" into ``op.size`` -- see _operand_signature.
_SIMM32_SIGNATURE_SIZE = 32
def _operand_signature(
inst: Instruction,
) -> tuple[tuple[str, str, int, bool, bool], ...]:
"""Return a canonical tuple of operand (name, type, size, is_input, is_output).
An operand is included exactly when it participates in the execute body
(``operand_participates``): field-bearing operands always, and fieldless
operands only if their policy reads a value. Inert fieldless side effects
are excluded, so adding one on a single ISA does not fragment a shared body.
Using the same predicate as execute generation keeps the signature a
faithful proxy for the body: two instructions with the same signature have
the same execute-visible operands, and promoting a fieldless operand to
participate is a single policy-table change that updates both sites at once.
The inline literal (``OPR_SIMM32``) size is canonicalized: it is always a
fixed 32-bit encoding word, but the specs record its declared size as the
datatype width it feeds (16 for f16, 32 for f32) and do so inconsistently
for the same instruction across ISAs -- e.g. ``V_FMAAK_F16``'s literal is
size 16 on rdna1/rdna2 but 32 on rdna3/rdna4. That datatype width is already
captured by the opcode and the register operands (f16 forms carry 16-bit
vdst/src0/vsrc1, f32 forms 32-bit), so folding it into the literal here would
only fragment otherwise-identical instructions and drop them from Identity
translation. f16 vs f32 stay distinct via their register operand sizes.
"""
return tuple(
(
op.name,
op.operand_type,
_SIMM32_SIGNATURE_SIZE if op.operand_type == 'OPR_SIMM32' else op.size,
op.is_input,
op.is_output,
)
for op in inst.operands
if operand_participates(op.fieldless, op.operand_type)
)
# ISA family groupings for family_shared classification.
# Sub-family groupings reflect encoding layout generations: ISAs within
# the same sub-family share encoding field layouts (e.g., ENC_MUBUF
# changed between RDNA2 and RDNA3), while ISAs across sub-families may
# diverge. _classify_family() checks sub-families first (finest to
# coarsest), so instructions that are identical within a generation get
# their own bucket rather than colliding at the coarse family level.
CDNA_GFX9 = frozenset({'cdna1', 'cdna2', 'cdna3', 'cdna4'})
RDNA_GFX10 = frozenset({'rdna1', 'rdna2'})
RDNA_GFX11 = frozenset({'rdna3', 'rdna3_5'})
RDNA_GFX12 = frozenset({'rdna4'})
CDNA_ISAS = CDNA_GFX9 # All CDNA ISAs currently share one generation.
RDNA_ISAS = RDNA_GFX10 | RDNA_GFX11 | RDNA_GFX12
ALL_ISAS = CDNA_ISAS | RDNA_ISAS
# Family table used by _classify_family() to pick the tightest-fitting
# bucket for a set of ISAs. Order is informational only — the lookup
# picks the entry with the smallest ``members`` set that is a superset
# of the input. On exact-size ties (e.g., CDNA_GFX9 vs CDNA_ISAS today,
# both size 4), the first matching entry wins, so finest-first ordering
# is preferred.
#
# Note for Tony (re commit 2316f85): you introduced an equivalent
# `_FAMILIES` table and a smallest-fits ``_classify_family`` in your
# v_pack_b32_f16 commit. This branch adopts that same shape so your
# rebase/merge should be near-trivial here. The deliberate differences
# vs. your version are the bucket *names* — we use ``rdna_gfx10``,
# ``cdna_gfx9``, etc. instead of bare ``gfx10`` / ``cdna``, because the
# generated code reads more clearly with the family prefix and our new
# tests in ``tests/test_cross_isa_analyzer.py`` hardcode these names.
# If you'd rather keep your shorter names, just rename the strings here
# and update the matching assertions in
# tests/test_cross_isa_analyzer.py (test_classify_family_sub_families,
# test_rdna_cross_generation_no_collision, and
# test_family_shared_uses_sub_family_keys) — the algorithm is identical.
_FAMILIES: list[tuple[str, frozenset[str]]] = [
# Sub-families (finest) — encoding layouts are identical within these.
('rdna_gfx12', RDNA_GFX12),
('rdna_gfx11', RDNA_GFX11),
('rdna_gfx10', RDNA_GFX10),
('cdna_gfx9', CDNA_GFX9),
# Coarse families — spans sub-families but stays within one family.
('rdna', RDNA_ISAS),
('cdna', CDNA_ISAS),
]
class CrossIsaAnalyzer:
"""Analyze instruction overlap across multiple ISA specs."""
def analyze(
self,
specs: list[tuple[str, IsaSpec, SemanticsSpec | None]],
) -> SharedInstructionPlan:
"""Run the cross-ISA analysis.
Args:
specs: List of ``(isa_name, isa_spec, semantics_spec)`` tuples,
one per ISA.
Returns:
A ``SharedInstructionPlan`` classifying each instruction.
"""
plan = SharedInstructionPlan()
# Step 1: Build mnemonic → list of (isa_name, encoding, instruction, sem).
# Keyed by mnemonic only — entries from different encodings of the
# same mnemonic (e.g., v_mov_b32 in VOP1 and VOP3) land in the same
# bucket. Step 2's classification logic handles multi-encoding
# mnemonics: their differing field layouts cause same_structure to
# be False, routing them through the grouping path which separates
# them by (field_sig, sem_key, operand_sig) and assigns each
# per-encoding group independently.
inst_map: dict[
str,
list[tuple[str, InstEncoding, Instruction, InstructionSemantics | None]],
] = {}
for isa_name, spec, sem_spec in specs:
for enc in spec.inst_encodings:
if not enc.insts:
continue
# Skip alt encodings — their instructions are classified
# under the parent encoding for cross-ISA comparison.
if spec.profile.is_alt_encoding(enc.enc_name):
continue
# Collect instructions from this encoding plus any child
# alt encodings whose instructions reside in the parent's
# file.
all_insts = list(enc.insts)
for child_enc in spec.inst_encodings:
if (
child_enc.insts
and spec.profile.is_alt_encoding(child_enc.enc_name)
and spec.profile.derive_parent_enc_name(child_enc.enc_name)
== enc.enc_name
):
all_insts.extend(child_enc.insts)
for inst in all_insts:
sem = sem_spec.instructions.get(inst.name) if sem_spec else None
inst_map.setdefault(inst.mnemonic, []).append(
(isa_name, enc, inst, sem)
)
isa_names_set = {name for name, _, _ in specs}
# Step 2: Classify each instruction.
for mnemonic, entries in inst_map.items():
present_isas = {e[0] for e in entries}
if len(present_isas) == 1:
# Only on one ISA → exclusive.
isa = next(iter(present_isas))
plan.isa_exclusive.setdefault(isa, set()).add(mnemonic)
continue
# Check if all entries have the same encoding layout, semantics,
# AND operand types. Operand type names differ across ISAs
# (e.g., OPR_SLEEP exists on RDNA4 but not CDNA1), so instructions
# with ISA-specific operand types cannot be shared.
field_sigs = set()
sem_keys = set()
opnd_sigs = set()
for isa_name, enc, inst, sem in entries:
field_sigs.add(_field_signature(enc))
sem_keys.add(_sem_key(sem))
opnd_sigs.add(_operand_signature(inst))
same_structure = (
len(field_sigs) == 1 and len(sem_keys) == 1 and len(opnd_sigs) == 1
)
if same_structure and present_isas == isa_names_set:
# Universal — identical on ALL ISAs.
# Any entry is representative: same_structure guarantees
# identical field_sig, sem_key, and opnd_sig across all.
_, enc0, inst0, sem0 = entries[0]
# Defensive: same_structure guarantees identical field
# layouts, but verify that the encoding name is also
# consistent — the mnemonic-only key in plan.universal
# assumes a single encoding name across all ISAs.
enc_names = {e[1].enc_name for e in entries}
assert len(enc_names) == 1, (
f"Universal instruction '{mnemonic}' has identical field "
f"layouts but inconsistent encoding names: {enc_names}. "
f"The universal dict is keyed by mnemonic alone and "
f"cannot represent multiple encoding names. This needs "
f"a composite key or per-encoding classification."
)
plan.universal[mnemonic] = SharedInstInfo(
mnemonic=mnemonic,
encoding_name=enc0.enc_name,
field_layout=_field_signature(enc0),
semantic_class=_sem_key(sem0)[0],
operation=_sem_key(sem0)[1],
data_type=_sem_key(sem0)[2],
isa_names=sorted(present_isas),
)
continue
if same_structure and len(present_isas) >= 2:
# Same structure on a subset of ISAs → family_shared.
# Determine which family this belongs to.
family_name = self._classify_family(present_isas)
_, enc0, inst0, sem0 = entries[0]
plan.family_shared.setdefault(family_name, {})[
(mnemonic, enc0.enc_name)
] = SharedInstInfo(
mnemonic=mnemonic,
encoding_name=enc0.enc_name,
field_layout=_field_signature(enc0),
semantic_class=_sem_key(sem0)[0],
operation=_sem_key(sem0)[1],
data_type=_sem_key(sem0)[2],
isa_names=sorted(present_isas),
)
continue
# Different structure across ISAs → each gets its own classification.
# This path also handles multi-encoding mnemonics (e.g., v_mov_b32
# in VOP1 + VOP3): the differing field layouts across encodings
# cause same_structure to be False, so each encoding is classified
# independently via the sub-groups below.
# Group by (field_sig, sem_key, operand_sig) and classify each group.
groups: dict[tuple, list[str]] = {}
for isa_name, enc, inst, sem in entries:
key = (_field_signature(enc), _sem_key(sem), _operand_signature(inst))
groups.setdefault(key, []).append(isa_name)
for (fsig, skey, osig), group_isas in groups.items():
if len(group_isas) >= 2:
# Shareable across 2+ ISAs
family_name = self._classify_family(set(group_isas))
# Find an entry that matches this group's signatures.
# _sem_key is not checked: semantics are derived from the
# mnemonic, which is identical for all entries in this
# inst_map bucket, so matching fsig + osig is sufficient.
next_match = next(
(
e
for e in entries
if e[0] == group_isas[0]
and _field_signature(e[1]) == fsig
and _operand_signature(e[2]) == osig
),
None,
)
if next_match is None:
raise ValueError(
f"Failed to find matching entry for mnemonic '{mnemonic}' "
f"in ISA '{group_isas[0]}' with field_sig={fsig} and "
f"operand_sig={osig}. This indicates an internal analyzer bug."
)
_, enc0, inst0, sem0 = next_match
inst_key = (mnemonic, enc0.enc_name)
fam_dict = plan.family_shared.setdefault(family_name, {})
# Guard against silent overwrites: two sub-groups with
# different field layouts can collide on the same
# (mnemonic, enc_name) key if they share a family name
# and encoding name but diverge in layout across ISA
# generations. This doesn't happen with current ISA
# specs, but if future specs introduce intra-family
# encoding layout changes, this assertion will fire.
# Fix by introducing finer sub-family groupings or by
# incorporating the field layout into the key.
assert (
inst_key not in fam_dict
or fam_dict[inst_key].field_layout == fsig
), (
f"Key collision in family_shared['{family_name}'] for "
f"{inst_key}: existing entry has field_layout="
f"{fam_dict[inst_key].field_layout}, new group has "
f"field_layout={fsig}. Two sub-groups with different "
f"field layouts map to the same (mnemonic, enc_name) "
f"key. Consider finer sub-family groupings or a "
f"layout-aware key."
)
fam_dict[inst_key] = SharedInstInfo(
mnemonic=mnemonic,
encoding_name=enc0.enc_name,
field_layout=fsig,
semantic_class=skey[0],
operation=skey[1],
data_type=skey[2],
isa_names=sorted(group_isas),
)
else:
# ISA-exclusive — only 1 ISA has this particular
# encoding variant (field layout / semantics / operands),
# even if the mnemonic itself exists on other ISAs
# with a different structure. Gets inline code.
plan.isa_exclusive.setdefault(group_isas[0], set()).add(mnemonic)
return plan
@staticmethod
def _classify_family(isas: set[str]) -> str:
"""Return the family name for a set of ISAs.
Picks the smallest entry from ``_FAMILIES`` that contains all
input ISAs. This avoids collisions where, e.g., {rdna1, rdna2}
and {rdna3, rdna3_5} would otherwise both map to the coarse
``'rdna'`` bucket and overwrite each other in
``family_shared``. Falls back to a joined ISA-name string for
sets that cross families (e.g., {cdna1, rdna1}).
"""
# See the note above _FAMILIES re commit 2316f85. This loop
# body is the same shape as Tony's; the difference is purely
# the bucket-name strings in the table.
best_name = '_'.join(sorted(isas))
best_size = float('inf')
for name, members in _FAMILIES:
if isas <= members and len(members) < best_size:
best_name = name
best_size = len(members)
return best_name