TorchLean

3.2. Backend Selection🔗

The previous page selected CPU, CUDA, or an optional provider 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. One CUDA build may contain a hand-written kernel, a cuBLAS call, and a LibTorch bridge for different operations. Their layouts, numerical behavior, backward support, and supporting evidence differ. The backend planner keeps those differences 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: checked_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: checked_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. 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. TorchLean currently knows three maintained ways to compute it: a composed TorchLean expression, a native fused CUDA implementation, and a LibTorch forward bridge with a TorchLean-owned backward pass. The operation is the same; the implementation contract is not.

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 CapsuleModules. Built-in attention, native CUDA, portable reference, and optional LibTorch code 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 Registry.validateModules #[Registry.libTorchModule, Registry.libTorchModule] 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.

The registry rejects the duplicate libtorch name. Distinct modules can still contribute implementations of 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, the evidence behind the claim, and optional provenance (source paths and native symbols). Provenance identifies the implementation being discussed; it is not evidence of correctness.

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 .scaledDotProductAttention) "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 scaled_dot_product_attention on device cpu"#eval planOp BackendProfile.checkedCpu.policy #[bsMisfiled] .scaledDotProductAttention |>.map fun k => k.capsule.name
Except.error "no admissible kernel capsule for op scaled_dot_product_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. A capsule cannot claim that a Lean theorem covers its native implementation, because no such theorem exists for any registered kernel. 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 CUDA and LibTorch capsules record an implementation-defined reduction. A fixed-left range trace therefore cannot be reused for a cuBLAS schedule merely because both capsules implement matmul. Rounding mode, subnormal handling, and multiply-add contraction are not recorded in the capsule, and nothing audits them.

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.

PyTorch Attention Kernel Selection🔗

PyTorch also chooses among several attention kernels, through a context manager that expresses a preference. The difference is when the choice is visible. Requesting the flash kernel for CPU tensors succeeds, and the returned tensor carries no record of which kernel produced it:

# The returned shape does not reveal which attention
# implementation was selected.
q = torch.zeros(1, 1, 4, 8)
with sdpa_kernel([SDPBackend.FLASH_ATTENTION]):
    out = F.scaled_dot_product_attention(q, q, q)
print(tuple(out.shape))
(1, 1, 4, 8)

When no kernel satisfies the request, the call raises an exception and reports the rejected conditions:

# This request changes both the device and dtype and may
# have no eligible kernel.
q64 = torch.zeros(1, 1, 4, 8, device="cuda", dtype=torch.float64)
with sdpa_kernel([SDPBackend.FLASH_ATTENTION]):
    F.scaled_dot_product_attention(q64, q64, q64)
UserWarning: Expected query, key and value to all be of dtype:
  {Half, BFloat16}. Got Query dtype: double, ...
RuntimeError: No available kernel. Aborting execution.

These examples expose kernel selection in different forms. PyTorch reports the failure at the tensor call site, including the unsupported dtype. TorchLean's planner returns a value before execution that tools can store, compare across builds, or attach to a benchmark. Runtime binding and hardware checks still follow; a successful plan alone does not establish that a kernel ran.

The failed request uses CUDA double-precision inputs, while the successful request uses CPU tensors. These transcripts describe those builds and invocations. To compare providers, we would also need to match the dtypes and operation settings.

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 computes the standard attention stages and ignores the 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 native CUDA capsule records the runtime guards, regression tests, source provenance, 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.

Read the theorem's Context α as the operations used to give both sides their meaning. The statement quantifies one context shared by the two expressions; it does not compare CPU arithmetic with CUDA arithmetic. The nonzero query and key dimensions are part of the attention context's contract. The axiom output reports logical dependencies of this Lean theorem and says nothing about a compiled CUDA binary. It is therefore useful evidence about the specification rewrite, while the capsule audit remains the place to inspect the implementation boundary.

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: no gradient is requested;

  • torchLeanTape: TorchLean owns the tape and backward traversal; each capsule declares whether its local VJP is expressed through TorchLean operations or a named backend kernel;

  • backendVJP: require capsules whose local VJP is computed by a backend kernel.

The preferred external-forward design is therefore precise: a provider may compute a fast forward value, TorchLean records the same operation on its tape, and TorchLean applies the backward rule. This requires enough forward information to be retained for that rule. If the bridge cannot provide it, the implementation must fall back or expose a larger trust boundary. Reverse-mode accumulation itself is the classical construction (Baydin et al., 2018); what is being negotiated here is only who owns each local rule.

The maintained LibTorch-forward profile implements this design for scaled-dot-product attention. Selection is per operation. The following profiles differ in provider preference and assurance:

