-
Notifications
You must be signed in to change notification settings - Fork 25
Port ContentAddressableMemory from #395 #573
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
07f969c
9fee2ec
3e58cc2
5bf3b58
7a14803
deb7ccc
f560816
a33e930
b7c097f
80df321
b79897e
785657b
a993985
1fccc59
a1da5f6
0cffe57
54dcb16
0ba17ec
283e9e7
6a76949
0d4ca9f
202f35e
6312e1c
16b55c8
0dd252c
b716644
42f8104
8933040
060a904
384fc09
cfc4cca
b67c4c1
d1f14f4
bbd1d7a
5e6ba59
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import random | ||
| from typing import Optional | ||
| from transactron.utils import SimpleLayout | ||
|
|
||
|
|
||
| def generate_based_on_layout(layout: SimpleLayout, *, max_bits: Optional[int] = None): | ||
| d = {} | ||
| for elem in layout: | ||
| if isinstance(elem[1], int): | ||
| if max_bits is None: | ||
| max_val = 2 ** elem[1] | ||
| else: | ||
| max_val = 2 ** min(max_bits, elem[1]) | ||
| d[elem[0]] = random.randrange(max_val) | ||
| else: | ||
| d[elem[0]] = generate_based_on_layout(elem[1]) | ||
| return d | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| from test.common import * | ||
| import random | ||
| from transactron.lib.storage import ContentAddressableMemory | ||
|
|
||
|
|
||
| class TestContentAddressableMemory(TestCaseWithSimulator): | ||
| def setUp(self): | ||
| random.seed(14) | ||
| self.test_number = 50 | ||
| self.addr_width = 4 | ||
| self.content_width = 5 | ||
| self.entries_count = 8 | ||
| self.addr_layout = data_layout(self.addr_width) | ||
| self.content_layout = data_layout(self.content_width) | ||
|
|
||
| self.circ = SimpleTestCircuit( | ||
| ContentAddressableMemory(self.addr_layout, self.content_layout, self.entries_count) | ||
| ) | ||
|
|
||
| self.memory = {} | ||
|
|
||
| def input_process(self): | ||
| for _ in range(self.test_number): | ||
| while True: | ||
| addr = generate_based_on_layout(self.addr_layout) | ||
| frozen_addr = frozenset(addr.items()) | ||
| if frozen_addr not in self.memory: | ||
| break | ||
| content = generate_based_on_layout(self.content_layout) | ||
| yield from self.circ.push.call(addr=addr, data=content) | ||
| yield Settle() | ||
| self.memory[frozen_addr] = content | ||
|
|
||
| def output_process(self): | ||
| yield Passive() | ||
| while True: | ||
| addr = generate_based_on_layout(self.addr_layout) | ||
| res = yield from self.circ.pop.call(addr=addr) | ||
| frozen_addr = frozenset(addr.items()) | ||
| if frozen_addr in self.memory: | ||
| self.assertEqual(res["not_found"], 0) | ||
| self.assertEqual(res["data"], self.memory[frozen_addr]) | ||
| self.memory.pop(frozen_addr) | ||
| else: | ||
| self.assertEqual(res["not_found"], 1) | ||
|
|
||
| def test_random(self): | ||
| with self.run_simulation(self.circ) as sim: | ||
| sim.add_sync_process(self.input_process) | ||
| sim.add_sync_process(self.output_process) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| from test.common import * | ||
| import random | ||
| from transactron.utils.amaranth_ext import MultiPriorityEncoder | ||
|
|
||
|
|
||
| class TestMultiPriorityEncoder(TestCaseWithSimulator): | ||
| def setUp(self): | ||
| random.seed(14) | ||
| self.test_number = 50 | ||
| self.input_width = 16 | ||
| self.output_count = 4 | ||
|
|
||
| self.circ = MultiPriorityEncoder(self.input_width, self.output_count) | ||
|
|
||
| def get_expected(self, input): | ||
| places = [] | ||
| for i in range(self.input_width): | ||
| if input % 2: | ||
| places.append(i) | ||
| input //= 2 | ||
| places += [None] * self.output_count | ||
| return places | ||
|
|
||
| def process(self): | ||
| for _ in range(self.test_number): | ||
| input = random.randrange(2**self.input_width) | ||
| yield self.circ.input.eq(input) | ||
| yield Settle() | ||
| expected_output = self.get_expected(input) | ||
| for ex, real, valid in zip(expected_output, self.circ.outputs, self.circ.valids): | ||
| if ex is None: | ||
| self.assertEqual((yield valid), 0) | ||
| else: | ||
| self.assertEqual((yield valid), 1) | ||
| self.assertEqual((yield real), ex) | ||
| yield Delay(1e-7) | ||
|
|
||
| def test_random(self): | ||
| with self.run_simulation(self.circ) as sim: | ||
| sim.add_process(self.process) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,12 +1,12 @@ | ||||||
| from amaranth import * | ||||||
| from amaranth.utils import * | ||||||
| from ..core import * | ||||||
| from ..utils import SrcLoc, get_src_loc | ||||||
| from ..utils import SrcLoc, get_src_loc, MultiPriorityEncoder | ||||||
| from typing import Optional | ||||||
| from transactron.utils import assign, AssignType | ||||||
| from transactron.utils import assign, AssignType, LayoutLike | ||||||
| from .reqres import ArgumentsToResultsZipper | ||||||
|
|
||||||
| __all__ = ["MemoryBank"] | ||||||
| __all__ = ["MemoryBank", "ContentAddressableMemory"] | ||||||
|
|
||||||
|
|
||||||
| class MemoryBank(Elaboratable): | ||||||
|
|
@@ -133,3 +133,76 @@ def _(arg): | |||||
| m.d.comb += assign(write_args, arg, fields=AssignType.ALL) | ||||||
|
|
||||||
| return m | ||||||
|
|
||||||
|
|
||||||
| class ContentAddressableMemory(Elaboratable): | ||||||
| """Content addresable memory | ||||||
|
|
||||||
| This module implements a transactorn interface for the content addressable memory. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it make sense to call it an interface, when there is no internal module which has a traditional interface? Shouldn't you write a few words to explain what a content addressable memory is? Also, this is a very specialized kind of content-addressable memory - no non-destructive reads, no writes.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And also, no possibility of adding an eviction mechanism, for CAMs used as a cache. |
||||||
|
|
||||||
| .. warning:: | ||||||
| Current implementation has critical path O(entries_number). If needed we can | ||||||
| optimise it in future to have O(log(entries_number)). | ||||||
|
|
||||||
|
|
||||||
| Attributes | ||||||
| ---------- | ||||||
| pop : Method | ||||||
| Looks for the data in memory and, if found, returns it and removes it. | ||||||
| push : Method | ||||||
| Inserts new data. | ||||||
| """ | ||||||
|
|
||||||
| def __init__(self, address_layout: LayoutLike, data_layout: LayoutLike, entries_number: int): | ||||||
| """ | ||||||
| Parameters | ||||||
| ---------- | ||||||
| address_layout : LayoutLike | ||||||
| The layout of the address records. | ||||||
| data_layout : LayoutLike | ||||||
| The layout of the data. | ||||||
| entries_number : int | ||||||
| The number of slots to create in memory. | ||||||
| """ | ||||||
| self.address_layout = address_layout | ||||||
| self.data_layout = data_layout | ||||||
| self.entries_number = entries_number | ||||||
|
|
||||||
| self.pop = Method(i=[("addr", self.address_layout)], o=[("data", self.data_layout), ("not_found", 1)]) | ||||||
| self.push = Method(i=[("addr", self.address_layout), ("data", self.data_layout)]) | ||||||
|
|
||||||
| def elaborate(self, platform) -> TModule: | ||||||
| m = TModule() | ||||||
|
|
||||||
| address_array = Array([Record(self.address_layout) for _ in range(self.entries_number)]) | ||||||
| data_array = Array([Record(self.data_layout) for _ in range(self.entries_number)]) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The data could be stored in a memory.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After change of interface I decided to not use the |
||||||
| valids = Signal(self.entries_number, name="valids") | ||||||
|
|
||||||
| m.submodules.encoder_addr = encoder_addr = MultiPriorityEncoder(self.entries_number, 1) | ||||||
| m.submodules.encoder_valids = encoder_valids = MultiPriorityEncoder(self.entries_number, 1) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These have a single output, so they are just standard priority encoders, right?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. But PriorityEncoders are going to be removed from |
||||||
| m.d.comb += encoder_valids.input.eq(~valids) | ||||||
|
|
||||||
| @def_method(m, self.push, ready=~valids.all()) | ||||||
| def _(addr, data): | ||||||
| id = Signal(range(self.entries_number), name="id_push") | ||||||
| m.d.comb += id.eq(encoder_valids.outputs[0]) | ||||||
|
lekcyjna123 marked this conversation as resolved.
Outdated
|
||||||
| m.d.sync += address_array[id].eq(addr) | ||||||
| m.d.sync += data_array[id].eq(data) | ||||||
| m.d.sync += valids.bit_select(id, 1).eq(1) | ||||||
|
|
||||||
| if_addr = Signal(self.entries_number, name="if_addr") | ||||||
| data_to_send = Record(self.data_layout) | ||||||
|
|
||||||
| @def_method(m, self.pop) | ||||||
| def _(addr): | ||||||
| m.d.top_comb += if_addr.eq(Cat([addr == stored_addr for stored_addr in address_array]) & valids) | ||||||
| id = encoder_addr.outputs[0] | ||||||
| with m.If(if_addr.any()): | ||||||
| m.d.comb += data_to_send.eq(data_array[id]) | ||||||
| m.d.sync += valids.bit_select(id, 1).eq(0) | ||||||
|
|
||||||
| return {"data": data_to_send, "not_found": ~if_addr.any()} | ||||||
|
|
||||||
| m.d.comb += encoder_addr.input.eq(if_addr) | ||||||
|
|
||||||
| return m | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ | |
| "ModuleConnector", | ||
| "Scheduler", | ||
| "RoundRobin", | ||
| "MultiPriorityEncoder", | ||
| ] | ||
|
|
||
|
|
||
|
|
@@ -239,3 +240,63 @@ def elaborate(self, platform): | |
| m.d.sync += self.valid.eq(self.requests.any()) | ||
|
|
||
| return m | ||
|
|
||
|
|
||
| class MultiPriorityEncoder(Elaboratable): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking that since the priority encoder is just a combinational logic, it would be nice if we could use it like: idx, valid = prio_encoder(m, one_hot_signal)instead of the boilerplate code m.submodules.prio_encoder = prio_encoder = PriorityEncoder(cnt)
m.d.av_comb += prio_encoder.i.eq(one_hot_singal)
idx = prio_encoder.o
valid = ~prio.encoder.n
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And another thing. It would be great if we could specify the bit order of the priority encoder. In theory by having one type of the priority encoder, we could get the other type by reversing the input and subtracting the outputs, but that requires a few adders. I know that this is pretty much a feature request, so feel free to ignore it - it can be done later
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have added Regarding to the different bit ordering I don't see a reason why we should add it, because it can be handled without subtraction. You pass the reversed array and you use indexes returned by encoder in this reversed array.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
First, that's a mouthful. Second, a shorthand for creating
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have updated the name to the |
||
| """Priority encoder with more outputs | ||
|
|
||
| This is an extension of the `PriorityEncoder` from amaranth that supports | ||
| more than one output from an input signal. In other words | ||
| it decodes multi-hot encoded signal into lists of signals in binary | ||
| format, each with the index of a different high bit in the input. | ||
|
|
||
| Attributes | ||
| ---------- | ||
| input_width : int | ||
| Width of the input signal | ||
| outputs_count : int | ||
| Number of outputs to generate at once. | ||
| input : Signal, in | ||
| Signal with 1 on `i`-th bit if `i` can be selected by encoder | ||
| outputs : list[Signal], out | ||
| Signals with selected indicies, sorted in ascending order, | ||
| if the number of ready signals is less than `outputs_count` | ||
| then valid signals are at the beginning of the list. | ||
| valids : list[Signals], out | ||
|
lekcyjna123 marked this conversation as resolved.
Outdated
|
||
| One bit for each output signal, indicating whether the output is valid or not. | ||
| """ | ||
|
|
||
| def __init__(self, input_width: int, outputs_count: int): | ||
| self.input_width = input_width | ||
| self.outputs_count = outputs_count | ||
|
|
||
| self.input = Signal(self.input_width) | ||
| self.outputs = [Signal(range(self.input_width), name="output") for _ in range(self.outputs_count)] | ||
| self.valids = [Signal(name="valid") for _ in range(self.outputs_count)] | ||
|
lekcyjna123 marked this conversation as resolved.
Outdated
|
||
|
|
||
| def elaborate(self, platform): | ||
| m = Module() | ||
|
|
||
| current_outputs = [Signal(range(self.input_width)) for _ in range(self.outputs_count)] | ||
| current_valids = [Signal() for _ in range(self.outputs_count)] | ||
| for j in reversed(range(self.input_width)): | ||
|
lekcyjna123 marked this conversation as resolved.
Outdated
|
||
| new_current_outputs = [Signal(range(self.input_width)) for _ in range(self.outputs_count)] | ||
| new_current_valids = [Signal() for _ in range(self.outputs_count)] | ||
| with m.If(self.input[j]): | ||
| m.d.comb += new_current_outputs[0].eq(j) | ||
| m.d.comb += new_current_valids[0].eq(1) | ||
| for k in range(self.outputs_count - 1): | ||
| m.d.comb += new_current_outputs[k + 1].eq(current_outputs[k]) | ||
| m.d.comb += new_current_valids[k + 1].eq(current_valids[k]) | ||
| with m.Else(): | ||
| for k in range(self.outputs_count): | ||
| m.d.comb += new_current_outputs[k].eq(current_outputs[k]) | ||
| m.d.comb += new_current_valids[k].eq(current_valids[k]) | ||
| current_outputs = new_current_outputs | ||
| current_valids = new_current_valids | ||
|
|
||
| for k in range(self.outputs_count): | ||
| m.d.comb += self.outputs[k].eq(current_outputs[k]) | ||
| m.d.comb += self.valids[k].eq(current_valids[k]) | ||
|
|
||
| return m | ||
Uh oh!
There was an error while loading. Please reload this page.