Code Monkey home page Code Monkey logo

stochasticrounding.jl's Introduction

StochasticRounding.jl

CI DOI

Stochastic rounding for floating-point arithmetic.

This package exports Float64sr, Float32sr,Float16sr, and BFloat16sr, four number formats that behave like their deterministic counterparts but with stochastic rounding that is proportional to the distance of the next representable numbers and therefore exact in expectation (see also example below in Usage. The only known hardware implementation available is Graphcore's IPU with stochastic rounding, but other vendors are likely working on stochastic rounding for their next-generation GPUs (and maybe CPUs too).

The software emulation in StochasticRounding.jl makes the number format slower, but e.g. Float32 with stochastic rounding is only about 2x slower than the default deterministically rounded Float64. Xoroshiro128Plus, a random number generator from the Xorshift family, is used from the RandomNumbers.jl package, due to its speed and statistical properties.

Every format of Float64sr, Float32sr,Float16sr, and BFloat16sr uses a higher precision format to obtain the "exact" arithmetic result which is then stochastically rounded to the respective lower precision format. Float16sr and BFloat16sr use Float32 for this, Float32sr uses Float64, and Float64sr uses Double64 from DoubleFloats.jl

You are welcome to raise issues, ask questions or suggest any changes or new features.

BFloat16sr is based on BFloat16s.jl
Float16sr is slow in Julia <1.6, but much faster in Julia >=1.6 due to LLVM's half support.

Usage

StochasticRounding.jl defines stochastic_round as a general interface to round stochastically to a floating-point number, e.g.

julia> stochastic_round(Float32,π)
3.1415927f0

julia> stochastic_round(Float32,π)
3.1415925f0

Here we round π (which is first converted to Float64) to Float32 where it is not exactly representable, hence we have results that differ in the last bit. The chance here is

julia> chance_roundup(Float32,π)
0.6333222836256027

about 63% to obtain the first (which therefore would be round to nearest, the default rounding mode), and hence 37% to obtain the latter (round away from nearest). The stochastic_round function is wrapped into the floating-point formats Float64sr, Float32sr, Float16sr and BFloat16sr are supposed to be drop-in replacements for their deterministically rounded counterparts. You can create data of those types as expected (which is bitwise identical to the deterministic formats respectively) and the type will trigger stochastic rounding on every arithmetic operation.

julia> a = BFloat16sr(1.0)
BFloat16sr(1.0)
julia> a/3
BFloat16sr(0.33398438)
julia> a/3
BFloat16sr(0.33203125)

As 1/3 is not exactly representable the rounding will be at 66.6% chance towards 0.33398438 and at 33.3% towards 0.33203125 such that in expectation the result is 0.33333... and therefore exact.

Example

Solving a linear equation system with LU decomposition and stochastic rounding:

A = randn(Float32sr,3,3)
b = randn(Float32sr,3)

Now execute the \ several times and the results will differ slightly due to stochastic rounding

julia> A\b
3-element Vector{Float32sr}:
  3.3321106f0
  2.0391452f0
 -0.59199476f0

julia> A\b
3-element Vector{Float32sr}:
  3.3321111f0
  2.0391457f0
 -0.5919949f0

The random number generator is randomly seeded on every import or using such that running the same calculations twice, will not yield bit-reproducible results. However, you can seed the random number generator at any time with any integer larger than zero as follows

julia> StochasticRounding.seed(2156712)

the state of the random number generator for StochasticRounding.jl is independent from Julia's default, which is used for rand(), randn() etc.

Theory

Round-to-nearest (tie to even) is the standard rounding mode for IEEE floats. Stochastic rounding is explained in the following schematic

The exact result x of an arithmetic operation (located at one fifth between x₂ and x₃ in this example) is always rounded down to x₂ for round-to-nearest. For stochastic rounding, only at 80% chance x is round down. At 20% chance it is round up to x₃, proportional to the distance of x between x₂ and x₃.

Installation

StochasticRounding.jl is registered in the Julia registry. Hence, simply do

julia>] add StochasticRounding

where ] opens the package manager.

Performance

StochasticRounding.jl is among the fastest software implementation of stochastic rounding for floating-point arithmetic. Define a few random 1000000-element arrays

julia> using StochasticRounding, BenchmarkTools, BFloat16s
julia> A = rand(Float64,1000000);
julia> B = rand(Float64,1000000);   # A, B shouldn't be identical as a+a=2a is not round

And similarly for the other number types. Then with Julia 1.6 on an Intel(R) Core(R) i5 (Ice Lake) @ 1.1GHz timings via @btime +($A,$B) are

