Skip to content

API: Subgrid interpolation (non-integer factors) - #3082

Closed
mloubout wants to merge 1 commit into
subgrid-strided-subsamplingfrom
subgrid-interp-fd
Closed

mloubout wants to merge 1 commit into
subgrid-strided-subsamplingfrom
subgrid-interp-fd

Conversation

@mloubout

Copy link
Copy Markdown
Contributor

Part 2 of subgrid support, on top of #3080 (integer subsampling). Merge #3080 first — this PR targets that branch, so the diff here is a single commit.

API

A SubGrid Function used in an equation is read at the left-hand side's location, the convention staggered Functions already follow. No wrapper, no flag:

sub = SubGrid(grid, factor=1.5)
vp  = Function(name='vp', grid=sub)
Eq(u, vp*u)          # vp interpolated onto u's x, y

SubGrid now dispatches on the factor:

integer factor non-integer factor
is_interpolated False True
shape ceil(n/factor) floor((n-1)/factor) + 1
dimensions ConditionalDimensions own SpaceDimensions
reading on the parent strided, exact interpolated at the LHS

Per-axis factors are independent; a factor of exactly 1 passes the parent's own Dimension through. Factors below 1 refine rather than coarsen.

Lowering

Three steps, each in the layer that owns the information:

  1. Function._eval_at emits a SubGridInterp marker.
  2. FD lowering leaves it inert — necessary because generate_indices needs a compile-time mid, and the subgrid offset varies with the iteration index.
  3. The new Cluster pass subgrid_interp expands it once the iteration space is known.

IndexDerivative is the precedent for exactly this deferral.

Reading a SubGrid at the parent's point x evaluates at x*q/p for factor = p/q, so

base = (x*q) // p        frac = ((x*q) % p) / p

frac depends on x only through x % p, so there are p phases and the weights are constants within a phase. They live in a compile-time table indexed by Mod(x, p) — which means no phase loop at all, and the inner loops stay unit-stride and branch-free:

float w0[6] = {1.0F, 0.0F, 3.333e-1F, 6.667e-1F, 6.667e-1F, 3.333e-1F};
#pragma omp for schedule(static,1)
for (int x = x_m; x <= x_M; x += 1)
  u[x + 2] = w0[2*((x)%(3)) + 1]*vp[((2*x) / 3) + 3] + w0[2*((x)%(3))]*vp[((2*x) / 3) + 2];

No LocalSum (benchmarked 1.7–3.5x slower and non-vectorizing), no floorf, no guard.

Two subtleties worth reviewer attention

  • Blocking runs before this pass. A parent Dimension then appears in the IterationSpace only through its block Dimensions, so a plain d in ispace.itdims test silently fails and 3D ends up half-relocated. Fixed via ispace.promote(lambda d: d.is_Block), the same idiom as ir/stree/algorithms.py:194.
  • A factor-1 axis shares the parent's Dimension (xs is x), so it must not be projected out of the IterationSpace — dropping it leaves a loop bound with no runtime value.

Both have dedicated regression tests, each verified to fail when its fix is reverted.

Tests

tests/test_subgrid.py, 27 tests: relocation follows the LHS; parent Dimensions only in the ispace; plain stencil (no LocalSum, no branch); tap counts; the two subtleties above; exactness on linear fields in 1D/2D/3D; constant field (weights sum to 1 at every phase); factor < 1 refines; integer factors keep Part 1's lowering.

Verified:

  • 138 passed × DEVITO_LANGUAGE ∈ {C, CXX, openmp, CXXopenmp} (with tests/test_subsampling.py, incl. 14 MPI tests)
  • 1167 passed across test_dimension, test_operator, test_dse, test_derivatives, test_staggered_utils, test_interpolation
  • 45 notebook cells under nbval; flake8 and all pre-commit hooks clean
  • Linear interpolation exact to fp32 (max err 3.8e-6) for factors 1.5, 2.5, 0.5, 0.75, 1.25 and mixed (2, 1.5), (2, 1.5, 1.0)

The tutorial examples/userapi/10_subgrids.ipynb gains an interpolation section showing the generated code for each case.

Not in this PR

  • Automatic halo derivation. The accesses are vp[IntDiv(...) + o], not affine, so affine inference does not see them; HaloComms.callback additionally gates on d._defines & hs.distributed_aindices, which fails because the loop iterates the parent's Dimensions while the halo is keyed on the subgrid's.
  • Halo reduction for the subgrid→parent direction under MPI.
  • Sinc and cubic schemes (linear only today).

🤖 Generated with Claude Code

A SubGrid Function used in an equation is now read at the left-hand side's
location, the convention staggered Functions already follow -- no wrapper and
no flag:

    sub = SubGrid(grid, factor=1.5)
    vp  = Function(name='vp', grid=sub)
    Eq(u, vp*u)

`SubGrid` accepts non-integer and sub-unit factors, dispatching on the factor:
integer factors keep the strided ConditionalDimension lowering, anything else
gets its own SpaceDimensions and interpolates. `is_interpolated` distinguishes
the two, and a factor of exactly 1 passes the parent's Dimension through.

`Function._eval_at` emits a `SubGridInterp` marker that FD lowering leaves
inert, and the new Cluster pass `subgrid_interp` expands it once the iteration
space is known -- the same deferral `IndexDerivative` uses. Reading a SubGrid at
the parent's point `x` evaluates at `x*q/p` for `factor = p/q`, so the base
index is `(x*q)//p` and the fractional part repeats with period `p`. The weights
are therefore constants within a phase and live in a compile-time table indexed
by `Mod(x, p)`: no phase loop, no branch, and unit-stride loops over the
parent's Dimensions alone.
@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@mloubout

Copy link
Copy Markdown
Contributor Author

Opened prematurely without approval; work is not ready for review. Closing.

@mloubout mloubout closed this Oct 21, 2026
@mloubout
mloubout deleted the subgrid-interp-fd branch October 21, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant