Skip to content

Commit

Permalink
[tmva][sofie] Add Where operator
Browse files Browse the repository at this point in the history
Add support for new operator Where and add corrisponding test

Fix broadcasting of tensor in case of boolean tensor where one uses a std::vector<bool> instead of a span of bool
  • Loading branch information
lmoneta committed Dec 12, 2024
1 parent 36815c5 commit d462aa0
Show file tree
Hide file tree
Showing 11 changed files with 411 additions and 43 deletions.
1 change: 1 addition & 0 deletions tmva/sofie/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ ROOT_STANDARD_LIBRARY_PACKAGE(ROOTTMVASofie
TMVA/ROperator_Split.hxx
TMVA/ROperator_SubGraph.hxx
TMVA/ROperator_Pad.hxx
TMVA/ROperator_Where.hxx
TMVA/SOFIE_common.hxx
TMVA/SOFIEHelpers.hxx

Expand Down
27 changes: 0 additions & 27 deletions tmva/sofie/inc/TMVA/ROperator_BasicBinary.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -51,33 +51,6 @@ struct BinaryOperatorTrait<T, Pow> {
static T Func (T t1, T t2) { return std::pow(t1,t2);}
};

template <typename T>
struct TensorType {};
template<>
struct TensorType<float> {
static const std::string Name() { return "float"; }
};
template<>
struct TensorType<double> {
static const std::string Name() { return "double"; }
};
template<>
struct TensorType<int64_t> {
static const std::string Name() { return "int64_t"; }
};
template<>
struct TensorType<int32_t> {
static const std::string Name() { return "int32_t"; }
};
template<>
struct TensorType<uint32_t> {
static const std::string Name() { return "uint32_t"; }
};
template<>
struct TensorType<uint64_t> {
static const std::string Name() { return "uint64_t"; }
};

template<typename T, EBasicBinaryOperator Op>
class ROperator_BasicBinary final : public ROperator{
private:
Expand Down
240 changes: 240 additions & 0 deletions tmva/sofie/inc/TMVA/ROperator_Where.hxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
#ifndef TMVA_SOFIE_ROperator_Where
#define TMVA_SOFIE_ROperator_Where

#include "TMVA/SOFIE_common.hxx"
#include "TMVA/ROperator.hxx"
#include "TMVA/RModel.hxx"

#include <sstream>

namespace TMVA{
namespace Experimental{
namespace SOFIE{



template<typename T>
class ROperator_Where final : public ROperator{
private:

bool fIsInputBoolTensor = false;


std::string fNA;
std::string fNB;
std::string fNC;
std::string fNBroadcastedA;
std::string fNBroadcastedB;
std::string fNBroadcastedC;
std::string fNY;


std::vector<size_t> fShapeA;
std::vector<size_t> fShapeB;
std::vector<size_t> fShapeC;
std::vector<size_t> fShapeY;


public:
ROperator_Where(){}
ROperator_Where(const std::string & nameA, const std::string & nameB, const std::string & nameC, const std::string & nameY):
fNA(UTILITY::Clean_name(nameA)), fNB(UTILITY::Clean_name(nameB)), fNC(UTILITY::Clean_name(nameC)), fNY(UTILITY::Clean_name(nameY)){}

// type of output given input
std::vector<ETensorType> TypeInference(std::vector<ETensorType> input) override {
return input;
}

// shape of output tensors given input tensors
std::vector<std::vector<size_t>> ShapeInference(std::vector<std::vector<size_t>> input) override {
// assume now inputs have same shape (no broadcasting)
auto ret = std::vector<std::vector<size_t>>(1, input[0]); // return vector size 1 with first input
return ret;
}

void Initialize(RModel& model) override {
// input must be a graph input, or already initialized intermediate tensor
if (!model.CheckIfTensorAlreadyExist(fNA)){
throw std::runtime_error(std::string("TMVA SOFIE Where Op Input Tensor ") + fNA + "is not found in model");
}
if (!model.CheckIfTensorAlreadyExist(fNB)) {
throw std::runtime_error(std::string("TMVA SOFIE Where Op Input Tensor ") + fNB + "is not found in model");
}
if (!model.CheckIfTensorAlreadyExist(fNC)) {
throw std::runtime_error(std::string("TMVA SOFIE Where Op Input Tensor ") + fNC + "is not found in model");
}
// check if fNC input tensor is boolean
if (model.IsReadyInputTensor(fNC))
fIsInputBoolTensor = true;
// check broadcast for A, B and C
fShapeA = model.GetTensorShape(fNA);
fShapeB = model.GetTensorShape(fNB);
fShapeC = model.GetTensorShape(fNC);
bool broadcast = !UTILITY::AreSameShape(fShapeA, fShapeB) || !UTILITY::AreSameShape(fShapeA, fShapeC);
if (broadcast) {
// find shape to broadcast between A,B,C looking for max length
size_t lengthA = ConvertShapeToLength(fShapeA);
size_t lengthB = ConvertShapeToLength(fShapeB);
size_t lengthC = ConvertShapeToLength(fShapeC);
bool broadcastA = false, broadcastB = false, broadcastC = false;
if (lengthA >= lengthB && lengthA >= lengthC) {
fShapeY = fShapeA;
//broadcast B and C if different than A
broadcastB = (lengthB != lengthA);
broadcastC = (lengthC != lengthA);
}
else if (lengthB >= lengthA && lengthB >= lengthC) {
fShapeY = fShapeB;
//broadcast A and C if different than B
broadcastA = (lengthA != lengthB);
broadcastC = (lengthC != lengthB);
}
else if (lengthC >= lengthA && lengthC >= lengthB) {
fShapeY = fShapeC;
//broadcast A and B if different than C
broadcastA = (lengthA != lengthC);
broadcastB = (lengthB != lengthC);
}

// Broadcast A to Y
if (broadcastA) {
fNBroadcastedA = "BC_" + fNA + "_to_" + fNY;
if (model.IsInitializedTensor(fNA)) {
auto data = model.GetInitializedTensorData(fNA);
std::shared_ptr<void> broadcastedData(
UTILITY::UnidirectionalBroadcast<T>(static_cast<T *>(data.get()), fShapeA, fShapeY),
std::default_delete<T[]>());
// Update the data and the shape of A
model.AddConstantTensor(fNBroadcastedA, model.GetTensorType(fNA), fShapeY, broadcastedData);
fShapeA = fShapeY;
} else {
// Add an intermediate tensor for broadcasting A
model.AddIntermediateTensor(fNBroadcastedA, model.GetTensorType(fNA), fShapeY);
}
}
// Broadcast B to Y
if (broadcastB) {
fNBroadcastedB = "BC_" + fNB + "_to_" + fNY;
if (model.IsInitializedTensor(fNB)) {
auto data = model.GetInitializedTensorData(fNB);
std::shared_ptr<void> broadcastedData(
UTILITY::UnidirectionalBroadcast<T>(static_cast<T *>(data.get()), fShapeB, fShapeY),
std::default_delete<T[]>());
// do not update tensor B but add broadcasted one (since it can be input to some other operators)
model.AddConstantTensor(fNBroadcastedB, model.GetTensorType(fNB), fShapeY, broadcastedData);
fShapeB = fShapeY;
} else {
// Add an intermediate tensor for broadcasting B
model.AddIntermediateTensor(fNBroadcastedB, model.GetTensorType(fNB), fShapeY);
}
}
// Broadcast C to Y
if (broadcastC) {
fNBroadcastedC = "BC_" + fNC + "_to_" + fNY;
if (model.IsInitializedTensor(fNC)) {
auto data = model.GetInitializedTensorData(fNC);
std::shared_ptr<void> broadcastedData(
UTILITY::UnidirectionalBroadcast<T>(static_cast<T *>(data.get()), fShapeC, fShapeY),
std::default_delete<T[]>());
// do not update tensor C but add broadcasted one (since it can be input to some other operators)
model.AddConstantTensor(fNBroadcastedC, model.GetTensorType(fNC), fShapeY, broadcastedData);
fShapeC = fShapeY;
} else {
// Add an intermediate tensor for broadcasting B
model.AddIntermediateTensor(fNBroadcastedC, model.GetTensorType(fNC), fShapeY);
}
}
} else {
fShapeY = fShapeA;
}
// check case of constant output (if all inputs are defined)
if (model.IsInitializedTensor(fNA) && model.IsInitializedTensor(fNB) && model.IsInitializedTensor(fNC)) {
std::string nameA = fNBroadcastedA.empty()? fNA : fNBroadcastedA;
std::string nameB = fNBroadcastedB.empty()? fNB : fNBroadcastedB;
std::string nameC = fNBroadcastedC.empty()? fNC : fNBroadcastedC;
auto dataA = static_cast<T *>(model.GetInitializedTensorData(nameA).get());
auto dataB = static_cast<T *>(model.GetInitializedTensorData(nameB).get());
auto dataC = static_cast<bool *>(model.GetInitializedTensorData(nameC).get());
std::vector<T> dataY(ConvertShapeToLength(fShapeY));
for (size_t i = 0; i < dataY.size(); i++)
dataY[i] = (dataC[i]) ? dataA[i] : dataB[i];
model.AddConstantTensor<T>(fNY, fShapeY, dataY.data());
// flag tensors to not be written in a file
model.SetNotWritableInitializedTensor(nameA);
model.SetNotWritableInitializedTensor(nameB);
model.SetNotWritableInitializedTensor(nameC);

fIsOutputConstant = true;
if (model.Verbose())
std::cout << "Where op ---> " << fNY << " " << ConvertShapeToString(fShapeY) << " : "
<< ConvertValuesToString(dataY) << std::endl;
}
else {
model.AddIntermediateTensor(fNY, model.GetTensorType(fNA), fShapeY);
}
}

std::string GenerateInitCode() override {
std::stringstream out;
return out.str();
}

std::string Generate(std::string OpName) override {

if (fIsOutputConstant) return "";

OpName = "op_" + OpName;

if (fShapeY.empty()) {
throw std::runtime_error("TMVA SOFIE Where Op called to Generate without being initialized first");
}
std::stringstream out;
out << SP << "\n//-------- Where \n";
size_t length = ConvertShapeToLength(fShapeY);
std::string typeName = TensorType<T>::Name();
// Broadcast A if it's uninitialized
if (fShapeA != fShapeY) {
out << SP << "// Broadcasting uninitialized tensor " << fNA << "\n";
//out << SP << "{\n";
out << SP << "TMVA::Experimental::SOFIE::UTILITY::UnidirectionalBroadcast<" << typeName << ">(tensor_" << fNA << ", " << ConvertShapeToString(fShapeA) << ", " << ConvertShapeToString(fShapeY)
<< ", fTensor_" << fNBroadcastedA << ");\n";
}
// Broadcast B if it's uninitialized
if (fShapeB != fShapeY) {
out << SP << "// Broadcasting uninitialized tensor " << fNB << "\n";
//out << SP << "{\n";
out << SP << "TMVA::Experimental::SOFIE::UTILITY::UnidirectionalBroadcast<" << typeName << ">(tensor_" << fNB << ", " << ConvertShapeToString(fShapeB) << ", " << ConvertShapeToString(fShapeY)
<< ", fTensor_" << fNBroadcastedB << ");\n";
}
// Broadcast C if it's uninitialized
if (fShapeC != fShapeY) {
// special case if C is an input tensor
if (fIsInputBoolTensor) {
size_t inputLength = ConvertShapeToLength(fShapeC);
out << SP << "std::vector<bool> fTensor_" << fNC << "(tensor_" << fNC << ", tensor_" << fNC << " + " << inputLength << ");\n";
}
out << SP << "// Broadcasting uninitialized tensor " << fNC << "\n";
//out << SP << "{\n";
// for boolean we need to pass vector<bool> and use the non-template version of the function
out << SP << "TMVA::Experimental::SOFIE::UTILITY::UnidirectionalBroadcast(fTensor_" << fNC << ", " << ConvertShapeToString(fShapeC) << ", " << ConvertShapeToString(fShapeY)
<< ", fTensor_" << fNBroadcastedC << ");\n";
}
std::string nameA = fNBroadcastedA.empty()? fNA : fNBroadcastedA;
std::string nameB = fNBroadcastedB.empty()? fNB : fNBroadcastedB;
std::string nameC = fNBroadcastedC.empty()? fNC : fNBroadcastedC;
out << SP << "for (size_t id = 0; id < " << length << " ; id++){\n";
// get output tensor applying condition (note we need to use directly the vector<bool> since v.data(), i.e the data pointer, does not exist)
out << SP << SP << "tensor_" << fNY << "[id] = " << "(fTensor_" << nameC << "[id]) ? tensor_"
<< nameA << "[id] : tensor_" + nameB + "[id];\n";
out << SP << "}\n";
return out.str();
}

};

}//SOFIE
}//Experimental
}//TMVA


#endif //TMVA_SOFIE_ROperator_Where
59 changes: 47 additions & 12 deletions tmva/sofie/inc/TMVA/SOFIE_common.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,35 @@ struct DynamicTensorInfo{
std::vector<Dim> shape;
};

// template traits for Tensor type
template <typename T>
struct TensorType {};
template<>
struct TensorType<float> {
static const std::string Name() { return "float"; }
};
template<>
struct TensorType<double> {
static const std::string Name() { return "double"; }
};
template<>
struct TensorType<int64_t> {
static const std::string Name() { return "int64_t"; }
};
template<>
struct TensorType<int32_t> {
static const std::string Name() { return "int32_t"; }
};
template<>
struct TensorType<uint32_t> {
static const std::string Name() { return "uint32_t"; }
};
template<>
struct TensorType<uint64_t> {
static const std::string Name() { return "uint64_t"; }
};


std::vector<Dim> ConvertShapeToDim(std::vector<size_t> shape);

std::vector<size_t> ConvertShapeToInt(std::vector<Dim> shape);
Expand Down Expand Up @@ -251,12 +280,12 @@ T* BroadcastConvBias(const T* data, const size_t channel, const std::vector<size
// Broadcast a tensor from shape to targetShape according to numpy broadcasting rules
// See more at https://numpy.org/doc/stable/user/basics.broadcasting.html
// and https://github.com/onnx/onnx/blob/main/docs/Broadcasting.md .
template<typename T>
void BroadcastTensor(const T* data, const std::vector<size_t>& shape, const std::vector<size_t>& targetShape, std::span<T> broadcastedData) {
template<typename T, class ContT = std::span<T> >
void BroadcastTensor(ContT data, const std::vector<size_t>& shape, const std::vector<size_t>& targetShape, ContT broadcastedData) {
// Size of the shapes (tensor input here have shapes with same sizes, we have already added the needed ones )
size_t size = shape.size();
// Current length of the broadcasted tensor
size_t curLength = ConvertShapeToLength(shape);
size_t curLength = data.size();
size_t targetLength = broadcastedData.size();
assert(ConvertShapeToLength(targetShape) == targetLength);
// special case when broadcasting last dimensions (initial shapes must be the same)
Expand All @@ -273,7 +302,7 @@ void BroadcastTensor(const T* data, const std::vector<size_t>& shape, const std:
return;
}

std::copy(data, data + curLength, broadcastedData.begin());
std::copy(data.begin(), data.end(), broadcastedData.begin());
// Product of the previous dimensions of targetShape
size_t arrayNum = 1;
// New broadcasted data: is this needed?
Expand Down Expand Up @@ -318,41 +347,47 @@ void BroadcastTensor(const T* data, const std::vector<size_t>& shape, const std:

// interface where we allocate a new array for broadcasted data
template<typename T>
T* BroadcastTensor(const T* data, const std::vector<size_t>& shape, const std::vector<size_t>& targetShape, size_t targetLength) {
T* CreateBroadcastTensor(T* data, const std::vector<size_t>& shape, const std::vector<size_t>& targetShape, size_t targetLength) {
// newShape is an array of size equal to dimension along which we are broadcasting the tensor
T* broadcastedData = new T[targetLength];
std::span<T> bData(broadcastedData, broadcastedData+targetLength);
BroadcastTensor(data, shape, targetShape, bData);
size_t curLength = ConvertShapeToLength(shape);
std::span<T> inData(data, data+curLength);
BroadcastTensor<T, std::span<T>>(inData, shape, targetShape, bData);
return broadcastedData;
}
// Unidirectional broadcasting shape to targetShape// In unidirectional broadcast - only tensor B can have the shape changed not
// tensor A - otherwise is a multidirectional broadcast
template<typename T>
T* UnidirectionalBroadcast(const T* data, const std::vector<size_t>& shape, const std::vector<size_t>& targetShape) {
T* UnidirectionalBroadcast(T* data, const std::vector<size_t>& shape, const std::vector<size_t>& targetShape) {
// Prepend shape with ones
if (shape.size() < targetShape.size()) {
size_t targetSize = targetShape.size();
std::vector<size_t> newShape(targetSize, 1);
size_t offset = targetSize - shape.size();
std::copy(shape.begin(), shape.end(), newShape.begin() + offset);
return BroadcastTensor<T>(data, newShape, targetShape, ConvertShapeToLength(targetShape));
return CreateBroadcastTensor<T>(data, newShape, targetShape, ConvertShapeToLength(targetShape));
}
return BroadcastTensor<T>(data, shape, targetShape, ConvertShapeToLength(targetShape));
return CreateBroadcastTensor<T>(data, shape, targetShape, ConvertShapeToLength(targetShape));
}

// Unidirectional broadcasting shape to targetShape using a passed vector to avoid allocations
template<typename T>
void UnidirectionalBroadcast(const T* data, const std::vector<size_t>& shape, const std::vector<size_t>& targetShape, std::span<T> broadcastedData) {
void UnidirectionalBroadcast(T* data, const std::vector<size_t>& shape, const std::vector<size_t>& targetShape, std::span<T> broadcastedData) {
size_t curLength = ConvertShapeToLength(shape);
std::span<T> inData(data, data+curLength);
// Prepend shape with ones
if (shape.size() < targetShape.size()) {
size_t targetSize = targetShape.size();
std::vector<size_t> newShape(targetSize, 1);
size_t offset = targetSize - shape.size();
std::copy(shape.begin(), shape.end(), newShape.begin() + offset);
BroadcastTensor<T>(data, newShape, targetShape, broadcastedData);
BroadcastTensor<T>(inData, newShape, targetShape, broadcastedData);
}
BroadcastTensor<T>(data, shape, targetShape, broadcastedData);
BroadcastTensor<T, std::span<T>>(inData, shape, targetShape, broadcastedData);
}
// specialization for vector of boolean
void UnidirectionalBroadcast(const std::vector<bool> & data, const std::vector<size_t>& shape, const std::vector<size_t>& targetShape, std::vector<bool> & broadcastedData);

/// compute stride of a tensor given its shape (assume layout is row-major)
std::vector<size_t> ComputeStrideFromShape(const std::vector<size_t> & shape);
Expand Down
Loading

0 comments on commit d462aa0

Please sign in to comment.