Kina.ai

Specification

Phase 1, Section 12: `reverseMode` result ABI

Compiler transform_ad only. Not Phase 2 handler AD. Not a Phase 1 exit-criteria replacement. No new operator. This section only says how an already-typed reverse-mode result leaves the compiler.


1. What this section commits to

Section 10 builds a primal and one cotangent per reverseMode parameter. Section 11 can apply those cotangents as explicit g tensors. Neither section returned the pair from MLIR, so a .so could not load Kina-executed gradients.

This section locks the result ABI:

  1. The language type of handle f(args…) with reverseMode is a flat tuple of tensors: primal first, then parameter gradients in parameter order. Discrete (non-floating) parameters are omitted; see Section 14 §3.
  2. That tuple lowers to an MLIR multi-result func.func, not an MLIR tuple<> value.
  3. A fn with that result type gets the existing llvm.emit_c_interface wrapper, so a host can inject arguments and read primal plus gradients from a .so.

There is no new surface keyword, IR constructor, or FFI.


2. What this section does not commit to


3. Language type (amends Section 10)

Section 10’s implementation used a nested pair

(primal, (g₀, …, gₙ₋₁))

The bootstrap surface cannot write a one-element tuple type (T), and the existing .so pipeline (one-shot-bufferize + LLVM C wrappers) does not bufferize MLIR tuple<> types. The result type is therefore flat:

Γ ⊢ f : (T₀, …, Tₙ₋₁) → R
Γ ⊢ argsᵢ : Tᵢ
R = Tensor[…]
each Tᵢ = Tensor[…]                 (this slice; discrete I64 stays an inner let)
─────────────────────────────────────────
Γ ⊢ handle f(args…) with reverseMode
      : (R, T₀, …, Tₙ₋₁)

Constructor order is primal, then each parameter’s cotangent. A single-parameter program has type (R, T₀), written in .kina as

(tensor<F32, [2, 2], @host>, tensor<F32, [2, 2], @host>)

Zero parameters collapse to just R (a one-element tuple unwraps to that tensor). This slice’s examples have at least one differentiable parameter.

The AD pass materializes a flat Tuple of those tensors. It does not wrap the gradients in a second Tuple.


4. Flattening

Any remaining nested TTuple of tensors (legacy IR, multi-carry while) flattens left-to-right, depth-first, before emission:

flatten(Tensor[T, σ]) = [Tensor[T, σ]]
flatten((τ₀, …, τₖ))  = flatten(τ₀) ++ … ++ flatten(τₖ)

A non-tensor element in that spine is unsupported in this slice (fail emit). After flattening, Section 10’s nested pair and §3’s flat tuple produce the same MLIR result list.


5. MLIR function ABI

Let ρ = [τ₀, …, τₘ₋₁] be the flattened tensor types.

m Function result return types
0 (no ->) return
1 -> tensor<…> return %v : tensor<…>
≥ 2 -> (tensor<…>, …) return %v0, … : tensor<…>, …

Do not emit tuple<tensor<…>, …> as a function result or operand type. Parentheses appear only on the func.func result list when m ≥ 2. The return op lists types without an extra tuple wrapper.

This applies to:

@main stays nullary. It does not gain llvm.emit_c_interface in this section; host .so loads use a fn that already has that attribute.

A call that returns m ≥ 2 values binds m SSA names:

%y, %g = func.call @ad(%x) : (tensor<2x2xf32>) -> (tensor<2x2xf32>, tensor<2x2xf32>)

6. LLVM C wrapper (host .so)

After --llvm-request-c-wrappers and function-boundary bufferization, the existing LLVM C wrapper is:

func.func @ad(%x: tensor<2x2xf32>) -> (tensor<2x2xf32>, tensor<2x2xf32>)
  attributes {llvm.emit_c_interface}

is

void _mlir_ciface_ad(
  struct { MemRefDescriptor primal; MemRefDescriptor g0; } *outs,
  MemRefDescriptor *in_x);

The callee allocates the result buffers and writes both descriptors into *outs. Ranked memref layout matches Section 11’s SGD loader. This packing is the LLVM C-wrapper rule for a multi-result func.func; it is not a new Kina FFI.


7. Surface

No new tokens. The existing tuple type

(type, type[, type…])

annotates a wrapper fn whose body is handle f(x) with reverseMode.

The StoryLM-0 numeric instance (neg, seed ones):

let f = fn(x: tensor<F32, [2, 2], @host>) -> tensor<F32, [2, 2], @host> {
  let y = -x in
  y
} in
let ad = fn(x: tensor<F32, [2, 2], @host>)
  -> (tensor<F32, [2, 2], @host>, tensor<F32, [2, 2], @host>) {
  handle f(x) with reverseMode
} in
let x = tensor<F32, [2, 2], @host> in
ad(x)

A program that is only handle f(x) with reverseMode (no wrapper) is still legal. @main then returns the flattened tensors computed from the bound literals.


8. Tests


9. Relation to SGD

Section 11 examples still take w and g as explicit arguments. Naming g inside .kina is Section 13.


10. Open issues

  1. Tuple projection. Specified in Section 13.
  2. @main C wrapper / argument ABI. Not required while wrapper fns exist; revisit if a closed program must be the .so entry.
  3. Non-tensor / device results in the flatten spine.
  4. Custom seed cotangents (not ones).
  5. Discrete parameters. Specified in Section 14: I64 (and other non-floating) arguments do not appear in the result tuple.