Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions lib/Dialect/ESI/runtime/cpp/include/esi/Accelerator.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ constexpr uint32_t ManifestPtrOffset = 0x10;
constexpr uint32_t CycleCountOffset = 0x20;
constexpr uint32_t CoreFreqOffset = 0x28;

/// Magic value which, when written to MMIO offset `ResetRequestOffset`,
/// requests a design reset. Keep in sync with 'ResetMagicNumber' in the PyCDE
/// BSP (python/esiaccel/bsp/common.py).
constexpr uint64_t ResetMagicNumber = 0x00000E510000B007;
/// Offset into the (global) MMIO space at which to request a design reset.
constexpr uint32_t ResetRequestOffset = 0x38;

//===----------------------------------------------------------------------===//
// Accelerator design hierarchy root.
//===----------------------------------------------------------------------===//
Expand Down Expand Up @@ -96,6 +103,11 @@ class AcceleratorConnection {
/// Disconnect from the accelerator cleanly.
virtual void disconnect();

/// Request a reset of the accelerator design. Returns true if the reset was
/// successfully requested, false if it could not be performed for any reason
/// -- most commonly because the backend (BSP) does not support resets.
virtual bool reset();

/// Return a pointer to the accelerator 'service' thread (or threads). If the
/// thread(s) are not running, they will be started when this method is
/// called. `std::thread` is used. If users don't want the runtime to spin up
Expand Down
3 changes: 3 additions & 0 deletions lib/Dialect/ESI/runtime/cpp/include/esi/backends/Trace.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ class TraceAccelerator : public esi::AcceleratorConnection {
static std::unique_ptr<AcceleratorConnection>
connect(Context &, std::string connectionString);

/// Resets are not supported.
bool reset() override { return false; }

/// Internal implementation.
struct Impl;
Impl &getImpl();
Expand Down
3 changes: 3 additions & 0 deletions lib/Dialect/ESI/runtime/cpp/include/esi/backends/Xrt.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class XrtAccelerator : public esi::AcceleratorConnection {
static std::unique_ptr<AcceleratorConnection>
connect(Context &, std::string connectionString);

/// Resets are not supported.
bool reset() override { return false; }

protected:
virtual Service *createService(Service::Type service, AppIDPath path,
std::string implName,
Expand Down
17 changes: 17 additions & 0 deletions lib/Dialect/ESI/runtime/cpp/lib/Accelerator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ AcceleratorConnection::AcceleratorConnection(Context &ctxt)
: ctxt(ctxt), serviceThread(nullptr) {}
AcceleratorConnection::~AcceleratorConnection() { disconnect(); }

// Request a design reset by writing the reset magic number to a particular
// MMIO offset.
bool AcceleratorConnection::reset() {
services::MMIO *mmio = getService<services::MMIO>();
if (!mmio)
return false;
// The MMIO write is a virtual backend interface which may throw.
try {
mmio->write(ResetRequestOffset, ResetMagicNumber);
} catch (const std::exception &e) {
getLogger().error("reset",
std::string("failed to request reset: ") + e.what());
return false;
}
return true;
}
Comment thread
teqdruid marked this conversation as resolved.

AcceleratorServiceThread *AcceleratorConnection::getServiceThread() {
if (!serviceThread)
serviceThread = std::make_unique<AcceleratorServiceThread>();
Expand Down
67 changes: 67 additions & 0 deletions lib/Dialect/ESI/runtime/cpp/tools/esitester.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ static void autoSerialCoordTranslateTest(AcceleratorConnection *, Accelerator *,
uint32_t numCoords);
static void channelTest(AcceleratorConnection *, Accelerator *,
uint32_t iterations);
static void resetTest(AcceleratorConnection *, Accelerator *);

// Default widths and default widths string for CLI help text.
constexpr std::array<uint32_t, 5> defaultWidths = {32, 64, 128, 256, 512};
Expand Down Expand Up @@ -323,6 +324,9 @@ int main(int argc, const char *argv[]) {
channelTestSub->add_option("-i,--iters", channelIters,
"Number of loopback iterations (default 10)");

CLI::App *resetSub = cli.add_subcommand(
"reset", "Test the design reset feature (telemetry clears after reset)");

if (int rc = cli.esiParse(argc, argv))
return rc;
if (!cli.get_help_ptr()->empty())
Expand Down Expand Up @@ -371,6 +375,8 @@ int main(int argc, const char *argv[]) {
autoCoordNumItems);
} else if (*channelTestSub) {
channelTest(acc, accel, channelIters);
} else if (*resetSub) {
resetTest(acc, accel);
}

acc->disconnect();
Expand Down Expand Up @@ -1143,6 +1149,67 @@ static void loopbackAddTest(AcceleratorConnection *conn, Accelerator *accel,
}
}

// Exercise the design reset feature using existing telemetry. Run a hostmem
// write operation on the 'writemem' module, which increments its
// 'addrCmdResponses' telemetry counter. Confirm the telemetry advanced,
// request a design reset, then confirm the telemetry has been cleared back to
// zero (the counters live in the user design which the reset clears).
static void resetTest(AcceleratorConnection *conn, Accelerator *accel) {
Logger &logger = conn->getLogger();
constexpr uint32_t width = 64;

// Run an existing test that increments telemetry. The hostmem write test
// bumps the writemem module's 'addrCmdResponses' counter.
hostmemTest(conn, accel, {width}, /*write=*/true, /*read=*/false);

// Grab the writemem module's response telemetry counter to observe the reset.
auto writeMemChildIter = accel->getChildren().find(AppID("writemem", width));
if (writeMemChildIter == accel->getChildren().end())
throw std::runtime_error("Reset test: no 'writemem' child");
auto &ports = writeMemChildIter->second->getPorts();
auto respIter = ports.find(AppID("addrCmdResponses"));
if (respIter == ports.end())
throw std::runtime_error(
"Reset test: no 'addrCmdResponses' telemetry port");
auto *respMetric =
respIter->second.getAs<services::TelemetryService::Metric>();
if (!respMetric)
throw std::runtime_error("Reset test: 'addrCmdResponses' not telemetry");
respMetric->connect();

uint64_t before = respMetric->readInt();
std::cout << "[reset] telemetry addrCmdResponses before reset = " << before
<< std::endl;
if (before == 0)
throw std::runtime_error(
"Reset test: telemetry was not incremented by the hostmem write");

// Request a design reset.
logger.info("esitester", "Requesting design reset");
if (!conn->reset())
throw std::runtime_error("Reset test: reset() reported failure");
std::cout << "[reset] reset requested" << std::endl;

// The reset is asserted a fixed number of cycles after the request (to let
// in-flight transactions drain), so poll the telemetry until it clears.
uint64_t after = before;
constexpr int maxPolls = 1000000;
for (int polls = 0; polls < maxPolls; ++polls) {
after = respMetric->readInt();
if (after == 0)
break;
std::this_thread::sleep_for(std::chrono::microseconds(1));
}
std::cout << "[reset] telemetry addrCmdResponses after reset = " << after
<< std::endl;
if (after != 0)
throw std::runtime_error(
"Reset test: telemetry was not cleared by the reset (got " +
std::to_string(after) + ")");

std::cout << "Reset test passed" << std::endl;
}

static void aggregateHostmemBandwidthTest(AcceleratorConnection *conn,
Accelerator *acc, uint32_t width,
uint32_t xferCount, bool read,
Expand Down
147 changes: 129 additions & 18 deletions lib/Dialect/ESI/runtime/python/esiaccel/bsp/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@
IndirectionMagicNumber = 0x312bf0cc_E5100E51 # random + ESI__ESI
IndirectionVersionNumber = 0 # Version 0: format subject to change

# Magic value which, when written by the host to header slot 7, requests a
# design reset. Keep in sync with 'ResetMagicNumber' in the runtime
# (cpp/include/esi/Accelerator.h). This magic number guards against "write
# spraying" which other devices have been know to do on boot.
ResetMagicNumber = 0x00000E510000B007
# Number of cycles to wait after a reset is requested before asserting it. This
# gives in-flight transactions time to drain.
ResetCycles = 8192


class ESI_Manifest_ROM(Module):
"""Module which will be created later by CIRCT which will contain the
Expand Down Expand Up @@ -66,17 +75,40 @@ class HeaderMMIO(Module):

clk = Clock()
rst = Reset()
read = Input(esi.MMIO.read.type)
read = Input(esi.MMIO.read_write.type)
# Asserted for one cycle when the host writes the reset magic number to
# header slot 7. Propagates up to the BSP which performs the actual reset.
reset_request = Output(Bits(1))

@generator
def build(ports):
clk = ports.clk
rst = ports.rst
data_chan_wire = Wire(Channel(esi.MMIODataType))
input_bundles = ports.read.unpack(data=data_chan_wire)
address_chan = input_bundles['offset']

address_ready = Wire(Bits(1))
address, address_valid = address_chan.unwrap(address_ready)
address_words = address.as_bits()[3:] # Lop off the lower three bits.
cmd_chan = input_bundles['cmd']

# Two-stage half-throughput pipeline: stage 1 captures the incoming
# command, stage 2 holds the looked-up response. Each stage carries its
# own occupancy bit.
cmd_ready = Wire(Bits(1))
data_chan_ready = Wire(Bits(1))
s1_to_s2_xact = Wire(Bits(1))
cmd_raw, cmd_valid = cmd_chan.unwrap(cmd_ready)

# Stage 1: command capture register and occupancy bit.
s1_load = cmd_valid & cmd_ready
cmd = cmd_raw.reg(clk, rst, ce=s1_load, name="cmd")
s1_valid = ControlReg(clk,
rst,
asserts=[s1_load],
resets=[s1_to_s2_xact],
name="s1_valid")
# Accept a new command when stage 1 is empty or is draining into stage 2.
cmd_ready.assign(~s1_valid)

address_words = cmd.offset.as_bits()[3:] # Lop off the lower three bits.
slot = address_words[:3]

cycles = Counter(64)(clk=ports.clk,
rst=ports.rst,
Expand All @@ -96,18 +128,39 @@ def build(ports):
0, # Reserved for future use.
cycles.out.as_bits(), # Cycle counter.
core_freq, # Core frequency, if known.
0,
0, # Slot 7: write the reset magic number here to request a reset.
])
header.name = "header"
header_response_valid = address_valid # Zero latency read.
# Select the appropriate header index.
header_out = header[address_words[:3]]
header_out.name = "header_out"

# Stage 2: registered response value and its occupancy bit.
s2_valid = Wire(Bits(1))
s2_xact = s2_valid & data_chan_ready
# Stage 1 advances into stage 2 only when stage 2 is empty.
s1_to_s2_xact.assign((s1_valid & ~s2_valid).as_bits())

header_out = header[slot].reg(clk=clk,
rst=rst,
ce=s1_to_s2_xact,
name="header_out")
s2_valid.assign(
ControlReg(clk,
rst,
asserts=[s1_to_s2_xact],
resets=[s2_xact],
name="header_out_valid"))
# Wrap the response.
data_chan, data_chan_ready = Channel(esi.MMIODataType).wrap(
header_out, header_response_valid)
data_chan, data_chan_ready_sig = Channel(esi.MMIODataType).wrap(
header_out, s2_valid)
data_chan_wire.assign(data_chan)
address_ready.assign(data_chan_ready)
data_chan_ready.assign(data_chan_ready_sig)

# Detect a write of the reset magic number to slot 7. Register the request
# so it is a clean one-cycle pulse, asserted as the command advances into
# the response stage. 'DesignResetController' latches it, so a single-cycle
# pulse is sufficient to trigger the reset.
reset_detect = (cmd.write & (slot == Bits(3)(7)) &
(cmd.data == Bits(64)(ResetMagicNumber))).as_bits()
ports.reset_request = (reset_detect & s1_to_s2_xact).as_bits()

return HeaderMMIO

Expand Down Expand Up @@ -316,6 +369,56 @@ def get_out(self, index: int) -> ChannelSignal:
return ChannelDemuxTree


@modparams
def DesignResetController(
delay_cycles: int) -> type["DesignResetControllerImpl"]:
"""Counts `delay_cycles` clock cycles after a reset request is observed, then
asserts `design_reset` for one cycle. This module must be driven by the
*external* reset only (not the reset it generates) so that the countdown is
not disturbed by the reset it produces.

`reset_pending` is asserted from the moment a reset is requested until it
fires. It is intended to be used to quiesce the design (e.g. stop accepting
new transactions) so that nothing is in flight when the reset is asserted."""

if delay_cycles < 1:
raise ValueError("'delay_cycles' must be at least 1.")

counter_width = max(clog2(delay_cycles), 1)

Comment thread
teqdruid marked this conversation as resolved.
class DesignResetControllerImpl(Module):
clk = Clock()
rst = Reset()
reset_request = Input(Bits(1))
design_reset = Output(Bits(1))
# High from the cycle a reset is requested until it fires. Use this to stop
# accepting new work so in-flight transactions can drain before the reset.
reset_pending = Output(Bits(1))

@generator
def build(ports):
fire = Wire(Bits(1))
# Latch that a reset has been requested until we fire the reset.
pending = ControlReg(clk=ports.clk,
rst=ports.rst,
asserts=[ports.reset_request],
resets=[fire],
name="reset_pending")
# Count cycles while a reset is pending.
count = Counter(counter_width)(clk=ports.clk,
rst=ports.rst,
clear=(fire | ~pending).as_bits(),
increment=pending,
instance_name="reset_delay_counter")
fire.assign(
(pending &
(count.out == UInt(counter_width)(delay_cycles - 1))).as_bits())
ports.design_reset = fire
ports.reset_pending = pending

return DesignResetControllerImpl


class ChannelMMIO(esi.ServiceImplementation):
"""MMIO service implementation with MMIO bundle interfaces. Should be
relatively easy to adapt to physical interfaces by wrapping the wires to
Expand Down Expand Up @@ -351,6 +454,10 @@ class ChannelMMIO(esi.ServiceImplementation):

cmd = Input(esi.MMIO.read_write.type)

# Asserted for one cycle when the host requests a design reset via an MMIO
# write to the header. Propagates up to the BSP which performs the reset.
reset_request = Output(Bits(1))

# Amount of register space each client gets. This is a GIANT HACK and needs to
# be replaced by parameterizable services.
# TODO: make the amount of register space each client gets a parameter.
Expand Down Expand Up @@ -406,11 +513,11 @@ def build_read(ports, manifest_loc: int, table: Dict[int, AssignableSignal]):

# Instantiate the header and manifest ROM. Fill in the read_table with
# bundle wires to be assigned identically to the other MMIO clients.
header_bundle_wire = Wire(esi.MMIO.read.type)
header_bundle_wire = Wire(esi.MMIO.read_write.type)
table[0] = header_bundle_wire
HeaderMMIO(manifest_loc)(clk=ports.clk,
rst=ports.rst,
read=header_bundle_wire)
header = HeaderMMIO(manifest_loc)(clk=ports.clk,
rst=ports.rst,
read=header_bundle_wire)

mani_bundle_wire = Wire(esi.MMIO.read.type)
table[manifest_loc] = mani_bundle_wire
Expand Down Expand Up @@ -464,6 +571,10 @@ def build_read(ports, manifest_loc: int, table: Dict[int, AssignableSignal]):
resp_channel = esi.ChannelMux(client_data_channels)
data_resp_channel.assign(resp_channel)

# The header surfaces a reset request when the host writes the reset magic
# number to slot 7. Propagate it up to the caller (the BSP).
ports.reset_request = header.reset_request

@staticmethod
def build_addr_read(read_addr_chan: ChannelSignal, num_clients: int,
manifest_loc: int) -> Tuple[BitsSignal, ChannelSignal]:
Expand Down
Loading
Loading