TorchLean

3.3. Backend Selection🔗

The previous page selected CPU or CUDA through the runtime API. A less visible question remains: when a graph asks for matrix multiplication, attention, or a reduction, how does TorchLean decide which implementation is allowed to answer?

A device name is not enough. TorchLean's CUDA attention composes matrix products and softmax in Lean over ATen numerical primitives. Its intermediate storage, backward rule, and supporting evidence need to be identified. The backend planner keeps those contracts in data and either returns an accepted plan or explains why it could not make one.

The path is:

\text{operation}+\text{profile}+\text{available providers} \longrightarrow \text{capsule} \longrightarrow \text{audit} \longrightarrow \text{contract check} \longrightarrow \text{accepted kernel} \longrightarrow \text{typed handler}.

The Lean examples run the planner during elaboration. We can ask it to select an attention kernel, misfile a contract deliberately, and inspect the rejection before any kernel executes.

Device selection first requires a profile. There are nine device constructors, and maintained profiles exist for two of them:

-- Inspect maintained policy defaults without probing a -- device or launching a kernel. cpu: checked_cpu cuda: libtorch_cuda rocm: no maintained profile metal: no maintained profile tpu: no maintained profile #eval do for d in [Device.cpu, .cuda, .rocm, .metal, .tpu] do match BackendProfile.maintainedForDevice? d with | some p => IO.println s!"{d.cliName}: {p.name}" | none => IO.println s!"{d.cliName}: no maintained profile"
cpu: checked_cpu
cuda: libtorch_cuda
rocm: no maintained profile
metal: no maintained profile
tpu: no maintained profile

The Option result distinguishes a maintained profile from an unsupported request. When the result is none, the caller must supply a profile describing admissible implementations and their evidence before execution can proceed.

The cpu and cuda lines report names of maintained policies, not results of hardware probes. The CPU default uses checked assurance. The CUDA default admits the explicitly recorded LibTorch boundary for operations without retained numerical comparisons. BackendProfile.checkedCuda remains available when we want planning to reject those operations instead. This distinction lets a tool inspect a CUDA plan on a machine that cannot execute it. The three none cases mean no default policy has been supplied for those devices; adding a constructor to Device would not change that result. A custom profile must describe the provider it intends to use, and the eventual runtime must bind that description to callable implementations. Keeping planning pure makes missing coverage visible before data transfer or training begins.

Kernel Capsules🔗

Suppose a graph reaches scaled dot-product attention. Its CUDA capsule describes a Lean composition of matrix products and softmax, with an explicit local VJP over saved probabilities. LibTorch supplies the numerical primitives while TorchLean owns the tape, saved buffers, and parameter gradients. The capsule's contract covers this composition; it does not select a fused attention implementation or record a LibTorch autograd graph.

A KernelCapsule records those differences:

-- These fields describe an implementation contract before
-- any handler is called.
structure KernelCapsule where
  name : String
  op : BackendOp
  provider : Provider
  device : Device
  trustLevel : TrustLevel
  supportsForward : Bool
  vjpMode : VJPMode
  shapeContract : ContractDescriptor
  layoutContract : ContractDescriptor
  valueContract : ContractDescriptor
  vjpContract : ContractDescriptor
  numericalPolicy : NumericalPolicy

Capsules are declared before the run and registered with the backend. The planner may select one only when its device, provider, gradient mode, and trust level fit the requested profile. If no capsule fits, planning stops with an error.

Capsules are collected in named Registry.CapsuleModules. Attention, LibTorch primitives, and portable reference operations contribute modules to the same registry. A downstream provider can prepend another module with BackendProfile.withCapsuleModules; it does not add a new model class or a branch to the graph walker. The model still lowers to ordinary BackendOps, and the planner either finds an admissible capsule for each operation or reports the missing operation.

The registry rejects duplicate module names:

-- Duplicate module identity is an error even if each module -- contains valid capsules. Except.error "duplicate backend capsule module `libtorch`"#eval Id.run do let m : Registry.CapsuleModule := { name := "libtorch", capsules := LibTorch.capsules } return Registry.validateModules #[m, m] Except.ok ()#eval Registry.validateModules Registry.maintainedModules
Except.error "duplicate backend capsule module `libtorch`"
Except.ok ()

BackendOp names semantic operation families such as matrix multiplication, reduction, pooling, or convolution. Rank, axes, padding, strides, and index tensors remain in the graph payload. This keeps capability discovery general without erasing the information needed to state the operation correctly. The catalog is one flat list; both maintained profiles share it, and the device filter happens when a plan is made, not when a module is registered.

The sources are Capsule.lean for the record and Registry.lean for the modules. The installation guide has an installation and kernel overview.

Distinct modules can still offer the same operation; the profile decides which one to use.

Capsule Contracts And Evidence🔗

