-
Notifications
You must be signed in to change notification settings - Fork 40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FEATURE] Add support for f128::f128
and half::{ f16, bf16 }
#46
Comments
I don't know nearly enough about either the parse algorithms or the bit layout of these types to begin to take this on myself. I'll reach out to Alex and see if he has the time or capability to weigh in. |
@myrrlyn This would be doable for sure. The only issue is that the number of significant digits would have to increase dramatically for the arbitrary-precision arithmetic. Currently, in base 10, we need 768 digits to accurately represent a 64-bit float for rounding, The formula, which you can find in comments in my source code, is as follows: ///
/// `−emin + p2 + ⌊(emin + 1) log(2, b) − log(1 − 2^(−p2), b)⌋`
///
/// For f32, this follows as:
/// emin = -126
/// p2 = 24
///
/// For f64, this follows as:
/// emin = -1022
/// p2 = 53
For
Which, would then require |
For the actual links to the documentation in the code, the comments documenting all this can be found here. The formula so you can test it on your own is here: def digits(emin, p2, b):
return -emin + p2 + math.floor((emin+ 1)*math.log(2, b)-math.log(1-2**(-p2), b)) Where in the above case, |
Support for f16 and bf16 should be trivial, however. For f128, I"m very worried about the following however, so it would never be the default (maybe on option like rust-lexical/lexical-core/src/atof/algorithm/bhcomp.rs Lines 55 to 103 in 905cdba
This is because it's fairly trivial with 767 mantissa digits + 324 exponent digits to stack-allocate everything which is ~1091 digits or ~3600 bits, meanwhile, with an f128, we'd need 11563 mantissa digits + 4932 exponent digits, or ~55,000 bits. We're not stack-allocating 7KB just for a single operation, which is totally doable using a Vec, but would require an allocator. EDIT: This might be mitigated a bit if we change to use associated type in the Currently, our two storage options (for different correctness algorithms, which have speed tradeoffs) are as follows: https://github.com/Alexhuszagh/rust-lexical/blob/master/lexical-core/src/atof/algorithm/bignum.rs#L9-L21 If we make it an associated type for the |
Given how often |
@Evrey What implementations do you want for f16, bf16, and f128. I see the following:
I don't see a Rust version of In the meantime, I will be feature-gating the code with features that don't (yet) exist for the associated constants, and better documentation for the required storage for each type in the comments, and add associated types for the storage required. |
Adds the associated constants `BIGINT_LIMBS`, `BIGFLOAT_LIMBS`, and `EXPONENT_SIZE` to `Float`. Adds the associated types `BigintStorage` and `BigfloatStorage` to `Float`. Implements `Bigfloat` and `Bigint` in terms of `FloatType`, and the associated storage can differ depending on the float size.
@RazrFalcon The preliminary logic for all of this is implemented, however, I need concrete types for anything more significant. The following additions have been added, with appropriate types, should make everything work out-of-the-box. In
I then re-implemented I also added extensive comments to ensure the choices for these sizes were clear: /// The minimum, denormal exponent can be calculated as follows: given
/// the number of exponent bits `exp_bits`, and the number of bits
/// in the mantissa `mantissa_bits`, we have an exponent bias
/// `exp_bias` equal to `2^(exp_bits-1) - 1 + mantissa_bits`. We
/// therefore have a denormal exponent `denormal_exp` equal to
/// `1 - exp_bias` and the minimum, denormal float `min_float` is
/// therefore `2^denormal_exp`.
///
/// For f16, this follows as:
/// exp_bits = 5
/// mantissa_bits = 10
/// exp_bias = 25
/// denormal_exp = -24
/// min_float = 5.96 * 10^−8
///
/// For bfloat16, this follows as:
/// exp_bits = 8
/// mantissa_bits = 7
/// exp_bias = 134
/// denormal_exp = -133
/// min_float = 9.18 * 10^−41
///
/// For f32, this follows as:
/// exp_bits = 8
/// mantissa_bits = 23
/// exp_bias = 150
/// denormal_exp = -149
/// min_float = 1.40 * 10^−45
///
/// For f64, this follows as:
/// exp_bits = 11
/// mantissa_bits = 52
/// exp_bias = 1075
/// denormal_exp = -1074
/// min_float = 5.00 * 10^−324
///
/// For f128, this follows as:
/// exp_bits = 15
/// mantissa_bits = 112
/// exp_bias = 16495
/// denormal_exp = -16494
/// min_float = 6.48 * 10^−4966
pub(super) fn max_digits<F>(radix: u32)
-> Option<usize>
where F: Float
{
match F::BITS {
32 => max_digits_f32(radix),
64 => max_digits_f64(radix),
_ => unreachable!(),
}
}} /// Storage for a big integer type.
///
/// This is used for the bhcomp::large_atof and bhcomp::small_atof
/// algorithms. Specifically, it stores all the significant digits
/// scaled to the proper exponent, as an integral type,
/// and then directly compares these digits.
///
/// This requires us to store the number of significant bits, plus the
/// number of exponent bits (required) since we scale everything
/// to the same exponent.
/// This therefore only needs the following number of digits to
/// determine the correct representation (the algorithm can be found in
/// `max_digits` in `bhcomp.rs`):
/// * `bfloat16` - 138
/// * `f16` - 29
/// * `f32` - 158
/// * `f64` - 1092
/// * `f128` - 16530
#[derive(Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(Debug))]
pub(crate) struct Bigint<F: Float> {
/// Internal storage for the Bigint, in little-endian order.
pub(crate) data: F::BigintStorage,
} /// Storage for a big floating-point type.
///
/// This is used for the bigcomp::atof algorithm, which crates a
/// representation of `b+h` and the float scaled into the range `[1, 10)`.
/// This therefore only needs the following number of digits to
/// determine the correct representation (the algorithm can be found in
/// `max_digits` in `bhcomp.rs`):
/// * `bfloat16` - 97
/// * `f16` - 22
/// * `f32` - 113
/// * `f64` - 768
/// * `f128` - 11564
#[derive(Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(Debug))]
pub struct Bigfloat<F: Float> {
/// Internal storage for the Bigfloat, in little-endian order.
///
/// Enough storage for up to 10^345, which is 2^1146, or more than
/// the max for f64.
pub(crate) data: F::BigfloatStorage,
/// It also makes sense to store an exponent, since this simplifies
/// normalizing and powers of 2.
pub(crate) exp: i32,
} This should simplify maintainability while also adding support for these types later down the road. If all checks pass, I'll merge this branch into master and keep this branch until I get concrete implementation details on the floats we want to use. |
@Alexhuszagh I'm fine with not having |
@Evrey I've just looked at the
I'm not exactly the world's best maintainer, so I would likely ask a few, well-trusted maintainers to ensure the crate is well-maintained (like with lexical) in the case my mental health deteriorates. |
Uff, okay, that's a lot of work. D: Sounds like effort is instead best put in preparation for more types, then, and pushing whatever E: Btw., while GPUs do |
@RazrFalcon I believe LLVM supports via it's intrinsics all of the ones we want. Not everything, so it would have to be feature-gated and implemented on supported hardware. Here's documentation from LLVM on f16 and bf16 and f128. This would likely have to be unstable, since Rustc inherently checks for any LLVM intrinsics. The other, fallback solution would simply be software emulation, which could be decently fast and not too difficult. The only reason this would be doable without too much work is there's a pretty big body of permissively licensed work I believe that has robust implementations of this. I would likely be using llvmint-like wrappers for this (since llvmint doesn't seem to be maintained). |
Ok I've taken a closer look at this, and it seems we have the following cases:
References for f16, bf16, and _Float16. References for f128. References for ARM half-precision types. References for x86 F16C extensions. References for x86 AVX-512 extensions for BF16. So, after a careful look, this seems a lot more trivial than initially imagined:
In terms of accuracy... f16 and bf16 would only have rounding issues with the following part of the fast-path algorithm, due to the lack of guard digits in this operation. Since it would be promoted to an f32, processed, and then truncated, it could lead to errors of a few ULP (not sure, I'd assume at max 1 but I'd have to test): float.pow(radix, exponent) I'm creating an initial, proof-of-concept crate now for support for these types. It's extremely preliminary, and currently private, but I'm working on it now. |
- Readded trivial implementation of algorithm_m. - Not compiled, just for future algorithm developments. - Removed hard-coded logic for u64 in correct and bigint parsing. - Should allow initial support for #46.
Ok, I've thought about it a bit more carefully, and as long as we implement the logic to and from f32 properly, no loss of accuracy is possible since the fast path only operates if, and only if, the mantissa is capable of completely storing the digits (including exponents being shifted from the exponent to the mantissa). The means, for the fast path, with:
This means the fast path will only be valid for exponents The code to generate these limits, in case you're wondering, can be found in the documentation: rust-lexical/lexical-core/src/table/decimal.rs Lines 86 to 157 in 7010efd
|
On a separate note, I believe I've removed all the hard-coded logic internally to support f128, removing all the assumptions about an f64 mantissa used to simplify some trait impls. I needed to refactor this anyway, but it means we have most of the initial barriers removed for support for |
This is exciting news! |
OK, I've been looking at this carefully and currently am implementing The current number of bits required to parse a f128 correctly would be:
So, we'd need enough bits to store The float-writing algorithms would be a lot easier, but would require support for 256-bit integer math (not too difficult to do, since have native 128-bit math). I'll be implementing |
Ok full support for This can be enabled using the |
What a lovely bulk-update! |
Linking this to #93. |
I'm going to drop considering support for f128 because there's a lot of optimizations that are effectively impossible, so using a more naive library would be ideal. It would only really make sense if using an antiquated algorithm like Grisu for floating point printing and even slow algorithms that have been removed from general support would which I have no plan on supporting right now and dealing with the preconditions and code changes to implement this. Also, 128-bit floats aren't commonly used like the bf16 and f16 variants and I know of practically no support for them outside one that requires linking to glibc. |
Good reasoning, and still a big win on half floats. Thanks for all the effort! |
Problem
Currently,
lexical-core
can only parsef32
andf64
, but especially for designers of programming languages supporting more number formats than Rust does would be nice.Solution
Offer a feature-gated default impl for
f128
using thef128
crate andf16, bf16
from thehalf
crate.Prerequisites
0.7.*
format
,correct
,radix
Alternatives
Don't see any beyond »let's not«.
Additional Context
Rust has
u128
, as having it for e.g. crypto is convenient, despite no mainstream CPU having 128-bit integer arithmetic and registers.f16
is very often used, e.g. in GPU code,bf16
specifically in neural network code, andf128
also finds some use here and there.There's also 8-bit floats, though not IEEE-standardised, and there's IEEE 754 binary256. However, I know of no handy softfloat crates for these.
As
lexical-core
aims to be a proglang-agnostic number parser, i.e. not tied to Rust formats and types, I see no reason to completely restrict oneself to just the built-in Rust machine types.The text was updated successfully, but these errors were encountered: