Skip to content

Monorepo Packages via Submodules/Subpackages: Support in Pkg and Julia Compiler #55516

Open
@ChrisRackauckas

Description

There are many things to be said about how to grow and scale repositories. However, I think it's clear that OrdinaryDiffEq.jl is a repo that is currently hitting the scaling limits of Julia and thus a good case to ground this discussion. It's a repository which is very performance-minded, has many different solvers (hundreds), and thus has many optimizations around. However, in order to be usable, it needs to load "not so badly". With the changes to v1.9 adding package binaries, we could get precompilation to finally build binaries of solvers and allow for first run times under a second. We need to continue to improve this, but job well done there.

However, that has started to show some cracks around it. In particular, the precompilation times to build the binaries are tremendous. At first they were measured close to an hour, but by tweaking with preferences we got it to around 5-10 minutes. The issue is that, again, there are many solvers, so if you want to make all solvers load fast, you need many binaries. Julia's package binary system is on the package level, so you only have a choice of whether to build the solvers of the package or not. We then setup Preferences.jl and by default tuned it down a bunch, but what that really meant is that while there is a solution to first solve problems, it's simply not usable in the vast majority of the cases by most users.

Also, one of the issues highlighted by @KristofferC was that some of the solvers were still causing unsolvable loading time issues simply by existing in the repo. As highlighted in SciML/OrdinaryDiffEq.jl#2177, for example the implicit extrapolation methods, which are a very rare method to use, caused 1.5 seconds of lowering time. This is something that we had discussed as potentially decreasing when the new lowering would come into play, but not to zero and likely still relatively non-zero, and so it was determined that post v1.10 we were likely to see no more improvements due to Julia Base simply because the repo is too large. Too much stuff = already saturated in load time improvements.

However, since the unit of package binaries is the package, the next solution is to simply make a ton of packages. Thus OrdinaryDiffEq.jl recently did a splitting process that changed it from 1 package into 30 packages. That's somewhat intermediate, I assume we'll get to about 50 packages over the next year as we refine it down to the specific binaries people want. There are a few interdependencies in there as well: everything relies on OrdinaryDiffEqCore, and implicit methods have a chain like OrdinaryDiffEqCore -> OrdinaryDiffEqDifferentiation -> OrdinaryDiffEqNonlinearSolve -> OrdinaryDiffEqBDF. This is all just libs in the same repo, so it's a tens of package monorepo. This effectively parallelizes package binary construction, only causes loading what the user requests (thus fixing the "too much stuff" problem), and allows for solvers to all be set to precompile and has an easy user-level way to pick the subset of the binaries they want (by picking the solver packages).

It also has a nice side benefit that the dependencies of most solvers are decreased. Since for example only the exponential integrators need ExponentialUtiltiies.jl, that's a dependency of the solver package but not the core now, meaning most people get less dependencies.

To an extent, this is simply using monorepos and tons of packages to solve the problem, since our one hammer is package binaries just make everything a package. However, there are several downsides to this approach:

  1. For one, the registration process is a bit of a nightmare. Any sweeping change requires that we register 30 packages, which is something that must be done in JuliaRegistrator comments, each must be a different post. I.e. you cannot do a multiline registration, and thus it cannot be done with a simple copy paste, you need to manually copy and paste 30 different lines and if you miss one it will take a long while to figure out that you forgot to release one out of the 30 packages. Even just getting the packages registered we're forgetting which ones are already released and which ones are not.
  2. Inter-Dependency management is a bit of a mess. This kind of monorepo setup naturally has deep dependencies between the solvers and the core repo, since they are not made to be working through public API but internal API. You can either try to be very diligent with minimum bound bumping (which cannot be tested with the current Pkg resolver, already discussed with @StefanKarpinski), or you can lock all solvers to versions of Core. The latter sounds like even more dependency hell so we're trying the first for now. But in theory the lock-step of such a monorepo could be enforced automatically: doing it by hand is very error-prone though and makes it so you have to bump all 30 solvers every time you make a change to OrdinaryDiffEqCore, making (1) a really big problem again.
  3. The testing of package-wide interfaces is a bit wonky. For all CI we simply always download all 30 packages because doing anything else isn't automatable with our current tools.
  4. Contributing to the package is hard because in order to even install it you need to install all 30 packages, which is not normal or obvious.

As a result, it's not a great experience, but it's the best that we have to scale today.

What's the real issue and solution?

The real issue here is that we have privileged packages in a way that we have not privileged modules. In a sense, OrdinaryDiffEqCore is a submodule, OrdinaryDiffEqTsit5 is a submodule. They are all submodules of the same package. However, since we only have dependencies, binary building, etc. as package level features, we put all of these submodules into different packages. Then:

using OrdinaryDiffEqCore, OrdinaryDiffEqTsit5

is "the solution", and we have 50 packages roaming around that are actually all submodules of 1 package. In reality, what would be really nice is to use the submodule system for this. For example:

using OrdinaryDiffEq: OrdinaryDiffEqCore, OrdinaryDiffEqTsit5

If these were submodules of OrdinaryDiffEq, we could just have one versioned version of the package and all of our issues would go away... if the following features were supported:

  1. Dependencies defined on a per-submodule basis. So you could for example have a dependency that is only added in some sense if the user requests access to the OrdinaryDiffEq.OrdinaryDiffEqTsit5 submodule. If done correctly, it could also handle some option dependency issues like Why does LinearSolve depend on MKL_jll SciML/LinearSolve.jl#524.
  2. Separate package binaries per submodule. Precompilation could in theory could be done per submodule of a package, not simply based on the main package module. And it could parallelize that precompilation process based on the dependencies between modules if that was made explicit instead of implicit.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    featureIndicates new feature / enhancement requestsmodulespackagesPackage management and loading

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions