Skip to content

Commit 526a873

Browse files
authored
Circular allocator (#151)
1 parent f8be938 commit 526a873

4 files changed

Lines changed: 261 additions & 28 deletions

File tree

test/lib/test_allocators.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,68 @@ async def order_verifier(sim: TestbenchContext):
9898
sim.add_testbench(order_verifier, background=True)
9999
sim.add_testbench(allocator)
100100
sim.add_testbench(deallocator)
101+
102+
103+
class TestCircularAllocator(TestCaseWithSimulator):
104+
@pytest.mark.parametrize("entries", [5, 8])
105+
@pytest.mark.parametrize("max_alloc", [1, 3])
106+
@pytest.mark.parametrize("max_free", [1, 3])
107+
@pytest.mark.parametrize("with_validate_arguments", [False, True])
108+
def test_allocator(self, entries: int, max_alloc: int, max_free: int, with_validate_arguments: bool):
109+
m = CircularAllocator(entries, max_alloc, max_free, with_validate_arguments=with_validate_arguments)
110+
dut = SimpleTestCircuit(m)
111+
112+
iterations = 5 * entries
113+
114+
start_idx = end_idx = allocated = 0
115+
116+
async def allocator(sim: TestbenchContext):
117+
nonlocal allocated, end_idx
118+
while True:
119+
curr_max_alloc = max_alloc if with_validate_arguments else min(entries - allocated, max_alloc)
120+
count = random.randrange(curr_max_alloc + 1)
121+
ret = await dut.alloc.call_try(sim, count=count)
122+
if ret is None:
123+
assert (with_validate_arguments and count > entries - allocated) or allocated == entries
124+
count = 1
125+
ret = await dut.alloc.call(sim, count=count)
126+
for i in range(max_alloc):
127+
assert ret.idents[i] == (end_idx + i) % entries
128+
await sim.delay(1e-12)
129+
allocated = allocated + count
130+
end_idx = (end_idx + count) % entries
131+
assert ret.new_end_idx == end_idx
132+
await sim.delay(1e-12)
133+
await self.random_wait_geom(sim)
134+
135+
async def deallocator(sim: TestbenchContext):
136+
nonlocal allocated, start_idx
137+
for _ in range(iterations):
138+
curr_max_free = max_free if with_validate_arguments else min(allocated, max_free)
139+
count = random.randrange(curr_max_free + 1)
140+
ret = await dut.free.call_try(sim, count=count)
141+
if ret is None:
142+
assert (with_validate_arguments and count > allocated) or allocated == 0
143+
count = 1
144+
ret = await dut.free.call(sim, count=count)
145+
for i in range(max_free):
146+
assert ret.idents[i] == (start_idx + i) % entries
147+
await sim.delay(1e-12)
148+
allocated = allocated - count
149+
start_idx = (start_idx + count) % entries
150+
assert ret.new_start_idx == start_idx
151+
await sim.delay(1e-12)
152+
await self.random_wait_geom(sim)
153+
154+
async def verifier(sim: TestbenchContext):
155+
while True:
156+
await sim.delay(2e-12)
157+
assert allocated == sim.get(m.allocated)
158+
assert start_idx == sim.get(m.start_idx)
159+
assert end_idx == sim.get(m.end_idx)
160+
await sim.tick()
161+
162+
with self.run_simulation(dut) as sim:
163+
sim.add_testbench(allocator, background=True)
164+
sim.add_testbench(deallocator)
165+
sim.add_testbench(verifier, background=True)

transactron/lib/allocators.py

Lines changed: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from amaranth import *
22

3-
from transactron.core import Method, Methods, TModule, def_method, def_methods
3+
from transactron.core import Method, Methods, TModule, def_method, def_methods, Provided
44
from transactron.utils.amaranth_ext.elaboratables import MultiPriorityEncoder
55
from amaranth.lib.data import ArrayLayout
66

7+
from transactron.utils.amaranth_ext.functions import mod_add
78

8-
__all__ = ["PriorityEncoderAllocator"]
9+
10+
__all__ = ["PriorityEncoderAllocator", "PreservedOrderAllocator", "CircularAllocator"]
911

1012

1113
class PriorityEncoderAllocator(Elaboratable):
@@ -129,3 +131,162 @@ def _():
129131
return {"used": used, "order": order}
130132

131133
return m
134+
135+
136+
class CircularAllocator(Elaboratable):
137+
"""Circular allocator.
138+
139+
Allows to allocate and deallocate identifiers in FIFO order. It is
140+
possible to allocate or deallocate multiple identifiers in a single
141+
clock cycle.
142+
"""
143+
144+
alloc: Provided[Method]
145+
"""
146+
Allocates new identifiers. Ready only if there are free identifiers
147+
available. The `count` argument must be less or equal to the number
148+
of available free identifiers.
149+
150+
If `with_validate_arguments` is false, invalid calls are allowed but can
151+
result in illegal state.
152+
153+
Parameters
154+
----------
155+
count: range(max_alloc + 1)
156+
The number of identifiers to allocate.
157+
158+
Returns
159+
-------
160+
idents: ArrayLayout(range(entries), max_alloc)
161+
Array of allocated identifiers.
162+
new_end_idx: range(entries)
163+
First identifier after the last allocated one.
164+
"""
165+
166+
free: Provided[Method]
167+
"""
168+
Frees previously allocated identifiers. Ready only if there are allocated
169+
identifiers. The `count` argument must be less or equal to the number of
170+
allocated identifiers.
171+
172+
If `with_validate_arguments` is false, invalid calls are allowed but can
173+
result in illegal state.
174+
175+
Parameters
176+
----------
177+
count: range(max_free + 1)
178+
The number of identifiers to deallocate.
179+
180+
Returns
181+
-------
182+
idents: ArrayLayout(range(entries), max_alloc)
183+
Array of freed identifiers.
184+
new_start_idx: range(entries)
185+
First identifier after the last freed one.
186+
"""
187+
188+
clear: Provided[Method]
189+
"""
190+
Restores the allocator to its initial state.
191+
"""
192+
193+
start_idx: Signal
194+
"""
195+
First pointer of the circular allocator. The oldest allocated identifier,
196+
if one exists.
197+
"""
198+
199+
end_idx: Signal
200+
"""
201+
Second pointer of the circular allocator. The first after the newest
202+
allocated identifier, if one exists.
203+
"""
204+
205+
allocated: Signal
206+
"""
207+
The number of allocated identifiers.
208+
"""
209+
210+
def __init__(self, entries: int, max_alloc: int = 1, max_free: int = 1, *, with_validate_arguments=True):
211+
"""
212+
Parameters
213+
----------
214+
entries: int
215+
The total number of identifiers available for allocation.
216+
max_alloc: int, optional
217+
The amount of identifiers that can be allocated in a single cycle.
218+
Defaults to 1.
219+
max_free: int, optional
220+
The amount of identifiers that can be freed in a single cycle.
221+
Defaults to 1.
222+
with_validate_arguments: bool, optional
223+
If true, `alloc` and `free` methods are guarded by argument
224+
validation so that it is impossible to put the allocator into
225+
an illegal state. Otherwise, the `count` argument needs to
226+
be verified using external logic.
227+
Defaults to true.
228+
"""
229+
self.entries = entries
230+
self.max_alloc = max_alloc
231+
self.max_free = max_free
232+
self.with_validate_arguments = with_validate_arguments
233+
234+
self.alloc = Method(
235+
i=[("count", range(max_alloc + 1))],
236+
o=[("idents", ArrayLayout(range(entries), max_alloc)), ("new_end_idx", range(entries))],
237+
)
238+
self.free = Method(
239+
i=[("count", range(max_free + 1))],
240+
o=[("idents", ArrayLayout(range(entries), max_free)), ("new_start_idx", range(entries))],
241+
)
242+
self.clear = Method()
243+
244+
self.start_idx = Signal(range(entries))
245+
self.end_idx = Signal(range(entries))
246+
self.allocated = Signal(range(entries + 1))
247+
248+
def elaborate(self, platform):
249+
m = TModule()
250+
251+
alloc_count = Signal(range(self.max_alloc + 1))
252+
free_count = Signal(range(self.max_free + 1))
253+
254+
m.d.sync += self.allocated.eq(self.allocated + alloc_count - free_count)
255+
256+
kwargs = {}
257+
if self.with_validate_arguments and self.max_alloc > 1:
258+
kwargs["validate_arguments"] = lambda count: self.allocated + count <= self.entries
259+
260+
@def_method(m, self.alloc, ready=self.allocated != self.entries, **kwargs)
261+
def _(count):
262+
new_end_idx = Signal.like(self.end_idx)
263+
m.d.av_comb += new_end_idx.eq(mod_add(self.end_idx, self.entries, count, self.max_alloc))
264+
m.d.sync += self.end_idx.eq(new_end_idx)
265+
m.d.comb += alloc_count.eq(count)
266+
return {
267+
"idents": [mod_add(self.end_idx, self.entries, i, i) for i in range(self.max_alloc)],
268+
"new_end_idx": new_end_idx,
269+
}
270+
271+
kwargs = {}
272+
if self.with_validate_arguments and self.max_free > 1:
273+
kwargs["validate_arguments"] = lambda count: count <= self.allocated
274+
275+
@def_method(m, self.free, ready=self.allocated != 0, **kwargs)
276+
def _(count):
277+
new_start_idx = Signal.like(self.start_idx)
278+
m.d.av_comb += new_start_idx.eq(mod_add(self.start_idx, self.entries, count, self.max_free))
279+
m.d.sync += self.start_idx.eq(new_start_idx)
280+
m.d.comb += free_count.eq(count)
281+
return {
282+
"idents": [mod_add(self.start_idx, self.entries, i, i) for i in range(self.max_free)],
283+
"new_start_idx": new_start_idx,
284+
}
285+
286+
@def_method(m, self.clear)
287+
def _():
288+
m.d.sync += self.start_idx.eq(0)
289+
m.d.sync += self.end_idx.eq(0)
290+
m.d.sync += self.allocated.eq(0)
291+
292+
return m

transactron/lib/fifo.py

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import amaranth.lib.data as data
55
from amaranth_types import ShapeLike, ValueLike, SrcLoc
66
from transactron import Method, def_method, Priority, TModule
7+
from transactron.lib.allocators import CircularAllocator
78
from transactron.utils.typing import MethodLayout, MethodStruct
89
from transactron.utils.amaranth_ext import mod_incr, rotate_vec_right, rotate_vec_left
910
from transactron.utils.amaranth_ext.functions import const_of
@@ -108,50 +109,39 @@ def __init__(self, layout: MethodLayout, depth: int, *, src_loc: int | SrcLoc =
108109
def elaborate(self, platform):
109110
m = TModule()
110111

111-
next_read_idx = Signal.like(self.read_idx)
112-
m.d.comb += next_read_idx.eq(mod_incr(self.read_idx, self.depth))
112+
m.submodules.allocator = allocator = CircularAllocator(self.depth)
113+
m.d.comb += self.read_idx.eq(allocator.start_idx)
114+
m.d.comb += self.write_idx.eq(allocator.end_idx)
115+
m.d.comb += self.level.eq(allocator.allocated)
113116

114117
m.submodules.data = self.data
115118
data_wrport = self.data.write_port()
116119
data_rdport = self.data.read_port(domain="sync", transparent_for=[data_wrport])
117120

118-
read_ready = Signal()
119-
write_ready = Signal()
120-
121-
m.d.comb += read_ready.eq(self.level != 0)
122-
m.d.comb += write_ready.eq(self.level != self.depth)
123-
124-
with m.If(self.read.run & ~self.write.run):
125-
m.d.sync += self.level.eq(self.level - 1)
126-
with m.If(self.write.run & ~self.read.run):
127-
m.d.sync += self.level.eq(self.level + 1)
128-
with m.If(self.clear.run):
129-
m.d.sync += self.level.eq(0)
130-
131-
m.d.comb += data_rdport.addr.eq(Mux(self.read.run, next_read_idx, self.read_idx))
121+
m.d.comb += data_rdport.addr.eq(self.read_idx)
132122
m.d.comb += self.head.eq(data_rdport.data)
133123

134-
@def_method(m, self.write, ready=write_ready)
124+
@def_method(m, self.write)
135125
def _(arg: MethodStruct) -> None:
136126
m.d.top_comb += data_wrport.addr.eq(self.write_idx)
137127
m.d.top_comb += data_wrport.data.eq(arg)
138128
m.d.comb += data_wrport.en.eq(1)
139129

140-
m.d.sync += self.write_idx.eq(mod_incr(self.write_idx, self.depth))
130+
allocator.alloc(m, count=1)
141131

142-
@def_method(m, self.read, read_ready)
132+
@def_method(m, self.read)
143133
def _() -> ValueLike:
144-
m.d.sync += self.read_idx.eq(next_read_idx)
134+
ret = allocator.free(m, count=1)
135+
m.d.comb += data_rdport.addr.eq(ret.new_start_idx)
145136
return self.head
146137

147-
@def_method(m, self.peek, read_ready, nonexclusive=True)
138+
@def_method(m, self.peek, allocator.free.ready, nonexclusive=True)
148139
def _() -> ValueLike:
149140
return self.head
150141

151142
@def_method(m, self.clear)
152143
def _() -> None:
153-
m.d.sync += self.read_idx.eq(0)
154-
m.d.sync += self.write_idx.eq(0)
144+
allocator.clear(m)
155145

156146
return m
157147

transactron/utils/amaranth_ext/functions.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Any
22
from amaranth import *
33
from amaranth.hdl import ShapeCastable, ValueCastable
4+
from amaranth.hdl._ast import SwitchValue
45
from amaranth.utils import bits_for, ceil_log2
56
from amaranth.lib import data
67
from collections.abc import Callable, Iterable, Mapping
@@ -11,6 +12,7 @@
1112

1213
__all__ = [
1314
"mod_incr",
15+
"mod_add",
1416
"popcount",
1517
"count_leading_zeros",
1618
"count_trailing_zeros",
@@ -28,15 +30,30 @@
2830
]
2931

3032

31-
def mod_incr(sig: Value, mod: int) -> Value:
33+
def mod_incr(sig: ValueLike, mod: int) -> Value:
3234
"""
3335
Perform `(sig+1) % mod` operation.
3436
"""
35-
if mod == 2 ** len(sig):
36-
return sig + 1
37+
assert mod > 0
38+
sig = Value.cast(sig)
39+
if not (mod & (mod - 1)):
40+
return (sig + 1) & (mod - 1)
3741
return Mux(sig == mod - 1, 0, sig + 1)
3842

3943

44+
def mod_add(sig: ValueLike, mod: int, incr: ValueLike, max_incr: int):
45+
"""
46+
Perform `(sig+incr) % mod` operation, for `0 < incr <= max_incr`.
47+
"""
48+
assert mod > 0
49+
assert max_incr >= 0
50+
sig = Value.cast(sig)
51+
incr = Value.cast(incr)
52+
if not (mod & (mod - 1)):
53+
return (sig + incr) & (mod - 1)
54+
return SwitchValue(sig + incr, [(mod + i, i) for i in range(0, max_incr)] + [(None, sig + incr)])
55+
56+
4057
def popcount(s: Value):
4158
sum_layers = [s[i] for i in range(len(s))]
4259

0 commit comments

Comments
 (0)