Traces of Frobenius
Written by Claude Opus 5.5, an AI model made by Anthropic, from the code, commit history and result notes (results/ap.md) of the Sagebrush project, led by William Stein (SageMath, Inc.).
Code: engine/ap. Source: Kedlaya and Sutherland's smalljac strategy for
genus 1.
The problem
For an elliptic curve and every prime up to a bound, compute . These are the inputs to -functions, Sato–Tate statistics and modularity checks. They are needed for millions of primes, so the cost per prime is what matters.
The method
- Arithmetic modulo in Montgomery form, with branchless addition and subtraction and an interleaved batch inversion.
- A point without square roots. For each prime, one Legendre symbol decides between and its quadratic twist. With , the point lies on .
- Baby-step giant-step over the Hasse interval , for every with . Points are in Jacobian coordinates with batched normalization.
- Halve the search when one Legendre symbol (of the cubic's discriminant) shows that is even.
- Never probabilistic. Take more points until exactly one is consistent with all of them. Below , count points directly.
- Parallel over chunks of primes for one curve, or over curves.
It is about 600 lines of Rust.
Checks
- Every of 11a for is identical to smalljac's output, all 78,497 primes.
- The sum of for 11a up to is identical (5,761,454 primes).
- Sage agrees for on 11 curves, covering ranks 0–3, CM curves and large coefficients.
- Sato–Tate moments of 11a up to are 1.000, 1.999, 4.998, 13.992 (theory: 1, 2, 5, 14). For the CM curve 27a3 they follow the CM distribution: 0.999, 2.997, 9.988, 34.955.
Speed
Curve 11a, all good primes up to :
| smalljac, 1 core | Rust, 1 thread | smalljac, 16 processes | Rust, 16 threads | |
|---|---|---|---|---|
| 3.99 s | 4.93 s | 0.58 s | 0.45 s | |
| 42.1 s | 67.4 s | 4.27 s | 6.10 s |
For many curves at once, of 1000 curves at all 9592 primes below takes 4.24 s from Python on 16 threads.
Where the gap is, and what did not work
- The gap grows with (1.23× at , 1.6× at ). The baby-step giant-step work grows like , so smalljac's advantage is per group operation.
- The kernel is latency-bound. About 45% of the time is Montgomery multiplication, but in serial chains: scalar multiplication and batch inversion are chains of roughly 12-cycle multiplications.
- Affine progressions with shared inversions (6 multiplications per point instead of about 16) were 20% slower at . Each extra inversion is a serial chain of about 33 multiplications. And a progression that adds to also hits a doubling, which at first made every call fall back.
What would have saved time
- Profile for latency, not just for instruction counts. Saving multiplications does not help a kernel whose multiplications wait on each other. The lever is interleaving several primes, so that the chains overlap.
- Check digests, not samples. A digest of all 78,497 of 11a sits in the test suite, so any regression anywhere shows up at once.