Refactor: Introduce OpBuilderBase/TapeBuilder as unified op-building interface#2905
Conversation
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>
There was a problem hiding this comment.
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
RewriterContextandNodeSink/TapeSinkto 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. |
❌ 1 Tests Failed:
View the full list of 3 ❄️ flaky test(s)
To view more test analytics, go to the Test Analytics Dashboard |
…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>
- 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>
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>
There was a problem hiding this comment.
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
TapeBuildercan now create initializers and track referenced opsets, butprocess_node()still harvests onlycontext.nodes. If a future adapter usesop.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
TapeBuildernow supportsinitializer()and records opsets, but this replacement path only returnscontext.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)
Summary
Introduces
OpBuilderBase(ABC) andTapeBuilder(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.pyOpBuilderBase— Abstract base class providing the op-building API:op.Relu(x),op.MatMul(a, b, _domain=...)op.op("Conv", inputs, attributes, domain=...)op.initializer(tensor, name=...)_add_node,_add_initializer,_record_opsetTapeBuilder— Concrete subclass with list-based storage and harvesting properties (nodes,initializers,used_opsets)onnxscriptRewriter
rewriter/_context.pyslimmed to re-exports + local aliasRewriterContext = OpBuilderBase_rewrite_rule.pyusesTapeBuilder()directly; harvests from the context_node_sink.py(the intermediate NodeSink/TapeSink layer)Optimizer
optimizer/_constant_folding.pydefinesOptimizerContext = OpBuilderBaselocallyTapeBuilder()instead of the old_tape.Builder()Version Converter
version_converter/_version_converter.pydefinesVCContext = OpBuilderBaselocallyTapeBuilder()instead of the old_tape.Builder()Cleanup
onnxscript/ir/_tape.pyand_tape_test.py(fully superseded)ir.Valuetype annotations frompattern()method signatures in rule files (pattern methods work with pattern-value objects, notir.Value)Design
This design allows future alternative implementations (e.g., graph-backed builder) by subclassing
OpBuilderBasewithout changing rule/evaluator code.