Skip to content

Commit 73a1b32

Browse files
authored
[ESI][Runtime] Support for multi-burst lists (#10784)
When a list cannot be contained in one burst, it needs to get split up into more than one burst. This was supported in the read direction, but not in the write direction. Add an integration test for the read direction and support for the write direction. Assisted-by: Github-Copilot:Claude Opus 4.8
1 parent bb4d4f9 commit 73a1b32

6 files changed

Lines changed: 438 additions & 22 deletions

File tree

lib/Dialect/ESI/runtime/python/esiaccel/codegen.py

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2145,6 +2145,14 @@ def _emit_window(self, hdr: TextIO, window_type: types.WindowType) -> None:
21452145
info["header_pad_bytes"],
21462146
count_type_synth)
21472147

2148+
# Largest list length encodable in a single burst's count field. Lists
2149+
# longer than this are split across multiple header/data bursts (the
2150+
# read-side `SerialListTypeDeserializer` reassembles them), each burst
2151+
# carrying at most this many data frames. `count_width` is <= 64 (the
2152+
# count field's storage type tops out at 64 bits), so this literal always
2153+
# fits in a `size_t`.
2154+
max_batch_val = (1 << info["count_width"]) - 1
2155+
21482156
hdr.write(
21492157
f"struct {info['window_name']} : public esi::SegmentedMessageData {{\n")
21502158
hdr.write("public:\n")
@@ -2156,26 +2164,43 @@ def _emit_window(self, hdr: TextIO, window_type: types.WindowType) -> None:
21562164
self._emit_window_frame(hdr, "header_frame", info["frame_bytes"],
21572165
header_layout)
21582166
hdr.write("\n")
2159-
hdr.write(" header_frame header{};\n")
2167+
hdr.write(" // One header per burst (bursts 2..N are continuations whose\n"
2168+
" // only meaningful field is the count); the data frames of\n"
2169+
" // every burst are stored contiguously in `data_frames` and\n"
2170+
" // sliced per burst by `segment()`.\n")
2171+
hdr.write(" std::vector<header_frame> headers;\n")
21602172
hdr.write(" std::vector<data_frame> data_frames;\n")
21612173
hdr.write(" header_frame footer{};\n\n")
2174+
hdr.write(" // Largest list length encodable in one burst's count field.\n"
2175+
" // Longer lists are chunked across multiple bursts.\n")
2176+
hdr.write(f" static constexpr size_t _kMaxBatch = {max_batch_val}ULL;\n\n")
21622177
hdr.write(f" void construct({frame_ctor_signature}) {{\n")
21632178
hdr.write(" if (frames.empty())\n")
21642179
hdr.write(
21652180
f" throw std::invalid_argument(\"{info['window_name']}: bulk windowed lists cannot be empty\");\n"
21662181
)
2182+
hdr.write(" data_frames = std::move(frames);\n")
2183+
hdr.write(" // Split the list into bursts no larger than the count\n"
2184+
" // field can encode. The read side reassembles the bursts\n"
2185+
" // back into a single list.\n")
2186+
hdr.write(" const size_t total = data_frames.size();\n")
21672187
hdr.write(
2168-
" if (frames.size() > std::numeric_limits<count_type>::max())\n")
2169-
hdr.write(
2170-
f" throw std::invalid_argument(\"{info['window_name']}: list too large for encoded count\");\n"
2188+
" const size_t numBatches = (total + _kMaxBatch - 1) / _kMaxBatch;\n"
21712189
)
2190+
hdr.write(" headers.assign(numBatches, header_frame{});\n")
2191+
hdr.write(" for (size_t b = 0; b < numBatches; ++b) {\n")
21722192
hdr.write(
2173-
f" header.{info['count_field_name']}(static_cast<count_type>(frames.size()));\n"
2193+
" const size_t batchSize =\n"
2194+
" std::min<size_t>(_kMaxBatch, total - b * _kMaxBatch);\n")
2195+
hdr.write(
2196+
f" headers[b].{info['count_field_name']}(static_cast<count_type>(batchSize));\n"
21742197
)
2198+
hdr.write(" }\n")
2199+
hdr.write(
2200+
" // Only the first burst's header carries the static fields.\n")
21752201
for name, _ in info["ctor_params"]:
2176-
hdr.write(f" header.{name}({name});\n")
2202+
hdr.write(f" headers.front().{name}({name});\n")
21772203
hdr.write(f" footer.{info['count_field_name']}(0);\n")
2178-
hdr.write(" data_frames = std::move(frames);\n")
21792204
hdr.write(" }\n\n")
21802205
hdr.write("public:\n")
21812206
hdr.write(f" {info['window_name']}({frame_ctor_signature}) {{\n")
@@ -2190,24 +2215,30 @@ def _emit_window(self, hdr: TextIO, window_type: types.WindowType) -> None:
21902215
hdr.write(" }\n")
21912216
hdr.write(f" construct({helper_call});\n")
21922217
hdr.write(" }\n\n")
2193-
hdr.write(" size_t numSegments() const override { return 3; }\n")
2218+
hdr.write(" size_t numSegments() const override {\n"
2219+
" return 2 * headers.size() + 1;\n"
2220+
" }\n")
21942221
hdr.write(" esi::Segment segment(size_t idx) const override {\n")
2195-
hdr.write(" if (idx == 0)\n")
2222+
hdr.write(" const size_t footerIdx = 2 * headers.size();\n")
2223+
hdr.write(" if (idx == footerIdx)\n")
21962224
hdr.write(
2197-
" return {reinterpret_cast<const uint8_t *>(&header), sizeof(header)};\n"
2225+
" return {reinterpret_cast<const uint8_t *>(&footer), sizeof(footer)};\n"
21982226
)
2199-
hdr.write(" if (idx == 1)\n")
2227+
hdr.write(" if (idx > footerIdx)\n")
22002228
hdr.write(
2201-
" return {reinterpret_cast<const uint8_t *>(data_frames.data()),\n"
2229+
f" throw std::out_of_range(\"{info['window_name']}: invalid segment index\");\n"
22022230
)
2203-
hdr.write(" data_frames.size() * sizeof(data_frame)};\n")
2204-
hdr.write(" if (idx == 2)\n")
2231+
hdr.write(" const size_t b = idx / 2;\n")
2232+
hdr.write(" if (idx % 2 == 0)\n")
2233+
hdr.write(" return {reinterpret_cast<const uint8_t *>(&headers[b]),\n"
2234+
" sizeof(header_frame)};\n")
2235+
hdr.write(" const size_t start = b * _kMaxBatch;\n")
22052236
hdr.write(
2206-
" return {reinterpret_cast<const uint8_t *>(&footer), sizeof(footer)};\n"
2207-
)
2237+
" const size_t batchSize =\n"
2238+
" std::min<size_t>(_kMaxBatch, data_frames.size() - start);\n")
22082239
hdr.write(
2209-
f" throw std::out_of_range(\"{info['window_name']}: invalid segment index\");\n"
2210-
)
2240+
" return {reinterpret_cast<const uint8_t *>(data_frames.data() + start),\n"
2241+
" batchSize * sizeof(data_frame)};\n")
22112242
hdr.write(" }\n\n")
22122243
hdr.write(
22132244
f" static constexpr std::string_view _ESI_ID = {self._cpp_string_literal(self._unwrap_aliases(window_type.into_type).id)};\n"
@@ -2239,9 +2270,11 @@ def _emit_window_data_accessors(self, hdr: TextIO, info) -> None:
22392270
cpp = self._cpp_type(field_type)
22402271
# The header-frame accessor returns by value (it pulls bytes out of
22412272
# the raw buffer), so this is also returned by value regardless of
2242-
# whether the underlying type is a scalar or an aggregate.
2273+
# whether the underlying type is a scalar or an aggregate. The static
2274+
# fields live on the first burst's header.
22432275
hdr.write(
2244-
f" {cpp} {field_name}() const {{ return header.{field_name}(); }}\n")
2276+
f" {cpp} {field_name}() const {{ return headers.front().{field_name}(); }}\n"
2277+
)
22452278
hdr.write(
22462279
f" size_t {list_field_name}_count() const {{ return data_frames.size(); }}\n"
22472280
)

