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
425 lines
19 KiB
Python
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
|