1 //===- MLModelRunnerTest.cpp - test for MLModelRunner ---------------------===//
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7 //===----------------------------------------------------------------------===//
9 #include "llvm/Analysis/MLModelRunner.h"
10 #include "llvm/ADT/StringExtras.h"
11 #include "llvm/Analysis/InteractiveModelRunner.h"
12 #include "llvm/Analysis/NoInferenceModelRunner.h"
13 #include "llvm/Analysis/ReleaseModeModelRunner.h"
14 #include "llvm/Config/llvm-config.h" // for LLVM_ON_UNIX
15 #include "llvm/Support/BinaryByteStream.h"
16 #include "llvm/Support/ErrorHandling.h"
17 #include "llvm/Support/FileSystem.h"
18 #include "llvm/Support/FileUtilities.h"
19 #include "llvm/Support/JSON.h"
20 #include "llvm/Support/Path.h"
21 #include "llvm/Support/raw_ostream.h"
22 #include "llvm/Testing/Support/SupportHelpers.h"
23 #include "gtest/gtest.h"
30 // This is a mock of the kind of AOT-generated model evaluator. It has 2 tensors
31 // of shape {1}, and 'evaluation' adds them.
32 // The interface is the one expected by ReleaseModelRunner.
33 class MockAOTModelBase
{
40 MockAOTModelBase() = default;
41 virtual ~MockAOTModelBase() = default;
43 virtual int LookupArgIndex(const std::string
&Name
) {
44 if (Name
== "prefix_a")
46 if (Name
== "prefix_b")
50 int LookupResultIndex(const std::string
&) { return 0; }
51 virtual void Run() = 0;
52 virtual void *result_data(int RIndex
) {
57 virtual void *arg_data(int Index
) {
69 class AdditionAOTModel final
: public MockAOTModelBase
{
71 AdditionAOTModel() = default;
72 void Run() override
{ R
= A
+ B
; }
75 class DiffAOTModel final
: public MockAOTModelBase
{
77 DiffAOTModel() = default;
78 void Run() override
{ R
= A
- B
; }
81 static const char *M1Selector
= "the model that subtracts";
82 static const char *M2Selector
= "the model that adds";
84 static MD5::MD5Result Hash1
= MD5::hash(arrayRefFromStringRef(M1Selector
));
85 static MD5::MD5Result Hash2
= MD5::hash(arrayRefFromStringRef(M2Selector
));
86 class ComposedAOTModel final
{
89 uint64_t Selector
[2] = {0};
91 bool isHashSameAsSelector(const std::pair
<uint64_t, uint64_t> &Words
) const {
92 return Selector
[0] == Words
.first
&& Selector
[1] == Words
.second
;
94 MockAOTModelBase
*getModel() {
95 if (isHashSameAsSelector(Hash1
.words()))
97 if (isHashSameAsSelector(Hash2
.words()))
99 llvm_unreachable("Should be one of the two");
103 ComposedAOTModel() = default;
104 int LookupArgIndex(const std::string
&Name
) {
105 if (Name
== "prefix_model_selector")
107 return getModel()->LookupArgIndex(Name
);
109 int LookupResultIndex(const std::string
&Name
) {
110 return getModel()->LookupResultIndex(Name
);
112 void *arg_data(int Index
) {
115 return getModel()->arg_data(Index
);
117 void *result_data(int RIndex
) { return getModel()->result_data(RIndex
); }
118 void Run() { getModel()->Run(); }
121 static EmbeddedModelRunnerOptions
makeOptions() {
122 EmbeddedModelRunnerOptions Opts
;
123 Opts
.setFeedPrefix("prefix_");
128 TEST(NoInferenceModelRunner
, AccessTensors
) {
129 const std::vector
<TensorSpec
> Inputs
{
130 TensorSpec::createSpec
<int64_t>("F1", {1}),
131 TensorSpec::createSpec
<int64_t>("F2", {10}),
132 TensorSpec::createSpec
<float>("F2", {5}),
135 NoInferenceModelRunner
NIMR(Ctx
, Inputs
);
136 NIMR
.getTensor
<int64_t>(0)[0] = 1;
137 std::memcpy(NIMR
.getTensor
<int64_t>(1),
138 std::vector
<int64_t>{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}.data(),
139 10 * sizeof(int64_t));
140 std::memcpy(NIMR
.getTensor
<float>(2),
141 std::vector
<float>{0.1f
, 0.2f
, 0.3f
, 0.4f
, 0.5f
}.data(),
143 ASSERT_EQ(NIMR
.getTensor
<int64_t>(0)[0], 1);
144 ASSERT_EQ(NIMR
.getTensor
<int64_t>(1)[8], 9);
145 ASSERT_EQ(NIMR
.getTensor
<float>(2)[1], 0.2f
);
148 TEST(ReleaseModeRunner
, NormalUse
) {
150 std::vector
<TensorSpec
> Inputs
{TensorSpec::createSpec
<int64_t>("a", {1}),
151 TensorSpec::createSpec
<int64_t>("b", {1})};
152 auto Evaluator
= std::make_unique
<ReleaseModeModelRunner
<AdditionAOTModel
>>(
153 Ctx
, Inputs
, "", makeOptions());
154 *Evaluator
->getTensor
<int64_t>(0) = 1;
155 *Evaluator
->getTensor
<int64_t>(1) = 2;
156 EXPECT_EQ(Evaluator
->evaluate
<int64_t>(), 3);
157 EXPECT_EQ(*Evaluator
->getTensor
<int64_t>(0), 1);
158 EXPECT_EQ(*Evaluator
->getTensor
<int64_t>(1), 2);
161 TEST(ReleaseModeRunner
, ExtraFeatures
) {
163 std::vector
<TensorSpec
> Inputs
{TensorSpec::createSpec
<int64_t>("a", {1}),
164 TensorSpec::createSpec
<int64_t>("b", {1}),
165 TensorSpec::createSpec
<int64_t>("c", {1})};
166 auto Evaluator
= std::make_unique
<ReleaseModeModelRunner
<AdditionAOTModel
>>(
167 Ctx
, Inputs
, "", makeOptions());
168 *Evaluator
->getTensor
<int64_t>(0) = 1;
169 *Evaluator
->getTensor
<int64_t>(1) = 2;
170 *Evaluator
->getTensor
<int64_t>(2) = -3;
171 EXPECT_EQ(Evaluator
->evaluate
<int64_t>(), 3);
172 EXPECT_EQ(*Evaluator
->getTensor
<int64_t>(0), 1);
173 EXPECT_EQ(*Evaluator
->getTensor
<int64_t>(1), 2);
174 EXPECT_EQ(*Evaluator
->getTensor
<int64_t>(2), -3);
177 TEST(ReleaseModeRunner
, ExtraFeaturesOutOfOrder
) {
179 std::vector
<TensorSpec
> Inputs
{
180 TensorSpec::createSpec
<int64_t>("a", {1}),
181 TensorSpec::createSpec
<int64_t>("c", {1}),
182 TensorSpec::createSpec
<int64_t>("b", {1}),
184 auto Evaluator
= std::make_unique
<ReleaseModeModelRunner
<AdditionAOTModel
>>(
185 Ctx
, Inputs
, "", makeOptions());
186 *Evaluator
->getTensor
<int64_t>(0) = 1; // a
187 *Evaluator
->getTensor
<int64_t>(1) = 2; // c
188 *Evaluator
->getTensor
<int64_t>(2) = -3; // b
189 EXPECT_EQ(Evaluator
->evaluate
<int64_t>(), -2); // a + b
190 EXPECT_EQ(*Evaluator
->getTensor
<int64_t>(0), 1);
191 EXPECT_EQ(*Evaluator
->getTensor
<int64_t>(1), 2);
192 EXPECT_EQ(*Evaluator
->getTensor
<int64_t>(2), -3);
195 // We expect an error to be reported early if the user tried to specify a model
196 // selector, but the model in fact doesn't support that.
197 TEST(ReleaseModelRunner
, ModelSelectorNoInputFeaturePresent
) {
199 std::vector
<TensorSpec
> Inputs
{TensorSpec::createSpec
<int64_t>("a", {1}),
200 TensorSpec::createSpec
<int64_t>("b", {1})};
201 EXPECT_DEATH((void)std::make_unique
<ReleaseModeModelRunner
<AdditionAOTModel
>>(
202 Ctx
, Inputs
, "", makeOptions().setModelSelector(M2Selector
)),
203 "A model selector was specified but the underlying model does "
204 "not expose a model_selector input");
207 TEST(ReleaseModelRunner
, ModelSelectorNoSelectorGiven
) {
209 std::vector
<TensorSpec
> Inputs
{TensorSpec::createSpec
<int64_t>("a", {1}),
210 TensorSpec::createSpec
<int64_t>("b", {1})};
212 (void)std::make_unique
<ReleaseModeModelRunner
<ComposedAOTModel
>>(
213 Ctx
, Inputs
, "", makeOptions()),
214 "A model selector was not specified but the underlying model requires "
215 "selecting one because it exposes a model_selector input");
218 // Test that we correctly set up the model_selector tensor value. We are only
219 // responsbile for what happens if the user doesn't specify a value (but the
220 // model supports the feature), or if the user specifies one, and we correctly
221 // populate the tensor, and do so upfront (in case the model implementation
222 // needs that for subsequent tensor buffer lookups).
223 TEST(ReleaseModelRunner
, ModelSelector
) {
225 std::vector
<TensorSpec
> Inputs
{TensorSpec::createSpec
<int64_t>("a", {1}),
226 TensorSpec::createSpec
<int64_t>("b", {1})};
227 // This explicitly asks for M1
228 auto Evaluator
= std::make_unique
<ReleaseModeModelRunner
<ComposedAOTModel
>>(
229 Ctx
, Inputs
, "", makeOptions().setModelSelector(M1Selector
));
230 *Evaluator
->getTensor
<int64_t>(0) = 1;
231 *Evaluator
->getTensor
<int64_t>(1) = 2;
232 EXPECT_EQ(Evaluator
->evaluate
<int64_t>(), -1);
235 Evaluator
= std::make_unique
<ReleaseModeModelRunner
<ComposedAOTModel
>>(
236 Ctx
, Inputs
, "", makeOptions().setModelSelector(M2Selector
));
237 *Evaluator
->getTensor
<int64_t>(0) = 1;
238 *Evaluator
->getTensor
<int64_t>(1) = 2;
239 EXPECT_EQ(Evaluator
->evaluate
<int64_t>(), 3);
241 // Asking for a model that's not supported isn't handled by our infra and we
242 // expect the model implementation to fail at a point.
245 #if defined(LLVM_ON_UNIX)
246 TEST(InteractiveModelRunner
, Evaluation
) {
248 // Test the interaction with an external advisor by asking for advice twice.
249 // Use simple values, since we use the Logger underneath, that's tested more
250 // extensively elsewhere.
251 std::vector
<TensorSpec
> Inputs
{
252 TensorSpec::createSpec
<int64_t>("a", {1}),
253 TensorSpec::createSpec
<int64_t>("b", {1}),
254 TensorSpec::createSpec
<int64_t>("c", {1}),
256 TensorSpec AdviceSpec
= TensorSpec::createSpec
<float>("advice", {1});
258 // Create the 2 files. Ideally we'd create them as named pipes, but that's not
259 // quite supported by the generic API.
261 llvm::unittest::TempDir
Tmp("tmpdir", /*Unique=*/true);
262 SmallString
<128> FromCompilerName(Tmp
.path().begin(), Tmp
.path().end());
263 SmallString
<128> ToCompilerName(Tmp
.path().begin(), Tmp
.path().end());
264 sys::path::append(FromCompilerName
, "InteractiveModelRunner_Evaluation.out");
265 sys::path::append(ToCompilerName
, "InteractiveModelRunner_Evaluation.in");
266 EXPECT_EQ(::mkfifo(FromCompilerName
.c_str(), 0666), 0);
267 EXPECT_EQ(::mkfifo(ToCompilerName
.c_str(), 0666), 0);
269 FileRemover
Cleanup1(FromCompilerName
);
270 FileRemover
Cleanup2(ToCompilerName
);
272 // Since the evaluator sends the features over and then blocks waiting for
273 // an answer, we must spawn a thread playing the role of the advisor / host:
274 std::atomic
<int> SeenObservations
= 0;
275 // Start the host first to make sure the pipes are being prepared. Otherwise
276 // the evaluator will hang.
277 std::thread
Advisor([&]() {
278 // Open the writer first. This is because the evaluator will try opening
279 // the "input" pipe first. An alternative that avoids ordering is for the
280 // host to open the pipes RW.
281 raw_fd_ostream
ToCompiler(ToCompilerName
, EC
);
283 int FromCompilerHandle
= 0;
285 sys::fs::openFileForRead(FromCompilerName
, FromCompilerHandle
));
286 sys::fs::file_t FromCompiler
=
287 sys::fs::convertFDToNativeFile(FromCompilerHandle
);
288 EXPECT_EQ(SeenObservations
, 0);
289 // Helper to read headers and other json lines.
290 SmallVector
<char, 1024> Buffer
;
291 auto ReadLn
= [&]() {
295 auto ReadOrErr
= sys::fs::readNativeFile(FromCompiler
, {&Chr
, 1});
296 EXPECT_FALSE(ReadOrErr
.takeError());
300 return StringRef(Buffer
.data(), Buffer
.size());
301 Buffer
.push_back(Chr
);
304 // See include/llvm/Analysis/Utils/TrainingLogger.h
305 // First comes the header
306 auto Header
= json::parse(ReadLn());
307 EXPECT_FALSE(Header
.takeError());
308 EXPECT_NE(Header
->getAsObject()->getArray("features"), nullptr);
309 EXPECT_NE(Header
->getAsObject()->getObject("advice"), nullptr);
310 // Then comes the context
311 EXPECT_FALSE(json::parse(ReadLn()).takeError());
313 int64_t Features
[3] = {0};
314 auto FullyRead
= [&]() {
316 const size_t ToRead
= 3 * Inputs
[0].getTotalTensorBufferSize();
317 char *Buff
= reinterpret_cast<char *>(Features
);
318 while (InsPt
< ToRead
) {
319 auto ReadOrErr
= sys::fs::readNativeFile(
320 FromCompiler
, {Buff
+ InsPt
, ToRead
- InsPt
});
321 EXPECT_FALSE(ReadOrErr
.takeError());
326 EXPECT_FALSE(json::parse(ReadLn()).takeError());
331 auto ReadNL
= [&]() {
333 auto ReadOrErr
= sys::fs::readNativeFile(FromCompiler
, {&Chr
, 1});
334 EXPECT_FALSE(ReadOrErr
.takeError());
340 EXPECT_EQ(Chr
, '\n');
341 EXPECT_EQ(Features
[0], 42);
342 EXPECT_EQ(Features
[1], 43);
343 EXPECT_EQ(Features
[2], 100);
347 float Advice
= 42.0012;
348 ToCompiler
.write(reinterpret_cast<const char *>(&Advice
),
349 AdviceSpec
.getTotalTensorBufferSize());
352 // Second observation, and same idea as above
353 EXPECT_FALSE(json::parse(ReadLn()).takeError());
356 EXPECT_EQ(Chr
, '\n');
357 EXPECT_EQ(Features
[0], 10);
358 EXPECT_EQ(Features
[1], -2);
359 EXPECT_EQ(Features
[2], 1);
362 ToCompiler
.write(reinterpret_cast<const char *>(&Advice
),
363 AdviceSpec
.getTotalTensorBufferSize());
365 sys::fs::closeFile(FromCompiler
);
368 InteractiveModelRunner
Evaluator(Ctx
, Inputs
, AdviceSpec
, FromCompilerName
,
371 Evaluator
.switchContext("hi");
373 EXPECT_EQ(SeenObservations
, 0);
374 *Evaluator
.getTensor
<int64_t>(0) = 42;
375 *Evaluator
.getTensor
<int64_t>(1) = 43;
376 *Evaluator
.getTensor
<int64_t>(2) = 100;
377 float Ret
= Evaluator
.evaluate
<float>();
378 EXPECT_EQ(SeenObservations
, 1);
379 EXPECT_FLOAT_EQ(Ret
, 42.0012);
381 *Evaluator
.getTensor
<int64_t>(0) = 10;
382 *Evaluator
.getTensor
<int64_t>(1) = -2;
383 *Evaluator
.getTensor
<int64_t>(2) = 1;
384 Ret
= Evaluator
.evaluate
<float>();
385 EXPECT_EQ(SeenObservations
, 2);
386 EXPECT_FLOAT_EQ(Ret
, 50.30);