Skip to content

Commit ebb225e

Browse files
hhr293guowangyzhulipeng
committed
[VL]:shuffle: extend TypeAwareCompress to INT128 (DECIMAL HUGEINT)
The TAC codec from apache#11894 only covered INT64. DECIMAL(p>18) is stored as 128-bit HugeInt and previously fell through to LZ4, missing a structural compression opportunity: in OLAP workloads (TPC-H prices, taxes, ...) DECIMAL values usually fit in INT64, so the high 64 bits are 0 and the low 64 bits are narrow -- exactly the pattern FFOR exploits. Implementation: split each 16B value into lo/hi uint64 sub-streams via stride-2 gather into stack-allocated scratch buffers, then run the existing 64-bit FFOR encoder on each. Wire format reuses the 64-bit per-block (bw, count, base) header twice -- the stream is self- describing, no hi/lo length prefix needed. hi sub-streams that are all equal to base degenerate to just the 16B header (bw=0). Velox HUGEINT is mapped to tac::kUInt128; shuffle writer and frame format unchanged. Results vs LZ4 on the same int128 input: compression ratio 0.116 (TAC) vs 0.193 (LZ4) -- 40% smaller End-to-end on TPC-H SF=6000, the wins concentrate on the queries with heavy decimal-keyed shuffles: q15: shuffle size -15%, latency -8% q17: shuffle size -8%, latency -3% q18: shuffle size -8%, latency -3% Generated-by: Claude claude-opus-4-7 Co-authored-by: Guo Wangyang <wangyang.guo@intel.com> Co-authored-by: Lipeng Zhu <lipeng.zhu@intel.com> Signed-off-by: huhengrui <hengrui.hu@intel.com>
1 parent 1d63348 commit ebb225e

7 files changed

Lines changed: 545 additions & 74 deletions

File tree

