Files
deeplearning4j--deeplearning4j/libnd4j/include/ops/declarable/generic/boolean/where.cpp
T
2026-07-13 12:47:05 +08:00

339 lines
12 KiB
C++

/* ******************************************************************************
*
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0.
*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
* 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.
*
* SPDX-License-Identifier: Apache-2.0
******************************************************************************/
//
// @author raver119@gmail.com
//
#include <system/op_boilerplate.h>
#if NOT_EXCLUDED(OP_Where)
#include <helpers/ShapeUtils.h>
#include <ops/declarable/CustomOperations.h>
#include <ops/declarable/helpers/where.h>
namespace sd {
namespace ops {
// Helper function to evaluate condition regardless of underlying data type
inline bool evaluateCondition(NDArray* condition, int index) {
switch(condition->dataType()) {
#if defined(HAS_BOOL)
case DataType::BOOL:
return condition->e<bool>(index);
#endif
#if defined(HAS_INT8)
case DataType::INT8:
return condition->e<int8_t>(index) != 0;
#endif
#if defined(HAS_INT16)
case DataType::INT16:
return condition->e<int16_t>(index) != 0;
#endif
#if defined(HAS_INT32)
case DataType::INT32:
return condition->e<int32_t>(index) != 0;
#endif
#if defined(HAS_LONG)
case DataType::INT64:
return condition->e<sd::LongType>(index) != 0;
#endif
#if defined(HAS_UINT8)
case DataType::UINT8:
return condition->e<uint8_t>(index) != 0;
#endif
#if defined(HAS_UINT16)
case DataType::UINT16:
return condition->e<uint16_t>(index) != 0;
#endif
#if defined(HAS_UINT32)
case DataType::UINT32:
return condition->e<uint32_t>(index) != 0;
#endif
#if defined(HAS_UNSIGNEDLONG)
case DataType::UINT64:
return condition->e<sd::UnsignedLong>(index) != 0;
#endif
#if defined(HAS_FLOAT16)
case DataType::HALF:
return condition->e<float16>(index) != static_cast<float16>(0.0f);
#endif
#if defined(HAS_BFLOAT16)
case DataType::BFLOAT16:
return condition->e<bfloat16>(index) != static_cast<bfloat16>(0.0f);
#endif
#if defined(HAS_FLOAT32)
case DataType::FLOAT32:
return condition->e<float>(index) != 0.0f;
#endif
#if defined(HAS_DOUBLE)
case DataType::DOUBLE:
return condition->e<double>(index) != 0.0;
#endif
default:
// Fallback: try to interpret as int32 and check if non-zero
#if defined(HAS_INT32)
#ifdef __cpp_exceptions
try {
return condition->e<int32_t>(index) != 0;
} catch (...) {
// Last resort: assume false to maintain safe behavior
return false;
}
#else
return condition->e<int32_t>(index) != 0;
#endif
#else
// If INT32 is not available, return false as safe default
return false;
#endif
}
}
// Helper function to perform element-wise where with proper broadcasting
void performBroadcastedWhere(NDArray* condition, NDArray* x, NDArray* y, NDArray* z) {
// We'll process each element of the output array z
// and determine the appropriate indices for condition, x, and y based on broadcasting rules
auto* zShape = z->getShapeAsVector();
auto* condShape = condition->getShapeAsVector();
auto* xShape = x->getShapeAsVector();
auto* yShape = y->getShapeAsVector();
// For each element in the output array
for (LongType i = 0; i < z->lengthOf(); i++) {
// Convert linear index to multi-dimensional indices for output array
std::vector<LongType> zIndices(z->rankOf());
LongType remainder = i;
for (int dim = z->rankOf() - 1; dim >= 0; dim--) {
zIndices[dim] = remainder % z->sizeAt(dim);
remainder /= z->sizeAt(dim);
}
// Calculate corresponding indices in condition, x, and y arrays using broadcasting rules
auto getLinearIndex = [](const std::vector<LongType>& multiIndices, const std::vector<LongType>& shape, NDArray* array) -> LongType {
LongType linearIndex = 0;
LongType stride = 1;
int srcDim = shape.size() - 1;
for (int dim = multiIndices.size() - 1; dim >= 0; dim--) {
LongType srcIndex = 0;
if (srcDim >= 0) {
if (shape[srcDim] == 1) {
srcIndex = 0; // Broadcast dimension
} else {
srcIndex = multiIndices[dim];
}
srcDim--;
}
linearIndex += srcIndex * stride;
if (srcDim >= 0) {
stride *= shape[srcDim + 1];
}
}
return linearIndex;
};
LongType condIndex = condition->lengthOf() == 1 ? 0 : getLinearIndex(zIndices, *condShape, condition);
LongType xIndex = x->lengthOf() == 1 ? 0 : getLinearIndex(zIndices, *xShape, x);
LongType yIndex = y->lengthOf() == 1 ? 0 : getLinearIndex(zIndices, *yShape, y);
// Apply the where logic
if (z->isR()) {
#ifdef HAS_DOUBLE
auto result = evaluateCondition(condition, condIndex) ?
x->e<double>(xIndex) : y->e<double>(yIndex);
#elif defined(HAS_FLOAT32)
auto result = evaluateCondition(condition, condIndex) ?
x->e<float>(xIndex) : y->e<float>(yIndex);
#else
#error "No floating-point type available for where operation"
#endif
z->p(i, result);
} else{
auto result = evaluateCondition(condition, condIndex) ?
x->e<LongType>(xIndex) : y->e<LongType>(yIndex);
z->p(i, result);
}
}
delete zShape;
delete condShape;
delete xShape;
delete yShape;
}
CUSTOM_OP_IMPL(Where, 1, 1, false, 0, 0) {
auto condition = INPUT_VARIABLE(0);
auto z = OUTPUT_VARIABLE(0);
if (z->isEmpty()) return Status::OK;
if (block.width() == 3) {
auto x = INPUT_VARIABLE(1);
auto y = INPUT_VARIABLE(2);
// Check if x and y can be broadcast together (instead of requiring exact same shape)
REQUIRE_TRUE(x->isSameShape(y) || ShapeUtils::areShapesBroadcastable(*x, *y), 0,
"X and Y must have equal shapes or be broadcastable. X shape: %s, Y shape: %s",
ShapeUtils::shapeAsString(x).c_str(), ShapeUtils::shapeAsString(y).c_str());
// Case 1: All arrays have exact shape matching (element-wise operation)
if (condition->isSameShape(x) && x->isSameShape(y)) {
// FIXME: for perf it might be better to issue memcpy here, and fill only mismatched values from either X or Y
for (int e = 0; e < condition->lengthOf(); e++) {
if (z->isR()) {
#ifdef HAS_DOUBLE
auto r = !evaluateCondition(condition, e) ? y->e<double>(e) : x->e<double>(e);
#elif defined(HAS_FLOAT32)
auto r = !evaluateCondition(condition, e) ? y->e<float>(e) : x->e<float>(e);
#else
#error "No floating-point type available for where operation"
#endif
z->p(e, r);
} else {
auto r = !evaluateCondition(condition, e) ? y->e<LongType>(e) : x->e<LongType>(e);
z->p(e, r);
}
}
}
// Case 2: Broadcasting is possible (most flexible case)
else if (ShapeUtils::areShapesBroadcastable(*condition, *x) &&
ShapeUtils::areShapesBroadcastable(*condition, *y) &&
ShapeUtils::areShapesBroadcastable(*x, *y)) {
performBroadcastedWhere(condition, x, y, z);
}
// Case 3: TAD-mask operation (legacy behavior for specific cases)
else if (condition->rankOf() == 1 && condition->lengthOf() == x->sizeAt(0)) {
std::vector<LongType> zero({0});
auto dims = ShapeUtils::evalDimsToExclude(x->rankOf(), 1, zero.data());
auto tadsX = x->allTensorsAlongDimension(*dims);
auto tadsY = y->allTensorsAlongDimension(*dims);
auto tadsZ = z->allTensorsAlongDimension(*dims);
for (int e = 0; e < tadsX.size(); e++) {
if (!evaluateCondition(condition, e)) {
tadsZ.at(e)->assign(tadsY.at(e));
} else {
tadsZ.at(e)->assign(tadsX.at(e));
}
}
delete dims;
}
// Case 4: Invalid shapes - provide detailed error message
else {
std::string condShape = ShapeUtils::shapeAsString(condition);
std::string xShape = ShapeUtils::shapeAsString(x);
std::string yShape = ShapeUtils::shapeAsString(y);
REQUIRE_TRUE(false, 0,
"Where operation: Invalid shapes for broadcasting. "
"Condition shape: %s, X shape: %s, Y shape: %s. "
"Condition must either: (1) match X/Y shapes exactly, "
"(2) be broadcastable with X/Y shapes, or "
"(3) be 1D with length equal to first dimension of X/Y for TAD-mask operation.",
condShape.c_str(), xShape.c_str(), yShape.c_str());
}
} else {
// in this case we return 2D matrix, which basically contains coordinates fo true
REQUIRE_TRUE(block.width() == 1, 0, "Where op takes either 1 or 3 operands, But got %d operands instead",
block.width());
auto output = OUTPUT_VARIABLE(0);
std::vector<LongType> zero({0});
int width = condition->rankOf();
if (z->isEmpty()) return Status::OK;
std::vector<LongType> *dims = ShapeUtils::evalDimsToExclude(width,1,zero.data());
helpers::_where(block.launchContext(), *condition, *output, block.workspace());
delete dims;
}
return Status::OK;
}
DECLARE_SHAPE_FN(Where) {
if (block.width() == 3) {
auto x = INPUT_VARIABLE(1);
auto y = INPUT_VARIABLE(2);
// Calculate the broadcast result shape for x and y
LongType* resultShapeInfo = nullptr;
bool canBroadcast = ShapeUtils::evalBroadcastShapeInfo(*x, *y, true, resultShapeInfo, block.getWorkspace());
if (canBroadcast && resultShapeInfo != nullptr) {
return SHAPELIST(CONSTANT(resultShapeInfo));
} else {
// Fallback to x's shape if broadcasting fails (should have been caught in validation)
auto inShape = inputShape->at(1);
return SHAPELIST(CONSTANT(inShape));
}
} else {
// FIXME: we can't estimate result here in this case
// output shape is the 2D tensor num_true x rankOf (inShape)
auto condition = INPUT_VARIABLE(0);
auto inShape = inputShape->at(0);
LongType numOfTrue = 0; // condition->reduceNumber(reduce::CountNonZero, nullptr).e<sd::LongType>(0);
for (LongType i = 0; i < condition->lengthOf(); i++)
if (evaluateCondition(condition, i)) numOfTrue++;
LongType * theNewShape;
if (numOfTrue > 0) {
LongType* newShape;
ALLOCATE(newShape, block.getWorkspace(), shape::shapeInfoLength(2), sd::LongType);
newShape[0] = 2;
newShape[1] = numOfTrue;
newShape[2] = shape::rank(inShape);
newShape[3] = 1;
newShape[4] = 1;
newShape[5] = 0;
newShape[6] = 1;
newShape[7] = 99;
#if defined(HAS_LONG)
ShapeUtils::updateStridesAndType(newShape, INT64, 'c');
#else
// Fallback to INT32 if INT64 is not available
ShapeUtils::updateStridesAndType(newShape, INT32, 'c');
#endif
theNewShape = CONSTANT(newShape);
RELEASE(newShape, block.getWorkspace());
} else {
#if defined(HAS_LONG)
theNewShape = ConstantShapeHelper::getInstance().emptyShapeInfo(INT64);
#else
// Fallback to INT32 if INT64 is not available
theNewShape = ConstantShapeHelper::getInstance().emptyShapeInfo(INT32);
#endif
}
return SHAPELIST(theNewShape);
}
}
DECLARE_TYPES(Where) {
getOpDescriptor()
->setAllowedInputTypes(0, ANY) // bool
->setAllowedInputTypes(1, ANY)
->setAllowedInputTypes(2, ANY)
->setAllowedOutputTypes(0, {ALL_INTS, ALL_FLOATS,BOOL});
}
} // namespace ops
} // namespace sd
#endif