Skip to main content

ferray_random/bitgen/
philox.rs

1// ferray-random: Philox 4x32 counter-based BitGenerator
2//
3// Reference: Salmon et al., "Parallel Random Numbers: As Easy as 1, 2, 3",
4// SC '11: Proceedings of the 2011 International Conference for High
5// Performance Computing.
6//
7// Philox 4x32-10: 4 words of 32 bits, 10 rounds.
8// Supports stream IDs natively (the key encodes the stream).
9
10use super::BitGenerator;
11
12/// Philox 4x32-10 counter-based pseudo-random number generator.
13///
14/// This generator natively supports stream IDs, making it ideal for
15/// parallel generation. Each (seed, `stream_id`) pair produces an
16/// independent, non-overlapping output sequence.
17///
18/// # Example
19/// ```
20/// use ferray_random::bitgen::Philox;
21/// use ferray_random::bitgen::BitGenerator;
22///
23/// let mut rng = Philox::seed_from_u64(42);
24/// let val = rng.next_u64();
25/// ```
26pub struct Philox {
27    /// 4x32-bit counter
28    counter: [u32; 4],
29    /// 2x32-bit key (derived from seed and stream)
30    key: [u32; 2],
31    /// Buffered output (4 x u32 = 2 x u64)
32    buffer: [u32; 4],
33    /// Index into buffer (0..4)
34    buf_idx: usize,
35}
36
37// Philox 4x32 round constants
38const PHILOX_M0: u32 = 0xD251_1F53;
39const PHILOX_M1: u32 = 0xCD9E_8D57;
40const PHILOX_W0: u32 = 0x9E37_79B9;
41const PHILOX_W1: u32 = 0xBB67_AE85;
42
43/// Single Philox round: two multiplications with xor-folding.
44///
45/// `key` is taken by reference so the caller's stationary key buffer
46/// stays put across the unrolled round sequence; copying the 8-byte key
47/// every round defeats register allocation in release builds.
48#[inline]
49#[allow(clippy::trivially_copy_pass_by_ref)]
50const fn philox_round(ctr: &mut [u32; 4], key: &[u32; 2]) {
51    let lo0 = ctr[0] as u64 * PHILOX_M0 as u64;
52    let lo1 = ctr[2] as u64 * PHILOX_M1 as u64;
53    let hi0 = (lo0 >> 32) as u32;
54    let lo0 = lo0 as u32;
55    let hi1 = (lo1 >> 32) as u32;
56    let lo1 = lo1 as u32;
57    let new0 = hi1 ^ ctr[1] ^ key[0];
58    let new1 = lo1;
59    let new2 = hi0 ^ ctr[3] ^ key[1];
60    let new3 = lo0;
61    ctr[0] = new0;
62    ctr[1] = new1;
63    ctr[2] = new2;
64    ctr[3] = new3;
65}
66
67/// Bump key between rounds.
68#[inline]
69const fn philox_bump_key(key: &mut [u32; 2]) {
70    key[0] = key[0].wrapping_add(PHILOX_W0);
71    key[1] = key[1].wrapping_add(PHILOX_W1);
72}
73
74/// Full Philox 4x32-10 bijection: 10 rounds.
75const fn philox4x32_10(counter: [u32; 4], key: [u32; 2]) -> [u32; 4] {
76    let mut ctr = counter;
77    let mut k = key;
78    // 10 rounds with key bumps between rounds
79    philox_round(&mut ctr, &k);
80    philox_bump_key(&mut k);
81    philox_round(&mut ctr, &k);
82    philox_bump_key(&mut k);
83    philox_round(&mut ctr, &k);
84    philox_bump_key(&mut k);
85    philox_round(&mut ctr, &k);
86    philox_bump_key(&mut k);
87    philox_round(&mut ctr, &k);
88    philox_bump_key(&mut k);
89    philox_round(&mut ctr, &k);
90    philox_bump_key(&mut k);
91    philox_round(&mut ctr, &k);
92    philox_bump_key(&mut k);
93    philox_round(&mut ctr, &k);
94    philox_bump_key(&mut k);
95    philox_round(&mut ctr, &k);
96    philox_bump_key(&mut k);
97    philox_round(&mut ctr, &k);
98    ctr
99}
100
101impl Philox {
102    /// Increment the 128-bit counter (4 x u32, little-endian).
103    fn increment_counter(&mut self) {
104        for word in &mut self.counter {
105            *word = word.wrapping_add(1);
106            if *word != 0 {
107                return;
108            }
109        }
110    }
111
112    /// Generate the next block of 4 random u32 values.
113    fn generate_block(&mut self) {
114        self.buffer = philox4x32_10(self.counter, self.key);
115        self.increment_counter();
116        self.buf_idx = 0;
117    }
118
119    /// Create a Philox generator with explicit key and starting counter.
120    fn new_with_key(key: [u32; 2], counter: [u32; 4]) -> Self {
121        let mut rng = Self {
122            counter,
123            key,
124            buffer: [0; 4],
125            buf_idx: 4, // Force generation on first call
126        };
127        rng.generate_block();
128        rng
129    }
130}
131
132impl BitGenerator for Philox {
133    fn next_u64(&mut self) -> u64 {
134        if self.buf_idx >= 4 {
135            self.generate_block();
136        }
137        let lo = self.buffer[self.buf_idx] as u64;
138        self.buf_idx += 1;
139        if self.buf_idx >= 4 {
140            self.generate_block();
141        }
142        let hi = self.buffer[self.buf_idx] as u64;
143        self.buf_idx += 1;
144        lo | (hi << 32)
145    }
146
147    fn seed_from_u64(seed: u64) -> Self {
148        let key = [seed as u32, (seed >> 32) as u32];
149        Self::new_with_key(key, [0; 4])
150    }
151
152    fn jump(&mut self) -> Option<()> {
153        // Philox supports jump by advancing the counter by 2^64
154        // (each counter value produces 4 u32 = 2 u64, so this is 2^65 u64 outputs)
155        self.counter[2] = self.counter[2].wrapping_add(1);
156        if self.counter[2] == 0 {
157            self.counter[3] = self.counter[3].wrapping_add(1);
158        }
159        self.buf_idx = 4; // Force re-generation
160        Some(())
161    }
162
163    fn stream(seed: u64, stream_id: u64) -> Option<Self> {
164        // Encode stream_id into the counter's upper 64 bits
165        let key = [seed as u32, (seed >> 32) as u32];
166        let counter = [0u32, 0u32, stream_id as u32, (stream_id >> 32) as u32];
167        Some(Self::new_with_key(key, counter))
168    }
169}
170
171impl Clone for Philox {
172    fn clone(&self) -> Self {
173        Self {
174            counter: self.counter,
175            key: self.key,
176            buffer: self.buffer,
177            buf_idx: self.buf_idx,
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn deterministic_output() {
188        let mut rng1 = Philox::seed_from_u64(42);
189        let mut rng2 = Philox::seed_from_u64(42);
190        for _ in 0..1000 {
191            assert_eq!(rng1.next_u64(), rng2.next_u64());
192        }
193    }
194
195    #[test]
196    fn different_seeds_differ() {
197        let mut rng1 = Philox::seed_from_u64(42);
198        let mut rng2 = Philox::seed_from_u64(43);
199        let mut same = true;
200        for _ in 0..100 {
201            if rng1.next_u64() != rng2.next_u64() {
202                same = false;
203                break;
204            }
205        }
206        assert!(!same);
207    }
208
209    #[test]
210    fn stream_support() {
211        let rng0 = Philox::stream(42, 0);
212        let rng1 = Philox::stream(42, 1);
213        assert!(rng0.is_some());
214        assert!(rng1.is_some());
215
216        let mut rng0 = rng0.unwrap();
217        let mut rng1 = rng1.unwrap();
218        // Different streams should produce different output
219        let v0 = rng0.next_u64();
220        let v1 = rng1.next_u64();
221        assert_ne!(v0, v1);
222    }
223
224    #[test]
225    fn jump_support() {
226        let mut rng = Philox::seed_from_u64(42);
227        assert!(rng.jump().is_some());
228    }
229
230    #[test]
231    fn stream_deterministic() {
232        let mut rng1 = Philox::stream(42, 7).unwrap();
233        let mut rng2 = Philox::stream(42, 7).unwrap();
234        for _ in 0..1000 {
235            assert_eq!(rng1.next_u64(), rng2.next_u64());
236        }
237    }
238}