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 ownedAccelerator.reset();
125 serviceCache.clear();
126 clientEngines.clear();
127 ownedEngines.clear();
128}
129
130/// Get the path to the currently running executable.
131static std::filesystem::path getExePath() {
132#ifdef __linux__
133 char result[PATH_MAX];
134 ssize_t count = readlink("/proc/self/exe", result, PATH_MAX);
135 if (count == -1)
136 throw std::runtime_error("Could not get executable path");
137 return std::filesystem::path(std::string(result, count));
138#elif _WIN32
139 char buffer[MAX_PATH];
140 DWORD length = GetModuleFileNameA(NULL, buffer, MAX_PATH);
141 if (length == 0)
142 throw std::runtime_error("Could not get executable path");
143 return std::filesystem::path(std::string(buffer, length));
144#else
145#eror "Unsupported platform"
146#endif
147}
148
149/// Get the path to the currently running shared library.
150static std::filesystem::path getLibPath() {
151#ifdef __linux__
152 Dl_info dl_info;
153 dladdr((void *)getLibPath, &dl_info);
154 return std::filesystem::path(std::string(dl_info.dli_fname));
155#elif _WIN32
156 HMODULE hModule = NULL;
157 if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
158 GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
159 reinterpret_cast<LPCSTR>(&getLibPath), &hModule)) {
160 // Handle error
161 return std::filesystem::path();
162 }
163
164 char buffer[MAX_PATH];
165 DWORD length = GetModuleFileNameA(hModule, buffer, MAX_PATH);
166 if (length == 0)
167 throw std::runtime_error("Could not get library path");
168
169 return std::filesystem::path(std::string(buffer, length));
170#else
171#eror "Unsupported platform"
172#endif
173}
174
175/// Get the list of directories to search for backend plugins.
176static std::vector<std::filesystem::path> getESIBackendDirectories() {
177 std::vector<std::filesystem::path> directories;
178
179 // First, check current directory.
180 directories.push_back(std::filesystem::current_path());
181
182 // Next, parse the ESI_BACKENDS environment variable and add those.
183 const char *esiBackends = std::getenv("ESI_BACKENDS");
184 if (esiBackends) {
185 // Use platform-specific path separator
186#ifdef _WIN32
187 const char separator = ';';
188#else
189 const char separator = ':';
190#endif
191
192 std::string pathsStr(esiBackends);
193 std::stringstream ss(pathsStr);
194 std::string path;
195
196 while (std::getline(ss, path, separator))
197 if (!path.empty())
198 directories.emplace_back(path);
199 }
200
201 // Next, try the directory of the executable.
202 directories.push_back(getExePath().parent_path());
203 // Finally, try the directory of the library.
204 directories.push_back(getLibPath().parent_path());
205
206 return directories;
207}
208
209/// Load a backend plugin dynamically. Plugins are expected to be named
210/// lib<BackendName>Backend.so and located in one of 1) CWD, 2) directories
211/// specified in ESI_BACKENDS environment variable, 3) in the same directory as
212/// the application, or 4) in the same directory as this library.
213static void loadBackend(Context &ctxt, std::string backend) {
214 Logger &logger = ctxt.getLogger();
215 backend[0] = toupper(backend[0]);
216
217 // Get the file name we are looking for.
218#ifdef __linux__
219 std::string backendFileName = "lib" + backend + "Backend.so";
220#elif _WIN32
221 // In MSVC debug builds, load the debug variant of the plugin DLL (e.g.
222 // CosimBackend_d.dll) to ensure compatibility.
223#if defined(_MSC_VER) && defined(_DEBUG)
224 std::string backendFileName = backend + "Backend_d.dll";
225#else
226 std::string backendFileName = backend + "Backend.dll";
227#endif
228#else
229#error "Unsupported platform"
230#endif
231
232 // First, try the current directory.
233 std::filesystem::path backendPath;
234 // Next, try directories specified in ESI_BACKENDS environment variable.
235 std::vector<std::filesystem::path> esiBackendDirs =
237 bool found = false;
238 for (const auto &dir : esiBackendDirs) {
239 backendPath = dir / backendFileName;
240 logger.debug("CONNECT",
241 "trying to find backend plugin: " + backendPath.string());
242 if (std::filesystem::exists(backendPath)) {
243 found = true;
244 break;
245 }
246 }
247
248 // If the path was found, convert it to a string.
249 if (found) {
250 backendPath = std::filesystem::absolute(backendPath);
251 logger.debug("CONNECT", "found backend plugin: " + backendPath.string());
252 } else {
253 // If all else fails, just try the name.
254 backendPath = backendFileName;
255 logger.debug("CONNECT",
256 "trying to find backend plugin: " + backendFileName);
257 }
258
259 // Attempt to load it.
260#ifdef __linux__
261 void *handle = dlopen(backendPath.string().c_str(), RTLD_NOW | RTLD_GLOBAL);
262 if (!handle) {
263 std::string error(dlerror());
264 logger.error("CONNECT",
265 "while attempting to load backend plugin: " + error);
266 throw std::runtime_error("While attempting to load backend plugin: " +
267 error);
268 }
269#elif _WIN32
270 // Set the DLL directory to the same directory as the backend DLL in case it
271 // has transitive dependencies.
272 if (found) {
273 std::filesystem::path backendPathParent = backendPath.parent_path();
274 // If backendPath has no parent directory (e.g., it's a relative path or
275 // a filename without a directory), fallback to the current working
276 // directory. This ensures a valid directory is used for setting the DLL
277 // search path.
278 if (backendPathParent.empty())
279 backendPathParent = std::filesystem::current_path();
280 logger.debug("CONNECT", "setting DLL search directory to: " +
281 backendPathParent.string());
282 if (SetDllDirectoryA(backendPathParent.string().c_str()) == 0)
283 throw std::runtime_error("While setting DLL directory: " +
284 std::to_string(GetLastError()));
285 }
286
287 // Load the backend plugin.
288 HMODULE handle = LoadLibraryA(backendPath.string().c_str());
289 if (!handle) {
290 DWORD error = GetLastError();
291 // Get the error message string
292 LPSTR messageBuffer = nullptr;
293 size_t size = FormatMessageA(
294 FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
295 FORMAT_MESSAGE_IGNORE_INSERTS,
296 nullptr, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
297 (LPSTR)&messageBuffer, 0, nullptr);
298
299 std::string errorMessage;
300 if (size > 0 && messageBuffer != nullptr) {
301 errorMessage = std::string(messageBuffer, size);
302 LocalFree(messageBuffer);
303 } else {
304 errorMessage = "Unknown error";
305 }
306
307 std::string fullError = "While attempting to load backend plugin '" +
308 backendPath.string() + "': " + errorMessage +
309 " (error code: " + std::to_string(error) + ")";
310
311 logger.error("CONNECT", fullError);
312 throw std::runtime_error(fullError);
313 }
314#else
315#eror "Unsupported platform"
316#endif
317 logger.info("CONNECT", "loaded backend plugin: " + backendPath.string());
318}
319
320namespace registry {
321namespace internal {
322
324public:
325 static std::map<std::string, BackendCreate> &get() {
326 static BackendRegistry instance;
327 return instance.backendRegistry;
328 }
329
330private:
331 std::map<std::string, BackendCreate> backendRegistry;
332};
333
334void registerBackend(const std::string &name, BackendCreate create) {
335 auto &registry = BackendRegistry::get();
336 if (registry.count(name))
337 throw std::runtime_error("Backend already exists in registry");
338 registry[name] = create;
339}
340} // namespace internal
341
342} // namespace registry
343
345 std::string connection) {
347 auto f = registry.find(backend);
348 if (f == registry.end()) {
349 // If it's not already found in the registry, try to load it dynamically.
350 loadBackend(*this, backend);
351 f = registry.find(backend);
352 if (f == registry.end()) {
353 ServiceImplDetails details;
354 details["backend"] = backend;
355 std::ostringstream loaded_backends;
356 bool first = true;
357 for (const auto &b : registry) {
358 if (!first)
359 loaded_backends << ", ";
360 loaded_backends << b.first;
361 first = false;
362 }
363 details["loaded_backends"] = loaded_backends.str();
364 getLogger().error("CONNECT", "backend '" + backend + "' not found",
365 &details);
366 throw std::runtime_error("Backend '" + backend + "' not found");
367 }
368 }
369 getLogger().info("CONNECT", "connecting to backend " + backend + " via '" +
370 connection + "'");
371 auto conn = f->second(*this, connection);
372 auto *connPtr = conn.get();
373 connections.emplace_back(std::move(conn));
374 return connPtr;
375}
376
378 Impl() {}
379 void start() { me = std::thread(&Impl::loop, this); }
380 void stop() {
381 shutdown = true;
382 me.join();
383 }
384 /// When there's data on any of the listenPorts, call the callback. This
385 /// method can be called from any thread.
386 void
387 addListener(std::initializer_list<ReadChannelPort *> listenPorts,
388 std::function<void(ReadChannelPort *, MessageData)> callback);
389
390 void addTask(std::function<void(void)> task) {
391 std::lock_guard<std::mutex> g(m);
392 taskList.push_back(task);
393 }
394
395private:
396 void loop();
397 volatile bool shutdown = false;
398 std::thread me;
399
400 // Protect the shared data structures.
401 std::mutex m;
402
403 // Map of read ports to callbacks.
404 std::map<ReadChannelPort *,
405 std::pair<std::function<void(ReadChannelPort *, MessageData)>,
406 std::future<MessageData>>>
408
409 /// Tasks which should be called on every loop iteration.
410 std::vector<std::function<void(void)>> taskList;
411};
412
413void AcceleratorServiceThread::Impl::loop() {
414 // These two variables should logically be in the loop, but this avoids
415 // reconstructing them on each iteration.
416 std::vector<std::tuple<ReadChannelPort *,
417 std::function<void(ReadChannelPort *, MessageData)>,
419 portUnlockWorkList;
420 std::vector<std::function<void(void)>> taskListCopy;
421 MessageData data;
422
423 while (!shutdown) {
424 // Ideally we'd have some wake notification here, but this sufficies for
425 // now.
426 // TODO: investigate better ways to do this. For now, just play nice with
427 // the other processes but don't waste time in between polling intervals.
428 std::this_thread::yield();
429
430 // Check and gather data from all the read ports we are monitoring. Put the
431 // callbacks to be called later so we can release the lock.
432 {
433 std::lock_guard<std::mutex> g(m);
434 for (auto &[channel, cbfPair] : listeners) {
435 assert(channel && "Null channel in listener list");
436 std::future<MessageData> &f = cbfPair.second;
437 if (f.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
438 portUnlockWorkList.emplace_back(channel, cbfPair.first, f.get());
439 f = channel->readAsync();
440 }
441 }
442 }
443
444 // Call the callbacks outside the lock.
445 for (auto [channel, cb, data] : portUnlockWorkList)
446 cb(channel, std::move(data));
447
448 // Clear the worklist for the next iteration.
449 portUnlockWorkList.clear();
450
451 // Call any tasks that have been added. Copy it first so we can release the
452 // lock ASAP.
453 {
454 std::lock_guard<std::mutex> g(m);
455 taskListCopy = taskList;
456 }
457 for (auto &task : taskListCopy)
458 task();
459 }
460}
461
462void AcceleratorServiceThread::Impl::addListener(
463 std::initializer_list<ReadChannelPort *> listenPorts,
464 std::function<void(ReadChannelPort *, MessageData)> callback) {
465 std::lock_guard<std::mutex> g(m);
466 for (auto port : listenPorts) {
467 if (listeners.count(port))
468 throw std::runtime_error("Port already has a listener");
469 listeners[port] = std::make_pair(callback, port->readAsync());
470 }
471}
472
474 : impl(std::make_unique<Impl>()) {
475 impl->start();
476}
478
480 if (impl) {
481 impl->stop();
482 impl.reset();
483 }
484}
485
486// When there's data on any of the listenPorts, call the callback. This is
487// kinda silly now that we have callback port support, especially given the
488// polling loop. Keep the functionality for now.
490 std::initializer_list<ReadChannelPort *> listenPorts,
491 std::function<void(ReadChannelPort *, MessageData)> callback) {
492 assert(impl && "Service thread not running");
493 impl->addListener(listenPorts, callback);
494}
495
497 assert(impl && "Service thread not running");
498 impl->addTask([&module]() { module.poll(); });
499}
500
502 if (serviceThread) {
503 serviceThread->stop();
504 serviceThread.reset();
505 }
506}
507
508} // 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.