Skip to content

Commit 4765616

Browse files
authored
deterministic encoding ordering (#5770)
pre register all encodings known by the session, so they have the same encoding index from run to run. Layout encoding indices don't suffer from this because we get them after we have the full layout tree, and we traverse it dept first in a single thread to get the layout indices Signed-off-by: Onur Satici <[email protected]>
1 parent baafc71 commit 4765616

File tree

4 files changed

+18
-27
lines changed

4 files changed

+18
-27
lines changed

docs/specs/file-format.md

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -117,26 +117,3 @@ The plan is that at write-time, a minimum supported reader version is declared.
117117
reader version can then be embedded into the file with WebAssembly decompression logic. Old readers are able to decompress new
118118
data (slower than native code, but still with SIMD acceleration) and read the file. New readers are able to make the best use of
119119
these encodings with native decompression logic and additional push-down compute functions (which also provides an incentive to upgrade).
120-
121-
## File Determinism and Reproducibility
122-
123-
### Encoding Order Indeterminism
124-
125-
When writing Vortex files, each array segment references its encoding via an integer index into the footer's `array_specs`
126-
list. During serialization, encodings are registered in the order they are first encountered via calls to
127-
`ArrayContext::encoding_idx()`. With concurrent writes, this encounter order depends on thread scheduling and lock
128-
acquisition timing, making the ordering in the footer non-deterministic between runs.
129-
130-
This affects the `encoding` field in each serialized array segment. The same encoding might receive index 0 in one run and
131-
index 1 in another, changing the integer value stored in each array segment that uses that encoding. FlatBuffers optimize
132-
storage by omitting fields with default values (such as 0), so when an encoding index is 0, the field may be omitted from
133-
the serialized representation. This saves approximately 2 bytes per affected array segment, and with alignment adjustments,
134-
can result in up to 4 bytes difference per array segment between runs.
135-
136-
:::{note}
137-
Despite this non-determinism, the practical impact is minimal:
138-
139-
- File size may vary by up to 4 bytes per affected array segment
140-
- All file contents remain semantically identical and fully readable
141-
- Segment ordering (the actual data layout) remains deterministic and consistent across writes
142-
:::

vortex-array/src/context.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ impl<T: Clone + Eq> VTableContext<T> {
2626
Self(Arc::new(RwLock::new(encodings)))
2727
}
2828

29+
pub fn from_registry_sorted(registry: &Registry<T>) -> Self
30+
where
31+
T: Display,
32+
{
33+
let mut encodings: Vec<T> = registry.items().collect();
34+
encodings.sort_by_key(|a| a.to_string());
35+
Self::new(encodings)
36+
}
37+
2938
pub fn try_from_registry<'a>(
3039
registry: &Registry<T>,
3140
ids: impl IntoIterator<Item = &'a str>,

vortex-file/src/writer.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use vortex_array::ArrayRef;
1919
use vortex_array::expr::stats::Stat;
2020
use vortex_array::iter::ArrayIterator;
2121
use vortex_array::iter::ArrayIteratorExt;
22+
use vortex_array::session::ArraySessionExt;
2223
use vortex_array::stats::PRUNING_STATS;
2324
use vortex_array::stream::ArrayStream;
2425
use vortex_array::stream::ArrayStreamAdapter;
@@ -138,8 +139,12 @@ impl VortexWriteOptions {
138139
mut write: W,
139140
stream: SendableArrayStream,
140141
) -> VortexResult<WriteSummary> {
141-
// Set up a Context to capture the encodings used in the file.
142-
let ctx = ArrayContext::empty();
142+
// NOTE(os): Setup an array context that already has all known encodings pre-populated.
143+
// This is preferred for now over having an empty context here, because only the
144+
// serialised array order is deterministic. The serialisation of arrays are done
145+
// parallel and with an empty context they can register their encodings to the context
146+
// in different order, changing the written bytes from run to run.
147+
let ctx = ArrayContext::from_registry_sorted(self.session.arrays().registry());
143148
let dtype = stream.dtype().clone();
144149

145150
let (mut ptr, eof) = SequenceId::root().split();

vortex-python/src/io.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ impl PyVortexWriteOptions {
222222
/// >>> vx.io.VortexWriteOptions.default().write_path(sprl, "chonky.vortex")
223223
/// >>> import os
224224
/// >>> os.path.getsize('chonky.vortex')
225-
/// 215196
225+
/// 215996
226226
/// ```
227227
///
228228
/// Wow, Vortex manages to use about two bytes per integer! So advanced. So tiny.
@@ -234,7 +234,7 @@ impl PyVortexWriteOptions {
234234
/// ```python
235235
/// >>> vx.io.VortexWriteOptions.compact().write_path(sprl, "tiny.vortex")
236236
/// >>> os.path.getsize('tiny.vortex')
237-
/// 54200
237+
/// 55116
238238
/// ```
239239
///
240240
/// Random numbers are not (usually) composed of random bytes!

0 commit comments

Comments
 (0)