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