Skip to content

[circle] Optimizing circe export passes by caching - #1103

Merged
mhs4670go merged 3 commits into
Samsung:mainfrom
Torrero:circle_pass_optimization
Nov 6, 2026
Merged

mhs4670go merged 3 commits into
Samsung:mainfrom
Torrero:circle_pass_optimization

Conversation

@Torrero

@Torrero Torrero commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Motivation

Exporting large quantized models to Circle (e.g. Qwen3-VL-4B, whose vision subgraph unrolls attention per chunk×head×block into ~10^5–10^6 operators) takes hours of single-threaded CPU with no artifact.

py-spy profiling showed ~93% of export time is spent in TICO's own pass machinery, and ~7% for torch.export:

Phase Share before
const propagation ~30%
fake-quantize decomposition ~29%
Circle O1 rule matching ~26%

Root causes

  1. CircleGraph._operator() rebuilt as_list(subgraph.operators) — an O(N) flatbuffer→list copy — on every accessor call, making every graph traversal O(N²).
  2. operator_is_live() ran a full-graph BFS (backward_operators) per operator per rule match → O(N³) in the rules engine.
  3. get_constant_from_tensor() re-walked scale/zero-point constant chains without memoization, once per fake-quantize node.

Changes

  • tico/circle/graph.py
    • Materialize _operators_list / _tensors_list once at construction (instances are revision-pinned immutable snapshots — the session rebuilds them on any subgraph mutation, so caches cannot go stale).
    • Memoize operator_inputs / operator_outputs.
    • Add live_operators(): one BFS per graph, cached as frozenset (immutable — callers cannot corrupt the shared cache).
  • tico/circle/passes/optimization/_pattern_utils.py
    • operator_is_live() uses the cached live_operators().
  • tico/passes/decompose_fake_quantize_tensor_qparams.py
    • Memoizing wrapper for get_constant_from_tensor() with a per-pass-run cache keyed by node name.
  • Tests: 5 new unit tests covering accessor memoization, snapshot stability, liveness caching/immutability, dead-branch reporting, and memo reuse.

Correctness

  • No semantic changes: read-path caching of pure functions only.
  • Exported artifacts are byte-identical (sha256-verified on quantized Qwen3-VL-4B decoder layers).
  • tico-circle verify passes on all produced artifacts.
  • Cache invalidation follows the pre-existing revision scheme (CircleOptimizationSession.graph()); the FX-pass cache lives exactly one call() and reads only immutable constant chains.

Performance (quantized Qwen3-VL-4B export)

Workload Before After
Text decoder layer (prefill) 20.0 s 14.4 s
Vision prefill, t1 (~100k ops) >2 hours (not finished) 9.4 min
Vision prefill, t8 (~800k ops) >14 hours (not finished) 2 h 51 min
fake-quantize decompose share 29% 2.5%
Circle O1 rules share 26% ~0%

Co-Authored-By: Cline SR

TICO-DCO-1.0-Signed-off-by: Evgenii Maltsev e.maltsev@samsung.com

@Torrero
Torrero requested review from dvsav and mhs4670go November 3, 2026 14:27

def get_constant_from_tensor(
node: Union[torch.fx.Node, float], ep: ExportedProgram
node: Union[torch.fx.Node, float], ep: ExportedProgram, _cache=None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

_cache is a public, positional third parameter of the exported function with no type annotation (_cache=None) in an otherwise typed module. The underscore convention on a positional argument is weak protection.

@dvsav

dvsav commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

(Optional)

fuse/linear.py:211-213 still keeps its own private _is_live() calling graph.backward_operators(graph.outputs) per operator — this pass has exactly the O(N²) problem the PR set out to eliminate. Would you consider optimizing this as well (in this PR or in another PR)?

@dvsav

dvsav commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

(Optional) The same O(N)-copy antipattern survives in code the O1 rules call per element

  • _utils.tensor_contract() and tensor_object() (_utils.py:196-215) do as_list(graph.subgraph.tensors) on every tensor inspection — once per rule match per candidate.
  • CircleGraph.is_constant() (graph.py:223-225) delegates to is_constant_tensor(model, subgraph, idx), which re-lists tensors and buffers per call, and in the zero-byte path walks all operators (graph.py:93-96). It's called per-operator in simplify/identity_ops.py:133,212, fuse/linear.py:747, analysis/purity.py:119-122.
  • fuse/linear.py:_linear_state re-lists all operators per operator, and _is_live (above).

This commit optimizes circe export passes by caching

Co-Authored-By: Cline

TICO-DCO-1.0-Signed-off-by:  Evgenii Maltsev <e.maltsev@samsung.com>
@Torrero
Torrero force-pushed the circle_pass_optimization branch from 1c15aa3 to 0d6b331 Compare November 5, 2026 19:08
@Torrero

Torrero commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

@dvsav Thank you for your comments, I fixed it

@mhs4670go mhs4670go left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@dvsav dvsav left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍 LGTM. Thank you!

@mhs4670go
mhs4670go merged commit 7f44d0a into Samsung:main Nov 6, 2026
9 checks passed
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.

3 participants