Skip to content

Commit

Permalink
Rename Phi to Tapir throughout package (#104)
Browse files Browse the repository at this point in the history
* Rename Phi to Tapir throughout package

* Add src back in

* Fix typo
  • Loading branch information
willtebbutt authored Mar 28, 2024
1 parent b243aa0 commit 41a1561
Show file tree
Hide file tree
Showing 42 changed files with 299 additions and 299 deletions.
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name = "Phi"
name = "Tapir"
uuid = "07d77754-e150-4737-8c94-cd238a1fb45b"
authors = ["Will Tebbutt and contributors"]
version = "0.1.0"
Expand All @@ -21,7 +21,7 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b"

[extensions]
PhiSpecialFunctionsExt = "SpecialFunctions"
TapirSpecialFunctionsExt = "SpecialFunctions"

[compat]
BenchmarkTools = "1"
Expand Down
31 changes: 14 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Phi
# Tapir

[![Build Status](https://github.com/withbayes/Phi.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/withbayes/Phi.jl/actions/workflows/CI.yml?query=branch%3Amain)
[![Build Status](https://github.com/withbayes/Tapir.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/withbayes/Tapir.jl/actions/workflows/CI.yml?query=branch%3Amain)
[![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/invenia/BlueStyle)
[![ColPrac: Contributor's Guide on Collaborative Practices for Community Packages](https://img.shields.io/badge/ColPrac-Contributor's%20Guide-blueviolet)](https://github.com/SciML/ColPrac)

The goal of the `Phi.jl` project is to produce a reverse-mode AD package which is written entirely in Julia, and improves over both `ReverseDiff.jl` and `Zygote.jl` in several ways, and is competitive with `Enzyme.jl`.
The goal of the `Tapir.jl` project is to produce a reverse-mode AD package which is written entirely in Julia, and improves over both `ReverseDiff.jl` and `Zygote.jl` in several ways, and is competitive with `Enzyme.jl`.

# How it works

`Phi.jl` is based around a function `rrule!!` (which computes vector-Jacobian products (VJPs)) and a related function `build_rrule` (which builds functions which are semantically identical to `rrule!!`).
`Tapir.jl` is based around a function `rrule!!` (which computes vector-Jacobian products (VJPs)) and a related function `build_rrule` (which builds functions which are semantically identical to `rrule!!`).
These VJPs can, for example, be used to compute gradients.
`rrule!!` is similar to ChainRules' `rrule` and Zygote's `_pullback`, but supports functions which mutate (modify) their arguments, in addition to those that do not, and immediately increments (co)tangents.
It has, perhaps unsurprisingly, wound up looking quite similar to the rule system in Enzyme.
Expand All @@ -34,7 +34,7 @@ Thus you should equate `rrule!!`'s support for mutation with good support for ex
Conversely, you should equate `Zygote.jl`'s / `ReverseDiff.jl`'s patchy support for mutation with patchy support for existing code.

`rrule!!`s impose no constraints on the types which can be operated on, as with `ChainRules`'s `rrule` and `Zygote`'s `_pullback`.
Consequently, there is in principle nothing to prevent `Phi.jl` from operating on any type, for example, structured arrays, GPU arrays, and complicated `struct`s / `mutable struct`s.
Consequently, there is in principle nothing to prevent `Tapir.jl` from operating on any type, for example, structured arrays, GPU arrays, and complicated `struct`s / `mutable struct`s.


### Correctness and Testing
Expand All @@ -55,7 +55,7 @@ This contrasts with e.g. `ReverseDiff.jl`'s compiled tape, which can give silent

### Performance

Hand-written `rrule!!`s have excellent performance, provided that they have been written well (most of the hand-written rules in `Phi.jl` have excellent performance, but some require optimisation. Doing this just requires investing some time).
Hand-written `rrule!!`s have excellent performance, provided that they have been written well (most of the hand-written rules in `Tapir.jl` have excellent performance, but some require optimisation. Doing this just requires investing some time).
Consequently, whether or not the overall AD system has good performance is largely a question of how much overhead is associated to the mechanism by which hand-written `rrules!!`s are algorithmically composed.

~~At present (11/2023), we do _not_ do this in a performant way, but this will change.~~
Expand All @@ -67,17 +67,14 @@ Additionally, the strategy of immediately incrementing (co)tangents resolves lon

### Written entirely in Julia

`Phi.jl` is written entirely in Julia.
`Tapir.jl` is written entirely in Julia.
This sits in contrast to `Enzyme.jl`, which targets LLVM and is primarily written in C++.
These two approaches entail different tradeoffs.

# Project Name

This package is called `Phi.jl` because 1) this package supports automatic differentiation for Julia functions containing input-dependent control flows (aka `phi`-nodes in SSA-IR), 2) the developers spent many hours figuring out how to handle the various edge cases associated with phi nodes, which inspired this name.
At the time of writing (25/03/2024), we continue to improve the performance of our implementation.
Additionally, `Phi.jl` is reasonably memorable, and snappy.

This package was initially called `Taped.jl`, but that name ceased to be helpful when we stopped using a classic "Wengert list"-style type to implement AD.
Before an initial release, this package was called `Taped.jl`, but that name ceased to be helpful when we stopped using a classic "Wengert list"-style type to implement AD.
For about 48 hours is was called `Phi.jl`, but the community guidelines state that the name of packages in the general registry should generally be at least 5 characters in length.

# Project Status

Expand All @@ -91,9 +88,9 @@ We aim to reach the maintenance phase of the project before 01/06/2024.

*Update: (22/03/2024)*
Phase 2 is now further along.
`Phi.jl` now uses something which could reasonably be described as a source-to-source system to perform AD.
`Tapir.jl` now uses something which could reasonably be described as a source-to-source system to perform AD.
At present the performance of this system is not as good as that of Enzyme, but often beats compiled ReverseDiff, and comfortably beats Zygote in any situations involving dynamic control flow.
The present focus is on dealing with some remaining performance limitations that should make `Phi.jl`'s performance much closer to that of Enzyme, and consistently beat ReverseDiff on a range of benchmarks.
The present focus is on dealing with some remaining performance limitations that should make `Tapir.jl`'s performance much closer to that of Enzyme, and consistently beat ReverseDiff on a range of benchmarks.
Fortunately, dealing with these performance limitations necessitates simplifying the internals substantially.

*Update: (16/01/2024)*
Expand All @@ -114,7 +111,7 @@ There is not presently a high-level interface to which we are yet commiting, but
They both provide a high-level interface which will let you differentiate things, and their implementation demonstrates how an `rrule!!` / rrule-like function should be used.
(There are a couple of things that you have to get right when using `rrule!!` / functions returned from `build_rrule`, so it's best to use `value_and_pullback!!`).

*Note:* I have found that using a mixture of `PProf` and the `@profview` functionality from Julia's `VSCode` extension essential when profiling code generated by `Phi.jl`.
*Note:* I have found that using a mixture of `PProf` and the `@profview` functionality from Julia's `VSCode` extension essential when profiling code generated by `Tapir.jl`.
`PProf` provides complete type information on its flame graphs, which is important for figuring out what is getting called, but it doesn't highilght type-instabilities.
Conversely, `@profview` does highlight type-instabilities, but fails to provide complete type information.
So if you use both at the same time, you can get all of the information needed.
Expand All @@ -133,7 +130,7 @@ Please be aware that by "performant" we mean similar or better performance than

### What won't work

While `Phi.jl` should now work on a very large subset of the language, there remain things that you should expect not to work. A non-exhaustive list of things to bear in mind includes:
While `Tapir.jl` should now work on a very large subset of the language, there remain things that you should expect not to work. A non-exhaustive list of things to bear in mind includes:
1. It is always necessary to produce hand-written for `ccall`s (and, more generally, foreigncall nodes). We have rules for many `ccall`s, but not all. If you encounter a foreigncall without a hand-written rule, you should get an informative error message which tells you what is going on and how to deal with it.
1. Builtins which require rules. The vast majority of them have rules now, but some don't. Notably, `apply_iterate` does not have a rule, so `Phi.jl` cannot currently AD through type-unstable splatting -- someone should resolve this.
1. Builtins which require rules. The vast majority of them have rules now, but some don't. Notably, `apply_iterate` does not have a rule, so `Tapir.jl` cannot currently AD through type-unstable splatting -- someone should resolve this.
1. Anything involving tasks / threading -- we have no thread safety guarantees and, at the time of writing, I'm not entirely sure what error you will find if you attempt to AD through code which uses Julia's task / thread system. The same applies to distributed computing. These limitations ought to be possible to resolve.
2 changes: 1 addition & 1 deletion bench/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
ReverseDiff = "37e2e3b7-166d-5795-8a7a-e32c996b4267"
Phi = "07d77754-e150-4737-8c94-cd238a1fb45b"
Tapir = "07d77754-e150-4737-8c94-cd238a1fb45b"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Turing = "fce5fe82-541a-59a6-adf8-730c64b5f9a0"
UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228"
Expand Down
21 changes: 12 additions & 9 deletions bench/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Benchmarking

There are two flavours of benchmarks implemented in `run_benchmarks.jl`.
One is a set of pass / fail tests designed to check that large performance regressions are avoided in Phi.
The other is a set of comparisons between a variety of frameworks -- this set is designed to give a rough sense of where Phi stands in comparison to other AD frameworks, and the results should not be thought of as pass / fail tests.
One is a set of pass / fail tests designed to check that large performance regressions are avoided in `Tapir.jl`.
The other is a set of comparisons between a variety of frameworks -- this set is designed to give a rough sense of where `Tapir.jl` stands in comparison to other AD frameworks, and the results should not be thought of as pass / fail tests.

## Phi-Only Benchmarking
## Tapir-Only Benchmarking

The benchmarking runs as part of CI, and evaluates a sequence of pass / fail tests.

Expand All @@ -21,9 +21,9 @@ to run the benchmarks which test the performance of AD. This will produce a `Dat
containing a run-down of the results. It has the following columns:
1. `tag`: a `String` with an automatically generated name for the test
1. `primal_time`: the time taken to run the original code
1. `phi_time`: the time is takes Phi to AD the code
1. `Phi`: `phi_time / primal_time`
1. `range`: a named tuple with fields `lb` and `ub` specifying the acceptable range of values for `Phi`.
1. `tapir_time`: the time is takes `Tapir.jl` to AD the code
1. `Tapir`: `tapir_time / primal_time`
1. `range`: a named tuple with fields `lb` and `ub` specifying the acceptable range of values for `Tapir.jl`.

From here you can look at whatever properties of the results you are interested in.

Expand All @@ -33,7 +33,7 @@ Note that the types of all of the columns are very simple, so it is fine to writ
CSV.write("file_name.csv", df)
```

Additionally, the convenience function `plot_ratio_histogram!` can be used to produce a histogram of `Phi` with formatting which is suited to this field. Call it as follows:
Additionally, the convenience function `plot_ratio_histogram!` can be used to produce a histogram of `Tapir.jl` with formatting which is suited to this field. Call it as follows:
```julia
derived_results = benchmark_derived_rrules!!(Xoshiro)
df = DataFrame(df)
Expand All @@ -42,10 +42,13 @@ plot_ratio_histogram!(df)

## Inter-framework Benchmarking

This comprises a small suite of functions that we AD using Phi, Zygote, ReverseDiff, and Enzyme. This suite of benchmarks is also run as part of CI, and the output is recorded in two ways:
This comprises a small suite of functions that we AD using `Tapir.jl`, `Zygote.jl`, `ReverseDiff.jl`, and `Enzyme.jl`.
This suite of benchmarks is also run as part of CI, and the output is recorded in two ways:
1. a table of results is posted as comment in a PR
1. the table and a corresponding graph are stored as github actions artifacts, and can be retrieved by going to the "Checks" tab of your PR, and clicking on the artifact button.

As with the pass / fail tests, these tests report the ratio of the time taken to perform AD to the time taken to run the function being tested.

If you wish to add to this suite, see the `generate_inter_framework_tests` function in `run_benchmarks.jl`. To run this suite locally, include `run_benchmarks.jl` and run the `create_inter_ad_benchmarks` function. This will output the graph and table mentioned above to the `bench` folder.
If you wish to add to this suite, see the `generate_inter_framework_tests` function in `run_benchmarks.jl`.
To run this suite locally, include `run_benchmarks.jl` and run the `create_inter_ad_benchmarks` function.
This will output the graph and table mentioned above to the `bench` folder.
32 changes: 16 additions & 16 deletions bench/run_benchmarks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ using
PrettyTables,
Random,
ReverseDiff,
Phi,
Tapir,
Test,
Turing,
Zygote

using Phi:
using Tapir:
CoDual,
generate_hand_written_rrule!!_test_cases,
generate_derived_rrule!!_test_cases,
Expand All @@ -27,7 +27,7 @@ using Phi:
PInterp,
_typeof

using Phi.TestUtils: _deepcopy, to_benchmark
using Tapir.TestUtils: _deepcopy, to_benchmark

function zygote_to_benchmark(ctx, x::Vararg{Any, N}) where {N}
out, pb = Zygote._pullback(ctx, x...)
Expand Down Expand Up @@ -84,7 +84,7 @@ _broadcast_sin_cos_exp(x::AbstractArray{<:Real}) = sum(sin.(cos.(exp.(x))))
# about all of the operations.
_simple_mlp(W2, W1, Y, X) = sum(abs2, Y - W2 * map(x -> x * (0 <= x), W1 * X))

# Only Zygote and Phi can actually handle this. Note that Phi only has rules for BLAS
# Only Zygote and Tapir can actually handle this. Note that Tapir only has rules for BLAS
# and LAPACK stuff, not explicit rules for things like the squared euclidean distance.
# Consequently, Zygote is at a major advantage.
_gp_lml(x, y, s) = logpdf(GP(SEKernel())(x, s), y)
Expand Down Expand Up @@ -174,12 +174,12 @@ function benchmark_rules!!(test_case_data, default_ratios, include_other_framewo
evals=1,
)

# Benchmark AD via Phi.
@info "phi"
rule = Phi.build_rrule(args...)
# Benchmark AD via Tapir.
@info "tapir"
rule = Tapir.build_rrule(args...)
coduals = map(x -> x isa CoDual ? x : zero_codual(x), args)
to_benchmark(rule, coduals...)
suite["phi"] = @benchmark(to_benchmark($rule, $coduals...))
suite["tapir"] = @benchmark(to_benchmark($rule, $coduals...))

if include_other_frameworks

Expand Down Expand Up @@ -219,16 +219,16 @@ end
function combine_results(result, tag, _range, default_range)
d = result[2]
primal_time = time(minimum(d["primal"]))
phi_time = time(minimum(d["phi"]))
tapir_time = time(minimum(d["tapir"]))
zygote_time = in("zygote", keys(d)) ? time(minimum(d["zygote"])) : missing
rd_time = in("rd", keys(d)) ? time(minimum(d["rd"])) : missing
ez_time = in("enzyme", keys(d)) ? time(minimum(d["enzyme"])) : missing
fallback_tag = string((result[1][1], map(Phi._typeof, result[1][2:end])...))
fallback_tag = string((result[1][1], map(Tapir._typeof, result[1][2:end])...))
return (
tag=tag === nothing ? fallback_tag : tag,
primal_time=primal_time,
phi_time=phi_time,
Phi=phi_time / primal_time,
tapir_time=tapir_time,
Tapir=tapir_time / primal_time,
zygote_time=zygote_time,
Zygote=zygote_time / primal_time,
rd_time=rd_time,
Expand Down Expand Up @@ -283,26 +283,26 @@ end
function flag_concerning_performance(ratios)
@testset "detect concerning performance" begin
@testset for ratio in ratios
@test ratio.range.lb < ratio.Phi < ratio.range.ub
@test ratio.range.lb < ratio.Tapir < ratio.range.ub
end
end
end

"""
plot_ratio_histogram!(df::DataFrame)
Constructs a histogram of the `phi_ratio` field of `df`, with formatting that is
Constructs a histogram of the `tapir_ratio` field of `df`, with formatting that is
well-suited to the numbers typically found in this field.
"""
function plot_ratio_histogram!(df::DataFrame)
bin = 10.0 .^ (0.0:0.05:6.0)
xlim = extrema(bin)
histogram(df.Phi; xscale=:log10, xlim, bin, title="log", label="")
histogram(df.Tapir; xscale=:log10, xlim, bin, title="log", label="")
end

function create_inter_ad_benchmarks()
results = benchmark_inter_framework_rules()
tools = [:Phi, :Zygote, :ReverseDiff, :Enzyme]
tools = [:Tapir, :Zygote, :ReverseDiff, :Enzyme]
df = DataFrame(results)[:, [:tag, tools...]]

# Plot graph of results.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module PhiSpecialFunctionsExt
module TapirSpecialFunctionsExt

using SpecialFunctions, Phi
using SpecialFunctions, Tapir

import Phi: @from_rrule, DefaultCtx
import Tapir: @from_rrule, DefaultCtx

@from_rrule DefaultCtx Tuple{typeof(airyai), Float64}
@from_rrule DefaultCtx Tuple{typeof(airyaix), Float64}
Expand Down
2 changes: 1 addition & 1 deletion src/Phi.jl → src/Tapir.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module Phi
module Tapir

const CC = Core.Compiler

Expand Down
18 changes: 9 additions & 9 deletions src/chain_rules_macro.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ __increment_shim!!(x, y) = increment!!(x, y)
"""
@from_rrule ctx sig
Creates a `Phi.rrule!!` from a `ChainRulesCore.rrule`. `ctx` is the type of the context in
Creates a `Tapir.rrule!!` from a `ChainRulesCore.rrule`. `ctx` is the type of the context in
which this rule should apply, and `sig` is the type-tuple which specifies which primal the
rule should apply to.
For example,
```julia
@from_rrule DefaultCtx Tuple{typeof(sin), Float64}
```
would define a `Phi.rrule!!` for `sin` of `Float64`s, by calling `ChainRulesCore.rrule`.
would define a `Tapir.rrule!!` for `sin` of `Float64`s, by calling `ChainRulesCore.rrule`.
Health warning:
Use this function with care. It has only been tested for `Float64` arguments and arguments
Expand All @@ -29,13 +29,13 @@ macro from_rrule(ctx, sig)
arg_type_symbols = sig.args[2:end]

arg_names = map(n -> Symbol("x_$n"), eachindex(arg_type_symbols))
arg_types = map(t -> :(Phi.CoDual{<:$t}), arg_type_symbols)
arg_types = map(t -> :(Tapir.CoDual{<:$t}), arg_type_symbols)
arg_exprs = map((n, t) -> :($n::$t), arg_names, arg_types)

call_rrule = Expr(
:call,
:(Phi.ChainRulesCore.rrule),
map(n -> :(Phi.primal($n)), arg_names)...,
:(Tapir.ChainRulesCore.rrule),
map(n -> :(Tapir.primal($n)), arg_names)...,
)

pb_arg_names = map(n -> Symbol("dx_$(n)"), eachindex(arg_names))
Expand All @@ -45,7 +45,7 @@ macro from_rrule(ctx, sig)
incrementers = Expr(
:tuple,
map(pb_arg_names, pb_output_names) do a, b
:(Phi.__increment_shim!!($a, $b))
:(Tapir.__increment_shim!!($a, $b))
end...,
)

Expand All @@ -62,18 +62,18 @@ macro from_rrule(ctx, sig)
rule_expr = ExprTools.combinedef(
Dict(
:head => :function,
:name => :(Phi.rrule!!),
:name => :(Tapir.rrule!!),
:args => arg_exprs,
:body => quote
y, pb = $call_rrule
$pb
return Phi.zero_codual(y), pb!!
return Tapir.zero_codual(y), pb!!
end,
)
)

ex = quote
Phi.is_primitive(::Type{$ctx}, ::Type{$sig}) = true
Tapir.is_primitive(::Type{$ctx}, ::Type{$sig}) = true
$rule_expr
end
return esc(ex)
Expand Down
Loading

2 comments on commit 41a1561

@willtebbutt
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register()

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/103806

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.1.0 -m "<description of version>" 41a1561dfe503d7c0f0aec44045b314b40a78327
git push origin v0.1.0

Please sign in to comment.