Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Metal backend #175

Merged
merged 49 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
eafb0dc
Add MetalExt and weak dependency
GiackAloZ Oct 8, 2024
33e46e8
Create entry point files (still empty)
GiackAloZ Oct 8, 2024
1fdcc74
Add defaults
GiackAloZ Oct 8, 2024
d98ab6f
Add shared functions
GiackAloZ Oct 8, 2024
649dad4
Fix shared functions
GiackAloZ Oct 8, 2024
f633876
Define Metal constants
GiackAloZ Oct 8, 2024
91ab97b
Add Metal kernel int type
GiackAloZ Oct 8, 2024
3059741
Add Metal allocators (not everything for CellArrays just yet)
GiackAloZ Oct 8, 2024
7490133
Add more Metal allocators
GiackAloZ Oct 8, 2024
8917bd8
Add Metal data module function
GiackAloZ Oct 8, 2024
0b716e2
Add function to get Metal streams (queues) in hide comm
GiackAloZ Oct 8, 2024
b69ad03
Add Metal to init parallel kernel
GiackAloZ Oct 8, 2024
0d3fdc9
Implement Metal specific kernel language functions
GiackAloZ Oct 8, 2024
c6c07d0
Add parallel kernel calls
GiackAloZ Oct 8, 2024
f00838e
Add Metal to shared
GiackAloZ Oct 9, 2024
740ced7
Add Metal to PS and tests
GiackAloZ Oct 9, 2024
453e64e
WIP tests and fix compatibility issue (bump Metal to v1.0)
GiackAloZ Oct 9, 2024
0a5858c
Replacing multiplications with floating point with division by intege…
GiackAloZ Oct 9, 2024
16105a5
Add multiple precisions testing and fix literals
GiackAloZ Oct 9, 2024
b2a4196
Add some documentation
GiackAloZ Oct 9, 2024
1dfaf38
Add more docs
GiackAloZ Oct 9, 2024
e4d2f09
Rollback litarals in macros
GiackAloZ Oct 9, 2024
2520510
More rollbacks
GiackAloZ Oct 9, 2024
5495c6d
Fix harmonic macros
GiackAloZ Oct 9, 2024
f8c751f
Partially fix rand metal
GiackAloZ Oct 10, 2024
477f25c
Check Sys if apple before importing Metal in tests
GiackAloZ Oct 10, 2024
0f804d2
Merge branch 'main' into metal
GiackAloZ Oct 10, 2024
e440c85
Fix compat for Metal to 1.2 or higher (restricted to v1)
GiackAloZ Oct 10, 2024
176387d
Put more constraints with Sys.isapple
GiackAloZ Oct 10, 2024
82a358d
Rollback
GiackAloZ Oct 10, 2024
1efcb4e
Update CellArrays version
GiackAloZ Oct 23, 2024
b3f7616
Merge branch 'main' into metal
GiackAloZ Oct 23, 2024
5584fcc
Merge branch 'main' into metal
GiackAloZ Oct 23, 2024
533cd10
Fix test for Metal
GiackAloZ Oct 28, 2024
527444b
Refactor harm macros to use `inv` function instead of division
GiackAloZ Oct 28, 2024
cc6e5ee
Rollback equality checks from approx to exact
GiackAloZ Oct 28, 2024
ab47d5f
Rollback average checks from division to multiplication with precisio…
GiackAloZ Oct 28, 2024
7c9877e
Fix tests for precision and comparisons
GiackAloZ Oct 29, 2024
19b8763
Check for `Sys.isapple()` before importing Metal to avoid errors in t…
GiackAloZ Oct 29, 2024
0fef75d
Fix runtests
GiackAloZ Oct 29, 2024
484ee17
Fix test_allocators
GiackAloZ Oct 29, 2024
73d15fa
Rollback some of the checks
GiackAloZ Oct 29, 2024
56a5d55
Apply suggestions from code review
GiackAloZ Oct 30, 2024
dfb98fe
Update test/test_parallel.jl
GiackAloZ Oct 30, 2024
2d7d128
Update test/test_parallel.jl
GiackAloZ Oct 30, 2024
f7d1d74
Update test_parallel.jl to use the specified precision for lam and dt…
GiackAloZ Oct 30, 2024
325defa
Fix bitwise identical checks for specific tests that were failing
GiackAloZ Oct 30, 2024
7826b4d
Update test/ParallelKernel/test_allocators.jl
GiackAloZ Oct 30, 2024
7179816
Update to use Metal.device() instead of Metal.c u rrent_device() (the…
GiackAloZ Oct 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,22 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
AMDGPU = "21141c5a-9bdb-4563-92ae-f87d6854732e"
CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9"
Metal = "dde4c033-4e86-420c-a63e-0dd931031962"
Polyester = "f517fe37-dbe3-4b94-8317-1923a5111588"

[extensions]
ParallelStencil_AMDGPUExt = "AMDGPU"
ParallelStencil_CUDAExt = "CUDA"
ParallelStencil_EnzymeExt = "Enzyme"
ParallelStencil_MetalExt = "Metal"

[compat]
AMDGPU = "0.6, 0.7, 0.8, 0.9, 1"
CUDA = "3.12, 4, 5"
CellArrays = "0.3"
Enzyme = "0.11, 0.12, 0.13"
MacroTools = "0.5"
Metal = "1.2"
Polyester = "0.7"
StaticArrays = "1"
julia = "1.10" # Minimum version supporting Data module creation
Expand All @@ -35,4 +38,4 @@ TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test", "TOML", "AMDGPU", "CUDA", "Enzyme", "Polyester"]
test = ["Test", "TOML", "AMDGPU", "CUDA", "Metal", "Enzyme", "Polyester"]
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ ParallelStencil empowers domain scientists to write architecture-agnostic high-l

<a id="fig_teff">![Performance ParallelStencil Teff](docs/images/perf_ps2.png)</a>

ParallelStencil relies on the native kernel programming capabilities of [CUDA.jl] and [AMDGPU.jl] and on [Base.Threads] for high-performance computations on GPUs and CPUs, respectively. It is seamlessly interoperable with [ImplicitGlobalGrid.jl], which renders the distributed parallelization of stencil-based GPU and CPU applications on a regular staggered grid almost trivial and enables close to ideal weak scaling of real-world applications on thousands of GPUs \[[1][JuliaCon20a], [2][JuliaCon20b], [3][JuliaCon19], [4][PASC19]\]. Moreover, ParallelStencil enables hiding communication behind computation with a simple macro call and without any particular restrictions on the package used for communication. ParallelStencil has been designed in conjunction with [ImplicitGlobalGrid.jl] for simplest possible usage by domain-scientists, rendering fast and interactive development of massively scalable high performance multi-GPU applications readily accessible to them. Furthermore, we have developed a self-contained approach for "Solving Nonlinear Multi-Physics on GPU Supercomputers with Julia" relying on ParallelStencil and [ImplicitGlobalGrid.jl] \[[1][JuliaCon20a]\]. ParallelStencil's feature to hide communication behind computation was showcased when a close to ideal weak scaling was demonstrated for a 3-D poro-hydro-mechanical real-world application on up to 1024 GPUs on the Piz Daint Supercomputer \[[1][JuliaCon20a]\]:
ParallelStencil relies on the native kernel programming capabilities of [CUDA.jl], [AMDGPU.jl], [Metal.jl] and on [Base.Threads] for high-performance computations on GPUs and CPUs, respectively. It is seamlessly interoperable with [ImplicitGlobalGrid.jl], which renders the distributed parallelization of stencil-based GPU and CPU applications on a regular staggered grid almost trivial and enables close to ideal weak scaling of real-world applications on thousands of GPUs \[[1][JuliaCon20a], [2][JuliaCon20b], [3][JuliaCon19], [4][PASC19]\]. Moreover, ParallelStencil enables hiding communication behind computation with a simple macro call and without any particular restrictions on the package used for communication. ParallelStencil has been designed in conjunction with [ImplicitGlobalGrid.jl] for simplest possible usage by domain-scientists, rendering fast and interactive development of massively scalable high performance multi-GPU applications readily accessible to them. Furthermore, we have developed a self-contained approach for "Solving Nonlinear Multi-Physics on GPU Supercomputers with Julia" relying on ParallelStencil and [ImplicitGlobalGrid.jl] \[[1][JuliaCon20a]\]. ParallelStencil's feature to hide communication behind computation was showcased when a close to ideal weak scaling was demonstrated for a 3-D poro-hydro-mechanical real-world application on up to 1024 GPUs on the Piz Daint Supercomputer \[[1][JuliaCon20a]\]:

![Parallel efficiency of ParallelStencil with CUDA C backend](docs/images/par_eff_c_julia2.png)

Expand All @@ -33,7 +33,7 @@ Beyond traditional high-performance computing, ParallelStencil supports automati
* [References](#references)

## Parallelization and optimization with one macro call
A simple call to `@parallel` is enough to parallelize and optimize a function and to launch it. The package used underneath for parallelization is defined in a call to `@init_parallel_stencil` beforehand. Supported are [CUDA.jl] and [AMDGPU.jl] for running on GPU and [Base.Threads] for CPU. The following example outlines how to run parallel computations on a GPU using the native kernel programming capabilities of [CUDA.jl] underneath (omitted lines are represented with `#(...)`, omitted arguments with `...`):
A simple call to `@parallel` is enough to parallelize and optimize a function and to launch it. The package used underneath for parallelization is defined in a call to `@init_parallel_stencil` beforehand. Supported are [CUDA.jl], [AMDGPU.jl] and [Metal.jl] for running on GPU and [Base.Threads] for CPU. The following example outlines how to run parallel computations on a GPU using the native kernel programming capabilities of [CUDA.jl] underneath (omitted lines are represented with `#(...)`, omitted arguments with `...`):
```julia
#(...)
@init_parallel_stencil(CUDA,...)
Expand Down Expand Up @@ -554,6 +554,7 @@ Please open an issue to discuss your idea for a contribution beforehand. Further
[CellArrays.jl]: https://github.com/omlins/CellArrays.jl
[CUDA.jl]: https://github.com/JuliaGPU/CUDA.jl
[AMDGPU.jl]: https://github.com/JuliaGPU/AMDGPU.jl
[Metal.jl]: https://github.com/JuliaGPU/Metal.jl
[Enzyme.jl]: https://github.com/EnzymeAD/Enzyme.jl
[MacroTools.jl]: https://github.com/FluxML/MacroTools.jl
[StaticArrays.jl]: https://github.com/JuliaArrays/StaticArrays.jl
Expand Down
4 changes: 4 additions & 0 deletions ext/ParallelStencil_MetalExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module ParallelStencil_MetalExt
include(joinpath(@__DIR__, "..", "src", "ParallelKernel", "MetalExt", "shared.jl"))
include(joinpath(@__DIR__, "..", "src", "ParallelKernel", "MetalExt", "allocators.jl"))
end
56 changes: 28 additions & 28 deletions src/FiniteDifferences.jl
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ macro d2(A) @expandargs(A); esc(:( ($A[$ixi+1] - $A[$ixi]) - ($A[$ixi] -
macro all(A) @expandargs(A); esc(:( $A[$ix ] )) end
macro inn(A) @expandargs(A); esc(:( $A[$ixi ] )) end
macro av(A) @expandargs(A); esc(:(($A[$ix] + $A[$ix+1] )*0.5 )) end
macro harm(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ix] + 1.0/$A[$ix+1])*2.0 )) end
macro harm(A) @expandargs(A); esc(:( inv(inv($A[$ix]) + inv($A[$ix+1]))*2.0 )) end
macro maxloc(A) @expandargs(A); esc(:( max( max($A[$ixi-1], $A[$ixi+1]), $A[$ixi] ) )) end
macro minloc(A) @expandargs(A); esc(:( min( min($A[$ixi-1], $A[$ixi+1]), $A[$ixi] ) )) end

Expand Down Expand Up @@ -172,11 +172,11 @@ macro av_xa(A) @expandargs(A); esc(:(($A[$ix ,$iy ] + $A[$ix+1,$iy ] )*0
macro av_ya(A) @expandargs(A); esc(:(($A[$ix ,$iy ] + $A[$ix ,$iy+1] )*0.5 )) end
macro av_xi(A) @expandargs(A); esc(:(($A[$ix ,$iyi ] + $A[$ix+1,$iyi ] )*0.5 )) end
macro av_yi(A) @expandargs(A); esc(:(($A[$ixi ,$iy ] + $A[$ixi ,$iy+1] )*0.5 )) end
macro harm(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ix ,$iy ] + 1.0/$A[$ix+1,$iy ] + 1.0/$A[$ix,$iy+1] + 1.0/$A[$ix+1,$iy+1])*4.0 )) end
macro harm_xa(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ix ,$iy ] + 1.0/$A[$ix+1,$iy ] )*2.0 )) end
macro harm_ya(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ix ,$iy ] + 1.0/$A[$ix ,$iy+1] )*2.0 )) end
macro harm_xi(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ix ,$iyi ] + 1.0/$A[$ix+1,$iyi ] )*2.0 )) end
macro harm_yi(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ixi ,$iy ] + 1.0/$A[$ixi ,$iy+1] )*2.0 )) end
macro harm(A) @expandargs(A); esc(:( inv(inv($A[$ix ,$iy ]) + inv($A[$ix+1,$iy ]) + inv($A[$ix,$iy+1]) + inv($A[$ix+1,$iy+1]))*4.0 )) end
macro harm_xa(A) @expandargs(A); esc(:( inv(inv($A[$ix ,$iy ]) + inv($A[$ix+1,$iy ]))*2.0 )) end
macro harm_ya(A) @expandargs(A); esc(:( inv(inv($A[$ix ,$iy ]) + inv($A[$ix ,$iy+1]))*2.0 )) end
macro harm_xi(A) @expandargs(A); esc(:( inv(inv($A[$ix ,$iyi ]) + inv($A[$ix+1,$iyi ]))*2.0 )) end
macro harm_yi(A) @expandargs(A); esc(:( inv(inv($A[$ixi ,$iy ]) + inv($A[$ixi ,$iy+1]))*2.0 )) end
macro maxloc(A) @expandargs(A); esc(:( max( max( max($A[$ixi-1,$iyi ], $A[$ixi+1,$iyi ]) , $A[$ixi ,$iyi ] ),
max($A[$ixi ,$iyi-1], $A[$ixi ,$iyi+1]) ) )) end
macro minloc(A) @expandargs(A); esc(:( min( min( min($A[$ixi-1,$iyi ], $A[$ixi+1,$iyi ]) , $A[$ixi ,$iyi ] ),
Expand Down Expand Up @@ -361,28 +361,28 @@ macro av_xzi(A) @expandargs(A); esc(:(($A[$ix ,$iyi ,$iz ] + $A[$ix+1,$iyi
$A[$ix ,$iyi ,$iz+1] + $A[$ix+1,$iyi ,$iz+1] )*0.25 )) end
macro av_yzi(A) @expandargs(A); esc(:(($A[$ixi ,$iy ,$iz ] + $A[$ixi ,$iy+1,$iz ] +
$A[$ixi ,$iy ,$iz+1] + $A[$ixi ,$iy+1,$iz+1] )*0.25 )) end
macro harm(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ix ,$iy ,$iz ] + 1.0/$A[$ix+1,$iy ,$iz ] +
1.0/$A[$ix+1,$iy+1,$iz ] + 1.0/$A[$ix+1,$iy+1,$iz+1] +
1.0/$A[$ix ,$iy+1,$iz+1] + 1.0/$A[$ix ,$iy ,$iz+1] +
1.0/$A[$ix+1,$iy ,$iz+1] + 1.0/$A[$ix ,$iy+1,$iz ] )*8.0)) end
macro harm_xa(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ix ,$iy ,$iz ] + 1.0/$A[$ix+1,$iy ,$iz ] )*2.0 )) end
macro harm_ya(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ix ,$iy ,$iz ] + 1.0/$A[$ix ,$iy+1,$iz ] )*2.0 )) end
macro harm_za(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ix ,$iy ,$iz ] + 1.0/$A[$ix ,$iy ,$iz+1] )*2.0 )) end
macro harm_xi(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ix ,$iyi ,$izi ] + 1.0/$A[$ix+1,$iyi ,$izi ] )*2.0 )) end
macro harm_yi(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ixi ,$iy ,$izi ] + 1.0/$A[$ixi ,$iy+1,$izi ] )*2.0 )) end
macro harm_zi(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ixi ,$iyi ,$iz ] + 1.0/$A[$ixi ,$iyi ,$iz+1] )*2.0 )) end
macro harm_xya(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ix ,$iy ,$iz ] + 1.0/$A[$ix+1,$iy ,$iz ] +
1.0/$A[$ix ,$iy+1,$iz ] + 1.0/$A[$ix+1,$iy+1,$iz ] )*4.0 )) end
macro harm_xza(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ix ,$iy ,$iz ] + 1.0/$A[$ix+1,$iy ,$iz ] +
1.0/$A[$ix ,$iy ,$iz+1] + 1.0/$A[$ix+1,$iy ,$iz+1] )*4.0 )) end
macro harm_yza(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ix ,$iy ,$iz ] + 1.0/$A[$ix ,$iy+1,$iz ] +
1.0/$A[$ix ,$iy ,$iz+1] + 1.0/$A[$ix ,$iy+1,$iz+1] )*4.0 )) end
macro harm_xyi(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ix ,$iy ,$izi ] + 1.0/$A[$ix+1,$iy ,$izi ] +
1.0/$A[$ix ,$iy+1,$izi ] + 1.0/$A[$ix+1,$iy+1,$izi ] )*4.0 )) end
macro harm_xzi(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ix ,$iyi ,$iz ] + 1.0/$A[$ix+1,$iyi ,$iz ] +
1.0/$A[$ix ,$iyi ,$iz+1] + 1.0/$A[$ix+1,$iyi ,$iz+1] )*4.0 )) end
macro harm_yzi(A) @expandargs(A); esc(:(1.0/(1.0/$A[$ixi ,$iy ,$iz ] + 1.0/$A[$ixi ,$iy+1,$iz ] +
1.0/$A[$ixi ,$iy ,$iz+1] + 1.0/$A[$ixi ,$iy+1,$iz+1] )*4.0 )) end
macro harm(A) @expandargs(A); esc(:( inv(inv($A[$ix ,$iy ,$iz ]) + inv($A[$ix+1,$iy ,$iz ]) +
inv($A[$ix+1,$iy+1,$iz ]) + inv($A[$ix+1,$iy+1,$iz+1]) +
inv($A[$ix ,$iy+1,$iz+1]) + inv($A[$ix ,$iy ,$iz+1]) +
inv($A[$ix+1,$iy ,$iz+1]) + inv($A[$ix ,$iy+1,$iz ]) )*8.0 )) end
macro harm_xa(A) @expandargs(A); esc(:( inv(inv($A[$ix ,$iy ,$iz ]) + inv($A[$ix+1,$iy ,$iz ]) )*2.0 )) end
macro harm_ya(A) @expandargs(A); esc(:( inv(inv($A[$ix ,$iy ,$iz ]) + inv($A[$ix ,$iy+1,$iz ]) )*2.0 )) end
macro harm_za(A) @expandargs(A); esc(:( inv(inv($A[$ix ,$iy ,$iz ]) + inv($A[$ix ,$iy ,$iz+1]) )*2.0 )) end
macro harm_xi(A) @expandargs(A); esc(:( inv(inv($A[$ix ,$iyi ,$izi ]) + inv($A[$ix+1,$iyi ,$izi ]) )*2.0 )) end
macro harm_yi(A) @expandargs(A); esc(:( inv(inv($A[$ixi ,$iy ,$izi ]) + inv($A[$ixi ,$iy+1,$izi ]) )*2.0 )) end
macro harm_zi(A) @expandargs(A); esc(:( inv(inv($A[$ixi ,$iyi ,$iz ]) + inv($A[$ixi ,$iyi ,$iz+1]) )*2.0 )) end
macro harm_xya(A) @expandargs(A); esc(:( inv(inv($A[$ix ,$iy ,$iz ]) + inv($A[$ix+1,$iy ,$iz ]) +
inv($A[$ix ,$iy+1,$iz ]) + inv($A[$ix+1,$iy+1,$iz ]) )*4.0 )) end
macro harm_xza(A) @expandargs(A); esc(:( inv(inv($A[$ix ,$iy ,$iz ]) + inv($A[$ix+1,$iy ,$iz ]) +
inv($A[$ix ,$iy ,$iz+1]) + inv($A[$ix+1,$iy ,$iz+1]) )*4.0 )) end
macro harm_yza(A) @expandargs(A); esc(:( inv(inv($A[$ix ,$iy ,$iz ]) + inv($A[$ix ,$iy+1,$iz ]) +
inv($A[$ix ,$iy ,$iz+1]) + inv($A[$ix ,$iy+1,$iz+1]) )*4.0 )) end
macro harm_xyi(A) @expandargs(A); esc(:( inv(inv($A[$ix ,$iy ,$izi ]) + inv($A[$ix+1,$iy ,$izi ]) +
inv($A[$ix ,$iy+1,$izi ]) + inv($A[$ix+1,$iy+1,$izi ]) )*4.0 )) end
macro harm_xzi(A) @expandargs(A); esc(:( inv(inv($A[$ix ,$iyi ,$iz ]) + inv($A[$ix+1,$iyi ,$iz ]) +
inv($A[$ix ,$iyi ,$iz+1]) + inv($A[$ix+1,$iyi ,$iz+1]) )*4.0 )) end
macro harm_yzi(A) @expandargs(A); esc(:( inv(inv($A[$ixi ,$iy ,$iz ]) + inv($A[$ixi ,$iy+1,$iz ]) +
inv($A[$ixi ,$iy ,$iz+1]) + inv($A[$ixi ,$iy+1,$iz+1]) )*4.0 )) end
macro maxloc(A) @expandargs(A); esc(:( max( max( max( max($A[$ixi-1,$iyi ,$izi ], $A[$ixi+1,$iyi ,$izi ]) , $A[$ixi ,$iyi ,$izi ] ),
max($A[$ixi ,$iyi-1,$izi ], $A[$ixi ,$iyi+1,$izi ]) ),
max($A[$ixi ,$iyi ,$izi-1], $A[$ixi ,$iyi ,$izi+1]) ) )) end
Expand Down
Loading
Loading