Each capsule has four obligations: shape, layout, forward value, and VJP. Each is a ContractDescriptor: a structured ContractClaim, a one-line summary for readers, and the evidence behind the claim.

The claim is one of shape safety, compatibility with a named tensor layout, refinement of a forward specification, refinement of a VJP specification, or vjpUnavailable for a forward-only capsule. Before planning, TorchLean checks that each descriptor has the right obligation kind and operation, and that a VJP descriptor names the capsule's declared VJP mode.

For example, moving a value test into the portable attention capsule's shape field makes the descriptor disagree with its assigned obligation:

-- Keep the value claim intact but deliberately assign it to -- the wrong obligation. def bsMisfiled : KernelCapsule := { Reference.attention with name := "demo.misfiled" shapeContract := ContractDescriptor.tested (.valueRefinement .attention) "value test filed as shape evidence" "demo suite" } (true, false)#eval (Reference.attention.contractsAligned, bsMisfiled.contractsAligned)
(true, false)

A misaligned capsule is not merely reported; KernelCapsule.admissible requires contractsAligned, so the planner will not select it at all:

-- The malformed descriptor must prevent selection, not -- merely appear in a warning. Except.error "no admissible kernel capsule for op attention on device cpu"#eval planOp BackendProfile.checkedCpu.policy #[bsMisfiled] .attention |>.map fun k => k.capsule.name
Except.error "no admissible kernel capsule for op attention on device cpu"

A value test placed in the shape field is therefore rejected rather than counted as shape evidence, and a forward-only capsule cannot describe .none as though it were a VJP refinement.

ContractEvidence classifies the evidence in four ways:

  • runtimeGuard name records validation performed at an execution boundary;

  • testSuite name records a regression or differential test suite;

  • trustedBoundary reason names code whose correctness is assumed for the claim;

  • notApplicable records an obligation that the capsule intentionally does not provide.

There is no "proved" evidence variant: this descriptor does not carry a Lean proof of native implementation correctness. What a capsule can say is which guards and tests stand behind it, or that it delegates to code TorchLean does not check. The related idea of proof-carrying code (Necula, 1997) requires a producer to ship a checkable certificate with its binary. A capsule carries a weaker record: a classified claim and its evidence source. The checker verifies that classification and the caller's policy, without verifying the kernel.

NumericalPolicy currently has one field, reduction : ReductionPolicy, with values fixedLeft, implementationDefined, and notApplicable. The portable matrix-product capsule records the fixed left fold used by the tensor semantics; the LibTorch CUDA capsules record an implementation-defined reduction. A fixed-left range trace therefore cannot be reused for an ATen schedule merely because both capsules implement matmul. The runtime has precision and deterministic-algorithm controls, but the capsule does not audit their readbacks or specify every rounding, subnormal, and multiply-add choice.

Trust has two levels, checked and trustedExternal. The maintained profiles accept checked capsules whose evidence is guards and tests. A capsule whose value contract rests on a trusted boundary is admitted only under the explicit external policy, even if the capsule itself is labelled checked: the contract check reads the evidence, not the label.

Alignment checks that each claim belongs in its assigned field. Whether a named test adequately covers that claim remains a separate question about the test and implementation.

For a matrix product, layout and numerical policy answer different questions. Layout determines which stored elements are interpreted as each row and column. Reduction policy determines the order in which products are added. Two implementations may agree on every address and still produce different last bits through their summation orders. A shape check can catch incompatible matrix dimensions but cannot establish either of those numerical relationships. This is why a capsule carries several obligations instead of one broad correctness label.

Kernel Selection Plans🔗

Planning is a pure function from a profile and a list of operations to a plan or an error message, so a plan can be printed. This helper prints one block per selected capsule:

-- Print the selected implementation and the evidence -- relevant to its forward values. def bsRows (profile : BackendProfile) (ops : Array BackendOp) : IO Unit := do match profile.planOps ops with | .error e => IO.println s!"planning failed: {e}" | .ok plan => for a in plan.audit.kernels do IO.println s!"{a.op.name} -> {a.capsuleName}" IO.println s!" provider {a.provider.label}, \ trust {a.trustLevel.label}" IO.println s!" reduction \ {a.numericalPolicy.reduction.label}" let value := a.valueContract.evidence.label IO.println s!" value: {value}" matmul -> reference.matmul provider reference, trust checked reduction fixed-left value: covered by test suite NN.Tests.Runtime.Floats.Suite relu -> reference.relu provider reference, trust checked reduction n/a value: covered by test suite NN.Tests.Runtime.Floats.Suite #eval bsRows BackendProfile.checkedCpu #[.matmul, .relu]
matmul -> reference.matmul
  provider reference, trust checked
  reduction fixed-left
  value: covered by test suite NN.Tests.Runtime.Floats.Suite
relu -> reference.relu
  provider reference, trust checked
  reduction n/a
  value: covered by test suite NN.Tests.Runtime.Floats.Suite

