Skip to content

Refactor: Introduce OpBuilderBase/TapeBuilder as unified op-building interface#2905

Merged
gramalingam merged 13 commits into
mainfrom
rama/rewriter-op2
May 5, 2026
Merged

Refactor: Introduce OpBuilderBase/TapeBuilder as unified op-building interface#2905
gramalingam merged 13 commits into
mainfrom
rama/rewriter-op2

Conversation

@gramalingam
Copy link
Copy Markdown
Collaborator

@gramalingam gramalingam commented May 2, 2026

Summary

Introduces OpBuilderBase (ABC) and TapeBuilder (concrete implementation) as the unified interface for building IR nodes across the rewriter, optimizer, and version converter. This is a step towards unifying these builders with the recently introduced GraphBuilder. As of now, there is still some differences internally (as these are oriented towards incrementally modifying an existing graph), but for end-users (eg., writing rewrite-rules) we should be able to move towards same API.

Changes

New: onnxscript/tape_builder.py

  • OpBuilderBase — Abstract base class providing the op-building API:
    • Dynamic dispatch: op.Relu(x), op.MatMul(a, b, _domain=...)
    • Explicit creation: op.op("Conv", inputs, attributes, domain=...)
    • Initializer creation: op.initializer(tensor, name=...)
    • Subclasses implement _add_node, _add_initializer, _record_opset
  • TapeBuilder — Concrete subclass with list-based storage and harvesting properties (nodes, initializers, used_opsets)
  • Both are exported as public names from onnxscript

Rewriter

  • rewriter/_context.py slimmed to re-exports + local alias RewriterContext = OpBuilderBase
  • _rewrite_rule.py uses TapeBuilder() directly; harvests from the context
  • Deleted _node_sink.py (the intermediate NodeSink/TapeSink layer)

Optimizer

  • optimizer/_constant_folding.py defines OptimizerContext = OpBuilderBase locally
  • Uses TapeBuilder() instead of the old _tape.Builder()

Version Converter

  • version_converter/_version_converter.py defines VCContext = OpBuilderBase locally
  • Uses TapeBuilder() instead of the old _tape.Builder()

Cleanup

  • Deleted onnxscript/ir/_tape.py and _tape_test.py (fully superseded)
  • Removed incorrect ir.Value type annotations from pattern() method signatures in rule files (pattern methods work with pattern-value objects, not ir.Value)

Design

OpBuilderBase (ABC)          <- shared interface
  _add_node()                <- abstract
  _add_initializer()         <- abstract
  _record_opset()            <- abstract
  __getattr__()              <- dynamic dispatch (concrete)
  op()                       <- explicit node creation (concrete)
  initializer()              <- initializer creation (concrete)

TapeBuilder(OpBuilderBase)   <- list-backed implementation
  nodes                      <- harvesting property
  initializers               <- harvesting property
  used_opsets                <- harvesting property

Aliases:
  RewriterContext  = OpBuilderBase  (in rewriter)
  OptimizerContext = OpBuilderBase  (in optimizer)
  VCContext        = OpBuilderBase  (in version converter)

This design allows future alternative implementations (e.g., graph-backed builder) by subclassing OpBuilderBase without changing rule/evaluator code.

gramalingam and others added 2 commits May 1, 2026 20:43
Introduce RewriterContext protocol and TapeContext concrete class in
onnxscript/rewriter/_context.py to cleanly separate the API surface:

- RewriterContext (Protocol): The restricted interface for rewrite rules.
  Exposes only op creation (op.OpName(...), op.op(...)) and initializer
  creation (op.initializer(...)). Rules should not access internal state.

- TapeContext: The full implementation used by the rewrite engine. Extends
  onnx_ir.tape.Tape with dynamic __getattr__ dispatch (same as the old
  Builder), and exposes .nodes, .initializers, .used_opsets for the engine
  to harvest replacement subgraphs.

The RewriterContext type alias in _rewrite_rule.py and pattern.py now
points to the protocol instead of the concrete Builder class. The engine
internally uses TapeContext. All existing rule code is unaffected since
rules only use the three protocol methods.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduce composition-based architecture separating the rule-facing API
from the storage backend:

- NodeSink (ABC): Abstract interface for where nodes and initializers go.
  Defines add_node(), add_initializer(), record_opset() for writing, and
  nodes/initializers/used_opsets properties for engine harvesting.

- TapeSink: Concrete list-based implementation (same semantics as the
  previous Tape-based approach).

- RewriterContext: Concrete class using composition (HAS-A NodeSink)
  instead of inheritance (WAS IS-A Tape). Uses __getattribute__ and
  __getattr__ to block access to forbidden attributes (nodes,
  initializers, used_opsets, _sink, etc.) with clear error messages.
  Rules can only use op.OpName(...), op.op(...), and op.initializer(...).

- Engine (ReplacementPatternFunction): Creates TapeSink, passes it to
  RewriterContext, harvests results from the sink after the rule returns.

Benefits:
- Hard runtime enforcement: op.nodes raises AttributeError in rules
- Swappable backend: GraphSink can replace TapeSink without changing rules
- Clean separation enables future schema-aware features (auto-casting,
  shape inference) in RewriterContext

