pub struct OpAttrs {Show 32 fields
pub scalars: Vec<f64>,
pub axis: Option<i64>,
pub perm: Vec<u8>,
pub target_shape: Vec<i64>,
pub dims: Vec<u8>,
pub cast_dtype: Option<String>,
pub slice_start: Option<u64>,
pub slice_len: Option<u64>,
pub roll_shift: Option<i64>,
pub pad_amounts: Vec<(u64, u64)>,
pub pad_mode: Option<u8>,
pub pad_value: Option<f64>,
pub keepdim: Option<bool>,
pub target_shape_rel: Option<ShapeExpr>,
pub slice_start_rel: Option<Dim>,
pub slice_len_rel: Option<Dim>,
pub axis_last: bool,
pub scalar_rel: Option<Dim>,
pub lhs_roles: Vec<u8>,
pub rhs_roles: Vec<u8>,
pub const_bits: Option<u64>,
pub slot_index: Option<u32>,
pub scan_role: Option<u8>,
pub scan_index: Option<u32>,
pub scan_n_carries: Option<u32>,
pub scan_n_xs: Option<u32>,
pub scan_bound: Option<u32>,
pub scan_emit: Option<u8>,
pub scan_early_exit: Option<bool>,
pub view_slot: Option<u32>,
pub rank0_target: bool,
pub fused_op: Option<String>,
}Expand description
Non-tensor attributes a PatternNode::Op carries (fkc-fusion-patterns
§3a.4; Baracuda reconcile §2). For scalar-param ops the value is not
baked — it identifies the slot the emitted extract: path points at, and
the matcher re-reads the live value from the matched graph node at match
time. Carries the load-bearing attributes the general vocabulary needs.
Fields§
§scalars: Vec<f64>Scalar value(s) for scalar-param ops (AddScalar/MulScalar/Clamp),
as the region’s snapshot of the slot (re-read live via extract:).
axis: Option<i64>Axis attribute for dim-bearing ops (reductions, Triu/Tril diagonal).
perm: Vec<u8>Permute/Transpose: the new axis order, ABSOLUTE — a permutation of
0..rank with out.axis[i] = in.axis[perm[i]] (matches Fuel
Op::Permute semantics exactly; Op::Transpose is the last-two-axes
special case). Empty ⇒ not a permuting node (a matcher wildcard). See
baracuda-layout-fusion-response.md F1/F2a.
target_shape: Vec<i64>BroadcastTo/Reshape: the target LOGICAL shape (the op’s output
shape); one field serves both (the OpTag disambiguates which op).
Empty ⇒ not a shape-target node (a matcher wildcard).
dims: Vec<u8>Squeeze/Unsqueeze: the affected dim list (0-based, output-rank terms).
Fuel’s single-dim Op::Squeeze/Op::Unsqueeze emit a one-element list.
Empty ⇒ not a squeeze/unsqueeze node (a matcher wildcard).
cast_dtype: Option<String>Cast target dtype as the stable DType::as_str() name (dep-free:
this crate can’t reference fuel_ir::DType; fuel-graph maps back via
DType::from_str). Also carries MaskedFill’s value dtype. None ⇒
not a dtype-carrying node (a matcher wildcard). Convergence Increment A.
slice_start: Option<u64>Slice start offset along axis (the dim rides the existing axis
field). None ⇒ not a slice node. Convergence Increment A.
slice_len: Option<u64>Slice length along axis. None ⇒ not a slice node. Convergence
Increment A.
roll_shift: Option<i64>Roll cyclic shift along axis (signed; the dim rides axis). None
⇒ not a roll node. Convergence Increment A.
pad_amounts: Vec<(u64, u64)>Pad per-axis (before, after) amounts, one pair per input dim. Empty
⇒ not a pad node. Convergence Increment A.
pad_mode: Option<u8>Pad fill mode code: 0=Constant, 1=Reflect, 2=Replicate (mirrors
Fuel’s PadMode order; dep-free integer code). None ⇒ not a pad node.
Convergence Increment A.
pad_value: Option<f64>Pad constant fill value (used by PadMode::Constant). None ⇒ not a
pad node. Convergence Increment A.
keepdim: Option<bool>Reduction keepdim flag (§6.19 reduce-schema conformance — serialized
in the canonical blob). NOT consumed by tag_to_op: Fuel’s reduce Ops
encode keepdim structurally (ReduceSumTo/ReduceMaxTo carry the kept
shape; SumDim/MeanDim are rank-reducing). Convergence Increment A.
target_shape_rel: Option<ShapeExpr>Shape-relative alternative to target_shape: the target is another
operand’s shape (SameAs { operand } over the Bind space). None ⇒
use target_shape (or not a shape-target node).
Not every output frame is expressible this way. A broadcast frame
assembled by per-axis max across TWO binds (a[N,1] ⊗ b[1,M] → [N,M])
is carried by NO single operand, so no SameAs spelling names it — the
resolver refuses such a BroadcastTo target as a Dims-class gap
(RelAttrError::FrameNotExpressible) rather than silently resolving to
one operand’s PARTIAL frame. The constructor that would express it is
the reserved shape_expr::TAG_DIMS entrant (KISS #80).
slice_start_rel: Option<Dim>Shape-relative alternative to slice_start: a DimExpr over the Bind
space (e.g. rope’s Div(Extent(0, LAST), 2)). None ⇒ use
slice_start.
slice_len_rel: Option<Dim>Shape-relative alternative to slice_len. None ⇒ use slice_len.
axis_last: boolRank-relative alternative to the op’s axis carrier (axis, or dims
for Squeeze/Unsqueeze): this op’s axis is its per-tag LAST — resolved
against rank(operand[0]) as rank − 1 for reduces/Slice/Concat/Flip/
CumSum/… and Squeeze, and rank (append) for Unsqueeze. false ⇒ use
the absolute carrier.
scalar_rel: Option<Dim>Shape-relative alternative to scalars for a scalar-param op
(AddScalar/MulScalar): the scalar VALUE is a DimExpr over the Bind
space, resolved to a concrete number at emit time
(fuel_graph::runtime_fused::resolve_rel_attrs) — the reduced_count
concept made emittable: Dim::Extent { operand: 0, axis: LAST } yields
n = extent of operand 0's last axis (the MulScalar(n) divisor a norm
backward needs). It rides the SAME Dim/eval_dim/resolve_axis
machinery as slice_start_rel/slice_len_rel (no parallel axis
resolver). Distinct from an OPEN scalar slot: a scalar_rel node is
filled from an input SHAPE, NOT from the params projection cursor, so it
is NOT counted by count_scalar_slots and never consumes a cursor value.
None ⇒ use scalars (a baked value or an open slot). Set together with
a non-empty scalars is a typed resolution error (rel XOR abs). Like the
other rel fields, it is IN-MEMORY recipe data — deliberately NOT
serialized to the §6.19 wire (pinned by
rel_attr_fields_are_absent_from_the_6_19_wire).
lhs_roles: Vec<u8>LHS per-axis contraction roles ({Batch=0,FreeM=1,FreeN=2,ContractedK=3}, u8 each; length = lhs rank). Empty ⇒ implicit (rank-polymorphic) matmul.
rhs_roles: Vec<u8>RHS per-axis contraction roles (same encoding; length = rhs rank). Empty ⇒ implicit (rank-polymorphic) matmul.
const_bits: Option<u64>OpTag::Const value: the scalar literal’s dtype-agnostic storage bit
pattern. Narrow dtypes place their storage bits LOW-order with the
upper bits zero (MBZ) — use const_bits_narrow. A NaN payload is
carried verbatim (never quieted). None ⇒ not a const leaf.
slot_index: Option<u32>OpTag::RuntimeScalar value: the index of the dispatch-bound scalar
slot this leaf reads. None ⇒ not a runtime-scalar leaf.
scan_role: Option<u8>OpTag::ScanPlaceholder role code: SCAN_ROLE_CARRY (0) or
SCAN_ROLE_ELEM (1). None ⇒ not a scan-placeholder leaf.
scan_index: Option<u32>OpTag::ScanPlaceholder index: which xs[i] an elem hole stands
for (a carry hole is index 0 in the v1 single-carry model). None ⇒
not a scan-placeholder leaf.
scan_n_carries: Option<u32>OpTag::Scan n_carries: how many recurrent carries the scan threads
(GAP-303). 1 is the v1 single-carry model.
⚠️ Like its siblings this is Fuel-internal and NOT on the KISS-OPS-6.19
wire: OpTag::Scan is in has_empty_canonical_body’s empty group BY
DESIGN, so no Scan param is serialized and adding one here changes no
bytes. Carry IDENTITY on the wire is already carried by
OpTag::ScanPlaceholder’s body, which serializes scan_index as a
u32 — Carry/0 and Carry/1 have always produced different bytes.
scan_n_xs: Option<u32>OpTag::Scan n_xs: number of per-step xs operands.
scan_bound: Option<u32>OpTag::Scan bound: the static step horizon.
scan_emit: Option<u8>OpTag::Scan emit code: 0 = All (stack per-step ys, slot 0), 1 =
Final (final carry). Matches fuel_graph::ScanEmit’s declaration order.
scan_early_exit: Option<bool>OpTag::Scan early-exit presence flag: Some(true) ⇒ the Phase-2
ScanPredicate marker is present (its predicate LOGIC rides the trailing
pred_exit operand). None/Some(false) ⇒ no early exit.
view_slot: Option<u32>OpTag::View slot index: which of the producer’s output_views slots
this projection reads. Fuel-internal (not on the §6.19 wire). None ⇒
not a view node.
rank0_target: boolMarks an INTENTIONAL rank-0 ([], scalar) target on a shape-target op
(ReduceSumTo/ReduceMaxTo/Reshape/BroadcastTo). A rank-0 shape has
ZERO dims, so its target_shape is empty — the SAME empty state a
wildcard / non-shape-target node carries — leaving the two
indistinguishable in memory. true ⇒ the empty target_shape denotes
the concrete rank-0 shape [] (a reduce-to-scalar loss tail, e.g. FSCE’s
ReduceSumTo([])); false (the default) ⇒ empty target_shape is unset
/ a matcher wildcard. Reusable for every future reduce-to-scalar loss.
Fuel-INTERNAL, off the §6.19 wire. The wire already encodes a rank-0
target correctly as a 0-length target_shape i64 list
(put_i64_list(&[])), so to_canonical_bytes needs no new byte — this
marker only disambiguates the in-memory wildcard-vs-rank-0 overload that
the recipe emitter (fuel_graph::runtime_fused::shape_from_attr) reads.
fused_op: Option<String>OpTag::Fused selector: the stable registry-entry name of the
nested fused op a recipe carries as-is (e.g. "SoftmaxLastDim" /
"SoftmaxLastDimBackward"), mirroring Self::cast_dtype’s
DType::as_str name-string precedent. fuel-graph’s
runtime_fused::tag_to_op resolves it back to a FusedOpId via
registry::id_for_name and reconstructs the param-less
Op::Fused(fid, params) through a small fid → params map. None ⇒ not a
nested-fused node (an honest miss for OpTag::Fused).
Fuel-INTERNAL, off the §6.19 wire. A Fused node serializes to the
empty canonical body (the _ arm of Self::to_canonical_bytes), so
this selector never flows to a cross-producer blob — a nested fused node
is a Fuel-side recipe-interior structural token, not a KISS base op.
Implementations§
Source§impl OpAttrs
impl OpAttrs
Sourcepub fn to_canonical_bytes(&self, op: OpTag) -> Result<Vec<u8>, UnresolvedAttr>
pub fn to_canonical_bytes(&self, op: OpTag) -> Result<Vec<u8>, UnresolvedAttr>
Serialize these attrs to the node-envelope blob for op in the
§6.19 positional shape (see the SCOPE note above: this is carrier
(a), and only the tags inside KISS-OPS-6.19-0003’s carrier set have a §6.19
schema at all):
op: a per-op positional little-endian body (no elision — the
OpTag determines the fixed schema), length-prefixed with a u32 LE
byte count. An op whose schema is empty (Add, Neg, Where,
comparisons, …) serializes as the single canonical form [0,0,0,0].
MatMul is empty-bodied ONLY when its role vectors are unset (the
implicit rank-polymorphic form); explicit roles serialize the LOCKED
§5 contraction descriptor. Deterministic + dependency-free.
Conformance scope (do not overclaim): byte-comparable with a
Baracuda-emitted blob for the positionally-conformant ops (elementwise,
cast, slice, concat, roll, pad, flip, iota, permute, (un)squeeze,
shape-target, matmul role-vectors — the shared cross-producer golden,
Baracuda #68). reduce emits Fuel’s single-axis {axis, keepdim} and
gather/scatter emit {axis}; oob_policy and a multi-axis
reduce_axes list are DEFERRED (no carrier/consumer yet), while
monoid/scatter_combine ride op_name and the index operand/dtype
ride child_edges/that operand node per the pinned node schema — so they
legitimately do not belong in this blob. See the module comment above and
kernel-seam-interop.md §7.3.2.
M-3: the unwrap_or(...) defaults below cannot distinguish an unset
field from a genuine zero (e.g. axis: None vs Some(0)), and for five
(op, field) pairs that collapse is LOSSY TODAY - two semantically
distinct ops serialize to identical bytes. Registry: GAP-287.
This paragraph previously read “harmless today … an op that reaches a
given arm always has the field set (op_to_attrs / tag_to_op
guarantee it)”. That was false, and the guarantor’s own doc says so:
fuel_graph::jit::op_to_attrs documents itself as “not exhaustive” and
leaves axis unset for CumSum/IndexSelect/Gather/IndexAdd/
ScatterAdd (its _ => {} arm). So Gather on axis 2 and on axis 0
emit the same bytes. KISS-OPS-6.19 makes axis MANDATORY with no
default, so unwrap_or(0) fabricates a value the schema does not permit.
keepdim is also set by nothing in the repository, and that one is
NOT a defect - the distinction worth keeping. Every tag on that arm
removes the reduced dim (Op::SumDim/MaxDim/MeanDim say so verbatim;
CumSum is “same shape as input”), so false is the true value.
keepdim=TRUE is Op::ReduceSumTo(Shape): a different tag, on the
shape-target arm, with no unwrap_or at all. A field being unset is
only a defect if the default CAN differ from the truth - “never set”
has several causes and only one of them is this bug.
const_bits, slot_index, scan_role and scan_index are reachable
only from hand-built regions (op_to_tag emits none of the four leaf
tags); scan_* are set together by both producers today, guaranteed by
nothing. Note scan_role.unwrap_or(SCAN_ROLE_CARRY) is the only default
here that names a semantic value rather than a neutral zero: an unset
role does not decay to “absent”, it asserts “carry”.
The old note also said “a future decoder must not round-trip None”.
A decoder cannot recover what this encoder already discarded - the
mitigation has to live on this side of the wire. And “harmless, there is
no decoder” was scoped to Fuel’s own repo while this format exists to
leave it - the decoder is the counterparty’s by design, i.e. the claim
was scoped to the one place the hazard cannot occur.
On what checks this format: exactly one conformance fixture exists,
matmul_role_vectors_serialize_the_locked_rank2_golden, and Baracuda
“has NO near-term binary arm” (its own comment), so there is no second
encoder. That golden covers the MatMul arm - which emits lhs_roles/
rhs_roles directly and contains no unwrap_or at all. The single
fixture this format has is aimed at an arm that cannot exhibit this
defect, which is why six lossy collapses were found by audit rather
than by a failing test.
⚠️ THIS CRATE’S TWO DIRECTIONS DISAGREE, AND axis IS ONE OF THEM.
fuel_graph::runtime_fused::tag_to_op - the DECODE direction this
serializer round-trips against - treats an unset required attr as a hard
decline: 24 attrs.<field>? declines over 11 fields, plus 5 explicit
return None, and its own comments name the policy (“an honest miss
(unset required attr)”). Seven fields appear on BOTH sides:
axis pad_mode roll_shift scan_index scan_role slice_len slice_startFor every one of them the decoder REFUSES what this encoder INVENTS.
Six are latent (op_to_attrs does set them, so the default is currently
unreachable); axis is LIVE, because the five ops above are the ones it
leaves unprojected. So making the encoder decline is not a new design -
it is this codebase’s own settled answer to this exact condition, and
the encoder is the side that dissents.
Gated by the collapse table in fuel-graph/src/jit.rs tests, which pins
exactly which (op, field) pairs are lossy so that fixing one is visible.
Fixing the encoder is a wire-format change and needs the external ask.
Serialize self for op as carrier (a): u32_le(body_len) ++ body.
The FRAME is separated from the per-op BODY here only because they are different things – the envelope is fixed for every tag, the body is the schema table. The table itself is deliberately NOT split further: it is one match over the wire schema, and a reader checking Fuel against the schema it borrows needs to see all of it at once. (Only the carrier-set tags are checkable against KISS-OPS-6.19 itself; for the rest the schema is Fuel’s own – SCOPE note above.)
Source§impl OpAttrs
impl OpAttrs
Sourcepub fn region_key(&self) -> RegionKey
pub fn region_key(&self) -> RegionKey
Structural region identity of these attrs — see RegionKey. Excludes the
scalar-param scalars VALUES (runtime ScalarExpr::Param), keys floats by to_bits,
keeps None distinct from Some(0), and is built by an exhaustive destructure so a
new OpAttrs field must be classified before this compiles.