Files
TheRock/rocm-libraries/projects/hipdnn/tools/DescriptorGenerator/codegen/models.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

1249 lines
45 KiB
Python

# Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
# SPDX-License-Identifier: MIT
"""Data models for descriptor code generation."""
import re
from dataclasses import dataclass, field
from typing import Optional
MODE_SENTINEL_VALUES: tuple[str, ...] = ("required", "optional", "none")
@dataclass
class EnumValue:
"""A single value in a mode enum.
Represents one enum constant across three layers: backend C-API, SDK, and frontend.
The ``name`` and ``value`` fields define the backend C-API constant. Override fields
(``sdk_name``, ``frontend_name``, ``frontend_value``) handle cases where the three
layers use different names or numeric values for the same logical constant.
Attributes:
name: Backend C-API suffix appended to ``EnumDef.backend_prefix``
(e.g., ``"CROSS_CORRELATION"`` → ``HIPDNN_CROSS_CORRELATION``).
value: Numeric value in the backend C-API enum typedef.
sentinel: If True, this value (typically UNSET/NOT_SET) is excluded from the
backend C-API enum but appears as ``NOT_SET = 0`` in the frontend enum class.
description: Doxygen-style description for the enum constant
(e.g., ``"Cross-correlation mode"``). Rendered as ``///< description`` in
generated code.
sdk_name: SDK (FlatBuffer) enum constant name when it differs from ``name``
(e.g., ``"MAX_OP"`` when backend uses ``"MAX"``).
frontend_name: Frontend enum class member name when it differs from ``name``
(e.g., ``"TOP_LEFT"`` when backend uses ``"TOP_LEFT_EXT"``).
frontend_value: Frontend enum numeric value when it differs from the backend
``value``. ``None`` means the frontend uses the same numeric value as the
backend (the common case).
"""
name: str
value: int
sentinel: bool = False
description: str = ""
sdk_name: str = ""
frontend_name: str = ""
frontend_value: Optional[int] = None
@property
def effective_sdk_name(self) -> str:
"""SDK enum constant name.
Fallback chain: sdk_name → frontend_name → name.
The frontend_name fallback covers cases like DiagonalAlignment where the SDK
uses ``TOP_LEFT`` (matching the frontend) but the backend uses ``TOP_LEFT_EXT``.
"""
return self.sdk_name or self.frontend_name or self.name
@property
def effective_frontend_name(self) -> str:
"""Frontend enum class member name (defaults to name)."""
return self.frontend_name or self.name
@property
def effective_frontend_value(self) -> int:
"""Frontend enum numeric value (defaults to backend value)."""
return self.frontend_value if self.frontend_value is not None else self.value
@dataclass
class EnumDef:
"""Full enum definition for code generation.
When present on a ``DataField`` with ``type="mode"`` and ``shared=False``,
the generator produces all mode enum plumbing: backend C-API header,
type tag fragment, SDK converters, attribute utils, string utils, and
frontend converters.
When ``shared=True``, the ``enum_def`` serves as documentation only — the
generator skips plumbing generation because the enum infrastructure already
exists (defined by another operation's config).
"""
backend_header: str = "" # Output header filename (e.g., "HipdnnPointwiseMode.h")
backend_prefix: str = "" # C-API constant prefix (e.g., "HIPDNN_POINTWISE_")
values: list[EnumValue] = field(default_factory=list)
@property
def non_sentinel_values(self) -> list[EnumValue]:
"""Values excluding sentinels, sorted by backend numeric value."""
return sorted([v for v in self.values if not v.sentinel], key=lambda v: v.value)
@dataclass
class TensorField:
"""A tensor field stored as shared_ptr<TensorDescriptor> + UID in _data."""
name: str
fbs_field: str
attr_suffix: str
required: bool = True
frontend_getter: str = ""
@property
def camel_name(self) -> str:
"""Field name in camelCase (e.g., 'inv_variance' -> 'invVariance')."""
return _to_camel_case(self.name)
@property
def pascal_name(self) -> str:
"""Field name in PascalCase (e.g., 'inv_variance' -> 'InvVariance')."""
parts = self.name.split("_")
return "".join(p.capitalize() for p in parts)
@property
def member_name(self) -> str:
return f"_{self.camel_name}Desc"
@property
def local_desc_name(self) -> str:
"""Local variable name for descriptor (e.g., 'invVarianceDesc')."""
return f"{self.camel_name}Desc"
@property
def err_name(self) -> str:
"""Error variable name for structured bindings (e.g., 'errInvVariance')."""
return f"err{self.pascal_name}"
@property
def uid_field(self) -> str:
return self.fbs_field
@property
def getter_name(self) -> str:
return f"get{self.pascal_name}Desc"
@property
def frontend_setter(self) -> str:
"""Derive setter method name for unpacker."""
return f"set_{self.name}"
def _to_camel_case(snake: str) -> str:
"""Convert snake_case to camelCase."""
parts = snake.split("_")
return parts[0] + "".join(p.capitalize() for p in parts[1:])
def _to_snake_case(pascal: str) -> str:
"""Convert PascalCase to snake_case (e.g., 'ConvolutionFwd' -> 'convolution_fwd')."""
s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", pascal)
s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s)
return s.lower()
def _derive_frontend_setter_name(frontend_getter: str, fallback_field_name: str) -> str:
"""Derive a frontend setter name from a getter spec.
"get_foo" / "get_foo()" -> "set_foo"
"foo" -> "foo" # member-access; setter is the member name
"" -> "set_<fallback_field_name>"
"""
if frontend_getter:
base = frontend_getter.replace("()", "")
if base.startswith("get_"):
return "set_" + base[4:]
return base
return f"set_{fallback_field_name}"
def _frontend_uses_member_access(frontend_getter: str) -> bool:
"""True when the frontend getter is a bare member name (no parentheses)."""
return bool(frontend_getter) and "(" not in frontend_getter
def _frontend_member_name(frontend_getter: str) -> str:
"""The bare member name, with any trailing ``()`` stripped."""
return frontend_getter.replace("()", "")
@dataclass
class DataField:
"""A scalar/vector/enum field stored in _data directly."""
name: str
fbs_field: str
attr_name: str
type: str # vector_int64, enum, mode, scalar_float, scalar_int64, bool
required: bool = True
frontend_getter: str = ""
frontend_converter: str = ""
cpp_enum: str = ""
frontend_type: str = ""
default_value: str = ""
test_value: Optional[list] = None
test_label: str = ""
build_node_check: bool = True
shared: bool = False
test_enum_value: str = ""
test_constant_name: str = ""
test_backend_value: str = ""
# FBS optional qualifier: True when the FBS field uses (optional) qualifier.
# Controls whether bool fields use optional patterns or plain scalar patterns.
# FBS `bool field = false` -> fbs_optional: false (plain bool, use setScalar/getScalar)
# FBS `bool field (optional)` -> fbs_optional: true (Optional<bool>, use setOptionalScalar)
fbs_optional: bool = False
# DEPRECATED: the loader now hard-rejects this YAML key
# (see ``config_loader._reject_deprecated_field``). The field is
# retained on the dataclass so existing call sites compile, but no
# successfully-loaded config will ever set it to ``True``.
# Optional-return logic is derived from ``fbs_optional`` via the
# ``frontend_returns_optional`` property below.
frontend_getter_returns_optional: bool = False
# Mode-specific config (for conv_mode, pointwise_mode, etc.)
backend_setter: str = ""
backend_getter: str = ""
backend_converter: str = ""
backend_type_name: str = ""
test_c_type: str = ""
test_default_value: str = ""
test_alt_enum_value: str = ""
# Lifting support (unpacker)
frontend_inverse_converter: str = ""
# Mode enum definition (for generating new enum plumbing)
enum_def: Optional[EnumDef] = None
# Mode-field sentinel-check policy (Task 0.3). One of:
# "required" — finalize sentinel check is emitted; sentinel MUST exist in enum_def
# "optional" — finalize sentinel check is emitted IF a sentinel exists; otherwise
# silently skipped (soft mode for in-flight migrations)
# "none" — finalize sentinel check is NEVER emitted; enum has no sentinel by design
# None — unset; resolved by `effective_mode_sentinel` via auto-detection
mode_sentinel: Optional[str] = None
@property
def has_enum_def(self) -> bool:
"""Whether this field has a generatable enum definition."""
return self.enum_def is not None and len(self.enum_def.values) > 0
@property
def has_sentinel_in_enum_def(self) -> bool:
"""Whether this field's enum_def contains a sentinel entry.
A sentinel is detected either by ``EnumValue.sentinel == True`` or by
``name``/``effective_sdk_name`` matching the conventional sentinel
names ``NOT_SET`` / ``UNSET``.
"""
if not self.enum_def:
return False
for v in self.enum_def.values:
if v.sentinel:
return True
if v.name in ("NOT_SET", "UNSET"):
return True
if v.effective_sdk_name in ("NOT_SET", "UNSET"):
return True
return False
@property
def effective_mode_sentinel(self) -> str:
"""Resolved mode-sentinel policy.
If ``mode_sentinel`` was set explicitly on the YAML, returns that value.
Otherwise auto-detects: ``"required"`` when the enum has a sentinel,
``"none"`` when it does not.
"""
if self.mode_sentinel is not None:
return self.mode_sentinel
return "required" if self.has_sentinel_in_enum_def else "none"
@property
def emit_mode_sentinel_check(self) -> bool:
"""Whether the finalize() sentinel check should be emitted for this mode field.
- ``required``: always emit (legacy behavior)
- ``optional``: emit only if a sentinel is present in the enum_def
- ``none``: never emit
"""
policy = self.effective_mode_sentinel
if policy == "none":
return False
if policy == "optional":
return self.has_sentinel_in_enum_def
# "required" or anything else falls back to legacy behavior
return True
@property
def effective_sentinel_value(self) -> str:
"""Sentinel enum value for finalize() validation.
Returns the sentinel name from enum_def if available, otherwise 'NOT_SET'.
"""
if self.enum_def:
for v in self.enum_def.values:
if v.sentinel:
return v.effective_sdk_name
return "NOT_SET"
@property
def camel_name(self) -> str:
"""Field name in camelCase (e.g., 'pre_padding' -> 'prePadding')."""
return _to_camel_case(self.name)
@property
def pascal_name(self) -> str:
"""Field name in PascalCase (e.g., 'relu_lower_clip' -> 'ReluLowerClip')."""
parts = self.name.split("_")
return "".join(p.capitalize() for p in parts)
@property
def is_vector(self) -> bool:
return self.type == "vector_int64"
@property
def is_enum(self) -> bool:
return self.type == "enum"
@property
def is_mode(self) -> bool:
return self.type == "mode"
@property
def is_scalar(self) -> bool:
return self.type in ("scalar_float", "scalar_int32", "scalar_int64", "bool")
@property
def is_optional_scalar(self) -> bool:
if self.type == "bool":
return self.fbs_optional
return self.is_scalar and not self.required
@property
def frontend_returns_optional(self) -> bool:
"""Whether the frontend getter for this field returns ``std::optional<T>``.
Derived from ``fbs_optional`` alone — when the FBS field is optional,
the frontend getter is expected to return ``std::optional<T>``.
Replaces the legacy ``frontend_getter_returns_optional`` YAML field,
which the loader now hard-rejects.
"""
return self.fbs_optional
@property
def cpp_type(self) -> str:
"""C++ type name for the field's element type."""
type_map = {
"vector_int64": "int64_t",
"scalar_float": "float",
"scalar_int32": "int32_t",
"scalar_int64": "int64_t",
"bool": "bool",
}
return type_map.get(self.type, "int64_t")
@property
def backend_type(self) -> str:
if self.type == "mode" and self.backend_type_name:
return self.backend_type_name
type_map = {
"vector_int64": "HIPDNN_TYPE_INT64",
"enum": "HIPDNN_TYPE_INT64",
"scalar_float": "HIPDNN_TYPE_FLOAT",
"scalar_int32": "HIPDNN_TYPE_INT32",
"scalar_int64": "HIPDNN_TYPE_INT64",
"bool": "HIPDNN_TYPE_BOOLEAN",
}
return type_map.get(self.type, "HIPDNN_TYPE_INT64")
@property
def setter_helper_name(self) -> str:
"""Name for the private helper method (enum fields only)."""
parts = self.name.split("_")
camel = "".join(p.capitalize() for p in parts)
return f"set{camel}"
@property
def getter_helper_name(self) -> str:
"""Name for the private helper method (enum fields only)."""
parts = self.name.split("_")
camel = "".join(p.capitalize() for p in parts)
return f"get{camel}"
@property
def enum_short_type(self) -> str:
"""Short enum type name (e.g., 'ConvMode' from full namespace)."""
if self.cpp_enum:
return self.cpp_enum.rsplit("::", 1)[-1]
return ""
@property
def effective_frontend_type(self) -> str:
"""Frontend C++ type for mode/enum fields.
Returns frontend_type if set, falls back to stripping the SDK namespace
from cpp_enum (e.g., 'hipdnn_flatbuffers_sdk::data_objects::ConvMode' -> 'ConvMode').
"""
if self.frontend_type:
return self.frontend_type
if self.cpp_enum:
return self.cpp_enum.rsplit("::", 1)[-1]
return ""
@property
def frontend_setter_name(self) -> str:
"""Derive setter method name for unpacker.
If frontend_getter is set (e.g., "get_convolution_mode()"), derives
"set_convolution_mode". Otherwise uses "set_{name}".
"""
return _derive_frontend_setter_name(self.frontend_getter, self.name)
@property
def frontend_uses_member_access(self) -> bool:
"""True when the frontend exposes this field as a public member, not a method.
Detected from ``frontend_getter``: bare names like ``"alibi_mask"`` (no
parentheses) indicate the in-tree class exposes the field as a public
member; method-call syntax like ``"get_alibi_mask()"`` indicates a
getter method. Hand-maintained classes such as ``SdpaAttributes``
expose ``std::optional<T>`` and ``bool`` members directly. The unpacker
emits ``attributes.<member> = value`` instead of
``attributes.set_<member>(value)`` when this is true, which is safer
than guessing setter names that may diverge (e.g.
``set_diagonal_band_left_bound`` for the ``left_bound`` member).
"""
return _frontend_uses_member_access(self.frontend_getter)
@property
def frontend_member_name(self) -> str:
"""Public member name when ``frontend_uses_member_access`` is true.
Returns the ``frontend_getter`` value with any trailing ``()`` stripped.
For non-member fields callers should use ``frontend_getter`` /
``frontend_setter_name`` instead.
"""
return _frontend_member_name(self.frontend_getter)
@property
def mode_converter_returns_optional(self) -> bool:
"""Whether the frontend→backend mode converter returns ``std::optional<T>``.
Mode converters return ``std::optional<T>`` when the enum has a
sentinel value (so the converter can signal "unsupported/unset" by
returning ``std::nullopt``). When ``mode_sentinel`` is explicitly
``"none"`` the enum has no sentinel by design and the converter
returns plain ``T`` — so the packer must NOT emit the
``.has_value()`` / ``*deref`` plumbing around the converter result.
"""
return self.is_mode and self.effective_mode_sentinel != "none"
@dataclass
class DataFieldsHelper:
"""Configuration for shared data field packing/unpacking helpers.
When set on an OperationConfig, the packer and unpacker templates call
the named helper functions instead of emitting per-field inline code.
"""
pack_function: str = ""
unpack_function: str = ""
include: str = ""
label: str = ""
@dataclass
class TensorArrayField:
"""A tensor array field (e.g., peer_stats_tensor_uid: [long])."""
name: str
fbs_field: str
attr_name: str
frontend_getter: str = ""
required: bool = False
test_uids: list[int] = field(default_factory=list)
test_label: str = ""
@property
def member_name(self) -> str:
"""Member variable name (e.g., 'peer_stats' -> '_peer_statsDescs')."""
return f"_{self.name}Descs"
@property
def camel_name(self) -> str:
"""Field name in camelCase (e.g., 'peer_stats' -> 'peerStats')."""
return _to_camel_case(self.name)
@property
def frontend_setter_name(self) -> str:
"""Derive setter method name for unpacker.
If frontend_getter is set (e.g., "get_peer_stats()"), derives
"set_peer_stats". Otherwise uses "set_{name}".
"""
return _derive_frontend_setter_name(self.frontend_getter, self.name)
@dataclass
class ExtraDataTypeField:
"""Operation-level ``DataType``-typed attribute beyond ``compute_data_type``.
Models cases like SDPA's ``mma_core_mode`` — a ``DataType`` value packed
via ``setDescriptorAttrDataType`` and unpacked via ``unpackGraphDataType``,
optionally gated on a sentinel value (so the attribute is omitted from
the descriptor when unset). The frontend exposes the field either via a
public member (when ``frontend_getter`` is a bare member name) or a
getter/setter pair (when ``frontend_getter`` ends with ``()``).
"""
name: str
attr_name: str
frontend_getter: str = ""
sentinel: str = ""
error_label: str = ""
@property
def frontend_uses_member_access(self) -> bool:
"""True when the frontend exposes this field as a public member."""
return _frontend_uses_member_access(self.frontend_getter)
@property
def frontend_member_name(self) -> str:
"""Public member name (only meaningful when member access is used)."""
return _frontend_member_name(self.frontend_getter)
@property
def frontend_setter_name(self) -> str:
"""Derived setter method name (only meaningful when method access is used).
Mirrors :attr:`DataField.frontend_setter_name` — strips trailing
``()`` and rewrites a leading ``get_`` to ``set_``.
"""
return _derive_frontend_setter_name(self.frontend_getter, self.name)
@property
def camel_name(self) -> str:
"""camelCase local variable name used in generated code."""
return _to_camel_case(self.name)
@property
def effective_error_label(self) -> str:
"""Sub-label appended to ``op.effective_error_label`` in error strings."""
return self.error_label or self.name
@dataclass
class TensorConfig:
"""Test tensor configuration."""
dims: list[int] = field(default_factory=list)
strides: list[int] = field(default_factory=list)
@dataclass
class TestData:
"""Test data for generated tests."""
tensor_uids: dict[str, int] = field(default_factory=dict)
tensor_configs: dict[str, TensorConfig] = field(default_factory=dict)
field_values: dict[str, list] = field(default_factory=dict)
constants_include: str = ""
tensor_const_prefix: Optional[str] = None
@dataclass
class FrontendTensorConfig:
"""An input or output tensor for the frontend Attributes class."""
name: str
enum_name: str = ""
enum_value: int = -1
required: bool = True
getter_name: str = ""
setter_name: str = ""
@property
def camel_name(self) -> str:
"""Name in camelCase (e.g., 'input_0' -> 'input0')."""
return _to_camel_case(self.name)
@property
def effective_enum_name(self) -> str:
"""Enum value name (default: uppercase of name)."""
return self.enum_name or self.name.upper()
@property
def effective_getter_name(self) -> str:
"""Getter method name (default: get_<name>)."""
return self.getter_name or f"get_{self.name}"
@property
def effective_setter_name(self) -> str:
"""Setter method name (default: set_<name>)."""
return self.setter_name or f"set_{self.name}"
@dataclass
class GraphMethodParam:
"""A parameter in the Graph class method signature."""
name: str
tensor_name: str = ""
type: str = "std::shared_ptr<TensorAttributes>"
optional: bool = False
@dataclass
class InferPropertiesConfig:
"""Configuration for infer_properties_node() code generation."""
strategy: str = "stub"
reference_input: str = ""
dimension_formula: str = ""
@dataclass
class ValidationConfig:
"""Configuration for pre_validate_node() code generation."""
required_input_tensors: list[str] = field(default_factory=list)
required_input_dims: list[str] = field(default_factory=list)
custom_checks: list[str] = field(default_factory=list)
@dataclass
class FrontendConfig:
"""Frontend-specific configuration."""
packer_function: str = ""
node_class: str = ""
attributes_class: str = ""
attributes_include: str = ""
# Optional override for the basename of the generated Attributes header
# (and matching `#include` paths). When unset, the basename is derived from
# ``attributes_class`` by stripping the trailing ``Attributes`` suffix.
# Use this when the in-tree filename diverges from the C++ class name (e.g.,
# class ``ConvFpropAttributes`` lives in file ``ConvolutionFpropAttributes.hpp``)
# or when the operation ``name`` would collide with the suffix (e.g.,
# ``BatchnormInferenceAttributesVarianceExt`` would otherwise become
# ``BatchnormInferenceAttributesVarianceExtAttributes``).
# Value is the basename WITHOUT the ``Attributes`` suffix
# (e.g., ``"ConvolutionFprop"`` -> filename ``"ConvolutionFpropAttributes.hpp"``).
attributes_filename: Optional[str] = None
# Lifting support (unpacker)
unpacker_function: str = ""
unpacker_include: str = ""
# Frontend generation fields
inputs: list[FrontendTensorConfig] = field(default_factory=list)
outputs: list[FrontendTensorConfig] = field(default_factory=list)
graph_method_name: str = ""
graph_method_params: list[GraphMethodParam] = field(default_factory=list)
graph_return_type: str = "single"
graph_return_outputs: list[str] = field(default_factory=list)
node_type_enum: str = ""
node_attributes_union_type: str = ""
compatibility_typedef: str = ""
@property
def effective_attributes_filename(self) -> str:
"""Basename for the Attributes header WITHOUT the ``Attributes`` suffix.
Resolution order:
1. ``attributes_filename`` if explicitly set on the YAML.
2. ``attributes_class`` with the trailing ``Attributes`` stripped
(e.g., ``ConvFpropAttributes`` -> ``ConvFprop``).
3. ``node_class`` with the trailing ``Node`` stripped
(e.g., ``ConvolutionFpropNode`` -> ``ConvolutionFprop``).
4. Empty string.
Mirrors the precedence used by ``OperationConfig._frontend_base_name``
(which delegates here): file basenames default to the attributes-class
basename, with the explicit ``attributes_filename`` override taking
priority for cases where the in-tree filename diverges (e.g., conv,
batchnorm_inference_variance_ext).
"""
if self.attributes_filename:
return self.attributes_filename
if self.attributes_class:
cls = self.attributes_class
if cls.endswith("Attributes"):
return cls[: -len("Attributes")]
return cls
if self.node_class:
cls = self.node_class
if cls.endswith("Node"):
return cls[: -len("Node")]
return cls
return ""
@property
def effective_attributes_filename_full(self) -> str:
"""Full basename for the Attributes header (with ``Attributes`` suffix).
Equivalent to ``effective_attributes_filename + "Attributes"``. This is the
canonical basename used both for the generated ``.hpp`` filename and for
any ``#include <hipdnn_frontend/attributes/<basename>.hpp>`` lines.
"""
base = self.effective_attributes_filename
if not base:
return ""
return f"{base}Attributes"
@property
def effective_attributes_include(self) -> str:
"""Include file name for the attributes class.
Resolution order (legacy semantics, preserved for back-compat):
1. ``attributes_include`` if explicitly set.
2. ``node_class`` with the trailing ``Node`` stripped, suffixed with
``Attributes`` (e.g., ``ConvolutionFpropNode`` -> ``ConvolutionFpropAttributes``).
3. ``attributes_class``.
Note: the new canonical accessor for the attributes header basename is
``effective_attributes_filename_full``, which honors the
``attributes_filename`` YAML override and uses ``attributes_class``
precedence to match generated output filenames. ``effective_attributes_include``
keeps its historical ``node_class``-first precedence so existing configs
(which rely on ``attributes_include`` or the node_class-derived include
path) continue to render unchanged. Phase 2 will migrate emit sites to
``effective_attributes_filename_full`` as configs adopt
``attributes_filename``.
"""
if self.attributes_include:
return self.attributes_include
# Derive from node_class: ConvolutionFpropNode -> ConvolutionFpropAttributes
if self.node_class:
base = (
self.node_class[:-4]
if self.node_class.endswith("Node")
else self.node_class
)
return f"{base}Attributes"
return self.attributes_class
@property
def required_inputs(self) -> list[FrontendTensorConfig]:
"""Input tensors that are required."""
return [t for t in self.inputs if t.required]
@property
def optional_inputs(self) -> list[FrontendTensorConfig]:
"""Input tensors that are optional."""
return [t for t in self.inputs if not t.required]
@property
def required_outputs(self) -> list[FrontendTensorConfig]:
"""Output tensors that are required."""
return [t for t in self.outputs if t.required]
@property
def optional_outputs(self) -> list[FrontendTensorConfig]:
"""Output tensors that are optional."""
return [t for t in self.outputs if not t.required]
@property
def all_tensors(self) -> list[FrontendTensorConfig]:
"""All input and output tensors combined."""
return self.inputs + self.outputs
@property
def deserialization_accessor(self) -> str:
"""FBS accessor method for deserializeFromFlatBuffer() switch case.
Derives from node_attributes_union_type by stripping the
'NodeAttributes_' prefix: e.g.,
'NodeAttributes_ConvolutionFwdAttributes' -> 'attributes_as_ConvolutionFwdAttributes'
"""
if self.node_attributes_union_type:
variant = self.node_attributes_union_type
if variant.startswith("NodeAttributes_"):
variant = variant[len("NodeAttributes_") :]
return f"attributes_as_{variant}"
return ""
@dataclass
class DescriptorTypeConfig:
"""Descriptor type enum configuration."""
enum_name: str = ""
@dataclass
class OperationConfig:
"""Complete configuration for one operation type."""
name: str
class_name: str
fbs_table: str
fbs_generated_header: str
descriptor_type: DescriptorTypeConfig = field(default_factory=DescriptorTypeConfig)
operation_attr_prefix: str = ""
frontend: FrontendConfig = field(default_factory=FrontendConfig)
tensor_fields: list[TensorField] = field(default_factory=list)
data_fields: list[DataField] = field(default_factory=list)
tensor_array_fields: list[TensorArrayField] = field(default_factory=list)
extra_data_type_fields: list[ExtraDataTypeField] = field(default_factory=list)
data_fields_helper: Optional[DataFieldsHelper] = None
has_compute_data_type: bool = True
compute_data_type_attr: str = ""
compute_data_type_shared: bool = False
# Lifting support (unpacker)
operation_type_enum: str = ""
error_label: str = ""
packer_operation_label: str = ""
packer_finalize_label: str = ""
test_params_method_name: str = ""
data_fields_section_label: str = ""
build_node_attrs_var: str = ""
# Frontend generation support
infer_properties: Optional[InferPropertiesConfig] = None
validation: Optional[ValidationConfig] = None
test_data: TestData = field(default_factory=TestData)
# --- Computed properties ---
@property
def effective_error_label(self) -> str:
"""Short label for error strings (e.g., 'conv')."""
return self.error_label or self.name.lower()
@property
def effective_packer_operation_label(self) -> str:
"""Human-readable operation label for packer comments/errors."""
return self.packer_operation_label or self.name.lower()
@property
def effective_packer_finalize_label(self) -> str:
"""Label for the finalize error message in packer."""
return self.packer_finalize_label or self.name.lower()
@property
def packer_params_label(self) -> str:
"""Label for the parameters section comment in packer."""
return self.packer_finalize_label or self.name.lower()
@property
def effective_test_params_method_name(self) -> str:
"""Method name for setting operation params in tests."""
return self.test_params_method_name or f"set{self.name}Params"
@property
def effective_data_fields_section_label(self) -> str:
"""Section label for data fields in test comments."""
return self.data_fields_section_label or "Data Fields"
@property
def effective_build_node_attrs_var(self) -> str:
"""Variable name for the attributes pointer in buildNode test."""
return self.build_node_attrs_var or "attrs"
@property
def error_prefix(self) -> str:
return f"{self.class_name}::"
@property
def fbs_namespace(self) -> str:
return "hipdnn_flatbuffers_sdk::data_objects"
@property
def fbs_t_type(self) -> str:
return f"{self.fbs_table}T"
@property
def node_attributes_union_member(self) -> str:
return self.fbs_table
@property
def header_filename(self) -> str:
return f"{self.class_name}.hpp"
@property
def source_filename(self) -> str:
return f"{self.class_name}.cpp"
@property
def packer_filename(self) -> str:
# Derive from frontend packer function name
if self.frontend.packer_function:
# createConvFpropOperation -> ConvolutionFpropPacker.hpp
# We use the node_class name if available
if self.frontend.node_class:
base = (
self.frontend.node_class[:-4]
if self.frontend.node_class.endswith("Node")
else self.frontend.node_class
)
return f"{base}Packer.hpp"
return f"{self.name}Packer.hpp"
@property
def unpacker_filename(self) -> str:
"""Filename for the generated unpacker header."""
if self.frontend.node_class:
base = (
self.frontend.node_class[:-4]
if self.frontend.node_class.endswith("Node")
else self.frontend.node_class
)
return f"{base}Unpacker.hpp"
return f"{self.name}Unpacker.hpp"
@property
def test_from_node_filename(self) -> str:
"""Filename for the from-node unit test."""
return f"Test{self.name}OperationFromNode.cpp"
@property
def test_descriptor_filename(self) -> str:
return f"Test{self.class_name}.cpp"
@property
def test_graph_filename(self) -> str:
return f"TestGraphDescriptor{self.name}.cpp"
@property
def test_integration_filename(self) -> str:
if self.frontend.node_class:
base = (
self.frontend.node_class[:-4]
if self.frontend.node_class.endswith("Node")
else self.frontend.node_class
)
return f"Integration{base}DescriptorLowering.cpp"
return f"Integration{self.name}DescriptorLowering.cpp"
@property
def test_integration_lifting_filename(self) -> str:
"""Filename for the lifting integration test."""
if self.frontend.node_class:
base = (
self.frontend.node_class[:-4]
if self.frontend.node_class.endswith("Node")
else self.frontend.node_class
)
return f"Integration{base}DescriptorLifting.cpp"
return f"Integration{self.name}DescriptorLifting.cpp"
@property
def required_tensor_fields(self) -> list[TensorField]:
return [f for f in self.tensor_fields if f.required]
@property
def optional_tensor_fields(self) -> list[TensorField]:
return [f for f in self.tensor_fields if not f.required]
@property
def required_data_fields(self) -> list[DataField]:
return [f for f in self.data_fields if f.required]
@property
def optional_data_fields(self) -> list[DataField]:
return [f for f in self.data_fields if not f.required]
@property
def enum_fields(self) -> list[DataField]:
return [f for f in self.data_fields if f.is_enum]
@property
def mode_fields(self) -> list[DataField]:
return [f for f in self.data_fields if f.is_mode]
@property
def has_mode_fields(self) -> bool:
return any(df.is_mode for df in self.data_fields)
@property
def vector_fields(self) -> list[DataField]:
return [f for f in self.data_fields if f.is_vector]
@property
def scalar_fields(self) -> list[DataField]:
return [f for f in self.data_fields if f.is_scalar]
@property
def optional_scalar_fields(self) -> list[DataField]:
return [f for f in self.data_fields if f.is_optional_scalar]
@property
def all_tensor_names(self) -> list[str]:
return [f.name for f in self.tensor_fields]
@property
def tensor_uid_list(self) -> list[int]:
"""Ordered list of tensor UIDs for template use."""
return [
self.test_data.tensor_uids.get(f.name, i + 1)
for i, f in enumerate(self.tensor_fields)
]
@property
def descriptor_type_enum(self) -> str:
return self.descriptor_type.enum_name
@property
def tensor_attr_cases(self) -> list[str]:
"""List of tensor attribute enum names for the switch statement."""
return [
f"{self.operation_attr_prefix}_{f.attr_suffix}" for f in self.tensor_fields
]
@property
def has_enum_fields(self) -> bool:
return len(self.enum_fields) > 0
@property
def has_vector_fields(self) -> bool:
return len(self.vector_fields) > 0
@property
def non_shared_data_fields(self) -> list[DataField]:
return [f for f in self.data_fields if not f.shared]
@property
def graph_verifiable_data_fields(self) -> list[DataField]:
"""Data fields included in verify<Op>Node helper (vector, mode, enum)."""
return [f for f in self.data_fields if f.is_vector or f.is_mode or f.is_enum]
@property
def has_tensor_array_fields(self) -> bool:
return len(self.tensor_array_fields) > 0
@property
def generatable_mode_fields(self) -> list[DataField]:
"""Mode data fields with enum_def set and shared == False."""
return [
df
for df in self.data_fields
if df.is_mode and df.has_enum_def and not df.shared
]
@property
def mode_field_backend_headers(self) -> list[str]:
"""Unique backend C-API header filenames for mode fields with converters.
Used by the packer template to emit ``#include`` directives for the
backend enum types referenced by ``backend_converter`` calls (e.g.,
``HipdnnConvolutionMode.h`` for ``toBackendConvMode``). Headers are
derived from each mode field's ``enum_def.backend_header`` and
deduplicated while preserving first-seen order.
"""
seen: set[str] = set()
ordered: list[str] = []
for df in self.data_fields:
if not df.is_mode or not df.backend_converter:
continue
if df.enum_def and df.enum_def.backend_header:
hdr = df.enum_def.backend_header
if hdr not in seen:
seen.add(hdr)
ordered.append(hdr)
return ordered
@property
def tensor_field_frontend_map(self) -> dict:
"""Maps tensor_field name -> matching FrontendTensorConfig from frontend inputs/outputs.
This enables templates to look up the correct frontend getter/setter for any
backend tensor_field. Resolution order:
1. ``tensor_fields[].frontend_getter`` override (set in YAML). Matches an
existing ``FrontendTensorConfig`` whose ``effective_getter_name`` equals
the override; otherwise synthesizes a stand-in carrying the override and
a derived ``set_<name>`` setter (when the override starts with ``get_``).
This handles tensor_fields that are not graph I/O (batchnorm scalars,
sdpa stats outputs) and divergent accessors (sdpa ``attn_mask`` →
``get_bias``).
2. Exact name match against ``frontend.inputs[].name`` /
``frontend.outputs[].name``.
3. Abbreviation fallback (``in_X`` ↔ ``input_X``, ``out_X`` ↔ ``output_X``)
so configs like pointwise lift without explicit overrides.
"""
if not self.frontend.inputs and not self.frontend.outputs:
return {}
result: dict = {}
all_frontend = self.frontend.inputs + self.frontend.outputs
for tf in self.tensor_fields:
# 1) Override beats name-match. Tolerate a trailing ``()`` on the
# stored value so models built directly (bypassing the loader,
# e.g., from tests) work the same as loader-built models.
override = (
tf.frontend_getter.removesuffix("()") if tf.frontend_getter else ""
)
if override:
matched_existing = None
for ft in all_frontend:
if ft.effective_getter_name == override:
matched_existing = ft
break
if matched_existing is not None:
result[tf.name] = matched_existing
else:
# Synthesize a stand-in. The synthetic carries
# ``name=tf.name`` so ``effective_enum_name`` derives from
# the backend tensor name -- safe because the packer/
# unpacker templates only consume ``effective_getter_name``
# / ``effective_setter_name`` / ``camel_name`` / ``name``
# for tensor lookup, not ``effective_enum_name``.
# Derive a setter only when the override follows the
# ``get_<name>`` convention; leave blank otherwise so
# ``effective_setter_name`` falls back to ``set_<name>``
# on the synthetic.
derived_setter = ""
if override.startswith("get_"):
derived_setter = "set_" + override[len("get_") :]
result[tf.name] = FrontendTensorConfig(
name=tf.name,
getter_name=override,
setter_name=derived_setter,
)
continue
# 2) Exact name match.
matched = False
for ft in all_frontend:
if tf.name == ft.name:
result[tf.name] = ft
matched = True
break
if matched:
continue
# 3) Abbreviation-aware fallback:
# in_X <-> input_X
# out_X <-> output_X
for ft in all_frontend:
if (
tf.name.startswith("in_")
and ft.name.startswith("input_")
and tf.name[3:] == ft.name[6:]
):
result[tf.name] = ft
matched = True
break
if (
tf.name.startswith("out_")
and ft.name.startswith("output_")
and tf.name[4:] == ft.name[7:]
):
result[tf.name] = ft
matched = True
break
return result
@property
def frontend_to_tensor_field_map(self) -> dict[str, str]:
"""Maps frontend tensor name -> backend tensor_field name.
Reverse of tensor_field_frontend_map. Used by integration test template
to resolve UID constant names from graph_method_params tensor names.
Example: 'input_0' -> 'in_0', 'output_0' -> 'out_0'.
"""
return {
ft.name: tf_name for tf_name, ft in self.tensor_field_frontend_map.items()
}
# --- Frontend filename computed properties ---
@property
def _frontend_base_name(self) -> str:
"""Base name for frontend files, derived from the frontend config or name.
Delegates to ``frontend.effective_attributes_filename`` (which honors the
``attributes_filename`` YAML override) when a frontend basename is
resolvable; falls back to the operation ``name`` otherwise.
Examples:
attributes_class='ConvFpropAttributes' -> 'ConvFprop'
attributes_class='ConvFpropAttributes' with attributes_filename='ConvolutionFprop'
-> 'ConvolutionFprop'
attributes_class='' with name='ConvolutionFwd' -> 'ConvolutionFwd'
"""
base = self.frontend.effective_attributes_filename
if base:
return base
return self.name
@property
def attributes_header_filename(self) -> str:
"""Filename for the frontend Attributes header (e.g., 'ConvFpropAttributes.hpp')."""
return f"{self._frontend_base_name}Attributes.hpp"
@property
def node_header_filename(self) -> str:
"""Filename for the frontend Node header (e.g., 'ConvFpropNode.hpp')."""
return f"{self._frontend_base_name}Node.hpp"
@property
def test_attributes_filename(self) -> str:
"""Filename for the frontend Attributes test (e.g., 'TestConvFpropAttributes.cpp')."""
return f"Test{self._frontend_base_name}Attributes.cpp"
@property
def test_node_filename(self) -> str:
"""Filename for the frontend Node test (e.g., 'TestConvFpropNode.cpp')."""
return f"Test{self._frontend_base_name}Node.cpp"
@property
def test_frontend_graph_filename(self) -> str:
"""Filename for the frontend Graph test (e.g., 'TestGraphConvFprop.cpp')."""
return f"TestGraph{self._frontend_base_name}.cpp"
@property
def effective_graph_method_name(self) -> str:
"""Graph method name, defaulting to snake_case of operation name."""
if self.frontend.graph_method_name:
return self.frontend.graph_method_name
return _to_snake_case(self.name)
@property
def effective_constants_include(self) -> str:
"""Constants header name for test files.
Returns constants_include from test_data if set (for operations with
pre-existing constants headers), otherwise derives the name from the
operation name (e.g., 'BatchnormInference' -> 'BatchnormInferenceConstants').
"""
if self.test_data.constants_include:
return self.test_data.constants_include
return f"{self.name}Constants"
@property
def tensor_const_prefix(self) -> str:
"""Prefix for tensor constant names.
Returns the explicit override from test_data if set, otherwise
'K_' for pre-existing constants headers (backward compatible),
'K_{NAME}_' for new operations (avoids collisions).
"""
if self.test_data and self.test_data.tensor_const_prefix:
return self.test_data.tensor_const_prefix
if self.test_data and self.test_data.constants_include:
return "K_"
return f"K_{self.name.upper()}_"