Skip to content

Optimize convolution with Kronecker substitution and performance tuning - #38

Closed
shakayami wants to merge 1 commit into
masterfrom
claude/convolution-speedup-research-emyfaq
Closed

Optimize convolution with Kronecker substitution and performance tuning#38
shakayami wants to merge 1 commit into
masterfrom
claude/convolution-speedup-research-emyfaq

Conversation

@shakayami

Copy link
Copy Markdown
Owner

Summary

This PR significantly optimizes the convolution implementation by adding a Kronecker substitution path for small-to-medium inputs and applying various performance improvements to the NTT-based path.

Key Changes

  • Kronecker Substitution Path: Added _convolution_int() method that uses integer multiplication with zero-padded slots to compute convolutions for inputs where the product coefficients fit within fixed-width integers. This is faster than NTT for many practical cases.

  • Adaptive Dispatch: The convolution() method now uses a cost model to choose between three paths:

    • Schoolbook multiplication for very small inputs (n*m ≤ 40)
    • Kronecker substitution for small-to-medium inputs with small coefficients
    • NTT for large inputs where FFT dominates
  • Performance Optimizations:

    • Hoisted instance attributes (mod, rate2, rate3, imag, etc.) into local variables in butterfly() and butterfly_inv() to reduce attribute lookup overhead in innermost loops
    • Added fast path for rot == 1 case in butterfly operations to avoid unnecessary multiplications
    • Pre-computed intermediate values (p2, p3, a02, a13, etc.) to reduce redundant calculations
    • Optimized schoolbook path with early zero-skipping and enumeration
    • Moved inverse scaling into the input preparation phase rather than post-processing
  • Struct-based Packing: Implemented _pack() and _unpack() methods using Python's struct module for efficient serialization of coefficient arrays into integers, with caching of struct formatters for repeated use.

  • Comprehensive Testing: Added extensive test coverage including:

    • Empty input handling
    • Random test cases across multiple moduli
    • Unnormalized input (negative and out-of-range coefficients)
    • Zero coefficients
    • Large and lopsided convolutions
    • Butterfly round-trip verification
  • Benchmark Workload: Added convolution_lopsided benchmark to measure performance on the common case of multiplying a long array by a short one.

Implementation Details

The cost model compares the estimated runtime of Kronecker substitution (based on Karatsuba integer multiplication complexity) against NTT (O(z log z) where z is the next power of 2). The threshold is empirically fitted to CPython 3.11 performance characteristics.

The Kronecker substitution approach encodes each coefficient into a fixed-width slot such that the product of two encoded values can be decoded without carry interference between slots, allowing CPython's optimized integer multiplication to perform the entire convolution in one operation.

https://claude.ai/code/session_015N4uDXqbEPEaCbPoE4Utz4

Profiling the existing NTT showed the cost is almost entirely CPython
interpreter overhead in the butterfly inner loops, and that the NTT is only
the right algorithm once both operands are large. Three changes:

1. Add an integer-packing (Kronecker substitution) path. Each coefficient is
   placed in its own zero-padded slot of a big integer, so a single CPython
   multiply performs the whole convolution in C. This is faster than the NTT
   whenever the smaller operand is small, the operands are lopsided, or the
   coefficients are small; a fitted cost model picks between the two. Slot
   width is derived from the actual maximum coefficient rather than the
   modulus, and struct is used for packing/unpacking instead of per-element
   to_bytes.

2. Optimize the butterfly loops: hoist instance attributes into locals,
   precompute the p/2p/3p index offsets, share common subexpressions between
   the four radix-4 outputs, and skip the twiddle multiplications for the
   groups where the rotation is 1.

3. Fold the 1/z normalization into b before its transform, so the inverse
   transform's output needs no extra scaling pass or extra list.

The schoolbook path now triggers on n*m instead of min(n,m), since integer
packing beats it above roughly 40 coefficient products.

Measured on CPython 3.11 with mod 998244353 (old -> new):

  n=m=1000            7.8ms  ->   1.6ms   (4.9x)
  n=m=8192           80.3ms  ->  46.8ms   (1.7x)
  n=m=65536        1040.3ms  -> 690.0ms   (1.5x)
  n=65536, m=256    773.5ms  ->  50.1ms  (15.4x)
  n=262144, m=256  4121.7ms  -> 200.6ms  (20.6x)
  n=m=8192, 0/1      76.9ms  ->   5.8ms  (13.2x)

Peak memory is unchanged to 75% lower. Tests cover both dispatch paths,
unnormalized/negative/zero inputs and several moduli, and the benchmark
suite gains a lopsided convolution workload alongside the balanced one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015N4uDXqbEPEaCbPoE4Utz4
@github-actions

Copy link
Copy Markdown

📊 Performance Benchmark Results

⏱️ Execution time (mean, lower is better)

Benchmark Base PR Delta
acl_math 2.063s 2.097s +1.6%
acl_string 255.28ms 266.24ms +4.3%
convolution 972.74ms 720.58ms -25.9% ✅
convolution_lopsided n/a 51.75ms n/a
dsu 718.90ms 720.93ms +0.3%
fenwicktree 1.231s 1.251s +1.6%
fps 626.86ms 622.42ms -0.7%
lazysegtree 2.071s 1.877s -9.4% ✅
maxflow 891.51ms 842.66ms -5.5% ✅
mincostflow 54.78ms 50.10ms -8.5% ✅
prime_fact 2.076s 2.103s +1.3%
scc 396.26ms 387.88ms -2.1% ✅
segtree 687.47ms 700.63ms +1.9%
two_sat 1.557s 1.491s -4.2% ✅

💾 Peak memory (lower is better)

Benchmark Base PR Delta
acl_math 4.9KiB 4.9KiB +0.0%
acl_string 13.5MiB 13.5MiB +0.0%
convolution 16.1MiB 15.1MiB -6.2% ✅
convolution_lopsided n/a 3.7MiB n/a
dsu 40.4MiB 40.4MiB +0.0%
fenwicktree 8.9MiB 8.9MiB +0.0%
fps 3.9MiB 3.9MiB -0.0%
lazysegtree 9.8MiB 9.8MiB +0.0%
maxflow 97.0MiB 97.0MiB +0.0%
mincostflow 16.3MiB 16.3MiB +0.0%
prime_fact 8.5KiB 8.5KiB +0.0%
scc 25.1MiB 25.1MiB +0.0%
segtree 6.0MiB 6.0MiB +0.0%
two_sat 174.0MiB 174.0MiB +0.0%

Base = target branch, PR = this branch. Negative delta = improvement. ✅ improvement ≥ 1%, ⚠️ regression > 5%. "n/a" base entries mean the target branch has no benchmark suite yet.

@shakayami shakayami linked an issue Jul 31, 2026 that may be closed by this pull request
@shakayami

Copy link
Copy Markdown
Owner Author

yosupo judgeだと早くなってない

@shakayami shakayami closed this Jul 31, 2026
@shakayami
shakayami deleted the claude/convolution-speedup-research-emyfaq branch July 31, 2026 15:18
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.

convolution

2 participants