Skip to content

[GLUTEN-10933][VL] cuDF: Move lock to ShuffleReader - #10934

Merged
jinchengchenghh merged 8 commits into
apache:mainfrom
jinchengchenghh:validation
Oct 30, 2025
Merged

[GLUTEN-10933][VL] cuDF: Move lock to ShuffleReader#10934
jinchengchenghh merged 8 commits into
apache:mainfrom
jinchengchenghh:validation

Conversation

@jinchengchenghh

@jinchengchenghh jinchengchenghh commented Oct 23, 2025

Copy link
Copy Markdown
Contributor

The lock in WholeStageResultIterator restrict the cpu thread to produce batch, move the lock to here can let threads produce first batch with 1 GB in advance. Maybe the threads should prepare more data and let the GPU consume, this depends on the GPU operator time.

Need to restrict the total stage can offload to GPU, otherwise, after fallback, the lock cannot make effect on GPU execution.

Related issue: #10933

@github-actions github-actions Bot added CORE works for Gluten Core VELOX labels Oct 23, 2025
@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

1 similar comment
@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@zhztheplayer zhztheplayer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @jinchengchenghh.

A few more questions / comments on this PR's change:

  1. Can we extract common code of GPUColumnarShuffleExchangeExec and ColumnarShuffleExchangeExec out to an abstract class, to avoid duplication?
  2. If putting the lock in shuffle reader, do we still waste CPU if part of the input plan is in vanilla Spark or CPU Velox?
  3. If 2 is true, should we finally implement this concurrency control in Velox? E.g., pass an integer option to control the maximum concurrency of cuDF computation? This way will look more reasonable to me, but I’d appreciate more insights on this.
  4. The implementation doesn't seem to take a corner case into consideration, when there are multiple Velox tasks in the same Spark task. E.g, a Velox plan that has one operator falling back to vanilla Spark between two Velox operators. According to the code, it's possible that two Spark tasks run into race conditions, when the lock is released by task A but the second half of cuDF computation of task A is not finished.

Comment thread cpp/velox/cudf/GpuLock.cc Outdated
Co-authored-by: Hongze Zhang <hongze.zzz123@gmail.com>
@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@jinchengchenghh

Copy link
Copy Markdown
Contributor Author
  1. Refactored.
  2. We will only offload the stage which can totally run in GPU.
  3. Yes, this will cause the lock does not take effect on GPU, now we only check the WholeStageTransformer operator, in my design, the total stage should can offload to GPU, I will add restriction in following PR, do not contain any format conversion in the stage plan.

Thanks for your advice! @zhztheplayer

Comment on lines +45 to +299
arrow::Result<BlockType> readBlockType(arrow::io::InputStream* inputStream) {
BlockType type;
ARROW_ASSIGN_OR_RAISE(auto bytes, inputStream->Read(sizeof(BlockType), &type));
if (bytes == 0) {
// Reach EOS.
return BlockType::kEndOfStream;
}
return type;
}

struct BufferViewReleaser {
BufferViewReleaser() : BufferViewReleaser(nullptr) {}

BufferViewReleaser(std::shared_ptr<arrow::Buffer> arrowBuffer) : bufferReleaser_(std::move(arrowBuffer)) {}

void addRef() const {}

void release() const {}

private:
const std::shared_ptr<arrow::Buffer> bufferReleaser_;
};

BufferPtr wrapInBufferViewAsOwner(const void* buffer, size_t length, std::shared_ptr<arrow::Buffer> bufferReleaser) {
return BufferView<BufferViewReleaser>::create(
static_cast<const uint8_t*>(buffer), length, {std::move(bufferReleaser)});
}

BufferPtr convertToVeloxBuffer(std::shared_ptr<arrow::Buffer> buffer) {
if (buffer == nullptr) {
return nullptr;
}
return wrapInBufferViewAsOwner(buffer->data(), buffer->size(), buffer);
}

template <TypeKind Kind, typename T = typename TypeTraits<Kind>::NativeType>
VectorPtr readFlatVector(
std::vector<BufferPtr>& buffers,
int32_t& bufferIdx,
uint32_t length,
std::shared_ptr<const Type> type,
const VectorPtr& dictionary,
memory::MemoryPool* pool) {
auto nulls = buffers[bufferIdx++];
auto valuesOrIndices = buffers[bufferIdx++];

nulls = nulls == nullptr || nulls->size() == 0 ? BufferPtr(nullptr) : nulls;

if (dictionary != nullptr) {
return BaseVector::wrapInDictionary(nulls, valuesOrIndices, length, dictionary);
}

return std::make_shared<FlatVector<T>>(
pool, type, nulls, length, std::move(valuesOrIndices), std::vector<BufferPtr>{});
}

template <>
VectorPtr readFlatVector<TypeKind::UNKNOWN>(
std::vector<BufferPtr>& buffers,
int32_t& bufferIdx,
uint32_t length,
std::shared_ptr<const Type> type,
const VectorPtr& dictionary,
memory::MemoryPool* pool) {
return BaseVector::createNullConstant(type, length, pool);
}

template <>
VectorPtr readFlatVector<TypeKind::HUGEINT>(
std::vector<BufferPtr>& buffers,
int32_t& bufferIdx,
uint32_t length,
std::shared_ptr<const Type> type,
const VectorPtr& dictionary,
memory::MemoryPool* pool) {
auto nulls = buffers[bufferIdx++];
auto valuesOrIndices = buffers[bufferIdx++];

// Because if buffer does not compress, it will get from netty, the address maynot aligned 16B, which will cause
// int128_t = xxx coredump by instruction movdqa
const auto* addr = valuesOrIndices->as<facebook::velox::int128_t>();
if ((reinterpret_cast<uintptr_t>(addr) & 0xf) != 0) {
auto alignedBuffer = AlignedBuffer::allocate<char>(valuesOrIndices->size(), pool);
fastCopy(alignedBuffer->asMutable<char>(), valuesOrIndices->as<char>(), valuesOrIndices->size());
valuesOrIndices = alignedBuffer;
}

nulls = nulls == nullptr || nulls->size() == 0 ? BufferPtr(nullptr) : nulls;

if (dictionary != nullptr) {
return BaseVector::wrapInDictionary(nulls, valuesOrIndices, length, dictionary);
}

return std::make_shared<FlatVector<int128_t>>(
pool, type, nulls, length, std::move(valuesOrIndices), std::vector<BufferPtr>{});
}

VectorPtr readFlatVectorStringView(
std::vector<BufferPtr>& buffers,
int32_t& bufferIdx,
uint32_t length,
std::shared_ptr<const Type> type,
const VectorPtr& dictionary,
memory::MemoryPool* pool) {
auto nulls = buffers[bufferIdx++];
auto lengthOrIndices = buffers[bufferIdx++];

nulls = nulls == nullptr || nulls->size() == 0 ? BufferPtr(nullptr) : nulls;

if (dictionary != nullptr) {
return BaseVector::wrapInDictionary(nulls, lengthOrIndices, length, dictionary);
}

auto valueBuffer = buffers[bufferIdx++];

const auto* rawLength = lengthOrIndices->as<StringLengthType>();
const auto* valueBufferPtr = valueBuffer->as<char>();

auto values = AlignedBuffer::allocate<char>(sizeof(StringView) * length, pool);
auto* rawValues = values->asMutable<StringView>();

uint64_t offset = 0;
for (int32_t i = 0; i < length; ++i) {
rawValues[i] = StringView(valueBufferPtr + offset, rawLength[i]);
offset += rawLength[i];
}

std::vector<BufferPtr> stringBuffers;
stringBuffers.emplace_back(valueBuffer);

return std::make_shared<FlatVector<StringView>>(
pool, type, nulls, length, std::move(values), std::move(stringBuffers));
}

template <>
VectorPtr readFlatVector<TypeKind::VARCHAR>(
std::vector<BufferPtr>& buffers,
int32_t& bufferIdx,
uint32_t length,
std::shared_ptr<const Type> type,
const VectorPtr& dictionary,
memory::MemoryPool* pool) {
return readFlatVectorStringView(buffers, bufferIdx, length, type, dictionary, pool);
}

template <>
VectorPtr readFlatVector<TypeKind::VARBINARY>(
std::vector<BufferPtr>& buffers,
int32_t& bufferIdx,
uint32_t length,
std::shared_ptr<const Type> type,
const VectorPtr& dictionary,
memory::MemoryPool* pool) {
return readFlatVectorStringView(buffers, bufferIdx, length, type, dictionary, pool);
}

std::unique_ptr<ByteInputStream> toByteStream(uint8_t* data, int32_t size) {
std::vector<ByteRange> byteRanges;
byteRanges.push_back(ByteRange{data, size, 0});
auto byteStream = std::make_unique<BufferInputStream>(byteRanges);
return byteStream;
}

RowVectorPtr readComplexType(BufferPtr buffer, RowTypePtr& rowType, memory::MemoryPool* pool) {
RowVectorPtr result;
auto byteStream = toByteStream(const_cast<uint8_t*>(buffer->as<uint8_t>()), buffer->size());
auto serde = std::make_unique<serializer::presto::PrestoVectorSerde>();
serializer::presto::PrestoVectorSerde::PrestoOptions options;
options.useLosslessTimestamp = true;
serde->deserialize(byteStream.get(), pool, rowType, &result, &options);
return result;
}

