Big refactor of the core + context - #12
Merged
Merged
Conversation
… rest of the package
… - not possible because my param injections are ot visible by the latter
Collaborator
Author
|
if the ref arrives anywhere, ignore the links to |
bgailleton
marked this pull request as draft
July 27, 2026 15:01
… standard. Almost done.
… Next one is a big one)
…ating point rounding errors)
Collaborator
Author
|
This is getting very nice. I am going on holidays for 2 weeks, but apart for some cleaning and a few obvious optimisations, it is close to ready! |
…specific settings. on my way to fix. took 2 days to identify. Even assisted with Claude.
…LATE THE BEHAVIOUR but the fix is there and almost 100% guaranteed. only supra edge cases may generate a few minor cycles in the graph (like a 512X512 DEM with only 5 different z values)
…it run and ingest the right stuff)
bgailleton
marked this pull request as ready for review
September 1, 2026 10:02
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Current framework limitations
In short, there were two main issues.
The old design was too convoluted and inflexible.
It relied on many stateful "context" objects, each carrying its own
parameters, kernels, helpers and bookkeeping. The result was hard to
understand, hard to extend, and monolithic: adding a variant usually meant
touching the context class itself.
The Taichi ecosystem has slowed down.
Taichi is still maintained, but its pace of development has dropped
significantly. This is a good opportunity to rebuild the framework around a
single composition philosophy while staying backend-agnostic. In
particular, Quadrants — a fork of Taichi actively developed since
mid-2025 — is a promising alternative backend.
Refactor
Why
GPU performance is dominated by two things.
Memory traffic. The bottleneck is rarely arithmetic; it is moving data
between global memory and the compute units. A quantity that is uniform in
space and time — say a constant Manning roughness coefficient — should be a
compile-time literal that costs no memory access at all, whereas a 2D varying
field of the same coefficient costs one fetch per node per step. The numerical
scheme is identical in both cases; only the data layout differs, and that
difference is worth a large constant factor.
Branch divergence. GPUs are fast on streamlined, branch-free code and slow
when threads in the same warp take different paths. A flexible grid — where a
single neighbour lookup inspects each cell and dispatches on its boundary type
(periodic, no-data, domain edge, normal interior) — is exactly the kind of
per-thread branching that kills throughput.
The usual escape is to hand-write a separate kernel for every combination of
parameter layout and boundary logic. That does not scale: the combinations
multiply, and it defeats reuse. The same base operations — local-minima
solver, flow accumulation, slope computation — need to run inside a hydraulic
simulation, a stream-power incision model and a topographic analysis, each
time with different parameters, different boundaries and different companion
kernels.
This refactor lets a numerical scheme be written once, generically, with
the parameter layout and the boundary implementation left as named slots. Both
are resolved when the kernel is compiled — early enough that a constant folds
into the generated code and the boundary logic becomes a single specialised
function with no runtime dispatch. The scheme itself is never edited or
duplicated.
Building blocks
Listed from the lowest-level piece to the highest.
Parameter — one named physical value (e.g. the erodability coefficient
K). Its mode fixes where the value lives and is chosen at compile time:const(baked into the code, uniform for the whole run),scalar(a singledevice value, retunable between steps), or
field(one value per node,spatially variable). The kernel reads it the same way regardless of mode.
Helper — a device-side function left as a placeholder in the kernel. A
call to
get_left_neighbouris bound at compile time to one concreteimplementation (
..._periodic,..._normal,..._nodata_check, …), soboundary conditions and stencils change without touching the kernel.
Group — a pure-structure bundle of Parameters and Helpers with no data
of its own, e.g. all of a grid's neighbour and node-distance logic. A kernel
composes a Group into its tree and reaches into it by dotted path
(
ctx.grid.dx.get(i),ctx.grid.neighbour(i)).Data — a handle to a device array taken from a shared memory pool. The
bulk fields a model works on (elevation, water depth, discharge) are passed
in at call time, not baked into the kernel; temporary buffers are acquired
and released automatically.
Kernel — the GPU computation, written once in generic form against a
ctxtree of slots.Routine — an ordered set of Kernels sharing one address space, compiled
into a single callable. It manages pool buffers and enables backend
optimisations: kernel fusion on Taichi and Quadrants, CUDA Graph execution
on cupy.
Sequence — a host-driven layer above Routines. It runs Routines and
standalone Kernels under ordinary Python control flow: e.g. "run these three
Routines N times with a reduction barrier between each", or "loop until a
counter reaches 0", or any other condition evaluated on the host.
A complete subsystem is delivered not as one monolithic object but as a pair
of factories:
make_<x>_group, returning the pure structure, andmake_<x>_parameters, returning the concrete Parameters the caller owns andbinds. You assemble exactly the pieces you need and keep ownership of the data.
Build → freeze → bind → compile
A Kernel, Routine or Sequence becomes runnable in distinct phases:
defined in sensu stricto way)
and compose in any Groups. That's where you declare which "undeclared"
variable is a parameter, a helper or whatever
no bound values and no device storage. A frozen block can be exported and
reused inside many different blocks (kernel/routine/sequence/...).
its named address. It's where data/parameter gets IRL concrete meaning
ti.kernel,qd.kernel, orCUDA
__global__).Changing a dependency means re-binding and recompiling; the template code is
never modified.
This is deliberately verbose. The verbosity is the price of composability and
it is paid once, at assembly time, by whoever wires the blocks together.
The end product — a physics model built with the framework — is compiled down
to code as tight as a handwritten kernel, and none of this machinery is
visible to whoever runs that model.
Backends
The building blocks are implemented for three backends:
programming model.
C; intended for experienced users.