[GPU] Preserve weight sharing when cloning weights Convert in ConvertMatMulToFullyConnected#36905
Open
alvoron wants to merge 6 commits into
Open
[GPU] Preserve weight sharing when cloning weights Convert in ConvertMatMulToFullyConnected#36905alvoron wants to merge 6 commits into
ConvertMatMulToFullyConnected#36905alvoron wants to merge 6 commits into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a GPU on-device weight memory regression introduced in ConvertMatMulToFullyConnected by preserving compressed-weight runtime markers on cloned Convert nodes and by deduplicating clones when the same weights-path Convert is shared across sibling MatMuls.
Changes:
- Add a per-pass cache to reuse a single cloned weights subgraph for shared
Convertnodes (keyed by(Convert, is_small_matmul)), preserving weight sharing. - Re-apply
DecompressionandDisableConstantFoldingmarkers to clonedConvertnodes to prevent unintended constant folding into materialized f32 weights. - Add unit tests covering (1) no duplicate converts for shared weights and (2) preservation of the no-fold marker on a cloned
Convert.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/plugins/intel_gpu/src/plugin/transformations/convert_matmul_to_fc.cpp |
Deduplicates shared weights-path Convert cloning and preserves key rt_info markers on the clone. |
src/plugins/intel_gpu/tests/unit/transformations/convert_matmul_to_fc_test.cpp |
Adds regression tests for shared-convert deduplication and no-fold marker preservation on the cloned convert. |
e-ddykim
reviewed
Jul 16, 2026
e-ddykim
left a comment
Contributor
There was a problem hiding this comment.
It looks like we can reduce code duplication by moving this logic into a separate helper function.
|
|
||
| if (order == expected_order) | ||
| fc_input_b = transpose_node; | ||
| if (is_small_matmul) { |
Contributor
There was a problem hiding this comment.
Suggested change
| if (is_small_matmul) { | |
| auto can_reuse_transpose = [&transpose_node](const ov::Output<ov::Node>& weights) { | |
| if (!transpose_node || transpose_node->get_input_size() != 2) { | |
| return false; | |
| } | |
| auto order_constant = ov::as_type_ptr<ov::op::v0::Constant>(transpose_node->get_input_node_shared_ptr(1)); | |
| if (!order_constant) { | |
| return false; | |
| } | |
| std::vector<size_t> expected_order(weights.get_partial_shape().size()); | |
| std::iota(expected_order.begin(), expected_order.end(), 0); | |
| std::swap(*(expected_order.end() - 1), *(expected_order.end() - 2)); | |
| return order_constant->cast_vector<size_t>() == expected_order; | |
| }; | |
| if (is_small_matmul) { |
Comment on lines
+264
to
+280
| bool can_reuse_transpose = false; | ||
| if (!matmul->get_transpose_b()) { | ||
| if (transpose_node && transpose_node->get_input_size() == 2) { | ||
| auto order_constant = ov::as_type_ptr<ov::op::v0::Constant>(transpose_node->get_input_node_shared_ptr(1)); | ||
| if (order_constant) { | ||
| std::vector<size_t> order = order_constant->cast_vector<size_t>(); | ||
|
|
||
| std::vector<size_t> expected_order(fc_input_b.get_partial_shape().size()); | ||
| std::iota(expected_order.begin(), expected_order.end(), 0); | ||
| std::swap(*(expected_order.end() - 1), *(expected_order.end() - 2)); | ||
|
|
||
| can_reuse_transpose = order == expected_order; | ||
| } | ||
| } | ||
|
|
||
| fc_input_b = can_reuse_transpose ? transpose_node | ||
| : create_transpose(fc_input_b, matmul->get_friendly_name() + "/transpose_b"); |
Contributor
There was a problem hiding this comment.
Suggested change
| bool can_reuse_transpose = false; | |
| if (!matmul->get_transpose_b()) { | |
| if (transpose_node && transpose_node->get_input_size() == 2) { | |
| auto order_constant = ov::as_type_ptr<ov::op::v0::Constant>(transpose_node->get_input_node_shared_ptr(1)); | |
| if (order_constant) { | |
| std::vector<size_t> order = order_constant->cast_vector<size_t>(); | |
| std::vector<size_t> expected_order(fc_input_b.get_partial_shape().size()); | |
| std::iota(expected_order.begin(), expected_order.end(), 0); | |
| std::swap(*(expected_order.end() - 1), *(expected_order.end() - 2)); | |
| can_reuse_transpose = order == expected_order; | |
| } | |
| } | |
| fc_input_b = can_reuse_transpose ? transpose_node | |
| : create_transpose(fc_input_b, matmul->get_friendly_name() + "/transpose_b"); | |
| if (!matmul->get_transpose_b()) { | |
| fc_input_b = can_reuse_transpose(fc_input_b) | |
| ? transpose_node | |
| : create_transpose(fc_input_b, matmul->get_friendly_name() + "/transpose_b"); | |
Comment on lines
+284
to
+295
| if (transpose_node && transpose_node->get_input_size() == 2) { | ||
| auto order_constant = ov::as_type_ptr<ov::op::v0::Constant>(transpose_node->get_input_node_shared_ptr(1)); | ||
| if (order_constant) { | ||
| std::vector<size_t> order = order_constant->cast_vector<size_t>(); | ||
|
|
||
| std::vector<size_t> expected_order(fc_input_b.get_partial_shape().size()); | ||
| std::iota(expected_order.begin(), expected_order.end(), 0); | ||
| std::swap(*(expected_order.end() - 1), *(expected_order.end() - 2)); | ||
|
|
||
| if (order == expected_order) | ||
| fc_input_b = transpose_node; | ||
| } |
Contributor
There was a problem hiding this comment.
Suggested change
| if (transpose_node && transpose_node->get_input_size() == 2) { | |
| auto order_constant = ov::as_type_ptr<ov::op::v0::Constant>(transpose_node->get_input_node_shared_ptr(1)); | |
| if (order_constant) { | |
| std::vector<size_t> order = order_constant->cast_vector<size_t>(); | |
| std::vector<size_t> expected_order(fc_input_b.get_partial_shape().size()); | |
| std::iota(expected_order.begin(), expected_order.end(), 0); | |
| std::swap(*(expected_order.end() - 1), *(expected_order.end() - 2)); | |
| if (order == expected_order) | |
| fc_input_b = transpose_node; | |
| } | |
| if (can_reuse_transpose(fc_input_b)) { | |
| fc_input_b = transpose_node; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue:
#35993 introduced a GPU memory regression that inflates on-device weight memory.
Root cause:
ConvertMatMulToFullyConnectednormalizes weights by inserting aTransposein front of the weights and re-wiring the decompressionConvert. #35993 changed this to alwaysconvert->clone_with_new_inputs(...)- correct in intent (it prevents corrupting aConvertshared by sibling MatMuls), butclone_with_new_inputs()has two side effects that were not handled:rt_info, and bothDecompressionandDisableConstantFoldingare intentionally non-copyable (is_copyable() == false), so they cannot be carried over. Without them, the subsequentConstantFoldingfolds the clonedf16-const → Convert(f32)weights path into a materialized f32 weight constant, doubling that weight's device memory.Convertis shared by several MatMuls, cloning it per consumer produces N independent weight subgraphs thatConstantFoldingmaterializes into N separate weight constants instead of one.Fix:
In
convert_matmul_to_fc.cpp:Convert-mark_as_decompressionanddisable_constant_folding- when the originalConverthad them, so the compressed weights are not const-folded into a materialized f32 weight.Convertreuse a single cloned weights subgraph, preserving cross-layer weight sharing.The cache is
std::map<std::pair<std::shared_ptr<Node> /*original Convert*/, bool /*is_small_matmul*/>, ov::Output<Node>>:shared_ptrkeeps the key node alive so its address cannot be reused by an unrelated node.is_small_matmulis part of the key because it selects the weights layout (and hence the resultingFullyConnected'stranspose_b). Sibling MatMuls sharing oneConvertcan differ inis_small_matmul(they may differ in M), so a clone built for one layout must not be reused for the other. The existing sibling guard already forces such siblings to sharematmul->transpose_b, so(Convert, is_small_matmul)fully determines the normalized weights.The cache lookup is performed before weights normalization: on a cache hit the already-normalized weights are reused and both the
Transposeinsertion and theConvertclone are skipped. Activation normalization (transpose_a) is hoisted above this block so it still runs for every MatMul, including cache hits - it is independent of the weights path. This keeps #35993's shared-Convert-corruption fix intact (the clone still isolates each transpose) while avoiding throwaway node creation on hits for models with many MatMuls sharing one weightsConvert.Tickets: