Skip to content

[fft] with_len: owned length-adjusted copies with cache riding - #82

Merged
ecnerwala merged 12 commits into
masterfrom
devin/1785364268-zero-extended
Aug 2, 2026
Merged

[fft] with_len: owned length-adjusted copies with cache riding#82
ecnerwala merged 12 commits into
masterfrom
devin/1785364268-zero-extended

Conversation

@devin-ai-integration

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

Copy link
Copy Markdown
Contributor

Summary

Adds explicit length adjustment for series operands while keeping the like contract dense: a span borrow is exactly len() coefficients, and first(n) (now part of the contract) requires n <= len().

  • series::with_len(s, n) -> resized<E>: an owned copy at logical length n (extending zero-fills, shrinking truncates). It carries a reference to a source cache when that cache still serves the copied coefficients (a zero tail doesn't change the transform), exposed via cache_opt(); the source must outlive the result.
  • maybe_cached<E, exact>: the runtime counterpart of cached_span in the borrow hierarchy prefix_cached/cached -> maybe_cached/cached_span -> span. first(n) on cached types returns it, keeping the cache only when it still serves the whole borrow.
  • One cache-discovery function, detail::cache_of(s) (has_cache -> cache(), has_cache_opt -> cache_opt(), else nullopt), used by as_cached_span, product_operand, and kth_term_of_rational_function seeding.
  • prefix_cached models has_cache (cache() = prefix_cache(nextPow2(len()))); has_prefix_cache is keyed on the prefix_cache(n) member. prefix_cached::first(k) returns maybe_cached (real prefix cache when k lines up, nullopt otherwise).
  • Archetype test: a minimal type exposing exactly the like surface instantiates every generic algorithm, so off-contract expressions in template bodies fail at build time (concepts can't enforce this).

An earlier revision of this PR prototyped a zero-copy zero_extended view with a "span borrows may be shorter than len(), tail is zero" contract; it was dropped because existing consumers reasonably assume a span borrow is the whole logical series, and the copy in with_len is O(n) against the O(n log n) FFT work while the cache (the expensive part) rides along.

Unit suite green; Library Checker AC under g++ + sanitizer for inv/exp/log/pow, multipoint, interpolation, kth_term, convolution.

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

@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1785364268-zero-extended branch from 2b7d2c1 to 8efecdb Compare July 29, 2026 22:56
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

GCC Code Coverage Report

📂 Overall coverage

Metric Coverage
Lines 🟡 8037/9828 (81.8%)
Functions 🟢 1089/1194 (91.2%)
Branches 🟡 6345/8211 (77.3%)

@devin-ai-integration devin-ai-integration Bot changed the title [fft] Prototype: zero_extended trunc views + polynomial divmod [fft] with_len: owned length-adjusted copies with cache riding; polynomial divmod Jul 30, 2026
Base automatically changed from devin/1785327766-series-span-cleanup to master July 31, 2026 04:24
devin-ai-integration Bot and others added 11 commits July 31, 2026 04:25
…ision; poly divmod

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

Co-Authored-By: Andrew He <he.andrew.mail@gmail.com>
…<= len(), cached when one lines up

Co-Authored-By: Andrew He <he.andrew.mail@gmail.com>
…whole_operand/product_operand honor optional caches

Co-Authored-By: Andrew He <he.andrew.mail@gmail.com>
Co-Authored-By: Andrew He <he.andrew.mail@gmail.com>
Co-Authored-By: Andrew He <he.andrew.mail@gmail.com>
Co-Authored-By: Andrew He <he.andrew.mail@gmail.com>
Co-Authored-By: Andrew He <he.andrew.mail@gmail.com>
Co-Authored-By: Andrew He <he.andrew.mail@gmail.com>
…t() returns maybe_cached; drop redundant conversions

Co-Authored-By: Andrew He <he.andrew.mail@gmail.com>
…ole cache, archetype tests

- zero_extended is gone: span borrows are exactly len() again, no zero-tail
  fine print on the like contract.
- with_len(s, n) returns resized<E>: an owned copy at logical length n
  carrying a reference to whichever of s's caches still serves it (the whole
  cache when extending, whatever first(n) carries when shrinking).
- cache_opt_of -> cache_of; kth_term_of_rational_function seeds through it,
  so maybe_cached/cache_opt operands seed too.
- prefix_cached models has_cache (prefix_cache(nextPow2(len())) is a whole
  cache), so whole-span ops stop re-transforming it into tmp;
  has_prefix_cache now keys on the prefix_cache member.
- series.test.cpp: like-archetype instantiations of the generic algorithms,
  proving they use only contract expressions.
- maybe_cached::cache_or was unused; removed.

Co-Authored-By: Andrew He <he.andrew.mail@gmail.com>
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1785364268-zero-extended branch from 5a6c05c to 246845f Compare July 31, 2026 04:26
Co-Authored-By: Andrew He <he.andrew.mail@gmail.com>
@devin-ai-integration devin-ai-integration Bot changed the title [fft] with_len: owned length-adjusted copies with cache riding; polynomial divmod [fft] with_len: owned length-adjusted copies with cache riding Aug 2, 2026
@ecnerwala
ecnerwala marked this pull request as ready for review August 2, 2026 04:47
@ecnerwala
ecnerwala merged commit a8bc1e0 into master Aug 2, 2026
12 checks passed
@ecnerwala
ecnerwala deleted the devin/1785364268-zero-extended branch August 2, 2026 04:48

@beta-devin-ai-integration beta-devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review (Beta)

Comment thread src/fft/series_core.hpp
Comment on lines +296 to +301
resized<E> r;
r.s.assign(size_t(n), T{});
std::span<const T> pc(p);
std::copy(pc.begin(), pc.end(), r.s.begin());
r.f = detail::cache_of(p);
return r;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Length-adjusted copies can crash or silently compute wrong products after the original series is used again

The length-adjusted copy stores a raw reference to the original series' internal transform slot (r.f = detail::cache_of(p) at src/fft/series_core.hpp:300) instead of its own copy, so any later use of the original can make that reference point at freed or re-used memory.
Impact: A program that keeps a length-adjusted copy and then keeps using the original series can crash or produce silently wrong multiplication results.

Mechanism: prefix_cached stores its transforms in a std::vector that both reallocates and recomputes entries

prefix_cached keeps its memoized transforms in mutable std::vector<entry> caches (src/fft/series_core.hpp:624) and hands out entry::t references from prefix_cache(n) (src/fft/series_core.hpp:607-618). with_len copies coefficients but keeps a std::reference_wrapper to that slot in resized::f, which is then handed to E::extend_to by the multiply paths.

Two observed failures (both reproduced against engines::ntt<modnum<998244353>>):

  1. Use-after-free. auto w = series::with_len(qa, 4); takes a reference to caches[2]; a subsequent series::square(qa) calls cache() -> prefix_cache(nextPow2(len)), which does caches.resize(...) and reallocates the vector. Using w afterwards reports heap-use-after-free inside ntt::extend_to under ASan.

  2. Stale cache. For qa of length 3, with_len(qa, 8) rides prefix_cache(4), whose entry covers 3 coefficients. After qa.append(...) and a later qa.first(4), that same slot is recomputed over 4 coefficients (c.len != e check at src/fft/series_core.hpp:613), and the copy's product then comes out wrong even though the copy's own coefficients never changed.

The doc comment at src/fft/series_core.hpp:265-268 only requires the source to outlive the result, which is not sufficient: the source must additionally never touch its caches again.

Prompt for agents
series::with_len stores a std::reference_wrapper to a source cache slot in resized::f (src/fft/series_core.hpp:290-302). For prefix_cached sources that slot lives inside a mutable std::vector<entry> caches (src/fft/series_core.hpp:607-624), whose elements are both reallocated (caches.resize on a new power-of-two scale) and recomputed in place (when min(n, len()) changes after append()). Consequently a resized value that outlives a single expression can end up referencing freed memory (confirmed heap-use-after-free under ASan when square(qa) follows with_len(qa, 4)) or a transform of a different coefficient prefix (confirmed wrong product after append + first(4)). Consider either giving prefix_cached stable storage for its entries (e.g. std::deque, or vector of unique_ptr) plus an invalidation scheme that never rewrites an entry in place, or making resized own its transform (copy or shared handle) rather than borrowing, and update the contract comment to state precisely what the source may/may not do while the copy is alive.
Open in Devin Review (Beta)

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/fft/series_core.hpp
Comment on lines +383 to 386
for (int n = 1; n < N; n *= 2) {
int m = 2 * n;
auto ta = E::transform(a.first(std::min(a.len(), m)), m);
auto ta = E::transform(a.first(std::min(N, m)), m);
auto tb = E::transform(std::span<const T>(b).first(n), m);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Series inversion of a prefix-cached input now does extra, discarded transforms

The inversion routine reads its input through the operand's own borrow helper (a.first(std::min(N, m)) at src/fft/series_core.hpp:385) instead of a plain coefficient view, so a prefix-caching input eagerly builds a transform at every doubling step that the routine immediately throws away.
Impact: Inverting a prefix-cached series is measurably slower (~30% in a 2^17-coefficient benchmark) and uses more memory than inverting the same plain series.

Mechanism: prefix_cached::first(k) has a transform-building side effect

Before this PR ps_inv started with span<E, false> a = a_;, so all subsequent first() calls went through the plain span. Now a is the generic operand: for prefix_cached, first(k) with k a power of two calls prefix_cache(k) (src/fft/series_core.hpp:597-603), which computes E::transform(s.first(e), 2 * k) and stores it. ps_inv then ignores that cache and computes E::transform(...) itself at size m, so each doubling level pays for one extra full-size forward transform.

Measured with engines::ntt<modnum<998244353>>, N = 2^17: 0.017s for trunc input vs 0.022s for the same coefficients wrapped in prefix_cached.

Prompt for agents
ps_inv (src/fft/series_core.hpp:373-399) now calls a.first(...) on the generic operand instead of first converting to span<E, false>. For prefix_cached operands, first(k) is not free: it populates and returns prefix_cache(k), an extra forward transform at size 2k that ps_inv never uses (it calls E::transform itself). Either take the plain span borrow up front for the read path, or actually reuse the returned cache (the TODO above ps_inv already contemplates populating/reusing these caches).
Open in Devin Review (Beta)

Was this helpful? React with 👍 or 👎 to provide feedback.

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