The simple beauty of XOR floating point compression
clemenswinter.com
clemenswinter.com
δzd is the best. https://justine.lol/sizetricks/#dzd
https://userweb.cs.txstate.edu/~burtscher/ https://computing.llnl.gov/projects/floating-point-compressi...
but it tends to be very application specific, where there tends to be high correlation / small deltas between neighboring values in a 2d/3d/4d/etc floating point array (e.g., you are compressing neighboring temperature grid points in a PDE weather simulation model; temperature differences in neighboring cells won't differ by that much).
In a lot of other cases (e.g., machine learning) the floating point significand bits (and sometimes the sign bit) tends to be incompressible noise. The exponent is the only thing that is really compressible, and the xor trick does not help you as much because neighboring values could still vary a bit in terms of exponents. An entropy encoder instead works well for that (encode closer to the actual underlying data distribution/entropy), and you also don't depend upon neighboring floats having similar exponents as well.
In 2022, I created dietgpu, a library to losslessly compress/decompress floating point data at up to 400 GB/s on an A100. It uses a general-purpose asymmetric numeral system encoder/decoder on GPU (the first such implementation of general ANS on GPU, predating nvCOMP) for exponent compression.
We have used this to losslessly compress floating point data between GPUs (e.g., over Infiniband/NVLink/ethernet/etc) in training massive ML models to speed up overall wall clock time of training across 100s/1000s of GPUs without changing anything about how the training works (it's lossless compression, it computes the same thing that it did before).
I've noticed that array-of-structs form is harder to compress than struct-of-arrays form. This is relevant because a floating-point number is really a struct containing 3 values: 1-bit sign, 8-bit exponent, and 23-bit mantissa.
--------
If you have 1000 floats, I would expect 1000-sign bits in order to be more compressible (whatever compression algorithm you use, it would better determine the pattern of signs).
Then 8000-bits of exponent would also be more compressible, because all the exponents would likely follow its own pattern.
Finally, the 23-bits of mantissa would probably be difficult to impossible for compression. But "extracting it out" before running a compression algorithm will give the sign + exponents more opportunity for comrpession.
--------
So yeah, just think about your data. And favor structure-of-arrays form for better compression.
It’s worth noting that the algorithm in the article, and it’s cousin, delta encoding, both do already take advantage of any cases where the sign & exponent bits don’t change with every sample, and/or where the mantissa ULPs are constant for a while at a time. It’d be interesting to compare the explicit SoA of floats compression idea to XOR or delta compression, but I think it’s also interesting in a meta kinda of way that these two seemingly different ideas have similar outcomes.
One of the key innovations in the AMBER MD engine that made it work OK on cheaper systems was lossless floating point compression. It still impresses me that you can compress floats, send them over MPI, and decompress them, all faster/lower latency than the transport can send the uncompressed data.
The interconnects are improving at a slower rate in general than compute on the CPU/GPU is and it can be exploited.
Sounds like something Numba should include perhaps.
I retired from HPC around the era of iPython, Open MPI, Infiniband, OFED, GPFS/Panasas/Lustre, Rocks Cluster, and Condor/PBS/SGE.
I was able to get all 220MB of the default SRB2kart maps down into 12.9MB, and could even get down to 5MB with heavy texture compression. LZMA alone could only do 37MB, so I was pretty pumped.
One of the harder parts is compressing the vertex data and adjacency information (which linedefs connect which vertices). I never figured out a satisfying way of doing that, but this makes me want to try again...
I bet you could improve the performance of this algorithm significantly, without harming its compression too badly, by one or both of:
A) Adjusting the sizes of some bit fields to make them evenly divide the system word size, e.g. requiring the run length of the 11-prefix to be a multiple of 4, so that it can be encoded in 4 bits instead of 6. (This is a net space savings of half a bit; you have to include an average of 1.5 more bits of nonzero data, but the length is two bits shorter.)
B) Writing the streams of bits for e.g. "mode", "number of leading zeroes", and "bit subsequences" separately for each block of compressed data, rather than combining them into a single stream. As a bonus, each of those streams will now be more amenable to compression by generic compressors.
I noticed this a decade ago when I stumbled on a voxel engine that used some complex height map compression scheme for game map data. It seemed gratuitous to me, so I tried the simplest thing I could come up with: gzipping a 3D array of voxel colors. It didn't really work until I chose the right stride order (xyz, zyx, yzx, etc) and then suddenly it was 5x better.
The parquet format has the BYTE_STREAM_SPLIT encoding strategy which basically splits an array of floats/doubles into 4/8 arrays of bytes so they can be compressed independently. https://parquet.apache.org/docs/file-format/data-pages/encod...
Just choosing the correct stride order before compression is still underutilized. It's also undersupported in our tools too; e.g. that voxel engine used numpy, but it was a pita to get numpy to transparently change the stride order in the memory representation without also affecting the indexing order.
1262
3418
9076
Because it’s 2D it has two stride orders, xy and yx, i.e. row-major and column-major, which would be 126234189076 and 139240617286 respectively.Whether or not the resulting ranges are encoded in a space saving manner (as this algorithm is doing) or used as input into a BW transform/Huffman/etc sorta depends on the resulting value sequences.
Much of this though should apply the understanding that data being compressed can frequently lead to data specific preprocessing that can significantly increase the compression ratios over generic encoders. Just taking the output of this routine and running it through deflate might yield another couple percent if it were tuned. Or put another way, all the bit prefix/shifting decisions might be redundant for certain cases because the 0's would end up being single bit tokens in a Huffman encoder.
Amiga famously used a version of hardware backed delta encoding [1] to display more colours on the screen than normally possible within its memory limits.
Just for fun I implemented this algorithm: https://go.dev/play/p/72GyYmVnwyc
The algorithm described in the article uses prefixes 0, 10, and 11. My algorithm uses different prefixes: 0, 10, 110, 1110, 11110, [...]. By default, the subsequence that is used is identical to that containing differing bits during the previous iteration. The prefixes describe previously can be used to widen the subsequence:
- 0: Keep the subsequence the same width.
- 10: Make the subsequence (1<<1)-1 = 1 bit wider, both on the left and right side.
- 110: Make the subsequence (1<<2)-1 = 3 bits wider, both on the left and right side.
- 1110: Make the subsequence (1<<3)-1 = 7 bits wider, both on the left and right side.
- ...
For the sequence of numbers presented under "Tweaking the algorithm", the algorithm in the article produces 969 bits of data. My algorithm produces just 823.
Edit: Tiny bug in original code.
Edit 2: maybe it’s smarter to widen the subsequence using triangular numbers. Doing it exponentially is a bit too aggressive, it seems.
If I'm understanding correctly, this means that if the first choice a sized subsection is oversized, this will carry through to future samples (unless the encoder chooses to re-size it explicitly). I wonder if, with your cheaper "loosen" encoding, it is worth automatically tightening the bit delta. For example, if I go to an initial window of "5 zeros, the 7 bits 1001101, remainder zeros" (which I'll call 5;7), and the next encoding is "same, 0101110", the leading and trailing zeros adjust the window to (in this example) 6;5, "tightening" around the non-zero bits. With your low-cost "loosen" doing this aggressively (instead of, say, if it happens for multiple samples in a row) seems sane. It also means that the fact that your loosen encoding is symmetric (which makes sense in terms of bit efficiency) is somewhat mitigated because the implicit tighten can break the symmetry.
> If I'm understanding correctly, this means that if the first choice a sized subsection is oversized, this will carry through to future samples (unless the encoder chooses to re-size it explicitly).
And that's exactly what I do: I resize it. The subsequence that is picked during round n is the one that actually contained the differing bits in round n-1.
So you can only widen the subsequence, never shrink it. Shrinking would address the issue raised by TFA:
> If a series contains an outlier value that requires a very large window to encode, and all subsequent values have nonzero values only in a much smaller subwindow, the inefficient larger window will get locked in because we never hit the condition that would trigger a reset of the window size.
I've tried their "Integer that increments by 1 at every timestep." case on 10000 integer. The algorithm described said "6.98x compression"
bzip2 gave 80000/8642 = 9.2x compression
zstd gave 80000/11316 = 7.1x compression
http://cbloomrants.blogspot.com/2023/07/notes-on-float-and-m...
> TLDR:
> For S16 deltas, bias by 0x8080 , for S32 deltas, bias by 0x80808080.
> De-interleaving multi-byte integers into homogenous streams can help, particularly with weaker back-end compressors.
> TLDR:
> For floats, just reinterpret F32 as U32.
> If the floats have a mix of positive and negative, convert the sign bit to two's-complement signed S32.
> Consider lossy elimination of negative zero -0.f
> If you didn't actually want the huge precision for floats near zero, a simple lossy encoding is just to do float += constant, which works for non-negative floats where you don't know the high end of the range so you can't just use fixed point.
> TLDR:
> The best way to compress numeric data that is larger than bytes (F32,F16,S32,S16) is usually to delta them in their original size integer, then de-interleave after the delta.
> Sometimes no filter or no deinterleave is best, particularly with stronger compressors, so being able to select filter on/off per-file can give big wins.
You would obviously be better off timing in the proper unit so that you have an integer number of milliseconds but that's the kind of situation where performance starts to drop, no one understands why until some dev starts digging deeply into the encoding and maybe even write a blog post about how he got a massive speedup by multiplying numbers by 1000.
Most of the approaches described in that paper should work well enough with a base + offset scheme, where the base is RLE-compressed or just set once every N elements.
So yeah, dropping more significant digits should lead to even better compression.
But if we are going to be massaging the data based on what we know/need, there are better ways to go about compressing it. e.g. I'd expect that simple fixed-point quantization with a delta encoding would compress better than this. What I find cool about this is that it dynamically figures things out without having to deal with any priors.
I don't really understand this. Is there an example?
Advanced context modeling compression algorithms can work even better than the scheme presented in the article. These are like mini-AIs, and they can recognize patterns more complex than just a string of unchanged bits, and with the appropriate entropy coder, give excellent compression. Or course the processing cost is huge compare to a few bit manipulations, and wouldn't make much sense unless you need extreme compression.
200MiB/s is too slow by a factor of 7. Though obviously some people have optimised it.
Even top end wifi tends to only be around 1.2gbit.
Although there's just not enough PCIe bandwidth in current consumer hardware for 100 Gbps...
You can buy second hand 100g networking cards pretty cheaply. The caveat is that they only support older PCIe standards, so you need PCIe 16x to use them at full speed.
Please also don't forget typical NVMe drives are pretty fast now. Just two of them can nearly saturate a 100g link.
Personally I know a lot of people in IT field who have single mode fiber SFP28 networks. I'd say 0.001% - 0.01% is closer to reality.
Remember we were talking about high end.
Surprisingly high latencies (compared to fiber, that is) and noisy fans due to a lot of heat generated.
Shrug. I guess your mileage may vary.
One big reason for fiber is a home lab. Transferring large VM images etc. is so much faster. Ability to run iSCSI fast is also amazing.
Single mode fiber is pretty nice. It's fast, has low power requirements and no need to worry about electrical issues. Prices have declined significantly. The only minus is that you can't have PoE, so the best option is to wire both.
(Ok, there's actually Power over Fiber, but that's just crazy stuff https://en.wikipedia.org/wiki/Power-over-fiber. A huge fire risk.)
It's the standard. I don't know what kind of world you live in to convince yourself it's exclusive.
100Gbps is also available pretty cheap, but typically only as a separate card.
200Mbps puts you in the top 15 countries by broadband speed (above Japan) and the top 4 by mobile speed (above South Korea). Not that that's relevant to anything. (I haven't edited out any mistakes. The child comment is lying.)
If you need to stream sequential floating point data out an internet link something more space efficient (and probably chunked instead) would probably be worthwhile over the simplicity.