@@ -327,20 +327,48 @@ class Qwen3_5VisionModel final : public nn::Module {
327327 std::vector<Tensor> forward (const std::vector<Tensor>& inputs, const std::vector<AnyValue>& args) override {
328328 const auto & pixel_values = inputs[0 ];
329329 const auto & grid_thw = inputs[1 ];
330+ if (pixel_values.shape ().size () != 2 || grid_thw.dtype () != kInt32 || grid_thw.device () != kCPU
331+ || grid_thw.shape ().size () != 2 || grid_thw.shape ()[0 ] <= 0 || grid_thw.shape ()[1 ] != 3 ) {
332+ throw std::invalid_argument (" Qwen3.5 vision model requires patches and one grid row per image" );
333+ }
334+ const auto * grids = grid_thw.ptr <int32_t >();
335+ int32_t patch_offset = 0 ;
336+ std::vector<Tensor> image_outputs;
337+ image_outputs.reserve (grid_thw.shape ()[0 ]);
338+ for (int32_t image_index = 0 ; image_index < grid_thw.shape ()[0 ]; ++image_index) {
339+ const auto * grid = grids + image_index * 3 ;
340+ if (grid[0 ] <= 0 || grid[1 ] <= 0 || grid[2 ] <= 0 ) {
341+ throw std::invalid_argument (" Qwen3.5 vision image grids must be positive" );
342+ }
343+ const int32_t patch_count = grid[0 ] * grid[1 ] * grid[2 ];
344+ if (patch_count > pixel_values.shape ()[0 ] - patch_offset) {
345+ throw std::invalid_argument (" Qwen3.5 image grids exceed the supplied patch rows" );
346+ }
347+ auto image_pixels = pixel_values[{{patch_offset, patch_offset + patch_count}, kAll }].contiguous ();
348+ auto image_grid = grid_thw[{{image_index, image_index + 1 }, kAll }].contiguous ();
349+ image_outputs.push_back (forwardImage (image_pixels, image_grid));
350+ patch_offset += patch_count;
351+ }
352+ if (patch_offset != pixel_values.shape ()[0 ]) {
353+ throw std::invalid_argument (" Qwen3.5 supplied patch rows exceed the image grids" );
354+ }
355+ return {image_outputs.size () == 1 ? image_outputs[0 ] : nn::functional::concat (image_outputs, 0 )};
356+ }
357+
358+ private:
359+ Tensor forwardImage (const Tensor& pixel_values, const Tensor& grid_thw) {
330360 auto hidden_states = patch_embed_ (pixel_values)[0 ];
331361 auto position_embedding = makeQwen3_5VisionBilinearPositionEmbedding (pos_embed_.weight (), grid_thw, spatial_merge_size_);
332362 if (hidden_states.shape () != position_embedding.shape ()) {
333363 throw std::invalid_argument (" Qwen3.5 patch embeddings do not match the image grid" );
334364 }
335365 hidden_states = hidden_states + position_embedding;
336-
337366 auto position_ids = makeQwen3_5VisionPositionIds (grid_thw, spatial_merge_size_);
338367 auto [sin, cos] = makeQwen3_5VisionRotaryEmbedding (position_ids, hidden_size_ / num_heads_);
339368 for (auto & block : blocks_.list ()) { hidden_states = block (hidden_states, sin, cos)[0 ]; }
340- return { merger_ (hidden_states)[0 ]} ;
369+ return merger_ (hidden_states)[0 ];
341370 }
342371
343- private:
344372 int32_t hidden_size_ = 768 ;
345373 int32_t num_heads_ = 12 ;
346374 int32_t spatial_merge_size_ = 2 ;
0 commit comments