rusteda-compute: the timing propagation task¶
The timing task (module timing of rusteda-compute) propagates arrival times, transition times
and predecessors forward over a levelized timing graph. A static timing engine lowers its timing
graph, the timing tables of its libraries, its start values and its tag maps into the task's
interface: flat arrays of plain records, borrowable from mapped tables. The task never sees
names, netlists, Liberty files or constraints. Its decisions are recorded in
decisions.md; the device layer and the backend kinds it builds on are in
README.md.
Notation: n nodes, T tags, tr the transition (0 rise, 1 fall), an the analysis (0 early,
reduced with the minimum; 1 late, reduced with the maximum). For analysis an, a value x is
better than y when x < y (early) or x > y (late), and the absent value is +inf
(early) or -inf (late). The task is unit-agnostic; engines lower times in ns and capacitances in
pF, the units in which COMPUTE-REQ-0019 states the single-precision bound.
COMPUTE-REQ-0010 Task and backends. TimingPropagationBackend is the task's trait; its
backends are cpu (pure Rust, f64, always compiled, authoritative) and cuda (feature cuda,
f64 by default, f32 on request). timing_backend(kind, precision) selects one:
- Cpu is the CPU backend; it computes in f64 only, and F32 with Cpu is unavailable;
- Cuda is the CUDA backend in the requested precision. It is unavailable when the build lacks the
feature cuda, or when the default CUDA device fails its preparation (the device self-test of
COMPUTE-REQ-0001, once per process);
- Gpu is the first compiled GPU backend (in the order of GpuRuntime::ALL) whose device passes
its preparation; it is unavailable when there is none.
Selection never falls back silently: an unavailable backend is the error of COMPUTE-REQ-0025, never another backend. The engine chooses the backend; the task has no default and no automatic choice.
COMPUTE-REQ-0011 Graph interface. LevelizedTimingCsr holds:
- nodes numbered 0..n by topological ID: level l holds the nodes level_ptr[l] ..
level_ptr[l+1], so that level_ptr starts at 0, never decreases and ends at n;
- the incoming arcs of node v, in_arcs[in_ptr[v] .. in_ptr[v+1]] (in_ptr has n + 1
entries, starts at 0, never decreases and ends at the arc count). An InArc (12 bytes) is
{src, class, map}: the source node, an index into the arc classes (COMPUTE-REQ-0013) and an
index into the tag maps or NONE (u32::MAX, the identity map). Every source lies on a lower
level than its destination: for a node of level l, src < level_ptr[l]. Within a node, the
arcs are sorted by source, never decreasing; the order of arcs with the same source is the
engine's canonical arc order. The CSR order is the only tie-break of the task;
- the outgoing adjacency out_dst[out_ptr[u] .. out_ptr[u+1]]: the distinct destinations of the
arcs whose source is u, increasing (out_ptr has n + 1 entries);
- the tag maps of COMPUTE-REQ-0012.
Records are #[repr(C)] plain old data in host byte order (bytemuck::Pod), and the arrays are
Cow slices, so that a table mapped from disk can be borrowed without a copy.
COMPUTE-REQ-0012 Tag maps. Map m is the record range maps[map_ptr[m] .. map_ptr[m+1]]
(map_ptr is empty or [0] when there are no maps; otherwise it starts at 0, never decreases and
ends at the record count). A TagMapRec (80 bytes) is {src_tag, dst_tag, shift, open[2],
close[2], close_slew[2], limit[2]}, the arrays indexed by analysis: arrivals of tag src_tag at
the arc's source enter tag dst_tag at its destination, shifted by shift (a change of time
origin between the tags), limited to a window from open (the opening edge) up to the closing
edge close minus the setup time of the arc's class at the closing-edge transition close_slew,
and at most limit (COMPUTE-REQ-0018). open = -inf, close = +inf and limit = +inf disable
the respective bound. A latch data-to-output arc re-launches in the opening-edge tag of its
enable this way; an exception state change (a -through point) is a record with a shift of 0 and
no window. An arc whose map is NONE keeps every tag.
COMPUTE-REQ-0013 Library interface. CompiledLutBank holds one corner's tables:
- luts: LutDesc (40 bytes) {n[3], off[3], arg[3], voff}. Axis k has n[k] points at
data[off[k] ..], strictly increasing, and is indexed by the canonical argument arg[k] (0, 1
or 2, COMPUTE-REQ-0014); n[k] = 0 marks an absent axis, and the axes in use come first (0 to
3 axes). The values start at data[voff], one value for a table without axes, otherwise
n[0] * n[1] * n[2] values (absent axes count 1) in row-major order: the last axis varies
fastest;
- data: axis points and values;
- classes: ArcClass (28 bytes) {flags, delay[2], slew[2], setup[2]}. Flag bit 0 (WIRE)
marks a wire arc; flag bit 1 + 2 ti + to lets input transition ti produce output
transition to (positive unate: rise to rise and fall to fall; negative unate: the crossed
pair; non-unate: all four; a disabled arc: none). delay[to] and slew[to] are the delay and
output transition tables of an output transition, setup[ti] the setup tables of the tag-map
window per data transition; every entry is a table index or NONE (a table of value 0). A wire
class has no delay or transition tables and only the bits rise to rise and fall to fall. Unate,
non-unate, three-state and latch arcs are cell classes that differ only in their bits and
tables.
COMPUTE-REQ-0014 Table evaluation. A table is evaluated at three canonical arguments
(x0, x1, x2): for the delay and output transition tables of an arc into node v, the
transition of the source pin, the load of v and again the load of v; for the setup tables of a
tag-map window, the record's close_slew (related pin), the transition of the source pin
(constrained pin) and the load of v (the output of the latch). Axis k with points p of count
m and argument x = x[arg[k]] gives the interval (i0, i1) and weight w: when m = 1,
(0, 0) and w = 0; otherwise i0 is the smallest index with i0 + 2 >= m or x <= p[i0+1],
i1 = i0 + 1 and w = (x - p[i0]) / (p[i1] - p[i0]), which extrapolates linearly outside the
points. With V(i, j, k) the values, the result is, in exactly this order of operations:
- no axis: V();
- one axis: V(i0) + (V(i1) - V(i0)) * w0;
- two axes: lo = V(i0,j0) + (V(i0,j1) - V(i0,j0)) * w1, hi = V(i1,j0) + (V(i1,j1) -
V(i1,j0)) * w1, result lo + (hi - lo) * w0;
- three axes: C(i, j) = V(i,j,k0) + (V(i,j,k1) - V(i,j,k0)) * w2 for the four pairs, then the
two-axis formula on C.
Each operation is one rounded IEEE 754 operation (round to nearest even), without contraction
into fused multiply-adds. A table index NONE evaluates to 0.
COMPUTE-REQ-0015 Corner inputs. Besides the graph and the bank, a propagation takes:
- ntags, the tag count T >= 1;
- load[v][tr * 2 + an], the load of node v for its output transition tr and analysis an;
- the start rows StartRows {node, arr, slew}: the nodes with start values, strictly increasing;
row k holds the arrivals arr[(k * T + t) * 4 + tr * 2 + an] and the transitions
slew[k * 4 + tr * 2 + an] of node[k]. Absent entries are allowed; a present arrival needs a
present transition of the same transition and analysis;
- the wire records: none (ideal wires), or one WireRec (64 bytes) {delay[4], slew[4]} per
incoming arc in CSR order, indexed to * 2 + an. A wire arc adds delay to the arrival and
degrades the transition s to s when the term k = slew is 0 and to sqrt(s * s + k * k)
otherwise; the records of other arcs are all zero.
TimingStateSoA {ntags, load, starts, arr, slew, pred} carries these inputs with the values of
COMPUTE-REQ-0016 for the single-call form (COMPUTE-REQ-0023).
COMPUTE-REQ-0016 Values. The values of a propagation are, for every node:
- the arrivals arr[((v * T + t) * 2 + tr) * 2 + an] per tag, transition and analysis;
- the transitions slew[(v * 2 + tr) * 2 + an] per pin: one per transition and analysis, shared
by all tags;
- the predecessors pred, indexed like the arrivals: src | (ti << 31) for the source node and
source transition of the arc whose candidate set the arrival, and NO_PRED (u32::MAX) for an
arrival that is a start value or absent. For an arc with a tag map, the source tag is the
src_tag of a record of the arc's map whose dst_tag is the arrival's tag.
Absent values are the absent value of their analysis. Every stored value is canonical: a zero is
stored as +0.0, never as -0.0, so that two values are equal exactly when their bits are equal.
COMPUTE-REQ-0017 Propagation. Levels are processed in increasing order; the nodes of a level
read only values of lower levels, so they may be computed in any order and in parallel. Node v:
1. takes its start row, when it has one, as its initial transitions and arrivals (predecessors
NO_PRED), and absent values otherwise;
2. then visits its incoming arcs in CSR order; for each arc (source u, class c), each input
transition ti and output transition to (in increasing order) that the class allows, and each
analysis an (early first) for which the source's transition s = slew(u, ti, an) is present:
- evaluates, with L = load(v, to, an): for a wire class, the delay d and transition q of
its wire record (COMPUTE-REQ-0015; d = 0, q = s without records); for a cell class, d
from table delay[to] and q from table slew[to], a negative q clamped to 0
(transitions clamp at 0, delays do not);
- replaces slew(v, to, an) by q when q is better;
- offers the arrivals of COMPUTE-REQ-0018 to the arrivals of v at (to, an): a candidate
replaces the current arrival only when it is better, so that among equal candidates the
start value and then the earliest in this order win, and sets the predecessor to
u | (ti << 31).
COMPUTE-REQ-0018 Arrivals through an arc. For the identity map, each tag t in increasing
order whose source arrival a = arr(u, t, ti, an) is present offers a + d to tag t. For a
tag map, each record in record order whose source arrival a = arr(u, src_tag, ti, an) is present
computes m = a + shift, then:
- if m < open[an], the record offers nothing;
- the upper bound h is limit[an]; when close[an] is finite, h becomes close[an] - S if
that is smaller, where S is the arc class's table setup[ti] (COMPUTE-REQ-0014);
- if m > h, then m = h;
and offers m + d to tag dst_tag.
COMPUTE-REQ-0019 Determinism and consistency. The values are a function of the inputs alone:
- every backend and every thread count gives the same values; the cpu backend and the cuda
backend in f64 agree bit for bit (arrivals, transitions and predecessors), with no reduction
that depends on the order of execution, and kernels compiled without fused multiply-add
contraction (COMPUTE-REQ-0028);
- repeated propagations of the same inputs give the same bits on every backend and precision;
- the cuda backend in f32 (tables, inputs and values rounded to single precision) agrees with
the cpu backend within max(1e-4 ns, 2e-5 |v|) for every value v with |v| <= 100 ns and
agrees on which values are absent, except where a candidate lies within that bound of a window
bound of a tag map (a near-tie of the window decision); its predecessors may differ where
candidates lie within that bound of each other.
Tolerance reason: single precision has a 24-bit significand, a relative rounding of 6e-8 per
operation; arrival sums over paths of a few hundred arcs and table interpolations stay within
2e-5 of the magnitude, and 1e-4 ns covers values near zero.
COMPUTE-REQ-0020 Validation. TimingProblem::validate() accepts a problem only when every
statement of COMPUTE-REQ-0011 to COMPUTE-REQ-0015 about it holds: array lengths and pointer
sequences, every index in range, sources on lower levels, sources sorted within a node, the
outgoing adjacency equal to the transposed incoming arcs, maps' tags below T, class flags with
only defined bits and a wire class without tables or inverting bits, table axes in use first and
strictly increasing, start nodes strictly increasing and below n, present arrivals with present
transitions, wire records of other arcs zero, no NaN in any value, finite table data, loads,
shifts and closing-edge transitions, open < +inf, close > -inf, limit > -inf, and fewer
than 2^31 nodes. A violation is ComputeError::Invalid naming the first offending item. Every
backend validates a problem when it opens a session, and every patch (COMPUTE-REQ-0021) before it
applies it, so that no kernel reads out of bounds.
COMPUTE-REQ-0021 Sessions. open(problem) validates the problem and makes it resident (on the
device for a GPU backend) as a TimingSession. propagate(request) computes the values of a
scope:
- All: every node;
- FromLevel(l): every node of level l and above, from the session's resident values below;
- Cone(seeds): the forward cone of the seed nodes and of every node patched since the last
propagation, level by level: a node of the cone is recomputed; when its arrivals, transitions
or predecessors differ in any bit from its resident values, they are replaced and the
destinations of its outgoing arcs join the cone; otherwise the cone stops at it.
FromLevel and Cone need resident values: a previous complete propagation, or values written
with write_values; otherwise, and after a cancelled propagation, they are rejected. The result of
every scope equals that of All on the same inputs when the resident values were complete for
the inputs before the patches. The patches set_loads(node, load), set_arc_classes(arc index,
class) and set_starts(start rows) change the inputs of a session; the patched nodes (the node,
the arc's destination, and the nodes whose start row changed in any bit) join the next cone.
changed() lists, after a Cone, the nodes whose values were replaced, increasing.
COMPUTE-REQ-0022 Readback and residency. A GPU backend keeps the inputs and the values of a
session on the device between levels and between propagations, and copies values to the host view
values() only as requested: Readback::All, None, Rows(nodes) (the rows of the listed
nodes: arrivals, transitions and predecessors) or Changed (the rows the propagation replaced:
after Cone the changed nodes, after FromLevel(l) the nodes from level l on, after All
every node). read(readback) copies later. A row read with Rows equals the same row read with
All. The CPU backend computes in place, so its host view is always complete.
COMPUTE-REQ-0023 Single call. The trait's provided method propagate_levels(graph, lut_bank,
state, deterministic), the signature of the development plan, opens a session on the inputs
carried by the state (COMPUTE-REQ-0015, ideal wires), propagates All, reads back All into the
state's arr, slew and pred, and returns the statistics.
COMPUTE-REQ-0024 Statistics. Every propagation returns PropagationStats: the backend kind
(Cpu or Cuda), the device description, the precision, whether the reductions were
deterministic (always true for the backends of this specification; a request with deterministic =
false permits order-dependent reductions but does not require them), the levels visited, the
kernel launches, the nodes evaluated and changed (for All and FromLevel every evaluated node
counts as changed), max_abs_ns, the largest magnitude of a present value among the evaluated
rows (0 when there is none), and the wall-clock seconds of upload, kernels, readback and the
total.
COMPUTE-REQ-0025 Errors. ComputeError and its envelope codes (rusteda.v1):
ComputeError |
Envelope code | Diagnostic |
|---|---|---|
NotCompiled, Unavailable |
BACKEND_UNAVAILABLE (exit 1) |
A0501 |
TooLarge (device memory or index limits) |
INPUT_REJECTED |
A0502 |
Compile (kernel compilation) |
INTERNAL |
A0503 |
Device (driver, launch or execution) |
INTERNAL |
A0504 |
Invalid (problem or patch), on lowered inputs |
INTERNAL |
A0505 |
Invalid, on inputs mapped from a table |
CORRUPT_TABLE |
A0505 |
Cancelled |
CANCELLED |
A0506 |
Diagnostic codes A0500 to A0599 belong to rusteda-compute. An error never comes with values: a
failed propagation leaves the session without resident values.
COMPUTE-REQ-0026 Cancellation and progress. A request carries a job context (JOB-REQ-0002):
propagation checks the cancel token before each level and stops with Cancelled, and reports
progress propagate per level (JOB-REQ-0004).
COMPUTE-REQ-0027 CPU backend. The cpu backend computes the nodes of a level on the threads of
the calling thread's rayon pool, each node sequentially as COMPUTE-REQ-0017 states, in f64.
Levels with few nodes run on the calling thread. The backend computes in the host view directly,
and its sessions borrow the problem's arrays until a patch copies the patched array.
COMPUTE-REQ-0028 CUDA backend. The cuda backend runs the kernels of
timing/kernels/timing.cu, compiled by NVRTC per precision: -DREAL_F64 with --fmad=false for
f64, and single precision with the default options otherwise (COMPUTE-REQ-0002,
COMPUTE-REQ-0004). It launches one kernel per level in which each thread computes one node as
COMPUTE-REQ-0017 states; a cone launch skips the nodes outside the cone and compares each
recomputed row with the resident one. Each session has its own stream. Before uploading, the
backend compares the memory a session needs with the free device memory and reports TooLarge
when it does not fit. Full readback goes through page-locked staging memory that is not
write-combined.
COMPUTE-REQ-0029 Preparation. prewarm(kind, precision) selects the backend
(COMPUTE-REQ-0010) and compiles its kernels for the precision, so that an engine can overlap the
preparation with other work on a second thread (COMPUTE-REQ-0003); its result is the result the
selection would give.
Prepared nonlinear arcs¶
COMPUTE-REQ-0030: TimingProblem may carry one 128-byte ArcValues per incoming arc, with f64 delay/slew indexed (input transition, output transition, bound). Prepared values override cell lookup and wire degradation. They are immutable snapshots of loads, starts and classes: session patch methods must reject edits until the caller re-prepares the model. Validate size, finite values and nonnegative transitions. CPU/CUDA use identical data and agree bitwise in f64. Include these buffers in host/device budgets; test a three-node case plus extracted-engine propagation without enumerating timing paths.