Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bittide-instances/bittide-instances.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ library
Bittide.Instances.Hitl.Utils.Program
Bittide.Instances.Hitl.Utils.Serial
Bittide.Instances.Hitl.Utils.Ugn
Bittide.Instances.Hitl.Utils.UgnGrooming
Bittide.Instances.Hitl.Utils.Usb
Bittide.Instances.Hitl.Utils.Utils
Bittide.Instances.Hitl.Utils.Vivado
Expand Down Expand Up @@ -325,6 +326,7 @@ test-suite unittests
Tests.Bittide.Instances.Hitl.Utils.MemoryMap
Tests.Bittide.Instances.Hitl.Utils.OpenOcd
Tests.Bittide.Instances.Hitl.Utils.Serial
Tests.Bittide.Instances.Hitl.Utils.UgnGrooming
Tests.ClockControlWb
Wishbone.AddressableBytesWb
Wishbone.Axi
Expand All @@ -349,6 +351,7 @@ test-suite unittests
clash-prelude-hedgehog,
clash-protocols-memmap,
clash-vexriscv,
containers,
directory,
extra,
filepath,
Expand Down
27 changes: 22 additions & 5 deletions bittide-instances/src/Bittide/Instances/Hitl/GenericDemo/Core.hs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import Protocols
import Bittide.CaptureUgn (captureUgns, sendUgn)
import Bittide.ClockControl (SpeedChange)
import Bittide.ClockControl.CallistoSw (SwcccInternalBusses, callistoSwClockControlC)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you have a look at the commit message? The general take is that it doesn't really convey information that's important.

import Bittide.ClockControl.Ugn.Corrections (correctionsWb)
import Bittide.DoubleBufferedRam (wbStorage)
import Bittide.ElasticBuffer (fromData, xilinxElasticBufferWb)
import Bittide.Extra.Maybe (toMaybe)
Expand All @@ -76,6 +77,7 @@ import Bittide.ProcessingElement (
import Bittide.RingBuffer (receiveRingBuffer, transmitRingBuffer)
import Bittide.SharedTypes (Bitbone, BitboneMm)
import Bittide.Sync (Sync)
import Bittide.TimedReset (timedResetWb)
import Bittide.Wishbone (readDnaPortE2WbWorker, timeWb, uartBytes, uartInterfaceWb)
import Clash.Class.BitPackC (ByteOrder)
import Clash.Cores.Xilinx (withXilinx)
Expand Down Expand Up @@ -105,8 +107,10 @@ type FifoSize = 5 -- = 2^5 = 32
- `timeWb`
- DNA
- UART
- UGN grooming corrections
- application reset (TimedReset)
-}
type NmuInternalBusses = 3 + PeInternalBusses
type NmuInternalBusses = 5 + PeInternalBusses

