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}