diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d2cd299..a435a82 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -20,7 +20,7 @@ jobs: - macOS-latest - windows-latest arch: - - x64 + - default - x86 exclude: # Test 32-bit only on Linux @@ -28,18 +28,24 @@ jobs: arch: x86 - os: windows-latest arch: x86 + # No native Apple Silicon binaries for Julia 1.0, test it on an Intel runner instead + - os: macOS-latest + version: "1.0" include: # Add a 1.5 job because that's what Invenia actually uses - os: ubuntu-latest version: "1.5" - arch: x64 + arch: default + - os: macos-15-intel + version: "1.0" + arch: default steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 + - uses: actions/checkout@v7 + - uses: julia-actions/setup-julia@v3 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v2 + - uses: actions/cache@v6 env: cache-name: cache-artifacts with: @@ -50,19 +56,19 @@ jobs: ${{ runner.os }}-${{ matrix.arch }}-test- ${{ runner.os }}-${{ matrix.arch }}- ${{ runner.os }}- - - uses: julia-actions/julia-buildpkg@latest - - uses: julia-actions/julia-runtest@latest + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v7 with: - file: lcov.info + files: lcov.info docs: name: Documentation runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 + - uses: actions/checkout@v7 + - uses: julia-actions/setup-julia@v3 with: version: '1.6' - run: | diff --git a/.github/workflows/IntegrationTest.yml b/.github/workflows/IntegrationTest.yml index 139a131..fbf1372 100644 --- a/.github/workflows/IntegrationTest.yml +++ b/.github/workflows/IntegrationTest.yml @@ -18,14 +18,13 @@ jobs: # - {user: Invenia, repo: Nabla.jl} steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 + - uses: actions/checkout@v7 + - uses: julia-actions/setup-julia@v3 with: version: ${{ matrix.julia-version }} - arch: x64 - - uses: julia-actions/julia-buildpkg@latest + - uses: julia-actions/julia-buildpkg@v1 - name: Clone Downstream - uses: actions/checkout@v2 + uses: actions/checkout@v7 with: repository: ${{ matrix.package.user }}/${{ matrix.package.repo }} path: downstream diff --git a/.github/workflows/JuliaNightly.yml b/.github/workflows/JuliaNightly.yml index 951a1a2..d5af79a 100644 --- a/.github/workflows/JuliaNightly.yml +++ b/.github/workflows/JuliaNightly.yml @@ -12,12 +12,11 @@ jobs: name: Julia Nightly - Ubuntu - x64 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 + - uses: actions/checkout@v7 + - uses: julia-actions/setup-julia@v3 with: version: nightly - arch: x64 - - uses: actions/cache@v2 + - uses: actions/cache@v6 env: cache-name: julia-nightly-cache-artifacts with: @@ -25,9 +24,9 @@ jobs: key: ${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} restore-keys: | ${{ env.cache-name }}- - - uses: julia-actions/julia-buildpkg@latest - - uses: julia-actions/julia-runtest@latest + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v7 with: - file: lcov.info + files: lcov.info diff --git a/docs/src/index.md b/docs/src/index.md index d29896f..3b60327 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -48,6 +48,12 @@ Packages like [ExprTools.jl](https://github.com/invenia/ExprTools.jl) can be hel Its generally fairly simple, though can become complex if you need to handle complicated type-constraints. Examples are shown below. +Make sure every generated overload requires at least one argument of your AD's type. +An overload such as `(op::typeof(+))(::Vararg{Union{Dual,Float64},2})` also matches +`+(::Float64, ::Float64)`, which is type piracy and is ambiguous with Base's methods. +One way to avoid this is to define one method per position of the first argument of your +AD's type, as the examples do. + The hook is automatically triggered whenever a package is loaded. It can also be triggers manually using `refresh_rules`(@ref). This is useful for example if new rules are define in the REPL, or if a package defining rules is modified. diff --git a/test/demos/forwarddiffzero.jl b/test/demos/forwarddiffzero.jl index ae93459..362f37d 100644 --- a/test/demos/forwarddiffzero.jl +++ b/test/demos/forwarddiffzero.jl @@ -40,17 +40,25 @@ function define_dual_overload(sig) all(argT isa Type && Float64 <: argT for argT in argTs) || return # only handling purely Float64 ops. N = length(sig.parameters) - 1 # skip the op - fdef = quote - # we use the function call overloading form as it lets us avoid namespacing issues - # as we can directly interpolate the function type into to the AST. - function (op::$opT)(dual_args::Vararg{Union{Dual, Float64}, $N}; kwargs...) - ȧrgs = (NoTangent(), partial.(dual_args)...) - args = (op, primal.(dual_args)...) - y, ẏ = frule(ȧrgs, args...; kwargs...) - return Dual(y, ẏ) # if y, ẏ are not `Float64` this will error. + names = [Symbol(:x, j) for j in 1:N] + # Define one method per position of the first `Dual` argument, so that every method + # requires at least one `Dual` and we never overload `op` for purely `Float64` arguments + # (which would be type piracy). In particular, nothing is defined if `op` takes no arguments. + for i in 1:N + Ts = [fill(Float64, i - 1); Dual; fill(Union{Dual, Float64}, N - i)] + fdef = quote + # we use the function call overloading form as it lets us avoid namespacing issues + # as we can directly interpolate the function type into the AST. + function (op::$opT)($(map((x, T) -> :($x::$T), names, Ts)...); kwargs...) + dual_args = ($(names...),) + ȧrgs = (NoTangent(), partial.(dual_args)...) + args = (op, primal.(dual_args)...) + y, ẏ = frule(ȧrgs, args...; kwargs...) + return Dual(y, ẏ) # if y, ẏ are not `Float64` this will error. + end end + eval(fdef) end - eval(fdef) end # !Important!: Attach the define function to the `on_new_rule` hook diff --git a/test/demos/reversediffzero.jl b/test/demos/reversediffzero.jl index 578c6e0..b291783 100644 --- a/test/demos/reversediffzero.jl +++ b/test/demos/reversediffzero.jl @@ -69,22 +69,30 @@ function define_tracked_overload(sig) all(argT isa Type && Float64 <: argT for argT in argTs) || return # only handling purely Float64 ops. N = length(sig.parameters) - 1 # skip the op - fdef = quote - # we use the function call overloading form as it lets us avoid namespacing issues - # as we can directly interpolate the function type into to the AST. - function (op::$opT)(tracked_args::Vararg{Union{Tracked, Float64}, $N}; kwargs...) - args = (op, primal.(tracked_args)...) - y, y_pullback = rrule(args...; kwargs...) - the_tape = get_tape(tracked_args) - y_tracked = Tracked(y, the_tape) do ȳ - # pull this partial back and propagate it to the input's partial store - _, ārgs = Iterators.peel(y_pullback(ȳ)) - accum!.(tracked_args, ārgs) + names = [Symbol(:x, j) for j in 1:N] + # Define one method per position of the first `Tracked` argument, so that every method + # requires at least one `Tracked` and we never overload `op` for purely `Float64` arguments + # (which would be type piracy). In particular, nothing is defined if `op` takes no arguments. + for i in 1:N + Ts = [fill(Float64, i - 1); Tracked; fill(Union{Tracked, Float64}, N - i)] + fdef = quote + # we use the function call overloading form as it lets us avoid namespacing issues + # as we can directly interpolate the function type into the AST. + function (op::$opT)($(map((x, T) -> :($x::$T), names, Ts)...); kwargs...) + tracked_args = ($(names...),) + args = (op, primal.(tracked_args)...) + y, y_pullback = rrule(args...; kwargs...) + the_tape = get_tape(tracked_args) + y_tracked = Tracked(y, the_tape) do ȳ + # pull this partial back and propagate it to the input's partial store + _, ārgs = Iterators.peel(y_pullback(ȳ)) + accum!.(tracked_args, ārgs) + end + return y_tracked end - return y_tracked end + eval(fdef) end - eval(fdef) end # !Important!: Attach the define function to the `on_new_rule` hook