Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .buildkite/jll.pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ steps:
fi
julia --project -e 'using Pkg; Pkg.resolve(); Pkg.instantiate()'
- JuliaCI/julia-test#v1:
test_args: "--quickfail --jobs=8 --verbose"
test_args: "--jobs=8 --verbose"
- JuliaCI/julia-coverage#v1:
dirs:
- src
Expand Down
2 changes: 1 addition & 1 deletion .buildkite/run_developer_ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,5 @@ cp LocalPreferences.toml test/LocalPreferences.toml

julia --color=yes --project=. -e '
using Pkg
Pkg.test("cuNumeric"; test_args = ["--quickfail", "--jobs=8", "--verbose"])
Pkg.test("cuNumeric"; test_args = ["--jobs=8", "--verbose"])
'
6 changes: 3 additions & 3 deletions .buildkite/upload_gpu_ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ message="${BUILDKITE_MESSAGE:-}"
run_jll=true
run_developer=true

# Keep both suites for main and PRs into main. For non-main PRs, select the
# suite whose wrapper matches the code under test.
# Keep both suites for main and PRs into main. For non-main PRs and post-merge
# develop builds, select the suite whose wrapper matches the code under test.
if [[ "$branch" != "main" && "$base_branch" != "main" ]]; then
if [[ "$message" =~ \[skip[[:space:]]jll\] ]]; then
echo "Skipping JLL GPU CI because the build message contains [skip jll]."
run_jll=false
elif [[ "$pull_request" != "false" && -n "$base_branch" ]]; then
elif [[ ("$pull_request" != "false" && -n "$base_branch") || "$branch" == "develop" ]]; then
base_ref="refs/remotes/origin/$WRAPPER_BASE_BRANCH"
# The published wrapper JLL tracks main, so compare against main even
# when the pull request targets develop.
Expand Down
37 changes: 28 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: CI CPU
name: CI

on:
workflow_dispatch:
Expand All @@ -18,7 +18,10 @@ on:
- 'scripts/**'
- 'deps/build.jl'
- 'Project.toml'
- 'lib/cunumeric_jl_wrapper/**'
- 'lib/CNPreferences/src/**'
- '.github/workflows/ci.yml'
- '.github/workflows/developer.yml'
tags:
- 'v*'
branches:
Expand All @@ -29,13 +32,17 @@ on:
- 'scripts/**'
- 'deps/build.jl'
- 'Project.toml'
- 'lib/cunumeric_jl_wrapper/**'
- 'lib/CNPreferences/src/**'
- '.github/workflows/ci.yml'
- '.github/workflows/developer.yml'
jobs:
pkg_resolve:
resolve:
name: Package resolution
uses: ./.github/workflows/pkg_resolve.yml

check_changes:
name: Check for wrapper changes
wrapper_changes:
name: Wrapper change detection
runs-on: ubuntu-latest
outputs:
wrapper_changed: ${{ steps.wrapper-changes.outputs.changed }}
Expand All @@ -60,10 +67,22 @@ jobs:
fi
fi

test:
name: Julia ${{ matrix.julia }} - ${{ matrix.os }}
needs: [pkg_resolve, check_changes]
if: ${{ github.base_ref == 'main' || needs.check_changes.outputs.wrapper_changed != 'true' }}
developer_tests:
name: Developer wrapper tests
needs: [resolve, wrapper_changes]
if: ${{ github.event_name != 'pull_request' || github.base_ref == 'main' || needs.wrapper_changes.outputs.wrapper_changed == 'true' }}
permissions:
contents: read
packages: write
attestations: write
id-token: write
actions: write
uses: ./.github/workflows/developer.yml