Also updates stale ir.tape.Tape type annotations in rule files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread onnxscript/rewriter/_node_sink.py Fixed
Comment thread onnxscript/rewriter/_node_sink.py Fixed
Comment thread onnxscript/rewriter/_node_sink.py Fixed
Comment thread onnxscript/rewriter/_node_sink.py Fixed
Comment thread onnxscript/rewriter/_node_sink.py Fixed
Comment thread onnxscript/rewriter/_node_sink.py Fixed
Comment thread onnxscript/rewriter/_node_sink.py Fixed
Comment thread onnxscript/rewriter/_node_sink.py Fixed
Comment thread onnxscript/rewriter/_node_sink.py Fixed
Comment thread onnxscript/rewriter/_node_sink.py Fixed
Comment thread onnxscript/rewriter/_node_sink.py Fixed
Comment thread onnxscript/rewriter/_node_sink.py Fixed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This draft PR refactors the builder used by rewrite rules by introducing a dedicated rewriter context and a pluggable sink abstraction, with the goal of decoupling rule authoring from the current tape-backed storage implementation. It fits into the rewriter subsystem as an internal step toward unifying builder behavior across rewrite, optimization, and graph-construction paths.

Changes:

  • Introduces RewriterContext and NodeSink/TapeSink to separate rule-facing node construction from storage/harvesting details.
  • Updates rewrite-rule internals to instantiate the new context/sink pair when building replacement subgraphs.
  • Adjusts several common rewrite rules to accept the new context type and adds focused unit tests for context/sink behavior.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
onnxscript/rewriter/rules/common/_remove_optional_bias.py Removes explicit ir.tape.Tape type annotations from rule hooks.
onnxscript/rewriter/rules/common/_fuse_pad_into_conv.py Updates rule hook signatures to use the new generic rewriter context parameter.
onnxscript/rewriter/rules/common/_fuse_conv_affine.py Drops concrete builder type annotations from rewrite methods.
onnxscript/rewriter/pattern.py Re-exports the new RewriterContext from _context instead of aliasing _tape.Builder.
onnxscript/rewriter/_rewrite_rule.py Switches replacement construction from direct builder instantiation to RewriterContext(TapeSink()).
onnxscript/rewriter/_node_sink.py Adds sink abstraction for collecting created nodes, initializers, and opset usage.
onnxscript/rewriter/_context_test.py Adds unit tests for sink harvesting, forbidden attribute access, and dynamic op creation.
onnxscript/rewriter/_context.py Implements the restricted rewrite-rule context API and sink-backed node/initializer creation.

Comment thread onnxscript/rewriter/pattern.py
@codecov
Copy link
Copy Markdown

codecov Bot commented May 2, 2026

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
14050 1 14049 2690
View the full list of 3 ❄️ flaky test(s)
onnxscript.rewriter.rules.common._basic_rules_test.ReshapeReshapeTest::test_reshape_reshape_dynamic_rule_1

Flake rate in main: 95.75% (Passed 45 times, Failed 1015 times)

