Skip to content

Figure out SciPy use-case #12

Closed
Closed

Description

@seberg

@lucascolley mentioned that SciPy has currently a light-weight dispatching that is limited to three specific types though (numpy, cupy, torch). Implemented across:

I think that everything for this use-case (practically) in-place, with maybe slight additions and some points on how to achieve things.
(Plus, one thing I realized is that the way we document backends may be very much open and likely needs customization for libraries.)

The basic pattern here would probably be (untested):

import abc

class JaxLike(abc.ABC):
    # We could just match "~jax.numpy:ndarray" below as well.
    # this is (maybe ridiculously so) closer to the original
    # by going via `__array_namespace__`.  (N.B. we look it up
    # on the class)
    @classmethod
    def __subclasscheck__(cls, other):
        get_xp = getattr(other, "__array_namespace__")
        if get_xp is None:
            return False

        # Ideally, we could just do this, but JAX doesn't define
        # this as a class method, but in spatch we prefer to match
        # types.  (Otherwise, we would just have to match anything
        # with `__array_function__` and use `should_run`...
        try:
           xp = get_xp()
        except Exception:
           pass
        else:
           return is_jax(xp)

        # Just guess that if it came from jax a jax module
        # we can use the JAX version.
        return getattr(xp, "__module__", "").startswith("jax.")


class JaxBackend:
    name = "jax_array_api"
    require_opt_in = False  # pure type dispatcher
    # Note that the "@" indicates that JaxLike is an abstract class
    primary_types = [f"@{JaxLike.__module__}:JaxLike"]
    secondary_types = []  # none, we don't allow NumPy coercion.
    functions = {
        # trivial to generate, you can do so even on import.
        "scipy.submodule:name": {
            "function": "jax.submodule:name",
            # more options, we don't need right now.
        }
    }


from spatch.backend_system import BackendSystem

_bs = BackendSystem(
    None,  # don't load entry-points for now (no 3rd party backends)
    "_SCIPY_INTERNAL_BACKENDS",  # spatch env-var prefix
    # could also allow subclasses for ndarray with "~numpy:ndarray".
    # Doesn't really matter, since we'll fall back to it anyway:
    default_primary_types=["numpy:ndarray"],
    # Register SciPy internal backends explicitly:
    backends=[JaxBackend],
)


@_bs.dispatchable(["argname"])
def scipy_function(argname, other_arg):
    # SciPy (numpy/fallback) version.

The JaxLike is a bit much, because spatch insists (or strongly prefers) to use type based semantics, but the Array API/JAX don't allow us to match "jax array API like" just based on the type (which could be changed there for sure), but this'll work in practice (or just change to list JAX types explicitly...).

Now, you are using also NumPy's __array_function__ pattern to have an ..._signature function that extracts the "relevant arguments", while the above passes a list of argument names instead.
The dedicated function version will be a lot faster for now (unless we move things to C, it may be a 1/3 or higher difference!).
I think we need to allow a callable there anyway if just for complex cases (i.e. numpy.concatenate considers all entries in the first argument as "relevant args", it needs to unpack that sequence.)

So @lucascolley if you are curious to test things out and need e.g. the version where you can do @_bs.dispatchable(..._signature), let's just add it here quickly (it is very easy of course).

Activity

  1. lucascolley commented on Jul 5, 2025

    @lucascolley
    Contributor
  2. seberg commented on Jul 5, 2025

    @seberg
    ContributorAuthor

    The above should work (of course this is alpha). I would think we can fold ndimage/special in just as well. Just need more backends?
    So, I suspect you can make things work, but I am sure you will find some things you want to change or make easier.

    One annoyance is filling in functions. It may be nicer to only do it when ndimage is imported (just to keep the information localized), while the backend itself I require creating immediately.

    That works fine though, you can just extend the functions dict (besides possible future user inspection API). It just must be filled in when the @dispatchable is created.

    One thing I just saw that (at least in ndimage?) The "fallback" mode actually still tries to fetch __array_namespace__, but it then wraps the NumPy version.
    Since that is a single "wrapper" (the "dask" version may be similar for chunking), that is something that I am not trying to make it easy yet. You would have to do this "manually".

    But of course, manually, just can be a trick like this:

    # Module (or class) to "manually" auto-wrap/try xp to numpy version
    
    def __getattr__(self, name):
        original = getattr(..., name)
        def wrap(*args, **kwargs):
            xp = ...
            res = original(*args, **kwargs)
            # code to re-wrap from NumPy result
    
        return wrap  # plus functools.wraps logic

    Then, add the function as {"original_mode:original_name": {"function": "this.module:original_name"}}

    And register this just with:

    class ArrayAPIEnabled(abc.ABC):
        @classmethod
        def __subclasscheck__(cls, other):
            # or just `hasattr`
            return getattr(other, "__array_namespace__", None) is not None

    If you do that, you would have to make sure either add ArrayAPIEnabled as the last one. (Users of entry-points, would have to do more than that, but for backends= we can partially use the passed order.)

  3. seberg commented on Jul 5, 2025

    @seberg
    ContributorAuthor

    Ah, I now see that there is one more thing that you need! xp = ... doesn't work yet. For that you will need to use "uses_context": True (in the functions dict). And I'll have to add that information to the context, which I'll just do right now :).

    EDIT: Need to re-org things slightly, so let's see if we want this. I always suspected we will, but had considered it a lower priority, it's just... you already have the situation.

  4. seberg commented on Jul 5, 2025

    @seberg
    ContributorAuthor

    OK, you'll want this: #13 and add the uses_context: True for all functions :).
    I suspect you are good with that (next thing may be the dispatch argument extraction via a function).

  5. seberg commented on Jul 5, 2025

    @seberg
    ContributorAuthor

    OK, and gh-14 contains also the parameter extraction part from a function. (I also realized that the old version assumes parameters are the first positional args unless you pass a dict, but that wasn't documented -- I fixed that but via inspect.signature, so not sure if we'll keep it).

    Either way, I hope that based on that (or once it's merged), you may have everything to get something working.

  6. seberg commented on Aug 22, 2025

    @seberg
    ContributorAuthor

    So, I see that scipy in some places has already chosen to make xp= public API! This is totally, fine, but it means I need to allow in spatch to directly return type rather than instances to the internal machinery.

    I.e. rather than saying the argument is jax.numpy.arange(100) when xp= is passed, we would have to say the argument is xp.ndarray. Of course there is one more problem, that the array class is not actually standardized to exist :).

    I think we could add that to the namespace information though, i.e. the array-namespace info should include what the types are.

  7. seberg commented on Nov 5, 2025

    @seberg
    ContributorAuthor

    I'll close this, the SciPy PR does cover this for now I think.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions