Skip to main content

Graph

Struct Graph 

Source
pub struct Graph { /* private fields */ }
Expand description

The graph arena. Stores every node added during a computation-building session. Nodes are append-only and indexed by NodeId.

Placement metadata (DeviceLocation per node) lives in a side-table so existing Node { ... } construction sites don’t need to be modified. This is the Phase-1 shape: placements are opt-in, inert hints that the executor may validate but does not yet act on. Default is implemented manually (not derived) so that every graph — however it is created — mints a fresh GraphId from the monotonic counter. A derived Default would hand out a zeroed id, so two Graph::default() graphs would claim the same identity and affinity diagnostics would report nonsense.

Implementations§

Source§

impl Graph

Source

pub fn new() -> Self

Create an empty graph.

Source

pub fn id(&self) -> GraphId

This graph’s process-unique GraphId. Used by affinity diagnostics to name which graphs disagree; see NodeHandle::graph_id.

Source

pub fn target_backend(&self, id: NodeId) -> Option<BackendId>

Look up the resolved target backend for a node, if any has been set. Returns None if dispatch resolution hasn’t happened yet for this node — common during the migration because old op-builder paths don’t call the resolver.

Phase 7.5 B2.

Source

pub fn set_target_backend(&mut self, id: NodeId, backend: BackendId)

Record the resolved target backend for id. Set once at DAG construction time (or first-realize time, depending on the dispatch policy chosen in Phase B3). Idempotent: calling twice with the same value is fine; calling twice with different values overwrites.

Phase 7.5 B2.

Source

pub fn target_backend_count(&self) -> usize

Number of nodes with a resolved target backend. Mostly for tests and diagnostics.

Source

pub fn storage_class(&self, id: NodeId) -> StorageClass

The storage class of id: the explicit override if one was set via Graph::set_storage_class, else the op-inferred default (infer_storage_class). Every node has a well-defined class.

Phase D substrate (PR-D1).

Source

pub fn set_storage_class(&mut self, id: NodeId, class: StorageClass)

Record an explicit storage-class override for id, replacing the op-inferred default. Used for nodes whose op does not determine the class — chiefly KV-cache placeholder Op::Consts, which are session state rather than shared weights.

Phase D substrate (PR-D1).

Source

pub fn has_storage_class_override(&self, id: NodeId) -> bool

Whether id carries an explicit storage-class override (as opposed to relying on the op-inferred default).

Source

pub fn storage_class_override_count(&self) -> usize

Number of explicit storage-class overrides. Mostly for tests and diagnostics.

Source

pub fn layout(&self, id: NodeId) -> Layout

The Layout associated with id. If the side-table has no explicit entry, returns Layout::contiguous(shape) — every Node logically has a Layout; the side-table is just the sparse storage for the strided / offset cases.

Phase 7.5 storage-unification: metadata-only view ops (Op::Transpose, Op::Permute, Op::BroadcastTo, 0-copy Op::Reshape) populate this side-table during graph construction. Kernel ops produce contiguous results and rely on the fallback.

Source

pub fn set_layout(&mut self, id: NodeId, layout: Layout)

Record an explicit Layout for id. The Layout’s shape() must equal nodes[id].shape — otherwise the graph becomes incoherent (downstream ops were validated against node.shape). Panics in debug_assertions mode on mismatch; in release the mismatch is silently kept because the alternative would require this method to return Result, which would break call ergonomics in graph-construction code.

Set this exactly when an op produces a strided / offset view over upstream Storage. For the common case (contiguous kernel output), don’t call this at all — Graph::layout returns Layout::contiguous(shape) as the implicit fallback.

Source

pub fn has_explicit_layout(&self, id: NodeId) -> bool

Whether id has an explicit Layout entry in the side-table (i.e. it’s a strided / offset view; not the default contiguous case).

Source

pub fn explicit_layout_count(&self) -> usize

Number of nodes with an explicit (non-contiguous) Layout. Mostly for tests and diagnostics.

Source

pub fn output_views(&self, id: NodeId) -> Option<&[OutputView]>

Borrow the per-slot output-view specs for id, if it’s been declared as a multi-output producer. Returns None for single-output nodes (the common case).

Op::View and Op::ViewOwned builders read this to derive the slot’s shape/dtype/layout at graph-build time.

Source

pub fn output_views_arc(&self, id: NodeId) -> Option<Arc<[OutputView]>>

Clone the Arc handle to id’s per-slot output-view specs. Used by the realization path to attach the same bundle metadata to the realized Storage without copying the slice.

Source

pub fn set_output_views( &mut self, id: NodeId, views: Arc<[OutputView]>, ) -> Result<(), Error>

Declare id as a multi-output producer with the given per-slot specs. Validates at graph-build time:

  1. id is in bounds.
  2. views is non-empty (a “multi-output” node with zero slots is a contract bug — use single-output if there’s nothing to project).
  3. Slot 0’s dtype equals nodes[id].dtype (the bundle’s primary dtype convention).
  4. Slot 0’s shape equals nodes[id].shape (slot 0 is the “primary” slot whose shape the single-output infrastructure already sees).
  5. Each slot’s layout.shape() equals the slot’s shape (Layout adds strides/offset; the visible shape is identical).

Idempotent on (id, views). Replacing an existing declaration asserts the new specs are byte-identical to the old in debug builds; in release the last write wins (matches the layouts side-table pattern).

Source

pub fn is_multi_output(&self, id: NodeId) -> bool

Whether id has been declared as a multi-output producer.

Source

pub fn multi_output_count(&self) -> usize

Number of nodes declared as multi-output producers. Mostly for tests and diagnostics.

Source

pub fn binds_data_determined_sym(&self, sym: SymId) -> bool

Whether some node in this graph produces (binds mid-realize) the runtime value sym — i.e. sym is data-determined, computed by an op during the pass rather than supplied up front. Used to tell a data-determined DynScalar::Sym (which is legitimately unbound in the compile-time SymEnv and must be resolved at execute time) apart from a caller error (a truly-unbound input-determined sym, which must surface as an error at realize). Today the only producer of a data-determined symbol is Op::NonZeroIndices (its count_sym); extend this match as more data-determined producers land.

Source

pub fn next_data_determined_sym(&self) -> SymId

The next data-determined SymId not yet used by any producer in this graph — one past the max Op::NonZeroIndices count_sym, or SymId(0) if none exist. Lets a builder that emits several data-determined producers (e.g. one NonZeroIndices per MoE expert, across several stacked layers on the same graph) allocate non-colliding count syms without threading an external fuel_ir::SymGen: each call reflects the producers already added, so successive layers claim strictly higher ranges.

This covers the data-determined space (the only producers of which are NonZeroIndices nodes). A graph that also mixes input-determined symbolic extents in the same low sym range should thread a shared SymGen instead — those live in a separate compile-time SymEnv this scan does not see.

Source

pub fn set_matmul_row_count(&mut self, id: NodeId, row_count: DynScalar)

Attach a data-determined row count to an Op::MatMul node — the sparse-MoE capacity-buffer support. The node’s Node::shape stays the row-capacity output shape; row_count (a DynScalar) is how many rows to actually compute. See NodeHandle::matmul_dyn_m.

Source

pub fn matmul_row_count(&self, id: NodeId) -> Option<DynScalar>

The data-determined row count for id, if it carries one (i.e. it is a dynamic-M matmul). None ⇒ an ordinary static-M matmul.

Source

pub fn storage_for(&self, id: NodeId) -> Option<Arc<RwLock<Storage>>>

Look up the realized storage for a node, if any has been registered. Returns an Arc clone — the slot stays in the map after this call.

Source

pub fn set_storage(&mut self, id: NodeId, storage: Arc<RwLock<Storage>>)

Register a realized-storage slot for id. Replaces any existing entry (callers responsible for ensuring this is the intended semantics — e.g. re-realization after eviction).

Source

pub fn set_storage_owned(&mut self, id: NodeId, storage: Storage)

Convenience: register an owned Storage directly. Wraps in Arc<RwLock<...>>.

Source

pub fn remove_storage(&mut self, id: NodeId) -> Option<Arc<RwLock<Storage>>>

Remove a slot. Used by eviction / Op::Release paths. Returns the Arc if present so callers that want to keep the bytes alive can hold it; otherwise drops on the floor and any outstanding Arc clones still see the bytes.

Source

pub fn has_storage(&self, id: NodeId) -> bool

Whether a slot is currently registered for id.

Source

pub fn storage_len(&self) -> usize

Number of registered storage slots.

Source

pub fn add_side_effect_root(&mut self, id: NodeId)

Register a NodeId as a side-effect root — a node whose output isn’t reachable from any user-requested root but whose execution the executor must still schedule. Used for destructive ops (e.g. Op::Release) emitted by graph-mutating rules: the rule needs Release to run (freeing device memory), but Release’s zero-element marker has no consumer.

The executor’s realize paths walk the user’s roots AND these side-effect roots via a single combined execution_plan call.

Source

pub fn side_effect_roots(&self) -> &[NodeId]

Snapshot the registered side-effect roots. Executor concatenates these with the user’s requested roots before computing the plan.

Source

pub fn len(&self) -> usize

The number of nodes currently in the graph.

Source

pub fn is_empty(&self) -> bool

Whether the graph has any nodes.

Source

pub fn node(&self, id: NodeId) -> &Node

Borrow a node by ID. Panics if the ID is out of bounds — it should not be possible for a valid NodeHandle handle to produce an invalid ID, so any such panic indicates a bug in graph construction.

Source

pub fn describe_node(&self, id: NodeId) -> String

Produce a one-line human-readable description of a node and its immediate inputs — the identifier string that executor panics prepend so realize-time failures can be localized to the exact graph position that blew up. Format is stable enough for grep but explicitly intended for humans reading panic output, not for parsing.

Source

pub fn push(&mut self, node: Node) -> NodeId

Append a node and return its fresh ID. Used by the NodeHandle builders, by opt passes that canonicalize or rewrite the graph by appending fresh nodes, and by external consumers (fuel-storage’s pipelined executor tests, custom-op authors) that need direct graph construction.

For metadata-only view ops (Op::Transpose, Op::Permute, Op::BroadcastTo, Op::Slice) this also populates the layouts side-table by deriving the output layout from the input’s layout via derive_view_output_layout. After this call, graph.layout(new_id) returns the strided/offset view layout without any further work — the side-table is the single source of truth for layout, with Layout::contiguous as the fallback for kernel-output nodes.

Misuse (malformed Node, dangling input ids, etc.) is caught at execute time as a typed error, not a panic — per project’s no-panic-in-production rule.

Source

pub fn collapse_variant_branch( &mut self, branch: NodeId, winner_arm: usize, ) -> Result<(), Error>

Collapse a decision point to a single baked arm — the graph-side half of optimize-time kernel-variant baking (04-optimization: “kernel-variant choice is largely baked at optimize time”). Given a finalized Op::Branch and the arm the optimizer chose (winner_arm, an index into the branch’s inputs), this rewrites the graph so the chosen arm is the realized route and the branch is no longer a decision point:

  1. The reconverge_at merge is rewired to read the winner’s exit (inputs[winner_arm]) in place of arm 0’s exit (inputs[0]). For winner_arm == 0 this is a no-op (the merge already reads arm 0, the runnability fallback).
  2. The branch’s inputs are reduced to [winner_exit] — a single-arm branch, which run::extract_runs_multi / non_chosen_arm_nodes / the route picker all treat as no decision (arms.len() < 2), so the winner’s region lowers inline and the non-chosen arms become orphaned (pruned from the reachable set — nothing reads them once the merge is rewired). The arm-0 runnability invariant is preserved: after the reduce, inputs[0] is the winner and the merge reads it.

This is the “the Branch collapses” resolution the constitution names for same-device kernel-variant choices (decomposed-region vs. a fused kernel on the same device), as distinct from placement choices (arms on different devices) which stay live Op::Branch decision points for the runtime route picker (06-runtime). The optimizer decides which branches are variant branches and computes winner_arm; this method only performs the structural collapse.

Never-panic: returns fuel_ir::Error::InvalidBranch on a non-Op::Branch node or an out-of-range winner_arm, mutating nothing. Idempotent on an already-collapsed (single-arm) branch when winner_arm == 0.

Source

pub fn set_placement(&mut self, id: NodeId, loc: DeviceLocation)

Tag a node with a target device. The executor will validate (post-Phase-1) that the node’s op can be evaluated on that device. In Phase 1 the tag is informational only.

Source

pub fn placement(&self, id: NodeId) -> Option<DeviceLocation>

Read a node’s placement hint, or None if the node inherits from the executor default.

Source

pub fn open_branch(&self, diverge: NodeId) -> BranchBuilder

Open a BranchBuilder rooted at a shared diverge point — the first half of the open_branch / add_arm / finalize_branches trio that constructs an Op::Branch (phi/merge) node with build-time validation (Phase A PR-A1 of the “plan IS the graph” rebuild).

diverge names the single node from which every candidate route (arm) departs; each arm’s exit (added via BranchBuilder::add_arm) becomes one inputs[i] of the emitted Op::Branch. The builder borrows nothing — it is a pure accumulator of NodeIds — so the graph stays free to keep building arms between open_branch and finalize_branches. All validation (descendant reconverge_at, internally-disjoint arms, cast-to-uniform shape/dtype, arm-0 runnability) happens in BranchBuilder::finalize_branches, which returns a typed fuel_ir::error::Error::InvalidBranch rather than panicking.

Source

pub fn verify_no_dangling(&self) -> Result<(), Error>

Verify that no NodeId reference in the graph points outside the arena — the post-compaction safety net for a missed remap. Checks every node’s inputs, the op-carried Op::Branch reconverge_at, every NodeId-keyed side-table, and the side_effect_roots vector. Returns the first offending reference as a typed error (never panics); used by crate::compact’s debug_assert and by tests.

Trait Implementations§

Source§

impl Debug for Graph

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for Graph

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

§

impl Freeze for Graph

§

impl RefUnwindSafe for Graph

§

impl Send for Graph

§

impl Sync for Graph

§

impl Unpin for Graph

§

impl UnsafeUnpin for Graph

§

impl UnwindSafe for Graph

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V