cpp/core/tests/FForCodecTest.cc

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,3 +643,171 @@ TEST(TypeAwareCompressCodecTest, UnsupportedType) {
643643
auto result = TypeAwareCompressCodec::compress(dummy, 8, dummy, 100, kSomeUnsupportedType);
644644
ASSERT_FALSE(result.ok());
645645
}
646+
647+
TEST(TypeAwareCompressCodecTest, Int128Supported) {
648+
ASSERT_TRUE(TypeAwareCompressCodec::support(tac::kUInt128));
649+
}
650+
651+
TEST(TypeAwareCompressCodecTest, Int128NarrowRoundtrip) {
652+
// 1024 INT128 values where the high 64 bits are 0 (typical DECIMAL fitting
653+
// in int64) and the low 64 bits range narrowly. Both hi and lo sub-streams
654+
// should compress extremely well.
655+
constexpr int64_t kNumValues = 1024;
656+
std::vector<uint8_t> data(kNumValues * sizeof(__int128_t), 0);
657+
for (int64_t i = 0; i < kNumValues; ++i) {
658+
uint64_t lo = 1000000ULL + static_cast<uint64_t>(i);
659+
std::memcpy(data.data() + i * sizeof(__int128_t), &lo, sizeof(uint64_t));
660+
}
661+
662+
const int64_t inputSize = data.size();
663+
auto maxLen = TypeAwareCompressCodec::maxCompressedLen(inputSize, tac::kUInt128);
664+
ASSERT_GT(maxLen, 0);
665+
666+
std::vector<uint8_t> compressed(maxLen);
667+
auto compResult =
668+
TypeAwareCompressCodec::compress(data.data(), inputSize, compressed.data(), compressed.size(), tac::kUInt128);
669+
ASSERT_TRUE(compResult.ok()) << compResult.status().ToString();
670+
const int64_t compressedSize = *compResult;
671+
672+
ASSERT_LT(compressedSize, inputSize / 2);
673+
674+
std::vector<uint8_t> decoded(inputSize, 0xff);
675+
auto decResult = TypeAwareCompressCodec::decompress(compressed.data(), compressedSize, decoded.data(), inputSize);
676+
ASSERT_TRUE(decResult.ok()) << decResult.status().ToString();
677+
ASSERT_EQ(*decResult, compressedSize);
678+
ASSERT_EQ(0, std::memcmp(decoded.data(), data.data(), inputSize));
679+
}
680+
681+
TEST(TypeAwareCompressCodecTest, Int128HighEntropyRoundtrip) {
682+
// High-entropy 128-bit values: both halves are random. Verifies round-trip
683+
// correctness on a workload FFor cannot compress effectively.
684+
constexpr int64_t kNumValues = 1024;
685+
std::vector<uint8_t> data(kNumValues * sizeof(__int128_t));
686+
std::mt19937_64 rng(42);
687+
for (int64_t i = 0; i < kNumValues; ++i) {
688+
uint64_t lo = rng();
689+
uint64_t hi = rng();
690+
std::memcpy(data.data() + i * sizeof(__int128_t), &lo, sizeof(uint64_t));
691+
std::memcpy(data.data() + i * sizeof(__int128_t) + 8, &hi, sizeof(uint64_t));
692+
}
693+
694+
const int64_t inputSize = data.size();
695+
auto maxLen = TypeAwareCompressCodec::maxCompressedLen(inputSize, tac::kUInt128);
696+
std::vector<uint8_t> compressed(maxLen);
697+
auto compResult =
698+
TypeAwareCompressCodec::compress(data.data(), inputSize, compressed.data(), compressed.size(), tac::kUInt128);
699+
ASSERT_TRUE(compResult.ok()) << compResult.status().ToString();
700+
701+
std::vector<uint8_t> decoded(inputSize);
702+
auto decResult = TypeAwareCompressCodec::decompress(compressed.data(), *compResult, decoded.data(), inputSize);
703+
ASSERT_TRUE(decResult.ok()) << decResult.status().ToString();
704+
ASSERT_EQ(*decResult, *compResult);
705+
ASSERT_EQ(0, std::memcmp(decoded.data(), data.data(), inputSize));
706+
}
707+
708+
namespace {
709+
710+
// Helper: build kNumValues 128-bit values whose lo halves form a narrow
711+
// arithmetic sequence and hi halves are zero (typical "DECIMAL fits in int64"
712+
// pattern). Returns the raw byte buffer.
713+
std::vector<uint8_t> makeNarrowInt128(int64_t kNumValues) {
714+
std::vector<uint8_t> data(kNumValues * sizeof(__int128_t), 0);
715+
for (int64_t i = 0; i < kNumValues; ++i) {
716+
uint64_t lo = 1000000ULL + static_cast<uint64_t>(i);
717+
std::memcpy(data.data() + i * sizeof(__int128_t), &lo, sizeof(uint64_t));
718+
}
719+
return data;
720+
}
721+
722+
} // namespace
723+
724+
TEST(TypeAwareCompressCodecTest, Int128TailRoundtrip) {
725+
// Element count that is NOT a multiple of kLanes (=4): 1027 = 256*4 + 3.
726+
// Forces compress128 to emit 1 full block plus a 3-element tail block,
727+
// exercising the kBwTailMarker path in both compress and decompress.
728+
constexpr int64_t kNumValues = 1027;
729+
auto data = makeNarrowInt128(kNumValues);
730+
731+
const int64_t inputSize = data.size();
732+
auto maxLen = TypeAwareCompressCodec::maxCompressedLen(inputSize, tac::kUInt128);
733+
std::vector<uint8_t> compressed(maxLen);
734+
auto compResult =
735+
TypeAwareCompressCodec::compress(data.data(), inputSize, compressed.data(), compressed.size(), tac::kUInt128);
736+
ASSERT_TRUE(compResult.ok()) << compResult.status().ToString();
737+
const int64_t compressedSize = *compResult;
738+
739+
std::vector<uint8_t> decoded(inputSize, 0xff);
740+
auto decResult = TypeAwareCompressCodec::decompress(compressed.data(), compressedSize, decoded.data(), inputSize);
741+
ASSERT_TRUE(decResult.ok()) << decResult.status().ToString();
742+
ASSERT_EQ(0, std::memcmp(decoded.data(), data.data(), inputSize));
743+
}
744+
745+
TEST(TypeAwareCompressCodecTest, Int128MisalignedRoundtrip) {
746+
// Drive compress128/decompress128 with input AND output pointers offset by
747+
// 1 byte so neither is uint64-aligned. Exercises the <false, false>
748+
// template instantiation, which is otherwise dead code under the natural
749+
// alignment of std::vector<uint8_t>::data().
750+
constexpr int64_t kNumValues = 1024;
751+
auto src = makeNarrowInt128(kNumValues);
752+
const int64_t inputSize = src.size();
753+
754+
// Stage src into a buffer whose payload starts at a 1-byte-offset address.
755+
std::vector<uint8_t> inBuf(inputSize + 1, 0);
756+
std::memcpy(inBuf.data() + 1, src.data(), inputSize);
757+
758+
auto maxLen = TypeAwareCompressCodec::maxCompressedLen(inputSize, tac::kUInt128);
759+
std::vector<uint8_t> compBuf(maxLen + 1, 0);
760+
// Skip the first byte to misalign the compressed-data start as well.
761+
auto compResult =
762+
TypeAwareCompressCodec::compress(inBuf.data() + 1, inputSize, compBuf.data() + 1, maxLen, tac::kUInt128);
763+
ASSERT_TRUE(compResult.ok()) << compResult.status().ToString();
764+
const int64_t compressedSize = *compResult;
765+
766+
std::vector<uint8_t> outBuf(inputSize + 1, 0xff);
767+
auto decResult = TypeAwareCompressCodec::decompress(compBuf.data() + 1, compressedSize, outBuf.data() + 1, inputSize);
768+
ASSERT_TRUE(decResult.ok()) << decResult.status().ToString();
769+
ASSERT_EQ(0, std::memcmp(outBuf.data() + 1, src.data(), inputSize));
770+
}
771+
772+
TEST(TypeAwareCompressCodecTest, Int128TruncatedInputRejected) {
773+
// Compress a normal stream, then hand decompress() a prefix that drops the
774+
// terminator tail header plus the final block's hi sub-header, leaving the
775+
// truncation point inside the final block's lo payload. decompress128Impl
776+
// must detect the short read via decodeBlock's bounds check and return a
777+
// value count below the expected number of 128-bit values, which
778+
// TypeAwareCompressCodec turns into an Invalid status.
779+
constexpr int64_t kNumValues = 1024;
780+
auto data = makeNarrowInt128(kNumValues);
781+
const int64_t inputSize = data.size();
782+
auto maxLen = TypeAwareCompressCodec::maxCompressedLen(inputSize, tac::kUInt128);
783+
std::vector<uint8_t> compressed(maxLen);
784+
auto compResult =
785+
TypeAwareCompressCodec::compress(data.data(), inputSize, compressed.data(), compressed.size(), tac::kUInt128);
786+
ASSERT_TRUE(compResult.ok()) << compResult.status().ToString();
787+
const int64_t compressedSize = *compResult;
788+
// 64 bytes = 16B tail header + 16B hi sub-header + 32B into the final lo
789+
// payload. Smaller drops can land on a clean tail boundary and silently
790+
// succeed.
791+
ASSERT_GT(compressedSize, 64);
792+
793+
std::vector<uint8_t> decoded(inputSize, 0);
794+
auto decResult =
795+
TypeAwareCompressCodec::decompress(compressed.data(), compressedSize - 64, decoded.data(), inputSize);
796+
ASSERT_FALSE(decResult.ok());
797+
}
798+
799+
TEST(TypeAwareCompressCodecTest, Int128OutputBufferTooSmallRejected) {
800+
// compress128 must reject an output buffer smaller than maxCompressedLen,
801+
// since the codec writes worst-case-sized headers up-front and only knows
802+
// the final length after the analyze step.
803+
constexpr int64_t kNumValues = 64;
804+
auto data = makeNarrowInt128(kNumValues);
805+
const int64_t inputSize = data.size();
806+
auto maxLen = TypeAwareCompressCodec::maxCompressedLen(inputSize, tac::kUInt128);
807+
ASSERT_GT(maxLen, 0);
808+
809+
std::vector<uint8_t> tooSmall(maxLen - 1);
810+
auto result =
811+
TypeAwareCompressCodec::compress(data.data(), inputSize, tooSmall.data(), tooSmall.size(), tac::kUInt128);
812+
ASSERT_FALSE(result.ok());
813+
}

