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}