Kernel Maps and Provider Selection
How CKE v8 decides which physical C kernel executes each logical operation — and why that decision lives in 284 machine-checked JSON maps instead of in Python conditionals.
The top-level
kernel_maps/README.md documents the older v7-era map format
(decoder layer plans and buffer layouts consumed by gen_kernel_specs.py). This page is about
the v8 provider-selection system in version/v8/kernel_maps/*.json — a
different format with a different job. Do not confuse the two.
The Three-Level Separation — and Why
One CKE runtime serves many model families × quantization formats × ISA variants × execution phases, all on CPUs. That product space is why v8 separates what math is required from which provider runs from how the op is physically executed:
Circuits declare the logical
version/v8/circuits/*.json (e.g. qwen3vl.json, qwen35.json) declare op instances, named port edges, and required_numerical_contracts. For hardened ops a circuit must not name a physical provider ID — it states requirements, not implementations.
Kernel maps own the physical
version/v8/kernel_maps/*.json — 284 maps — own the operation interface, numerical-contract identity, port dtype/layout/stride/storage, prefill/decode phase support, persistent-state and alias semantics, the C call ABI, lifecycle status, and priority.
The DSL resolves mechanically
version/v8/scripts/build_ir_v8.py filters by contract, then by phase/dtype/shape/layout/ISA/alias compatibility, ranks by lifecycle then priority, and fails closed on ambiguity. No model-name checks are allowed in the resolver.
Anatomy of a Kernel Map
version/v8/kernel_maps/memcpy.json is the canonical annotated example — the production map behind
the residual_save op. Every field group has one job:
Selection metadata is schema-enforced
The selection block is required by
version/v8/schemas/kernel_provider_selection.schema.json and carries exactly four fields:
| Field | Type | Rule |
|---|---|---|
| status | production | candidate | diagnostic | deprecated | Lifecycle rank. Only production providers auto-select; candidate, diagnostic, and deprecated never do. |
| priority | integer | Ranks providers within one equivalence group only. It cannot compare across groups. |
| equivalence_group | non-empty string | Mandatory. A provider that omits it is a HARD KERNEL SELECTION FAULT. |
| phases | non-empty unique list of init | prefill | decode | training | backward | Declares which execution phases the provider supports. |
The hard rules: an equal-priority tie between explicit production providers is a fault, not a coin flip.
And the map — not the code generator — owns the call ABI: dims, typed params
(here _memcpy_bytes of type size_t), and a versioned call_abi block
naming the exact argument list (dst, src, size) with each argument's
source port. The alias and arithmetic contract is part of the map too: constraints.notes
states this is a byte copy, performs no arithmetic, and is undefined for overlapping writable views.
Reading a Kernel Map: Field Reference
The anatomy figure above shows the shape of one map. This section is the member-by-member reference: what each field means, which component consumes it, and what happens when it is missing or wrong.
version/v8/scripts/audit_kernel_map_interfaces_v8.py) sorts every map into explicit buckets:
selection-managed (a valid selection block),
hardened (operation_interface + numerical_capabilities, where each
capability entry requires contract_id, status, phases, function, explicit_selector, implementation,
arithmetic per numerical_kernel_capability.schema.json),
interface+ABI cross-validated (hardened, plus a call_abi whose
ports entries cover every declared input/weight/output port exactly), and
legacy (none of the above). A missing optional block moves a map into an older bucket;
it is not an error. The examples below are all production and selection-managed, and each lacks a
different piece of the hardened format — that is deliberate, and the page says so per example.
| Field | What it means | Consumed by | If missing or wrong |
|---|---|---|---|
| id | Registry identity of the provider. | Resolver, X-Ray (selections and rejections are recorded by id), audits. | Every map has one in practice; X-Ray and audit output key on it. |
| op | The logical operation this map provides (e.g. gemm, residual_save, kv_cache_store). | Resolver — matches the circuit's op name. | Wrong op: the provider simply never matches that operation. |
| operation_interface | Hardened interface identity (e.g. residual_save.memcpy_copy.v1, kv_cache_store.bf16.head_major.v1) that circuit contracts bind to. | Resolver, audit. | Optional. Absent: map stays in the legacy/contract-pending bucket. Present: the audit hard-fails unsafe port aliasing on the map. |
| variant | Human/machine tag for the variant (e.g. fp32_copy, bf16_cache, q8_0_w_q8_0_a). | Resolver — direction ranking reads variant/kernel id to demote backward providers during inference. | Optional; a misleading name can misrank inference vs backward. |
| quant {weight, activation, output} | Weight, activation, and output dtypes of the provider. | Resolver — dtype compatibility filtering and activation-preference ranking. | Wrong dtype: the provider is filtered out for mismatched tensors (recorded as a rejection reason, e.g. weight_dtype_mismatch). |
| selection {status, priority, equivalence_group, phases} | Selection metadata; see the schema table above. | Resolver ranking, audit. | The block itself is optional — absent, the provider is treated as legacy and never outranks a production provider. Present but invalid (bad status, non-integer priority, empty group, bad phases): audit failure; a missing equivalence_group is a HARD KERNEL SELECTION FAULT. |
| inputs / weights / outputs / scratch | Port lists. Each port has name, dtype, symbolic shape, layout, and a desc; depending on map vintage also access, storage_class, consumption (memcpy, kv_cache_store) or placement, optional (gemm). storage_class: state marks persistent state rather than an ephemeral output. | Resolver (dtype/layout/alias compatibility), lowering and the memory planner, audit cross-validation. | Member sets vary by vintage — that is expected. On interface-declaring maps, an unsafe alias_of or writable overlap is an audit hard fault. |
| dims | Symbolic dimension names the call ABI and lowering resolve to integers (e.g. _memcpy_bytes = embed_dim × seq_len × 4 computed at lowering). | Lowering, codegen. | Missing/undeclared dims cannot be referenced by the call ABI. |
| params | Extra typed scalar parameters beyond ports and dims (e.g. _memcpy_bytes of type size_t). | Lowering, codegen. | Optional; empty for most GEMM-class maps whose arguments are all ports and dims. |
| call_abi | The exact C argument list: ordered params, each with name and a namespaced source (activation:, weight:, output:, dim:, runtime:, const:, …), plus optional cast and ports. | Codegen (emits exactly this call), _validate_kernel_call_abi in build_ir_v8.py, audit. | Optional — absent, codegen falls back to legacy argument conventions and the audit counts the map as legacy ABI. Present but malformed (unknown field, version ≠ 1, missing name/source, bad source namespace): HARD CALL ABI FAULT. |
| parallelization {supported, preferred, strategies} | Declared threading policy: which partitionings exist, which is preferred per phase, and per-strategy constraints (e.g. split by tokens vs by output features, minimum chunk, alignment). | Lowering / thread dispatch. | Optional; governs scheduling only, never numerics (reduction_order_effect is declared separately). |
| constraints {notes, alignment} | The semantics contract in prose and machine-checkable alignment (e.g. K must be a multiple of 32; "byte copy, no arithmetic, no overlapping writable views"). | Humans, audits, parity reviewers. | Optional but load-bearing: this is where alias and arithmetic promises live. |
| numerical_contract | Contract ID string naming the exact arithmetic (e.g. q8_0_weight_q8_0_input_llama_fp32_output). The audit calls maps carrying it "legacy contract-shaped". | Resolver/contract validation, parity gates. | Optional extension; where present it must name a real registered contract. |
| impl {function, sources, variants[]} | Which C function fulfills the contract, which sources compile it, and ISA-gated variants — each with name, requires (ISA features), compile_flags, and optionally its own priority and shape_constraints. | Codegen (build), resolver/ISA filtering (requires gates eligibility). | An ineligible variant is filtered out, never guessed at; a missing source file fails the build. |
| tests | Unit, bench, and parity oracles with tolerances (e.g. a bit-exact llama.cpp comparison). | Parity gates, CI. | Optional; memcpy's is empty — a pure byte copy has no arithmetic to oracle. |
| reference | The scalar contract oracle: reference function, adapter, validation status, and external oracles. production.reference_comparison can demand bit_exact agreement. | Parity gates. | Optional extension; where present, the optimized path must match the oracle's reduction order. |
| phase_selection, production, implementation | Map-specific extension blocks: per-phase kernel families and scheduling policy, the production function with its comparison requirement, and execution metadata (ISA dispatch style, storage formats, threading runtime). | Lowering, resolver, parity gates. | Optional; richer maps carry them, minimal maps omit them. |
Worked Example 1: memcpy — the Baseline Read
memcpy.json is the minimal production map: one op, two ports, one computed parameter, no ISA
variants, no numerical contract — a byte copy has no arithmetic to contract. Verbatim excerpt
(… marks elided lines):
{
"id": "memcpy",
"op": "residual_save",
"operation_interface": "residual_save.memcpy_copy.v1",
"selection": {"status": "production", "priority": 100, "equivalence_group": "residual_save.memcpy_copy.v1", "phases": ["prefill", "decode"]},
"variant": "fp32_copy",
"quant": {"weight": null, "activation": "fp32", "output": "fp32"},
"inputs": [{"name": "src", "dtype": "fp32", "shape": ["N"], "layout": "contiguous", "access": "read", "storage_class": "activation", …}],
"outputs": [{"name": "dst", "dtype": "fp32", "shape": ["N"], "layout": "contiguous", "access": "write", "storage_class": "activation", …}],
"dims": ["N"],
"params": [{"name": "_memcpy_bytes", "type": "size_t", "desc": "Number of bytes to copy"}],
"constraints": {"notes": "Byte copy implementing logical residual_save; performs no arithmetic. src and dst must not overlap …"},
"impl": {"function": "memcpy", "sources": [], "variants": [{"name": "default", "requires": [], "compile_flags": []}]},
"call_abi": {"version": 1, "params": [
{"name": "dst", "source": "output:dst", "cast": "void*", …},
{"name": "src", "source": "activation:src", "cast": "const void*", …},
{"name": "size", "source": "dim:_memcpy_bytes"} ]},
"tests": {}
}
How to read it. The resolver reaches this map when a circuit needs
residual_save in prefill or decode: the selection block makes it eligible in both
phases, quant matches FP32 activations, and the single default variant passes ISA filtering
vacuously (requires: []). Codegen then emits exactly
memcpy(dst, src, size) — no argument reconstruction. What it deliberately lacks:
numerical_contract, numerical_capabilities, non-empty tests. The audit
therefore classes it as selection-managed and interface-ready, not fully "hardened" — a legitimate state for
a copy with one possible behavior.
Worked Example 2: gemm_nt_q8_0_q8_0 — the Rich One
The Q8 prefill GEMM shows every group memcpy omits: a named numerical contract, ISA-gated variants, map-owned ABI for seven arguments, and validation oracles.
{
"id": "gemm_nt_q8_0_q8_0",
"op": "gemm",
"selection": {"status": "production", "priority": 200,
"equivalence_group": "q8_0_weight_q8_0_input_llama_fp32_output",
"phases": ["prefill"]},
"numerical_contract": "q8_0_weight_q8_0_input_llama_fp32_output",
"quant": {"weight": "q8_0", "activation": "q8_0", "output": "fp32"},
"inputs": [
{"name": "A", "dtype": "q8_0", "shape": ["M", "K"], "layout": "token_major_contiguous", …},
{"name": "B", "dtype": "q8_0", "shape": ["N", "K"], "layout": "opaque_packed", …},
{"name": "bias", "dtype": "fp32", "shape": ["N"], …, "optional": true, …} ],
"outputs": [{"name": "C", "dtype": "fp32", "shape": ["M", "N"], "layout": "token_major_contiguous", …}],
"dims": ["M", "N", "K"],
"constraints": {"alignment": {"K": 32}, "notes": "Q8_0 requires K dimension multiple of 32 …"},
"phase_selection": {"decode": {"op_family": "gemv", "default_kernel": "gemv_q8_0_q8_0",
"notes": "Single-token decode retains the certified GEMV path."}, …},
"impl": {"function": "gemm_nt_q8_0_q8_0", "sources": ["src/kernels/gemm_batch_int8.c", …],
"variants": [
{"name": "avx2_m2n4", "function": "gemm_nt_q8_0_q8_0_m2n4", "priority": 240,
"requires": ["avx2", "fma"], "shape_constraints": {"M_min": 2, "K_multiple": 32},
"numerical_contract": "q8_0_weight_q8_0_input_llama_fp32_output"},
{"name": "avx2", "requires": ["avx2", "fma"], "compile_flags": ["-mavx2", "-mfma"]},
{"name": "avx_vnni", "requires": ["avx2", "avx_vnni", "fma"], "compile_flags": ["-mavx2", "-mavxvnni", "-mfma"]} ]},
"reference": {"function": "vec_dot_q8_0_q8_0_ref", "kind": "scalar_contract_oracle", …},
"production": {"function": "gemm_nt_q8_0_q8_0", …, "reference_comparison": {"requirement": "bit_exact", …}},
"call_abi": {"version": 1, "params": [
{"name": "A", "source": "activation:a", "cast": "const void*"},
{"name": "B", "source": "weight:_first_weight"},
{"name": "bias", "source": "weight_f:_bias"},
{"name": "C", "source": "output:c", "cast": "float*"},
{"name": "M", "source": "dim:_m"}, {"name": "N", "source": "dim:_output_dim"},
{"name": "K", "source": "dim:_input_dim"} ]},
…
}
How the resolver uses it. Three facts interact:
- The equivalence-group name encodes the arithmetic.
equivalence_groupandnumerical_contractare the same string —q8_0_weight_q8_0_input_llama_fp32_output: Q8_0 weights times Q8_0 activations producing FP32 output under llama.cpp-compatible semantics. Because priority ranks only within a group, no priority value can ever promote a provider with different arithmetic over this one; cross-group ties are a hard fault ("priority cannot choose between different equivalence groups"), not a silent substitution. - Phases filter before priority ranks.
phases: ["prefill"]means a decode op never sees this provider at all —phase_selection.decodedocuments that single-token decode keeps the certifiedgemv_q8_0_q8_0GEMV path instead. Priority 200 is only ever compared against other prefill Q8 GEMM providers in the same group. - Variants carry ISA eligibility. Each variant's
requireslist gates it by CPU features: on a machine without AVX-VNNI theavx_vnnivariant is ineligible and filtered out. Theavx2_m2n4tile variant adds its ownpriority: 240and shape constraints (M ≥ 2,Kmultiple of 32) — variant-level preference, still inside the same numerical contract, which it repeats verbatim.
Honesty note: this map has no operation_interface and no
numerical_capabilities block, so the audit classes it as selection-managed and
legacy-contract-shaped — a production provider carrying the older contract keys, exactly the kind of map the
migration ratchet tracks.
Worked Example 3: kv_cache_store_bf16 — the Persistent-State One
Cache stores look like ordinary ops but their outputs are state, not ephemeral tensors. Verbatim excerpt:
{
"id": "kv_cache_store_bf16",
"op": "kv_cache_store",
"operation_interface": "kv_cache_store.bf16.head_major.v1",
"selection": {"status": "production", "priority": 100,
"equivalence_group": "kv_cache_store.bf16.head_major.v1", "phases": ["decode"]},
"variant": "bf16_cache",
"quant": {"activation": "fp32", "output": "bf16"},
"inputs": [
{"name": "k", "dtype": "fp32", "shape": ["KV", "D"], "layout": "head_major_contiguous",
"access": "read", "storage_class": "activation", "consumption": "required", …},
{"name": "v", …} ],
"outputs": [
{"name": "kv_cache_k", "dtype": "bf16", "shape": ["KV", "S_max", "D"], "layout": "head_major_contiguous",
"access": "read_write", "storage_class": "state", "consumption": "required",
"desc": "Packed BF16 K cache slice at current pos. Physical capacity S_max rows; valid-token count is tracked separately by the caller (append index); …"},
{"name": "kv_cache_v", …} ],
"dims": ["num_kv_heads", "head_dim", "num_layers", "max_seq_len", "KV", "S_max", "D"],
"constraints": {"notes": "Scheduling may round the extent to physical capacity S_max; kernels must never read or write KV rows beyond the valid-token count declared by the append index."},
"impl": {"function": "kv_cache_store_bf16", …, "sources": ["src/kernels/kv_cache_kernels.c"]},
"call_abi": {"version": 1, "params": [
{"name": "kv_cache_k", "source": "runtime:kv_cache_k_layer_u16", "cast": "uint16_t*", …},
{"name": "kv_cache_v", "source": "runtime:kv_cache_v_layer_u16", "cast": "uint16_t*", …},
{"name": "k", "source": "activation:k", "cast": "const float*", …},
{"name": "v", "source": "activation:v", "cast": "const float*", …},
{"name": "layer", "source": "const:0"}, {"name": "pos", "source": "runtime:pos"},
{"name": "num_kv_heads", "source": "dim:num_kv_heads"},
{"name": "head_dim", "source": "dim:head_dim"}, {"name": "max_seq_len", "source": "dim:max_seq_len"} ]},
…
}
How to read it.
- State, not output. The
kv_cache_k/kv_cache_vports arestorage_class: "state"withaccess: "read_write"— the kernel appends into a persistent cache slice rather than producing a fresh tensor. That classification is what lets the memory planner treat the cache as long-lived state instead of a per-op allocation. - Capacity vs validity. The shape
["KV", "S_max", "D"]is physical capacity:S_maxrows exist, but the valid-token count is tracked separately by the caller through the append index (posin the call ABI, sourced fromruntime:pos). The constraint is explicit: append writes exactly the rows at the append index and never reads beyond valid rows, even if scheduling rounds extents up toS_max. - Storage dtype and layout are part of the contract. The interface and equivalence group —
kv_cache_store.bf16.head_major.v1— bake in BF16 storage and head-major layout. An FP16 or FP32 cache store is a different group, so the resolver can never swap storage formats as a "priority" decision;quantsays FP32 in, BF16 stored. - Decode-only is deliberate.
phases: ["decode"]keeps this provider out of prefill entirely; prefill KV writes use their own providers. Like memcpy it carriesoperation_interfacebut nonumerical_capabilities, so the audit classes it as selection-managed and legacy-interface-ready.
With the format and three real maps in hand, the resolution algorithm below is mechanical: everything the resolver knows about a provider comes from these fields, and everything it rejects leaves a recorded reason.
The Resolution Algorithm
Provider selection lives in _provider_selection_metadata and _rank_provider_matches
(build_ir_v8.py:5666-5770). The pipeline:
- Validate selection metadata. Every provider must declare an
equivalence_group; a missing or malformedselectionblock is aHARD KERNEL SELECTION FAULT. Providers with noselectionblock at all are treated as legacy. - Filter by compatibility. Contract identity, phase, dtype, shape, layout, ISA, and alias safety — any mismatch rejects the provider with a recorded reason.
- Rank the survivors. The ranking tuple is direction (inference vs backward) → activation preference → lifecycle rank (
productionfirst) → priority (higher wins, within the group). - Fail closed on ambiguity. A tie among explicit production providers raises
"equal-priority production providers are ambiguous"when they share a group, or"priority cannot choose between different equivalence groups"when they do not. Zero compatible providers is also a compile-time failure — the build never silently substitutes different numerics.
Evidence, not assertion. The X-Ray fixture
version/v8/tests/fixtures/xray/provider_selection_trace.json records a real selection trace:
a candidate provider with priority 900 is rejected for status_not_production,
a priority-800 production provider for phase_mismatch, and a priority-700 production provider
for weight_dtype_mismatch — all before the compatible priority-100 production provider is
selected at rank 0. Higher priority loses to incompatibility every time, because compatibility filtering
happens before priority ranking.
Case Study: residual_save → memcpy
This is the headline example of DSL logic that legitimately remains — and of what hardened selection replaced. The two concerns are different, and the code now keeps them in different places:
- Insertion is scheduling, and stays in the DSL.
should_insert_residual_save(build_ir_v8.py:3743) fires when the current op is a pre-norm, the next op starts a branch (attention or MLP), and the previous op is not already a residual save. The op is auto-inserted and stamped"kernel": "memcpy"atbuild_ir_v8.py:7328; the_memcpy_bytesbuffer size (embed_dim × seq_len × 4) is computed during lowering atbuild_ir_v8.py:8610. Deciding that a residual must be saved is graph scheduling — it belongs here. - Selection is hardened. The
memcpymap is selection-managed with declared copy semantics — a byte copy, not residual arithmetic mislabeled under a copy's name. It wins or loses through the same mechanical resolver as every other hardened provider. - The debt is named, not hidden.
map_op_to_kernel(build_ir_v8.py:6712) still carries template-override branches:rope_qkvia_resolve_rope_qk_kernel,rope_q,mrope_qk,position_embeddingsvia_resolve_position_embeddings_kernel,kv_cache_store_shared_q, and theassistant_layer_scaleoverride. Separately,_make_decode_kv_store_op(build_ir_v8.py:8782) fabricates decode KV store ops. This is migration debt being actively burned down — tracked by the ratchet below.
Where the Weights Side Fits
Provider selection decides which kernel runs; the weights pipeline decides which bytes it reads. Converted
BUMP weights carry a metadata sidecar describing tensor names, dtypes, and layouts (see
GGUF to Bump and
ADR 0006).
Circuits and templates reference weight tensors, and the map's quant block and
weights ports must match what the sidecar describes — a map that expects Q4_K weights will not
pass compatibility filtering against an FP32 tensor. The conversion format itself is documented on the BUMP
page; it is not re-explained here.
Evolution Timeline
An honest, PR-dated history. The direction is monotonic: physics moves into the maps, heuristics leave the DSL.
- Legacy:
kernel_bindings.jsonand a function-name ABI fallback — the name was the contract. - PR #302: hardened provider selection (lifecycle, priority, equivalence groups) and the operation-interface format introduced.
- PR #305: shared RoPE, residual-copy, and KV-cache providers migrated to selection-managed maps.
- PR #318 / #320: layout provider selection and direct-layout attention providers — physical layout joins the map-owned contract.
- Future: resolver cleanup (rebind circuits to hardened metadata, delete the legacy branches), then attention and quantized GEMM migrations. The end state is an agnostic DSL: circuits state contracts, maps own everything physical, the resolver contains no model knowledge at all.
Migration Scoreboard
version/v8/contracts/kernel_interface_migration_baseline.json is a monotonic ratchet
checked by version/v8/scripts/audit_kernel_map_interfaces_v8.py: floors may only rise, ceilings
may only fall. The audit currently reports the baseline values exactly — this is the starting line the
burn-down is measured from:
| Metric | Bound | Direction |
|---|---|---|
| hardened maps | ≥ 32 | floor — only rises |
| interface + ABI cross-validated | ≥ 32 | floor — only rises |
| map-owned call ABI | ≥ 131 | floor — only rises |
| selection-managed maps | ≥ 43 | floor — only rises |
| legacy-interface-ready maps | ≥ 25 | floor — only rises |
| contract-pending maps | ≤ 54 | ceiling — only falls |
| legacy maps | ≤ 191 | ceiling — only falls |
| legacy selection conditionals | ≤ 73 | ceiling — only falls |
| operation-specific conditionals | ≤ 35 | ceiling — only falls |
Is This Over-Engineered?
Fair question — the project owner has asked it directly. 284 JSON maps, a schema for four fields, a ratchet file, and an audit script can look like architecture for its own sake. The honest answer has three parts.
Why the layering exists
One runtime serves many model families × quant formats × ISA variants × execution phases on CPUs. When provider choice is a Python conditional, the failure mode is not a crash — it is silently wrong numerics that pass leaf tests and corrupt a stitched model. Fail-closed selection converts that class of bug into a compile-time error with a recorded reason.
The costs are real
284 maps to maintain. 73 legacy selection conditionals still in the resolver. A migration still in flight, with named debt (RoPE, position embeddings, decode KV store) that has not yet moved into maps. Nobody should pretend this is finished or free.
Why it is not ceremony
The burn-down metrics are ratcheted and shrinking — the audit fails if they regress. The resolver has no model-name conditionals. Every layer is schema-validated and test-enforced: tests/test_v8_provider_selection.py, tests/test_v8_shared_provider_migration.py, tests/test_v8_kernel_call_abi.py, and the interface audit script.
Related: v8 Numerical Contracts, System Architecture, Code Generation, GGUF to Bump, and Architecture Links.