cpp/core/utils/tac/FForCodec.cc

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ FForCodec::compress(const uint8_t* input, int64_t inputSize, uint8_t* output, in
3737
size_t numValues = inputSize / sizeof(uint64_t);
3838
auto maxLen = static_cast<int64_t>(ffor::compress64Bound(numValues));
3939
if (outputSize < maxLen) {
40-
return arrow::Status::Invalid("FForCodec: output buffer too small.");
40+
return arrow::Status::Invalid(
41+
"FForCodec: output buffer too small (need ", maxLen, " bytes, got ", outputSize, ").");
4142
}
4243

4344
auto written = ffor::compress64(reinterpret_cast<const uint64_t*>(input), numValues, output);
@@ -57,4 +58,47 @@ FForCodec::decompress(const uint8_t* input, int64_t inputSize, uint8_t* output,
5758
return static_cast<int64_t>(nDecoded);
5859
}
5960

61+
int64_t FForCodec::maxCompressedLength128(int64_t inputSize) {
62+
if (inputSize % sizeof(__int128_t) != 0) {
63+
return 0;
64+
}
65+
size_t numValues = inputSize / sizeof(__int128_t);
66+
return static_cast<int64_t>(ffor::compress128Bound(numValues));
67+
}
68+
69+
arrow::Result<int64_t>
70+
FForCodec::compress128(const uint8_t* input, int64_t inputSize, uint8_t* output, int64_t outputSize) {
71+
if (inputSize == 0) {
72+
return 0;
73+
}
74+
if (inputSize % sizeof(__int128_t) != 0) {
75+
return arrow::Status::Invalid(
76+
"FForCodec: input size ", inputSize, " is not a multiple of ", sizeof(__int128_t), ".");
77+
}
78+
79+
size_t numValues = inputSize / sizeof(__int128_t);
80+
auto maxLen = static_cast<int64_t>(ffor::compress128Bound(numValues));
81+
if (outputSize < maxLen) {
82+
return arrow::Status::Invalid(
83+
"FForCodec: output buffer too small for 128-bit compression (need ", maxLen, " bytes, got ", outputSize, ").");
84+
}
85+
86+
auto written = ffor::compress128(input, numValues, output);
87+
return static_cast<int64_t>(written);
88+
}
89+
90+
arrow::Result<int64_t>
91+
FForCodec::decompress128(const uint8_t* input, int64_t inputSize, uint8_t* output, int64_t outputSize) {
92+
if (outputSize == 0) {
93+
return 0;
94+
}
95+
if (outputSize % sizeof(__int128_t) != 0) {
96+
return arrow::Status::Invalid(
97+
"FForCodec: output size ", outputSize, " is not a multiple of ", sizeof(__int128_t), ".");
98+
}
99+
100+
auto nDecoded = ffor::decompress128(input, inputSize, output, static_cast<size_t>(outputSize));
101+
return static_cast<int64_t>(nDecoded);
102+
}
103+
60104
} // namespace gluten

