inline void HybridConvPerChannel(
    const ConvParams& params, float* scaling_factors_ptr,
    const RuntimeShape& input_shape, const int8_t* input_data,
    const RuntimeShape& filter_shape, const int8_t* filter_data,
    const RuntimeShape& bias_shape, const float* bias_data,
    const RuntimeShape& output_shape, float* output_data,
    const RuntimeShape& im2col_shape, int8_t* im2col_data,
    const float* per_channel_scale, int32_t* input_offset,
    const RuntimeShape& scratch_shape, int32_t* scratch, int32_t* row_sums,
    bool* compute_row_sums, CpuBackendContext* cpu_backend_context) {
  ruy::profiler::ScopeLabel label("ConvHybridPerChannel");
  const int stride_width = params.stride_width;
  const int stride_height = params.stride_height;
  const int dilation_width_factor = params.dilation_width_factor;
  const int dilation_height_factor = params.dilation_height_factor;
  TFLITE_DCHECK_EQ(input_shape.DimensionsCount(), 4);
  TFLITE_DCHECK_EQ(filter_shape.DimensionsCount(), 4);
  TFLITE_DCHECK_EQ(output_shape.DimensionsCount(), 4);

  const int8* gemm_input_data = nullptr;
  const RuntimeShape* gemm_input_shape = nullptr;
  const int filter_width = filter_shape.Dims(2);
  const int filter_height = filter_shape.Dims(1);
  const bool need_dilated_im2col =
      dilation_width_factor != 1 || dilation_height_factor != 1;
  const bool need_im2col = stride_width != 1 || stride_height != 1 ||
                           filter_width != 1 || filter_height != 1;

  const int batch_size = input_shape.Dims(0);

  if (need_dilated_im2col) {
    TFLITE_DCHECK(im2col_data);
    optimized_ops::DilatedIm2col(params, input_shape, input_data, filter_shape,
                                 output_shape, im2col_data, input_offset,
                                 batch_size);
    gemm_input_data = im2col_data;
    gemm_input_shape = &im2col_shape;
  } else if (need_im2col) {
    Im2col(params, filter_height, filter_width, input_offset, batch_size,
           input_shape, input_data, im2col_shape, im2col_data);
    gemm_input_data = im2col_data;
    gemm_input_shape = &im2col_shape;
  } else {
    TFLITE_DCHECK(!im2col_data);
    gemm_input_data = input_data;
    gemm_input_shape = &input_shape;
  }

  const int filter_rows = filter_shape.Dims(0);
  const int filter_cols = FlatSizeSkipDim(filter_shape, 0);

  const int gemm_input_rows = gemm_input_shape->Dims(3);
  const int gemm_input_cols = FlatSizeSkipDim(*gemm_input_shape, 3);
  const int output_rows = output_shape.Dims(3);
  const int output_cols =
      output_shape.Dims(0) * output_shape.Dims(1) * output_shape.Dims(2);

  TFLITE_DCHECK_EQ(output_rows, filter_rows);
  TFLITE_DCHECK_EQ(output_cols, gemm_input_cols);
  TFLITE_DCHECK_EQ(filter_cols, gemm_input_rows);
  TFLITE_DCHECK_EQ(bias_shape.FlatSize(), output_rows);
  TFLITE_DCHECK_EQ(scratch_shape.FlatSize(), output_shape.FlatSize());
  if (!compute_row_sums || *compute_row_sums) {
    tensor_utils::ReductionSumVector(filter_data, row_sums, filter_rows,
                                     filter_cols);
    if (compute_row_sums) {
      *compute_row_sums = false;
    }
  }

  cpu_backend_gemm::MatrixParams<int8> lhs_params;
  lhs_params.rows = filter_rows;
  lhs_params.cols = filter_cols;
  lhs_params.order = cpu_backend_gemm::Order::kRowMajor;

  cpu_backend_gemm::MatrixParams<int8> rhs_params;
  rhs_params.order = cpu_backend_gemm::Order::kColMajor;
  rhs_params.rows = gemm_input_rows;
  rhs_params.cols = gemm_input_cols;

  cpu_backend_gemm::MatrixParams<int32> dst_params;
  dst_params.order = cpu_backend_gemm::Order::kColMajor;
  dst_params.rows = output_rows;
  dst_params.cols = output_cols;

  // TODO(b/149003801): Use hybrid gemm once supported in Ruy.
  cpu_backend_gemm::GemmParams<int32_t, int32_t> gemm_params;
  cpu_backend_gemm::Gemm(lhs_params, filter_data, rhs_params, gemm_input_data,
                         dst_params, scratch, gemm_params, cpu_backend_context);

  MatrixMap<float> out_mat(output_data, filter_rows, output_cols);
  MatrixMap<int32_t> in_mat(scratch, filter_rows, output_cols);
  VectorMap<const float> bias_data_vec(bias_data, filter_rows, 1);
  VectorMap<int32_t> row_sums_vec(row_sums, filter_rows, 1);
  VectorMap<const float> per_channel_scale_vec(per_channel_scale, filter_rows,
                                               1);
  const int cols_per_batch = output_cols / batch_size;
  for (int c = 0; c < output_cols; c++) {
    const int b = c / cols_per_batch;
    const float input_scale = scaling_factors_ptr[b];
    const int32_t zero_point = input_offset[b];
    out_mat.col(c) =
        (((in_mat.col(c) - (row_sums_vec * zero_point))
              .cast<float>()
              .cwiseProduct((per_channel_scale_vec * input_scale))) +
         bias_data_vec)
            .cwiseMin(params.float_activation_max)
            .cwiseMax(params.float_activation_min);
  }
}