treams_core/fpenv.rs
1//! IEEE 754 subnormals for the numerical work, whatever the caller's floating-point mode.
2//! treams-rs extension.
3//!
4//! The kernels assume gradual underflow. Some callers flush subnormals to
5//! zero: XLA sets flush-to-zero and denormals-are-zero on the thread that runs a
6//! computation, also while a `jax.pure_callback` runs,
7//! `torch.set_flush_denormal(True)` sets them on the calling thread, and Linux
8//! threads inherit the mode of their creator. Flushing breaks algorithms that
9//! scale through the subnormal range: faer's complex reciprocal of an LU pivot
10//! `z` with `max(|Re z|, |Im z|) <= 1 < |z|` forms `f64::MIN_POSITIVE / |z|^2`,
11//! so a flushed scalar solve of `1 + 0.1i` returns zero.
12//!
13//! [`ieee`] runs work with subnormals on the calling thread and then restores
14//! the caller's flushing bits; every Python entry point of the bindings runs its
15//! whole body in it. `flushing` does the opposite, for tests. What precedes
16//! the body stays in the caller's mode: `PyO3` reads a `float32` subnormal as
17//! zero on a flushing thread, and `float64` values that the Python layer
18//! computes from subnormal inputs flush before the call into Rust.
19//!
20//! [`ieee`] guards the calling thread only. The workers of the thread pool
21//! ([`crate::threads`]) clear their flushing bits once, as each one starts
22//! (`keep_subnormals_on_worker`), so they keep subnormals whatever the mode of
23//! the thread that builds the pool; Linux threads would otherwise inherit their
24//! creator's mode for good. treams-rs never uses Rayon's global pool, whose
25//! workers would keep the mode of the thread that started it.
26//!
27//! This is the only module that reads or writes the control register: MXCSR
28//! on x86-64 (flush-to-zero, bit 15, and denormals-are-zero, bit 6) and FPCR on
29//! `AArch64` (FZ, bit 24, which XLA sets, and FIZ, bit 0, defined by
30//! `FEAT_AFP`). On other targets both functions only run their work.
31//!
32//! # Soundness
33//!
34//! Rust assumes the default floating-point environment everywhere, and its
35//! standard library documents changing flush-to-zero or denormals-are-zero as
36//! undefined behaviour even when the change is undone (`_mm_setcsr` in
37//! `core::arch::x86_64`): the compiler does not model the control register, so
38//! it may move floating-point arithmetic across the instructions that write it.
39//! A flushing caller has already left that assumption when it calls Rust
40//! code. [`ieee`] cannot formally remove the violation, but it keeps the work
41//! between its two register writes by data dependence rather than by code
42//! layout. Each write is an `asm!` block without `nomem`, `readonly` or `pure`,
43//! which the Rust Reference treats like a call to an unknown function that may
44//! read and write memory, and it takes a pointer operand:
45//!
46//! - The clearing write takes a pointer to the closure, so for the compiler it
47//! may read and replace every captured value. Arithmetic on the inputs can
48//! only start after it, whether they live in registers or in memory.
49//! - The restoring write takes a pointer to the result, so for the compiler it
50//! reads the result and may replace it. The result is complete before the
51//! write, and code after it reads the result back instead of recomputing it.
52//! - Accesses to memory visible outside the function, such as `NumPy` buffers,
53//! and calls with side effects stay on their side of either write.
54//!
55//! This relies on the specified meaning of `asm!` operands, which no
56//! optimization may see through, not on [`core::hint::black_box`], which is
57//! documented as best effort only. Operations whose operands are all
58//! compile-time constants are not anchored: LLVM usually folds them with IEEE
59//! rules, but one that it leaves to run time may run outside the window, so
60//! guarded work must not rely on subnormal results of constant expressions.
61//! `guarded_register_arithmetic_stays_inside` fails at opt-level 1 when either
62//! pointer operand is removed, and `tests/bindings/test_float_environment.py`
63//! compares every scalar binding and ufunc loop on flushing threads bit for bit
64//! with callers that keep subnormals.
65
66// The control register is reachable only through inline assembly, one
67// instruction per block below.
68#![allow(unsafe_code)]
69
70use std::{mem, ptr};
71
72use control::{FLUSH, Word};
73
74#[cfg(target_arch = "x86_64")]
75mod control {
76 use std::arch::asm;
77
78 /// The SSE control and status register, MXCSR, which scalar and vector
79 /// `f64` arithmetic uses on x86-64.
80 pub(super) type Word = u32;
81
82 /// MXCSR flush-to-zero (bit 15) and denormals-are-zero (bit 6).
83 pub(super) const FLUSH: Word = 1 << 15 | 1 << 6;
84
85 /// The flushing bits XLA sets on the thread that runs a computation.
86 pub(super) const XLA: Word = FLUSH;
87
88 /// The current thread's MXCSR.
89 pub(super) fn read() -> Word {
90 let mut word: Word = 0;
91 // SAFETY: `stmxcsr` stores the 4-byte MXCSR at the address of `word`,
92 // which is valid, aligned and writable, and has no other effect. Every
93 // x86-64 processor implements SSE.
94 unsafe {
95 asm!(
96 "stmxcsr dword ptr [{}]",
97 in(reg) &raw mut word,
98 options(nostack, preserves_flags),
99 );
100 }
101 word
102 }
103
104 /// Load `word` into the current thread's MXCSR. For the compiler, the write
105 /// may also read and write the memory `data` points to.
106 ///
107 /// # Safety
108 ///
109 /// `word` differs from the current MXCSR only in [`FLUSH`] bits that the
110 /// processor implements, for example the bits it had before.
111 pub(super) unsafe fn write(word: Word, data: *mut ()) {
112 // SAFETY: `ldmxcsr` loads MXCSR from the address of `word`, which is
113 // valid and aligned. By the safety requirement, no reserved bit is set,
114 // which would fault, and rounding, exception masks and status flags stay
115 // as the thread had them. `data` appears only in a comment, so the
116 // instruction does not access it; without `nomem`, the compiler must
117 // assume it may (see the module's soundness section).
118 unsafe {
119 asm!(
120 "ldmxcsr dword ptr [{word}]",
121 "/* {data} */",
122 word = in(reg) &raw const word,
123 data = in(reg) data,
124 options(nostack, preserves_flags),
125 );
126 }
127 }
128}
129
130#[cfg(target_arch = "aarch64")]
131mod control {
132 use std::arch::asm;
133
134 /// The floating-point control register, FPCR.
135 pub(super) type Word = u64;
136
137 /// FPCR flush-to-zero (FZ, bit 24) and flush-inputs-to-zero (FIZ, bit 0).
138 pub(super) const FLUSH: Word = 1 << 24 | 1;
139
140 /// The flushing bit XLA sets on the thread that runs a computation. FZ is
141 /// part of every FPCR; FIZ is reserved without `FEAT_AFP`.
142 pub(super) const XLA: Word = 1 << 24;
143
144 /// The current thread's FPCR.
145 pub(super) fn read() -> Word {
146 let word: Word;
147 // SAFETY: `mrs` copies FPCR, which is readable at EL0, into a general
148 // register and has no other effect.
149 unsafe {
150 asm!("mrs {}, fpcr", out(reg) word, options(nomem, nostack, preserves_flags));
151 }
152 word
153 }
154
155 /// Write `word` to the current thread's FPCR. For the compiler, the write
156 /// may also read and write the memory `data` points to.
157 ///
158 /// # Safety
159 ///
160 /// `word` differs from the current FPCR only in [`FLUSH`] bits that the
161 /// processor implements, for example the bits it had before.
162 pub(super) unsafe fn write(word: Word, data: *mut ()) {
163 // SAFETY: `msr` writes FPCR, which is writable at EL0. By the safety
164 // requirement, reserved bits keep their values, and rounding, trap enables
165 // and the other controls stay as the thread had them. `data` appears only
166 // in a comment, so the instruction does not access it; without `nomem`,
167 // the compiler must assume it may (see the module's soundness section).
168 unsafe {
169 asm!(
170 "msr fpcr, {word}",
171 "/* {data} */",
172 word = in(reg) word,
173 data = in(reg) data,
174 options(nostack, preserves_flags),
175 );
176 }
177 }
178}
179
180#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
181mod control {
182 /// No control register is managed on this target.
183 pub(super) type Word = u8;
184
185 /// No flushing bits are managed on this target.
186 pub(super) const FLUSH: Word = 0;
187
188 /// No flushing bits are set for tests either.
189 pub(super) const XLA: Word = 0;
190
191 /// No flushing bits are set.
192 pub(super) fn read() -> Word {
193 0
194 }
195
196 /// Nothing to write.
197 ///
198 /// # Safety
199 ///
200 /// Always safe; the requirement matches the other targets.
201 pub(super) unsafe fn write(_: Word, _: *mut ()) {}
202}
203
204/// Set the current thread's flushing bits to `flush & FLUSH`, keeping every
205/// other bit, and return the flushing bits it had. A write, when needed, may
206/// read and write `*data` for the compiler.
207///
208/// # Safety
209///
210/// The processor implements every bit of `flush & FLUSH`, as it does the
211/// flushing bits a thread had before; clearing (`flush == 0`) is always valid.
212unsafe fn swap_flush(flush: Word, data: *mut ()) -> Word {
213 let word = control::read();
214 let wanted = (word & !FLUSH) | (flush & FLUSH);
215 if wanted != word {
216 // SAFETY: `wanted` differs from the register only in FLUSH bits, and
217 // by the safety requirement the processor implements those it sets.
218 unsafe { control::write(wanted, data) };
219 }
220 word & FLUSH
221}
222
223/// Restores the flushing bits a thread had before [`with_flush`] changed them.
224struct Restore {
225 /// The caller's flushing bits.
226 caller: Word,
227 /// Whether the register still holds other bits than `caller`.
228 changed: bool,
229}
230
231impl Restore {
232 /// Write back the caller's bits once. For the compiler, the write may read
233 /// and write `*data`.
234 fn restore(&mut self, data: *mut ()) {
235 if mem::take(&mut self.changed) {
236 // SAFETY: `caller` holds bits read from this thread's register, the
237 // value never leaving `with_flush`, so the processor implements them.
238 unsafe { swap_flush(self.caller, data) };
239 }
240 }
241}
242
243impl Drop for Restore {
244 /// Restore the caller's bits while unwinding from a panic of the work.
245 fn drop(&mut self) {
246 self.restore(ptr::null_mut());
247 }
248}
249
250/// Run `work` with the flushing bits `flush & FLUSH` on the current thread,
251/// then restore the caller's flushing bits, also while unwinding a panic.
252///
253/// The writes take pointers to `work` and to its result, which keeps the work
254/// between them (see the module's soundness section).
255///
256/// # Safety
257///
258/// The processor implements every bit of `flush & FLUSH`.
259#[inline]
260unsafe fn with_flush<T>(flush: Word, work: impl FnOnce() -> T) -> T {
261 let mut work = work;
262 // SAFETY: by this function's safety requirement, the processor implements the
263 // bits set here.
264 let caller = unsafe { swap_flush(flush, (&raw mut work).cast()) };
265 let mut restore = Restore {
266 caller,
267 changed: caller != flush & FLUSH,
268 };
269 let mut value = work();
270 restore.restore((&raw mut value).cast());
271 value
272}
273
274/// Run `work` with IEEE 754 subnormals on the current thread.
275///
276/// Clears flush-to-zero and denormals-are-zero (FZ and FIZ on `AArch64`) for
277/// `work`, then restores the caller's flushing bits exactly, also while
278/// unwinding a panic; other control bits and the status flags keep their
279/// values. A caller that keeps subnormals pays one register read, one that
280/// flushes a read and a write on each side. Work on compile-time constants only
281/// is not anchored (see the module's soundness section).
282#[inline]
283pub fn ieee<T>(work: impl FnOnce() -> T) -> T {
284 // SAFETY: clearing flushing bits is always valid.
285 unsafe { with_flush(0, work) }
286}
287
288/// Clear the current thread's flushing bits for the rest of its life.
289///
290/// The thread pool of [`crate::threads`] calls this as each worker starts, before
291/// the worker runs any work.
292pub(crate) fn keep_subnormals_on_worker() {
293 // SAFETY: clearing flushing bits is always valid. The write returns the
294 // worker to the default environment that Rust assumes, before the worker
295 // runs any floating-point work that the write could be reordered against.
296 unsafe { swap_flush(0, ptr::null_mut()) };
297}
298
299/// Run `work` with the flushing bits XLA sets on the current thread, then
300/// restore the caller's flushing bits, also while unwinding a panic.
301///
302/// For tests only: `work` runs as in a JAX callback, in an environment that
303/// Rust does not support outside [`ieee`] (see the module's soundness section).
304/// Pool workers keep subnormals regardless. On other targets `work` runs
305/// unchanged.
306#[doc(hidden)]
307pub fn flushing<T>(work: impl FnOnce() -> T) -> T {
308 // SAFETY: XLA's flushing bits are implemented: FZ is part of every AArch64
309 // FPCR, and every x86-64 processor has flush-to-zero and denormals-are-zero
310 // (only early 32-bit SSE2 processors lack the latter).
311 unsafe { with_flush(control::XLA, work) }
312}
313
314#[cfg(test)]
315#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
316mod tests {
317 use std::{hint::black_box, panic, process::Command};
318
319 use nalgebra::DMatrix;
320 use proptest::{prelude::*, test_runner::TestCaseError};
321
322 use super::*;
323 use crate::{
324 Complex, Error, linalg,
325 numerics::parallel,
326 test_support::{DEFAULT_CASES, complex},
327 };
328
329 use control::XLA;
330
331 /// Caller modes that flush: flush-to-zero and denormals-are-zero alone, too.
332 #[cfg(target_arch = "x86_64")]
333 const CALLERS: [Word; 3] = [1 << 15, 1 << 6, XLA];
334 /// Caller modes that flush. FIZ alone is left out: a processor without
335 /// `FEAT_AFP` reserves its bit.
336 #[cfg(target_arch = "aarch64")]
337 const CALLERS: [Word; 1] = [XLA];
338
339 /// MXCSR status flags, which arithmetic sets; the control bits are the rest.
340 #[cfg(target_arch = "x86_64")]
341 const STATUS: Word = 0x3f;
342 /// FPCR holds no status flags.
343 #[cfg(target_arch = "aarch64")]
344 const STATUS: Word = 0;
345
346 /// Pivots in the band that faer's reciprocal flushes to zero:
347 /// `max(|Re z|, |Im z|) <= 1 < |z|`.
348 const BAND: [Complex; 5] = [
349 Complex::new(1.0, 0.1),
350 Complex::new(0.9, 0.6),
351 Complex::new(-0.3, -0.99),
352 Complex::new(1.0, 1.0),
353 Complex::new(-1.0, 0.5),
354 ];
355
356 /// Names the child test that its parent test runs in a fresh process.
357 const CHILD: &str = "TREAMS_FPENV_CHILD";
358
359 fn control_bits() -> Word {
360 control::read() & !STATUS
361 }
362
363 /// Whether subnormal results and operands keep their values on this thread:
364 /// flush-to-zero zeroes `MIN_POSITIVE / 2`, denormals-are-zero reads the
365 /// smallest subnormal as zero.
366 fn keeps_subnormals() -> bool {
367 let half = black_box(f64::MIN_POSITIVE) * black_box(0.5);
368 half.to_bits() == 1 << 51 && black_box(f64::from_bits(1)) > black_box(0.0)
369 }
370
371 /// Run `work` on this thread with the flushing bits `flush`, some of XLA's,
372 /// as [`flushing`] does with all of them, then restore the thread's bits.
373 fn flushing_caller<T>(flush: Word, work: impl FnOnce() -> T) -> T {
374 assert_eq!(flush & !XLA, 0, "{flush:#x} sets only XLA's flushing bits");
375 // SAFETY: XLA's flushing bits are implemented, as `flushing` states.
376 unsafe { with_flush(flush, work) }
377 }
378
379 /// `x * two * half`, which is `x` for `two * half == 1` when subnormals are
380 /// kept. For the subnormal `x = MIN_POSITIVE / 2`, denormals-are-zero reads
381 /// `x` as zero and flush-to-zero zeroes the final product.
382 fn scaled(x: f64, two: f64, half: f64) -> f64 {
383 x * two * half
384 }
385
386 /// `1 / z` by an LU solve of the `1 x 1` system `z x = 1`.
387 fn reciprocal(z: Complex) -> crate::Result<Complex> {
388 let one = DMatrix::from_element(1, 1, Complex::ONE);
389 Ok(linalg::solve(&DMatrix::from_element(1, 1, z), one)?.value()[(0, 0)])
390 }
391
392 fn bits(values: &[Complex]) -> Vec<[u64; 2]> {
393 values
394 .iter()
395 .map(|z| [z.re.to_bits(), z.im.to_bits()])
396 .collect()
397 }
398
399 #[test]
400 fn the_guard_keeps_subnormals_and_restores_the_caller_mode() {
401 assert!(keeps_subnormals());
402 for flush in CALLERS {
403 let (flushed, set, inside, after) = flushing_caller(flush, || {
404 let set = control_bits();
405 let inside = ieee(|| (keeps_subnormals(), control::read() & FLUSH));
406 (!keeps_subnormals(), set, inside, control_bits())
407 });
408 assert!(flushed, "{flush:#x} must flush subnormal arithmetic");
409 assert_eq!(set & FLUSH, flush);
410 assert_eq!(inside, (true, 0), "{flush:#x}");
411 assert_eq!(after, set, "{flush:#x}");
412 }
413 // No flushing bits: the guard leaves the register as it is.
414 let before = control_bits();
415 let inside = ieee(control_bits);
416 assert_eq!((inside, control_bits()), (before, before));
417 }
418
419 #[test]
420 fn flushing_sets_the_bits_of_xla_and_restores_the_caller_mode() {
421 let before = control_bits();
422 let (set, flushed, guarded, nested) = flushing(|| {
423 let nested = flushing(control_bits);
424 (
425 control_bits(),
426 !keeps_subnormals(),
427 ieee(keeps_subnormals),
428 nested,
429 )
430 });
431 assert_eq!(set, (before & !FLUSH) | XLA);
432 assert!(flushed && guarded);
433 assert_eq!(nested, set, "a nested call keeps the flushing bits");
434 assert_eq!(control_bits(), before);
435 let unwound = panic::catch_unwind(|| {
436 flushing(|| panic::resume_unwind(Box::new("unwinding through flushing")))
437 });
438 assert!(unwound.is_err());
439 assert_eq!(control_bits(), before);
440 }
441
442 /// `scaled` in `ieee`, like a binding that takes and returns register values.
443 #[inline(never)]
444 fn guarded_scaled(x: f64, two: f64, half: f64) -> f64 {
445 ieee(|| scaled(x, two, half))
446 }
447
448 /// `guarded_scaled` in a loop, which makes `scaled` loop-invariant.
449 #[inline(never)]
450 fn guarded_scaled_loop(x: f64, two: f64, half: f64, values: &mut [f64]) {
451 for value in values {
452 *value = ieee(|| scaled(x, two, half));
453 }
454 }
455
456 /// Arithmetic on register values stays between the two control-register writes
457 /// of `ieee`, where the optimizer could otherwise sink it past the restoring write
458 /// (`guarded_scaled`) or hoist it out of a loop (`guarded_scaled_loop`).
459 #[test]
460 fn guarded_register_arithmetic_stays_inside() {
461 let (x, two, half) = (f64::MIN_POSITIVE / 2.0, black_box(2.0), black_box(0.5));
462 let x = black_box(x);
463 for flush in CALLERS {
464 let (flushed, single, looped) = flushing_caller(flush, || {
465 let mut looped = vec![0.0; black_box(4)];
466 guarded_scaled_loop(x, two, half, &mut looped);
467 let single = guarded_scaled(x, two, half);
468 (scaled(black_box(x), two, half), single, looped)
469 });
470 assert_eq!(flushed.to_bits(), 0, "{flush:#x} must flush `scaled`");
471 assert_eq!(single.to_bits(), x.to_bits(), "{flush:#x}: {single:e}");
472 assert!(
473 looped.iter().all(|value| value.to_bits() == x.to_bits()),
474 "{flush:#x}: {looped:?}"
475 );
476 }
477 }
478
479 #[test]
480 fn the_guard_restores_the_caller_mode_after_errors_panics_and_nesting() {
481 flushing_caller(XLA, || {
482 let caller = control_bits();
483 assert!(ieee(|| Err::<(), _>(Error::Singular)).is_err());
484 assert_eq!(control_bits(), caller);
485 let unwound = panic::catch_unwind(|| {
486 ieee(|| {
487 assert!(keeps_subnormals());
488 panic::resume_unwind(Box::new("unwinding through the guard"));
489 })
490 });
491 assert!(unwound.is_err());
492 assert_eq!(control_bits(), caller);
493 ieee(|| {
494 ieee(|| ());
495 assert!(keeps_subnormals(), "an inner guard restores the outer one");
496 });
497 assert_eq!(control_bits(), caller);
498 });
499 }
500
501 #[test]
502 fn lu_pivots_in_the_flushed_band_solve_exactly_under_the_guard() -> crate::Result<()> {
503 for z in BAND {
504 let expected = reciprocal(z)?;
505 assert!((expected - z.inv()).norm() <= 2.0 * f64::EPSILON * expected.norm());
506 let (flushed, guarded) =
507 flushing_caller(XLA, || (reciprocal(z), ieee(|| reciprocal(z))));
508 // The defect the guard removes: the flushed reciprocal is zero.
509 assert_eq!(flushed?, Complex::ZERO, "{z}");
510 assert_eq!(bits(&[guarded?]), bits(&[expected]), "{z}");
511 }
512 Ok(())
513 }
514
515 /// Solves and their adjoints on a guarded flushing thread are bitwise those
516 /// of a thread that keeps subnormals.
517 fn check_guarded_solve(
518 a: &DMatrix<Complex>,
519 b: &DMatrix<Complex>,
520 ) -> Result<(), TestCaseError> {
521 let solve = || -> crate::Result<_> {
522 let residual = linalg::solve(a, b.clone())?;
523 let value = residual.value().clone();
524 let gradient = residual.pullback(value.clone())?;
525 Ok([value, gradient.operator, gradient.rhs])
526 };
527 let expected = solve().map_err(|e| TestCaseError::reject(e.to_string()))?;
528 let actual =
529 flushing_caller(XLA, || ieee(solve)).map_err(|e| TestCaseError::fail(e.to_string()))?;
530 for (actual, expected) in actual.iter().zip(&expected) {
531 prop_assert_eq!(bits(actual.as_slice()), bits(expected.as_slice()));
532 }
533 Ok(())
534 }
535
536 proptest! {
537 #![proptest_config(ProptestConfig::with_cases(DEFAULT_CASES))]
538
539 /// Diagonals in the flushed band, which partial pivoting keeps as
540 /// pivots up to the small off-diagonal updates.
541 #[test]
542 fn guarded_solves_on_flushing_threads_match_ieee_threads(
543 (a, b) in (1_usize..=8, 1_usize..=3).prop_flat_map(|(n, k)| (
544 (
545 prop::collection::vec(complex(0.1), n * n),
546 prop::collection::vec(
547 complex(1.0).prop_filter("flushed band", |z| z.norm_sqr() > 1.0),
548 n,
549 ),
550 )
551 .prop_map(move |(entries, diagonal)| {
552 let mut a = DMatrix::from_vec(n, n, entries);
553 a.set_diagonal(&diagonal.into());
554 a
555 }),
556 crate::test_support::complex_matrix(n, k, 1.0),
557 )),
558 ) {
559 check_guarded_solve(&a, &b)?;
560 }
561 }
562
563 /// Scalar solves in the flushed band on the pool's workers.
564 fn reciprocals_on_workers() -> crate::Result<Vec<Complex>> {
565 parallel::try_map(64, true, |i| reciprocal(BAND[i % BAND.len()]))
566 }
567
568 fn reciprocals_on_this_thread() -> crate::Result<Vec<Complex>> {
569 (0..64).map(|i| reciprocal(BAND[i % BAND.len()])).collect()
570 }
571
572 /// Whether this process runs `name` as the child of
573 /// `fresh_pools_keep_subnormals_whatever_their_first_caller`, which is its
574 /// only purpose.
575 fn is_child(name: &str) -> bool {
576 std::env::var(CHILD).is_ok_and(|child| child == name)
577 }
578
579 fn check_child(name: &str, threads: &str) {
580 let output = Command::new(std::env::current_exe().unwrap())
581 .args([&format!("fpenv::tests::{name}"), "--exact", "--ignored"])
582 .args(["--test-threads=1"])
583 .env(CHILD, name)
584 .env("TREAMS_RS_NUM_THREADS", threads)
585 .output()
586 .unwrap();
587 let stdout = String::from_utf8_lossy(&output.stdout);
588 assert!(
589 output.status.success() && stdout.contains(" 1 passed"),
590 "{name}, {threads} threads\n{stdout}\n{}",
591 String::from_utf8_lossy(&output.stderr),
592 );
593 }
594
595 #[test]
596 fn fresh_pools_keep_subnormals_whatever_their_first_caller() {
597 for threads in ["1", "4"] {
598 for name in [
599 "pool_started_under_a_guard",
600 "pool_started_without_a_guard",
601 "helpers_keep_subnormals_and_restore_the_caller",
602 ] {
603 check_child(name, threads);
604 }
605 }
606 for name in [
607 "map_keeps_subnormals_when_pool_is_unavailable",
608 "fill_keeps_subnormals_when_pool_is_unavailable",
609 "helpers_guard_existing_global_workers_when_pool_is_unavailable",
610 ] {
611 check_child(name, "4");
612 }
613 }
614
615 /// The unavailable owned pool lets Rayon initialize its fallback workers.
616 /// Each helper must guard that first call and restore the flushing caller.
617 fn check_unavailable_pool(name: &str, map: bool) -> crate::Result<()> {
618 if !is_child(name) {
619 return Ok(());
620 }
621 assert_eq!(crate::threads::current_num_threads(), 4);
622 let expected = bits(&reciprocals_on_this_thread()?);
623 let (values, restored) = crate::threads::with_unavailable_pool(|| {
624 flushing(|| {
625 let caller = control_bits();
626 let values = if map {
627 parallel::try_map(64, true, |i| reciprocal(BAND[i % BAND.len()]))
628 } else {
629 let mut values = vec![Complex::default(); 64];
630 parallel::try_fill_chunks(&mut values, 1, true, |i, chunk| {
631 chunk[0] = reciprocal(BAND[i % BAND.len()])?;
632 Ok::<_, Error>(())
633 })
634 .map(|()| values)
635 };
636 (values, control_bits() == caller)
637 })
638 });
639 assert!(restored, "the fallback restores the caller's mode");
640 assert!(crate::threads::info().pool_threads.is_none());
641 assert_eq!(bits(&values?), expected);
642 Ok(())
643 }
644
645 #[test]
646 #[ignore = "run in a fresh process by its parent test"]
647 fn map_keeps_subnormals_when_pool_is_unavailable() -> crate::Result<()> {
648 check_unavailable_pool("map_keeps_subnormals_when_pool_is_unavailable", true)
649 }
650
651 #[test]
652 #[ignore = "run in a fresh process by its parent test"]
653 fn fill_keeps_subnormals_when_pool_is_unavailable() -> crate::Result<()> {
654 check_unavailable_pool("fill_keeps_subnormals_when_pool_is_unavailable", false)
655 }
656
657 #[test]
658 #[ignore = "run in a fresh process by its parent test"]
659 fn helpers_guard_existing_global_workers_when_pool_is_unavailable() -> crate::Result<()> {
660 if !is_child("helpers_guard_existing_global_workers_when_pool_is_unavailable") {
661 return Ok(());
662 }
663 rayon::ThreadPoolBuilder::new()
664 .num_threads(4)
665 .spawn_handler(|thread| {
666 std::thread::Builder::new().spawn(|| flushing(|| thread.run()))?;
667 Ok(())
668 })
669 .build_global()
670 .unwrap();
671 let modes = || {
672 crate::threads::with_unavailable_pool(|| {
673 crate::threads::install(|| rayon::broadcast(|_| keeps_subnormals()))
674 })
675 };
676 assert!(modes().iter().all(|&kept| !kept));
677 let expected = bits(&reciprocals_on_this_thread()?);
678 crate::threads::with_unavailable_pool(|| {
679 let (mapped, filled, folded, restored) = flushing(|| {
680 let caller = control_bits();
681 let mapped = parallel::try_map(64, true, |i| reciprocal(BAND[i % BAND.len()]));
682 let mut filled = vec![Complex::default(); 64];
683 let fill = parallel::try_fill_chunks(&mut filled, 1, true, |i, chunk| {
684 chunk[0] = reciprocal(BAND[i % BAND.len()])?;
685 Ok::<_, Error>(())
686 });
687 let folded = parallel::try_fold_ordered(
688 (0..64).collect(),
689 true,
690 Vec::new,
691 |mut out, _, i| {
692 out.push(reciprocal(BAND[i % BAND.len()])?);
693 Ok::<_, Error>(out)
694 },
695 |mut left, right| {
696 assert!(keeps_subnormals());
697 left.extend(right);
698 left
699 },
700 );
701 (
702 mapped,
703 fill.map(|()| filled),
704 folded,
705 control_bits() == caller,
706 )
707 });
708 assert!(restored);
709 assert_eq!(bits(&mapped?), expected);
710 assert_eq!(bits(&filled?), expected);
711 assert_eq!(bits(&folded?), expected);
712 Ok::<_, Error>(())
713 })?;
714 assert!(modes().iter().all(|&kept| !kept));
715 Ok(())
716 }
717
718 /// Exercise maps, chunk fills, ordered combines and nested calls on both
719 /// the caller and the pool. Only integer bit comparisons run while flushed.
720 #[test]
721 #[ignore = "run in a fresh process by its parent test"]
722 fn helpers_keep_subnormals_and_restore_the_caller() -> crate::Result<()> {
723 if !is_child("helpers_keep_subnormals_and_restore_the_caller") {
724 return Ok(());
725 }
726 let expected = bits(&reciprocals_on_this_thread()?);
727 for parallel in [false, true] {
728 let (mapped, filled, folded, error, restored) = flushing(|| {
729 let mapped = parallel::try_map(64, parallel, |i| {
730 let nested = parallel::try_map(1, false, |_| reciprocal(BAND[i % BAND.len()]))?;
731 Ok::<_, Error>(nested[0])
732 });
733 let mut filled = vec![Complex::default(); 64];
734 let fill = parallel::try_fill_chunks(&mut filled, 1, parallel, |i, chunk| {
735 chunk[0] = reciprocal(BAND[i % BAND.len()])?;
736 Ok::<_, Error>(())
737 });
738 let folded = parallel::try_fold_ordered(
739 (0..64).collect(),
740 parallel,
741 Vec::new,
742 |mut out, _, i| {
743 out.push(reciprocal(BAND[i % BAND.len()])?);
744 Ok::<_, Error>(out)
745 },
746 |mut left, right| {
747 assert!(keeps_subnormals(), "combine runs on the guarded caller");
748 left.extend(right);
749 left
750 },
751 );
752 let error = parallel::try_map(1, parallel, |_| {
753 assert!(keeps_subnormals());
754 Err::<(), _>("recorded failure")
755 });
756 (
757 mapped,
758 fill.map(|()| filled),
759 folded,
760 error,
761 !keeps_subnormals(),
762 )
763 });
764 assert!(restored);
765 assert_eq!(bits(&mapped?), expected);
766 assert_eq!(bits(&filled?), expected);
767 assert_eq!(bits(&folded?), expected);
768 assert_eq!(error, Err("recorded failure"));
769 }
770 Ok(())
771 }
772
773 /// The first call into Rust that uses Rayon, made inside a JAX callback: the
774 /// pool starts under the guard of a flushing thread, and its workers keep
775 /// subnormals after the caller flushes again.
776 #[test]
777 #[ignore = "run in a fresh process by its parent test"]
778 fn pool_started_under_a_guard() -> crate::Result<()> {
779 if !is_child("pool_started_under_a_guard") {
780 return Ok(());
781 }
782 let (started, restored, workers, values) = flushing(|| {
783 let started = ieee(reciprocals_on_workers);
784 let restored = !keeps_subnormals();
785 let workers = crate::threads::install(|| rayon::broadcast(|_| keeps_subnormals()));
786 (started, restored, workers, reciprocals_on_workers())
787 });
788 let expected = bits(&reciprocals_on_this_thread()?);
789 assert!(restored, "the guard restores the caller's mode");
790 assert!(workers.iter().all(|&kept| kept), "{workers:?}");
791 assert_eq!(bits(&started?), expected);
792 assert_eq!(bits(&values?), expected);
793 Ok(())
794 }
795
796 /// Rust code that starts the pool on a flushing thread without a guard still
797 /// gets workers that keep subnormals, and neither path starts Rayon's global
798 /// pool.
799 #[test]
800 #[ignore = "run in a fresh process by its parent test"]
801 fn pool_started_without_a_guard() -> crate::Result<()> {
802 if !is_child("pool_started_without_a_guard") {
803 return Ok(());
804 }
805 ieee(|| ());
806 let (workers, values) = flushing(|| {
807 let workers = crate::threads::install(|| rayon::broadcast(|_| keeps_subnormals()));
808 (workers, ieee(reciprocals_on_workers))
809 });
810 assert!(workers.iter().all(|&kept| kept), "{workers:?}");
811 assert_eq!(bits(&values?), bits(&reciprocals_on_this_thread()?));
812 let built = rayon::ThreadPoolBuilder::new().build_global();
813 assert!(built.is_ok(), "the global pool stays unused");
814 Ok(())
815 }
816}