Module Ocannl_tensor.Tensor

Construction of runtime-compiled code supporting backpropagation.

type ndarray = Ir.Ndarray.t
type tn = Ir.Tnode.t
type tn_set = Base.Set.M(Ir.Tnode).t
type asgns = Ir.Assignments.t
type fetch_op = Ir.Assignments.fetch_op
type projections = {
  1. projections_debug : Base.string;
  2. projections : Ir.Indexing.projections Base.Lazy.t;
  3. product_shape : Shape.t Base.Lazy.t;
    (*

    The product-space proxy shape of the operation (gh-512), for %cd *_pspace intermediates; see Shape.product_space_shape. Forcing it emits shape constraints, so it must be forced (if at all) while the operation's code is being built. Unlike projections, it does not need the operation's shape update step, which op creates only after op_asn returns: forcing projections from within an op_asn is rejected.

    *)
}
type handout =
  1. | Not_handed_out
  2. | Taken
    (*

    What became of one side of a tensor's code once it stopped being a root: Taken by consume_forward_code, take_forward_code (the %cd embedding path) or consume_backprop_code; Not_handed_out while it is a root, and for a parameter or a subterm whose code a consumer embeds.

    *)
type owner_cell

The tensor's forward owner cell (gh-ocannl-1796): whether its forward code is owned by an unconsumed forward root, kept apart from the session's weak root map so that consume_forward_code's refusal does not depend on what the GC has reclaimed.

type consumption = {
  1. mutable fwd : handout;
  2. mutable bprop : handout;
  3. fwd_owner : owner_cell;
}

A mutable cell shared by every copy of the record, so the marker follows the tensor; it is what lets the consume functions say "already consumed" rather than "not a root".

type diff = {
  1. grad : tn;
  2. zero_grads : comp;
    (*

    Prepares for backpropagation: zeroes this tensor's gradient and, transitively, the gradients its backprop accumulates into. Beware of the "missing zero_grads" bug. The gradients are its Ir.Assignments.embedded_nodes, so it can be compiled as a routine on its own (see Train.zero_params_grads).

    *)
  3. backprop : comp;
    (*

    Backpropagates for the tensor and its descendants; which typically means adding partial gradients to the gradient tensor of the subtensors, then for sub-subtensors etc.

    *)
}
type t = {
  1. params : (t, comparator_witness) Base.Set.t;
    (*

    Parameters t.params are the descendants of t whose forward code is not included in t.forward as it is meant for initialization. This is the state set — what must be initialized, saved and restored for t's forward pass, including non-differentiable running statistics — so a parameter detached behind Operation.stop_gradient (a frozen backbone) is deliberately included even though t's backprop never writes its gradient. Optimizer-side consumers want the trained subset instead: Train.trainable_params derives it from the backprop code (gh-ocannl-673).

    *)
  2. forward : comp;
  3. diff : diff Base.option;
  4. value : tn;
    (*

    The session id and namespace of the tensor are value.id / value.namespace; tensor identity (comparison) follows value.uid.

    *)
  5. top_down_prec : Base.bool;
    (*

    Whether to propagate precision bottom-up (the default) or top-down.

    *)
  6. shape : Shape.t;
    (*

    The eventual shape of t.value and t.diff.grad, incorporating the current state of shape inference.

    *)
  7. children : subtensor Base.list;
  8. consumption : consumption;
}

Information needed for compositional code generation.

and subtensor = {
  1. subtensor : t;
  2. embedded : Base.bool;
    (*

    A tensor can be an embedded child at most once -- that's where its forward computation ends up when used as part of a bigger computation.

    *)
}
and comparator_witness
val sexp_of_t : t -> Sexplib0.Sexp.t
val sexp_of_subtensor : subtensor -> Sexplib0.Sexp.t
val sexp_of_comparator_witness : comparator_witness -> Sexplib0.Sexp.t
val comparator : (t, comparator_witness) Base.Comparator.t
val init_params : ?skip:'a Base.Map.M(Ir.Tnode).t -> t -> comp

