Skip to content

Conversation

tafk7
Copy link
Collaborator

@tafk7 tafk7 commented Sep 26, 2025

Summary

This PR enhances the CustomOp registration system to support Python module namespace lookups alongside the existing dictionary-based approach. The new system provides better IDE support and a more Pythonic API while maintaining full backward compatibility.

Key Changes

Core Registry (src/qonnx/custom_op/registry.py)

  • Added namespace-first lookup with dictionary fallback
  • Implemented domain aliasing system (generalizes the "brevitas exception")
  • Added add_op_to_domain() for runtime op registration

Module Structure Updates

  • The CustomOp imports in the channels_last and general modules automatically register them
  • Maintained legacy custom_op dictionaries for backward compatibility
  • Fixed missing Quant import in general module

Benefits

  • Maintainability: Cleaner, more Pythonic registration pattern
  • Developer Experience: Better IDE autocomplete and type hints
  • Extensibility: External packages can register ops via domain-based imports
  • Compatibility: Zero breaking changes - all existing code continues to work

Migration Path

The system automatically falls back to legacy dictionaries, so no immediate changes are required. New CustomOps can be registered by:

  1. Importing it in <domain_path>/__init__.py
  2. Using add_op_to_domain() for runtime registration
  3. Continuing to use custom_op dictionaries (legacy)

  Replace dictionary-based registration with module namespace lookup:
  - Direct attribute lookup in module namespace (preferred)
  - Legacy custom_op dictionary support (backward compatibility)
  - Domain aliasing (e.g., onnx.brevitas -> qonnx.custom_op.general)
  - Runtime op registration via add_op_to_domain()
  - No expensive fallbacks (removed case-insensitive matching, full module scan)
  - Clear, actionable error messages

  Benefits:
  - Better IDE support with direct imports
  - Cleaner, more Pythonic API
  - O(1) lookup performance
  - Predictable behavior
  - External packages work via domain-based imports (e.g., finn.custom_op.fpgadataflow)
@jurevreca12 jurevreca12 mentioned this pull request Sep 29, 2025
@jurevreca12 jurevreca12 self-requested a review October 5, 2025 18:22
@jurevreca12
Copy link
Collaborator

How are these ops in the custom domain suppose to execute?
qonnx/core/onnx_exec.py uses an if statement to check for custom ops.

if is_finn_op(node.domain):
    ...

However, the function is_finn_op remains unchanged. It still checks for the "classic" domains:

def is_finn_op(op_type):
    return op_type.startswith("finn") or op_type.startswith("qonnx.custom_op") or op_type.startswith("onnx.brevitas")

I also tested the code on chisel4ml, with a custom defined in the chisel4ml.preprocess namespace. I then called
qonnx.custom_op.registry.add_domain_alias("chisel4ml.preprocess", "qonnx.custom_op.general"). However, the if statement described above did not trigger and I get an onnx runtime error.

Am I missing something? should I be calling something else? Honestly, I just don't see how this can execute.

Example:
add_op_to_domain("qonnx.custom_op.general", "TestOp", TestOp)
"""
if not inspect.isclass(op_class) or not issubclass(op_class, CustomOp):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is not correct. It evaluates to True even if op_class is not subclass of CustomOp. This code will evaluate to True on anything, even if op_class is a float.

Regardless of that, I do not see the point of using inspect here. If the intention is to prevent the user from using an object instance here, issubclass will raise a TypeError. Which is a more appropriate error than ValueError.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is from an old iteration where it cached all CustomOps at a given domain on first access, this avoided illegal comparisons. Thanks for catching this, will remove in next commit.

# Strategy 1: Direct namespace lookup (preferred)
if hasattr(module, op_type):
obj = getattr(module, op_type)
if inspect.isclass(obj) and issubclass(obj, CustomOp):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is obj a class? judging from the code bellow it is a class not an object. This makes the variable name obj somewhat misleading. Also the same argument about inspect holds here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants