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
834 lines
28 KiB
Python
834 lines
28 KiB
Python
# Copyright (c) 2026 Advanced Micro Devices, Inc.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""Cross-ISA legalization table generator for DBT.
|
|
|
|
Classifies every instruction on a source ISA into one of five
|
|
legalization actions for a given target ISA:
|
|
|
|
- **IDENTITY** — encoding-identical; copy instruction word(s) verbatim.
|
|
- **SUBSTITUTE** — same encoding layout, different opcode; swap opcode field.
|
|
- **LOWER** — semantically equivalent but encoding differs; emit a
|
|
target-native sequence via the matching ``lower_*()`` method in
|
|
``BinaryTranslator``.
|
|
- **EXPAND** — no target equivalent; emit a software emulation sequence.
|
|
- **ILLEGAL** — no legalization rule exists for this instruction on this
|
|
target pair.
|
|
|
|
The classifier reuses ``CrossIsaAnalyzer`` for structural comparison and
|
|
adds a mnemonic rename map for cross-generation name changes plus
|
|
domain-specific override rules for waitcnt, barrier, flat memory,
|
|
MFMA, and AccVGPR instructions.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum, auto
|
|
from typing import TYPE_CHECKING
|
|
|
|
from amdisa.cross_isa import CrossIsaAnalyzer, _field_signature, _operand_signature
|
|
|
|
if TYPE_CHECKING:
|
|
from amdisa.gpuisa import InstEncoding, Instruction, IsaSpec
|
|
from amdisa.semantics import SemanticsSpec
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Legalization action types
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class LoweringKind(Enum):
|
|
WAITCNT = auto()
|
|
BARRIER = auto()
|
|
FLAT_MEMORY = auto()
|
|
WAVE_EXEC = auto()
|
|
GENERIC = auto()
|
|
|
|
|
|
class ExpansionKind(Enum):
|
|
MFMA = auto()
|
|
ACCVGPR = auto()
|
|
WMMA = auto()
|
|
CMP_REMOVED = auto()
|
|
|
|
|
|
@dataclass
|
|
class LegalizationAction:
|
|
kind: str # 'identity', 'substitute', 'lower', 'expand', 'illegal'
|
|
target_opcode: int = 0
|
|
lowering_kind: LoweringKind | None = None
|
|
expansion_kind: ExpansionKind | None = None
|
|
|
|
@staticmethod
|
|
def identity() -> LegalizationAction:
|
|
return LegalizationAction('identity')
|
|
|
|
@staticmethod
|
|
def substitute(target_opcode: int) -> LegalizationAction:
|
|
return LegalizationAction('substitute', target_opcode=target_opcode)
|
|
|
|
@staticmethod
|
|
def lower(
|
|
kind: LoweringKind = LoweringKind.GENERIC, target_opcode: int = 0
|
|
) -> LegalizationAction:
|
|
return LegalizationAction(
|
|
'lower', target_opcode=target_opcode, lowering_kind=kind
|
|
)
|
|
|
|
@staticmethod
|
|
def expand(kind: ExpansionKind) -> LegalizationAction:
|
|
return LegalizationAction('expand', expansion_kind=kind)
|
|
|
|
@staticmethod
|
|
def illegal() -> LegalizationAction:
|
|
# Retained for API compatibility but should never appear in
|
|
# generated tables. All unmatched instructions route through
|
|
# _no_match_action() which returns LOWER or EXPAND.
|
|
return LegalizationAction('illegal')
|
|
|
|
|
|
@dataclass
|
|
class LegalizationEntry:
|
|
src_mnemonic: str
|
|
src_encoding: str
|
|
src_encoding_order: int
|
|
src_encoding_bits: int
|
|
src_opcode: int
|
|
action: LegalizationAction
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mnemonic rename map — cross-generation name changes that preserve semantics
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _build_rename_map() -> dict[str, str]:
|
|
"""Build canonical mnemonic rename map.
|
|
|
|
Maps old-style mnemonics to their canonical (newest) form so that
|
|
instructions with different names but identical semantics are
|
|
recognized as equivalent. The canonical form is the RDNA4/CDNA4
|
|
(newest generation) mnemonic.
|
|
"""
|
|
renames: dict[str, str] = {}
|
|
|
|
def _add(old: str, new: str) -> None:
|
|
if old != new:
|
|
renames[old] = new
|
|
|
|
# --- Memory size nomenclature: DWORD → B32 ---
|
|
_dword_to_b = {
|
|
'DWORD': 'B32',
|
|
'DWORDX2': 'B64',
|
|
'DWORDX3': 'B96',
|
|
'DWORDX4': 'B128',
|
|
'DWORDX8': 'B256',
|
|
'DWORDX16': 'B512',
|
|
}
|
|
for prefix in (
|
|
'S_LOAD_',
|
|
'S_STORE_',
|
|
'S_BUFFER_LOAD_',
|
|
'S_BUFFER_STORE_',
|
|
'S_ATOMIC_',
|
|
'S_BUFFER_ATOMIC_',
|
|
'BUFFER_LOAD_',
|
|
'BUFFER_STORE_',
|
|
'SCRATCH_LOAD_',
|
|
'SCRATCH_STORE_',
|
|
'DS_LOAD_',
|
|
'DS_STORE_',
|
|
'DS_READ_',
|
|
'DS_WRITE_',
|
|
'FLAT_LOAD_',
|
|
'FLAT_STORE_',
|
|
'GLOBAL_LOAD_',
|
|
'GLOBAL_STORE_',
|
|
):
|
|
for old_suffix, new_suffix in _dword_to_b.items():
|
|
_add(f'{prefix}{old_suffix}', f'{prefix}{new_suffix}')
|
|
|
|
# NOTE: FLAT_LOAD_/FLAT_STORE_ instructions canonicalize within the FLAT
|
|
# family (FLAT_LOAD_DWORD -> FLAT_LOAD_B32) via the prefix sweep above,
|
|
# NOT cross-family to GLOBAL_LOAD_*. CDNA4 and later have both FLAT (flat
|
|
# segment) and FLAT_GLBL/GLOBAL (global segment) as distinct encodings,
|
|
# so a FLAT-family mnemonic should not collapse onto a GLOBAL-family one.
|
|
|
|
# --- Sub-dword type nomenclature: UBYTE→U8, SBYTE→I8, etc. ---
|
|
_sub_dword = {
|
|
'UBYTE': 'U8',
|
|
'SBYTE': 'I8',
|
|
'USHORT': 'U16',
|
|
'SSHORT': 'I16',
|
|
'BYTE': 'B8',
|
|
'SHORT': 'B16',
|
|
'UBYTE_D16': 'U8_D16',
|
|
'SBYTE_D16': 'I8_D16',
|
|
'USHORT_D16': 'U16_D16',
|
|
'SSHORT_D16': 'I16_D16',
|
|
'UBYTE_D16_HI': 'U8_D16_HI',
|
|
'SBYTE_D16_HI': 'I8_D16_HI',
|
|
'USHORT_D16_HI': 'U16_D16_HI',
|
|
'SSHORT_D16_HI': 'I16_D16_HI',
|
|
'SHORT_D16': 'B16_D16',
|
|
'BYTE_D16_HI': 'B8_D16_HI',
|
|
'SHORT_D16_HI': 'B16_D16_HI',
|
|
}
|
|
for prefix in (
|
|
'FLAT_LOAD_',
|
|
'FLAT_STORE_',
|
|
'GLOBAL_LOAD_',
|
|
'GLOBAL_STORE_',
|
|
'SCRATCH_LOAD_',
|
|
'SCRATCH_STORE_',
|
|
'BUFFER_LOAD_',
|
|
'BUFFER_STORE_',
|
|
):
|
|
for old_suffix, new_suffix in _sub_dword.items():
|
|
_add(f'{prefix}{old_suffix}', f'{prefix}{new_suffix}')
|
|
# NOTE: see comment above on the dword sweep - FLAT sub-dword should
|
|
# canonicalize within the FLAT family, not cross to GLOBAL.
|
|
|
|
# --- BUFFER_LOAD/STORE format renames ---
|
|
for suffix in (
|
|
'D16_X',
|
|
'D16_XY',
|
|
'D16_XYZ',
|
|
'D16_XYZW',
|
|
'D16_HI_X',
|
|
'X',
|
|
'XY',
|
|
'XYZ',
|
|
'XYZW',
|
|
):
|
|
_add(f'BUFFER_LOAD_FORMAT_{suffix}', f'BUFFER_LOAD_FORMAT_{suffix}')
|
|
_add(f'BUFFER_STORE_FORMAT_{suffix}', f'BUFFER_STORE_FORMAT_{suffix}')
|
|
_add(f'BUFFER_LOAD_FORMAT_D16_{suffix}', f'BUFFER_LOAD_D16_FORMAT_{suffix}')
|
|
_add(f'BUFFER_STORE_FORMAT_D16_{suffix}', f'BUFFER_STORE_D16_FORMAT_{suffix}')
|
|
_add(
|
|
f'BUFFER_LOAD_FORMAT_D16_HI_{suffix}', f'BUFFER_LOAD_D16_HI_FORMAT_{suffix}'
|
|
)
|
|
_add(
|
|
f'BUFFER_STORE_FORMAT_D16_HI_{suffix}',
|
|
f'BUFFER_STORE_D16_HI_FORMAT_{suffix}',
|
|
)
|
|
|
|
# --- Atomic operation type suffixes ---
|
|
# GFX9/GFX10 atomics have no type suffix; GFX12 adds _U32/_U64/_B32/_B64/_I32/_I64
|
|
_atomic_type_suffixes = {
|
|
'ADD': 'ADD_U32',
|
|
'ADD_X2': 'ADD_U64',
|
|
'SUB': 'SUB_U32',
|
|
'SUB_X2': 'SUB_U64',
|
|
'INC': 'INC_U32',
|
|
'INC_X2': 'INC_U64',
|
|
'DEC': 'DEC_U32',
|
|
'DEC_X2': 'DEC_U64',
|
|
'SMIN': 'MIN_I32',
|
|
'SMIN_X2': 'MIN_I64',
|
|
'SMAX': 'MAX_I32',
|
|
'SMAX_X2': 'MAX_I64',
|
|
'UMIN': 'MIN_U32',
|
|
'UMIN_X2': 'MIN_U64',
|
|
'UMAX': 'MAX_U32',
|
|
'UMAX_X2': 'MAX_U64',
|
|
'AND': 'AND_B32',
|
|
'AND_X2': 'AND_B64',
|
|
'OR': 'OR_B32',
|
|
'OR_X2': 'OR_B64',
|
|
'XOR': 'XOR_B32',
|
|
'XOR_X2': 'XOR_B64',
|
|
'SWAP': 'SWAP_B32',
|
|
'SWAP_X2': 'SWAP_B64',
|
|
'CMPSWAP': 'CMPSWAP_B32',
|
|
'CMPSWAP_X2': 'CMPSWAP_B64',
|
|
}
|
|
for prefix in (
|
|
'BUFFER_ATOMIC_',
|
|
'FLAT_ATOMIC_',
|
|
'GLOBAL_ATOMIC_',
|
|
'SCRATCH_ATOMIC_',
|
|
'IMAGE_ATOMIC_',
|
|
'DS_',
|
|
):
|
|
for old_suffix, new_suffix in _atomic_type_suffixes.items():
|
|
_add(f'{prefix}{old_suffix}', f'{prefix}{new_suffix}')
|
|
# FLAT_ATOMIC → GLOBAL_ATOMIC
|
|
for old_suffix, new_suffix in _atomic_type_suffixes.items():
|
|
_add(f'FLAT_ATOMIC_{old_suffix}', f'GLOBAL_ATOMIC_{new_suffix}')
|
|
|
|
# DS atomics also have return variants
|
|
for old_suffix, new_suffix in _atomic_type_suffixes.items():
|
|
_add(f'DS_{old_suffix}_RTN', f'DS_{new_suffix}_RTN')
|
|
_add(f'DS_RTN_{old_suffix}', f'DS_{new_suffix}_RTN')
|
|
|
|
# --- DS compare-store rename: CMPST → CMPSTORE ---
|
|
for suffix in (
|
|
'B32',
|
|
'B64',
|
|
'F32',
|
|
'F64',
|
|
'RTN_B32',
|
|
'RTN_B64',
|
|
'RTN_F32',
|
|
'RTN_F64',
|
|
):
|
|
_add(f'DS_CMPST_{suffix}', f'DS_CMPSTORE_{suffix}')
|
|
|
|
# --- DS read/write → load/store ---
|
|
for suffix in (
|
|
'B32',
|
|
'B64',
|
|
'B128',
|
|
'B96',
|
|
'U8',
|
|
'I8',
|
|
'U16',
|
|
'I16',
|
|
'U8_D16',
|
|
'U8_D16_HI',
|
|
'I8_D16',
|
|
'I8_D16_HI',
|
|
'U16_D16',
|
|
'U16_D16_HI',
|
|
'I16_D16',
|
|
'I16_D16_HI',
|
|
'ADDTID_B32',
|
|
):
|
|
_add(f'DS_READ_{suffix}', f'DS_LOAD_{suffix}')
|
|
_add(f'DS_WRITE_{suffix}', f'DS_STORE_{suffix}')
|
|
# 2-address variants
|
|
for suffix in ('B32', 'B64'):
|
|
_add(f'DS_READ2_B32', f'DS_LOAD_2ADDR_B32')
|
|
_add(f'DS_READ2_B64', f'DS_LOAD_2ADDR_B64')
|
|
_add(f'DS_READ2ST64_B32', f'DS_LOAD_2ADDR_STRIDE64_B32')
|
|
_add(f'DS_READ2ST64_B64', f'DS_LOAD_2ADDR_STRIDE64_B64')
|
|
_add(f'DS_WRITE2_B32', f'DS_STORE_2ADDR_B32')
|
|
_add(f'DS_WRITE2_B64', f'DS_STORE_2ADDR_B64')
|
|
_add(f'DS_WRITE2ST64_B32', f'DS_STORE_2ADDR_STRIDE64_B32')
|
|
_add(f'DS_WRITE2ST64_B64', f'DS_STORE_2ADDR_STRIDE64_B64')
|
|
|
|
# --- Float min/max → min_num/max_num (IEEE 754-2019 quiet NaN) ---
|
|
for size in ('F16', 'F32', 'F64'):
|
|
_add(f'V_MIN_{size}', f'V_MIN_NUM_{size}')
|
|
_add(f'V_MAX_{size}', f'V_MAX_NUM_{size}')
|
|
_add(f'DS_MIN_{size}', f'DS_MIN_NUM_{size}')
|
|
_add(f'DS_MAX_{size}', f'DS_MAX_NUM_{size}')
|
|
_add(f'DS_MIN_RTN_{size}', f'DS_MIN_NUM_RTN_{size}')
|
|
_add(f'DS_MAX_RTN_{size}', f'DS_MAX_NUM_RTN_{size}')
|
|
# Packed
|
|
_add('V_PK_MIN_F16', 'V_PK_MIN_NUM_F16')
|
|
_add('V_PK_MAX_F16', 'V_PK_MAX_NUM_F16')
|
|
_add('V_PK_MIN_F32', 'V_PK_MIN_NUM_F32')
|
|
_add('V_PK_MAX_F32', 'V_PK_MAX_NUM_F32')
|
|
# Buffer/global/flat atomic min/max float
|
|
for prefix in ('BUFFER_ATOMIC_', 'FLAT_ATOMIC_', 'GLOBAL_ATOMIC_'):
|
|
_add(f'{prefix}FMIN', f'{prefix}MIN_NUM_F32')
|
|
_add(f'{prefix}FMAX', f'{prefix}MAX_NUM_F32')
|
|
_add(f'{prefix}MIN_F32', f'{prefix}MIN_NUM_F32')
|
|
_add(f'{prefix}MAX_F32', f'{prefix}MAX_NUM_F32')
|
|
|
|
# --- Scalar bitwise renames (GFX9→GFX11): S_ANDN2→S_AND_NOT1, etc. ---
|
|
for w in ('32', '64'):
|
|
_add(f'S_ANDN2_B{w}', f'S_AND_NOT1_B{w}')
|
|
_add(f'S_ORN2_B{w}', f'S_OR_NOT1_B{w}')
|
|
_add(f'S_NAND_B{w}', f'S_NAND_B{w}')
|
|
_add(f'S_NOR_B{w}', f'S_NOR_B{w}')
|
|
_add(f'S_XNOR_B{w}', f'S_XNOR_B{w}')
|
|
_add('S_ANDN1_SAVEEXEC_B64', 'S_AND_NOT0_SAVEEXEC_B64')
|
|
_add('S_ANDN2_SAVEEXEC_B64', 'S_AND_NOT1_SAVEEXEC_B64')
|
|
_add('S_ORN1_SAVEEXEC_B64', 'S_OR_NOT0_SAVEEXEC_B64')
|
|
_add('S_ORN2_SAVEEXEC_B64', 'S_OR_NOT1_SAVEEXEC_B64')
|
|
|
|
# --- EXP → EXPORT ---
|
|
_add('EXP', 'EXPORT')
|
|
|
|
# --- Conditional subtract: CSUB → COND_SUB ---
|
|
for prefix in (
|
|
'BUFFER_ATOMIC_',
|
|
'FLAT_ATOMIC_',
|
|
'GLOBAL_ATOMIC_',
|
|
'SCRATCH_ATOMIC_',
|
|
'DS_',
|
|
):
|
|
_add(f'{prefix}CSUB_U32', f'{prefix}COND_SUB_U32')
|
|
_add(f'{prefix}CSUB_RTN_U32', f'{prefix}COND_SUB_RTN_U32')
|
|
|
|
# --- BUFFER_WBL2 / BUFFER_INV / BUFFER_GL0_INV etc. ---
|
|
_add('BUFFER_WBL2', 'BUFFER_GL1_INV')
|
|
_add('BUFFER_INV', 'BUFFER_INV')
|
|
|
|
# --- V_INTERP renames (GFX9→GFX11) ---
|
|
_add('V_INTERP_P1_F32', 'V_INTERP_P10_F32_INREG')
|
|
_add('V_INTERP_P2_F32', 'V_INTERP_P2_F32_INREG')
|
|
_add('V_INTERP_MOV_F32', 'V_INTERP_P10_F32_INREG')
|
|
|
|
# --- V_CMP / V_CMPX: F (false) and T/TRU (true) are removed in GFX11+ ---
|
|
# These always-false/always-true comparisons are trivially emulated with
|
|
# s_mov_b{32,64} of 0 or exec. Map them to the same canonical so the
|
|
# classifier sees them as needing LOWER. We use the mnemonic of the
|
|
# equivalent s_mov instruction as the canonical form.
|
|
# (Handled in domain_rules rather than rename map — see _apply_domain_rules)
|
|
|
|
# --- S_CMPK → same (these exist on both, just encoding differences) ---
|
|
# These are handled by encoding comparison, not rename.
|
|
|
|
# --- V_MAD → V_FMA (GFX9→GFX11: V_MAD removed, V_FMA is the replacement)
|
|
# These are NOT renames — MAD is fused multiply-add without IEEE
|
|
# intermediate rounding, FMA is with. They must be LOWER, not rename.
|
|
# Handled in domain_rules.
|
|
|
|
return renames
|
|
|
|
|
|
_RENAME_MAP: dict[str, str] | None = None
|
|
|
|
|
|
def _rename_map() -> dict[str, str]:
|
|
global _RENAME_MAP
|
|
if _RENAME_MAP is None:
|
|
_RENAME_MAP = _build_rename_map()
|
|
return _RENAME_MAP
|
|
|
|
|
|
def canonical_mnemonic(name: str) -> str:
|
|
"""Normalize an instruction mnemonic via the rename map."""
|
|
return _rename_map().get(name, name)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Minimal e-graph (union-find over mnemonic equivalence classes)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class _UnionFind:
|
|
"""Path-compressing union-find for transitive mnemonic equivalence."""
|
|
|
|
def __init__(self) -> None:
|
|
self._parent: dict[int, int] = {}
|
|
|
|
def make_set(self, x: int) -> None:
|
|
if x not in self._parent:
|
|
self._parent[x] = x
|
|
|
|
def find(self, x: int) -> int:
|
|
while self._parent[x] != x:
|
|
self._parent[x] = self._parent[self._parent[x]]
|
|
x = self._parent[x]
|
|
return x
|
|
|
|
def union(self, a: int, b: int) -> int:
|
|
ra, rb = self.find(a), self.find(b)
|
|
if ra != rb:
|
|
self._parent[rb] = ra
|
|
return ra
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ISA capability queries (used by domain-specific rules)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class WaitcntModel(Enum):
|
|
GFX9_CLASSIC = auto()
|
|
GFX10_PLUS_VSCNT = auto()
|
|
GFX11_SPLIT = auto()
|
|
GFX12_SPLIT = auto()
|
|
|
|
|
|
_WAITCNT_MODELS: dict[str, WaitcntModel] = {
|
|
'cdna1': WaitcntModel.GFX9_CLASSIC,
|
|
'cdna2': WaitcntModel.GFX9_CLASSIC,
|
|
'cdna3': WaitcntModel.GFX9_CLASSIC,
|
|
'cdna4': WaitcntModel.GFX9_CLASSIC,
|
|
'rdna1': WaitcntModel.GFX10_PLUS_VSCNT,
|
|
'rdna2': WaitcntModel.GFX10_PLUS_VSCNT,
|
|
'rdna3': WaitcntModel.GFX11_SPLIT,
|
|
'rdna3_5': WaitcntModel.GFX11_SPLIT,
|
|
'rdna4': WaitcntModel.GFX12_SPLIT,
|
|
}
|
|
|
|
_SPLIT_BARRIER_ISAS = frozenset({'cdna4', 'rdna3', 'rdna3_5', 'rdna4'})
|
|
_SEPARATE_GLOBAL_ISAS = frozenset(
|
|
{
|
|
'cdna3',
|
|
'cdna4',
|
|
'rdna1',
|
|
'rdna2',
|
|
'rdna3',
|
|
'rdna3_5',
|
|
'rdna4',
|
|
}
|
|
)
|
|
_MFMA_ISAS = frozenset({'cdna1', 'cdna2', 'cdna3', 'cdna4'})
|
|
_ACCVGPR_ISAS = frozenset({'cdna2', 'cdna3', 'cdna4'})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Core classifier
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass
|
|
class _InstRecord:
|
|
"""Internal record for one instruction on one ISA."""
|
|
|
|
mnemonic: str
|
|
canonical: str
|
|
isa_name: str
|
|
enc_name: str
|
|
enc_order: int
|
|
enc_bits: int
|
|
opcode: int
|
|
field_sig: tuple[tuple[str, int], ...]
|
|
opnd_sig: tuple
|
|
eclass_id: int = -1
|
|
|
|
|
|
class LegalizationGenerator:
|
|
"""Generate cross-ISA legalization tables for all supported pairs.
|
|
|
|
Usage::
|
|
|
|
specs = [('cdna3', cdna3_spec, cdna3_sem), ('cdna4', ...)]
|
|
gen = LegalizationGenerator(specs)
|
|
for src, dst, entries in gen.generate_all():
|
|
...
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
specs: list[tuple[str, IsaSpec, SemanticsSpec | None]],
|
|
) -> None:
|
|
self._specs = specs
|
|
self._records: dict[str, list[_InstRecord]] = {}
|
|
self._uf = _UnionFind()
|
|
self._next_id = 0
|
|
self._canon_to_ids: dict[str, list[int]] = {}
|
|
self._id_to_record: dict[int, _InstRecord] = {}
|
|
|
|
self._build_records()
|
|
self._discover_equivalences()
|
|
|
|
@staticmethod
|
|
def _dt_index(enc, spec) -> int:
|
|
if enc.primary_dt_ptrs is None:
|
|
parent_name = spec.profile.derive_parent_enc_name(enc.enc_name)
|
|
if parent_name in spec.encoding_map:
|
|
return LegalizationGenerator._dt_index(
|
|
spec.encoding_map[parent_name], spec
|
|
)
|
|
return 0
|
|
for ptr in enc.primary_dt_ptrs:
|
|
if ptr != -1:
|
|
return ptr
|
|
return 0
|
|
|
|
@staticmethod
|
|
def _enc_field_value(enc, spec) -> int:
|
|
dt_idx = LegalizationGenerator._dt_index(enc, spec)
|
|
max_enc_bits = spec.profile.max_enc_bits
|
|
dont_care = max_enc_bits - enc.enc_field_bit_cnt
|
|
return dt_idx >> dont_care
|
|
|
|
def _alloc_id(self) -> int:
|
|
eid = self._next_id
|
|
self._next_id += 1
|
|
self._uf.make_set(eid)
|
|
return eid
|
|
|
|
def _build_records(self) -> None:
|
|
for isa_name, spec, _ in self._specs:
|
|
records: list[_InstRecord] = []
|
|
for enc in spec.inst_encodings:
|
|
if not enc.insts:
|
|
continue
|
|
fsig = _field_signature(enc)
|
|
for inst in enc.insts:
|
|
opsig = _operand_signature(inst)
|
|
eid = self._alloc_id()
|
|
rec = _InstRecord(
|
|
mnemonic=inst.name,
|
|
canonical=canonical_mnemonic(inst.name),
|
|
isa_name=isa_name,
|
|
enc_name=enc.enc_name,
|
|
enc_order=self._dt_index(enc, spec),
|
|
enc_bits=enc.enc_field_bit_cnt,
|
|
opcode=inst.opcode,
|
|
field_sig=fsig,
|
|
opnd_sig=opsig,
|
|
eclass_id=eid,
|
|
)
|
|
self._id_to_record[eid] = rec
|
|
self._canon_to_ids.setdefault(rec.canonical, []).append(eid)
|
|
records.append(rec)
|
|
self._records[isa_name] = records
|
|
|
|
def _discover_equivalences(self) -> None:
|
|
"""Merge e-classes for instructions with the same canonical mnemonic."""
|
|
for canon, ids in self._canon_to_ids.items():
|
|
if len(ids) < 2:
|
|
continue
|
|
first = ids[0]
|
|
for other in ids[1:]:
|
|
self._uf.union(first, other)
|
|
|
|
def _equivalent(self, a: _InstRecord, b: _InstRecord) -> bool:
|
|
return self._uf.find(a.eclass_id) == self._uf.find(b.eclass_id)
|
|
|
|
def classify(
|
|
self,
|
|
src_isa: str,
|
|
dst_isa: str,
|
|
) -> list[LegalizationEntry]:
|
|
"""Classify every source instruction for a (src, dst) pair."""
|
|
src_records = self._records.get(src_isa, [])
|
|
dst_records = self._records.get(dst_isa, [])
|
|
|
|
dst_by_class: dict[int, list[_InstRecord]] = {}
|
|
for rec in dst_records:
|
|
root = self._uf.find(rec.eclass_id)
|
|
dst_by_class.setdefault(root, []).append(rec)
|
|
|
|
entries: list[LegalizationEntry] = []
|
|
for src_rec in src_records:
|
|
src_root = self._uf.find(src_rec.eclass_id)
|
|
candidates = dst_by_class.get(src_root, [])
|
|
|
|
if not candidates:
|
|
action = self._no_match_action(src_rec)
|
|
else:
|
|
action = self._best_match_action(src_rec, candidates)
|
|
|
|
entries.append(
|
|
LegalizationEntry(
|
|
src_mnemonic=src_rec.mnemonic,
|
|
src_encoding=src_rec.enc_name,
|
|
src_encoding_order=src_rec.enc_order,
|
|
src_encoding_bits=src_rec.enc_bits,
|
|
src_opcode=src_rec.opcode,
|
|
action=action,
|
|
)
|
|
)
|
|
|
|
_apply_domain_rules(entries, src_isa, dst_isa)
|
|
return entries
|
|
|
|
@staticmethod
|
|
def _best_match_action(
|
|
src: _InstRecord,
|
|
candidates: list[_InstRecord],
|
|
) -> LegalizationAction:
|
|
for dst in candidates:
|
|
if src.field_sig == dst.field_sig and src.opnd_sig == dst.opnd_sig:
|
|
if src.opcode == dst.opcode:
|
|
return LegalizationAction('identity', target_opcode=dst.opcode)
|
|
return LegalizationAction('substitute', target_opcode=dst.opcode)
|
|
# Encoding differs — LOWER. Prefer a candidate in the same encoding
|
|
# format; fall back to any candidate; if none has a valid opcode, Expand.
|
|
same_enc = [c for c in candidates if c.enc_name == src.enc_name]
|
|
best = same_enc[0] if same_enc else candidates[0] if candidates else None
|
|
if best:
|
|
if best.opcode == 0 and src.opcode != 0:
|
|
return LegalizationAction.expand(ExpansionKind.CMP_REMOVED)
|
|
return LegalizationAction('lower', target_opcode=best.opcode)
|
|
return LegalizationAction.expand(ExpansionKind.CMP_REMOVED)
|
|
|
|
@staticmethod
|
|
def _no_match_action(src: _InstRecord) -> LegalizationAction:
|
|
"""Classify an instruction with no equivalent on the target.
|
|
|
|
Never returns ILLEGAL — every instruction can be lowered or
|
|
expanded. The BinaryTranslator dispatches to the appropriate
|
|
handler based on the action kind and lowering/expansion tag.
|
|
"""
|
|
name = src.mnemonic
|
|
if name.startswith('V_MFMA_') or name.startswith('V_SMFMAC_'):
|
|
return LegalizationAction.expand(ExpansionKind.MFMA)
|
|
if name.startswith('V_ACCVGPR_'):
|
|
return LegalizationAction.expand(ExpansionKind.ACCVGPR)
|
|
if name.startswith('V_WMMA_'):
|
|
return LegalizationAction.expand(ExpansionKind.WMMA)
|
|
if name.startswith('DS_GWS_'):
|
|
return LegalizationAction.lower(LoweringKind.GENERIC)
|
|
if name.startswith('V_MAD_'):
|
|
return LegalizationAction.lower(LoweringKind.GENERIC)
|
|
if name.startswith('V_INTERP_'):
|
|
return LegalizationAction.lower(LoweringKind.GENERIC)
|
|
if name.startswith('IMAGE_'):
|
|
return LegalizationAction.lower(LoweringKind.GENERIC)
|
|
# All remaining unmatched instructions are EXPAND — no target
|
|
# equivalent exists. BinaryTranslator emits s_nop placeholder.
|
|
return LegalizationAction.expand(ExpansionKind.CMP_REMOVED)
|
|
|
|
def generate_all(
|
|
self,
|
|
pairs: list[tuple[str, str]] | None = None,
|
|
) -> list[tuple[str, str, list[LegalizationEntry]]]:
|
|
"""Generate legalization tables for all (or specified) ISA pairs."""
|
|
if pairs is None:
|
|
pairs = _default_pairs(self._specs)
|
|
results = []
|
|
for src, dst in pairs:
|
|
entries = self.classify(src, dst)
|
|
results.append((src, dst, entries))
|
|
return results
|
|
|
|
def summary(
|
|
self,
|
|
entries: list[LegalizationEntry],
|
|
) -> dict[str, int]:
|
|
"""Count entries by action kind."""
|
|
counts: dict[str, int] = {
|
|
'identity': 0,
|
|
'substitute': 0,
|
|
'lower': 0,
|
|
'expand': 0,
|
|
'illegal': 0,
|
|
}
|
|
for e in entries:
|
|
counts[e.action.kind] += 1
|
|
return counts
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Domain-specific override rules
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _apply_domain_rules(
|
|
entries: list[LegalizationEntry],
|
|
src_isa: str,
|
|
dst_isa: str,
|
|
) -> None:
|
|
src_wc = _WAITCNT_MODELS.get(src_isa)
|
|
dst_wc = _WAITCNT_MODELS.get(dst_isa)
|
|
dst_has_split_barrier = dst_isa in _SPLIT_BARRIER_ISAS
|
|
src_has_split_barrier = src_isa in _SPLIT_BARRIER_ISAS
|
|
dst_has_mfma = dst_isa in _MFMA_ISAS
|
|
dst_has_accvgpr = dst_isa in _ACCVGPR_ISAS
|
|
|
|
for entry in entries:
|
|
name = entry.src_mnemonic
|
|
|
|
# Helper: preserve any target_opcode the classifier identified, so that
|
|
# downstream encoding/semantic translators have the right dst_op even
|
|
# when the action is overridden to flag a domain-specific lowering.
|
|
prev_op = entry.action.target_opcode
|
|
|
|
if name in (
|
|
'S_WAITCNT',
|
|
'S_WAITCNT_VSCNT',
|
|
'S_WAITCNT_VMCNT',
|
|
'S_WAITCNT_LGKMCNT',
|
|
'S_WAITCNT_EXPCNT',
|
|
):
|
|
if src_wc != dst_wc:
|
|
entry.action = LegalizationAction.lower(
|
|
LoweringKind.WAITCNT, target_opcode=prev_op
|
|
)
|
|
|
|
if name.startswith('S_WAIT_') and name not in (
|
|
'S_WAITCNT',
|
|
'S_WAITCNT_VSCNT',
|
|
'S_WAITCNT_VMCNT',
|
|
'S_WAITCNT_LGKMCNT',
|
|
'S_WAITCNT_EXPCNT',
|
|
):
|
|
if src_wc != dst_wc:
|
|
entry.action = LegalizationAction.lower(
|
|
LoweringKind.WAITCNT, target_opcode=prev_op
|
|
)
|
|
|
|
if name == 'S_BARRIER':
|
|
if not src_has_split_barrier and dst_has_split_barrier:
|
|
entry.action = LegalizationAction.lower(
|
|
LoweringKind.BARRIER, target_opcode=prev_op
|
|
)
|
|
if name in ('S_BARRIER_SIGNAL', 'S_BARRIER_WAIT'):
|
|
if src_has_split_barrier and not dst_has_split_barrier:
|
|
entry.action = LegalizationAction.lower(
|
|
LoweringKind.BARRIER, target_opcode=prev_op
|
|
)
|
|
|
|
if name.startswith('FLAT_LOAD_') or name.startswith('FLAT_STORE_'):
|
|
if (
|
|
dst_isa in _SEPARATE_GLOBAL_ISAS
|
|
and src_isa not in _SEPARATE_GLOBAL_ISAS
|
|
):
|
|
entry.action = LegalizationAction.lower(
|
|
LoweringKind.FLAT_MEMORY, target_opcode=prev_op
|
|
)
|
|
|
|
if name.startswith('V_MFMA_') and not dst_has_mfma:
|
|
entry.action = LegalizationAction.expand(ExpansionKind.MFMA)
|
|
|
|
if name.startswith('V_ACCVGPR_') and not dst_has_accvgpr:
|
|
entry.action = LegalizationAction.expand(ExpansionKind.ACCVGPR)
|
|
|
|
# V_CMP_F_* (always-false) and V_CMP_TRU_* / V_CMPX_TRU_* (always-true)
|
|
# are removed on GFX11+ (RDNA3/3.5/4). They have no target equivalent
|
|
# and must be expanded to s_mov_b32 vcc, 0 / s_mov_b64 vcc, exec.
|
|
if name.startswith(
|
|
(
|
|
'V_CMP_F_',
|
|
'V_CMPX_F_',
|
|
'V_CMP_TRU_',
|
|
'V_CMPX_TRU_',
|
|
'V_CMP_T_',
|
|
'V_CMPX_T_',
|
|
)
|
|
):
|
|
if entry.action.target_opcode == 0:
|
|
entry.action = LegalizationAction.expand(ExpansionKind.CMP_REMOVED)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Default translation pairs
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _default_pairs(
|
|
specs: list[tuple[str, IsaSpec, SemanticsSpec | None]],
|
|
) -> list[tuple[str, str]]:
|
|
"""Return the set of supported translation pairs from the loaded ISAs."""
|
|
names = {name for name, _, _ in specs}
|
|
all_pairs = [
|
|
# Intra-CDNA upgrades
|
|
('cdna1', 'cdna2'),
|
|
('cdna1', 'cdna3'),
|
|
('cdna1', 'cdna4'),
|
|
('cdna2', 'cdna3'),
|
|
('cdna2', 'cdna4'),
|
|
('cdna3', 'cdna4'),
|
|
# CDNA → RDNA
|
|
('cdna1', 'rdna1'),
|
|
('cdna1', 'rdna2'),
|
|
('cdna1', 'rdna3'),
|
|
('cdna1', 'rdna4'),
|
|
('cdna2', 'rdna3'),
|
|
('cdna2', 'rdna4'),
|
|
('cdna3', 'rdna3'),
|
|
('cdna3', 'rdna4'),
|
|
('cdna4', 'rdna3'),
|
|
('cdna4', 'rdna4'),
|
|
# CDNA downgrade used to run CDNA4 code objects on CDNA3 hosts.
|
|
('cdna4', 'cdna3'),
|
|
# Intra-RDNA upgrades
|
|
('rdna1', 'rdna2'),
|
|
('rdna1', 'rdna3'),
|
|
('rdna1', 'rdna4'),
|
|
('rdna2', 'rdna3'),
|
|
('rdna2', 'rdna4'),
|
|
('rdna3', 'rdna4'),
|
|
('rdna3_5', 'rdna4'),
|
|
# RDNA → CDNA
|
|
('rdna1', 'cdna3'),
|
|
('rdna1', 'cdna4'),
|
|
('rdna3', 'cdna4'),
|
|
('rdna4', 'cdna4'),
|
|
]
|
|
return [(s, d) for s, d in all_pairs if s in names and d in names]
|