init_params ?skip t collects into a single sequence the forward code of t.params, and transitively the initializations of the parameters of the parameters. If skip is provided, it is used to filter out the parameters belonging to skip (e.g. skip can be a set of parameters that are already initialized). NOTE: it always outputs code with a block comment, even if the params set is empty.

val is_fwd_root : t -> Base.bool

Whether t's forward code is still unconsumed: no consumer embedded it and it was not handed out. The session holds its forward roots WEAKLY, checkpoints included (gh-ocannl-1796), as it does its backprop roots: a graph built and dropped without ever being evaluated is reclaimed by the GC, as is a root only the last closure's checkpoint captured. consume_forward_code's refusal of a tensor whose reads another forward root computes does not read the map, so it does not depend on the GC: a root dropped before its code was consumed keeps refusing its readers.

val is_bprop_root : t -> Base.bool

Whether t's backprop code is still unconsumed. The session holds its backprop roots WEAKLY, checkpoints included (gh-ocannl-1745). The entry lives while any record sharing t's consumption cell is reachable from outside the session's root maps: t itself, a { t with ... } copy that keeps ownership, or a tensor that holds t as a child. A differentiable graph whose forward code was handed out (Train.forward, Train.forward_once), and which the caller then dropped, is reclaimed by the GC. A loss built over a tensor the caller still holds embeds its backprop as before. A dropped root no longer vetoes consume_backprop_code; a dependence on its code is refused by the compilation lineage's requirement check (Context.run).

val take_forward_code : t -> comp

The unchecked half of consume_forward_code, for a tensor the caller has established is a forward root (is_fwd_root): removes it from the forward roots, marks its forward code as taken, and returns the code. It is an EMBEDDING, not a handout: rewind_to_last_closure treats an embedding made since the last closure as discarded work and makes the tensor a root again; code a caller keeps is handed out with consume_forward_code. This is the handout every path that embeds a tensor's forward code goes through -- consume_forward_code and the %cd operands' embedding alike -- so that a later consume_forward_code on the same tensor returns the same comp.