rounding mode Float64 Float32 Float16 BFloat16
round to nearest 1132 μs 452 μs 1588 μs 315 μs
stochastic rounding 11,368 μs 2650 μs 3310 μs 1850 μs

Stochastic rounding imposes an about x5 performance decrease for Float32 and BFloat16, but only x2 for Float16, however, 10x for Float64 due to the use of Double64. For more complicated benchmarks the performance decrease is usually within x10. About 50% of the time is spend on the random number generation with Xoroshiro128+.

Citation

This package was written for the following publications. If you use this package in your publications please cite us

Klöwer, M, PV Coveney, EA Paxton, TN Palmer, 2023. Periodic orbits in chaotic dynamical systems simulated at low precision, Scientific Reports, 10.1038/s41598-023-37004-4.

Paxton EA, M Chantry, M Klöwer, L Saffin, TN Palmer, 2022. Climate Modelling in Low-Precision: Effects of both Deterministic & Stochastic Rounding, Journal of Climate, 10.1175/JCLI-D-21-0343.1.

stochasticrounding.jl's People

Contributors

avleenk2312 avatar milankl avatar tomkimpson avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

stochasticrounding.jl's Issues

Float16sr subnormals also rounds somewhere else then up/down

This test failure keeps coming up

 Test for U(0,1): Test Failed at /home/runner/work/StochasticRounding.jl/StochasticRounding.jl/test/float16sr.jl:114
  Expression: Ndown + Nup == N
   Evaluated: 99998 == 100000

Meaning that there's some stochastic rounding with Float16sr that isn't to either of the prev/next representable number but to something else.

Idempotence of conversion

Should be implemented

julia> float(float(1f0))
1.0f0

julia> stochastic_float(stochastic_float(1f0))
ERROR: MethodError: no method matching stochastic_float(::Type{Float32sr})

Restructure

At the moment we have a lot of code redundancy, meaning for every missing method we have implement separate methods for each of the (currently) 4 stochastic rounding formats. This issue could be resolved by

  1. subtyping
abstract type AbstractStochasticFloat <: AbstractFloat end
primitive type Float32sr <: AbstractStochasticFloat 32 end

with

float(::Type{Float32sr}) = Float32
stochastic_float(::Type{Float32}) = Float32sr
widen(::Type{Float32}) = Float64    # used for exact arithmetic

to define the corresponding stochastic-deterministic types.

  1. general stochastic_round function

Every new stochastic format then implements

stochastic_round(::Type{<:AbstractStochasticFloat},x::AbstractFloat)

for example

stochastic_round(::Type{Float32sr},x::Float64) = ...
  1. methods for stochastic floats

should then be defined once, e.g.

method(x::T) where {T<:AbstractStochasticFloat} = T(method(float(T)(x)))
method(::Type{T}) where {T<:AbstractStochasticFloat} = method(float(T))
method(x::T,y::T) where {T<:AbstractStochasticFloat} = T(method(widen(T)(x),widen(T)(y)))

Stochastically rounded conversions

We currently do all of them deterministically, stochastic rounded is only for arithmetics, but I think we should change this depending on the target type

  • deterministic -> deterministic is deterministically rounded
  • stochastic -> deterministic is deterministically rounded
  • deter -> stoch is stoch rounded
  • stoch -> stoch also stoch rounded

which is also how the IPU does it? @giordano

I think only these lines need changing

# other floats, irrational and rationals
(::Type{T})(x::Real) where {T<:AbstractStochasticFloat} = stochastic_float(float(T)(x))
(::Type{T})(x::Rational) where {T<:AbstractStochasticFloat} = stochastic_float(float(T)(x))
(::Type{T})(x::AbstractStochasticFloat) where {T<:AbstractFloat} = convert(T,float(x))
(::Type{T})(x::AbstractStochasticFloat) where {T<:AbstractStochasticFloat} = stochastic_float(convert(float(T),float(x)))

Float64sr via Double64?

I would start with Double64s which are basically a+b with a,b Float64s and b about eps times smaller than a. So b is just the precision that cannot be represented with a and so with double precision arithmetic a+b=a. For our purposes that means that one would only need to decide based on b, whether to not round a or to round it up/down. That might be relatively easy to implement, but the devil is always in the detail.

@avleenk2312

Algorithm shortcut

Note to self: Something like

function BFloat16_stochastic_round(x::Float32)
    ui = reinterpret(UInt32,x)
    ui += rand(UInt16)
    return reinterpret(BFloat16sr,(ui >> 16) % UInt16)
end

is another shortcut for SR

TagBot trigger issue

This issue is used to trigger TagBot; feel free to unsubscribe.

If you haven't already, you should update your TagBot.yml to include issue comment triggers.
Please see this post on Discourse for instructions and more details.

[BUG] Invalid stochastic rounding with infinite

Hi,

We've spotted an issue in Float32_stochastic_round() where rounding an Infinite will sometime return NaN, where we may expect Infinite, see output below.

julia> Float32(Float32_stochastic_round(Inf))
Inf32

julia> Float32(Float32_stochastic_round(Inf))
NaN32

The only difference between NaN and infinite is an empty mantissa, hence adding a random noise will result in NaN.

`Float16_chance_roundup` direct from a Float64?

Great tool, looking forward to playing with it!

This works:

julia> Float32_chance_roundup(1.0/3)
0.666666666045785

But this does not:

julia> Float16_chance_roundup(1.0/3)
ERROR: MethodError: no method matching Float16_chance_roundup(::Float64)

Closest candidates are:
  Float16_chance_roundup(::Float32)
   @ StochasticRounding ~/.julia/packages/StochasticRounding/oFmFq/src/float16sr.jl:73

Stacktrace:
 [1] top-level scope
   @ REPL[7]:1

Of course, I can:

julia> Float16_chance_roundup(Float32(1.0/3))
0.33337402f0

Is there a reason to disallow ::Float64?

Define `LinearAlgebra.floatmin2`

In cboutsikas/stoch_rounding_iplicit_reg#1 I ran into

julia> GenericLinearAlgebra.svd!(rand(Float32sr,3,3))
ERROR: DomainError with -51:
Cannot raise an integer x to a negative power -51.
Convert input to float.
Stacktrace:
  [1] throw_domerr_powbysq(::Float32sr, p::Int64)
    @ Base ./intfuncs.jl:262
  [2] power_by_squaring(x_::Float32sr, p::Int64)
    @ Base ./intfuncs.jl:286
  [3] ^
    @ ./intfuncs.jl:311 [inlined]
  [4] floatmin2(::Type{Float32sr})
    @ LinearAlgebra ~/.julia/juliaup/julia-1.10.2+0.x64.apple.darwin14/share/julia/stdlib/v1.10/LinearAlgebra/src/givens.jl:69

caused by

julia> using LinearAlgebra, StochasticRounding

julia> LinearAlgebra.floatmin2(Float32sr)
ERROR: DomainError with -51:
Cannot raise an integer x to a negative power -51.
Convert input to float.
Stacktrace:
 [1] throw_domerr_powbysq(::Float32sr, p::Int64)
   @ Base ./intfuncs.jl:262
 [2] power_by_squaring(x_::Float32sr, p::Int64)
   @ Base ./intfuncs.jl:286
 [3] ^
   @ ./intfuncs.jl:311 [inlined]
 [4] floatmin2(::Type{Float32sr})
   @ LinearAlgebra ~/.julia/juliaup/julia-1.10.2+0.x64.apple.darwin14/share/julia/stdlib/v1.10/LinearAlgebra/src/givens.jl:69

Generically defined as

https://github.com/JuliaLang/julia/blob/64de065a183ac70bb049f7f9e30d790f8845dd2b/stdlib/LinearAlgebra/src/givens.jl#L67-L69

floatmin2(::Type{T}) where {T} = (twopar = 2one(T); twopar^trunc(Integer,log(floatmin(T)/eps(T))/log(twopar)/twopar))

but that somehow throws an error

Float64sr round down for powers of two

julia> stochastic_round(Float64,Double64(2))
1.9999999999999998

because eps changes from [1,2) to [2,4) the eps calculated from 2 reaches into prevfloat(2.0) making it possible to round down from 2 at a chance of 1/4.

Complex numbers

@avleenk2312 suggests to add complex number support. This is without any additional code generally supported as Float16sr, BFloat16sr and Float32sr are all <:AbstractFloat

julia> b = Complex{Float16sr}.(rand(4))
4-element Vector{Complex{Float16sr}}:
  Float16sr(0.5683594) + Float16sr(0.0)im
 Float16sr(0.25219727) + Float16sr(0.0)im
 Float16sr(0.07757568) + Float16sr(0.0)im
 Float16sr(0.07165527) + Float16sr(0.0)im

julia> A = Complex{Float16sr}.(rand(ComplexF64,4,4))
4×4 Matrix{Complex{Float16sr}}:
 Float16sr(0.16442871)+Float16sr(0.027038574)im      Float16sr(0.8027344)+Float16sr(0.9423828)im
 Float16sr(0.64501953)+Float16sr(0.3605957)im        Float16sr(0.40014648)+Float16sr(0.8051758)im
  Float16sr(0.7607422)+Float16sr(0.5761719)im       Float16sr(0.027954102)+Float16sr(0.25805664)im
  Float16sr(0.7861328)+Float16sr(0.13891602)im        Float16sr(0.6381836)+Float16sr(0.44384766)im

julia> A\b
4-element Vector{Complex{Float16sr}}:
  Float16sr(-1.2207031) + Float16sr(0.8798828)im
   Float16sr(0.1508789) - Float16sr(0.71191406)im
   Float16sr(0.8178711) - Float16sr(0.77197266)im
 Float16sr(-0.19714355) + Float16sr(0.38745117)im

However, for packages like GenericFFT, additional functionality may be required. @avleenk2312 could you post your errors here?

GenericFFT.jl compatibility

We have a few missing methods to execute fft from GenericFFT.jl

For powers of 2

julia> using GenericFFT, StochasticRounding
julia> x = Float32sr.(rand(Float32,4))
4-element Vector{Float32sr}:
 Float32sr(0.53207225)
 Float32sr(0.7393281)
 Float32sr(0.77091336)
 Float32sr(0.7059386)

julia> fft(x)
ERROR: MethodError: no method matching maxintfloat(::Type{Float32sr})

julia> Base.maxintfloat(::Type{Float32sr}) = reinterpret(Float32sr,maxintfloat(Float32))

else

julia> x = Float32sr.(rand(Float32,7))
7-element Vector{Float32sr}:
 Float32sr(0.75317705)
 Float32sr(0.43815964)
 Float32sr(0.28255802)
 Float32sr(0.8404123)
 Float32sr(0.8307252)
 Float32sr(0.9371242)
 Float32sr(0.9544312)

julia> fft(x)
ERROR: MethodError: no method matching Float32sr(::Irrational{:π})

julia> Float32sr(x::Irrational) = reinterpret(Float32sr,Float32(x))

julia> fft(x)
ERROR: StackOverflowError:
Stacktrace:
     [1] sincos(x::Float32sr)
       @ Base.Math ./special/trig.jl:205

julia> function Base.sincos(x::Float32sr)
           x = Float32(x)
           s,c = sincos(x)
           return (Float32sr(s),Float32sr(c))
       end

julia> fft(x)
7-element Vector{Complex{Float32sr}}:
   Float32sr(2.9032762) - Float32sr(9.20723e-8)im
 Float32sr(-0.20278877) + Float32sr(0.32545948)im
 Float32sr(-0.07813281) + Float32sr(0.9623062)im
  Float32sr(-0.5656596) + Float32sr(0.1649232)im
  Float32sr(-0.5656599) - Float32sr(0.16492161)im
 Float32sr(-0.07813434) - Float32sr(0.962306)im
  Float32sr(-0.2027903) - Float32sr(0.32545915)im

@avleenk2312

Using SR.jl types in conjunction with DE.jl

using DifferentialEquations
using StochasticRounding
f = (u,p,t) -> (p*u)
prob_ode_linear = ODEProblem(f,Float32sr(1.0)/Float32sr(2.0),(Float32sr(0.0),Float32sr(1.0)),Float32sr(1.01));
sol =solve(prob_ode_linear,Tsit5())

Throws a MethodError: no method matching Float32sr(::BigFloat)

Discussed on Slack - may be able to do something similar to how BigFloats are handled in https://github.com/milankl/SoftPosit.jl

odd-float32 x can be round to prevfloat(x)

function test(x::Float64,N::Int)
    xd = Float32sr(x)
    for _ in 1:N
        xr = Float32_stochastic_round(x)
        if xr != xd
            println(xr)
        end
    end
end

yields

julia> test(Float64(prevfloat(1f0)),1000000000)
Float32sr(0.9999999)
Float32sr(0.9999999)
Float32sr(0.9999999)

with chance 2^-29 = 2e-9, possible solution

x::Float64
xi = reinterpret(Int64,x)
xi += (rand(Int64) >> 35) + (xi & one(Int64))

this turns [-u/2,u/2) into (-u/2,u/2] for ever odd (by isolating the last bit via xi & one(Int64)) float. So basically an open bound that always faces the tie where the round away could occur.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.