{- Busses per link:
- Elastic buffer
Expand Down Expand Up @@ -144,6 +148,10 @@ type UserCoreCircuit userCoreBusses muRemBusWidth =
Enable Bittide ->
Signal Bittide (Unsigned 64) ->
Signal Bittide (Maybe (BitVector 96)) ->
-- Application reset, released by the management unit's 'timedResetWb' at a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update documentation after switching to programmable mux.

-- chosen local-counter cycle (the UGN-grooming relabel). Demos that have an
-- application counter gate it on this reset; others may ignore it.
Reset Bittide ->
Circuit
( Vec userCoreBusses (ToConstBwd Mm, Bitbone Bittide muRemBusWidth)
, "RXS2_RAW" ::: CSignal Bittide (Vec LinkCount (BitVector 64))
Expand Down Expand Up @@ -201,6 +209,7 @@ managementUnit ::
Circuit
(ToConstBwd Mm.Mm, Jtag dom)
( Df dom (BitVector 8)
, "APP_RESET" ::: Reset dom
, Vec
(NmuExternalBusses userCoreBusses)
( ToConstBwd Mm.Mm
Expand All @@ -211,16 +220,24 @@ managementUnit externalCounter maybeDna =
circuit $ \(mm, jtag) -> do
-- Core and interconnect
allBusses <- processingElement NoDumpVcd muConfig -< (mm, jtag)
([timeBus, uartBus, dnaBus], restBusses) <- Vec.split -< allBusses
([timeBus, uartBus, dnaBus, correctionsBus, timedResetBus], restBusses) <-
Vec.split -< allBusses

-- Peripherals
_localCounter <- timeWb (Just externalCounter) -< timeBus
(uartOut, _uartStatus) <-
uartInterfaceWb d16 d16 uartBytes -< (uartBus, Fwd (pure Nothing))
readDnaPortE2WbWorker maybeDna -< dnaBus
-- Host-written UGN grooming corrections, polled and applied by the MU CPU.
correctionsWb (SNat @LinkCount) -< correctionsBus
-- Application reset for the demo's user core: the management unit chooses the
-- local-counter cycle at which the application leaves reset (the UGN-grooming
-- relabel). The reset lives here, in the layer around the user core; the
-- application counter it gates lives in the user core itself.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the UGN-grooming relabel

The what now?

appReset <- timedResetWb externalCounter -< timedResetBus

-- Output
idC -< (uartOut, restBusses)
idC -< (uartOut, appReset, restBusses)

core ::
forall userCoreBusses ringBufferDepth.
Expand Down Expand Up @@ -262,7 +279,7 @@ core bufferDepth mkUserCore (refClk, refRst) (bitClk, bitRst, bitEna) rxClocks r
localCounter = register bitClk bitRst bitEna 0 (localCounter + 1)

-- Start management unit
(muUartBytesBittide, muWbAll) <-
(muUartBytesBittide, Fwd appReset, muWbAll) <-
withBittideClockResetEnable managementUnit localCounter maybeDna -< (muMm, muJtag)
(ebWbs, muWbs1) <- Vec.split -< muWbAll
(rxBufferBusses, muWbs2) <- Vec.split -< muWbs1
Expand Down Expand Up @@ -329,7 +346,7 @@ core bufferDepth mkUserCore (refClk, refRst) (bitClk, bitRst, bitEna) rxClocks r

-- Start user core: post-handshake stage drives the GTH-TX wire.
Fwd txsOut <-
mkUserCore bitClk bitRst bitEna localCounter maybeDna
mkUserCore bitClk bitRst bitEna localCounter maybeDna appReset
-< ( extraMuBusses
, Fwd (bundle rxs2Raw)
, Fwd (bundle handshakesOut.toNeighbors)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ ringBufferDepth :: SNat RingBufferDepth
ringBufferDepth = SNat

mkUserCore :: UserCoreCircuit UserCoreBusses (NmuRemBusWidth UserCoreBusses)
mkUserCore _bitClk _bitRst _bitEna _localCounter _maybeDna =
mkUserCore _bitClk _bitRst _bitEna _localCounter _maybeDna _appReset =
circuit $ \(emptyUserCoreBusses, _rxs2Raw, handshakeOut) -> do
[] <- idC -< emptyUserCoreBusses
idC -< handshakeOut
81 changes: 81 additions & 0 deletions bittide-instances/src/Bittide/Instances/Hitl/Utils/Gdb.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,20 @@ module Bittide.Instances.Hitl.Utils.Gdb where

import Prelude

import Clash.Prelude (Signed, Unsigned, Vec, checkedFromIntegral, toList)

import Bittide.Hitl (DeviceInfo (..))
import Bittide.Instances.Hitl.Setup (LinkCount)
Comment on lines +9 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See other comment in this module: this module shouldn't contain device-specific functions.

import Bittide.Instances.Hitl.Utils.Driver (getTargetIndex)
import Bittide.Instances.Hitl.Utils.MemoryMap (getPathAddress)
import Bittide.Instances.Hitl.Utils.OpenOcd (TapInfo (..))
import Bittide.Wishbone (TimeCmd (Capture))
import Control.Monad (forM_)
import GHC.Stack (HasCallStack)
import Gdb (Gdb)
import Project.FilePath
import Project.Handle (expectRight)
import Protocols.MemoryMap (MemoryMap)
import System.FilePath ((</>))
import Vivado.Tcl (HwTarget)

Expand All @@ -26,3 +35,75 @@ initGdb hitlDir binName gdb tapInfo (hwT, _d) = do
Gdb.setTimeout gdb Nothing
Gdb.runCommand gdb "echo connected to target device"
pure ()

-- * GDB-based component HAL accessors

--
-- Read/write a component's registers over GDB, addressed through a 'MemoryMap'. Generic
-- across demos (the memory map is a parameter). All require the CPU to be halted.

-- | Read the current local-counter value from a device's @Timer@ peripheral.
readCurrentTime :: (HasCallStack) => MemoryMap -> (HwTarget, DeviceInfo) -> Gdb -> IO (Unsigned 64)
readCurrentTime mm (_, d) gdb = do
putStrLn $ "Getting current time from device " <> d.deviceId
commandAddress <- expectRight $ getPathAddress mm ["0", "Timer", "command"]
scratchAddress <- expectRight $ getPathAddress mm ["0", "Timer", "scratchpad"]
Gdb.writeLe gdb commandAddress Capture
Gdb.readLe gdb scratchAddress
Comment on lines +45 to +52

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function already exists in WireDemo. Could you move them to their own module per memory mapped device? Also see issue #1336.


{- | Write a node's reset-release cycle to its @TimedReset@ peripheral. Once the local
counter exceeds this value the node's application (and its application counter) leave
reset.
-}
writeReleaseCycle :: (HasCallStack) => MemoryMap -> Gdb -> Unsigned 64 -> IO ()
writeReleaseCycle mm gdb releaseCycle = do
addr <- expectRight $ getPathAddress mm ["0", "TimedReset", "release_cycle"]
Gdb.writeLe gdb addr releaseCycle

{- | Write the per-link frame corrections and set the @valid@ flag on the
@UgnCorrections@ peripheral. Resuming the CPU makes the management unit apply the
corrections to its elastic buffers.
-}
writeCorrections :: (HasCallStack) => MemoryMap -> Gdb -> Vec LinkCount (Signed 64) -> IO ()
writeCorrections mm gdb corrections = do
correctionsAddr <- expectRight $ getPathAddress mm ["0", "UgnCorrections", "corrections"]
validAddr <- expectRight $ getPathAddress mm ["0", "UgnCorrections", "valid"]
Gdb.writeLe gdb correctionsAddr corrections
Gdb.writeLe gdb validAddr True

{- | Read the per-link hardware UGN counter captures from the @CaptureUgns@ peripheral:
the @(localCounter, remoteCounter)@ pair per link, with @localCounter@ already adjusted
for the elastic-buffer frames added/removed since capture (@elastic_buffer_delta@).
-}
readHardwareUgns ::
(HasCallStack) => MemoryMap -> (HwTarget, DeviceInfo) -> Gdb -> IO [(Unsigned 64, Unsigned 64)]
readHardwareUgns mm (_, d) gdb = do
let
getUgnRegister reg = expectRight $ getPathAddress mm ["0", "CaptureUgns", reg]

-- Adjust the local counter for the frames added/removed from the elastic
-- buffer after capturing the UGN. Leaves the remote counter untouched.
adjustLocalCounter :: (Unsigned 64, Unsigned 64, Signed 32) -> (Unsigned 64, Unsigned 64)
adjustLocalCounter (localCounter, remoteCounter, delta) =
(addSigned localCounter delta, remoteCounter)
where
addSigned :: Unsigned 64 -> Signed 32 -> Unsigned 64
addSigned u s = checkedFromIntegral (toInteger u + toInteger s)

putStrLn $ "Getting UGNs for device " <> d.deviceId
localCounters <- Gdb.readLe @(Vec LinkCount (Unsigned 64)) gdb =<< getUgnRegister "local_counter"
remoteCounters <- Gdb.readLe @(Vec LinkCount (Unsigned 64)) gdb =<< getUgnRegister "remote_counter"
deltas <- Gdb.readLe @(Vec LinkCount (Signed 32)) gdb =<< getUgnRegister "elastic_buffer_delta"
let ugnTriples = zip3 (toList localCounters) (toList remoteCounters) (toList deltas)
forM_ ugnTriples $ \triple -> putStrLn $ "Raw UGN triple: " <> show triple
pure $ adjustLocalCounter <$> ugnTriples

{- | Read the relabeled application counter exposed by the user core's
@AppCounterReadback@ peripheral. With the application running, @localCounter -
app_counter@ equals the node's reset-release cycle, letting the host verify the relabel
landed on hardware.
-}
readAppCounter :: (HasCallStack) => MemoryMap -> (HwTarget, DeviceInfo) -> Gdb -> IO (Unsigned 64)
readAppCounter mm (_, _) gdb = do
addr <- expectRight $ getPathAddress mm ["0", "AppCounterReadback", "app_counter"]
Gdb.readLe gdb addr
25 changes: 25 additions & 0 deletions bittide-instances/src/Bittide/Instances/Hitl/Utils/Ugn.hs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,31 @@ timingOracleToUgnEdge oracle =
, ugn = bitCoerce oracle.dstCounter - bitCoerce oracle.srcCounter
}

{- | Turn a per-node, per-link table of UGN values (@λ@) into 'UgnEdge's keyed by DNA
node id. The outer vector is FPGA-indexed (the capturing / destination node); each
inner vector is link-indexed (matching 'knownLinkConfigs').
-}
ugnEdges :: CP.Vec FpgaCount (CP.Vec LinkCount (Signed 64)) -> [UgnEdge]
ugnEdges lams =
Comment on lines +214 to +219

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably take the map as an argument and move to bittide.

L.concat (CP.toList (CP.imap nodeEdges lams))
where
nodeEdges dstIndex links = CP.toList (CP.imap (edge dstIndex) links)
edge dstIndex port lam =
UgnEdge
{ srcNode = indexToNodeId ((knownLinkConfigs CP.!! dstIndex) CP.!! port)
, srcPort = port
, dstNode = indexToNodeId dstIndex
, dstPort = port
, ugn = lam
}

{- | As 'ugnEdges', but from raw @(localCounter, remoteCounter)@ captures: the UGN is
their signed difference @local - remote@.
-}
hardwareUgnEdges ::
CP.Vec FpgaCount (CP.Vec LinkCount (Unsigned 64, Unsigned 64)) -> [UgnEdge]
hardwareUgnEdges = ugnEdges . CP.map (CP.map (\(l, r) -> bitCoerce l - bitCoerce r))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you use numConvert?


{- | Compare hardware and software UGN edges with detailed difference reporting.

Performs edge-by-edge comparison and reports:
Expand Down
140 changes: 140 additions & 0 deletions bittide-instances/src/Bittide/Instances/Hitl/Utils/UgnGrooming.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
-- SPDX-FileCopyrightText: 2026 Google LLC
--
-- SPDX-License-Identifier: Apache-2.0

{- |
The application-facing UGN grooming layer for HITL: it adapts the generic grooming
algorithm ("Bittide.ClockControl.Ugn.Grooming", on "Bittide.Graph.Weighted") to the HITL
'UgnEdge' type and turns its result into a directly-applyable 'RelabelPlan'.

Nodes are identified by their DNA-derived 'BitVector' 32 id (the @srcNode@ / @dstNode@
fields of a 'UgnEdge'), so the graph is keyed directly on those ids. Edges are matched
between the measured @λ@ and stored @λ^safe@ snapshots by their @(srcNode, dstNode)@ pair
— unique because the demo topology is a complete graph (one link per ordered node pair).

@Signed 64@ UGN values are widened to 'Integer' for the (overflow-prone) relaxation, and
the resulting reset offsets / frame counts are narrowed back with a bounds check.
Comment on lines +6 to +16

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

He talked a lot, but said nothing.

-}
module Bittide.Instances.Hitl.Utils.UgnGrooming (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright. After reading through this whole commit, I feel that UgnGrooming.hs should move to bittide. The same goes for Utils/Ugn.hs. The only reason they now exist in bittide-instances, is a dependency on LinkCount and FpgaCount, which can just as well be solved by polymorphism.

There is also a bunch of duplication, though I feel that that will solve itself if you just move everything to bittide. This is especially obvious in the tests for this module, as they largely correspond to the ones for Bittide.ClockControl.Ugn.Grooming.

ugnGraph,
safeMargin,
canonicalizeUgn,
RelabelPlan (..),
computeRelabel,
) where

import Prelude

import Clash.Prelude (BitVector, Signed, Vec, checkedFromIntegral)

import Bittide.ClockControl.Ugn.Grooming (GroomResult (..), groomCorrection)
import Bittide.Graph.Weighted (Graph)
import Bittide.Instances.Hitl.Setup (FpgaCount, LinkCount, knownLinkConfigs)
import Bittide.Instances.Hitl.Utils.Ugn (UgnEdge (..), indexToNodeId)

import qualified Bittide.Graph.Weighted as G
import qualified Clash.Prelude as CP
import qualified Data.Map.Strict as Map

{- | Build a weighted graph from UGN edges, keyed on the DNA-derived node ids with
the (signed) UGN as the 'Integer' edge weight.
Comment on lines +39 to +40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{- | Build a weighted graph from UGN edges, keyed on the DNA-derived node ids with
the (signed) UGN as the 'Integer' edge weight.
{- | Build a weighted graph from UGN edges, where nodes are represented
based on their DNA (@BitVector 32@).

-}
ugnGraph :: [UgnEdge] -> Graph (BitVector 32) Integer
ugnGraph es = G.fromEdges [(e.srcNode, e.dstNode, toInteger e.ugn) | e <- es]
Comment on lines +42 to +43

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this function move to Bittide.Instances.Hitl.Utils.Ugn.edgesToGraph?


{- | Add a safety margin @ε@ to every edge: @λ^safe = λ + ε@. Mirrors the
record-update style of 'Bittide.Instances.Hitl.Utils.Ugn.addLatencyEdge'.
-}
safeMargin :: Signed 64 -> [UgnEdge] -> [UgnEdge]
safeMargin eps = map (\e -> e{ugn = e.ugn + eps})

Comment on lines +45 to +50

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. The documentation isn't great. (Lekker boeien that you use a record-update style that's similar to some other random function.)
  2. safeMargin isn't a great name. Perhaps addToUgn / addToUgns.
  3. It feels like this should be moved to Ugn.hs.

{- | Relabel UGN edges to their minimal non-negative form: the Bellman-Ford reduced
costs @λ_{i->j} + q_i - q_j@ (potentials @q@ from the super-source), which are all
@>= 0@ and leave every round-trip / cycle sum unchanged.

This is a pure gauge change (a relabeling), so it represents the /same/ physical
system, but in a gauge where the UGNs are small and non-negative. Storing
@λ^safe@ in this gauge keeps the application's per-link UGNs (and hence its fixed
schedule) inside the working range a depth-bounded mux/elastic-buffer needs, while
still honouring the true /asymmetric/ cycle constraints (unlike a symmetric
midpoint, which is only a valid relabeling on an acyclic graph).
Comment on lines +51 to +60

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{- | Relabel UGN edges to their minimal non-negative form: the Bellman-Ford reduced
costs @λ_{i->j} + q_i - q_j@ (potentials @q@ from the super-source), which are all
@>= 0@ and leave every round-trip / cycle sum unchanged.
This is a pure gauge change (a relabeling), so it represents the /same/ physical
system, but in a gauge where the UGNs are small and non-negative. Storing
@λ^safe@ in this gauge keeps the application's per-link UGNs (and hence its fixed
schedule) inside the working range a depth-bounded mux/elastic-buffer needs, while
still honouring the true /asymmetric/ cycle constraints (unlike a symmetric
midpoint, which is only a valid relabeling on an acyclic graph).
{- | Relabel UGN edges to their minimal non-negative form.


If the edges are not physically allowed (a negative cycle), they are returned
unchanged.
Comment on lines +62 to +63

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it return an Either String [UgnEdge].

martijn@volthe:~$ python3 -c "import this"  | head -n 12 | tail -n 1
Errors should never pass silently.

-}
canonicalizeUgn :: [UgnEdge] -> [UgnEdge]
canonicalizeUgn es =
Comment on lines +65 to +66

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear to me whether this is here because of your work on Manticore, or whether it's fundamentally needed for the demo in this PR. Could you clarify?

case G.potentials (ugnGraph es) of
Nothing -> es
Just q ->
[ e{ugn = checkedFromIntegral (toInteger e.ugn + qOf e.srcNode - qOf e.dstNode)}
| e <- es
]
where
qOf n = Map.findWithDefault 0 n q

{- | The result of grooming a measured boot onto a target: per-node reset offsets and
per-node, per-link frame corrections, both FPGA-/link-indexed and ready to apply.
-}
data RelabelPlan = RelabelPlan
{ resetOffsets :: Vec FpgaCount (Signed 64)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this just an absolute clock cycle? What's this all about a shared base all of a sudden?

{- ^ Per-node offset to add to the shared base to get its reset-release cycle. This
realises the relabel @q@: every node's application counter leaves reset in the
relabeled domain. Gauged so node 0's offset is 0.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to relate to the canonicalization step, but that's not clear to me.

-}
, corrections :: Vec FpgaCount (Vec LinkCount (Signed 64))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is derived from framesToInsert. The record that houses that field also has a field called corrrection, which -- confusingly -- doesn't correspond to this corrections.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a Signed 64? None of the underlying algorithms can produce negative numbers.

{- ^ Per-node, per-link frame corrections: the frames the management unit inserts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overly complex.

(@> 0@) or removes (@< 0@) on each receiving link to reach @λ^safe@ after relabeling.
-}
}
deriving (Show)

{- | Groom this boot's measured UGNs onto the target @λ^safe@ and produce an applyable
'RelabelPlan': per-node reset offsets (the relabel @q@) and per-link frame corrections.
Returns 'Left' (with the witnessing node ids) if the UGNs changed too much to fit under
@λ^safe@ (a negative cycle in the slack graph).

Calls 'groomCorrection' (Bellman-Ford, per the UGN grooming slides) directly on the
measured / target 'ugnGraph's. The relabel @q@ is the per-node potential that fits this
boot's @λ@ under the stored @λ^safe@ (@λ + B^T q <= λ^safe@); the residual
@frames = λ^safe - (λ + B^T q)@ are the small, non-negative per-link corrections the
elastic buffers insert. So:

* @resetOffsets_i = -(q_i - q_0)@ — the reset release that applies the relabel
(gauged so node 0 is 0); and
* @corrections@ — the per-receiving-link frames. Since 'groomCorrection' returns frames
keyed by @(srcNode, dstNode)@, the receiving @(fpga, port)@ is reconstructed via
'knownLinkConfigs' (the same link map 'Bittide.Instances.Hitl.Utils.Ugn.ugnEdges'
uses).

When @λ^safe@ is stored in a small non-negative gauge (see 'canonicalizeUgn'), the relabel
absorbs this boot's counter offsets so the groomed app-frame UGNs land back on @λ^safe@ —
small, non-negative, and identical every run, which is what makes a single fixed schedule
reusable.

Edges are matched by @(srcNode, dstNode)@; pass the full measured + target graphs to
groom every link.
Comment on lines +92 to +116

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay...

  1. Don't mention Bellman-Ford
  2. Don't mention the slides
  3. Don't explain the fields of a different data type.
  4. It's not clear why the word "gauge" keeps cropping up everywhere.
  5. Documentation is not self-contained, points to a lot of concepts which the reader is just supposed to find on their own.
  6. The last sentence doesn't add anything.

In general it's a lot of prose which only achieves confusion. Just call it what it is: a call to groomCorrection and a group by FPGA index.

-}
computeRelabel ::
-- | Measured UGNs @λ@
[UgnEdge] ->
-- | Target UGNs @λ^safe@
[UgnEdge] ->
Either [BitVector 32] RelabelPlan
computeRelabel measured target =
case groomCorrection (ugnGraph measured) (ugnGraph target) of
Infeasible ns -> Left ns
Feasible{correction = q, framesToInsert} ->
Right
RelabelPlan
{ resetOffsets = (\idx -> negate (qOf idx - qOf 0)) <$> CP.indicesI
, corrections = (\idx -> (\port -> frameOf idx port) <$> CP.indicesI) <$> CP.indicesI
}
where
qOf idx = checkedFromIntegral (Map.findWithDefault 0 (indexToNodeId idx) q)
frameByEdge = Map.fromList [((i, j), f) | (i, j, f) <- framesToInsert]
frameOf dstIdx port =
checkedFromIntegral (Map.findWithDefault 0 (srcNode, dstNode) frameByEdge)
where
srcNode = indexToNodeId ((knownLinkConfigs CP.!! dstIdx) CP.!! port)
dstNode = indexToNodeId dstIdx
Loading