Skip to content

Commit ce291f6

Browse files
committed
[OP] SelectiveSSM and PagedSelectiveSSM operations
1 parent 4501634 commit ce291f6

15 files changed

Lines changed: 1440 additions & 0 deletions

src/core/dev_api/dev_headers.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ set(DEV_HEADERS
2828
${CMAKE_CURRENT_LIST_DIR}/openvino/op/paged_attention.hpp
2929
${CMAKE_CURRENT_LIST_DIR}/openvino/op/paged_causal_conv1d.hpp
3030
${CMAKE_CURRENT_LIST_DIR}/openvino/op/paged_gated_delta_net.hpp
31+
${CMAKE_CURRENT_LIST_DIR}/openvino/op/paged_selective_ssm.hpp
3132
${CMAKE_CURRENT_LIST_DIR}/openvino/op/rms_norm.hpp
33+
${CMAKE_CURRENT_LIST_DIR}/openvino/op/selective_ssm.hpp
3234
${CMAKE_CURRENT_LIST_DIR}/openvino/op/util/node_util.hpp
3335
${CMAKE_CURRENT_LIST_DIR}/openvino/op/util/slice_plan.hpp
3436
${CMAKE_CURRENT_LIST_DIR}/openvino/opsets/opset10_decl.hpp
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (C) 2018-2026 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
#pragma once
5+
6+
#include "openvino/op/op.hpp"
7+
8+
namespace ov::op::internal {
9+
/// \note PagedSelectiveSSM op class is under development and subject to change
10+
///
11+
/// \brief Operator performing paged SelectiveSSM computation for continuous batching.
12+
///
13+
/// Paged variant of the SelectiveSSM (Mamba2 selective state-space, arXiv:2405.21060) recurrence. Processes
14+
/// tokens from multiple sequences packed into a single batch and manages the recurrent SSM state (a per-head
15+
/// ``[head_dim, state_size]`` matrix) using a paged block table, enabling non-contiguous memory allocation
16+
/// across sequences. Per token: ``dA = exp(A * dt)``, ``dtB = dt * B``, ``dBx = dtB (x) x``,
17+
/// ``state = state * dA + dBx``, ``output = sum(state * C)``.
18+
///
19+
/// ``B`` and ``C`` are grouped and shared across heads: each head ``h`` reads group
20+
/// ``g = h / heads_per_group``, where ``heads_per_group = num_heads / num_groups``.
21+
///
22+
/// ``recurrent_state_table`` is updated in place. For sequence ``s``, the state is cached every
23+
/// ``cache_interval[s]`` tokens into the blocks addressed by
24+
/// ``la_block_indices[la_block_indices_begins[s] : la_block_indices_begins[s+1]]``;
25+
/// ``cache_interval[s] <= 0`` disables caching for that sequence. ``num_processed_tokens[s]`` gives the
26+
/// count of previously processed tokens, used to resume from the correct cached state and block offset.
27+
/// \ingroup ov_ops_cpp_api
28+
class OPENVINO_API PagedSelectiveSSM : public ov::op::Op {
29+
public:
30+
OPENVINO_OP("PagedSelectiveSSM");
31+
32+
PagedSelectiveSSM() = default;
33+
/// \brief Constructs a PagedSelectiveSSM operation.
34+
///
35+
/// \param A (Negative) log-decay rates per head [num_heads].
36+
/// \param dt Per-token, per-head time steps used for discretization [batch_size_in_tokens, num_heads].
37+
/// \param B Grouped input projection [batch_size_in_tokens, num_groups, state_size].
38+
/// \param x Input hidden states [batch_size_in_tokens, num_heads, head_dim].
39+
/// \param C Grouped output projection [batch_size_in_tokens, num_groups, state_size].
40+
/// \param recurrent_state_table Paged table of recurrent state snapshots, updated in place
41+
/// [num_blocks, num_heads, head_dim, state_size]; an all-zeros tensor before any tokens are cached.
42+
/// \param subsequence_begins Start indices of each sequence's tokens in the flattened token batch
43+
/// [batch_size_in_sequences + 1], element type i32 or i64.
44+
/// \param la_block_indices Physical block row indices into recurrent_state_table, concatenated across
45+
/// all sequences [num_blocks], element type i32 or i64.
46+
/// \param la_block_indices_begins Splits la_block_indices among sequences
47+
/// [batch_size_in_sequences + 1], element type i32 or i64.
48+
/// \param num_processed_tokens Number of tokens already processed for each sequence
49+
/// [batch_size_in_sequences], element type i32 or i64.
50+
/// \param cache_interval Interval (in tokens) at which the recurrent state is cached for each sequence;
51+
/// a value <= 0 disables caching [batch_size_in_sequences], element type i32 or i64.
52+
PagedSelectiveSSM(const Output<Node>& A,
53+
const Output<Node>& dt,
54+
const Output<Node>& B,
55+
const Output<Node>& x,
56+
const Output<Node>& C,
57+
const Output<Node>& recurrent_state_table,
58+
const Output<Node>& subsequence_begins,
59+
const Output<Node>& la_block_indices,
60+
const Output<Node>& la_block_indices_begins,
61+
const Output<Node>& num_processed_tokens,
62+
const Output<Node>& cache_interval);
63+
64+
/// \brief Constructs a PagedSelectiveSSM operation from input vector.
65+
///
66+
/// \param args Input tensor vector (11 inputs in order listed above).
67+
explicit PagedSelectiveSSM(const ov::OutputVector& args);
68+
69+
void validate_and_infer_types() override;
70+
bool visit_attributes(AttributeVisitor& visitor) override;
71+
std::shared_ptr<ov::Node> clone_with_new_inputs(const ov::OutputVector& new_args) const override;
72+
};
73+
74+
} // namespace ov::op::internal
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (C) 2018-2026 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
#pragma once
5+
6+
#include "openvino/op/op.hpp"
7+
8+
namespace ov::op::internal {
9+
/// \note SelectiveSSM op class is under development and subject to change
10+
///
11+
/// \brief Operator performing the Mamba2 selective state-space recurrence (arXiv:2405.21060).
12+
///
13+
/// Discretizes ``A`` (log-decay rates) and ``B`` (input projection) with ``dt`` (time steps) ahead of the
14+
/// recurrence: ``dA = exp(A * dt)``, ``dtB = dt * B``. Then, for each token ``t``:
15+
/// ``dBx_t = dtB_t (x) x_t``, ``state_t = state_{t-1} * dA_t + dBx_t``, ``y_t = sum(state_t * C_t)``.
16+
///
17+
/// ``B`` and ``C`` are grouped and shared across heads: each head ``h`` reads group
18+
/// ``g = h / heads_per_group``, where ``heads_per_group = num_heads / num_groups``.
19+
/// \ingroup ov_ops_cpp_api
20+
class OPENVINO_API SelectiveSSM : public ov::op::Op {
21+
public:
22+
OPENVINO_OP("SelectiveSSM");
23+
24+
SelectiveSSM() = default;
25+
/// \brief Constructs a SelectiveSSM operation.
26+
///
27+
/// \param A (Negative) log-decay rates per head [num_heads].
28+
/// \param dt Per-token, per-head time steps used for discretization [batch_size, seq_len, num_heads].
29+
/// \param B Grouped input projection [batch_size, seq_len, num_groups, state_size].
30+
/// \param x Input hidden states [batch_size, seq_len, num_heads, head_dim].
31+
/// \param C Grouped output projection [batch_size, seq_len, num_groups, state_size].
32+
/// \param recurrent_state Initial SSM hidden state [batch_size, num_heads, head_dim, state_size];
33+
/// an all-zeros tensor for a fresh sequence.
34+
SelectiveSSM(const Output<Node>& A,
35+
const Output<Node>& dt,
36+
const Output<Node>& B,
37+
const Output<Node>& x,
38+
const Output<Node>& C,
39+
const Output<Node>& recurrent_state);
40+
41+
/// \brief Constructs a SelectiveSSM operation from input vector.
42+
///
43+
/// \param args Input tensor vector in order: A, dt, B, x, C, recurrent_state.
44+
explicit SelectiveSSM(const ov::OutputVector& args);
45+
46+
void validate_and_infer_types() override;
47+
bool visit_attributes(AttributeVisitor& visitor) override;
48+
std::shared_ptr<ov::Node> clone_with_new_inputs(const ov::OutputVector& new_args) const override;
49+
};
50+
51+
} // namespace ov::op::internal

src/core/shape_inference/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ target_sources(${TARGET_NAME}
100100
${SHAPE_INFER_INCLUDE_DIR}/paged_attention_shape_inference.hpp
101101
${SHAPE_INFER_INCLUDE_DIR}/paged_causal_conv1d_shape_inference.hpp
102102
${SHAPE_INFER_INCLUDE_DIR}/paged_gated_delta_net_shape_inference.hpp
103+
${SHAPE_INFER_INCLUDE_DIR}/paged_selective_ssm_shape_inference.hpp
103104
${SHAPE_INFER_INCLUDE_DIR}/pooling_shape_inference_util.hpp
104105
${SHAPE_INFER_INCLUDE_DIR}/prior_box_clustered_shape_inference.hpp
105106
${SHAPE_INFER_INCLUDE_DIR}/prior_box_shape_inference.hpp
@@ -130,6 +131,7 @@ target_sources(${TARGET_NAME}
130131
${SHAPE_INFER_INCLUDE_DIR}/search_sorted_shape_inference.hpp
131132
${SHAPE_INFER_INCLUDE_DIR}/segment_max_shape_inference.hpp
132133
${SHAPE_INFER_INCLUDE_DIR}/select_shape_inference.hpp
134+
${SHAPE_INFER_INCLUDE_DIR}/selective_ssm_shape_inference.hpp
133135
${SHAPE_INFER_INCLUDE_DIR}/sequence_generator.hpp
134136
${SHAPE_INFER_INCLUDE_DIR}/shape_infer_type_utils.hpp
135137
${SHAPE_INFER_INCLUDE_DIR}/shape_nodes.hpp
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright (C) 2018-2026 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
5+
#pragma once
6+
7+
#include "openvino/op/paged_selective_ssm.hpp"
8+
#include "utils.hpp"
9+
10+
namespace ov::op::internal {
11+
12+
template <class T, class TRShape = result_shape_t<T>>
13+
std::vector<TRShape> shape_infer(const PagedSelectiveSSM* op, const std::vector<T>& input_shapes) {
14+
NODE_VALIDATION_CHECK(op, input_shapes.size() == 11);
15+
16+
NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[0].rank().compatible(1));
17+
NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[1].rank().compatible(2));
18+
NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[2].rank().compatible(3));
19+
NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[3].rank().compatible(3));
20+
NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[4].rank().compatible(3));
21+
NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[5].rank().compatible(4));
22+
for (size_t i = 6; i < 11; ++i) {
23+
NODE_SHAPE_INFER_CHECK(op, input_shapes, input_shapes[i].rank().compatible(1));
24+
}
25+
26+
const auto& A_ps = input_shapes[0];
27+
const auto& dt_ps = input_shapes[1];
28+
const auto& B_ps = input_shapes[2];
29+
const auto& x_ps = input_shapes[3];
30+
const auto& C_ps = input_shapes[4];
31+
const auto& state_ps = input_shapes[5];
32+
const auto& subsequence_begins_ps = input_shapes[6];
33+
const auto& la_block_indices_ps = input_shapes[7];
34+
const auto& la_block_indices_begins_ps = input_shapes[8];
35+
const auto& num_processed_tokens_ps = input_shapes[9];
36+
const auto& cache_interval_ps = input_shapes[10];
37+
38+
const auto A_rank_is_static = A_ps.rank().is_static();
39+
const auto dt_rank_is_static = dt_ps.rank().is_static();
40+
const auto B_rank_is_static = B_ps.rank().is_static();
41+
const auto x_rank_is_static = x_ps.rank().is_static();
42+
const auto C_rank_is_static = C_ps.rank().is_static();
43+
const auto state_rank_is_static = state_ps.rank().is_static();
44+
const auto subsequence_begins_rank_is_static = subsequence_begins_ps.rank().is_static();
45+
const auto la_block_indices_rank_is_static = la_block_indices_ps.rank().is_static();
46+
const auto la_block_indices_begins_rank_is_static = la_block_indices_begins_ps.rank().is_static();
47+
const auto num_processed_tokens_rank_is_static = num_processed_tokens_ps.rank().is_static();
48+
const auto cache_interval_rank_is_static = cache_interval_ps.rank().is_static();
49+
50+
Dimension token_dim, num_heads_dim, head_dim_dim, state_size_dim;
51+
52+
bool token_ok = true;
53+
if (x_rank_is_static)
54+
token_ok &= Dimension::merge(token_dim, token_dim, x_ps[0]);
55+
if (dt_rank_is_static)
56+
token_ok &= Dimension::merge(token_dim, token_dim, dt_ps[0]);
57+
if (B_rank_is_static)
58+
token_ok &= Dimension::merge(token_dim, token_dim, B_ps[0]);
59+
if (C_rank_is_static)
60+
token_ok &= Dimension::merge(token_dim, token_dim, C_ps[0]);
61+
NODE_SHAPE_INFER_CHECK(op,
62+
input_shapes,
63+
token_ok,
64+
"The token dimension of `dt`, `B`, `x` and `C` should be the same.");
65+
66+
bool num_heads_ok = true;
67+
if (x_rank_is_static)
68+
num_heads_ok &= Dimension::merge(num_heads_dim, num_heads_dim, x_ps[1]);
69+
if (A_rank_is_static)
70+
num_heads_ok &= Dimension::merge(num_heads_dim, num_heads_dim, A_ps[0]);
71+
if (dt_rank_is_static)
72+
num_heads_ok &= Dimension::merge(num_heads_dim, num_heads_dim, dt_ps[1]);
73+
if (state_rank_is_static)
74+
num_heads_ok &= Dimension::merge(num_heads_dim, num_heads_dim, state_ps[1]);
75+
NODE_SHAPE_INFER_CHECK(op,
76+
input_shapes,
77+
num_heads_ok,
78+
"The number of heads of `A`, `dt`, `x` and `recurrent_state_table` should be the same.");
79+
80+
if (B_rank_is_static && C_rank_is_static) {
81+
NODE_SHAPE_INFER_CHECK(op,
82+
input_shapes,
83+
C_ps[1].compatible(B_ps[1]),
84+
"The number of groups of `B` and `C` should be the same.");
85+
}
86+
87+
bool head_dim_ok = true;
88+
if (x_rank_is_static)
89+
head_dim_ok &= Dimension::merge(head_dim_dim, head_dim_dim, x_ps[2]);
90+
if (state_rank_is_static)
91+
head_dim_ok &= Dimension::merge(head_dim_dim, head_dim_dim, state_ps[2]);
92+
NODE_SHAPE_INFER_CHECK(op,
93+
input_shapes,
94+
head_dim_ok,
95+
"The head dimension of `x` and `recurrent_state_table` should be the same.");
96+
97+
bool state_size_ok = true;
98+
if (state_rank_is_static)
99+
state_size_ok &= Dimension::merge(state_size_dim, state_size_dim, state_ps[3]);
100+
if (B_rank_is_static)
101+
state_size_ok &= Dimension::merge(state_size_dim, state_size_dim, B_ps[2]);
102+
if (C_rank_is_static)
103+
state_size_ok &= Dimension::merge(state_size_dim, state_size_dim, C_ps[2]);
104+
NODE_SHAPE_INFER_CHECK(op,
105+
input_shapes,
106+
state_size_ok,
107+
"The state size of `B`, `C` and `recurrent_state_table` should be the same.");
108+
109+
if (num_heads_dim.is_static() && B_rank_is_static) {
110+
const auto& num_groups = B_ps[1];
111+
if (num_groups.is_static()) {
112+
NODE_SHAPE_INFER_CHECK(
113+
op,
114+
input_shapes,
115+
num_groups.get_length() != 0 && num_heads_dim.get_length() % num_groups.get_length() == 0,
116+
"The number of heads should be divisible by the number of groups.");
117+
}
118+
}
119+
120+
if (la_block_indices_rank_is_static && state_rank_is_static) {
121+
NODE_SHAPE_INFER_CHECK(op,
122+
input_shapes,
123+
la_block_indices_ps[0].compatible(state_ps[0]),
124+
"The number of blocks of `la_block_indices` and `recurrent_state_table` should "
125+
"be the same.");
126+
}
127+
if (subsequence_begins_rank_is_static && la_block_indices_begins_rank_is_static) {
128+
NODE_SHAPE_INFER_CHECK(op,
129+
input_shapes,
130+
subsequence_begins_ps[0].compatible(la_block_indices_begins_ps[0]),
131+
"The number of sequences of `subsequence_begins` and `la_block_indices_begins` "
132+
"should be the same.");
133+
}
134+
if (num_processed_tokens_rank_is_static && cache_interval_rank_is_static) {
135+
NODE_SHAPE_INFER_CHECK(op,
136+
input_shapes,
137+
num_processed_tokens_ps[0].compatible(cache_interval_ps[0]),
138+
"The number of sequences of `num_processed_tokens` and `cache_interval` should "
139+
"be the same.");
140+
}
141+
if (subsequence_begins_rank_is_static && num_processed_tokens_rank_is_static) {
142+
NODE_SHAPE_INFER_CHECK(op,
143+
input_shapes,
144+
(subsequence_begins_ps[0] - Dimension(1)).compatible(num_processed_tokens_ps[0]),
145+
"The number of sequences of `subsequence_begins` and `num_processed_tokens` "
146+
"should be the same.");
147+
}
148+
149+
auto output_shapes = std::vector<TRShape>{x_ps};
150+
if (x_rank_is_static) {
151+
output_shapes[0] = TRShape{token_dim, num_heads_dim, head_dim_dim};
152+
}
153+
return output_shapes;
154+
}
155+
156+
} // namespace ov::op::internal

0 commit comments

Comments
 (0)