Skip to main content

binius_field/
transpose.rs

1// Copyright 2023-2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4use binius_utils::checked_arithmetics::checked_log_2;
5
6use super::packed::PackedField;
7use crate::{BinaryField, ExtensionField, PackedSubfield, UnderlierView, packed_extension};
8
9/// Transpose square blocks of elements within packed field elements in place.
10///
11/// The input elements are interpreted as a rectangular matrix with height `n = 2^n` in row-major
12/// order. This matrix is interpreted as a vector of square matrices of field elements, and each
13/// square matrix is transposed in-place.
14///
15/// # Arguments
16///
17/// * `log_n`: The base-2 logarithm of the dimension of the n x n square matrix. Must be less than
18///   or equal to the base-2 logarithm of the packing width.
19/// * `elems`: The packed field elements, length is a power-of-two multiple of `1 << log_n`.
20///
21/// # Preconditions
22///
23/// * `log_n` must be at most `P::LOG_WIDTH`.
24/// * `elems.len()` must be a power of two and at least `2^log_n`.
25///
26/// A caller whose dimensions are compile-time constants should use the fixed-size form below.
27/// That form unrolls the butterfly and keeps the array in registers.
28pub fn transpose_square_blocks<P: PackedField>(log_n: usize, elems: &mut [P]) {
29	assert!(P::LOG_WIDTH >= log_n, "dimension n of square blocks must divide packing width");
30
31	let size = elems.len();
32	assert!(size.is_power_of_two(), "elems length must be a power of two, got {size}");
33	let log_size = checked_log_2(size);
34	assert!(
35		log_size >= log_n,
36		"elems must have length at least 2^log_n = {}, got {size}",
37		1 << log_n
38	);
39
40	let log_w = log_size - log_n;
41
42	// See Hacker's Delight, Section 7-3.
43	// https://dl.acm.org/doi/10.5555/2462741
44	for i in 0..log_n {
45		for j in 0..1 << (log_n - i - 1) {
46			for k in 0..1 << (log_w + i) {
47				let idx0 = (j << (log_w + i + 1)) | k;
48				let idx1 = idx0 | (1 << (log_w + i));
49
50				let v0 = elems[idx0];
51				let v1 = elems[idx1];
52				let (v0, v1) = v0.interleave(v1, i);
53				elems[idx0] = v0;
54				elems[idx1] = v1;
55			}
56		}
57	}
58}
59
60/// Transposes square blocks of scalars across a fixed-size array of packed elements, in place.
61///
62/// # Overview
63///
64/// The runtime-sized form in this module computes the same permutation.
65/// This form is for a caller whose block dimension and array length are both constants.
66///
67/// Constant sizes let the compiler unroll the butterfly.
68/// The whole array then stays in registers, which is what a caller in a hot loop wants.
69///
70/// # Algorithm
71///
72/// A butterfly network over `LOG_N` rounds, as in Hacker's Delight, Section 7-3.
73/// Round `i` interleaves element pairs `2^(log_w + i)` apart at block granularity `2^i`.
74///
75/// # Preconditions
76///
77/// All three are checked at compile time, so a violating instantiation fails to build:
78///
79/// * The array length must be a power of two.
80/// * The block dimension must not exceed the base-2 log of the array length.
81/// * The block dimension must not exceed the base-2 log of the packed width.
82pub fn transpose_square_blocks_array<P: PackedField, const LOG_N: usize, const S: usize>(
83	elems: &mut [P; S],
84) {
85	const {
86		assert!(LOG_N <= P::LOG_WIDTH, "LOG_N must not exceed the packed width");
87		assert!(LOG_N <= checked_log_2(S), "LOG_N must not exceed the array length");
88	}
89
90	let log_size = checked_log_2(S);
91
92	// Elements per block that stays contiguous through the butterfly.
93	let log_w = log_size - LOG_N;
94
95	for i in 0..LOG_N {
96		for j in 0..1 << (LOG_N - i - 1) {
97			for k in 0..1 << (log_w + i) {
98				// Partner elements for this round, one stride apart.
99				let idx0 = (j << (log_w + i + 1)) | k;
100				let idx1 = idx0 | (1 << (log_w + i));
101
102				// Interleaving at block granularity 2^i swaps the axes one bit at a time.
103				let (v0, v1) = elems[idx0].interleave(elems[idx1], i);
104				elems[idx0] = v0;
105				elems[idx1] = v1;
106			}
107		}
108	}
109}
110
111pub fn square_transforms_extension_field<F, FE>(values: &mut [FE])
112where
113	F: BinaryField,
114	FE: PackedField<Scalar: ExtensionField<F>> + UnderlierView,
115	PackedSubfield<FE, F>: PackedField<Scalar = F>,
116{
117	transpose_square_blocks(
118		FE::Scalar::LOG_DEGREE,
119		packed_extension::cast_bases_mut::<F, FE>(values),
120	);
121}
122
123#[cfg(test)]
124mod tests {
125	use std::array;
126
127	use proptest::prelude::*;
128	use rand::{SeedableRng, rngs::StdRng};
129
130	use super::*;
131	use crate::{PackedBinaryField64x1b, PackedBinaryField128x1b, PackedField, Random};
132
133	#[test]
134	fn test_transpose_square_blocks_128x1b() {
135		let mut elems = [
136			PackedBinaryField128x1b::from(0x00000000000000000000000000000000u128),
137			PackedBinaryField128x1b::from(0x00000000000000000000000000000000u128),
138			PackedBinaryField128x1b::from(0x00000000000000000000000000000000u128),
139			PackedBinaryField128x1b::from(0x00000000000000000000000000000000u128),
140			PackedBinaryField128x1b::from(0xffffffffffffffffffffffffffffffffu128),
141			PackedBinaryField128x1b::from(0xffffffffffffffffffffffffffffffffu128),
142			PackedBinaryField128x1b::from(0xffffffffffffffffffffffffffffffffu128),
143			PackedBinaryField128x1b::from(0xffffffffffffffffffffffffffffffffu128),
144		];
145		transpose_square_blocks(3, &mut elems);
146
147		let expected = [
148			PackedBinaryField128x1b::from(0xf0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0u128),
149			PackedBinaryField128x1b::from(0xf0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0u128),
150			PackedBinaryField128x1b::from(0xf0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0u128),
151			PackedBinaryField128x1b::from(0xf0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0u128),
152			PackedBinaryField128x1b::from(0xf0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0u128),
153			PackedBinaryField128x1b::from(0xf0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0u128),
154			PackedBinaryField128x1b::from(0xf0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0u128),
155			PackedBinaryField128x1b::from(0xf0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0u128),
156		];
157		assert_eq!(elems, expected);
158	}
159
160	#[test]
161	fn test_transpose_square_blocks_128x1b_multi_row() {
162		let mut elems = [
163			PackedBinaryField128x1b::from(0x00000000000000000000000000000000u128),
164			PackedBinaryField128x1b::from(0x00000000000000000000000000000000u128),
165			PackedBinaryField128x1b::from(0x00000000000000000000000000000000u128),
166			PackedBinaryField128x1b::from(0x00000000000000000000000000000000u128),
167			PackedBinaryField128x1b::from(0xffffffffffffffffffffffffffffffffu128),
168			PackedBinaryField128x1b::from(0xffffffffffffffffffffffffffffffffu128),
169			PackedBinaryField128x1b::from(0xffffffffffffffffffffffffffffffffu128),
170			PackedBinaryField128x1b::from(0xffffffffffffffffffffffffffffffffu128),
171		];
172		transpose_square_blocks(1, &mut elems);
173
174		let expected = [
175			PackedBinaryField128x1b::from(0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaau128),
176			PackedBinaryField128x1b::from(0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaau128),
177			PackedBinaryField128x1b::from(0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaau128),
178			PackedBinaryField128x1b::from(0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaau128),
179			PackedBinaryField128x1b::from(0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaau128),
180			PackedBinaryField128x1b::from(0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaau128),
181			PackedBinaryField128x1b::from(0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaau128),
182			PackedBinaryField128x1b::from(0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaau128),
183		];
184		assert_eq!(elems, expected);
185	}
186
187	// The fixed-size form exists only to unroll the loop, so it must compute exactly the
188	// permutation the runtime form computes. Pinning them equal is what justifies keeping both.
189	//
190	//     same input -> runtime form   -> A
191	//                -> fixed-size form -> B
192	//     A == B for every block dimension the array length admits
193	fn check_forms_agree<P, const LOG_N: usize, const S: usize>(seed: u64)
194	where
195		P: PackedField + Random,
196	{
197		let mut rng = StdRng::seed_from_u64(seed);
198
199		// Random lanes over many trials cover every one of the S * WIDTH scalar positions.
200		for _ in 0..100 {
201			let input: [P; S] = array::from_fn(|_| P::random(&mut rng));
202
203			let mut runtime = input;
204			transpose_square_blocks(LOG_N, &mut runtime);
205
206			let mut fixed = input;
207			transpose_square_blocks_array::<P, LOG_N, S>(&mut fixed);
208
209			assert_eq!(fixed, runtime, "forms disagree at LOG_N = {LOG_N}, S = {S}");
210		}
211	}
212
213	#[test]
214	fn fixed_size_form_agrees_with_runtime_form() {
215		// Cover both row widths the callers run at, and every block dimension each admits.
216		//
217		//     64 lanes  -> LOG_N up to 6, array length 8 admits up to 3
218		//     128 lanes -> LOG_N up to 7, array length 8 admits up to 3
219		check_forms_agree::<PackedBinaryField64x1b, 0, 8>(0);
220		check_forms_agree::<PackedBinaryField64x1b, 1, 8>(1);
221		check_forms_agree::<PackedBinaryField64x1b, 3, 8>(2);
222		check_forms_agree::<PackedBinaryField128x1b, 3, 8>(3);
223
224		// A block dimension equal to the array length exercises the widest butterfly.
225		check_forms_agree::<PackedBinaryField64x1b, 4, 16>(4);
226		check_forms_agree::<PackedBinaryField128x1b, 5, 32>(5);
227	}
228
229	#[test]
230	fn transpose_exchanges_element_axis_with_low_scalar_bits() {
231		let mut rng = StdRng::seed_from_u64(0);
232
233		// The permutation itself, stated directly rather than through either implementation.
234		// Splitting a scalar position into a high part and its low three bits:
235		//
236		//     input : element r, position 8i + j  =  value at (r, 8i + j)
237		//     output: element j, position 8i + t  =  value at (t, 8i + j)
238		//
239		// So the element index and the low three bits of the position trade places.
240		for _ in 0..100 {
241			let input: [PackedBinaryField128x1b; 8] =
242				array::from_fn(|_| PackedBinaryField128x1b::random(&mut rng));
243			let mut output = input;
244			transpose_square_blocks_array::<_, 3, 8>(&mut output);
245
246			// Read both sides as scalars, so the assertion is about positions and not underliers.
247			let scalars = |elems: &[PackedBinaryField128x1b; 8]| {
248				elems
249					.iter()
250					.map(|e| e.iter().collect::<Vec<_>>())
251					.collect::<Vec<_>>()
252			};
253			let before = scalars(&input);
254			let after = scalars(&output);
255
256			// High part of the position, which the permutation leaves alone.
257			for i in 0..PackedBinaryField128x1b::WIDTH / 8 {
258				// Element of the output, which is the low three bits of the input position.
259				for j in 0..8 {
260					// Element of the input, which becomes the low three bits of the output.
261					for t in 0..8 {
262						assert_eq!(
263							after[j][i * 8 + t],
264							before[t][i * 8 + j],
265							"i={i}, j={j}, t={t}"
266						);
267					}
268				}
269			}
270		}
271	}
272
273	proptest! {
274		#[test]
275		fn transpose_is_an_involution(values in prop::collection::vec(any::<u128>(), 8)) {
276			// Exchanging two axes twice restores the original layout.
277			// This holds for the fixed-size form on any input, so it is a property, not a case.
278			let input: [PackedBinaryField128x1b; 8] =
279				array::from_fn(|i| PackedBinaryField128x1b::from(values[i]));
280
281			let mut roundtrip = input;
282			transpose_square_blocks_array::<_, 3, 8>(&mut roundtrip);
283			transpose_square_blocks_array::<_, 3, 8>(&mut roundtrip);
284
285			prop_assert_eq!(roundtrip, input);
286		}
287	}
288}