CIRCT  19.0.0git
Cosim.cpp
Go to the documentation of this file.
1 //===- Cosim.cpp - Connection to ESI simulation via GRPC ------------------===//
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 // DO NOT EDIT!
10 // This file is distributed as part of an ESI package. The source for this file
11 // should always be modified within CIRCT
12 // (lib/dialect/ESI/runtime/cpp/lib/backends/Cosim.cpp).
13 //
14 //===----------------------------------------------------------------------===//
15 
16 #include "esi/backends/Cosim.h"
17 #include "esi/Services.h"
18 #include "esi/Utils.h"
19 
20 #include "cosim.grpc.pb.h"
21 
22 #include <grpc/grpc.h>
23 #include <grpcpp/channel.h>
24 #include <grpcpp/client_context.h>
25 #include <grpcpp/create_channel.h>
26 #include <grpcpp/security/credentials.h>
27 
28 #include <fstream>
29 #include <iostream>
30 #include <set>
31 
32 using namespace esi;
33 using namespace esi::cosim;
34 using namespace esi::services;
35 using namespace esi::backends::cosim;
36 
37 using grpc::Channel;
38 using grpc::ClientContext;
39 using grpc::ClientReader;
40 using grpc::ClientReaderWriter;
41 using grpc::ClientWriter;
42 using grpc::Status;
43 
44 static void checkStatus(Status s, const std::string &msg) {
45  if (!s.ok())
46  throw std::runtime_error(msg + ". Code " + to_string(s.error_code()) +
47  ": " + s.error_message() + " (" +
48  s.error_details() + ")");
49 }
50 
51 /// Hack around C++ not having a way to forward declare a nested class.
53  StubContainer(std::unique_ptr<ChannelServer::Stub> stub)
54  : stub(std::move(stub)) {}
55  std::unique_ptr<ChannelServer::Stub> stub;
56 
57  /// Get the type ID for a channel name.
58  bool getChannelDesc(const std::string &channelName,
59  esi::cosim::ChannelDesc &desc);
60 };
62 
63 /// Parse the connection std::string and instantiate the accelerator. Support
64 /// the traditional 'host:port' syntax and a path to 'cosim.cfg' which is output
65 /// by the cosimulation when it starts (which is useful when it chooses its own
66 /// port).
67 std::unique_ptr<AcceleratorConnection>
68 CosimAccelerator::connect(Context &ctxt, std::string connectionString) {
69  std::string portStr;
70  std::string host = "localhost";
71 
72  size_t colon;
73  if ((colon = connectionString.find(':')) != std::string::npos) {
74  portStr = connectionString.substr(colon + 1);
75  host = connectionString.substr(0, colon);
76  } else if (connectionString.ends_with("cosim.cfg")) {
77  std::ifstream cfg(connectionString);
78  std::string line, key, value;
79 
80  while (getline(cfg, line))
81  if ((colon = line.find(":")) != std::string::npos) {
82  key = line.substr(0, colon);
83  value = line.substr(colon + 1);
84  if (key == "port")
85  portStr = value;
86  else if (key == "host")
87  host = value;
88  }
89 
90  if (portStr.size() == 0)
91  throw std::runtime_error("port line not found in file");
92  } else if (connectionString == "env") {
93  char *hostEnv = getenv("ESI_COSIM_HOST");
94  if (hostEnv)
95  host = hostEnv;
96  else
97  host = "localhost";
98  char *portEnv = getenv("ESI_COSIM_PORT");
99  if (portEnv)
100  portStr = portEnv;
101  else
102  throw std::runtime_error("ESI_COSIM_PORT environment variable not set");
103  } else {
104  throw std::runtime_error("Invalid connection std::string '" +
105  connectionString + "'");
106  }
107  uint16_t port = stoul(portStr);
108  auto conn = make_unique<CosimAccelerator>(ctxt, host, port);
109 
110  // Using the MMIO manifest method is really only for internal debugging, so it
111  // doesn't need to be part of the connection string.
112  char *manifestMethod = getenv("ESI_COSIM_MANIFEST_MMIO");
113  if (manifestMethod != nullptr)
114  conn->setManifestMethod(ManifestMethod::MMIO);
115 
116  return conn;
117 }
118 
119 /// Construct and connect to a cosim server.
120 CosimAccelerator::CosimAccelerator(Context &ctxt, std::string hostname,
121  uint16_t port)
123  // Connect to the simulation.
124  auto channel = grpc::CreateChannel(hostname + ":" + std::to_string(port),
125  grpc::InsecureChannelCredentials());
126  rpcClient = new StubContainer(ChannelServer::NewStub(channel));
127 }
129  if (rpcClient)
130  delete rpcClient;
131  channels.clear();
132 }
133 
134 namespace {
135 class CosimSysInfo : public SysInfo {
136 public:
137  CosimSysInfo(ChannelServer::Stub *rpcClient) : rpcClient(rpcClient) {}
138 
139  uint32_t getEsiVersion() const override {
140  ::esi::cosim::Manifest response = getManifest();
141  return response.esi_version();
142  }
143 
144  std::vector<uint8_t> getCompressedManifest() const override {
145  ::esi::cosim::Manifest response = getManifest();
146  std::string compressedManifestStr = response.compressed_manifest();
147  return std::vector<uint8_t>(compressedManifestStr.begin(),
148  compressedManifestStr.end());
149  }
150 
151 private:
152  ::esi::cosim::Manifest getManifest() const {
153  ::esi::cosim::Manifest response;
154  // To get around the a race condition where the manifest may not be set yet,
155  // loop until it is. TODO: fix this with the DPI API change.
156  do {
157  ClientContext context;
158  VoidMessage arg;
159  Status s = rpcClient->GetManifest(&context, arg, &response);
160  checkStatus(s, "Failed to get manifest");
161  std::this_thread::sleep_for(std::chrono::milliseconds(10));
162  } while (response.esi_version() < 0);
163  return response;
164  }
165 
166  esi::cosim::ChannelServer::Stub *rpcClient;
167 };
168 } // namespace
169 
170 namespace {
171 /// Cosim client implementation of a write channel port.
172 class WriteCosimChannelPort : public WriteChannelPort {
173 public:
174  WriteCosimChannelPort(ChannelServer::Stub *rpcClient, const ChannelDesc &desc,
175  const Type *type, std::string name)
176  : WriteChannelPort(type), rpcClient(rpcClient), desc(desc), name(name) {}
177  ~WriteCosimChannelPort() = default;
178 
179  void connectImpl(std::optional<unsigned> bufferSize) override {
180  if (desc.type() != getType()->getID())
181  throw std::runtime_error("Channel '" + name +
182  "' has wrong type. Expected " +
183  getType()->getID() + ", got " + desc.type());
184  if (desc.dir() != ChannelDesc::Direction::ChannelDesc_Direction_TO_SERVER)
185  throw std::runtime_error("Channel '" + name +
186  "' is not a to server channel");
187  assert(desc.name() == name);
188  }
189 
190  /// Send a write message to the server.
191  void write(const MessageData &data) override {
192  ClientContext context;
193  AddressedMessage msg;
194  msg.set_channel_name(name);
195  msg.mutable_message()->set_data(data.getBytes(), data.getSize());
196  VoidMessage response;
197  grpc::Status sendStatus = rpcClient->SendToServer(&context, msg, &response);
198  if (!sendStatus.ok())
199  throw std::runtime_error("Failed to write to channel '" + name +
200  "': " + std::to_string(sendStatus.error_code()) +
201  " " + sendStatus.error_message() +
202  ". Details: " + sendStatus.error_details());
203  }
204 
205 protected:
206  ChannelServer::Stub *rpcClient;
207  /// The channel description as provided by the server.
208  ChannelDesc desc;
209  /// The name of the channel from the manifest.
210  std::string name;
211 };
212 } // namespace
213 
214 namespace {
215 /// Cosim client implementation of a read channel port. Since gRPC read protocol
216 /// streams messages back, this implementation is quite complex.
217 class ReadCosimChannelPort
218  : public ReadChannelPort,
219  public grpc::ClientReadReactor<esi::cosim::Message> {
220 public:
221  ReadCosimChannelPort(ChannelServer::Stub *rpcClient, const ChannelDesc &desc,
222  const Type *type, std::string name)
223  : ReadChannelPort(type), rpcClient(rpcClient), desc(desc), name(name),
224  context(nullptr) {}
225  virtual ~ReadCosimChannelPort() { disconnect(); }
226 
227  void connectImpl(std::optional<unsigned> bufferSize) override {
228  // Sanity checking.
229  if (desc.type() != getType()->getID())
230  throw std::runtime_error("Channel '" + name +
231  "' has wrong type. Expected " +
232  getType()->getID() + ", got " + desc.type());
233  if (desc.dir() != ChannelDesc::Direction::ChannelDesc_Direction_TO_CLIENT)
234  throw std::runtime_error("Channel '" + name +
235  "' is not a to server channel");
236  assert(desc.name() == name);
237 
238  // Initiate a stream of messages from the server.
239  context = std::make_unique<ClientContext>();
240  rpcClient->async()->ConnectToClientChannel(context.get(), &desc, this);
241  StartCall();
242  StartRead(&incomingMessage);
243  }
244 
245  /// Gets called when there's a new message from the server. It'll be stored in
246  /// `incomingMessage`.
247  void OnReadDone(bool ok) override {
248  if (!ok)
249  // This happens when we are disconnecting since we are canceling the call.
250  return;
251 
252  // Read the delivered message and push it onto the queue.
253  const std::string &messageString = incomingMessage.data();
254  MessageData data(reinterpret_cast<const uint8_t *>(messageString.data()),
255  messageString.size());
256  while (!callback(data))
257  // Blocking here could cause deadlocks in specific situations.
258  // TODO: Implement a way to handle this better.
259  std::this_thread::sleep_for(std::chrono::milliseconds(10));
260 
261  // Initiate the next read.
262  StartRead(&incomingMessage);
263  }
264 
265  /// Disconnect this channel from the server.
266  void disconnect() override {
267  if (!context)
268  return;
269  context->TryCancel();
270  context.reset();
272  }
273 
274 protected:
275  ChannelServer::Stub *rpcClient;
276  /// The channel description as provided by the server.
277  ChannelDesc desc;
278  /// The name of the channel from the manifest.
279  std::string name;
280 
281  std::unique_ptr<ClientContext> context;
282  /// Storage location for the incoming message.
283  esi::cosim::Message incomingMessage;
284 };
285 
286 } // namespace
287 
288 std::map<std::string, ChannelPort &>
290  const BundleType *bundleType) {
291  std::map<std::string, ChannelPort &> channelResults;
292 
293  // Find the client details for the port at 'fullPath'.
294  auto f = clientChannelAssignments.find(idPath);
295  if (f == clientChannelAssignments.end())
296  return channelResults;
297  const std::map<std::string, std::string> &channelAssignments = f->second;
298 
299  // Each channel in a bundle has a separate cosim endpoint. Find them all.
300  for (auto [name, dir, type] : bundleType->getChannels()) {
301  auto f = channelAssignments.find(name);
302  if (f == channelAssignments.end())
303  throw std::runtime_error("Could not find channel assignment for '" +
304  idPath.toStr() + "." + name + "'");
305  std::string channelName = f->second;
306 
307  // Get the endpoint, which may or may not exist. Construct the port.
308  // Everything is validated when the client calls 'connect()' on the port.
309  ChannelDesc chDesc;
310  if (!rpcClient->getChannelDesc(channelName, chDesc))
311  throw std::runtime_error("Could not find channel '" + channelName +
312  "' in cosimulation");
313 
314  ChannelPort *port;
315  if (BundlePort::isWrite(dir)) {
316  port = new WriteCosimChannelPort(rpcClient->stub.get(), chDesc, type,
317  channelName);
318  } else {
319  port = new ReadCosimChannelPort(rpcClient->stub.get(), chDesc, type,
320  channelName);
321  }
322  channels.emplace(port);
323  channelResults.emplace(name, *port);
324  }
325  return channelResults;
326 }
327 
328 /// Get the channel description for a channel name. Iterate through the list
329 /// each time. Since this will only be called a small number of times on a small
330 /// list, it's not worth doing anything fancy.
331 bool StubContainer::getChannelDesc(const std::string &channelName,
332  ChannelDesc &desc) {
333  ClientContext context;
334  VoidMessage arg;
335  ListOfChannels response;
336  Status s = stub->ListChannels(&context, arg, &response);
337  checkStatus(s, "Failed to list channels");
338  for (const auto &channel : response.channels())
339  if (channel.name() == channelName) {
340  desc = channel;
341  return true;
342  }
343  return false;
344 }
345 
346 namespace {
347 class CosimMMIO : public MMIO {
348 public:
349  CosimMMIO(Context &ctxt, StubContainer *rpcClient) {
350  // We have to locate the channels ourselves since this service might be used
351  // to retrieve the manifest.
352  ChannelDesc readArg, readResp;
353  if (!rpcClient->getChannelDesc("__cosim_mmio_read.arg", readArg) ||
354  !rpcClient->getChannelDesc("__cosim_mmio_read.result", readResp))
355  throw std::runtime_error("Could not find MMIO channels");
356 
357  const esi::Type *i32Type = getType(ctxt, new UIntType(readArg.type(), 32));
358  const esi::Type *i64Type = getType(ctxt, new UIntType(readResp.type(), 64));
359 
360  // Get ports, create the function, then connect to it.
361  readArgPort = std::make_unique<WriteCosimChannelPort>(
362  rpcClient->stub.get(), readArg, i32Type, "__cosim_mmio_read.arg");
363  readRespPort = std::make_unique<ReadCosimChannelPort>(
364  rpcClient->stub.get(), readResp, i64Type, "__cosim_mmio_read.result");
365  readMMIO.reset(FuncService::Function::get(AppID("__cosim_mmio_read"),
366  *readArgPort, *readRespPort));
367  readMMIO->connect();
368  }
369 
370  // Call the read function and wait for a response.
371  uint64_t read(uint32_t addr) const override {
372  auto arg = MessageData::from(addr);
373  std::future<MessageData> result = readMMIO->call(arg);
374  result.wait();
375  return *result.get().as<uint64_t>();
376  }
377 
378  void write(uint32_t addr, uint64_t data) override {
379  // TODO: this.
380  throw std::runtime_error("Cosim MMIO write not implemented");
381  }
382 
383 private:
384  const esi::Type *getType(Context &ctxt, esi::Type *type) {
385  if (auto t = ctxt.getType(type->getID())) {
386  delete type;
387  return *t;
388  }
389  ctxt.registerType(type);
390  return type;
391  }
392  std::unique_ptr<WriteCosimChannelPort> readArgPort;
393  std::unique_ptr<ReadCosimChannelPort> readRespPort;
394  std::unique_ptr<FuncService::Function> readMMIO;
395 };
396 
397 class CosimHostMem : public HostMem {
398 public:
399  CosimHostMem() {}
400 
401  struct CosimHostMemRegion : public HostMemRegion {
402  CosimHostMemRegion(std::size_t size) {
403  ptr = malloc(size);
404  this->size = size;
405  }
406  virtual ~CosimHostMemRegion() { free(ptr); }
407  virtual void *getPtr() const { return ptr; }
408  virtual std::size_t getSize() const { return size; }
409 
410  private:
411  void *ptr;
412  std::size_t size;
413  };
414 
415  virtual std::unique_ptr<HostMemRegion> allocate(std::size_t size,
416  HostMem::Options opts) const {
417  return std::unique_ptr<HostMemRegion>(new CosimHostMemRegion(size));
418  }
419  virtual bool mapMemory(void *ptr, std::size_t size,
420  HostMem::Options opts) const {
421  return true;
422  }
423  virtual void unmapMemory(void *ptr) const {}
424 };
425 
426 } // namespace
427 
429  AppIDPath idPath, std::string implName,
430  const ServiceImplDetails &details,
431  const HWClientDetails &clients) {
432  // Compute our parents idPath path.
433  AppIDPath prefix = std::move(idPath);
434  if (prefix.size() > 0)
435  prefix.pop_back();
436 
437  if (implName == "cosim") {
438  // Get the channel assignments for each client.
439  for (auto client : clients) {
440  AppIDPath fullClientPath = prefix + client.relPath;
441  std::map<std::string, std::string> channelAssignments;
442  for (auto assignment : std::any_cast<std::map<std::string, std::any>>(
443  client.implOptions.at("channel_assignments")))
444  channelAssignments[assignment.first] =
445  std::any_cast<std::string>(assignment.second);
446  clientChannelAssignments[fullClientPath] = std::move(channelAssignments);
447  }
448  }
449 
450  if (svcType == typeid(services::MMIO)) {
451  return new CosimMMIO(getCtxt(), rpcClient);
452  } else if (svcType == typeid(services::HostMem)) {
453  return new CosimHostMem();
454  } else if (svcType == typeid(SysInfo)) {
455  switch (manifestMethod) {
456  case ManifestMethod::Cosim:
457  return new CosimSysInfo(rpcClient->stub.get());
458  case ManifestMethod::MMIO:
459  return new MMIOSysInfo(getService<services::MMIO>());
460  }
461  } else if (svcType == typeid(CustomService) && implName == "cosim") {
462  return new CustomService(idPath, details, clients);
463  }
464  return nullptr;
465 }
466 
468  manifestMethod = method;
469 }
470 
assert(baseType &&"element must be base type")
esi::backends::cosim::CosimAccelerator::StubContainer StubContainer
Definition: Cosim.cpp:61
static void checkStatus(Status s, const std::string &msg)
Definition: Cosim.cpp:44
REGISTER_ACCELERATOR("cosim", backends::cosim::CosimAccelerator)
Abstract class representing a connection to an accelerator.
Definition: Accelerator.h:78
Context & getCtxt() const
Definition: Accelerator.h:82
std::string toStr() const
Definition: Manifest.cpp:604
static bool isWrite(BundleType::Direction bundleDir)
Compute the direction of a channel given the bundle direction and the bundle port's direction.
Definition: Ports.h:151
Bundles represent a collection of channels.
Definition: Types.h:44
const ChannelVector & getChannels() const
Definition: Types.h:54
Unidirectional channels are the basic communication primitive between the host and accelerator.
Definition: Ports.h:33
AcceleratorConnections, Accelerators, and Manifests must all share a context.
Definition: Context.h:30
A logical chunk of data representing serialized data.
Definition: Common.h:86
static MessageData from(T &t)
Cast from a type to its raw bytes.
Definition: Common.h:112
A ChannelPort which reads data from the accelerator.
Definition: Ports.h:69
virtual void disconnect() override
Definition: Ports.h:74
Root class of the ESI type system.
Definition: Types.h:27
ID getID() const
Definition: Types.h:33
Unsigned integer.
Definition: Types.h:124
A ChannelPort which sends data to the accelerator.
Definition: Ports.h:57
Connect to an ESI simulation.
Definition: Cosim.h:37
std::map< AppIDPath, std::map< std::string, std::string > > clientChannelAssignments
Definition: Cosim.h:78
void setManifestMethod(ManifestMethod method)
Definition: Cosim.cpp:467
virtual std::map< std::string, ChannelPort & > requestChannelsFor(AppIDPath, const BundleType *) override
Request the host side channel ports for a particular instance (identified by the AppID path).
Definition: Cosim.cpp:289
virtual Service * createService(Service::Type service, AppIDPath path, std::string implName, const ServiceImplDetails &details, const HWClientDetails &clients) override
Called by getServiceImpl exclusively.
Definition: Cosim.cpp:428
std::set< std::unique_ptr< ChannelPort > > channels
Definition: Cosim.h:75
A service for which there are no standard services registered.
Definition: Services.h:61
Implement the SysInfo API for a standard MMIO protocol.
Definition: Services.h:102
Parent class of all APIs modeled as 'services'.
Definition: Services.h:42
const std::type_info & Type
Definition: Services.h:44
Information about the Accelerator system.
Definition: Services.h:77
def connect(destination, source)
Definition: support.py:37
Direction get(bool isOutput)
Returns an output direction if isOutput is true, otherwise returns an input direction.
Definition: CalyxOps.cpp:54
Definition: esi.py:1
std::map< std::string, std::any > ServiceImplDetails
Definition: Common.h:81
std::vector< HWClientDetail > HWClientDetails
Definition: Common.h:80
Hack around C++ not having a way to forward declare a nested class.
Definition: Cosim.cpp:52
std::unique_ptr< ChannelServer::Stub > stub
Definition: Cosim.cpp:55
bool getChannelDesc(const std::string &channelName, esi::cosim::ChannelDesc &desc)
Get the type ID for a channel name.
Definition: Cosim.cpp:331
StubContainer(std::unique_ptr< ChannelServer::Stub > stub)
Definition: Cosim.cpp:53
Options for allocating host memory.
Definition: Services.h:131