CIRCT 22.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 <filesystem>
19#include <map>
20#include <stdexcept>
21
22#include <iostream>
23
24#ifdef __linux__
25#include <dlfcn.h>
26#include <linux/limits.h>
27#include <unistd.h>
28#elif _WIN32
29#include <windows.h>
30#endif
31
32using namespace esi;
33using namespace esi::services;
34
35namespace esi {
37 : ctxt(ctxt), serviceThread(nullptr) {}
39
41 if (!serviceThread)
42 serviceThread = std::make_unique<AcceleratorServiceThread>();
43 return serviceThread.get();
44}
45void AcceleratorConnection::createEngine(const std::string &engineTypeName,
46 AppIDPath idPath,
47 const ServiceImplDetails &details,
48 const HWClientDetails &clients) {
49 std::unique_ptr<Engine> engine = ::esi::registry::createEngine(
50 *this, engineTypeName, idPath, details, clients);
51 registerEngine(idPath, std::move(engine), clients);
52}
53
55 std::unique_ptr<Engine> engine,
56 const HWClientDetails &clients) {
57 assert(engine);
58 auto [engineIter, _] = ownedEngines.emplace(idPath, std::move(engine));
59
60 // Engine is now owned by the accelerator connection, so the std::unique_ptr
61 // is no longer valid. Resolve a new one from the map iter.
62 Engine *enginePtr = engineIter->second.get();
63 // Compute our parents idPath path.
64 AppIDPath prefix = std::move(idPath);
65 if (prefix.size() > 0)
66 prefix.pop_back();
67
68 for (const auto &client : clients) {
69 AppIDPath fullClientPath = prefix + client.relPath;
70 for (const auto &channel : client.channelAssignments)
71 clientEngines[fullClientPath].setEngine(channel.first, enginePtr);
72 }
73}
74
76 AppIDPath id,
77 std::string implName,
78 ServiceImplDetails details,
79 HWClientDetails clients) {
80 std::unique_ptr<Service> &cacheEntry = serviceCache[make_tuple(&svcType, id)];
81 if (cacheEntry == nullptr) {
82 Service *svc = createService(svcType, id, implName, details, clients);
83 if (!svc)
84 svc = ServiceRegistry::createService(this, svcType, id, implName, details,
85 clients);
86 if (!svc)
87 return nullptr;
88 cacheEntry = std::unique_ptr<Service>(svc);
89 }
90 return cacheEntry.get();
91}
92
94AcceleratorConnection::takeOwnership(std::unique_ptr<Accelerator> acc) {
96 throw std::runtime_error(
97 "AcceleratorConnection already owns an accelerator");
98 ownedAccelerator = std::move(acc);
99 return ownedAccelerator.get();
100}
101
102/// Get the path to the currently running executable.
103static std::filesystem::path getExePath() {
104#ifdef __linux__
105 char result[PATH_MAX];
106 ssize_t count = readlink("/proc/self/exe", result, PATH_MAX);
107 if (count == -1)
108 throw std::runtime_error("Could not get executable path");
109 return std::filesystem::path(std::string(result, count));
110#elif _WIN32
111 char buffer[MAX_PATH];
112 DWORD length = GetModuleFileNameA(NULL, buffer, MAX_PATH);
113 if (length == 0)
114 throw std::runtime_error("Could not get executable path");
115 return std::filesystem::path(std::string(buffer, length));
116#else
117#eror "Unsupported platform"
118#endif
119}
120
121/// Get the path to the currently running shared library.
122static std::filesystem::path getLibPath() {
123#ifdef __linux__
124 Dl_info dl_info;
125 dladdr((void *)getLibPath, &dl_info);
126 return std::filesystem::path(std::string(dl_info.dli_fname));
127#elif _WIN32
128 HMODULE hModule = NULL;
129 if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
130 GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
131 reinterpret_cast<LPCSTR>(&getLibPath), &hModule)) {
132 // Handle error
133 return std::filesystem::path();
134 }
135
136 char buffer[MAX_PATH];
137 DWORD length = GetModuleFileNameA(hModule, buffer, MAX_PATH);
138 if (length == 0)
139 throw std::runtime_error("Could not get library path");
140
141 return std::filesystem::path(std::string(buffer, length));
142#else
143#eror "Unsupported platform"
144#endif
145}
146
147/// Load a backend plugin dynamically. Plugins are expected to be named
148/// lib<BackendName>Backend.so and located in one of 1) CWD, 2) in the same
149/// directory as the application, or 3) in the same directory as this library.
150static void loadBackend(Context &ctxt, std::string backend) {
151 Logger &logger = ctxt.getLogger();
152 backend[0] = toupper(backend[0]);
153
154 // Get the file name we are looking for.
155#ifdef __linux__
156 std::string backendFileName = "lib" + backend + "Backend.so";
157#elif _WIN32
158 std::string backendFileName = backend + "Backend.dll";
159#else
160#eror "Unsupported platform"
161#endif
162
163 // Look for library using the C++ std API.
164
165 // First, try the current directory.
166 std::filesystem::path backendPath = backendFileName;
167 std::string backendPathStr;
168 logger.debug("CONNECT",
169 "trying to find backend plugin: " + backendPath.string());
170 if (!std::filesystem::exists(backendPath)) {
171 // Next, try the directory of the executable.
172 backendPath = getExePath().parent_path().append(backendFileName);
173 logger.debug("CONNECT",
174 "trying to find backend plugin: " + backendPath.string());
175 if (!std::filesystem::exists(backendPath)) {
176 // Finally, try the directory of the library.
177 backendPath = getLibPath().parent_path().append(backendFileName);
178 logger.debug("CONNECT",
179 "trying to find backend plugin: " + backendPath.string());
180 if (!std::filesystem::exists(backendPath)) {
181 // If all else fails, just try the name.
182 backendPathStr = backendFileName;
183 logger.debug("CONNECT",
184 "trying to find backend plugin: " + backendPathStr);
185 }
186 }
187 }
188 // If the path was found, convert it to a string.
189 if (backendPathStr.empty()) {
190 backendPath = std::filesystem::absolute(backendPath);
191 logger.debug("CONNECT", "found backend plugin: " + backendPath.string());
192 backendPathStr = backendPath.string();
193 } else {
194 // Otherwise, signal that the path wasn't found by clearing the path and
195 // just use the name. (This is only used on Windows to add the same
196 // directory as the backend DLL to the DLL search path.)
197 backendPath.clear();
198 }
199
200 // Attempt to load it.
201#ifdef __linux__
202 void *handle = dlopen(backendPathStr.c_str(), RTLD_NOW | RTLD_GLOBAL);
203 if (!handle) {
204 std::string error(dlerror());
205 logger.error("CONNECT",
206 "while attempting to load backend plugin: " + error);
207 throw std::runtime_error("While attempting to load backend plugin: " +
208 error);
209 }
210#elif _WIN32
211 // Set the DLL directory to the same directory as the backend DLL in case it
212 // has transitive dependencies.
213 if (backendPath != std::filesystem::path()) {
214 std::filesystem::path backendPathParent = backendPath.parent_path();
215 // If backendPath has no parent directory (e.g., it's a relative path or
216 // a filename without a directory), fallback to the current working
217 // directory. This ensures a valid directory is used for setting the DLL
218 // search path.
219 if (backendPathParent.empty())
220 backendPathParent = std::filesystem::current_path();
221 logger.debug("CONNECT", "setting DLL search directory to: " +
222 backendPathParent.string());
223 if (SetDllDirectoryA(backendPathParent.string().c_str()) == 0)
224 throw std::runtime_error("While setting DLL directory: " +
225 std::to_string(GetLastError()));
226 }
227
228 // Load the backend plugin.
229 HMODULE handle = LoadLibraryA(backendPathStr.c_str());
230 if (!handle) {
231 DWORD error = GetLastError();
232 // Get the error message string
233 LPSTR messageBuffer = nullptr;
234 size_t size = FormatMessageA(
235 FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
236 FORMAT_MESSAGE_IGNORE_INSERTS,
237 nullptr, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
238 (LPSTR)&messageBuffer, 0, nullptr);
239
240 std::string errorMessage;
241 if (size > 0 && messageBuffer != nullptr) {
242 errorMessage = std::string(messageBuffer, size);
243 LocalFree(messageBuffer);
244 } else {
245 errorMessage = "Unknown error";
246 }
247
248 std::string fullError = "While attempting to load backend plugin '" +
249 backendPathStr + "': " + errorMessage +
250 " (error code: " + std::to_string(error) + ")";
251
252 logger.error("CONNECT", fullError);
253 throw std::runtime_error(fullError);
254 }
255#else
256#eror "Unsupported platform"
257#endif
258 logger.info("CONNECT", "loaded backend plugin: " + backendPathStr);
259}
260
261namespace registry {
262namespace internal {
263
265public:
266 static std::map<std::string, BackendCreate> &get() {
267 static BackendRegistry instance;
268 return instance.backendRegistry;
269 }
270
271private:
272 std::map<std::string, BackendCreate> backendRegistry;
273};
274
275void registerBackend(const std::string &name, BackendCreate create) {
276 auto &registry = BackendRegistry::get();
277 if (registry.count(name))
278 throw std::runtime_error("Backend already exists in registry");
279 registry[name] = create;
280}
281} // namespace internal
282
283std::unique_ptr<AcceleratorConnection> connect(Context &ctxt,
284 const std::string &backend,
285 const std::string &connection) {
286 auto &registry = internal::BackendRegistry::get();
287 auto f = registry.find(backend);
288 if (f == registry.end()) {
289 // If it's not already found in the registry, try to load it dynamically.
290 loadBackend(ctxt, backend);
291 f = registry.find(backend);
292 if (f == registry.end()) {
294 details["backend"] = backend;
295 std::ostringstream loaded_backends;
296 bool first = true;
297 for (const auto &b : registry) {
298 if (!first)
299 loaded_backends << ", ";
300 loaded_backends << b.first;
301 first = false;
302 }
303 details["loaded_backends"] = loaded_backends.str();
304 ctxt.getLogger().error("CONNECT", "backend '" + backend + "' not found",
305 &details);
306 throw std::runtime_error("Backend '" + backend + "' not found");
307 }
308 }
309 ctxt.getLogger().info("CONNECT", "connecting to backend " + backend +
310 " via '" + connection + "'");
311 return f->second(ctxt, connection);
312}
313
314} // namespace registry
315
317 Impl() {}
318 void start() { me = std::thread(&Impl::loop, this); }
319 void stop() {
320 shutdown = true;
321 me.join();
322 }
323 /// When there's data on any of the listenPorts, call the callback. This
324 /// method can be called from any thread.
325 void
326 addListener(std::initializer_list<ReadChannelPort *> listenPorts,
327 std::function<void(ReadChannelPort *, MessageData)> callback);
328
329 void addTask(std::function<void(void)> task) {
330 std::lock_guard<std::mutex> g(m);
331 taskList.push_back(task);
332 }
333
334private:
335 void loop();
336 volatile bool shutdown = false;
337 std::thread me;
338
339 // Protect the shared data structures.
340 std::mutex m;
341
342 // Map of read ports to callbacks.
343 std::map<ReadChannelPort *,
344 std::pair<std::function<void(ReadChannelPort *, MessageData)>,
345 std::future<MessageData>>>
347
348 /// Tasks which should be called on every loop iteration.
349 std::vector<std::function<void(void)>> taskList;
350};
351
352void AcceleratorServiceThread::Impl::loop() {
353 // These two variables should logically be in the loop, but this avoids
354 // reconstructing them on each iteration.
355 std::vector<std::tuple<ReadChannelPort *,
356 std::function<void(ReadChannelPort *, MessageData)>,
358 portUnlockWorkList;
359 std::vector<std::function<void(void)>> taskListCopy;
360 MessageData data;
361
362 while (!shutdown) {
363 // Ideally we'd have some wake notification here, but this sufficies for
364 // now.
365 // TODO: investigate better ways to do this. For now, just play nice with
366 // the other processes but don't waste time in between polling intervals.
367 std::this_thread::yield();
368
369 // Check and gather data from all the read ports we are monitoring. Put the
370 // callbacks to be called later so we can release the lock.
371 {
372 std::lock_guard<std::mutex> g(m);
373 for (auto &[channel, cbfPair] : listeners) {
374 assert(channel && "Null channel in listener list");
375 std::future<MessageData> &f = cbfPair.second;
376 if (f.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
377 portUnlockWorkList.emplace_back(channel, cbfPair.first, f.get());
378 f = channel->readAsync();
379 }
380 }
381 }
382
383 // Call the callbacks outside the lock.
384 for (auto [channel, cb, data] : portUnlockWorkList)
385 cb(channel, std::move(data));
386
387 // Clear the worklist for the next iteration.
388 portUnlockWorkList.clear();
389
390 // Call any tasks that have been added. Copy it first so we can release the
391 // lock ASAP.
392 {
393 std::lock_guard<std::mutex> g(m);
394 taskListCopy = taskList;
395 }
396 for (auto &task : taskListCopy)
397 task();
398 }
399}
400
401void AcceleratorServiceThread::Impl::addListener(
402 std::initializer_list<ReadChannelPort *> listenPorts,
403 std::function<void(ReadChannelPort *, MessageData)> callback) {
404 std::lock_guard<std::mutex> g(m);
405 for (auto port : listenPorts) {
406 if (listeners.count(port))
407 throw std::runtime_error("Port already has a listener");
408 listeners[port] = std::make_pair(callback, port->readAsync());
409 }
410}
411
412} // namespace esi
413
415 : impl(std::make_unique<Impl>()) {
416 impl->start();
417}
419
421 if (impl) {
422 impl->stop();
423 impl.reset();
424 }
425}
426
427// When there's data on any of the listenPorts, call the callback. This is
428// kinda silly now that we have callback port support, especially given the
429// polling loop. Keep the functionality for now.
431 std::initializer_list<ReadChannelPort *> listenPorts,
432 std::function<void(ReadChannelPort *, MessageData)> callback) {
433 assert(impl && "Service thread not running");
434 impl->addListener(listenPorts, callback);
435}
436
438 assert(impl && "Service thread not running");
439 impl->addTask([&module]() { module.poll(); });
440}
441
443 if (serviceThread) {
444 serviceThread->stop();
445 serviceThread.reset();
446 }
447}
assert(baseType &&"element must be base type")
virtual void disconnect()
Disconnect from the accelerator cleanly.
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.
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.
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.
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.
void stop()
Instruct the service thread to stop running.
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::unique_ptr< Impl > impl
void addPoll(HWModule &module)
Poll this module.
Top level accelerator class.
Definition Accelerator.h:60
AcceleratorConnections, Accelerators, and Manifests must all share a context.
Definition Context.h:31
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 logical chunk of data representing serialized data.
Definition Common.h:105
A ChannelPort which reads data from the accelerator.
Definition Ports.h:124
std::map< std::string, BackendCreate > backendRegistry
static std::map< std::string, BackendCreate > & get()
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:333
Parent class of all APIs modeled as 'services'.
Definition Services.h:46
const std::type_info & Type
Definition Services.h:48
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< AcceleratorConnection > connect(Context &ctxt, const std::string &backend, const std::string &connection)
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:507
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:100
static void loadBackend(Context &ctxt, std::string backend)
Load a backend plugin dynamically.
static std::filesystem::path getLibPath()
Get the path to the currently running shared library.
std::vector< HWClientDetail > HWClientDetails
Definition Common.h:99
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.