Skip to content

[teeny] Phase B4+B5: regulariser boundary — reg_field/reg_flow import via tny::from_dlpack, reg_dispatch.h drops the CPU offset-width leaf #129

Description

@balbasty

Agent:claude-fastfields-to-teeny

Part of the tensor-native-boundaries umbrella (fastfields-lib#57), Phase B. One coordinated PR-set with the two impl halves (fastfields-cpu-impl#64 reg_field, fastfields-cpu-impl#65 reg_flow); this issue covers the cpu-lib repo: reg_field.cpp, reg_flow.cpp, reg_dispatch.h, tests/test_reg_dispatch.cpp, tests/test_reg_op.cpp. Convention: TEENY-MIGRATION.md §9.

Why ONE cpu-lib issue for both regulariser modules

reg_dispatch.h is the shared dispatch chain (cpu-lib#69/#70): its dtype×offset leaf dispatch_dtype<Op, D, BOUND...>(code, bits, use_32bits, args...) instantiates Op::template run<D, scalar_t, offset_t, BOUND...>. Dropping the CPU int32 arm (D1/R5) and the offset_t slot changes the Op::run contract for every caller of the chain at once — a field-only PR would need an interim two-convention shim in the shared header, reviewed twice and deleted a week later. One PR moves the shared leaf and both Op adapter sets together; the two impl PRs stay separate and land first (the impl submodule pin insulates this repo until it is re-pointed, Phase A's merge order).

The changes

1. reg_dispatch.h — the dtype leaf loses use_32bits and offset_t (D1/R5):

// today: Op::template run<D, float, int32_t, BOUND...>(...) / <D, float, int64_t, ...>// after: Op::template run<D, float, BOUND...>(...) // offset width lives in the carrier (int64)template <classOp, int D, bound::type... BOUND, class... Args>
inlinevoiddispatch_dtype(DLDataTypeCode code, uint8_t bits, Args &&... args);

dispatch_nd_bound/repeat_bound keep their shapes; every rejection message and its outermost-first evaluation order stays verbatim (that ordering is pinned behavior).

2. reg_field.cpp / reg_flow.cpp — the _field_* / _flow_* wrappers import instead of exploding. Today (_field_matvec): 3× copy_if_needed, VOIDPTR/CVOIDPTR, static_casts, 3× free_if_needed. After:

template <int ndim, typenamescalar_t, bound::type... BOUND>
inlinevoid_field_matvec(DLTensor & out, const DLTensor & inp,
constdouble * voxel_size, constdouble * absolute,
constdouble * membrane, constdouble * bending)
{
auto ao = tny::from_dlpack<scalar_t>(&out);
auto ai = tny::from_dlpack<constscalar_t>(&inp);
// as_weights / vx synthesis unchanged, then the highest-order-penalty branch calls
reg_field::matvec_bending<ndim, '=', reduce_t>(ao, ai, vx, a.data(), m.data(), b.data());
// (or membrane/absolute) -- carrier args per cpu-impl#64's target signatures
}

ContiguousStrides, VOIDPTR/CVOIDPTR, all 18 copy_if_needed uses across the two files: deleted. CHECK_* macros, check_selfadjoint_bound (and its exact wording), the nbatch < 0 rank guard, NDIM_MSG — all verbatim. flow_relax's niter plumbing unchanged.

3. tests/test_reg_dispatch.cpp — the ONE suite whose check count changes BY DESIGN. It pins the routing against the pre-#69 macro pyramid including the use_32bits → int32_t/int64_t leaf (its trace<NDIM, float, int32_t, ...> reference arms). The reference pyramid is updated to the new contract in the same PR; the count delta must decompose exactly as "the offset-width dimension of the cross product removed, nothing else" (today's baseline: 6030 checks, field 96 reached/2816 rejected, flow 96 reached/2816 rejected — clang make test, 2026-08-04). A stash-and-cross-test check (Phase A's technique) proving the new reference still reproduces the OLD routing on the surviving axes is the review bar.

4. tests/test_reg_op.cpp — calls the impl entry points directly, so its call sites convert to carriers (same 186 checks, unchanged count).

D1 note

This is the regularisers' first real int32-arm deletion. Oracle counts unchanged everywhere except the designed test_reg_dispatch delta; small tensors now run int64 instantiations (measured a wash — distance-slice review). Instantiation count halves across the ndim×bound×dtype product: expected compile-time/binary win, not a regression.

Preserved behavior (spell out in the PR)

  1. Exported signatures (field_matvec/field_diag/field_kernel, flow_matvec/flow_diag/flow_kernel/flow_relax) byte-for-byte unchanged.
  2. Every rejection message + outermost-first order (ndim → bound → dtype), and the self-adjointness rejections.
  3. strides == NULL / byte_offset != 0 still work via from_dlpack — pin with direct tests if not covered.

Gate (shared with cpu-impl#64/#65)

  1. Kernel-layer (stencil.h/stap.h/nd.h engine) instantiations byte-identical at -O2 in reg_field.o/reg_flow.o (int64 arms), both compilers — precedent kernels#60.
  2. Re-skinned wrappers: no new per-voxel instructions in any driver loop (criteria in the impl issues).
  3. test_reg_field525, test_reg_flow1123, test_reg_op186 — identical counts, clang++ AND g++, true make clean between; test_reg_dispatch delta exactly as designed above; whole 12-binary suite otherwise unchanged; asan/ubsan on touched paths.
  4. g++ nuance: untouched-neighbor whole-TU inlining jitter acceptable (Phase A finding).

Include-order footnote (R7)

Fastfields' "dlpack.h" (v1.2) before <teeny/dlpack.h> (v1.1, same guard) in both .cpp files (reg_dispatch.h already includes "dlpack.h" — keep it ahead of any teeny DLPack include).

Scope

The five files above only. NOT other modules, NOT cuda-lib (its own Phase B issue mirrors this shape, keeping its int32 arm per D1 — see there), NOT autocast.h itself (last-user rule; after this PR-set its only CPU users are the distance spline/mesh paths, Phases D/E).

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions