bb_ir/wire_shape.rs
1//! Canonical Send / Recv NodeProto shape — the contract every
2//! wire-IR-touching pass + runtime gate agrees on.
3//!
4//! Wire ops carry a specific NodeProto shape. Centralizing the
5//! contract here keeps the DSL emitters, compiler passes, and
6//! runtime gates from drifting on string literals or attribute
7//! layouts.
8//!
9//! This module is the **single declarative description** of that
10//! shape. The DSL emits to it; the compiler passes mutate within
11//! it; the runtime consumes it. [`crate::verify::wire_shape`]
12//! checks a `ModelProto` against the contract.
13//!
14//! ## The shape
15//!
16//! ### Send
17//!
18//! ```text
19//! NodeProto {
20//! op_type: "Send",
21//! domain: "ai.bytesandbrains.wire",
22//! input: [payload_0, payload_1, ..., payload_{N-1}, peer],
23//! output: [handle],
24//! attribute: [
25//! (name: "peer", type: BYTES, t: <PeerId.to_bytes() multihash>),
26//! (name: "dest_suffix.{name}", type: BYTES, t: <multiaddr-bytes>),
27//! (name: "deadline_ns", type: INT, i: <i64-ns>), // optional
28//! ],
29//! metadata_props: [
30//! ("ai.bytesandbrains.wire.wire_id", "<u64>"),
31//! ("ai.bytesandbrains.wire_transport", "data" | "trigger_only"),
32//! ("ai.bytesandbrains.batch_group_id", "<u32>"),
33//! ("ai.bytesandbrains.dest_site_name.{name}", "<recv-site-name>"),
34//! ],
35//! }
36//! ```
37//!
38//! ### Recv
39//!
40//! ```text
41//! NodeProto {
42//! op_type: "Recv",
43//! domain: "ai.bytesandbrains.wire",
44//! input: [],
45//! output: [received_0, received_1, ..., received_{N-1}, sender],
46//! metadata_props: [
47//! ("ai.bytesandbrains.wire.wire_id", "<u64 matching paired Send>"),
48//! ("ai.bytesandbrains.wire_transport", "data" | "trigger_only"),
49//! ],
50//! }
51//! ```
52//!
53//! `wire_id` pairs the Send/Recv halves across the cut.
54//! `wire_transport` tells the runtime whether each fill carries a
55//! payload (`data`) or is firing-signal-only (`trigger_only`).
56//!
57//! ## Key invariants the contract pins
58//!
59//! - **`ATTR_PEER` is bytes, not i64.** The peer attribute on a
60//! Send carries the PeerId's canonical multihash byte form
61//! (`PeerId::to_bytes()`), NOT a `u64`-collapsed identity hash.
62//! The runtime gates parse via
63//! `PeerId::from_bytes(&attr.t)`.
64//!
65//! - **`wire_id` is the pairing token.** The DSL `Graph::wire`
66//! mints a monotonic u64 and stamps it on BOTH halves; the
67//! compiler's `discover_wire_edges` pair Send/Recv by it.
68//!
69//! - **`wire_transport` lives on the NodeProto itself.**
70//! `analyze_wire_edges` stamps it onto
71//! `partition.functions[0].node[i].metadata_props` in place, so
72//! downstream passes and the runtime read a single source of
73//! truth.
74//!
75//! - **`SlotFill.type_hash` is populated from the sender side's
76//! `T::HASH`.** Receivers dispatch wire bytes via
77//! `if envelope.fill.type_hash == T::HASH { T::deserialize(&fill.payload) }`.
78
79/// Wire-op domain. All Send / Recv NodeProtos live under here.
80pub const WIRE_DOMAIN: &str = "ai.bytesandbrains.wire";
81
82/// op_type for a Send node.
83pub const OP_SEND: &str = "Send";
84
85/// op_type for a Recv node.
86pub const OP_RECV: &str = "Recv";
87
88/// Attribute key on a Send NodeProto carrying the destination peer
89/// as **multihash bytes** (`PeerId::to_bytes()`), i.e. the
90/// `attribute.t` (bytes) field, not `attribute.i` (i64).
91///
92/// Aliased from [`crate::syscall_ids::ATTR_PEER`] so consumers can
93/// reach for `bb_ir::wire_shape::ATTR_PEER` or `bb_ir::keys::ATTR_PEER`
94/// — they're the same key, kept in one place.
95pub use crate::syscall_ids::ATTR_PEER;
96
97/// Attribute key prefix for per-fill multiaddr destination
98/// suffixes. Full key is `format!("{DEST_SUFFIX_ATTR_PREFIX}{slot_name}")`.
99pub use crate::keys::DEST_SUFFIX_ATTR_PREFIX;
100
101/// Attribute key for the optional static deadline (in nanoseconds
102/// since the reference clock epoch) stamped by
103/// `insert_async_deadlines`.
104pub use crate::syscall_ids::ATTR_DEADLINE_NS;
105
106/// `metadata_props` key carrying the wire-pairing token.
107pub use crate::keys::WIRE_ID_KEY;
108
109/// `metadata_props` key carrying the data-vs-trigger-only
110/// classification.
111pub use crate::keys::WIRE_TRANSPORT_KEY;
112
113use crate::proto::onnx::{attribute_proto, AttributeProto, StringStringEntryProto};
114
115/// Value of [`WIRE_TRANSPORT_KEY`] for full-payload edges.
116pub use crate::keys::WIRE_TRANSPORT_DATA;
117
118/// Value of [`WIRE_TRANSPORT_KEY`] for trigger-only edges.
119pub use crate::keys::WIRE_TRANSPORT_TRIGGER_ONLY;
120
121/// `metadata_props` key prefix for per-fill recv-site names. Full
122/// key is `format!("{DEST_SITE_NAME_PREFIX}{slot_name}")`.
123pub use crate::keys::DEST_SITE_NAME_PREFIX;
124
125/// Return `true` if the NodeProto is a `wire.Send`.
126pub fn is_send(node: &crate::proto::onnx::NodeProto) -> bool {
127 node.op_type == OP_SEND && node.domain == WIRE_DOMAIN
128}
129
130/// Return `true` if the NodeProto is a `wire.Recv`.
131pub fn is_recv(node: &crate::proto::onnx::NodeProto) -> bool {
132 node.op_type == OP_RECV && node.domain == WIRE_DOMAIN
133}
134
135/// Read the wire_id metadata stamp from a Send or Recv node.
136/// Returns `None` if the key is absent or non-numeric.
137pub fn read_wire_id(node: &crate::proto::onnx::NodeProto) -> Option<u64> {
138 node.metadata_props
139 .iter()
140 .find(|p| p.key == WIRE_ID_KEY)
141 .and_then(|p| p.value.parse::<u64>().ok())
142}
143
144/// Read the destination peer's multihash bytes from a Send / gate
145/// NodeProto. Returns `None` if the attribute is absent or carries
146/// no byte content. The byte payload lives on `attribute.s` per the
147/// ONNX convention for raw bytes (paired with
148/// `AttributeType::String`); callers reconstruct the PeerId via
149/// `PeerId::from_bytes(read_peer_bytes(node)?)`.
150pub fn read_peer_bytes(node: &crate::proto::onnx::NodeProto) -> Option<&[u8]> {
151 let attr = node.attribute.iter().find(|a| a.name == ATTR_PEER)?;
152 if attr.s.is_empty() {
153 None
154 } else {
155 Some(&attr.s)
156 }
157}
158
159/// Stamp the destination peer onto a Send / gate NodeProto's
160/// `attribute.s` (bytes) using the canonical multihash form. Used
161/// by the compiler's gate-insertion passes and any pass synthesizing
162/// new Send NodeProtos.
163pub fn stamp_peer_bytes(node: &mut crate::proto::onnx::NodeProto, peer_bytes: Vec<u8>) {
164 let attr_type = attribute_proto::AttributeType::String as i32;
165 if let Some(existing) = node.attribute.iter_mut().find(|a| a.name == ATTR_PEER) {
166 existing.s = peer_bytes;
167 existing.r#type = attr_type;
168 existing.i = 0;
169 } else {
170 node.attribute.push(AttributeProto {
171 name: ATTR_PEER.to_string(),
172 s: peer_bytes,
173 r#type: attr_type,
174 ..Default::default()
175 });
176 }
177}
178
179/// Stamp the [`WIRE_TRANSPORT_KEY`] classification onto a wire-op
180/// NodeProto's `metadata_props` (idempotent: replaces an existing
181/// value).
182pub fn stamp_wire_transport(node: &mut crate::proto::onnx::NodeProto, transport: &str) {
183 if let Some(entry) = node
184 .metadata_props
185 .iter_mut()
186 .find(|p| p.key == WIRE_TRANSPORT_KEY)
187 {
188 entry.value = transport.to_string();
189 } else {
190 node.metadata_props.push(StringStringEntryProto {
191 key: WIRE_TRANSPORT_KEY.to_string(),
192 value: transport.to_string(),
193 });
194 }
195}
196