Skip to content

Commit

Permalink
Merge branch 'kondys_feat_cocotb_mvb_transactions' into 'devel'
Browse files Browse the repository at this point in the history
Cocotb - MVB: implementation of MVB transactions and their usage in drivers and monitors + a new rate limiter (for items)

See merge request ndk/ndk-fpga!77
  • Loading branch information
jakubcabal committed Nov 4, 2024
2 parents cf4d634 + c18ee6e commit 8c4af13
Show file tree
Hide file tree
Showing 11 changed files with 445 additions and 146 deletions.
25 changes: 18 additions & 7 deletions comp/mvb_tools/storage/fifox/cocotb/cocotb_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# cocotb_test.py:
# Copyright (C) 2024 CESNET z. s. p. o.
# Author(s): Ondřej Schwarz <[email protected]>
# Daniel Kondys <[email protected]>
#
# SPDX-License-Identifier: BSD-3-Clause

Expand All @@ -11,18 +12,20 @@
from cocotb.triggers import RisingEdge, ClockCycles
from cocotbext.ofm.mvb.drivers import MVBDriver
from cocotbext.ofm.mvb.monitors import MVBMonitor
from cocotbext.ofm.ver.generators import random_packets
from cocotbext.ofm.ver.generators import random_integers
from cocotb_bus.drivers import BitDriver
from cocotb_bus.scoreboard import Scoreboard
from cocotbext.ofm.utils.throughput_probe import ThroughputProbe, ThroughputProbeMvbInterface
from cocotbext.ofm.base.generators import ItemRateLimiter
from cocotbext.ofm.mvb.transaction import MvbTrClassic


class testbench():
def __init__(self, dut, debug=False):
self.dut = dut
self.stream_in = MVBDriver(dut, "RX", dut.CLK)
self.backpressure = BitDriver(dut.TX_DST_RDY, dut.CLK)
self.stream_out = MVBMonitor(dut, "TX", dut.CLK)
self.stream_out = MVBMonitor(dut, "TX", dut.CLK, tr_type=MvbTrClassic)

self.throughput_probe = ThroughputProbe(ThroughputProbeMvbInterface(self.stream_out), throughput_units="items")
self.throughput_probe.set_log_period(10)
Expand Down Expand Up @@ -51,17 +54,25 @@ async def reset(self):


@cocotb.test()
async def run_test(dut, pkt_count=10000, item_width=1):
async def run_test(dut, pkt_count=10000):
# Start clock generator
cocotb.start_soon(Clock(dut.CLK, 5, units="ns").start())
tb = testbench(dut, debug=False)
# Change MVB drivers IdleGenerator to ItemRateLimiter
# Note: the RateLimiter's rate is affected by backpressure (DST_RDY).
# Eventhough it takes into account cycles with DST_RDY=0, the desired rate might not be achievable.
idle_gen_conf = dict(random_idles=True, max_idles=5, zero_idles_chance=50)
tb.stream_in.set_idle_generator(ItemRateLimiter(rate_percentage=30, **idle_gen_conf))
await tb.reset()
tb.backpressure.start((1, i % 5) for i in itertools.count())

for transaction in random_packets(item_width, item_width, pkt_count):
tb.model(transaction)
cocotb.log.debug(f"generated transaction: {transaction.hex()}")
tb.stream_in.append(transaction)
data_width = tb.stream_in.item_widths["data"]
for transaction in random_integers(0, 2**data_width-1, pkt_count):
cocotb.log.debug(f"generated transaction: {hex(transaction)}")
mvb_tr = MvbTrClassic
mvb_tr.data = transaction
tb.model(mvb_tr)
tb.stream_in.append(mvb_tr)

last_num = 0

Expand Down
16 changes: 16 additions & 0 deletions comp/mvb_tools/storage/fifox/cocotb/prepare.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/sh

ROOT_PATH=../../../../..

PKG_COCOTBEXT_OFM=$ROOT_PATH/python/cocotbext/

# Python virtual environment
python -m venv venv-fifox
source venv-fifox/bin/activate

python -m pip install setuptools
python -m pip install $PKG_COCOTBEXT_OFM

echo ""
echo "Now activate environment with:"
echo "source venv-fifox/bin/activate"
21 changes: 13 additions & 8 deletions comp/mvb_tools/storage/mvb_hash_table_simple/cocotb/cocotb_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from cocotbext.ofm.utils.servicer import Servicer
from cocotbext.ofm.utils.device import get_dtb
from cocotbext.ofm.utils.math import ceildiv
from cocotbext.ofm.mvb.transaction import MvbTrClassic

import itertools
from math import log2
Expand Down Expand Up @@ -117,7 +118,7 @@ def load_file(self, path: str, params: dict) -> (dict, list, list, dict):
out_keys.append(mvb_key)
out_data[mvb_key] = data

