Skip to content

Commit 0e3d35c

Browse files
dfa1claude
andcommitted
fix(writer): train ALP-RD dictionary on a stratified sample
ALP-RD built its left-parts dictionary from the first SAMPLE_SIZE physical rows (findBestDictionaryF64/F32 iterated values[0..sampleLen)). The cascade measures ALP-RD's cost on its own stratified sample, so a head-only dictionary could look cheap in the competition yet flood the tail with exceptions on the full re-encode when the leading rows are unrepresentative (sorted or clustered floats) — a size regression newly reachable now that ALP-RD is a top-level cascade candidate (#304 review). Train on a stratified sample spanning the whole array instead, matching AlpEncodingEncoder.findExponentsF64. Also harden AlpRdCascadeSelectionIntegrationTest: besides asserting the categorical ALP-RD win (decided on a 4096-row sample), assert the file compresses below raw F64, so the test can't pass on a degenerate "selected but didn't beat raw" outcome. nyc-311 unchanged at 1644.25 MB (its leading rows were already representative); 408 round-trip property tests, 16 ALP-RD unit tests, and 217 interop tests pass; full verify green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4228c30 commit 0e3d35c

2 files changed

Lines changed: 27 additions & 2 deletions

File tree

integration/src/test/java/io/github/dfa1/vortex/integration/AlpRdCascadeSelectionIntegrationTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import java.io.IOException;
1717
import java.nio.channels.FileChannel;
18+
import java.nio.file.Files;
1819
import java.nio.file.Path;
1920
import java.nio.file.StandardOpenOption;
2021
import java.util.List;
@@ -58,6 +59,13 @@ void highPrecisionF64_selectsAlpRd_andRoundTrips(@TempDir Path tmp) throws IOExc
5859
.contains("vortex.alprd");
5960
}
6061

62+
// And it delivers real compression — the file is well below raw F64 (the pre-#304 fallback
63+
// stored these columns as raw vortex.primitive). This guards a degenerate "selected but did
64+
// not actually beat raw" win, independent of which encoding categorically won the sample.
65+
assertThat(Files.size(file))
66+
.as("ALP-RD compresses the high-precision column below raw F64")
67+
.isLessThan(rows * (long) Double.BYTES);
68+
6169
// And every value round-trips exactly.
6270
try (var vf = VortexReader.open(file, ReadRegistry.loadAll())) {
6371
double[] got = new double[rows];

writer/src/main/java/io/github/dfa1/vortex/writer/encode/AlpRdEncodingEncoder.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,17 @@ private static EncodeResult encodeF64(double[] values, EncodeContext ctx) {
4949
}
5050

5151
int sampleLen = Math.min(SAMPLE_SIZE, n);
52-
Dictionary64 best = findBestDictionaryF64(values, sampleLen);
52+
// Train the dictionary on a stratified sample spanning the whole array, not the first
53+
// `sampleLen` rows. The cascade measures ALP-RD's cost on its own stratified sample, so a
54+
// head-only dictionary here can look cheap in the competition yet flood the tail with
55+
// exceptions on the full re-encode when the leading rows are unrepresentative (sorted or
56+
// clustered floats). Mirrors AlpEncodingEncoder.findExponentsF64 (#304 review).
57+
double[] sample = new double[sampleLen];
58+
long stride = Math.max(1L, (long) n / sampleLen);
59+
for (int i = 0; i < sampleLen; i++) {
60+
sample[i] = values[(int) Math.min(i * stride, (long) n - 1)];
61+
}
62+
Dictionary64 best = findBestDictionaryF64(sample, sampleLen);
5363

5464
Map<Short, Short> lookup = buildLookup(best.dict);
5565
long rightMask = -1L >>> (64 - best.rightBitWidth);
@@ -128,7 +138,14 @@ private static EncodeResult encodeF32(float[] values, EncodeContext ctx) {
128138
}
129139

130140
int sampleLen = Math.min(SAMPLE_SIZE, n);
131-
Dictionary32 best = findBestDictionaryF32(values, sampleLen);
141+
// Stratified sample across the whole array (see encodeF64): a head-only dictionary can be
142+
// measured cheap by the cascade yet explode on the tail during the full re-encode.
143+
float[] sample = new float[sampleLen];
144+
long stride = Math.max(1L, (long) n / sampleLen);
145+
for (int i = 0; i < sampleLen; i++) {
146+
sample[i] = values[(int) Math.min(i * stride, (long) n - 1)];
147+
}
148+
Dictionary32 best = findBestDictionaryF32(sample, sampleLen);
132149

133150
Map<Short, Short> lookup = buildLookup(best.dict);
134151
int rightMask = -1 >>> (32 - best.rightBitWidth);

0 commit comments

Comments
 (0)