Skip to content

[fft] Prototype: out-parameter storage forms + clobber conventions (NTT first) - #81

Draft
devin-ai-integration[bot] wants to merge 3 commits into
masterfrom
devin/1785329461-storage-outparams
Draft

[fft] Prototype: out-parameter storage forms + clobber conventions (NTT first)#81
devin-ai-integration[bot] wants to merge 3 commits into
masterfrom
devin/1785329461-storage-outparams

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Draft/prototype, stacked on #79. Separates storage from semantics the way we discussed: no opaque byte arena — instead the caller pre-constructs/reserves a transformed/product and the engine builds into it, and clobber permission is encoded in the parameter form. engine.hpp states the convention:

const&          borrowed, read-only
&& / by-value   sink: the callee owns it and may clobber (finish works in the product's buffer)
& out-param     overwritten, reusing the object's existing capacity

Each engine concept requirement now carries a comment giving the expected function signature and its reference-passing semantics, e.g.:

// transform(std::span<const value_type> in, int n) -> transformed
// extend_to(transformed& t, int m, std::span<const value_type> coeffs): grows t in place
// mul/sq/mul2(const transformed&..., int n) -> product: inputs borrowed, fresh product returned
// finish(product&& p, std::span<value_type> out, Op = assign_op{}): sink;
// consumes p's contents (the inverse transform runs in p's buffer), but p keeps its allocation

Engine-side (NTT only for now, optional per engine):

transform(in, n, transformed& out)
mul(a, b, n, product& out)
downsample(t, n, odd, out)        // out may alias t (reads are forward of writes)
negate_arg(t, n, transformed& out) // must not alias

The value-returning forms become one-line wrappers over these. Generic callers use dispatch helpers that fall back to move-assignment for engines without the forms:

fft::transform_into<E>(in, n, out);
fft::mul_into<E>(a, b, n, out);
fft::downsample_into<E>(t, n, odd, out);
fft::negate_arg_into<E>(t, n, out);

kth_term_of_rational_function is the consumer proof: tnq/prod/half are hoisted out of the loop, and since the sizes are stable per iteration (transforms at n, products at n/2), each object allocates at most once for the whole computation on engines with the out-param forms.

Findings (allocation audit)

Counting operator new inside one kth_term_of_rational_function call (NTT, warm buffer_pool):

allocs bytes
before (#79), d=1000 k≈10^12 202 1.3 MB
before, d=100000 k≈10^15 252 211 MB
after, d=1000 7 45 KB
after, d=100000 7 5.5 MB

(~5% wall-clock improvement at d=100000; identical results.) The per-iteration allocations were: negate_arg (n), mul (n), downsample (n/2) ×2 products, plus the realloc when extend_to doubles the recycled half back to n — all now capacity reuse. Remaining copies: tp = half / tq = half (n/2 each) are inherent while finish consumes its product in place; a non-consuming finish would have to copy anyway.

Open questions before rolling out further:

  • whether to promote the out-param forms into the engine concept (making them required) once split/real/crt/algebras implement them, and retire the _into helpers in favor of calling E:: directly;
  • sq/mul2/add out-param forms (not needed by this consumer, so omitted);
  • whether split's two-span product / algebras' per-channel storage want a different reserve story (they'd benefit the most — their products are the big allocations).

Testing

  • Full audit of all fft code on this branch: the NTT out-param aliasing claims verified against core::even_half/odd_half's forward-read structure; the new concept signature comments checked against every engine's actual overloads (ntt/crt/split/real/algebras); finish's keeps-allocation claim confirmed for all engines (each runs the inverse in the product's own buffers). One stale preamble typo fixed (finish(..., span<value_type>& out) → by-value span).
  • ./build/tests "[fft]" → 26727 assertions, 85 cases, all pass (the _into fallback path covers the engines without out-param forms).
  • Full Library Checker verification on this branch: every fft-related problem AC under both g++ and g++-sanitizer environments, including kth_term_of_linearly_recurrent_sequence 20/20.
  • Allocation harness results above (old vs new binaries agree on outputs).

Link to Devin session: https://app.devin.ai/sessions/66a2d877f2354e30b3fdfa0c15061db2
Requested by: @ecnerwala

@ecnerwala ecnerwala self-assigned this Jul 29, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

GCC Code Coverage Report

📂 Overall coverage

Metric Coverage
Lines 🟡 8167/9961 (82.0%)
Functions 🟢 1113/1218 (91.4%)
Branches 🟡 6348/8220 (77.2%)

Base automatically changed from devin/1785329305-cached-span-operands to devin/1785327766-series-span-cleanup July 29, 2026 19:58
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1785329461-storage-outparams branch from 0d0e673 to 699a309 Compare July 29, 2026 19:59
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1785329461-storage-outparams branch 5 times, most recently from eeee217 to ca2c7b6 Compare July 29, 2026 22:56
ecnerwala added a commit that referenced this pull request Jul 31, 2026
…n, zero-pad-tolerant extend_to (#79)

## Summary

Removes the recurring rough edges in the `series::` layer (now includes
the operator normalization originally split out as #80):

1. **`underlying()` is gone.** The `like` contract is now: direct
indexing plus two span borrows — into the engine primitives and into the
series layer's own exactness-tagged span:
   ```cpp
   concept like = ... requires(const S& s, int i) {
       { s[i] } -> convertible_to<const value_type&>;
{ std::span<const value_type>(s) }; // borrows into engine ops
requires convertible_to<const S&, span<E, S::exact_v>>; // borrows into
the series layer
   };
   ```
Each wrapper provides `operator span<E, exact_v>` (`vec` already had it;
`cached`/`cached_span`/`prefix_cached` gain it), and `vec` gains
`explicit vec(span<E, exact_>)` so materializing an owned copy of any
series-like is just `exact<E> r(c)`. `std::span<const T>` borrows go
through std::span's range constructor (deliberately no conversion
operator on `series::span` — offering both paths makes every implicit
conversion ambiguous under `-Wconversion`). `cached`/`prefix_cached`
keep a non-contract `uncached()` unwrapper for the by-reference case
(`subproduct_tree::rev_prod`). At the sites: `q.underlying()[0]` →
`q[0]`, `span<E, false> a = a_.underlying();` → `span<E, false> a =
a_;`.

2. **The `sz(coeffs)` zero-padding footgun.** `extend_to`'s doubling
loop clamps each step's read to the coefficients that fit:
   ```cpp
   while (t.size() < m) {
       int s = t.size();
       t.v.resize(2 * s);
       core::extend(t.v, coeffs.first(min(sz(coeffs), 2 * s)));
   }
   ```
By the prefix contract, a size-`s` transform can only exist if all
nonzero coefficients fit in `2s` — so anything past the clamp is
necessarily zero and dropping it is exact (no value inspection, no
float-equality trimming). The top-level `sz(coeffs) <= 2 * m` assert is
the one conservative check kept. The old *"must `extend_to` before
padding"* ordering constraint in `kth_term_of_rational_function` is
gone, and a cache seeded from short coeffs can later be grown with a
longer zero-padded buffer of the same sequence.

3. **Series operators normalized onto `cached_span`** (folded from #80).
`detail::whole_operand` is the whole-span counterpart to
`product_operand`: any `like` operand becomes a `cached_span` (borrowed
coefficients + the cache serving them):
   ```cpp
   template <like S>
cached_span<E, S::exact_v> whole_operand(const S& s,
fft::transformed<E>& tmp) {
       return {s, whole_cache_or(s, tmp)};
   }
   ```
`square`/`multiply_add2`/`middle_product` and
`kth_term_of_linear_recurrence` run on that form (`auto av =
detail::whole_operand(a, ta_);` then `av, av.cache()` straight into the
`fft::` entry points); `operator*`'s call sites,
`operator+`/`operator-`, `ps_inv`, `ps_log`'s assert, and
`cached::operator==` drop their coefficient plumbing. Internals
uniformly use `series::span`, not `std::span`, for coefficient views.

No algorithmic or semantic changes: cache selection, precisions, and
transform sizes are identical throughout.

## Testing

- Full audit of all fft code (all engines'
`extend_to`/`transform`/`finish`/`downsample`/`negate_arg` against the
clamped-prefix contract; series operator cache pairings;
`poly.hpp`/`online.hpp` call sites) — one issue found and fixed: the
dual span-conversion ambiguity above (it produced `-Wconversion`
warnings at every implicit borrow).
- Full unit suite green (2822854 assertions, 106 cases, all engines)
after the `underlying()` removal.
- Full Library Checker verification: every fft-related problem AC under
both `g++` and `g++-sanitizer` environments (convolutions incl.
crt/split, all FPS ops, composition, multipoint/interpolation,
characteristic polynomial, `kth_term_of_linearly_recurrent_sequence`
20/20); kth_term/multipoint/interpolation re-verified after the contract
change.
- Padded-extend harness: seed a transform from unpadded coeffs, extend
with a longer zero-padded buffer, compare `finish(sq(...))` against a
fresh transform — exact agreement (err = 0) across ntt/split/real for
lengths {1,2,3,5} × seeds {2,4} × targets {8,16} × paddings.
- Real-engine length-1 `extend_to` verified bit-identical to fresh
transforms across seed/target sizes.

Stacked follow-up: #81 (storage-separation prototype).

Link to Devin session:
https://app.devin.ai/sessions/66a2d877f2354e30b3fdfa0c15061db2
Requested by: @ecnerwala
<!-- devin-review-badge-beta-begin -->

---

<a href="https://app.beta.devin.ai/review/ecnerwala/cp-book/pull/79"
target="_blank">
  <picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://static.devin.ai/assets/gh-open-in-devin-review-beta-dark.svg?v=1">
<img
src="https://static.devin.ai/assets/gh-open-in-devin-review-beta-light.svg?v=1"
alt="Open in Devin Review (Beta)">
  </picture>
</a>
<!-- devin-review-badge-beta-end -->

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Andrew He <he.andrew.mail@gmail.com>
Base automatically changed from devin/1785327766-series-span-cleanup to master July 31, 2026 04:24
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1785329461-storage-outparams branch from ca2c7b6 to 48dd415 Compare July 31, 2026 04:26
devin-ai-integration Bot and others added 3 commits August 2, 2026 04:48
Engines may provide out-parameter forms of the value-returning primitives
(transform/mul/downsample/negate_arg building into a caller-reserved object,
reusing its capacity); fft::*_into helpers dispatch to them when present and
fall back to move-assignment. NTT implements them; the Bostan-Mori loop hoists
its transforms/products out of the loop and runs on the helpers.
engine.hpp documents the ownership conventions (const& borrow, sink clobbers,
& out-param overwrites).

Co-Authored-By: Andrew He <he.andrew.mail@gmail.com>
…gine concept

Co-Authored-By: Andrew He <he.andrew.mail@gmail.com>
Co-Authored-By: Andrew He <he.andrew.mail@gmail.com>
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1785329461-storage-outparams branch from 48dd415 to ac82edf Compare August 2, 2026 04:48
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