Skip to content

Commit 5b8dc7c

Browse files
committed
[mlgo] Introduce an "InteractiveModelRunner"
This is a model runner for ML researchers using environments like CompilerGym. In such environments, researchers host the compiler and want to be able to observe the problem space (features) at each decision step of some optimization pass, at which point the compiler is stopped, waiting for the host makes a decision and provide an advice back to the compiler, which then continues its normal operation, and so on. The InteractiveModelRunner supports this scenario for the feature set exposed by the compiler at a given time. It uses 2 files - ideally FIFO pipes - one to pass data to the host, the other to get advices back from the host. This means this scenario is supported with no special dependencies. The file creation and deletion is the responsibility of the host. Hooking up this model evaluator to a MLGO-ed pass is the responsibilty of the pass author, and subsequent patches will do so for the current set of mlgo passes, and offer an API to easily "just opt in" by default when mlgo-ing a new pass. The data protocol is that of the training logger: the host sees a training log doled out observation by observation by reading from one of the files, and passes back its advice as a serialized tensor (i.e. tensor value memory dump) via the other file. There are some differences wrt the log seen during training: the interactive model doesn't currently include the outcome (because it should be identical to the decision, and it's also not present in the "release" mode); and partial rewards aren't currently communicated back. The assumption - just like with the training logger - is that the host is co-located, thus avoiding any endianness concerns. In a distributed environment, it is up to the hosting infrastructure to intermediate that. Differential Revision: https://reviews.llvm.org/D142642
1 parent aad5984 commit 5b8dc7c

File tree

9 files changed

+309
-1
lines changed

9 files changed

+309
-1
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//===- InteractiveModelRunner.h ---- "gym" ML model runner -----*- C++ -*-===//
2+
//
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
6+
//
7+
//===----------------------------------------------------------------------===//
8+
//
9+
10+
#ifndef LLVM_ANALYSIS_INTERACTIVEMODELRUNNER_H
11+
#define LLVM_ANALYSIS_INTERACTIVEMODELRUNNER_H
12+
13+
#include "llvm/Analysis/MLModelRunner.h"
14+
#include "llvm/Analysis/TensorSpec.h"
15+
#include "llvm/Analysis/Utils/TrainingLogger.h"
16+
#include "llvm/Config/llvm-config.h"
17+
#include "llvm/Support/raw_ostream.h"
18+
#include <system_error>
19+
20+
namespace llvm {
21+
22+
/// A MLModelRunner that asks for advice from an external agent, or host. It
23+
/// uses 2 files - ideally named pipes - one to send data to that agent, and
24+
/// one to receive advice.
25+
/// The data exchange uses the training logger (Utils/TrainingLogger.h) format.
26+
/// Specifically, the compiler will send the log header, set the context, and
27+
/// send observations; the host is expected to reply with a tensor value after
28+
/// each observation as a binary buffer that's conforming to the shape of the
29+
/// advice. Interleaved, the data closely resembles the training log for a
30+
/// log where we don't capture the reward signal.
31+
///
32+
/// Note that the correctness of the received data is the responsibility of the
33+
/// host. In particular, if insufficient data were sent, the compiler will block
34+
/// when waiting for an advice.
35+
class InteractiveModelRunner : public MLModelRunner {
36+
public:
37+
InteractiveModelRunner(LLVMContext &Ctx,
38+
const std::vector<TensorSpec> &Inputs,
39+
const TensorSpec &Advice, StringRef OutboundName,
40+
StringRef InboundName);
41+
42+
static bool classof(const MLModelRunner *R) {
43+
return R->getKind() == MLModelRunner::Kind::Interactive;
44+
}
45+
void switchContext(StringRef Name) {
46+
Log.switchContext(Name);
47+
Log.flush();
48+
}
49+
50+
private:
51+
void *evaluateUntyped() override;
52+
const std::vector<TensorSpec> InputSpecs;
53+
const TensorSpec OutputSpec;
54+
std::error_code OutEC;
55+
std::error_code InEC;
56+
raw_fd_stream Inbound;
57+
std::vector<char> OutputBuffer;
58+
Logger Log;
59+
};
60+
} // namespace llvm
61+
#endif // LLVM_ANALYSIS_INTERACTIVEMODELRUNNER_H

llvm/include/llvm/Analysis/MLModelRunner.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class MLModelRunner {
4747
return (const_cast<MLModelRunner *>(this))->getTensorUntyped(Index);
4848
}
4949

50-
enum class Kind : int { Unknown, Release, Development, NoOp };
50+
enum class Kind : int { Unknown, Release, Development, NoOp, Interactive };
5151
Kind getKind() const { return Type; }
5252

5353
protected:

llvm/include/llvm/Analysis/TensorSpec.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ class TensorSpec final {
103103
size_t ElementSize = 0;
104104
};
105105

