Debug why an OpenVINO MatcherPass transformation is not firing. Use this skill immediately when a user says a transformation is "not applied", a "pass has no effect", a "matcher never triggers", a pattern "doesn't match", a "callback never fires", "WrapType predicate is too strict", a subgraph "not fused" despite the pass being registered, or they see "END: PATTERN DIDN'T MATCH" in matcher logs. Also trigger when a MatcherPass works on one model but silently skips another, when the user wants to add a reproducer test for a transformation that should fire but doesn't, or when they suspect an opset version mismatch preventing a match. Do NOT trigger for: writing a new MatcherPass from scratch, debugging a pass that fires but produces wrong numerical results, crashes in pass registration, or general questions about what MatcherPass is.
72
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
An end-to-end workflow for diagnosing why an OpenVINO MatcherPass-based transformation does not fire on a given model.
Produce two deliverables before finishing:
TEST_F case, then compile and confirm it reproduces the non-firing behavior.The only filesystem change should be the test file edit. Do not create additional output files.
Early exit — all passes fired: If Step 2a shows CALLBACK SUCCEDED > 0 for every pass under investigation, skip Steps 3–6. Post a lightweight confirmation summary (use the "all fired" variant in the Diagnosis Report Template) and do not write a reproducer test. There is no bug to reproduce.
Before touching any files or logs, confirm:
EliminateSplitConcat, or a list of several).build/Release if not specified by the user.Ask the user only if the transformation name or run command is missing. The build directory is never a blocker.
If the user provides multiple transformation names (e.g., "these passes should fire but at least one didn't"), treat this as a pipeline cascade investigation:
OV_MATCHERS_TO_LOG=Pass1,Pass2,... (comma-separated, one entry per pass).CALLBACK SUCCEDED (spelling matches the literal matcher log output) = ✅ fired; phrase never appears = ❌ did not fire.The matcher logging macros are compiled in only when ENABLE_DEBUG_CAPS=ON. Check whether the current build already has it:
grep -i "ENABLE_DEBUG_CAPS" build/*/CMakeCache.txtIf the flag is absent or set to OFF, reconfigure CMake using the existing build directory and build type from Step 0:
cmake -B <build_dir> -DENABLE_DEBUG_CAPS=ON
cmake --build <build_dir> --parallelNote: Logging also works in
Releasebuilds as long asENABLE_DEBUG_CAPS=ONis set at configure time.
Run the failing command with matcher logging enabled and redirect output to a file:
# Log only the transformation of interest (replace TransformationName):
OV_MATCHER_LOGGING=true \
OV_MATCHERS_TO_LOG=TransformationName \
<your_run_command> 2>&1 | tee matcher.logAdditional env vars:
OV_MATCHERS_TO_LOG — comma-separated list of matcher names to filter (omit to log all matchers, which produces very large output).OV_VERBOSE_LOGGING=true — prints additional node details (element type, shape, attributes); use when the basic log does not identify the failure clearly.Matcher logs are interleaved with normal output. If the command is long-running (e.g., model compilation on GPU), wait for it to finish before analyzing. Parsing a partial log will produce incorrect statistics (e.g., undercounting successful callbacks). Verify completion by checking for the final your_run_command output line or the process exit code.
Before diving into detailed log analysis, run a quick tally to build an overview of which passes fired and how many times. This is especially important for pipeline cascade investigations with multiple passes.
Use this Python snippet (adjust the pass_names list):
python3 -c "
import re, sys
pass_names = ['PassA', 'PassB'] # <-- replace with actual pass names
with open('matcher.log') as f:
content = f.read()
for p in pass_names:
s = len(re.findall(rf'\[{p}\] END: PATTERN MATCHED, CALLBACK SUCCEDED', content))
m = len(re.findall(rf'\[{p}\] END: PATTERN MATCHED', content))
d = len(re.findall(rf'\[{p}\] END: PATTERN DIDN.T MATCH', content))
print(f'{p}: CALLBACK SUCCEDED={s} MATCHED={m} DIDN\'T MATCH={d}')
"Interpret the output:
false (check callback logic).GraphRewrite wrapper — log the inner MatcherPass names instead).Note on
GraphRewritewrappers: If a pass is aGraphRewritethat callsadd_matcher<InnerPass>(), the matcher log will use the inner pass names, not the wrapper name. Check the header for inner pass names and use those inOV_MATCHERS_TO_LOG.
For pipeline cascades, use the tally to quickly classify each pass as ✅ fired / ❌ root cause / ❌ downstream casualty before spending time on detailed log analysis.
Open the log file and search for the transformation name. The log uses a tree structure with { (open block) and } (close block) markers, and colors to signal success (green) or failure (red).
Search the log for the transformation name:
Not found at all → The matcher is either not registered in the pass manager for this execution path, or it runs on an already-transformed/empty graph. Verify registration:
# Locate where the pass is registered in the pipeline:
grep -rn "register_pass<.*TransformationName" src/ --include="*.cpp" --include="*.hpp"Check that the pipeline is reachable from your run command (e.g., the correct plugin, the correct compilation path). If the pass is registered, confirm the graph is non-empty before it runs by adding a temporary ov::pass::VisualizeTree pass immediately before it.
Found → Proceed to 3b.
Search for:
END: PATTERN DIDN'T MATCHThis confirms the pattern attempted to match but failed. If this line is never followed by END: PATTERN MATCHED, the transformation never fires.
Work inward through the nested blocks to find the deepest } labeled with a failure. The following table maps log phrases to root causes:
| Log phrase | Root cause |
|---|---|
NODES' TYPE DIDN'T MATCH. EXPECTED: X. OBSERVED: Y | The op type in the graph does not match the WrapType in the pattern. Most commonly an unexpected node has been inserted between two nodes the pattern expects to be directly connected (e.g., a Convert or Reshape inserted by a prior pass); alternatively, the candidate is simply a different op type. In rare cases the cause is an opset version mismatch (e.g., pattern expects opset3::ShapeOf, graph has opset1::ShapeOf). |
NODES' TYPE MATCHED, but PREDICATE FAILED | The op type matches but the lambda inside WrapType(...) returned false. Inspect the predicate in the transformation source — common checks: element type, rank, dynamic shapes, consumer count. |
PREDICATE \name` FAILED` | A named pattern::op::Predicate with this name returned false. Locate it by name in the transformation source file. |
NUMBER OF ARGUMENTS DOESN'T MATCH. EXPECTED: N. OBSERVED: M | The graph node has a different number of inputs than the pattern expects. The model graph has a different input structure. |
ARGUMENT N DIDN'T MATCH | The N-th input of the candidate node fails its sub-pattern recursively. |
NONE OF OR BRANCHES MATCHED | A pattern::op::Or exhausted all alternative branches. Check each BRANCH N DIDN'T MATCH sub-block. |
NONE OF PERMUTATIONS MATCHED | Commutative op (e.g., Add, Multiply): tried all argument orderings but none satisfied the pattern. |
LABEL DIDN'T MATCH | A captured label's value does not satisfy its constraint (e.g., shape symbol equality, consumer requirements). |
BLOCK "name" DIDN'T MATCH | A grouped pattern block failed — look inside the block for the deeper cause. |
ATTRIBUTES MISMATCH: VALUE OF \attr` IS ... EXPECTED ...` (verbose only) | A specific attribute value (axis, group, mode, etc.) does not match the expected value in the pattern. |
ANY INPUT DIDN'T MATCH BECAUSE OF PREDICATE | A label wrapping any_input() has an attached predicate that failed. |
Convert, Reshape, Transpose) between two nodes the pattern expects to be directly connected, so the type observed at that edge doesn't match the pattern.Multiply vs MatMul).WrapType lambda checks element type (f32 vs f16), rank, dynamic vs static shapes, or broadcast type. The actual node doesn't qualify.node->output(0).get_target_inputs().size() == 1), but the graph has multiple consumers.ConstantFolding) has not yet run.node->output(0) but the graph provides node->output(1).group == 1, axis == 0) that doesn't match the actual node.opset::OpX but the frontend or a prior pass has already replaced it with a different version or a decomposed form.If the log file is empty or contains no matcher output even though the flag is set:
ENABLE_DEBUG_CAPS=ON in CMakeCache.txt for the binary you are actually running (not a different build directory).export OV_MATCHER_LOGGING=true or prefix it inline.os.environ or prefix the full command.bin/ location.Search for existing tests using the transformation class name and/or its header:
# By class name
grep -rl "TransformationX" src/common/transformations/tests/
grep -rl "TransformationX" src/plugins/*/tests/ tests/layer_tests/
# By header include
grep -rl "transformations/path/to/transformation_x.hpp" \
src/common/transformations/tests/ \
src/plugins/*/tests/Common test locations:
src/common/transformations/tests/common_optimizations/ — most shared passessrc/common/transformations/tests/op_conversions/ — op-conversion passessrc/plugins/<plugin>/tests/functional/ — plugin-specific transformationsOnce you have identified the failing scenario from the log (Step 3), add a minimal test to the existing test file for the transformation.
Use TransformationTestsF (from common_test_utils/ov_test_utils.hpp), which provides model, model_ref, and manager members and automatically compares them after the pass runs:
TEST_F(TransformationTestsF, TransformationX_DescribeFailingScenario) {
{
// 1. Build the input model that should trigger TransformationX.
// Replicate the exact op type / attribute values / graph topology
// that appears in the log as the candidate but fails to match.
auto data = std::make_shared<opset5::Parameter>(element::f32, Shape{2, 2});
// ... construct the ops that match (or almost match) the pattern ...
model = std::make_shared<Model>(OutputVector{/* output */}, ParameterVector{data});
// 2. Register the target pass
manager.register_pass<ov::pass::InitNodeInfo>();
// add any other prerequisite passes that should run first if needed
manager.register_pass<ov::pass::TransformationX>();
}
// Do NOT define model_ref here — leave it unset so TransformationTestsF
// auto-clones the input model as the reference.
}TransformationTestsF runs manager on model and compares it against model_ref. When model_ref is not explicitly set, it is auto-cloned from the input model before the pass runs. This means: if the transformation does not fire (the bug is present), model is unchanged and equals the clone → the test passes (green). A green test here is the confirmation that the bug is reproduced — the transformation did not alter the graph.
NODES' TYPE DIDN'T MATCH showed an unexpected Convert inserted between two ops, include that Convert in the input model so the test proves the transformation handles (or correctly skips) that topology.EliminateSplitConcat_DifferentSplitAxis).Find the CMake test target that owns the test file:
grep -rn "add_executable\|ov_add_test_target" \
src/common/transformations/tests/CMakeLists.txt \
src/plugins/*/tests/CMakeLists.txt | grep -i transformationBuild only that target and run the new test:
cmake --build <build_dir> --target <test_binary_name> --parallel
bin/intel64/<build_type>/<test_binary_name> --gtest_filter="*TransformationName_DescribeFailingScenario*"Expected outcome before the fix: the test passes (green). This is correct — the transformation did not fire, so the model is unchanged and matches the auto-cloned model_ref. A passing test here is a necessary but not sufficient confirmation that the bug is reproduced.
A green test alone is not enough: the test could be passing for the wrong reason (e.g., the transformation was never even attempted on the small test graph, rather than failing at the expected point). Always cross-check by re-running the test binary with matcher logging enabled and verifying that the log shows the same failure phrase and the same node as in the original model log from Step 3:
OV_MATCHER_LOGGING=true \
OV_MATCHERS_TO_LOG=TransformationName \
bin/intel64/<build_type>/<test_binary_name> \
--gtest_filter="*TransformationName_DescribeFailingScenario*" 2>&1 | tee matcher_test.logCompare matcher_test.log against the original matcher.log:
NODES' TYPE DIDN'T MATCH. EXPECTED: X. OBSERVED: Y) must appear.If the logs diverge (e.g., the test log shows a different failure phrase, or END: PATTERN DIDN'T MATCH never appears at all), the test model does not faithfully reproduce the original bug — revise the test graph to closer match the topology identified in Step 3 and repeat.
Once both conditions are met (test is green and logs agree), the reproducer is valid. Record the relevant log excerpt in the diagnosis report.
Once the fix is implemented and the transformation fires correctly, the test will fail because model no longer matches the unmodified clone. At that point, add an explicit model_ref block with the expected transformed graph to turn it into a proper regression guard.
Based on the root cause identified in Step 3, suggest one or more of the following to the user:
| Root cause | Resolution |
|---|---|
| Unexpected intermediate node | Extend the pattern to optionally absorb the inserted node (e.g., wrap it in pattern::op::Optional), or ensure the pass responsible for inserting that node runs after this transformation |
| Wrong op type | Verify the model topology; if the op is a valid alternative, add it to the WrapType list |
| Predicate fails (type/shape) | Relax or correct the predicate; run shape/type inference before the pass if shapes are unresolved |
| Consumer count check fails | The single-consumer guard is usually a correctness constraint: replacing a node that feeds multiple consumers would silently alter other paths in the graph. First verify whether skipping is the correct behavior for this model. If the transformation is provably safe for all consumers, remove the consumer-count check from the predicate. If the constraint is intentional, the model simply does not qualify and no fix is needed in the transformation. |
| Pass not registered / wrong order | Register the pass in the correct pipeline position; use grep -rn "register_pass" to find the pipeline source |
| Attribute mismatch | Either extend the pattern to cover the additional attribute values, or verify that the model is correct and not pathological |
| Transformation already applied | Check the pass order — a duplicate or conflicting pass may be consuming the node earlier |
| Output index mismatch | Adjust the pattern to match the correct output index, or add an Or branch covering both |
| Wrong opset version in pattern | Update WrapType in the transformation to include the correct opset, e.g., wrap_type<opset1::ShapeOf, opset3::ShapeOf>(predicate) |
Always pair the resolution suggestion with:
If after Steps 1–3 you find:
VisualizeTree before the pass) and no prior pass should have produced it → the problem is upstream of the matcher (wrong frontend conversion, wrong pass pipeline entry point). Diagnose the upstream issue separately.When both deliverables are complete, post the following in the chat (do not save to a file). See references/example-diagnosis-report.md for a fully filled example — use it as a quality bar for level of detail, especially for Log evidence (must be a direct quote from the log, not paraphrased from source) and Resolution (must include file + line).
## MatcherPass Diagnosis: <TransformationName>
<!-- If multiple passes were investigated, add this table: -->
## Summary of passes
| Pass | Result | Callbacks | Matches |
|---|---|---|---|
| `PassA` | ✅ Fired | 40 | 40 |
| `PassB` | ❌ Did not fire — root cause | 0 | 0 |
| `PassC` | ❌ Did not fire — downstream casualty | 0 | 0 |
**Root cause:** <one-sentence summary>
**Log evidence:** `<exact log phrase that identified the failure>`
**Failing node:** <op type, location in graph>
**Resolution:** <what needs to change and where (file:line)>
## Reproducer Test
File: <path to test file>
Test name: `<TEST_F name>`
Status before fix: PASS (transformation did not fire — model unchanged, matches auto-cloned ref; confirms bug reproduced)When all passes fired successfully (Step 2a), use this shorter template instead:
## MatcherPass Diagnosis: <TransformationName(s)>
## Summary of passes
| Pass | Result | Callbacks | Matches |
|---|---|---|---|
| `PassA` | ✅ Fired | <N> | <N> |
| `PassB` | ✅ Fired | <N> | <N> |
**All transformations fired successfully.** No issues found.
## Reproducer Test
Not needed — no bug to reproduce.4c20980
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.