Skip to content

Commit 21f4beb

Browse files
authored
[NPUW] Fixed chunked prefill for new LFM2 IR (#36933)
### Details: - *This is a cherry-pick of #36929 to master* - *Fixed chunked prefill with new IR version of LFM2*: - *New IR version wires `attention_mask` with Conv states by slicing that `attention_mask` from the left on the size of current prompt.* - *NPUW places new tokens in the right end of the `input_ids` and `attention_mask` in static shape scenario, so mask should be sliced from the right end instead.* ### Tickets: - *CVS-190129* - *CVS-190528* ### AI Assistance: - *AI assistance used: yes* - *Assistant is used for parallel debugging, small issues correction in the fix and tests*
1 parent a9e7448 commit 21f4beb

5 files changed

Lines changed: 403 additions & 0 deletions

File tree

src/plugins/intel_npu/src/plugin/npuw/llm_compiled_model.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "npuw_transformations/replace_deepstack_scatter_with_add.hpp"
2121
#include "npuw_transformations/reshape_sliced_head_to_static.hpp"
2222
#include "npuw_transformations/reshape_to_static.hpp"
23+
#include "npuw_transformations/right_align_mask_slice_for_conv.hpp"
2324
#include "npuw_transformations/slice_out_embeds.hpp"
2425
#include "npuw_transformations/split_kvcache_into_blocks.hpp"
2526
#include "openvino/op/convert.hpp"
@@ -821,6 +822,8 @@ ov::npuw::LLMCompiledModel::LLMCompiledModel(const std::shared_ptr<ov::Model>& m
821822
} else {
822823
LOG_DEBUG("Adding position_ids input in case it doesn't exist in model: LFM-2 case.");
823824
ov::npuw::AddPositionIdsParam().run_on_model(kvcache_model);
825+
LOG_DEBUG("Right-align attention_mask slice for Conv operations: LFM-2 case.");
826+
ov::npuw::RightAlignMaskSliceForConv().run_on_model(kvcache_model);
824827
LOG_DEBUG("Transform kvcache model from stateful to stateless.");
825828
ov::pass::StatefulToStateless().run_on_model(kvcache_model);
826829
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright (C) 2018-2026 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
5+
#include "right_align_mask_slice_for_conv.hpp"
6+
7+
#include "openvino/op/ops.hpp"
8+
#include "openvino/openvino.hpp"
9+
#include "openvino/pass/manager.hpp"
10+
#include "openvino/pass/matcher_pass.hpp"
11+
#include "openvino/pass/pass.hpp"
12+
#include "openvino/pass/pattern/op/wrap_type.hpp"
13+
#include "openvino/pass/validate.hpp"
14+
15+
namespace opp = ov::pass::pattern;
16+
17+
namespace {
18+
19+
// diagnostics warnings on OPENVINO_MATCHER_PASS_RTTI() definition: visibility hidden
20+
#ifdef __GNUC__
21+
# pragma GCC diagnostic push
22+
# pragma GCC diagnostic ignored "-Wattributes"
23+
#endif
24+
25+
class RightAlignMaskSliceForConvImpl : public ov::pass::MatcherPass {
26+
public:
27+
// In LFM2-like models, where `attention_mask` is also consumed by the Conv subgraph,
28+
// the Slice operation is inserted to extract the current tokens from that `attention_mask`
29+
// and pass to the Conv subgraph.
30+
// This is done to allow skipping processing of some tokens in the prefill stage.
31+
// Slice is done from zero till the number of current tokens, assuming that in most cases
32+
// tokens from the right are not the useful ones.
33+
// However, in NPUW static-shape mode, we place useful current tokens exactly to the right end
34+
// in the prefill stage.
35+
// Therefore, Slice operation will extract the left part of mask, that in case of chunked
36+
// prefill will be padded either with zeroes (in the first iteration) or with ones
37+
// (in subsequent iterations) but for past tokens.
38+
// This pass re-align the Slice operation to extract the right part of the mask,
39+
// that contains useful current tokens.
40+
OPENVINO_MATCHER_PASS_RTTI("ov::npuw::RightAlignMaskSliceForConvImpl");
41+
explicit RightAlignMaskSliceForConvImpl() {
42+
auto attention_mask = opp::wrap_type<ov::op::v0::Parameter>();
43+
auto attention_mask_slice = opp::wrap_type<ov::op::v8::Slice>(
44+
{attention_mask, opp::any_input(), opp::any_input(), opp::any_input(), opp::any_input()});
45+
auto unsqueeze = opp::wrap_type<ov::op::v0::Unsqueeze>({attention_mask_slice, opp::any_input()});
46+
auto convert = opp::wrap_type<ov::op::v0::Convert>({unsqueeze});
47+
auto multiply = opp::wrap_type<ov::op::v1::Multiply>({convert, opp::any_input()});
48+
auto add = opp::wrap_type<ov::op::v1::Add>({multiply, opp::any_input()});
49+
auto multiply_with_embedd = opp::wrap_type<ov::op::v1::Multiply>({opp::any_input(), add});
50+
auto matmul = opp::wrap_type<ov::op::v0::MatMul>({multiply_with_embedd, opp::any_input()});
51+
auto transpose = opp::wrap_type<ov::op::v1::Transpose>({matmul, opp::any_input()});
52+
// VariadicSplit into B,C,~h in LFM2.
53+
auto variadic_split =
54+
opp::wrap_type<ov::op::v1::VariadicSplit>({transpose, opp::any_input(), opp::any_input()});
55+
56+
auto callback = [=](opp::Matcher& m) {
57+
auto& node_to_output = m.get_pattern_value_map();
58+
59+
auto matched_attention_mask = node_to_output.at(attention_mask).get_node_shared_ptr();
60+
const auto names = matched_attention_mask->output(0).get_names();
61+
if (names.count(ov::npuw::attention_mask_name) == 0 &&
62+
matched_attention_mask->get_friendly_name() != ov::npuw::attention_mask_name) {
63+
return false;
64+
}
65+
auto matched_attention_mask_slice = node_to_output.at(attention_mask_slice).get_node_shared_ptr();
66+
const auto slice_start_const = ov::as_type_ptr<ov::op::v0::Constant>(
67+
matched_attention_mask_slice->input_value(1).get_node_shared_ptr());
68+
if (!slice_start_const || slice_start_const->cast_vector<int64_t>() != std::vector<int64_t>{0}) {
69+
return false;
70+
}
71+
auto matched_attention_mask_unsqueeze = node_to_output.at(unsqueeze).get_node_shared_ptr();
72+
73+
auto const_zero = std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{}, 0);
74+
auto const_one = std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{}, 1);
75+
auto mask_shape_of = std::make_shared<ov::op::v3::ShapeOf>(matched_attention_mask);
76+
auto mask_len = std::make_shared<ov::op::v8::Gather>(mask_shape_of, const_one, const_zero);
77+
auto mask_len_rank_one = std::make_shared<ov::op::v0::Unsqueeze>(mask_len, const_zero);
78+
// 2nd argument to Slice is stop point:
79+
auto current_len = matched_attention_mask_slice->input(2).get_source_output().get_node_shared_ptr();
80+
auto current_offset = std::make_shared<ov::op::v1::Subtract>(mask_len, current_len);
81+
82+
auto const_one_rank_one = std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{1}, 1);
83+
auto new_slice = std::make_shared<ov::op::v8::Slice>(matched_attention_mask,
84+
current_offset,
85+
mask_len_rank_one,
86+
const_one_rank_one,
87+
const_one_rank_one);
88+
matched_attention_mask_unsqueeze->input(0).replace_source_output(new_slice);
89+
return true;
90+
};
91+
register_matcher(std::make_shared<opp::Matcher>(variadic_split, "RightAlignMaskSliceForConvImpl"),
92+
std::move(callback));
93+
}
94+
};
95+
96+
#ifdef __GNUC__
97+
# pragma GCC diagnostic pop
98+
#endif
99+
} // anonymous namespace
100+
101+
bool ov::npuw::RightAlignMaskSliceForConv::run_on_model(const std::shared_ptr<ov::Model>& model) {
102+
ov::pass::Manager manager("right-align-mask-slice-for-conv");
103+
manager.set_per_pass_validation(true);
104+
manager.register_pass<RightAlignMaskSliceForConvImpl>();
105+
manager.run_passes(model);
106+
return true;
107+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (C) 2018-2026 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
5+
#pragma once
6+
7+
#include "openvino/openvino.hpp"
8+
9+
namespace ov::npuw {
10+
11+
constexpr const char* attention_mask_name = "attention_mask";
12+
13+
class RightAlignMaskSliceForConv : public ov::pass::ModelPass {
14+
public:
15+
OPENVINO_MODEL_PASS_RTTI("ov::npuw::RightAlignMaskSliceForConv");
16+
bool run_on_model(const std::shared_ptr<ov::Model>& model) override;
17+
};
18+
} // namespace ov::npuw

src/plugins/intel_npu/tests/unit/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ ov_add_test_target(
109109
${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/reshape_sliced_head_to_static.cpp
110110
${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/reshape_to_static.cpp
111111
${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/replace_deepstack_scatter_with_add.cpp
112+
${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/right_align_mask_slice_for_conv.cpp
112113
${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/npuw_transformations/slice_out_embeds.cpp
113114
# kokoro pipeline
114115
${OpenVINO_SOURCE_DIR}/src/plugins/intel_npu/src/plugin/npuw/pipelines/kokoro/kokoro_compiled_model.cpp
@@ -161,6 +162,7 @@ target_sources(${TARGET_NAME} PRIVATE
161162
${CMAKE_CURRENT_SOURCE_DIR}/npuw/subgraph_behavior_infer_test.cpp
162163
${CMAKE_CURRENT_SOURCE_DIR}/npuw/preserve_const_matmul_pattern_test.cpp
163164
${CMAKE_CURRENT_SOURCE_DIR}/npuw/pipeline_passes/add_position_ids_param_test.cpp
165+
${CMAKE_CURRENT_SOURCE_DIR}/npuw/pipeline_passes/right_align_mask_slice_for_conv_test.cpp
164166
${CMAKE_CURRENT_SOURCE_DIR}/npuw/pipeline_passes/collapse_unqdq_test.cpp
165167
${CMAKE_CURRENT_SOURCE_DIR}/npuw/pipeline_passes/conv_to_matmul_test.cpp
166168
${CMAKE_CURRENT_SOURCE_DIR}/npuw/pipeline_passes/patch_sliding_mask_test.cpp

0 commit comments

Comments
 (0)