Ir.OpsOperation types shared by all backends; and precision types.
module Lazy = Utils.Lazytype ('ocaml, 'impl) precision = | Byte : (Base.char, uint8_elt) precision| Uint16 : (Base.int, uint16_elt) precision| Int32 : (Base.int32, int32_elt) precision| Uint32 : (Base.int32, int32_elt) precisionUsing int32_elt representation but treating as unsigned
*)| Int64 : (Base.int64, int64_elt) precision| Uint64 : (Base.int64, int64_elt) precisionUsing int64_elt representation but treating as unsigned
*)| Uint4x32 : (Stdlib.Complex.t, Stdlib.Bigarray.complex64_elt) precisionA 128-bit value that corresponds to e.g. CUDA's uint4 type. Luckily, the OCaml Bigarray library supports complex64_elt which is a 128-bit value, so we avoid dims conversions.
*)| Half : (Base.float, float16_elt) precision| Bfloat16 : (Base.int, uint16_elt) precisionUsing uint16 representation for now
*)| Fp8 : (Base.char, uint8_elt) precisionUsing uint8 representation for now
*)| Single : (Base.float, float32_elt) precision| Double : (Base.float, float64_elt) precisionval sexp_of_precision :
'ocaml 'impl. ('ocaml -> Sexplib0.Sexp.t) ->
('impl -> Sexplib0.Sexp.t) ->
('ocaml, 'impl) precision ->
Sexplib0.Sexp.ttype prec = | Void_prec| Byte_prec of (Base.char, uint8_elt) precision| Uint16_prec of (Base.int, uint16_elt) precision| Int32_prec of (Base.int32, int32_elt) precision| Uint32_prec of (Base.int32, int32_elt) precision| Int64_prec of (Base.int64, int64_elt) precision| Uint64_prec of (Base.int64, int64_elt) precision| Uint4x32_prec of (Stdlib.Complex.t, Stdlib.Bigarray.complex64_elt) precision| Half_prec of (Base.float, float16_elt) precision| Bfloat16_prec of (Base.int, uint16_elt) precision| Fp8_prec of (Base.char, uint8_elt) precision| Single_prec of (Base.float, float32_elt) precision| Double_prec of (Base.float, float64_elt) precisionval byte : precval uint16 : precval int32 : precval uint32 : precval int64 : precval uint64 : precval uint4x32 : precval half : precval bfloat16 : precval fp8 : precval single : precval double : precval all_precs : prec listCanonical witnesses for every precision, including Void_prec. prec carries GADT witnesses, so its enumeration cannot use @@deriving enumerate. Keep the exhaustive sentinel beside this list: a new constructor requires an explicit enumeration decision here.
val _all_precs_is_complete : prec -> Base.unitFamilies describe arithmetic, not the Bigarray representation: bf16 and fp8 are floats despite integer storage witnesses. Uint4x32_prec is packed RNG state, not a scalar integer; Void_prec represents no value and belongs to neither arithmetic family.
val prec_family : prec -> prec_familyval is_integer : prec -> boolScalar integer arithmetic only; excludes packed uint4x32 and Void_prec.
val is_float : prec -> boolval scalar_precs : prec Base.List.tScalar arithmetic formats, including integer and floating-point formats.
val integer_precs : prec Base.List.tval float_precs : prec Base.List.tval storage_precs : prec Base.List.tAll storage-bearing formats, including packed RNG state; Void_prec is excluded. Backend rendering sweeps use this set even though individual operators can reject particular pairs.
val index_prec : unit -> precReturns the precision to use for indexing arithmetic: signed int32, or int64 under the large_models setting (docs/proposals/signed-index-precision.md). Signedness makes machine index arithmetic agree with mathematical integers (no wrap traps for negative intermediates such as convolution offset = -padding forms), and is friendlier to C compilers than defined-wrap unsigned. Unsigned precisions remain for data domains only (uint4x32 RNG state, uint8/uint32 stored values such as class IDs). Int32 overflow is excluded by contract, not by widening: every per-node (padded) element count must fit int32 unless 64-bit indices are in use -- enforced once per node by Tnode.create forcing dims. large_models still selects the 64-bit width here pending the tnode-granular, interval-driven codegen resolution (which also needs a separate resolution for the Metal pool-slot width before the flag can be retired).
val is_up_to_fp16 : prec -> boolval is_narrow_float : prec -> boolThe floating-point formats narrower than f32: the ones a CPU has no arithmetic for, so that every operator over them is a widen/op/narrow round-trip through f32 (gh-ocannl-517). Integer precisions are excluded — their arithmetic is native at every width.
Whether a constant is too large to be safe in a half-precision node. The cutoff (check_half_prec_constants_cutoff, default 2^14) sits well below fp16's 65504 max finite: it is headroom against overflow during arithmetic on the constant, not a representability test.
Both infinities are therefore out of scope by construction, not by exception (gh-ocannl-547). They are exactly representable in IEEE binary16; they are emitted deliberately as reduction identities — see neutral_elem, whose Max is neg_infinity and Min is infinity, with C_syntax rendering them as (-INFINITY) / INFINITY and the GPU backends converting through __float2half / __double2half / a (half) cast, all of which map infinities per IEEE; and no arithmetic can push an infinity past a finite bound, so there is no headroom to preserve. An identity is a sentinel arithmetic is expected to consume, not to accumulate.
Refusing them made every fp16 max-reduction — hence every fp16 softmax, hence every attention model at f16 — fail during lowering, on every backend. NaN is likewise out of scope, by the comparison below being false for it.
val sexp_of_prec : prec -> Base.Sexp.tval prec_of_sexp : Base.Sexp.t -> precval precision_to_string : ('ocaml, 'elt_t) precision -> stringval prec_string : prec -> stringval prec_of_string : Base.String.t -> precWhether every value of precision from converts to to_ exactly, so that a conversion through to_ is the identity on it: equal precisions, or a float widening (fp8 into any wider float -- both e4m3 and e5m2 fit half's and bfloat16's exponent ranges and significands -- half or bfloat16 into single or double, single into double). Integer conversions are conservatively false (gh-ocannl-1788).
val prec_in_bytes : prec -> intval significand_bits : prec -> intThe significand width of a floating-point precision, the implicit leading bit included: a format with p such bits holds every integer up to 2^p exactly. 53 for double, 24 for single, 11 for half, 8 for bfloat16 and 3 for fp8 (e5m2: two stored mantissa bits). The one table behind exactness bounds such as schedule_bench's and the tests' Ll_test.exact_partials ~sig_bits (gh-ocannl-1741). Raises Invalid_argument on a non-float precision, which has no significand.
Prefer precision which is more likely to remain functional in the resulting computations. uint4x32 always dominates, because operations that work on uint4x32 do not support other precisions. Otherwise, fractional number precisions dominate; within them, larger dynamic range precisions dominate.
val apply_prec : ?default:??? -> 'a apply_prec -> prec -> 'aval c_typ_of_prec : prec -> stringval vec_unop_lanes : prec -> Base__Int.tval c_vec_typ_of_prec : length:int -> prec -> stringval hum_typ_of_prec : prec -> stringSee: tinygrad ops, CUDA Math API (intrinsics).
This is a redundant set of operations, aiming to expose hardware-supported "intrinsics", to reduce the need for backends to pattern-match and optimize. Also for convenience.
type binop = | Arg1| Arg2| Add| Sub| Mul| Div| ToPowOf| Relu_gate| Satur01_gate| Max| Min| Mod| Cmplt| Cmple| Cmpeq| Cmpne| Or| And| Threefry4x32_crypto4x32-bit Threefry PRNG, 20-round cryptographic version. Requires a 128-bit key and a 128-bit counter and outputs a 128-bit value (precision Uint4x32).
| Threefry4x32_light4x32-bit Threefry PRNG, 2-round light version (as in JAX/XLA). Requires a 128-bit key and a 128-bit counter and outputs a 128-bit value (precision Uint4x32).
| Uint4x32_to_prec_uniform_laneExtracts one lane of the packed uniform conversion: the first argument is a Uint4x32 block, the second an integer lane index in [0, vec_unop_lanes prec); the result is lane i of vec_unop.Uint4x32_to_prec_uniform applied to the block, guaranteed bitwise-identical to the corresponding vectorized store. IR-internal (gh-509 task 4): minted by the virtualizer to inline packed-uniform results per cell; not exposed in the user-facing syntaxes. Heterogeneous: the arguments must not be converted to the result precision.
val sexp_of_binop : binop -> Sexplib0.Sexp.tval all_of_binop : binop Base.listtype unop = | Identity| Relu| Satur01Saturate (truncate) to within the interval [0; 1].
| Exp| Log| Exp2| Log2| Sin| Cos| Sqrt| Recip| Recip_sqrt| Neg| TruncTruncate toward zero to an integer value (matches a C (int) cast).
| Tanh_approx| Not0. -> 1. | _ -> 0.
*)| Uint4x32_to_prec_uniform1Non-vectorized variant of Uint4x32_to_prec_uniform that converts the given Uint4x32 to a single value of the output precision. Less bit-efficient but operates poitwise. For random bits, the result is uniform over the range of the precision for integer precisions, and over the range [0.0, 1.0) for floating point precisions.
val sexp_of_unop : unop -> Sexplib0.Sexp.tval all_of_unop : unop Base.listtype vec_unop = | Uint4x32_to_prec_uniformConverts the given Uint4x32 to the precision of the output in a bit-efficient manner. For random bits, the result is uniform over the range of the precision for integer precisions, and over the range [0.0, 1.0) for floating point precisions. When used in an access pattern, the indices are converted to a byte offset depending on the given precision. NOTE: this operation, unlike any others, impacts projections and shape inference (one input cell corresponds to a few output cells).
*)val sexp_of_vec_unop : vec_unop -> Sexplib0.Sexp.tval all_of_vec_unop : vec_unop Base.listval sexp_of_ternop : ternop -> Sexplib0.Sexp.tval all_of_ternop : ternop Base.listval sexp_of_op : op -> Sexplib0.Sexp.tWhich of an operator's operands actually evaluate — decided here, next to the op definitions, and nowhere else (gh-ocannl-582). An operator whose rendering discards or short-circuits an operand is a fact three unrelated passes have to agree on:
binop_c_syntax, ternop_c_syntax and every backend's shadowing binop_syntax / ternop_syntax, plus C_syntax.pp_scalar, which emits a projection's selected operand alone) decide what a kernel actually evaluates;Low_level.affine_accesses enumerates the accesses an unevaluated operand does not make;Cost_model's upper and floor walks charge, respectively, the most and the least an operand tree can cost, and its certainty pre-pass decides whose reads are conditional.They drifted apart three times in one review because each restated the same case analysis. Both classifiers below are matched exhaustively (no wildcard arm), so adding an operator to binop or ternop forces the decision here, and every consumer inherits it. The renderers are checked against it rather than consulting it — see C_syntax.operand_conditionality_violations.
The classification says which operands a rendering is REQUIRED to leave unevaluated, at most. One per-instance exception evaluates more (gh-ocannl-1622): a backend with a branchless select_blend_syntax (cc, HIP) evaluates every arm of a value select that Low_level.speculable_select admits. Floors over this classification stay floors.
type binop_conditionality = | Both_operandsBoth operands are always evaluated.
*)| Gated_secondThe first operand is always evaluated, the second one only when the first passes a test: the C-family renderings are &&, || and the gates' ?:. Under a guards-taken upper bound the second operand counts; under a guards-never-taken floor it does not.
| Only_firstA projection: the operator is its first operand, and the second one is never rendered, hence never evaluated — it costs nothing and accesses nothing under any bound.
*)| Only_secondA projection onto the second operand; the first is never rendered.
*)val sexp_of_binop_conditionality : binop_conditionality -> Sexplib0.Sexp.ttype ternop_conditionality = | All_threeAll three operands are always evaluated.
*)| Cond_and_one_armThe first operand (the condition) is always evaluated, then exactly one of the second and third: the renderings are short-circuiting ?: on every backend, deliberately so — MSL's select would evaluate both arms and defeat the range guards that lower to Where (see Metal_backend's ternop_syntax).
val sexp_of_ternop_conditionality : ternop_conditionality -> Sexplib0.Sexp.tval binop_conditionality : binop -> binop_conditionalityval ternop_conditionality : ternop -> ternop_conditionalityval neutral_elem : binop -> Base.Float.tEither the left-neutral or right-neutral element of the operation. Unspecified if the operation does not have a neutral element.
Add's is +0., deliberately not IEEE's additive identity -0. (gh-ocannl-1457): two zeros sum to -0 only when both are -0, so a +0 start makes every all-zero sum +0 on every schedule, while zero-fringe padding, tensor-core K-padding and split partials add +0 terms that a -0 start would make the sign depend on. The decision record: docs/agent-notes/backend-precision-and-simd.md, pinned by test/operations/sum_signed_zero.
val interpret_binop : binop -> Base.Float.t -> Base.Float.t -> Base.Float.tval interpret_unop : unop -> Base.Float.t -> Base.Float.tval interpret_ternop :
ternop ->
Base.Float.t ->
Base.Float.t ->
Base.Float.t ->
Base.Float.tNote: currently the %cd syntax only supports infix binops as assignment ops.
val is_binop_nice_infix : binop -> boolval binop_cd_syntax : binop -> stringval binop_cd_fallback_syntax : binop -> stringIn the %cd syntax, we support uncurried notation for binary ops in addition to the infix notation.
val is_assign_op : binop -> boolval assign_op_cd_syntax : initialize_neutral:bool -> binop -> stringval unop_cd_syntax : unop -> stringNote: currently we do not support unary prefix symbols.
val vec_unop_cd_syntax : vec_unop -> stringval ternop_cd_syntax : ternop -> stringIn the %cd syntax, we use uncurried notation for ternary ops.
val no_plain_uint4x32_conversion_message : to_:prec -> Base.String.tgh-ocannl-1785: what a plain conversion out of packed RNG state means -- nothing, on every backend. A uint4x32 block holds 128 random bits; which of them make a scalar, and how, is what the uniform operators name (Uint4x32_to_prec_uniform1, Uint4x32_to_prec_uniform, and Uint4x32_to_prec_uniform_lane, which names the lane). Metal used to read a plain conversion as the uniform one and CUDA/HIP as lane 0's bits; every backend's convert_precision now raises this one refusal instead.
val no_plain_uint4x32_conversion : to_:prec -> 'aval sexp_of_voidptr : voidptr -> Base.Sexp.tval c_rawptr_to_string : Base.nativeint -> prec -> Base.String.tval rawptr_to_string_hum : Base.nativeint -> prec -> Base.String.tval c_ptr_to_string : 'elem Ctypes.ptr -> prec -> Base.String.tval ptr_to_string_hum : 'elem Ctypes.ptr -> prec -> Base.String.tval axis_padding_of_sexp : Sexplib0.Sexp.t -> axis_paddingval sexp_of_axis_padding : axis_padding -> Sexplib0.Sexp.tval equal_axis_padding : axis_padding -> axis_padding -> Base.boolOne-step f64 -> e5m2, for the host side (an OCaml float is a double) and for the C backends' Double_prec sources. Narrowing via f32 first rounds twice, which disagrees with the GPU fp8 types for values just off an f32 tie (gh-ocannl-648).
val copy_with_padding_c :
('a, 'b, Stdlib.Bigarray.c_layout) Stdlib.Bigarray.Genarray.t ->
('a, 'b, Stdlib.Bigarray.c_layout) Stdlib.Bigarray.Genarray.t ->
axis_padding Base.array ->
Base.unitThreefry4x32 PRNG - 20 round cryptographic version
Threefry4x32 PRNG - 2 round light version
Threefry4x32 PRNG - default version
Conversion from uint4x32 to various uniform distributions
val is_homogeneous_prec_unop : unop -> boolReturns true if the unary operation is homogeneous in precision, meaning its argument should be converted to the result precision.
val is_homogeneous_prec_vec_unop : vec_unop -> boolReturns true if the vec_unop operation is homogeneous in precision, meaning its argument should be converted to the result precision.
val is_homogeneous_prec_binop : binop -> boolReturns true if the binary operation is homogeneous in precision, meaning its arguments should be converted to the result precision.
val is_homogeneous_prec_ternop : ternop -> boolReturns true if the ternary operation is homogeneous in precision, meaning its arguments should be converted to the result precision.
What an operator yields when it is evaluated at an integer compute precision, by which the renderer (C_syntax) and the simplifier (Low_level.float_valued, the classification's one reader over expression trees) agree on the rule: a float-valued subexpression is computed in float -- through the operators that follow their operands, as C's usual arithmetic conversions do -- and converted to the integer precision once, at the store or scope holding it, or at an operand of an Integer_only operator.
Real_valued: a math function with no integer meaning; computed in float whatever its operand.Follows_operands: float-valued exactly when one of its value operands is.Truth: a comparison or logical connective; with a float-valued operand it compares in float, and its 0/1 result is an integer.Integer_only: its integer spelling takes integers only (Mod's %); a float-valued operand converts at the operand.Projection: the selected operand itself (binop_conditionality).Integer_table: rendered by the integer table as it stands, never float-valued: the heterogeneous RNG operators, and the operators refused at an integer precision (FMA, ToPowOf) -- an integer power is a meaning of its own, refused identically on every backend.val sexp_of_integer_valuation : integer_valuation -> Sexplib0.Sexp.tval equal_integer_valuation :
integer_valuation ->
integer_valuation ->
Base.boolval unop_integer_valuation : unop -> integer_valuationval binop_integer_valuation : binop -> integer_valuationval ternop_integer_valuation : ternop -> integer_valuationWhere follows its two arms; its condition keeps its own precision and type, as in C.