Skip to content

Commit

Permalink
First draft of Core
Browse files Browse the repository at this point in the history
Fuzzing time!
  • Loading branch information
Liamolucko committed Dec 4, 2024
1 parent e7bd8d2 commit 1aa2a51
Show file tree
Hide file tree
Showing 11 changed files with 996 additions and 89 deletions.
4 changes: 4 additions & 0 deletions net_finder/core/base_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
from amaranth.utils import ceil_log2


def next_power_of_two(n: int):
return 1 << ceil_log2(n)


def net_size(max_area: int):
"""Returns the width/height of the net."""

Expand Down
859 changes: 815 additions & 44 deletions net_finder/core/core.py

Large diffs are not rendered by default.

104 changes: 99 additions & 5 deletions net_finder/core/main_pipeline.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import enum

from amaranth import *
from amaranth.lib import wiring
from amaranth.lib import data, wiring
from amaranth.lib.memory import ReadPort
from amaranth.lib.wiring import In, Out
from amaranth.utils import ceil_log2
Expand All @@ -10,12 +10,92 @@
from net_finder.core.net import Net, shard_depth

from .base_types import instruction_layout, net_size
from .core import FINDERS_PER_CORE, run_stack_entry_layout
from .memory import ChunkedMemory
from .neighbour_lookup import neighbour_lookup_layout
from .skip_checker import SkipChecker, undo_lookup_layout
from .utils import pipe

FINDERS_PER_CORE = 4


def instruction_ref_layout(max_area: int):
"""Returns the layout of an instruction reference."""

return data.StructLayout(
{
# The index of the instruction's parent in the run stack.
"parent": ceil_log2(max_area),
# The index of this instruction in its parent's list of valid children.
#
# If the index is past the end of that list, it represents the last valid
# child. Then we always store the last valid child as 11, so that when
# backtracking we can immediately see 'oh this is the last one, so we need to
# move onto the next instruction'.
"child_index": 2,
}
)


def max_potential_len(max_area: int):
"""
Returns the maximum number of potential instructions there can be at any given
time.
"""

# The upper bound of how many potential instructions there can be is if every
# square on the surfaces, except for the ones set by the first instruction, has
# 4 potential instructions trying to set it: 1 from each direction.
#
# While this isn't actually possible, it's a nice clean upper bound.
#
# TODO: I think we can reduce this to 4 + 2 * (max_run_stack_len - 1), since:
# - The first instruction can produce at most 4 potential instructions.
# - Each instruction after that:
# - Reduces the max. potential instructions by one (since we'd previously
# pessimistically assumed it was a potential instruction, but now clearly it
# isn't because we've run it)
# - Increases the max. potential instructions by 3.
# - So in total, it increases the maximum by 2.
return 4 * (max_area - 1)


def max_decisions_len(max_area: int):
"""Returns the maximum number of decisions there can be at any given time."""

# There's always 1 decision for the first instruction, then the upper bound is
# that every square has 4 instructions setting it, 3 of which we decided not to
# run and the last one we did.
#
# TODO: smaller upper bound:
#
# Say you have a list of decisions.
#
# If it's of maximal length, it should have max_run_stack_len 1s.
#
# The first 1 produces at most 4 instructions, and the rest produce at most 3:
# so then the maximum number of decisions is 4 + 3 * (max_run_stack_len - 1).
return 1 + 4 * (max_area - 1)


def run_stack_entry_layout(cuboids: int, max_area: int):
"""Returns the layout of a run stack entry."""

return data.StructLayout(
{
# The instruction that was run.
"instruction": instruction_layout(cuboids, max_area),
# A reference to where in the run stack this instruction originally came from.
"source": instruction_ref_layout(max_area),
# Whether this instruction's child in each direction was valid at the time this
# instruction was run.
"children": 4,
# The number of potential instructions there were at the point when it was run.
"potential_len": ceil_log2(max_potential_len(max_area) + 1),
# The index of the decision to run this instruction in the list of decisions.
"decision_index": ceil_log2(max_decisions_len(max_area)),
}
)


