1 //===- TFUtils.cpp - tensorflow evaluation utilities ----------------------===// 2 // 3 // The LLVM Compiler Infrastructure 4 // 5 // This file is distributed under the University of Illinois Open Source 6 // License. See LICENSE.TXT for details. 7 // 8 //===----------------------------------------------------------------------===// 9 // 10 // This file implements utilities for interfacing with tensorflow C APIs. 11 // 12 //===----------------------------------------------------------------------===// 13 #include "llvm/Config/config.h" 14 #if defined(LLVM_HAVE_TF_API) 15 16 #include "llvm/ADT/Twine.h" 17 #include "llvm/Analysis/Utils/TFUtils.h" 18 #include "llvm/Support/CommandLine.h" 19 #include "llvm/Support/Debug.h" 20 #include "llvm/Support/JSON.h" 21 #include "llvm/Support/ManagedStatic.h" 22 #include "llvm/Support/MemoryBuffer.h" 23 #include "llvm/Support/Path.h" 24 #include "llvm/Support/raw_ostream.h" 25 26 #include "google/protobuf/text_format.h" 27 #include "tensorflow/c/c_api.h" 28 #include "tensorflow/c/c_api_experimental.h" 29 #include "tensorflow/core/example/example.pb.h" 30 #include <cassert> 31 #include <numeric> 32 33 using namespace llvm; 34 35 static cl::opt<bool> 36 ProtobufTextMode("tfutils-text-log", cl::init(false), cl::Hidden, 37 cl::desc("Output textual (human-readable) protobuf.")); 38 39 namespace { 40 41 using TFGraphPtr = std::unique_ptr<TF_Graph, decltype(&TF_DeleteGraph)>; 42 using TFSessionOptionsPtr = 43 std::unique_ptr<TF_SessionOptions, decltype(&TF_DeleteSessionOptions)>; 44 using TFStatusPtr = std::unique_ptr<TF_Status, decltype(&TF_DeleteStatus)>; 45 46 struct TFInitializer { 47 TFInitializer() { 48 assert(!IsInitialized && "TFInitialized should be called only once"); 49 int Argc = 1; 50 const char *Name = ""; 51 const char **NamePtr = &Name; 52 TF_InitMain(Name, &Argc, const_cast<char ***>(&NamePtr)); 53 IsInitialized = true; 54 } 55 bool IsInitialized = false; 56 }; 57 58 llvm::ManagedStatic<TFInitializer> TFLibInitializer; 59 60 bool ensureInitTF() { return TFLibInitializer->IsInitialized; } 61 62 TFGraphPtr createTFGraph() { 63 return TFGraphPtr(TF_NewGraph(), &TF_DeleteGraph); 64 } 65 66 TFStatusPtr createTFStatus() { 67 return TFStatusPtr(TF_NewStatus(), &TF_DeleteStatus); 68 } 69 70 TFSessionOptionsPtr createTFSessionOptions() { 71 return TFSessionOptionsPtr(TF_NewSessionOptions(), &TF_DeleteSessionOptions); 72 } 73 74 /// Write a list of tensors as a sequence of TensorFlow FeatureList protobufs. 75 /// The tensors are assumed to be stored contiguously, in row-major format, 76 /// in the TensorData buffer. Each tensor has the shape given by Spec. The 77 /// feature name in the output is either the provided LoggingName, if 78 /// specified, otherwise it's the name of the tensor (as given by Spec). 79 void writeRawTensorsAsFeatureLists(tensorflow::FeatureLists *FE, 80 const LoggedFeatureSpec &LoggedSpec, 81 const char *TensorData, size_t TensorCount, 82 bool FinalReward = false) { 83 const auto &Spec = LoggedSpec.Spec; 84 // The 'Feature' protobuf only has 3 possible fields: float_list, 85 // int64_list, or bytes_list, so we capture int32 values as int64. We don't 86 // support any other types. 87 tensorflow::FeatureList &FL = (*FE->mutable_feature_list())[( 88 LoggedSpec.LoggingName ? *LoggedSpec.LoggingName : Spec.name())]; 89 90 const char *CurrentTensor = TensorData; 91 const size_t TensorByteSize = 92 Spec.getElementCount() * Spec.getElementByteSize(); 93 const size_t ElemCount = Spec.getElementCount(); 94 for (size_t E = 0; E < TensorCount; ++E) { 95 const bool ShouldWrite = E + 1 == TensorCount || !FinalReward; 96 97 if (Spec.isElementType<int64_t>()) { 98 auto *MF = FL.add_feature()->mutable_int64_list()->mutable_value(); 99 MF->Resize(ElemCount, 0); 100 if (ShouldWrite) 101 memcpy(MF->mutable_data(), CurrentTensor, TensorByteSize); 102 } else if (Spec.isElementType<int32_t>()) { 103 auto *MF = FL.add_feature()->mutable_int64_list()->mutable_value(); 104 MF->Resize(ElemCount, 0); 105 if (ShouldWrite) { 106 const int32_t *TD = reinterpret_cast<const int32_t *>(CurrentTensor); 107 for (size_t I = 0; I < ElemCount; ++I) 108 (*MF)[I] = TD[I]; 109 } 110 } else if (Spec.isElementType<float>()) { 111 auto *MF = FL.add_feature()->mutable_float_list()->mutable_value(); 112 MF->Resize(ElemCount, 0.0); 113 if (ShouldWrite) 114 memcpy(MF->mutable_data(), CurrentTensor, TensorByteSize); 115 } else { 116 llvm_unreachable("Unsupported tensor type."); 117 } 118 if (ShouldWrite) 119 CurrentTensor += TensorByteSize; 120 } 121 } 122 } // namespace 123 124 namespace llvm { 125 class EvaluationResultImpl { 126 public: 127 EvaluationResultImpl(size_t OutputSize) 128 : OutputSize(OutputSize), Output(OutputSize){}; 129 130 ~EvaluationResultImpl() { 131 for (auto *P : Output) 132 if (P) 133 TF_DeleteTensor(P); 134 } 135 136 EvaluationResultImpl(const EvaluationResultImpl &) = delete; 137 EvaluationResultImpl(EvaluationResultImpl &&Other) = delete; 138 std::vector<TF_Tensor *> &getOutput() { return Output; } 139 140 private: 141 const size_t OutputSize; 142 std::vector<TF_Tensor *> Output; 143 }; 144 145 size_t TensorSpec::getElementByteSize() const { 146 return TF_DataTypeSize(static_cast<TF_DataType>(TypeIndex)); 147 } 148 149 TensorSpec::TensorSpec(const std::string &Name, int Port, int TypeIndex, 150 const std::vector<int64_t> &Shape) 151 : Name(Name), Port(Port), TypeIndex(TypeIndex), Shape(Shape), 152 ElementCount(std::accumulate(Shape.begin(), Shape.end(), 1, 153 std::multiplies<int64_t>())) {} 154 155 Optional<TensorSpec> getTensorSpecFromJSON(LLVMContext &Ctx, 156 const json::Value &Value) { 157 auto EmitError = [&](const llvm::Twine &Message) -> Optional<TensorSpec> { 158 std::string S; 159 llvm::raw_string_ostream OS(S); 160 OS << Value; 161 Ctx.emitError("Unable to parse JSON Value as spec (" + Message + "): " + S); 162 return None; 163 }; 164 // FIXME: accept a Path as a parameter, and use it for error reporting. 165 json::Path::Root Root("tensor_spec"); 166 json::ObjectMapper Mapper(Value, Root); 167 if (!Mapper) 168 return EmitError("Value is not a dict"); 169 170 std::string TensorName; 171 int TensorPort = -1; 172 std::string TensorType; 173 std::vector<int64_t> TensorShape; 174 175 if (!Mapper.map<std::string>("name", TensorName)) 176 return EmitError("'name' property not present or not a string"); 177 if (!Mapper.map<std::string>("type", TensorType)) 178 return EmitError("'type' property not present or not a string"); 179 if (!Mapper.map<int>("port", TensorPort)) 180 return EmitError("'port' property not present or not an int"); 181 if (!Mapper.map<std::vector<int64_t>>("shape", TensorShape)) 182 return EmitError("'shape' property not present or not an int array"); 183 184 #define PARSE_TYPE(T, E) \ 185 if (TensorType == #T) \ 186 return TensorSpec::createSpec<T>(TensorName, TensorShape, TensorPort); 187 TFUTILS_SUPPORTED_TYPES(PARSE_TYPE) 188 #undef PARSE_TYPE 189 return None; 190 } 191 192 Optional<std::vector<LoggedFeatureSpec>> 193 loadOutputSpecs(LLVMContext &Ctx, StringRef ExpectedDecisionName, 194 StringRef ModelPath, StringRef SpecFileOverride) { 195 SmallVector<char, 128> OutputSpecsPath; 196 StringRef FileName = SpecFileOverride; 197 if (FileName.empty()) { 198 llvm::sys::path::append(OutputSpecsPath, ModelPath, "output_spec.json"); 199 FileName = {OutputSpecsPath.data(), OutputSpecsPath.size()}; 200 } 201 202 auto BufferOrError = MemoryBuffer::getFileOrSTDIN(FileName); 203 if (!BufferOrError) { 204 Ctx.emitError("Error opening output specs file: " + FileName + " : " + 205 BufferOrError.getError().message()); 206 return None; 207 } 208 auto ParsedJSONValues = json::parse(BufferOrError.get()->getBuffer()); 209 if (!ParsedJSONValues) { 210 Ctx.emitError("Could not parse specs file: " + FileName); 211 return None; 212 } 213 auto ValuesArray = ParsedJSONValues->getAsArray(); 214 if (!ValuesArray) { 215 Ctx.emitError("Expected an array of {tensor_spec:<TensorSpec>, " 216 "logging_name:<name>} dictionaries"); 217 return None; 218 } 219 std::vector<LoggedFeatureSpec> Ret; 220 for (const auto &Value : *ValuesArray) 221 if (const auto *Obj = Value.getAsObject()) 222 if (const auto *SpecPart = Obj->get("tensor_spec")) 223 if (auto TensorSpec = getTensorSpecFromJSON(Ctx, *SpecPart)) 224 if (auto LoggingName = Obj->getString("logging_name")) { 225 if (!TensorSpec->isElementType<int64_t>() && 226 !TensorSpec->isElementType<int32_t>() && 227 !TensorSpec->isElementType<float>()) { 228 Ctx.emitError( 229 "Only int64, int32, and float tensors are supported. " 230 "Found unsupported type for tensor named " + 231 TensorSpec->name()); 232 return None; 233 } 234 Ret.push_back({*TensorSpec, LoggingName->str()}); 235 } 236 237 if (ValuesArray->size() != Ret.size()) { 238 Ctx.emitError( 239 "Unable to parse output spec. It should be a json file containing an " 240 "array of dictionaries. Each dictionary must have a 'tensor_spec' key, " 241 "with a json object describing a TensorSpec; and a 'logging_name' key, " 242 "which is a string to use as name when logging this tensor in the " 243 "training log."); 244 return None; 245 } 246 if (Ret.empty() || *Ret[0].LoggingName != ExpectedDecisionName) { 247 Ctx.emitError("The first output spec must describe the decision tensor, " 248 "and must have the logging_name " + 249 StringRef(ExpectedDecisionName)); 250 return None; 251 } 252 return Ret; 253 } 254 255 class TFModelEvaluatorImpl { 256 public: 257 TFModelEvaluatorImpl(StringRef SavedModelPath, 258 const std::vector<TensorSpec> &InputSpecs, 259 function_ref<TensorSpec(size_t)> GetOutputSpecs, 260 size_t OutputSpecsSize, const char *Tags); 261 262 bool isValid() const { return IsValid; } 263 size_t OutputSize() const { return OutputFeed.size(); } 264 265 void evaluate(TF_Tensor **Output, TF_Status *Status) { 266 TF_SessionRun(Session, nullptr, InputFeed.data(), Input.data(), 267 Input.size(), OutputFeed.data(), Output, OutputFeed.size(), 268 nullptr, 0, nullptr, Status); 269 } 270 271 void initInput(size_t Index, TF_DataType Type, 272 const std::vector<int64_t> &Dimensions); 273 const std::vector<TF_Tensor *> &getInput() const { return Input; } 274 275 ~TFModelEvaluatorImpl(); 276 277 private: 278 /// The objects necessary for carrying out an evaluation of the SavedModel. 279 /// They are expensive to set up, and we maintain them accross all the 280 /// evaluations of the model. 281 TF_Session *Session = nullptr; 282 TFGraphPtr Graph; 283 TFSessionOptionsPtr Options; 284 285 /// The specification of the input nodes. 286 std::vector<TF_Output> InputFeed; 287 288 /// The input tensors. They must match by index of the corresponding InputFeed 289 /// value. We set up the tensors once and just mutate theirs scalars before 290 /// each evaluation. The input tensors keep their value after an evaluation. 291 std::vector<TF_Tensor *> Input; 292 293 /// The specification of the output nodes. When evaluating, the tensors in the 294 /// output tensor vector must match by index the corresponding element in the 295 /// OutputFeed. 296 std::vector<TF_Output> OutputFeed; 297 298 void invalidate() { IsValid = false; } 299 300 bool IsValid = true; 301 302 /// Reusable utility for ensuring we can bind the requested Name to a node in 303 /// the SavedModel Graph. 304 bool checkReportAndInvalidate(const TF_Output &Output, 305 const TensorSpec &OutputSpec); 306 }; 307 } // namespace llvm 308 309 TFModelEvaluatorImpl::TFModelEvaluatorImpl( 310 StringRef SavedModelPath, const std::vector<TensorSpec> &InputSpecs, 311 function_ref<TensorSpec(size_t)> GetOutputSpecs, size_t OutputSpecsSize, 312 const char *Tags = "serve") 313 : Graph(createTFGraph()), Options(createTFSessionOptions()), 314 InputFeed(InputSpecs.size()), Input(InputSpecs.size()), 315 OutputFeed(OutputSpecsSize) { 316 if (!ensureInitTF()) { 317 errs() << "Tensorflow should have been initialized"; 318 return; 319 } 320 auto Status = createTFStatus(); 321 322 Session = TF_LoadSessionFromSavedModel(Options.get(), nullptr, 323 SavedModelPath.str().c_str(), &Tags, 1, 324 Graph.get(), nullptr, Status.get()); 325 if (TF_GetCode(Status.get()) != TF_Code::TF_OK) { 326 errs() << TF_Message(Status.get()); 327 invalidate(); 328 } 329 for (size_t I = 0; I < InputSpecs.size(); ++I) { 330 auto &InputSpec = InputSpecs[I]; 331 InputFeed[I] = { 332 TF_GraphOperationByName(Graph.get(), (InputSpec.name()).c_str()), 333 InputSpec.port()}; 334 if (!checkReportAndInvalidate(InputFeed[I], InputSpec)) 335 return; 336 initInput(I, static_cast<TF_DataType>(InputSpec.typeIndex()), 337 InputSpec.shape()); 338 } 339 for (size_t I = 0; I < OutputSpecsSize; ++I) { 340 auto OutputSpec = GetOutputSpecs(I); 341 OutputFeed[I] = { 342 TF_GraphOperationByName(Graph.get(), (OutputSpec.name()).c_str()), 343 OutputSpec.port()}; 344 if (!checkReportAndInvalidate(OutputFeed[I], OutputSpec)) 345 return; 346 } 347 } 348 349 TFModelEvaluator::TFModelEvaluator( 350 StringRef SavedModelPath, const std::vector<TensorSpec> &InputSpecs, 351 function_ref<TensorSpec(size_t)> GetOutputSpecs, size_t OutputSpecsSize, 352 const char *Tags) 353 : Impl(new TFModelEvaluatorImpl(SavedModelPath, InputSpecs, GetOutputSpecs, 354 OutputSpecsSize, Tags)) { 355 if (!Impl->isValid()) 356 Impl.reset(); 357 } 358 359 TFModelEvaluator::TFModelEvaluator(StringRef SavedModelPath, 360 const std::vector<TensorSpec> &InputSpecs, 361 const std::vector<TensorSpec> &OutputSpecs, 362 const char *Tags) 363 : TFModelEvaluator( 364 SavedModelPath, InputSpecs, [&](size_t I) { return OutputSpecs[I]; }, 365 OutputSpecs.size(), Tags) {} 366 367 TFModelEvaluatorImpl::~TFModelEvaluatorImpl() { 368 for (auto *T : Input) { 369 TF_DeleteTensor(T); 370 } 371 if (Session == nullptr) 372 return; 373 auto Status = createTFStatus(); 374 TF_DeleteSession(Session, Status.get()); 375 Session = nullptr; 376 if (TF_GetCode(Status.get()) != TF_Code::TF_OK) 377 errs() << "Could not delete TF session"; 378 } 379 380 bool TFModelEvaluatorImpl::checkReportAndInvalidate( 381 const TF_Output &Output, const TensorSpec &OutputSpec) { 382 if (Output.oper) 383 return true; 384 errs() << "Could not find TF_Output named: " + OutputSpec.name(); 385 IsValid = false; 386 return IsValid; 387 } 388 389 Optional<TFModelEvaluator::EvaluationResult> TFModelEvaluator::evaluate() { 390 if (!isValid()) 391 return None; 392 std::unique_ptr<EvaluationResultImpl> Ret = 393 std::make_unique<EvaluationResultImpl>(Impl->OutputSize()); 394 auto Status = createTFStatus(); 395 Impl->evaluate(Ret->getOutput().data(), Status.get()); 396 if (TF_GetCode(Status.get()) != TF_Code::TF_OK) { 397 errs() << TF_Message(Status.get()); 398 Impl.reset(); 399 return None; 400 } 401 return EvaluationResult(std::move(Ret)); 402 } 403 404 void TFModelEvaluatorImpl::initInput(size_t Index, TF_DataType Type, 405 const std::vector<int64_t> &Dimensions) { 406 int64_t TotalSize = TF_DataTypeSize(Type); 407 for (auto &D : Dimensions) 408 TotalSize *= D; 409 410 Input[Index] = 411 TF_AllocateTensor(Type, Dimensions.data(), Dimensions.size(), TotalSize); 412 std::memset(TF_TensorData(Input[Index]), 0, TotalSize); 413 } 414 415 void *TFModelEvaluator::getUntypedInput(size_t Index) { 416 return TF_TensorData(Impl->getInput()[Index]); 417 } 418 419 TFModelEvaluator::EvaluationResult::EvaluationResult( 420 std::unique_ptr<EvaluationResultImpl> Impl) 421 : Impl(std::move(Impl)) {} 422 423 TFModelEvaluator::EvaluationResult::EvaluationResult(EvaluationResult &&Other) 424 : Impl(std::move(Other.Impl)) {} 425 426 TFModelEvaluator::EvaluationResult & 427 TFModelEvaluator::EvaluationResult::operator=(EvaluationResult &&Other) { 428 Impl = std::move(Other.Impl); 429 return *this; 430 } 431 432 void *TFModelEvaluator::EvaluationResult::getUntypedTensorValue(size_t Index) { 433 return TF_TensorData(Impl->getOutput()[Index]); 434 } 435 436 const void * 437 TFModelEvaluator::EvaluationResult::getUntypedTensorValue(size_t Index) const { 438 return TF_TensorData(Impl->getOutput()[Index]); 439 } 440 441 #define TFUTILS_GETDATATYPE_IMPL(T, E) \ 442 template <> int TensorSpec::getDataType<T>() { return E; } 443 444 TFUTILS_SUPPORTED_TYPES(TFUTILS_GETDATATYPE_IMPL) 445 446 #undef TFUTILS_GETDATATYPE_IMPL 447 448 TFModelEvaluator::EvaluationResult::~EvaluationResult() {} 449 TFModelEvaluator::~TFModelEvaluator() {} 450 451 void Logger::print(raw_ostream &OS) { 452 tensorflow::SequenceExample SE; 453 454 if (RawLogData.empty()) 455 return; 456 if (RawLogData[0].empty()) 457 return; 458 size_t Tensor0Size = FeatureSpecs[0].Spec.getElementCount() * 459 FeatureSpecs[0].Spec.getElementByteSize(); 460 size_t NumberOfRecords = RawLogData[0].size() / Tensor0Size; 461 if (NumberOfRecords == 0) 462 return; 463 size_t RewardSize = 464 RewardSpec.getElementCount() * RewardSpec.getElementByteSize(); 465 size_t NumberOfRewards = RawLogData.back().size() / RewardSize; 466 467 tensorflow::FeatureLists *FE = SE.mutable_feature_lists(); 468 for (size_t I = 0; I < FeatureSpecs.size(); ++I) 469 writeRawTensorsAsFeatureLists(FE, FeatureSpecs[I], RawLogData[I].data(), 470 NumberOfRecords); 471 472 if (IncludeReward) 473 writeRawTensorsAsFeatureLists(FE, {RewardSpec, None}, 474 RawLogData.back().data(), NumberOfRecords, 475 NumberOfRewards == 1); 476 std::string OutStr; 477 if (ProtobufTextMode) { 478 google::protobuf::TextFormat::PrintToString(SE, &OutStr); 479 } else { 480 OutStr = SE.SerializeAsString(); 481 } 482 OS << OutStr; 483 } 484 #endif // defined(LLVM_HAVE_TF_API) 485