Ocannl_tensor.ShapeDefinition and properties of the syntax of labels specifications and einsum notation:
',', '+', '*', '^' anywhere in the initial text, the multicharacter version is used,'>', '|', '-', ',', '=', ';', '+', '*', '_', '^'.'<' | '=' when use_padding is in scope.If labels_spec does not contain "|" nor "->", each label is of the kind Output. If the spec doesn't contain "|", labels to the left of "->" are Input and to the right Output. Labels to the left of "|" are Batch, and between "|" and "->" are Input.
A row whose kind separator is omitted altogether reads as the context ellipsis "...": x is equivalent to ...|...->x, so batch and input axes broadcast through terse specs by default. Writing the separator with an empty row spec closes the row: | ->x means no batch and no input axes, |x means no batch axes, ->x means no input axes. In einsum specs the distinction matters wherever another row of the same kind is written "...": an omitted row shares the context row variable with it. On the result side of => this means those axes pass through (write e.g. => |->0 to reduce over them instead), and across operands it forces the rows equal (close the row, e.g. a |-prefixed kernel slot, when an operand must not receive the broadcast axes).
The labels ".."ident"..", "..." (where ident does not contain any of the special characters) are only allowed once for a kind. They are used to enable (in-the-middle) broadcasting for the axis kind in the einsum-related shape inference (like the ellipsis "..." in numpy.einsum), and are translated to row variables. The ellipsis "..." is context dependent: it stands for a reserved row variable specific to the kind of the row it appears in (batch, input or output), shared by every "..." of that kind within one einsum spec. The reserved variables cannot be referred to by name, so identifiers such as batch remain available as ordinary dimension labels and "..batch.." is an ordinary user row variable. When the same row variable is used in multiple rows, the corresponding broadcasted axes are matched pointwise in the resulting operation.
The label "_" is a place-holder: it is not output to the resulting map but aligns the axes of other labels.
Conv expressions have the form stride*output< + dilation*kernel + stride_offset or with = in place of <, where stride and dilation are optional integer coefficients (defaulting to 1), output/kernel are axis labels, and stride_offset is an optional integer offset that's smaller than stride. This syntax enables convolution-style indexing where input_dimension = stride * output_iterator + dilation * kernel_iterator. Conv expressions automatically trigger multichar mode. Under syntax extensions %cd and %op, the integer coefficients can be spliced-in OCaml identifiers and the specifier < vs. = can be omitted if use_padding is in scope.
Adding < after the output label (e.g., stride*output<+kernel) indicates no-padding mode, where indices must stay within the input bounds. In this mode, the input dimension must satisfy: (input - effective_kernel_span) mod stride = 0, where effective_kernel_span = 1 + (kernel - 1) * dilation. With = instead, padding is applied for the kernel needs, and the constraint is simply input mod stride = 0.
Concat expressions have the form label1 ^ label2 ^ ..., indicating that multiple axes are concatenated into a single axis. Concat expressions automatically trigger multichar mode.
Note: currently, OCANNL shapes always allow broadcasting. Row variables track the broadcasted axes -- if there is no row variable, broadcasted axes are not tracked. In the notation case `row_spec` = `axes_spec`, the axes are the rightmost axes (broadcasting to the left). In the past, we supported preventing broadcasting, but removed that to reduce complexity.
type padding = Row.axis_padding Base.array Base.optionval sexp_of_padding : padding -> Sexplib0.Sexp.tval padding_of_sexp : Sexplib0.Sexp.t -> paddingtype t = private {mutable batch : Row.t;mutable input : Row.t;mutable output : Row.t;mutable batch_padding : padding;mutable input_padding : padding;mutable output_padding : padding;mutable padding_elem : Base.float Base.option Base.option;The padding element for this shape's tensors. None means "unknown" (not yet determined), Some (Some v) means all operations use neutral element v, Some None means different operations require different neutral elements (margin must be reset before each operation).
mutable closed : Base.bool;Set by a successful finish_inference on every shape of the steps it commits (gh-ocannl-1301): a closed shape's rows (modulo projection ids), paddings and padding_elem no longer change -- a write that would change them raises before writing anything. Not part of the shape's value: dropped from sexps, ignored by equal.
mutable discarded_by : Base.string Base.option;Set by Tensor.rewind_to_last_closure on every open shape it discards, to the rewind's provenance: a step over a discarded shape is refused before it is registered. Not part of the shape's value.
id : Base.int;A node that has the same shape as this shape, or -1.
debug_name : Base.string;}Private (gh-ocannl-1506): fields are read and matched freely, but only Shape's guarded setters write them and only Shape constructs a shape, so the closed-shape guard cannot be bypassed. The privacy is shallow: a padding array read off a shape is still a mutable array.
include Ppx_compare_lib.Equal.S with type t := tval equal : t Base__Ppx_compare_lib.equalval sexp_of_t : t -> Sexplib0.Sexp.tval compare_deduce_within_shape :
deduce_within_shape ->
deduce_within_shape ->
Base.intval sexp_of_deduce_within_shape : deduce_within_shape -> Sexplib0.Sexp.tval deduce_within_shape_of_sexp : Sexplib0.Sexp.t -> deduce_within_shapetype delayed_var_ref = private {var_ref : Ir.Indexing.variable_ref;mutable var : [ `Row of Row.row_var | `Dim of Row.dim_var | `Not_set_yet ];uid : Base.int;The reference's identity for rewind tracking; ignored by equal and sexps.
mutable discarded_by : Base.string Base.option;The provenance of the rewind that discarded the reference, which every setter then refuses (gh-ocannl-1789); ignored by equal and sexps.
}Private (gh-ocannl-1573): read and matched freely, written only by Shape -- through guarded setters that refuse a reference a rewind discarded and count the write as a session mutation -- and built only by get_variable_ref. The Ir.Indexing.variable_ref it carries is solved only by Shape too: Ir.Indexing.set_solved_dim and set_solved_sym take the process's one Ir.Indexing.shape_writer, which Shape claims as it initializes (gh-ocannl-1624).
val equal_delayed_var_ref : delayed_var_ref -> delayed_var_ref -> Base.boolval sexp_of_delayed_var_ref : delayed_var_ref -> Sexplib0.Sexp.tval get_variable_ref : Base.string -> delayed_var_refReturns a fully unset variable reference with the given label.
A reference passed through ~capture_dims to several operations names one dimension, in whichever order inference solves them (gh-ocannl-1545): within one inference (up to the next finish_inference) the operations' dimension variables are equated, as are their row variables, and a dimension variable with a row variable has the row's product. Across a closure the reference carries its solved value, for a row variable the product of its dimensions (see set_dim).
A reference whose closure recorded no value -- a row holding a symbolic extent has no numeric product -- has lost its constraint: capturing it again, or relating it by set_dim, set_sym_dim, set_equal or set_scale, raises Row.Shape_error naming the closure (gh-ocannl-1655).
val set_dim : delayed_var_ref -> Base.int -> Base.unitSets the dimension resp. total elements of the dim resp. row variable reference to the given value. This will propagate through shape inference.
For row variables, this means the product of the dimensions, via the Total_elems constraint.
val set_sym_dim : delayed_var_ref -> Ir.Indexing.static_symbol -> Base.unitSets the dim variable reference to a symbolic extent (gh-490): during shape inference the axis is a rigid constant equal only to itself, and it is materialized at its declared maximum extent (static_range, which is required and must be positive) for Tnode dims and projections.
Row variables are not supported: a symbolic total-elements constraint would require symbolic arithmetic (raises Row.Shape_error). Conflicts with a prior set_dim, or a different symbol set on the same reference, also raise Row.Shape_error.
val set_equal : delayed_var_ref -> delayed_var_ref -> Base.unitSets the two variable references to be equal (in some sense). This will propagate through shape inference.
When both references are dimension variables or both are row variables, this means they are precisely equal: two rows get the same axes, not only the same product, whatever either reference has been solved to so far (gh-ocannl-1617). When one is a dimension variable and the other is a row variable, this means they have the same number of total elements.
A row reference bound by an inference that has since closed keeps only its row's product (solved_dim), so a Row-Row call involving it raises Row.Shape_error: relate the rows before the closure.
An unbound reference (from get_variable_ref, not yet passed to ~capture_dims) paired with a row reference takes or gives a number, the row's product. Binding it to a row afterwards would leave the two rows tied by product alone, so that binding raises Row.Shape_error (gh-ocannl-1655): bind it first to have the rows equal. Binding it to a dimension stands.
val set_scale :
factor:Base.int ->
delayed_var_ref ->
delayed_var_ref ->
Base.unitAsserts a multiplicative relation between two dimension variable references: the first ref equals factor times the second. Mirrors set_equal in spirit but expresses non-convolutional scaling -- the motivating use case is hourglass-style networks where one dimension is a constant multiple of another (e.g. an MLP hidden width that is twice the input width).
Concretely, if the first ref ends up bound to dimension d_large and the second to d_small, this asserts d_large = factor * d_small.
Restrictions:
Row.Shape_error regardless of which side is solved (no silent routing through set_dim's row total-elements path).factor must be a positive integer (>= 1). factor = 1 is accepted and behaves exactly like set_equal; callers should prefer set_equal in that case for clarity.set_equal, calling on two fresh refs from get_variable_ref (both still `Not_set_yet) raises Row.Shape_error; use set_dim on one side first, or capture both refs through einsum notation (e.g. +*) so that shape propagation can bind them to dimension variables before this call.On a both-solved mismatch, on a one-solved-larger case where the solved dimension is not divisible by factor, on insufficient information (both refs are `Not_set_yet), or when either ref resolves to a row variable, this raises Row.Shape_error.
type compose_type = | Pointwise_binNumPy-style broadcast matching batch, input and output axes, e.g. as in s1 + s2.
| ComposeCompose the outputs of the second shape with the inputs of the first shape, i.e. the shape of fun x -> s1(s2(x)), or s1 * s2 where * is the inner product (e.g. matrix multiply).
| Einsum of Base.string * delayed_var_ref Base.listThe binary "einsum" syntax: RHS1;RHS2=>LHS, where RHSi, LHS are labels specifications. OCANNL's extended einsum notation supports both axis variables and row variables. The axis_labels use pseudo-labels local to the notation, to line up the axes and row variables. The symmetric difference / disjunctive union of RHS1 and RHS2's pseudo-labels should be equal to LHS pseudo-labels.
Unlike Pointwise_bin and Compose, einsum operations use equations only (not inequalities), so they do NOT permit broadcasting. This makes einsum more restrictive but also more precise for operations where exact shape matching is required.
The optional Ir.Indexing.variable_refs will capture the solutions of the dimensions corresponding to the specification labels equal to ref_label of a reference.
Note: The "right-hand-side" is on the left! I.e. the syntax is "rhs=>lhs", "rhs1;rhs2=>lhs".
*)| Defined_by_cd_logicA placeholder for operations where the shape logic is defined by the %cd extension.
*)val sexp_of_compose_type : compose_type -> Sexplib0.Sexp.tval equal_compose_type : compose_type -> compose_type -> Base.booltype transpose_type = | TransposeSwaps inputs and outputs of a shape, preserves batch axes.
*)| Pointwise_unPreserves the shape.
*)| Permute of Base.string * delayed_var_ref Base.listThe unary "einsum" syntax: RHS1=>LHS.
Unlike Pointwise_un, permute operations use equations only (not inequalities), so they do NOT permit broadcasting. This makes permute more restrictive but also more precise for operations where exact shape matching is required.
The optional Ir.Indexing.variable_refs will capture the solutions of the dimensions corresponding to the specification labels equal to ref_label of a reference.
| Batch_slice of Ir.Indexing.static_symbolRemoves the leftmost batch axis.
*)| Uint4x32_to_prec of Ir.Ops.prec Base.Lazy.tConverts precision in a bit-effient way, with a corresponding conversion in total number of elements. Currently, assumes the incoming tensor (RHS) has just a single axis to not force unnecessary minimum sizes on output axes.
*)| Defined_by_cd_logicA placeholder for operations where the shape logic is defined by the %cd extension.
*)val equal_transpose_type : transpose_type -> transpose_type -> Base.boolval sexp_of_transpose_type : transpose_type -> Sexplib0.Sexp.ttype ternary_type = | Pointwise_ternAs in the operation Where.
| Compose_accumulateAs in the operation FMA.
| Defined_by_cd_logicA placeholder for operations where the shape logic is defined by the %cd extension.
*)| Einsum_tern of Base.string * delayed_var_ref Base.listTernary einsum contraction. The spec string has three RHS slots separated by ; and one LHS slot after =>. Axis labels absent from the LHS become reduction axes. E.g. "ij;jk;km=>im" for a chain contraction (note: O(Nā“) ā prefer binary chains for chain contractions; "p;a;b=>out" for select-before-reduce with where.
val equal_ternary_type : ternary_type -> ternary_type -> Base.boolval sexp_of_ternary_type : ternary_type -> Sexplib0.Sexp.tExtracts any available shape information from the initialization or fetch.
val equal_terminal_type : terminal_type -> terminal_type -> Base.boolval sexp_of_terminal_type : terminal_type -> Sexplib0.Sexp.tval make :
?batch_dims:Base.int Base.list ->
?input_dims:Base.int Base.list ->
?output_dims:Base.int Base.list ->
?batch_axes:(Base.string * Base.int) Base.list ->
?input_axes:(Base.string * Base.int) Base.list ->
?output_axes:(Base.string * Base.int) Base.list ->
?deduced:deduce_within_shape ->
debug_name:Base.string ->
id:Base.int ->
Base.unit ->
tCreates a shape. id should be the id the associated tensor (if any). At most one of the pairs batch_dims, batch_axes etc. should be given: if none, the corresponding row will be inferred. batch_axes etc. provide bases (semantic annotations) for the dimensions of the corresponding axes. Note that these are dimension bases and not axis labels: they need not be unique for a row, are inferred when provided, and must match whenever the axis sizes must match.
val to_string_hum : ?style:Row.print_style -> t -> Base.stringBring global state to its initialization values. This invalidates any unfinished inference.
val set_terminal : is_param:Base.bool -> t -> Base.unitMark the shape as terminal, so that its rows can be closed to Greatest Lower Bounds (GLBs). This function is only intended for parameters shapes, which would otherwise not be terminal because of the initialization expressions of the parameters.
val set_resolve_at_use : t -> Base.unitMark the shape's rows as resolved by their use sites (gh-ocannl-544): at the final solver stages their row variables may close to the GLB of the use sites' rows, where operation results close to an empty remainder by default. Used by Tensor.param for parameter-initialization cones and by the stretch operation; terminal shapes (leaves, parameters) are marked automatically.
type logic = | Broadcast of compose_type * t * tMatches the shapes for a binary operation.
For Broadcast (Einsum (ls1, ls2, ls3), s1, s2), the labels of s1 and s2 must match according to the ls1, ls2 lineup, and the resulting shape inherits the labels according to the ls3 lineup.
| Transpose of transpose_type * tPermutes the axes of a shape. One case of Transpose is to swap inputs with outputs of s1, hence the name.
| Broadcast_tern of ternary_type * t * t * tMatches the shapes for a ternary operation.
*)| Terminal of {is_param : Base.bool;logic : terminal_type;}Extracts any available shape information from the initialization. The is_param field indicates if this is a parameter tensor that requires gradients.
| Block of {spec : Base.string;delayed_vars : delayed_var_ref Base.list;rhses : t Base.list;}Block tensor / concatenation operation. Takes an n-ary einsum specification where concatenated axes use the a^b^c syntax, e.g., "x;y;z => x^y^z".
How to propagate shape updates and do the last update of Tensor.t.shape when finalizing the tensor. Axes are broadcast-expanded on a bottom-up update to fit the incoming shape.
val sexp_of_logic : logic -> Sexplib0.Sexp.tval hash_fold_update_id :
Ppx_hash_lib.Std.Hash.state ->
update_id ->
Ppx_hash_lib.Std.Hash.stateval hash_update_id : update_id -> Ppx_hash_lib.Std.Hash.hash_valueval sexp_of_update_id : update_id -> Sexplib0.Sexp.tval update_id_of_sexp : Sexplib0.Sexp.t -> update_idval logic_to_spec : logic -> Base.stringConverts a shape logic to its string specification for debugging/display purposes.
type update_step = private {shape : t;logic : logic;id : update_id;mutable unsafe_projections : Ir.Indexing.projections Base.option;neutral_elem : Base.float Base.option;The neutral element of the operation's accumulation, fixed at creation: Some v when all assignment ops in the update step use the same neutral element v, None when different operations have different neutral elements or when there are no accumulator operations. derive_projections reads it for the clamped-window decision (gh-504) and to commit the margins' neutral (padding_elem), so a step is only created once its operation's assignments are known -- see Tensor.op.
mutable discarded_by : Base.string Base.option;The provenance of the rewind that discarded the step, which get_projections then refuses (gh-ocannl-1789); left out of sexps.
}Data required for a shape inference update step. Ideally, an update should be performed at least twice, the second time after all the other relevant updates have been performed for the first time. In OCANNL, this is achieved by performing updates both as the tensors are constructed, and via lazy callbacks as the corresponding Ir.Indexing dimensions and projections are first accessed.
Private (gh-ocannl-1573): read and matched freely; built only by make_update_step, and only Shape's projection derivation publishes unsafe_projections, once.
val sexp_of_update_step : update_step -> Sexplib0.Sexp.tval make_update_step :
shape:t ->
logic:logic ->
neutral_elem:Base.float Base.option ->
update_stepA fresh update step with a new id and no projections yet. It is not registered: see propagate_shapes.
val to_dims : t -> Base.int Base.arrayUses the matrix convention of putting the input axes last.
The product-space proxy shape of an einsum-family operation (gh-512): the result's axes followed by the reduced-over (contracted) axes, in the product-space order of the derived projections. The proxy shares the operation's shape-inference solution (its constraints are emitted on construction), so infer_equal-ing a %cd *_pspace intermediate against it gives the intermediate the dimensions of the operation's full product space (e.g. the (output x kernel) pair space of a windowed reduction). Must be called before the operation's inference completes. Raises Row.Shape_error when the product space is not expressible as a shape: a non-einsum operation, more than one reduced-over row variable, or a reduced-over row variable combined with an open result input row.
val to_padding : t -> (Ir.Ops.axis_padding Base.array * Base.float) Base.optionReturns the padding of the shape, if any. The inner float option is the padded value: Some v when all operations use the same neutral element, None when different operations require different neutral elements (margin must be reset before each operation). Uses the matrix convention of putting the input axes last.
Runs f with the construction-in-progress depth raised. While it is positive, every entry point that finalizes inference (to_dims, to_padding, get_projections) raises Row.Shape_error at the finalizer, before anything is committed: Tensor.op runs its op_asn under it, since the operation has not registered its constraints yet and a finalization there would close its operands without them (gh-ocannl-830).
val propagate_shapes : update_step -> Base.unitRegisters the step and solves its constraints eagerly. Refused while the session is tainted; a failure after registering taints it.
finish_inference is the session's only commit point: it marks every shape of the steps it commits closed. A construction that fails after mutating the inference session (see Row.mutation_serial) taints it; while tainted, constructions, finalization and compilation raise Row.Shape_error naming the first failure. Tensor.rewind_to_last_closure restores a usable session.
The first failure's provenance, when the session is tainted.
The message of a Row.Shape_error or Utils.User_error, else the printed exception.
The base of the reserved range from which a window that is always rewound -- a shape-scheme trace, a definition check -- allocates its tensors' ids, above every session id (gh-ocannl-1751). Tensor allocates from it; error_to_string_hum recognizes such ids.
val error_to_string_hum :
Base.string ->
Row.error_trace Base.list ->
Base.stringerror_to_string_hum msg trace renders a Row.Shape_error (msg, trace) for a person (gh-ocannl-1799): the message, then one indented line per finding -- the conflicting sizes or rows (or the one axis or row the message is about), the shapes involved, the failed constraint (with ā for "refines", as in Tensor.Scheme.to_string), and the tensors it relates with their axis kinds and the operation that related them. Variables are named by first appearance ('a, ..'b.., followed by :<name> for a named dimension variable), and a tensor of a trace or a check, whose id comes from the reserved range, by first appearance too (n'1, #'1), so the text depends on neither the session's id counters nor the reserved ones; a session tensor keeps its id. This is what Printexc.to_string -- and so the toplevel -- prints for a Row.Shape_error (prefixed "Shape_error: "), and what its sexp converter (Exn.to_string) carries, one atom per line; the setting shape_error_full_sexp=true restores the full constraint sexp to both.
Runs f; if it raises after the session was mutated, taints the session with describe exn, then re-raises with the original backtrace.
Installs the hook called at the end of every successful closure (Tensor records its checkpoint there).
Raises when called within a construction window; what names the caller in the message.
Whether an explicit checkpoint can be taken now: outside a construction window and untainted.
A shape-scheme trace refused by design (gh-ocannl-1751): the function is valid, but a scheme cannot carry what it does -- a precision-dependent total-elements constraint (random number generation), set_dim, set_sym_dim, set_equal or set_scale on a capture reference made outside the trace (gh-ocannl-1791; binding one is an input), or forcing dims or projections during the trace. Raised by Scheme.trace, prefixed "tracing the shape scheme <label>: "; a contradiction raises Row.Shape_error instead.
as_tracing ~label f runs f as part of the shape-scheme trace label: its update steps are not counted or timed as inline registrations. Scheme.trace runs its function this way, and Tensor.Scheme makes a trace's tracers this way just before the trace starts.
type registration_counts = {checked_applications : Base.int;inline_registrations : Base.int;instantiated_constraints : Base.int;instantiated_solutions : Base.int;instantiated_boundary : Base.int;inline_constraints : Base.int;closures : Base.int;closure_constraints : Base.int;rank_search_visits : Base.int;}Registration statistics (gh-ocannl-1752), process-wide since the last reset: checked %op applications, and every registration outside a trace that ran get_inequalities. Projection derivation is not counted.
The constraint counts (gh-ocannl-1752) say what each source adds to the open constraint store: a scheme instantiation's renamed trace constraints (C'), its renamed solution equations (E'), and its boundary and anchor equations -- checked instantiations included, though their window is rewound -- and every registration's own inequalities outside a trace. closure_constraints sums the store's size at the entry of each finish_inference (closures counts them).
rank_search_visits is the rank-cycle search's work counter (gh-ocannl-1802): the states it visited, for every rank fact recorded since the reset, inside traces too.
val sexp_of_registration_counts : registration_counts -> Sexplib0.Sexp.ttype registration_times = {tracing_s : Base.float;instantiation_s : Base.float;inline_steps_s : Base.float;closure_s : Base.float;}Where shape-registration wall time goes (gh-ocannl-1752), process-wide since the last reset, in seconds: tracing schemes (the body on tracers, and the rewind), instantiating them (checked applications and explicit instantiations), every step registered outside a trace, and finish_inference. A phase nested in another (an instantiation inside a closure built mid-construction, say) is charged to the inner phase alone. Construction work that is not shape registration (building tensors, their code) is in none of them.
val sexp_of_registration_times : registration_times -> Sexplib0.Sexp.tval timed : phase -> (Base.unit -> 'a) -> 'aRuns the function, charging its wall time, less that of nested timed phases, to the phase.
val registration_times : Base.unit -> registration_timesval registration_counts : Base.unit -> registration_countsmodule Checkpoint : sig ... endval checkpoint : label:Base.string -> id_mark:Base.int -> Checkpoint.tAn explicit checkpoint; id_mark is the next tensor id. Refuse it while tainted or within a construction window first (Tensor.checkpoint does).
val closure_checkpoint : id_mark:Base.int -> Checkpoint.tThe last closure's checkpoint (empty frontier), which stays valid until the next closure.
The records the session holds of what rewinds discarded, by kind: a diagnostic for tests.
val get_projections : update_step -> Ir.Indexing.projectionsReturns the projections for this update step, computing them if not already done. This triggers finish_inference and then retrieves the projections from unsafe_projections. Use this instead of derive_projections directly.
val of_spec :
?deduced:deduce_within_shape ->
debug_name:Base.string ->
id:Base.int ->
Base.string ->
tval default_display_indices : t -> Base.int Base.arrayval to_bases : t -> Base.string Base.arrayThe per-axis dimension bases (semantic annotations); the basis is total, so unannotated axes read back as "default" and the broadcast top as "bcast_if_1". Uses the matrix convention of putting the input axes last.
val to_bases_bio :
t ->
Base.string Base.array * Base.string Base.array * Base.string Base.arrayPer-kind dimension bases (batch, input, output), so callers can check which row a basis landed on rather than only that it appears somewhere in the flattened to_bases list.
Parse a N5_layout priority string (e.g., "0,1,2") into display indices. Only supports integer labels (Fixed_index).
module Scheme : sig ... end