|
26 | 26 | #include "openvino/op/subtract.hpp" |
27 | 27 | #include "openvino/op/transpose.hpp" |
28 | 28 | #include "openvino/op/unsqueeze.hpp" |
| 29 | +#include "openvino/op/variadic_split.hpp" |
29 | 30 | #include "openvino/pass/pattern/op/optional.hpp" |
30 | 31 | #include "openvino/pass/pattern/op/pattern.hpp" |
31 | 32 | #include "openvino/pass/pattern/op/wrap_type.hpp" |
@@ -152,6 +153,7 @@ bool SDPAFusion::run_on_model(const std::shared_ptr<ov::Model>& model) { |
152 | 153 | ov::pass::SymbolicOptimizations symbolic_optimizations(false, get_pass_config()); |
153 | 154 | auto symbolic_ctx_manager = symbolic_optimizations.get_manager(); |
154 | 155 | symbolic_ctx_manager->register_pass<ov::pass::SDPAFusionMatcher>(); |
| 156 | + symbolic_ctx_manager->register_pass<ov::pass::SDPASplitAttentionFusionMatcher>(); |
155 | 157 | symbolic_ctx_manager->register_pass<ov::pass::SDPAFusionMatcherSinks>(); |
156 | 158 | symbolic_ctx_manager->register_pass<ov::pass::SDPAReshapeFusion>(); |
157 | 159 | return symbolic_optimizations.run_on_model(model); |
@@ -632,4 +634,196 @@ SDPAFusionMatcherSinks::SDPAFusionMatcherSinks() { |
632 | 634 | this->register_matcher(m, callback); |
633 | 635 | } |
634 | 636 |
|
| 637 | +SDPASplitAttentionFusionMatcher::SDPASplitAttentionFusionMatcher() { |
| 638 | + MATCHER_SCOPE(SDPASplitAttentionFusionMatcher); |
| 639 | + |
| 640 | + // Match the "split-attention" pattern; see header documentation. |
| 641 | + auto q_input = any_input(); |
| 642 | + auto k_cache_input = any_input(); |
| 643 | + auto k_new_input = any_input(); |
| 644 | + auto v_cache_input = any_input(); |
| 645 | + auto v_new_input = any_input(); |
| 646 | + auto mask_input = any_input(); |
| 647 | + |
| 648 | + auto qk_cache = wrap_type<v0::MatMul>({q_input, k_cache_input}, consumers_count(1)); |
| 649 | + auto qk_new = wrap_type<v0::MatMul>({q_input, k_new_input}, consumers_count(1)); |
| 650 | + |
| 651 | + auto scores_concat = wrap_type<v0::Concat>({qk_cache, qk_new}, consumers_count(1)); |
| 652 | + auto masked_scores = wrap_type<v1::Add>({scores_concat, mask_input}, consumers_count(1)); |
| 653 | + auto softmax = wrap_type<v8::Softmax>({masked_scores}, consumers_count(1)); |
| 654 | + |
| 655 | + auto split_axis = any_input(); |
| 656 | + auto split_sizes = any_input(); |
| 657 | + auto vsplit = wrap_type<v1::VariadicSplit>({softmax, split_axis, split_sizes}); |
| 658 | + |
| 659 | + // VariadicSplit has multiple outputs; the v0::MatMul wrap cannot easily express |
| 660 | + // "use output 0/1 of vsplit", so the first MatMul input is matched as any_input and |
| 661 | + // verified explicitly in the callback below. |
| 662 | + auto attn_cache = wrap_type<v0::MatMul>({any_input(), v_cache_input}); |
| 663 | + auto attn_new = wrap_type<v0::MatMul>({any_input(), v_new_input}); |
| 664 | + auto output_add = wrap_type<v1::Add>({attn_cache, attn_new}); |
| 665 | + |
| 666 | + ov::matcher_pass_callback callback = [=](Matcher& m) { |
| 667 | + if (transformation_callback(m.get_match_root())) { |
| 668 | + return false; |
| 669 | + } |
| 670 | + |
| 671 | + auto add_node = ov::as_type_ptr<v1::Add>(m.get_match_root()); |
| 672 | + if (!add_node) { |
| 673 | + return false; |
| 674 | + } |
| 675 | + |
| 676 | + auto matmul_cache = ov::as_type_ptr<v0::MatMul>(add_node->input_value(0).get_node_shared_ptr()); |
| 677 | + auto matmul_new = ov::as_type_ptr<v0::MatMul>(add_node->input_value(1).get_node_shared_ptr()); |
| 678 | + if (!matmul_cache || !matmul_new) { |
| 679 | + return false; |
| 680 | + } |
| 681 | + |
| 682 | + // Both V matmuls' first input must come from the same VariadicSplit (output 0 and 1). |
| 683 | + auto cache_src = matmul_cache->input_value(0); |
| 684 | + auto new_src = matmul_new->input_value(0); |
| 685 | + auto vsplit_node = ov::as_type_ptr<v1::VariadicSplit>(cache_src.get_node_shared_ptr()); |
| 686 | + if (!vsplit_node || |
| 687 | + vsplit_node != ov::as_type_ptr<v1::VariadicSplit>(new_src.get_node_shared_ptr())) { |
| 688 | + return false; |
| 689 | + } |
| 690 | + if (cache_src.get_index() != 0 || new_src.get_index() != 1) { |
| 691 | + return false; |
| 692 | + } |
| 693 | + |
| 694 | + auto softmax_node = ov::as_type_ptr<v8::Softmax>(vsplit_node->input_value(0).get_node_shared_ptr()); |
| 695 | + if (!softmax_node) { |
| 696 | + return false; |
| 697 | + } |
| 698 | + |
| 699 | + // Softmax must be on the last axis. |
| 700 | + auto sm_rank = softmax_node->get_output_partial_shape(0).rank(); |
| 701 | + if (sm_rank.is_dynamic()) { |
| 702 | + return false; |
| 703 | + } |
| 704 | + auto sm_axis = softmax_node->get_axis(); |
| 705 | + if (sm_axis < 0) { |
| 706 | + sm_axis += sm_rank.get_length(); |
| 707 | + } |
| 708 | + if (sm_axis != sm_rank.get_length() - 1) { |
| 709 | + return false; |
| 710 | + } |
| 711 | + |
| 712 | + auto mask_add_node = ov::as_type_ptr<v1::Add>(softmax_node->input_value(0).get_node_shared_ptr()); |
| 713 | + if (!mask_add_node) { |
| 714 | + return false; |
| 715 | + } |
| 716 | + |
| 717 | + auto concat_node = ov::as_type_ptr<v0::Concat>(mask_add_node->input_value(0).get_node_shared_ptr()); |
| 718 | + if (!concat_node || concat_node->get_input_size() != 2) { |
| 719 | + return false; |
| 720 | + } |
| 721 | + auto concat_rank = concat_node->get_output_partial_shape(0).rank(); |
| 722 | + if (concat_rank.is_dynamic()) { |
| 723 | + return false; |
| 724 | + } |
| 725 | + auto concat_axis = concat_node->get_axis(); |
| 726 | + if (concat_axis < 0) { |
| 727 | + concat_axis += concat_rank.get_length(); |
| 728 | + } |
| 729 | + if (concat_axis != concat_rank.get_length() - 1) { |
| 730 | + return false; |
| 731 | + } |
| 732 | + |
| 733 | + auto qk_cache_node = ov::as_type_ptr<v0::MatMul>(concat_node->input_value(0).get_node_shared_ptr()); |
| 734 | + auto qk_new_node = ov::as_type_ptr<v0::MatMul>(concat_node->input_value(1).get_node_shared_ptr()); |
| 735 | + if (!qk_cache_node || !qk_new_node) { |
| 736 | + return false; |
| 737 | + } |
| 738 | + |
| 739 | + // Both QK matmuls must share the same Q and agree on transpose flags. |
| 740 | + if (qk_cache_node->input_value(0) != qk_new_node->input_value(0)) { |
| 741 | + return false; |
| 742 | + } |
| 743 | + if (qk_cache_node->get_transpose_a() || qk_new_node->get_transpose_a()) { |
| 744 | + return false; |
| 745 | + } |
| 746 | + if (qk_cache_node->get_transpose_b() != qk_new_node->get_transpose_b()) { |
| 747 | + return false; |
| 748 | + } |
| 749 | + if (matmul_cache->get_transpose_a() || matmul_new->get_transpose_a()) { |
| 750 | + return false; |
| 751 | + } |
| 752 | + if (matmul_cache->get_transpose_b() != matmul_new->get_transpose_b()) { |
| 753 | + return false; |
| 754 | + } |
| 755 | + |
| 756 | + auto Q = qk_cache_node->input_value(0); |
| 757 | + auto K_cache = qk_cache_node->input_value(1); |
| 758 | + auto K_new = qk_new_node->input_value(1); |
| 759 | + auto V_cache = matmul_cache->input_value(1); |
| 760 | + auto V_new = matmul_new->input_value(1); |
| 761 | + auto mask_value = mask_add_node->input_value(1); |
| 762 | + |
| 763 | + // Require static 4D ranks for predictable layout reasoning. |
| 764 | + for (const auto& in : {Q, K_cache, K_new, V_cache, V_new}) { |
| 765 | + const auto& ps = in.get_partial_shape(); |
| 766 | + if (ps.rank().is_dynamic() || ps.rank().get_length() != 4) { |
| 767 | + return false; |
| 768 | + } |
| 769 | + } |
| 770 | + |
| 771 | + // Interpret MatMul transpose flags as a physical-layout hint: |
| 772 | + // qk transpose_b == true; K stored as [B,H,S,D] (standard); MatMul performs Q ┬╖ K^T. |
| 773 | + // qk transpose_b == false; K stored as [B,H,D,S] (already K^T in memory). |
| 774 | + // attn transpose_b == true; V stored as [B,H,D,S]; MatMul performs probs ┬╖ V^T. |
| 775 | + // attn transpose_b == false; V stored as [B,H,S,D] (standard). |
| 776 | + const bool k_is_transposed = !qk_cache_node->get_transpose_b(); |
| 777 | + const bool v_is_transposed = matmul_cache->get_transpose_b(); |
| 778 | + |
| 779 | + // Concat along the physical sequence axis of each input, then add an explicit |
| 780 | + // Transpose to bring it to the [B,H,S,D] layout expected by v13::SDPA. |
| 781 | + const int64_t k_concat_axis = k_is_transposed ? 3 : 2; |
| 782 | + const int64_t v_concat_axis = v_is_transposed ? 3 : 2; |
| 783 | + |
| 784 | + auto k_concat = std::make_shared<v0::Concat>(ov::OutputVector{K_cache, K_new}, k_concat_axis); |
| 785 | + auto v_concat = std::make_shared<v0::Concat>(ov::OutputVector{V_cache, V_new}, v_concat_axis); |
| 786 | + |
| 787 | + std::shared_ptr<ov::Node> k_input = k_concat; |
| 788 | + std::shared_ptr<ov::Node> v_input = v_concat; |
| 789 | + |
| 790 | + ov::NodeVector new_nodes{k_concat, v_concat}; |
| 791 | + auto add_transpose = [&new_nodes](std::shared_ptr<ov::Node>& target) { |
| 792 | + auto perm = v0::Constant::create(ov::element::i64, ov::Shape{4}, {0, 1, 3, 2}); |
| 793 | + auto transpose = std::make_shared<v1::Transpose>(target, perm); |
| 794 | + new_nodes.push_back(perm); |
| 795 | + new_nodes.push_back(transpose); |
| 796 | + target = transpose; |
| 797 | + }; |
| 798 | + if (k_is_transposed) { |
| 799 | + add_transpose(k_input); |
| 800 | + } |
| 801 | + if (v_is_transposed) { |
| 802 | + add_transpose(v_input); |
| 803 | + } |
| 804 | + |
| 805 | + // Scale = 1.0: the matched pattern has no explicit scale node; any scaling is |
| 806 | + // assumed to be baked into Q by the model (e.g. Gemma's query_pre_attn_scalar). |
| 807 | + auto scale_const = v0::Constant::create(Q.get_element_type(), ov::Shape{}, {1.0f}); |
| 808 | + new_nodes.push_back(scale_const); |
| 809 | + |
| 810 | + auto sdpa = std::make_shared<v13::ScaledDotProductAttention>(Q, |
| 811 | + k_input, |
| 812 | + v_input, |
| 813 | + mask_value, |
| 814 | + scale_const, |
| 815 | + /*is_causal=*/false); |
| 816 | + new_nodes.push_back(sdpa); |
| 817 | + |
| 818 | + sdpa->set_friendly_name(add_node->get_friendly_name()); |
| 819 | + ov::copy_runtime_info(m.get_matched_nodes(), new_nodes); |
| 820 | + ov::replace_node(add_node, sdpa); |
| 821 | + |
| 822 | + return true; |
| 823 | + }; |
| 824 | + |
| 825 | + auto m = std::make_shared<Matcher>(output_add, matcher_name); |
| 826 | + this->register_matcher(m, callback); |
| 827 | +} |
| 828 | + |
635 | 829 | } // namespace ov::pass |
0 commit comments