-- Change assurance while preserving preference to expose -- the admissibility filter. def bsStrictLibTorch : BackendProfile := { BackendProfile.libTorchForwardCuda with name := "libtorch_forward_strict" policy := { BackendProfile.libTorchForwardCuda.policy with assurance := .checked } } checked_cuda: #[torchlean.composed_attention] libtorch_forward_cuda: #[libtorch.sdpa_forward] libtorch_forward_strict: #[native_cuda.direct_attention] #eval do let attention := #[BackendOp.scaledDotProductAttention] let profiles : List BackendProfile := [BackendProfile.checkedCuda, BackendProfile.libTorchForwardCuda, bsStrictLibTorch] profiles.forM fun (p : BackendProfile) => do match p.planOps attention with | .error e => IO.println s!"{p.name}: {e}" | .ok plan => IO.println s!"{p.name}: {plan.capsuleNames}"
checked_cuda: #[torchlean.composed_attention]
libtorch_forward_cuda: #[libtorch.sdpa_forward]
libtorch_forward_strict: #[native_cuda.direct_attention]

The third profile asks for LibTorch by preference but keeps the checked assurance policy, so the LibTorch capsule is not admissible at all, and chooseCapsuleFor? falls back to the first admissible capsule in catalog order. Preference is a request, and the trust level is a filter that a request cannot override. Fallback follows catalog order. A profile that requires a particular provider must use only rather than a preference.

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.

The three selected capsule names make the interaction between preference and assurance visible. checked_cuda prefers the composed TorchLean route for this request. The external profile admits the LibTorch forward boundary and selects it. Tightening assurance while retaining that preference forces selection to another admissible implementation. A preference therefore cannot be used as a measurement label without checking the result: a run requesting LibTorch may legally select a different provider. If the experiment requires LibTorch specifically, provider restriction and failure are more informative than fallback.

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. Native CUDA skips blocked entries, while the LibTorch bridge passes a boolean mask directly to scaled dot-product attention. 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 is 0.000000 and every row still sums to one. 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. Its proof is short for a reason: with the numerator defined as 0 rather than as \exp of a sentinel, the blocked coordinate reduces to zero definitionally, and the lemma is one unfold and one simp. 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.

The LibTorch-forward plan illustrates the evidence check. Holding the selected capsule fixed, check its obligations under both assurance policies:

-- Hold the selected capsule fixed and inspect which -- evidence each policy accepts. checked: rejected libtorch.sdpa_forward value trusted boundary: LibTorch/CUDA SDPA implementation external: accepted #eval do let attention := #[BackendOp.scaledDotProductAttention] let profile := BackendProfile.libTorchForwardCuda match profile.planOps attention with | .error e => IO.println s!"planning failed: {e}" | .ok plan => 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 libtorch.sdpa_forward value
  trusted boundary: LibTorch/CUDA SDPA implementation
external: accepted

One obligation out of four fails, and the report names which one: the forward value. The capsule's shape and layout obligations are runtime guards, its VJP obligation is TorchLean's own rule, and only the forward value relies on the named LibTorch boundary. Accepting external permits that assumption while retaining the other recorded obligations.

These are Lean data structures rather than an informal convention between command-line flags. The eager runtime consumes the accepted per-operation value, binds it to the implementation it will call, and records the capsule it actually used. Inspection tools can 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. The bsPrefer block above shows both halves of that sentence at once: under checked the LibTorch capsule was never selected, and under external it was selected and then accepted.

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/CUDA boundary repeats critical size and overflow checks. These guards prevent malformed launches; 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.

The rejected value obligation does not say that the LibTorch answer was numerically wrong. It says that the chosen policy does not permit that obligation to rest on an external assumption. Likewise, external: accepted does not report a numerical comparison; it records agreement between the evidence class and the caller's policy. This gives an application a precise way to allow an external forward implementation while continuing to require shape guards and a TorchLean-owned reverse path. A single unchecked/checked switch for the whole application would hide which part of the computation actually relies on that assumption.

Runtime Configuration🔗

The model API stays independent of these implementation details. The same builder is used for both execution modes, and the mode is an ordinary argument:

-- Changing execution 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 (execution : Runtime.ExecutionMode) := Trainer.new bsModel { objective := .meanSquaredError optimizer := optim.adam { learningRate := 0.01 } execution := execution } bsTrainer Runtime.Autograd.Torch.ExecutionMode.eager : Trainer [4] [1]#check bsTrainer .eager
bsTrainer Runtime.Autograd.Torch.ExecutionMode.eager : Trainer [4] [1]

The trainer's type is Trainer [4] [1] either way: the execution mode is not part of the model's interface, so switching it cannot change which inputs the model accepts. Device selection lives in the same configuration record:

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

This block only projects a field from the configuration; it does not validate or execute it. The current typed graph trainer rejects this CUDA combination when the session opens.

Typed graph execution records and reuses a typed SSA graph. The execution chapter describes this lowering and its proof boundary; backend selection here determines which registered implementation may execute each operation. Graph construction alone is not an optimizing compiler and does not prove the stored derivative rules correct, which is why the two chapters stay separate.

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 native 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 native attention kernel implements the fused spec" requires a native refinement argument;

  • "the LibTorch result is correct" depends on the explicitly named LibTorch boundary unless a stronger checker or theorem covers it.

A backend report records the selected provider and the evidence attached to it. Keeping that report beside a benchmark makes “CUDA” concrete: readers can see which operations were native or external and which guards and tests stand behind each one.

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 Execution Modes 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 kernel-selection behavior we compare against 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).