cpp/core/utils/tac/FForCodec.h

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@
2323

2424
namespace gluten {
2525

26-
// FFOR (Frame-of-Reference) codec for uint64_t data using 4-lane layout.
27-
// Used for INT64/UINT64 columns in shuffle.
26+
// FFOR (Frame-of-Reference) codec for uint64_t / 128-bit data using 4-lane layout.
27+
// Used for INT64/UINT64 and INT128/HUGEINT (DECIMAL) columns in shuffle.
2828
class FForCodec {
2929
public:
3030
// Returns the maximum compressed size in bytes for the given input size.
31+
// Input is treated as a stream of uint64 values; size must be a multiple of 8.
3132
static int64_t maxCompressedLength(int64_t inputSize);
3233

3334
// Compress uint64_t data.
@@ -40,6 +41,24 @@ class FForCodec {
4041
// Returns the number of uint64_t values decoded.
4142
static arrow::Result<int64_t>
4243
decompress(const uint8_t* input, int64_t inputSize, uint8_t* output, int64_t outputSize);
44+
45+
// Worst-case compressed size for a stream of 128-bit values.
46+
// inputSize must be a multiple of sizeof(__int128_t); returns 0 otherwise.
47+
static int64_t maxCompressedLength128(int64_t inputSize);
48+
49+
// Compress a stream of 128-bit values whose in-memory layout consists of
50+
// two 64-bit halves: the low 8B at offset 0 and the high 8B at offset 8
51+
// (DECIMAL128 in Velox). Internally splits each value into hi/lo uint64
52+
// sub-streams per block and runs the 64-bit FFOR encoder on each.
53+
// inputSize must be a multiple of sizeof(__int128_t); returns the number
54+
// of compressed bytes written to output.
55+
static arrow::Result<int64_t>
56+
compress128(const uint8_t* input, int64_t inputSize, uint8_t* output, int64_t outputSize);
57+
58+
// Decompress data produced by compress128(). outputSize must be a multiple
59+
// of sizeof(__int128_t); returns the number of 128-bit values decoded.
60+
static arrow::Result<int64_t>
61+
decompress128(const uint8_t* input, int64_t inputSize, uint8_t* output, int64_t outputSize);
4362
};
4463

4564
} // namespace gluten

cpp/core/utils/tac/TypeAwareCompressCodec.cc

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,18 @@
2121
namespace gluten {
2222

2323
bool TypeAwareCompressCodec::support(int8_t tacType) {
24-
return tacType == tac::kUInt64;
24+
return tacType == tac::kUInt64 || tacType == tac::kUInt128;
2525
}
2626

2727
int64_t TypeAwareCompressCodec::maxCompressedLen(int64_t inputLen, int8_t tacType) {
28-
if (!support(tacType)) {
29-
return 0;
28+
switch (tacType) {
29+
case tac::kUInt64:
30+
return kPayloadHeaderSize + FForCodec::maxCompressedLength(inputLen);
31+
case tac::kUInt128:
32+
return kPayloadHeaderSize + FForCodec::maxCompressedLength128(inputLen);
33+
default:
34+
return 0;
3035
}
31-
return kPayloadHeaderSize + FForCodec::maxCompressedLength(inputLen);
3236
}
3337

3438
arrow::Result<int64_t> TypeAwareCompressCodec::compress(
@@ -50,10 +54,21 @@ arrow::Result<int64_t> TypeAwareCompressCodec::compress(
5054
auto* out = output;
5155
*out++ = static_cast<uint8_t>(CodecId::kFFor);
5256
*out++ = static_cast<uint8_t>(tacType);
57+
int64_t availableOutput = outputLen - kPayloadHeaderSize;
5358

54-
auto availableOutput = outputLen - kPayloadHeaderSize;
55-
ARROW_ASSIGN_OR_RAISE(auto compressedLen, FForCodec::compress(input, inputLen, out, availableOutput));
56-
59+
int64_t compressedLen = 0;
60+
switch (tacType) {
61+
case tac::kUInt64: {
62+
ARROW_ASSIGN_OR_RAISE(compressedLen, FForCodec::compress(input, inputLen, out, availableOutput));
63+
break;
64+
}
65+
case tac::kUInt128: {
66+
ARROW_ASSIGN_OR_RAISE(compressedLen, FForCodec::compress128(input, inputLen, out, availableOutput));
67+
break;
68+
}
69+
default:
70+
return arrow::Status::Invalid("Unsupported tac type in compress: ", static_cast<int>(tacType));
71+
}
5772
return kPayloadHeaderSize + compressedLen;
5873
}
5974

@@ -65,18 +80,41 @@ TypeAwareCompressCodec::decompress(const uint8_t* input, int64_t inputLen, uint8
6580

6681
auto* in = input;
6782
auto codecId = static_cast<CodecId>(*in++);
68-
[[maybe_unused]] auto tacType = *in++;
83+
auto tacType = static_cast<int8_t>(*in++);
6984
auto dataLen = inputLen - kPayloadHeaderSize;
7085

7186
switch (codecId) {
72-
case CodecId::kFFor: {
73-
ARROW_ASSIGN_OR_RAISE(auto nDecoded, FForCodec::decompress(in, dataLen, output, outputLen));
74-
(void)nDecoded;
75-
return inputLen;
76-
}
87+
case CodecId::kFFor:
88+
break;
7789
default:
7890
return arrow::Status::Invalid("Unknown type-aware codec ID: ", static_cast<int>(codecId));
7991
}
92+
93+
int64_t nDecoded = 0;
94+
int64_t valueSize = 0;
95+
const char* typeName = nullptr;
96+
switch (tacType) {
97+
case tac::kUInt64: {
98+
ARROW_ASSIGN_OR_RAISE(nDecoded, FForCodec::decompress(in, dataLen, output, outputLen));
99+
valueSize = sizeof(uint64_t);
100+
typeName = "uint64";
101+
break;
102+
}
103+
case tac::kUInt128: {
104+
ARROW_ASSIGN_OR_RAISE(nDecoded, FForCodec::decompress128(in, dataLen, output, outputLen));
105+
valueSize = 2 * sizeof(uint64_t);
106+
typeName = "uint128";
107+
break;
108+
}
109+
default:
110+
return arrow::Status::Invalid("Unknown tac type in decompress: ", static_cast<int>(tacType));
111+
}
112+
const int64_t expected = outputLen / valueSize;
113+
if (nDecoded != expected) {
114+
return arrow::Status::Invalid(
115+
"TAC decompress ", typeName, " value count mismatch: expected ", expected, " got ", nDecoded);
116+
}
117+
return inputLen;
80118
}
81119

82120
} // namespace gluten

cpp/core/utils/tac/TypeAwareCompressCodec.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ namespace tac {
3030
enum TacDataType : int8_t {
3131
kUnsupported = -1, // Not compressible by TAC.
3232
kUInt64 = 0, // 8-byte unsigned integer (also used for int64, double, date64).
33+
kUInt128 = 1, // 16-byte unsigned integer (used for HUGEINT / DECIMAL128).
3334
};
3435

3536
} // namespace tac
@@ -38,7 +39,8 @@ enum TacDataType : int8_t {
3839
/// compression algorithm based on the data type of the buffer.
3940
///
4041
/// Currently supported:
41-
/// kUInt64 -> FFor (Frame-of-Reference + Bit-Packing) for uint64_t streams.
42+
/// kUInt64 -> FFOR (Frame-of-Reference + Bit-Packing) for uint64_t streams.
43+
/// kUInt128 -> FFOR applied to lo/hi uint64 sub-streams of 128-bit values.
4244
///
4345
/// The compressed wire format is self-describing: decompress() does not need
4446
/// a type hint because codec ID and element width are embedded in the header.

0 commit comments

Comments
 (0)