val with_unchanged_roots : f:(Base.unit -> 'a) -> 'a

Runs f and restores the forward and backprop root maps afterwards, including the consumption markers of the roots that were live at entry.

val default_value_prec : Ir.Ops.prec Base.ref

The default precision for the value node of terminal (i.e. non-composite) tensors.

Note: the precision of a node can be set arbitrarily via Ir.Tnode.update_prec. The default precision for value nodes of composite tensors is the maximum of precisions of the value nodes of sub-tensors.

val default_grad_prec : Ir.Ops.prec Base.ref

The default precision for the gradient node of terminal (i.e. non-composite) tensors.

Note: the precision of a node can be set arbitrarily via Ir.Tnode.update_prec. The default precision for gradient nodes of composite tensors is the maximum of precisions of the gradient nodes of sub-tensors.

exception Session_error of Base.string * t Base.option
val max_sublabel_length : Base.int Base.ref
val buffer_of : is_grad:Base.bool -> is_merge:Base.bool -> t -> Ir.Assignments.buffer
val raw_accum : initialize_neutral:Base.bool -> accum:Ir.Ops.binop -> t:t -> lhs_is_grad:Base.bool -> shape_logic:Shape.logic -> rhs:Ir.Assignments.accum_rhs -> asgns
type grad_spec =
  1. | Require_grad
  2. | Prohibit_grad
  3. | If_needed
val sexp_of_grad_spec : grad_spec -> Sexplib0.Sexp.t
val is_prohibit_grad : grad_spec -> Base.bool
type param_op_fun = ?input_dims:Base.int Base.list -> ?output_dims:Base.int Base.list -> ?input_axes:(Base.string * Base.int) Base.list -> ?output_axes:(Base.string * Base.int) Base.list -> ?deduced:Shape.deduce_within_shape -> Base.unit -> t
type op_fun = ?label:Base.string Base.list -> ?top_down_prec:Base.bool -> ?batch_dims:Base.int Base.list -> ?batch_axes:(Base.string * Base.int) Base.list -> param_op_fun

Labels are collected in tensor construction order, with more specific information first.

val binop : ?op_label:Base.string -> ?compose_op:Shape.compose_type -> op_asn:(t:t -> t1:t -> t2:t -> projections:projections -> comp) -> grad_asn:(t:t -> g:tn -> t1:t -> t2:t -> projections:projections -> comp) -> ?grad_spec:grad_spec -> t -> t -> op_fun

The defaults are pointwise operations. The grad_asn function receives the non-differentiable variant of the tensor as an argument, which can be used to access the tensor's value in a tensor expression. op_label is prepended to label of the op_fun signature.

The operation's neutral element is read off the assignments op_asn returns, and its shape update step is created afterwards, so op_asn must not force projections.projections, nor finalize inference by any other route (Shape.to_dims/to_padding): the construction is rejected (a Session_error, resp. a Row.Shape_error raised at the finalizer, before anything is committed). The rejection is a bug report, not a recovery API -- shape inference is not transactional (gh-ocannl-903), so the session is reset rather than continued. projections.product_shape and *_pspace intermediates are fine. An operation that needs shape information uses Shape.set_dim or the einsum capture syntax instead; captured references are bound at registration, after op_asn, so relating them (Shape.set_equal/set_scale) belongs at the operation-expression level.

val unop : ?op_label:Base.string -> ?transpose_op:Shape.transpose_type -> op_asn:(t:t -> t1:t -> projections:projections -> comp) -> grad_asn:(t:t -> g:tn -> t1:t -> projections:projections -> comp) -> ?grad_spec:grad_spec -> t -> op_fun

See binop -- same comments apply.

val ternop : ?op_label:Base.string -> ?ternary_op:Shape.ternary_type -> op_asn:(t:t -> t1:t -> t2:t -> t3:t -> projections:projections -> comp) -> grad_asn: (t:t -> g:tn -> t1:t -> t2:t -> t3:t -> projections:projections -> comp) -> ?grad_spec:grad_spec -> t -> t -> t -> op_fun

See binop -- same comments apply.

val blockop : ?op_label:Base.string -> spec:Base.string -> delayed_vars:Shape.delayed_var_ref Base.list -> op_asn:(t:t -> projections:projections -> comp) -> grad_asn:(t:t -> g:tn -> projections:projections -> comp) -> ?grad_spec:grad_spec -> t Base.array -> op_fun

Like binop, but uses block/concatenation shape logic over an array of RHS tensors.

val term : ?init_data:Ir.Assignments.init_data -> ?fetch_op:fetch_op -> ?grad_spec:grad_spec -> op_fun

A terminal: a constant, a parameter, an input of the model. The semantics of shape specification is the same as in Shape.make, and by default the shape will be inferred. At most one of init_data or fetch_op should be provided. If init_data is provided, it is used to initialize the tensor's value node. If fetch_op is provided, it is used to generate the tensor's forward code. If init_data is provided, it is also used to verify the shape of the tensor's value node: Broadcast fills the inferred shape with a scalar without constraining its element count, Reshape (the default) if the data is not padded and both the tensor's shape and padding are inferred, Keep_shape_no_padding if the tensor should not be padded and the shape is as given by the ndarray, and Padded if the data is already padded as given, and the shape is as given by the ndarray.

With ~grad_spec:Require_grad the term is a differentiable leaf that is not a parameter: it stays out of params (nothing initializes it; its value is its data or its forward code), and its gradient carries observation intent (Ir.Tnode.set_observable), as a parameter's does -- the host and later routines are its only readers, so it can be read with Context.get_values or stepped through the optimizers' ?params without a materialization hint. Its value carries the intent too, since a recomputed gradient reads it. A leaf whose value comes from fetch_op is re-established by every forward run, so an optimized input should be data-backed (init_data) or a parameter.

val number : ?label:Base.string Base.list -> ?axis_basis:Base.string -> ?grad_spec:grad_spec -> Base.float -> t

A number: a tensor with a single axis of one dimension, initialized to the given value. axis_basis sets the dimension basis (semantic annotation) of that axis. grad_spec is by default Prohibit_grad.

val bits : ?label:Base.string Base.list -> ?axis_basis:Base.string -> ?grad_spec:grad_spec -> Base.int64 -> t

A number with exact bit representation: a tensor with a single axis of one dimension, initialized to the given int64 value. Useful for initializing uint4x32 tensors where exact bit patterns matter. grad_spec is by default Prohibit_grad.

val ndarray : ?grad_spec:grad_spec -> Base.float Base.array -> op_fun

A tensor with an explicit shape, initialized to the given values. Omitted shape rows default to no axes. grad_spec is by default Prohibit_grad. If strict is true (the default), the given values must fill the tensor's value node precisely; otherwise, the values will be looped over to populate the value node.

val param : ?require_grad:Base.bool -> t:op_fun -> Base.string -> ?more_label:Base.string Base.list -> param_op_fun

param is intended for all tensors with initialization; these typically are parameters, but param is also used for non-differentiable tensors that need to be initialized. For proper parameters, pass require_grad:true. t should produce a tensor with no batch axes; input and output axes should by default be inferred. t's label is the passed string, appended by more_label if any, other parameters are forwarded to t. This function returns t's result with the field params replaced by a singleton set containing that result, and it also updates the memory modes. If require_grad is true, any gradient structure inherited from the initialization expression is replaced by a fresh gradient for the final parameter value only. The result has fresh consumption state, independent of any prior handouts of t’s result.

val param_postprocess : (t -> t) Base.ref

Post-processing hook applied by param to each fully-constructed parameter before it is returned to user code; defaults to the identity. Mixed-precision recipes install a wrapper here that returns a reduced-precision "cast twin" consuming the parameter (gh-ocannl-492 master weights): the graph then reads the twin while the optimizer keeps updating the master parameter, which remains the sole member of the result's params. Reset by unsafe_reinitialize.

val term_init : ?grad_spec:grad_spec -> Base.float Base.array -> op_fun

A term wrapper that sets up the value node initialization (it generalizes ndarray to tensors with inferred shapes).

val consume_forward_code : t -> comp

A forward root is a tensor that is not (currently) used to compute another tensor. On the first handout, checks that non-embedded reads are not owned by other unconsumed roots -- including a root the caller dropped before consuming it, whose code can then never run, so its readers must be rebuilt (gh-ocannl-1796) -- then removes t from the forward roots and returns its comp. Subsequent calls return the same comp, including after take_forward_code (the %cd embedding path), without checking or modifying roots constructed since the first handout. This is equivalent to retaining the original comp; the caller still owns execution order and the availability of non-embedded inputs. Parameters and subtensors embedded in a tensor consumer remain rejected.

The conflicting-roots check carries correctness, not just an early diagnosis (gh-ocannl-1741): no lineage check backs it, since a computation's requires names backprop fragments only, and the run-time initialization check accepts a read the owner never computed when the node is zero-valid or was written by other means (set_values, an earlier routine).

val consume_backprop_code : t -> comp

A backprop root is a differentiable tensor not currently receiving gradients from another tensor. Checks non-embedded reads against other live backprop roots on the first handout and removes t from the backprop roots. Subsequent calls return the same comp without checking or modifying newer roots, as for consume_forward_code; compiling the replayed code a second time in one compilation lineage is refused by Context.compile (gh-ocannl-1724). Non-differentiable tensors and embedded subtensors remain rejected. The returned code does not include zero_grads or seeding the output gradient; callers must still arrange those for each run.

The conflicting-roots check is an early diagnostic (gh-ocannl-1741): a gradient write into another root's propagation is a backprop fragment the returned comp requires, which Context.run refuses unless the routine's own lineage owns it (gh-ocannl-1724). It stays because it fires at the handout, naming the root to consume first, and refuses the order outright where the lineage check would wait for the run.

val iter_embedded : f:(tn -> Base.unit) -> t -> Base.unit

iter_embedded t iterates over all descendant nodes that are embedded, i.e. are members of t.forward.embedded_nodes or 't.diff.backprop.embedded_nodes' (if any). Note: iter_embedded should only be called after shape inference finishes.

val bump_next_id : Base.int -> Base.unit

bump_next_id id ensures the next allocated tensor ID will be strictly greater than id. Called after loading tensors from a checkpoint file to prevent ID collisions with subsequently created tensors.

val get_next_id : Base.unit -> Base.int

get_next_id () returns the next tensor ID that will be allocated by the session.

val rewind_to_last_closure : ?reason:Base.string -> Base.unit -> Base.unit

Restores a usable session after a failed construction, without discarding closed or compiled work (gh-ocannl-1301). A construction that fails after mutating the inference session taints it: from then on, constructions, Shape.finish_inference (so forcing dims or projections), code handout and every Context compile raise, naming the first failure. Rewinding discards everything opened since the last successful Shape.finish_inference -- the failed construction and every construction since that closure, successful or not -- and clears the taint:

  • the solver's open state is emptied (closed shapes carry no variables, and their committed rows, paddings and neutral elements cannot have changed: a write that would change them is refused);
  • the forward and backprop root maps are restored to their state at the closure, with the handout markers of the restored roots reset; code handed out to callers since then stays handed out;
  • the tensors allocated since the closure are discarded (exactly the nodes the frontend allocated, by uid: a node created in that window outside the frontend is not): using one as an operand, handing out or compiling its code, printing it or transferring its node raises Session_error naming the rewind's provenance ("discarded by the rewind after ..."), as do their update steps' projections;
  • variable references the discarded work bound or set are refused by Shape.set_dim and the other reference setters, and by any later einsum they are passed to;
  • the cached random seed is restored to the closure's.

Counters are never reset, so later tensors never collide with discarded ones. Contexts, routines and buffers are untouched, and nothing executed is undone. Without a taint it simply discards the open work (reason names it in the stale-handle errors). Raises when called from within an op_asn. The explicit checkpoints taken since the closure can no longer be rewound to.

Explicit checkpoints (gh-ocannl-903)

A finer rewind than rewind_to_last_closure: back to a point inside the open work -- a REPL phrase, a notebook cell, a tentative trace such as a shape scheme's. Not to be confused with Ocannl.Checkpoint, which saves trained state to files.

type checkpoint

A snapshot of the open inference session.

val checkpoint : ?label:Base.string -> Base.unit -> checkpoint

Snapshots the open frontier: the solver's and the graph's session state (persistent values, recorded as pointers), and every mutable field of the open work's shapes, update steps and tensor nodes and of every live capture reference (Shape.get_variable_ref). The cost is linear in that frontier, so take one per phrase or trace, not per operation. label names it in messages (default "checkpoint #n"). Raises when the session is tainted (rewind the failure first) and when called from within an op_asn.

val rewind : ?reason:Base.string -> checkpoint -> Base.unit

Puts the session back as it was at the checkpoint, and clears the taint:

  • every object open at the checkpoint gets its fields back -- the shapes' rows, paddings and neutral elements (rewritten in place by later operations), the capture references' bindings and solutions (a reference made before the checkpoint and solved after it is unsolved again), the open nodes' precision links, memory-mode intents, ownership and observability (a Tensor.param over an open initializer included);
  • the solver's environment, constraints, active steps, inference sets and rank graph, the graph-root maps (with the handout markers of the restored roots reset) and the random seed are the checkpoint's; code handed out to callers since stays handed out;
  • what the window created is discarded, as by rewind_to_last_closure: its tensors (exactly the nodes the frontend allocated), update steps and the capture references it made and bound raise Session_error / Shape_error naming the rewind's provenance when used.

Counters are never reset. Closed and compiled work is untouched, and nothing executed is undone. The checkpoint stays valid, so it can be rewound to again; the checkpoints taken after it can not. Raises Session_error, before anything is written, when the checkpoint cannot be rewound to: inference closed since it was taken (a closure commits); a rewind past it, rewind_to_last_closure or unsafe_reinitialize ran since; or the window memoized state of the frontier that no rewind can reset -- a dims, padding or projections read of an open shape that raised (its lazy keeps the exception), or an open node's precision that began settling. rewind_to_last_closure is always available. A Tensor.take_forward_code called directly on a tensor that was not a root at the checkpoint leaves its marker taken. Raises when called from within an op_asn.

val with_checkpoint : ?label:Base.string -> (Base.unit -> 'a) -> 'a

with_checkpoint f runs f; when f raises, rewinds to a checkpoint taken at entry and re-raises f's exception with its backtrace. When that checkpoint can no longer be rewound to (say f closed inference), f's exception still wins, and the session keeps any taint f left. A normal return keeps f's work.

val with_rewind : ?label:Base.string -> (Base.unit -> 'a) -> 'a

with_rewind f runs f and always rewinds to a checkpoint taken at entry -- a trace that leaves nothing in the session -- returning f's result. Read what you need of the open state inside f; tensors f creates are discarded handles afterwards, so create the ones meant to outlive the trace (shared parameters) before calling it. When f returns but its checkpoint can no longer be rewound to, the rewind's Session_error is raised; when f raises, f's exception wins, as in with_checkpoint.

module Scheme : sig ... end
module For_testing : sig ... end

Diagnostics and negative controls for the checkpoint machinery's tests.

val update_operand_prec : t -> Ir.Ops.prec -> Base.unit

Sets an operand's precision ahead of an operation that requires it; refused (raises Session_error) when it would change a closed operand's precision.

val update_operands_prec : t Base.list -> Ir.Ops.prec -> Base.unit

update_operand_prec over several operands, all validated before any is written.

val check_not_discarded : what:Base.string -> t -> Base.unit

Raises Session_error when the tensor's value or gradient node was discarded by a rewind.

val unsafe_reinitialize : ?namespace:Base.string -> Base.unit -> Base.unit

Bring global state to its initialization values. This invalidates any previously defined tensors and tensor nodes. Also reinitializes the modules: Shape, Ir.Tnode.

namespace sets the namespace stamped on subsequently created tensor nodes (gh-ocannl-372); this is the only way to change it. Defaults to Ir.Tnode.default_namespace (ocannl) — also when omitted, i.e. reinitializing never preserves a previously set namespace. Namespaces must match A-Za-z_A-Za-z0-9_* so they can qualify identifiers in generated code and debug names (rendered as ns__n42; the default namespace is elided).

While this function is intended for testing, using it can prevent unintentional session state pollution errors.

val set_random_seed : ?seed:Base.int -> Base.unit -> Base.unit

Creates the random seed tensor. If seed is provided, it is used to set the random seed. Otherwise, the seed is taken from the settings.

val get_random_seed : Base.unit -> t

Returns a tensor with the current random seed. Lazily initialized using set_random_seed and reset when unsafe_reinitialize is called. IMPORTANT: all sites using the same global random seed, e.g. using get_random_seed () not separated by a call to unsafe_reinitialize, must descend from the first caller's optimization context.

val random_seeds : Base.unit -> Base.int Base.list

The seeds of the random-seed singletons this session created (set_random_seed, directly or lazily through get_random_seed), oldest first, without one a rewind discarded: every seed the session's graphs can draw from, including those built under with_saved_random_seed.

val with_saved_random_seed : (Base.unit -> 'a) -> 'a

Runs f, restoring the global random-seed singleton (the one get_random_seed returns) to its prior value afterwards, even on exception. Use this to scope set_random_seed mutations — e.g. building several graphs with distinct per-graph seeds — without perturbing the caller's seed. Tensors built inside f keep referencing the seed tensors that were current when they were constructed; only the global pointer is restored.

Printing.

val header : t -> Base.string

Converts ID, label and the dimensions of a node to a string.

val log_debug_info : from_log_level:Base.int -> t -> Base.unit

Logs debug information about the tensor on the default ppx_minidebug runtime.

type array_print_style = [
  1. | `Default
    (*

    The inner rectangles comprise both an input and an output axis, if available. Similarly, the outer rectangle comprises a second-from-end input axis and a second-from-end output axis, if available. At least one batch axis is output, when available. The axes that couldn't be output are printed at position/dimension 0.

    *)
  2. | `N5_layout of Base.string
    (*

    The string should provide exclusively non-negative integer pseudo-labels. The numbers 0-4 represent the priorities of the axes to be printed out, where the priorities correspond to, from highest: horizontal, vertical direction of the inner rectangle, horizontal, vertical direction of the outer rectangle, repetition (see also Node.pp_print). The numbers n >= 5 stand for the actual positions n - 5 within the corresponding axes.

    *)
  3. | `Label_layout of (Base.string * Base.int) Base.list
    (*

    The association from axis labels to integers. The negative numbers -5 to -1 represent the priorities of the axes to be printed out, where the priorities correspond to, from highest: horizontal, vertical direction of the inner rectangle, horizontal, vertical direction of the outer rectangle, repetition (as above). The numbers n >= 0 stand for the actual positions within the corresponding axes. Unspecified axes are printed at position 0.

    *)
  4. | `Inline
    (*

    The tensors are printed linearly, in a bracketed manner, optionally prefixed with the labels specification. Note that the syntax causes ambiguity for 1-dimensional input axes (underscores are used for axes without explicit labels); when there is a 1-dimensional input axis, we output the labels specification even if there are no axis labels as a way to display the number of axes. The axis nesting is right-to-left (rightmost is innermost). The input axes are innermost and the batch axes outermost. The input axes use , as a separator and () as axis delimiters, but the delimiter for the outermost (i.e. leftmost) axis is omitted. The output axes use ; as a separator and [] as axis delimiters (obligatory). The batch axes use ; as a separator and [||] as axis delimiters (obligatory).

    *)
]

We print out up to 5 axes when printing a tensor, as a grid (outer rectangle) of (inner) rectangles, possibly repeated (screens).

val sexp_of_array_print_style : array_print_style -> Sexplib0.Sexp.t
val to_printbox : ?single_node:Base.bool -> ?embedded_only:Base.bool -> ?entries_per_axis:Base.int -> ?with_id:Base.bool -> ?force:Base.bool -> ?ctx:Context.t -> ?with_shape:Base.bool -> ?with_value:Base.bool -> with_grad:Base.bool -> depth:Base.int -> t -> PrintBox.t

to_printbox is the box print_tree prints, laid out by depth the same way, with with_id and with_value defaulting to false and true.

val to_doc : ?ctx:Context.t -> force:Base.bool -> with_grad:Base.bool -> with_code:Base.bool -> ?with_low_level:Base.bool -> array_print_style -> t -> PPrint.document
val print : ?here:Ppx_here_lib.position -> ?force:Base.bool -> ?ctx:Context.t -> with_grad:Base.bool -> with_code:Base.bool -> ?with_low_level:Base.bool -> array_print_style -> t -> Base.unit

print renders a tensor's metadata and, when an explicit ?ctx is supplied, its values via an on-demand device-to-host transfer (gh-ocannl-333). Without ?ctx (the default) values are shown as a placeholder.

val print_forward_roots : with_grad:Base.bool -> with_code:Base.bool -> array_print_style -> Base.unit
val print_tree : ?here:Ppx_here_lib.position -> ?force:Base.bool -> ?ctx:Context.t -> ?entries_per_axis:Base.int -> ?with_backend_info:Base.bool -> ?with_id:Base.bool -> ?with_shape:Base.bool -> ?with_value:Base.bool -> ?embedded_only:Base.bool -> with_grad:Base.bool -> depth:Base.int -> t -> Base.unit

print_tree renders the tensor's graph: its outermost depth levels side by side in boxes, the levels below them as an indented tree. depth shapes the layout only, so every node of the graph is shown. With ?ctx, the values of all the nodes it shows are read together through Context.to_host_all, so the virtual ones are recomputed by one throwaway routine (gh-ocannl-1737); an unreadable node is shown as a placeholder naming why (Context.observability_tag). print reads the value and gradient it shows the same way.

val debug_name : t -> Base.string