The portable matrix product commits to a fixed left fold, and relu reports n/a because a pointwise operation performs no reduction at all. Those are the labels a range trace or a benchmark record should quote, and Runtime Approximation explains why a summation order is not a detail.

A missing implementation is an ordinary value, available before any kernel is launched:

-- These family-level requests require dedicated capsules in -- the selected CPU catalog. no admissible kernel capsule for op selective_scan on device cpu no admissible kernel capsule for op fft_fno on device cpu #eval do for op in [BackendOp.selectiveScan, BackendOp.fftFno] do match BackendProfile.checkedCpu.planOps #[op] with | .ok plan => IO.println s!"{plan.capsuleNames}" | .error e => IO.println e
no admissible kernel capsule for op selective_scan on device cpu
no admissible kernel capsule for op fft_fno on device cpu

The registered selective_scan and fft_fno capsules are CUDA-only, so the CPU profile cannot plan those operation-family requests. Generic differentiable scan and real-FFT reference paths can instead express their computation through supported ordinary operations. The error identifies a missing capsule for the requested family, not the absence of every CPU implementation of the underlying algorithm.

The reference.matmul and reference.relu rows also explain how to read a successful plan: the name identifies the implementation family, the provider identifies its owner, and the value-evidence line names the retained test source. The planner does not run that suite while printing the report. Its input is the registered evidence record. A benchmark that stores only checked_cpu loses these per-operation choices; storing the selected capsules makes later changes in preference or registry order inspectable.

The Implementation Inside An Attention Capsule🔗

Selecting libtorch.direct_attention identifies TorchLean's Lean-composed attention route. The name is retained for capsule identity; ATen chooses the implementations of its numerical primitives, but there is no fused attention provider selection. The public controls in LibTorch still expose the SDK's flash, efficient, math, and cuDNN attention permissions.

For example, this definition changes the SDK's flash permission when called; it does not change TorchLean's attention composition:

def bsDisableFlash : IO Unit := Runtime.Autograd.LibTorch.setSDPEnabled .flash false

getSDPEnabled reads that SDK permission. TorchLean's attention calls matrix-product and softmax primitives directly, so these permissions do not govern its forward or backward execution.

The planner can inspect and reject the capsule before data transfer. Its accepted plan does not establish that the request fits device memory: attention materializes the full score matrix and saves probabilities, using quadratic memory in sequence length. GPU and CUDA follows those buffers through forward and backward.

The Attention Specification Theorem🔗

TorchLean's FlashAttention specification gives a precise target for refinement:

-- Both sides share one scalar context; this statement -- compares Lean specifications. @Spec.flashAttention_eq_scaledDotProductAttention : ∀ {α : Type} [inst : Storage α] [inst_1 : Context α] [inst_2 : DecidableRel fun x1 x2 => x1 > x2] (config : Spec.FlashAttentionConfig) {nQ nK dModel : ℕ} {h1 : nQ ≠ 0} {h2 : nK ≠ 0} (ctx : Spec.AttentionContext α nQ nK dModel h1 h2), Spec.flashAttention config ctx = Spec.scaledDotProductAttention ctx#check @Spec.flashAttention_eq_scaledDotProductAttention
@Spec.flashAttention_eq_scaledDotProductAttention : ∀ {α : Type} [inst : Storage α] [inst_1 : Context α]
  [inst_2 : DecidableRel fun x1 x2 => x1 > x2] (config : Spec.FlashAttentionConfig) {nQ nK dModel : ℕ} {h1 : nQ ≠ 0}
  {h2 : nK ≠ 0} (ctx : Spec.AttentionContext α nQ nK dModel h1 h2),
  Spec.flashAttention config ctx = Spec.scaledDotProductAttention ctx

Both sides are Spec functions. The theorem says that the fused Lean specification has the same denotation as TorchLean's standard scaled-dot-product-attention specification, for every element type with the required operation instances and for every block configuration (Dao et al., 2022). That is what licenses a semantic graph rewrite, and it is what a fused implementation can be asked to refine. The Lean definition reuses the standard attention calculation and ignores tile sizes; this theorem does not verify an online tiled algorithm or its memory traffic. Its axiom audit is:

-- Inspect logical dependencies of the specification -- theorem, not native-kernel evidence. 'Spec.flashAttention_eq_scaledDotProductAttention' depends on axioms: [propext, Classical.choice, Quot.sound]#print axioms Spec.flashAttention_eq_scaledDotProductAttention
'Spec.flashAttention_eq_scaledDotProductAttention' depends on axioms: [propext, Classical.choice, Quot.sound]

The three standard Lean axioms, and no sorryAx. The companion theorem flashAttentionBackward_eq_scaledDotProductAttentionBackward has the same axiom set for the backward pass; both live in FlashAttention.lean.

