diff --git a/docs/src/notebook-html/jacobi_2D.html b/docs/src/notebook-html/jacobi_2D.html deleted file mode 100644 index 0615cbc..0000000 --- a/docs/src/notebook-html/jacobi_2D.html +++ /dev/null @@ -1,16611 +0,0 @@ - - - - - -jacobi_2D - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
-
-
- - -
-
-
- - -
- -
- - - - - -
- -
-
- - -
- -
- - - - - -
- -
-
- - -
- -
- - - - - -
- -
-
- - -
- -
- - - - - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
- - - - - -
- -
-
- - -
- -
-
- - -
- -
- - - - - -
- -
-
- - -
- -
- - - - - -
- -
-
- - -
- -
- - - - - - - - - diff --git a/docs/src/notebook-html/jacobi_method.html b/docs/src/notebook-html/jacobi_method.html deleted file mode 100644 index ecad42b..0000000 --- a/docs/src/notebook-html/jacobi_method.html +++ /dev/null @@ -1,16332 +0,0 @@ - - - - - -jacobi_method - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
- - - - - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
- - - - - -
- -
-
- - -
- -
- - - - - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
- - - - - -
- -
-
- - -
- -
- - - - - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
- - - - - - - - - diff --git a/docs/src/notebook-html/julia_async.html b/docs/src/notebook-html/julia_async.html deleted file mode 100644 index 16a2ce4..0000000 --- a/docs/src/notebook-html/julia_async.html +++ /dev/null @@ -1,16211 +0,0 @@ - - - - - -julia_async - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
- - - - - - - - - diff --git a/docs/src/notebook-html/julia_basics.html b/docs/src/notebook-html/julia_basics.html deleted file mode 100644 index 86800ea..0000000 --- a/docs/src/notebook-html/julia_basics.html +++ /dev/null @@ -1,17402 +0,0 @@ - - - - - -julia_basics - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
- - - - - - - - - diff --git a/docs/src/notebook-html/julia_distributed.html b/docs/src/notebook-html/julia_distributed.html deleted file mode 100644 index 7d1cbd9..0000000 --- a/docs/src/notebook-html/julia_distributed.html +++ /dev/null @@ -1,16937 +0,0 @@ - - - - - -julia_distributed - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
- - - - - - - - - diff --git a/docs/src/notebook-html/julia_intro.html b/docs/src/notebook-html/julia_intro.html deleted file mode 100644 index 1118dca..0000000 --- a/docs/src/notebook-html/julia_intro.html +++ /dev/null @@ -1,16142 +0,0 @@ - - - - - -julia_intro - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
- - - - - - - - - diff --git a/docs/src/notebook-html/julia_jacobi.html b/docs/src/notebook-html/julia_jacobi.html deleted file mode 100644 index 242d236..0000000 --- a/docs/src/notebook-html/julia_jacobi.html +++ /dev/null @@ -1,15919 +0,0 @@ - - - - - -julia_jacobi - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
- - - - - -
- -
-
- - -
- -
- - - - - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
- - - - - - - - - diff --git a/docs/src/notebook-html/julia_tutorial.html b/docs/src/notebook-html/julia_tutorial.html deleted file mode 100644 index 98dff6a..0000000 --- a/docs/src/notebook-html/julia_tutorial.html +++ /dev/null @@ -1,15559 +0,0 @@ - - - - - -julia_tutorial - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
- - - - - - - - - diff --git a/docs/src/notebook-html/matrix_matrix.html b/docs/src/notebook-html/matrix_matrix.html deleted file mode 100644 index 50a4d26..0000000 --- a/docs/src/notebook-html/matrix_matrix.html +++ /dev/null @@ -1,16584 +0,0 @@ - - - - - -matrix_matrix - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
- - - - - - - - - diff --git a/docs/src/notebook-html/mpi_tutorial.html b/docs/src/notebook-html/mpi_tutorial.html deleted file mode 100644 index fb8c4c1..0000000 --- a/docs/src/notebook-html/mpi_tutorial.html +++ /dev/null @@ -1,15559 +0,0 @@ - - - - - -mpi_tutorial - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
- - - - - - - - - diff --git a/docs/src/notebook-html/notebook-hello.html b/docs/src/notebook-html/notebook-hello.html deleted file mode 100644 index 6b4583a..0000000 --- a/docs/src/notebook-html/notebook-hello.html +++ /dev/null @@ -1,15345 +0,0 @@ - - - - - -notebook-hello - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
- -
- - - - - - - - - diff --git a/docs/src/notebook-html/solutions.html b/docs/src/notebook-html/solutions.html deleted file mode 100644 index f158397..0000000 --- a/docs/src/notebook-html/solutions.html +++ /dev/null @@ -1,15376 +0,0 @@ - - - - - -solutions - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-
- - -
- -
- - - - - - - - - diff --git a/docs/src/notebook-html/tsp.html b/docs/src/notebook-html/tsp.html deleted file mode 100644 index 33a5b88..0000000 --- a/docs/src/notebook-html/tsp.html +++ /dev/null @@ -1,15456 +0,0 @@ - - - - - -tsp - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
- -
-
- - -
- -
- - - - - -
- -
-
- - -
- -
-
- - -
- -
- - - - - -
- -
-
- - -
- -
- - - - - -
- -
-
- - -
- -
- - - - - -
- -
-
- - -
- -
-
- - -
- -
- - - - - - - - - diff --git a/docs/src/notebooks/jacobi_2D.ipynb b/docs/src/notebooks/jacobi_2D.ipynb deleted file mode 100644 index dd2358f..0000000 --- a/docs/src/notebooks/jacobi_2D.ipynb +++ /dev/null @@ -1,2126 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "25dd0180", - "metadata": {}, - "source": [ - "# Successive Over-relaxation (SOR)\n", - "\n", - "In this section, we want to examine another parallel algorithm called Successive Over-relaxation (SOR). \n", - "\n", - "The SOR algorithm is an iterative method used to solve Laplace equations. The underlying data structure of the SOR algorithm is a two-dimensional grid, whose elements are updated iteratively through some weighted function that considers the old value as well as the values of neighbouring cells. \n", - "\n", - "![Grid](images/SOR_grid_painted.png)\n", - "\n", - "This algorithm is applied, for instance, in physics to simulate the climate or temperature of some object.\n", - "\n", - "\n", - "Climate Simulation | Temperature of a metal plate\n", - ":------:|:----:\n", - "![](images/SOR_climate_simulation.png) | ![](images/SOR_temperature_metal_plate.png)\n", - "\n", - "\n", - "## Sequential algorithm\n", - "\n", - "The sequential SOR algorithm is as follows (where `f` is some update function and `N`,`M` are the grid sizes):\n", - "\n", - "```julia\n", - "grid = zeros(N, M)\n", - "grid_new = zeros(N, M)\n", - "for step in 1:NSTEPS\n", - " for i in 2:N #update grid\n", - " for j in 2:M\n", - " grid_new[i,j] = f(grid[i,j], grid[i-1,j], grid[i+1,j], grid[i,j-1], grid[i,j+1])\n", - " end\n", - " end\n", - "end\n", - "```\n", - "\n", - "## Diffusion equation on grid \n", - "\n", - "Consider the diffusion of a chemical substance on a two-dimensional grid. The concentration of the chemical is given as $c(x,y)$, a function of the coordinates $x$ and $y$. We will consider a square grid with $0 \\leq x,y \\leq 1$ and the boundary conditions $c(x,y=1) = 1$ and $c(x,y=0) = 0$. That is, the concentration at the top of the grid is always 1 and the concentration at the very bottom is always 0. Furthermore, in the x-direction we will assume periodic boundary conditions, i.e. $c(x=0, y) = c(x=1,y)$. \n", - "\n", - "![diffusion](images/SOR_time_dep_diffusion.png)\n", - "\n", - "We will take the initial condition $c(x,y) = 0$ for $0 \\leq x \\leq 1, 0 \\leq y < 1$.\n", - "\n", - "The stable state of the diffusion, that is, when the concentraion does not change anymore, can be described by the Laplace equation\n", - "\n", - "$$\n", - "\\nabla^2 c = 0.\n", - "$$\n", - "\n", - "Numerically, we can approximate the solution with the Jacobi iteration \n", - "\n", - "$$\n", - "\\frac{1}{4}(c^k_{i,j+1} + c^k_{i,j-1} + c^k_{i+1,j} + c^k_{i-1,j}) = c^{k+1}_{i,j}.\n", - "$$\n", - "\n", - "The superscript $k$ denotes the $k$-th iteration. The algorithm stepwise updates the cells of the grid, until a steady state is reached. To determine the end of the algorithm, we use the stopping condition \n", - "\n", - "$$\n", - "\\max_{i,j} \\lvert c^{k+1}_{i,j} - c^{k}_{i,j} \\rvert < \\epsilon.\n", - "$$\n", - "\n", - "That is, we stop when all changes to cell values are smaller than some small number, say $\\epsilon = 10^{-5}$.\n", - "\n", - "Furthermore, for this set of initial and boundary conditions, there exists an analytical solution for the stable state, namely \n", - "$$\n", - "c(x,y) = y.\n", - "$$\n", - "That is, the concentration profile is the identity function of the y-coordinate. " - ] - }, - { - "cell_type": "markdown", - "id": "84167bc6", - "metadata": {}, - "source": [ - "## Serial Program" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "b082bce8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "SOR! (generic function with 1 method)" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Update grid until threshold reached\n", - "function update_grid!(new_grid, grid, M, N)\n", - " @assert grid[1,:] == grid[M-1,:]\n", - " @assert grid[M,:] == grid[2,:] \n", - " @assert grid[:,1] == fill(0,M) == new_grid[:,1]\n", - " @assert grid[:,N] == fill(1,M) == new_grid[:,N]\n", - " # Remove ghost cells\n", - " g_left = grid[1:M-2, 2:N-1]\n", - " g_right = grid[3:M, 2:N-1]\n", - " g_up = grid[2:M-1, 3:N]\n", - " g_down = grid[2:M-1, 1:N-2]\n", - " # Jacobi iteration\n", - " new_grid[2:M-1,2:N-1] = 0.25 * (g_up + g_down + g_left + g_right)\n", - " # Update ghost cells\n", - " new_grid[1,:] = new_grid[M-1,:]\n", - " new_grid[M,:] = new_grid[2,:]\n", - " return new_grid\n", - "end\n", - "\n", - "function SOR!(new_grid, grid, ϵ, M, N)\n", - " grid_old = true\n", - " while true\n", - " if grid_old\n", - " update_grid!(new_grid, grid, M, N)\n", - " else \n", - " update_grid!(grid, new_grid, M, N)\n", - " end\n", - " # Flip boolean value\n", - " grid_old = !grid_old\n", - " diffs = abs.(new_grid[2:M-1, :] - grid[2:M-1, :])\n", - " if maximum(diffs) < ϵ\n", - " break\n", - " end\n", - " end \n", - "end\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "e280865a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[32m\u001b[1mTest Passed\u001b[22m\u001b[39m" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "using Test, Plots\n", - "N = 100\n", - "M = N + 2\n", - "ϵ = 10^-5\n", - "\n", - "# initialize grid\n", - "grid = zeros(M,N)\n", - "grid[:,N] .= 1\n", - "new_grid = zeros(M,N)\n", - "new_grid[:,N] .= 1\n", - "\n", - "update_grid!(new_grid, grid, M, N)\n", - "\n", - "# Test if first iteration successful\n", - "@test all(new_grid[:,N-1] .≈ fill(0.25, M))\n", - "@test all(new_grid[:,N-2] .≈ fill(0,M))\n", - "@test all(new_grid[:,N] == fill(1,M))\n", - "@test all(new_grid[:,1] == fill(0,M))" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "f2f49b93", - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "data": { - "image/png": "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uiu+oruyfEX+/v734vMfffTRlJQUSfo/uwl9fX2VQpMmTX7//Xel/Pvvv9erV69BgwbadsBwL5aiAgCgiV27dk2cOPH48eNeXl6TJ0+WZfnjjz/W9hEMOAAANVdUVFT//v3btGnj7e1tNBq/+eYbzR/BiBAAUNNlZWXl5eW1bNnSYDBo/uEEQgCATWOhFADApjFHWCPk5ORs27YtJSXFzc0tLi6ubdu2ll/t2rVrx44dfn5+kyZN0nytVG107dq1zZs3d+3aNSwsTKlJTU1dvXp1UVHRiBEjHnroIX27p7vz58+vW7cuOzs7LCzssccec3BwEEJcunQpKSmpoKBg6NChlr3JtkmSpA0bNhw9etTNzS0hIaFNmzZK/R9//PHPf/7z0qVL3bt3j4uL07eT919RUdGJEydOnTrVsGHD/v37W+pNJtPq1atPnz6t/HWyrGM/e/bsp59+ajKZRo8eHR4erlOvNcOIsEaYNWvWli1bfHx8CgoKOnfu/O9//1up//TTTx977LFmzZqdOnWqa9euxcXF+vazJpg+ffrzzz///fffKz9eunSpY8eOt27datCgwSOPPHLo0CF9u6evffv2RUREXLt2LSgo6MCBA8ruq+zs7E6dOl29etXf33/AgAG7du3Su5t6mj59+uuvv96mTRtJkjp16vTzzz8LIWRZjoqK2rdvX0hIyAsvvLBkyRK9u3m/LVy4cPz48cuWLVu+fHnZ+kmTJq1atapFixYrV66cMmWKUnn+/PnOnTvLslyvXr1u3bodP35cjy5rSkYNUFhYaCnPmjVr6NChsixLktSmTZsNGzYo5Q4dOnz22We6dbFm2Lp1a3x8fO/evd99912l5uWXXx4xYoRSnj9/flxcnH6905nZbA4JCUlKSipXv2jRon79+inld955p3fv3ve7ZzWJr6/v7t27lXJCQsLcuXNlWd61a1dgYGBJSYksy/v27fP19S0uLtaxk/efsnF5+fLlffv2tVReuHDByckpKytLluWsrCwnJ6f09HRZlp955pmJEycqbWbNmjV27Fg9uqwlRoQ1grOzs6VcVFTk6uoqhLh69eovv/wSFRUlhDAYDFFRUXv37tWtizVAXl7erFmz3n777bKV+/bt69u3r1KOjo625a/o7Nmz6enpPXv2/OCDD1auXJmXl6fU79u3Lzo6WilHR0fv37+/3M5lmxIaGnrs2DEhxK1bt3755Rclwb53795HHnlESSN37dr15s2bv/76q84dvb8qPFjnwIEDbdu2VTa2+/r6hoWFHTx4UNTF/9MRCGuWkydPJiUlzZw5UwiRkZHh6Ojo4eGh/MrPz+/KlSu69k5nzzzzzLPPPhsQEFC2MiMjw8fHRyn7+vrm5uYWFRXp0Tv9paWlOTs7x8fHZ2dn7969u127dtnZ2eK2r6i0tPT69eu69lRPycnJK1asaNGiRWBgYP/+/RMSEoQQmZmZlq/IaDR6e3vb+P/XFGW/FlHmP0Hl/kZlZmbKtXz3AYGwBrl48eKgQYPeeuutBx98UAjh4OBgNpst/3gvLS215YOgduzYceHChUmTJpWrt7e3N5lMStlkMhkMBps9l85oNObn5y9fvnzOnDlr164NDg5WDuAo9xUJIRwdHfXsqK4mT54cGRm5devWjRs3btiwYfPmzUIIe3t7s/n/3wdXWlpqy1+RxZ2+lnJ/o+zt7e/F3r77yUb/k1EDpaenP/LII88//7zlv/WNGjUym81ZWVnK+X6XL1++Rwf91QrJyckXLlzo1KmTEOLs2bO//fZbRkbG66+/HhAQYPnH++XLl/38/Gw2ECpj5dDQUOXH0NDQCxcuKPVlvyIXFxdLmsHWXLx4cfv27bm5uR4eHm3atPnrX/+6cuXKuLi4gICAEydOKG2Ki4uzs7MbNWqkb1drgoCAgMuXL1t+vHz5svK1lPsbVS5JUxsxIqwRsrKy+vbtO2XKlCeffNJS6enp2b17940bNwohCgsLt23bNmjQIP36qLP58+dv3Ljxo48++uijj1q3bj1q1CjlXwyxsbEbN25UMjMbNmyIjY3Vu6e6CQ0Nbdmy5eHDh4UQsiz/8MMPSlCMjY398ssvlX/a2/hX5OnpaTQaz58/r/x4/vx5b29vIURsbOy3336bk5MjhNi6dWvTpk1btWqlZ0drhj59+ly4cCElJUUIkZKSkp6e3rt3byFEbGzshg0blDZ15G+Uzot1IMuyLI8cOdLZ2Tniv8aMGaPU7927t0GDBuPGjevQocOAAQOUlV0ou2o0Pz+/Xbt2jzzySHx8vL+///nz5/Xtm762bt3q6+s7adKkrl27RkZGKquRb9261blz565du44cOdLHxyclJUXvbupp7ty5fn5+U6ZMGThwoK+vr+XbePzxx1u3bv344497e3tv3rxZ307efzt37oyIiGjcuHH9+vUjIiISExOV+kWLFgUEBEyYMCEgIGDx4sVK5fXr11u1ahUTEzNkyJAmTZooS0lrNY5YqxHOnz9vWeMnhHBxcbHs871y5YqynrtXr15cmaY4e/ash4eH5ZaWoqKi3bt3FxYWRkVF2WzSzyI9Pf3gwYP+/v7dunWz3HFWUlLy/fff5+fn9+nTh2MZzpw5c/LkyXr16nXt2rXszXYHDhxIT0+PjIwMCgrSr3f6yM3NtVx1JIRo0KCB5Us4ceKEsqG+Xbt2lga3bt369ttvzWZzVFSU5rcD3n8EQgCATWOEAQCwaQRCAIBNIxACAGwagRAAYNMIhAAAm0YgBADYNAIhAMCmEQgBADaNQAgAsGkEQgCATSMQAgBs2v8DTyGyCcgrPFYAAAAASUVORK5CYII=", - "image/svg+xml": [ - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/html": [ - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "N = 100\n", - "M = N + 2\n", - "ϵ = 10^-5\n", - "\n", - "# initialize grid\n", - "grid = zeros(M,N)\n", - "grid[:,N] .= 1\n", - "new_grid = zeros(M,N)\n", - "new_grid[:,N] .= 1\n", - "\n", - "SOR!(new_grid, grid, ϵ, M, N)\n", - "plt = heatmap(transpose(new_grid[2:M-1,:]))\n", - "display(plt)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "4aefd11d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[32m\u001b[1mTest Passed\u001b[22m\u001b[39m" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# analytical solution\n", - "function analytical_solution(N)\n", - " # Returns the analytical solution as a square grid of size N\n", - " grid = zeros(N,N)\n", - " for i in 2:N\n", - " grid[:,i] .= (i-1)/(N-1)\n", - " end\n", - " return grid\n", - "end\n", - "\n", - "# Test if solution is identical with analytical solution\n", - "sol = analytical_solution(N)\n", - "@test maximum(abs.(sol - new_grid[2:M-1,:])) < 0.01 * N" - ] - }, - { - "cell_type": "markdown", - "id": "f9f090f1", - "metadata": {}, - "source": [ - "## Parallel Program with MPI" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "49c4e9b4", - "metadata": {}, - "outputs": [], - "source": [ - "#] add MPIClusterManagers DelimitedFiles" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "6ec8e2eb", - "metadata": { - "code_folding": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " From worker 5:\tProc 4 gets rows 17:33 and columns 2:17\n", - " From worker 8:\tProc 7 gets rows 34:50 and columns 2:17\n", - " From worker 3:\tProc 2 gets rows 1:16 and columns 18:34\n", - " From worker 6:\tProc 5 gets rows 17:33 and columns 18:34\n", - " From worker 9:\tProc 8 gets rows 34:50 and columns 18:34\n", - " From worker 4:\tProc 3 gets rows 1:16 and columns 35:51\n", - " From worker 10:\tProc 9 gets rows 34:50 and columns 35:51\n", - " From worker 7:\tProc 6 gets rows 17:33 and columns 35:51\n", - " From worker 2:\tProc 1 gets rows 1:16 and columns 2:17\n", - " From worker 2:\tTHRESHOLD SUBCEEDED - TERMINATE SOR\n" - ] - } - ], - "source": [ - "# to import MPIManager\n", - "using MPIClusterManagers\n", - "\n", - "# need to also import Distributed to use addprocs()\n", - "using Distributed\n", - "\n", - "# specify, number of mpi workers, launch cmd, etc.\n", - "manager=MPIWorkerManager(9)\n", - "\n", - "# start mpi workers and add them as julia workers too.\n", - "addprocs(manager)\n", - "\n", - "@mpi_do manager begin\n", - " \n", - "function calculate_partition(p, N, nrows, ncols)\n", - " # Calculates the row and column indices of this processor\n", - " # Get row and column number for processor p \n", - " if mod(p,ncols) == 0\n", - " i = div(p,ncols)\n", - " else\n", - " i = floor(div(p,ncols)) + 1\n", - " end\n", - " j = p - (i-1)*ncols\n", - " # Rows\n", - " if mod(N,nrows) == 0\n", - " prows = div(N, nrows)\n", - " row_range =((i-1) * prows + 1) : (i*prows)\n", - " else\n", - " # nlower processors get the smaller partition\n", - " nlower = nrows - (mod(N,nrows)) \n", - " n_floor = floor(div(N,nrows))\n", - " if i <= nlower\n", - " row_range =((i-1) * n_floor + 1) : (i*n_floor)\n", - " else\n", - " row_range = ((i-1) * n_floor + (i - nlower)) : (i*n_floor + (i-nlower))\n", - " end\n", - " end\n", - " # Columns\n", - " if mod(N,ncols) == 0\n", - " prows = div(N, ncols)\n", - " col_range =((j-1) * prows + 1) : (j*prows)\n", - " else\n", - " nlower = ncols - (mod(N,ncols)) \n", - " n_floor = floor(div(N,ncols))\n", - " if j <= nlower\n", - " col_range =((j-1) * n_floor + 1) : (j*n_floor)\n", - " else\n", - " col_range = ((j-1) * n_floor + (j - nlower)) : (j*n_floor + (j-nlower))\n", - " end\n", - " end\n", - " # Add 1 to each column index because of ghost cells\n", - " col_range = col_range .+ 1\n", - " return row_range, col_range\n", - "end\n", - "\n", - " \n", - "function update_grid(grid)\n", - " # Returns the updated grid as M-2 x N-2 matrix where M and N are sizes of grid\n", - " M = size(grid, 1)\n", - " N = size(grid, 2) \n", - " # Remove ghost cells\n", - " g_left = grid[2:M - 1,1:N-2]\n", - " g_right = grid[2:M - 1, 3:N]\n", - " g_up = grid[1:M-2, 2:N-1]\n", - " g_down = grid[3:M, 2:N-1]\n", - " # Jacobi iteration\n", - " return 0.25 * (g_up + g_down + g_left + g_right)\n", - "end\n", - "\n", - " using MPI\n", - " comm=MPI.COMM_WORLD\n", - " id = MPI.Comm_rank(comm) + 1\n", - " \n", - "\n", - " M = 50\n", - " N = M + 2\n", - " ϵ = 10^-5 # Stopping threshold\n", - " nrows = 3 # Number of grid rows\n", - " ncols = 3 # Number of grid columns\n", - " n_procs = nrows * ncols\n", - " @assert n_procs == MPI.Comm_size(comm)\n", - " max_diffs = ones(n_procs) # Differences between iterations\n", - " max_diff_buf = MPI.UBuffer(max_diffs,1) # Buffer to store maximum differences\n", - " \n", - "\n", - " # initialize grid\n", - " if id == 1\n", - " grid_a = zeros(M,N)\n", - " grid_a[1,:] .= 1\n", - " grid_b = zeros(M,N)\n", - " grid_b[1,:] .= 1\n", - " else\n", - " grid_a = nothing\n", - " grid_b = nothing\n", - " end\n", - " \n", - " # Broadcast matrix to other processors\n", - " grid_a = MPI.bcast(grid_a, 0, comm)\n", - " grid_b = MPI.bcast(grid_b, 0, comm)\n", - "\n", - " # Determine if processor is in top or bottom row of grid\n", - " top_pos = id <= ncols\n", - " bottom_pos = id > ((nrows-1) * ncols)\n", - " local grid_a_old = false # Grid a is the source grid for the first update\n", - "\n", - " # Get local partition\n", - " ind_rows, ind_cols = calculate_partition(id, M, nrows, ncols)\n", - " println(\"Proc $(id) gets rows $(ind_rows) and columns $(ind_cols)\")\n", - "\n", - " # Determine neighbors\n", - " n_left = id - 1\n", - " n_right = id + 1\n", - " n_down = id + ncols\n", - " n_up = id - ncols\n", - " if mod(id, ncols) == 1\n", - " # Left neighbor is last in row\n", - " n_left = id + ncols - 1\n", - " end \n", - " if mod(id, ncols) == 0\n", - " # Right neighbor is first in row\n", - " n_right = id - ncols + 1\n", - " end\n", - "\n", - " #println(\"Proc $(id) has neighbors left $(n_left) and right $(n_right) and up $(n_up) and down $(n_down)\")\n", - "\n", - " local finished = false\n", - "\n", - " #Perform SOR\n", - " while !finished\n", - " # Flip old and new grid\n", - " grid_a_old = !grid_a_old \n", - " \n", - " # Determine which grid is updated\n", - " if grid_a_old \n", - " old_grid = grid_a\n", - " new_grid = grid_b\n", - " else\n", - " old_grid = grid_b\n", - " new_grid = grid_a\n", - " end \n", - " \n", - " # send left and right columns\n", - " left_ind = first(ind_cols)\n", - " right_ind = last(ind_cols)\n", - " left_col = old_grid[ind_rows, left_ind]\n", - " right_col = old_grid[ind_rows, right_ind]\n", - " slreq = MPI.Isend(left_col, comm; dest=n_left-1)\n", - " srreq = MPI.Isend(right_col, comm; dest=n_right-1)\n", - " \n", - " # Send bottom row if not bottom\n", - " bottom_ind = last(ind_rows)\n", - " if !bottom_pos\n", - " bottom_col = old_grid[bottom_ind, ind_cols]\n", - " sbreq = MPI.Isend(bottom_col, comm; dest=n_down -1)\n", - " end\n", - "\n", - " # Send top row if not at the top \n", - " top_ind = first(ind_rows)\n", - " if !top_pos\n", - " top_row = old_grid[top_ind, ind_cols]\n", - " streq = MPI.Isend(top_row, comm; dest = n_up -1)\n", - " end\n", - "\n", - " # Receive left and right column\n", - " left_buf = Array{Float64,1}(undef, length(ind_rows))\n", - " right_buf = Array{Float64,1}(undef, length(ind_rows))\n", - " rlreq = MPI.Irecv!(left_buf, comm; source=n_left -1)\n", - " rrreq = MPI.Irecv!(right_buf, comm; source=n_right -1)\n", - "\n", - " # Receive top row if not at the top\n", - " if !top_pos\n", - " top_buf = Array{Float64,1}(undef, length(ind_cols))\n", - " rtreq = MPI.Irecv!(top_buf, comm; source=n_up -1)\n", - " end\n", - "\n", - " # Receive bottom row if not at the bottom \n", - " if !bottom_pos\n", - " bottom_buf = Array{Float64,1}(undef, length(ind_cols))\n", - " rbreq = MPI.Irecv!(bottom_buf, comm; source=n_down -1)\n", - " end\n", - "\n", - " # Wait for results\n", - " statlr = MPI.Waitall([rlreq, rrreq], MPI.Status)\n", - " old_grid[ind_rows, left_ind - 1] = left_buf\n", - " old_grid[ind_rows, right_ind + 1] = right_buf\n", - " #println(\"Proc $(id) received left $(old_grid[ind_rows, left_ind - 1]) and right $(old_grid[ind_rows, right_ind + 1])\")\n", - " \n", - " if !top_pos\n", - " statt = MPI.Wait(rtreq)\n", - " old_grid[top_ind - 1, ind_cols] = top_buf\n", - " #println(\"Proc $(id) received top $(old_grid[top_ind - 1, ind_cols])\")\n", - " end\n", - "\n", - " if !bottom_pos\n", - " statb = MPI.Wait(rbreq)\n", - " old_grid[bottom_ind + 1, ind_cols] = bottom_buf\n", - " #println(\"Proc $(id) received bottom $(old_grid[bottom_ind + 1, ind_cols])\")\n", - " end \n", - "\n", - " # Get local subgrid\n", - " if !top_pos & !bottom_pos\n", - " local_with_ghosts = old_grid[top_ind - 1 : bottom_ind + 1, left_ind - 1 : right_ind + 1]\n", - " lb_row = top_ind\n", - " ub_row = bottom_ind\n", - " elseif top_pos \n", - " local_with_ghosts = old_grid[top_ind : bottom_ind + 1, left_ind - 1 : right_ind + 1]\n", - " lb_row = top_ind + 1\n", - " ub_row = bottom_ind\n", - " elseif bottom_pos\n", - " local_with_ghosts = old_grid[top_ind - 1 : bottom_ind, left_ind - 1 : right_ind + 1]\n", - " lb_row = top_ind\n", - " ub_row = bottom_ind - 1\n", - " end\n", - "\n", - " # Perform one step of Jacobi iteration\n", - " new_grid[lb_row : ub_row, left_ind : right_ind] = update_grid(local_with_ghosts)\n", - "\n", - " # Calculate max difference\n", - " diffs = abs.(new_grid[lb_row : ub_row, left_ind : right_ind] - old_grid[lb_row : ub_row, left_ind : right_ind])\n", - " maxdiff = maximum(diffs)\n", - " \n", - " # Gather maxdiffs in processor 1 \n", - " MPI.Gather!(maxdiff, max_diff_buf, comm; root=0)\n", - "\n", - " # First processor determines if threshold is exeeded\n", - " if id == 1\n", - " if all(max_diffs .< ϵ)\n", - " finished = true\n", - " println(\"THRESHOLD SUBCEEDED - TERMINATE SOR\") \n", - " end\n", - " end\n", - " \n", - " finished = MPI.bcast(finished, 0, comm)\n", - "\n", - " if finished\n", - " # Set ghost cells to zero again so MPI.Reduce gives correct output\n", - " new_grid[ind_rows, left_ind - 1] .= 0.0\n", - " new_grid[ind_rows, right_ind + 1] .= 0.0\n", - " if !bottom_pos\n", - " new_grid[bottom_ind + 1, ind_cols] .= 0.0\n", - " end\n", - " if !top_pos\n", - " new_grid[top_ind - 1, ind_cols] .= 0.0\n", - " end\n", - " end\n", - " end\n", - "\n", - " using DelimitedFiles\n", - "\n", - " # Reduce matrix & store result\n", - " if !grid_a_old\n", - " sor_result = grid_a\n", - " else \n", - " sor_result = grid_b\n", - " end\n", - "\n", - " MPI.Reduce!(sor_result, +, comm, root = 0)\n", - " sor_result[1,:] .= 1.0\n", - " if id == 1\n", - " writedlm(\"SOR_result.txt\", sor_result)\n", - " end \n", - "\n", - " MPI.Finalize()\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "9465e78a", - "metadata": {}, - "outputs": [], - "source": [ - "rmprocs(manager);" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "d0bbbb61", - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "data": { - "image/png": "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", - "image/svg+xml": [ - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/html": [ - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "using Plots, DelimitedFiles\n", - "final_grid = readdlm(\"SOR_result.txt\")\n", - "M = size(final_grid, 1)\n", - "N = size(final_grid, 2)\n", - "plt = heatmap(final_grid[:, 2:N-1])\n", - "display(plt)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "22b071c2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[32m\u001b[1mTest Passed\u001b[22m\u001b[39m" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Test if solution is identical with analytical solution\n", - "sol = analytical_solution(M)\n", - "# Bring solution into correct form \n", - "sol = reverse(transpose(sol), dims = 1)\n", - "@test maximum(abs.(sol - final_grid[:,2:N-1])) < 0.01 * M" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3130c9fb", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Julia 1.9.1", - "language": "julia", - "name": "julia-1.9" - }, - "language_info": { - "file_extension": ".jl", - "mimetype": "application/julia", - "name": "julia", - "version": "1.9.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/src/notebooks/jacobi_2D.md b/docs/src/notebooks/jacobi_2D.md deleted file mode 100644 index dbc5463..0000000 --- a/docs/src/notebooks/jacobi_2D.md +++ /dev/null @@ -1,30 +0,0 @@ -```@meta -EditURL = "https://github.com/fverdugo/XM_40017/blob/main/docs/src/notebooks/jacobi_2D.ipynb" -``` - -```@raw html -
-
Tip
-
- -
-
-``` - -```@raw html - - -``` diff --git a/docs/src/notebooks/jacobi_method.ipynb b/docs/src/notebooks/jacobi_method.ipynb deleted file mode 100644 index 5b57582..0000000 --- a/docs/src/notebooks/jacobi_method.ipynb +++ /dev/null @@ -1,978 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "2e15ced8", - "metadata": {}, - "source": [ - "\n", - "\n", - "### Programming large-scale parallel systems" - ] - }, - { - "cell_type": "markdown", - "id": "e8549215", - "metadata": {}, - "source": [ - "# Jacobi method" - ] - }, - { - "cell_type": "markdown", - "id": "6e0ef563", - "metadata": {}, - "source": [ - "## Contents\n", - "\n", - "In this notebook, we will learn\n", - "\n", - "- How to paralleize a Jacobi method\n", - "- How the data partition can impact the performance of a distributed algorithm\n", - "- How to use latency hiding\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "1dc78750", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "jacobi_4_check (generic function with 1 method)" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "using Printf\n", - "\n", - "function answer_checker(answer,solution)\n", - " if answer == solution\n", - " \"🥳 Well done! \"\n", - " else\n", - " \"It's not correct. Keep trying! 💪\"\n", - " end |> println\n", - "end\n", - "gauss_seidel_1_check(answer) = answer_checker(answer,\"c\")\n", - "jacobi_1_check(answer) = answer_checker(answer, \"d\")\n", - "jacobi_2_check(answer) = answer_checker(answer, \"b\")\n", - "jacobi_3_check(answer) = answer_checker(answer, \"c\")\n", - "jacobi_4_check(anwswer) = answer_checker(answer, \"d\")" - ] - }, - { - "cell_type": "markdown", - "id": "d4cb59d5", - "metadata": {}, - "source": [ - "## The Jacobi method for the Laplace equation\n", - "\n", - "\n", - "The [Jacobi method](https://en.wikipedia.org/wiki/Jacobi_method) is a numerical tool to solve systems of linear algebraic equations. One of the main applications of the method is to solve boundary value problems (BVPs). I.e., given the values at the boundary (of a grid), the Jacobi method will find the interior values that fulfill a certain equation.\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "6a2bdbc6", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "93e84ff8", - "metadata": {}, - "source": [ - "When solving a Laplace equation in 1D, the Jacobi method leads to this iterative scheme. The entry $i$ of vector $u$ at iteration $t+1$ is computed as:\n", - "\n", - "$u^{t+1}_i = \\dfrac{u^t_{i-1}+u^t_{i+1}}{2}$" - ] - }, - { - "cell_type": "markdown", - "id": "e63a5792", - "metadata": {}, - "source": [ - "### Serial implementation\n", - "\n", - "The following code implements the iterative scheme above for boundary conditions -1 and 1." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "14a58308", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "jacobi (generic function with 1 method)" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "function jacobi(n,niters)\n", - " u = zeros(n+2)\n", - " u[1] = -1\n", - " u[end] = 1\n", - " u_new = copy(u)\n", - " for t in 1:niters\n", - " for i in 2:(n+1)\n", - " u_new[i] = 0.5*(u[i-1]+u[i+1])\n", - " end\n", - " u, u_new = u_new, u\n", - " end\n", - " u\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "76e1eba1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "7-element Vector{Float64}:\n", - " -1.0\n", - " 0.0\n", - " 0.0\n", - " 0.0\n", - " 0.0\n", - " 0.0\n", - " 1.0" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "jacobi(5,0)" - ] - }, - { - "cell_type": "markdown", - "id": "6e085701", - "metadata": {}, - "source": [ - "
\n", - "Note: In our version of the jacobi method, we return after a given number of iterations. Other stopping criteria are possible. For instance, iterate until the difference between u and u_new is below a tolerance. \n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "d6918c31", - "metadata": {}, - "source": [ - "\n", - "### Where can we exploit parallelism?\n", - "\n", - "Look at the two nested loops in the sequential implementation:\n", - "\n", - "```julia\n", - "for t in 1:nsteps\n", - " for i in 2:(n+1)\n", - " u_new[i] = 0.5*(u[i-1]+u[i+1])\n", - " end\n", - " u, u_new = u_new, u\n", - "end\n", - "```\n", - "\n", - "- The outer loop cannot be parallelized. The value of `u` at step `t+1` depends on the value at the previous step `t`.\n", - "- The inner loop can be parallelized.\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "798968b1", - "metadata": {}, - "source": [ - "### The Gauss-Seidel method\n", - "\n", - "The usage of `u_new` seems a bit unnecessary at first sight, right?. If we remove it, we get another method called Gauss-Seidel.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0f77b547", - "metadata": {}, - "outputs": [], - "source": [ - "function gauss_seidel(n,niters)\n", - " u = zeros(n+2)\n", - " u[1] = -1\n", - " u[end] = 1\n", - " for t in 1:niters\n", - " for i in 2:(n+1)\n", - " u[i] = 0.5*(u[i-1]+u[i+1])\n", - " end\n", - " end\n", - " u\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "0dbc5358", - "metadata": {}, - "source": [ - "Note that the final solution is the same (up to machine precision)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ca31518b", - "metadata": {}, - "outputs": [], - "source": [ - "gauss_seidel(5,1000)" - ] - }, - { - "cell_type": "markdown", - "id": "c92e9c73", - "metadata": {}, - "source": [ - "
\n", - "Question: Which of the two loops in the Gauss-Seidel method are trivially parallelizable?\n", - "
\n", - "\n", - "```julia\n", - "for t in 1:niters\n", - " for i in 2:(n+1)\n", - " u[i] = 0.5*(u[i-1]+u[i+1])\n", - " end\n", - "end\n", - "```\n", - "\n", - " a) Both of them\n", - " b) The outer, but not the inner\n", - " c) None of them\n", - " d) The inner, but not the outer\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4edad93f", - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "answer = \"x\" # replace x with a, b, c or d\n", - "gauss_seidel_1_check(answer)" - ] - }, - { - "cell_type": "markdown", - "id": "9df06442", - "metadata": {}, - "source": [ - "## Parallelization of the Jacobi method\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "97a2d7d5", - "metadata": {}, - "source": [ - "### Parallelization strategy\n", - "\n", - "- Each worker updates a consecutive section of the array `u_new` \n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "c4e2d000", - "metadata": {}, - "source": [ - "### Data dependencies\n", - "\n", - "Recall:\n", - "\n", - "`u_new[i] = 0.5*(u[i-1]+u[i+1])`\n", - "\n", - "\n", - "Thus, each process will need values from the neighboring processes to perform the update of its boundary values." - ] - }, - { - "cell_type": "markdown", - "id": "428bce86", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "1b3c8c05", - "metadata": {}, - "source": [ - "### Ghost (aka halo) cells\n", - "\n", - "A usual way of handling this type of data dependencies is using so-called ghost cells. Ghost cells represent the missing data dependencies in the data owned by each process. After importing the appropriate values from the neighbor processes one can perform the usual sequential Jacobi update locally in the processes." - ] - }, - { - "cell_type": "markdown", - "id": "cd1e3760", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "5c397005", - "metadata": {}, - "source": [ - "
\n", - "Question: Which is the communication and computation complexity in each process? N is the length of the vector and P the number of processes. \n", - "
\n", - "\n", - " a) Communication: O(P), computation: O(N/P)\n", - " b) Communication: O(1), computation: O(N)\n", - " c) Communication: O(P), computation: O(N)\n", - " d) Communication: O(1), computation: O(N/P)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3a03fc4c", - "metadata": {}, - "outputs": [], - "source": [ - "answer = \"x\" # replace x with a, b, c or d\n", - "jacobi_1_check(answer)" - ] - }, - { - "cell_type": "markdown", - "id": "8ed4129c", - "metadata": {}, - "source": [ - "## Implementation\n", - "\n", - "We consider the implementation using MPI. The programming model of MPI is generally better suited for data-parallel algorithms like this one than the task-based model provided by Distributed.jl. In any case, one can also implement it using Distributed, but it requires some extra effort to setup remote channels right for the communication between neighbor processes.\n", - "\n", - "Take a look at the implementation below and try to understand it. Note that we have used MPIClustermanagers and Distributed just to run the MPI code on the notebook. When running it on a cluster, MPIClustermanagers and Distributed are not needed.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e15082fb", - "metadata": {}, - "outputs": [], - "source": [ - "] add MPI MPIClusterManagers" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a66cbf9a", - "metadata": {}, - "outputs": [], - "source": [ - "using MPIClusterManagers \n", - "using Distributed" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e0d63c6b", - "metadata": {}, - "outputs": [], - "source": [ - "if procs() == workers()\n", - " nw = 3\n", - " manager = MPIWorkerManager(nw)\n", - " addprocs(manager)\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a0923606", - "metadata": {}, - "outputs": [], - "source": [ - "# Test cell, remove me\n", - "u = [-1, 0, 0, 0, 0, 1]\n", - "view(u, 6:6)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "68851107", - "metadata": { - "code_folding": [] - }, - "outputs": [], - "source": [ - "@mpi_do manager begin\n", - " using MPI\n", - " comm = MPI.Comm_dup(MPI.COMM_WORLD)\n", - " nw = MPI.Comm_size(comm)\n", - " iw = MPI.Comm_rank(comm)+1\n", - " function jacobi_mpi(n,niters)\n", - " if mod(n,nw) != 0\n", - " println(\"n must be a multiple of nw\")\n", - " MPI.Abort(comm,1)\n", - " end\n", - " n_own = div(n,nw)\n", - " u = zeros(n_own+2)\n", - " u[1] = -1\n", - " u[end] = 1\n", - " u_new = copy(u)\n", - " for t in 1:niters\n", - " reqs = MPI.Request[]\n", - " # Exchange cell values with neighbors\n", - " if iw != 1\n", - " neig_rank = (iw-1)-1\n", - " req = MPI.Isend(view(u,2:2),comm,dest=neig_rank,tag=0)\n", - " push!(reqs,req)\n", - " req = MPI.Irecv!(view(u,1:1),comm,source=neig_rank,tag=0)\n", - " push!(reqs,req)\n", - " end\n", - " if iw != nw\n", - " neig_rank = (iw+1)-1\n", - " s = n_own+1\n", - " r = n_own+2\n", - " req = MPI.Isend(view(u,s:s),comm,dest=neig_rank,tag=0)\n", - " push!(reqs,req)\n", - " req = MPI.Irecv!(view(u,r:r),comm,source=neig_rank,tag=0)\n", - " push!(reqs,req)\n", - " end\n", - " MPI.Waitall(reqs)\n", - " for i in 2:(n_own+1)\n", - " u_new[i] = 0.5*(u[i-1]+u[i+1])\n", - " end\n", - " u, u_new = u_new, u\n", - " end\n", - " u\n", - " @show u\n", - " # Gather results in root process\n", - " results = zeros(n+2)\n", - " results[1] = -1\n", - " results[n+2] = 1\n", - " MPI.Gather!(view(u,2:n_own+1), view(results, 2:n+1), root=0, comm)\n", - " if iw == 1\n", - " @show results\n", - " end \n", - " end\n", - " niters = 100\n", - " load = 4\n", - " n = load*nw\n", - " jacobi_mpi(n,niters)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "eff25246", - "metadata": {}, - "source": [ - "
\n", - "Question: How many messages per iteration are sent from a process away from the boundary?\n", - "
\n", - "\n", - " a) 1\n", - " b) 2\n", - " c) 3\n", - " d) 4\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "98bd9b5e", - "metadata": {}, - "outputs": [], - "source": [ - "answer = \"x\" # replace x with a, b, c or d\n", - "jacobi_2_check(answer)" - ] - }, - { - "cell_type": "markdown", - "id": "075dd6d8", - "metadata": {}, - "source": [ - "
\n", - "Question: After the end of the for-loop (line 43), ...\n", - "
\n", - "\n", - " a) each worker holds the complete solution.\n", - " b) the root process holds the solution. \n", - " c) the ghost cells contain redundant values. \n", - " d) all ghost cells contain the initial values -1 and 1. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c3b58002", - "metadata": {}, - "outputs": [], - "source": [ - "answer = \"x\" # replace x with a, b, c or d\n", - "jacobi_3_check(answer)" - ] - }, - { - "cell_type": "markdown", - "id": "4537661d", - "metadata": {}, - "source": [ - "
\n", - "Question: In line 35 of the code, we wait for all receive and send requests. Is it possible to instead wait for just the receive requests?\n", - "
\n", - "\n", - " \n", - " a) No, because the send buffer might be overwritten if we don't wait for send requests.\n", - " b) No, because MPI does not allow an asynchronous send without a Wait().\n", - " c) Yes, because each send has a matching receive, so all requests are done when the receive requests return. \n", - " d) Yes, because there are no writes to the send buffer in this iteration." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e16ea5eb", - "metadata": {}, - "outputs": [], - "source": [ - "answer = \"x\" # replace x with a, b, c or d.\n", - "jacobi_4_check(answer)" - ] - }, - { - "cell_type": "markdown", - "id": "c9aa2901", - "metadata": {}, - "source": [ - "### Latency hiding\n", - "\n", - "Note that we only need communications to update the values at the boundary of the portion owned by each process. The other values (the one in green in the figure below) can be updated without communications. This provides the opportunity of overlapping the computation of the interior values (green cells in the figure) with the communication of the ghost values. This technique is called latency hiding, since we are hiding communication latency by overlapping it with communications that we need to do anyway.\n", - "\n", - "The modification of the implementation above to include latency hiding is leaved as an exercise (see below).\n" - ] - }, - { - "attachments": { - "fig.png": { - "image/png": "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" - } - }, - "cell_type": "markdown", - "id": "5ae8701f", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "9d4de5a9", - "metadata": {}, - "source": [ - "## Extension to 2D\n", - "\n", - "\n", - "Now, let us study the method for a 2D example." - ] - }, - { - "attachments": { - "fig_jacobi_07.png": { - "image/png": "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" - } - }, - "cell_type": "markdown", - "id": "df867b3d", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "id": "b1e1a12b", - "metadata": {}, - "source": [ - "For the Laplace equation in 2D, the interior values in the computational grid (represented by a matrix $u$) are computed with this iterative scheme. The entry $(i,j)$ of matrix $u$ at iteration $t+1$ is computed as:\n", - "\n", - "\n", - "$u^{t+1}_{(i,j)} = \\dfrac{u^t_{(i-1,j)}+u^t_{(i+1,j)}+u^t_{(i,j-1)}+u^t_{(i,j+1)}}{4}$" - ] - }, - { - "cell_type": "markdown", - "id": "6f5d2895", - "metadata": {}, - "source": [ - "### Serial implementation\n", - "\n", - "The next code implements a simple example, where the boundary values are equal to 1." - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "id": "4ab59b2f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "jacobi_2d (generic function with 1 method)" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "function jacobi_2d(n,niters)\n", - " u = zeros(n+2,n+2)\n", - " u[1,:] = u[end,:] = u[:,1] = u[:,end] .= 1\n", - " u_new = copy(u)\n", - " for t in 1:niters\n", - " for j in 2:(n+1)\n", - " for i in 2:(n+1)\n", - " north = u[i,j+1]\n", - " south = u[i,j-1]\n", - " east = u[i+1,j]\n", - " west = u[i-1,j]\n", - " u_new[i,j] = 0.25*(north+south+east+west)\n", - " end\n", - " end\n", - " u, u_new = u_new, u\n", - " end\n", - " u\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "6da0aa54", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "12×12 Matrix{Float64}:\n", - " 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0\n", - " 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0\n", - " 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0\n", - " 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0\n", - " 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0\n", - " 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0\n", - " 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0\n", - " 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0\n", - " 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0\n", - " 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0\n", - " 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0\n", - " 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "u = jacobi_2d(10,0)" - ] - }, - { - "cell_type": "markdown", - "id": "45d786dd", - "metadata": {}, - "source": [ - "### Where do we can exploit parallelism?\n", - "\n", - "```julia\n", - "for t in 1:niters\n", - " for j in 2:(n+1)\n", - " for i in 2:(n+1)\n", - " north = u[i,j+1]\n", - " south = u[i,j-1]\n", - " east = u[i+1,j]\n", - " west = u[i-1,j]\n", - " u_new[i,j] = 0.25*(north+south+east+west)\n", - " end\n", - " end\n", - " u, u_new = u_new, u\n", - "end\n", - "```\n", - "\n", - "- The outer loop cannot be parallelized (like in the 1d case). \n", - "- The two inner loops can be parallelized\n" - ] - }, - { - "cell_type": "markdown", - "id": "267ecd2a", - "metadata": {}, - "source": [ - "### Parallelization strategies" - ] - }, - { - "attachments": { - "fig_jacobi_07.png": { - "image/png": "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" - } - }, - "cell_type": "markdown", - "id": "7445dc4a", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "969c42ee", - "metadata": {}, - "source": [ - "### Data dependencies" - ] - }, - { - "attachments": { - "fig_jacobi_07.png": { - "image/png": "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" - } - }, - "cell_type": "markdown", - "id": "b343f4c1", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "id": "df861921", - "metadata": {}, - "source": [] - }, - { - "attachments": { - "fig_jacobi_07.png": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABJoAAAFICAYAAAAYp5QFAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAewgAAHsIBbtB1PgAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7d13lGRVoTbutycPDMMAQ44CEiUrIAh+fMDVK6LiVYyIAS4mMHsVAxjhKgYMfB+KiYyoiBe8SBBBEUEkShIkSRAYYGBy6v7+6J7qU6F75sr5sQ8/n2etM2vvWjVrvevU7qrqt2qf7ksyEACWenmS80uHoM1jSVYtHQKgIQ5OcnLpEAAwkjGlAwAAAADw/w/jSgcAAAB4FlspyXNKhxhFX5LnVub3JZlfKMtINkgyaWg8I8njBbP0Mj3D366ek+SBgll6WSHJekPjgSR3FMzSS+ca/FuSeYWyjGT9JJOHxk1cg6sNHUkyN8n9BbMsU2fR9NEk15UIMopzkkwZGt+c5P0Fs/RyXJLthsZzk7yyYJZeXpXkPZX5/mneC8tFlfEPkpxeKsgIfpbBNxBJcmuSIwpm6eU/k+w4NJ6fwce4SfZL+8/tvyV5qlCWkfwyyfjSIVgun09yWekQHc7I4BvQJLk3ySEFs/TyySQvHhovTvKvBbP0smuSz1Xm70pyZ6EsI7kwg2+S84YPfTw77Pm/C8dp98VD3pSZjz6SJNl533/Na977wcKJ2p3x1WNz3WWXJElWXWvtfOzEZu36+sv1f8pJR32sNT/ye6dl2vQ1Cibq9vm3vT5PPf5Y6RiMbM8k55UOAdAUnUXTdUkuLhFkFIsr4yfTvHxPVMZL0rx8W3bMf5NkdoEcy+vONO8cLqqMm7gGq217E9fgJh3zyzP4KUGTuFbds8ef07w1vqAynpPm5Tu0Mu5P8/JN7phfleZ96DWQoaJpoy23zo577VM4TrsJEye1xqutvU7j8v3qtB+2xhMnTW5cvv7+/rb58174oqyx3gaF0vQ2bsKE0hEAYLm5RhMAAAAAtXCNJgAAgHoszOC1Xppk1Qxe/iFJ8oWzz8vmO76gYJxub95m48yfO2fp9LIkBxaM08svk+yUJNvuvmc+ffLZheO0+9kJx+f0r3yxetOapbKMYIUkdy+dfPbMc7PVC3YtGKfbQdtuknlzWht/rkjy6oJxevlFkl2SZOtddstnTj+ncJx2N15xeT77lte25oomAACA+jxSOkCHtv2hU6atkmmrN+s6ZIObk1sWpXnnsHUpjbHjxzfu/E1accXOm5p2/qa0TVae1rhz2DembRE2cQ22LinUxDU4ZeVpbXNb5wAAAACohaIJAAAAgFoomgAAAACohaIJAAAAgFoomgAAAACohaIJAAAAgFoomgAAAACohaIJAAAAgFoomgAAAACohaIJAAAAgFoomgAAAACohaIJAAAAgFoomgAAAACohaIJAAAAgFoomgAAAACohaIJAAAAgFoomgAAAACohaIJAAAAgFoomgAAAACohaIJAAAAgFoomgAAAACohaIJAAAAgFoomgAAAACohaIJAAAAgFoomgAAAACohaIJAAAAgFoomgAAAACohaIJAAAAgFoomgAAAACohaIJAAAAgFoomgAAAACohaIJAAAAgFqM65ifm2RxiSCjmFoZ75rkyVJBRjClMl4pzcs3sWP+YJKBEkGW01FJ/qN0iA7VNbhzmvcYr9gxblq+zjV4V5q3BidUxlskOb9UEHpaqTL+QZLvlAoygmq+LdO8n8Hqc8SENC/f+I7579K89yKtD+aOe/c7cvwH3l0yS5e5s2a1xr867Ye57Jyzy4XpYcHcOa3x3++7J69Yd+WCabotWbSobf6OnbdOX1+zPoudN3tWdbpNqRwAsDw6i6YViqRYfmPS/kt/EzU930rLvktRE9L+S3/TWINPX9PXYNPz/TOqvlZNHjqaqi/N/xlser5GvxdZtHBBFi1cUDrGiPqXLMncWU+VjjGigf7+RudLknmzZ5eOsCxN/xkG4J9csz6uAQAAAOBZqy/tW1huTDKzUJaR7J5kbJLsseOO2ev5zy8cp93J552Xex58MEmy/lpr5W2veEXhRO3+ePPN+e8rrmjNP3nooRnT11cwUbfPfmd4F8ybXvaybLLeegXTdPvyj36UeQsGP73effvts/fOOxdO1O6755yThx59NEmy7hpr5B2velXhRO2uuOGGXHLVVdWbrkyyaIS7l7JHBp8Pk+Tfk3y3YBa6zcnwt1xuTfJowSy9vDDD278WJLlqlPuWsE2SVYbGA0l+WzBLL2tkcMtqkuSDBx2UKZOb9aW1z333uxkYGHy79KL9D8hztm7WzqWfnXB85jw1uCNy0213yAtftn/hRO0u+fHpefCuO5MkK01bJa965+GFE7W777Zb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" - } - }, - "cell_type": "markdown", - "id": "6f97781b", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "id": "850b1848", - "metadata": {}, - "source": [ - "### Complexity analysis\n", - "\n", - "The complexity analysis is let as an exercise (see below).\n" - ] - }, - { - "cell_type": "markdown", - "id": "47643bf6", - "metadata": {}, - "source": [ - "## Exercises" - ] - }, - { - "cell_type": "markdown", - "id": "0edeee84", - "metadata": {}, - "source": [ - "### Exercise 1\n", - "\n", - "Transform the following parallel implementation of the 1d Jacobi method (it is copied from above) to use latency hiding (overlap between computation of interior values and communication)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "66db180d", - "metadata": {}, - "outputs": [], - "source": [ - "@mpi_do manager begin\n", - " using MPI\n", - " comm = MPI.Comm_dup(MPI.COMM_WORLD)\n", - " nw = MPI.Comm_size(comm)\n", - " iw = MPI.Comm_rank(comm)+1\n", - " function jacobi_mpi(n,niters)\n", - " if mod(n,nw) != 0\n", - " println(\"n must be a multiple of nw\")\n", - " MPI.Abort(comm,1)\n", - " end\n", - " n_own = div(n,nw)\n", - " u = zeros(n_own+2)\n", - " u[1] = -1\n", - " u[end] = 1\n", - " u_new = copy(u)\n", - " for t in 1:niters\n", - " reqs = MPI.Request[]\n", - " # Exchange cell values with neighbors\n", - " if iw != 1\n", - " neig_rank = (iw-1)-1\n", - " req = MPI.Isend(view(u,2:2),comm,dest=neig_rank,tag=0)\n", - " push!(reqs,req)\n", - " req = MPI.Irecv!(view(u,1:1),comm,source=neig_rank,tag=0)\n", - " push!(reqs,req)\n", - " end\n", - " if iw != nw\n", - " neig_rank = (iw+1)-1\n", - " s = n_own+1\n", - " r = n_own+2\n", - " req = MPI.Isend(view(u,s:s),comm,dest=neig_rank,tag=0)\n", - " push!(reqs,req)\n", - " req = MPI.Irecv!(view(u,r:r),comm,source=neig_rank,tag=0)\n", - " push!(reqs,req)\n", - " end\n", - " MPI.Waitall(reqs)\n", - " for i in 2:(n_own+1)\n", - " u_new[i] = 0.5*(u[i-1]+u[i+1])\n", - " end\n", - " u, u_new = u_new, u\n", - " end\n", - " u\n", - " @show u\n", - " # Gather results in root process\n", - " results = zeros(n+2)\n", - " results[1] = -1\n", - " results[n+2] = 1\n", - " MPI.Gather!(view(u,2:n_own+1), view(results, 2:n+1), root=0, comm)\n", - " if iw == 1\n", - " @show results\n", - " end \n", - " end\n", - " niters = 100\n", - " load = 4\n", - " n = load*nw\n", - " jacobi_mpi(n,niters)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "ebb650d0", - "metadata": {}, - "source": [ - "### Exercise 2\n", - "\n", - "Compute the complexity of the communication and computation of the three data partition strategies (1d block partition, 2d block partition, and 2d cyclic partition) when computing a single iteration of the Jacobi method in 2D. Assume that the grid is of size $N \\times N$ and the number of processes $P$ is a perfect square number, i.e. $\\sqrt{P}$ is an integer. Hint: For the complexity analysis, you can ignore the effect of the boundary conditions.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7b3d7cb3", - "metadata": {}, - "outputs": [], - "source": [ - "# TODO" - ] - }, - { - "cell_type": "markdown", - "id": "6d3430ad", - "metadata": {}, - "source": [ - "# License\n", - "\n", - "TODO: replace link to website\n", - "\n", - "This notebook is part of the course [Programming Large Scale Parallel Systems](http://localhost:8000/) at Vrije Universiteit Amsterdam and may be used under a [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3d72ff47", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Julia 1.9.1", - "language": "julia", - "name": "julia-1.9" - }, - "language_info": { - "file_extension": ".jl", - "mimetype": "application/julia", - "name": "julia", - "version": "1.9.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/src/notebooks/jacobi_method.md b/docs/src/notebooks/jacobi_method.md deleted file mode 100644 index d1d92fe..0000000 --- a/docs/src/notebooks/jacobi_method.md +++ /dev/null @@ -1,30 +0,0 @@ -```@meta -EditURL = "https://github.com/fverdugo/XM_40017/blob/main/docs/src/notebooks/jacobi_method.ipynb" -``` - -```@raw html -
-
Tip
-
- -
-
-``` - -```@raw html - - -``` diff --git a/docs/src/notebooks/julia_async.ipynb b/docs/src/notebooks/julia_async.ipynb deleted file mode 100644 index 9fab7f6..0000000 --- a/docs/src/notebooks/julia_async.ipynb +++ /dev/null @@ -1,794 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "f64b009a", - "metadata": {}, - "source": [ - "\n", - "\n", - "### Programming large-scale parallel systems\n", - "\n", - "\n", - "# Asynchronous programming in Julia\n" - ] - }, - { - "cell_type": "markdown", - "id": "bf68ad38", - "metadata": {}, - "source": [ - "## Contents\n", - "\n", - "In this notebook, we will learn the basics of asynchronous programming in Julia. In particular, we will learn about:\n", - "\n", - "- Tasks\n", - "- Channels\n", - "\n", - "Understanding these concepts is important to learn later distributed computing." - ] - }, - { - "cell_type": "markdown", - "id": "caf64254", - "metadata": {}, - "source": [ - "## Tasks\n", - "\n", - "### Creating a task\n", - "\n", - "A task is a piece of computation work that can be run asynchronously (i.e., that can be run in the background). To create a task, we first need to create a function that represents the work to be done in the task. In next cell, we generate a task that generates and sums two matrices." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fe668cb1", - "metadata": {}, - "outputs": [], - "source": [ - "function work()\n", - " println(\"Starting work\")\n", - " sleep(7)\n", - " a = rand(3,3)\n", - " b = rand(3,3)\n", - " r = a + b\n", - " println(\"Finishing work\")\n", - " r\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "67ee0328", - "metadata": {}, - "outputs": [], - "source": [ - "t = Task(work)" - ] - }, - { - "cell_type": "markdown", - "id": "e459c5c2", - "metadata": {}, - "source": [ - "### Scheduling a task\n", - "\n", - "The task has been created, but the corresponding work has not started. Note that we do not see any output from function `work` yet. To run the task we need to schedule it." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8778c199", - "metadata": {}, - "outputs": [], - "source": [ - "schedule(t)" - ] - }, - { - "cell_type": "markdown", - "id": "f1fb9283", - "metadata": {}, - "source": [ - "### Fetching the task result\n", - "\n", - "The task has been executed, but we do not see the result. To get the result we need to fetch it." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0c7b626e", - "metadata": {}, - "outputs": [], - "source": [ - "fetch(t)" - ] - }, - { - "cell_type": "markdown", - "id": "fedbbd71", - "metadata": {}, - "source": [ - "### Tasks run asynchronously\n", - "\n", - "It is important to note that tasks run asynchronously. To illustrate this let's create and schedule a new task." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4ccc996c", - "metadata": {}, - "outputs": [], - "source": [ - "t = Task(work)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "015bea27", - "metadata": {}, - "outputs": [], - "source": [ - "schedule(t)" - ] - }, - { - "cell_type": "markdown", - "id": "5ec0718e", - "metadata": {}, - "source": [ - "Note that while the task is running we can execute Julia code. To check this, execute the next two cells while the task is running." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a70fcbe8", - "metadata": {}, - "outputs": [], - "source": [ - "sin(4π)*exp(-0.1)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6def444b", - "metadata": {}, - "outputs": [], - "source": [ - "1 + 1" - ] - }, - { - "cell_type": "markdown", - "id": "d483d4d0", - "metadata": {}, - "source": [ - "How is this possible? Tasks run in the brackground and this particular task is sleeping for most of the time. Thus, it is possible to use the current Julia process for other operations while the tasks is sleeping." - ] - }, - { - "cell_type": "markdown", - "id": "910323fd", - "metadata": {}, - "source": [ - "### Tasks do not run in parallel\n", - "\n", - "It is also important to note that tasks do not run in parallel. We were able to run code while previous tasks was running because the task was idling most of the time. If the task does actual work, the current process will be busy running this task and it is likely that we cannot run other code at the same time. Let's illustrate this with an example. The following code computes an approximation of $\\pi$ using [Leibniz formula](https://en.wikipedia.org/wiki/Leibniz_formula_for_pi). The quality of the approximation increases with the value of `n`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b53ac640", - "metadata": {}, - "outputs": [], - "source": [ - "function compute_π(n)\n", - " s = 1.0\n", - " for i in 1:n\n", - " s += (isodd(i) ? -1 : 1) / (i*2+1)\n", - " end\n", - " 4*s\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "7614df94", - "metadata": {}, - "source": [ - " Call this function with a large number. Note that it will take some time." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "63f2aec3", - "metadata": {}, - "outputs": [], - "source": [ - "compute_π(4_000_000_000)" - ] - }, - { - "cell_type": "markdown", - "id": "840ff590", - "metadata": {}, - "source": [ - "Create a task that performs this computation." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f16ade6c", - "metadata": {}, - "outputs": [], - "source": [ - "fun = () -> compute_π(4_000_000_000)\n", - "t = Task(fun)" - ] - }, - { - "cell_type": "markdown", - "id": "60c2567c", - "metadata": {}, - "source": [ - "Schedule the tasks and then try to execute the 2nd cell bellow. Note that the current process will be busy running the task." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1f28a388", - "metadata": {}, - "outputs": [], - "source": [ - "schedule(t)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "52e060e7", - "metadata": {}, - "outputs": [], - "source": [ - "1+1" - ] - }, - { - "cell_type": "markdown", - "id": "25048665", - "metadata": {}, - "source": [ - "### Tasks take a function with no arguments\n", - "\n", - "This function needs to have zero arguments, but it can capture variables if needed. If we try to create a task with a function that has arguments, it will result in an error when we schedule it." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "87397749", - "metadata": {}, - "outputs": [], - "source": [ - "add(a,b) = a + b" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e99766c7", - "metadata": {}, - "outputs": [], - "source": [ - "t = Task(add)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1a785bae", - "metadata": {}, - "outputs": [], - "source": [ - "schedule(t)" - ] - }, - { - "cell_type": "markdown", - "id": "ebfce725", - "metadata": {}, - "source": [ - "If we need, we can capture variables in the function to be run by the task as shown in the next cells." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "06f54fa0", - "metadata": {}, - "outputs": [], - "source": [ - "a = rand(3,3)\n", - "b = rand(3,3);" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4c9e586e", - "metadata": {}, - "outputs": [], - "source": [ - "fun = () -> a + b\n", - "t = Task(fun)\n", - "schedule(t)" - ] - }, - { - "cell_type": "markdown", - "id": "cd829a64", - "metadata": {}, - "source": [ - "### Useful macro: `@async`\n", - "\n", - "So far, we have created tasks using low-level functions, but there are more convenient ways of creating and scheduling tasks. For instance using the `@async` macro. This macro is used to run a piece of code asynchronously. Under the hood it puts the code in an anonymous function, creates a task, and schedules it. For instance, the next cell is equivalent to previous one." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9b5152d1", - "metadata": {}, - "outputs": [], - "source": [ - "@async a + b" - ] - }, - { - "cell_type": "markdown", - "id": "ac3262d8", - "metadata": {}, - "source": [ - "### Another useful macro: `@sync`\n", - "\n", - "This macro is used to wait for all the tasks created with `@async` in a given block of code. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ef2e49f9", - "metadata": {}, - "outputs": [], - "source": [ - "@sync begin\n", - " @async sleep(3)\n", - " @async sleep(4)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "98f0d685", - "metadata": {}, - "source": [ - "## Channels\n", - "\n", - "### Sending data between tasks\n", - "\n", - "Julia provides channels as a way to send data between tasks. A channel is like a FIFO queue in which tasks can put and take values from. In next example, we create a channel and a task that puts five values into the channel. Finally, the task closes the channel." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b88d5308", - "metadata": {}, - "outputs": [], - "source": [ - "chnl = Channel{Int}()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3421c7a6", - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "@async begin\n", - " for i in 1:5\n", - " put!(chnl,i)\n", - " end\n", - " close(chnl)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "1df0508c", - "metadata": {}, - "source": [ - "By executing next cell several times, we will get the values from the channel. We are indeed communicating values from two different tasks. If we execute the cell more than 5 times, it will raise an error since the channel is closed. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "64b9436e", - "metadata": {}, - "outputs": [], - "source": [ - "take!(chnl)" - ] - }, - { - "cell_type": "markdown", - "id": "757a1b07", - "metadata": {}, - "source": [ - "### Channels are iterable\n", - "\n", - "Instead of taking values from a channel until an error occurs, we can also iterate over the channel in a for loop until the channel is closed." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2fc22dfa", - "metadata": {}, - "outputs": [], - "source": [ - "chnl = Channel{Int}()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fa62a4df", - "metadata": {}, - "outputs": [], - "source": [ - "@async begin\n", - " for i in 1:5\n", - " put!(chnl,i)\n", - " end\n", - " close(chnl)\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e511e19b", - "metadata": {}, - "outputs": [], - "source": [ - "for i in chnl\n", - " @show i\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "b1a2a557", - "metadata": {}, - "source": [ - "### Calls to `put!` and `take!` are blocking\n", - "\n", - "Note that `put!` and `take!` are blocking operations. Calling `put!` blocks the tasks until another task calls `take!` and viceversa. Thus, we need at least 2 tasks for this to work. If we call `put!` and `take!` from the same task, it will result in a dead lock. We have added a print statement to previous example. Run it again and note how `put!` blocks until we call `take!`. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f34373ca", - "metadata": {}, - "outputs": [], - "source": [ - "chnl = Channel{Int}()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bfde2ecd", - "metadata": {}, - "outputs": [], - "source": [ - "@async begin\n", - " for i in 1:5\n", - " put!(chnl,i)\n", - " println(\"I have put $i\")\n", - " end\n", - " close(chnl)\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5830659d", - "metadata": {}, - "outputs": [], - "source": [ - "take!(chnl)" - ] - }, - { - "cell_type": "markdown", - "id": "006140bd", - "metadata": {}, - "source": [ - "### Buffered channels\n", - "\n", - "We can be a bit more flexible and use a buffered channel. In this case, `put!` will block only if the channel is full and `take!` will block if the channel is empty. We repeat previous example, but with a buffered channel of size 2. Note that we can call `put!` until the channel is full. At this point, we need to wait to until we call `take!` which removes an item from the channel, making room for a new item." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dfe06b5f", - "metadata": {}, - "outputs": [], - "source": [ - "buffer_size = 2\n", - "chnl = Channel{Int}(buffer_size)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6289bc2e", - "metadata": {}, - "outputs": [], - "source": [ - "@async begin\n", - " for i in 1:5\n", - " put!(chnl,i)\n", - " println(\"I have put $i\")\n", - " end\n", - " close(chnl)\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2a87cd5f", - "metadata": {}, - "outputs": [], - "source": [ - "take!(chnl)" - ] - }, - { - "cell_type": "markdown", - "id": "9ddd66ca", - "metadata": {}, - "source": [ - "## Questions" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dd45ae08", - "metadata": {}, - "outputs": [], - "source": [ - "t = @elapsed compute_π(100_000_000)" - ] - }, - { - "cell_type": "markdown", - "id": "2a23b1c3", - "metadata": {}, - "source": [ - "
\n", - "Question (Q1): How long will the compute time of next cell be? \n", - "
\n", - "\n", - " a) 10*t\n", - " b) t\n", - " c) 0.1*t\n", - " d) near 0*t \n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "18d6cfe3", - "metadata": {}, - "outputs": [], - "source": [ - "@time for i in 1:10\n", - " compute_π(100_000_000)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "5f19d38c", - "metadata": {}, - "source": [ - "
\n", - "Question (Q2): How long will the compute time of next cell be? \n", - "
\n", - "\n", - " a) 10*t\n", - " b) t\n", - " c) 0.1*t\n", - " d) near 0*t \n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dac0c92a", - "metadata": {}, - "outputs": [], - "source": [ - "@time for i in 1:10\n", - " @async compute_π(100_000_000)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "5041c355", - "metadata": {}, - "source": [ - "
\n", - "Question (Q3): How long will the compute time of next cell be? \n", - "
\n", - "\n", - " a) 10*t\n", - " b) t\n", - " c) 0.1*t\n", - " d) near 0*t \n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c06dc4a5", - "metadata": {}, - "outputs": [], - "source": [ - "@time @sync for i in 1:10\n", - " @async compute_π(100_000_000)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "841b690e", - "metadata": {}, - "source": [ - "
\n", - "Question (Q4): How long will the compute time of the 2nd cell be? \n", - "
\n", - "\n", - " a) infinity\n", - " b) 1 second\n", - " c) near 0 seconds\n", - " d) 3 seconds" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6ac116bb", - "metadata": {}, - "outputs": [], - "source": [ - "buffer_size = 4\n", - "chnl = Channel{Int}(buffer_size)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "25363a90", - "metadata": {}, - "outputs": [], - "source": [ - "@time begin\n", - " put!(chnl,3)\n", - " i = take!(chnl)\n", - " sleep(i)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "df663f11", - "metadata": {}, - "source": [ - "
\n", - "Question (Q5): How long will the compute time of the 2nd cell be? \n", - "
\n", - "\n", - " a) infinity\n", - " b) 1 second\n", - " c) near 0 seconds\n", - " d) 3 seconds" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9abeed40", - "metadata": {}, - "outputs": [], - "source": [ - "chnl = Channel{Int}()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8e8428ce", - "metadata": {}, - "outputs": [], - "source": [ - "@time begin\n", - " put!(chnl,3)\n", - " i = take!(chnl)\n", - " sleep(i)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "a5d3730b", - "metadata": {}, - "source": [ - "# License\n", - "\n", - "TODO: replace link to website\n", - "\n", - "This notebook is part of the course [Programming Large Scale Parallel Systems](http://localhost:8000/) at Vrije Universiteit Amsterdam and may be used under a [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f9863011", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Julia 1.9.1", - "language": "julia", - "name": "julia-1.9" - }, - "language_info": { - "file_extension": ".jl", - "mimetype": "application/julia", - "name": "julia", - "version": "1.9.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/src/notebooks/julia_async.md b/docs/src/notebooks/julia_async.md deleted file mode 100644 index 23b0ac3..0000000 --- a/docs/src/notebooks/julia_async.md +++ /dev/null @@ -1,30 +0,0 @@ -```@meta -EditURL = "https://github.com/fverdugo/XM_40017/blob/main/docs/src/notebooks/julia_async.ipynb" -``` - -```@raw html -
-
Tip
-
- -
-
-``` - -```@raw html - - -``` diff --git a/docs/src/notebooks/julia_basics.ipynb b/docs/src/notebooks/julia_basics.ipynb deleted file mode 100644 index 993708d..0000000 --- a/docs/src/notebooks/julia_basics.ipynb +++ /dev/null @@ -1,1625 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "faecad82", - "metadata": {}, - "source": [ - "\n", - "\n", - "### Programming large-scale parallel systems\n", - "\n", - "\n", - "# Julia basics" - ] - }, - { - "cell_type": "markdown", - "id": "0f54a3f8", - "metadata": {}, - "source": [ - "## Contents\n", - "\n", - "In this notebook, we will cover the basic parts of Julia needed later to learn parallel computing. In particular, we will learn about:\n", - "\n", - "- Variables\n", - "- Functions\n", - "- Arrays\n", - "\n", - "For a more general introduction to Julia see the nice tutorials made available by JuliaAcademy [here](https://github.com/JuliaAcademy/JuliaTutorials/) or the official Julia educational resources [here](https://julialang.org/learning/).\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "a23a48f4", - "metadata": {}, - "source": [ - "## Using Jupyter notebooks in Julia\n", - "\n", - "We are going to use Jupyter notebooks in this and other lectures. You provably have worked with notebooks (in Python). If not, here are the basic concepts you need to know to follow the lessons.\n", - "\n", - "
\n", - "Tip: Did you know that Jupyter stands for Julia, Python and R?\n", - "
\n", - "\n", - "### How to start a Jupyter nootebook in Julia\n", - "\n", - "To run a Julia Jupyther notebook, open a Julia REPL and type\n", - "\n", - "```julia\n", - "julia> ]\n", - "pkg> add IJulia\n", - "julia> using IJulia\n", - "julia> notebook()\n", - "```\n", - "A new browser window will open. Navigate to the corresponding notebook and open it.\n", - "\n", - "
\n", - "Warning: Make sure that the notebook is using the same Julia version as the one you used to launch `IJulia`. If it is not the same, go to Kernel > Change Kernel and choose the right version.\n", - "
\n", - "\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "bcefe994", - "metadata": {}, - "source": [ - "### Running a cell\n", - "To run a cell, click on a cell and press `Shift` + `Enter`. You can also use the \"Run\" button in the toolbar above." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4eae9070", - "metadata": {}, - "outputs": [], - "source": [ - "1+3\n", - "4*5" - ] - }, - { - "cell_type": "markdown", - "id": "0a02fb75", - "metadata": {}, - "source": [ - "As you can see from the output of previous cell, the value of the last line is displayed. We can suppress the output with a semicolon. Try it. Execute next cell." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3923b7a5", - "metadata": {}, - "outputs": [], - "source": [ - "1+3\n", - "4*5;" - ] - }, - { - "cell_type": "markdown", - "id": "ffd3a444", - "metadata": {}, - "source": [ - "### Cell order is important\n", - "\n", - "Running the two cells below in reverse order won't work (try it). " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f7597471", - "metadata": {}, - "outputs": [], - "source": [ - "foo() = \"Well done!\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4a5d4f50", - "metadata": {}, - "outputs": [], - "source": [ - "foo()" - ] - }, - { - "cell_type": "markdown", - "id": "92112bd1", - "metadata": {}, - "source": [ - "### REPL modes\n", - "\n", - "This is particular to Julia notebooks. You can use package, help, and shell mode just like in the Julia REPL." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9a445020", - "metadata": {}, - "outputs": [], - "source": [ - "] add MPI" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "edefc54f", - "metadata": {}, - "outputs": [], - "source": [ - "? print" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e69fd5f1", - "metadata": {}, - "outputs": [], - "source": [ - "; ls" - ] - }, - { - "cell_type": "markdown", - "id": "8b94fc33", - "metadata": {}, - "source": [ - "## Variables\n", - "\n", - "The usage of variables in Julia is pretty similar to Python and quite different from C/C++ and Fortran. However, there are also some differences with Python. \n", - "\n", - "### Creating a variable\n", - "\n", - "A variable is a name associated (bound) to a value. We associate variables with values with `=` as usual." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "149b7a62", - "metadata": {}, - "outputs": [], - "source": [ - "a = 1" - ] - }, - { - "cell_type": "markdown", - "id": "77b557a8", - "metadata": {}, - "source": [ - "When assigning a variable, the value on the right hand side is not copied into the variable. It is just an association of a name with a value (much like in Python)." - ] - }, - { - "cell_type": "markdown", - "id": "ba08ea11", - "metadata": {}, - "source": [ - "### Re-assign a variable\n", - "\n", - "We can re-assign a variable, even with a value of another type. However, avoid changing the variable type for performance reasons." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f62e49c9", - "metadata": {}, - "outputs": [], - "source": [ - "a = 2" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "86ac71ec", - "metadata": {}, - "outputs": [], - "source": [ - "a = 1.0" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e67d8480", - "metadata": {}, - "outputs": [], - "source": [ - "a = \"Hi!\"" - ] - }, - { - "cell_type": "markdown", - "id": "767563a7", - "metadata": {}, - "source": [ - "### Unreachable objects\n", - "\n", - "When an object is not associated with a variable any more, it cannot be reached by the user. This can happen, e.g., when we re-assign a variable. Another case is when local variables, e.g in a function, go out of scope. The following line allocates a large array and assigns it to variable a" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "202e222b", - "metadata": {}, - "outputs": [], - "source": [ - " a = zeros(300000000);" - ] - }, - { - "cell_type": "markdown", - "id": "35742a64", - "metadata": {}, - "source": [ - "If we re-assign the variable to another value, the large array will be inaccessible." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e6cd1c24", - "metadata": {}, - "outputs": [], - "source": [ - "a = nothing" - ] - }, - { - "cell_type": "markdown", - "id": "2883bf04", - "metadata": {}, - "source": [ - "### Garbage collector\n", - "\n", - "Luckily, Julia has a garbage collector that deallocates unreachable objects. You don't need to bother about manual deallocation! Julia is not constantly looking for unreachable objects. Thus, garbage collection does not happen instantaneously, but it will happen at some point. You can also explicitly call the garbage collector, but it is almost never done in practice." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "89516ede", - "metadata": {}, - "outputs": [], - "source": [ - "GC.gc()" - ] - }, - { - "cell_type": "markdown", - "id": "c110aa22", - "metadata": {}, - "source": [ - "### Type declarations are optional\n", - "\n", - "Julia knows the type of the object associated with a variable." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c8d93d64", - "metadata": {}, - "outputs": [], - "source": [ - "a = 1\n", - "typeof(a)" - ] - }, - { - "cell_type": "markdown", - "id": "df2e48c2", - "metadata": {}, - "source": [ - "We can annotate types if we want, but this will not improve performance (except in very special situations). Thus, annotating types is not done in practice." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "95fd3ef4", - "metadata": {}, - "outputs": [], - "source": [ - "c::Int = 1\n", - "typeof(c)" - ] - }, - { - "cell_type": "markdown", - "id": "67883016", - "metadata": {}, - "source": [ - "If you annotate a variable with a type, then it cannot refer to objects of other types." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f3cdc27b", - "metadata": {}, - "outputs": [], - "source": [ - "c = \"I am a string\"" - ] - }, - { - "cell_type": "markdown", - "id": "64f5aa94", - "metadata": {}, - "source": [ - "### Variable names\n", - "\n", - "There is a great flexibility to choose variable names in Julia. See all the rules in the [manual](https://docs.julialang.org/en/v1/manual/variables/#man-allowed-variable-names) if you are interested." - ] - }, - { - "cell_type": "markdown", - "id": "09eb4365", - "metadata": {}, - "source": [ - "We can use Unicode (UTF-8 encoding) characters in variables and function names." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "94667496", - "metadata": {}, - "outputs": [], - "source": [ - "🐱 = \"I am a cat\"\n", - "🐶 = \"I am a dog\"\n", - "🐱 == 🐶" - ] - }, - { - "cell_type": "markdown", - "id": "beaf9680", - "metadata": {}, - "source": [ - "We can also use Greek letters and other mathematical symbols. Just write the corresponding [LaTeX](https://oeis.org/wiki/List_of_LaTeX_mathematical_symbols) command and press `Tab`. For example: `ω` is written `\\omega` + `Tab`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b9b3079e", - "metadata": {}, - "outputs": [], - "source": [ - "ω = 1.234\n", - "sin(ω)" - ] - }, - { - "cell_type": "markdown", - "id": "4f7de553", - "metadata": {}, - "source": [ - "In fact, some useful mathematical constants are predefined in Julia with math Greek letters." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "89d6d94e", - "metadata": {}, - "outputs": [], - "source": [ - "sin(π/2)" - ] - }, - { - "cell_type": "markdown", - "id": "d36ba0ae", - "metadata": {}, - "source": [ - "\n", - "
\n", - "Question: Which will be the value of `x` in the last line ? (Think your answer before executing next cell to find out the result) \n", - "
\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f6f4e13e", - "metadata": {}, - "outputs": [], - "source": [ - "x = 1\n", - "y = x\n", - "y = 2\n", - "x" - ] - }, - { - "cell_type": "markdown", - "id": "4d2cb752", - "metadata": {}, - "source": [ - "## Functions\n", - "\n", - "Julia is very much a functional programming language. In consequence, Julia is more centered on functions than on types. This is in contrast to object-oriented languages, which are more centered on types (classes). For instance, you don't need to know the details of the Julia type system to learn parallel programming in Julia, but you need to have a quite advanced knowledge of how Julia functions work.\n", - "\n", - "### Defining functions\n", - "\n", - "Functions are defined as shown in next cell. The closing `end` is necessary. Do to forget it! However, the `return` is optional. The value of last line is returned by default. Indentation is recommended, but it is also optional. That's why the closing `end` is needed." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "266b0e1b", - "metadata": {}, - "outputs": [], - "source": [ - "function add(a,b)\n", - " return a + b \n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "de95c63c", - "metadata": {}, - "source": [ - "Once defined, a function can be called using bracket notation as you would expect." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "39b452a7", - "metadata": {}, - "outputs": [], - "source": [ - "add(1,3)" - ] - }, - { - "cell_type": "markdown", - "id": "afceb0fc", - "metadata": {}, - "source": [ - "### Broadcast syntax\n", - "\n", - "We can apply functions to arrays element by element using broadcast (dot) syntax." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e65be5e8", - "metadata": {}, - "outputs": [], - "source": [ - "a = [1,2,3]\n", - "b = [4,5,6]\n", - "add.(a,b)" - ] - }, - { - "cell_type": "markdown", - "id": "e4ca76a4", - "metadata": {}, - "source": [ - "Mathematical operators can also be broadcasted (like in Matlab). The following cell won't work. If we want to multiply element by element, we can use the broadcasted version below." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ae0b3f6d", - "metadata": {}, - "outputs": [], - "source": [ - "a .* b" - ] - }, - { - "cell_type": "markdown", - "id": "b131e208", - "metadata": {}, - "source": [ - "
\n", - "Question: Which will be the value of `x` in the last line ?\n", - "
\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3d637d87", - "metadata": {}, - "outputs": [], - "source": [ - "function q(x)\n", - " x = 2\n", - " x\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "882d8e59", - "metadata": {}, - "outputs": [], - "source": [ - "x = 1\n", - "y = q(x)\n", - "x" - ] - }, - { - "cell_type": "markdown", - "id": "d20bc3a4", - "metadata": {}, - "source": [ - "### Defining functions (shorter way)\n", - "\n", - "For short functions, we can skip the `function` and `end` keywords as follows.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7815aea5", - "metadata": {}, - "outputs": [], - "source": [ - "add_short(a,b) = a+b" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7aa48db2", - "metadata": {}, - "outputs": [], - "source": [ - "add_short(1,3)" - ] - }, - { - "cell_type": "markdown", - "id": "f39c31c0", - "metadata": {}, - "source": [ - "### Anonymous (lambda) functions\n", - "\n", - "Since we can assign function to variables, it is not needed for a function to have a function name in many cases. We can simply create an anonymous function (i.e., a function without name) and assign it to a variable." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "94ce2be7", - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "add_anonymous = (a,b) -> a+b" - ] - }, - { - "cell_type": "markdown", - "id": "b15b84d8", - "metadata": {}, - "source": [ - "We can call the function by using the variable name." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "96d80a59", - "metadata": {}, - "outputs": [], - "source": [ - "add_anonymous(2.0,3.5)" - ] - }, - { - "cell_type": "markdown", - "id": "1723e4f1", - "metadata": {}, - "source": [ - "Note that `add_anonymous` is not a function name. It is just a variable associated with a function with no function name (well, it has a name technically, but with an arbitrary value)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5bf111a0", - "metadata": {}, - "outputs": [], - "source": [ - "nameof(add_anonymous)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7e23f9f8", - "metadata": {}, - "outputs": [], - "source": [ - "nameof(add)" - ] - }, - { - "cell_type": "markdown", - "id": "e530acd3", - "metadata": {}, - "source": [ - "### Functions are first-class objects\n", - "\n", - "We can work with Julia functions like with any other type of object. For instance, we can assign functions to variables." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e57fe028", - "metadata": {}, - "outputs": [], - "source": [ - "a = add" - ] - }, - { - "cell_type": "markdown", - "id": "7a8f4bb0", - "metadata": {}, - "source": [ - "Now, we can call the function using the variable name." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c7e4d4ec", - "metadata": {}, - "outputs": [], - "source": [ - "a(4,5)" - ] - }, - { - "cell_type": "markdown", - "id": "c01be92e", - "metadata": {}, - "source": [ - "We can also create an array of functions (this will not work in Python)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3837bf25", - "metadata": {}, - "outputs": [], - "source": [ - "funs = [+,-,*]" - ] - }, - { - "cell_type": "markdown", - "id": "0aac151b", - "metadata": {}, - "source": [ - "To call a specific function in the array, we index the array and then call the returned function" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "aab45992", - "metadata": {}, - "outputs": [], - "source": [ - "funs[2](2,3)" - ] - }, - { - "cell_type": "markdown", - "id": "f0c668c8", - "metadata": {}, - "source": [ - "### Higher-order functions\n", - "\n", - "Higher order functions are functions that take and/or return other functions. And example is the `count` function in Julia.\n", - "\n", - "For instance, we can pass a user-defined function to count the number of even elements in an array." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "22115f82", - "metadata": {}, - "outputs": [], - "source": [ - "func = i->i%2==0\n", - "a = [1,2,3,5,32,2,4]\n", - "count(func,a)" - ] - }, - { - "cell_type": "markdown", - "id": "0b352cdc", - "metadata": {}, - "source": [ - "### Do-blocks\n", - "\n", - "There is yet another way to define anonymous functions. If a function takes a function in its first argument (like `count`) we can skip the first argument, when calling the function, and define the function we want to pass in a do-block. This is useful, e.g., if we want to define a multi-line anonymous function. The two next cells are equivalent." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "533a21f3", - "metadata": {}, - "outputs": [], - "source": [ - "function f(i)\n", - " m = i%2\n", - " m != 0\n", - "end\n", - "count(f,[1,2,3,5,32,2,4])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "68b5165a", - "metadata": {}, - "outputs": [], - "source": [ - "count([1,2,3,5,32,2,4]) do i\n", - " m = i%2\n", - " m != 0\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "559a084d", - "metadata": {}, - "source": [ - "### Returning multiple values\n", - "\n", - "Julia functions always return a single variable. To return multiple values, we can wrap them in a tuple.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "eba30561", - "metadata": {}, - "outputs": [], - "source": [ - "function divrem(a,b)\n", - " α = div(a,b)\n", - " β = rem(a,b)\n", - " (α,β)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "1a6e9d0d", - "metadata": {}, - "source": [ - "The output is a tuple as expected, but we can recover the individual values by unpacking the tuple." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cb0c2806", - "metadata": {}, - "outputs": [], - "source": [ - "d,r = divrem(10,3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "31428737", - "metadata": {}, - "outputs": [], - "source": [ - "d" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "76424bb5", - "metadata": {}, - "outputs": [], - "source": [ - "r" - ] - }, - { - "cell_type": "markdown", - "id": "c73ae75b", - "metadata": {}, - "source": [ - "### Variable number of input arguments\n", - "\n", - "Functions with multiple arguments are also supported. The following example iterates over the given arguments and prints them. `args` is just a tuple with all arguments." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a7f94ff0", - "metadata": {}, - "outputs": [], - "source": [ - "function showargs(args...)\n", - " for (i,arg) in enumerate(args)\n", - " println(\"args[$i] = $arg\")\n", - " end\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7828534e", - "metadata": {}, - "outputs": [], - "source": [ - "showargs(1,\"Hi!\",π)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f31fc3ff", - "metadata": {}, - "outputs": [], - "source": [ - "showargs(6)" - ] - }, - { - "cell_type": "markdown", - "id": "dc47d83b", - "metadata": {}, - "source": [ - "### Positional and keyword arguments\n", - "\n", - "Functions can combine positional and keyword arguments much like in Python, but keyword arguments start with semicolon `;` in Julia." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "37cd0314", - "metadata": {}, - "outputs": [], - "source": [ - "function foo(a,b;c,d)\n", - " println(\"Positional: a=$a, b=$b. Keyword: c=$c, d=$d\")\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ee19db3d", - "metadata": {}, - "outputs": [], - "source": [ - "foo(3,4,d=2,c=1)" - ] - }, - { - "cell_type": "markdown", - "id": "4919ef95", - "metadata": {}, - "source": [ - "### Optional arguments\n", - "\n", - "We can provide default values to arguments to make them optional." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "650e6d67", - "metadata": {}, - "outputs": [], - "source": [ - "function bar(a,b=0;c,d=1)\n", - " println(\"Positional: a=$a, b=$b. Keyword: c=$c, d=$d\")\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e5d03536", - "metadata": {}, - "outputs": [], - "source": [ - "bar(1,c=2)" - ] - }, - { - "cell_type": "markdown", - "id": "94599fa3", - "metadata": {}, - "source": [ - "
\n", - "Question: Which will be the value of `x` below? \n", - "
\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "79bd84bd", - "metadata": {}, - "outputs": [], - "source": [ - "function hofun(x)\n", - " y -> x*y\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a9a7b774", - "metadata": {}, - "outputs": [], - "source": [ - "f2 = hofun(2)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f6606086", - "metadata": {}, - "outputs": [], - "source": [ - "x = f2(3)\n", - "x" - ] - }, - { - "cell_type": "markdown", - "id": "bc8e9bcf", - "metadata": {}, - "source": [ - "## Arrays\n", - "\n", - "Julia supports multi-dimensional arrays. They are very similar to Numpy arrays in Python. Let's learn the basics of Julia arrays.\n", - "\n", - "\n", - "### Array literals\n", - "\n", - "We can create (small) arrays from the given values using array literals.\n", - "\n", - "Next cell creates a vector with 3 integers. Note for Python users: there is no difference between vectors and lists in Julia." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "08652696", - "metadata": {}, - "outputs": [], - "source": [ - "vec = [1,2,3]" - ] - }, - { - "cell_type": "markdown", - "id": "70f15e92", - "metadata": {}, - "source": [ - "We can create a matrix as follows." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a3aacf71", - "metadata": {}, - "outputs": [], - "source": [ - "mat = [1 2 3 4\n", - " 5 6 7 8\n", - " 9 10 11 12]" - ] - }, - { - "cell_type": "markdown", - "id": "e096b198", - "metadata": {}, - "source": [ - "### Array initialization\n", - "\n", - "We can create arrays with all the entries equal to zero, to one, or to a specific given value. The value can be any Julia object, even a function!\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9357edac", - "metadata": {}, - "outputs": [], - "source": [ - "zeros(4)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dfc88af3", - "metadata": {}, - "outputs": [], - "source": [ - "zeros(Int,4)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "49e5a779", - "metadata": {}, - "outputs": [], - "source": [ - "ones(2,3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6bbde94a", - "metadata": {}, - "outputs": [], - "source": [ - "fill(5.0,3,4)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d2c44cce", - "metadata": {}, - "outputs": [], - "source": [ - "fill(add,3,4)" - ] - }, - { - "cell_type": "markdown", - "id": "495827bc", - "metadata": {}, - "source": [ - "### Array comprehensions\n", - "\n", - "We can also create the items in the array using a loop within an array comprehension." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4b3c56b4", - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "squares = [ i^2 for i in 1:8 ]" - ] - }, - { - "cell_type": "markdown", - "id": "cee954ce", - "metadata": {}, - "source": [ - "### Indexing\n", - "\n", - "We can get and set the items of an array by indexing the array." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "df901c08", - "metadata": {}, - "outputs": [], - "source": [ - "squares[3]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dc58cf4c", - "metadata": {}, - "outputs": [], - "source": [ - "squares[end]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "aa7936e0", - "metadata": {}, - "outputs": [], - "source": [ - "squares[2:4]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6cf66ef0", - "metadata": {}, - "outputs": [], - "source": [ - "squares[4] = 16" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9125cbeb", - "metadata": {}, - "outputs": [], - "source": [ - "squares[2:3] = [4,9]" - ] - }, - { - "cell_type": "markdown", - "id": "f64021ab", - "metadata": {}, - "source": [ - "### Immutable element type" - ] - }, - { - "cell_type": "markdown", - "id": "6650d6e7", - "metadata": {}, - "source": [ - "Note that once set, the type of the elements in the array cannot be changed. If we try to set an item with an object of a different type, Julia will try to do a conversion, which can fail depending on the passed value." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c79b297e", - "metadata": {}, - "outputs": [], - "source": [ - "a = [10,11,12,13]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2694f3d4", - "metadata": {}, - "outputs": [], - "source": [ - "a[2] = \"Hi!\"" - ] - }, - { - "cell_type": "markdown", - "id": "92bae51d", - "metadata": {}, - "source": [ - "### Arrays of any element type\n", - "\n", - "Arrays of fixed element type seem to be very rigid, right? Python list have not this limitation. However, we can use arrays of `Any` type, which are as flexible as Python lists, or even more since they can also contain functions. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d656b9ce", - "metadata": {}, - "outputs": [], - "source": [ - "a = Any[10,11,12,13]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3d08e4ba", - "metadata": {}, - "outputs": [], - "source": [ - "a[3] = \"HI!\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "92bf2cab", - "metadata": {}, - "outputs": [], - "source": [ - "a" - ] - }, - { - "cell_type": "markdown", - "id": "1c72351a", - "metadata": {}, - "source": [ - "### Loops\n", - "\n", - "The loop in next cell visits the elements in `a` one after the other. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "72dfc0f2", - "metadata": {}, - "outputs": [], - "source": [ - "a = [10,20,30,40]\n", - "for ai in a\n", - " @show ai\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "d4906337", - "metadata": {}, - "source": [ - "This loop visits the integers from 1 to the length of the array and indexes the array at each of these integers." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "75f5ab89", - "metadata": {}, - "outputs": [], - "source": [ - "for i in 1:length(a)\n", - " ai = a[i]\n", - " @show (i,ai)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "e7abfdd6", - "metadata": {}, - "source": [ - "This loop \"enumerates\" the items in the array." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "96dfab12", - "metadata": {}, - "outputs": [], - "source": [ - "for (i,ai) in enumerate(a)\n", - " @show (i,ai)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "b4747037", - "metadata": {}, - "source": [ - "### Arrays indices are 1-based by default\n", - "\n", - "Be aware of this if you are a C or Python user." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "694ec903", - "metadata": {}, - "outputs": [], - "source": [ - "a = [10,20,30,40]\n", - "a[0]" - ] - }, - { - "cell_type": "markdown", - "id": "a2ed5b01", - "metadata": {}, - "source": [ - "### Slicing allocates a new array\n", - "\n", - "This is also different from Numpy in Python." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "961a82a3", - "metadata": {}, - "outputs": [], - "source": [ - "a = [1 2 3\n", - " 4 5 6]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1c9f2162", - "metadata": {}, - "outputs": [], - "source": [ - "s = a[:,2]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4664eeb7", - "metadata": {}, - "outputs": [], - "source": [ - "s[2] = 0" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c356e67d", - "metadata": {}, - "outputs": [], - "source": [ - "a" - ] - }, - { - "cell_type": "markdown", - "id": "b75a2b83", - "metadata": {}, - "source": [ - "### Array views\n", - "\n", - "If you want to modify the original array, use `view` instead." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3d8e2ab7", - "metadata": {}, - "outputs": [], - "source": [ - "v = view(a,:,2)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "329079bd", - "metadata": {}, - "outputs": [], - "source": [ - "v[1] = 0" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3b26a483", - "metadata": {}, - "outputs": [], - "source": [ - "a" - ] - }, - { - "cell_type": "markdown", - "id": "fef8495e", - "metadata": {}, - "source": [ - "## Exercises" - ] - }, - { - "cell_type": "markdown", - "id": "7d84ceaf", - "metadata": {}, - "source": [ - "### Exercise 1\n", - "\n", - "Implement a function `ex1(a)` that finds the largest item in the array `a`. It should return the largest item and its corresponding position in the array. If there are multiple maximal elements, then the first one will be returned. Assume that the array is not empty. Implement the function in the next cell. Test your implementation with the other one." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8e6c7091", - "metadata": {}, - "outputs": [], - "source": [ - "# Implement here" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "370afcca", - "metadata": {}, - "outputs": [], - "source": [ - "using Test\n", - "arr = [3,4,7,3,1,7,2]\n", - "@test ex1(arr) == (7,3)" - ] - }, - { - "cell_type": "markdown", - "id": "7be19eb4", - "metadata": {}, - "source": [ - "### Exercise 2\n", - "\n", - "Implement a function `ex2(f,g)` that takes two functions `f(x)` and `g(x)` and returns a new function `h(x)` representing the sum of `f` and `g`, i.e., `h(x)=f(x)+g(x)`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "331e90b6", - "metadata": {}, - "outputs": [], - "source": [ - "# Implement here" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9e86c981", - "metadata": {}, - "outputs": [], - "source": [ - "h = ex2(sin,cos)\n", - "xs = LinRange(0,2π,100)\n", - "@test all(x-> h(x) == sin(x)+cos(x), xs)" - ] - }, - { - "cell_type": "markdown", - "id": "3324a991", - "metadata": {}, - "source": [ - "### Exercise 3 (hard)\n", - "\n", - "Function `mandel` estimates if a given point `(x,y)` in the complex plane belongs to the [Mandelbrot set](https://en.wikipedia.org/wiki/Mandelbrot_set)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0ac481c1", - "metadata": {}, - "outputs": [], - "source": [ - "function mandel(x,y,max_iters)\n", - " z = Complex(x,y)\n", - " c = z\n", - " threshold=2\n", - " for n in 1:max_iters\n", - " if abs(z)>threshold\n", - " return n-1\n", - " end\n", - " z = z^2 +c\n", - " end\n", - " max_iters\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "86ea2591", - "metadata": {}, - "source": [ - "If the value of `mandel` is less than `max_iters`, the point is provably outside the Mandelbrot set. If `mandel` is equal to `max_iters`, then the point is provably inside the set. The larger `max_iters`, the better the quality of the estimate (the nicer will be your plot).\n", - "\n", - "Plot the value of function `mandel` for each pixel in a 2D grid of the box.\n", - "\n", - "$$(-1.7,0.7)\\times(-1.2,1.2).$$\n", - "\n", - "Use a grid resolution of at least 1000 points in each direction and `max_iters` at least 10. You can increase these values to get nicer plots. To plot the values use function `heatmap` from the Julia package `GLMakie`. Use `LinRange` to divide the horizontal and vertical axes into pixels. See the documentation of these functions for help. `GLMakie` is a GPU-accelerated plotting back-end for Julia. It is a large package and it can take some time to install and to generate the first plot. Be patient." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "33137ad9", - "metadata": {}, - "outputs": [], - "source": [ - "# Implement here" - ] - }, - { - "cell_type": "markdown", - "id": "357e0490", - "metadata": {}, - "source": [ - "# License\n", - "\n", - "TODO: replace link to website\n", - "\n", - "This notebook is part of the course [Programming Large Scale Parallel Systems](http://localhost:8000/) at Vrije Universiteit Amsterdam and may be used under a [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f8d92f25", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Julia 1.9.1", - "language": "julia", - "name": "julia-1.9" - }, - "language_info": { - "file_extension": ".jl", - "mimetype": "application/julia", - "name": "julia", - "version": "1.9.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/src/notebooks/julia_basics.md b/docs/src/notebooks/julia_basics.md deleted file mode 100644 index baa6dea..0000000 --- a/docs/src/notebooks/julia_basics.md +++ /dev/null @@ -1,30 +0,0 @@ -```@meta -EditURL = "https://github.com/fverdugo/XM_40017/blob/main/docs/src/notebooks/julia_basics.ipynb" -``` - -```@raw html -
-
Tip
-
- -
-
-``` - -```@raw html - - -``` diff --git a/docs/src/notebooks/julia_distributed.ipynb b/docs/src/notebooks/julia_distributed.ipynb deleted file mode 100644 index 72db93d..0000000 --- a/docs/src/notebooks/julia_distributed.ipynb +++ /dev/null @@ -1,1335 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "287c5272", - "metadata": {}, - "source": [ - "\n", - "\n", - "### Programming large-scale parallel systems\n" - ] - }, - { - "cell_type": "markdown", - "id": "2133c064", - "metadata": {}, - "source": [ - "# Distributed computing in Julia\n" - ] - }, - { - "cell_type": "markdown", - "id": "a7b64d5a", - "metadata": {}, - "source": [ - "## Contents\n", - "\n", - "In this notebook, we will learn the basics of distributed computing in Julia. In particular, we will focus on the Distributed module available in the Julia standard library. The main topics we are going to cover are:\n", - "\n", - "- How to create Julia processes\n", - "- How to execute code remotely\n", - "- How to send and receive data\n", - "\n", - "With this knowledge you will be able to implement simple and complex parallel algorithms in Julia." - ] - }, - { - "cell_type": "markdown", - "id": "ec225103", - "metadata": {}, - "source": [ - "
\n", - "Note: Do not forget to execute the next cell before starting this notebook! \n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "aebf6482", - "metadata": {}, - "outputs": [], - "source": [ - "using Printf\n", - "function answer_checker(answer,solution)\n", - " if answer == solution\n", - " \"🥳 Well done! \"\n", - " else\n", - " \"It's not correct. Keep trying! 💪\"\n", - " end |> println\n", - "end\n", - "q_1_check(answer) = answer_checker(answer,\"a\")\n", - "q_2_check(answer) = answer_checker(answer,\"b\")" - ] - }, - { - "cell_type": "markdown", - "id": "01af032c", - "metadata": {}, - "source": [ - "## How to create Julia processes\n", - "\n", - "First of all, we need several processes in order to run parallel algorithms *in parallel*. In this section, we discuss different ways to create new processes in Julia." - ] - }, - { - "cell_type": "markdown", - "id": "036a25d7", - "metadata": {}, - "source": [ - "### Adding processes locally\n", - "\n", - " The simplest way of creating processes for parallel computing is to add them locally in the current Julia session. This is done by using the following commands.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c6fed889", - "metadata": {}, - "outputs": [], - "source": [ - "using Distributed" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d16faba9", - "metadata": {}, - "outputs": [], - "source": [ - "addprocs(3)" - ] - }, - { - "cell_type": "markdown", - "id": "f07ac76c", - "metadata": {}, - "source": [ - "Last cell created 3 new Julia processes. By default, they run locally in the same computer as the current Julia session, using multiple cores if possible. However, it is also possible to start the new processes in other machines as long as they are interconnected (more details on this later).\n", - "\n", - "\n", - "
\n", - "Tip: We can also start new processes when launching Julia from the command line by suing the `-p` command-line argument. E.g., `$ julia -p 3 ` would launch Julia with 3 extra processes.\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "id": "d2e507dc", - "metadata": {}, - "source": [ - "### Each process runs a separated Julia instance\n", - "\n", - "When adding the new processes, you can imagine that 3 new Julia REPLs have started under the hood (see figure below). The main point of the Distributed module is to provide a way of coordinating all these Julia processes to run code in parallel. It is important to note that each process runs in a separated Julia instance. This means that each process has its own memory space and therefore they do not share memory. This results in distributed-memory parallelism, and allows one to run processes in different machines.\n", - " \n", - "
\n", - "\n", - "
\n", - " \n", - " " - ] - }, - { - "cell_type": "markdown", - "id": "da2ae9aa", - "metadata": {}, - "source": [ - "### Basic info about processes\n", - "\n", - "The following functions provide basic information about the underlying processes. If more than one process is available, the first process is called the *main* or *master* and the other the *workers*. If only a single process is available, it is the master and the first worker simultaneously." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9214029b", - "metadata": {}, - "outputs": [], - "source": [ - "procs()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7a8498e6", - "metadata": {}, - "outputs": [], - "source": [ - "workers()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c6a8d8e4", - "metadata": {}, - "outputs": [], - "source": [ - "nprocs()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6f8b2888", - "metadata": {}, - "outputs": [], - "source": [ - "nworkers()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8583dee0", - "metadata": {}, - "outputs": [], - "source": [ - "myid()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "17f1541f", - "metadata": {}, - "outputs": [], - "source": [ - "@everywhere println(myid())" - ] - }, - { - "cell_type": "markdown", - "id": "44633d04", - "metadata": {}, - "source": [ - "In previous cell, we have used the macro `@everywhere` that evaluates the given code on all processes. As a result, each process will print its own process id." - ] - }, - { - "cell_type": "markdown", - "id": "2d104013", - "metadata": {}, - "source": [ - "### Creating workers in other machines\n", - "\n", - "For large parallel computations, one typically needs to use different computers in parallel. Function `addprocs` also provides a low-level method to start workers in other machines. Next code example would create 3 workers in `server1` and 4 new workers in server `server2` (see figure below). Under the hood, Julia connects via ssh to the other machines and starts the new processes there. In order this to work, the local computer and the remote servers need to be properly configured (see the Julia manual for details). \n", - "\n", - "\n", - "\n", - "\n", - "```julia\n", - "using Distributed\n", - "machines = [(\"user@server1\",3),(\"user@server2\",4)]\n", - "addprocs(machines)\n", - "```\n", - "\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "3869f1f7", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "
\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "6c09b2e5", - "metadata": {}, - "source": [ - "### Adding workers with ClusterManagers.jl\n", - "\n", - "Previous way of starting workers in other machines is very low-level. Happily, there is a Julia package called [ClusterManagers.jl](https://github.com/JuliaParallel/ClusterManagers.jl) that helps to create workers remotely in number of usual scenarios. For instance, when running the following code from the login node in a computer cluster, it will submit a job to the cluster queue allocating 128 threads. A worker will be generated for each one of these threads. If the compute node have 64 cores, 2 compute nodes will be used to create to contain the 128 workers (see below).\n", - "\n", - "\n", - "```julia\n", - "using Distributed\n", - "using ClusterManagers\n", - "addprocs(SlurmManager(128), partition=\"debug\", t=\"00:5:00\")\n", - "```\n" - ] - }, - { - "cell_type": "markdown", - "id": "7c470ed9", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "8c8bc619", - "metadata": {}, - "source": [ - "## Executing code remotely\n", - "\n", - "We have added new processes to our Julia session. Let's start using them!" - ] - }, - { - "cell_type": "markdown", - "id": "76a8bf7a", - "metadata": {}, - "source": [ - "### Function `remotecall`\n", - "\n", - "The most basic thing we can do with a remote processor is to execute a given function on it. This is done by using function `remotecall`. To make clear how local and remote executions compare, let's call a function locally and then remotely. Next cell uses function `ones` to create a matrix locally.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e2d0803a", - "metadata": {}, - "outputs": [], - "source": [ - "a = ones(2,3)" - ] - }, - { - "cell_type": "markdown", - "id": "a87fe170", - "metadata": {}, - "source": [ - "The next cell does the same operation, but remotely on process 2. Note that `remotecall` takes the function we want to execute remotely, the process id where we want to execute it and, finally, the function arguments." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "78010eb4", - "metadata": {}, - "outputs": [], - "source": [ - "proc = 2\n", - "ftr = remotecall(ones,proc,2,3)" - ] - }, - { - "cell_type": "markdown", - "id": "b931f9c5", - "metadata": {}, - "source": [ - "Note that `remotecall` does not return the result of the underlying function, but a `Future`. This object represents a reference to a task running on the remote process. To move a copy of the result to the current process we can use `fetch`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e8163967", - "metadata": {}, - "outputs": [], - "source": [ - "fetch(ftr)" - ] - }, - { - "cell_type": "markdown", - "id": "613c4a20", - "metadata": {}, - "source": [ - "### `remotecall` is asynchronous\n", - "\n", - "It is important to note that `remotecall` does not wait for the remote process to finish. It turns immediately. This can be checked be calling remotely the following function that sleeps for 10 secods and then generates a matrix." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d918200b", - "metadata": {}, - "outputs": [], - "source": [ - "fun = (m,n) -> (sleep(10); ones(m,n))" - ] - }, - { - "cell_type": "markdown", - "id": "d5fba3bc", - "metadata": {}, - "source": [ - "When running next cell, it will return immediately, event though the remote process will sleep for 10 seconds. We can even run code in parallel. To try this execute the second next cell while the remote call is running in the worker." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "339e425b", - "metadata": {}, - "outputs": [], - "source": [ - "proc = 2\n", - "ftr = remotecall(fun,proc,2,3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8ff8dc16", - "metadata": {}, - "outputs": [], - "source": [ - "1+1" - ] - }, - { - "cell_type": "markdown", - "id": "40f9d6c4", - "metadata": {}, - "source": [ - "However, when fetching the result, the current process blocks waiting until the result is available in the remote process and arrives to its destination." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "76705f73", - "metadata": {}, - "outputs": [], - "source": [ - "fetch(ftr)" - ] - }, - { - "cell_type": "markdown", - "id": "75cb8b37", - "metadata": {}, - "source": [ - "### Useful macro: `@spawnat`\n", - "\n", - "You have provably realized that in order to use `remotecall` we have written auxiliary anonymous functions. They are needed to wrap the code we want to execute remotely. Writing these functions can be tedious. Happily, the macro `@spawnat` generates an auxiliary function from the given block of code and calls `remotecall` for us. For instance, the two following cells are equivalent." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4b550925", - "metadata": {}, - "outputs": [], - "source": [ - "@spawnat proc ones(2,3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2394cc7f", - "metadata": {}, - "outputs": [], - "source": [ - "fun = () -> ones(2,3)\n", - "remotecall(fun,proc)" - ] - }, - { - "cell_type": "markdown", - "id": "8631fd6f", - "metadata": {}, - "source": [ - "### `@async` vs `@spawnat`\n", - "\n", - "The relation between `@async` and `@pawnat` is obvious. From the user perspective they work almost in the same way. However, `@async` generates a task that runs asynchronously in the current process, whereas `@spawnat` executes a task in a remote process in parallel. In both cases, the result is obtained using `fetch`. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7d681ed9", - "metadata": {}, - "outputs": [], - "source": [ - "tsk = @async begin\n", - " sleep(3)\n", - " zeros(2)\n", - "end\n", - "fetch(tsk)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c8141cff", - "metadata": {}, - "outputs": [], - "source": [ - "ftr = @spawnat :any begin\n", - " sleep(3)\n", - " zeros(2)\n", - "end\n", - "fetch(ftr)" - ] - }, - { - "cell_type": "markdown", - "id": "10899cd4", - "metadata": {}, - "source": [ - "### Another usefull macro: `@fetchfrom`\n", - "\n", - "Macro `@fetchfrom` is the blocking version of `@spawnat`. It blocks and returns the corresponding result instead of a `Future` object. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f17da98e", - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "a = @fetchfrom proc begin\n", - " sleep(3)\n", - " zeros(2)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "e9d37079", - "metadata": {}, - "source": [ - "## Data movement\n", - "\n", - "Data movement is a crucial part in distributed-memory computations and it is usually one of its main computational\n", - "bottlenecks. Being aware of the data we are moving when using functions such as `remotecall` is important to write efficient distributed algorithms in Julia. Julia also provides a special type of channel, called remote channel, to send and receive data between processes." - ] - }, - { - "cell_type": "markdown", - "id": "6d6e04d9", - "metadata": {}, - "source": [ - "### Data movement in `remotecall` / `fetch`\n", - "\n", - "When usig `remotecall` we send to the remote process a function and its arguments. In this example, we send function name `+` and matrices `a` and `b` to proc 4. When fetching the result we receive a copy of the matrix from proc 4." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fd901fcc", - "metadata": {}, - "outputs": [], - "source": [ - "proc = 4\n", - "a = rand(10,10)\n", - "b = rand(10,10)\n", - "ftr = remotecall(+,proc,a,b)\n", - "fetch(ftr);" - ] - }, - { - "cell_type": "markdown", - "id": "eda62267", - "metadata": {}, - "source": [ - "### Implicit data movement\n", - "\n", - "Be aware that data movements can be implicit. This usually happens when we execute remotely functions that capture variables. In the following example, we are also sending matrices `a` and `b` to proc 4, even though they do not appear as arguments in the remote call. These variables are captured by the anonymous function and will be sent to proc 4." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fe9866ac", - "metadata": {}, - "outputs": [], - "source": [ - "proc = 4\n", - "a = rand(10,10)\n", - "b = rand(10,10)\n", - "fun = () -> a+b\n", - "ftr = remotecall(fun,proc)\n", - "fetch(ftr);" - ] - }, - { - "cell_type": "markdown", - "id": "3860df18", - "metadata": {}, - "source": [ - "### Data movement with remote channels\n", - "\n", - "Another way of moving data between processes is to use remote channels. Their usage is very similar to conventional channels for moving data between tasks, but there are some important differences. In the next cell, we create a remote channel. Process 4 puts several values and closes the channel. Like for conventional channels, calls to `put!` are blocking, but next cell is not blocking the master process since the call to `put!` runs asynchronously on process 4." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "98260fd3", - "metadata": {}, - "outputs": [], - "source": [ - "fun = ()->Channel{Int}()\n", - "chnl = RemoteChannel(fun)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0a729a00", - "metadata": {}, - "outputs": [], - "source": [ - "@spawnat 4 begin\n", - " for i in 1:5\n", - " put!(chnl,i)\n", - " end\n", - " close(chnl)\n", - "end;" - ] - }, - { - "cell_type": "markdown", - "id": "c2ce01e0", - "metadata": {}, - "source": [ - "We can take values from the remote channel form any process using `take!`. Run next cell several times. The sixth time it should raise and error since the channel was closed." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "82fbbb88", - "metadata": {}, - "outputs": [], - "source": [ - "take!(chnl)" - ] - }, - { - "cell_type": "markdown", - "id": "1b3f8dcc", - "metadata": {}, - "source": [ - "### Remote channels can be buffered\n", - "\n", - "Just like conventional channels, remote channels can be buffered. The buffer is stored in the process that owns the remote channel. By default this corresponds to process that creates the remote channel, but it can be a different one. For instance, process 3 will be the owner in the following example." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "426274ac", - "metadata": {}, - "outputs": [], - "source": [ - "buffer_size = 2\n", - "owner = 3\n", - "fun = ()->Channel{Int}(buffer_size)\n", - "chnl = RemoteChannel(fun,owner)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "db3b6fd5", - "metadata": {}, - "outputs": [], - "source": [ - "@spawnat 4 begin\n", - " println(\"start\")\n", - " for i in 1:5\n", - " put!(chnl,i)\n", - " println(\"I have put $i\")\n", - " end\n", - " close(chnl)\n", - " println(\"stop\")\n", - "end;" - ] - }, - { - "cell_type": "markdown", - "id": "7e85654e", - "metadata": {}, - "source": [ - "Note that since the channel is buffered, worker 4 can start putting values into it before any call to `take!`. Run next cell several times until the channel is closed." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a8500649", - "metadata": {}, - "outputs": [], - "source": [ - "take!(chnl)" - ] - }, - { - "cell_type": "markdown", - "id": "b8b411f1", - "metadata": {}, - "source": [ - "### Remote channels are not iterable\n", - "\n", - "One main difference with respect to conventional channels is that remote channels cannot be iterated. Let's repeat the example above." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "18c60a68", - "metadata": {}, - "outputs": [], - "source": [ - "fun = ()->Channel{Int}()\n", - "chnl = RemoteChannel(fun)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "73088996", - "metadata": {}, - "outputs": [], - "source": [ - "@spawnat 4 begin\n", - " for i in 1:5\n", - " put!(chnl,i)\n", - " end\n", - " close(chnl)\n", - "end;" - ] - }, - { - "cell_type": "markdown", - "id": "4bb35283", - "metadata": {}, - "source": [ - "Now, try to iterate over the channel in a for loop. It will result in an error since channels are not iterable." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1a9bcca0", - "metadata": {}, - "outputs": [], - "source": [ - "for j in chnl\n", - " @show j\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "8eaea2fa", - "metadata": {}, - "source": [ - "If we want to take values form a remote channel and stop automatically when the channel is closed, we can combine a while loop and a try-catch statement. This works since `take!` raises an error if the channel is closed, which will execute the `catch` block and breaks the loop." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "72acd664", - "metadata": {}, - "outputs": [], - "source": [ - "while true\n", - " try\n", - " j = take!(chnl)\n", - " @show j\n", - " catch\n", - " break\n", - " end\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "1a3986c9", - "metadata": {}, - "source": [ - "## Questions" - ] - }, - { - "cell_type": "markdown", - "id": "fd22b74b", - "metadata": {}, - "source": [ - "\n", - "
\n", - "Question (Q1): How many integers are transferred between master and worker? Including both directions. \n", - "
\n", - "\n", - "\n", - "\n", - "```julia\n", - "a = rand(Int,4,4)\n", - "proc = 4\n", - "@fetchfrom proc sum(a^2)\n", - "```\n", - "\n", - " a) 17\n", - " b) 32\n", - " c) 16^2\n", - " d) 65" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "de4e32eb", - "metadata": {}, - "outputs": [], - "source": [ - "answer = \"x\" #Replace x with a, b, c, or d\n", - "q_1_check(answer)" - ] - }, - { - "cell_type": "markdown", - "id": "dbe373d1", - "metadata": {}, - "source": [ - "
\n", - "Question (Q2): How many integers are transferred between master and worker? Including both directions. \n", - "
\n", - "\n", - "\n", - "\n", - "```julia\n", - "a = rand(Int,4,4)\n", - "proc = 4\n", - "@fetchfrom proc sum(a[2,2]^2)\n", - "```\n", - "\n", - " a) 2\n", - " b) 17\n", - " c) 5\n", - " d) 32" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a234ceaa", - "metadata": {}, - "outputs": [], - "source": [ - "answer = \"x\" #Replace x with a, b, c, or d\n", - "q_2_check(answer)" - ] - }, - { - "cell_type": "markdown", - "id": "c561a73d", - "metadata": {}, - "source": [ - "\n", - "
\n", - "Question (Q3): Which value will be the value of `x` ? \n", - "
\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b71a2171", - "metadata": {}, - "outputs": [], - "source": [ - "a = zeros(Int,3)\n", - "proc = 3\n", - "@sync @spawnat proc a[2] = 2\n", - "x = a[2]\n", - "x" - ] - }, - { - "cell_type": "markdown", - "id": "835080aa", - "metadata": {}, - "source": [ - "
\n", - "Question (Q4): Which value will be the value of `x` ? \n", - "
\n", - "\n", - "Which value will be the value of `x` ?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b8678fd1", - "metadata": {}, - "outputs": [], - "source": [ - "a = zeros(Int,3)\n", - "proc = myid()\n", - "@sync @spawnat proc a[2] = 2\n", - "x = a[2]\n", - "x" - ] - }, - { - "cell_type": "markdown", - "id": "9e985c61", - "metadata": {}, - "source": [ - "## Remember: each process runs in a separated Julia instance\n", - "\n", - "In particular, this means that each process can load different functions or packages. In consequence, it is important to make sure that the code we run is defined in the corresponding process.\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "cdc07cba", - "metadata": {}, - "source": [ - "### Functions are defined in a single process\n", - "\n", - "This is a very common pitfall when running parallel code. If we define a function in a process, it is not automatically available in the other processes. This is illustrated in the next example. The remote call in the last line in next cell will fail since the function `sleep_ones` is only being defined in the local process. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4544ca4c", - "metadata": {}, - "outputs": [], - "source": [ - "function sleep_ones(m,n)\n", - " sleep(4)\n", - " ones(m,n)\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b54a1a84", - "metadata": {}, - "outputs": [], - "source": [ - "proc = 3\n", - "remotecall_fetch(sleep_ones,proc,3,4)" - ] - }, - { - "cell_type": "markdown", - "id": "9ec5d8fe", - "metadata": {}, - "source": [ - "To fix this, we can define the function on all processes with the `@everywhere` macro." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7eb1e210", - "metadata": {}, - "outputs": [], - "source": [ - "@everywhere function sleep_ones(m,n)\n", - " sleep(4)\n", - " ones(m,n)\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b55cbedf", - "metadata": {}, - "outputs": [], - "source": [ - "proc = 3\n", - "remotecall_fetch(sleep_ones,proc,3,4)" - ] - }, - { - "cell_type": "markdown", - "id": "9f71e7fd", - "metadata": {}, - "source": [ - "### Anonymous functions are available everywhere\n", - "\n", - "If a function has a name, Julia only sends the function name to the corresponding process. Then, Julia looks for the corresponding function code in the remote process and executes it. This is why the function needs to be defined also in the remote process. However, if a function is anonymous, Julia needs to send the complete function definition to the remote process. This is why anonymous functions do not need to be defined with the macro `@everywhere` to work in a remote call.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d9734913", - "metadata": {}, - "outputs": [], - "source": [ - "fun = (m,n) -> (sleep(4);ones(m,n))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f08131c3", - "metadata": {}, - "outputs": [], - "source": [ - "proc = 3\n", - "remotecall_fetch(fun,proc,3,4)" - ] - }, - { - "cell_type": "markdown", - "id": "ebec4f5e", - "metadata": {}, - "source": [ - "### Each proc uses packages independently\n", - "\n", - "When using a package in a process, it is not available in the other ones. For instance, if we load the `LinearAlgebra` package in the current process and use one of its exported functions in another process, we will get an error." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e0cf3545", - "metadata": {}, - "outputs": [], - "source": [ - "using LinearAlgebra" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0498748c", - "metadata": {}, - "outputs": [], - "source": [ - "@fetchfrom 3 norm([1,2,3])" - ] - }, - { - "cell_type": "markdown", - "id": "97462cbd", - "metadata": {}, - "source": [ - "To fix this, we can load the package on all processors with the `@everywhere` macro." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "14ee3498", - "metadata": {}, - "outputs": [], - "source": [ - "@everywhere using LinearAlgebra" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f8b3ba73", - "metadata": {}, - "outputs": [], - "source": [ - "@fetchfrom 3 norm([1,2,3])" - ] - }, - { - "cell_type": "markdown", - "id": "3743ae18", - "metadata": {}, - "source": [ - "### Each process has its own active package environment\n", - "\n", - "This is another very common source of errors. You can check that if you activate the current directory, this will have no effect in the other processes." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "abf3afe5", - "metadata": {}, - "outputs": [], - "source": [ - "] activate ." - ] - }, - { - "cell_type": "markdown", - "id": "4d8bfa90", - "metadata": {}, - "source": [ - "We have activated the current folder. Now let's see which is the active project in another process, say process 2. You will see that process 2 is provably still using the global package environment." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3c6f29fb", - "metadata": {}, - "outputs": [], - "source": [ - "@everywhere using Pkg" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ee04b3cc", - "metadata": {}, - "outputs": [], - "source": [ - "@spawnat 2 Pkg.status();" - ] - }, - { - "cell_type": "markdown", - "id": "8244ae89", - "metadata": {}, - "source": [ - "To fix this, you need to activate the current directory on all processes." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "99991a93", - "metadata": {}, - "outputs": [], - "source": [ - "@everywhere Pkg.activate(\".\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dfc8ea20", - "metadata": {}, - "outputs": [], - "source": [ - "@spawnat 2 Pkg.status();" - ] - }, - { - "cell_type": "markdown", - "id": "4bfbe073", - "metadata": {}, - "source": [ - "## Easy ways of parallelizing code\n", - "\n", - "A part from the low-level parallel routines we have seen so-far, Julia also provides much more simple ways to parallelizing loops and maps." - ] - }, - { - "cell_type": "markdown", - "id": "89e4b22b", - "metadata": {}, - "source": [ - "### Useful macro: @distributed\n", - "\n", - "This macro is used when we want to perform a very large for loops made of independent small iterations. To illustrate this, let's consider again the function that computes $\\pi$ with Leibniz formula." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f5e27d24", - "metadata": {}, - "outputs": [], - "source": [ - "function compute_π(n)\n", - " s = 1.0\n", - " for i in 1:n\n", - " s += (isodd(i) ? -1 : 1) / (i*2+1)\n", - " end\n", - " 4*s\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "d0921cb9", - "metadata": {}, - "source": [ - "Paralelizing this function might require some work with low-level functions like `remotecall`, but it is trivial using the macro `@distributed`. This macro runs the for loop using the available processes and optionally reduces the result using a given reduction function (`+` in this case)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d4efb990", - "metadata": {}, - "outputs": [], - "source": [ - "function compute_π_dist(n)\n", - " s = 1.0\n", - " r = @distributed (+) for i in 1:n\n", - " (isodd(i) ? -1 : 1) / (i*2+1)\n", - " end\n", - " 4*(s+r)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "8dbc2240", - "metadata": {}, - "source": [ - "Run next cell to measure the performance of the serial function for a large value of `n`. Run it at least 2 times to get rid of compilation times." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ec604251", - "metadata": {}, - "outputs": [], - "source": [ - "@time compute_π(4_000_000_000)" - ] - }, - { - "cell_type": "markdown", - "id": "c21b12b5", - "metadata": {}, - "source": [ - "Run next cell to measure the performance of the parallel function." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "60fca9ae", - "metadata": {}, - "outputs": [], - "source": [ - "@time compute_π_dist(4_000_000_000)" - ] - }, - { - "cell_type": "markdown", - "id": "f996ec0b", - "metadata": {}, - "source": [ - "### Useful function: `pmap`\n", - "\n", - "This function is used when we want to call a very expensive function a small number of evaluations and we want to distribute these evaluations over the available processes. To illustrate the usage of `pmap` consider the following example. Next cell generates sixty 30x30 matrices. The goal is to compute the singular value decomposition of all of them. This operation is known to be expensive for large matrices. Thus, this is a perfect scenario for `pmap`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3d137f35", - "metadata": {}, - "outputs": [], - "source": [ - "a = [ rand(300,300) for i in 1:60];" - ] - }, - { - "cell_type": "markdown", - "id": "b95d3558", - "metadata": {}, - "source": [ - "First, lets measure the serial performance" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9f6e302b", - "metadata": {}, - "outputs": [], - "source": [ - "using LinearAlgebra" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "74f16257", - "metadata": {}, - "outputs": [], - "source": [ - "@time svd.(a);" - ] - }, - { - "cell_type": "markdown", - "id": "25de96e4", - "metadata": {}, - "source": [ - "If we use `pmap` instead of broadcast, the different calls to `svd` will be distributed over the available processes." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4f9c5c2d", - "metadata": {}, - "outputs": [], - "source": [ - "@time pmap(svd,a);" - ] - }, - { - "cell_type": "markdown", - "id": "ad00cf8f", - "metadata": {}, - "source": [ - "## Summary\n", - "\n", - "We have seen the basics of distributed computing in Julia. The programming model is essentially an extension of tasks and channels to parallel computations on multiple machines. The low-level functions are `remotecall` and `RemoteChannel`, but there are other functions and macros like `pmap` and `@distributed` that simplify the implementation of parallel algorithms." - ] - }, - { - "cell_type": "markdown", - "id": "9a49ad48", - "metadata": {}, - "source": [ - "# License\n", - "\n", - "TODO: replace link to website\n", - "\n", - "This notebook is part of the course [Programming Large Scale Parallel Systems](http://localhost:8000/) at Vrije Universiteit Amsterdam and may be used under a [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8e36ae43", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Julia 1.9.1", - "language": "julia", - "name": "julia-1.9" - }, - "language_info": { - "file_extension": ".jl", - "mimetype": "application/julia", - "name": "julia", - "version": "1.9.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/src/notebooks/julia_distributed.md b/docs/src/notebooks/julia_distributed.md deleted file mode 100644 index d9bdc3e..0000000 --- a/docs/src/notebooks/julia_distributed.md +++ /dev/null @@ -1,30 +0,0 @@ -```@meta -EditURL = "https://github.com/fverdugo/XM_40017/blob/main/docs/src/notebooks/julia_distributed.ipynb" -``` - -```@raw html -
-
Tip
-
- -
-
-``` - -```@raw html - - -``` diff --git a/docs/src/notebooks/julia_intro.ipynb b/docs/src/notebooks/julia_intro.ipynb deleted file mode 100644 index 4034f11..0000000 --- a/docs/src/notebooks/julia_intro.ipynb +++ /dev/null @@ -1,746 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "ae2a0512", - "metadata": {}, - "source": [ - "\n", - "# Is Julia fast?" - ] - }, - { - "cell_type": "markdown", - "id": "e040636a", - "metadata": {}, - "source": [ - "## Contents\n", - "\n", - "With this notebook, you will learn\n", - "\n", - "- Two basic julia concepts related performance:\n", - " - type-inference\n", - " - JIT compilation\n", - "- Some Julia syntax\n", - "- Some useful Julia packages" - ] - }, - { - "cell_type": "markdown", - "id": "beef4d5e", - "metadata": {}, - "source": [ - "## Using Jupyter notebooks in Julia\n", - "\n", - "We are going to use Jupyter notebooks in this and other lectures. You provably have worked with notebooks (in Python). If not, here are the basic concepts you need to know to follow the lessons.\n", - "\n", - "
\n", - "Tip: Did you know that Jupyter stands for Julia, Python and R?\n", - "
\n", - "\n", - "### How to start a Jupyter nootebook in Julia\n", - "\n", - "To run a Julia Jupyther notebook, open a Julia REPL and type\n", - "\n", - "```julia\n", - "julia> ]\n", - "pkg> add IJulia\n", - "julia> using IJulia\n", - "julia> notebook()\n", - "```\n", - "A new browser window will open. Navigate to the corresponding notebook and open it.\n", - "\n", - "
\n", - "Warning: Make sure that the notebook is using the same Julia version as the one you used to launch `IJulia`. If it is not the same, go to Kernel > Change Kernel and choose the right version.\n", - "
\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "7cdd5195", - "metadata": {}, - "source": [ - "### Running a cell\n", - "\n", - "To run a cell, click on a cell and press `Shift` + `Enter`. You can also use the \"Run\" button in the toolbar above." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ec0f9041", - "metadata": {}, - "outputs": [], - "source": [ - "1+3\n", - "4*5" - ] - }, - { - "cell_type": "markdown", - "id": "a8474251", - "metadata": {}, - "source": [ - "As you can see from the output of previous cell, the value of the last line is displayed. We can suppress the output with a semicolon. Try it. Execute next cell." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fb3eaec4", - "metadata": {}, - "outputs": [], - "source": [ - "1+3\n", - "4*5;" - ] - }, - { - "cell_type": "markdown", - "id": "0823876a", - "metadata": {}, - "source": [ - "### Cell order is important\n", - "\n", - "Running the two cells below in reverse order won't work (try it). " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bc1da6cb", - "metadata": {}, - "outputs": [], - "source": [ - "foo() = \"Well done!\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ddea57d8", - "metadata": {}, - "outputs": [], - "source": [ - "foo()" - ] - }, - { - "cell_type": "markdown", - "id": "629189d9", - "metadata": {}, - "source": [ - "### REPL modes\n", - "\n", - "This is particular to Julia notebooks. You can use package, help, and shell mode just like in the Julia REPL." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "defa851f", - "metadata": {}, - "outputs": [], - "source": [ - "] add MPI" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1e6fbe66", - "metadata": {}, - "outputs": [], - "source": [ - "? print" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "123c0f6a", - "metadata": {}, - "outputs": [], - "source": [ - "; ls" - ] - }, - { - "cell_type": "markdown", - "id": "44c03fde", - "metadata": {}, - "source": [ - "## How fast is Julia code?" - ] - }, - { - "cell_type": "markdown", - "id": "f1e716db", - "metadata": {}, - "source": [ - "NB. Most of the examples below are taken from the lecture by S.G. Johnson at MIT. See here:\n", - "https://github.com/mitmath/18S096/blob/master/lectures/lecture1/Boxes-and-registers.ipynb" - ] - }, - { - "cell_type": "markdown", - "id": "6c088d9e", - "metadata": {}, - "source": [ - "### Example\n", - "\n", - "Sum entries of a given array $a = [a_1,a_2,...,a_n]$\n", - "\n", - " $$s = \\sum_{i=1}^n a_i$$\n", - "\n", - "### " - ] - }, - { - "cell_type": "markdown", - "id": "34e3c7dd", - "metadata": {}, - "source": [ - "### Hand-written sum function" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a672edac", - "metadata": { - "code_folding": [] - }, - "outputs": [], - "source": [ - "function sum_hand(a)\n", - " s = zero(eltype(a))\n", - " for ai in a\n", - " s += ai\n", - " end\n", - " s\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "0494a1b3", - "metadata": {}, - "source": [ - "### Test it\n", - "\n", - "The Julia macro `@test` which is provided in the `Test` package is useful to write (unit) tests in Julia." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ebac7b9e", - "metadata": {}, - "outputs": [], - "source": [ - "using Test" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "aa5505d4", - "metadata": {}, - "outputs": [], - "source": [ - "a = rand(5)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fc11923c", - "metadata": {}, - "outputs": [], - "source": [ - "@test sum_hand(a) ≈ sum(a)" - ] - }, - { - "cell_type": "markdown", - "id": "e23f3ddf", - "metadata": {}, - "source": [ - "## Benchmarking\n", - "\n", - "In Julia, the most straight-forward way of measuring the computation time of a piece of code is with the macro `@time`. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "caff0307", - "metadata": {}, - "outputs": [], - "source": [ - "a = rand(10^7);" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6d073236", - "metadata": {}, - "outputs": [], - "source": [ - "@time sum_hand(a)" - ] - }, - { - "cell_type": "markdown", - "id": "443ac8da", - "metadata": {}, - "source": [ - "Note that `@time` also measures the compile time of a function if it's the first call to that function. So make sure to run `@time` twice on a freshly compiled function in order to get a more meaningful result." - ] - }, - { - "cell_type": "markdown", - "id": "c664522e", - "metadata": {}, - "source": [ - "A part of getting rid of compilation time, one typically wants to measure the runtime several times and compute sole. To do this we can call our code in a for-loop and gather the runtimes using the Julia macro `@elapsed`. This measures the runtime of an expression in seconds, just as the `@time` macro, only `@elapsed` discards the result of the computation and returns the elapsed time instead." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bba864c1", - "metadata": {}, - "outputs": [], - "source": [ - "@elapsed sum_hand(a)" - ] - }, - { - "cell_type": "markdown", - "id": "70317d33", - "metadata": {}, - "source": [ - "## BenchmarkTools\n", - "\n", - "The `BenchmarkTools` extension package provides useful macros for sampling runtimes automatically. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "424ffe03", - "metadata": {}, - "outputs": [], - "source": [ - "using BenchmarkTools" - ] - }, - { - "cell_type": "markdown", - "id": "079ef7d0", - "metadata": {}, - "source": [ - "First of all, the `@benchmark` macro runs the code multiple times and gives out a lot of details: the minimum and maximum time, mean time, median time, number of samples taken, memory allocations, etc. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b5bb9a12", - "metadata": {}, - "outputs": [], - "source": [ - "bch_sum_hand = @benchmark sum_hand($a)" - ] - }, - { - "cell_type": "markdown", - "id": "c77d454b", - "metadata": {}, - "source": [ - "For quick sanity checks, one can use the `@btime` macro, which is a convenience wrapper around `@benchmark`. It returns only the minimum execution time and memory allocations. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fee5493d", - "metadata": {}, - "outputs": [], - "source": [ - "@btime sum_hand($a)" - ] - }, - { - "cell_type": "markdown", - "id": "3a1a774c", - "metadata": {}, - "source": [ - "Similar to the `@elapsed` macro, `BenchmarkTool`'s `@belapsed` discards the return value of the function and instead returns the minimum runtime in seconds. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8e0cdb8c", - "metadata": {}, - "outputs": [], - "source": [ - "@belapsed sum_hand($a)" - ] - }, - { - "cell_type": "markdown", - "id": "fb530b87", - "metadata": {}, - "source": [ - "As opposed to `@time` and `@elapsed`, `@btime` and `@belapsed` run the code several times and return the minimum runtime, thus eliminating possible compilation times from the measurement. " - ] - }, - { - "cell_type": "markdown", - "id": "782be14f", - "metadata": {}, - "source": [ - "### Built-in sum function" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "07df4939", - "metadata": {}, - "outputs": [], - "source": [ - "bch_sum = @benchmark sum($a)" - ] - }, - { - "cell_type": "markdown", - "id": "18972fa7", - "metadata": {}, - "source": [ - "### Hand-written sum in Python\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "038702c0", - "metadata": {}, - "outputs": [], - "source": [ - "using PyCall" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8e1dcbfa", - "metadata": {}, - "outputs": [], - "source": [ - "py\"\"\"\n", - "def sum_py_hand(A):\n", - " s = 0.0\n", - " for a in A:\n", - " s += a\n", - " return s\n", - "\"\"\"\n", - "sum_py_hand = py\"sum_py_hand\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "60ca4517", - "metadata": {}, - "outputs": [], - "source": [ - "@test sum(a) ≈ sum_py_hand(a)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2f009076", - "metadata": {}, - "outputs": [], - "source": [ - "bch_sum_py_hand = @benchmark sum_py_hand($a)" - ] - }, - { - "cell_type": "markdown", - "id": "22d29afa", - "metadata": {}, - "source": [ - "### Numpy sum " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0f7ffb8e", - "metadata": {}, - "outputs": [], - "source": [ - "using Conda" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e667fc80", - "metadata": {}, - "outputs": [], - "source": [ - "numpy = pyimport(\"numpy\")\n", - "sum_numpy = numpy[\"sum\"]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ee0361df", - "metadata": {}, - "outputs": [], - "source": [ - "@test sum_numpy(a) ≈ sum(a)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d2f4e9ea", - "metadata": {}, - "outputs": [], - "source": [ - "bch_sum_numpy = @benchmark sum_numpy($a)" - ] - }, - { - "cell_type": "markdown", - "id": "d7c3cbd6", - "metadata": {}, - "source": [ - "### Sumary of the results\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5ce4c97a", - "metadata": {}, - "outputs": [], - "source": [ - "timings = [bch_sum_hand,bch_sum,bch_sum_py_hand,bch_sum_numpy]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9d5caabd", - "metadata": {}, - "outputs": [], - "source": [ - "methods = [\"sum_hand\",\"sum\",\"sum_py_hand\",\"sum_numpy\"]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8b7a3661", - "metadata": {}, - "outputs": [], - "source": [ - "using DataFrames" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "82b11610", - "metadata": {}, - "outputs": [], - "source": [ - "df = DataFrame(method=methods,time=timings)" - ] - }, - { - "cell_type": "markdown", - "id": "f03c4281", - "metadata": {}, - "source": [ - "### Improving the hand-written sum in Julia\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7696719c", - "metadata": {}, - "outputs": [], - "source": [ - "# ✍️ Exercise 3\n", - "function sum_hand_fast(a)\n", - " s = 0.0\n", - " @simd for ai in a\n", - " s += ai\n", - " end\n", - " s\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0b461036", - "metadata": {}, - "outputs": [], - "source": [ - "@test sum_hand_fast(a) ≈ sum(a)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fc228cba", - "metadata": {}, - "outputs": [], - "source": [ - "@benchmark sum_hand_fast($a)" - ] - }, - { - "cell_type": "markdown", - "id": "328f9128", - "metadata": {}, - "source": [ - "## Conlcusions so far\n", - "\n", - "- Julia code (for loops) are much faster than in Python\n", - "- Julia code can be as fast as optimized C code" - ] - }, - { - "cell_type": "markdown", - "id": "f9ce5464", - "metadata": {}, - "source": [ - "## Why Julia is fast?\n", - "\n", - "- Julia is a compiled language (like C, C++, Fortran)\n", - "- Julia is JIT compiled (C, C++, Fortran are AOT compiled)\n", - "- Type declarations are optional in Julia\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "aa5254e4", - "metadata": {}, - "source": [ - "# Conclusion: Why we use Julia in this course\n", - "\n", - "- Julia code is fast (it can be as fast as C)\n", - "- Julia is a high-level language with simpler syntax than C \n", - "- Julia supports different parallel programming models\n", - "\n", - "We will look into the third point in a later section of this course. \n" - ] - }, - { - "cell_type": "markdown", - "id": "d4efafe2", - "metadata": {}, - "source": [ - "## Solution to the exercises\n", - "\n", - "### Solution to Exercise 1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "17f73c0f", - "metadata": {}, - "outputs": [], - "source": [ - "function sum_hand(a)\n", - " s = 0.0\n", - " for ai in a\n", - " s += ai\n", - " end\n", - " s\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "d12d6400", - "metadata": {}, - "source": [ - "### Solution to Exercise 2" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e39094da", - "metadata": {}, - "outputs": [], - "source": [ - "using Statistics\n", - "\n", - "a = rand(10^7)\n", - "num_it = 15\n", - "runtimes = zeros(num_it)\n", - "for i in 1:num_it\n", - " runtimes[i] = @elapsed sum_hand(a)\n", - "end\n", - "@show mean(runtimes) \n", - "@show std(runtimes)\n", - "@show minimum(runtimes)\n", - "@show maximum(runtimes);" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7943f1cc", - "metadata": {}, - "outputs": [], - "source": [ - "# ✍️ Exercise 3\n", - "function sum_hand_fast(a)\n", - " s = 0.0\n", - " @simd for ai in a\n", - " s += ai\n", - " end\n", - " s\n", - "end" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Julia 1.9.0", - "language": "julia", - "name": "julia-1.9" - }, - "language_info": { - "file_extension": ".jl", - "mimetype": "application/julia", - "name": "julia", - "version": "1.9.0" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/src/notebooks/julia_intro.md b/docs/src/notebooks/julia_intro.md deleted file mode 100644 index 2770451..0000000 --- a/docs/src/notebooks/julia_intro.md +++ /dev/null @@ -1,30 +0,0 @@ -```@meta -EditURL = "https://github.com/fverdugo/XM_40017/blob/main/docs/src/notebooks/julia_intro.ipynb" -``` - -```@raw html -
-
Tip
-
- -
-
-``` - -```@raw html - - -``` diff --git a/docs/src/notebooks/julia_jacobi.ipynb b/docs/src/notebooks/julia_jacobi.ipynb deleted file mode 100644 index 61425cc..0000000 --- a/docs/src/notebooks/julia_jacobi.ipynb +++ /dev/null @@ -1,652 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "2133c064", - "metadata": {}, - "source": [ - "\n", - "\n", - "### Programming large-scale parallel systems\n", - "### Lectures on Julia for HPC\n", - "\n", - "\n", - "# Distributed Jacobi (SOR) method\n", - "\n", - "by Francesc Verdugo (VU Amsterdam)\n", - "\n", - "Version fall 2022" - ] - }, - { - "cell_type": "markdown", - "id": "a7b64d5a", - "metadata": {}, - "source": [ - "## Contents\n", - "\n", - "- How to parallelize the Jacobi method in Julia" - ] - }, - { - "cell_type": "markdown", - "id": "8bfa86d6", - "metadata": {}, - "source": [ - "## Mathematical background\n", - "\n", - "\n", - "### 1D Laplace equation\n", - "\n", - "\n", - "Find a function $u(x)$ such that\n", - "\n", - "\n", - "$u''(x) = 0 $ for $x\\in(0,L)$\n", - "\n", - "$u(0) = -1$\n", - "\n", - "$u(L) = 1$\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "de45f04c", - "metadata": {}, - "source": [ - "### Analytical solution\n", - "\n", - "$u(x) = (x-L)/L$" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "bba2fb43", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "id": "392dd6e1", - "metadata": {}, - "source": [ - "### A 3D Laplace equation in a complex domain" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "e7d815e0", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "id": "f1450f45", - "metadata": {}, - "source": [ - "### The jacobi method" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "e794b036", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "bb20b40a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "jacobi (generic function with 1 method)" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "function jacobi(n,nsteps)\n", - " u = zeros(n+2)\n", - " u_new = zeros(n+2)\n", - " u[1] = -1\n", - " u[end] = 1\n", - " for istep in 1:nsteps\n", - " for i in 2:(n+1)\n", - " u_new[i] = 0.5*(u[i-1]+u[i+1])\n", - " end\n", - " u[2:end-1] = u_new[2:end-1]\n", - " end\n", - " u\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "9d251dba", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "7-element Vector{Float64}:\n", - " -1.0\n", - " -0.6666666666666666\n", - " -0.3333333333333333\n", - " 0.0\n", - " 0.3333333333333333\n", - " 0.6666666666666666\n", - " 1.0" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "jacobi(5,1000)" - ] - }, - { - "cell_type": "markdown", - "id": "dda71761", - "metadata": {}, - "source": [ - "## Parallelization strategy" - ] - }, - { - "cell_type": "markdown", - "id": "7c5a0d60", - "metadata": {}, - "source": [ - "### Data partition and dependencies\n", - "\n", - "`u_new[i] = 0.5*(u[i-1]+u[i+1])`" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "52626d50", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "id": "d5e089f4", - "metadata": {}, - "source": [ - "### \"Ghost\" entries" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "1dc26426", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "id": "acbbd646", - "metadata": {}, - "source": [ - "### Remote channels" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "08dab820", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "034fa536", - "metadata": {}, - "source": [ - "## Implementation" - ] - }, - { - "cell_type": "markdown", - "id": "a98e7ef1", - "metadata": {}, - "source": [ - "### Disclaimer\n", - "\n", - "The most natural way of implementing a distributed Jacobi method is to use a library that provides distributed arrays:\n", - "\n", - "- [DistributedArrays.jl](https://github.com/JuliaParallel/DistributedArrays.jl)\n", - "- [PartitionedArrays.jl](https://github.com/fverdugo/PartitionedArrays.jl)\n", - "\n", - "We will use low-level Julia functions as an academic exercise.\n" - ] - }, - { - "cell_type": "markdown", - "id": "5a2437ae", - "metadata": {}, - "source": [ - "### Adding some procs" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d95ff334", - "metadata": {}, - "outputs": [], - "source": [ - "using Distributed" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2d64e580", - "metadata": {}, - "outputs": [], - "source": [ - "addprocs(3)" - ] - }, - { - "cell_type": "markdown", - "id": "9cd52702", - "metadata": {}, - "source": [ - "### Helper functions" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4aad98a4", - "metadata": {}, - "outputs": [], - "source": [ - "@everywhere function initialize_vectors(n)\n", - " @assert mod(n,nworkers()) == 0\n", - " local_n = div(n,nworkers())\n", - " u = zeros(local_n+2)\n", - " u_new = zeros(local_n+2)\n", - " u[1] = -1.0\n", - " u[end] = 1.0\n", - " u,u_new\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3fceebbd", - "metadata": {}, - "outputs": [], - "source": [ - "@everywhere function compute_error(u,n)\n", - " local_n = length(u)-2\n", - " u_exact = copy(u)\n", - " p = myid() - 1\n", - " for local_i in 0:(local_n+1)\n", - " global_i = local_i + (p-1)*local_n + 1\n", - " Δ = 1/(n+1)\n", - " u_exact[local_i+1] = 2*(Δ*global_i-Δ)-1\n", - " end\n", - " maximum(abs.(u[2:end-1].-u_exact[2:end-1]))\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "fd06d27f", - "metadata": {}, - "source": [ - "### Distributed Jacobi" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "56d3bd49", - "metadata": {}, - "outputs": [], - "source": [ - "function jacobi_dist(n,nsteps)\n", - " nw = nworkers()\n", - " ftrs_prev_snd = Vector{Future}(undef,nw)\n", - " ftrs_next_snd = Vector{Future}(undef,nw)\n", - " fun = ()->Channel{Float64}(1)\n", - " for w in workers()\n", - " p = w-1\n", - " ftrs_prev_snd[p] = @spawnat w RemoteChannel(fun,myid())\n", - " ftrs_next_snd[p] = @spawnat w RemoteChannel(fun,myid())\n", - " end\n", - " ftrs = Vector{Future}(undef,nw)\n", - " @sync for w in workers()\n", - " ftrs[w-1] = @spawnat w jacobi_worker(\n", - " n,nsteps,ftrs_prev_snd,ftrs_next_snd)\n", - " end\n", - " reduce(max,fetch.(ftrs))\n", - "end\n" - ] - }, - { - "cell_type": "markdown", - "id": "ab2c25f3", - "metadata": {}, - "source": [ - "### Exercise: complete the cell below" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "739ccfa5", - "metadata": {}, - "outputs": [], - "source": [ - "@everywhere function jacobi_worker(\n", - " n,nsteps,ftrs_prev_snd,ftrs_next_snd)\n", - " u, u_new = initialize_vectors(n)\n", - " w = myid(); p = w-1\n", - " # TODO: Setup channels\n", - " #chn_prev_snd\n", - " #chn_prev_rcv\n", - " #chn_next_snd\n", - " #chn_next_rcv\n", - " for step in 1:nsteps \n", - " # TODO: Communicate\n", - " # Local computations\n", - " for i in 2:(length(u)-1)\n", - " u_new[i] = 0.5*(u[i-1]+u[i+1])\n", - " end\n", - " u[2:end-1] = u_new[2:end-1]\n", - " end\n", - " @show u\n", - " err = compute_error(u,n)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "edbb2318", - "metadata": {}, - "source": [ - "### Test-driven development" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b2301383", - "metadata": {}, - "outputs": [], - "source": [ - "err = jacobi_dist(6,1000)" - ] - }, - { - "cell_type": "markdown", - "id": "63ec8554", - "metadata": {}, - "source": [ - "### Remember: remote channels" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "d4f59c15", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "b2d8bdbc", - "metadata": {}, - "source": [ - "## Possible enhancements" - ] - }, - { - "cell_type": "markdown", - "id": "0af07603", - "metadata": {}, - "source": [ - "### Is it possible to overlap computation and communication?" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "d401c647", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "id": "80e67eb9", - "metadata": {}, - "source": [ - "### 2nd exercise:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f9aaa50c", - "metadata": {}, - "outputs": [], - "source": [ - "@everywhere function jacobi_worker(\n", - " n,nsteps,ftrs_prev_snd,ftrs_next_snd)\n", - " u, u_new = initialize_vectors(n)\n", - " p = myid()-1\n", - " chn_prev_snd = fetch(ftrs_prev_snd[p])\n", - " chn_next_snd = fetch(ftrs_next_snd[p])\n", - " if myid()!=2\n", - " chn_prev_rcv = fetch(ftrs_next_snd[p-1])\n", - " end\n", - " if myid() != nprocs()\n", - " chn_next_rcv = fetch(ftrs_prev_snd[p+1])\n", - " end\n", - " for step in 1:nsteps\n", - " # TODO overlap communication and computation\n", - " if myid() != 2\n", - " put!(chn_prev_snd,u[2])\n", - " u[1] = take!(chn_prev_rcv)\n", - " end\n", - " if myid() != nprocs()\n", - " put!(chn_next_snd,u[end-1])\n", - " u[end] = take!(chn_next_rcv)\n", - " end\n", - " for i in 2:(length(u)-1)\n", - " u_new[i] = 0.5*(u[i-1]+u[i+1])\n", - " end\n", - " u[2:end-1] = u_new[2:end-1]\n", - " end\n", - " @show u\n", - " err = compute_error(u,n)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "d9f12348", - "metadata": {}, - "source": [ - "## Solution of the exercises" - ] - }, - { - "cell_type": "markdown", - "id": "d7d067f8", - "metadata": {}, - "source": [ - "### 1st exercise" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0e2514b0", - "metadata": {}, - "outputs": [], - "source": [ - "@everywhere function jacobi_worker(\n", - " n,nsteps,ftrs_prev_snd,ftrs_next_snd)\n", - " u, u_new = initialize_vectors(n)\n", - " p = myid()-1\n", - " chn_prev_snd = fetch(ftrs_prev_snd[p])\n", - " chn_next_snd = fetch(ftrs_next_snd[p])\n", - " if myid()!=2\n", - " chn_prev_rcv = fetch(ftrs_next_snd[p-1])\n", - " end\n", - " if myid() != nprocs()\n", - " chn_next_rcv = fetch(ftrs_prev_snd[p+1])\n", - " end\n", - " for step in 1:nsteps \n", - " if myid() != 2\n", - " put!(chn_prev_snd,u[2])\n", - " u[1] = take!(chn_prev_rcv)\n", - " end\n", - " if myid() != nprocs()\n", - " put!(chn_next_snd,u[end-1])\n", - " u[end] = take!(chn_next_rcv)\n", - " end\n", - " # Local computations\n", - " for i in 2:(length(u)-1)\n", - " u_new[i] = 0.5*(u[i-1]+u[i+1])\n", - " end\n", - " u[2:end-1] = u_new[2:end-1]\n", - " end\n", - " @show u\n", - " err = compute_error(u,n)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "157e705d", - "metadata": {}, - "source": [ - "### 2nd exercise" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b6f5a89f", - "metadata": {}, - "outputs": [], - "source": [ - "@everywhere function jacobi_worker(\n", - " n,nsteps,ftrs_prev_snd,ftrs_next_snd)\n", - " u, u_new = initialize_vectors(n)\n", - " p = myid()-1\n", - " chn_prev_snd = fetch(ftrs_prev_snd[p])\n", - " chn_next_snd = fetch(ftrs_next_snd[p])\n", - " if myid()!=2\n", - " chn_prev_rcv = fetch(ftrs_next_snd[p-1])\n", - " end\n", - " if myid() != nprocs()\n", - " chn_next_rcv = fetch(ftrs_prev_snd[p+1])\n", - " end\n", - " for step in 1:nsteps \n", - " if myid() != 2\n", - " put!(chn_prev_snd,u[2])\n", - " t1 = @async u[1] = take!(chn_prev_rcv)\n", - " end\n", - " if myid() != nprocs()\n", - " put!(chn_next_snd,u[end-1])\n", - " t2 = @async u[end] = take!(chn_next_rcv)\n", - " end\n", - " # Local computations (interior)\n", - " for i in 3:(length(u)-2)\n", - " u_new[i] = 0.5*(u[i-1]+u[i+1])\n", - " end\n", - " # Wait for ghost values\n", - " if myid() != 2\n", - " wait(t1)\n", - " end\n", - " if myid() != nprocs()\n", - " wait(t2)\n", - " end\n", - " # Update near boundary values\n", - " for i in [2,length(u)-1]\n", - " u_new[i] = 0.5*(u[i-1]+u[i+1])\n", - " end\n", - " u[2:end-1] = u_new[2:end-1]\n", - " end\n", - " @show u\n", - " err = compute_error(u,n)\n", - "end" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Julia 1.9.0", - "language": "julia", - "name": "julia-1.9" - }, - "language_info": { - "file_extension": ".jl", - "mimetype": "application/julia", - "name": "julia", - "version": "1.9.0" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/src/notebooks/julia_jacobi.md b/docs/src/notebooks/julia_jacobi.md deleted file mode 100644 index d39ec11..0000000 --- a/docs/src/notebooks/julia_jacobi.md +++ /dev/null @@ -1,30 +0,0 @@ -```@meta -EditURL = "https://github.com/fverdugo/XM_40017/blob/main/docs/src/notebooks/julia_jacobi.ipynb" -``` - -```@raw html -
-
Tip
-
- -
-
-``` - -```@raw html - - -``` diff --git a/docs/src/notebooks/julia_tutorial.ipynb b/docs/src/notebooks/julia_tutorial.ipynb deleted file mode 100644 index 1c98c21..0000000 --- a/docs/src/notebooks/julia_tutorial.ipynb +++ /dev/null @@ -1,523 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "\n", - "\n", - "### Programming large-scale parallel systems\n", - "### Lectures on Julia for HPC\n", - "\n", - "\n", - "# Tutorial: Getting started with Julia\n", - "\n", - "by Francesc Verdugo (VU Amsterdam)\n", - "\n", - "Version fall 2022" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Contents\n", - "\n", - "- Using the Julia REPL\n", - "- Running serial and parallel code\n", - "- Installing and managing packages" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Introduction\n", - "\n", - "Julia has its own way of running code and using packages. Many educational sources about Julia assume that you have this basic knowledge, which can be confusing to new users. In this lesson, we will learn these basic skills so that you can start learning more on Julia." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Preliminaries\n", - "\n", - "This is a tutorial. To follow it:\n", - "\n", - "- Download and install Julia from https://julialang.org/\n", - "- Download and install VSCode and its Julia extension. How to: https://www.julia-vscode.org/docs/dev/gettingstarted/\n", - "- Continue reading this document \n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## The Julia REPL\n", - "\n", - "There are several ways of opening Julia depending on your operating system and your IDE, but it is usually as simple as launching the Julia app. With VSCode, open a folder (File > Open Folder). Then, press `Ctrl`+`Shift`+`P` to open the command bar, and execute `Julia: Start REPL`. If this does not work, make sure you have the Julia extension for VSCode installed. Independently of the method you use, opening Julia results in a window with some text ending with:\n", - "\n", - "```\n", - "julia> \n", - "```\n", - "\n", - "You have just opened the Julia *read-evaluate-print loop*, or simply the Julia *REPL*. Congrats! You will spend most of time using the REPL, when working in Julia. The REPL is a console waiting for user input. Just as in other consoles, the string of text right before the input area (`julia>` in the case) is called the *command prompt* or simply the *prompt*.\n", - "\n", - "\n", - "The usage is as follows:\n", - "- You write some input\n", - "- press enter\n", - "- you get the output\n", - "\n", - "For instance, try this\n", - "\n", - "```julia\n", - "julia> 1 + 1\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Hello world!\n", - "\n", - "A \"Hello world\" example looks like this in Julia\n", - "\n", - "```julia\n", - "julia> println(\"Hello, world!\")\n", - "```\n", - "\n", - "Try to run it in the REPL." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Help mode\n", - "\n", - "\n", - "\n", - "Curious about what function `println` does? Enter into *help* mode to look into the documentation. This is done by typing a question mark (`?`) into the inut field:\n", - "\n", - "```julia\n", - "julia> ?\n", - "```\n", - "\n", - "After typing `?`, the command prompt changes to `help?>`. It means we are in help mode. Now, we can type a function name to see its documentation.\n", - "\n", - "```julia\n", - "help?> println\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Package and shell modes\n", - "\n", - "The REPL comes with two more modes, namely *package* and *shell* modes. To enter package mode type\n", - "\n", - "```julia\n", - "julia> ]\n", - "```\n", - "\n", - "Package mode is used to install and manage packages. We are going to discuss the package mode in greater detail later. To return back to normal mode press the backspace key several times.\n", - "\n", - "To enter shell mode type semicolon (`;`)\n", - "```julia\n", - "julia> ;\n", - "```\n", - "The prompt should have changed to `shell>` indicating that we are in shell mode. Now you can type commands that you would normally do on your system command line. For instance, \n", - "\n", - "```julia\n", - "shell> ls\n", - "```\n", - "will display the contents of the current folder in Mac or Linux. Using shell mode in Windows is not straightforward, and thus not recommended for beginners.\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Running more complex code\n", - "\n", - "Real-world Julia programs are not typed in the REPL in practice. They are written in one or more files and *included* in the REPL. To try this, create a new file called `hello.jl`, write the code of the \"Hello world\" example above, and save it. If you are using VSCode, you can create the file using File > New File > Julia File. Once the file is saved with the name `hello.jl`, execute it as follows\n", - "\n", - "```julia\n", - "julia> include(\"hello.jl\")\n", - "```\n", - "\n", - "
\n", - "Warning: Make sure that the file \"hello.jl\" is located in the current working directory of your Julia session. You can query the current directory with function pwd(). You can change to another directory with function cd() if needed. Also, make sure that the file extension is .jl.\n", - "
\n", - "\n", - "The recommended way of running Julia code is using the REPL as we did. But it is also possible to run code directly from the system command line. To this end, open a terminal and call Julia followed buy the path to the file containing the code you want to execute.\n", - "\n", - "```\n", - "$ julia hello.jl\n", - "```\n", - "\n", - "Previous line assumes that you have Julia properly installed in the system and that is usable from the terminal. In UNIX systems (Linux and Mac), the Julia binary needs to be in one of the directories listed in the `PATH` environment variable. To check that Julia is properly installed, you can use\n", - "\n", - "```\n", - "$ julia --version\n", - "```\n", - "\n", - "If this runs without error and you see a version number, you are good to go!\n", - "\n", - "\n", - "
\n", - "Tip: In this tutorial, when a code snipped starts with $, it should be run in the terminal. Otherwise, the code is to be run in the Julia REPL.\n", - "
\n", - "\n", - "
\n", - "Tip: Avoid calling Julia code from the terminal, use the Julia REPL instead! Each time you call Julia from the terminal, you start a fresh Julia session and Julia will need to compile your code from scratch. This can be time consuming for large projects. In contrast, if you execute code in the REPL, Julia will compile code incrementally, which is much faster. Running code in a cluster (like in DAS-5 for the Julia assignment) is among the few situations you need to run Julia code from the terminal.\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Running parallel code\n", - "\n", - "\n", - "Since we are in a parallel computing course, let's run a parallel \"hello world\" example in Julia. Open a Julia REPL and write \n", - "\n", - "```julia\n", - "julia> using Distributed\n", - "julia> @everywhere println(\"Hello, world! I am proc $(myid()) from $(nprocs())\")\n", - "```\n", - "\n", - "Here, we are using the `Distributed` package, which is part of the Julia standard library that provides distributed memory parallel support. The code prints the process id and the number of processes in the current Julia session.\n", - "\n", - "You will provably only see output from 1 proces. We need to add more processes to run the example in parallel. This is done with the `addprocs` function.\n", - "\n", - "```julia\n", - "julia> addprocs(3)\n", - "```\n", - "We have added 3 new processes, plus the old one, we have 4 processes. Run the code again.\n", - "\n", - "```julia\n", - "julia> @everywhere println(\"Hello, world! I am proc $(myid()) from $(nprocs())\")\n", - "```\n", - "\n", - "Now, you should see output from 4 processes.\n", - "\n", - "It is possible to specify the number of processes when starting Julia from the terminal with the `-p` argument (useful, e.g., when running in a cluster). If you launch Julia from the terminal as\n", - "\n", - "```\n", - "$ julia -p 3\n", - "```\n", - "and then run \n", - "\n", - "```julia\n", - "julia> @everywhere println(\"Hello, world! I am proc $(myid()) from $(nprocs())\")\n", - "```\n", - "\n", - "You should get output from 4 processes as before.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Installing packages\n", - "\n", - "One of the most useful features of Julia is its package manager. It allows one to install Julia packages in a straightforward and platform independent way. To illustrate this, let us consider the following parallel \"Hello world\" example.\n", - "\n", - "\n", - "Copy the following block of code into a new file named `\"hello_mpi.jl\"`\n", - "\n", - "```julia\n", - "# file hello_mpi.jl\n", - "using MPI\n", - "MPI.Init()\n", - "comm = MPI.COMM_WORLD\n", - "rank = MPI.Comm_rank(comm)\n", - "nranks = MPI.Comm_size(comm)\n", - "println(\"Hello world, I am rank $rank of $nranks\")\n", - "```\n", - "\n", - "As you can see from this example, one can access MPI from Julia in a clean way, without type annotations and other complexities of C/C++ code.\n", - "\n", - "Now, run the file from the REPL\n", - "```julia\n", - "julia> incude(\"hello_mpi.jl\")\n", - "```\n", - "\n", - "It provably didn't work, right? Read the error message and note that the MPI package needs to be installed to run this code.\n", - "\n", - "To install a package, we need to enter *package* mode. Remember that we entered into help mode by typing `?`. Package mode is activated by typing `]`\n", - "```julia\n", - "julia> ]\n", - "```\n", - "At this point, the promp should have changed to `(@v1.8) pkg>` indicating that we are in package mode. The text between parenthesis indicates which is the active *project*, i.e., where packages are going to be installed. In this case, we are working with the global project associated with our Julia installation (which is Julia 1.8 in this example, but it can be another version in your case).\n", - "\n", - "To install the MPI package, type\n", - "```julia\n", - "(@v1.8) pkg> add MPI\n", - "```\n", - "Congrats, you have installed MPI!\n", - "\n", - "
\n", - " Tip: Many Julia package names end with .jl. This is just a way of signaling that a package is written in Julia. When using such packages, the .jl needs to be ommited. In this case, we have isntalled the MPI.jl package even though we have only typed MPI in the REPL.\n", - "
\n", - "\n", - "
\n", - " Tip:\n", - "The package you have installed it is the Julia interface to MPI, called MPI.jl. Note that it is not a MPI library by itself. It is just a thin wrapper between MPI and Julia. To use this interface, you need an actual MPI library installed in your system such as OpenMPI or MPICH. Julia downloads and installs a MPI library for you, but it is also possible to use a MPI library already available in your system. This is useful, e.g., when running on HPC clusters. See the documentation of MPI.jl for further details.\n", - "
\n", - "\n", - "To check that the package was installed properly, exit package mode by pressing the backspace key several times, and run it again\n", - "\n", - "```julia\n", - "julia> incude(\"hello_mpi.jl\")\n", - "```\n", - "\n", - "Now, it should work, but you provably get output from a single MPI rank only.\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Running MPI code\n", - "\n", - "To run MPI applications in parallel, you need a launcher like `mpiexec`. MPI codes written in Julia are not an exception to this rule. From the system terminal, you can run\n", - "```\n", - "$ mpiexec -np 4 julia hello_mpi.jl\n", - "```\n", - "But it will provably don't work since the version of `mpiexec` needs to match with the MPI version we are using from Julia. You can find the path to the `mpiexec` binary you need to use with these commands\n", - "\n", - "```julia\n", - "julia> using MPI\n", - "julia> MPI.mpiexec_path\n", - "```\n", - "\n", - "and then try again\n", - "```\n", - "$ /path/to/my/mpiexec -np 4 julia hello_mpi.jl\n", - "```\n", - "with your particular path.\n", - "\n", - "However, this is not very convenient. Don't worry if you could not make it work! A more elegant way to run MPI code is from the Julia REPL directly, by using these commands:\n", - "```julia\n", - "julia> using MPI\n", - "julia> mpiexec(cmd->run(`$cmd -np 4 julia hello_mpi.jl`))\n", - "```\n", - "\n", - "Now, you should see output from 4 ranks." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Installing packages locally\n", - "\n", - "We have installed the `MPI` package globally and it will be available in all Julia sessions. However, in some situations, we want to work with different versions of the same package or to install packages in an isolated way to avoid potential conflicts with other packages. This can be done by using local projects.\n", - "\n", - "A project is simply a folder in the hard disk. To use a particular folder as your project, you need to *activate* it. This is done by entering package mode and using the `activate` command followed by the path to the folder you want to activate.\n", - "```julia\n", - "(@v1.8) pkg> activate .\n", - "```\n", - " Previous command will activate the current working directory. Note that the dot `.` is indeed the path to the current folder.\n", - " \n", - "The prompt has changed to `(lessons) pkg>` indicating that we are in the project within the `lessons` folder. The particular folder name can be different in your case.\n", - "\n", - "
\n", - " Tip: You can activate a project directly when opening Julia from the terminal using the --project flag. The command $ julia --project=. will open Julia and activate a project in the current directory. You can also achieve the same effect by setting the environment variable JULIA_PROJECT with the path of the folder you want to activate.\n", - "
\n", - " \n", - " \n", - "
\n", - " Tip: The active project folder and the current working directory are two independent concepts! For instance, (@v1.8) pkg> activate folderB and then julia> cd(\"folderA\"), will activate the project in folderB and change the current working directory to folderA.\n", - " \n", - "
\n", - "\n", - "At this point all package-related operations will be local to the new project. For instance, install the `DataFrames` package.\n", - "\n", - "```julia\n", - "(lessons) pkg> add DataFrames\n", - "```\n", - "Use the package to check that it is installed\n", - "\n", - "```julia\n", - "julia> using DataFrames\n", - "julia> DataFrame(a=[1,2],b=[3,4])\n", - "```\n", - "Now, we can return to the global project to check that `DataFrames` has not been installed there. To return to the global environment, use `activate` without a folder name.\n", - "\n", - "```julia\n", - "(lessons) pkg> activate\n", - "```\n", - "The prompt is again `(@v1.8) pkg>`\n", - "\n", - "Now, try to use `DataFrames`.\n", - "\n", - "```julia\n", - "julia> using DataFrames\n", - "julia> DataFrame(a=[1,2],b=[3,4])\n", - "```\n", - "You should get an error or a warning unless you already had `DataFrames` installed globally.\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Project and Manifest files\n", - "\n", - "The information about a project is stored in two files `Project.toml` and `Manifest.toml`.\n", - "\n", - "- `Project.toml` contains the packages explicitly installed (the direct dependencies)\n", - "\n", - "- `Manifest.toml` contains direct and indirect dependencies along with the concrete version of each package.\n", - "\n", - "\n", - "In other words, `Project.toml` contains the packages relevant for the user, whereas `Manifest.toml` is the detailed snapshot of all dependencies. The `Manifest.toml` can be used to reproduce the same envinonment in another machine.\n", - "\n", - "You can see the path to the current `Project.toml` file by using the `status` operator (or `st` in its short form) while in package mode\n", - "\n", - "```julia\n", - "(@v1.8) pkg> status\n", - "```\n", - "\n", - "The information about the `Manifest.toml` can be inspected by passing the `-m` flag.\n", - "\n", - "```julia\n", - "(@v1.8) pkg> status -m\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Installing packages from a project file\n", - "\n", - "Project files can be used to install lists of packages defined by others. E.g., to install all the dependencies of a Julia application.\n", - "\n", - "Assume that a colleague has sent to you a `Project.toml` file with this content:\n", - "\n", - "```\n", - "[deps]\n", - "BenchmarkTools = \"6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf\"\n", - "DataFrames = \"a93c6f00-e57d-5684-b7b6-d8193f3e46c0\"\n", - "MPI = \"da04e1cc-30fd-572f-bb4f-1f8673147195\"\n", - "```\n", - "\n", - "Copy the contents of previous code block into a file called `Project.toml` and place it in an empty folder named `newproject`. It is important that the file is named `Project.toml`. You can create a new folder from the REPL with \n", - "\n", - "```julia\n", - "julia> mkdir(\"newproject\")\n", - "```\n", - "\n", - "To install all the packages registered in this file you need to activate the folder containing your `Project.toml` file\n", - "```julia\n", - "(@v1.8) pkg> activate newproject\n", - "```\n", - "and then *instantiating* it\n", - "```julia\n", - "(newproject) pkg> instantiate\n", - "```\n", - "\n", - "The instantiate command will download and install all listed packages and their dependencies in just one click." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Getting help in package mode\n", - "\n", - "You can get help about a particular package operator by writing `help` in front of it\n", - "\n", - "```julia\n", - "(@v1.8) pkg> help activate\n", - "```\n", - "\n", - "You can get an overview of all package commands by typing `help` alone\n", - "```julia\n", - "(@v1.8) pkg> help\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Package operations in Julia code\n", - "\n", - "In some situations it is required to use package commands in Julia code, e.g., to automatize installation and deployment of Julia applications. This can be done using the `Pkg` package. For instance\n", - "\n", - "```julia\n", - "julia> using Pkg\n", - "julia> Pkg.status()\n", - "```\n", - "is equivalent to call `status` in package mode.\n", - "```julia\n", - "(@v1.8) pkg> status\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Conclusion\n", - "\n", - "We have learned the basics of how to work with Julia. Now, you should be ready to start learning more on the language. If you want to further dig into the topics we have covered here, you can take a look and the following links\n", - "\n", - "- Julia Manual https://docs.julialang.org/en/v1/manual/getting-started/\n", - "- Package manager https://pkgdocs.julialang.org/v1/getting-started/\n", - "\n", - "If you want to interact with the Julia community on discourse, sign in at https://discourse.julialang.org/" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# License\n", - "\n", - "TODO: replace link to website\n", - "\n", - "This notebook is part of the course [Programming Large Scale Parallel Systems](http://localhost:8000/) at Vrije Universiteit Amsterdam and may be used under a [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Julia 1.8.5", - "language": "julia", - "name": "julia-1.8" - }, - "language_info": { - "file_extension": ".jl", - "mimetype": "application/julia", - "name": "julia", - "version": "1.8.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/src/notebooks/julia_tutorial.md b/docs/src/notebooks/julia_tutorial.md deleted file mode 100644 index 0e7850f..0000000 --- a/docs/src/notebooks/julia_tutorial.md +++ /dev/null @@ -1,30 +0,0 @@ -```@meta -EditURL = "https://github.com/fverdugo/XM_40017/blob/main/docs/src/notebooks/julia_tutorial.ipynb" -``` - -```@raw html -
-
Tip
-
- -
-
-``` - -```@raw html - - -``` diff --git a/docs/src/notebooks/matrix_matrix.ipynb b/docs/src/notebooks/matrix_matrix.ipynb deleted file mode 100644 index 242fb63..0000000 --- a/docs/src/notebooks/matrix_matrix.ipynb +++ /dev/null @@ -1,1148 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "bea76753", - "metadata": {}, - "source": [ - "\n", - "\n", - "### Programming large-scale parallel systems" - ] - }, - { - "cell_type": "markdown", - "id": "038e5442", - "metadata": {}, - "source": [ - "# Matrix-matrix multiplication" - ] - }, - { - "cell_type": "markdown", - "id": "f70e2f35", - "metadata": {}, - "source": [ - "## Contents\n", - "\n", - "In this notebook, we will:\n", - "\n", - "- Parallelize a simple algorithm\n", - "- Study the performance of different parallelization strategies\n", - "- Implement them using Julia" - ] - }, - { - "cell_type": "markdown", - "id": "480af594", - "metadata": {}, - "source": [ - "
\n", - "Note: Do not forget to execute the cells below before starting this notebook! \n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b4d5f1a1", - "metadata": {}, - "outputs": [], - "source": [ - "] add BenchmarkTools" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2f8ba040", - "metadata": {}, - "outputs": [], - "source": [ - "using Distributed\n", - "using BenchmarkTools\n", - "using Printf\n", - "if procs() == workers()\n", - " addprocs(4)\n", - "end\n", - "function answer_checker(answer,solution)\n", - " if answer == solution\n", - " \"🥳 Well done! \"\n", - " else\n", - " \"It's not correct. Keep trying! 💪\"\n", - " end |> println\n", - "end\n", - "alg_1_deps_check(answer) = answer_checker(answer,\"b\")\n", - "alg_1_comm_overhead_check(answer) = answer_checker(answer, \"c\")\n", - "alg_1_comp_check(answer) = answer_checker(answer, \"a\")\n", - "alg_2_complex_check(answer) = answer_checker(answer, \"b\")\n", - "alg_2_deps_check(answer) = answer_checker(answer,\"d\")\n", - "alg_3_deps_check(answer) = answer_checker(answer, \"c\")\n", - "alg_3_complex_check(answer) = answer_checker(answer, \"d\")" - ] - }, - { - "cell_type": "markdown", - "id": "96d2693d", - "metadata": {}, - "source": [ - "## Problem Statement\n", - "\n", - "Let us consider the (dense) matrix-matrix product `C=A*B`." - ] - }, - { - "cell_type": "markdown", - "id": "88bc2633", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "id": "a358ee60", - "metadata": {}, - "source": [ - "### Goals\n", - "\n", - "We want to\n", - "\n", - "- compute the product in parallel using more than one process (distributed implementation)\n", - "- study the performance of different parallelization alternatives\n", - "- implement the algorithms using Julia\n" - ] - }, - { - "cell_type": "markdown", - "id": "495ef679", - "metadata": {}, - "source": [ - "### Assumptions\n", - "\n", - "- All matrices `A`,`B`, and `C` are initially stored in the master process\n", - "- The result will be overwritten in `C`" - ] - }, - { - "cell_type": "markdown", - "id": "5828e243", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "id": "c57f340c", - "metadata": {}, - "source": [ - "### Steps\n", - "\n", - "To develop and study the parallel implementation, we will follow these steps:\n", - "\n", - "- Identify the parts of the sequential algorithm that can be parallelized\n", - "- Consider different parallelization strategies\n", - "- Discuss the (theoretical) performance of these implementations\n" - ] - }, - { - "cell_type": "markdown", - "id": "ca56a7fe", - "metadata": {}, - "source": [ - "## Serial implementation\n", - "\n", - "We start by considering the (naive) sequential algorithm:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "af8dfb37", - "metadata": {}, - "outputs": [], - "source": [ - "@everywhere function matmul_seq!(C,A,B)\n", - " m = size(C,1)\n", - " n = size(C,2)\n", - " l = size(A,2)\n", - " @assert size(A,1) == m\n", - " @assert size(B,2) == n\n", - " @assert size(B,1) == l\n", - " z = zero(eltype(C))\n", - " for j in 1:n\n", - " for i in 1:m\n", - " Cij = z\n", - " for k in 1:l\n", - " @inbounds Cij += A[i,k]*B[k,j]\n", - " end\n", - " C[i,j] = Cij\n", - " end\n", - " end\n", - " C\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "f967d2ea", - "metadata": {}, - "source": [ - "
\n", - "Note: The matrix-matrix multiplication naively implemented with 3 nested loops as above is known to be very inefficient (memory bound). Libraries such as BLAS provide much more efficient implementations, which are the ones used in practice (e.g., by the `*` operator in Julia). We consider, our hand-written implementation as a simple way of expressing the algorithm we are interested in.\n", - "
\n", - "\n", - "Run the following cell to compare the performance of our hand-written function with respect to the built in function `mul!`\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "725387f6", - "metadata": {}, - "outputs": [], - "source": [ - "using LinearAlgebra\n", - "N = 1000\n", - "A = rand(N,N)\n", - "B = rand(N,N)\n", - "C = rand(N,N)\n", - "@btime matmul_seq!(C,A,B)\n", - "@btime mul!(C,A,B);" - ] - }, - { - "cell_type": "markdown", - "id": "0eedd28a", - "metadata": {}, - "source": [ - "### Where do we can exploit parallelism?\n", - "\n", - "Look at the three nested loops in the sequential implementation:\n", - "\n", - "```julia\n", - "for j in 1:n\n", - " for i in 1:m\n", - " Cij = z\n", - " for k in 1:l\n", - " @inbounds Cij += A[i,k]*B[k,j]\n", - " end\n", - " C[i,j] = Cij\n", - " end\n", - "end\n", - "```\n", - "- Loops over `i` and `j` are trivially parallelizable.\n", - "- The loop over `k` can be parallelized but it requires a reduction." - ] - }, - { - "cell_type": "markdown", - "id": "b50aecff", - "metadata": {}, - "source": [ - "### Parallel algorithms\n", - "\n", - "All the entries of matrix C can be potentially computed in parallel, but *is it the most efficient solution to solve all these entries in parallel in a distributed system?* To find this we will consider different parallelization strategies:\n", - "\n", - "- Algorithm 1: each worker computes a single entry of C\n", - "- Algorithm 2: each worker computes a single row of C\n", - "- Algorithm 3: each worker computes a block rows of C" - ] - }, - { - "cell_type": "markdown", - "id": "6a706283", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "id": "bd759cb2", - "metadata": {}, - "source": [ - "## Parallel algorithm 1" - ] - }, - { - "cell_type": "markdown", - "id": "bdae0a02", - "metadata": {}, - "source": [ - "### Data dependencies\n", - "\n", - "Moving data through the network is expensive and reducing data movement is one of the key points in a distributed algorithm. To this end, we determine which is the minimum data needed by a worker to perform its computations.\n", - "\n", - "In algorithm 1, each worker computes only an entry of the result matrix C." - ] - }, - { - "cell_type": "markdown", - "id": "acfb354b", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "be3c4a01", - "metadata": {}, - "source": [ - "
\n", - "Question: Which are the data dependencies of the computations done by the worker in charge of computing entry C[i,j] ? \n", - "
\n", - "\n", - " a) column A[:,i] and row B[j,:]\n", - " b) row A[i,:] and column B[:,j]\n", - " c) the whole matrices A and B\n", - " d) row A[i,:] and the whole matrix B" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a8b7d1e1", - "metadata": {}, - "outputs": [], - "source": [ - "answer = \"x\" # replace x with a, b, c, or d \n", - "alg_1_deps_check(answer)" - ] - }, - { - "cell_type": "markdown", - "id": "06e1977a", - "metadata": {}, - "source": [ - "### Implementation\n", - "\n", - "Taking into account the data dependencies, the parallel algorithm 1 can be efficiently implemented following these steps from the worker perspective:\n", - "\n", - "1. The worker receives the corresponding row A[i,:] and column B[:,j] from the master process\n", - "2. The worker computes the dot product of A[i,:] and B[:,j]\n", - "3. The worker sends back the result of C[i,j] to the master process" - ] - }, - { - "cell_type": "markdown", - "id": "70087bce", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "9d22ccea", - "metadata": {}, - "source": [ - "A possible implementation of this algorithm in Julia is as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e4697fda", - "metadata": {}, - "outputs": [], - "source": [ - "function matmul_dist_1!(C, A, B)\n", - " m = size(C,1)\n", - " n = size(C,2)\n", - " l = size(A,2)\n", - " @assert size(A,1) == m\n", - " @assert size(B,2) == n\n", - " @assert size(B,1) == l\n", - " z = zero(eltype(C))\n", - " @assert nworkers() == m*n\n", - " iw = 0 \n", - " @sync for j in 1:n\n", - " for i in 1:m\n", - " Ai = A[i,:]\n", - " Bj = B[:,j]\n", - " iw += 1\n", - " w = workers()[iw]\n", - " ftr = @spawnat w begin\n", - " Cij = z\n", - " for k in 1:l\n", - " @inbounds Cij += Ai[k]*Bj[k]\n", - " end\n", - " Cij\n", - " end\n", - " @async C[i,j] = fetch(ftr)\n", - " end\n", - " end\n", - " C\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "f0e5a38b", - "metadata": {}, - "source": [ - "You can execute the following cells to test this implementation." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "13920a31", - "metadata": {}, - "outputs": [], - "source": [ - "using Test\n", - "N = 2\n", - "A = rand(N,N)\n", - "B = rand(N,N)\n", - "C = similar(A)\n", - "@test matmul_dist_1!(C,A,B) ≈ A*B" - ] - }, - { - "cell_type": "markdown", - "id": "f69d3333", - "metadata": {}, - "source": [ - "### Performance\n", - "\n", - "Let us study the performance of this algorithm. To this end, we will analyze if algorithm 1 is able to achieve the optimal parallel *speedup*. The parallel speedup on $P$ processes is defined as \n", - "\n", - "$$\n", - "S_P = \\frac{T_1}{T_P},\n", - "$$\n", - "\n", - "where $T_1$ denotes the runtime of the sequential algorithm on one node and $T_P$ denotes the runtime of the parallel algorithm on $P$ processes. If we run an optimal parallel algorithm with $P$ processes we expect it to run $p$ times faster than the sequential implementation. I.e., the *optimal* speedup of a parallel algorithm on $p$ processes is equal to $P$:\n", - "\n", - "$$\n", - "S^{*}_p = P.\n", - "$$\n", - "\n", - "The ratio of the actual speedup over the optimal one is called the parallel efficiency\n", - "\n", - "$$\n", - "E_p = \\frac{S_p}{S^{*}_p} = \\frac{T_1/T_P}{P}.\n", - "$$\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "b8eb224d", - "metadata": {}, - "source": [ - "### Experimental speedup\n", - "\n", - "The following cell measures the speedup of parallel algorithm 1. Do we achieve the optimal speedup?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cc698aa8", - "metadata": {}, - "outputs": [], - "source": [ - "N = 2\n", - "A = rand(N,N)\n", - "B = rand(N,N)\n", - "C = similar(A)\n", - "T1 = @belapsed matmul_seq!(C,A,B)\n", - "C = similar(A)\n", - "TP = @belapsed matmul_dist_1!(C,A,B)\n", - "P = nworkers()\n", - "println(\"Speedup = \", T1/TP)\n", - "println(\"Optimal speedup = \", P)\n", - "println(\"Efficiency = \", 100*(T1/TP)/P, \"%\")" - ] - }, - { - "cell_type": "markdown", - "id": "dac6a50b", - "metadata": {}, - "source": [ - "### Communication overhead\n", - "\n", - "Since communication is usually the main bottleneck in a distributed algorithm, we want to reduce the amount of communication per unit of computation in a worker. Let us compute the (theoretical) communication overhead for algorithm 1. This will help us understand why the speedup of this algorithm was so bad.\n", - "\n", - "Remember, algorithm 1 consisted of these main steps:\n", - "\n", - "1. The worker receives the corresponding row A[i,:] and column B[:,j] from the master process\n", - "2. The worker computes the dot product of A[i,:] and B[:,j]\n", - "3. The worker sends back the result of C[i,j] to the master process\n", - "\n", - "
\n", - "Question: How many scalars are communicated from and to a worker? Assume that matrices A, B, and C are N by N matrices.\n", - "
\n", - "\n", - " a) 3N\n", - " b) 2N + 2\n", - " c) 2N + 1\n", - " d) N² + 1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e78cbc7b", - "metadata": {}, - "outputs": [], - "source": [ - "answer = \"x\" # replace x with a, b, c, or d \n", - "alg_1_comm_overhead_check(answer)" - ] - }, - { - "cell_type": "markdown", - "id": "b27a4d3f", - "metadata": {}, - "source": [ - "
\n", - "Question: How many operations are done in a worker? \n", - "
\n", - "\n", - " a) O(N)\n", - " b) O(N²)\n", - " c) O(N³)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fcc9b903", - "metadata": {}, - "outputs": [], - "source": [ - "answer = \"x\" # replace x with a, b, or c\n", - "alg_1_comp_check(answer)" - ] - }, - { - "cell_type": "markdown", - "id": "d4c301de", - "metadata": {}, - "source": [ - "From these results we can conclude:\n", - "\n", - "- The communication complexity is O(N)\n", - "- The computation complexity is O(N)\n", - "- The ratio communication over computation (the communication overhead) is O(1)\n", - "\n", - "In other words, the communication cost is of the same order of magnitude as the computation cost. Since, communication is orders of magnitude slower in real systems, the runtime in the worker will be dominated by communication. This explains why we obtained such a bad speedup.\n" - ] - }, - { - "cell_type": "markdown", - "id": "b15cbaf4", - "metadata": {}, - "source": [ - "## Parallel algorithm 2\n", - "\n", - "Let us study the next algorithm to see if we can improve the efficiency by augmenting the granularity (i.e. the amount of work) in each parallel task. In parallel algorithm 2, each worker computes an entire row of C.\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "62e5c637", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "fa6cad0e", - "metadata": {}, - "source": [ - "### Data dependencies" - ] - }, - { - "cell_type": "markdown", - "id": "d4312f2c", - "metadata": {}, - "source": [ - "
\n", - "Question: Which are the data dependencies of the computations done by the worker in charge of computing row C[i,:] ? \n", - "
\n", - "\n", - " a) column A[:,i] and row B[j,:]\n", - " b) row A[i,:] and column B[:,j]\n", - " c) the whole matrices A and B\n", - " d) row A[i,:] and the whole matrix B" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cdb46cd8", - "metadata": {}, - "outputs": [], - "source": [ - "answer = \"x\" # replace x with a, b, c, or d \n", - "alg_2_deps_check(answer)" - ] - }, - { - "cell_type": "markdown", - "id": "a9d84ac2", - "metadata": {}, - "source": [ - "### Implementation\n", - "\n", - "These are the main steps of the implementation of algorithm 2:\n", - "\n", - "1. The worker receives the corresponding row A[i,:] and matrix B from the master process\n", - "2. The worker computes the product of row A[i,:] times B\n", - "3. The worker sends back the result of row C[i,:] to the master process\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "fb6b572b", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "id": "a662afa4", - "metadata": {}, - "source": [ - "A possible implementation of this algorithm in Julia is as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "365dc58e", - "metadata": {}, - "outputs": [], - "source": [ - "function matmul_dist_2!(C, A, B)\n", - " m = size(C,1)\n", - " n = size(C,2)\n", - " l = size(A,2)\n", - " @assert size(A,1) == m\n", - " @assert size(B,2) == n\n", - " @assert size(B,1) == l\n", - " z = zero(eltype(C))\n", - " @assert nworkers() == m\n", - " iw = 0\n", - " @sync for i in 1:m\n", - " Ai = A[i,:]\n", - " iw += 1\n", - " w = workers()[iw]\n", - " ftr = @spawnat w begin\n", - " Ci = fill(z,l)\n", - " for j in 1:n\n", - " for k in 1:l\n", - " @inbounds Ci[j] += Ai[k]*B[k,j]\n", - " end\n", - " end\n", - " Ci\n", - " end\n", - " @async C[i,:] = fetch(ftr)\n", - " end\n", - " C\n", - " end" - ] - }, - { - "cell_type": "markdown", - "id": "8de835b9", - "metadata": {}, - "source": [ - "Test it using next cell" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "267ac8b2", - "metadata": {}, - "outputs": [], - "source": [ - "using Test\n", - "N = 4\n", - "A = rand(N,N)\n", - "B = rand(N,N)\n", - "C = similar(A)\n", - "@test matmul_dist_2!(C,A,B) ≈ A*B" - ] - }, - { - "cell_type": "markdown", - "id": "f1f30faf", - "metadata": {}, - "source": [ - "### Experimental speedup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fe42f069", - "metadata": {}, - "outputs": [], - "source": [ - "N = 4\n", - "A = rand(N,N)\n", - "B = rand(N,N)\n", - "C = similar(A)\n", - "T1 = @belapsed matmul_seq!(C,A,B)\n", - "C = similar(A)\n", - "TP = @belapsed matmul_dist_2!(C,A,B)\n", - "P = nworkers()\n", - "println(\"Speedup = \", T1/TP)\n", - "println(\"Optimal speedup = \", P)\n", - "println(\"Efficiency = \", 100*(T1/TP)/P, \"%\")" - ] - }, - { - "cell_type": "markdown", - "id": "e2c6f60a", - "metadata": {}, - "source": [ - "### Complexity\n", - "\n", - "The speedup is still far from the optimal one. Let us study the communication overhead for this algorithm. Remember, algorithm 2 consists in these main steps:\n", - "\n", - "1. The worker receives the corresponding row A[i,:] and matrix B from the master process\n", - "2. The worker computes the product of row A[i,:] times B\n", - "3. The worker sends back the result of row C[i,:] to the master process\n", - "\n", - "
\n", - "Question: Which is the complexity of the communication and computations done by a worker in algorithm 2?\n", - "
\n", - "\n", - " a) O(N) communication and O(N^2) computation\n", - " b) O(N^2) communication and O(N^2) computation\n", - " c) O(N^3) communication and O(N^3) computation\n", - " d) O(N) communication and O(N) computation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1bf8feff", - "metadata": {}, - "outputs": [], - "source": [ - "answer = \"x\" # replace x with a, b, c, or d \n", - "alg_2_complex_check(answer)" - ] - }, - { - "cell_type": "markdown", - "id": "a2038e04", - "metadata": {}, - "source": [ - "The communication and computation cost are still of the same order of magnitude even though we have increased the grain size. " - ] - }, - { - "cell_type": "markdown", - "id": "71088fb9", - "metadata": {}, - "source": [ - "## Parallel algorithm 3\n", - "\n", - "Let us increase even more the granularity of the parallel tasks by computing several rows of C in a worker. Each worker computes N/P consecutive rows of C, where P is the number of workers.\n" - ] - }, - { - "cell_type": "markdown", - "id": "f1b8c712", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "4d456fed", - "metadata": {}, - "source": [ - "### Data dependencies" - ] - }, - { - "cell_type": "markdown", - "id": "67b65ea6", - "metadata": {}, - "source": [ - "
\n", - "Question: Which are the data dependencies of the computations done by the worker in charge of computing the range of rows C[rows,:] ? \n", - "
\n", - "\n", - "\n", - " a) A[rows,:] and B[:,rows] \n", - " b) the whole matrix A and B[:,rows]\n", - " c) A[rows,:] and the whole matrix B\n", - " d) the whole matrices A and B" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2825385a", - "metadata": {}, - "outputs": [], - "source": [ - "answer = \"x\" # replace x with a, b, c, or d \n", - "alg_3_deps_check(answer)" - ] - }, - { - "cell_type": "markdown", - "id": "429faa32", - "metadata": {}, - "source": [ - "### Implementation\n", - "\n", - "These are the main steps of the implementation of algorithm 3:\n", - "\n", - "1. The worker receives the corresponding rows A[rows,:] and matrix B from the master process\n", - "2. The worker computes the product of A[rows,:] times B\n", - "3. The worker sends back the result of C[rows,:] to the master process" - ] - }, - { - "cell_type": "markdown", - "id": "c14ebcb3", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "7d7952c1", - "metadata": {}, - "source": [ - "The implementation of this variant is let as an exercise (see below)." - ] - }, - { - "cell_type": "markdown", - "id": "0323d6d8", - "metadata": {}, - "source": [ - "### Communication overhead\n", - "\n", - "Let us analyze the (theoretical) communication overhead for algorithm 3.\n", - "\n", - "
\n", - "Question: Which is the complexity of the communication and computations done by a worker in algorithm 3?\n", - "
\n", - "\n", - " a) O(N²) communication and O(N³) computation\n", - " b) O(N²) communication and O(N³/P) computation\n", - " c) O(N²) communication and O(N²) computation\n", - " d) O(N²/P) communication and O(N³/P) computation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "50b8bf53", - "metadata": {}, - "outputs": [], - "source": [ - "answer = \"x\" # replace x with a, b, c, or d \n", - "alg_3_complex_check(answer)" - ] - }, - { - "cell_type": "markdown", - "id": "3f0d99e6", - "metadata": {}, - "source": [ - "In this case, the ratio between communication and computation is O(P/N). If the matrix size N is much larger than the number of workers P, then the communication overhead O(P/N) would be negligible. This opens the door to an scalable implementation." - ] - }, - { - "cell_type": "markdown", - "id": "7bb65f2e", - "metadata": {}, - "source": [ - "## Summary\n", - "\n", - "The table below compares the three parallel algorithms. \n", - "\n", - "
\n", - "\n", - "| Algorithm | Parallelism
(#workers) | Communication
per worker | Computation
per worker | Ratio communication/
computation |\n", - "|---|---|---|---|---|\n", - "| 1 | N² | 2N + 1 | N | O(1) |\n", - "| 2 | N | 2N + N² | N² | O(1) |\n", - "| 3 | P | N² + 2N²/P | N³/P | O(P/N) |\n", - "\n", - "\n", - "- Matrix-matrix multiplication is trivially parallelizable (all entries in the result matrix can be computed in parallel, at least in theory)\n", - "- However, we cannot exploit all the potential parallelism in a distributed system due to communication overhead\n", - "- We need a sufficiently large grain size to obtain a near optimal speedup\n" - ] - }, - { - "cell_type": "markdown", - "id": "8b83e744", - "metadata": {}, - "source": [ - "## Exercises" - ] - }, - { - "cell_type": "markdown", - "id": "a628a1df", - "metadata": {}, - "source": [ - "### Exercise 1\n", - "\n", - "Implement algorithm 3 in the function below. For simplicity, assume that the number of rows of C is a multiple of the number of workers.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8e50b923", - "metadata": {}, - "outputs": [], - "source": [ - "function matmul_dist_3!(C,A,B)\n", - " m = size(C,1)\n", - " n = size(C,2)\n", - " l = size(A,2)\n", - " @assert size(A,1) == m\n", - " @assert size(B,2) == n\n", - " @assert size(B,1) == l\n", - " @assert mod(m,nworkers()) == 0\n", - " # Implement here\n", - " \n", - " C\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "4506dcfb", - "metadata": {}, - "source": [ - "Use test-driven development to implement the algorithm. Use this test:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "28cde36a", - "metadata": {}, - "outputs": [], - "source": [ - "using Test\n", - "P = nworkers()\n", - "load = 100\n", - "N = load*P\n", - "A = rand(N,N)\n", - "B = rand(N,N)\n", - "C = similar(A)\n", - "@test matmul_dist_3!(C,A,B) ≈ A*B" - ] - }, - { - "cell_type": "markdown", - "id": "03952b0b", - "metadata": {}, - "source": [ - "Measure the performance of your implementation by running next cell. Do you get close to the optimal speedup?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b3aa2b7c", - "metadata": {}, - "outputs": [], - "source": [ - "P = nworkers()\n", - "load = 100\n", - "N = load*P\n", - "A = rand(N,N)\n", - "B = rand(N,N)\n", - "C = similar(A)\n", - "T1 = @belapsed matmul_seq!(C,A,B)\n", - "C = similar(A)\n", - "TP = @belapsed matmul_dist_3!(C,A,B)\n", - "println(\"Speedup = \", T1/TP)\n", - "println(\"Optimal speedup = \", P)\n", - "println(\"Efficiency = \", 100*(T1/TP)/P, \"%\")" - ] - }, - { - "cell_type": "markdown", - "id": "fa8d7f40", - "metadata": {}, - "source": [ - "### Exercise 2" - ] - }, - { - "cell_type": "markdown", - "id": "0e7c607e", - "metadata": {}, - "source": [ - "The implementation of algorithm 1 is very impractical. One needs as many processors as entries in the result matrix C. For 1000 times 1000 matrix one would need a supercomputer with one million processes! We can easily fix this problem by using less processors and spawning the computation of an entry in any of the available processes.\n", - "See the following code:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "023b20d1", - "metadata": {}, - "outputs": [], - "source": [ - "function matmul_dist_1_v2!(C, A, B)\n", - " m = size(C,1)\n", - " n = size(C,2)\n", - " l = size(A,2)\n", - " @assert size(A,1) == m\n", - " @assert size(B,2) == n\n", - " @assert size(B,1) == l\n", - " z = zero(eltype(C))\n", - " @sync for j in 1:n\n", - " for i in 1:m\n", - " Ai = A[i,:]\n", - " Bj = B[:,j]\n", - " ftr = @spawnat :any begin\n", - " Cij = z\n", - " for k in 1:l\n", - " @inbounds Cij += Ai[k]*Bj[k]\n", - " end\n", - " Cij\n", - " end\n", - " @async C[i,j] = fetch(ftr)\n", - " end\n", - " end\n", - " C\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "52005ca1", - "metadata": {}, - "source": [ - "With this new implementation, we can multiply matrices of arbitrary size with a fixed number of workers. Test it:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c1d3595b", - "metadata": {}, - "outputs": [], - "source": [ - "using Test\n", - "N = 50\n", - "A = rand(N,N)\n", - "B = rand(N,N)\n", - "C = similar(A)\n", - "@test matmul_dist_1_v2!(C,A,B) ≈ A*B" - ] - }, - { - "cell_type": "markdown", - "id": "ab609c18", - "metadata": {}, - "source": [ - "Run the next cell to check the performance of this implementation. Note that we are far away from the optimal speed up. Why? To answer this question compute the theoretical communication over computation ratio for this implementation and reason about the obtained result. Hint: the number of times a worker is spawned in this implementation is N^3/P on average." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d7d31710", - "metadata": {}, - "outputs": [], - "source": [ - "N = 100\n", - "A = rand(N,N)\n", - "B = rand(N,N)\n", - "C = similar(A)\n", - "P = nworkers()\n", - "T1 = @belapsed matmul_seq!(C,A,B)\n", - "C = similar(A)\n", - "TP = @belapsed matmul_dist_1_v2!(C,A,B)\n", - "println(\"Speedup = \", T1/TP)\n", - "println(\"Optimal speedup = \", P)\n", - "println(\"Efficiency = \", 100*(T1/TP)/P, \"%\")" - ] - }, - { - "cell_type": "markdown", - "id": "8e171362", - "metadata": {}, - "source": [ - "# License\n", - "\n", - "TODO: replace link to website\n", - "\n", - "This notebook is part of the course [Programming Large Scale Parallel Systems](http://localhost:8000/) at Vrije Universiteit Amsterdam and may be used under a [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "86b7b044", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Julia 1.9.1", - "language": "julia", - "name": "julia-1.9" - }, - "language_info": { - "file_extension": ".jl", - "mimetype": "application/julia", - "name": "julia", - "version": "1.9.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/src/notebooks/matrix_matrix.md b/docs/src/notebooks/matrix_matrix.md deleted file mode 100644 index a1d4690..0000000 --- a/docs/src/notebooks/matrix_matrix.md +++ /dev/null @@ -1,30 +0,0 @@ -```@meta -EditURL = "https://github.com/fverdugo/XM_40017/blob/main/docs/src/notebooks/matrix_matrix.ipynb" -``` - -```@raw html -
-
Tip
-
- -
-
-``` - -```@raw html - - -``` diff --git a/docs/src/notebooks/mpi_tutorial.ipynb b/docs/src/notebooks/mpi_tutorial.ipynb deleted file mode 100644 index e142a42..0000000 --- a/docs/src/notebooks/mpi_tutorial.ipynb +++ /dev/null @@ -1,413 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "8d800917", - "metadata": {}, - "source": [ - "# Tutorial: Using MPI in Julia\n", - "Message Passing Interface (MPI) is a standardized and portable library specification for communication between parallel processes in distributed memory systems. Julia offers a convenient way to work with MPI for creating efficient parallel and distributed applications. In this tutorial, you will learn how to use MPI from Julia to perform parallel computing tasks.\n", - "\n", - "## MPI launches separate Julia instances\n", - "When you run an MPI-enabled Julia script, MPI takes care of spawning multiple instances of the Julia executable, each acting as a separate process. These workers can communicate with each other using MPI communication functions. This enables parallel processing and distributed computation. Here's a summary of how it works:\n", - "\n", - "-- TODO: insert picture here --\n", - "\n", - "- **MPI Spawns Processes**: The `mpiexec` command launches multiple instances of the Julia executable, creating separate worker processes. In this example, 4 Julia workers are spawned.\n", - "\n", - "- **Worker Communication**: These workers can communicate with each other using MPI communication functions, allowing them to exchange data and coordinate actions.\n", - "\n", - "- **Parallel Tasks**: The workers execute parallel tasks simultaneously, working on different parts of the computation to potentially speed up the process.\n", - "\n", - "\n", - " \n", - "\n", - "\n", - "## Installing MPI.jl and MPIClusterManagers Packages\n", - "To use MPI in Julia, you'll need the MPI.jl package, and if you intend to run MPI programs in a Jupyter Notebook, you'll also need the MPIClusterManagers package. These packages provide the necessary bindings to the MPI library and cluster management capabilities. To install the packages, open a terminal and run the following commands:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3cb5f151", - "metadata": {}, - "outputs": [], - "source": [ - "using Pkg\n", - "Pkg.add(\"MPI\")\n", - "Pkg.add(\"MPIClusterManagers\")" - ] - }, - { - "cell_type": "markdown", - "id": "ed45a4b2", - "metadata": {}, - "source": [ - "
\n", - " Tip:\n", - "The package MPI.jl is the Julia interface to MPI. Note that it is not a MPI library by itself. It is just a thin wrapper between MPI and Julia. To use this interface, you need an actual MPI library installed in your system such as OpenMPI or MPICH. Julia downloads and installs a MPI library for you, but it is also possible to use a MPI library already available in your system. This is useful, e.g., when running on HPC clusters. See the documentation of MPI.jl for further details.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "7a36916e", - "metadata": {}, - "source": [ - "## Writing a HelloWorld MPI Program in Julia\n", - "Let's start by creating a simple MPI program that prints a message along with the rank of each worker. \n", - "\n", - "Create a new Julia script, for example, `mpi_hello_world.jl`:\n", - "\n", - "```julia\n", - "using MPI\n", - "\n", - "# Initialize MPI\n", - "MPI.Init()\n", - "\n", - "# Get the default communicator (MPI_COMM_WORLD) for all processes\n", - "comm = MPI.COMM_WORLD\n", - "\n", - "# Get the number of processes in this communicator\n", - "nranks = MPI.Comm_size(comm)\n", - "\n", - "# Get the rank of the current process within the communicator\n", - "rank = MPI.Comm_rank(comm)\n", - "\n", - "# Print a message with the rank of the current process\n", - "println(\"Hello, I am process $rank of $nranks processes!\")\n", - "\n", - "# Finalize MPI\n", - "MPI.Finalize()\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "6caa8d74", - "metadata": {}, - "source": [ - "### MPI Communicators\n", - "In MPI, a **communicator** is a context in which a group of processes can communicate with each other. `MPI_COMM_WORLD` is one of the MPI standard communicators, it represents all processes in the MPI program. Custom communicators can also be created to group processes based on specific requirements or logical divisions. \n", - "\n", - "The **rank** of a processor is a unique identifier assigned to each process within a communicator. It allows processes to distinguish and address each other in communication operations. " - ] - }, - { - "cell_type": "markdown", - "id": "19f41e38", - "metadata": {}, - "source": [ - "## Running the HelloWorld MPI Program\n", - "\n", - "To run MPI applications in parallel, you need a launcher like `mpiexec`. MPI codes written in Julia are not an exception to this rule. From the system terminal, you can run\n", - "```\n", - "$ mpiexec -np 4 mpi_hello_world.jl\n", - "```\n", - "In this command, `-np 4` specifies the desired number of processes. \n", - "But it will probably not work since the version of `mpiexec` needs to match with the MPI version we are using from Julia. You can find the path to the `mpiexec` binary you need to use with these commands\n", - "\n", - "```julia\n", - "julia> using MPI\n", - "julia> MPI.mpiexec_path\n", - "```\n", - "\n", - "and then try again\n", - "```\n", - "$ /path/to/my/mpiexec -np 4 julia mpi_hello_world.jl\n", - "```\n", - "with your particular path.\n", - "\n", - "However, this is not very convenient. Don't worry if you could not make it work! A more elegant way to run MPI code is from the Julia REPL directly, by using these commands:\n", - "```julia\n", - "julia> using MPI\n", - "julia> mpiexec(cmd->run(`$cmd -np 4 julia mpi_hello_world.jl`))\n", - "```\n", - "\n", - "Now, you should see output from 4 ranks.\n" - ] - }, - { - "cell_type": "markdown", - "id": "0592e58c", - "metadata": {}, - "source": [ - "## Running MPI Programs in Jupyter Notebook with MPIClusterManagers\n", - "If you want to run your MPI code from a Jupyter Notebook, you can do so using the `MPIClusterManagers` package.\n", - "\n", - "1. Load the packages and start an MPI cluster with the desired number of workers:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cf66dd39", - "metadata": {}, - "outputs": [], - "source": [ - "using MPIClusterManagers\n", - "# Distributed package is needed for addprocs()\n", - "using Distributed\n", - "\n", - "manager = MPIWorkerManager(4)\n", - "addprocs(manager)" - ] - }, - { - "cell_type": "markdown", - "id": "d40fe3ee", - "metadata": {}, - "source": [ - "2. Run your MPI code inside a `@mpi_do` block to execute it on the cluster workers:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0a51d1f2", - "metadata": {}, - "outputs": [], - "source": [ - "@mpi_do manager begin\n", - " using MPI\n", - " comm = MPI.COMM_WORLD\n", - " rank = MPI.Comm_rank(comm)\n", - " println(\"Hello from process $rank\")\n", - "end\n" - ] - }, - { - "cell_type": "markdown", - "id": "38ed88c1", - "metadata": {}, - "source": [ - "MPI is automatically initialized and finalized within the `@mpi_do` block.\n", - "\n", - "3. Remove processes when done:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e0b53cc1", - "metadata": {}, - "outputs": [], - "source": [ - "rmprocs(manager)" - ] - }, - { - "cell_type": "markdown", - "id": "5466a650", - "metadata": {}, - "source": [ - "## Point-to-Point Communication with MPI\n", - "MPI provides point-to-point communication using blocking send and receiving functions `MPI.send`, `MPI.recv`; or their non-blocking versions `MPI.Isend`, and `MPI.Irecv!`. These functions allow individual processes to send and receive data between each other.\n", - "\n", - "### Blocking communication\n", - "\n", - "Let's demonstrate how to send and receive with an example:\n", - "\n", - "```julia\n", - "using MPI\n", - "\n", - "MPI.Init()\n", - "\n", - "comm = MPI.COMM_WORLD\n", - "rank = MPI.Comm_rank(comm)\n", - "\n", - "# Send and receive messages using blocking MPI.send and MPI.recv\n", - "if rank == 0\n", - " data = \"Hello from process $rank !\"\n", - " MPI.send(data, comm, dest=1)\n", - "elseif rank == 1\n", - " received_data = MPI.recv(comm, source=0)\n", - " println(\"Process $rank received: $received_data\")\n", - "end\n", - "\n", - "MPI.Finalize()\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "d4dfe654", - "metadata": {}, - "source": [ - "In this example, process 0 sends a message using `MPI.send`, and process 1 receives it using `MPI.recv`.\n", - "\n", - "### Non-blocking communication\n", - "\n", - "To demonstrate asynchronous communication, let's modify the example using `MPI.Isend` and `MPI.Irecv!`:\n", - "\n", - "```julia\n", - "using MPI\n", - "\n", - "MPI.Init()\n", - "\n", - "comm = MPI.COMM_WORLD\n", - "rank = MPI.Comm_rank(comm)\n", - "\n", - "# Asynchronous communication using MPI.Isend and MPI.Irecv!\n", - "if rank == 0\n", - " data = \"Hello from process $rank !\"\n", - " request = MPI.Isend(data, comm, dest=1)\n", - " # Other computation can happen here\n", - " MPI.Wait(request)\n", - "elseif rank == 1\n", - " received_data = Array{UInt8}(undef, 50) # Preallocate buffer\n", - " request = MPI.Irecv!(received_data, comm, source=0)\n", - " # Other computation can happen here\n", - " MPI.Wait(request)\n", - " println(\"Process $rank received: $(String(received_data))\")\n", - "end\n", - "\n", - "MPI.Finalize()\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "024db538", - "metadata": {}, - "source": [ - "In this example, process 0 uses `MPI.Isend` to send the message asynchronously. This function returns immediately, allowing the sender process to continue its execution. However, the actual sending of data is done asynchronously in the background. Similar to `MPI.Isend`, `MPI.Irecv!` returns immediately, allowing the receiver process to continue executing. \n", - "\n", - "
\n", - "Important: In asynchronous communication, always use MPI.Wait() to ensure the communication is finished before accessing the send or receive buffer.\n", - "
\n", - "\n", - "\n", - "## Collective Communication with MPI\n", - "MPI provides collective communication functions for communication involving multiple processes. Let's explore some of these functions:\n", - "\n", - "- MPI.Gather: Gathers data from all processes to a single process.\n", - "- MPI.Scatter: Distributes data from one process to all processes.\n", - "- MPI.Bcast: Broadcasts data from one process to all processes.\n", - "- MPI.Barrier: Synchronizes all processes.\n", - "\n", - "\n", - "Let's illustrate the use of `MPI.Gather` and `MPI.Scatter` with an example:\n", - "\n", - "```julia\n", - "# TODO: check if this runs correctly\n", - "using MPI\n", - "using Random\n", - "\n", - "MPI.Init()\n", - "\n", - "comm = MPI.COMM_WORLD\n", - "rank = MPI.Comm_rank(comm)\n", - "size = MPI.Comm_size(comm)\n", - "\n", - "# Root processor generates random data\n", - "data = rand(rank == 0 ? size * 2 : 0)\n", - "\n", - "# Scatter data to all processes\n", - "local_data = Vector{Float64}(undef, 2)\n", - "MPI.Scatter!(data, local_data, comm, root=0)\n", - "\n", - "# Compute local average\n", - "local_average = sum(local_data) / length(local_data)\n", - "\n", - "# Gather local averages at the root processor\n", - "gathered_averages = Vector{Float64}(undef, size)\n", - "MPI.Gather!(local_average, gathered_averages, comm, root=0)\n", - "\n", - "if rank == 0\n", - " # Compute global average of sub-averages\n", - " global_average = sum(gathered_averages) / size\n", - " println(\"Global average: $global_average\")\n", - "end\n", - "\n", - "MPI.Finalize()\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e65cb53f", - "metadata": {}, - "outputs": [], - "source": [ - "using MPI\n", - "using Random\n", - "\n", - "# TODO: check if this runs correctly\n", - "\n", - "MPI.Init()\n", - "\n", - "comm = MPI.COMM_WORLD\n", - "rank = MPI.Comm_rank(comm)\n", - "size = MPI.Comm_size(comm)\n", - "\n", - "# Root processor generates random data\n", - "data = rand(rank == 0 ? size * 2 : 0)\n", - "\n", - "# Scatter data to all processes\n", - "local_data = Vector{Float64}(undef, 2)\n", - "MPI.Scatter!(data, local_data, comm, root=0)\n", - "\n", - "# Compute local average\n", - "local_average = sum(local_data) / length(local_data)\n", - "\n", - "# Gather local averages at the root processor\n", - "gathered_averages = Vector{Float64}(undef, size)\n", - "MPI.Gather!(local_average, gathered_averages, comm, root=0)\n", - "\n", - "if rank == 0\n", - " # Compute global average of sub-averages\n", - " global_average = sum(gathered_averages) / size\n", - " println(\"Global average: $global_average\")\n", - "end\n", - "\n", - "MPI.Finalize()" - ] - }, - { - "cell_type": "markdown", - "id": "dfd5da9e", - "metadata": {}, - "source": [ - "In this example, the root processor generates random data and then scatters it to all processes using MPI.Scatter. Each process calculates the average of its local data, and then the local averages are gathered using MPI.Gather. The root processor computes the global average of all sub-averages and prints it." - ] - }, - { - "cell_type": "markdown", - "id": "5e8f6e6a", - "metadata": {}, - "source": [ - "# License\n", - "\n", - "TODO: replace link to website\n", - "\n", - "This notebook is part of the course [Programming Large Scale Parallel Systems](http://localhost:8000/) at Vrije Universiteit Amsterdam and may be used under a [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c9364808", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Julia 1.9.1", - "language": "julia", - "name": "julia-1.9" - }, - "language_info": { - "file_extension": ".jl", - "mimetype": "application/julia", - "name": "julia", - "version": "1.9.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/src/notebooks/mpi_tutorial.md b/docs/src/notebooks/mpi_tutorial.md deleted file mode 100644 index a57c6a2..0000000 --- a/docs/src/notebooks/mpi_tutorial.md +++ /dev/null @@ -1,30 +0,0 @@ -```@meta -EditURL = "https://github.com/fverdugo/XM_40017/blob/main/docs/src/notebooks/mpi_tutorial.ipynb" -``` - -```@raw html -
-
Tip
-
- -
-
-``` - -```@raw html - - -``` diff --git a/docs/src/notebooks/notebook-hello.ipynb b/docs/src/notebooks/notebook-hello.ipynb deleted file mode 100644 index 51ff0f8..0000000 --- a/docs/src/notebooks/notebook-hello.ipynb +++ /dev/null @@ -1,187 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "babf8079", - "metadata": {}, - "source": [ - "# Hello, world!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f71f2545", - "metadata": {}, - "outputs": [], - "source": [ - "] add Distributed" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4d6b67b6", - "metadata": {}, - "outputs": [], - "source": [ - "] add https://github.com/fverdugo/W2bYx4.git" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "815d977e", - "metadata": {}, - "outputs": [], - "source": [ - "using W2bYx4" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2cf68b65", - "metadata": {}, - "outputs": [], - "source": [ - "W2bYx4.greet()" - ] - }, - { - "cell_type": "markdown", - "id": "81236a40", - "metadata": {}, - "source": [ - "This is an attached picture:" - ] - }, - { - "attachments": { - "logo.png": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAN0AAADdCAYAAAA/xHcaAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAuIwAALiMBeKU/dgAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAABTFSURBVHic7d15fBRlmgfwX1Vf6dzk4j4CqOAip4IozoiiogKKyqCgyKWIeIBCgro7M7s7uu6uH2d2jo/HjsfoCDi646iMN4EBDPcVRW7CkXSn0+lO0t3p9FVV+wfqB1Ek6Treequf7z8KprseU/2rqq563+cV8AOGTH91gCBipiLgegjoBwVlAMQf+llCyLdkCGiEgloByoeA8sYXK+YePfOHhNP/MPSu13IUSf5PBbgXgMOwUgmxpiQE4XlXTmz5jhcXRL/5y29DN2zWn3pKKWU1gOFMyiPEogRgV0KWJh1YNc/z9Z+BsdP+4g47ousBXMy0OkKsa7doE8fVvD6rTQSAsLPtGVDgCNHTcFlW/gMAhCHTXx0AG/aBvsMRoreEaBMHibAJd4ICR4gRnJIkzxQBZSLrSgjJFIIgXC8C6M+6EEIyhqKUiwBKWNdBSAYpE0EjTQgxko0CR4jBKHSEGIxCR4jBKHSEGIxCR4jBKHSEGIxCR4jBKHSEGIxCR4jBKHSEGMzOugBWnHbA7ZSR55bhsClIpASE20VEEyJSEuvqiJVZNnSiAPTvmkD/rin0K02ivDSJfmUJ9OySQr5bgc2mnPW1SUlAKCrgZMCBY34HahsdOO534LDv1D8JUcNSoetVnMKl57Vj7HntGH1eDIXZclrv47ApKM5TUJwXx/B+8e/8t0BYxI6jWdh0yI3dx7JwuIFCSDpHGDLj1bMf8jlQXpbE5FFtmDQqgu6FKcO3f9TnwOqdOVi9IxfeFksdw4hOuAxdjkvBlIvDmDyqDRf1iZ/7BQaQFWD7kSy8tz0XH+7ORcL4/BNOcBW6wmwZM8aFMGNcCAVpXjoaIRix4c3qPLy2oQCRduHcLyAZhYvQFefJmHNlC6ZdGkG2y7xhO1Nzmw1vbMjDGxvzEYnR0xlyiqlDJwrApFERLJsSTPumiBk0hWz49Qdd8P6OHCgKnfkynWlDN7hnAv9ya8A039m0sP1IFp58p5jueGY404XOaQeW3BjEjHEhiBY8KSQlAc9/Wog/rimAbKrfPDGKqb5o9Ogi4ZWFXtx5hTUDB5x6BvjgxGb874IGlObT0JdMZJrQXT0kirceqcfQvta5nPwxowfG8PYjHlx2fjvrUojBmIdOEBQsvqEZv5ndiHw3vzdL0lGUK+G5+T7ccXmYdSnEQExDJ4rAz28LYN5VrSzLYEoUgcenBrDkxmbWpRCDMAud0w78951+3DYmwqoEU5k7vhW/mt70owOxiTUwGSzocih4br4PlwyIsdi8ad10SQQup4LKN0ohZ9aVdkYx/EwnisDTM/wUuLOYOKwN/zw1wLoMoiNDQycICn5xWxMmXBQ99w9nsGljw7j/2hbWZRCdGBq6xTe04JbR9B2uIxZe24Lpl9FdTSsyLHTXDW/D3PGZe5cyHY/dFMTI8sx4bplJDAldn+IkfnkbfU/pLJtNwX/d6UeXHBq5YiW6h85pB56Z5UduFt2OS0fXghSeuqPJssPiMpHuoVs6OYDBPRN6b8bSxg1qx91XhliXQTSia+hG9IvjdroZoIkHrguib2mSdRlEA7qFzmZT8PgtAQh0WaQJpx147OYg6zKIBnQL3cxxIQzqQZeVWrr8gnZ6xmkBuoSuLF+ih7s6qbwpALeTxmfyTJfQzb+6FTku+mDooVuhhGlj6aYKzzQPXXGejFtG080TPc35aQguBx3UeKV56Gb9pJU+EDoryZdw8yU0nI5XmoYu163gZ5fSpY8R5l3VCgfNveOSpqG7dXQYuW76IBihe2EK44dQfxUeaRq6KRfTJY+RJo+i78480ix0g3okcH53ei5npCsuiKE4j8a08kaz0E0e1abVW5EOstkUTBxGVxe80SR0ogjcMIJ2Pgt0sOOPJqEb3DOOEupWzMSFveJ0ickZTUI3eiDNbmZFEICLB9BdTJ5oEzra6UyNps5qXFEdOptNwYhy2uksjRlIv3+eqA7dP/VK0OBmxvqWJmkFII6oDl3/MprNbAb9u9J+4IXq0JVT6EyhH7Vy4Ibq0FHfDnOg/cAP1aGjI6w5lNN+4Ibq0PUqTmlRB1GpN+0HbqgKndMOuOx059IMcrPo7iUvVIUux0U72ixysujgxwtVocum53OmkeVQaBVXTlDoLCSbetNwQVXo3E4a3W4m1A+TD6pC154wfPVk8iPaE9TDngeqUtMWp51sJtEk7Q8eqDvTUehMI5YUIEm0P3ig8kxn06oOolJbjALHC1WhS6SAOF3SmEIkRgdAXqi+E1IXtGtRB1HpZID2Ay9Uh+6Y36FFHUSlWtoP3FAfukba2WZA+4EfqkNHR1hzoCsOfqgO3ZEGpxZ1EJWO0pmOG6pDt8/jQCRGI1NYOua3oylEdy95oTotkiRgV61Li1pImrYccrMugXSCJqeorUeytHgbkqatRyh0PNEkdHSkZUdRgG100OOKJqE74HGikb5TMPHlSReaI/Sdmiea7C1ZAT7YmavFW5FOen9HDtPtd0lFMadxK9MaeKPZIfLd7RQ6o6Uk4KM9bH/vDzRsxIPeDegbb2ZaB080C93hBgcOeOlZkZE2HMhmemk5MNaEW5tq4FAkLPasY1YHbzTdY+9uy9Py7cg5rN7O9tKyor4KNpxq2TGh9RAuCx9jWg8vNA3dO9vyEKYH5YaoC9ixZm82s+1f03oQY88IWUX9mm9DSM5O04RE2gW8WU1nOyO8tLaA2UxxpyJhsecf3/v7AbEApjXtZlARXzQ/Lf1pXQGicTrb6ckfsuF9hjeuZjduRZ+z3Dh5wLsRhSlamffHaJ6OlqiIv26lO5l6enltAeIpNme54nM8IiiQYljg22RgRfzR5ZT00toCRNqpjYMePEE73trM7hL+Ec865ErxH/2ZO/w7MbDdb0xBHNIldE0hG37/cRc93jrjPf23ImZ9aS6MNmBS895z/pwNMpZ7qgyoiE+6fflaWZ2PffU0105LG/e7sfYrNncsBShYXr8GotKxLtJjwsdxZeiIzlXxSbfQyTLwb2+XQKZO35qIJwU8+U4xs+1PDu7FiLb6Tr2mom4NnAqt7HQmXW8zfnnSiZWf5+u5iYzx248KUceo41eWnMKD3g2dfl3vRAtm+HfoUBHfdL+3/8z7Rag5TpNc1diwz43X17M7eN3j24RuyXBar73PV43SZETjivime+hSErB8ZSndzUxTQ4sNT6wqgaKw+f11T4Qwy78t7dfnSAksavhcw4r4Z8hT7JNNdvzirRIjNmUpkiSg4s9laG5jN1dxWX0VsmR165lPDdZgSLRBo4r4Z9jQkU9qcvDiZwVGbY57igL86q/F2HWM3aX56MgJXNN6UPX7iIqCyrrPIIDuqgEGhg4AfvdRF7qx0kF/+LgQb29hN7JHVGRU1K/R7P2GRz24rnm/Zu/HM8MHST79bhE+qWE7JcXs3qzOwwufFTKt4bZADS7QeFTJUs86uOWkpu/JI8NDJ8vA4ytKsOUQNdP5IR/sysFTDJ/HAUCeFMcDDRs1f9+uyTBmU2sH40MHAPGUgPtf6oaPd9MZ73SrPs/HYytKmQ8oWOj7HF1SUV3ee17jVvRIhHR5b14wm4OTSAEVK0rxl000/w44NXPgyXeKmAeuPB7EHf6dur2/S07i4R+Yi5dJmE58k2Xg3/+vGM9/UogODumzHEkS8K9vF+PXfzfHAPGK+irYFX1nf1/fsh8jI3W6bsPMTDHb9A+fFOLhV8rQGjVFOYbxtdox9/lueJvhVJ3T/SR0BONCR3XfTmcHT1uNaT7la7/KxrRne2A3w+dSRtp8yI3pv+mBnSZZB8KuyFjmWWvY9ga3+3BT8EvDtmcmpgkdAHhb7JjzXHe8uq4AskX728RTAp79exHufbEMgbB5fv0z/TvQLxY0dJuLvf8454RYK7KVXXTzL1kXcTpZATYddGPd3mxc0COBboXWmRqy5VAWHnylK9buzQZgnrGoRakonj32LlwGT8Nxy0mIArA5r5+h22XNPIfaM+z3OHHX77vhiVUlTMceaqExdGrQ8vwXuqHWhIs3PuhdjzxGZ5y7Grejb9zYMyxrpjvTfZeAAx4n/rYtF5Ii4PzuSTjt/Hz5DoRFvPBpIZ5YVYIvT5rju9uZBrU34ud1nzIbF2mDgu7JMD7sMpjJ9lkQhsx4lZtPcY5LwdTRYcwb34qSfPNedgbCIl5bX4AVG/MRY9TPpKNeObwKF0dOsC4D9w2Yhs/zylmXYQiuQveNbJeMG0e2YcqoCIb1jUMwwedaloHNh914b3suPtmTjSSjRrCdcV3Lfjxz7D3WZQAAjrqKceugOUgJpv3GoxkuQ3e6PsVJTBrVhkkjI+hdom7eVzoOep14f0cOPtiZy9UafS45iff2v4weiVbWpXzrqV4TsLJkJOsydMd96E7XqziFkeUxjOgXxxWD29G1QPsQNoVs2FnrwqZDblQfdMMTZNO3RK37fNVY5NV+ULMaIVsWJg2ej2Y7uzUajGCp0J1OEBT0K01hQNck+pYm0a80ifKyJHoXp5DjkuFynP1/uz0hIBITcdzvwLEm+6l/Njpx2Odg1hxIS2WJMFbv/6Mpp9msLBmJp3pNYF2Grvj/BJ2FogiobXSc9Ra9KAK5Lhm5bhk2EUilgLa4iHBMYNaPxCiPes07r216YDfeKh6GQ+5S1qXoxvrfWs9CloFQuwhP0I6TTXZ4W+wItYuWD9ywtnpcb+IZ3KIio1LDGetmlLGhy0SioqCyvsr0vUrGRE5gfOsh1mXohkKXQaY0f4mLol7WZXRIRX2VZbtDU+gyRI6UwENpdGlmpVeiFXf6t7MuQxcUugxxr28Td52Weay5Iyh0GaBXohUzm/hbU4C3s3NHUegywLL6KrhUdmlmhafvoR1FobO4MZETuIrjO4G83HHtDAqdhVnlmZfZny12FoXOwqYH9uA8i6z9beZRNJ1FobOofCmGhRZaoqosEcZc3xbWZWiCQmdRi7wbdevSzMoc/zZTTUVKF4XOgvrHA/hZYDfrMjTnkpNY4l3PugzVKHQWVFGnf5dmViY27zNFewk1KHQWM771EC4P17IuQ1eV9VUQOT6oUOgsxKFIeDQDFucY1N6IqcEvWJeRNgqdhWRSD8mHvBu47Q5NobOIolQU9zRuZl2GYYpSUSzwbWJdRloodBaRiesCsFh/QQsUOgvI1BVwHIqEpQauNKQVCp0FZPJabz8NHcE4zu7WUug4d0PLvoxe1RQAlnH2XJJCxzFav/uU/vEAbm/Sb510rVHoODavcSt6JEKsyzCFhQ3V3Iw1pdBxqmsyjNmNW1mXYRr5UgyLOJlVQaHj1FKPdeaXaWVaYA/Oj5l//iCFjkPDox5cZ6GZ1FoRFRmVdeafKU+h44yoKKis+8xSPUO0NDpyAhNM3hOGQseZqcEaDIk2sC7D1JZ61pq6+xmFjiM5UoKbmwUs9Yy3YJZ/G+syzopCx5H7fNWW7Hish/m+LSgz6e+KQseJ3okWzPDz16WZlWw5gYdM2tqBQseJiro1ll3FRi9TgnsxNOphXcb3UOg4MCZ8HFeGjrAugzsCzNkdmkJncjbIWO6pYl0Gt4a2eTAp+BXrMr6DQmdyt/t3YaBFujSzsrhhPbLlBOsyvkWhM7ECKYb7fNWsy+BeWSKMuSYap0qhM7EHvBtRmGpnXYYlzPFtQZ94M+syAFDoTGtALIBpTdbr0syKU5HwsEkeIVDoTKqifg1s4Gc2NA+ubTmAseFjrMug0JnR1S0HcZkJPhxWVFFfxfxgRqEzGYciYYmXWjDoZWCsCbcEapjWQKEzmbsbt6GvSb7wW9XDnvUokGLMtk+hM5HiVBTzGq2x8KGZFUgx3NvA7lEMhc5ElnjWZVyXZlZm+ndiYKyJybYpdCYxOOrD5Oa9rMvIGDbIqKxnM7yOQmcCApSM7tLMyqXhY7gidNTw7VLoTODG5q8wsi2zuzSzsrze+ClTFDrGsuQUHvJuYF1GxuoTbza8OzSFjrH5jZvRnbo0M7WwoRolqTbDtkehY6hbMoy7TTT6PVPlSnEs8m40bHsUOoaW1Vchy8St4jLJLcEaXGhQa0MKHSMj2upxTctB1mWQr4nKqTvIRrR2oNAxYOQOJh1n1IGQQsfArcE9hl3KkM4x4pKfQmewXCmO+73UpdmsjLi5RaEzmNG3p0nn6f0Yh0JnIBYPYknn6T1ggUJnIBZDjkh69ByaR6EzyFhGg2tJevQchE6hM4ANMioYTSMh6dNruhWFzgAzGE6YJOroMbGYQqezAimGBQxbAxB19GihQaHTGesmOEQ9rZtFUeh0ZIZ2b0Q9rdsiUuh09Hjdp8wbmxJtaNkAmEKnk2tbDuCSyEnWZRANadXqnkKnA5ecwiPUpdlytFrUhUKng9mNW9Ez3sK6DKIDLZYvo9BpzGwLEBJtabFQJ4VOY0u85lpql2hP7ZLUFDoNDW3z4MZmcy0qT7Rng4zlnvSH9VHoNCJAQWV9FbVgyBBjwsdxZehIWq+l0GlkSnAvhkY9rMsgBqqoS2+qFoVOA9lyAg+ZZD1rYpzeiRbM9O/o9OsodBq4x7cZZckI6zIIAwt81Sjt5L6n0KnUM96Cu/zbWZdBGMmREljU0LlGUxQ6lZZ61sJFXZoz2tRgDYZ0oqUihU6F0ZETmNB6iHUZhDFRUVBZ91mH71xT6NIkKjIq69awLoOYxPCoBxOb93foZyl0aZoW2IPzY+mPSiDW86hnHdxy8pw/R6FLQ74U6/SXZ2J9XZNhzO7AuFu7AbVYTnEqit92v4J1GcSEYuK5I0WhS0Otqwi1riLWZRBO0eUlIQaj0BFiMAodIQaj0BFiMAodIQYTAWrMSIiBJBECGllXQUimEACfCAW1rAshJFMoQK0oQPmQdSGEZA7hAxFQ3gBw7lGahBC1EqJNWCl+sWLuUQV4gXU1hFif8lzN67NqRQDIT2ZXANjGuCJCLEsAdrlyE49//e+nXHD7Sz2com21AoxgVxohlrRDEm2T9/35Li9w2sPxA6vmeZy58XEQhN8BoL7ghKiXAJT/ceXGf/JN4IDTznSnu2jGy/1liDMEQbgeilIOoCto9Aoh5yID8AE4CgUfinZxRc3rs773SO7/ASo4P8zLykOMAAAAAElFTkSuQmCC" - } - }, - "cell_type": "markdown", - "id": "7c2e8d25", - "metadata": {}, - "source": [ - "\n", - "
\n", - "\n", - "
\n" - ] - }, - { - "attachments": { - "logo.png": { - "image/png": "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" - } - }, - "cell_type": "markdown", - "id": "d0f1685d", - "metadata": {}, - "source": [ - "Another attached picture with markdown syntax `![]()`: \n", - "\n", - "\n", - "![logo.png](data:image/png;base64,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)" - ] - }, - { - "cell_type": "markdown", - "id": "7f4de0d2", - "metadata": {}, - "source": [ - "A picture embedded with Base64: \n", - "\n", - "\n", - "\n", - "
\n", - "\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "0d36cf9e", - "metadata": {}, - "source": [ - "This is a linked picture:" - ] - }, - { - "cell_type": "markdown", - "id": "0b541664", - "metadata": {}, - "source": [ - "
\n", - "\n", - "
\n" - ] - }, - { - "cell_type": "markdown", - "id": "cae7f8ac", - "metadata": {}, - "source": [ - "```julia\n", - "julia> 1 + 1\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6603cd99", - "metadata": {}, - "outputs": [], - "source": [ - "1+1" - ] - }, - { - "cell_type": "markdown", - "id": "c3b59ed4", - "metadata": {}, - "source": [ - "
\n", - "Warning: Make sure that the file \"hello.jl\" is located in the current working directory of your Julia session.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "5b3e3d68", - "metadata": {}, - "source": [ - "
\n", - "Tip: Avoid calling Julia code from the terminal\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b7267f6e", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Julia 1.9.1", - "language": "julia", - "name": "julia-1.9" - }, - "language_info": { - "file_extension": ".jl", - "mimetype": "application/julia", - "name": "julia", - "version": "1.9.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/src/notebooks/notebook-hello.md b/docs/src/notebooks/notebook-hello.md deleted file mode 100644 index 2603130..0000000 --- a/docs/src/notebooks/notebook-hello.md +++ /dev/null @@ -1,30 +0,0 @@ -```@meta -EditURL = "https://github.com/fverdugo/XM_40017/blob/main/docs/src/notebooks/notebook-hello.ipynb" -``` - -```@raw html -
-
Tip
-
- -
-
-``` - -```@raw html - - -``` diff --git a/docs/src/notebooks/solutions.ipynb b/docs/src/notebooks/solutions.ipynb deleted file mode 100644 index 240bc9b..0000000 --- a/docs/src/notebooks/solutions.ipynb +++ /dev/null @@ -1,226 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "f48b9a60", - "metadata": {}, - "source": [ - "# Solutions to Notebook Exercises\n", - "\n", - "## Julia Basics: Exercise 1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a06fd02a", - "metadata": {}, - "outputs": [], - "source": [ - "function ex1(a)\n", - " j = 1\n", - " m = a[j]\n", - " for (i,ai) in enumerate(a)\n", - " if m < ai\n", - " m = ai\n", - " j = i\n", - " end\n", - " end\n", - " (m,j)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "175b6c35", - "metadata": {}, - "source": [ - "## Julia Basics: Exercise 2" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bb289acd", - "metadata": {}, - "outputs": [], - "source": [ - "ex2(f,g) = x -> f(x) + g(x) " - ] - }, - { - "cell_type": "markdown", - "id": "86250e27", - "metadata": {}, - "source": [ - "## Julia Basics: Exercise 3" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "41b537ab", - "metadata": {}, - "outputs": [], - "source": [ - "function compute_values(n,max_iters)\n", - " x = LinRange(-1.7,0.7,n)\n", - " y = LinRange(-1.2,1.2,n)\n", - " values = zeros(Int,n,n)\n", - " for j in 1:n\n", - " for i in 1:n\n", - " values[i,j] = mandel(x[i],y[j],max_iters)\n", - " end\n", - " end\n", - " values\n", - "end\n", - "values = compute_values(1000,10)\n", - "using GLMakie\n", - "heatmap(x,y,values)" - ] - }, - { - "cell_type": "markdown", - "id": "d6d12733", - "metadata": {}, - "source": [ - "## Matrix Multiplication : Exercise 1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "be73e87a", - "metadata": {}, - "outputs": [], - "source": [ - "function matmul_dist_3!(C,A,B)\n", - " m = size(C,1)\n", - " n = size(C,2)\n", - " l = size(A,2)\n", - " @assert size(A,1) == m\n", - " @assert size(B,2) == n\n", - " @assert size(B,1) == l\n", - " @assert mod(m,nworkers()) == 0\n", - " # Implement here\n", - " nrows_w = div(m,nworkers())\n", - " @sync for (i,w) in enumerate(workers())\n", - " rows_w = (1:nrows_w) .+ (i-1)*nrows_w\n", - " Aw = A[rows_w,:]\n", - " ftr = @spawnat w begin\n", - " Cw = similar(Aw,nrows_w,n)\n", - " matmul_seq!(Cw,Aw,B)\n", - " Cw\n", - " end\n", - " @async C[rows_w,:] = fetch(ftr)\n", - " end\n", - " C\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "2d9f4813", - "metadata": {}, - "source": [ - "## Jacobi Method : Exercise 1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cf3b1e72", - "metadata": {}, - "outputs": [], - "source": [ - "@mpi_do manager begin\n", - " using MPI\n", - " comm = MPI.Comm_dup(MPI.COMM_WORLD)\n", - " nw = MPI.Comm_size(comm)\n", - " iw = MPI.Comm_rank(comm)+1\n", - " function jacobi_mpi(n,niters)\n", - " if mod(n,nw) != 0\n", - " println(\"n must be a multiple of nw\")\n", - " MPI.Abort(comm,1)\n", - " end\n", - " n_own = div(n,nw)\n", - " u = zeros(n_own+2)\n", - " u[1] = -1\n", - " u[end] = 1\n", - " u_new = copy(u)\n", - " for t in 1:niters\n", - " reqs_snd = MPI.Request[]\n", - " reqs_rcv = MPI.Request[]\n", - " if iw != 1\n", - " neig_rank = (iw-1)-1\n", - " req = MPI.Isend(view(u,2:2),comm,dest=neig_rank,tag=0)\n", - " push!(reqs_snd,req)\n", - " req = MPI.Irecv!(view(u,1:1),comm,source=neig_rank,tag=0)\n", - " push!(reqs_rcv,req)\n", - " end\n", - " if iw != nw\n", - " neig_rank = (iw+1)-1\n", - " s = n_own+1\n", - " r = n_own+2\n", - " req = MPI.Isend(view(u,s:s),comm,dest=neig_rank,tag=0)\n", - " push!(reqs_snd,req)\n", - " req = MPI.Irecv!(view(u,r:r),comm,source=neig_rank,tag=0)\n", - " push!(reqs_rcv,req)\n", - " end\n", - " for i in 3:n_own\n", - " u_new[i] = 0.5*(u[i-1]+u[i+1])\n", - " end\n", - " MPI.Waitall(reqs_rcv)\n", - " for i in (2,n_own+1)\n", - " u_new[i] = 0.5*(u[i-1]+u[i+1])\n", - " end\n", - " MPI.Waitall(reqs_snd)\n", - " u, u_new = u_new, u\n", - " end\n", - " u\n", - " @show u\n", - " end\n", - " niters = 100\n", - " load = 4\n", - " n = load*nw\n", - " jacobi_mpi(n,niters)\n", - "end" - ] - }, - { - "cell_type": "markdown", - "id": "47d88e7a", - "metadata": {}, - "source": [ - "# License\n", - "\n", - "TODO: replace link to website\n", - "\n", - "This notebook is part of the course [Programming Large Scale Parallel Systems](http://localhost:8000/) at Vrije Universiteit Amsterdam and may be used under a [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "968304a6", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Julia 1.9.1", - "language": "julia", - "name": "julia-1.9" - }, - "language_info": { - "file_extension": ".jl", - "mimetype": "application/julia", - "name": "julia", - "version": "1.9.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/src/notebooks/solutions.md b/docs/src/notebooks/solutions.md deleted file mode 100644 index b6e5f86..0000000 --- a/docs/src/notebooks/solutions.md +++ /dev/null @@ -1,30 +0,0 @@ -```@meta -EditURL = "https://github.com/fverdugo/XM_40017/blob/main/docs/src/notebooks/solutions.ipynb" -``` - -```@raw html -
-
Tip
-
- -
-
-``` - -```@raw html - - -``` diff --git a/docs/src/notebooks/tsp.ipynb b/docs/src/notebooks/tsp.ipynb deleted file mode 100644 index f8017c4..0000000 --- a/docs/src/notebooks/tsp.ipynb +++ /dev/null @@ -1,260 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "8850d90e", - "metadata": {}, - "outputs": [], - "source": [ - "using Distributed" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "5d4935ee", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "4-element Vector{Int64}:\n", - " 2\n", - " 3\n", - " 4\n", - " 5" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "if procs() == workers()\n", - " addprocs(4)\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "4a2756ae", - "metadata": {}, - "outputs": [], - "source": [ - "@everywhere function visited(city,hops,path)\n", - " for i = 1:hops\n", - " if path[i] == city\n", - " return true\n", - " end\n", - " end\n", - " return false\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "39e9e667", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tsp_serial_impl (generic function with 1 method)" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "function tsp_serial_impl(connections,hops,path,current_distance,min_distance)\n", - " num_cities = length(connections)\n", - " if hops == num_cities\n", - " if current_distance < min_distance\n", - " return current_distance\n", - " end\n", - " else\n", - " current_city = path[hops]\n", - " next_hops = hops + 1\n", - " for (next_city,distance_increment) in connections[current_city]\n", - " if !visited(next_city,hops,path)\n", - " path[next_hops] = next_city\n", - " next_distance = current_distance + distance_increment\n", - " if next_distance < min_distance\n", - " return tsp_serial_impl(connections,next_hops,path,next_distance,min_distance)\n", - " end\n", - " end\n", - " end \n", - " end\n", - " min_distance\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "83b58881", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tsp_serial (generic function with 1 method)" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "function tsp_serial(connections,city)\n", - " num_cities = length(connections)\n", - " path=zeros(Int,num_cities)\n", - " hops = 1\n", - " path[hops] = city\n", - " current_distance = 0\n", - " min_distance = typemax(Int)\n", - " min_distance = tsp_serial_impl(connections,hops,path,current_distance,min_distance)\n", - " (;path=path,distance=min_distance)\n", - "end" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "78095098", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(path = [1, 4, 5, 2, 3, 6], distance = 222)" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "connections = [\n", - " [(1,0),(4,39),(5,76), (6,78),(3,94),(2,97)],\n", - " [(2,0),(5,25),(4,58),(3,62),(1,97),(6,109)],\n", - " [(3,0),(6,58),(2,62),(4,68),(5,70),(1,94)],\n", - " [(4,0),(5,38),(1,39),(2,58),(3,68),(6,78)],\n", - " [(5,0),(2,25),(4,38),(3,70),(1,76),(6,104)],\n", - " [(6,0),(3,58),(1,78),(4,78),(5,104),(2,109)]\n", - "]\n", - "city = 1\n", - "tsp_serial(connections,city)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "03f0dd8e", - "metadata": {}, - "outputs": [], - "source": [ - "@everywhere function tsp_dist_impl(connections,hops,path,current_distance,min_distance,max_hops,jobs_chnl,ftr_result)\n", - " num_cities = length(connections)\n", - " if hops == num_cities\n", - " if current_distance < min_distance\n", - " if ftr_result !== nothing\n", - " @spawnat 1 begin\n", - " result = fetch(ftr_result)\n", - " result.path .= path\n", - " result.min_distance_ref[] = current_distance\n", - " end |> wait\n", - " end\n", - " return current_distance\n", - " end\n", - " elseif hops <= max_hops\n", - " current_city = path[hops]\n", - " next_hops = hops + 1\n", - " for (next_city,distance_increment) in connections[current_city]\n", - " if !visited(next_city,hops,path)\n", - " path[next_hops] = next_city\n", - " next_distance = current_distance + distance_increment\n", - " if next_distance < min_distance\n", - " return tsp_dist_impl(connections,next_hops,path,next_distance,min_distance,max_hops,jobs_chnl,ftr_result)\n", - " end\n", - " end\n", - " end \n", - " else\n", - " if jobs_channel !== nothing\n", - " put!(jobs_chnl,(;hops,path,current_distance))\n", - " end\n", - " end\n", - " min_distance\n", - "end\n", - "\n", - "function tsp_dist(connections,city)\n", - " max_hops = 2\n", - " num_cities = length(connections)\n", - " path=zeros(Int,num_cities)\n", - " hops = 1\n", - " path[hops] = city\n", - " current_distance = 0\n", - " min_distance = typemax(Int)\n", - " jobs_chnl = RemoteChannel(()->Channel{Any}(10))\n", - " ftr_result = @spawnat 1 (;path,min_distance_ref=Ref(min_distance))\n", - " task = @async begin\n", - " tsp_dist_impl(connections,hops,path,current_distance,min_distance,max_hops,jobs_chnl,nothing)\n", - " for w in workers()\n", - " put!(job_chnl,nothing)\n", - " end\n", - " end\n", - " @sync for w in workers()\n", - " @spawnat w begin\n", - " max_hops = typemax(Int)\n", - " jobs_channel = nothing\n", - " while true\n", - " job = take!(jobs_chnl)\n", - " if job == nothing\n", - " break\n", - " end\n", - " hobs = job.hobs\n", - " path = job.path\n", - " current_distance = job.current_distance\n", - " tsp_dist_impl(connections,hops,path,current_distance,min_distance,max_hops,jobs_chnl,ftr_result)\n", - " end\n", - " end\n", - " end \n", - " (;path=path,distance=min_distance)\n", - "end\n", - "city = 1\n", - "tsp_dist(connections,city)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "370a1205", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Julia 1.9.0", - "language": "julia", - "name": "julia-1.9" - }, - "language_info": { - "file_extension": ".jl", - "mimetype": "application/julia", - "name": "julia", - "version": "1.9.0" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/src/notebooks/tsp.md b/docs/src/notebooks/tsp.md deleted file mode 100644 index 6b525f9..0000000 --- a/docs/src/notebooks/tsp.md +++ /dev/null @@ -1,30 +0,0 @@ -```@meta -EditURL = "https://github.com/fverdugo/XM_40017/blob/main/docs/src/notebooks/tsp.ipynb" -``` - -```@raw html -
-
Tip
-
- -
-
-``` - -```@raw html - - -```