ftz 0.0.1
Fast, reproducible floating-point arithmetic
Loading...
Searching...
No Matches
ftz.controls.ccm
1module;
2#include <cstdint>
3#include <array>
4#include <thread>
5#include <cassert>
6#include <stdexcept>
7#include <cstdio>
8#include <exception>
9#if defined(_M_X64) || defined(__x86_64__)
10#include <xmmintrin.h>
11#endif
12export module ftz.controls;
13
14export namespace ftz {
15 // Native CPU environment only. These controls do not configure GPUs or Wasm.
25 enum class native_fp32_mode { gradual, flush };
29 std::uint64_t control, status;
31 [[nodiscard]] friend bool operator==(native_fp_state const &, native_fp_state const &) = default;
32 };
33
36 [[nodiscard]] bool native_fp32_environment_available() noexcept;
40 [[nodiscard]] native_fp_state read_native_fp_state() noexcept;
41
42 // Application-owned numerical thread entry: set RNE, masked exceptions,
43 // clear status flags and select denormal handling. No qualification, owner
44 // lookup, allocation or restoration. Invalid/unavailable modes leave state
45 // untouched and return false. ISA availability is a separate startup check.
49 [[nodiscard]] bool set_native_fp32_mode(native_fp32_mode mode) noexcept;
50
51 // Borrowed numerical region: save and restore the caller's complete state.
52 // Construction establishes RNE, masked exceptions and clear status flags.
53 // Native FTZ is not defined here as post-round software bit normalization.
61 struct [[nodiscard]] native_fp32_scope {
62 private:
63 native_fp_state previous_, requested_;
64 std::thread::id owner_;
65 public:
69 ~native_fp32_scope() noexcept;
79 [[nodiscard]] native_fp_state previous() const noexcept { return previous_; }
81 [[nodiscard]] native_fp_state requested() const noexcept { return requested_; }
83 [[nodiscard]] bool controls_match() const noexcept;
84
85 // Restore the environment agreed with the caller while invoking external
86 // code; restore this numerical region's current state on return/unwind.
90 struct [[nodiscard]] external_scope {
91 private:
92 native_fp_state previous_;
93 std::thread::id owner_;
94 public:
96 explicit external_scope(native_fp32_scope const & region) noexcept;
98 ~external_scope() noexcept;
107 };
108 };
109}
110
111namespace ftz {
113#if defined(_M_X64) || defined(__x86_64__) || defined(__aarch64__) || defined(__arm64__)
114 return true;
115#else
116 return false;
117#endif
118 }
120#if defined(_M_X64) || defined(__x86_64__)
121 auto word = _mm_getcsr();
122 return {word & ~63u, word & 63u};
123#elif defined(__aarch64__) || defined(__arm64__)
124 native_fp_state state;
125 __asm__ volatile("mrs %0, fpcr" : "=r"(state.control) : : "memory");
126 __asm__ volatile("mrs %0, fpsr" : "=r"(state.status) : : "memory");
127 return state;
128#else
129 return {};
130#endif
131 }
132 namespace detail::fp32_environment {
133 [[noreturn]] void failure(char const * reason, bool invalid = false) {
134#if defined(__cpp_exceptions) || defined(_CPPUNWIND)
135 if (invalid) throw std::invalid_argument(reason);
136 throw std::runtime_error(reason);
137#else
138 (void)invalid;
139 std::fputs(reason, stderr);
140 std::fputc('\n', stderr);
141 std::terminate();
142#endif
143 }
144 void write_native_fp_state(native_fp_state state) noexcept {
145#if defined(_M_X64) || defined(__x86_64__)
146 _mm_setcsr(static_cast<unsigned int>(state.control | state.status));
147#elif defined(__aarch64__) || defined(__arm64__)
148 __asm__ volatile("msr fpcr, %0\n\tisb" : : "r"(state.control) : "memory");
149 __asm__ volatile("msr fpsr, %0" : : "r"(state.status) : "memory");
150#else
151 (void)state;
152#endif
153 }
154 native_fp_state requested_state(native_fp_state saved, native_fp32_mode mode) {
156 failure("Native FP32 environment unavailable; use software arithmetic policy");
157 if (mode != native_fp32_mode::gradual && mode != native_fp32_mode::flush)
158 failure("Invalid native FP32 mode", true);
159#if defined(_M_X64) || defined(__x86_64__)
160 // The admitted x86 CPU environment supports MXCSR denormal controls.
161 auto control = (saved.control & ~std::uint64_t(0xe040)) | 0x1f80u;
162 if (mode == native_fp32_mode::flush) control |= 0x8040u;
163 return {control, 0};
164#elif defined(__aarch64__) || defined(__arm64__)
165 // Standard ARM handling: AH/FIZ/NEP clear, RNE, FP traps disabled.
166 // Preserve unrelated fields, including FP16 mode and default-NaN policy.
167 constexpr std::uint64_t clear = 7u | (0x1full << 8) | (1ull << 15) |
168 (3ull << 22) | (1ull << 24);
169 auto control = saved.control & ~clear;
170 if (mode == native_fp32_mode::flush) control |= 1ull << 24;
171 return {control, 0};
172#else
173 return saved;
174#endif
175 }
176 }
179 (mode != native_fp32_mode::gradual && mode != native_fp32_mode::flush)) return false;
180 auto requested = detail::fp32_environment::requested_state(read_native_fp_state(), mode);
181 detail::fp32_environment::write_native_fp_state(requested);
182 return true;
183 }
185 : previous_(read_native_fp_state()), requested_(detail::fp32_environment::requested_state(previous_, mode)),
186 owner_(std::this_thread::get_id()) {
187 detail::fp32_environment::write_native_fp_state(requested_);
188 if (read_native_fp_state() != requested_) {
189 detail::fp32_environment::write_native_fp_state(previous_);
190 detail::fp32_environment::failure("Native FP32 environment did not accept requested controls");
191 }
192 }
194 assert(owner_ == std::this_thread::get_id());
195 detail::fp32_environment::write_native_fp_state(previous_);
196 }
197 bool native_fp32_scope::controls_match() const noexcept {
198 return owner_ == std::this_thread::get_id() && read_native_fp_state().control == requested_.control;
199 }
201 : previous_(read_native_fp_state()), owner_(std::this_thread::get_id()) {
202 assert(region.owner_ == owner_);
203 detail::fp32_environment::write_native_fp_state(region.previous_);
204 }
206 assert(owner_ == std::this_thread::get_id());
207 detail::fp32_environment::write_native_fp_state(previous_);
208 }
209}
210
211// SPDX-FileCopyrightText: 2026 Edward Kmett <ekmett@gmail.com>
212// SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0
213// CPU rounding and denormal state ownership, independent of every SIMD profile.
214
215// Admission data and decisions belong to this baseline module. Only the
216// supplied startup evaluator is compiled with a numerical profile's options.
217export namespace ftz {
220 enum class ftz32_cpu_failure {
221 none, environment_unavailable, normal_arithmetic, core_contract, hardware_ftz
222 };
223
224 // A startup observation for this compiled profile and the calling thread.
225 // It neither changes controls nor configures another thread or any GPU.
230 ftz32_cpu_failure failure = ftz32_cpu_failure::environment_unavailable;
231 bool normal_rne = false, fused_fma = false, separate_rounding = false;
232 bool signed_input_ftz = false, signed_output_ftz = false, core_exact = false;
233 bool explicit_core_exact = false, controls_stable = false;
234 ftz::native_fp_state before{}, after{};
236 [[nodiscard]] bool admitted() const noexcept { return failure == ftz32_cpu_failure::none; }
238 [[nodiscard]] bool explicit_compatible() const noexcept {
239 return normal_rne && fused_fma && separate_rounding && explicit_core_exact && controls_stable;
240 }
241 };
242
243}
244export namespace ftz::detail {
245 struct ftz32_cpu_witness { unsigned int operation, a, b, c, expected; };
246 enum class ftz32_cpu_path { raw, compiled_core, explicit_core };
247 using ftz32_cpu_evaluator = unsigned int (*)(ftz32_cpu_witness, ftz32_cpu_path);
248 [[nodiscard]] ftz32_cpu_admission ftz32_cpu_probe(ftz32_cpu_evaluator evaluate, bool hardware_ftz);
249}
250namespace ftz::detail {
251
252 // Operations: 0 add, 1 multiply, 2 fused multiply-add, 3 separately rounded
253 // multiply/add, 4 canonical word import. All literals are binary32 words.
254 inline constexpr std::array ftz32_normal_witnesses {
255 ftz32_cpu_witness{0,0x3f800000u,0x33800000u,0u,0x3f800000u},
256 ftz32_cpu_witness{0,0xbf800000u,0xb3800000u,0u,0xbf800000u},
257 ftz32_cpu_witness{0,0x3f800000u,0x34400000u,0u,0x3f800002u},
258 ftz32_cpu_witness{0,0xbf800000u,0xb4400000u,0u,0xbf800002u},
259 ftz32_cpu_witness{1,0x3f800001u,0x3f800001u,0u,0x3f800002u},
260 ftz32_cpu_witness{1,0xbf800001u,0x3f800001u,0u,0xbf800002u}
261 };
262 inline constexpr std::array ftz32_fusion_witnesses {
263 ftz32_cpu_witness{2,0x3f800001u,0x3f7ffffeu,0xbf800000u,0xa8800000u},
264 ftz32_cpu_witness{2,0xbf800001u,0x3f7ffffeu,0x3f800000u,0x28800000u},
265 ftz32_cpu_witness{3,0x3f800001u,0x3f7ffffeu,0xbf800000u,0u}
266 };
267 inline constexpr std::array ftz32_flush_witnesses {
268 ftz32_cpu_witness{1,1u,0x7e800000u,0u,0u},
269 ftz32_cpu_witness{1,0x80000001u,0x7e800000u,0u,0x80000000u},
270 ftz32_cpu_witness{2,1u,0x7e800000u,0u,0u},
271 ftz32_cpu_witness{2,0x80000001u,0x7e800000u,0x80000000u,0x80000000u},
272 ftz32_cpu_witness{1,0x00800000u,0x3f000000u,0u,0u},
273 ftz32_cpu_witness{1,0x80800000u,0x3f000000u,0u,0x80000000u},
274 ftz32_cpu_witness{2,0x00800000u,0x3f000000u,0u,0u},
275 ftz32_cpu_witness{2,0x80800000u,0x3f000000u,0x80000000u,0x80000000u},
276 ftz32_cpu_witness{0,0x00800000u,0x80800001u,0u,0x80000000u},
277 ftz32_cpu_witness{0,0x80800000u,0x00800001u,0u,0u}
278 };
279 inline constexpr std::array ftz32_core_witnesses {
280 ftz32_cpu_witness{1,0x00800000u,0x3f7fffffu,0u,0x00800000u},
281 ftz32_cpu_witness{1,0x80800000u,0x3f7fffffu,0u,0x80800000u},
282 ftz32_cpu_witness{1,0x20000001u,0x1ffffffeu,0u,0x00800000u},
283 ftz32_cpu_witness{1,0xa0000001u,0x1ffffffeu,0u,0x80800000u},
284 ftz32_cpu_witness{2,0x00800000u,0x3f7fffffu,0u,0x00800000u},
285 ftz32_cpu_witness{2,0x80800000u,0x3f7fffffu,0x80000000u,0x80800000u},
286 ftz32_cpu_witness{2,0x20000001u,0x1ffffffeu,0u,0x00800000u},
287 ftz32_cpu_witness{2,0xa0000001u,0x1ffffffeu,0x80000000u,0x80800000u},
288 ftz32_cpu_witness{1,0x00800000u,0x3f7ffffeu,0u,0u},
289 ftz32_cpu_witness{1,0x80800000u,0x3f7ffffeu,0u,0x80000000u},
290 ftz32_cpu_witness{0,0x00800000u,0x80800001u,0u,0x80000000u},
291 ftz32_cpu_witness{0,0x80800000u,0x00800001u,0u,0u},
292 ftz32_cpu_witness{3,0x00800000u,0x3f800000u,0x80800001u,0x80000000u},
293 ftz32_cpu_witness{3,0x80800000u,0x3f800000u,0x00800001u,0u},
294 ftz32_cpu_witness{4,1u,0u,0u,0u},
295 ftz32_cpu_witness{4,0x807fffffu,0u,0u,0x80000000u},
296 ftz32_cpu_witness{4,0xff800001u,0u,0u,0x7fc00000u},
297 ftz32_cpu_witness{4,0x00800000u,0u,0u,0x00800000u}
298 };
299 struct ftz32_cpu_observation {
300 bool available = false, exception_masks = false, rounding_controls = false;
301 ftz::native_fp_state before{}, after{};
302 std::array<unsigned int, ftz32_normal_witnesses.size()> normal{};
303 std::array<unsigned int, ftz32_fusion_witnesses.size()> fusion{};
304 std::array<unsigned int, ftz32_flush_witnesses.size()> flush{};
305 std::array<unsigned int, ftz32_core_witnesses.size()> core{}, explicit_core{};
306 };
307
308 template<std::size_t N> inline bool ftz32_cpu_matches(
309 std::array<unsigned int,N> const & actual, std::array<ftz32_cpu_witness,N> const & rows,
310 std::size_t first = 0, std::size_t end = N) noexcept {
311 for (auto i = first; i < end; ++i) {
312 unsigned int expected = rows[i].expected;
313 // NaN sign and payload are outside the public value contract. Every
314 // non-NaN bit, including the sign of zero, remains exact.
315 if (actual[i] != expected && !((actual[i] & 0x7fffffffu) > 0x7f800000u && (expected & 0x7fffffffu) > 0x7f800000u)) return false;
316 }
317 return true;
318 }
319 ftz32_cpu_admission ftz32_cpu_classify(ftz32_cpu_observation const & o, bool hardware_ftz) noexcept {
320 ftz32_cpu_admission r;
321 r.before=o.before; r.after=o.after;
322 if (!o.available) return r;
323 r.controls_stable=o.before.control==o.after.control;
324 r.normal_rne=o.exception_masks && o.rounding_controls && ftz32_cpu_matches(o.normal,ftz32_normal_witnesses);
325 r.fused_fma=ftz32_cpu_matches(o.fusion,ftz32_fusion_witnesses,0,2);
326 r.separate_rounding=ftz32_cpu_matches(o.fusion,ftz32_fusion_witnesses,2,3);
327 r.signed_input_ftz=ftz32_cpu_matches(o.flush,ftz32_flush_witnesses,0,4);
328 r.signed_output_ftz=ftz32_cpu_matches(o.flush,ftz32_flush_witnesses,4);
329 r.core_exact=ftz32_cpu_matches(o.core,ftz32_core_witnesses);
330 r.explicit_core_exact=ftz32_cpu_matches(o.explicit_core,ftz32_core_witnesses);
331 bool hardware_missing = false;
332 if (hardware_ftz)
333 hardware_missing = !r.signed_input_ftz || !r.signed_output_ftz;
334 if (!r.normal_rne || !r.fused_fma || !r.separate_rounding || !r.controls_stable)
335 r.failure=ftz32_cpu_failure::normal_arithmetic;
336 else if (hardware_missing)
337 r.failure=ftz32_cpu_failure::hardware_ftz;
338 else if (!r.core_exact) r.failure=ftz32_cpu_failure::core_contract;
339 else r.failure=ftz32_cpu_failure::none;
340 return r;
341 }
342 ftz32_cpu_observation ftz32_cpu_observe(ftz32_cpu_evaluator evaluate) {
343 ftz32_cpu_observation o;
345 o.before=ftz::read_native_fp_state();
346#if defined(_M_X64) || defined(__x86_64__)
347 o.exception_masks=(o.before.control & 0x1f80u)==0x1f80u;
348 o.rounding_controls=(o.before.control & 0x6000u)==0u;
349#elif defined(__aarch64__) || defined(__arm64__)
350 o.exception_masks=(o.before.control & ((0x1full<<8)|(1ull<<15)))==0u;
351 o.rounding_controls=(o.before.control & ((3ull<<22)|7u))==0u;
352#endif
353 // Do not risk a hardware FP exception when the caller did not first
354 // establish its numerical-thread scope. Wrong rounding is safe to test.
355 if (o.available && o.exception_masks) {
356 for (std::size_t i=0;i<o.normal.size();++i) o.normal[i]=evaluate(ftz32_normal_witnesses[i],ftz32_cpu_path::raw);
357 for (std::size_t i=0;i<o.fusion.size();++i) o.fusion[i]=evaluate(ftz32_fusion_witnesses[i],ftz32_cpu_path::raw);
358 for (std::size_t i=0;i<o.flush.size();++i) o.flush[i]=evaluate(ftz32_flush_witnesses[i],ftz32_cpu_path::raw);
359 for (std::size_t i=0;i<o.core.size();++i) {
360 o.core[i]=evaluate(ftz32_core_witnesses[i],ftz32_cpu_path::compiled_core);
361 o.explicit_core[i]=evaluate(ftz32_core_witnesses[i],ftz32_cpu_path::explicit_core);
362 }
363 }
365 return o;
366 }
367 ftz32_cpu_admission ftz32_cpu_probe(ftz32_cpu_evaluator evaluate, bool hardware_ftz) {
368 return ftz32_cpu_classify(ftz32_cpu_observe(evaluate), hardware_ftz);
369 }
370}
bool native_fp32_environment_available() noexcept
Reports whether this build can access native CPU FP controls; this is not numerical admission.
ftz32_cpu_failure
Identifies the first rejected admission requirement.
bool set_native_fp32_mode(native_fp32_mode mode) noexcept
Initializes an application-owned numerical thread, without restoration or admission.
native_fp32_mode
Selects gradual denormals or signed native input/output flushing.
native_fp_state read_native_fp_state() noexcept
Reads this thread's controls and status without changing them; unsupported hosts return zero fields.
Results of finite startup witnesses for this scalar compilation and calling thread....
bool admitted() const noexcept
Reports whether every requirement of the requested policy passed.
bool explicit_compatible() const noexcept
Reports explicit-policy compatibility without requiring hardware signed flushing.
external_scope(external_scope &&)=delete
Disallows transfer of a guard whose restoration belongs to its constructing thread.
external_scope & operator=(external_scope &&)=delete
Disallows transfer of a guard whose restoration belongs to its constructing thread.
~external_scope() noexcept
Restores the numerical state captured by this external-call guard.
external_scope(external_scope const &)=delete
Disallows transfer of a guard whose restoration belongs to its constructing thread.
external_scope(native_fp32_scope const &region) noexcept
Saves the current state, then restores region.previous() on the same thread.
external_scope & operator=(external_scope const &)=delete
Disallows transfer of a guard whose restoration belongs to its constructing thread.
native_fp32_scope & operator=(native_fp32_scope const &)=delete
Disallows transfer of a guard whose restoration belongs to its constructing thread.
native_fp_state requested() const noexcept
Returns the controls and initially clear status requested by construction.
native_fp32_scope(native_fp32_mode mode)
Saves the current thread state and establishes the requested numerical region.
native_fp32_scope & operator=(native_fp32_scope &&)=delete
Disallows transfer of a guard whose restoration belongs to its constructing thread.
~native_fp32_scope() noexcept
Restores the complete saved state on the constructing thread.
native_fp32_scope(native_fp32_scope const &)=delete
Disallows transfer of a guard whose restoration belongs to its constructing thread.
native_fp32_scope(native_fp32_scope &&)=delete
Disallows transfer of a guard whose restoration belongs to its constructing thread.
native_fp_state previous() const noexcept
Returns the state captured before this numerical region.
bool controls_match() const noexcept
Checks thread identity and controls; accrued exception status does not cause failure.
Opaque native control/status snapshot, suitable for exact restoration checks.
friend bool operator==(native_fp_state const &, native_fp_state const &)=default
Compares both control and status words exactly.