jll_tests:
name: JLL wrapper tests - Julia ${{ matrix.julia }} - ${{ matrix.os }}
needs: [resolve, wrapper_changes]
if: ${{ github.base_ref == 'main' || needs.wrapper_changes.outputs.wrapper_changed != 'true' }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
Expand Down Expand Up @@ -125,4 +144,4 @@ jobs:
LEGATE_AUTO_CONFIG: "0"
LEGATE_SKIP_RUNTIME: "true"
LEGATE_CONFIG: "--cpus 1 --utility 1 --sysmem 500"
run: julia --project -e 'using Pkg; Pkg.test(test_args=["--quickfail", "--jobs=2", "--verbose"])'
run: julia --project -e 'using Pkg; Pkg.test(test_args=["--jobs=2", "--verbose"])'
2 changes: 1 addition & 1 deletion .github/workflows/container.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:
required: false
default: false
workflow_run:
workflows: ['CI CPU']
workflows: ['CI']
types: [completed]
branches:
- main
Expand Down
57 changes: 7 additions & 50 deletions .github/workflows/developer.yml
Original file line number Diff line number Diff line change
@@ -1,55 +1,12 @@
# Develeper CI test. This will build the workflow using Jlls and building wrappers from SRC
name: Develeper CI test
# Developer wrapper tests build the wrappers from source instead of using JLLs.
name: Developer Wrapper Tests

on:
workflow_dispatch:
inputs:
tag:
description: 'Tag to build instead'
required: false
default: ''
mark_as_latest:
description: 'Mark as latest'
type: boolean
required: false
default: false
push:
paths:
- 'src/**'
- 'scripts/**'
- 'deps/build.jl'
- 'Project.toml'
- 'lib/cunumeric_jl_wrapper/src/**'
- 'lib/cunumeric_jl_wrapper/include/**'
- 'lib/CNPreferences/src/**'
- '.github/workflows/developer.yml'
tags:
- 'v*'
branches:
- main
pull_request:
paths:
- 'src/**'
- 'scripts/**'
- 'deps/build.jl'
- 'Project.toml'
- 'lib/cunumeric_jl_wrapper/src/**'
- 'lib/cunumeric_jl_wrapper/include/**'
- 'lib/CNPreferences/src/**'
- '.github/workflows/developer.yml'
jobs:
pkg_resolve:
uses: ./.github/workflows/pkg_resolve.yml
workflow_call:

docs:
name: Developer CI test - Julia ${{ matrix.julia }}
needs: pkg_resolve
permissions:
contents: read
packages: write
attestations: write
id-token: write
actions: write
jobs:
test:
name: Julia ${{ matrix.julia }}
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -157,4 +114,4 @@ jobs:

cp LocalPreferences.toml test/LocalPreferences.toml

julia --color=yes --project=. -e 'using Pkg; Pkg.test("cuNumeric"; test_args=["--quickfail", "--jobs=2", "--verbose"])'
julia --color=yes --project=. -e 'using Pkg; Pkg.test("cuNumeric"; test_args=["--jobs=2", "--verbose"])'
4 changes: 2 additions & 2 deletions .github/workflows/pkg_resolve.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
name: Pkg Resolve
name: Package Resolution

on:
workflow_call:

jobs:
resolve:
name: Pkg.resolve
name: Resolve dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ benchmark/results/*
benchmark/plots**

compile_wrapper.sh
__plot_results.jl

*.tar.gz
# generated by CMake
Expand Down
89 changes: 21 additions & 68 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,128 +1,89 @@
<h1>
<img src="docs/src/assets/logo.png" alt="cuNumeric.jl" width="50">
<a href="https://julialegate.github.io/cuNumeric.jl/dev/">cuNumeric.jl</a>
</h1>
<h1>
<img src="docs/src/assets/logo.png" alt="cuNumeric.jl" width="50">
<a href="https://julialegate.github.io/cuNumeric.jl/dev/">cuNumeric.jl</a>
<a href="https://julialegate.github.io/cuNumeric.jl/dev">cuNumeric.jl</a>
</h1>

[![Documentation dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://julialegate.github.io/cuNumeric.jl/dev/) [![codecov](https://codecov.io/github/julialegate/cuNumeric.jl/branch/main/graph/badge.svg)](https://app.codecov.io/github/JuliaLegate/cuNumeric.jl) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
[![Documentation dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://julialegate.github.io/cuNumeric.jl/dev/) [![codecov](https://codecov.io/github/julialegate/cuNumeric.jl/branch/main/graph/badge.svg)](https://app.codecov.io/github/JuliaLegate/cuNumeric.jl) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
[![Documentation dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://julialegate.github.io/cuNumeric.jl/dev) [![codecov](https://codecov.io/github/julialegate/cuNumeric.jl/branch/main/graph/badge.svg)](https://app.codecov.io/github/JuliaLegate/cuNumeric.jl) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)

cuNumeric.jl wraps and extends the [cuPyNumeric](https://github.com/nv-legate/cupynumeric) library from NVIDIA to bring distributed array computing on GPUs and CPUs to Julia. The central type is `NDArray`, which behaves like Julia's `Array` or the `CuArray` from [CUDA.jl](https://github.com/juliagpu/cuda.jl), but executes across multiple GPUs/CPUs. We implement array-level operations on `NDArray` which can be composed into larger programs without the need for explicit MPI calls or writing CUDA kernels.
cuNumeric.jl wraps and extends the [cuPyNumeric](https://github.com/nv-legate/cupynumeric) library from NVIDIA to bring distributed array computing on GPUs and CPUs to Julia. The central type is `NDArray`, which behaves like Julia's `Array` or the `CuArray` from [CUDA.jl](https://github.com/juliagpu/cuda.jl), but executes across multiple GPUs/CPUs. We implement array-level operations on `NDArray` which can be composed into larger programs without the need for explicit MPI calls or writing CUDA kernels.

cuNumeric.jl requires x86 Linux, an NVIDIA GPU, and Julia >= 1.10. If ARM support is of interest open an issue.
cuNumeric.jl requires x86 Linux, an NVIDIA GPU, and Julia >= 1.10. If ARM support is of interest open an issue.

### Quick Start

cuNumeric.jl can be installed with the Julia package manager. Activate your preferred environment and then from the Julia REPL run:


cuNumeric.jl can be installed with the Julia package manager. Activate your preferred environment and then from the Julia REPL run:

```julia
using Pkg
Pkg.add(url = "https://github.com/JuliaLegate/cuNumeric.jl", rev = "main")
using Pkg
Pkg.add(url = "https://github.com/JuliaLegate/cuNumeric.jl", rev = "main")
```

The first time might take awhile as it has to install multiple large dependencies such as the CUDA SDK (if you have an NVIDIA GPU). To use a local build of cupynumeric.so, see [Build Modes](./install.md).

The first time might take awhile as it has to install multiple large dependencies such as the CUDA SDK (if you have an NVIDIA GPU). To use a local build of cupynumeric.so, see [Build Modes](./install.md).
The first installation can take a while because it includes several large dependencies, such as the CUDA SDK. To use a local cupynumeric build, see [Build Modes](https://julialegate.github.io/cuNumeric.jl/dev/install).

```julia
using cuNumeric
using cuNumeric
cuNumeric.versioninfo()
```

> [!WARNING]
> Starting more than one instance of cuNumeric.jl can lead to a hard-crash. The default hardware configuration reserves all available resources.

For more details, see [Hardware](./configuration/hardware.md).
For more details, see [Hardware](https://julialegate.github.io/cuNumeric.jl/dev/configuration/hardware).

### How `NDArray`s work

The semantics of `NDArray` closely mirror Julia's `Array`, and in most cases it is a drop-in replacement. You can use the same constructors (i.e., `zeros`, `ones`, `rand`), broadcasting, slicing, and linear algebra. Under the hood a few details differ from Base, and knowing them can help you write fast code.

**Data may live across many devices.** An `NDArray` is a logical array whose physical buffers can be partitioned over GPUs and CPUs by the Legate runtime. You write ordinary array code and Legate decides where the data lives and how/when it is communicated between devices. As a result, elementwise indexing (i.e. `arr[1]`) is slow (and is prevented by default). Scalar indexing like this forces synchronization and blocks other tasks from executing. Functions like `println` result in data being copied to the host and can also be slow.
**Data may live across many devices.** An `NDArray` is a logical array whose physical buffers can be partitioned over GPUs and CPUs by the Legate runtime. You write ordinary array code and Legate decides where the data lives and how/when it is communicated between devices. As a result, elementwise indexing (i.e. `arr[1]`) is slow (and is prevented by default). Scalar indexing like this forces synchronization and blocks other tasks from executing.

**Slices are views.** Indexing an `NDArray` with ranges returns a view onto the same store, not a copy. That differs from Base Julia, where `A[1:n]` allocates a new `Array`. Mutations through an `NDArray` slice are visible through other aliases of the same data.

**Reductions return arrays, not Julia scalars.** Reductions such as `sum(A)` produce a **0D or 1D** `NDArray` (axis reductions produce a lower-rank `NDArray`), rather than a bare `Float64` / `Float32`. That keeps the Legate task graph asynchronous instead of forcing synchronization to communite with the Julia runtime. When you need a plain Julia number, call `unwrap` or `only`:
**Reductions return arrays, not Julia scalars.** Reductions such as `sum(A)` produce a **0D or 1D** `NDArray` (axis reductions produce a lower-rank `NDArray`), rather than a bare `Float64` / `Float32`. That keeps the Legate task graph asynchronous instead of forcing synchronization to communicate with the Julia runtime. When you need a plain Julia number, call `unwrap`:

```julia
s = sum(A) # NDArray{T,0}
x = unwrap(s) # T, e.g. Float32
x2 = only(s)
```
s = sum(A) # NDArray{T,0}
x = unwrap(s) # T, e.g. Float32
```

**The Legate runtime builds a DAG asynchronously.** Calling `cuNumeric.zeros` or `A .+ B` records work into Legate's task graph rather than blocking until every GPU kernel finishes. Results are materialized when you need them (for example `println`, `unwrap`, or converting with `Array(A)`). Hiding latency enables performant code.
**The Legate runtime builds a DAG asynchronously.** Calling `cuNumeric.zeros` or `A .+ B` records work into Legate's task graph rather than blocking until every GPU kernel finishes. Results are materialized when you need them (for example `println`, `unwrap`, or converting with `Array(A)`). Hiding latency enables performant code.

For API details see [Initialization](./api_initialization.md) and [NDArray Reference](./api.md). For anti-patterns that kill performance, see [Patterns to Avoid](./perf/patterns_to_avoid.md).
For API details see [Initialization](./api_initialization.md) and [NDArray Reference](./api.md). For anti-patterns that kill performance, see [Patterns to Avoid](./perf/patterns_to_avoid.md).
For API details see [Initialization](https://julialegate.github.io/cuNumeric.jl/dev/api_initialization) and [NDArray Reference](https://julialegate.github.io/cuNumeric.jl/dev/api). For common performance pitfalls, see [Patterns to Avoid](https://julialegate.github.io/cuNumeric.jl/dev/perf/patterns_to_avoid).

### Kernel Fusion
### Kernel Fusion

Nested broadcast expressions fuse into a single kernel by default when on GPU. Prefer `@.` for multi-op elementwise code so every operator is dotted and the expression stays completely fused. Even just forgetting the `.` on unary negation (i.e., `y .= -a .+ b .* c`) will result in unfused code. Use the following pattern instead.
Nested broadcast expressions fuse into a single kernel by default when on GPU. Prefer `@.` for multi-op elementwise code so every operator is dotted and the expression stays completely fused. Even just forgetting the `.` on unary negation (i.e., `y .= -a .+ b .* c`) will result in unfused code. Use the following pattern instead.

```julia
y .= @. -a + b * c
```julia
y .= @. -a + b * c
```

See [Kernel Fusion](./perf/kernel_fusion.md) and [Debugging](./debugging.md) for controls and pretty printers.

See [Kernel Fusion](./perf/kernel_fusion.md) and [Debugging](./debugging.md) for controls and pretty printers.

### Helping the Garbage Collector

Many calls such as array slicing and un-fused broadcasts allocate a new `NDArray`. The Legate runtime keeps track of all references to the underlying data and will not free the memory until Julia's GC frees the `NDArray` handles. Because Julia's GC runs on memory pressure and an `NDArray` only stores a pointer (i.e., Julia's GC does not know the true size), many dead buffers accumulate and can cause out-of-memory errors.
Many calls such as array slicing and un-fused broadcasts allocate a new `NDArray`. The Legate runtime keeps track of all references to the underlying data and will not free the memory until Julia's GC frees the `NDArray` handles. Because Julia's GC runs on memory pressure and an `NDArray` only stores a pointer (i.e., Julia's GC does not know the true size), many dead buffers accumulate and can cause out-of-memory errors.

`@analyze_lifetimes` performs a **static last-use analysis** at macro-expansion time and inserts eager calls to immediately free unused `NDArrays`. These buffers can then be reused by legate later for same-sized allocations.
`@analyze_lifetimes` performs a **static last-use analysis** at macro-expansion time and inserts eager calls to immediately free unused `NDArrays`. These buffers can then be reused by legate later for same-sized allocations.
See [Kernel Fusion](https://julialegate.github.io/cuNumeric.jl/dev/perf/kernel_fusion) and [Debugging](https://julialegate.github.io/cuNumeric.jl/dev/debugging) for controls and diagnostics.

```julia
@analyze_lifetimes begin
result = @. A[1:end, :] + B[1:end, :]
C .= @. result * 2.0f0
result = @. A[1:end, :] + B[1:end, :]
C .= @. result * 2.0f0
end
```
### The `@accelerate` macro

### Performance at a glance
`@accelerate` fuses eligible GPU broadcasts within and across statements, then releases materialized temporary `NDArray`s after their last use on CPU or GPU. See [The `@accelerate` Macro](https://julialegate.github.io/cuNumeric.jl/dev/perf/reduce_allocations) for usage guidance.

A representative benchmark figure will go here (add something like `docs/src/images/benchmarks-overview.png` when ready).
### Benchmarks

Numbers, plots, and how to reproduce them live under [Benchmark Results](./benchmarks/results.md) and [How to Benchmark](./benchmarks/howto.md).
Results and reproduction instructions live under [Benchmark Results](https://julialegate.github.io/cuNumeric.jl/dev/benchmarks/results) and [How to Benchmark](https://julialegate.github.io/cuNumeric.jl/dev/benchmarks/howto).

### Try an example

```julia
using cuNumeric

integrand = (x) -> @. exp(-x^2)
integrand(x) = @. exp(-x^2)

@accelerate function monte_carlo(N, x_max)
Ω = 2 * x_max
raw_samples = cuNumeric.rand(N)
samples = @. Ω * raw_samples - x_max
return (Ω / N) * sum(integrand(samples))
end

N = 1_000_000
x_max = 10.0f0
Ω = 2 * x_max

samples = Ω .* cuNumeric.rand(N)
samples = samples .- x_max
estimate = (Ω / N) .* sum(integrand(samples))
estimate = monte_carlo(N, x_max)

println("Monte-Carlo Estimate: $(estimate)")
```
Expand All @@ -131,11 +92,3 @@ More worked examples (initialization, Gray-Scott, …) are in the documentation
### Known Limitations

- There is no support for `Float16` or `ComplexF16`
- Arrays with 4 or more dimensions might have worse performance
- Maximum array dimension is 6


### Known Deviations from Base Julia
- Reductions return 0D stores instead of scalars
- Slices return views
- `inv` does not throw `SingularException` for singular matrices
7 changes: 5 additions & 2 deletions benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Benchmarks are declared in `benchmarks.toml`. `run.jl` parses it.
## Running

```bash
julia --project run.jl # runs whatever benchmarks.toml configures
julia --project=. run.jl # runs whatever benchmarks.toml configures
```

`run.jl` runs each (benchmark, backend) pair in its own process via
Expand Down Expand Up @@ -35,7 +35,7 @@ n_warmup = 5
n_iter = 1000
n_trial = 5

[[gemm]] # name registered in src/benchmarks.jl
[[gemm]] # name registered under src/benchmarks/
T = "Float32" # element type
gpus = 1
cpus = 2
Expand All @@ -59,6 +59,9 @@ two axes:
default `true`); it only affects cuNumeric, so comparison backends run once, not
per variant.

`grayscott_baseline` measures the step as written;
`grayscott_accelerated` wraps the step in `@accelerate` and is cuNumeric-only.

Each zipped field must be one of:

- a scalar or single-element list (`cpus = 2` or `[2]`) -> broadcast to every config
Expand Down
Loading
Loading