Skip to content

Commit 49f2b1e

Browse files
committed
initial commit
0 parents  commit 49f2b1e

9 files changed

+516
-0
lines changed

Diff for: Readme.md

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Fast Fourier Transform over Finite Field Performance in Mojo and Python
2+
3+
[Fast Fourier Transform (FFT)](https://en.wikipedia.org/wiki/Fast_Fourier_transform) over [Finite Field](https://en.wikipedia.org/wiki/Finite_field) is used in Cryptography.
4+
The Cooley-Tukey FFT algorithm can be implemented in less than 20 lines of Python. With a slight modification, the domain can be changed to a finite field, instead of complex number.
5+
6+
## Implementations
7+
8+
### Python version
9+
10+
The Python version is in [`python/fft-python.py`](python/fft-python.py). The algorithm looks as following:
11+
12+
```python
13+
def fft_over_finite_field( P: list[FieldElement], w: FieldElement ) -> list[FieldElement]:
14+
n = len(P)
15+
if n == 1:
16+
return P
17+
18+
w_square = w * w
19+
Pe, Po = P[::2], P[1::2]
20+
ye, yo = fft_over_finite_field(Pe, w_square), fft_over_finite_field(Po, w_square)
21+
y = [FieldElement(0)] * n
22+
w_power = FieldElement(1)
23+
for j in range(n // 2):
24+
u = ye[j]
25+
v = (w_power * yo[j])
26+
y[j] = u + v
27+
y[j + n // 2] = u - v
28+
w_power = w_power * w
29+
return y
30+
```
31+
32+
It depends on the `FieldElement` class, which is defined in the [`python/field.py`](python/field.py). The characteristic prime of the field is hardcoded as `3 * 2**30 + 1` for simplicity.
33+
34+
35+
### Mojo version
36+
37+
The Mojo version was implemented using the DynamicVector.
38+
39+
```mojo
40+
fn fft_over_finite_field( P: DynamicVector[FieldElement], w: FieldElement ) \
41+
-> DynamicVector[FieldElement]:
42+
let n = len(P)
43+
if n == 1:
44+
return P
45+
46+
let w_square = w * w
47+
let Pe = half_vector( P, 0 )
48+
let Po = half_vector( P, 1 )
49+
let ye = fft_over_finite_field( Pe, w_square )
50+
let yo = fft_over_finite_field( Po, w_square )
51+
var y = make_vector( n )
52+
var w_power = FieldElement(1)
53+
for j in range(n // 2):
54+
let u = ye[j]
55+
let v = (w_power * yo[j])
56+
y[j] = u + v
57+
y[j + n // 2] = u - v
58+
w_power = w_power * w
59+
return y
60+
```
61+
62+
I factored out `make_vector` and `half_vector` functions since Mojo does not yet support list comprehension. The `FieldElement` struct is defined in the [`mojo/field.mojo`](mojo/field.mojo)
63+
64+
65+
### Python `galoios` module
66+
67+
I also tried the Python `galois` module that was available as a pip package. It implements the finite field math and uses Numpy as backend. Thus, one could use Numpy's FFT implementation over finite field elements.
68+
69+
70+
## Performance
71+
72+
I generated a random array of size 1024 * 256 and ran FFT in each version. It was measured on my M1 MacBook Pro laptop.
73+
74+
| Implementation | Runtime (secs) |
75+
| --- | --- |
76+
| Python standalone | 4.3244 |
77+
| Python `galois` module | 9.5888 |
78+
| Mojo | 0.0573 |
79+
80+
Mojo's speedup was 75x over the standalone Python implementation.
81+
82+
The `galois` module turned out to be much slower even if it used Numpy's FFT implementation. I guess Numpy couldn't use the optimized C kernel, since it has to interact with `galois`'s `GF` class.

Diff for: mojo/fft-mojo.mojo

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from random import random_ui64
2+
from field import FieldElement
3+
import benchmark
4+
5+
#=============================================================================
6+
# Helper functions
7+
8+
fn make_vector( size: Int ) -> DynamicVector[FieldElement]:
9+
let zero = FieldElement(0)
10+
var vec = DynamicVector[FieldElement]()
11+
vec.resize( size, zero )
12+
return vec
13+
14+
fn generate_random_values( size: Int ) raises -> DynamicVector[FieldElement]:
15+
var values = make_vector( size )
16+
for i in range(size):
17+
let r: Int = int(random_ui64(0, FieldElement.characteristic()-1))
18+
values[i] = FieldElement(r)
19+
return values
20+
21+
fn to_str( vec: DynamicVector[FieldElement] ) -> String:
22+
var s: String = "["
23+
let l = len(vec)
24+
if l < 6:
25+
for i in range(l):
26+
if i > 0:
27+
s += ", "
28+
s += str(vec[i])
29+
else:
30+
for i in range(3):
31+
if i > 0:
32+
s += ", "
33+
s += str(vec[i])
34+
s += " ... "
35+
for i in range(3):
36+
if i > 0:
37+
s += ", "
38+
s += str(vec[l-3-1+i])
39+
return s + "]"
40+
41+
42+
#=============================================================================
43+
# Implementation of Cooley-Tukey FFT over Finite Field
44+
45+
fn half_vector( vec: DynamicVector[FieldElement], start: Int ) -> DynamicVector[FieldElement]:
46+
var result = make_vector( len(vec) // 2 )
47+
debug_assert( start + (len(result)-1) * 2 < len(vec), "out of bounds" )
48+
for i in range(len(result)):
49+
result[i] = vec[start + i*2]
50+
return result
51+
52+
fn fft_over_finite_field( P: DynamicVector[FieldElement], w: FieldElement ) \
53+
-> DynamicVector[FieldElement]:
54+
let n = len(P)
55+
if n == 1:
56+
return P
57+
58+
let w_square = w * w
59+
let Pe = half_vector( P, 0 )
60+
let Po = half_vector( P, 1 )
61+
let ye = fft_over_finite_field( Pe, w_square )
62+
let yo = fft_over_finite_field( Po, w_square )
63+
var y = make_vector( n )
64+
var w_power = FieldElement(1)
65+
for j in range(n // 2):
66+
let u = ye[j]
67+
let v = (w_power * yo[j])
68+
y[j] = u + v
69+
y[j + n // 2] = u - v
70+
w_power = w_power * w
71+
return y
72+
73+
74+
#=============================================================================
75+
# Main program
76+
77+
let size: Int = 1024 * 256
78+
let group_element_power: Int = 2 ** 30 - size
79+
let g = FieldElement.primitive_element() ** (3 * group_element_power)
80+
81+
fn main() raises:
82+
print( "size:", size )
83+
84+
# print( "generating random values..." )
85+
let values = generate_random_values( size )
86+
# print( "values:", to_str(values) )
87+
88+
# let p = fft_over_finite_field( values, g )
89+
# print( "p:", to_str(p) )
90+
91+
@parameter
92+
fn bench():
93+
_ = fft_over_finite_field( values, g )
94+
95+
let report = benchmark.run[bench]( 1, 3, 4, 10 )
96+
print( "mean runtime:", report.mean("s") )

Diff for: mojo/field.mojo

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Ported from starkware101 tutorial's Python version[1].
2+
# [1]: https://github.com/starkware-industries/stark101
3+
4+
@value
5+
struct FieldElement(CollectionElement, Stringable):
6+
7+
# ------------------------------------------------------------------------
8+
# Static methods
9+
10+
@staticmethod
11+
fn characteristic() -> Int:
12+
return 3 * 2**30 + 1
13+
14+
@staticmethod
15+
fn primitive_element() -> Self:
16+
return Self(5)
17+
18+
19+
# ------------------------------------------------------------------------
20+
# Fields and basic methods
21+
22+
var val: Int
23+
24+
fn __init__(inout self, val: Int):
25+
self.val = val % Self.characteristic()
26+
27+
fn __str__(self) -> String:
28+
return String(self.val)
29+
30+
31+
# ------------------------------------------------------------------------
32+
# Arithmetic operators
33+
34+
fn __eq__(self, other: Self) -> Bool:
35+
return self.val == other.val
36+
37+
fn __neg__(self) -> Self:
38+
return Self(-self.val)
39+
40+
fn __add__(self, other: Self) -> Self:
41+
return Self(self.val + other.val)
42+
43+
fn __sub__(self, other: Self) -> Self:
44+
return Self(self.val - other.val)
45+
46+
fn __mul__(self, other: Self) -> Self:
47+
return Self(self.val * other.val)
48+
49+
fn __imul__(inout self, other: Self):
50+
self.val = (self.val * other.val) % Self.characteristic()
51+
52+
fn inverse(self) -> Self:
53+
var t: Int = 0
54+
var new_t: Int = 1
55+
var r = FieldElement.characteristic()
56+
var new_r = self.val
57+
while new_r != 0:
58+
let quotient = r // new_r
59+
t, new_t = new_t, (t - (quotient * new_t))
60+
r, new_r = new_r, (r - (quotient * new_r))
61+
debug_assert( r == 1, "inverse() failed" )
62+
return Self(t)
63+
64+
fn __truediv__(self, other: Self) -> Self:
65+
return self * other.inverse()
66+
67+
fn __pow__(self, _n: Int) -> Self:
68+
debug_assert( _n >= 0, "unexpected negative argument" )
69+
var cur_pow = self
70+
var res = FieldElement(1)
71+
var n = _n
72+
while n > 0:
73+
if n % 2 != 0:
74+
res *= cur_pow
75+
n = n // 2
76+
cur_pow *= cur_pow
77+
return res
78+
79+
fn __pow__(self, n: Self) -> Self:
80+
return self.__pow__(n.val)

Diff for: python/fft-python-galois.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import galois
2+
import numpy as np
3+
from utility import measure_runtime
4+
5+
# The prime number for the field: 3 * 2^30 + 1 (= 3221225473)
6+
p = 3 * 2**30 + 1
7+
8+
GF = galois.GF(p, repr="power")
9+
# print(GF.properties)
10+
11+
size = 1024 * 256
12+
print( "size:", size )
13+
14+
# print( "generating random values..." )
15+
values = GF.Random( size )
16+
# print( "values:", values )
17+
18+
p, runtime = measure_runtime( np.fft.fft, values )
19+
# print( "p:", p )
20+
print( "runtime:", runtime )

Diff for: python/fft-python.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from field import FieldElement
2+
from utility import measure_runtime
3+
import galois
4+
5+
#=============================================================================
6+
# Implementation of Cooley-Tukey FFT over Finite Field
7+
8+
def fft_over_finite_field( P: list[FieldElement], w: FieldElement ) -> list[FieldElement]:
9+
n = len(P)
10+
if n == 1:
11+
return P
12+
13+
w_square = w * w
14+
Pe, Po = P[::2], P[1::2]
15+
ye, yo = fft_over_finite_field(Pe, w_square), fft_over_finite_field(Po, w_square)
16+
y = [FieldElement(0)] * n
17+
w_power = FieldElement(1)
18+
for j in range(n // 2):
19+
u = ye[j]
20+
v = (w_power * yo[j])
21+
y[j] = u + v
22+
y[j + n // 2] = u - v
23+
w_power = w_power * w
24+
return y
25+
26+
27+
#=============================================================================
28+
# Main program
29+
30+
size = 1024 * 256
31+
group_element_power = 2 ** 30 - size
32+
g = FieldElement.generator() ** (3 * group_element_power)
33+
34+
print( "size:", size )
35+
36+
# print( "generating random values..." )
37+
GF = galois.GF(FieldElement.k_modulus)
38+
values = [FieldElement(int(x)) for x in GF.Random(size)]
39+
# print( "values:", values[0:3], "...", values[-3:] )
40+
41+
p, runtime = measure_runtime( fft_over_finite_field, values, g )
42+
# print( "p:", p[0:3], "...", p[-3:] )
43+
print( "runtime:", runtime )

0 commit comments

Comments
 (0)