Those theorems compare two Lean specifications. The LibTorch attention capsule records the runtime guards, regression tests, and checked trust level of the actual kernel. Connecting PTX or a library call all the way to the specification would require another refinement argument over Float32, layout, compiler, and hardware behavior, and no capsule field can stand in for it.

Both expressions use the same Context α; the theorem does not compare CPU and CUDA arithmetic. Its attention context also requires nonzero query and key dimensions.

Forward And Backward Ownership🔗

Inference asks for a forward value. Training asks for more: the value must remain connected to the derivative rule used by the optimizer.

TorchLean distinguishes three VJP modes:

  • none: a capsule supplies no reverse rule; a profile requests forward execution only;

  • torchLeanTape: a capsule composes its local VJP from TorchLean runtime operations;

  • backendVJP: a capsule supplies its local VJP through a native bridge routine.

A profile requesting torchLeanTape admits either local VJP implementation while retaining TorchLean's tape and backward traversal. A profile requesting backendVJP specifically requires the native local rule. Attention's capsule uses torchLeanTape: Lean computes its matrix products and explicit softmax VJP over tape-owned probabilities, then adds the input cotangents to the surrounding graph's contributions. There is no LibTorch autograd graph to traverse. Reverse-mode accumulation remains the classical construction (Baydin et al., 2018).

GPU attention uses LibTorch automatically. There is no separate composed-attention provider to select. The registered LibTorch.attention contract records the shape checks, hard-mask convention, local VJP, and implementation-defined reduction order.

No-grad sessions request none automatically. During training, a differentiable operation cannot select a forward-only capsule. Seeded random sources are the deliberate exception: they create non-differentiable values, so they do not need a local VJP of their own.

Keep the selected capsule names with an experiment. A preference alone does not identify what ran, and libtorch.direct_attention still leaves the numerical primitives' implementation choices to the SDK.

Boolean Attention Masks🔗

TorchLean gives boolean attention masks one semantics across specifications and runtimes. A blocked entry contributes exactly zero to the softmax numerator, as if its score were negative infinity. The CUDA composition forms zero numerators for blocked entries and handles fully blocked rows explicitly. Additive score biases remain a separate operation. Modern Models runs that mask against PyTorch and shows the theorem which makes “exactly zero” exact over the reals rather than approximate.

Replacing blocked scores by a large negative sentinel and calling an ordinary softmax changes the function. The original transformer paper writes the mask as -\infty before the softmax (Vaswani et al., 2017), and \exp(-\infty)=0 exactly; a sentinel is a finite number and \exp of it is not zero. TorchLean therefore builds the softmax numerators directly: an allowed entry contributes \exp(\text{score}-\text{rowMax}) and a blocked entry contributes literally 0. In the three-token causal example below, each row is a query and each column is a key. A query may attend to its own position and earlier positions, so entries above the diagonal are blocked:

-- Each row is a query; the causal mask excludes keys -- strictly to its right. def bsScores : Tensor Float [3, 3] := [[1.0, 2.0, 3.0], [0.5, 0.0, -0.5], [2.0, 2.0, 2.0]] def bsHard : Tensor Float [3, 3] := Spec.hardMaskedSoftmaxSpec bsScores (Spec.causalMask 3) [[1.000000, 0.000000, 0.000000], [0.622459, 0.377541, 0.000000], [0.333333, 0.333333, 0.333333]]#eval bsHard
[[1.000000, 0.000000, 0.000000], [0.622459, 0.377541, 0.000000], [0.333333, 0.333333, 0.333333]]

Every entry above the diagonal prints 0.000000. In the real specification each nonempty row sums to one; the displayed Float entries approximate those normalized weights. The first query has only one allowed key, which receives all its mass. The second row normalizes its first two scores. The third row is uniform because all three keys are allowed and their scores are equal.

In row two, the ratio of the allowed weights is \exp(0.5)/\exp(0), so the first key receives about 0.622459 and the second 0.377541. The third key receives no mass even though its score is a finite number. This is the contract a provider must preserve when it accepts a boolean mask. Multiplying the resulting weights by value vectors then prevents the blocked value from contributing through that attention edge. Causality of a whole model also requires checking its other paths, such as convolutions and any unmasked attention layers.

Finite Sentinels And Attention Leakage🔗

Now run the same scores through an ordinary softmax after substituting a sentinel, and look at the weight the model gives to a position it is not allowed to see. The first number is scaled by 10^{12} because Repr Float prints six decimals and the leak is smaller than that:

-- Scale the tiny blocked weight so six-decimal formatting -- cannot hide the leak. def bsSentinel (m : Float) : Tensor Float [3, 3] := Activation.softmaxSpec 1 (Tensor.map2Spec (fun s ok => if ok then s else m) bsScores (Spec.causalMask 3)) (0.034425, 0.000000, 0.000000)#eval (Spec.get2 (bsSentinel (-30.0)) 0 2 * 1.0e12, Spec.get2 (bsSentinel (-1.0e30)) 0 2, Spec.get2 bsHard 0 2)
(0.034425, 0.000000, 0.000000)