106+
/// For debugging.
107+
std::string tensorValueToString(const char *Buffer, const TensorSpec &Spec);
108+
106109
/// Construct a TensorSpec from a JSON dictionary of the form:
107110
/// { "name": <string>,
108111
/// "port": <int>,

llvm/include/llvm/Analysis/Utils/TrainingLogger.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class Logger final {
116116
void switchContext(StringRef Name);
117117
void startObservation();
118118
void endObservation();
119+
void flush() { OS->flush(); }
119120

120121
const std::string &currentContext() const { return CurrentContext; }
121122

llvm/lib/Analysis/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ add_llvm_component_library(LLVMAnalysis
7676
InstCount.cpp
7777
InstructionPrecedenceTracking.cpp
7878
InstructionSimplify.cpp
79+
InteractiveModelRunner.cpp
7980
Interval.cpp
8081
IntervalPartition.cpp
8182
LazyBranchProbabilityInfo.cpp
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//===- InteractiveModelRunner.cpp - noop ML model runner ----------------===//
2+
//
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
6+
//
7+
//===----------------------------------------------------------------------===//
8+
//
9+
// A runner that communicates with an external agent via 2 file descriptors.
10+
//===----------------------------------------------------------------------===//
11+
#include "llvm/Analysis/InteractiveModelRunner.h"
12+
#include "llvm/Analysis/MLModelRunner.h"
13+
#include "llvm/Analysis/TensorSpec.h"
14+
#include "llvm/Support/CommandLine.h"
15+
#include "llvm/Support/ErrorHandling.h"
16+
#include "llvm/Support/raw_ostream.h"
17+
18+
using namespace llvm;
19+
20+
#define _IMR_CL_VALS(T, N) clEnumValN(TensorType::N, #T, #T),
21+
22+
static cl::opt<TensorType> DebugReply(
23+
"interactive-model-runner-echo-type", cl::init(TensorType::Invalid),
24+
cl::Hidden,
25+
cl::desc("The InteractiveModelRunner will echo back to stderr "
26+
"the data received "
27+
"from the host as the specified type (for debugging purposes)."),
28+
cl::values(SUPPORTED_TENSOR_TYPES(_IMR_CL_VALS)
29+
clEnumValN(TensorType::Invalid, "disable", "Don't echo")));
30+
31+
#undef _IMR_CL_VALS
32+
33+
InteractiveModelRunner::InteractiveModelRunner(
34+
LLVMContext &Ctx, const std::vector<TensorSpec> &Inputs,
35+
const TensorSpec &Advice, StringRef OutboundName, StringRef InboundName)
36+
: MLModelRunner(Ctx, MLModelRunner::Kind::Interactive, Inputs.size()),
37+
InputSpecs(Inputs), OutputSpec(Advice), Inbound(InboundName, InEC),
38+
OutputBuffer(OutputSpec.getTotalTensorBufferSize()),
39+
Log(std::make_unique<raw_fd_ostream>(OutboundName, OutEC), InputSpecs,
40+
Advice, /*IncludeReward=*/false) {
41+
if (InEC) {
42+
Ctx.emitError("Cannot open inbound file: " + InEC.message());
43+
return;
44+
}
45+
if (OutEC) {
46+
Ctx.emitError("Cannot open outbound file: " + OutEC.message());
47+
return;
48+
}
49+
// Just like in the no inference case, this will allocate an appropriately
50+
// sized buffer.
51+
for (size_t I = 0; I < InputSpecs.size(); ++I)
52+
setUpBufferForTensor(I, InputSpecs[I], nullptr);
53+
Log.flush();
54+
}
55+
56+
void *InteractiveModelRunner::evaluateUntyped() {
57+
Log.startObservation();
58+
for (size_t I = 0; I < InputSpecs.size(); ++I)
59+
Log.logTensorValue(I, reinterpret_cast<const char *>(getTensorUntyped(I)));
60+
Log.endObservation();
61+
Log.flush();
62+
63+
size_t InsPoint = 0;
64+
char *Buff = OutputBuffer.data();
65+
const size_t Limit = OutputBuffer.size();
66+
while (InsPoint < Limit) {
67+
auto Read = Inbound.read(Buff + InsPoint, OutputBuffer.size() - InsPoint);
68+
if (Read < 0) {
69+
Ctx.emitError("Failed reading from inbound file");
70+
break;
71+
}
72+
InsPoint += Read;
73+
}
74+
if (DebugReply != TensorType::Invalid)
75+
dbgs() << tensorValueToString(OutputBuffer.data(), OutputSpec);
76+
return OutputBuffer.data();
77+
}

llvm/lib/Analysis/TensorSpec.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
// utils.
1111
//
1212
//===----------------------------------------------------------------------===//
13+
#include "llvm/ADT/STLExtras.h"
1314
#include "llvm/Config/config.h"
1415

16+
#include "llvm/ADT/StringExtras.h"
1517
#include "llvm/ADT/Twine.h"
1618
#include "llvm/Analysis/TensorSpec.h"
1719
#include "llvm/Support/CommandLine.h"
@@ -102,4 +104,21 @@ std::optional<TensorSpec> getTensorSpecFromJSON(LLVMContext &Ctx,
102104
return std::nullopt;
103105
}
104106

107+
std::string tensorValueToString(const char *Buffer, const TensorSpec &Spec) {
108+
switch (Spec.type()) {
109+
#define _IMR_DBG_PRINTER(T, N) \
110+
case TensorType::N: { \
111+
const T *TypedBuff = reinterpret_cast<const T *>(Buffer); \
112+
auto R = llvm::make_range(TypedBuff, TypedBuff + Spec.getElementCount()); \
113+
return llvm::join( \
114+
llvm::map_range(R, [](T V) { return std::to_string(V); }), ","); \
115+
}
116+
SUPPORTED_TENSOR_TYPES(_IMR_DBG_PRINTER)
117+
#undef _IMR_DBG_PRINTER
118+
case TensorType::Total:
119+
case TensorType::Invalid:
120+
llvm_unreachable("invalid tensor type");
121+
}
122+
}
123+
105124
} // namespace llvm

llvm/unittests/Analysis/MLModelRunnerTest.cpp

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,18 @@
77
//===----------------------------------------------------------------------===//
88

99
#include "llvm/Analysis/MLModelRunner.h"
10+
#include "llvm/Analysis/InteractiveModelRunner.h"
1011
#include "llvm/Analysis/NoInferenceModelRunner.h"
1112
#include "llvm/Analysis/ReleaseModeModelRunner.h"
13+
#include "llvm/Support/BinaryByteStream.h"
14+
#include "llvm/Support/FileUtilities.h"
15+
#include "llvm/Support/JSON.h"
16+
#include "llvm/Support/raw_ostream.h"
1217
#include "gtest/gtest.h"
1318

19+
#include <atomic>
20+
#include <thread>
21+
1422
using namespace llvm;
1523

1624
namespace llvm {
@@ -116,4 +124,135 @@ TEST(ReleaseModeRunner, ExtraFeaturesOutOfOrder) {
116124
EXPECT_EQ(*Evaluator->getTensor<int64_t>(0), 1);
117125
EXPECT_EQ(*Evaluator->getTensor<int64_t>(1), 2);
118126
EXPECT_EQ(*Evaluator->getTensor<int64_t>(2), -3);
127+
}
128+
129+
TEST(InteractiveModelRunner, Evaluation) {
130+
LLVMContext Ctx;
131+
// Test the interaction with an external advisor by asking for advice twice.
132+
// Use simple values, since we use the Logger underneath, that's tested more
133+
// extensively elsewhere.
134+
std::vector<TensorSpec> Inputs{
135+
TensorSpec::createSpec<int64_t>("a", {1}),
136+
TensorSpec::createSpec<int64_t>("b", {1}),
137+
TensorSpec::createSpec<int64_t>("c", {1}),
138+
};
139+
TensorSpec AdviceSpec = TensorSpec::createSpec<float>("advice", {1});
140+
141+
// Create the 2 files. Ideally we'd create them as named pipes, but that's not
142+
// quite supported by the generic API.
143+
std::error_code EC;
144+
SmallString<64> FromCompilerName;
145+
SmallString<64> ToCompilerName;
146+
int FromCompilerFD = 0;
147+
int ToCompilerFD = 0;
148+
ASSERT_EQ(sys::fs::createTemporaryFile("InteractiveModelRunner_Evaluation",
149+
"temp", FromCompilerFD,
150+
FromCompilerName),
151+
std::error_code());
152+
153+
ASSERT_EQ(sys::fs::createTemporaryFile("InteractiveModelRunner_Evaluation",
154+
"temp", ToCompilerFD, ToCompilerName),
155+
std::error_code());
156+
157+
raw_fd_stream FromCompiler(FromCompilerName, EC);
158+
EXPECT_FALSE(EC);
159+
raw_fd_ostream ToCompiler(ToCompilerName, EC);
160+
EXPECT_FALSE(EC);
161+
FileRemover Cleanup1(FromCompilerName);
162+
FileRemover Cleanup2(ToCompilerName);
163+
InteractiveModelRunner Evaluator(Ctx, Inputs, AdviceSpec, FromCompilerName,
164+
ToCompilerName);
165+
166+
Evaluator.switchContext("hi");
167+
168+
// Helper to read headers and other json lines.
169+
SmallVector<char, 1024> Buffer;
170+
auto ReadLn = [&]() {
171+
Buffer.clear();
172+
while (true) {
173+
char Chr = 0;
174+
auto Read = FromCompiler.read(&Chr, 1);
175+
EXPECT_GE(Read, 0);
176+
if (!Read)
177+
continue;
178+
if (Chr == '\n')
179+
return StringRef(Buffer.data(), Buffer.size());
180+
Buffer.push_back(Chr);
181+
}
182+
};
183+
// See include/llvm/Analysis/Utils/TrainingLogger.h
184+
// First comes the header
185+
auto Header = json::parse(ReadLn());
186+
EXPECT_FALSE(Header.takeError());
187+
EXPECT_NE(Header->getAsObject()->getArray("features"), nullptr);
188+
// Then comes the context
189+
EXPECT_FALSE(json::parse(ReadLn()).takeError());
190+
191+
// Since the evaluator sends the features over and then blocks waiting for
192+
// an answer, we must spawn a thread playing the role of the advisor / host:
193+
std::atomic<int> SeenObservations = 0;
194+
std::thread Advisor([&]() {
195+
EXPECT_EQ(SeenObservations, 0);
196+
int64_t Features[3] = {0};
197+
auto FullyRead = [&]() {
198+
size_t InsPt = 0;
199+
const size_t ToRead = 3 * Inputs[0].getTotalTensorBufferSize();
200+
char *Buff = reinterpret_cast<char *>(Features);
201+
while (InsPt < ToRead) {
202+
auto Read = FromCompiler.read(Buff + InsPt, ToRead - InsPt);
203+
EXPECT_GE(Read, 0);
204+
InsPt += Read;
205+
}
206+
};
207+
// Observation
208+
EXPECT_FALSE(json::parse(ReadLn()).takeError());
209+
// Tensor values
210+
FullyRead();
211+
// a "\n"
212+
char Chr = 0;
213+
while (FromCompiler.read(&Chr, 1) == 0) {
214+
}
215+
EXPECT_EQ(Chr, '\n');
216+
EXPECT_EQ(Features[0], 42);
217+
EXPECT_EQ(Features[1], 43);
218+
EXPECT_EQ(Features[2], 100);
219+
++SeenObservations;
220+
221+
// Send the advice
222+
float Advice = 42.0012;
223+
ToCompiler.write(reinterpret_cast<const char *>(&Advice),
224+
AdviceSpec.getTotalTensorBufferSize());
225+
ToCompiler.flush();
226+
227+
// Second observation, and same idea as above
228+
EXPECT_FALSE(json::parse(ReadLn()).takeError());
229+
FullyRead();
230+
while (FromCompiler.read(&Chr, 1) == 0) {
231+
}
232+
EXPECT_EQ(Chr, '\n');
233+
EXPECT_EQ(Features[0], 10);
234+
EXPECT_EQ(Features[1], -2);
235+
EXPECT_EQ(Features[2], 1);
236+
++SeenObservations;
237+
Advice = 50.30;
238+
ToCompiler.write(reinterpret_cast<const char *>(&Advice),
239+
AdviceSpec.getTotalTensorBufferSize());
240+
ToCompiler.flush();
241+
});
242+
243+
EXPECT_EQ(SeenObservations, 0);
244+
*Evaluator.getTensor<int64_t>(0) = 42;
245+
*Evaluator.getTensor<int64_t>(1) = 43;
246+
*Evaluator.getTensor<int64_t>(2) = 100;
247+
float Ret = Evaluator.evaluate<float>();
248+
EXPECT_EQ(SeenObservations, 1);
249+
EXPECT_FLOAT_EQ(Ret, 42.0012);
250+
251+
*Evaluator.getTensor<int64_t>(0) = 10;
252+
*Evaluator.getTensor<int64_t>(1) = -2;
253+
*Evaluator.getTensor<int64_t>(2) = 1;
254+
Ret = Evaluator.evaluate<float>();
255+
EXPECT_EQ(SeenObservations, 2);
256+
EXPECT_FLOAT_EQ(Ret, 50.30);
257+
Advisor.join();
119258
}

llvm/unittests/Analysis/TensorSpecTest.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,10 @@ TEST(TensorSpecTest, TensorSpecSizesAndTypes) {
5959
EXPECT_EQ(Spec3DLarge.getElementByteSize(), sizeof(float));
6060
EXPECT_EQ(Spec1D.getElementByteSize(), sizeof(int16_t));
6161
}
62+
63+
TEST(TensorSpecTest, PrintValueForDebug) {
64+
std::vector<int32_t> Values{1, 3};
65+
EXPECT_EQ(tensorValueToString(reinterpret_cast<const char *>(Values.data()),
66+
TensorSpec::createSpec<int32_t>("name", {2})),
67+
"1,3");
68+
}

0 commit comments

Comments
 (0)