Skip to content

src/modules/nn/conv2d.cc

Namespaces

Name
tenzr
tenzr::nn

Source code

// Copyright 2026 The Tenzr Project Authors
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "tenzr/modules/nn/conv2d.h"

#include <algorithm>
#include <cmath>
#include <vector>

#include "tenzr/common/error.h"
#include "tenzr/frontend/op_registry.h"
#include "tenzr/modules/random/random.h"
#include "tenzr/plax/plax.h"
#include "tenzr/txpr/custom_vjp.h"

namespace tenzr::nn {

  Conv2D::Conv2D(int64_t in_channels, int64_t out_channels, std::pair<int64_t, int64_t> kernel_size, std::pair<int64_t, int64_t> stride,
                 std::pair<int64_t, int64_t> padding, bool use_bias, DType dtype, std::optional<uint64_t> seed)
      : in_channels_(in_channels), out_channels_(out_channels), kernel_size_(kernel_size), stride_(stride), padding_(padding), use_bias_(use_bias) {
    TENZR_CHECK(in_channels > 0, "in_channels must be positive");
    TENZR_CHECK(out_channels > 0, "out_channels must be positive");
    TENZR_CHECK(kernel_size.first > 0 && kernel_size.second > 0, "kernel dimensions must be positive");
    TENZR_CHECK(stride.first > 0 && stride.second > 0, "stride dimensions must be positive");
    TENZR_CHECK(padding.first >= 0 && padding.second >= 0, "padding dimensions must be non-negative");

    int64_t fan_in = in_channels * kernel_size.first * kernel_size.second;
    if (seed.has_value()) {
      random::Generator gen(*seed);
      weight_ = RegisterParameter("weight", random::KaimingUniform({out_channels_, in_channels_, kernel_size_.first, kernel_size_.second}, fan_in, dtype, gen));
    } else {
      weight_ = RegisterParameter(
          "weight", random::KaimingUniform({out_channels_, in_channels_, kernel_size_.first, kernel_size_.second}, fan_in, dtype, random::DefaultGenerator()));
    }

    if (use_bias_) {
      bias_ = RegisterParameter("bias", Tensor::Zeros({out_channels_}, dtype));
    }
  }

  Tensor Conv2D::Forward(const Tensor& input) {
    TENZR_CHECK(input.IsDefined(), "Input tensor is uninitialized");
    TENZR_CHECK(input.shape().Rank() == 4, "Conv2D input must have rank 4 [B, C, H, W]");
    TENZR_CHECK(input.shape().Dim(1) == in_channels_,
                "Input channels (" + std::to_string(input.shape().Dim(1)) + ") does not match layer channels (" + std::to_string(in_channels_) + ")");

    int64_t b = input.shape().Dim(0);
    int64_t h = input.shape().Dim(2);
    int64_t w = input.shape().Dim(3);

    int64_t out_h = (h + 2 * padding_.first - kernel_size_.first) / stride_.first + 1;
    int64_t out_w = (w + 2 * padding_.second - kernel_size_.second) / stride_.second + 1;
    TENZR_CHECK(out_h > 0 && out_w > 0, "Output spatial dimensions must be positive");

    Shape out_shape({b, out_channels_, out_h, out_w});

    // Traceable / differentiable path: lower to plax::conv2d (im2col + matmul),
    // which composes differentiable primitives so jit/grad work.
    if (txpr::IsGradTracingActive() || input.IsSymbolic() || !input.HasData() || !weight_.HasData()) {
      Tensor out = plax::Conv2d(input, weight_, stride_, padding_);
      if (use_bias_) {
        out = out + bias_.Reshape(Shape({1, out_channels_, 1, 1}));
      }
      return out;
    }

    if (input.HasData() && weight_.HasData()) {
      auto in_vec = input.ToVectorAs<double>();
      auto w_vec = weight_.ToVectorAs<double>();
      std::vector<double> b_vec;
      if (use_bias_ && bias_.HasData()) {
        b_vec = bias_.ToVectorAs<double>();
      }

      std::vector<double> out_vec(static_cast<size_t>(out_shape.NumElements()), 0.0);

      int64_t in_c = in_channels_;
      int64_t out_c = out_channels_;
      int64_t kh = kernel_size_.first;
      int64_t kw = kernel_size_.second;
      int64_t sh = stride_.first;
      int64_t sw = stride_.second;
      int64_t ph = padding_.first;
      int64_t pw = padding_.second;

      for (int64_t bi = 0; bi < b; ++bi) {
        for (int64_t oc = 0; oc < out_c; ++oc) {
          double bias_val = b_vec.empty() ? 0.0 : b_vec[static_cast<size_t>(oc)];
          for (int64_t oh = 0; oh < out_h; ++oh) {
            int64_t ih_start = oh * sh - ph;
            for (int64_t ow = 0; ow < out_w; ++ow) {
              int64_t iw_start = ow * sw - pw;
              double sum = bias_val;

              for (int64_t ic = 0; ic < in_c; ++ic) {
                for (int64_t ki = 0; ki < kh; ++ki) {
                  int64_t ih = ih_start + ki;
                  if (ih < 0 || ih >= h) {
                    continue;
                  }
                  for (int64_t kj = 0; kj < kw; ++kj) {
                    int64_t iw = iw_start + kj;
                    if (iw < 0 || iw >= w) {
                      continue;
                    }

                    size_t in_idx = static_cast<size_t>(((bi * in_c + ic) * h + ih) * w + iw);
                    size_t w_idx = static_cast<size_t>(((oc * in_c + ic) * kh + ki) * kw + kj);
                    sum += in_vec[in_idx] * w_vec[w_idx];
                  }
                }
              }

              size_t out_idx = static_cast<size_t>(((bi * out_c + oc) * out_h + oh) * out_w + ow);
              out_vec[out_idx] = sum;
            }
          }
        }
      }

      return Tensor::FromVector<double>(out_vec, out_shape).Astype(input.Dtype());
    }

    return Tensor::CustomOp("conv2d", {input, weight_});
  }
}  // namespace tenzr::nn