lib/Dialect/ESI/runtime/tests/integration/hw/test_codegen.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,85 @@ def construct(ports):
735735
esi.ChannelService.to_host(AppID("data"), p2s.serial_out)
736736

737737

738+
# Multi-burst read: the list is longer than the serial encoder's data FIFO, so
739+
# `ListWindowToSerial` emits it as several header/data bursts (via its
740+
# split-on-full path) terminated by a single count==0 footer, rather than one
741+
# big burst. The host-side `SerialListTypeDeserializer` must stitch those
742+
# bursts back into a single list. The count field stays 16 bits (byte-aligned,
743+
# header fills the frame), so this exercises the multi-burst *reassembly*
744+
# without depending on the sub-byte frame layout the C++ facade codegen does
745+
# not yet model.
746+
_MULTIBURST_READ_TAG = 0xF00D
747+
_MULTIBURST_READ_ITEMS = [0x1000 + i for i in range(10)]
748+
_MULTIBURST_READ_FIFO_DEPTH = 4
749+
750+
751+
class ChannelMultiBurstListRead(Module):
752+
"""To-host channel that emits a list split across multiple serial bursts.
753+
754+
Same shape as `ChannelWindowedListRead`, but the ten-element list exceeds
755+
the serial encoder's data FIFO (depth 4), so `ListWindowToSerial` emits the
756+
list as several header/data bursts (4 + 4 + 2) followed by a single count==0
757+
footer. Verifies that the host-side `SerialListTypeDeserializer` reassembles
758+
a multi-burst transfer back into one list. A write to offset ``0x10`` of the
759+
``trigger`` MMIO region arms one transfer.
760+
"""
761+
762+
clk = Clock()
763+
rst = Reset()
764+
765+
@generator
766+
def construct(ports):
767+
clk = ports.clk
768+
rst = ports.rst
769+
770+
# MMIO trigger: any write to offset 0x10 arms one transfer.
771+
trigger_bundle = esi.MMIO.read_write(appid=AppID("trigger"))
772+
resp_chan = Wire(Channel(Bits(64)))
773+
cmd_chan = trigger_bundle.unpack(data=resp_chan)["cmd"]
774+
cmd_xact, cmd = cmd_chan.snoop_xact()
775+
resp_chan.assign(cmd_chan.transform(lambda c: Bits(64)(c.data)))
776+
trigger_xact = cmd_xact & (cmd.offset == UInt(32)(0x10))
777+
778+
n_items = len(_MULTIBURST_READ_ITEMS)
779+
burst_end = Wire(Bits(1))
780+
781+
# ``armed`` is high for the duration of one list: set on a trigger write,
782+
# cleared on the cycle the final item handshakes. The producer streams all
783+
# ten items with a single ``last`` on item 9 -- the split into bursts
784+
# happens entirely inside `ListWindowToSerial`.
785+
armed = ControlReg(clk, rst, asserts=[trigger_xact], resets=[burst_end])
786+
787+
par_ready = Wire(Bits(1))
788+
handshake = armed & par_ready
789+
idx_counter = Counter(4)(clk=clk,
790+
rst=rst,
791+
clear=burst_end,
792+
increment=handshake,
793+
instance_name="multiburst_read_idx")
794+
idx = idx_counter.out
795+
last_bit = (idx == UInt(4)(n_items - 1))
796+
burst_end.assign(handshake & last_bit)
797+
item_value = Array(Bits(32), n_items)(_MULTIBURST_READ_ITEMS)[idx.as_bits()]
798+
799+
parallel_window_type = Window.default_of(_window_probe_struct)
800+
par_struct = parallel_window_type.lowered_type({
801+
"tag": Bits(16)(_MULTIBURST_READ_TAG),
802+
"items": item_value,
803+
"last": last_bit,
804+
})
805+
par_window = parallel_window_type.wrap(par_struct)
806+
parallel_chan, parallel_ready = Channel(parallel_window_type).wrap(
807+
par_window, armed)
808+
par_ready.assign(parallel_ready)
809+
810+
p2s = ListWindowToSerial(parallel_window_type, _WINDOW_PROBE_BULK_WIDTH,
811+
_WINDOW_PROBE_ITEMS_PER_FRAME,
812+
_MULTIBURST_READ_FIFO_DEPTH)(
813+
clk=clk, rst=rst, parallel_in=parallel_chan)
814+
esi.ChannelService.to_host(AppID("data"), p2s.serial_out)
815+
816+
738817
class ChannelWindowedListWrite(Module):
739818
"""From-host channel of ``window<{tag, list<si32>}>``.
740819
@@ -812,6 +891,99 @@ def construct(ports):
812891
lambda _: final_match.as_bits(1).pad_or_truncate(64).as_bits(64)))
813892

814893

894+
# Multi-burst bare-list window. The bulk count is 8 bits, so each burst can
895+
# carry at most 255 items; a 256-element list must therefore be split by the
896+
# host serializer into two bursts (255 + 1). Hardware reassembles the bursts
897+
# via `ListWindowToParallel` and validates every item in order, proving the
898+
# write-side burst chunking round-trips through hardware.
899+
#
900+
# The window is a *bare* list of byte-sized items (no static header fields)
901+
# with an 8-bit count, so both the header frame (a single ui8 count) and each
902+
# data frame (one ui8) fill exactly one byte with no frame padding. That keeps
903+
# the on-wire layout unambiguous: a narrower (sub-byte) count would introduce
904+
# sub-byte frame padding, which the C++ facade codegen does not currently
905+
# model.
906+
_MULTIBURST_N_ITEMS = 256
907+
_MULTIBURST_BULK_WIDTH = 8
908+
_MULTIBURST_ITEMS_PER_FRAME = 1
909+
_multiburst_struct = StructType([("items", List(Bits(8)))])
910+
_multiburst_window = Window.serial_of(_multiburst_struct,
911+
_MULTIBURST_BULK_WIDTH,
912+
_MULTIBURST_ITEMS_PER_FRAME)
913+
914+
915+
class ChannelMultiBurstListWrite(Module):
916+
"""From-host channel of ``window<{list<ui8>}>`` with an 8-bit bulk count.
917+
918+
The 8-bit bulk count caps each burst at 255 items, so the host splits the
919+
256-element list into two bursts (255 + 1). Hardware reassembles the bursts
920+
via `ListWindowToParallel`, AND-reduces per-beat equality against the
921+
expected ``item[i] == i`` pattern, and latches the result into the ``match``
922+
MMIO region. This is the end-to-end check that the host-side multi-burst
923+
serialization is correct.
924+
"""
925+
926+
clk = Clock()
927+
rst = Reset()
928+
929+
@generator
930+
def construct(ports):
931+
clk = ports.clk
932+
rst = ports.rst
933+
934+
chan = esi.ChannelService.from_host(AppID("data"), _multiburst_window)
935+
s2p = ListWindowToParallel(_multiburst_window)(clk=clk,
936+
rst=rst,
937+
serial_in=chan)
938+
par_ready = Wire(Bits(1))
939+
par_window, par_valid = s2p.parallel_out.unwrap(par_ready)
940+
par_struct = par_window.unwrap()
941+
par_ready.assign(Bits(1)(1))
942+
943+
handshake = par_valid
944+
last_bit = par_struct["last"].as_bits(1)
945+
946+
# Counter cycles 0..255, clears on the burst-end beat. Each item's value
947+
# equals its position, so the expected value is simply the index.
948+
idx_clr = (handshake & last_bit).as_bits(1)
949+
idx_counter = Counter(8)(clk=clk,
950+
rst=rst,
951+
clear=idx_clr,
952+
increment=handshake,
953+
instance_name="multiburst_write_idx")
954+
idx = idx_counter.out
955+
956+
beat_ok = (par_struct["items"].as_bits(8) == idx.as_bits(8)).as_bits(1)
957+
958+
# Running AND-reduce across the whole (reassembled) list; latches into
959+
# ``final_match`` on the last item's beat.
960+
running_match = Wire(Bits(1))
961+
running_match_reg = Reg(Bits(1),
962+
clk=clk,
963+
rst=rst,
964+
rst_value=1,
965+
ce=handshake,
966+
name="multiburst_match_running")
967+
running_match.assign((running_match_reg & beat_ok).as_bits(1))
968+
running_match_reg.assign(running_match)
969+
970+
final_match = Reg(Bits(1),
971+
clk=clk,
972+
rst=rst,
973+
rst_value=0,
974+
ce=idx_clr,
975+
name="multiburst_match_final")
976+
final_match.assign(running_match)
977+
978+
# Expose the latched flag via MMIO read.
979+
mmio_bundle = esi.MMIO.read(appid=AppID("match"))
980+
resp_chan = Wire(Channel(Bits(64)))
981+
addr_chan = mmio_bundle.unpack(data=resp_chan)["offset"]
982+
resp_chan.assign(
983+
addr_chan.transform(
984+
lambda _: final_match.as_bits(1).pad_or_truncate(64).as_bits(64)))
985+
986+
815987
class Top(Module):
816988
clk = Clock()
817989
rst = Reset()
@@ -870,9 +1042,16 @@ def construct(ports):
8701042
ChannelWindowedListRead(clk=ports.clk,
8711043
rst=ports.rst,
8721044
appid=AppID("channel_windowed_list_read_inst"))
1045+
ChannelMultiBurstListRead(clk=ports.clk,
1046+
rst=ports.rst,
1047+
appid=AppID("channel_multiburst_list_read_inst"))
8731048
ChannelWindowedListWrite(clk=ports.clk,
8741049
rst=ports.rst,
8751050
appid=AppID("channel_windowed_list_write_inst"))
1051+
ChannelMultiBurstListWrite(
1052+
clk=ports.clk,
1053+
rst=ports.rst,
1054+
appid=AppID("channel_multiburst_list_write_inst"))
8761055
CallbackWindowedList(clk=ports.clk,
8771056
rst=ports.rst,
8781057
appid=AppID("callback_windowed_list_inst"))

0 commit comments

Comments
 (0)