Skip to main content

fuel_dispatch/fkc/
schema.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! `serde::Deserialize` structs mirroring the FKC structured schema
3//! (§3.1 file anatomy + §3.3 per-kernel block + §3.2 tensor descriptors).
4//!
5//! Design decisions for this deserialization layer:
6//!
7//! - **Tokens are `String`.** Every dtype / family / granularity / ggml_dtype /
8//!   role / determinism token deserializes as a `String` (or `Option<String>`).
9//!   §3.8 mandates quoted scalars, and keeping these as strings is also what
10//!   disarms the YAML-1.1 "Norway problem" (a `family: no` cannot silently
11//!   become a bool when the target type is `String`). The string → typed
12//!   conversion (dtype → `DType`, op_kind → `OpKind`, …) is the **lowering**
13//!   step's job (a later slice), not this parse step's.
14//! - **Cost / shape EXPRESSION fields stay `String`.** FKC has its own
15//!   expression mini-parser (§4.4); YAML never tokenizes `"2 * m * n * k"`.
16//!   They are carried verbatim here as `Option<String>`.
17//! - **`return` is a reserved word** → `#[serde(rename = "return")]` on the
18//!   field, exposed as `return_`.
19//! - **Booleans are literal `true`/`false`** only (`audited`, `in_place`,
20//!   `requires_ext`, the layout flags' coherence helpers carry their own
21//!   string-valued tri-states so they are NOT bools here — see [`LayoutSpec`]).
22//!
23//! The structs are intentionally permissive (`#[serde(default)]`, `Option`,
24//! and `deny_unknown_fields` deliberately NOT set so the additive-versioning
25//! posture of FKC G7 holds — unknown forward-looking fields are ignored). The
26//! required-field enforcement (V-FKC-1) is a validation concern, not a parse
27//! concern.
28
29use serde::Deserialize;
30
31// ===========================================================================
32// File-level front-matter (§3.1)
33// ===========================================================================
34
35/// The file-level `---`-fenced YAML front-matter (§3.1).
36#[derive(Debug, Clone, Deserialize)]
37pub struct FkcFrontMatter {
38    /// Format version (e.g. `1`).
39    pub fkc_version: u32,
40    /// Provider identity block.
41    pub provider: FkcProvider,
42}
43
44/// Provider identity (front-matter `provider:` mapping, §3.1).
45#[derive(Debug, Clone, Deserialize)]
46pub struct FkcProvider {
47    /// Provider name (e.g. `fuel-cpu-backend`).
48    pub name: String,
49    /// Backend tag (e.g. `Cpu`) — maps to a `BackendId` at lower time.
50    pub backend: String,
51    /// The `BindingEntry.kernel_source` tag (e.g. `"portable-cpu"`).
52    pub kernel_source: String,
53    /// `link_registry` symbol path (e.g. `fuel_cpu_backend::fkc::ENTRY_POINTS`).
54    /// Optional so a structure-only lint corpus can omit it.
55    #[serde(default)]
56    pub link_registry: Option<String>,
57    /// Provider build id folded into `kernel_revision_hash` (e.g. `"git:f41137b4"`).
58    #[serde(default)]
59    pub revision_base: Option<String>,
60}
61
62// ===========================================================================
63// Top-level parsed file
64// ===========================================================================
65
66/// A fully parsed FKC file: front-matter + one record per `## ` kernel section.
67#[derive(Debug, Clone)]
68pub struct FkcFile {
69    /// File-level front-matter.
70    pub front_matter: FkcFrontMatter,
71    /// One per `## ` section, in source order.
72    pub kernels: Vec<FkcKernel>,
73}
74
75// ===========================================================================
76// Per-kernel structured block (§3.3)
77// ===========================================================================
78
79/// The serde default for [`FkcKernel::registrable`] — `true` so a section
80/// that omits the field registers exactly as before (§3.10, additive §11).
81fn default_registrable() -> bool {
82    true
83}
84
85/// The per-kernel ` ```fkc ` structured contract (§3.3).
86///
87/// Exactly one of `op_kind` / `fused_op` is present (a primitive vs fused
88/// contract) **unless** the section is describe-only (`registrable: false`,
89/// §3.10), in which case `op_kind`/`fused_op` need not resolve to a real
90/// dispatch target; the structural check is a validation step, not a parse
91/// step.
92#[derive(Debug, Clone, Deserialize)]
93pub struct FkcKernel {
94    // ----- identity -----
95    /// Unique-within-file diagnostic kernel name.
96    pub kernel: String,
97    /// Describe-only marker (§3.10). When `false` this section is
98    /// **documentation-only**: it is NOT registered and is NOT required to
99    /// name a real dispatch `op_kind`/`fused_op` (the op may be `~` or a
100    /// descriptive non-dispatch token). Its descriptive fields (dtypes,
101    /// layout, quant) are STILL validated as docs. Defaults to `true` so
102    /// every existing contract registers exactly as before (additive
103    /// versioning, §11).
104    #[serde(default = "default_registrable")]
105    pub registrable: bool,
106    /// The Fuel `OpKind` this kernel implements (primitive contract).
107    #[serde(default)]
108    pub op_kind: Option<String>,
109    /// OR a `FusedOpId` name (fused contract). Exactly one of these two.
110    #[serde(default)]
111    pub fused_op: Option<String>,
112    /// One-line blurb; MUST equal the prose blurb (§10.11, checked later).
113    #[serde(default)]
114    pub blurb: Option<String>,
115    /// Backend (inherited from front-matter unless overridden).
116    #[serde(default)]
117    pub backend: Option<String>,
118    /// `kernel_source` tag (inherited unless overridden).
119    #[serde(default)]
120    pub kernel_source: Option<String>,
121    /// Symbolic ref into the provider link registry → `KernelRef` (P9, §12.6).
122    #[serde(default)]
123    pub entry_point: Option<String>,
124    /// Revision hash hex, OR `"auto"` to derive (§4.7). String per §3.8.
125    #[serde(default)]
126    pub kernel_revision_hash: Option<String>,
127    /// OPAQUE specialization-variant tag (Baracuda `variant:`, e.g.
128    /// `"splitk_partial"`). Retained verbatim so a contract-declared variant
129    /// survives lowering and can ride into a telemetry record as an opaque
130    /// annotation; the entry point remains the true kernel identity. Purely a
131    /// string on both sides — Fuel never parses or validates it beyond being a
132    /// string (additive §11, no `deny_unknown_fields`). Distinct from
133    /// [`OpParamsSchema::variant`], which names the `OpParams` Rust variant.
134    #[serde(default)]
135    pub variant: Option<String>,
136
137    // ----- accept / return -----
138    /// Accept contract (§3.6).
139    #[serde(default)]
140    pub accept: Option<AcceptBlock>,
141    /// Return contract (§3.6). `return` is a reserved word.
142    #[serde(rename = "return", default)]
143    pub return_: Option<ReturnBlock>,
144
145    // ----- capability + cost + precision + determinism (§4) -----
146    /// Capability block (§4.1–§4.6).
147    #[serde(default)]
148    pub caps: Option<CapsBlock>,
149    /// Cost block (§4.4).
150    #[serde(default)]
151    pub cost: Option<CostBlock>,
152    /// Precision block (§4.8) → `PrecisionGuarantee`.
153    #[serde(default)]
154    pub precision: Option<PrecisionBlock>,
155    /// Determinism (§4.9): `bitwise` | `same_hardware_bitwise` | `nondeterministic`.
156    #[serde(default)]
157    pub determinism: Option<String>,
158}
159
160// ===========================================================================
161// Accept (§3.6 / §3.7)
162// ===========================================================================
163
164/// The `accept:` block.
165#[derive(Debug, Clone, Deserialize)]
166pub struct AcceptBlock {
167    /// Ordered list of input tensor descriptors (§3.2).
168    #[serde(default)]
169    pub inputs: Vec<TensorDesc>,
170    /// Op-param schema (§3.7).
171    #[serde(default)]
172    pub op_params: Option<OpParamsSchema>,
173}
174
175/// Op-param schema (§3.7): names the params variant + per-field constraints.
176#[derive(Debug, Clone, Deserialize)]
177pub struct OpParamsSchema {
178    /// The `OpParams` (primitive) / `FusedOpParams` (fused) variant name.
179    #[serde(default)]
180    pub variant: Option<String>,
181    /// Per-field constraint specs (kept loose; the field map is free-form so
182    /// the importer can validate constraints in a later step). Keys are field
183    /// names; values carry `kind` / `constraint` / `note`.
184    #[serde(default)]
185    pub fields: std::collections::BTreeMap<String, OpParamFieldSpec>,
186}
187
188/// A single op-param field spec (`{ kind, constraint?, note? }`, §3.7).
189#[derive(Debug, Clone, Deserialize)]
190pub struct OpParamFieldSpec {
191    /// The field's Rust type token (e.g. `usize`, `QuantType`, `DynScalar`).
192    #[serde(default)]
193    pub kind: Option<String>,
194    /// A free-text constraint expression (validated by FKC's own parser later).
195    #[serde(default)]
196    pub constraint: Option<String>,
197    /// Free-text note.
198    #[serde(default)]
199    pub note: Option<String>,
200}
201
202// ===========================================================================
203// Return (§5)
204// ===========================================================================
205
206/// The `return:` block.
207#[derive(Debug, Clone, Deserialize)]
208pub struct ReturnBlock {
209    /// Ordered list of output descriptors + return rules (§5).
210    #[serde(default)]
211    pub outputs: Vec<OutputDesc>,
212    /// OR a list of bundle slot specs for multi-output ops (§5.5). Kept opaque
213    /// for this slice (the rank/name validation is a later step).
214    #[serde(default)]
215    pub bundle: Option<serde_yaml_ng::Value>,
216}
217
218/// An output descriptor + return rules (§5.1–§5.4). Rule fields are `String`
219/// expressions parsed by FKC later, not YAML.
220#[derive(Debug, Clone, Deserialize)]
221pub struct OutputDesc {
222    /// Output role name (§5.5).
223    #[serde(default)]
224    pub name: Option<String>,
225    /// Dtype rule, e.g. `passthrough(lhs)` / `fixed(F32)` (§5.1).
226    #[serde(default)]
227    pub dtype_rule: Option<String>,
228    /// Shape rule, e.g. `same_as(lhs)` / `from_params(batch, m, n)` (§5.2).
229    #[serde(default)]
230    pub shape_rule: Option<String>,
231    /// Layout guarantee, e.g. `contiguous` / `preallocated` (§5.3).
232    #[serde(default)]
233    pub layout_guarantee: Option<String>,
234    /// Aliasing, e.g. `none` (§5.4).
235    #[serde(default)]
236    pub aliasing: Option<String>,
237}
238
239// ===========================================================================
240// Tensor descriptor (§3.2)
241// ===========================================================================
242
243/// A tensor descriptor (operand or, with return rules, an output; §3.2).
244#[derive(Debug, Clone, Deserialize)]
245pub struct TensorDesc {
246    /// Operand role name (diagnostic + maps to an FDX view name, §5.5).
247    #[serde(default)]
248    pub name: Option<String>,
249    /// `true` ⇒ this INPUT operand is OPTIONAL (§3.4 optional-operand support):
250    /// production registers the op BOTH with and without it (e.g. conv's
251    /// `bias`). The importer's key-builder fans each dtype variant into two
252    /// keys — one omitting this operand, one including it — when set. Only the
253    /// LAST input may be optional. Defaults to `false` (a required operand);
254    /// `#[serde(default)]` so every existing contract that omits the field
255    /// keeps building exactly one key per dtype variant (additive §11).
256    #[serde(default)]
257    pub optional: bool,
258    /// Accepted DLPack dtype names (Fuel `DType` names; FDX §6.1). Strings;
259    /// the string → `DType` conversion is a lowering concern.
260    #[serde(default)]
261    pub dtypes: Vec<String>,
262    /// Optional shorthand `int|uint|float|any` that expands at lower time (§3.4).
263    #[serde(default)]
264    pub dtype_class: Option<String>,
265    /// Layout capability flags (§4.1).
266    #[serde(default)]
267    pub layout: Option<LayoutSpec>,
268    /// Rank: exact int, `"any"`, or a range `"2..=4"`. Kept as a raw scalar so
269    /// `2` and `"any"` both parse (the FKC parser interprets it later).
270    #[serde(default)]
271    pub rank: Option<serde_yaml_ng::Value>,
272    /// Free-predicate shape constraint (§3.5). Carried verbatim.
273    #[serde(default)]
274    pub shape_constraint: Option<String>,
275    /// DLPack-extension (FDX) requirements (§3.2 `fdx:`).
276    #[serde(default)]
277    pub fdx: Option<FdxSpec>,
278    /// Placement device (inherited from front-matter unless overridden).
279    #[serde(default)]
280    pub device: Option<String>,
281    /// FDX substrate class.
282    #[serde(default)]
283    pub substrate: Option<String>,
284}
285
286/// The five-flag layout capability set (§4.1). Each flag is a string tri-state
287/// (`required`/`accepted`/`n/a` for `contiguous`; `accepted`/`rejected` for the
288/// rest) carried verbatim — projection onto today's `KernelCaps.strided_input`
289/// is a lowering concern (§6).
290#[derive(Debug, Clone, Deserialize)]
291pub struct LayoutSpec {
292    /// `required` | `accepted` | `n/a`.
293    #[serde(default)]
294    pub contiguous: Option<String>,
295    /// `accepted` | `rejected`.
296    #[serde(default)]
297    pub strided: Option<String>,
298    /// `accepted` | `rejected`.
299    #[serde(default)]
300    pub broadcast_stride0: Option<String>,
301    /// The iteration-axis indices on which the operand MUST have stride 0 —
302    /// the §6-additive broadcast-axis mask. Present only with
303    /// `broadcast_stride0: required` (a baked-broadcast cell); absent ⇒
304    /// today's behavior. `validate` (Rule 4) enforces present-iff-required
305    /// and non-empty. The layout check accepts iff the operand's stride-0
306    /// axis set equals this exactly — that exact-match + the routing land in
307    /// path 1b; path 1a parses + validates it and projects only the
308    /// `requires_broadcast` boolean (from `broadcast_stride0: required`) onto
309    /// [`crate::kernel::KernelCaps`], excluding the baked-broadcast kernel
310    /// from dense-operand selection.
311    #[serde(default)]
312    pub broadcast_axes: Option<Vec<i64>>,
313    /// `accepted` | `rejected`.
314    #[serde(default)]
315    pub start_offset: Option<String>,
316    /// `accepted` | `rejected` — NEGATIVE strides (§4.1.1).
317    #[serde(default)]
318    pub reverse_strides: Option<String>,
319    /// Optional per-operand override of `caps.awkward_layout_strategy` (§4.3.1).
320    #[serde(default)]
321    pub awkward_layout_strategy: Option<String>,
322}
323
324/// DLPack-extension (FDX) requirements on an operand (§3.2 `fdx:`).
325#[derive(Debug, Clone, Deserialize)]
326pub struct FdxSpec {
327    /// `true` ⇒ this operand's meaning needs an FDX sidecar.
328    #[serde(default)]
329    pub requires_ext: Option<bool>,
330    /// Quant facts (§3.2 `fdx.quant:`).
331    #[serde(default)]
332    pub quant: Option<QuantSpec>,
333    /// Sub-byte logical_dtype code when the base carries opaque uint8 (FDX §6.1).
334    #[serde(default)]
335    pub sub_byte: Option<String>,
336    /// `rejected`|`tolerated`|`required` symbolic-extent tolerance (§4.5).
337    #[serde(default)]
338    pub symbolic_extent: Option<String>,
339    /// `rejected`|`scalar`|`range`|`affine` (§3.9.2).
340    #[serde(default)]
341    pub extent_kind: Option<String>,
342    /// Paged / indexed-residency (gather) operand spec (§3.9.1).
343    #[serde(default)]
344    pub gather: Option<GatherSpec>,
345}
346
347/// Quant facts (§3.2 `fdx.quant:`). All tokens are `String` (disarms Norway;
348/// the typed conversion is a lowering concern).
349#[derive(Debug, Clone, Deserialize)]
350pub struct QuantSpec {
351    /// FDXQuant.family symbol: `none|GGML_BLOCK|MX|AFFINE_INT|AFFINE_FLOAT|AFFINE_BLOCK` (FDX §6.2).
352    #[serde(default)]
353    pub family: Option<String>,
354    /// `GgmlDType` variant NAME when family=GGML_BLOCK (e.g. `Q4_0`, `Q4K`); §3.4.
355    #[serde(default)]
356    pub ggml_dtype: Option<String>,
357    /// FDXScaleGranularity symbol: `PerTensor|PerToken|PerChannel|PerBlock` (FDX §6.2).
358    #[serde(default)]
359    pub granularity: Option<String>,
360    /// Per-output-row per-block grain (e.g. `[block_size]`) for AFFINE_BLOCK (§3.9.3 / NF4).
361    #[serde(default)]
362    pub block_shape: Option<serde_yaml_ng::Value>,
363    /// `activation`|`weight` (FDX ScalePair role).
364    #[serde(default)]
365    pub role: Option<String>,
366    /// Role of the SEPARATE-GRAPH-INPUT scale operand when the ABI takes the
367    /// scale as its own input (§3.9.3). Mutually exclusive with a sidecar
368    /// `scale_buffer` (the `ScaleDoubleDeclared` check, a later step).
369    #[serde(default)]
370    pub scale_operand: Option<String>,
371}
372
373/// Paged / indexed-residency (gather) operand spec (§3.9.1).
374#[derive(Debug, Clone, Deserialize)]
375pub struct GatherSpec {
376    /// `~` | `paged_blocks` — FDX FDXIndexedResidency.kind symbol.
377    #[serde(default)]
378    pub kind: Option<String>,
379    /// Role of the block-table operand (a separate accept.input).
380    #[serde(default)]
381    pub block_table: Option<String>,
382    /// Role of the per-sequence live-length operand (a separate accept.input).
383    #[serde(default)]
384    pub context_lens: Option<String>,
385}
386
387// ===========================================================================
388// caps / cost / precision (§4)
389// ===========================================================================
390
391/// Capability block (§4.1–§4.6).
392#[derive(Debug, Clone, Deserialize)]
393pub struct CapsBlock {
394    /// `requires_contiguous` | `handles_strided` | `contiguize_internally` (§4.3).
395    #[serde(default)]
396    pub awkward_layout_strategy: Option<String>,
397    /// Declared fast-path predicates (§4.2). Kept opaque for this slice.
398    #[serde(default)]
399    pub fast_paths: Option<serde_yaml_ng::Value>,
400    /// `in_place` (§4.6).
401    #[serde(default)]
402    pub in_place: Option<bool>,
403    /// Mirrors `BackendCapabilities.required_alignment`.
404    #[serde(default)]
405    pub alignment_bytes: Option<u64>,
406    /// Access granularity in bits.
407    #[serde(default)]
408    pub access_granularity_bits: Option<u64>,
409}
410
411/// Cost block (§4.4). Coefficient EXPRESSION fields stay `String` — FKC's own
412/// parser reads them later, NOT YAML. `~` (YAML null) deserializes to `None`.
413#[derive(Debug, Clone, Deserialize)]
414pub struct CostBlock {
415    /// `declared` | `judge_measured` — REQUIRED, both first-class (§4.4).
416    #[serde(default)]
417    pub provenance: Option<String>,
418    /// Coarse relative cost class bucket (§4.4).
419    #[serde(default)]
420    pub class: Option<String>,
421    /// Names a REGISTERED cost fn to PIN as the binding's `CostFn` (§4.4
422    /// cost-fn trampoline, Task-F). When present the importer resolves this
423    /// name through the provider's `LinkRegistry`
424    /// ([`crate::fkc::LinkRegistry::resolve_cost_fn`]) and registers the
425    /// binding with THAT `CostFn` instead of the `unknown_cost` sentinel — so
426    /// the `fill_unset_cost_for_backend` pass does NOT overwrite it with the
427    /// op/backend default. This is the mechanism that lets a contract pin a
428    /// real, shape-aware cost model (e.g. the CUDA `flash_decoding` static
429    /// infeasibility gate `cost_flash_decoding_cuda`). An unresolved name is a
430    /// typed [`crate::fkc::FkcError::UnknownCostFn`] import error (never a
431    /// silent fallback). The name is opaque here (a String, disarming Norway);
432    /// the coefficient-expression fields below stay independent. Additive
433    /// (§11; no `deny_unknown_fields`). Serde key `cost_fn`.
434    #[serde(default)]
435    pub cost_fn: Option<String>,
436    /// Symbolic FLOPs expression over shape/param symbols (§4.4).
437    #[serde(default)]
438    pub flops: Option<String>,
439    /// Symbolic bytes-moved expression (§4.4).
440    #[serde(default)]
441    pub bytes_moved: Option<String>,
442    /// Launch overhead. May be a literal number or `~`; kept as a raw scalar.
443    #[serde(default)]
444    pub overhead_ns: Option<serde_yaml_ng::Value>,
445    /// Per-tier memory footprint (§4.4) — [consumer-ahead] beyond device_bytes.
446    #[serde(default)]
447    pub memory: Option<CostMemory>,
448}
449
450/// Per-tier memory footprint (§4.4). Each tier is an expression `String` (or a
451/// literal `0`); kept as a raw scalar so `0` and `"n * 4"` both parse.
452#[derive(Debug, Clone, Deserialize)]
453pub struct CostMemory {
454    /// Output device alloc (executor pre-allocates).
455    #[serde(default)]
456    pub device_bytes: Option<serde_yaml_ng::Value>,
457    /// Host-tier bytes.
458    #[serde(default)]
459    pub host_bytes: Option<serde_yaml_ng::Value>,
460    /// Disk-tier bytes.
461    #[serde(default)]
462    pub disk_bytes: Option<serde_yaml_ng::Value>,
463}
464
465/// Precision block (§4.8) → `PrecisionGuarantee`. Bound fields are raw scalars
466/// (`0`, a float, or `~`); the typed conversion is a lowering concern.
467#[derive(Debug, Clone, Deserialize)]
468pub struct PrecisionBlock {
469    /// Whether bit-stable on the same hardware.
470    #[serde(default)]
471    pub bit_stable_on_same_hardware: Option<bool>,
472    /// Max ULP error (`~` ⇒ none).
473    #[serde(default)]
474    pub max_ulp: Option<serde_yaml_ng::Value>,
475    /// Max relative error (`~` ⇒ none).
476    #[serde(default)]
477    pub max_relative: Option<serde_yaml_ng::Value>,
478    /// Max absolute error (`~` ⇒ none).
479    #[serde(default)]
480    pub max_absolute: Option<serde_yaml_ng::Value>,
481    /// `false` ⇒ UNAUDITED; `true` + all-null ⇒ none(reason) audited-no-bound.
482    #[serde(default)]
483    pub audited: Option<bool>,
484    /// Free-text precision notes.
485    #[serde(default)]
486    pub notes: Option<String>,
487}