Model Tree
A schema describes what belongs together. Leaves represent typed values; branches combine related values into context; the root represents one observation. The same tree defines where reconstruction targets and exported embeddings live.
Follow One Observation
An order can contain several line items. This is one illustrative record:
customer_tier: gold
line_items:
- sku: A12
quantity: 2
price: 19.99
- sku: B07
quantity: 1
price: 45.50
returned: falseThe parent supplies each child’s name:
import relflow as rf
model = rf.Model(
d_model=64,
n_layers=2,
n_heads=4,
customer_tier=rf.Category,
line_items=rf.Branch(
length=32,
sku=rf.Category,
quantity=rf.Number,
price=rf.Number,
),
returned=rf.Boolean(mask=True),
)Fields at the same line-item position belong to the same item. The branch contextualizes those inputs and reduces the item collection before routing it to the order. The root combines that representation with customer tier. mask=True keeps the return answer out of the encoder and creates its reconstruction objective.
A decoder pools representations along its ancestor path. Learned decoder queries can also be conditioned on visible sibling representations when their coordinates align. It is therefore misleading to treat every decoder as reading only the root, or to treat a tree edge as a database join. Explicit joins and derived values belong in preprocessing.
Names And Addresses
The generated root is anonymous and has address /. It is always a singleton; repeated data belongs in a child branch. There is no root-name option. Diagrams use a descriptive root label such as order or customer to identify the observation. That label is illustrative, not model configuration or an address prefix; the root address remains rf.Address().
A child branch reads a list even when length=1; a bare child mapping is not a branch collection. Branch and leaf definitions have no name or positional arguments.
Bare tensorfield classes such as amount=rf.Number and sku=rf.Category use the field’s defaults. Call them only to configure options, such as returned=rf.Boolean(mask=True). Reusing a definition creates independent named nodes and learned modules; it does not share model weights.
Use a fields mapping when generating children or when a child name collides with a configuration option:
measurements = rf.Branch(
length=8,
fields={"length": rf.Number, "description": rf.Category},
)An address starts with / and identifies a schema location, such as /line_items/price. A field named name is an ordinary child: name=rf.Category has address /name. The leading slash is not an input-data wrapper or a query. Addresses appear in prediction fields, metric names, and mutation predicates. They describe the model’s structure even when an explicit query reads a differently named source path. Names and nesting match input data by default; see Binding Data for other source layouts.
Use rf.Address(...) with one argument per child name to distinguish a schema address from an ordinary data key or query string. The leading slash and separators are added automatically; no arguments selects the root:
root = rf.Address()
price = rf.Address("line_items", "price")
selected = model.select(rf.where("address") == price)For example, rf.Address("transactions", "amount") represents /transactions/amount. Serialized paths such as /transactions/amount can also be read back with rf.Address(path).
rf.Address is a string subclass, so the same value works with RelFlow accessors and Arrow or Polars prediction-field lookups. Serialized output still uses plain string keys.
Configure The Context
d_model, n_layers, and n_heads are required when building with rf.Model(...) from fields. The size factories supply defaults for those settings and the rest of the tree. d_model is the shared embedding width. The model’s encoder settings configure the root; child branches have their own settings.
| Setting | Controls |
|---|---|
attention |
The branch encoder: "mha", "gqa", "mqa", or None. |
n_layers, n_heads, dropout |
That branch’s encoder. |
reduction |
What the branch passes to its parent. Defaults to rf.Attention(). |
length |
Maximum retained records in a child branch. |
overflow |
Keep the first records ("head", default), last records ("tail"), or raise ("error"). |
mask |
Which inputs to hide or reconstruct. |
embed=True |
Export the representation at that address. |
Encoder and reduction settings are separate. attention=None disables encoder attention but leaves the selected reduction in place. An attention reduction owns its n_outputs, n_layers, optional n_heads/dropout, and position; omitted reduction heads/dropout inherit the branch values. rf.Mean() averages present inputs. reduction=None preserves routed child slots instead of summarizing them. See Branch for the complete reduction contract.
Short collections are padded. Padding and structurally skipped inputs do not contribute to attention or mean pooling. An explicit null remains a modeled value state and is different from padding.
Only declared options are accepted. Use description for notes. A leaf’s active=False excludes it from execution while retaining it in the schema; change an existing model through mutation methods. For learning and output roles, continue to Masking and Embeddings.
Choose A Size
rf.Model.xs(...), .sm(...), .md(...), .lg(...), and .xl(...) build the same kinds of model trees with different capacity defaults. Start with md for a nested relational task or sm for faster iteration, then compare on validation data. These are starting configurations; schema size, vocabularies, and decoded fields also affect parameter counts and compute.
For a configuration file, use rf.Model.from_yaml(path) with a preset and named field definitions. Model Templates describes the YAML format and override rules.
The bundled revision-1 configurations are:
| Factory | Width | Root layers | Child branch layers | Heads | Root outputs | Child branch outputs |
|---|---|---|---|---|---|---|
rf.Model.xs |
64 | 1 | 1 | 2 | 1 | 1 |
rf.Model.sm |
128 | 2 | 1 | 4 | 2 | 2 |
rf.Model.md |
256 | 3 | 2 | 8 | 4 | 4 |
rf.Model.lg |
384 | 4 | 2 | 8 | 8 | 4 |
rf.Model.xl |
512 | 6 | 3 | 8 | 16 | 8 |
Every child branch receives its preset defaults at every depth. All five presets use MHA encoders, zero dropout, and attention reductions with one layer and positional information. Reduction heads and dropout inherit their owning branch. Leaf decoders use query pooling, one cross-attention block, zero dropout, and the preset head count; their automatic positional policy remains decoder_position=None.
For example, this model uses the md defaults for customer history, with an explicit root depth and a larger history reduction:
import relflow as rf
model = rf.Model.md(
n_layers=4,
balance=rf.Number,
customer_tier=rf.Category,
transactions=rf.Branch(
length=128,
overflow="tail",
reduction=rf.Attention(n_outputs=8),
merchant=rf.Category,
amount=rf.Number,
days_ago=rf.Number,
),
returned=rf.Boolean(mask=True),
)Explicit settings win at their own node. Here, n_layers=4 configures only the root; the transaction branch keeps two encoder layers. A leaf’s decoder heads remain independent of its parent’s overrides. Changing d_model changes the shared width and must remain compatible with every effective head count.
An explicit reduction replaces the whole preset reduction. Passing reduction=rf.Attention() selects its one-output defaults even under xl. rf.Mean() and None also retain their usual behavior. Explicit values equal to ordinary defaults still win, including n_layers=1 on a branch and dropout=None for zero dropout. Reusing a branch or field definition across different factories leaves that definition unchanged.
Presets leave collection lengths, overflow, queries, masking, datatype options, optimizer settings, and runtime settings under application control. They keep the anonymous singleton root at /. Reduction outputs are context vectors; they do not create prediction records. With embed=True, xs exports one width-64 root vector, while md exports four width-256 root vectors. Set reduction=rf.Attention(n_outputs=1) explicitly when an application needs one root vector, and evaluate that bottleneck.
The resolved schema records the actual architecture. model.preset holds the selected immutable policy, including its name and revision; ordinary rf.Model(...) construction has no preset policy. Restore a schema directly with rf.Model(schema=schema); preset factories reject schema=....
The factories bind immutable configurations from rf.presets. An application can use the same factory to declare a classmethod on its own subclass:
import relflow as rf
class SupportModel(rf.Model):
compact = rf.presets.factory(rf.presets.SM)SupportModel.compact(...) accepts the usual model construction keywords and returns a SupportModel with the bound sm defaults.
Checkpoints retain the full original preset policy alongside the resolved schema. New nodes added with extend(...) receive those saved defaults after loading, even if the bundled configurations change later. Existing nodes keep their resolved settings. A schema-only model constructed without a saved policy uses ordinary defaults when extending it.