44
55#include " batched.hpp"
66
7+ #include < algorithm>
78#include < utility>
89
910#include " openvino/core/except.hpp"
@@ -168,29 +169,35 @@ void ov::npuw::batched::InferRequest::infer() {
168169 OPENVINO_ASSERT (wrapper_inputs.size () == in_ports.size () && wrapper_outputs.size () == out_ports.size (),
169170 " Batched element: inner request I/O does not match the wrapped model" );
170171
171- // Batch size is taken from the first input that carries a batch dimension.
172- std::size_t batch = 1 ;
172+ // Snapshot the public inputs once -- read repeatedly below.
173+ std::vector<ov::SoPtr<ov::ITensor>> in_tensors;
174+ in_tensors.reserve (wrapper_inputs.size ());
173175 for (const auto & port : wrapper_inputs) {
174- const auto tensor = get_tensor (port);
175- if (has_batch_dim (tensor)) {
176- batch = tensor->get_shape ()[0 ];
177- break ;
176+ in_tensors.push_back (get_tensor (port));
177+ }
178+
179+ // Batch is the largest leading dim across inputs, so a shared [1, ...] input is broadcast
180+ // rather than mistaken for the batch. Stays 1 when nothing is batched.
181+ std::size_t batch = 1 ;
182+ bool any_batched = false ;
183+ for (const auto & tensor : in_tensors) {
184+ if (!has_batch_dim (tensor)) {
185+ continue ;
178186 }
187+ const std::size_t in_batch = tensor->get_shape ()[0 ];
188+ batch = any_batched ? std::max (batch, in_batch) : in_batch;
189+ any_batched = true ;
179190 }
180- // A zero-sized batch has no rows to score and would leave the outputs unpopulated
181- // (publishing null tensors). Reject it explicitly rather than silently no-op.
191+ // A zero-sized batch has no rows and would publish unpopulated outputs; reject it.
182192 OPENVINO_ASSERT (batch > 0 , " Batched element: batch size must be > 0, got an input with batch dimension 0." );
183193
184- // Validate every batched input up front: each row-carrying input must have a batch
185- // dimension equal to `batch`, or exactly 1 (a shared/broadcast input passed to every row).
186- // Anything else (e.g. a [M, ...] input with M != batch and M != 1) cannot be sliced per row
187- // and would otherwise be fed whole into the batch-1 inner request -> wrong results.
194+ // Every batched input must match `batch` or be a broadcast (dim 0 == 1); anything else
195+ // cannot be sliced per row.
188196 for (std::size_t i = 0 ; i < wrapper_inputs.size (); ++i) {
189- const auto full = get_tensor (wrapper_inputs[i]);
190- if (!has_batch_dim (full)) {
197+ if (!has_batch_dim (in_tensors[i])) {
191198 continue ;
192199 }
193- const std::size_t in_batch = full ->get_shape ()[0 ];
200+ const std::size_t in_batch = in_tensors[i] ->get_shape ()[0 ];
194201 OPENVINO_ASSERT (in_batch == batch || in_batch == 1 ,
195202 " Batched element: input '" ,
196203 wrapper_inputs[i].get_any_name (),
@@ -201,56 +208,61 @@ void ov::npuw::batched::InferRequest::infer() {
201208 " nor 1 (broadcast)." );
202209 }
203210
204- // Aggregated [batch, ...] outputs, allocated lazily once the per-row output shape is known.
205- std::vector<ov::SoPtr<ov::ITensor>> aggregated_outputs (wrapper_outputs.size ());
206-
207- // The inner request's variable states are stable across rows, so query them once and reset
208- // them per row rather than re-querying (which may allocate) on every iteration.
211+ // States are stable across rows: query once, reset per row so row i never sees row i-1.
209212 const auto inner_states = inner_query_state ();
210-
211- for (std::size_t row = 0 ; row < batch; ++row) {
212- // Rows are independent prompts: clear the inner request's variable state
213- // (KV-cache) so row i never sees row i-1. Harmless for stateless inners.
213+ const auto reset_inner_state = [&] {
214214 for (const auto & state : inner_states) {
215215 state->reset ();
216216 }
217+ };
217218
218- // Bind the row's slice of every batched input; pass shared ([1 , ...] or non-batched)
219- // inputs through unchanged.
219+ // Slice per- row inputs out of [batch , ...]; pass broadcast/ non-batched inputs through whole.
220+ const auto bind_row = [&](std:: size_t row) {
220221 for (std::size_t i = 0 ; i < wrapper_inputs.size (); ++i) {
221- const auto full = get_tensor (wrapper_inputs [i]) ;
222+ const auto & full = in_tensors [i];
222223 const bool sliceable = batch > 1 && has_batch_dim (full) && full->get_shape ()[0 ] == batch;
223224 inner_set_tensor (in_ports[i], sliceable ? row_slice (full, row) : full);
224225 }
226+ };
227+
228+ // Per-row outputs stacked along axis 0. A single row has nothing to stack, so its output is
229+ // published as-is (no aggregation buffer or copy); a non-batched output collapses to the last row.
230+ std::vector<ov::SoPtr<ov::ITensor>> aggregated_outputs (wrapper_outputs.size ());
225231
232+ for (std::size_t row = 0 ; row < batch; ++row) {
233+ reset_inner_state ();
234+ bind_row (row);
226235 inner_infer ();
227236
228- // Stack each per-row output into row i of the aggregated [batch, ...] tensor.
229237 for (std::size_t i = 0 ; i < wrapper_outputs.size (); ++i) {
230238 const auto inner_out = inner_get_tensor (out_ports[i]);
239+ if (batch == 1 ) {
240+ aggregated_outputs[i] = inner_out;
241+ continue ;
242+ }
231243 if (!aggregated_outputs[i]) {
232244 ov::Shape out_shape = inner_out->get_shape ();
233245 if (!out_shape.empty ()) {
234246 out_shape[0 ] = batch;
235247 }
236248 aggregated_outputs[i] = ov::get_tensor_impl (ov::Tensor (inner_out->get_element_type (), out_shape));
237249 }
238- if (batch > 1 && has_batch_dim (inner_out)) {
239- const auto slot = row_slice (aggregated_outputs[i], row);
240- inner_out->copy_to (slot._ptr );
250+ if (has_batch_dim (inner_out)) {
251+ inner_out->copy_to (row_slice (aggregated_outputs[i], row)._ptr );
241252 } else {
242253 inner_out->copy_to (aggregated_outputs[i]._ptr );
243254 }
244255 }
245256 }
246257
247- // Publish the aggregated outputs as this request's public output tensors.
248258 for (std::size_t i = 0 ; i < wrapper_outputs.size (); ++i) {
249259 set_tensor (wrapper_outputs[i], aggregated_outputs[i]);
250260 }
251261}
252262
253- void ov::npuw::batched::InferRequest::check_tensors () const {}
263+ void ov::npuw::batched::InferRequest::check_tensors () const {
264+ // No-op: batched tensors are unrolled and validated per row by the inner request.
265+ }
254266
255267std::vector<ov::SoPtr<ov::IVariableState>> ov::npuw::batched::InferRequest::query_state () const {
256268 // The batched element resets inner state between rows and exposes no
0 commit comments