With a sentinel of -30 the future token still receives about 3.4\times10^{-14} of the attention mass. A comparison with a tolerance larger than this leak would miss it. The nonzero weight can violate exact independence from a future value; a theorem claiming that independence would need additional hypotheses or a different masking definition. With a sentinel of -10^{30} the exponential underflows and the leak becomes exactly zero in this evaluation. That result depends on the floating-point format and scores; the hard-mask definition sets blocked numerators to zero directly.

Fully Blocked Rows🔗

A padded sequence can produce a query row whose every key is blocked. In that case, subtracting the row maximum from equal sentinel values leaves zeros, so their exponentials do not underflow. Hard masking defines the row to be zero:

-- A fully blocked row distinguishes hard masking from equal -- finite sentinels. [[0.000000, 0.000000, 0.000000], [0.000000, 0.000000, 0.000000], [0.000000, 0.000000, 0.000000]]#eval Spec.hardMaskedSoftmaxSpec bsScores (Spec.allFalseMask 3 3) [[0.333333, 0.333333, 0.333333], [0.333333, 0.333333, 0.333333], [0.333333, 0.333333, 0.333333]]#eval Activation.softmaxSpec 1 (Tensor.map2Spec (fun s ok => if ok then s else -1.0e30) bsScores (Spec.allFalseMask 3 3))
[[0.000000, 0.000000, 0.000000], [0.000000, 0.000000, 0.000000], [0.000000, 0.000000, 0.000000]]
[[0.333333, 0.333333, 0.333333], [0.333333, 0.333333, 0.333333], [0.333333, 0.333333, 0.333333]]

The second result distributes attention over three forbidden keys. Every sentinel equals the row maximum, every shifted exponent is \exp(0)=1, and normalization divides by three. The result contains ordinary finite weights, so a numerical-validity check would not detect the masking error.

The all-false case tests a different part of the definition from the ordinary causal triangle. A causal row has at least its diagonal entry available, while a fully blocked row has no valid normalization mass. Defining its result as zero gives the caller a specific behavior for padding and empty visibility. It also avoids treating three equally forbidden keys as three equally plausible keys. A parity fixture should include this case because ordinary triangular examples would not expose the sentinel implementation's uniform-row behavior.

Exact Zero Over The Reals🔗

The evaluations above use one score matrix and one dtype. The general specification states that every strict-future coordinate of a causal attention matrix is zero, over the reals where \exp has its usual laws:

-- Instantiate the general future-zero theorem at three -- tokens and a zero score tensor. open NN.Proofs.Models.Attention in example (i j : Fin 3) (hij : i.val < j.val) : Spec.get2 (Spec.hardMaskedSoftmaxSpec (α := ℝ) 0 (Spec.causalMask 3)) i j = 0 := hardMaskedSoftmaxSpec_causal_future_zero 0 i j hij

hardMaskedSoftmaxSpec_causal_future_zero in CausalMask.lean is stated for every sequence length, every score tensor, and every strict-future pair, so the instance above needs no tactic block beyond naming it. The proof applies the general blocked-coordinate lemma to the causal mask. That lemma handles the empty-row branch and simplifies the zero numerator through real division; it does not rely on an exponential underflowing. A capsule on any provider is then measured against this specification rather than against another implementation's tolerance.

Contract Checks🔗

Planning and acceptance are separate steps. Planning finds capsules for graph operations. Auditing turns their contract fields into obligation reports. The evidence check classifies those reports under an AssurancePolicy; execution acceptance also rechecks the full KernelPolicy.

The implementation follows one explicit path:

  1. Availability states the devices and providers declared for planning. Eager execution performs the separate linked-library and hardware probes before launching a kernel.

  2. Registry supplies the capsule modules, and Planner chooses one PlannedKernel for each operation. Together these choices form a KernelPlan; for a graph, NN.Backend.IR plans one capsule per runtime-relevant node and Grouping coalesces adjacent nodes served by the same capsule.

  3. Audit turns the plan into a KernelPlanAudit: one KernelAudit per selected capsule with its four contract descriptors and reduction policy.

  4. KernelPlanAudit.checkContracts policy filters the obligations whose evidence the policy does not accept. Its result is a ContractCheck: accepted, or rejected with the failing ObligationReports. The complete acceptance gate also checks policy compatibility. Eager execution receives an AcceptedKernel for each operation; graph planning produces an AcceptedGraphKernelPlan that a later graph executor could consume. The current runtime does not execute from this graph metadata.

  5. The eager session binds the selected capsule to a KernelHandler with the same operation, provider, and device through KernelCapsule.bind. If this build has no such handler, execution fails before entering a different provider's code.

  6. Report renders providers, trust levels, VJP modes, reduction policies, and per-obligation evidence for logs and benchmark records.

