# 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 + 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_" """ 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, 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``. Derived from ``fbs_optional`` alone — when the FBS field is optional, the frontend getter is expected to return ``std::optional``. 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`` and ``bool`` members directly. The unpacker emits ``attributes. = value`` instead of ``attributes.set_(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``. Mode converters return ``std::optional`` 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_).""" return self.getter_name or f"get_{self.name}" @property def effective_setter_name(self) -> str: """Setter method name (default: set_).""" 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" 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 .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 verifyNode 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_`` 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_`` convention; leave blank otherwise so # ``effective_setter_name`` falls back to ``set_`` # 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()}_"