| //===- ModelUnderTrainingRunner.cpp - 'development' mode runner -----------===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| // |
| // Implementation of a MLModelRunner for 'development' mode, i.e. evaluation |
| // happens off a model that's provided from the command line and is interpreted. |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "llvm/ADT/STLExtras.h" |
| #include "llvm/Config/config.h" |
| #if defined(LLVM_HAVE_TFLITE) |
| #include "llvm/Analysis/ModelUnderTrainingRunner.h" |
| #include "llvm/Support/MemoryBuffer.h" |
| #include "llvm/Support/Path.h" |
| #include <optional> |
| |
| using namespace llvm; |
| namespace { |
| struct LoggedFeatureSpec { |
| TensorSpec Spec; |
| std::optional<std::string> LoggingName; |
| }; |
| |
| std::optional<std::vector<LoggedFeatureSpec>> |
| loadOutputSpecs(LLVMContext &Ctx, StringRef ExpectedDecisionName, |
| StringRef ModelPath, StringRef SpecFileOverride) { |
| SmallVector<char, 128> OutputSpecsPath; |
| StringRef FileName = SpecFileOverride; |
| if (FileName.empty()) { |
| llvm::sys::path::append(OutputSpecsPath, ModelPath, "output_spec.json"); |
| FileName = {OutputSpecsPath.data(), OutputSpecsPath.size()}; |
| } |
| |
| auto BufferOrError = MemoryBuffer::getFileOrSTDIN(FileName); |
| if (!BufferOrError) { |
| Ctx.emitError("Error opening output specs file: " + FileName + " : " + |
| BufferOrError.getError().message()); |
| return std::nullopt; |
| } |
| auto ParsedJSONValues = json::parse(BufferOrError.get()->getBuffer()); |
| if (!ParsedJSONValues) { |
| Ctx.emitError("Could not parse specs file: " + FileName); |
| return std::nullopt; |
| } |
| auto ValuesArray = ParsedJSONValues->getAsArray(); |
| if (!ValuesArray) { |
| Ctx.emitError("Expected an array of {tensor_spec:<TensorSpec>, " |
| "logging_name:<name>} dictionaries"); |
| return std::nullopt; |
| } |
| std::vector<LoggedFeatureSpec> Ret; |
| for (const auto &Value : *ValuesArray) |
| if (const auto *Obj = Value.getAsObject()) |
| if (const auto *SpecPart = Obj->get("tensor_spec")) |
| if (auto TensorSpec = getTensorSpecFromJSON(Ctx, *SpecPart)) |
| if (auto LoggingName = Obj->getString("logging_name")) { |
| if (!TensorSpec->isElementType<int64_t>() && |
| !TensorSpec->isElementType<int32_t>() && |
| !TensorSpec->isElementType<float>()) { |
| Ctx.emitError( |
| "Only int64, int32, and float tensors are supported. " |
| "Found unsupported type for tensor named " + |
| TensorSpec->name()); |
| return std::nullopt; |
| } |
| Ret.push_back({*TensorSpec, LoggingName->str()}); |
| } |
| |
| if (ValuesArray->size() != Ret.size()) { |
| Ctx.emitError( |
| "Unable to parse output spec. It should be a json file containing an " |
| "array of dictionaries. Each dictionary must have a 'tensor_spec' key, " |
| "with a json object describing a TensorSpec; and a 'logging_name' key, " |
| "which is a string to use as name when logging this tensor in the " |
| "training log."); |
| return std::nullopt; |
| } |
| if (Ret.empty() || *Ret[0].LoggingName != ExpectedDecisionName) { |
| Ctx.emitError("The first output spec must describe the decision tensor, " |
| "and must have the logging_name " + |
| StringRef(ExpectedDecisionName)); |
| return std::nullopt; |
| } |
| return Ret; |
| } |
| } // namespace |
| |
| ModelUnderTrainingRunner::ModelUnderTrainingRunner( |
| LLVMContext &Ctx, const std::string &ModelPath, |
| const std::vector<TensorSpec> &InputSpecs, |
| const std::vector<TensorSpec> &OutputSpecs, |
| const std::vector<TensorSpec> &ExtraOutputsForLogging) |
| : MLModelRunner(Ctx, MLModelRunner::Kind::Development, InputSpecs.size()), |
| OutputSpecs(OutputSpecs), ExtraOutputsForLogging(ExtraOutputsForLogging) { |
| Evaluator = |
| std::make_unique<TFModelEvaluator>(ModelPath, InputSpecs, OutputSpecs); |
| if (!Evaluator || !Evaluator->isValid()) { |
| Ctx.emitError("Failed to create saved model evaluator"); |
| Evaluator.reset(); |
| return; |
| } |
| |
| for (size_t I = 0, E = InputSpecs.size(); I < E; ++I) { |
| setUpBufferForTensor(I, InputSpecs[I], Evaluator->getUntypedInput(I)); |
| } |
| } |
| |
| void *ModelUnderTrainingRunner::evaluateUntyped() { |
| LastEvaluationResult = Evaluator->evaluate(); |
| if (!LastEvaluationResult.has_value()) { |
| Ctx.emitError("Error evaluating model."); |
| return nullptr; |
| } |
| return LastEvaluationResult->getUntypedTensorValue(0); |
| } |
| |
| std::unique_ptr<ModelUnderTrainingRunner> |
| ModelUnderTrainingRunner::createAndEnsureValid( |
| LLVMContext &Ctx, const std::string &ModelPath, StringRef DecisionName, |
| const std::vector<TensorSpec> &InputSpecs, |
| StringRef OutputSpecsPathOverride) { |
| if (auto MaybeOutputSpecs = loadOutputSpecs(Ctx, DecisionName, ModelPath, |
| OutputSpecsPathOverride)) { |
| std::unique_ptr<ModelUnderTrainingRunner> MUTR; |
| std::vector<TensorSpec> OutputSpecs; |
| std::vector<TensorSpec> ExtraOutputsForLogging; |
| append_range(OutputSpecs, |
| map_range(*MaybeOutputSpecs, [](const LoggedFeatureSpec &LFS) { |
| return LFS.Spec; |
| })); |
| append_range(ExtraOutputsForLogging, |
| map_range(drop_begin(*MaybeOutputSpecs), |
| [](const LoggedFeatureSpec &LFS) { |
| return TensorSpec(LFS.LoggingName |
| ? *LFS.LoggingName |
| : LFS.Spec.name(), |
| LFS.Spec); |
| })); |
| |
| MUTR.reset(new ModelUnderTrainingRunner( |
| Ctx, ModelPath, InputSpecs, OutputSpecs, ExtraOutputsForLogging)); |
| if (MUTR && MUTR->isValid()) |
| return MUTR; |
| |
| Ctx.emitError("Could not load or create model evaluator."); |
| return nullptr; |
| } |
| Ctx.emitError("Could not load the policy model from the provided path"); |
| return nullptr; |
| } |
| |
| #endif // defined(LLVM_HAVE_TFLITE) |