To isolate the evidence check, make a teaching capsule whose forward-value evidence is an assumption. Its other descriptors come from the registered matmul capsule. This is a deliberately constructed diagnostic plan, not a new provider or an executable selection:

-- Change one evidence field, then hold the plan fixed. checked: rejected demo.assumed_matmul value trusted boundary: demonstration assumption external: accepted #eval do let cap : KernelCapsule := { LibTorch.matmul with name := "demo.assumed_matmul" valueContract := { LibTorch.matmul.valueContract with evidence := .trustedBoundary "demonstration assumption" } } let plan : KernelPlan := { kernels := #[{ op := .matmul, capsule := cap }] } let policies : List AssurancePolicy := [AssurancePolicy.checked, AssurancePolicy.external] policies.forM fun (policy : AssurancePolicy) => do match KernelPlan.checkContracts policy plan with | .accepted => IO.println s!"{policy.label}: accepted" | .rejected reports => for r in reports do IO.println s!"{policy.label}: rejected \ {r.capsuleName} {r.obligation.label}" IO.println s!" {r.evidence.label}"
checked: rejected demo.assumed_matmul value
  trusted boundary: demonstration assumption
external: accepted

One obligation out of four fails, and the report names which one: the forward value. Shape and layout still have runtime guards, and the VJP still names its regression suite. Accepting external permits the demonstration assumption while retaining those other obligations. The registered LibTorch.matmul uses test-suite evidence for its forward value; the example changed that field specifically to expose the policy boundary.

The eager runtime caches and reports an accepted selection before looking up its handler. It then binds a matching handler and runs it, or fails if no handler is available. The cached selection is therefore not a record of completed calls. Inspection tools can also retain rejected graph plans and explain why they failed.

AcceptedKernel carries a proof that PlannedKernel.acceptable policy = true. This gate checks operation identity, forward support, contract alignment, trust, provider, device, VJP mode, and evidence. AcceptedGraphKernelPlan carries the corresponding result for the groups derived from its stored graph plan. A diagnostic plan alone does not authorize execution: these accepted values require the complete gate to succeed.

AssurancePolicy has one field, allowTrustedExternal. The checked policy leaves it false and admits runtime guards, test suites, and notApplicable; the external policy sets it true and also admits trusted boundaries. The same record decides which capsules the planner may select (by trust level) and which evidence the selected capsules may rely on (by evidence kind). A profile's acceptGraph uses its configured policy for both steps. Diagnostic evidence checks, such as bsCheck, can deliberately compare policies without producing an executable accepted value. Provider preference chooses between admissible implementations. The evidence check is a separate filter: asking for a provider cannot override an obligation's rejected evidence.

The evidence check accepts exactly when the filtered list of rejected obligations is empty. What it establishes is that every selected capsule rests on the evidence classes the caller agreed to. It does not establish the numerical correctness of any kernel, and no registered capsule claims a proof. The sources are Audit.lean and ContractCheck.lean.

The maintained CUDA wrappers also perform concrete checks at the FFI boundary. Convolution and pooling validate rank and dimension conversion, nonzero strides, representable element counts, buffer lengths, and operation-specific domains such as finite nonzero smooth-max \beta after conversion to Float32. The C++ adapter repeats critical size and overflow checks. These guards prevent malformed calls; they complement rather than replace a mathematical value-refinement argument.

Capsules record reduction order and layout claims but have no scalar-type field. The runtime configuration and native conversion checks determine which arithmetic and buffer format actually execute; a capsule label alone does not certify their relationship.

Neither result reports a numerical comparison: this example only checks which evidence classes the policy permits.

Runtime Configuration🔗

The model API stays independent of these implementation details. The same builder is used on every device, and the device is an ordinary argument:

-- Changing the device does not alter the builder's input -- and output shapes. def bsModel : nn.Builder (nn.Sequential [4] [1]) := nn.Sequential![nn.linear 4 8, nn.relu, nn.linear 8 1] def bsTrainer (device : Runtime.Device) := Trainer.new bsModel { objective := mse optimizer := optim.adam { lr := 0.01 } device := device } bsTrainer gpu : Trainer [4] [1]#check bsTrainer gpu
bsTrainer gpu : Trainer [4] [1]

The trainer's type is Trainer [4] [1] on every device: the device is not part of the model's interface, so switching it cannot change which inputs the model accepts. The same holds for a bare runtime configuration:

-- Projecting a configuration field performs no runtime -- compatibility check. "cuda"#eval ({ device := gpu : Runtime.Config }).device.cliName
"cuda"

This block only projects a field from the configuration; it does not validate or execute it. A build without LibTorch rejects this request when the session opens.

Graph Kernel Planning🔗

Everything above planned a list of operations. Graph-level planning additionally records source node identities: some nodes need no kernel, and adjacent selections with the same capsule can share one audit row. Lower the model to the canonical verification IR and inspect its plan. This is a diagnostic graph plan; eager execution selects handlers per operation.