RowTypePtr getComplexWriteType(const std::vector<TypePtr>& types) {
std::vector<std::string> complexTypeColNames;
std::vector<TypePtr> complexTypeChildrens;
for (int32_t i = 0; i < types.size(); ++i) {
auto kind = types[i]->kind();
switch (kind) {
case TypeKind::ROW:
case TypeKind::MAP:
case TypeKind::ARRAY: {
complexTypeColNames.emplace_back(types[i]->name());
complexTypeChildrens.emplace_back(types[i]);
} break;
default:
break;
}
}
return std::make_shared<const RowType>(std::move(complexTypeColNames), std::move(complexTypeChildrens));
}

RowVectorPtr deserialize(
RowTypePtr type,
uint32_t numRows,
std::vector<BufferPtr>& buffers,
const std::vector<int32_t>& dictionaryFields,
const std::vector<VectorPtr>& dictionaries,
memory::MemoryPool* pool) {
std::vector<VectorPtr> children;
auto types = type->as<TypeKind::ROW>().children();

std::vector<VectorPtr> complexChildren;
auto complexRowType = getComplexWriteType(types);
if (complexRowType->children().size() > 0) {
complexChildren = readComplexType(buffers[buffers.size() - 1], complexRowType, pool)->children();
}

int32_t bufferIdx = 0;
int32_t complexIdx = 0;
int32_t dictionaryIdx = 0;
for (size_t i = 0; i < types.size(); ++i) {
const auto kind = types[i]->kind();
switch (kind) {
case TypeKind::ROW:
case TypeKind::MAP:
case TypeKind::ARRAY: {
children.emplace_back(std::move(complexChildren[complexIdx]));
complexIdx++;
} break;
default: {
VectorPtr dictionary{nullptr};
if (!dictionaryFields.empty() && dictionaryIdx < dictionaryFields.size() &&
dictionaryFields[dictionaryIdx] == i) {
dictionary = dictionaries[dictionaryIdx++];
}
auto res = VELOX_DYNAMIC_SCALAR_TYPE_DISPATCH_ALL(
readFlatVector, kind, buffers, bufferIdx, numRows, types[i], dictionary, pool);
children.emplace_back(std::move(res));
} break;
}
}

return std::make_shared<RowVector>(pool, type, BufferPtr(nullptr), numRows, children);
}

std::shared_ptr<VeloxColumnarBatch> makeColumnarBatch(
RowTypePtr type,
uint32_t numRows,
std::vector<std::shared_ptr<arrow::Buffer>> arrowBuffers,
const std::vector<int32_t>& dictionaryFields,
const std::vector<VectorPtr>& dictionaries,
memory::MemoryPool* pool,
int64_t& deserializeTime) {
ScopedTimer timer(&deserializeTime);
std::vector<BufferPtr> veloxBuffers;
veloxBuffers.reserve(arrowBuffers.size());
for (auto& buffer : arrowBuffers) {
veloxBuffers.push_back(convertToVeloxBuffer(std::move(buffer)));
}
auto rowVector = deserialize(type, numRows, veloxBuffers, dictionaryFields, dictionaries, pool);
return std::make_shared<VeloxColumnarBatch>(std::move(rowVector));
}

} // namespace

@zhztheplayer zhztheplayer Oct 30, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @jinchengchenghh, any idea on reducing the code duplication of this part?

Should we just put the GPU reader implementation to VeloxShuffleReader.h / VeloxShuffleReader.cc ? cc @marin-ma

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These all will be removed after support Buffer to cudf::table, a draft version is https://github.com/jinchengchenghh/gluten/blob/shuffle/cpp/velox/shuffle/GpuShuffleReader.cc

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the explanation.

@zhztheplayer zhztheplayer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. I only went through the design and code roughly, feel free to call for other committers for review if you think is needed.

@jinchengchenghh
jinchengchenghh merged commit aeb2c67 into apache:main Oct 30, 2025
60 checks passed
metrics: Map[String, SQLMetric],
shuffleWriterType: ShuffleWriterType): Serializer = {
shuffleWriterType: ShuffleWriterType,
enableCudf: Boolean): Serializer = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed this change. Is it possible to just add one or more new ShuffleWriterType for cuff so we can avoid adding new parameters?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shuffle writer is same, only the shuffle reader is different. Each ShuffleWriterType matches to its own GpuShuffleReader

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shuffle writer type is also used on the reader side to determine which deserialiser to use. If the deserialisation are different, then would be better to have different shuffle writer type like gpu_hash, gpu_sort, etc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense, and I notice the shuffle writer type name exists in cpp and scala code, should we use protobuf for them?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Would be nice to have a better way to align them.

jinchengchenghh added a commit to jinchengchenghh/gluten that referenced this pull request Nov 4, 2025
The lock in WholeStageResultIterator restrict the cpu thread to produce batch, move the lock to here can let threads produce first batch with 1 GB in advance. Maybe the threads should prepare more data and let the GPU consume, this depends on the GPU operator time.

Need to restrict the total stage can offload to GPU, otherwise, after fallback, the lock cannot make effect on GPU execution.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLICKHOUSE CORE works for Gluten Core VELOX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants