CIRCT 24.0.0git
Loading...
Searching...
No Matches
Cosim.cpp
Go to the documentation of this file.
1//===- Cosim.cpp - Connection to ESI simulation ---------------------------===//
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/Engines.h"
18#include "esi/Manifest.h"
19#include "esi/Ports.h"
20#include "esi/Services.h"
21#include "esi/Utils.h"
23
24#include <algorithm>
25#include <bit>
26#include <cstdlib>
27#include <cstring>
28#include <format>
29#include <fstream>
30#include <initializer_list>
31#include <iostream>
32#include <memory>
33#include <new>
34#include <set>
35#include <string_view>
36#include <vector>
37
38using namespace esi;
39using namespace esi::services;
40using namespace esi::backends::cosim;
41
42namespace {
43
44//===----------------------------------------------------------------------===//
45// WriteCosimChannelPort
46//===----------------------------------------------------------------------===//
47
48/// Cosim client implementation of a write channel port. Applies the cosim
49/// protocol's credit-based flow control: while the number of frames not yet
50/// consumed by the simulation is at the in-flight limit, `tryWrite()` fails
51/// and `write()` blocks. Frames sent before the simulation has connected its
52/// end are held by the server, and count as in flight. The limit is
53/// `ConnectOptions::bufferSize` (0 = unbounded) if specified, else
54/// `defaultMaxInFlight`.
55class WriteCosimChannelPort : public WriteChannelPort {
56public:
57 WriteCosimChannelPort(
58 AcceleratorConnection &conn, RpcClient &client,
59 const RpcClient::ChannelDesc &desc, const Type *type, std::string name,
60 unsigned defaultMaxInFlight = RpcClient::DefaultToServerMaxInFlight)
61 : WriteChannelPort(type), conn(conn), client(client), desc(desc),
62 name(std::move(name)), defaultMaxInFlight(defaultMaxInFlight) {}
63 ~WriteCosimChannelPort() = default;
64
65 void connectImpl(const ChannelPort::ConnectOptions &options) override {
66 if (desc.dir != RpcClient::ChannelDirection::ToServer)
67 throw std::runtime_error("Channel '" + name +
68 "' is not a to server channel");
69 client.setToServerMaxInFlight(
70 name, options.bufferSize.value_or(defaultMaxInFlight));
71 }
72
73protected:
74 void traceWrite(const MessageData &data) {
75 conn.getLogger().trace(
76 [this,
77 &data](std::string &subsystem, std::string &msg,
78 std::unique_ptr<std::map<std::string, std::any>> &details) {
79 subsystem = "cosim_write";
80 msg = "Writing message to channel '" + name + "'";
81 details = std::make_unique<std::map<std::string, std::any>>();
82 (*details)["channel"] = name;
83 (*details)["data_size"] = data.getSize();
84 (*details)["message_data"] = data.toHex();
85 });
86 }
87
88 void writeImpl(const MessageData &data) override {
89 auto frames = getMessageFrames(data);
90 for (const auto &frame : frames) {
91 traceWrite(data);
92 client.writeToServer(name, frame);
93 }
94 }
95 bool tryWriteImpl(const MessageData &data) override {
96 auto frames = getMessageFrames(data);
97 // Admit the rest of a translated (windowed) message once its first frame
98 // was, like the frames of a multi-frame message, so `tryWrite()` never
99 // leaves part of a message waiting for a `flush()` which may not come.
100 bool rest = translateMessages && translationBufferIdx > 0;
101 if (!client.tryWriteToServer(name, frames, /*force=*/rest))
102 return false;
103 for (size_t i = 0; i < frames.size(); ++i)
104 traceWrite(data);
105 return true;
106 }
107
108private:
109 AcceleratorConnection &conn;
110 RpcClient &client;
111 RpcClient::ChannelDesc desc;
112 std::string name;
113 unsigned defaultMaxInFlight;
114};
115
116//===----------------------------------------------------------------------===//
117// ReadCosimChannelPort
118//===----------------------------------------------------------------------===//
119
120/// Cosim client implementation of a read channel port. The wire transport
121/// (see `CosimRpc`) delivers messages via callback, so this class just
122/// forwards them to the registered `ReadChannelPort` consumer.
123class ReadCosimChannelPort : public ReadChannelPort {
124public:
125 ReadCosimChannelPort(AcceleratorConnection &conn, RpcClient &client,
126 const RpcClient::ChannelDesc &desc, const Type *type,
127 std::string name)
128 : ReadChannelPort(type), conn(conn), client(client), desc(desc),
129 name(std::move(name)) {}
130
131 ~ReadCosimChannelPort() = default;
132
133 void connectImpl(const ChannelPort::ConnectOptions &options) override {
134 if (desc.dir != RpcClient::ChannelDirection::ToClient)
135 throw std::runtime_error("Channel '" + name +
136 "' is not a to client channel");
137
138 // Connect to the channel and set up callback.
139 connection = client.connectClientReceiver(
140 name,
141 [this](std::unique_ptr<SegmentedMessageData> &data) {
142 // Add trace logging for the received message.
143 conn.getLogger().trace(
144 [this, &data](
145 std::string &subsystem, std::string &msg,
146 std::unique_ptr<std::map<std::string, std::any>> &details) {
147 subsystem = "cosim_read";
148 msg = "Received message from channel '" + name + "'";
149 details = std::make_unique<std::map<std::string, std::any>>();
150 MessageData flat = data->toMessageData();
151 (*details)["channel"] = name;
152 (*details)["data_size"] = flat.getSize();
153 (*details)["message_data"] = flat.toHex();
154 });
155
156 bool consumed = invokeCallback(data);
157
158 if (consumed) {
159 // Log the message consumption.
160 conn.getLogger().trace(
161 [this](
162 std::string &subsystem, std::string &msg,
163 std::unique_ptr<std::map<std::string, std::any>> &details) {
164 subsystem = "cosim_read";
165 msg = "Message from channel '" + name + "' consumed";
166 });
167 }
168
169 return consumed;
170 },
172 }
173
174 void disconnect() override {
175 conn.getLogger().debug("cosim_read", "Disconnecting channel " + name);
176 if (connection) {
177 connection->disconnect();
178 connection.reset();
179 }
181 }
182
183private:
184 AcceleratorConnection &conn;
185 RpcClient &client;
186 RpcClient::ChannelDesc desc;
187 std::string name;
188 std::unique_ptr<RpcClient::ReadChannelConnection> connection;
189};
190
191} // anonymous namespace
192
193//===----------------------------------------------------------------------===//
194// CosimAccelerator
195//===----------------------------------------------------------------------===//
196
197/// Parse the connection std::string and instantiate the accelerator. Support
198/// the traditional 'host:port' syntax and a path to 'cosim.cfg' which is output
199/// by the cosimulation when it starts (which is useful when it chooses its own
200/// port).
201std::unique_ptr<AcceleratorConnection>
202CosimAccelerator::connect(Context &ctxt, std::string connectionString) {
203 std::string portStr;
204 std::string host = "localhost";
205
206 size_t colon;
207 if ((colon = connectionString.find(':')) != std::string::npos) {
208 portStr = connectionString.substr(colon + 1);
209 host = connectionString.substr(0, colon);
210 } else if (connectionString.ends_with("cosim.cfg")) {
211 std::ifstream cfg(connectionString);
212 std::string line, key, value;
213
214 while (getline(cfg, line))
215 if ((colon = line.find(":")) != std::string::npos) {
216 key = line.substr(0, colon);
217 value = line.substr(colon + 1);
218 if (key == "port")
219 portStr = value;
220 else if (key == "host")
221 host = value;
222 }
223
224 if (portStr.size() == 0)
225 throw std::runtime_error("port line not found in file");
226 } else if (connectionString == "env") {
227 char *hostEnv = getenv("ESI_COSIM_HOST");
228 if (hostEnv)
229 host = hostEnv;
230 else
231 host = "localhost";
232 char *portEnv = getenv("ESI_COSIM_PORT");
233 if (portEnv)
234 portStr = portEnv;
235 else
236 throw std::runtime_error("ESI_COSIM_PORT environment variable not set");
237 } else {
238 throw std::runtime_error("Invalid connection std::string '" +
239 connectionString + "'");
240 }
241 uint16_t port = stoul(portStr);
242 auto conn = make_unique<CosimAccelerator>(ctxt, host, port);
243
244 // Using the MMIO manifest method is really only for internal debugging, so it
245 // doesn't need to be part of the connection string.
246 char *manifestMethod = getenv("ESI_COSIM_MANIFEST_MMIO");
247 if (manifestMethod != nullptr)
248 conn->setManifestMethod(ManifestMethod::MMIO);
249
250 return conn;
251}
252
253/// Construct and connect to a cosim server.
255 uint16_t port)
257 // Connect to the simulation.
258 rpcClient = std::make_unique<RpcClient>(getLogger(), hostname, port);
259}
265
266namespace {
267class CosimSysInfo : public SysInfo {
268#pragma pack(push, 1)
269 struct CycleInfo {
270 uint64_t freq;
271 uint64_t cycle;
272 };
273#pragma pack(pop)
274
275public:
276 CosimSysInfo(CosimAccelerator &conn, RpcClient *rpcClient)
277 : SysInfo(conn), rpcClient(rpcClient) {
278 // This is an optional interface; if the channels aren't present, we simply
279 // report no cycle/frequency information.
280 RpcClient::ChannelDesc argDesc, resultDesc;
281 if (!rpcClient->getChannelDesc("__cosim_cycle_count.arg", argDesc) ||
282 !rpcClient->getChannelDesc("__cosim_cycle_count.result", resultDesc))
283 return;
284
285 Context &ctxt = conn.getCtxt();
286 const esi::Type *i1Type = getType(ctxt, new BitsType("i1", 1));
287 const esi::Type *i64Type = getType(ctxt, new BitsType("i64", 64));
288 const esi::Type *resultType =
289 getType(ctxt, new StructType(resultDesc.type,
290 {{"cycle", i64Type}, {"freq", i64Type}}));
291
292 reqPort = std::make_unique<WriteCosimChannelPort>(
293 conn, *rpcClient, argDesc, i1Type, "__cosim_cycle_count.arg");
294 respPort = std::make_unique<ReadCosimChannelPort>(
295 conn, *rpcClient, resultDesc, resultType, "__cosim_cycle_count.result");
296 auto *bundleType =
297 new BundleType("cosimCycleCount",
298 {{"arg", BundleType::Direction::To, i1Type},
299 {"result", BundleType::Direction::From, resultType}});
300 func.reset(FuncService::Function::get(AppID("__cosim_cycle_count"),
301 bundleType, *reqPort, *respPort));
302 func->connect();
303 }
304
305 uint32_t getEsiVersion() const override { return rpcClient->getEsiVersion(); }
306 std::optional<uint64_t> getCycleCount() const override {
307 if (!func)
308 return std::nullopt;
309 return getCycleInfo().cycle;
310 }
311 std::optional<uint64_t> getCoreClockFrequency() const override {
312 if (!func)
313 return std::nullopt;
314 return getCycleInfo().freq;
315 }
316
317 std::vector<uint8_t> getCompressedManifest() const override {
318 return rpcClient->getCompressedManifest();
319 }
320
321private:
322 const esi::Type *getType(Context &ctxt, esi::Type *type) {
323 if (auto t = ctxt.getType(type->getID())) {
324 delete type;
325 return *t;
326 }
327 ctxt.registerType(type);
328 return type;
329 }
330
331 RpcClient *rpcClient;
332 std::unique_ptr<WriteCosimChannelPort> reqPort;
333 std::unique_ptr<ReadCosimChannelPort> respPort;
334 std::unique_ptr<FuncService::Function> func;
335
336 CycleInfo getCycleInfo() const {
337 MessageData arg({1}); // 1-bit trigger message
338 std::future<MessageData> result = func->call(arg);
339 result.wait();
340 MessageData respMsg = result.get();
341 return *respMsg.as<CycleInfo>();
342 }
343};
344} // namespace
345
346namespace {
347class CosimMMIO : public MMIO {
348public:
349 CosimMMIO(CosimAccelerator &conn, Context &ctxt, const AppIDPath &idPath,
350 RpcClient *rpcClient, const HWClientDetails &clients)
351 : MMIO(conn, idPath, clients) {
352 // We have to locate the channels ourselves since this service might be used
353 // to retrieve the manifest.
354 RpcClient::ChannelDesc cmdArg, cmdResp;
355 if (!rpcClient->getChannelDesc("__cosim_mmio_read_write.arg", cmdArg) ||
356 !rpcClient->getChannelDesc("__cosim_mmio_read_write.result", cmdResp))
357 throw std::runtime_error("Could not find MMIO channels");
358
359 const esi::Type *i64Type = getType(ctxt, new UIntType(cmdResp.type, 64));
360 const esi::Type *cmdType = getType(
361 ctxt, new StructType(cmdArg.type, {{"write", new BitsType("i1", 1)},
362 {"offset", new UIntType("ui32", 32)},
363 {"data", new BitsType("i64", 64)}}));
364
365 // Get ports, create the function, then connect to it.
366 cmdArgPort = std::make_unique<WriteCosimChannelPort>(
367 conn, *rpcClient, cmdArg, cmdType, "__cosim_mmio_read_write.arg");
368 cmdRespPort = std::make_unique<ReadCosimChannelPort>(
369 conn, *rpcClient, cmdResp, i64Type, "__cosim_mmio_read_write.result");
370 auto *bundleType = new BundleType(
371 "cosimMMIO", {{"arg", BundleType::Direction::To, cmdType},
372 {"result", BundleType::Direction::From, i64Type}});
373 cmdMMIO.reset(FuncService::Function::get(AppID("__cosim_mmio"), bundleType,
374 *cmdArgPort, *cmdRespPort));
375 cmdMMIO->connect();
376 }
377
378#pragma pack(push, 1)
379 struct MMIOCmd {
380 uint64_t data;
381 uint32_t offset;
382 bool write;
383 };
384#pragma pack(pop)
385
386 // Call the read function and wait for a response.
387 uint64_t read(uint32_t addr) const override {
388 MMIOCmd cmd{.data = 0, .offset = addr, .write = false};
389 auto arg = MessageData::from(cmd);
390 std::lock_guard<std::mutex> g(mmioCmdLock);
391 std::future<MessageData> result = cmdMMIO->call(arg);
392 result.wait();
393 uint64_t ret = *result.get().as<uint64_t>();
394 conn.getLogger().trace(
395 [addr, ret](std::string &subsystem, std::string &msg,
396 std::unique_ptr<std::map<std::string, std::any>> &details) {
397 subsystem = "cosim_mmio";
398 msg = "MMIO[0x" + toHex(addr) + "] = 0x" + toHex(ret);
399 });
400 return ret;
401 }
402
403 void write(uint32_t addr, uint64_t data) override {
404 conn.getLogger().trace(
405 [addr,
406 data](std::string &subsystem, std::string &msg,
407 std::unique_ptr<std::map<std::string, std::any>> &details) {
408 subsystem = "cosim_mmio";
409 msg = "MMIO[0x" + toHex(addr) + "] <- 0x" + toHex(data);
410 });
411 MMIOCmd cmd{.data = data, .offset = addr, .write = true};
412 auto arg = MessageData::from(cmd);
413 std::lock_guard<std::mutex> g(mmioCmdLock);
414 std::future<MessageData> result = cmdMMIO->call(arg);
415 result.wait();
416 }
417
418private:
419 const esi::Type *getType(Context &ctxt, esi::Type *type) {
420 if (auto t = ctxt.getType(type->getID())) {
421 delete type;
422 return *t;
423 }
424 ctxt.registerType(type);
425 return type;
426 }
427 std::unique_ptr<WriteCosimChannelPort> cmdArgPort;
428 std::unique_ptr<ReadCosimChannelPort> cmdRespPort;
429 std::unique_ptr<FuncService::Function> cmdMMIO;
430};
431
432#pragma pack(push, 1)
433struct HostMemReadReq {
434 uint8_t tag;
435 uint32_t length;
436 uint64_t address;
437};
438
439using HostMemWriteResp = uint8_t;
440#pragma pack(pop)
441
442// ESI lowers a parallel-window frame MSB-first (the first-declared field
443// occupies the highest bits) into a little-endian byte buffer; array elements
444// are packed least-index-first (element[0] in the low bits). The sub-byte
445// `last` / `data_size` fields make these frames bit-packed and not
446// byte-aligned, so -- to stay ABI-portable across toolchains (bit-field and
447// sub-byte `#pragma pack` layout is not guaranteed under MSVC) -- each frame is
448// a plain byte array whose bits are assembled/extracted explicitly with these
449// helpers. `bitOff` counts from the LSB (bit 0 of byte 0).
450void putBits(uint8_t *buf, size_t bitOff, size_t width, uint64_t val) {
451 for (size_t i = 0; i < width; ++i)
452 if ((val >> i) & 1ULL)
453 buf[(bitOff + i) >> 3] |= static_cast<uint8_t>(1u << ((bitOff + i) & 7));
454}
455uint64_t getBits(const uint8_t *buf, size_t bitOff, size_t width) {
456 uint64_t val = 0;
457 for (size_t i = 0; i < width; ++i)
458 if (buf[(bitOff + i) >> 3] & (1u << ((bitOff + i) & 7)))
459 val |= (1ULL << i);
460 return val;
461}
462
463const StructType &getHostMemPayload(const Type *type,
464 std::string_view direction) {
465 auto *payload = getUnwrappedAs<StructType>(type);
466 if (!payload)
467 throw std::runtime_error(
468 std::format("Invalid HostMem {} channel layout", direction));
469 return *payload;
470}
471
472size_t getHostMemDataBytes(const StructType &payload,
473 std::string_view direction) {
474 const auto &fields = payload.getFields();
475 auto data = std::find_if(fields.begin(), fields.end(), [](const auto &field) {
476 return field.first == "data";
477 });
478 if (data == fields.end() ||
479 !dynamic_cast<const BitVectorType *>(data->second))
480 throw std::runtime_error(
481 std::format("Invalid HostMem {} field: data", direction));
482 auto width = data->second->getBitWidth();
483 if (width < 8 || !std::has_single_bit(static_cast<uint64_t>(width)))
484 throw std::runtime_error(std::format(
485 "HostMem {} width must be a power of two and at least 8", direction));
486 return width / 8;
487}
488
489void validateHostMemFields(
490 const StructType &payload,
491 std::initializer_list<std::pair<std::string_view, size_t>> expected,
492 std::string_view direction) {
493 if (payload.getFields().size() != expected.size())
494 throw std::runtime_error(
495 std::format("Invalid HostMem {} channel layout", direction));
496 auto field = payload.getFields().begin();
497 for (auto [name, width] : expected) {
498 if (field->first != name ||
499 (!dynamic_cast<const BitVectorType *>(field->second) &&
500 !(width == 0 && dynamic_cast<const VoidType *>(field->second))) ||
501 field->second->getBitWidth() != static_cast<std::ptrdiff_t>(width))
502 throw std::runtime_error(
503 std::format("Invalid HostMem {} field: {}", direction, field->first));
504 ++field;
505 }
506}
507
508// One read response beat, packed MSB-first: tag | data | last. The data starts
509// at bit 1, and unused bytes in a partial final beat are zero-filled.
510class HostMemReadRespFrame {
511public:
512 HostMemReadRespFrame(uint8_t tag, const uint8_t *data, size_t validBytes,
513 size_t wordBytes, bool last)
514 : bytes(wordBytes + 2, 0) {
515 putBits(bytes.data(), 0, 1, last);
516 for (size_t i = 0; i < validBytes; ++i)
517 putBits(bytes.data(), 1 + i * 8, 8, data[i]);
518 putBits(bytes.data(), 1 + wordBytes * 8, 8, tag);
519 }
520
521 MessageData toMessage() const {
522 return MessageData(bytes.data(), bytes.size());
523 }
524
525private:
526 std::vector<uint8_t> bytes;
527};
528
529// Write-request frame: one bus beat of a burst write. The upstream write
530// request is packed MSB-first: address | tag | data | data_size | last.
531// `data_size` holds (valid_bytes - 1), with log2(bus bytes) bits.
532// The data width is discovered from the advertised channel type.
533class HostMemWriteReqFrame {
534public:
535 HostMemWriteReqFrame(const uint8_t *raw, size_t dataBytes, size_t sizeWidth)
536 : bytes(raw), sizeWidth(sizeWidth), dataOff(1 + sizeWidth),
537 tagOff(dataOff + dataBytes * 8), addrOff(tagOff + 8) {}
538
539 uint64_t address() const { return getBits(bytes, addrOff, 64); }
540 uint8_t tag() const { return getBits(bytes, tagOff, 8); }
541 uint8_t dataByte(size_t i) const {
542 return getBits(bytes, dataOff + 8 * i, 8);
543 }
544 // Number of valid data bytes in this beat (data_size holds valid_bytes - 1).
545 size_t validBytes() const { return getBits(bytes, 1, sizeWidth) + 1; }
546 bool last() const { return getBits(bytes, 0, 1) != 0; }
547
548private:
549 const uint8_t *bytes;
550 size_t sizeWidth, dataOff, tagOff, addrOff;
551};
552
553// PCIe caps a single memory read request at the Max_Read_Request_Size, whose
554// largest encoding (PCIe Gen 4 and earlier) is 4096 bytes, but root ports often
555// negotiate a smaller limit. Model a conservative 64-double-word (256-byte) cap
556// here, raised to one beat if the configured cosim word is larger.
557static constexpr uint32_t kPcieMaxReadRequestBytes = 64 * 4;
558
559class CosimHostMem : public HostMem {
560public:
561 CosimHostMem(AcceleratorConnection &acc, Context &ctxt, RpcClient *rpcClient)
562 : HostMem(acc), acc(acc), ctxt(ctxt), rpcClient(rpcClient) {}
563
564 void start() override {
565 // We have to locate the channels ourselves since this service might be used
566 // to retrieve the manifest.
567
568 if (writeRespPort)
569 return;
570
571 RpcClient::ChannelDesc writeArg, writeResp;
572 if (!rpcClient->getChannelDesc("__cosim_hostmem_write.arg", writeArg) ||
573 !rpcClient->getChannelDesc("__cosim_hostmem_write.result", writeResp))
574 throw std::runtime_error("Could not find HostMem write channels");
575
576 RpcClient::ChannelDesc readArg, readResp;
577 if (!rpcClient->getChannelDesc("__cosim_hostmem_read_req.data", readArg) ||
578 !rpcClient->getChannelDesc("__cosim_hostmem_read_resp.data", readResp))
579 throw std::runtime_error("Could not find HostMem read channels");
580
581 // HostMem may be started before the caller has parsed the manifest.
582 if (!ctxt.getType(writeArg.type) || !ctxt.getType(readResp.type))
583 Manifest manifest(ctxt, acc.getService<SysInfo>()->getJsonManifest());
584 auto channelType = [this](const RpcClient::ChannelDesc &desc) {
585 auto type = ctxt.getType(desc.type);
586 if (!type)
587 throw std::runtime_error("HostMem channel type not in manifest: " +
588 desc.type);
589 return *type;
590 };
591 const esi::Type *writeReqType = channelType(writeArg);
592 const auto &writePayload = getHostMemPayload(writeReqType, "write");
593 writeDataBytes = getHostMemDataBytes(writePayload, "write");
594 writeSizeWidth = std::countr_zero(writeDataBytes);
595 validateHostMemFields(writePayload,
596 {{"address", 64},
597 {"tag", 8},
598 {"data", writeDataBytes * 8},
599 {"data_size", writeSizeWidth},
600 {"last", 1}},
601 "write");
602 writeFrameBytes =
603 (64 + 8 + writeDataBytes * 8 + writeSizeWidth + 1 + 7) / 8;
604
605 // Setup the read side callback.
606 const esi::Type *readRespType = channelType(readResp);
607 const auto &readPayload = getHostMemPayload(readRespType, "read");
608 readDataBytes = getHostMemDataBytes(readPayload, "read");
609 validateHostMemFields(
610 readPayload, {{"tag", 8}, {"data", readDataBytes * 8}, {"last", 1}},
611 "read");
612 const esi::Type *readReqType = channelType(readArg);
613
614 // Get ports. Unfortunately, we can't model this as a callback since there
615 // will sometimes be multiple responses per request. Like PCIe read
616 // completions, responses are not flow controlled: the requester must
617 // already have room for them, so give them no in-flight limit.
618 readRespPort = std::make_unique<WriteCosimChannelPort>(
619 conn, *rpcClient, readResp, readRespType,
620 "__cosim_hostmem_read_resp.data", /*defaultMaxInFlight=*/0);
621 readRespPort->connect();
622 readReqPort = std::make_unique<ReadCosimChannelPort>(
623 conn, *rpcClient, readArg, readReqType,
624 "__cosim_hostmem_read_req.data");
625 readReqPort->connect(
626 [this](const MessageData &req) { return serviceRead(req); });
627
628 // Setup the write side callback.
629 const esi::Type *writeRespType = channelType(writeResp);
630
631 // Get ports, create the function, then connect to it. Write completions
632 // are unbounded for the same reason as read responses.
633 writeRespPort = std::make_unique<WriteCosimChannelPort>(
634 conn, *rpcClient, writeResp, writeRespType,
635 "__cosim_hostmem_write.result", /*defaultMaxInFlight=*/0);
636 writeReqPort = std::make_unique<ReadCosimChannelPort>(
637 conn, *rpcClient, writeArg, writeReqType, "__cosim_hostmem_write.arg");
638 auto *bundleType = new BundleType(
639 "cosimHostMem",
640 {{"arg", BundleType::Direction::To, writeReqType},
641 {"result", BundleType::Direction::From, writeRespType}});
642 write.reset(CallService::Callback::get(acc, AppID("__cosim_hostmem_write"),
643 bundleType, *writeRespPort,
644 *writeReqPort));
645 write->connect([this](const MessageData &req) { return serviceWrite(req); },
646 true);
647 }
648
649 // Service the read request as a callback. Simply reads the data from the
650 // location specified. TODO: check that the memory has been mapped.
651 bool serviceRead(const MessageData &reqBytes) {
652 const HostMemReadReq *req = reqBytes.as<HostMemReadReq>();
653 acc.getLogger().trace(
654 [&](std::string &subsystem, std::string &msg,
655 std::unique_ptr<std::map<std::string, std::any>> &details) {
656 subsystem = "hostmem";
657 msg = "Read request: addr=0x" + toHex(req->address) +
658 " len=" + std::to_string(req->length) +
659 " tag=" + std::to_string(req->tag);
660 });
661 // Send one response per bus word. Zero-length reads (e.g. void / zero-width
662 // types) indicates a bug in the hardware and we log an error, but we still
663 // send a single response.
664 const auto *dataPtr = reinterpret_cast<const uint8_t *>(req->address);
665 size_t numDataResps =
666 req->length == 0 ? 0 : 1 + (req->length - 1) / readDataBytes;
667 if (numDataResps == 0)
668 acc.getLogger().error(
669 "hostmem",
670 std::format("Read request with length=0 from addr=0x{} tag={}. "
671 "Reads of length 0 are not valid and indicate a bug "
672 "in the requester.",
673 toHex(req->address), req->tag));
674 size_t maxReadBytes =
675 std::max<size_t>(kPcieMaxReadRequestBytes, readDataBytes);
676 if (req->length > maxReadBytes)
677 acc.getLogger().error(
678 "hostmem",
679 std::format("Read request length={} from addr=0x{} tag={} exceeds "
680 "the cosim maximum read request size ({} bytes). The "
681 "requester must split reads larger than this into "
682 "multiple requests.",
683 req->length, toHex(req->address), req->tag,
684 maxReadBytes));
685 size_t numResps = std::max<size_t>(numDataResps, 1);
686 for (size_t i = 0; i < numResps; ++i) {
687 size_t offset = i * readDataBytes;
688 size_t validBytes = std::min<size_t>(req->length - offset, readDataBytes);
689 bool last = i + 1 == numResps;
690 HostMemReadRespFrame frame(req->tag,
691 validBytes ? dataPtr + offset : nullptr,
692 validBytes, readDataBytes, last);
693 acc.getLogger().trace(
694 [&](std::string &subsystem, std::string &msg,
695 std::unique_ptr<std::map<std::string, std::any>> &details) {
696 subsystem = "HostMem";
697 msg = std::format("Read result: valid_bytes={} tag={} last={:d}",
698 validBytes, req->tag, last);
699 });
700 readRespPort->write(frame.toMessage());
701 }
702 return true;
703 }
704
705 // Service a write request as a callback. Simply write the data to the
706 // location specified. TODO: check that the memory has been mapped.
707 MessageData serviceWrite(const MessageData &reqBytes) {
708 if (reqBytes.getSize() != writeFrameBytes)
709 throw std::runtime_error("HostMem write frame size mismatch. Size is " +
710 std::to_string(reqBytes.getSize()) +
711 ", expected " + std::to_string(writeFrameBytes) +
712 ".");
713 HostMemWriteReqFrame req(reqBytes.getBytes(), writeDataBytes,
714 writeSizeWidth);
715 acc.getLogger().trace(
716 [&](std::string &subsystem, std::string &msg,
717 std::unique_ptr<std::map<std::string, std::any>> &details) {
718 subsystem = "hostmem";
719 msg = "Write request: addr=0x" + toHex(req.address()) +
720 " valid_bytes=" + std::to_string(req.validBytes()) +
721 " tag=" + std::to_string(req.tag()) +
722 " last=" + std::to_string(req.last());
723 });
724 uint8_t *dataPtr = reinterpret_cast<uint8_t *>(req.address());
725 size_t validBytes = req.validBytes();
726 for (size_t i = 0; i < validBytes; ++i)
727 dataPtr[i] = req.dataByte(i);
728 HostMemWriteResp resp = req.tag();
729 return MessageData::from(resp);
730 }
731
732 struct CosimHostMemRegion : public HostMemRegion {
733 CosimHostMemRegion(std::size_t size) : ptr(malloc(size)), size(size) {
734 if (ptr)
735 memset(ptr, 0xFF, size);
736 }
737 virtual ~CosimHostMemRegion() { free(ptr); }
738 virtual void *getPtr() const override { return ptr; }
739 virtual std::size_t getSize() const override { return size; }
740
741 private:
742 void *ptr;
743 std::size_t size;
744 };
745
746 virtual std::unique_ptr<HostMemRegion>
747 allocate(std::size_t size, HostMem::Options opts) const override {
748 if (size == 0)
749 return nullptr;
750 std::unique_ptr<HostMemRegion> ret(new (std::nothrow)
751 CosimHostMemRegion(size));
752 if (!ret || !ret->getPtr())
753 return nullptr;
754 acc.getLogger().debug(
755 [&](std::string &subsystem, std::string &msg,
756 std::unique_ptr<std::map<std::string, std::any>> &details) {
757 subsystem = "HostMem";
758 msg = "Allocated host memory region at 0x" + toHex(ret->getPtr()) +
759 " of size " + std::to_string(size);
760 });
761 return ret;
762 }
763 virtual bool mapMemory(void *ptr, std::size_t size,
764 HostMem::Options opts) const override {
765 return true;
766 }
767 virtual void unmapMemory(void *ptr) const override {}
768
769private:
770 AcceleratorConnection &acc;
771 Context &ctxt;
772 RpcClient *rpcClient;
773 size_t readDataBytes = 0;
774 size_t writeDataBytes = 0;
775 size_t writeSizeWidth = 0;
776 size_t writeFrameBytes = 0;
777 std::unique_ptr<WriteCosimChannelPort> readRespPort;
778 std::unique_ptr<ReadCosimChannelPort> readReqPort;
779 std::unique_ptr<CallService::Callback> read;
780 std::unique_ptr<WriteCosimChannelPort> writeRespPort;
781 std::unique_ptr<ReadCosimChannelPort> writeReqPort;
782 std::unique_ptr<CallService::Callback> write;
783};
784} // namespace
785
786namespace esi::backends::cosim {
787/// Implement the magic cosim channel communication.
788class CosimEngine : public Engine {
789public:
791 const ServiceImplDetails &details, const HWClientDetails &clients)
792 : Engine(conn), conn(conn) {
793 // Compute our parents idPath path.
794 AppIDPath prefix = std::move(idPath);
795 if (prefix.size() > 0)
796 prefix.pop_back();
797
798 for (auto client : clients) {
799 AppIDPath fullClientPath = prefix + client.relPath;
800 std::map<std::string, std::string> channelAssignments;
801 for (auto assignment : client.channelAssignments)
802 if (assignment.second.type == "cosim")
803 channelAssignments[assignment.first] = std::any_cast<std::string>(
804 assignment.second.implOptions.at("name"));
805 clientChannelAssignments[fullClientPath] = std::move(channelAssignments);
806 }
807 }
808
809 std::unique_ptr<ChannelPort> createPort(AppIDPath idPath,
810 const std::string &channelName,
812 const Type *type) override;
813
814private:
816 std::map<AppIDPath, std::map<std::string, std::string>>
818};
819} // namespace esi::backends::cosim
820
821std::unique_ptr<ChannelPort>
822CosimEngine::createPort(AppIDPath idPath, const std::string &channelName,
823 BundleType::Direction dir, const Type *type) {
824
825 // Find the client details for the port at 'fullPath'.
826 auto f = clientChannelAssignments.find(idPath);
827 if (f == clientChannelAssignments.end())
828 throw std::runtime_error("Could not find port for '" + idPath.toStr() +
829 "." + channelName + "'");
830 const std::map<std::string, std::string> &channelAssignments = f->second;
831 auto cosimChannelNameIter = channelAssignments.find(channelName);
832 if (cosimChannelNameIter == channelAssignments.end())
833 throw std::runtime_error("Could not find channel '" + idPath.toStr() + "." +
834 channelName + "' in cosimulation");
835
836 // Get the endpoint, which may or may not exist. Construct the port.
837 // Everything is validated when the client calls 'connect()' on the port.
839 if (!conn.rpcClient->getChannelDesc(cosimChannelNameIter->second, chDesc))
840 throw std::runtime_error("Could not find channel '" + idPath.toStr() + "." +
841 channelName + "' in cosimulation");
842
843 std::unique_ptr<ChannelPort> port;
844 std::string fullChannelName = idPath.toStr() + "." + channelName;
845 if (BundlePort::isWrite(dir))
846 port = std::make_unique<WriteCosimChannelPort>(
847 conn, *conn.rpcClient, chDesc, type, fullChannelName);
848 else
849 port = std::make_unique<ReadCosimChannelPort>(conn, *conn.rpcClient, chDesc,
850 type, fullChannelName);
851 return port;
852}
853
854void CosimAccelerator::createEngine(const std::string &engineTypeName,
855 AppIDPath idPath,
856 const ServiceImplDetails &details,
857 const HWClientDetails &clients) {
858
859 std::unique_ptr<Engine> engine = nullptr;
860 if (engineTypeName == "cosim")
861 engine = std::make_unique<CosimEngine>(*this, idPath, details, clients);
862 else
863 engine = ::esi::registry::createEngine(*this, engineTypeName, idPath,
864 details, clients);
865 registerEngine(idPath, std::move(engine), clients);
866}
868 AppIDPath idPath, std::string implName,
869 const ServiceImplDetails &details,
870 const HWClientDetails &clients) {
871 if (svcType == typeid(services::MMIO)) {
872 return new CosimMMIO(*this, getCtxt(), idPath, rpcClient.get(), clients);
873 } else if (svcType == typeid(services::HostMem)) {
874 return new CosimHostMem(*this, getCtxt(), rpcClient.get());
875 } else if (svcType == typeid(SysInfo)) {
876 switch (manifestMethod) {
878 return new CosimSysInfo(*this, rpcClient.get());
881 }
882 }
883 return nullptr;
884}
885
889
#define REGISTER_ACCELERATOR(Name, TAccelerator)
static constexpr unsigned DefaultToClientMaxInFlight
Default maximum number of frames in flight (queued in the simulator or sent but not yet consumed by t...
Definition RpcClient.h:71
static constexpr unsigned DefaultToServerMaxInFlight
Default maximum number of frames sent to a server-bound channel which the simulation has not yet cons...
Definition RpcClient.h:68
services::Service Service
ServiceClass * getService(AppIDPath id={}, std::string implName={}, ServiceImplDetails details={}, HWClientDetails clients={})
Get a typed reference to a particular service type.
Context & getCtxt() const
void clearOwnedObjects()
Drop accelerator-owned objects before a derived backend destroys resources that those objects may ref...
Context & ctxt
ESI accelerator context.
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...
virtual void disconnect()
Disconnect from the accelerator cleanly.
Logger & getLogger() const
AcceleratorConnection(Context &ctxt)
std::string toStr() const
Definition Manifest.cpp:816
Bits are just an array of bits.
Definition Types.h:224
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:641
Bundles represent a collection of channels.
Definition Types.h:104
AcceleratorConnections, Accelerators, and Manifests must all share a context.
Definition Context.h:34
std::optional< const Type * > getType(Type::ID id) const
Resolve a type id to the type.
Definition Context.h:50
void registerType(Type *type)
Register a type with the context. Takes ownership of the pointer type.
Definition Context.cpp:33
Engine(AcceleratorConnection &conn)
Definition Engines.h:44
const uint8_t * getBytes() const
Definition Common.h:208
const T * as() const
Cast to a type.
Definition Common.h:232
size_t getSize() const
Get the size of the data in bytes.
Definition Common.h:222
static MessageData from(T &t)
Cast from a type to its raw bytes.
Definition Common.h:242
A ChannelPort which reads data from the accelerator.
Definition Ports.h:479
virtual void disconnect() override
Disconnect the channel.
Definition Ports.cpp:70
Structs are an ordered collection of fields, each with a name and a type.
Definition Types.h:264
const FieldVector & getFields() const
Definition Types.h:272
Root class of the ESI type system.
Definition Types.h:36
ID getID() const
Definition Types.h:42
A ChannelPort which sends data to the accelerator.
Definition Ports.h:308
Connect to an ESI simulation.
Definition Cosim.h:36
void createEngine(const std::string &engineTypeName, AppIDPath idPath, const ServiceImplDetails &details, const HWClientDetails &clients) override
Create a new engine for channel communication with the accelerator.
Definition Cosim.cpp:854
std::unique_ptr< RpcClient > rpcClient
Definition Cosim.h:65
void setManifestMethod(ManifestMethod method)
Definition Cosim.cpp:886
static std::unique_ptr< AcceleratorConnection > connect(Context &, std::string connectionString)
Parse the connection std::string and instantiate the accelerator.
Definition Cosim.cpp:202
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:867
CosimAccelerator(Context &, std::string hostname, uint16_t port)
Construct and connect to a cosim server.
Definition Cosim.cpp:254
std::set< std::unique_ptr< ChannelPort > > channels
Definition Cosim.h:69
CosimEngine(CosimAccelerator &conn, AppIDPath idPath, const ServiceImplDetails &details, const HWClientDetails &clients)
Definition Cosim.cpp:790
std::map< AppIDPath, std::map< std::string, std::string > > clientChannelAssignments
Definition Cosim.cpp:817
std::unique_ptr< ChannelPort > createPort(AppIDPath idPath, const std::string &channelName, BundleType::Direction dir, const Type *type) override
Each engine needs to know how to create a ports.
Definition Cosim.cpp:822
A client for the cosim RPC server.
Definition RpcClient.h:34
std::vector< uint8_t > getCompressedManifest() const
Get the compressed manifest from the server.
uint32_t getEsiVersion() const
Get the ESI version from the manifest.
bool getChannelDesc(const std::string &channelName, ChannelDesc &desc) const
Get the channel description for a channel name.
static Callback * get(AcceleratorConnection &acc, AppID id, const BundleType *type, WriteChannelPort &result, ReadChannelPort &arg)
Definition Services.cpp:357
static Function * get(AppID id, BundleType *type, WriteChannelPort &arg, ReadChannelPort &result)
Definition Services.cpp:308
Implement the SysInfo API for a standard MMIO protocol.
Definition Services.h:213
const std::type_info & Type
Definition Services.h:61
Information about the Accelerator system.
Definition Services.h:113
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:562
Definition esi.py:1
std::string toHex(void *val)
Definition Common.cpp:38
std::vector< HWClientDetail > HWClientDetails
Definition Common.h:108
std::map< std::string, std::any > ServiceImplDetails
Definition Common.h:109
const T * getUnwrappedAs(const Type *type)
Unwrap aliases and channels, then cast the payload to T.
Definition Types.h:203
write(addr, data)
Definition xrt_cosim.py:30
read(addr)
Definition xrt_cosim.py:23
Description of a channel from the server.
Definition RpcClient.h:53
std::optional< unsigned > bufferSize
The buffer size is optional and should be considered merely a hint.
Definition Ports.h:124
Description of a channel from the server.
Definition RpcClient.h:53