Minimal usage example#
A minimal example can be found below. If you are interested in the implementation side of this, please check the source.
This is a very minimalistic example to show some of the concepts of creating a library and backends and how a user might work with them.
The “library” contains only:
backend_opts()
a context manager for the user to change dispatching.A
divide
function that is dispatching enabled and assumed to be designed for onlyint
inputs.
We then have two backends with their corresponding definitions in backend.py
.
The entry-points are entry_point.py
and entry_point2.py
and these files
can be run to generate their functions
context (i.e. if you add more functions).
For users we have the following basic capabilities. Starting with normal type dispatching. First, import the functions and set up tracing globally:
>>> import pprint
>>> from spatch._spatch_example.library import divide, backend_opts
>>> opts = backend_opts(trace=True) # enable tracing
>>> opts.enable_globally() # or with opts() as trace:
Now try calling the various implementations:
>>> divide(1, 2) # use the normal library implementation (int inputs)
0
>>> divide(1., 2.) # uses backend 1 (float input)
hello from backend 1
0.5
>>> divide(1j, 2.) # uses backend 2 (complex input)
hello from backend 2
0.5j
>>> pprint.pprint(opts.trace)
[('spatch._spatch_example.library:divide', [('default', 'called')]),
('spatch._spatch_example.library:divide', [('backend1', 'called')]),
('spatch._spatch_example.library:divide', [('backend2', 'called')])]
The user can use backend_opts
to modify the dispatching behavior as
a context manager (or via the enable_globally()
).
The first thing is to prioritize the use of a backend over another
(possibly including the default implementation).
Backend 1 also has integers as a primary type, so we can prefer it over the default implementation for integer inputs as well:
>>> with backend_opts(prioritize="backend1"):
... divide(1, 2) # now uses backend1
hello from backend 1
0
Similarly backend 2 supports floats, so we can prefer it over backend 1. We can still also prioritize “backend1” if we want:
>>> with backend_opts(prioritize=["backend2", "backend1"]):
... divide(1., 2.) # now uses backend2
hello from backend 2
0.5
>>> pprint.pprint(opts.trace[-2:])
[('spatch._spatch_example.library:divide', [('backend1', 'called')]),
('spatch._spatch_example.library:divide', [('backend2', 'called')])]
The default priorities are based on the backend types or an explicit request to have a higher priority by a backend (otherwise default first and then alphabetically). Backends do have to make sure that the priorities make sense (i.e. there are no priority circles).
Prioritizing a backend will often effectively enable it. If such a backend changes behavior (e.g. faster but less precision) this can change results and confuse third party library functions. This is a user worry, backends must make sure that they never change types (even if prioritized), though.
In the array world there use-cases that are not covered in the above:
There are functions that create new arrays (say random number generators) without inputs. We may wish to change their behavior within a scope or globally.
A user may try to bluntly modify behavior to use e.g. arrays on the GPU.
This is supported, but requires indicating the type preference and users must be aware that this can even easier break their or third party code:
>>> with backend_opts(type=float):
... divide(1, 2) # returns float (via backend 1)
hello from backend 1
0.5
>>> with backend_opts(type=complex):
... # backen 2 returning a float for complex "input" is probably OK
... # (but may be debateable)
... divide(1, 2)
hello from backend 2
0.5
>>> with backend_opts(type=float, prioritize="backend2"):
... # we can of course combine both type and prioritize.
... divide(1, 2) # backend 2 with float result (int inputs).
hello from backend 2
0.5
>>> pprint.pprint(opts.trace[-3:])
[('spatch._spatch_example.library:divide', [('backend1', 'called')]),
('spatch._spatch_example.library:divide', [('backend2', 'called')]),
('spatch._spatch_example.library:divide', [('backend2', 'called')])]
How types work precisely should be decided by the backend, but care should be taken.
E.g. it is not clear if returning a float is OK when the user said type=complex
.
(In the future, we may want to think more about this, especially if type=complex|real
may make sense, or if we should fall back if no implementation can be found.)