Skip to content

Commit d2ad2e2

Browse files
committed
Add convolution exercise
1 parent 5f488fd commit d2ad2e2

16 files changed

+407
-0
lines changed

source-code/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ to create it. There is some material not covered in the presentation as well.
4141
1. `pypy`: code to experiment with the Pypy interpreter.
4242
1. `file-formats`: influcence of file formats on performance.
4343
1. `performance`: general considerations about performance.
44+
1. `convolution`: wrap up exercise to apply all techniques.
4445

4546
**Note:** the GPU code in this repository was moved to
4647
[its own repository](https://github.com/gjbex/Python-on-GPUs)

source-code/convolution/README.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Convolution
2+
3+
Convolution of an image using a kernel makes a nice problem to implement
4+
using various HPC technologies. It is conceptually simple enough to be used
5+
as an exercise, yet computationally sufficiently challenging to make it
6+
interesting.
7+
8+
To get you started, you get a
9+
* [Python](python) implementations, and
10+
* [C++](cpp) implementation.
11+
12+
13+
You can try to:
14+
15+
* use numba,
16+
* use Cython,
17+
* use Swig to bind the C++ implementation,
18+
* use PyBind11 to bind the C++ implementation,
19+
* parallelize the code using Cython,
20+
* parallelize the code using multiprocessing,
21+
* parallelize and run the application on multiple nodes MPI.
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
cmake_minimum_required(VERSION 3.18)
2+
project(algorithms LANGUAGES CXX)
3+
4+
set(CMAKE_CXX_STANDARD 23)
5+
set(CMaKE_CXX_STANDARD_REQUIRED YES)
6+
set(CMAKE_CXX_EXTENSIONS NO)
7+
8+
add_compile_options(-Wall -Wextra -Wpedantic)
9+
10+
add_subdirectory(src)

source-code/convolution/cpp/README.md

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# C++ implementation
2+
3+
This is a (pretty naive) C++ implementation of 2D convolution.
4+
5+
6+
## What is it?
7+
8+
1. `src/convolution/convolution.h`: declaration of the convolution function.
9+
1. `src/convolution/convolution.cpp`: definition of the convolution function.
10+
1. `src/benchmark_convolution.cpp`: a C++ application to benchmark the
11+
implementation.
12+
1. `src/test_convolution.cpp`: a C++ application to test the implementation.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
add_subdirectory(convolution)
2+
3+
add_executable(test_convolution.exe
4+
test_convolution.cpp)
5+
target_include_directories(test_convolution.exe
6+
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/convolution)
7+
target_link_libraries(test_convolution.exe
8+
PRIVATE convolution)
9+
10+
add_executable(benchmark_convolution.exe
11+
benchmark_convolution.cpp)
12+
target_include_directories(benchmark_convolution.exe
13+
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/convolution)
14+
target_link_libraries(benchmark_convolution.exe
15+
PRIVATE convolution)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#include <convolution.h>
2+
#include <chrono>
3+
#include <iostream>
4+
#include <numeric>
5+
#include <random>
6+
7+
Matrix create_image(int rows, int cols) {
8+
std::mt19937 gen(1234);
9+
std::uniform_real_distribution<double> dis(0.0, 1.0);
10+
Matrix image(rows, cols);
11+
for (int i = 0; i < rows; ++i) {
12+
for (int j = 0; j < cols; ++j) {
13+
image(i, j) = dis(gen);
14+
}
15+
}
16+
return image;
17+
}
18+
19+
Matrix create_kernel(int rows, int cols) {
20+
Matrix kernel(rows, cols);
21+
for (int i = 0; i < rows; ++i) {
22+
for (int j = 0; j < cols; ++j) {
23+
kernel(i, j) = 1.0/(rows*cols);
24+
}
25+
}
26+
return kernel;
27+
}
28+
29+
double element_sum(const Matrix& matrix) {
30+
return std::accumulate(matrix.data(), matrix.data() + matrix.rows()*matrix.cols(), 0.0);
31+
}
32+
33+
int main(int argc, char** argv) {
34+
int rows = 1000;
35+
int cols = 1000;
36+
int kernel_rows = 7;
37+
int kernel_cols = 7;
38+
if (argc > 1) {
39+
rows = atoi(argv[1]);
40+
cols = atoi(argv[1]);
41+
}
42+
if (argc > 2) {
43+
kernel_rows = atoi(argv[2]);
44+
kernel_cols = atoi(argv[2]);
45+
}
46+
std::cout << "Image size: " << rows << "x" << cols << "\n";
47+
std::cout << "Kernel size: " << kernel_rows << "x" << kernel_cols << "\n";
48+
Matrix image = create_image(rows, cols);
49+
Matrix kernel = create_kernel(kernel_rows, kernel_cols);
50+
auto start = std::chrono::high_resolution_clock::now();
51+
auto result = convolve(image, kernel);
52+
auto end = std::chrono::high_resolution_clock::now();
53+
std::chrono::duration<double> diff = end - start;
54+
std::cout << "Time: " << diff.count() << " s\n";
55+
std::cout << "Sum: " << element_sum(result) << "\n";
56+
return 0;
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
add_library(convolution SHARED
2+
convolution.cpp matrices.cpp)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#include "convolution.h"
2+
#include <exception>
3+
4+
Matrix convolve(const Matrix& image, const Matrix& kernel) {
5+
if (kernel.rows() % 2 != 1 || kernel.cols() % 2 != 1) {
6+
throw std::invalid_argument("Only odd dimensions on kernel supported");
7+
}
8+
/*
9+
s_mid and t_mid are number of pixels between the center pixel
10+
and the edge, ie for a 5x5 filter they will be 2.
11+
12+
The output size is calculated by adding s_mid, t_mid to each
13+
side of the dimensions of the input image.
14+
*/
15+
auto s_mid {kernel.rows()/2};
16+
auto t_mid {kernel.cols()/2};
17+
auto x_max {image.rows() + 2*s_mid};
18+
auto y_max {image.cols() + 2*t_mid};
19+
// Allocate result image.
20+
Matrix new_image(x_max, y_max);
21+
// Do convolution
22+
for (int x = 0; x < x_max; ++x) {
23+
for (int y = 0; y < y_max; ++y) {
24+
// Calculate pixel value for h at (x,y). Sum one component
25+
// for each pixel (s, t) of the filter kernel.
26+
auto s_from {std::max(s_mid - x, -s_mid)};
27+
auto s_to {std::min((x_max - x) - s_mid, s_mid + 1)};
28+
auto t_from {std::max(t_mid - y, -t_mid)};
29+
auto t_to {std::min((y_max - y) - t_mid, t_mid + 1)};
30+
double value {0.0};
31+
for (int s = s_from; s < s_to; ++s) {
32+
for (int t = t_from; t < t_to; ++t) {
33+
auto v {x - s_mid + s};
34+
auto w {y - t_mid + t};
35+
value += kernel(s_mid - s, t_mid - t)*image(v, w);
36+
}
37+
}
38+
new_image(x, y) = value;
39+
}
40+
}
41+
return new_image;
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#ifndef CONVOLUTION_HDR
2+
#define CONVOLUTION_HDR
3+
4+
#include "matrices.h"
5+
6+
/**
7+
* @brief Compute the convolution of an image with a kernel.
8+
* @param image The image to convolve. This is a 2D matrix with m rows and n columns.
9+
* @param kernel The kernel to convolve with. This is a 2D matrix with k rows and l columns,
10+
* where k and l ard odd integers.
11+
* @return The result of the convolution. This is a 2D matrix with m + k - 1 rows
12+
* and n + l -1 columns.
13+
*/
14+
Matrix convolve(const Matrix& image, const Matrix& kernel);
15+
16+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#include "matrices.h"
2+
3+
Matrix::Matrix(const Matrix& other) :
4+
rows_(other.rows_), cols_(other.cols_),
5+
data_(new double[rows_ * cols_]) {
6+
for (int i = 0; i < rows_ * cols_; ++i) {
7+
data_[i] = other.data_[i];
8+
}
9+
}
10+
11+
Matrix& Matrix::operator=(const Matrix& other) {
12+
if (this != &other) {
13+
rows_ = other.rows_;
14+
cols_ = other.cols_;
15+
data_.reset(new double[rows_ * cols_]);
16+
for (int i = 0; i < rows_ * cols_; ++i) {
17+
data_[i] = other.data_[i];
18+
}
19+
}
20+
return *this;
21+
}
22+
Matrix::Matrix(Matrix&& other) noexcept :
23+
rows_{other.rows_}, cols_{other.cols_},
24+
data_{std::move(other.data_)} {
25+
other.rows_ = 0;
26+
other.cols_ = 0;
27+
}
28+
29+
Matrix& Matrix::operator=(Matrix&& other) noexcept {
30+
if (&other != this) {
31+
rows_ = other.rows_;
32+
cols_ = other.cols_;
33+
data_ = std::move(other.data_);
34+
35+
other.rows_ = 0;
36+
other.cols_ = 0;
37+
}
38+
return *this;
39+
}
40+
41+
std::ostream& operator<<(std::ostream& os, const Matrix& m) {
42+
for (int i = 0; i < m.rows_; ++i) {
43+
for (int j = 0; j < m.cols_; ++j) {
44+
os << m(i, j) << " ";
45+
}
46+
os << std::endl;
47+
}
48+
return os;
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#ifndef MATRICES_HDR
2+
#define MATRICES_HDR
3+
4+
#include <iostream>
5+
#include <memory>
6+
7+
8+
struct Matrix {
9+
private:
10+
int rows_;
11+
int cols_;
12+
std::unique_ptr<double[]> data_;
13+
public:
14+
Matrix(int rows, int cols) :
15+
rows_(rows), cols_(cols), data_(new double[rows * cols]) {}
16+
// copy constructor & assignment operator
17+
Matrix(const Matrix& other);
18+
Matrix& operator=(const Matrix& other);
19+
// move constructor & assignment operator
20+
Matrix(Matrix&& other) noexcept;
21+
Matrix& operator=(Matrix&& other) noexcept;
22+
// matrix indexing by row and column
23+
double& operator()(int i, int j) { return data_[i * cols_ + j]; }
24+
double operator()(int i, int j) const { return data_[i * cols_ + j]; }
25+
// getters for number of rows and columns
26+
int rows() const { return rows_; }
27+
int cols() const { return cols_; }
28+
// accessors for the data
29+
double* data() { return data_.get(); }
30+
const double* data() const { return data_.get(); }
31+
// destructor
32+
~Matrix() = default;
33+
// textual representation of the matrix
34+
friend std::ostream& operator<<(std::ostream& os, const Matrix& m);
35+
};
36+
37+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#include <convolution.h>
2+
#include <iostream>
3+
4+
int main() {
5+
// Create a 10x10 image
6+
Matrix image(10, 10);
7+
for (int i = 0; i < image.rows(); ++i) {
8+
for (int j = 0; j < image.cols(); ++j) {
9+
image(i, j) = i*image.cols() + j;
10+
}
11+
}
12+
13+
// Print the image
14+
std::cout << image << std::endl;
15+
16+
// Create a 3x3 kernel
17+
Matrix kernel(3, 3);
18+
for (int i = 0; i < kernel.rows(); ++i) {
19+
for (int j = 0; j < kernel.cols(); ++j) {
20+
kernel(i, j) = 1.0/(kernel.rows()*kernel.cols());
21+
}
22+
}
23+
24+
// Print the kernel
25+
std::cout << kernel << std::endl;
26+
27+
// Create a convolution object
28+
auto new_image = convolve(image, kernel);
29+
30+
// Print the result
31+
std::cout << new_image << std::endl;
32+
33+
return 0;
34+
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Python implementation
2+
3+
This is a (pretty naive) Python implementation of 2D convolution.
4+
5+
6+
## What is it?
7+
8+
1. `convolution.py`: module that contains the implementation.
9+
1. `benchmark_convolution.py`: a Python script to benchmark the implementation.
10+
1. `test_convolution.py`: a Python script to test the implementation.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env python
2+
3+
import argparse
4+
import convolution
5+
import numpy as np
6+
import timeit
7+
8+
def create_image(rows, cols):
9+
return np.random.uniform(0.0, 1.0, size=(rows, cols))
10+
11+
def create_kernel(rows, cols):
12+
return np.ones((rows, cols))/(rows*cols)
13+
14+
def element_sum(matrix):
15+
return np.sum(matrix)
16+
17+
def main():
18+
parser = argparse.ArgumentParser()
19+
parser.add_argument("--rows", type=int, default=1_000)
20+
parser.add_argument("--cols", type=int, default=1_000)
21+
parser.add_argument("--kernel_rows", type=int, default=7)
22+
parser.add_argument("--kernel_cols", type=int, default=7)
23+
args = parser.parse_args()
24+
25+
print(f'Image size: {args.rows}x{args.cols}')
26+
print(f'Kernel size: {args.kernel_rows}x{args.kernel_cols}')
27+
28+
image = create_image(args.rows, args.cols)
29+
kernel = create_kernel(args.kernel_rows, args.kernel_cols)
30+
31+
32+
print(f'Time: {timeit.timeit(lambda: convolution.convolve(image, kernel), number=1)} s')
33+
print(f'Sum: {element_sum(convolution.convolve(image, kernel))}')
34+
35+
if __name__ == "__main__":
36+
main()

0 commit comments

Comments
 (0)