Harvey-van der Hoeven multiplication in Python
Integer multiplication in O(n log n) bit operations, matching the conjectured optimum.
- File
harvey_van_der_hoeven_multiplication.py, 189 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/harvey-van-der-hoeven-multiplication/. The authors are listed in prov/H0NKE4.jsonl.
"""A toy structural model of the first stage of the Harvey-van der Hoeven algorithm.
An integer product becomes a cyclic convolution of length S = s_1 * ... * s_d
for distinct primes s_i. The Chinese remainder theorem turns it into a
d-dimensional cyclic convolution of size s_1 x ... x s_d (Agarwal and Cooley),
which is done by a d-dimensional DFT, one dimension at a time. Each DFT of
prime length s is turned into a cyclic convolution of length s - 1 by Rader's
algorithm, the step of the paper's conditional variant. The arithmetic is
exact, modulo a prime q = 1 (mod S), instead of approximate complex numbers.
Gaussian resampling, the synthetic FFTs and the recursion of the real
algorithm are not modelled: the convolutions of length s - 1 are done
directly and only counted.
"""
from dataclasses import dataclass
from math import prod
@dataclass
class Counts:
"""Operations of the toy, by kind."""
prime_dfts: int = 0
rader_convolutions: int = 0
pointwise_products: int = 0
def is_prime(n: int) -> bool:
"""Deterministic Miller-Rabin, valid for n < 3.3 * 10^24."""
if n < 2:
return False
small = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41)
for p in small:
if n % p == 0:
return n == p
d, r = n - 1, 0
while d % 2 == 0:
d //= 2
r += 1
for a in small:
x = pow(a, d, n)
if x in (1, n - 1):
continue
for _ in range(r - 1):
x = x * x % n
if x == n - 1:
break
else:
return False
return True
def _prime_factors(n: int) -> list[int]:
out, p = [], 2
while p * p <= n:
if n % p == 0:
out.append(p)
while n % p == 0:
n //= p
p += 1
if n > 1:
out.append(n)
return out
def primitive_root(p: int) -> int:
"""The smallest generator of the multiplicative group modulo the prime p."""
factors = _prime_factors(p - 1)
g = 1
while True:
g += 1
if all(pow(g, (p - 1) // f, p) != 1 for f in factors):
return g
class Plan:
"""The sizes s_i, the modulus q and the roots of unity."""
def __init__(self, sizes: tuple[int, ...], b: int):
if len(set(sizes)) != len(sizes) or not all(is_prime(s) for s in sizes):
raise ValueError("the sizes must be distinct primes")
self.sizes, self.b = tuple(sizes), b
self.S = prod(sizes)
self.chunks = (self.S + 1) // 2 # 2 * chunks - 1 <= S: no cyclic wrap
bound = self.chunks * ((1 << b) - 1) ** 2
q = (bound // self.S + 1) * self.S + 1
while not is_prime(q):
q += self.S
self.q = q # a prime, q = 1 (mod S), q > every coefficient of the product
gq = primitive_root(q)
self.roots = [pow(gq, (q - 1) // s, q) for s in sizes] # primitive s_i-th roots
self.gens = [primitive_root(s) for s in sizes]
self.strides = [prod(sizes[i + 1 :]) for i in range(len(sizes))]
def cyclic_convolution(a: list[int], c: list[int], q: int) -> list[int]:
"""Cyclic convolution of two vectors of the same length, mod q, done directly."""
n = len(a)
out = [0] * n
for i, x in enumerate(a):
if x:
for j, y in enumerate(c):
k = i + j - n if i + j >= n else i + j
out[k] = (out[k] + x * y) % q
return out
def prime_dft(x: list[int], root: int, g: int, q: int, counts: Counts) -> list[int]:
"""DFT of prime length s with the root `root`, by Rader's algorithm."""
s = len(x)
counts.prime_dfts += 1
out = [0] * s
out[0] = sum(x) % q # @line L10
if s == 2:
out[1] = (x[0] - x[1]) % q
return out
ginv = pow(g, -1, s)
a = [x[pow(g, k, s)] for k in range(s - 1)] # @line L11
c = [pow(root, pow(ginv, k, s), q) for k in range(s - 1)]
conv = cyclic_convolution(a, c, q)
counts.rader_convolutions += 1
for p in range(s - 1): # @line L12
out[pow(ginv, p, s)] = (x[0] + conv[p]) % q
return out
def dft_d(cells: list[int], plan: Plan, sign: int, counts: Counts) -> list[int]:
"""d-dimensional DFT of an array stored in row-major order, one dimension at a time."""
q, S = plan.q, plan.S
cells = list(cells)
for i, s in enumerate(plan.sizes): # @line L7
stride = plan.strides[i]
root = pow(plan.roots[i], sign, q)
for base in range(S): # @line L8
if (base // stride) % s:
continue # base is the start of a line along dimension i
idx = [base + stride * k for k in range(s)]
line = prime_dft([cells[t] for t in idx], root, plan.gens[i], q, counts)
for t, val in zip(idx, line):
cells[t] = val
return cells # @line L9
def crt_cell(j: int, plan: Plan) -> int:
"""The row-major index of the cell (j mod s_1, ..., j mod s_d)."""
return sum((j % s) * st for s, st in zip(plan.sizes, plan.strides))
def hvdh_toy_multiply(u: int, v: int, plan: Plan, counts: Counts | None = None) -> int:
"""Return u * v for 0 <= u, v < 2^(b * chunks)."""
counts = counts if counts is not None else Counts()
b, S, q = plan.b, plan.S, plan.q
if u < 0 or v < 0:
raise ValueError("operands must be nonnegative")
if max(u.bit_length(), v.bit_length()) > b * plan.chunks:
raise ValueError("operands too long for this plan")
mask = (1 << b) - 1
cell = [crt_cell(j, plan) for j in range(S)]
def to_cells(z: int) -> list[int]:
arr = [0] * S
for j in range(plan.chunks): # @line L1
arr[cell[j]] = z & mask # @line L2
z >>= b
return arr
fu = dft_d(to_cells(u), plan, 1, counts) # @line L3
fv = dft_d(to_cells(v), plan, 1, counts)
prodcells = []
for x, y in zip(fu, fv): # @line L4
prodcells.append(x * y % q)
counts.pointwise_products += 1
back = dft_d(prodcells, plan, -1, counts) # @line L5
inv_s = pow(S, -1, q)
result = 0
for j in range(S - 1, -1, -1): # @line L6
result = (result << b) + back[cell[j]] * inv_s % q
return result
def expected_counts(sizes: tuple[int, ...]) -> tuple[int, int]:
"""(prime DFTs, Rader convolutions) of one d-dimensional DFT."""
S = prod(sizes)
dfts = sum(S // s for s in sizes)
convs = sum(S // s for s in sizes if s > 2)
return dfts, convs
test_harvey_van_der_hoeven_multiplication.py 110 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/harvey-van-der-hoeven-multiplication/. The authors are listed in prov/H0NKE4.jsonl.
import random
from math import prod
import pytest
from harvey_van_der_hoeven_multiplication import (
Counts,
Plan,
crt_cell,
expected_counts,
hvdh_toy_multiply,
is_prime,
prime_dft,
primitive_root,
)
SIZES = [(2,), (3,), (5,), (2, 3), (3, 5), (2, 5, 7), (3, 5, 7), (5, 7, 11), (2, 3, 5, 7)]
def test_is_prime_small():
sieve = [True] * 2000
sieve[0] = sieve[1] = False
for i in range(2, 2000):
if sieve[i]:
for j in range(i * i, 2000, i):
sieve[j] = False
assert all(is_prime(n) == sieve[n] for n in range(2000))
assert is_prime(998244353) and not is_prime(998244353 * 7)
def test_primitive_root():
for p in (3, 5, 7, 11, 13, 1831):
g = primitive_root(p)
assert len({pow(g, k, p) for k in range(p - 1)}) == p - 1
@pytest.mark.parametrize("s", [2, 3, 5, 7, 11, 13])
def test_prime_dft_matches_definition(s):
plan = Plan((s,), 6)
q, root, g = plan.q, plan.roots[0], plan.gens[0]
assert pow(root, s, q) == 1 and (s == 1 or pow(root, 1, q) != 1)
rng = random.Random(s)
for sign in (1, -1):
w = pow(root, sign, q)
for _ in range(10):
x = [rng.randrange(q) for _ in range(s)]
want = [sum(x[n] * pow(w, n * k, q) for n in range(s)) % q for k in range(s)]
assert prime_dft(x, w, g, q, Counts()) == want
@pytest.mark.parametrize("sizes", SIZES)
def test_crt_map_is_a_bijection(sizes):
plan = Plan(sizes, 4)
assert sorted(crt_cell(j, plan) for j in range(plan.S)) == list(range(plan.S))
@pytest.mark.parametrize("sizes", SIZES)
def test_counts(sizes):
plan = Plan(sizes, 4)
counts = Counts()
hvdh_toy_multiply(3, 5, plan, counts)
dfts, convs = expected_counts(sizes)
assert (counts.prime_dfts, counts.rader_convolutions, counts.pointwise_products) == (3 * dfts, 3 * convs, prod(sizes))
def test_example_from_the_page():
plan = Plan((3, 5), 4)
assert plan.q == 1831 and plan.chunks == 8
counts = Counts()
assert hvdh_toy_multiply(0xBEEF, 0xCAFE, plan, counts) == 0x97660722
assert (counts.prime_dfts, counts.rader_convolutions, counts.pointwise_products) == (24, 24, 15)
def test_edge_cases():
for sizes in SIZES:
for b in (1, 3, 8):
plan = Plan(sizes, b)
n = b * plan.chunks
top = (1 << n) - 1
for u, v in [(0, 0), (0, top), (1, 1), (1, top), (top, top), (1 << (n - 1), top)]:
assert hvdh_toy_multiply(u, v, plan) == u * v
def test_random_against_python_ints():
rng = random.Random(2019)
plans = [Plan(sizes, b) for sizes in SIZES[:7] for b in (1, 5, 8)]
checked = 0
for plan in plans:
n = plan.b * plan.chunks
for _ in range(25):
u = rng.getrandbits(rng.randrange(1, n + 1))
v = rng.getrandbits(rng.randrange(1, n + 1))
assert hvdh_toy_multiply(u, v, plan) == u * v
checked += 1
assert checked >= 500
def test_rejects_bad_input():
with pytest.raises(ValueError):
Plan((3, 3), 4)
with pytest.raises(ValueError):
Plan((4, 5), 4)
plan = Plan((3, 5), 4)
with pytest.raises(ValueError):
hvdh_toy_multiply(1 << 32, 1, plan)
with pytest.raises(ValueError):
hvdh_toy_multiply(-1, 1, plan)
What the next level needs
- the tests did not run in this build
Read the full page about Harvey-van der Hoeven multiplication