def child_index_to_direction(children: int, child_index: int) -> int | None:
"""
Expand Down Expand Up @@ -99,6 +179,15 @@ def __init__(self, cuboids: int, max_area: int):
"task": In(Task),
# The run stack entry we're operating on.
"entry": In(run_stack_entry_layout(cuboids, max_area)),
# Whether the instruction to advance/check is a child of `entry`, not
# `entry.instruction` itself.
#
# This should always be 0 when backtracking.
#
# This is almost always 1 when advancing/checking: the only exception is when
# running the first instruction, since it gets handed to us from outside and
# doesn't have a parent.
"child": In(1),
# The index of the child of `self.entry` we're operating on (if we're advancing or
# checking).
"child_index": In(2),
Expand All @@ -118,10 +207,14 @@ def __init__(self, cuboids: int, max_area: int):
shape=ul_layout.shape,
)
).array(cuboids - 1),
# Signals coming from VC stage.
#
# The instruction the pipeline ended up operating on - so, the neighbour of
# `entry` when advancing/checking, and `entry.instruction` itself when
# backtracking.
"instruction": Out(instruction_layout(cuboids, max_area)),
# Signals coming from WB stage.
#
# Whether or not `instruction` was valid.
"instruction_valid": Out(1),
# Whether or not the neighbours of `instruction` in each direction were valid.
Expand Down Expand Up @@ -157,6 +250,7 @@ def elaborate(self, platform) -> Module:
nl_start_mapping_index = self.start_mapping_index
nl_task = self.task
nl_entry = self.entry
nl_child = self.child
nl_child_index = self.child_index
nl_clear_index = self.clear_index

Expand Down Expand Up @@ -186,7 +280,7 @@ def elaborate(self, platform) -> Module:
wiring.flipped(self.neighbour_lookups[i]),
)
m.d.comb += neighbour_lookup.input.eq(nl_entry.instruction)
m.d.comb += neighbour_lookup.t_mode.eq(nl_task != Task.Backtrack)
m.d.comb += neighbour_lookup.t_mode.eq(nl_child)
m.d.comb += neighbour_lookup.direction.eq(nl_child_direction)

# Valid check (VC) stage
Expand Down Expand Up @@ -235,12 +329,12 @@ def elaborate(self, platform) -> Module:
m.d.comb += skip_checker.fixed_family.eq(vc_fixed_family)
m.d.comb += skip_checker.transform.eq(vc_transform)

m.d.comb += self.instruction.eq(vc_middle)

# Write back (WB) stage
#
# This is the stage where we write back any changes that were made to the net
# and surfaces.
#
# This occurs at the same time as the outer pipeline's IF stage.

wb_finder = pipe(m, vc_finder)
wb_task = pipe(m, vc_task)
Expand Down
55 changes: 39 additions & 16 deletions net_finder/core/memory.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from itertools import chain

from amaranth import *
from amaranth.hdl import ShapeLike, ValueLike
from amaranth.lib import wiring
Expand Down Expand Up @@ -56,12 +58,13 @@ def __init__(self, *, shape: ShapeLike, depth: int, chunks: int):
self._depth = depth
self._chunks = chunks

self._read_ports: list[PureInterface] = []
self._sdp_ports: list[tuple[PureInterface, PureInterface]] = []
self._read_ports: list[tuple[PureInterface, str]] = []
self._write_ports: list[PureInterface] = []
self._sdp_ports: list[tuple[tuple[PureInterface, str], PureInterface]] = []

super().__init__({})

def read_port(self) -> PureInterface:
def read_port(self, domain="sync") -> PureInterface:
# Return a disconnected interface, which we then add to an array and hook up
# during `elaborate`.
port = ChunkedReadPortSignature(
Expand All @@ -70,11 +73,24 @@ def read_port(self) -> PureInterface:
shape=self._shape,
).create()

self._read_ports.append(port)
self._read_ports.append((port, domain))

return port

def sdp_port(self) -> tuple[PureInterface, PureInterface]:
def write_port(self) -> PureInterface:
# Return a disconnected interface, which we then add to an array and hook up
# during `elaborate`.
port = ChunkedWritePortSignature(
chunk_width=ceil_log2(self._chunks),
addr_width=ceil_log2(self._depth),
shape=self._shape,
).create()

self._write_ports.append(port)

return port

def sdp_port(self, read_domain="sync") -> tuple[PureInterface, PureInterface]:
# Return disconnected interfaces, which we then add to an array and hook up
# during `elaborate`.
read_port = ChunkedReadPortSignature(
Expand All @@ -88,7 +104,7 @@ def sdp_port(self) -> tuple[PureInterface, PureInterface]:
shape=self._shape,
).create()

self._sdp_ports.append((read_port, write_port))
self._sdp_ports.append(((read_port, read_domain), write_port))

return read_port, write_port

Expand All @@ -106,7 +122,7 @@ def elaborate(self, platform) -> Module:

# Give the chunk a port corresponding to each of our outer ports, and hook up
# their inputs.
for port_index, (read_port, write_port) in enumerate(self._sdp_ports):
for port_index, ((read_port, _), write_port) in enumerate(self._sdp_ports):
inner_read_port = chunk.read_port()
inner_write_port = chunk.write_port()

Expand All @@ -123,18 +139,25 @@ def elaborate(self, platform) -> Module:

inner_sdp_read_ports[port_index].append(inner_read_port)

for port_index, port in enumerate(self._read_ports):
inner_port = chunk.read_port()
for port_index, (port, domain) in enumerate(self._read_ports):
inner_port = chunk.read_port(domain=domain)
m.d.comb += inner_port.addr.eq(port.addr)
inner_read_ports[port_index].append(inner_port)

# Connect up the SDP read ports' outputs.
for (port, _), inner_ports in zip(self._sdp_ports, inner_sdp_read_ports):
m.d.comb += port.data.eq(Array(inner_ports)[port.chunk].data)

# Connect up the regular read ports' outputs.
for port, inner_ports in zip(self._read_ports, inner_read_ports):
m.d.comb += port.data.eq(Array(inner_ports)[port.chunk].data)
for port_index, port in enumerate(self._write_ports):
inner_port = chunk.write_port()
m.d.comb += inner_port.addr.eq(port.addr)
m.d.comb += inner_port.data.eq(port.data)
m.d.comb += inner_port.en.eq(port.en & (port.chunk == chunk_index))

# Connect up the read ports' outputs.
for (port, domain), inner_ports in zip(
chain(self._read_ports, (r for r, _ in self._sdp_ports)),
chain(inner_read_ports, inner_sdp_read_ports),
):
chunk = Signal.like(port.chunk)
m.d[domain] += chunk.eq(port.chunk)
m.d.comb += port.data.eq(Array(inner_ports)[chunk].data)

return m

Expand Down
4 changes: 2 additions & 2 deletions net_finder/core/net.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from amaranth.lib.wiring import In, Out
from amaranth.utils import ceil_log2

from .base_types import PosLayout, PosView, net_size
from .base_types import PosLayout, PosView, net_size, next_power_of_two
from .memory import ChunkedMemory
from .utils import pipe

Expand All @@ -13,7 +13,7 @@ def shard_depth(max_area: int):
net_size_ = net_size(max_area)
# We need to round one of the dimensions up to the next power of two in order
# for concatenating the x and y coordinates to work properly.
return (net_size_ << ceil_log2(net_size_)) // 4
return net_size_ * next_power_of_two(net_size_) // 4


def neighbour_shards(m: Module, pos: PosView):
Expand Down
4 changes: 2 additions & 2 deletions net_finder/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
from amaranth.hdl import ValueLike


def pipe(m: Module, input: ValueLike) -> Signal:
def pipe(m: Module, input: ValueLike, **kwargs) -> Signal:
# src_loc_at tells Signal how far up in the call chain to look for what to name
# the signal: so, setting it to 1 means we want it to use the name of the
# variable the caller's assigning our result to.
output = Signal.like(input, src_loc_at=1)
output = Signal.like(input, src_loc_at=1, **kwargs)
m.d.sync += output.eq(input)
return output
1 change: 1 addition & 0 deletions net_finder/soc/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ def __init__(self, cuboids: list[Cuboid], n: int):
# Whether or not each core is splittable (is active and has a `base_decision` of
# `splittable_base`).
splittable = Cat(
# TODO: base_decision is garbage while sending, so this decision-making might be a bit off.
cores_active[i] & (core.base_decision == splittable_base)
for i, core in enumerate(cores)
)
Expand Down
24 changes: 9 additions & 15 deletions shell.nix
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,17 @@ let
in
pkgs.mkShell {
venvDir = ".venv";
packages =
[
pkgs.python311.pkgs.venvShellHook
packages = [
pkgs.python311.pkgs.venvShellHook

openocd
pkgs.yosys
openocd
pkgs.yosys

# Needed by Verilator simulations
pkgs.json_c
pkgs.libevent
pkgs.zlib
]
++ pkgs.lib.optionals pkgs.stdenv.isDarwin [
# Needed by Rust code
pkgs.libiconv
pkgs.darwin.apple_sdk.frameworks.CoreFoundation
];
# Needed by Verilator simulations
pkgs.json_c
pkgs.libevent
pkgs.zlib
];

postVenvCreation = ''
${pkgs.uv}/bin/uv pip install -r requirements.txt
Expand Down
3 changes: 2 additions & 1 deletion src/bin/dump_neighbours.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! Dumps the information required by `test_neighbour_lookup.py` as JSON to stdout.
//! Dumps the information required by `test_neighbour_lookup.py` as JSON to
//! stdout.
use std::io;
use std::time::Duration;
Expand Down
12 changes: 10 additions & 2 deletions src/geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,14 @@ impl Net<bool> {
self.color_with_cache(cuboid, &square_cache)
}

// TODO: make a variant of `color` that returns all the colorings, then filters
// them down to the ones that are actually different: that is, renumber all the
// faces in the order they occur so that mapping to different faces doesn't make
// a difference, and then return the ones that are distinct under that
// representation. I think that's a good definition of different foldings:
// if you fold along the same lines every time, you should get the same result,
// and same coloring = same lines.

/// Return a version of this net with its squares 'colored' with which faces
/// they're on.
///
Expand Down Expand Up @@ -1695,8 +1703,8 @@ impl Class {
.unwrap()
}

/// Returns the list of all the transformations you can perform to get from the
/// root of this class's family to this class.
/// Returns the list of all the transformations you can perform to get from
/// the root of this class's family to this class.
pub fn alternate_transforms(
self,
cache: &SquareCache,
Expand Down
Loading

0 comments on commit 1aa2a51

Please sign in to comment.