#include "neuroflow/tensor.hpp" #include "neuroflow/tensor_ops.hpp" #include #include #include #include #include #include #include #ifdef USE_CBLAS extern "C" { #include } #define cblas_sgemm scipy_cblas_sgemm #endif #ifdef USE_CUDA #include "cuda_context.hpp" #endif #ifdef __AVX2__ #include #define HAS_AVX2 1 #endif #ifdef __ARM_NEON #include #define HAS_NEON 1 #endif #ifdef _OPENMP #include #endif #include #include #include #include #include namespace neuroflow { namespace { constexpr size_t OMP_MIN_ITER = 1024; #ifdef _OPENMP using omp_idx_t = long long; #else using omp_idx_t = size_t; #endif #ifndef _OPENMP class ThreadPool { public: explicit ThreadPool(size_t n = std::thread::hardware_concurrency()) { n = std::max(n, size_t(1)); for (size_t i = 0; i < n; ++i) { workers_.emplace_back([this] { while (true) { std::function task; { std::unique_lock lock(mutex_); cv_.wait(lock, [this] { return stop_ || !tasks_.empty(); }); if (stop_ && tasks_.empty()) return; task = std::move(tasks_.front()); tasks_.pop(); } task(); } }); } } ~ThreadPool() { { std::lock_guard lock(mutex_); stop_ = true; } cv_.notify_all(); for (auto& w : workers_) { if (w.joinable()) w.join(); } } void submit(std::function task) { { std::lock_guard lock(mutex_); tasks_.push(std::move(task)); } cv_.notify_one(); } void wait_all() { while (true) { std::lock_guard lock(mutex_); if (tasks_.empty()) break; } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } private: std::vector workers_; std::queue> tasks_; std::mutex mutex_; std::condition_variable cv_; bool stop_ = false; }; #endif } void TensorOps::gemm(const Tensor& A, const Tensor& B, Tensor& C, bool transA, bool transB, float alpha, float beta) { size_t K_A = transA ? A.shape_[0] : A.shape_[1]; size_t K_B = transB ? B.shape_[1] : B.shape_[0]; if (K_A != K_B) { throw std::runtime_error("GEMM dimension mismatch: K dimensions don't match"); } if (A.dtype_ != QuantType::FP32 || B.dtype_ != QuantType::FP32) { quantized_gemm(A, B, C); return; } size_t M = transA ? A.shape_[1] : A.shape_[0]; size_t K = transA ? A.shape_[0] : A.shape_[1]; size_t N = transB ? B.shape_[0] : B.shape_[1]; #ifdef USE_CUDA if (CudaContext::instance().is_available() && A.is_on_gpu() && B.is_on_gpu()) { C.to_gpu(); // 确保输出张量在GPU上有内存分配 const float* d_a = A.as_gpu_fp32(); const float* d_b = B.as_gpu_fp32(); float* d_c = C.as_gpu_fp32(); int lda = static_cast(transA ? M : K); int ldb = static_cast(transB ? K : N); int ldc = static_cast(N); CudaContext::instance().sgemm_rowmajor(transA, transB, static_cast(M), static_cast(N), static_cast(K), alpha, d_a, lda, d_b, ldb, beta, d_c, ldc); C.gpu_dirty_ = true; return; } #endif auto a = reinterpret_cast(A.data_.get()); auto b = reinterpret_cast(B.data_.get()); auto c = reinterpret_cast(C.data_.get()); #ifdef USE_CBLAS CBLAS_TRANSPOSE ctransA = transA ? CblasTrans : CblasNoTrans; CBLAS_TRANSPOSE ctransB = transB ? CblasTrans : CblasNoTrans; CBLAS_ORDER order = CblasRowMajor; size_t lda_cblas = transA ? M : K; size_t ldb_cblas = transB ? K : N; cblas_sgemm(order, ctransA, ctransB, M, N, K, alpha, a, lda_cblas, b, ldb_cblas, beta, c, N); #elif HAS_AVX2 gemm_avx2(a, b, c, M, K, N, transA, transB, alpha, beta); #elif HAS_NEON gemm_neon(a, b, c, M, K, N, transA, transB, alpha, beta); #else gemm_scalar(a, b, c, M, K, N, transA, transB, alpha, beta); #endif } #ifdef HAS_AVX2 void TensorOps::gemm_avx2(const float* a, const float* b, float* c, size_t M, size_t K, size_t N, bool transA, bool transB, float alpha, float beta) { if (beta != 0.0f) { for (size_t i = 0; i < M * N; ++i) c[i] *= beta; } else { memset(c, 0, M * N * sizeof(float)); } const size_t BLOCK = 8; for (size_t i = 0; i < M; ++i) { for (size_t j = 0; j < N; j += BLOCK) { __m256 sum = _mm256_setzero_ps(); for (size_t k = 0; k < K; ++k) { float av = transA ? a[k * M + i] : a[i * K + k]; __m256 bv; if (transB) { float bvals[BLOCK]; for (size_t jj = 0; jj < BLOCK && j + jj < N; ++jj) { bvals[jj] = b[(j + jj) * K + k]; } bv = _mm256_loadu_ps(bvals); } else { bv = _mm256_loadu_ps(&b[k * N + j]); } sum = _mm256_fmadd_ps(_mm256_set1_ps(av), bv, sum); } sum = _mm256_mul_ps(sum, _mm256_set1_ps(alpha)); if (j + BLOCK <= N) { __m256 cv = _mm256_loadu_ps(&c[i * N + j]); cv = _mm256_add_ps(cv, sum); _mm256_storeu_ps(&c[i * N + j], cv); } else { float tmp[BLOCK]; _mm256_storeu_ps(tmp, sum); for (size_t jj = 0; j + jj < N; ++jj) { c[i * N + j + jj] += tmp[jj]; } } } } } #endif #ifdef HAS_NEON void TensorOps::gemm_neon(const float* a, const float* b, float* c, size_t M, size_t K, size_t N, bool transA, bool transB, float alpha, float beta) { if (beta != 0.0f) { for (size_t i = 0; i < M * N; ++i) c[i] *= beta; } else { memset(c, 0, M * N * sizeof(float)); } const size_t BLOCK = 4; for (size_t i = 0; i < M; ++i) { for (size_t j = 0; j < N; j += BLOCK) { float32x4_t sum = vdupq_n_f32(0.0f); for (size_t k = 0; k < K; ++k) { float av = transA ? a[k * M + i] : a[i * K + k]; float32x4_t bv; if (transB) { float bvals[BLOCK]; for (size_t jj = 0; jj < BLOCK && j + jj < N; ++jj) { bvals[jj] = b[(j + jj) * K + k]; } bv = vld1q_f32(bvals); } else { bv = vld1q_f32(&b[k * N + j]); } sum = vmlaq_n_f32(sum, bv, av); } sum = vmulq_n_f32(sum, alpha); float32x4_t cv = vld1q_f32(&c[i * N + j]); cv = vaddq_f32(cv, sum); vst1q_f32(&c[i * N + j], cv); } } } #endif void TensorOps::gemm_scalar(const float* a, const float* b, float* c, size_t M, size_t K, size_t N, bool transA, bool transB, float alpha, float beta) { if (beta != 0.0f) { for (size_t i = 0; i < M * N; ++i) c[i] *= beta; } else { memset(c, 0, M * N * sizeof(float)); } for (size_t i = 0; i < M; ++i) { for (size_t j = 0; j < N; ++j) { float sum = 0.0f; for (size_t k = 0; k < K; ++k) { float av = transA ? a[k * M + i] : a[i * K + k]; float bv = transB ? b[j * K + k] : b[k * N + j]; sum += av * bv; } c[i * N + j] += alpha * sum; } } } void TensorOps::quantized_gemm(const Tensor& A, const Tensor& B, Tensor& C) { if (A.dtype_ == QuantType::INT8 && B.dtype_ == QuantType::FP32) { const int8_t* a = reinterpret_cast(A.data_.get()); const float* b = reinterpret_cast(B.data_.get()); float* c = reinterpret_cast(C.data_.get()); size_t M = A.shape_[0]; size_t K = A.shape_[1]; size_t N = B.shape_[1]; memset(c, 0, M * N * sizeof(float)); for (size_t i = 0; i < M; ++i) { for (size_t j = 0; j < N; ++j) { int32_t sum = 0; for (size_t k = 0; k < K; ++k) { sum += a[i * K + k] * static_cast(b[k * N + j]); } c[i * N + j] = static_cast(sum); } } } } void TensorOps::layer_norm(Tensor& x, Tensor& weight, Tensor& bias, float eps) { float* data = x.as_fp32(); float* w = weight.as_fp32(); float* b = bias.as_fp32(); size_t batch = x.shape_[0]; size_t dim = x.shape_[1]; for (size_t i = 0; i < batch; ++i) { float mean = 0.0f; float var = 0.0f; for (size_t j = 0; j < dim; ++j) { mean += data[i * dim + j]; } mean /= dim; for (size_t j = 0; j < dim; ++j) { float diff = data[i * dim + j] - mean; var += diff * diff; } var /= dim; float std = std::sqrt(var + eps); for (size_t j = 0; j < dim; ++j) { data[i * dim + j] = w[j] * (data[i * dim + j] - mean) / std + b[j]; } } } void TensorOps::gelu(Tensor& x) { float* data = x.as_fp32(); size_t n = x.numel(); const float SQRT_2_PI = 0.7978845608f; const float COEFF = 0.044715f; for (size_t i = 0; i < n; ++i) { float v = data[i]; float inner = SQRT_2_PI * (v + COEFF * v * v * v); data[i] = 0.5f * v * (1.0f + std::tanh(inner)); } } void TensorOps::softmax(Tensor& x, int axis) { float* data = x.as_fp32(); if (axis == -1) axis = static_cast(x.shape_.size()) - 1; size_t outer = 1; for (int i = 0; i < axis; ++i) outer *= x.shape_[i]; size_t inner_dim = x.shape_[axis]; size_t inner = 1; for (size_t i = axis + 1; i < x.shape_.size(); ++i) inner *= x.shape_[i]; for (size_t o = 0; o < outer; ++o) { for (size_t i = 0; i < inner; ++i) { float* row = data + o * inner_dim * inner + i; float max_val = row[0]; for (size_t d = 1; d < inner_dim; ++d) { max_val = std::max(max_val, row[d * inner]); } float sum = 0.0f; for (size_t d = 0; d < inner_dim; ++d) { row[d * inner] = std::exp(row[d * inner] - max_val); sum += row[d * inner]; } for (size_t d = 0; d < inner_dim; ++d) { row[d * inner] /= sum; } } } } void TensorOps::dropout(Tensor& x, float rate, bool training) { if (!training || rate == 0.0f) return; float* data = x.as_fp32(); size_t n = x.numel(); float scale = 1.0f / (1.0f - rate); size_t addr_bits = reinterpret_cast(data); std::mt19937 rng(static_cast( std::hash{}(std::this_thread::get_id()) ^ addr_bits ^ n)); std::uniform_real_distribution dist(0.0f, 1.0f); for (size_t i = 0; i < n; ++i) { if (dist(rng) < rate) { data[i] = 0.0f; } else { data[i] *= scale; } } } void TensorOps::quantize_int8(const Tensor& src, Tensor& dst, Tensor& scale) { if (src.dtype_ != QuantType::FP32) throw std::runtime_error("Source must be FP32"); const float* sdata = reinterpret_cast(src.data_.get()); int8_t* ddata = reinterpret_cast(dst.data_.get()); float* scales = reinterpret_cast(scale.data_.get()); size_t rows = src.shape_[0]; size_t cols = src.shape_[1]; for (size_t i = 0; i < rows; ++i) { float max_abs = 0.0f; for (size_t j = 0; j < cols; ++j) { max_abs = std::max(max_abs, std::abs(sdata[i * cols + j])); } scales[i] = max_abs / 127.0f; if (scales[i] < 1e-8f) scales[i] = 1e-8f; for (size_t j = 0; j < cols; ++j) { float v = sdata[i * cols + j] / scales[i]; ddata[i * cols + j] = static_cast(std::round(std::clamp(v, -128.0f, 127.0f))); } } } void TensorOps::dequantize_int8(const Tensor& src, Tensor& dst, const Tensor& scale) { const int8_t* sdata = reinterpret_cast(src.data_.get()); float* ddata = reinterpret_cast(dst.data_.get()); const float* scales = reinterpret_cast(scale.data_.get()); size_t rows = src.shape_[0]; size_t cols = src.shape_[1]; for (size_t i = 0; i < rows; ++i) { for (size_t j = 0; j < cols; ++j) { ddata[i * cols + j] = static_cast(sdata[i * cols + j]) * scales[i]; } } } void TensorOps::add(Tensor& a, const Tensor& b) { if (a.dtype_ != QuantType::FP32 || b.dtype_ != QuantType::FP32) throw std::runtime_error("add: only FP32 supported"); float* da = a.as_fp32(); const float* db = reinterpret_cast(b.data_.get()); size_t n = a.numel(); for (size_t i = 0; i < n; ++i) da[i] += db[i]; } void TensorOps::mul(Tensor& a, float scalar) { float* data = a.as_fp32(); size_t n = a.numel(); for (size_t i = 0; i < n; ++i) data[i] *= scalar; } void TensorOps::parallel_gemm(const Tensor& A, const Tensor& B, Tensor& C, bool transA, bool transB, float alpha, float beta, size_t num_threads) { if (A.dtype_ != QuantType::FP32 || B.dtype_ != QuantType::FP32) { quantized_gemm(A, B, C); return; } size_t M = transA ? A.shape_[1] : A.shape_[0]; size_t K = transA ? A.shape_[0] : A.shape_[1]; size_t N = transB ? B.shape_[0] : B.shape_[1]; auto* a = reinterpret_cast(A.data_.get()); auto* b = reinterpret_cast(B.data_.get()); auto* c = reinterpret_cast(C.data_.get()); #ifdef USE_CBLAS CBLAS_TRANSPOSE ctransA = transA ? CblasTrans : CblasNoTrans; CBLAS_TRANSPOSE ctransB = transB ? CblasTrans : CblasNoTrans; size_t lda = transA ? M : K; size_t ldb = transB ? K : N; cblas_sgemm(CblasRowMajor, ctransA, ctransB, M, N, K, alpha, a, lda, b, ldb, beta, c, N); #else if (beta != 0.0f) { for (size_t i = 0; i < M * N; ++i) c[i] *= beta; } else { memset(c, 0, M * N * sizeof(float)); } if (num_threads <= 1 || M < 8) { gemm_scalar(a, b, c, M, K, N, transA, transB, alpha, beta); return; } #ifdef _OPENMP size_t use_threads = std::min(static_cast(omp_get_max_threads()), num_threads); #pragma omp parallel for schedule(static) num_threads(use_threads) if(M >= 64) for (omp_idx_t i = 0; i < static_cast(M); ++i) { for (size_t j = 0; j < N; ++j) { float sum = 0.0f; for (size_t k = 0; k < K; ++k) { float av = transA ? a[k * M + i] : a[i * K + k]; float bv = transB ? b[j * K + k] : b[k * N + j]; sum += av * bv; } c[i * N + j] += alpha * sum; } } #else ThreadPool pool; size_t rows_per_thread = (M + num_threads - 1) / num_threads; for (size_t t = 0; t < num_threads; ++t) { size_t row_start = t * rows_per_thread; size_t row_end = std::min(row_start + rows_per_thread, M); if (row_start >= M) break; pool.submit([=]() { for (size_t i = row_start; i < row_end; ++i) { for (size_t j = 0; j < N; ++j) { float sum = 0.0f; for (size_t k = 0; k < K; ++k) { float av = transA ? a[k * M + i] : a[i * K + k]; float bv = transB ? b[j * K + k] : b[k * N + j]; sum += av * bv; } c[i * N + j] += alpha * sum; } } }); } pool.wait_all(); #endif #endif } Tensor TensorOps::matmul(const Tensor& A, const Tensor& B) { if (A.shape_.size() != 2 || B.shape_.size() != 2) throw std::runtime_error("matmul requires 2D tensors"); if (A.shape_[1] != B.shape_[0]) throw std::runtime_error("matmul dimension mismatch"); Tensor C({A.shape_[0], B.shape_[1]}, A.dtype_); gemm(A, B, C, false, false); return C; } Tensor TensorOps::elementwise_add(const Tensor& A, const Tensor& B) { if (A.shape_ != B.shape_) throw std::runtime_error("shape mismatch in add"); Tensor C = A.clone(); add(C, B); return C; } Tensor TensorOps::elementwise_mul(const Tensor& A, const Tensor& B) { if (A.shape_ != B.shape_) throw std::runtime_error("shape mismatch in mul"); Tensor C(A.shape_, A.dtype_); const float* a = A.as_fp32(); const float* b = B.as_fp32(); float* c = C.as_fp32(); size_t n = A.numel(); #pragma omp parallel for schedule(static) if(n >= OMP_MIN_ITER) for (omp_idx_t i = 0; i < static_cast(n); ++i) c[i] = a[i] * b[i]; return C; } Tensor TensorOps::scalar_mul(const Tensor& A, float s) { Tensor C = A.clone(); mul(C, s); return C; } Tensor TensorOps::elementwise_sub(const Tensor& A, const Tensor& B) { if (A.shape_ != B.shape_) throw std::runtime_error("shape mismatch in sub"); Tensor C(A.shape_, A.dtype_); const float* a = A.as_fp32(); const float* b = B.as_fp32(); float* c = C.as_fp32(); size_t n = A.numel(); #pragma omp parallel for schedule(static) if(n >= OMP_MIN_ITER) for (omp_idx_t i = 0; i < static_cast(n); ++i) c[i] = a[i] - b[i]; return C; } Tensor TensorOps::transpose2d(const Tensor& A) { if (A.shape_.size() != 2) throw std::runtime_error("transpose2d requires 2D tensor"); size_t rows = A.shape_[0], cols = A.shape_[1]; Tensor C({cols, rows}, A.dtype_); const float* a = A.as_fp32(); float* c = C.as_fp32(); for (size_t i = 0; i < rows; ++i) for (size_t j = 0; j < cols; ++j) c[j * rows + i] = a[i * cols + j]; return C; } Tensor TensorOps::broadcast_add(const Tensor& A, const Tensor& B) { Tensor C = A.clone(); float* c = C.as_fp32(); const float* b = B.as_fp32(); if (B.shape_.size() == 1 && A.shape_.size() == 2 && B.shape_[0] == A.shape_[1]) { size_t batch = A.shape_[0], dim = A.shape_[1]; size_t n = batch * dim; #pragma omp parallel for schedule(static) if(n >= OMP_MIN_ITER) for (omp_idx_t i = 0; i < static_cast(batch); ++i) for (size_t j = 0; j < dim; ++j) c[i * dim + j] += b[j]; } else if (B.shape_ == A.shape_) { size_t n = A.numel(); #pragma omp parallel for schedule(static) if(n >= OMP_MIN_ITER) for (omp_idx_t i = 0; i < static_cast(n); ++i) c[i] += b[i]; } else { throw std::runtime_error("broadcast_add: incompatible shapes"); } return C; } void TensorOps::fill(Tensor& A, float value) { float* a = A.as_fp32(); size_t n = A.numel(); if (n >= 1024) { memset(a, 0, n * sizeof(float)); if (value != 0.0f) { #pragma omp parallel for schedule(static) if(n >= OMP_MIN_ITER) for (omp_idx_t i = 0; i < static_cast(n); ++i) a[i] = value; } } else { for (size_t i = 0; i < n; ++i) a[i] = value; } } void TensorOps::copy_data(Tensor& dst, const Tensor& src) { if (dst.numel() != src.numel()) throw std::runtime_error("copy_data: size mismatch"); memcpy(dst.as_fp32(), src.as_fp32(), dst.numel() * sizeof(float)); } float TensorOps::dot(const Tensor& A, const Tensor& B) { if (A.shape_ != B.shape_) throw std::runtime_error("dot: shape mismatch"); const float* a = A.as_fp32(); const float* b = B.as_fp32(); size_t n = A.numel(); float sum = 0.0f; #pragma omp parallel for schedule(static) reduction(+:sum) if(n >= OMP_MIN_ITER) for (omp_idx_t i = 0; i < static_cast(n); ++i) sum += a[i] * b[i]; return sum; } float TensorOps::norm2(const Tensor& A) { return std::sqrt(dot(A, A)); } Tensor TensorOps::reduce_sum(const Tensor& A, int axis) { if (axis == -1) axis = static_cast(A.shape_.size()) - 1; if (A.shape_.size() != 2 || axis != 1) throw std::runtime_error("reduce_sum: only 2D axis=1 supported"); size_t batch = A.shape_[0], dim = A.shape_[1]; Tensor C({batch, 1}, QuantType::FP32); const float* a = A.as_fp32(); float* c = C.as_fp32(); for (size_t i = 0; i < batch; ++i) { float sum = 0.0f; for (size_t j = 0; j < dim; ++j) sum += a[i * dim + j]; c[i] = sum; } return C; } Tensor TensorOps::slice(const Tensor& A, size_t dim, size_t start, size_t end) { if (dim >= A.shape_.size()) throw std::runtime_error("slice: dim out of range"); if (end > A.shape_[dim]) end = A.shape_[dim]; auto new_shape = A.shape_; new_shape[dim] = end - start; Tensor C(new_shape, A.dtype_); if (A.shape_.size() == 2 && dim == 1) { size_t rows = A.shape_[0]; const float* a = A.as_fp32(); float* c = C.as_fp32(); for (size_t i = 0; i < rows; ++i) memcpy(c + i * (end - start), a + i * A.shape_[1] + start, (end - start) * sizeof(float)); } else if (A.shape_.size() == 2 && dim == 0) { const float* a = A.as_fp32(); float* c = C.as_fp32(); memcpy(c, a + start * A.shape_[1], (end - start) * A.shape_[1] * sizeof(float)); } else { throw std::runtime_error("slice: only 2D supported"); } return C; } Tensor TensorOps::pad1d(const Tensor& A, size_t left, size_t right, float value) { if (A.shape_.size() != 2) throw std::runtime_error("pad1d requires 2D tensor"); size_t batch = A.shape_[0], dim = A.shape_[1]; size_t new_dim = left + dim + right; Tensor C({batch, new_dim}, QuantType::FP32); float* c = C.as_fp32(); const float* a = A.as_fp32(); for (size_t i = 0; i < batch; ++i) { for (size_t j = 0; j < left; ++j) c[i * new_dim + j] = value; memcpy(c + i * new_dim + left, a + i * dim, dim * sizeof(float)); for (size_t j = left + dim; j < new_dim; ++j) c[i * new_dim + j] = value; } return C; } void TensorOps::apply_inplace(Tensor& A, std::function fn) { float* a = A.as_fp32(); size_t n = A.numel(); #pragma omp parallel for schedule(static) if(n >= OMP_MIN_ITER) for (omp_idx_t i = 0; i < static_cast(n); ++i) a[i] = fn(a[i]); } Tensor TensorOps::apply(const Tensor& A, std::function fn) { Tensor C = A.clone(); apply_inplace(C, fn); return C; } Tensor TensorOps::relu(const Tensor& A) { Tensor C = A.clone(); float* c = C.as_fp32(); size_t n = A.numel(); #pragma omp parallel for schedule(static) if(n >= OMP_MIN_ITER) for (omp_idx_t i = 0; i < static_cast(n); ++i) c[i] = std::max(0.0f, c[i]); return C; } Tensor TensorOps::sigmoid(const Tensor& A) { Tensor C = A.clone(); float* c = C.as_fp32(); size_t n = A.numel(); #pragma omp parallel for schedule(static) if(n >= OMP_MIN_ITER) for (omp_idx_t i = 0; i < static_cast(n); ++i) c[i] = 1.0f / (1.0f + std::exp(-c[i])); return C; } Tensor TensorOps::tanh_act(const Tensor& A) { Tensor C = A.clone(); float* c = C.as_fp32(); size_t n = A.numel(); #pragma omp parallel for schedule(static) if(n >= OMP_MIN_ITER) for (omp_idx_t i = 0; i < static_cast(n); ++i) c[i] = std::tanh(c[i]); return C; } Tensor TensorOps::log(const Tensor& A) { Tensor C = A.clone(); float* c = C.as_fp32(); size_t n = A.numel(); #pragma omp parallel for schedule(static) if(n >= OMP_MIN_ITER) for (omp_idx_t i = 0; i < static_cast(n); ++i) c[i] = std::log(std::max(c[i], 1e-7f)); return C; } Tensor TensorOps::exp(const Tensor& A) { Tensor C = A.clone(); float* c = C.as_fp32(); size_t n = A.numel(); #pragma omp parallel for schedule(static) if(n >= OMP_MIN_ITER) for (omp_idx_t i = 0; i < static_cast(n); ++i) c[i] = std::exp(c[i]); return C; } void TensorOps::clip_inplace(Tensor& A, float min_val, float max_val) { float* a = A.as_fp32(); size_t n = A.numel(); #pragma omp parallel for schedule(static) if(n >= OMP_MIN_ITER) for (omp_idx_t i = 0; i < static_cast(n); ++i) a[i] = std::clamp(a[i], min_val, max_val); } Tensor TensorOps::concat(const std::vector& tensors, size_t axis) { if (tensors.empty()) throw std::runtime_error("Empty tensors"); auto new_shape = tensors[0].shape_; size_t total = 0; for (auto& t : tensors) { total += t.shape_[axis]; } new_shape[axis] = total; Tensor result(new_shape); float* out = result.as_fp32(); size_t offset = 0; size_t outer = 1; for (size_t i = 0; i < axis; ++i) outer *= tensors[0].shape_[i]; size_t inner = 1; for (size_t i = axis + 1; i < tensors[0].shape_.size(); ++i) inner *= tensors[0].shape_[i]; for (auto& t : tensors) { const float* in = reinterpret_cast(t.data_.get()); size_t dim = t.shape_[axis]; for (size_t o = 0; o < outer; ++o) { memcpy(out + o * total * inner + offset * inner, in + o * dim * inner, dim * inner * sizeof(float)); } offset += dim; } return result; } } // namespace neuroflow