table.append([h, (mvb_key << (self.stream_out._item_width * 8 + 1)) + ((data << 1) + 1)])
table.append([h, (mvb_key << ((self.stream_out.item_widths["data"] // 8) * 8 + 1)) + ((data << 1) + 1)])

out_config.append(table)

Expand Down Expand Up @@ -175,9 +176,9 @@ async def run_test(dut, config_file: str = "test_configs/test_config_1B.yaml", c
cocotb.log.debug(f"TABLE_CAPACITY: {table_capacity}")

"""Asserting that the read configuration match configuration of the drivers connected to the component."""
assert mvb_items == tb.stream_in._items
assert mvb_key_width_bytes == tb.stream_in._item_width
assert data_out_width_bytes == tb.stream_out._item_width
assert mvb_items == tb.stream_in.items
assert mvb_key_width_bytes == tb.stream_in.item_widths["data"] // 8 # FIXME
assert data_out_width_bytes == tb.stream_out.item_widths["data"] // 8 # FIXME
assert hash_width == log2(table_capacity)

"""Loading configuration from a config file"""
Expand Down Expand Up @@ -237,19 +238,23 @@ async def run_test(dut, config_file: str = "test_configs/test_config_1B.yaml", c
for transaction in random_packets(item_width, item_width, pkt_count):
int_transaction = int.from_bytes(transaction, "little")

mvb_tr = MvbTrClassic()
if int_transaction in model_keys:
tb.model((model_data[int_transaction].to_bytes(data_out_width_bytes, 'little'), 1))
mvb_tr.data = model_data[int_transaction]
vld = 1
else:
tb.model(((0).to_bytes(data_out_width_bytes, 'little'), 0))
mvb_tr.data = 0
vld = 0
tb.model((mvb_tr, vld))

cocotb.log.info(f"generated transaction: {transaction.hex()}")
tb.stream_in.append(transaction)

last_num = 0

while (tb.stream_out.item_cnt < pkt_count):
if (tb.stream_out.item_cnt // 1000) > last_num:
last_num = tb.stream_out.item_cnt // 1000
if (num := tb.stream_out.item_cnt // 1000) > last_num:
last_num = num
cocotb.log.info(f"Number of random transactions processed: {tb.stream_out.item_cnt}/{pkt_count}")
await ClockCycles(dut.CLK, 100)

Expand Down
27 changes: 22 additions & 5 deletions comp/mvb_tools/storage/mvb_hash_table_simple/cocotb/monitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,35 @@
# SPDX-License-Identifier: BSD-3-Clause

from cocotbext.ofm.mvb.monitors import MVBMonitor
from cocotbext.ofm.mvb.transaction import MvbTrClassic


class MVB_HASH_TABLE_SIMPLE_Monitor(MVBMonitor):
_signals = ["data", "match", "vld", "src_rdy", "dst_rdy"]

def receive_data(self, data, offset):
def recv_bytes(self, vld):
data_val = self.bus.data.value
data_val.big_endian = False
data_bytes = data_val.buff

match_val = self.bus.match.value
match_val.big_endian = False

self.log.debug(f"MATCH: {match_val}")

if match_val[offset] == 1:
self._recv((data[offset*self._item_width:(offset+1)*self._item_width], 1))
else:
self._recv((self._item_width * b'\x00', 0))
item_bytes = self._item_widths["data"] // 8
for i in range(self._items):
# Mask and shift the Valid signal per each Item
if vld & 1:
if match_val & 1:
# Getting the data slice (Item) from the "bytes" transaction
data_b = data_bytes[i*item_bytes : (i+1)*item_bytes]
# Converting the data slice (Item) to the MvbTrClassic object
mvb_tr = MvbTrClassic.from_bytes(data_b)
self._recv((mvb_tr, 1))
else:
mvb_tr = MvbTrClassic()
mvb_tr.data = 0
self._recv((mvb_tr, 0))
match_val >>= 1
vld >>= 1
11 changes: 6 additions & 5 deletions comp/mvb_tools/storage/mvb_hash_table_simple/prepare.sh
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
#!/bin/sh

OFM_PATH=../../../../../ofm
ROOT_PATH=../../../../..

#swbase=../../../swbase/
swbase=git+https://github.com/CESNET/ndk-sw.git#subdirectory=

PKG_PYNFB=${swbase}pynfb/
PKG_LIBNFBEXT_PYTHON=${swbase}ext/libnfb_ext_python/
PKG_COCOTBEXT_OFM=$OFM_PATH/python/cocotbext/
PKG_COCOTBEXT=$ROOT_PATH/python/cocotbext/

# Python virtual environment
python -m venv venv-cocotb
Expand All @@ -18,9 +18,10 @@ python -m pip install pylibfdt fdt
python -m pip install scapy
python -m pip install colorama
python -m pip install pyyaml
python -m pip install $PKG_PYNFB
python -m pip install $PKG_LIBNFBEXT_PYTHON
python -m pip install $PKG_COCOTBEXT_OFM
python -m pip wheel -w ./cocotbwheels $PKG_PYNFB
python -m pip install --find-links ./cocotbwheels nfb
python -m pip install --find-links ./cocotbwheels $PKG_LIBNFBEXT_PYTHON
python -m pip install $PKG_COCOTBEXT

echo ""
echo "Now activate environment with:"
Expand Down
88 changes: 88 additions & 0 deletions python/cocotbext/cocotbext/ofm/base/generators.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from random import choices

from .transaction import Transaction, IdleTransaction


Expand Down Expand Up @@ -105,3 +107,89 @@ def put(self, transaction, **kwargs):
# decrease current rate with the expected target rate to maintain value near zero
ir -= items * self._target_rate
self._current_rate = 0 if ir < 0 else ir


class ItemRateLimiter(IdleGenerator):
"""
Limit throughput to achieve specified rate by generating IdleTransactions.
Supply the "random_idles" argument to generate IdleTransactions with a grain of randomness.
If unsupplied or False, IdleTransactions are generated quite periodically (depends on dst_rdy).
Expects:
rate_percentage (int): throughput percentage.
E.g., rate_percentage=90 means 90% throughput (10% IdleTransactions).
When set to 0 (default), IdleTransactions are generated at random.
Optional (kwargs):
'random_idles' (bool): Return the number of IdleTransactions with a grain of randomness, yet still trying to preserve the ratio.
If unsupplied or False, IdleTransactions are generated quite periodically (dependant on dst_rdy).
Irrelevant when `rate_percentage` is set to 0 (=IdleTransactions are generated at random).
'max_idles' (int): Maximum number of IdleTransactions that can be returned by the `get` method.
Used only when `rate_percentage` is set to 0 (=IdleTransactions are generated at random).
'zero_idles_chance' (int): Probability of the `get` method returning 0 IdleTransactions compared to all other values.
This is to reduce the number of IdleTransactions.
Expected values are between (inclusive) 0 and 100 (100 results in full-speed).
When `max_idles` is set to 0, `zero_idles_chance` is automatically set to 100.
Used only when `rate_percentage` is set to 0 (=IdleTransactions are generated at random).
"""

def __init__(self, rate_percentage=0, **kwargs):
super().__init__()

self._rate_percentage = rate_percentage
self._idles_random = kwargs.get("random_idles", True)
self._max_idles = kwargs.get("max_idles", 5)
self._zero_idles_chance = kwargs.get("zero_idles_chance", 50)

self._target_rate_ratio = self._rate_percentage / 100
self._idle_rate_ratio = 1 - self._target_rate_ratio
self._idle_transactions = 0
self._total_transactions = 0

def configure(self, **kwargs):
super().configure(**kwargs)

self._rate_percentage = kwargs.get("rate_percentage", self._rate_percentage)
self._idles_random = kwargs.get("random_idles", self._idles_random)
self._max_idles = kwargs.get("max_idles", self._max_idles)
self._zero_idles_chance = kwargs.get("zero_idles_chance", self._zero_idles_chance)
if self._max_idles == 0:
self._zero_idles_chance = 100

def get(self, transaction, **kwargs):

# Return random number of Idle transactions (=random throughput)
if self._rate_percentage == 0:
# The number of Idle transactions is in range (0, self._max_idles)
# with 100-self._zero_idles_chance % chance of returning 0.
idles = [i for i in range(0, self._max_idles)]
wghts = [(100 - self._zero_idles_chance) // max(len(idles), 1)] * len(idles)
return choices(population=[0, *idles], weights=[self._zero_idles_chance, *wghts], k=1)[0]

## Calculate the amount of Idle Transactions to uphold the set ratio (throughput)
# The basic formula:
# self._idle_rate_ratio = self._idle_transactions / self._total_transactions
# When sending (using the put function) non-idle transactions,
# self._total_transactions increments and I need to find value x, which
# represents the number of idle transactions to send to preserve the set ratio.
# Hence:
# self._idle_rate_ratio = (self._idle_transactions + x) / self._total_transactions
# Find the value of x (+truncate) like so:
x = int(self._idle_rate_ratio * self._total_transactions - self._idle_transactions)
if self._idles_random:
if x < 0:
return 0
else:
# This allows to return +1 more than is actually calculated.
return choices(population=range(x+2), weights=None, k=1)[0]
else:
return max(0, x)

def put(self, transaction, **kwargs):
items_count = kwargs.get("items", 1)

if isinstance(transaction, IdleTransaction):
self._idle_transactions += items_count

self._total_transactions += items_count
Loading

0 comments on commit 8c4af13

Please sign in to comment.