CIRCT 23.0.0git
Loading...
Searching...
No Matches
Accelerator.cpp
Go to the documentation of this file.
1//===- Accelerator.cpp - ESI accelerator system API -----------------------===//
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 (lib/dialect/ESI/runtime/cpp/).
12//
13//===----------------------------------------------------------------------===//
14
15#include "esi/Accelerator.h"
16
17#include <cassert>
18#include <cstdlib>
19#include <filesystem>
20#include <map>
21#include <sstream>
22#include <stdexcept>
23#include <vector>
24
25#include <iostream>
26
27#ifdef __linux__
28#include <dlfcn.h>
29#include <linux/limits.h>
30#include <unistd.h>
31#elif _WIN32
32#include <windows.h>
33#endif
34
35using namespace esi;
36using namespace esi::services;
37
38namespace esi {
40 : ctxt(ctxt), serviceThread(nullptr) {}
42
43// Request a design reset by writing the reset magic number to a particular
44// MMIO offset.
46 services::MMIO *mmio = getService<services::MMIO>();
47 if (!mmio)
48 return false;
49 // The MMIO write is a virtual backend interface which may throw.
50 try {
52 } catch (const std::exception &e) {
53 getLogger().error("reset",
54 std::string("failed to request reset: ") + e.what());
55 return false;
56 }
57 return true;
58}
59
61 if (!serviceThread)
62 serviceThread = std::make_unique<AcceleratorServiceThread>();
63 return serviceThread.get();
64}
65void AcceleratorConnection::createEngine(const std::string &engineTypeName,
66 AppIDPath idPath,
67 const ServiceImplDetails &details,
68 const HWClientDetails &clients) {
69 std::unique_ptr<Engine> engine = ::esi::registry::createEngine(
70 *this, engineTypeName, idPath, details, clients);
71 registerEngine(idPath, std::move(engine), clients);
72}
73
75 std::unique_ptr<Engine> engine,
76 const HWClientDetails &clients) {
77 assert(engine);
78 auto [engineIter, _] = ownedEngines.emplace(idPath, std::move(engine));
79
80 // Engine is now owned by the accelerator connection, so the std::unique_ptr
81 // is no longer valid. Resolve a new one from the map iter.
82 Engine *enginePtr = engineIter->second.get();
83 // Compute our parents idPath path.
84 AppIDPath prefix = std::move(idPath);
85 if (prefix.size() > 0)
86 prefix.pop_back();
87
88 for (const auto &client : clients) {
89 AppIDPath fullClientPath = prefix + client.relPath;
90 for (const auto &channel : client.channelAssignments)
91 clientEngines[fullClientPath].setEngine(channel.first, enginePtr);
92 }
93}
94
96 AppIDPath id,
97 std::string implName,
98 ServiceImplDetails details,
99 HWClientDetails clients) {
100 std::unique_ptr<Service> &cacheEntry =
101 serviceCache[make_tuple(std::string(svcType.name()), id)];
102 if (cacheEntry == nullptr) {
103 Service *svc = createService(svcType, id, implName, details, clients);
104 if (!svc)
105 svc = ServiceRegistry::createService(this, svcType, id, implName, details,
106 clients);
107 if (!svc)
108 return nullptr;
109 cacheEntry = std::unique_ptr<Service>(svc);
110 }
111 return cacheEntry.get();
112}
113
115AcceleratorConnection::takeOwnership(std::unique_ptr<Accelerator> acc) {
117 throw std::runtime_error(
118 "AcceleratorConnection already owns an accelerator");
119 ownedAccelerator = std::move(acc);
120 return ownedAccelerator.get();
121}
122
124 // Destroy engines (and the ports they own) before the accelerator they may
125 // reference during teardown -- order matters.
126 clientEngines.clear();
127 ownedEngines.clear();
128 serviceCache.clear();
129 ownedAccelerator.reset();
130}
131
132/// Get the path to the currently running executable.
133static std::filesystem::path getExePath() {
134#ifdef __linux__
135 char result[PATH_MAX];
136 ssize_t count = readlink("/proc/self/exe", result, PATH_MAX);
137 if (count == -1)
138 throw std::runtime_error("Could not get executable path");
139 return std::filesystem::path(std::string(result, count));
140#elif _WIN32
141 char buffer[MAX_PATH];
142 DWORD length = GetModuleFileNameA(NULL, buffer, MAX_PATH);
143 if (length == 0)
144 throw std::runtime_error("Could not get executable path");
145 return std::filesystem::path(std::string(buffer, length));
146#else
147#eror "Unsupported platform"
148#endif
149}
150
151/// Get the path to the currently running shared library.
152static std::filesystem::path getLibPath() {
153#ifdef __linux__
154 Dl_info dl_info;
155 dladdr((void *)getLibPath, &dl_info);
156 return std::filesystem::path(std::string(dl_info.dli_fname));
157#elif _WIN32
158 HMODULE hModule = NULL;
159 if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
160 GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
161 reinterpret_cast<LPCSTR>(&getLibPath), &hModule)) {
162 // Handle error
163 return std::filesystem::path();
164 }
165
166 char buffer[MAX_PATH];
167 DWORD length = GetModuleFileNameA(hModule, buffer, MAX_PATH);
168 if (length == 0)
169 throw std::runtime_error("Could not get library path");
170
171 return std::filesystem::path(std::string(buffer, length));
172#else
173#eror "Unsupported platform"
174#endif
175}
176
177/// Get the list of directories to search for backend plugins.
178static std::vector<std::filesystem::path> getESIBackendDirectories() {
179 std::vector<std::filesystem::path> directories;
180
181 // First, check current directory.
182 directories.push_back(std::filesystem::current_path());
183
184 // Next, parse the ESI_BACKENDS environment variable and add those.
185 const char *esiBackends = std::getenv("ESI_BACKENDS");
186 if (esiBackends) {
187 // Use platform-specific path separator
188#ifdef _WIN32
189 const char separator = ';';
190#else
191 const char separator = ':';
192#endif
193
194 std::string pathsStr(esiBackends);
195 std::stringstream ss(pathsStr);
196 std::string path;
197
198 while (std::getline(ss, path, separator))
199 if (!path.empty())
200 directories.emplace_back(path);
201 }
202
203 // Next, try the directory of the executable.
204 directories.push_back(getExePath().parent_path());
205 // Finally, try the directory of the library.
206 directories.push_back(getLibPath().parent_path());
207
208 return directories;
209}
210
211/// Load a backend plugin dynamically. Plugins are expected to be named
212/// lib<BackendName>Backend.so and located in one of 1) CWD, 2) directories
213/// specified in ESI_BACKENDS environment variable, 3) in the same directory as
214/// the application, or 4) in the same directory as this library.
215static void loadBackend(Context &ctxt, std::string backend) {
216 Logger &logger = ctxt.getLogger();
217 backend[0] = toupper(backend[0]);
218
219 // Get the file name we are looking for.
220#ifdef __linux__
221 std::string backendFileName = "lib" + backend + "Backend.so";
222#elif _WIN32
223 // In MSVC debug builds, load the debug variant of the plugin DLL (e.g.
224 // CosimBackend_d.dll) to ensure compatibility.
225#if defined(_MSC_VER) && defined(_DEBUG)
226 std::string backendFileName = backend + "Backend_d.dll";
227#else
228 std::string backendFileName = backend + "Backend.dll";
229#endif
230#else
231#error "Unsupported platform"
232#endif
233
234 // First, try the current directory.
235 std::filesystem::path backendPath;
236 // Next, try directories specified in ESI_BACKENDS environment variable.
237 std::vector<std::filesystem::path> esiBackendDirs =
239 bool found = false;
240 for (const auto &dir : esiBackendDirs) {
241 backendPath = dir / backendFileName;
242 logger.debug("CONNECT",
243 "trying to find backend plugin: " + backendPath.string());
244 if (std::filesystem::exists(backendPath)) {
245 found = true;
246 break;
247 }
248 }
249
250 // If the path was found, convert it to a string.
251 if (found) {
252 backendPath = std::filesystem::absolute(backendPath);
253 logger.debug("CONNECT", "found backend plugin: " + backendPath.string());
254 } else {
255 // If all else fails, just try the name.
256 backendPath = backendFileName;
257 logger.debug("CONNECT",
258 "trying to find backend plugin: " + backendFileName);
259 }
260
261 // Attempt to load it.
262#ifdef __linux__
263 void *handle = dlopen(backendPath.string().c_str(), RTLD_NOW | RTLD_GLOBAL);
264 if (!handle) {
265 std::string error(dlerror());
266 logger.error("CONNECT",
267 "while attempting to load backend plugin: " + error);
268 throw std::runtime_error("While attempting to load backend plugin: " +
269 error);
270 }
271#elif _WIN32
272 // Set the DLL directory to the same directory as the backend DLL in case it
273 // has transitive dependencies.
274 if (found) {
275 std::filesystem::path backendPathParent = backendPath.parent_path();
276 // If backendPath has no parent directory (e.g., it's a relative path or
277 // a filename without a directory), fallback to the current working
278 // directory. This ensures a valid directory is used for setting the DLL
279 // search path.
280 if (backendPathParent.empty())
281 backendPathParent = std::filesystem::current_path();
282 logger.debug("CONNECT", "setting DLL search directory to: " +
283 backendPathParent.string());
284 if (SetDllDirectoryA(backendPathParent.string().c_str()) == 0)
285 throw std::runtime_error("While setting DLL directory: " +
286 std::to_string(GetLastError()));
287 }
288
289 // Load the backend plugin.
290 HMODULE handle = LoadLibraryA(backendPath.string().c_str());
291 if (!handle) {
292 DWORD error = GetLastError();
293 // Get the error message string
294 LPSTR messageBuffer = nullptr;
295 size_t size = FormatMessageA(
296 FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
297 FORMAT_MESSAGE_IGNORE_INSERTS,
298 nullptr, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
299 (LPSTR)&messageBuffer, 0, nullptr);
300
301 std::string errorMessage;
302 if (size > 0 && messageBuffer != nullptr) {
303 errorMessage = std::string(messageBuffer, size);
304 LocalFree(messageBuffer);
305 } else {
306 errorMessage = "Unknown error";
307 }
308
309 std::string fullError = "While attempting to load backend plugin '" +
310 backendPath.string() + "': " + errorMessage +
311 " (error code: " + std::to_string(error) + ")";
312
313 logger.error("CONNECT", fullError);
314 throw std::runtime_error(fullError);
315 }
316#else
317#eror "Unsupported platform"
318#endif
319 logger.info("CONNECT", "loaded backend plugin: " + backendPath.string());
320}
321
322namespace registry {
323namespace internal {
324
326public:
327 static std::map<std::string, BackendCreate> &get() {
328 static BackendRegistry instance;
329 return instance.backendRegistry;
330 }
331
332private:
333 std::map<std::string, BackendCreate> backendRegistry;
334};
335
336void registerBackend(const std::string &name, BackendCreate create) {
337 auto &registry = BackendRegistry::get();
338 if (registry.count(name))
339 throw std::runtime_error("Backend already exists in registry");
340 registry[name] = create;
341}
342} // namespace internal
343
344} // namespace registry
345
347 std::string connection) {
349 auto f = registry.find(backend);
350 if (f == registry.end()) {
351 // If it's not already found in the registry, try to load it dynamically.
352 loadBackend(*this, backend);
353 f = registry.find(backend);
354 if (f == registry.end()) {
355 ServiceImplDetails details;
356 details["backend"] = backend;
357 std::ostringstream loaded_backends;
358 bool first = true;
359 for (const auto &b : registry) {
360 if (!first)
361 loaded_backends << ", ";
362 loaded_backends << b.first;
363 first = false;
364 }
365 details["loaded_backends"] = loaded_backends.str();
366 getLogger().error("CONNECT", "backend '" + backend + "' not found",
367 &details);
368 throw std::runtime_error("Backend '" + backend + "' not found");
369 }
370 }
371 getLogger().info("CONNECT", "connecting to backend " + backend + " via '" +
372 connection + "'");
373 auto conn = f->second(*this, connection);
374 auto *connPtr = conn.get();
375 connections.emplace_back(std::move(conn));
376 return connPtr;
377}
378
380 Impl() {}
381 void start() { me = std::thread(&Impl::loop, this); }
382 void stop() {
383 shutdown = true;
384 me.join();
385 }
386 /// When there's data on any of the listenPorts, call the callback. This
387 /// method can be called from any thread.
388 void
389 addListener(std::initializer_list<ReadChannelPort *> listenPorts,
390 std::function<void(ReadChannelPort *, MessageData)> callback);
391
392 void addTask(std::function<void(void)> task) {
393 std::lock_guard<std::mutex> g(m);
394 taskList.push_back(task);
395 }
396
397private:
398 void loop();
399 volatile bool shutdown = false;
400 std::thread me;
401
402 // Protect the shared data structures.
403 std::mutex m;
404
405 // Map of read ports to callbacks.
406 std::map<ReadChannelPort *,
407 std::pair<std::function<void(ReadChannelPort *, MessageData)>,
408 std::future<MessageData>>>
410
411 /// Tasks which should be called on every loop iteration.
412 std::vector<std::function<void(void)>> taskList;
413};
414
415void AcceleratorServiceThread::Impl::loop() {
416 // These two variables should logically be in the loop, but this avoids
417 // reconstructing them on each iteration.
418 std::vector<std::tuple<ReadChannelPort *,
419 std::function<void(ReadChannelPort *, MessageData)>,
421 portUnlockWorkList;
422 std::vector<std::function<void(void)>> taskListCopy;
423 MessageData data;
424
425 while (!shutdown) {
426 // Ideally we'd have some wake notification here, but this sufficies for
427 // now.
428 // TODO: investigate better ways to do this. For now, just play nice with
429 // the other processes but don't waste time in between polling intervals.
430 std::this_thread::yield();
431
432 // Check and gather data from all the read ports we are monitoring. Put the
433 // callbacks to be called later so we can release the lock.
434 {
435 std::lock_guard<std::mutex> g(m);
436 for (auto &[channel, cbfPair] : listeners) {
437 assert(channel && "Null channel in listener list");
438 std::future<MessageData> &f = cbfPair.second;
439 if (f.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
440 portUnlockWorkList.emplace_back(channel, cbfPair.first, f.get());
441 f = channel->readAsync();
442 }
443 }
444 }
445
446 // Call the callbacks outside the lock.
447 for (auto [channel, cb, data] : portUnlockWorkList)
448 cb(channel, std::move(data));
449
450 // Clear the worklist for the next iteration.
451 portUnlockWorkList.clear();
452
453 // Call any tasks that have been added. Copy it first so we can release the
454 // lock ASAP.
455 {
456 std::lock_guard<std::mutex> g(m);
457 taskListCopy = taskList;
458 }
459 for (auto &task : taskListCopy)
460 task();
461 }
462}
463
464void AcceleratorServiceThread::Impl::addListener(
465 std::initializer_list<ReadChannelPort *> listenPorts,
466 std::function<void(ReadChannelPort *, MessageData)> callback) {
467 std::lock_guard<std::mutex> g(m);
468 for (auto port : listenPorts) {
469 if (listeners.count(port))
470 throw std::runtime_error("Port already has a listener");
471 listeners[port] = std::make_pair(callback, port->readAsync());
472 }
473}
474
476 : impl(std::make_unique<Impl>()) {
477 impl->start();
478}
480
482 if (impl) {
483 impl->stop();
484 impl.reset();
485 }
486}
487
488// When there's data on any of the listenPorts, call the callback. This is
489// kinda silly now that we have callback port support, especially given the
490// polling loop. Keep the functionality for now.
492 std::initializer_list<ReadChannelPort *> listenPorts,
493 std::function<void(ReadChannelPort *, MessageData)> callback) {
494 assert(impl && "Service thread not running");
495 impl->addListener(listenPorts, callback);
496}
497
499 assert(impl && "Service thread not running");
500 impl->addTask([&module]() { module.poll(); });
501}
502
504 // Stop polling before tearing down engines.
505 if (serviceThread) {
506 serviceThread->stop();
507 serviceThread.reset();
508 }
509 // Drain engines while the accelerator (and its MMIO regions/services) is
510 // still alive, since engine/port teardown may touch accelerator-owned
511 // resources. Idempotent: disconnect() may be called more than once (e.g.
512 // explicitly and again from the destructor).
513 for (auto &[idPath, engine] : ownedEngines)
514 engine->disconnect();
515 clientEngines.clear();
516 ownedEngines.clear();
517}
518
519} // namespace esi
assert(baseType &&"element must be base type")
Abstract class representing a connection to an accelerator.
Definition Accelerator.h:96
virtual Service * createService(Service::Type service, AppIDPath idPath, std::string implName, const ServiceImplDetails &details, const HWClientDetails &clients)=0
Called by getServiceImpl exclusively.
ServiceClass * getService(AppIDPath id={}, std::string implName={}, ServiceImplDetails details={}, HWClientDetails clients={})
Get a typed reference to a particular service type.
virtual bool reset()
Request a reset of the accelerator design.
void clearOwnedObjects()
Drop accelerator-owned objects before a derived backend destroys resources that those objects may ref...
std::map< AppIDPath, BundleEngineMap > clientEngines
Mapping of clients to their servicing engines.
void registerEngine(AppIDPath idPath, std::unique_ptr< Engine > engine, const HWClientDetails &clients)
If createEngine is overridden, this method should be called to register the engine and all of the cha...
std::map< ServiceCacheKey, std::unique_ptr< Service > > serviceCache
std::unique_ptr< AcceleratorServiceThread > serviceThread
std::unique_ptr< Accelerator > ownedAccelerator
Accelerator object owned by this connection.
virtual void disconnect()
Disconnect from the accelerator cleanly.
std::map< AppIDPath, std::unique_ptr< Engine > > ownedEngines
Collection of owned engines.
virtual void createEngine(const std::string &engineTypeName, AppIDPath idPath, const ServiceImplDetails &details, const HWClientDetails &clients)
Create a new engine for channel communication with the accelerator.
Logger & getLogger() const
AcceleratorServiceThread * getServiceThread()
Return a pointer to the accelerator 'service' thread (or threads).
AcceleratorConnection(Context &ctxt)
Accelerator * takeOwnership(std::unique_ptr< Accelerator > accel)
Assume ownership of an accelerator object.
Background thread which services various requests.
std::unique_ptr< Impl > impl
void stop()
Instruct the service thread to stop running.
void addPoll(HWModule &module)
Poll this module.
void addListener(std::initializer_list< ReadChannelPort * > listenPorts, std::function< void(ReadChannelPort *, MessageData)> callback)
When there's data on any of the listenPorts, call the callback.
Top level accelerator class.
Definition Accelerator.h:77
AcceleratorConnections, Accelerators, and Manifests must all share a context.
Definition Context.h:34
Logger & getLogger()
Definition Context.h:69
std::vector< std::unique_ptr< AcceleratorConnection > > connections
Definition Context.h:73
AcceleratorConnection * connect(std::string backend, std::string connection)
Connect to an accelerator backend.
Engines implement the actual channel communication between the host and the accelerator.
Definition Engines.h:42
Represents either the top level or an instance of a hardware module.
Definition Design.h:47
virtual void error(const std::string &subsystem, const std::string &msg, const std::map< std::string, std::any > *details=nullptr)
Report an error.
Definition Logging.h:64
virtual void info(const std::string &subsystem, const std::string &msg, const std::map< std::string, std::any > *details=nullptr)
Report an informational message.
Definition Logging.h:75
void debug(const std::string &subsystem, const std::string &msg, const std::map< std::string, std::any > *details=nullptr)
Report a debug message.
Definition Logging.h:83
A concrete flat message backed by a single vector of bytes.
Definition Common.h:155
A ChannelPort which reads data from the accelerator.
Definition Ports.h:453
std::map< std::string, BackendCreate > backendRegistry
static std::map< std::string, BackendCreate > & get()
virtual void write(uint32_t addr, uint64_t data)=0
Write a 64-bit value to the global MMIO space.
static Service * createService(AcceleratorConnection *acc, Service::Type svcType, AppIDPath id, std::string implName, ServiceImplDetails details, HWClientDetails clients)
Create a service instance from the given details.
Definition Services.cpp:487
Parent class of all APIs modeled as 'services'.
Definition Services.h:59
const std::type_info & Type
Definition Services.h:61
void registerBackend(const std::string &name, BackendCreate create)
std::function< std::unique_ptr< AcceleratorConnection >(Context &, std::string)> BackendCreate
Backends can register themselves to be connected via a connection string.
std::unique_ptr< Engine > createEngine(AcceleratorConnection &conn, const std::string &dmaEngineName, AppIDPath idPath, const ServiceImplDetails &details, const HWClientDetails &clients)
Create an engine by name.
Definition Engines.cpp:555
Definition esi.py:1
static std::filesystem::path getExePath()
Get the path to the currently running executable.
std::map< std::string, std::any > ServiceImplDetails
Definition Common.h:108
static void loadBackend(Context &ctxt, std::string backend)
Load a backend plugin dynamically.
constexpr uint64_t ResetMagicNumber
Magic value which, when written to MMIO offset ResetRequestOffset, requests a design reset.
Definition Accelerator.h:64
static std::filesystem::path getLibPath()
Get the path to the currently running shared library.
constexpr uint32_t ResetRequestOffset
Offset into the (global) MMIO space at which to request a design reset.
Definition Accelerator.h:66
static std::vector< std::filesystem::path > getESIBackendDirectories()
Get the list of directories to search for backend plugins.
std::vector< HWClientDetail > HWClientDetails
Definition Common.h:107
std::map< ReadChannelPort *, std::pair< std::function< void(ReadChannelPort *, MessageData)>, std::future< MessageData > > > listeners
void addTask(std::function< void(void)> task)
void addListener(std::initializer_list< ReadChannelPort * > listenPorts, std::function< void(ReadChannelPort *, MessageData)> callback)
When there's data on any of the listenPorts, call the callback.
std::vector< std::function< void(void)> > taskList
Tasks which should be called on every loop iteration.