You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pinot.apache.org by "xiangfu0 (via GitHub)" <gi...@apache.org> on 2026/07/15 18:42:02 UTC
[PR] [core] Optimize exact and TDigest percentile aggregation [pinot]
xiangfu0 opened a new pull request, #18996:
URL: https://github.com/apache/pinot/pull/18996
## Summary
- replace exact-percentile full sorting with an introspective three-way rank selector
- batch raw TDigest values and linearly merge them with sorted centroids instead of repeatedly indirect-sorting old centroids
- normalize optimized TDigest state to the existing `MergingDigest` intermediate representation
- add deterministic 100-million-row JMH benchmarks, correctness coverage, sample queries, and reproduction instructions
## Why
Exact percentile previously sorted every retained value even though a query needs only one rank, making extraction
`O(n log n)`. TDigest 3.2 profiling showed most raw aggregation time in its indirect quicksort/insertion-sort path,
which re-sorts existing centroids whenever the input buffer is compressed.
The exact path now uses three-way introselect with median/ninther pivoting and a bounded sort fallback. The TDigest path
sorts only small primitive-double batches, linearly merges them with already-sorted centroids, and applies the same
weight-limit rule as `MergingDigest`.
There are no query, table configuration, intermediate serialization, or distributed merge changes. Group-by, serialized
BYTES, and star-tree TDigest aggregation paths remain unchanged.
## Benchmark results
Single-thread JMH 1.37 on an Apple M4 Pro with 24 GiB RAM and OpenJDK 21.0.10. Each workload processes 100 million
deterministic doubles in 10,000-row Pinot blocks, with 2 forks and 5 measurements per fork.
| Workload | Baseline | Optimized | Latency reduction | Speedup |
|---|---:|---:|---:|---:|
| Exact P75, fresh holder | 7,710.804 ms/op | 764.626 ms/op | 90.084% | 10.084x |
| Exact P75, reused holder | 7,499.571 ms/op | 707.555 ms/op | 90.565% | 10.599x |
| Exact P75 extraction | 7,448.796 ms/op | 618.253 ms/op | 91.700% | 12.048x |
| TDigest P75 aggregation | 5,677.240 ms/op | 2,569.679 ms/op | 54.737% | 2.209x |
Exact results match a fully sorted oracle bit-for-bit. Optimized TDigest P75 was `0.7499280847` versus exact
`0.7499315464`, an absolute error of `0.0000034617`. These are aggregation-function measurements, not distributed
end-to-end query latency.
Full commands and sample SQL are recorded in
`pinot-perf/benchmark-results/percentile-aggregation-100m-results.md`.
## Validation
- `./mvnw spotless:apply -pl pinot-core,pinot-perf`
- `./mvnw license:format -pl pinot-core,pinot-perf`
- `./mvnw checkstyle:check -pl pinot-core,pinot-perf`
- `./mvnw license:check -pl pinot-core,pinot-perf`
- `./mvnw test-compile -pl pinot-core,pinot-perf -am -Dmaven.compiler.showDeprecation=true -Dmaven.compiler.showWarnings=true '-Dmaven.compiler.compilerArgs=-Xlint:all'`
- `./mvnw -pl pinot-core -Dtest=PercentileAggregationFunctionTest,PercentileTDigestAggregationFunctionTest,InterSegmentAggregationMultiValueRawQueriesTest test` — 90 tests passed
- broader TDigest query, MV, serialization, and star-tree suite — 42 tests passed
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org
Re: [PR] [core] Optimize exact and TDigest percentile aggregation, including StarTree group-by [pinot]
Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #18996:
URL: https://github.com/apache/pinot/pull/18996#discussion_r3591440895
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunction.java:
##########
@@ -215,15 +205,9 @@ public void aggregateGroupByMV(int length, int[][] groupKeysArray, GroupByResult
byte[][] bytesValues = blockValSet.getBytesValuesSV();
forEachNotNull(length, blockValSet, (from, to) -> {
for (int i = from; i < to; i++) {
- TDigest value = ObjectSerDeUtils.TDIGEST_SER_DE.deserialize(bytesValues[i]);
for (int groupKey : groupKeysArray[i]) {
- TDigest tDigest = groupByResultHolder.getResult(groupKey);
- if (tDigest != null) {
- tDigest.add(value);
- } else {
- // Create a new TDigest for the group
- groupByResultHolder.setValueForKey(groupKey, ObjectSerDeUtils.TDIGEST_SER_DE.deserialize(bytesValues[i]));
- }
+ getSerializedAccumulator(groupByResultHolder, groupKey, bytesValues[i])
+ .addSerializedTDigest(bytesValues[i]);
}
Review Comment:
In the BYTES + group-by-MV path, the same serialized TDigest is parsed once per group key (`addSerializedTDigest(bytesValues[i])` inside the inner `for (int groupKey : groupKeysArray[i])`). This is a regression vs. the previous implementation that deserialized once per row and re-used the decoded representation across all group keys for that row. For multi-value group-bys with many keys per row, this can reintroduce significant per-row overhead and undermine the intended allocation/CPU savings of the serialized merge path.
Consider decoding the serialized digest once per row and reusing the decoded centroids when merging into multiple groups (e.g., add an accumulator API that accepts already-decoded centroid arrays / a shared decode buffer, or a helper that decodes into temporary primitive arrays once and then merges them into each group accumulator).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org
Re: [PR] [core] Optimize exact and TDigest percentile aggregation, including StarTree group-by [pinot]
Posted by "Jackie-Jiang (via GitHub)" <gi...@apache.org>.
Jackie-Jiang commented on code in PR #18996:
URL: https://github.com/apache/pinot/pull/18996#discussion_r3591466702
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileAggregationFunction.java:
##########
@@ -35,6 +35,8 @@
public class PercentileAggregationFunction extends NullableSingleInputAggregationFunction<DoubleArrayList, Double> {
private static final double DEFAULT_FINAL_RESULT = Double.NEGATIVE_INFINITY;
+ private static final int SORT_THRESHOLD = 32;
+ private static final int NINTHER_THRESHOLD = 128;
Review Comment:
Can you document the meaning of these 2 thresholds? How are they used?
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunction.java:
##########
@@ -108,20 +108,20 @@ public void aggregate(int length, AggregationResultHolder aggregationResultHolde
if (blockValSet.getValueType() == DataType.BYTES) {
// Serialized TDigest
byte[][] bytesValues = blockValSet.getBytesValuesSV();
- foldNotNull(length, blockValSet, (TDigest) aggregationResultHolder.getResult(), (tDigest, from, toEx) -> {
- if (tDigest != null) {
- for (int i = from; i < toEx; i++) {
- tDigest.add(ObjectSerDeUtils.TDIGEST_SER_DE.deserialize(bytesValues[i]));
- }
- } else {
- tDigest = ObjectSerDeUtils.TDIGEST_SER_DE.deserialize(bytesValues[0]);
- aggregationResultHolder.setValue(tDigest);
- for (int i = 1; i < length; i++) {
- tDigest.add(ObjectSerDeUtils.TDIGEST_SER_DE.deserialize(bytesValues[i]));
+ PercentileTDigestAccumulator accumulator = foldNotNull(length, blockValSet,
Review Comment:
Can we keep the same pattern?
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAccumulator.java:
##########
@@ -0,0 +1,448 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.core.query.aggregation.function;
+
+import com.tdunning.math.stats.MergingDigest;
+import com.tdunning.math.stats.TDigest;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/// Accumulates raw values and serialized TDigests into centroids without repeatedly sorting existing centroids.
+///
+/// The accumulator is used while a percentile TDigest result, or one of its group-by results, is being built. Raw
+/// values are sorted in small batches, while serialized TDigest centroids are decoded in their existing sorted order.
+/// Both are linearly merged with the accumulated centroids and compressed using the same weight-limit rule as
+/// [MergingDigest]. [#toTDigest()] normalizes the result to a standard [MergingDigest], so intermediate-result
+/// serialization and distributed merging remain unchanged.
+///
+/// Serialized group state keeps its first digest pending, and allocates primitive centroid buffers only when another
+/// input must be merged. The raw-value buffer remains unallocated for serialized state until a raw value is added.
+///
+/// Instances are thread-confined to one aggregation result or group-by result.
+final class PercentileTDigestAccumulator {
Review Comment:
Is this almost re-implementing the TDigest? Should we consider directly implementing TDigest if the original library has poor support?
##########
pinot-core/src/test/java/org/apache/pinot/queries/PercentileTDigestMVQueriesTest.java:
##########
@@ -55,6 +56,12 @@
public class PercentileTDigestMVQueriesTest extends PercentileTDigestQueriesTest {
private static final int MAX_NUM_MULTI_VALUES = 10;
+ @Override
+ @Test(enabled = false)
+ public void testStarTreeGroupBy() {
+ // The StarTree fixture stores the single-value TDigest aggregation only.
Review Comment:
Does this test fail? It should work right?
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunction.java:
##########
@@ -268,21 +252,25 @@ protected void aggregateMVGroupByMV(int length, int[][] groupKeysArray, GroupByR
@Override
public TDigest extractAggregationResult(AggregationResultHolder aggregationResultHolder) {
- TDigest tDigest = aggregationResultHolder.getResult();
- if (tDigest == null) {
+ Object result = aggregationResultHolder.getResult();
+ if (result == null) {
return TDigest.createMergingDigest(_compressionFactor);
+ } else if (result instanceof PercentileTDigestAccumulator) {
+ return ((PercentileTDigestAccumulator) result).toTDigest();
} else {
- return tDigest;
+ return (TDigest) result;
Review Comment:
Is this possible to reach?
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunction.java:
##########
@@ -268,21 +252,25 @@ protected void aggregateMVGroupByMV(int length, int[][] groupKeysArray, GroupByR
@Override
public TDigest extractAggregationResult(AggregationResultHolder aggregationResultHolder) {
- TDigest tDigest = aggregationResultHolder.getResult();
- if (tDigest == null) {
+ Object result = aggregationResultHolder.getResult();
+ if (result == null) {
return TDigest.createMergingDigest(_compressionFactor);
+ } else if (result instanceof PercentileTDigestAccumulator) {
+ return ((PercentileTDigestAccumulator) result).toTDigest();
} else {
- return tDigest;
+ return (TDigest) result;
}
}
@Override
public TDigest extractGroupByResult(GroupByResultHolder groupByResultHolder, int groupKey) {
- TDigest tDigest = groupByResultHolder.getResult(groupKey);
- if (tDigest == null) {
+ Object result = groupByResultHolder.getResult(groupKey);
+ if (result == null) {
return TDigest.createMergingDigest(_compressionFactor);
+ } else if (result instanceof PercentileTDigestAccumulator) {
+ return ((PercentileTDigestAccumulator) result).toTDigest();
} else {
- return tDigest;
+ return (TDigest) result;
Review Comment:
Is this possible to reach?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org
Re: [PR] [core] Optimize exact and TDigest percentile aggregation, including StarTree group-by [pinot]
Posted by "xiangfu0 (via GitHub)" <gi...@apache.org>.
xiangfu0 commented on code in PR #18996:
URL: https://github.com/apache/pinot/pull/18996#discussion_r3598218721
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunction.java:
##########
@@ -305,13 +300,16 @@ public ColumnDataType getIntermediateResultColumnType() {
@Override
public SerializedIntermediateResult serializeIntermediateResult(TDigest tDigest) {
+ if (tDigest instanceof PercentileTDigestAccumulator) {
+ tDigest = ((PercentileTDigestAccumulator) tDigest).toTDigest();
Review Comment:
Fixed in `912280c814`.
`PercentileTDigestAccumulator` now preserves validated pending TDigest bytes and directly emits standard small encoding when a materialized accumulator needs its explicit centroid capacity. Normal-sized accumulator results still use verbose encoding.
The regression covers both lazy and materialized compression-20 / 80-centroid states through `serializeIntermediateResult()` and the stock TDigest 3.2 reader, asserting encoding, total weight, centroid count, bounds, and finite monotonic quantiles.
Validation: all 312 `PercentileTDigestAggregationFunctionTest` cases and all 6 `PercentileTDigestQueriesTest` cases pass; spotless, checkstyle, license format, and license check pass for `pinot-core`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org
Re: [PR] [core] Optimize exact and TDigest percentile aggregation, including StarTree group-by [pinot]
Posted by "xiangfu0 (via GitHub)" <gi...@apache.org>.
xiangfu0 commented on code in PR #18996:
URL: https://github.com/apache/pinot/pull/18996#discussion_r3591721451
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunction.java:
##########
@@ -215,15 +205,9 @@ public void aggregateGroupByMV(int length, int[][] groupKeysArray, GroupByResult
byte[][] bytesValues = blockValSet.getBytesValuesSV();
forEachNotNull(length, blockValSet, (from, to) -> {
for (int i = from; i < to; i++) {
- TDigest value = ObjectSerDeUtils.TDIGEST_SER_DE.deserialize(bytesValues[i]);
for (int groupKey : groupKeysArray[i]) {
- TDigest tDigest = groupByResultHolder.getResult(groupKey);
- if (tDigest != null) {
- tDigest.add(value);
- } else {
- // Create a new TDigest for the group
- groupByResultHolder.setValueForKey(groupKey, ObjectSerDeUtils.TDIGEST_SER_DE.deserialize(bytesValues[i]));
- }
+ getSerializedAccumulator(groupByResultHolder, groupKey, bytesValues[i])
+ .addSerializedTDigest(bytesValues[i]);
}
Review Comment:
Fixed in 633aab66c6. aggregateGroupByMV() now resets one invocation-local SerializedTDigestInput once per non-null row, lazily decodes centroids once, and reuses the read-only primitive arrays for every group key. New groups retain only the serialized bytes, so raw and merge buffers stay lazy. Coverage includes compact and verbose inputs, existing and new groups, duplicate keys, empty digests, and zero-key rows.
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileAggregationFunction.java:
##########
@@ -35,6 +35,8 @@
public class PercentileAggregationFunction extends NullableSingleInputAggregationFunction<DoubleArrayList, Double> {
private static final double DEFAULT_FINAL_RESULT = Double.NEGATIVE_INFINITY;
+ private static final int SORT_THRESHOLD = 32;
+ private static final int NINTHER_THRESHOLD = 128;
Review Comment:
Documented and renamed both constants in 633aab66c6. Direct sorting applies to at most 32 candidate values; Tukey's ninther starts at 128 or more. Existing selection tests bracket 31, 32, 33 and 127, 128, 129.
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunction.java:
##########
@@ -108,20 +108,20 @@ public void aggregate(int length, AggregationResultHolder aggregationResultHolde
if (blockValSet.getValueType() == DataType.BYTES) {
// Serialized TDigest
byte[][] bytesValues = blockValSet.getBytesValuesSV();
- foldNotNull(length, blockValSet, (TDigest) aggregationResultHolder.getResult(), (tDigest, from, toEx) -> {
- if (tDigest != null) {
- for (int i = from; i < toEx; i++) {
- tDigest.add(ObjectSerDeUtils.TDIGEST_SER_DE.deserialize(bytesValues[i]));
- }
- } else {
- tDigest = ObjectSerDeUtils.TDIGEST_SER_DE.deserialize(bytesValues[0]);
- aggregationResultHolder.setValue(tDigest);
- for (int i = 1; i < length; i++) {
- tDigest.add(ObjectSerDeUtils.TDIGEST_SER_DE.deserialize(bytesValues[i]));
+ PercentileTDigestAccumulator accumulator = foldNotNull(length, blockValSet,
Review Comment:
Restored the existing holder-set-inside-foldNotNull pattern in 633aab66c6. The range loop still uses from and toExclusive so leading and interior nulls remain correct.
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunction.java:
##########
@@ -268,21 +252,25 @@ protected void aggregateMVGroupByMV(int length, int[][] groupKeysArray, GroupByR
@Override
public TDigest extractAggregationResult(AggregationResultHolder aggregationResultHolder) {
- TDigest tDigest = aggregationResultHolder.getResult();
- if (tDigest == null) {
+ Object result = aggregationResultHolder.getResult();
+ if (result == null) {
return TDigest.createMergingDigest(_compressionFactor);
+ } else if (result instanceof PercentileTDigestAccumulator) {
+ return ((PercentileTDigestAccumulator) result).toTDigest();
} else {
- return tDigest;
+ return (TDigest) result;
Review Comment:
Not on a valid execution path. Non-group aggregation state is now typed as PercentileTDigestAccumulator, and 633aab66c6 removes the unreachable TDigest fallback. Null remains only for empty or all-null input.
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunction.java:
##########
@@ -268,21 +252,25 @@ protected void aggregateMVGroupByMV(int length, int[][] groupKeysArray, GroupByR
@Override
public TDigest extractAggregationResult(AggregationResultHolder aggregationResultHolder) {
- TDigest tDigest = aggregationResultHolder.getResult();
- if (tDigest == null) {
+ Object result = aggregationResultHolder.getResult();
+ if (result == null) {
return TDigest.createMergingDigest(_compressionFactor);
+ } else if (result instanceof PercentileTDigestAccumulator) {
+ return ((PercentileTDigestAccumulator) result).toTDigest();
} else {
- return tDigest;
+ return (TDigest) result;
}
}
@Override
public TDigest extractGroupByResult(GroupByResultHolder groupByResultHolder, int groupKey) {
- TDigest tDigest = groupByResultHolder.getResult(groupKey);
- if (tDigest == null) {
+ Object result = groupByResultHolder.getResult(groupKey);
+ if (result == null) {
return TDigest.createMergingDigest(_compressionFactor);
+ } else if (result instanceof PercentileTDigestAccumulator) {
+ return ((PercentileTDigestAccumulator) result).toTDigest();
} else {
- return tDigest;
+ return (TDigest) result;
Review Comment:
Yes. Raw numeric group-by still stores a TDigest, while serialized group-by stores PercentileTDigestAccumulator. I kept both branches and added testRawNumericGroupByStillUsesTDigestState to document the reachable TDigest case.
##########
pinot-core/src/test/java/org/apache/pinot/queries/PercentileTDigestMVQueriesTest.java:
##########
@@ -55,6 +56,12 @@
public class PercentileTDigestMVQueriesTest extends PercentileTDigestQueriesTest {
private static final int MAX_NUM_MULTI_VALUES = 10;
+ @Override
+ @Test(enabled = false)
+ public void testStarTreeGroupBy() {
+ // The StarTree fixture stores the single-value TDigest aggregation only.
Review Comment:
Yes, it works. I enabled the inherited test and added a matching real StarTree configuration to the MV fixture. The suite now passes all 6 tests, including testStarTreeGroupBy. Expected total digest weight counts MV values instead of documents, and the documents-scanned assertion confirms the StarTree path.
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAccumulator.java:
##########
@@ -0,0 +1,448 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.core.query.aggregation.function;
+
+import com.tdunning.math.stats.MergingDigest;
+import com.tdunning.math.stats.TDigest;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/// Accumulates raw values and serialized TDigests into centroids without repeatedly sorting existing centroids.
+///
+/// The accumulator is used while a percentile TDigest result, or one of its group-by results, is being built. Raw
+/// values are sorted in small batches, while serialized TDigest centroids are decoded in their existing sorted order.
+/// Both are linearly merged with the accumulated centroids and compressed using the same weight-limit rule as
+/// [MergingDigest]. [#toTDigest()] normalizes the result to a standard [MergingDigest], so intermediate-result
+/// serialization and distributed merging remain unchanged.
+///
+/// Serialized group state keeps its first digest pending, and allocates primitive centroid buffers only when another
+/// input must be merged. The raw-value buffer remains unallocated for serialized state until a raw value is added.
+///
+/// Instances are thread-confined to one aggregation result or group-by result.
+final class PercentileTDigestAccumulator {
Review Comment:
Kept this as a package-private ingestion adapter, not a TDigest replacement. It owns only raw and serialized ingestion plus linear centroid merging. Quantile, CDF, centroid iteration, intermediate serialization, and distributed merging remain on the canonical MergingDigest returned by toTDigest(). Implementing TDigest directly would duplicate the full library API and wire format. The class documentation now makes that boundary explicit.
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAccumulator.java:
##########
@@ -0,0 +1,448 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.core.query.aggregation.function;
+
+import com.tdunning.math.stats.MergingDigest;
+import com.tdunning.math.stats.TDigest;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/// Accumulates raw values and serialized TDigests into centroids without repeatedly sorting existing centroids.
+///
+/// The accumulator is used while a percentile TDigest result, or one of its group-by results, is being built. Raw
+/// values are sorted in small batches, while serialized TDigest centroids are decoded in their existing sorted order.
+/// Both are linearly merged with the accumulated centroids and compressed using the same weight-limit rule as
+/// [MergingDigest]. [#toTDigest()] normalizes the result to a standard [MergingDigest], so intermediate-result
+/// serialization and distributed merging remain unchanged.
+///
+/// Serialized group state keeps its first digest pending, and allocates primitive centroid buffers only when another
+/// input must be merged. The raw-value buffer remains unallocated for serialized state until a raw value is added.
+///
+/// Instances are thread-confined to one aggregation result or group-by result.
+final class PercentileTDigestAccumulator {
+ private static final int MIN_RAW_BUFFER_SIZE = 256;
+ private static final int MAX_RAW_BUFFER_SIZE = 10_000;
+ private static final int CENTROID_CAPACITY_PADDING = 10;
+ private static final int VERBOSE_ENCODING = 1;
+ private static final int SMALL_ENCODING = 2;
+ private static final int VERBOSE_HEADER_SIZE = 32;
+ private static final int VERBOSE_CENTROID_SIZE = 16;
+ private static final int SMALL_CENTROID_SIZE = 8;
+
+ private final double _compression;
+ private final int _centroidCapacity;
+ private double[] _rawValues;
+ private double[] _centroidMeans;
+ private double[] _centroidWeights;
+ private double[] _outputMeans;
+ private double[] _outputWeights;
+ private double[] _incomingMeans;
+ private double[] _incomingWeights;
+ private byte[] _pendingSerializedTDigest;
+ private boolean _hasSerializedInput;
+
+ private int _numRawValues;
+ private int _numCentroids;
+ private double _totalWeight;
+ private double _min = Double.POSITIVE_INFINITY;
+ private double _max = Double.NEGATIVE_INFINITY;
+
+ PercentileTDigestAccumulator(int compression) {
+ this((double) compression, true, true);
+ }
+
+ private PercentileTDigestAccumulator(double compression, boolean allocateRawBuffer, boolean allocateMergeBuffers) {
+ if (!(compression > 0.0) || !Double.isFinite(compression)) {
+ throw new IllegalArgumentException("TDigest compression must be positive: " + compression);
+ }
+ _compression = compression;
+ long roundedCompression = (long) Math.ceil(compression);
+ _centroidCapacity = Math.toIntExact(
+ Math.addExact(Math.multiplyExact(2L, roundedCompression), CENTROID_CAPACITY_PADDING));
+ if (allocateRawBuffer) {
+ _rawValues = new double[getRawBufferSize(roundedCompression)];
+ }
+ if (allocateMergeBuffers) {
+ _centroidMeans = new double[_centroidCapacity];
+ _centroidWeights = new double[_centroidCapacity];
+ _outputMeans = new double[_centroidCapacity];
+ _outputWeights = new double[_centroidCapacity];
+ }
+ }
+
+ static PercentileTDigestAccumulator forSerializedTDigest(byte[] bytes) {
+ return new PercentileTDigestAccumulator(readCompression(bytes), false, false);
+ }
+
+ static PercentileTDigestAccumulator forSerializedTDigestWithMergeBuffers(byte[] bytes) {
+ return new PercentileTDigestAccumulator(readCompression(bytes), false, true);
+ }
+
+ void add(double[] values, int from, int toExclusive) {
+ if (from >= toExclusive) {
+ return;
+ }
+ materializePendingSerializedTDigest();
+ ensureRawBuffer();
+ while (from < toExclusive) {
+ int numValues = Math.min(toExclusive - from, _rawValues.length - _numRawValues);
+ int rawOffset = _numRawValues;
+ for (int i = 0; i < numValues; i++) {
+ double value = values[from + i];
+ if (Double.isNaN(value)) {
+ throw new IllegalArgumentException("Cannot add NaN to t-digest");
+ }
+ _rawValues[rawOffset + i] = value;
+ }
+ from += numValues;
+ _numRawValues += numValues;
+ if (_numRawValues == _rawValues.length) {
+ flush();
+ }
+ }
+ }
+
+ void addSerializedTDigest(byte[] bytes) {
+ flush();
+ if (!_hasSerializedInput && _numCentroids == 0 && _totalWeight == 0.0) {
+ validateSerializedTDigest(bytes);
+ _pendingSerializedTDigest = bytes;
+ _hasSerializedInput = true;
+ return;
+ }
+ materializePendingSerializedTDigest();
+ mergeSerializedTDigest(bytes);
+ _hasSerializedInput = true;
+ }
+
+ private void mergeSerializedTDigest(byte[] bytes) {
+
+ ByteBuffer input = ByteBuffer.wrap(bytes);
+ int encoding = input.getInt();
+ double min = input.getDouble();
+ double max = input.getDouble();
+ int numCentroids;
+ boolean initialize = _numCentroids == 0 && _totalWeight == 0.0;
+ double[] incomingMeans;
+ double[] incomingWeights;
+ if (encoding == VERBOSE_ENCODING) {
+ input.getDouble();
+ numCentroids = input.getInt();
+ checkCentroidCount(numCentroids);
+ checkCentroidsAvailable(input, numCentroids, VERBOSE_CENTROID_SIZE);
+ if (initialize) {
+ ensureCentroidCapacity(numCentroids);
+ incomingMeans = _centroidMeans;
+ incomingWeights = _centroidWeights;
+ } else {
+ ensureIncomingCapacity(numCentroids);
+ incomingMeans = _incomingMeans;
+ incomingWeights = _incomingWeights;
+ }
+ for (int i = 0; i < numCentroids; i++) {
+ incomingWeights[i] = input.getDouble();
+ incomingMeans[i] = input.getDouble();
+ }
+ } else if (encoding == SMALL_ENCODING) {
+ input.getFloat();
+ input.getShort();
+ input.getShort();
+ numCentroids = input.getShort();
+ checkCentroidCount(numCentroids);
+ checkCentroidsAvailable(input, numCentroids, SMALL_CENTROID_SIZE);
+ if (initialize) {
+ ensureCentroidCapacity(numCentroids);
+ incomingMeans = _centroidMeans;
+ incomingWeights = _centroidWeights;
+ } else {
+ ensureIncomingCapacity(numCentroids);
+ incomingMeans = _incomingMeans;
+ incomingWeights = _incomingWeights;
+ }
+ for (int i = 0; i < numCentroids; i++) {
+ incomingWeights[i] = input.getFloat();
+ incomingMeans[i] = input.getFloat();
+ }
+ } else {
+ throw new IllegalStateException("Invalid format for serialized histogram");
+ }
+
+ if (initialize) {
+ double totalWeight = getTotalWeight(incomingWeights, numCentroids);
+ if (totalWeight != 0.0) {
+ _numCentroids = numCentroids;
+ _totalWeight = totalWeight;
+ _min = min;
+ _max = max;
+ }
+ } else if (mergeSorted(incomingMeans, incomingWeights, numCentroids)) {
+ _min = Math.min(_min, min);
+ _max = Math.max(_max, max);
+ }
+ }
+
+ TDigest toTDigest() {
+ if (_pendingSerializedTDigest != null) {
+ return MergingDigest.fromBytes(ByteBuffer.wrap(_pendingSerializedTDigest));
+ }
+ flush();
+ if (_numCentroids == 0) {
+ return TDigest.createMergingDigest(_compression);
+ }
+
+ ByteBuffer buffer = ByteBuffer.allocate(VERBOSE_HEADER_SIZE + VERBOSE_CENTROID_SIZE * _numCentroids);
Review Comment:
Fixed in 633aab66c6. Materialized oversized compact input now preserves a valid main and merge-buffer capacity. When the selected compression is not exactly float-representable, centroids are recompressed and emitted verbose so compression remains exact. Fully populated oversized verbose input and malformed compact capacities still fail with canonical exception parity. Regression coverage includes an 80-centroid compression-20 compact input, mixed compression 20.0000001, a subsequent add, compact sentinels, and malformed capacities.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org
Re: [PR] [core] Optimize TDigest percentile aggregation, reduction, and StarTree paths [pinot]
Posted by "xiangfu0 (via GitHub)" <gi...@apache.org>.
xiangfu0 merged PR #18996:
URL: https://github.com/apache/pinot/pull/18996
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org
Re: [PR] [core] Optimize exact and TDigest percentile aggregation, including StarTree group-by [pinot]
Posted by "xiangfu0 (via GitHub)" <gi...@apache.org>.
xiangfu0 commented on PR #18996:
URL: https://github.com/apache/pinot/pull/18996#issuecomment-4990254170
Reducer/group-by follow-up is now pushed in b7b01bd667.
Benchmark highlights (TDigest 3.2, 2 forks × 5 measurements):
- Representative full IndexedTable combine, 32-way fan-in: 0.2683 ms pairwise → 0.0340 ms promoted local (7.90×), 0.0201 ms accumulator-local (13.33×), and 0.0233 ms serialized/wire (11.52×).
- Production-shaped 1,440 groups × 7 metrics × 32 segments: 4,574.4 ± 309.3 ms pairwise → 223.0 ± 2.3 ms accumulator-local (20.51×) and 283.0 ± 6.2 ms wire (16.16×). Allocation fell 93.87% and 92.22%.
- The 36 workload shapes × 4 implementations retained exact weight, finite positive centroids, monotonic quantiles, and standard TDigest 3.2 wire bytes. Maximum sampled P75 error was 0.001353 pairwise versus 0.000709 for the primitive candidates.
- The dependency-only TDigest 3.3 experiment was a no-go despite a 4.47× kernel geometric-mean speedup: mean P75 error increased 18.9× and the maximum sampled error reached 0.01138.
The full report and reproduction commands are in pinot-perf/benchmark-results/percentile-aggregation-100m-results.md.
Validation: 318 focused reducer/serde/concurrency tests passed; the 64-module warning-enabled test compilation passed (921 goals); spotless, checkstyle, license format/check, and diff-check all pass. New CI is running on this head.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org
Re: [PR] [core] Optimize exact and TDigest percentile aggregation [pinot]
Posted by "codecov-commenter (via GitHub)" <gi...@apache.org>.
codecov-commenter commented on PR #18996:
URL: https://github.com/apache/pinot/pull/18996#issuecomment-4984564042
## [Codecov](https://app.codecov.io/gh/apache/pinot/pull/18996?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
:x: Patch coverage is `97.04142%` with `5 lines` in your changes missing coverage. Please review.
:white_check_mark: Project coverage is 56.88%. Comparing base ([`068b458`](https://app.codecov.io/gh/apache/pinot/commit/068b458883d645ee2c686ab2a5e123ef2ffe8cff?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)) to head ([`52f9ca2`](https://app.codecov.io/gh/apache/pinot/commit/52f9ca25496d30f3f9b048933f2bb64979279a4b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)).
:warning: Report is 2 commits behind head on master.
| [Files with missing lines](https://app.codecov.io/gh/apache/pinot/pull/18996?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Patch % | Lines |
|---|---|---|
| [...egation/function/PercentileTDigestAccumulator.java](https://app.codecov.io/gh/apache/pinot/pull/18996?src=pr&el=tree&filepath=pinot-core%2Fsrc%2Fmain%2Fjava%2Forg%2Fapache%2Fpinot%2Fcore%2Fquery%2Faggregation%2Ffunction%2FPercentileTDigestAccumulator.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-cGlub3QtY29yZS9zcmMvbWFpbi9qYXZhL29yZy9hcGFjaGUvcGlub3QvY29yZS9xdWVyeS9hZ2dyZWdhdGlvbi9mdW5jdGlvbi9QZXJjZW50aWxlVERpZ2VzdEFjY3VtdWxhdG9yLmphdmE=) | 95.55% | [2 Missing and 2 partials :warning: ](https://app.codecov.io/gh/apache/pinot/pull/18996?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [...function/PercentileTDigestAggregationFunction.java](https://app.codecov.io/gh/apache/pinot/pull/18996?src=pr&el=tree&filepath=pinot-core%2Fsrc%2Fmain%2Fjava%2Forg%2Fapache%2Fpinot%2Fcore%2Fquery%2Faggregation%2Ffunction%2FPercentileTDigestAggregationFunction.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-cGlub3QtY29yZS9zcmMvbWFpbi9qYXZhL29yZy9hcGFjaGUvcGlub3QvY29yZS9xdWVyeS9hZ2dyZWdhdGlvbi9mdW5jdGlvbi9QZXJjZW50aWxlVERpZ2VzdEFnZ3JlZ2F0aW9uRnVuY3Rpb24uamF2YQ==) | 93.33% | [0 Missing and 1 partial :warning: ](https://app.codecov.io/gh/apache/pinot/pull/18996?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
> :exclamation: There is a different number of reports uploaded between BASE (068b458) and HEAD (52f9ca2). Click for more details.
>
> <details><summary>HEAD has 16 uploads less than BASE</summary>
>
>| Flag | BASE (068b458) | HEAD (52f9ca2) |
>|------|------|------|
>|java-21|5|1|
>|unittests|2|1|
>|temurin|5|1|
>|unittests2|1|0|
>|integration|3|0|
>|integration1|1|0|
>|integration2|1|0|
>|custom-integration1|1|0|
></details>
<details><summary>Additional details and impacted files</summary>
```diff
@@ Coverage Diff @@
## master #18996 +/- ##
============================================
- Coverage 65.22% 56.88% -8.35%
+ Complexity 1405 1 -1404
============================================
Files 3418 2623 -795
Lines 214651 155168 -59483
Branches 33922 25302 -8620
============================================
- Hits 140006 88262 -51744
+ Misses 63356 59359 -3997
+ Partials 11289 7547 -3742
```
| [Flag](https://app.codecov.io/gh/apache/pinot/pull/18996/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | |
|---|---|---|
| [custom-integration1](https://app.codecov.io/gh/apache/pinot/pull/18996/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `?` | |
| [integration](https://app.codecov.io/gh/apache/pinot/pull/18996/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `?` | |
| [integration1](https://app.codecov.io/gh/apache/pinot/pull/18996/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `?` | |
| [integration2](https://app.codecov.io/gh/apache/pinot/pull/18996/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `?` | |
| [java-21](https://app.codecov.io/gh/apache/pinot/pull/18996/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `56.88% <97.04%> (-8.35%)` | :arrow_down: |
| [temurin](https://app.codecov.io/gh/apache/pinot/pull/18996/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `56.88% <97.04%> (-8.35%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/apache/pinot/pull/18996/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `56.88% <97.04%> (-8.35%)` | :arrow_down: |
| [unittests1](https://app.codecov.io/gh/apache/pinot/pull/18996/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `56.88% <97.04%> (+0.01%)` | :arrow_up: |
| [unittests2](https://app.codecov.io/gh/apache/pinot/pull/18996/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `?` | |
Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#carryforward-flags-in-the-pull-request-comment) to find out more.
</details>
[:umbrella: View full report in Codecov by Harness](https://app.codecov.io/gh/apache/pinot/pull/18996?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
<details><summary> :rocket: New features to boost your workflow: </summary>
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org
Re: [PR] [core] Optimize exact and TDigest percentile aggregation, including StarTree group-by [pinot]
Posted by "xiangfu0 (via GitHub)" <gi...@apache.org>.
xiangfu0 commented on code in PR #18996:
URL: https://github.com/apache/pinot/pull/18996#discussion_r3594229701
##########
pinot-segment-local/src/test/java/org/apache/pinot/segment/local/aggregator/PercentileTDigestValueAggregatorTest.java:
##########
@@ -0,0 +1,150 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.segment.local.aggregator;
+
+import com.tdunning.math.stats.TDigest;
+import java.nio.ByteBuffer;
+import java.util.List;
+import org.apache.pinot.common.request.Literal;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.segment.local.utils.CustomSerDeUtils;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+/// Tests the serialization-size contract and deferred compression used by [PercentileTDigestValueAggregator].
+public class PercentileTDigestValueAggregatorTest {
+
+ @Test(dataProvider = "compressionBounds")
+ public void testRawValuesTrackMaxVerboseSizeWithoutCompression(int compression, int numValues,
+ int expectedMaxByteSize) {
+ PercentileTDigestValueAggregator aggregator = newAggregator(compression);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), 0);
+
+ TDigest digest = aggregator.getInitialAggregatedValue(0.0);
+ for (int i = 1; i < numValues; i++) {
+ aggregator.applyRawValue(digest, i);
+ }
+ assertEquals(digest.centroidCount(), 0);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), expectedMaxByteSize);
+ byte[] serialized = aggregator.serializeAggregatedValue(digest);
+ assertTrue(serialized.length <= expectedMaxByteSize);
+ }
+
+ @DataProvider
+ public static Object[][] compressionBounds() {
+ return new Object[][]{
+ {10, 30, 512},
+ {100, 210, 3_392},
+ {1_000, 2_010, 32_192}
+ };
+ }
+
+ @Test
+ public void testRawUpdatesDoNotForceCompression() {
+ PercentileTDigestValueAggregator aggregator = newAggregator(100);
+ TDigest digest = aggregator.getInitialAggregatedValue(0.0);
+ for (int i = 1; i < 256; i++) {
+ aggregator.applyRawValue(digest, i);
+ }
+
+ assertEquals(digest.size(), 256L);
+ assertEquals(digest.centroidCount(), 0);
+ byte[] serialized = aggregator.serializeAggregatedValue(digest);
+ assertTrue(digest.centroidCount() > 0);
+ assertTrue(serialized.length <= aggregator.getMaxAggregatedValueByteSize());
Review Comment:
Fixed in bb9600bc63. The test now captures getMaxAggregatedValueByteSize() before serialization and compares the serialized length against that pre-serialization bound.
##########
pinot-segment-local/src/test/java/org/apache/pinot/segment/local/aggregator/PercentileTDigestValueAggregatorTest.java:
##########
@@ -0,0 +1,150 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.segment.local.aggregator;
+
+import com.tdunning.math.stats.TDigest;
+import java.nio.ByteBuffer;
+import java.util.List;
+import org.apache.pinot.common.request.Literal;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.segment.local.utils.CustomSerDeUtils;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+/// Tests the serialization-size contract and deferred compression used by [PercentileTDigestValueAggregator].
+public class PercentileTDigestValueAggregatorTest {
+
+ @Test(dataProvider = "compressionBounds")
+ public void testRawValuesTrackMaxVerboseSizeWithoutCompression(int compression, int numValues,
+ int expectedMaxByteSize) {
+ PercentileTDigestValueAggregator aggregator = newAggregator(compression);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), 0);
+
+ TDigest digest = aggregator.getInitialAggregatedValue(0.0);
+ for (int i = 1; i < numValues; i++) {
+ aggregator.applyRawValue(digest, i);
+ }
+ assertEquals(digest.centroidCount(), 0);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), expectedMaxByteSize);
+ byte[] serialized = aggregator.serializeAggregatedValue(digest);
+ assertTrue(serialized.length <= expectedMaxByteSize);
+ }
+
+ @DataProvider
+ public static Object[][] compressionBounds() {
+ return new Object[][]{
+ {10, 30, 512},
+ {100, 210, 3_392},
+ {1_000, 2_010, 32_192}
+ };
+ }
+
+ @Test
+ public void testRawUpdatesDoNotForceCompression() {
+ PercentileTDigestValueAggregator aggregator = newAggregator(100);
+ TDigest digest = aggregator.getInitialAggregatedValue(0.0);
+ for (int i = 1; i < 256; i++) {
+ aggregator.applyRawValue(digest, i);
+ }
+
+ assertEquals(digest.size(), 256L);
+ assertEquals(digest.centroidCount(), 0);
+ byte[] serialized = aggregator.serializeAggregatedValue(digest);
+ assertTrue(digest.centroidCount() > 0);
+ assertTrue(serialized.length <= aggregator.getMaxAggregatedValueByteSize());
+ }
+
+ @Test
+ public void testPreAggregatedCompressionExpandsRegisteredBound() {
+ TDigest input = TDigest.createMergingDigest(200);
+ input.add(42.0);
+ byte[] inputBytes = CustomSerDeUtils.TDIGEST_SER_DE.serialize(input);
+
+ PercentileTDigestValueAggregator aggregator = newAggregator(10);
+ TDigest result = aggregator.getInitialAggregatedValue(inputBytes);
+ for (int i = 0; i < 409; i++) {
+ aggregator.applyRawValue(result, i);
+ }
+
+ assertEquals(result.compression(), 200.0);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), 6_592);
+ assertTrue(aggregator.serializeAggregatedValue(result).length <= aggregator.getMaxAggregatedValueByteSize());
Review Comment:
Fixed in bb9600bc63. The registered maximum is captured before serializeAggregatedValue(), so this now validates the pre-serialization sizing contract.
##########
pinot-perf/src/main/java/org/apache/pinot/perf/aggregation/BenchmarkPercentileTDigestValueAggregator.java:
##########
@@ -0,0 +1,104 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.perf.aggregation;
+
+import com.tdunning.math.stats.TDigest;
+import java.util.List;
+import java.util.SplittableRandom;
+import java.util.concurrent.TimeUnit;
+import org.apache.pinot.segment.local.aggregator.PercentileTDigestValueAggregator;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+/// Measures the raw-value TDigest aggregation kernel used while constructing a star-tree index.
+///
+/// The default models one million source rows split into groups of 1,000 rows with identical dimension keys. Each
+/// group is serialized because every aggregate eventually becomes a star-tree forward-index entry. Use
+/// `-p _numRows=100000000` for a 100-million-row run and vary `_rowsPerGroup` to model the table's dimension
+/// cardinality. This deliberately excludes dimension sorting and forward-index I/O so it isolates
+/// [PercentileTDigestValueAggregator].
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Warmup(iterations = 2, time = 1)
+@Measurement(iterations = 5, time = 1)
+@Fork(value = 2, jvmArgsAppend = {"-Xms4g", "-Xmx8g"})
+@Threads(1)
+public class BenchmarkPercentileTDigestValueAggregator {
+ private static final int VALUE_BLOCK_SIZE = 1 << 16;
+
+ @Param({"1000000"})
+ int _numRows;
+
+ @Param({"1000"})
+ int _rowsPerGroup;
+
+ private double[] _values;
+
+ @Setup
+ public void setUp() {
+ if (_numRows <= 0) {
+ throw new IllegalArgumentException("numRows must be positive");
+ }
+ if (_rowsPerGroup <= 0) {
+ throw new IllegalArgumentException("rowsPerGroup must be positive");
+ }
+ _values = new double[VALUE_BLOCK_SIZE];
+ SplittableRandom random = new SplittableRandom(42);
+ for (int i = 0; i < VALUE_BLOCK_SIZE; i++) {
+ _values[i] = random.nextDouble();
+ }
+ }
+
+ @Benchmark
+ public long aggregateAndSerializeRawValues() {
+ PercentileTDigestValueAggregator aggregator = new PercentileTDigestValueAggregator(List.of());
+ long totalAggregatedRows = 0;
+ long totalSerializedBytes = 0;
+ for (int from = 0; from < _numRows; from += _rowsPerGroup) {
+ int to = Math.min(_numRows, from + _rowsPerGroup);
+ TDigest digest = aggregator.getInitialAggregatedValue(_values[from & (VALUE_BLOCK_SIZE - 1)]);
+ for (int i = from + 1; i < to; i++) {
+ aggregator.applyRawValue(digest, _values[i & (VALUE_BLOCK_SIZE - 1)]);
+ }
+ byte[] serialized = aggregator.serializeAggregatedValue(digest);
+ if (serialized.length > aggregator.getMaxAggregatedValueByteSize()) {
+ throw new IllegalStateException(
+ "Serialized TDigest exceeds registered maximum: " + serialized.length + " > "
+ + aggregator.getMaxAggregatedValueByteSize());
+ }
Review Comment:
Fixed in bb9600bc63. The benchmark captures the registered maximum before serializeAggregatedValue() and uses that saved bound in both the check and failure message.
##########
pinot-segment-local/src/test/java/org/apache/pinot/segment/local/aggregator/PercentileTDigestValueAggregatorTest.java:
##########
@@ -0,0 +1,150 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.segment.local.aggregator;
+
+import com.tdunning.math.stats.TDigest;
+import java.nio.ByteBuffer;
+import java.util.List;
+import org.apache.pinot.common.request.Literal;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.segment.local.utils.CustomSerDeUtils;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+/// Tests the serialization-size contract and deferred compression used by [PercentileTDigestValueAggregator].
+public class PercentileTDigestValueAggregatorTest {
+
+ @Test(dataProvider = "compressionBounds")
+ public void testRawValuesTrackMaxVerboseSizeWithoutCompression(int compression, int numValues,
+ int expectedMaxByteSize) {
+ PercentileTDigestValueAggregator aggregator = newAggregator(compression);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), 0);
+
+ TDigest digest = aggregator.getInitialAggregatedValue(0.0);
+ for (int i = 1; i < numValues; i++) {
+ aggregator.applyRawValue(digest, i);
+ }
+ assertEquals(digest.centroidCount(), 0);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), expectedMaxByteSize);
+ byte[] serialized = aggregator.serializeAggregatedValue(digest);
+ assertTrue(serialized.length <= expectedMaxByteSize);
+ }
+
+ @DataProvider
+ public static Object[][] compressionBounds() {
+ return new Object[][]{
+ {10, 30, 512},
+ {100, 210, 3_392},
+ {1_000, 2_010, 32_192}
+ };
+ }
+
+ @Test
+ public void testRawUpdatesDoNotForceCompression() {
+ PercentileTDigestValueAggregator aggregator = newAggregator(100);
+ TDigest digest = aggregator.getInitialAggregatedValue(0.0);
+ for (int i = 1; i < 256; i++) {
+ aggregator.applyRawValue(digest, i);
+ }
+
+ assertEquals(digest.size(), 256L);
+ assertEquals(digest.centroidCount(), 0);
+ byte[] serialized = aggregator.serializeAggregatedValue(digest);
+ assertTrue(digest.centroidCount() > 0);
+ assertTrue(serialized.length <= aggregator.getMaxAggregatedValueByteSize());
+ }
+
+ @Test
+ public void testPreAggregatedCompressionExpandsRegisteredBound() {
+ TDigest input = TDigest.createMergingDigest(200);
+ input.add(42.0);
+ byte[] inputBytes = CustomSerDeUtils.TDIGEST_SER_DE.serialize(input);
+
+ PercentileTDigestValueAggregator aggregator = newAggregator(10);
+ TDigest result = aggregator.getInitialAggregatedValue(inputBytes);
+ for (int i = 0; i < 409; i++) {
+ aggregator.applyRawValue(result, i);
+ }
+
+ assertEquals(result.compression(), 200.0);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), 6_592);
+ assertTrue(aggregator.serializeAggregatedValue(result).length <= aggregator.getMaxAggregatedValueByteSize());
+ }
+
+ @Test
+ public void testOversizedSmallEncodedDigestRemainsReadable() {
+ int centroidCount = 300;
+ byte[] smallEncoding = createSmallEncoding(10, 400, 500, centroidCount);
+
+ PercentileTDigestValueAggregator aggregator = newAggregator(10);
+ TDigest result = aggregator.getInitialAggregatedValue(smallEncoding);
+ byte[] serialized = aggregator.serializeAggregatedValue(result);
+ TDigest clone = aggregator.cloneAggregatedValue(result);
+
+ assertEquals(result.centroidCount(), centroidCount);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), 4_832);
+ assertEquals(ByteBuffer.wrap(serialized).getInt(), 2);
+ assertEquals(serialized.length, smallEncoding.length);
+ assertTrue(serialized.length <= aggregator.getMaxAggregatedValueByteSize());
+ assertEquals(clone.size(), result.size());
+ assertEquals(clone.centroidCount(), result.centroidCount());
+ assertEquals(clone.quantile(0.75), result.quantile(0.75));
+ }
+
+ @Test
+ public void testAggregatedUpdateDoesNotForceDestinationCompression() {
+ PercentileTDigestValueAggregator aggregator = newAggregator(100);
+ TDigest destination = aggregator.getInitialAggregatedValue(1.0);
+ TDigest source = TDigest.createMergingDigest(100);
+ source.add(2.0);
+ source.add(3.0);
+
+ aggregator.applyAggregatedValue(destination, source);
+
+ assertEquals(destination.size(), 3L);
+ assertEquals(destination.centroidCount(), 0);
+ byte[] serialized = aggregator.serializeAggregatedValue(destination);
+ assertTrue(serialized.length <= aggregator.getMaxAggregatedValueByteSize());
Review Comment:
Fixed in bb9600bc63. The destination bound is captured before serialization and the result is checked against that saved value.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org
Re: [PR] [core] Optimize exact and TDigest percentile aggregation, including StarTree group-by [pinot]
Posted by "xiangfu0 (via GitHub)" <gi...@apache.org>.
xiangfu0 commented on code in PR #18996:
URL: https://github.com/apache/pinot/pull/18996#discussion_r3594210060
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAccumulator.java:
##########
@@ -0,0 +1,448 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.core.query.aggregation.function;
+
+import com.tdunning.math.stats.MergingDigest;
+import com.tdunning.math.stats.TDigest;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/// Accumulates raw values and serialized TDigests into centroids without repeatedly sorting existing centroids.
+///
+/// The accumulator is used while a percentile TDigest result, or one of its group-by results, is being built. Raw
+/// values are sorted in small batches, while serialized TDigest centroids are decoded in their existing sorted order.
+/// Both are linearly merged with the accumulated centroids and compressed using the same weight-limit rule as
+/// [MergingDigest]. [#toTDigest()] normalizes the result to a standard [MergingDigest], so intermediate-result
+/// serialization and distributed merging remain unchanged.
+///
+/// Serialized group state keeps its first digest pending, and allocates primitive centroid buffers only when another
+/// input must be merged. The raw-value buffer remains unallocated for serialized state until a raw value is added.
+///
+/// Instances are thread-confined to one aggregation result or group-by result.
+final class PercentileTDigestAccumulator {
Review Comment:
Follow-up in b7b01bd667: the reducer-specific optimization now makes PercentileTDigestAccumulator extend TDigest, so process-local IndexedTable reduction and distributed deserialization retain primitive centroid state. The public/wire representation is still canonical TDigest 3.2 bytes, and serialized group state keeps both centroid buffers and the raw-value buffer lazy until needed.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org
Re: [PR] [core] Optimize exact and TDigest percentile aggregation, including StarTree group-by [pinot]
Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #18996:
URL: https://github.com/apache/pinot/pull/18996#discussion_r3593427044
##########
pinot-segment-local/src/test/java/org/apache/pinot/segment/local/aggregator/PercentileTDigestValueAggregatorTest.java:
##########
@@ -0,0 +1,150 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.segment.local.aggregator;
+
+import com.tdunning.math.stats.TDigest;
+import java.nio.ByteBuffer;
+import java.util.List;
+import org.apache.pinot.common.request.Literal;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.segment.local.utils.CustomSerDeUtils;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+/// Tests the serialization-size contract and deferred compression used by [PercentileTDigestValueAggregator].
+public class PercentileTDigestValueAggregatorTest {
+
+ @Test(dataProvider = "compressionBounds")
+ public void testRawValuesTrackMaxVerboseSizeWithoutCompression(int compression, int numValues,
+ int expectedMaxByteSize) {
+ PercentileTDigestValueAggregator aggregator = newAggregator(compression);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), 0);
+
+ TDigest digest = aggregator.getInitialAggregatedValue(0.0);
+ for (int i = 1; i < numValues; i++) {
+ aggregator.applyRawValue(digest, i);
+ }
+ assertEquals(digest.centroidCount(), 0);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), expectedMaxByteSize);
+ byte[] serialized = aggregator.serializeAggregatedValue(digest);
+ assertTrue(serialized.length <= expectedMaxByteSize);
+ }
+
+ @DataProvider
+ public static Object[][] compressionBounds() {
+ return new Object[][]{
+ {10, 30, 512},
+ {100, 210, 3_392},
+ {1_000, 2_010, 32_192}
+ };
+ }
+
+ @Test
+ public void testRawUpdatesDoNotForceCompression() {
+ PercentileTDigestValueAggregator aggregator = newAggregator(100);
+ TDigest digest = aggregator.getInitialAggregatedValue(0.0);
+ for (int i = 1; i < 256; i++) {
+ aggregator.applyRawValue(digest, i);
+ }
+
+ assertEquals(digest.size(), 256L);
+ assertEquals(digest.centroidCount(), 0);
+ byte[] serialized = aggregator.serializeAggregatedValue(digest);
+ assertTrue(digest.centroidCount() > 0);
+ assertTrue(serialized.length <= aggregator.getMaxAggregatedValueByteSize());
+ }
+
+ @Test
+ public void testPreAggregatedCompressionExpandsRegisteredBound() {
+ TDigest input = TDigest.createMergingDigest(200);
+ input.add(42.0);
+ byte[] inputBytes = CustomSerDeUtils.TDIGEST_SER_DE.serialize(input);
+
+ PercentileTDigestValueAggregator aggregator = newAggregator(10);
+ TDigest result = aggregator.getInitialAggregatedValue(inputBytes);
+ for (int i = 0; i < 409; i++) {
+ aggregator.applyRawValue(result, i);
+ }
+
+ assertEquals(result.compression(), 200.0);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), 6_592);
+ assertTrue(aggregator.serializeAggregatedValue(result).length <= aggregator.getMaxAggregatedValueByteSize());
Review Comment:
This assertion serializes inside the check and then compares to `getMaxAggregatedValueByteSize()`, but serialization itself can increase the recorded max. To actually verify the aggregator’s registered bound is sufficient *before* serialization, capture the max first, then serialize and compare the resulting length against the captured value.
##########
pinot-segment-local/src/test/java/org/apache/pinot/segment/local/aggregator/PercentileTDigestValueAggregatorTest.java:
##########
@@ -0,0 +1,150 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.segment.local.aggregator;
+
+import com.tdunning.math.stats.TDigest;
+import java.nio.ByteBuffer;
+import java.util.List;
+import org.apache.pinot.common.request.Literal;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.segment.local.utils.CustomSerDeUtils;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+/// Tests the serialization-size contract and deferred compression used by [PercentileTDigestValueAggregator].
+public class PercentileTDigestValueAggregatorTest {
+
+ @Test(dataProvider = "compressionBounds")
+ public void testRawValuesTrackMaxVerboseSizeWithoutCompression(int compression, int numValues,
+ int expectedMaxByteSize) {
+ PercentileTDigestValueAggregator aggregator = newAggregator(compression);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), 0);
+
+ TDigest digest = aggregator.getInitialAggregatedValue(0.0);
+ for (int i = 1; i < numValues; i++) {
+ aggregator.applyRawValue(digest, i);
+ }
+ assertEquals(digest.centroidCount(), 0);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), expectedMaxByteSize);
+ byte[] serialized = aggregator.serializeAggregatedValue(digest);
+ assertTrue(serialized.length <= expectedMaxByteSize);
+ }
+
+ @DataProvider
+ public static Object[][] compressionBounds() {
+ return new Object[][]{
+ {10, 30, 512},
+ {100, 210, 3_392},
+ {1_000, 2_010, 32_192}
+ };
+ }
+
+ @Test
+ public void testRawUpdatesDoNotForceCompression() {
+ PercentileTDigestValueAggregator aggregator = newAggregator(100);
+ TDigest digest = aggregator.getInitialAggregatedValue(0.0);
+ for (int i = 1; i < 256; i++) {
+ aggregator.applyRawValue(digest, i);
+ }
+
+ assertEquals(digest.size(), 256L);
+ assertEquals(digest.centroidCount(), 0);
+ byte[] serialized = aggregator.serializeAggregatedValue(digest);
+ assertTrue(digest.centroidCount() > 0);
+ assertTrue(serialized.length <= aggregator.getMaxAggregatedValueByteSize());
Review Comment:
`serializeAggregatedValue()` updates `_maxByteSize` internally, so asserting `serialized.length <= aggregator.getMaxAggregatedValueByteSize()` after serialization is not validating the pre-serialization sizing contract (it will always pass unless serialization fails to update the max). Capture the max *before* serialization and compare against that instead.
##########
pinot-segment-local/src/test/java/org/apache/pinot/segment/local/aggregator/PercentileTDigestValueAggregatorTest.java:
##########
@@ -0,0 +1,150 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.segment.local.aggregator;
+
+import com.tdunning.math.stats.TDigest;
+import java.nio.ByteBuffer;
+import java.util.List;
+import org.apache.pinot.common.request.Literal;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.segment.local.utils.CustomSerDeUtils;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+/// Tests the serialization-size contract and deferred compression used by [PercentileTDigestValueAggregator].
+public class PercentileTDigestValueAggregatorTest {
+
+ @Test(dataProvider = "compressionBounds")
+ public void testRawValuesTrackMaxVerboseSizeWithoutCompression(int compression, int numValues,
+ int expectedMaxByteSize) {
+ PercentileTDigestValueAggregator aggregator = newAggregator(compression);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), 0);
+
+ TDigest digest = aggregator.getInitialAggregatedValue(0.0);
+ for (int i = 1; i < numValues; i++) {
+ aggregator.applyRawValue(digest, i);
+ }
+ assertEquals(digest.centroidCount(), 0);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), expectedMaxByteSize);
+ byte[] serialized = aggregator.serializeAggregatedValue(digest);
+ assertTrue(serialized.length <= expectedMaxByteSize);
+ }
+
+ @DataProvider
+ public static Object[][] compressionBounds() {
+ return new Object[][]{
+ {10, 30, 512},
+ {100, 210, 3_392},
+ {1_000, 2_010, 32_192}
+ };
+ }
+
+ @Test
+ public void testRawUpdatesDoNotForceCompression() {
+ PercentileTDigestValueAggregator aggregator = newAggregator(100);
+ TDigest digest = aggregator.getInitialAggregatedValue(0.0);
+ for (int i = 1; i < 256; i++) {
+ aggregator.applyRawValue(digest, i);
+ }
+
+ assertEquals(digest.size(), 256L);
+ assertEquals(digest.centroidCount(), 0);
+ byte[] serialized = aggregator.serializeAggregatedValue(digest);
+ assertTrue(digest.centroidCount() > 0);
+ assertTrue(serialized.length <= aggregator.getMaxAggregatedValueByteSize());
+ }
+
+ @Test
+ public void testPreAggregatedCompressionExpandsRegisteredBound() {
+ TDigest input = TDigest.createMergingDigest(200);
+ input.add(42.0);
+ byte[] inputBytes = CustomSerDeUtils.TDIGEST_SER_DE.serialize(input);
+
+ PercentileTDigestValueAggregator aggregator = newAggregator(10);
+ TDigest result = aggregator.getInitialAggregatedValue(inputBytes);
+ for (int i = 0; i < 409; i++) {
+ aggregator.applyRawValue(result, i);
+ }
+
+ assertEquals(result.compression(), 200.0);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), 6_592);
+ assertTrue(aggregator.serializeAggregatedValue(result).length <= aggregator.getMaxAggregatedValueByteSize());
+ }
+
+ @Test
+ public void testOversizedSmallEncodedDigestRemainsReadable() {
+ int centroidCount = 300;
+ byte[] smallEncoding = createSmallEncoding(10, 400, 500, centroidCount);
+
+ PercentileTDigestValueAggregator aggregator = newAggregator(10);
+ TDigest result = aggregator.getInitialAggregatedValue(smallEncoding);
+ byte[] serialized = aggregator.serializeAggregatedValue(result);
+ TDigest clone = aggregator.cloneAggregatedValue(result);
+
+ assertEquals(result.centroidCount(), centroidCount);
+ assertEquals(aggregator.getMaxAggregatedValueByteSize(), 4_832);
+ assertEquals(ByteBuffer.wrap(serialized).getInt(), 2);
+ assertEquals(serialized.length, smallEncoding.length);
+ assertTrue(serialized.length <= aggregator.getMaxAggregatedValueByteSize());
+ assertEquals(clone.size(), result.size());
+ assertEquals(clone.centroidCount(), result.centroidCount());
+ assertEquals(clone.quantile(0.75), result.quantile(0.75));
+ }
+
+ @Test
+ public void testAggregatedUpdateDoesNotForceDestinationCompression() {
+ PercentileTDigestValueAggregator aggregator = newAggregator(100);
+ TDigest destination = aggregator.getInitialAggregatedValue(1.0);
+ TDigest source = TDigest.createMergingDigest(100);
+ source.add(2.0);
+ source.add(3.0);
+
+ aggregator.applyAggregatedValue(destination, source);
+
+ assertEquals(destination.size(), 3L);
+ assertEquals(destination.centroidCount(), 0);
+ byte[] serialized = aggregator.serializeAggregatedValue(destination);
+ assertTrue(serialized.length <= aggregator.getMaxAggregatedValueByteSize());
Review Comment:
As written, this check compares `serialized.length` to `getMaxAggregatedValueByteSize()` *after* calling `serializeAggregatedValue()`, which updates the max internally. Capture the max before serialization so the test actually validates the sizing contract.
##########
pinot-perf/src/main/java/org/apache/pinot/perf/aggregation/BenchmarkPercentileTDigestValueAggregator.java:
##########
@@ -0,0 +1,104 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.perf.aggregation;
+
+import com.tdunning.math.stats.TDigest;
+import java.util.List;
+import java.util.SplittableRandom;
+import java.util.concurrent.TimeUnit;
+import org.apache.pinot.segment.local.aggregator.PercentileTDigestValueAggregator;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+/// Measures the raw-value TDigest aggregation kernel used while constructing a star-tree index.
+///
+/// The default models one million source rows split into groups of 1,000 rows with identical dimension keys. Each
+/// group is serialized because every aggregate eventually becomes a star-tree forward-index entry. Use
+/// `-p _numRows=100000000` for a 100-million-row run and vary `_rowsPerGroup` to model the table's dimension
+/// cardinality. This deliberately excludes dimension sorting and forward-index I/O so it isolates
+/// [PercentileTDigestValueAggregator].
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Warmup(iterations = 2, time = 1)
+@Measurement(iterations = 5, time = 1)
+@Fork(value = 2, jvmArgsAppend = {"-Xms4g", "-Xmx8g"})
+@Threads(1)
+public class BenchmarkPercentileTDigestValueAggregator {
+ private static final int VALUE_BLOCK_SIZE = 1 << 16;
+
+ @Param({"1000000"})
+ int _numRows;
+
+ @Param({"1000"})
+ int _rowsPerGroup;
+
+ private double[] _values;
+
+ @Setup
+ public void setUp() {
+ if (_numRows <= 0) {
+ throw new IllegalArgumentException("numRows must be positive");
+ }
+ if (_rowsPerGroup <= 0) {
+ throw new IllegalArgumentException("rowsPerGroup must be positive");
+ }
+ _values = new double[VALUE_BLOCK_SIZE];
+ SplittableRandom random = new SplittableRandom(42);
+ for (int i = 0; i < VALUE_BLOCK_SIZE; i++) {
+ _values[i] = random.nextDouble();
+ }
+ }
+
+ @Benchmark
+ public long aggregateAndSerializeRawValues() {
+ PercentileTDigestValueAggregator aggregator = new PercentileTDigestValueAggregator(List.of());
+ long totalAggregatedRows = 0;
+ long totalSerializedBytes = 0;
+ for (int from = 0; from < _numRows; from += _rowsPerGroup) {
+ int to = Math.min(_numRows, from + _rowsPerGroup);
+ TDigest digest = aggregator.getInitialAggregatedValue(_values[from & (VALUE_BLOCK_SIZE - 1)]);
+ for (int i = from + 1; i < to; i++) {
+ aggregator.applyRawValue(digest, _values[i & (VALUE_BLOCK_SIZE - 1)]);
+ }
+ byte[] serialized = aggregator.serializeAggregatedValue(digest);
+ if (serialized.length > aggregator.getMaxAggregatedValueByteSize()) {
+ throw new IllegalStateException(
+ "Serialized TDigest exceeds registered maximum: " + serialized.length + " > "
+ + aggregator.getMaxAggregatedValueByteSize());
+ }
Review Comment:
This benchmark’s safety check is ineffective because `serializeAggregatedValue()` updates `getMaxAggregatedValueByteSize()` before the comparison, so `serialized.length > aggregator.getMaxAggregatedValueByteSize()` should never be true. Capture the max before serialization and compare against that to validate the contract being benchmarked.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org
Re: [PR] [core] Optimize exact and TDigest percentile aggregation, including StarTree group-by [pinot]
Posted by "xiangfu0 (via GitHub)" <gi...@apache.org>.
xiangfu0 commented on code in PR #18996:
URL: https://github.com/apache/pinot/pull/18996#discussion_r3591611690
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAccumulator.java:
##########
@@ -0,0 +1,448 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.core.query.aggregation.function;
+
+import com.tdunning.math.stats.MergingDigest;
+import com.tdunning.math.stats.TDigest;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/// Accumulates raw values and serialized TDigests into centroids without repeatedly sorting existing centroids.
+///
+/// The accumulator is used while a percentile TDigest result, or one of its group-by results, is being built. Raw
+/// values are sorted in small batches, while serialized TDigest centroids are decoded in their existing sorted order.
+/// Both are linearly merged with the accumulated centroids and compressed using the same weight-limit rule as
+/// [MergingDigest]. [#toTDigest()] normalizes the result to a standard [MergingDigest], so intermediate-result
+/// serialization and distributed merging remain unchanged.
+///
+/// Serialized group state keeps its first digest pending, and allocates primitive centroid buffers only when another
+/// input must be merged. The raw-value buffer remains unallocated for serialized state until a raw value is added.
+///
+/// Instances are thread-confined to one aggregation result or group-by result.
+final class PercentileTDigestAccumulator {
+ private static final int MIN_RAW_BUFFER_SIZE = 256;
+ private static final int MAX_RAW_BUFFER_SIZE = 10_000;
+ private static final int CENTROID_CAPACITY_PADDING = 10;
+ private static final int VERBOSE_ENCODING = 1;
+ private static final int SMALL_ENCODING = 2;
+ private static final int VERBOSE_HEADER_SIZE = 32;
+ private static final int VERBOSE_CENTROID_SIZE = 16;
+ private static final int SMALL_CENTROID_SIZE = 8;
+
+ private final double _compression;
+ private final int _centroidCapacity;
+ private double[] _rawValues;
+ private double[] _centroidMeans;
+ private double[] _centroidWeights;
+ private double[] _outputMeans;
+ private double[] _outputWeights;
+ private double[] _incomingMeans;
+ private double[] _incomingWeights;
+ private byte[] _pendingSerializedTDigest;
+ private boolean _hasSerializedInput;
+
+ private int _numRawValues;
+ private int _numCentroids;
+ private double _totalWeight;
+ private double _min = Double.POSITIVE_INFINITY;
+ private double _max = Double.NEGATIVE_INFINITY;
+
+ PercentileTDigestAccumulator(int compression) {
+ this((double) compression, true, true);
+ }
+
+ private PercentileTDigestAccumulator(double compression, boolean allocateRawBuffer, boolean allocateMergeBuffers) {
+ if (!(compression > 0.0) || !Double.isFinite(compression)) {
+ throw new IllegalArgumentException("TDigest compression must be positive: " + compression);
+ }
+ _compression = compression;
+ long roundedCompression = (long) Math.ceil(compression);
+ _centroidCapacity = Math.toIntExact(
+ Math.addExact(Math.multiplyExact(2L, roundedCompression), CENTROID_CAPACITY_PADDING));
+ if (allocateRawBuffer) {
+ _rawValues = new double[getRawBufferSize(roundedCompression)];
+ }
+ if (allocateMergeBuffers) {
+ _centroidMeans = new double[_centroidCapacity];
+ _centroidWeights = new double[_centroidCapacity];
+ _outputMeans = new double[_centroidCapacity];
+ _outputWeights = new double[_centroidCapacity];
+ }
+ }
+
+ static PercentileTDigestAccumulator forSerializedTDigest(byte[] bytes) {
+ return new PercentileTDigestAccumulator(readCompression(bytes), false, false);
+ }
+
+ static PercentileTDigestAccumulator forSerializedTDigestWithMergeBuffers(byte[] bytes) {
+ return new PercentileTDigestAccumulator(readCompression(bytes), false, true);
+ }
+
+ void add(double[] values, int from, int toExclusive) {
+ if (from >= toExclusive) {
+ return;
+ }
+ materializePendingSerializedTDigest();
+ ensureRawBuffer();
+ while (from < toExclusive) {
+ int numValues = Math.min(toExclusive - from, _rawValues.length - _numRawValues);
+ int rawOffset = _numRawValues;
+ for (int i = 0; i < numValues; i++) {
+ double value = values[from + i];
+ if (Double.isNaN(value)) {
+ throw new IllegalArgumentException("Cannot add NaN to t-digest");
+ }
+ _rawValues[rawOffset + i] = value;
+ }
+ from += numValues;
+ _numRawValues += numValues;
+ if (_numRawValues == _rawValues.length) {
+ flush();
+ }
+ }
+ }
+
+ void addSerializedTDigest(byte[] bytes) {
+ flush();
+ if (!_hasSerializedInput && _numCentroids == 0 && _totalWeight == 0.0) {
+ validateSerializedTDigest(bytes);
+ _pendingSerializedTDigest = bytes;
+ _hasSerializedInput = true;
+ return;
+ }
+ materializePendingSerializedTDigest();
+ mergeSerializedTDigest(bytes);
+ _hasSerializedInput = true;
+ }
+
+ private void mergeSerializedTDigest(byte[] bytes) {
+
+ ByteBuffer input = ByteBuffer.wrap(bytes);
+ int encoding = input.getInt();
+ double min = input.getDouble();
+ double max = input.getDouble();
+ int numCentroids;
+ boolean initialize = _numCentroids == 0 && _totalWeight == 0.0;
+ double[] incomingMeans;
+ double[] incomingWeights;
+ if (encoding == VERBOSE_ENCODING) {
+ input.getDouble();
+ numCentroids = input.getInt();
+ checkCentroidCount(numCentroids);
+ checkCentroidsAvailable(input, numCentroids, VERBOSE_CENTROID_SIZE);
+ if (initialize) {
+ ensureCentroidCapacity(numCentroids);
+ incomingMeans = _centroidMeans;
+ incomingWeights = _centroidWeights;
+ } else {
+ ensureIncomingCapacity(numCentroids);
+ incomingMeans = _incomingMeans;
+ incomingWeights = _incomingWeights;
+ }
+ for (int i = 0; i < numCentroids; i++) {
+ incomingWeights[i] = input.getDouble();
+ incomingMeans[i] = input.getDouble();
+ }
+ } else if (encoding == SMALL_ENCODING) {
+ input.getFloat();
+ input.getShort();
+ input.getShort();
+ numCentroids = input.getShort();
+ checkCentroidCount(numCentroids);
+ checkCentroidsAvailable(input, numCentroids, SMALL_CENTROID_SIZE);
+ if (initialize) {
+ ensureCentroidCapacity(numCentroids);
+ incomingMeans = _centroidMeans;
+ incomingWeights = _centroidWeights;
+ } else {
+ ensureIncomingCapacity(numCentroids);
+ incomingMeans = _incomingMeans;
+ incomingWeights = _incomingWeights;
+ }
+ for (int i = 0; i < numCentroids; i++) {
+ incomingWeights[i] = input.getFloat();
+ incomingMeans[i] = input.getFloat();
+ }
+ } else {
+ throw new IllegalStateException("Invalid format for serialized histogram");
+ }
+
+ if (initialize) {
+ double totalWeight = getTotalWeight(incomingWeights, numCentroids);
+ if (totalWeight != 0.0) {
+ _numCentroids = numCentroids;
+ _totalWeight = totalWeight;
+ _min = min;
+ _max = max;
+ }
+ } else if (mergeSorted(incomingMeans, incomingWeights, numCentroids)) {
+ _min = Math.min(_min, min);
+ _max = Math.max(_max, max);
+ }
+ }
+
+ TDigest toTDigest() {
+ if (_pendingSerializedTDigest != null) {
+ return MergingDigest.fromBytes(ByteBuffer.wrap(_pendingSerializedTDigest));
+ }
+ flush();
+ if (_numCentroids == 0) {
+ return TDigest.createMergingDigest(_compression);
+ }
+
+ ByteBuffer buffer = ByteBuffer.allocate(VERBOSE_HEADER_SIZE + VERBOSE_CENTROID_SIZE * _numCentroids);
Review Comment:
This materialized path always re-emits verbose TDigest bytes, but t-digest 3.2 verbose decoding constructs a digest with only the default centroid capacity. If the accumulator first materializes a valid small-encoded digest whose centroid count exceeds that default capacity, `MergingDigest.fromBytes(buffer)` can throw during query result extraction. Preserve the small-encoding capacity or build the result digest with enough centroid capacity before returning it.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org
Re: [PR] [core] Optimize exact and TDigest percentile aggregation, including StarTree group-by [pinot]
Posted by "xiangfu0 (via GitHub)" <gi...@apache.org>.
xiangfu0 commented on code in PR #18996:
URL: https://github.com/apache/pinot/pull/18996#discussion_r3594928077
##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunction.java:
##########
@@ -305,13 +300,16 @@ public ColumnDataType getIntermediateResultColumnType() {
@Override
public SerializedIntermediateResult serializeIntermediateResult(TDigest tDigest) {
+ if (tDigest instanceof PercentileTDigestAccumulator) {
+ tDigest = ((PercentileTDigestAccumulator) tDigest).toTDigest();
Review Comment:
This conversion loses the small-encoding capacity for oversized compact TDigest inputs. `toTDigest()` can preserve an oversized small-encoded digest, but the generic `TDIGEST_SER_DE.serialize()` below calls `asBytes()` and rewrites it as verbose bytes. A valid compact digest with more centroids than `2 * ceil(compression) + 10` then becomes unreadable by the t-digest 3.2 decoder because verbose bytes no longer carry the larger main capacity, so distributed StarTree percentile queries can fail during server-to-broker intermediate reduction. Please preserve small encoding for these capacity-preserving accumulators and add an intermediate-result round-trip test for this case.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org