-- Retain source node ids so each selected capsule can be -- traced to the lowered model. def bsBuilt : nn.Sequential [4] [1] := nn.build 2026 bsModel def bsGraph : Except String NN.IR.Graph := (Verification.lowerForwardToIR (α := Float) bsBuilt (nn.initialState bsBuilt)).map (·.graph) nodes 18, planned 13 1 NN.IR.OpKind.reshape [4] [1, 4] -> reference.reshape 3 NN.IR.OpKind.transpose 0 1 -> reference.permute 4 NN.IR.OpKind.matmul -> reference.matmul 6 NN.IR.OpKind.broadcastTo [8] [1, 8] -> reference.broadcast 7 NN.IR.OpKind.add -> reference.add 8 NN.IR.OpKind.reshape [1, 8] [8] -> reference.reshape 9 NN.IR.OpKind.relu -> reference.relu 10 NN.IR.OpKind.reshape [8] [1, 8] -> reference.reshape 12 NN.IR.OpKind.transpose 0 1 -> reference.permute 13 NN.IR.OpKind.matmul -> reference.matmul 15 NN.IR.OpKind.broadcastTo [1] [1, 1] -> reference.broadcast 16 NN.IR.OpKind.add -> reference.add 17 NN.IR.OpKind.reshape [1, 1] [1] -> reference.reshape #eval show IO Unit from do let g ← IO.ofExcept bsGraph let plan ← IO.ofExcept (BackendProfile.checkedCpu.planGraphNodes g) IO.println s!"nodes {g.nodes.size}, \ planned {plan.kernels.size}" for k in plan.kernels do IO.println s!" {k.nodeId} {repr k.kind} \ -> {k.capsule.name}"
nodes 18, planned 13
  1 NN.IR.OpKind.reshape [4] [1, 4] -> reference.reshape
  3 NN.IR.OpKind.transpose 0 1 -> reference.permute
  4 NN.IR.OpKind.matmul -> reference.matmul
  6 NN.IR.OpKind.broadcastTo [8] [1, 8] -> reference.broadcast
  7 NN.IR.OpKind.add -> reference.add
  8 NN.IR.OpKind.reshape [1, 8] [8] -> reference.reshape
  9 NN.IR.OpKind.relu -> reference.relu
  10 NN.IR.OpKind.reshape [8] [1, 8] -> reference.reshape
  12 NN.IR.OpKind.transpose 0 1 -> reference.permute
  13 NN.IR.OpKind.matmul -> reference.matmul
  15 NN.IR.OpKind.broadcastTo [1] [1, 1] -> reference.broadcast
  16 NN.IR.OpKind.add -> reference.add
  17 NN.IR.OpKind.reshape [1, 1] [1] -> reference.reshape

The plan has thirteen kernels for eighteen nodes. The five missing ones are the input and the four weight and bias constants, because NN.Backend.IR.op? maps input, const, and detach to none. A constant weight tensor is data the executor already holds and needs no operation kernel.

Every kernel here came from the reference.* family, because checkedCpu declares CPU availability and the portable module is the only one offering CPU capsules. The same graph under a CUDA profile selects LibTorch capsules for the same operations, and the audit rows change accordingly while the graph does not.

The node list exposes how a vector linear layer becomes ordinary tensor operations. A reshape introduces a batch axis, a transpose presents the stored weights in matrix-product orientation, and a broadcast aligns the bias with the result. The next reshape removes that batch axis. Those operations are part of the plan even though the user wrote one nn.linear. The four constants retain the model's two weight matrices and two biases. Reading the plan at this level helps locate where a provider gap arises without changing the model's public shape interface.

Kernel Groups In Audit Reports🔗

Adjacent nodes that selected the same capsule for the same operation are collected into one KernelGroup. On the graph above it never fires:

-- Count audit groups separately from the kernels -- represented by those groups. kernels 13, groups 13 #[1] reshape #[3] permute #[4] matmul #[6] broadcast #[7] add #[8] reshape #[9] relu #[10] reshape #[12] permute #[13] matmul #[15] broadcast #[16] add #[17] reshape #eval show IO Unit from do let g ← IO.ofExcept bsGraph let plan ← IO.ofExcept (BackendProfile.checkedCpu.planGraphNodes g) let grouped := plan.toCoalescedGroups IO.println s!"kernels {plan.kernels.size}, \ groups {grouped.groups.size}" for gr in grouped.groups do IO.println s!" {gr.nodeIds} {gr.op.name}"
kernels 13, groups 13
  #[1] reshape
  #[3] permute
  #[4] matmul
  #[6] broadcast
  #[7] add
  #[8] reshape
  #[9] relu
  #[10] reshape
  #[12] permute
  #[13] matmul
  #[15] broadcast
  #[16] add
  #[17] reshape

