CIRCT 24.0.0git
Loading...
Searching...
No Matches
test_backpressure.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 integration tests for channel backpressure (cosim protocol v4 flow
5control), using the DUTs in `hw/backpressure.py`.
6
7 * Host -> simulation: with the loopback's gate closed the hardware never
8 consumes `in`, so `try_write` must start failing after roughly the
9 default in-flight limit (16) plus the endpoint's internal buffering, and
10 recover once the gate opens.
11
12 * Simulation -> host: with `stream` connected with a small buffer and not
13 read, the producer must stall (its `Cosim_Endpoint_ToHost` deasserts
14 ready) instead of pushing everything into host memory.
15"""
16
17from __future__ import annotations
18
19import time
20
21import esiaccel
22from esiaccel.accelerator import AcceleratorConnection
23from esiaccel.cosim.pytest import cosim_test
24
25from .conftest import HW_DIR
26
27# Must match RpcClient::DefaultToServerMaxInFlight.
28DEFAULT_TO_SERVER_MAX_IN_FLIGHT = 16
29# Slack for messages absorbed by hardware buffering (cosim endpoints, BSP
30# pipeline stages) beyond the cosim flow-control in-flight limits.
31HW_SLACK = 8
32
33
34def _fill(try_write, start: int) -> int:
35 """Call `try_write(value)` with increasing values from `start` until it keeps
36 failing for a while, so credits for anything the hardware takes in the
37 meantime are observed. Returns the next unwritten value."""
38 value = start
39 quiet_since = time.monotonic()
40 while time.monotonic() - quiet_since < 1.0:
41 if try_write(value):
42 value += 1
43 quiet_since = time.monotonic()
44 assert value - start < 1000, "try_write never applied backpressure"
45 else:
46 time.sleep(0.01)
47 return value
48
49
50def _wait_stable(read_value, settle: float = 1.0, timeout: float = 60.0):
51 """Poll `read_value()` until it stops changing for `settle` seconds."""
52 deadline = time.monotonic() + timeout
53 last = read_value()
54 last_change = time.monotonic()
55 while time.monotonic() - last_change < settle:
56 assert time.monotonic() < deadline, "value never settled"
57 time.sleep(0.1)
58 cur = read_value()
59 if cur != last:
60 last, last_change = cur, time.monotonic()
61 return last
62
63
64@cosim_test(HW_DIR / "backpressure.py")
66
67 def test_host_to_sim(self, conn: AcceleratorConnection) -> None:
68 acc = conn.build_accelerator()
69 dut = acc.children[esiaccel.AppID("loopback")]
70 gate = dut.ports[esiaccel.AppID("gate")]
71 gate.connect()
72 inp = dut.ports[esiaccel.AppID("in")].write_port("data").connect()
73 # Unbounded, so the loopback output never stalls the input.
74 out = dut.ports[esiaccel.AppID("out")].read_port("data").connect(
75 buffer_size=0)
76
77 # Gate closed (reset state): writes must back up.
78 accepted = _fill(inp.try_write, 0)
79 assert DEFAULT_TO_SERVER_MAX_IN_FLIGHT <= accepted <= (
80 DEFAULT_TO_SERVER_MAX_IN_FLIGHT + HW_SLACK), accepted
81 assert not inp.try_write(accepted)
82
83 # Open the gate: everything accepted so far comes out, in order.
84 gate.write(1)
85 for i in range(accepted):
86 assert out.read() == i
87
88 # With the consumer running, blocking writes flow despite the limit.
89 total = accepted + 200
90 for i in range(accepted, total):
91 inp.write(i)
92 for i in range(accepted, total):
93 assert out.read() == i
94
95 # Close the gate again and check try_write recovers once it reopens.
96 gate.write(0)
97 stalled_at = _fill(inp.try_write, total)
98 assert stalled_at - total <= DEFAULT_TO_SERVER_MAX_IN_FLIGHT + HW_SLACK
99 gate.write(1)
100 for i in range(total, stalled_at):
101 assert out.read() == i
102 deadline = time.monotonic() + 30
103 while not inp.try_write(stalled_at):
104 assert time.monotonic() < deadline, "try_write never recovered"
105 time.sleep(0.01)
106 assert out.read() == stalled_at
107
108 def test_sim_to_host(self, conn: AcceleratorConnection) -> None:
109 acc = conn.build_accelerator()
110 dut = acc.children[esiaccel.AppID("producer")]
111 produce = dut.ports[esiaccel.AppID("produce")]
112 query = dut.ports[esiaccel.AppID("query")]
113 sent = dut.ports[esiaccel.AppID("sent")]
114 for p in (produce, query, sent):
115 p.connect()
116 # Small buffer: both the host polling queue and the in-flight limit.
117 buffer = 4
118 stream = dut.ports[esiaccel.AppID("stream")].read_port("data").connect(
119 buffer_size=buffer)
120
121 def sent_count() -> int:
122 query.write(0)
123 return sent.read().result()
124
125 n = 1000
126 produce.write(n)
127 stalled = _wait_stable(sent_count)
128 # Host polling queue + in-flight limit + hardware buffering.
129 assert 2 * buffer <= stalled <= 2 * buffer + HW_SLACK, stalled
130
131 # Draining the stream lets the producer finish, with nothing lost.
132 for i in range(n):
133 assert stream.read() == i
134 assert sent_count() == n
None test_sim_to_host(self, AcceleratorConnection conn)
None test_host_to_sim(self, AcceleratorConnection conn)
_wait_stable(read_value, float settle=1.0, float timeout=60.0)