Stack Traces | 0.011s run time
..../test_ort_nightly/lib/python3.11.../site-packages/parameterized/parameterized.py:620: in standalone_func
    return func(*(a + p.args), **p.kwargs, **kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.../rules/common/_basic_rules_test.py:531: in test_reshape_reshape_dynamic_rule
    testing.assert_numerically_equal(model, updated_model, (inputs,), atol=0, rtol=0)
onnxscript/rewriter/testing.py:102: in assert_numerically_equal
    np.testing.assert_allclose(
E   AssertionError: 
E   Not equal to tolerance rtol=0, atol=0
E   
E   (shapes (1, 0, 6, 3), (1, 0, 0, 3) mismatch)
E    ACTUAL: array([], shape=(1, 0, 6, 3), dtype=float32)
E    DESIRED: array([], shape=(1, 0, 0, 3), dtype=float32)
tests.function_libs.torch_lib.ops_test.TestOutputConsistencyFullGraphCPU::test_output_match_opinfo__addmv_cpu_float32

Flake rate in main: 14.56% (Passed 223 times, Failed 38 times)

Stack Traces | 0.628s run time
.../function_libs/torch_lib/ops_test.py:243: in run_test_output_match
    torch.testing.assert_close(
E   AssertionError: Tensor-likes are not close!
E   
E   Mismatched elements: 1 / 5 (20.0%)
E   Greatest absolute difference: 1.52587890625e-05 at index (0,) (up to 1e-05 allowed)
E   Greatest relative difference: 7.915093192423228e-06 at index (0,) (up to 1.3e-06 allowed)
tests.function_libs.torch_lib.ops_test.TestOutputConsistencyFullGraphCPU::test_output_match_opinfo__logsumexp_cpu_float16

Flake rate in main: 14.93% (Passed 114 times, Failed 20 times)

Stack Traces | 0.571s run time
.../function_libs/torch_lib/ops_test.py:243: in run_test_output_match
    torch.testing.assert_close(
E   AssertionError: Tensor-likes are not close!
E   
E   Mismatched elements: 1 / 5 (20.0%)
E   Greatest absolute difference: 2.288818359375e-05 at index (1,) (up to 1e-05 allowed)
E   Greatest relative difference: 0.0022869110107421875 at index (1,) (up to 0.001 allowed)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

gramalingam and others added 8 commits May 2, 2026 02:29
…methods

Address CodeQL 'Statement has no effect' warnings by replacing ... with
raise NotImplementedError in all abstract method/property bodies.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop __getattribute__ override and _FORBIDDEN_ATTRS blocklist. The
__getattr__ now simply delegates to _make_node without checks, keeping
the class straightforward. The sink is stored as a plain self._sink.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Consolidate _create_single_output_node and _create_multi_output_node
into _make_node with a single ir.Node constructor call. The only
branching is on output handling (return single value vs sequence).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the composition pattern (RewriterContext HAS-A NodeSink) with
inheritance (RewriterContext ABC -> TapeRewriterContext).

- RewriterContext is now an ABC with abstract _add_node, _add_initializer,
  _record_opset methods; concrete op(), initializer(), __getattr__,
  _make_node remain in the base class.
- TapeRewriterContext is the concrete subclass with list-based storage
  and nodes/initializers/used_opsets harvesting properties.
- Delete _node_sink.py (NodeSink ABC and TapeSink are no longer needed).
- Update _rewrite_rule.py to use TapeRewriterContext directly.
- Update tests to use TapeRewriterContext.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pattern methods work with pattern-value objects, not ir.Value. Remove
the misleading type annotations from pattern() signatures in rule files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ases

- Rename the ABC class from RewriterContext to OpBuilderBase
- Add RewriterContext = OpBuilderBase alias (for rewrite rules)
- Add OptimizerContext = OpBuilderBase alias (for optimizer)
- Update optimizer/_constant_folding.py to use OptimizerContext

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rename TapeRewriterContext to TapeBuilder (with backward-compat alias)
- Optimizer now instantiates TapeBuilder instead of _tape.Builder,
  establishing a formal relationship with OpBuilderBase
- Remove unused _tape import from optimizer

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace _tape.Builder with TapeBuilder in version converter
- Define VCContext = OpBuilderBase as the type alias for adapter functions
- Remove unused _tape import

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread onnxscript/version_converter/_version_converter.py Fixed
Comment thread onnxscript/version_converter/_version_converter.py Fixed
gramalingam and others added 2 commits May 5, 2026 00:09
- Create onnxscript/tape_builder.py as the canonical location for
  OpBuilderBase (ABC) and TapeBuilder (concrete implementation)
- Export OpBuilderBase and TapeBuilder from onnxscript.__init__
- Slim down rewriter/_context.py to re-export and define local aliases
- Optimizer and version converter import directly from tape_builder,
  defining their own local aliases (OptimizerContext, VCContext)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Builder class in _tape.py is fully replaced by OpBuilderBase/TapeBuilder
in onnxscript/tape_builder.py. No remaining references exist outside of
the deleted files themselves.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@gramalingam gramalingam changed the title [DRAFT] Refactor builder used by rewrite rules Refactor: Introduce OpBuilderBase/TapeBuilder as unified op-building interface May 5, 2026
Consistent with the placement of builder.py and other internal modules.
Public exports from onnxscript.__init__ remain unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (2)

onnxscript/version_converter/_version_converter.py:263

  • TapeBuilder can now create initializers and track referenced opsets, but process_node() still harvests only context.nodes. If a future adapter uses op.initializer(...) or creates a node in another domain, the converted graph will drop those initializers/opset imports and become invalid even though the new context API advertises that functionality.
        context = TapeBuilder()
        output = adapter(node, context)
        if output is not None:
            if isinstance(output, ir.Value):
                output = [output]
            return Replacement(output, context.nodes)

onnxscript/optimizer/_constant_folding.py:1192

  • TapeBuilder now supports initializer() and records opsets, but this replacement path only returns context.nodes. Any partial evaluator that starts using the unified builder to emit an initializer or a node from a non-default domain will silently lose that state when the replacement is applied, producing an invalid rewrite.
            context = TapeBuilder()
            try:
                output = optimizer(node, context, self._state)
            except Exception as e:
                raise RuntimeError(
                    f"Error during constant folding for node {node.name!r} ({node.domain}::{node.op_type})"
                ) from e
            if output is not None:
                if isinstance(output, Replacement):
                    return output
                if isinstance(output, ir.Value):
                    output = [output]
                return Replacement(output, context.nodes)

Comment thread onnxscript/rewriter/_context.py
@gramalingam gramalingam enabled auto-merge (squash) May 5, 2026 17:07
@gramalingam gramalingam merged commit fed67b9 into main May 5, 2026
32 of 41 checks passed
@gramalingam gramalingam deleted the rama/rewriter-op2 branch May 5, 2026 17:07
@github-project-automation github-project-automation Bot moved this from Todo to Done in ONNX Script Review Board May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

4 participants