Thirteen kernels, thirteen groups. Lowering interleaves reshapes and transposes between the arithmetic, so no two neighbours ever agree on an operation. The case grouping exists for is the graph that repeats one operation, which is easy enough to write by hand:

-- Consecutive identical ReLU contracts can share an audit -- row while retaining three nodes. def bsChain : NN.IR.Graph := { nodes := #[{ id := 0, parents := #[], kind := .input outShape := [4] }, { id := 1, parents := #[0], kind := .relu outShape := [4] }, { id := 2, parents := #[1], kind := .relu outShape := [4] }, { id := 3, parents := #[2], kind := .relu outShape := [4] }] } kernels 3, groups 1 #[1, 2, 3] relu reference.relu #eval show IO Unit from do let plan ← IO.ofExcept (BackendProfile.checkedCpu.planGraphNodes bsChain) let grouped := plan.toCoalescedGroups IO.println s!"kernels {plan.kernels.size}, \ groups {grouped.groups.size}" for gr in grouped.groups do IO.println s!" {gr.nodeIds} {gr.op.name} \ {gr.capsule.name}"
kernels 3, groups 1
  #[1, 2, 3] relu reference.relu

Three ReLU nodes, three planned kernels, one audit row. What that row says is that nodes 1, 2, and 3 all rest on the same evidence. The module docstring in Grouping.lean says it first: grouping "does not translate graph semantics, fuse operations, or claim that a group executes as one kernel launch." Three ReLU nodes still run as three operations. Fusing them would be a semantic rewrite, and a semantic rewrite needs a theorem of the kind Spec.flashAttention_eq_scaledDotProductAttention provides, not a grouping pass.

Accepted Plans And Policy Checks🔗

Contract checking a graph plan returns the accepted record, including its policy check:

-- Only the accepted branch carries evidence of the complete -- policy gate. accepted under checked 13 rows, distinct capsules: reference.reshape reference.permute reference.matmul reference.broadcast reference.add reference.relu #eval show IO Unit from do let g ← IO.ofExcept bsGraph match BackendProfile.checkedCpu.acceptGraph g with | .error e => IO.println s!"planning failed: {e}" | .ok (.accepted accepted) => IO.println s!"accepted under \ {accepted.policy.assurance.label}" IO.println s!"{accepted.capsuleNames.size} rows, \ distinct capsules:" for n in accepted.capsuleNames.toList.eraseDups do IO.println s!" {n}" | .ok (.rejected _ failures) => for failure in failures do IO.println s!"rejected: {failure.message}"
accepted under checked
13 rows, distinct capsules:
  reference.reshape
  reference.permute
  reference.matmul
  reference.broadcast
  reference.add
  reference.relu

The .accepted constructor holds a proof of groupedPlan.acceptable policy = true. acceptable rechecks operation identity, forward support, contract alignment, trust, provider, device, VJP ownership, and contract evidence for every grouped kernel. A later graph executor can therefore require an AcceptedGraphKernelPlan and obtain that check from the argument. The current runtime does not execute from this record: graph planning and auditing are diagnostic, while eager execution selects and binds handlers per operation.

The accepted report has thirteen rows but only six distinct capsule names. Reuse of a name means the same implementation contract serves several nodes; it does not mean those nodes share a result or a launch. Grouping compares complete capsule records, including evidence and numerical policy, so identically named capsules with different obligations cannot silently share an audit group. The stored source node identifiers let a diagnostic connect a rejected contract back to the particular graph operations that depended on it.

Backend Evidence And Guarantees🔗

These statements have different strengths:

  • "the example ran on CUDA" reports an execution path;

  • "CUDA matched the CPU reference on this test suite" reports finite parity evidence;

  • "the fused attention spec equals standard attention" cites a Lean semantic theorem;

  • "the Lean composition over ATen primitives implements the attention spec" requires an implementation refinement argument;

  • "this LibTorch build passed the attention tests" reports the tested cases and configuration, while the foreign implementation remains outside the Lean proof.

A backend report records the selected provider and its declared evidence. Keep it beside a benchmark to identify the intended implementations, together with execution results, SDK version and runtime settings. The report alone does not establish that their handlers completed.

For example, a shape guard cannot justify a claim about attention's numerical result. Follow the selected capsule's value obligation to its cited test or trusted boundary, then check what that evidence covers.

Read Choosing How A Model Runs for the runtime API, and GPU and CUDA for the native implementation details. Runtime Approximation is where reduction order and Float32 rounding are related back to real-valued specifications. The Installation page lists platform commands and the profiles currently wired into the repository.

References🔗

The framework whose tensor library supplies CUDA execution is Paszke et al. (2019); the fused attention algorithm is Dao et al. (2022); reverse-mode accumulation is surveyed by Baydin et al. (2018); and the proof-carrying-code approach used for comparison is Necula (1997).