Skip to main content

ftts_kernels/
sleef.rs

1//! SLEEF's 1-ulp `sinf` and `expf`, ported to safe scalar Rust.
2//!
3// The coefficient literals below are bit-faithful copies of upstream SLEEF's. Truncating them to
4// clippy's taste or substituting `std::f32::consts` values would silently change the arithmetic
5// this module exists to reproduce exactly (AGENTS.md doctrine #8: no silent numerics changes).
6#![allow(clippy::excessive_precision, clippy::approx_constant)]
7//!
8//! # Why this exists
9//!
10//! The pinned CPU-fp32 oracle does not evaluate elementwise transcendentals with the platform's
11//! scalar libm. Its CPU kernels run through a vectorized `Vectorized<float>`, and on AArch64 that
12//! type's `sin` and `exp` dispatch to SLEEF's `Sleef_sinf4_u10` / `Sleef_expf4_u10` — routines that
13//! are accurate to 1 ulp rather than correctly rounded. `codec_snake_bisect` proved by measurement
14//! that this is the whole remaining question at the SnakeBeta seam: every other operation there is
15//! a correctly-rounded f32 `*`, `+` or `/` with no freedom at all, and *widening* `sin` or `exp`
16//! toward the true value moves us further from the oracle, not closer. That direction is only
17//! possible if the target itself is a ~1-ulp routine.
18//!
19//! So this module answers the question the bisect posed: it is the candidate implementation, in
20//! pure portable Rust, that an Accelerate `vvsinf` call could only ever approximate.
21//!
22//! # What is ported, and what is not
23//!
24//! Both routines here are the per-lane arithmetic of SLEEF's AArch64 (`advsimd`) kernels with
25//! `ENABLE_FMA_SP` on, which is what a PyTorch AArch64 build compiles. Every lane of those kernels
26//! is an independent branch-free expression, so evaluating one element at a time is faithful — with
27//! one exception, recorded here rather than hidden: `xsinf_u1` switches its whole vector to a
28//! Payne–Hanek reduction when *any* lane exceeds [`TRIGRANGEMAX2_F`], and that branch is NOT ported.
29//! [`sinf_u10`] falls back to a correctly-rounded f64 evaluation above that threshold and
30//! [`sinf_u10_in_fast_range`] lets a caller assert it never got there.
31//!
32//! Nothing in this module is on the production path. It is selected only by the parity harness,
33//! through [`crate::f32ref::F32Transcendental`].
34
35/// `1 / π`, rounded once to f32 — SLEEF's `M_1_PIf`.
36const M_1_PI_F: f32 = 0.318_309_886_183_790_671_537_767_526_745_028_724_f32;
37/// The three-part Cody–Waite split of π used by the medium-range reduction.
38const PI_A2_F: f32 = 3.141_479_492_187_5;
39const PI_B2_F: f32 = 0.000_113_159_418_106_079_101_56;
40const PI_C2_F: f32 = 1.984_187_258_941_005_893_6e-9;
41/// Above this magnitude SLEEF abandons the Cody–Waite reduction for Payne–Hanek.
42pub const TRIGRANGEMAX2_F: f32 = 125.0;
43
44/// `1 / ln 2`, rounded once to f32 — SLEEF's `R_LN2f`.
45const R_LN2_F: f32 = 1.442_695_040_888_963_407_359_924_681_001_892_137_4_f32;
46/// The two-part split of `ln 2`.
47const L2U_F: f32 = 0.693_145_751_953_125;
48const L2L_F: f32 = 1.428_606_765_330_187_045e-6;
49
50/// A number held as an unevaluated sum of two f32s — SLEEF's `vfloat2`.
51///
52/// The high part carries the value, the low part the rounding error the high part dropped. Every
53/// helper below is one of SLEEF's `df*` primitives under its own name; the FMA forms are used
54/// because AArch64 always has FMA and SLEEF compiles `ENABLE_FMA_SP` there.
55#[derive(Clone, Copy, Debug)]
56struct Df {
57    high: f32,
58    low: f32,
59}
60
61/// `dfadd2_vf2_vf_vf` — Knuth's two-sum, which needs no ordering assumption.
62fn df_two_sum(x: f32, y: f32) -> Df {
63    let high = x + y;
64    let v = high - x;
65    let low = (x - (high - v)) + (y - v);
66    Df { high, low }
67}
68
69/// `dfadd_vf2_vf_vf` — Dekker's fast two-sum, valid only because `|x| >= |y|`.
70fn df_fast_two_sum(x: f32, y: f32) -> Df {
71    let high = x + y;
72    Df {
73        high,
74        low: (x - high) + y,
75    }
76}
77
78/// `dfadd_vf2_vf2_vf` — fast two-sum of a double-float and a float.
79fn df_add_f32(x: Df, y: f32) -> Df {
80    let high = x.high + y;
81    Df {
82        high,
83        low: ((x.high - high) + y) + x.low,
84    }
85}
86
87/// `dfadd_vf2_vf_vf2` — fast two-sum of a float and a double-float.
88fn df_add_to_f32(x: f32, y: Df) -> Df {
89    let high = x + y.high;
90    Df {
91        high,
92        low: ((x - high) + y.high) + y.low,
93    }
94}
95
96/// `dfsqu_vf2_vf2` — the square of a double-float, FMA form.
97fn df_square(x: Df) -> Df {
98    let high = x.high * x.high;
99    Df {
100        high,
101        low: (x.high + x.high).mul_add(x.low, x.high.mul_add(x.high, -high)),
102    }
103}
104
105/// `dfmul_vf2_vf2_vf2` — the product of two double-floats, FMA form.
106fn df_mul(x: Df, y: Df) -> Df {
107    let high = x.high * y.high;
108    let mut low = x.high.mul_add(y.high, -high);
109    low = x.low.mul_add(y.high, low);
110    low = x.high.mul_add(y.low, low);
111    Df { high, low }
112}
113
114/// `dfmul_vf_vf2_vf2` — the same product, rounded down to a single f32, FMA form.
115fn df_mul_to_f32(x: Df, y: Df) -> f32 {
116    x.high
117        .mul_add(y.high, x.low.mul_add(y.high, x.high * y.low))
118}
119
120/// True when `x` takes SLEEF's Cody–Waite branch, the only one ported here.
121#[must_use]
122pub fn sinf_u10_in_fast_range(x: f32) -> bool {
123    x.abs() < TRIGRANGEMAX2_F
124}
125
126/// `Sleef_sinf_u10` — `sin(d)` to within 1 ulp.
127///
128/// Outside [`sinf_u10_in_fast_range`] this returns a correctly-rounded result instead of SLEEF's
129/// Payne–Hanek branch, which is a deliberate documented divergence and not SLEEF's answer.
130#[must_use]
131pub fn sinf_u10(d: f32) -> f32 {
132    if !sinf_u10_in_fast_range(d) {
133        return f64::from(d).sin() as f32;
134    }
135
136    let scaled = (d * M_1_PI_F).round_ties_even();
137    let quadrant = scaled as i32;
138
139    // The reduced argument, carried as a double-float so the three Cody–Waite terms do not lose
140    // the low bits that decide the last ulp of the result.
141    let reduced = scaled.mul_add(-PI_A2_F, d);
142    let mut reduced = df_two_sum(reduced, scaled * -PI_B2_F);
143    reduced = df_add_f32(reduced, scaled * -PI_C2_F);
144
145    let argument = reduced;
146    let square = df_square(reduced);
147
148    let mut poly = 2.608_315_980_978_659_354_150_3e-6_f32;
149    poly = poly.mul_add(square.high, -0.000_198_106_907_191_686_332_225_8);
150    poly = poly.mul_add(square.high, 0.008_333_078_585_565_090_179_443_36);
151
152    let series = df_add_to_f32(
153        1.0,
154        df_mul(
155            df_fast_two_sum(-0.166_666_597_127_914_428_710_938, poly * square.high),
156            square,
157        ),
158    );
159    let result = df_mul_to_f32(argument, series);
160
161    if d == 0.0 {
162        // `sin(-0.0)` is `-0.0`, which the polynomial's sign flip would not produce.
163        return d;
164    }
165    if quadrant & 1 == 0 { result } else { -result }
166}
167
168/// `Sleef_expf_u10` — `exp(d)` to within 1 ulp. SLEEF ships no lower-accuracy `expf`.
169#[must_use]
170pub fn expf_u10(d: f32) -> f32 {
171    let exponent = (d * R_LN2_F).round_ties_even() as i32;
172    let scaled = exponent as f32;
173
174    let mut reduced = scaled.mul_add(-L2U_F, d);
175    reduced = scaled.mul_add(-L2L_F, reduced);
176
177    let mut poly = 0.000_198_527_617_612_853_646_278_381_f32;
178    poly = poly.mul_add(reduced, 0.001_393_043_552_525_341_510_772_71);
179    poly = poly.mul_add(reduced, 0.008_333_360_776_305_198_669_433_59);
180    poly = poly.mul_add(reduced, 0.041_666_485_369_205_474_853_515_6);
181    poly = poly.mul_add(reduced, 0.166_666_671_633_720_397_949_219);
182    poly = poly.mul_add(reduced, 0.5);
183
184    let mantissa = 1.0 + (reduced * reduced).mul_add(poly, reduced);
185    let result = ldexp2(mantissa, exponent);
186
187    if d < -104.0 {
188        return 0.0;
189    }
190    if d > 100.0 {
191        return f32::INFINITY;
192    }
193    result
194}
195
196/// `vldexp2_vf_vf_vi2` — `x * 2^exponent`, split in half so neither factor can overflow.
197fn ldexp2(x: f32, exponent: i32) -> f32 {
198    let half = exponent >> 1;
199    x * pow2i(half) * pow2i(exponent - half)
200}
201
202/// `vpow2i_vf_vi2` — `2^exponent` built directly out of the exponent field.
203fn pow2i(exponent: i32) -> f32 {
204    f32::from_bits(((exponent + 0x7f) << 23) as u32)
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    /// Distance in representable f32 steps between a candidate and the correctly-rounded value.
212    ///
213    /// This is the port's own correctness proof and it is independent of any oracle: a routine
214    /// documented at 1 ulp that is transcribed wrongly does not stay within 1 ulp, it lands
215    /// hundreds or millions of steps away. Passing this says the algorithm is SLEEF's; only the
216    /// parity harness can say whether SLEEF is what the oracle ran.
217    fn ulp_distance(candidate: f32, exact: f64) -> i64 {
218        let rounded = exact as f32;
219        assert!(
220            candidate.is_finite() && rounded.is_finite(),
221            "finite inputs"
222        );
223        let ordered = |value: f32| -> i64 {
224            let bits = i64::from(value.to_bits() as i32);
225            if bits < 0 {
226                i64::from(i32::MIN) - bits
227            } else {
228                bits
229            }
230        };
231        (ordered(candidate) - ordered(rounded)).abs()
232    }
233
234    /// A deterministic even spread of `count` values over `[-limit, limit]`.
235    fn sweep(limit: f32, count: u32) -> impl Iterator<Item = f32> {
236        (0..count).map(move |step| {
237            let unit = f64::from(step) / f64::from(count - 1);
238            ((unit * 2.0 - 1.0) * f64::from(limit)) as f32
239        })
240    }
241
242    #[test]
243    fn sinf_u10_is_within_one_ulp_over_the_cody_waite_range() {
244        let mut worst = 0;
245        for x in sweep(TRIGRANGEMAX2_F * 0.999, 40_001) {
246            worst = worst.max(ulp_distance(sinf_u10(x), f64::from(x).sin()));
247        }
248        assert!(worst <= 1, "sinf_u10 drifted {worst} ulps from correct");
249    }
250
251    #[test]
252    fn sinf_u10_is_within_one_ulp_near_zero_where_the_seam_lives() {
253        let mut worst = 0;
254        for x in sweep(8.0, 60_001) {
255            worst = worst.max(ulp_distance(sinf_u10(x), f64::from(x).sin()));
256        }
257        assert!(worst <= 1, "sinf_u10 drifted {worst} ulps near zero");
258    }
259
260    #[test]
261    fn expf_u10_is_within_one_ulp() {
262        let mut worst = 0;
263        for x in sweep(80.0, 60_001) {
264            worst = worst.max(ulp_distance(expf_u10(x), f64::from(x).exp()));
265        }
266        assert!(worst <= 1, "expf_u10 drifted {worst} ulps from correct");
267    }
268
269    #[test]
270    fn the_exact_cases_stay_exact() {
271        assert_eq!(sinf_u10(0.0), 0.0);
272        assert!(sinf_u10(-0.0).is_sign_negative());
273        assert_eq!(expf_u10(0.0), 1.0);
274        assert_eq!(expf_u10(-200.0), 0.0);
275        assert_eq!(expf_u10(200.0), f32::INFINITY);
276    }
277
278    #[test]
279    fn the_payne_hanek_range_is_flagged_rather_than_claimed() {
280        assert!(sinf_u10_in_fast_range(124.9));
281        assert!(!sinf_u10_in_fast_range(125.0));
282        assert!(!sinf_u10_in_fast_range(f32::NAN));
283    }
284}