CIRCT 24.0.0git
Loading...
Searching...
No Matches
test_hostmem_read.py
Go to the documentation of this file.
1# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
2# See https://llvm.org/LICENSE.txt for license information.
3# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4"""Cosim tests for configurable widths and multiple outstanding HostMem reads.
5
6`hw/hostmem_read.py` builds one host-driven `read_list` client per element
7width under `CosimBSP(max_outstanding_reads=N)`. Each test closes the client's
8response gate, queues several reads of varied lengths, checks that exactly
9min(#reads, N) are accepted while nothing drains (so that many logical requests
10are genuinely in flight inside `HostMemReadReqSplitter`), then opens the gate
11and checks every returned element, the per-request element counts and that
12`last` lands exactly on each request's final element.
13"""
14
15from __future__ import annotations
16
17import ctypes
18import time
19from pathlib import Path
20from typing import List, Tuple
21
22import esiaccel
23import pytest
24from esiaccel.accelerator import AcceleratorConnection
25from esiaccel.cosim.pytest import cosim_test
26
27from .conftest import HW_DIR
28
29# Must match hw/hostmem_read.py.
30ELEMENT_WIDTHS = [24, 32, 96]
31
32# (element offset, element count). The first read is long enough that the
33# read path's buffering cannot absorb its whole burst, so nothing behind it can
34# complete while the gate is closed. Lengths cover single elements, counts
35# that leave a partial final 64-bit word, and reads spanning several 256-byte
36# upstream chunks.
37GATED_READS: List[Tuple[int, int]] = [
38 (0, 120),
39 (5, 1),
40 (17, 3),
41 (40, 7),
42 (3, 90),
43 (300, 2),
44 (11, 33),
45 (64, 5),
46]
47# Short reads streamed with the gate open, so requests are accepted while
48# earlier bursts are still draining.
49STREAMED_READS: List[Tuple[int,
50 int]] = [(i * 7 % 50, 1 + i % 3) for i in range(24)]
51
52BUFFER_ELEMENTS = 4096
53
54
55@pytest.fixture(autouse=True)
56def read_frame_log(monkeypatch):
57 monkeypatch.setenv("COSIM_DEBUG_FILE", "hostmem_read.log")
58
59
60def _pattern(num_bytes: int) -> bytes:
61 return bytes((i * 37 + 11) & 0xFF for i in range(num_bytes))
62
63
64def _as_int(v) -> int:
65 if isinstance(v, (bytes, bytearray)):
66 return int.from_bytes(v, "little")
67 return int(v)
68
69
70class _Dut:
71
72 def __init__(self, conn: AcceleratorConnection, acc, width: int):
73 self.width = width
74 self.stride = (width + 7) // 8
75 dut = acc.children[esiaccel.AppID("rd", width)]
76 self.req = dut.ports[esiaccel.AppID("req")]
77 self.gate = dut.ports[esiaccel.AppID("gate")]
78 self.resp = dut.ports[esiaccel.AppID("resp")]
79 self.accepted = dut.ports[esiaccel.AppID("accepted")]
80 for p in (self.req, self.gate, self.resp, self.accepted):
81 p.connect()
82
83 hostmem = conn.get_service_hostmem()
84 hostmem.start()
85 nbytes = BUFFER_ELEMENTS * self.stride + 64
86 self.mem = hostmem.allocate(nbytes)
87 self.data = _pattern(nbytes)
88 ctypes.memmove(self.mem.ptr, self.data, nbytes)
89
90 def expected(self, offset: int) -> int:
91 start = offset * self.stride
92 raw = int.from_bytes(self.data[start:start + self.stride], "little")
93 return raw & ((1 << self.width) - 1)
94
95 def accepted_count(self) -> int:
96 return self.accepted.cpp_port.readInt()
97
98 def issue(self, reads: List[Tuple[int, int]]) -> None:
99 for offset, count in reads:
100 self.req.write({
101 "address": self.mem.ptr + offset * self.stride,
102 "length": count
103 })
104
105 def check_responses(self, reads: List[Tuple[int, int]]) -> None:
106 for ridx, (offset, count) in enumerate(reads):
107 for e in range(count):
108 msg = self.resp.read().result()
109 got = _as_int(msg["data"])
110 last = _as_int(msg["last"])
111 exp = self.expected(offset + e)
112 assert got == exp, (
113 f"width {self.width} read {ridx} element {e}: got {got:#x}, "
114 f"expected {exp:#x}")
115 assert last == (e == count - 1), (
116 f"width {self.width} read {ridx} (count {count}) element {e}: "
117 f"last={last}")
118
119
120def _wait_for_accepted(dut: _Dut, target: int, timeout_s: float = 30) -> int:
121 deadline = time.time() + timeout_s
122 count = dut.accepted_count()
123 while count < target and time.time() < deadline:
124 time.sleep(0.05)
125 count = dut.accepted_count()
126 return count
127
128
129def _check_frames(expected_reads, bus_width):
130 """Check actual transport requests and responses, including zero padding."""
131 lines = Path("hostmem_read.log").read_text().splitlines()
132 requests = [
133 bytes.fromhex(line.split("]", 1)[1])
134 for line in lines
135 if "__cosim_hostmem_read_req.data" in line
136 ]
137 responses = [
138 bytes.fromhex(line.split("]", 1)[1])
139 for line in lines
140 if "__cosim_hostmem_read_resp.data" in line
141 ]
142 word_bytes = bus_width // 8
143 cap = max(256, word_bytes)
144 chunks = [(address + offset, data[offset:offset + cap])
145 for address, data in expected_reads
146 for offset in range(0, len(data), cap)]
147 assert len(requests) == len(chunks)
148 assert len(responses) == sum(
149 (len(data) + word_bytes - 1) // word_bytes for _, data in expected_reads)
150 response_idx = 0
151 for request, (address, data) in zip(requests, chunks):
152 assert len(request) == 13
153 assert int.from_bytes(request[1:5], "little") == len(data)
154 assert int.from_bytes(request[5:], "little") == address
155 for offset in range(0, len(data), word_bytes):
156 response = responses[response_idx]
157 response_idx += 1
158 assert len(response) == word_bytes + 2
159 packed = int.from_bytes(response, "little")
160 assert packed & 1 == int(offset + word_bytes >= len(data))
161 assert packed >> (bus_width + 1) == request[0]
162 assert (packed >> 1) & ((1 << bus_width) - 1) == int.from_bytes(
163 data[offset:offset + word_bytes], "little")
164
165
166def _run(conn: AcceleratorConnection,
167 max_outstanding: int,
168 bus_width: int = 64) -> None:
169 hostmem = conn.get_service_hostmem()
170 hostmem.start()
171 acc = conn.build_accelerator()
172 expected_reads = []
173 for width in ELEMENT_WIDTHS:
174 dut = _Dut(conn, acc, width)
175 # Keep the first burst larger than the response buffering even on wide
176 # buses, so it cannot finish while the client response gate is closed.
177 gated_reads = [(0, max(120, 4 * (bus_width // 8) // dut.stride + 1))
178 ] + GATED_READS[1:]
179
180 # Gate closed (its reset state): queue every read, then confirm exactly
181 # min(#reads, max_outstanding) of them are accepted and held in flight.
182 dut.gate.write(0)
183 dut.issue(gated_reads)
184 in_flight = min(len(gated_reads), max_outstanding)
185 assert _wait_for_accepted(dut, in_flight) >= in_flight
186 time.sleep(0.5)
187 assert dut.accepted_count() == in_flight, (
188 f"width {width}: expected {in_flight} reads in flight with the "
189 f"response side stalled")
190
191 dut.gate.write(1)
192 dut.check_responses(gated_reads)
193 assert dut.accepted_count() == len(gated_reads)
194
195 # Gate open: stream short reads back to back.
196 dut.issue(STREAMED_READS)
197 dut.check_responses(STREAMED_READS)
198 assert dut.accepted_count() == len(gated_reads) + len(STREAMED_READS)
199 for offset, count in gated_reads + STREAMED_READS:
200 start = offset * dut.stride
201 expected_reads.append(
202 (dut.mem.ptr + start, dut.data[start:start + count * dut.stride]))
203
204 for width in [13, bus_width, bus_width + 24, 16384]:
205 dut = acc.children[esiaccel.AppID("single", width)]
206 req = dut.ports[esiaccel.AppID("req")]
207 resp = dut.ports[esiaccel.AppID("resp")]
208 req.connect()
209 resp.connect()
210 size = (width + 7) // 8
211 mem = hostmem.allocate(size + bus_width // 8 + 6)
212 ctypes.memset(mem.ptr, 0xA5, mem.size)
213 data = _pattern(size)
214 ctypes.memmove(mem.ptr + 3, data, size)
215 req.write(mem.ptr + 3)
216 assert resp.read().result(timeout=30) == (int.from_bytes(data, "little") &
217 ((1 << width) - 1))
218 expected_reads.append((mem.ptr + 3, data))
219 _check_frames(expected_reads, bus_width)
220
221
222@cosim_test(HW_DIR / "hostmem_read.py", args=("{tmp_dir}", "1"))
224
225 def test_reads(self, conn: AcceleratorConnection) -> None:
226 _run(conn, 1)
227
228
229@cosim_test(HW_DIR / "hostmem_read.py", args=("{tmp_dir}", "4"))
231
232 def test_reads(self, conn: AcceleratorConnection) -> None:
233 _run(conn, 4)
234
235
236@cosim_test(HW_DIR / "hostmem_read.py", args=("{tmp_dir}", "4", "8"))
238
239 def test_reads(self, conn: AcceleratorConnection) -> None:
240 _run(conn, 4, 8)
241
242
243@cosim_test(HW_DIR / "hostmem_read.py", args=("{tmp_dir}", "4", "512"))
245
246 def test_reads(self, conn: AcceleratorConnection) -> None:
247 _run(conn, 4, 512)
248
249
250@cosim_test(HW_DIR / "hostmem_read.py", args=("{tmp_dir}", "4", "2048"))
252
253 def test_reads(self, conn: AcceleratorConnection) -> None:
254 _run(conn, 4, 2048)
255
256
257@cosim_test(HW_DIR / "hostmem_read.py", args=("{tmp_dir}", "4", "4096"))
259
260 def test_reads(self, conn: AcceleratorConnection) -> None:
261 _run(conn, 4, 4096)
None test_reads(self, AcceleratorConnection conn)
None test_reads(self, AcceleratorConnection conn)
None test_reads(self, AcceleratorConnection conn)
None test_reads(self, AcceleratorConnection conn)
__init__(self, AcceleratorConnection conn, acc, int width)
None check_responses(self, List[Tuple[int, int]] reads)
None issue(self, List[Tuple[int, int]] reads)
int _wait_for_accepted(_Dut dut, int target, float timeout_s=30)
_check_frames(expected_reads, bus_width)
None _run(AcceleratorConnection conn, int max_outstanding, int bus_width=64)