1mod caps_map;
43pub(crate) mod cost_compile;
44pub(crate) mod cost_expr;
45mod cpu_link;
46#[cfg(feature = "cuda")]
47mod cuda_link;
48mod error;
49mod lower;
50mod parse;
51mod precision;
52mod register;
53mod return_check;
54mod revhash;
55mod schema;
56mod shape_constraint;
57pub mod shape_expr;
62pub(crate) mod shape_expr_parse;
63mod validate;
64pub(crate) mod verify;
67#[cfg(any(test, feature = "dev-tools"))]
72pub(crate) mod contract_audit_flip;
73#[cfg(feature = "vulkan")]
74mod vulkan_link;
75mod warn;
76
77pub use caps_map::{ResolvedLayout, Tri, is_generic_contract};
78pub use cost_expr::{
79 CompiledCostExpr, CostEvalError, CostNode, bind_cost_symbols, cost_estimate, eval as eval_cost,
80};
81pub use cpu_link::{
82 CPU_AFFINE_CLAMP_POWI_ENTRY_POINTS, CPU_ATTENTION_ENTRY_POINTS, CPU_BINARY_ENTRY_POINTS,
83 CPU_COMPARE_ENTRY_POINTS, CPU_CONV_ENTRY_POINTS, CPU_FUSED_CONV_ROPE_ENTRY_POINTS,
84 CPU_FUSED_LINEAR_QUANT_ENTRY_POINTS, CPU_FUSED_NORM_ENTRY_POINTS, CPU_INPLACE_ENTRY_POINTS,
85 CPU_MATMUL_ENTRY_POINTS, CPU_NORM_BACKWARD_ENTRY_POINTS, CPU_NORM_ENTRY_POINTS,
86 CPU_PADDING_ENTRY_POINTS, CPU_REDUCE_ENTRY_POINTS, CPU_REDUCE_TO_ENTRY_POINTS,
87 CPU_ROPE_ENTRY_POINTS, CPU_SHAPE_OPS_ENTRY_POINTS, CPU_SSM_ENTRY_POINTS,
88 CPU_UNARY_ENTRY_POINTS, CPU_WHERE_ENTRY_POINTS, CpuLinkRegistry,
89};
90#[cfg(feature = "cuda")]
91pub use cuda_link::{
92 CUDA_AFFINE_ENTRY_POINTS, CUDA_ARG_REDUCE_ENTRY_POINTS, CUDA_AWQ_MATMUL_ENTRY_POINTS,
93 CUDA_BINARY_ENTRY_POINTS, CUDA_CAST_ENTRY_POINTS, CUDA_CAUSAL_CONV1D_ENTRY_POINTS,
94 CUDA_CLAMP_ENTRY_POINTS, CUDA_CLAMP_INPLACE_ENTRY_POINTS, CUDA_CONCAT_ENTRY_POINTS,
95 CUDA_COST_FNS, CUDA_CUMSUM_ENTRY_POINTS, CUDA_FLASH_DECODING_ENTRY_POINTS,
96 CUDA_FLIP_ENTRY_POINTS, CUDA_GEMM_DENSE_ENTRY_POINTS, CUDA_GEMM_INT_ENTRY_POINTS,
97 CUDA_INDEXING_ENTRY_POINTS, CUDA_INPLACE_AFFINE_ENTRY_POINTS, CUDA_INPLACE_UNARY_ENTRY_POINTS,
98 CUDA_NORM_ENTRY_POINTS, CUDA_PAD_BACKWARD_ENTRY_POINTS, CUDA_PAD_ENTRY_POINTS,
99 CUDA_POWI_BACKWARD_ENTRY_POINTS, CUDA_POWI_ENTRY_POINTS, CUDA_POWI_INPLACE_ENTRY_POINTS,
100 CUDA_REDUCE_ENTRY_POINTS, CUDA_REDUCE_TO_ENTRY_POINTS, CUDA_ROLL_ENTRY_POINTS,
101 CUDA_ROPE_ENTRY_POINTS, CUDA_SOFTMAX_ENTRY_POINTS, CUDA_TRIANGULAR_ENTRY_POINTS,
102 CUDA_UNARY_ENTRY_POINTS, CUDA_WRITE_SLICE_ENTRY_POINTS, CUDA_WRITE_SLICE_ROTATING_ENTRY_POINTS,
103 CudaLinkRegistry,
104};
105pub use error::FkcError;
106pub use lower::{LinkRegistry, Resolved, ResolvedFused, ResolvedPrimitive, lower_file};
107pub use parse::{parse_file, parse_path};
108pub use register::{
109 ImportedProvider, fused_unknown_cost, import_bundle, import_bundle_str, import_glob,
110};
111pub use revhash::compute_revision;
112pub use schema::{
113 AcceptBlock, CapsBlock, CostBlock, CostMemory, FdxSpec, FkcFile, FkcFrontMatter, FkcKernel,
114 FkcProvider, GatherSpec, LayoutSpec, OpParamFieldSpec, OpParamsSchema, OutputDesc,
115 PrecisionBlock, QuantSpec, ReturnBlock, TensorDesc,
116};
117pub use shape_constraint::{
118 AxisIndex, ProbeCombo, RankSpec, ShapeAtom, ShapeConstraint, parse_rank_spec,
119 parse_shape_constraint, solve_probe_shapes,
120};
121pub use validate::{FKC_SUPPORTED_VERSIONS, validate_file, validate_kernel};
122#[cfg(feature = "vulkan")]
123pub use vulkan_link::{
124 VULKAN_CAST_ENTRY_POINTS, VULKAN_ELEMENTWISE_ENTRY_POINTS, VulkanLinkRegistry,
125};
126pub use warn::ImportWarning;
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 const ELEMENTWISE_BINARY: &str =
136 include_str!("../../kernel-contracts/cpu/elementwise-binary.fkc.md");
137 const QUANT_MATMUL: &str = include_str!("../../kernel-contracts/cpu/quant-matmul.fkc.md");
141 const ROPE_APPLY: &str = include_str!("../../kernel-contracts/cuda/rope-apply.fkc.md");
153
154 #[test]
159 fn parses_real_elementwise_binary_contract() {
160 let file =
161 parse_file(ELEMENTWISE_BINARY).expect("authored elementwise-binary.fkc.md must parse");
162
163 assert_eq!(file.front_matter.fkc_version, 1);
165 assert_eq!(file.front_matter.provider.name, "fuel-cpu-backend");
166 assert_eq!(file.front_matter.provider.backend, "Cpu");
167 assert_eq!(file.front_matter.provider.kernel_source, "portable-cpu");
168 assert_eq!(
169 file.front_matter.provider.link_registry.as_deref(),
170 Some("fuel_cpu_backend::fkc::ENTRY_POINTS")
171 );
172
173 assert!(
175 !file.kernels.is_empty(),
176 "elementwise-binary must yield kernels"
177 );
178
179 let names: Vec<&str> = file.kernels.iter().map(|k| k.kernel.as_str()).collect();
181 assert!(
182 names.contains(&"binary"),
183 "expected the `binary` chassis section; got {names:?}"
184 );
185 assert!(
186 names.contains(&"add_f32"),
187 "expected `add_f32`; got {names:?}"
188 );
189
190 let add_f32 = file
192 .kernels
193 .iter()
194 .find(|k| k.kernel == "add_f32")
195 .expect("add_f32 present");
196 assert_eq!(add_f32.op_kind.as_deref(), Some("AddElementwise"));
197 assert!(add_f32.fused_op.is_none());
198
199 let accept = add_f32.accept.as_ref().expect("add_f32 has accept");
201 assert_eq!(accept.inputs.len(), 2);
202 assert_eq!(accept.inputs[0].name.as_deref(), Some("lhs"));
203 assert_eq!(accept.inputs[0].dtypes, vec!["F32".to_string()]);
204 let layout = accept.inputs[0].layout.as_ref().expect("lhs has layout");
206 assert_eq!(layout.contiguous.as_deref(), Some("required"));
207 assert_eq!(layout.strided.as_deref(), Some("rejected"));
208
209 let ret = add_f32.return_.as_ref().expect("add_f32 has return");
211 assert_eq!(ret.outputs.len(), 1);
212 assert_eq!(
213 ret.outputs[0].dtype_rule.as_deref(),
214 Some("passthrough(lhs)")
215 );
216
217 let cost = add_f32.cost.as_ref().expect("add_f32 has cost");
219 assert_eq!(cost.provenance.as_deref(), Some("judge_measured"));
220 assert_eq!(cost.flops.as_deref(), Some("n"));
221 assert_eq!(cost.bytes_moved.as_deref(), Some("3 * n * 4"));
222
223 assert_eq!(
225 add_f32.determinism.as_deref(),
226 Some("same_hardware_bitwise")
227 );
228 }
229
230 #[test]
231 fn parses_real_quant_matmul_contract() {
232 let file = parse_file(QUANT_MATMUL).expect("authored quant-matmul.fkc.md must parse");
233
234 assert_eq!(file.front_matter.provider.name, "fuel-cpu-backend");
235 assert!(!file.kernels.is_empty());
236
237 let names: Vec<&str> = file.kernels.iter().map(|k| k.kernel.as_str()).collect();
238 assert!(
239 names.contains(&"qmatmul_q4_0_f32"),
240 "expected qmatmul_q4_0_f32; got {names:?}"
241 );
242 assert!(
243 names.contains(&"nf4_matmul_f32"),
244 "expected nf4_matmul_f32; got {names:?}"
245 );
246
247 let q40 = file
250 .kernels
251 .iter()
252 .find(|k| k.kernel == "qmatmul_q4_0_f32")
253 .unwrap();
254 assert_eq!(q40.op_kind.as_deref(), Some("QMatMul"));
255 let weight = q40
256 .accept
257 .as_ref()
258 .unwrap()
259 .inputs
260 .iter()
261 .find(|d| d.name.as_deref() == Some("weight"))
262 .expect("q4_0 has a weight operand");
263 let quant = weight
264 .fdx
265 .as_ref()
266 .unwrap()
267 .quant
268 .as_ref()
269 .expect("weight has fdx.quant");
270 assert_eq!(quant.family.as_deref(), Some("GGML_BLOCK"));
271 assert_eq!(quant.ggml_dtype.as_deref(), Some("Q4_0"));
272 assert_eq!(quant.role.as_deref(), Some("weight"));
273 assert!(
274 quant.scale_operand.is_none(),
275 "INLINE scale: no separate operand"
276 );
277
278 let op_params = q40.accept.as_ref().unwrap().op_params.as_ref().unwrap();
280 assert_eq!(op_params.variant.as_deref(), Some("QMatMul"));
281 assert!(op_params.fields.contains_key("k"));
282
283 let nf4 = file
285 .kernels
286 .iter()
287 .find(|k| k.kernel == "nf4_matmul_f32")
288 .unwrap();
289 let nf4_inputs = &nf4.accept.as_ref().unwrap().inputs;
290 assert!(
291 nf4_inputs
292 .iter()
293 .any(|d| d.name.as_deref() == Some("absmax")),
294 "NF4 has a separate absmax operand"
295 );
296 let w_packed = nf4_inputs
297 .iter()
298 .find(|d| d.name.as_deref() == Some("w_packed"))
299 .unwrap();
300 let nf4_quant = w_packed.fdx.as_ref().unwrap().quant.as_ref().unwrap();
301 assert_eq!(nf4_quant.family.as_deref(), Some("AFFINE_BLOCK"));
302 assert_eq!(nf4_quant.scale_operand.as_deref(), Some("absmax"));
303 }
304
305 #[test]
312 fn parses_and_lowers_real_rope_apply_contract() {
313 use fuel_ir::DType;
314 use fuel_ir::dispatch::OpKind;
315 use fuel_ir::probe::BackendId;
316
317 let file = parse_file(ROPE_APPLY).expect("rope-apply.fkc.md must parse");
318 validate_file(&file).expect("rope-apply.fkc.md must pass the V-FKC-* validators");
319
320 assert_eq!(file.front_matter.provider.name, "fuel-cuda-backend");
321 assert_eq!(file.front_matter.provider.backend, "Cuda");
322 assert_eq!(file.front_matter.provider.kernel_source, "baracuda");
323
324 let names: Vec<&str> = file.kernels.iter().map(|k| k.kernel.as_str()).collect();
325 assert!(
326 names.contains(&"rope_apply"),
327 "expected a `rope_apply` section; got {names:?}"
328 );
329
330 let kernel = file
331 .kernels
332 .iter()
333 .find(|k| k.kernel == "rope_apply")
334 .unwrap();
335 assert_eq!(kernel.op_kind.as_deref(), Some("Rope"));
336 assert!(
337 kernel.fused_op.is_none(),
338 "rope_apply is a primitive op_kind contract, not fused"
339 );
340
341 let accept = kernel.accept.as_ref().expect("rope_apply has accept");
344 assert_eq!(accept.inputs.len(), 3);
345 assert_eq!(accept.inputs[0].name.as_deref(), Some("x"));
346 assert_eq!(
347 accept.inputs[0].dtypes,
348 vec![
349 "F32".to_string(),
350 "F16".to_string(),
351 "BF16".to_string(),
352 "F64".to_string()
353 ],
354 );
355 assert_eq!(accept.inputs[1].name.as_deref(), Some("cos"));
356 assert_eq!(accept.inputs[1].dtypes, vec!["F32".to_string()]);
357 assert_eq!(accept.inputs[2].name.as_deref(), Some("sin"));
358 assert_eq!(accept.inputs[2].dtypes, vec!["F32".to_string()]);
359
360 fn dummy_kernel(
364 _inputs: &[std::sync::Arc<std::sync::RwLock<fuel_memory::Storage>>],
365 _outputs: &mut [std::sync::Arc<std::sync::RwLock<fuel_memory::Storage>>],
366 _layouts: &[fuel_ir::Layout],
367 _params: &crate::kernel::OpParams,
368 ) -> fuel_ir::Result<()> {
369 Ok(())
370 }
371 struct StubRopeApplyLink;
372 impl LinkRegistry for StubRopeApplyLink {
373 fn resolve_primitive(&self, symbol: &str) -> Option<crate::kernel::KernelRef> {
374 const KNOWN: &[&str] = &[
375 "baracuda_kernels_rope_apply_f32",
376 "baracuda_kernels_rope_apply_f16",
377 "baracuda_kernels_rope_apply_bf16",
378 "baracuda_kernels_rope_apply_f64",
379 ];
380 if KNOWN.contains(&symbol) {
381 Some(dummy_kernel)
382 } else {
383 None
384 }
385 }
386 fn resolve_fused(&self, _symbol: &str) -> Option<crate::kernel::KernelRef> {
387 None
388 }
389 }
390
391 let mut warnings = Vec::new();
392 let resolved = lower_file(&file, &StubRopeApplyLink, &mut warnings)
393 .expect("rope-apply.fkc.md must lower against a registry resolving its 4 real symbols");
394 let primitives: Vec<_> = resolved
395 .iter()
396 .filter_map(|r| match r {
397 Resolved::Primitive(p) => Some(p),
398 Resolved::Fused(_) => None,
399 })
400 .collect();
401 assert_eq!(
402 primitives.len(),
403 4,
404 "expected 4 dtype variants (F32/F16/BF16/F64); got {}",
405 primitives.len(),
406 );
407 for p in &primitives {
408 assert_eq!(p.op, OpKind::Rope);
409 assert_eq!(p.backend, BackendId::Cuda);
410 assert_eq!(
412 p.dtypes.len(),
413 4,
414 "expected [x, cos, sin, out] key; got {:?}",
415 p.dtypes
416 );
417 assert_eq!(p.dtypes[1], DType::F32, "cos is always F32");
418 assert_eq!(p.dtypes[2], DType::F32, "sin is always F32");
419 assert_eq!(
420 p.dtypes[0], p.dtypes[3],
421 "output passthrough(x) must match x's dtype"
422 );
423 }
424 let x_dtypes: std::collections::HashSet<DType> =
425 primitives.iter().map(|p| p.dtypes[0]).collect();
426 assert_eq!(
427 x_dtypes,
428 [DType::F32, DType::F16, DType::BF16, DType::F64]
429 .into_iter()
430 .collect(),
431 "expected exactly the F32/F16/BF16/F64 fan",
432 );
433 }
434
435 const VALID_MINIMAL: &str = "\
440---
441fkc_version: 1
442provider:
443 name: test-provider
444 backend: Cpu
445 kernel_source: \"test-cpu\"
446---
447
448# test bundle
449
450## demo
451
452A blurb.
453
454```fkc
455kernel: demo
456op_kind: AddElementwise
457blurb: \"demo\"
458entry_point: \"x::y\"
459```
460";
461
462 #[test]
463 fn valid_minimal_parses() {
464 let file = parse_file(VALID_MINIMAL).expect("minimal valid file parses");
465 assert_eq!(file.kernels.len(), 1);
466 assert_eq!(file.kernels[0].kernel, "demo");
467 }
468
469 #[test]
470 fn tab_indentation_in_block_is_rejected() {
471 let src = "\
473---
474fkc_version: 1
475provider:
476 name: p
477 backend: Cpu
478 kernel_source: \"c\"
479---
480
481## demo
482
483b
484
485```fkc
486kernel: demo
487accept:
488\tinputs: []
489```
490";
491 let err = parse_file(src).expect_err("tab must be rejected");
492 assert!(
493 matches!(err, FkcError::TabIndentation { .. }),
494 "got {err:?}"
495 );
496 }
497
498 #[test]
499 fn anchor_is_rejected() {
500 let src = "\
501---
502fkc_version: 1
503provider:
504 name: p
505 backend: Cpu
506 kernel_source: \"c\"
507---
508
509## demo
510
511b
512
513```fkc
514kernel: &base demo
515op_kind: AddElementwise
516```
517";
518 let err = parse_file(src).expect_err("anchor must be rejected");
519 assert!(
520 matches!(err, FkcError::AnchorDisallowed { .. }),
521 "got {err:?}"
522 );
523 }
524
525 #[test]
526 fn alias_is_rejected() {
527 let src = "\
528---
529fkc_version: 1
530provider:
531 name: p
532 backend: Cpu
533 kernel_source: \"c\"
534---
535
536## demo
537
538b
539
540```fkc
541kernel: demo
542blurb: *base
543```
544";
545 let err = parse_file(src).expect_err("alias must be rejected");
546 assert!(
547 matches!(err, FkcError::AliasDisallowed { .. }),
548 "got {err:?}"
549 );
550 }
551
552 #[test]
553 fn merge_key_is_rejected() {
554 let src = "\
555---
556fkc_version: 1
557provider:
558 name: p
559 backend: Cpu
560 kernel_source: \"c\"
561---
562
563## demo
564
565b
566
567```fkc
568kernel: demo
569caps:
570 <<: defaults
571```
572";
573 let err = parse_file(src).expect_err("merge key must be rejected");
574 assert!(
575 matches!(err, FkcError::MergeKeyDisallowed { .. }),
576 "got {err:?}"
577 );
578 }
579
580 #[test]
581 fn unquoted_norway_token_is_rejected() {
582 let src = "\
585---
586fkc_version: 1
587provider:
588 name: p
589 backend: Cpu
590 kernel_source: \"c\"
591---
592
593## demo
594
595b
596
597```fkc
598kernel: demo
599family: no
600```
601";
602 let err = parse_file(src).expect_err("unquoted Norway token must be rejected");
603 assert!(
604 matches!(err, FkcError::NorwayToken { ref token, .. } if token == "no"),
605 "got {err:?}"
606 );
607 }
608
609 #[test]
610 fn quoted_norway_token_is_accepted_as_string() {
611 let src = "\
614---
615fkc_version: 1
616provider:
617 name: p
618 backend: Cpu
619 kernel_source: \"c\"
620---
621
622## demo
623
624b
625
626```fkc
627kernel: demo
628accept:
629 inputs:
630 - name: lhs
631 dtypes: [F32]
632 fdx:
633 quant:
634 family: \"none\"
635```
636";
637 let file = parse_file(src).expect("quoted token parses");
638 let fam = file.kernels[0].accept.as_ref().unwrap().inputs[0]
639 .fdx
640 .as_ref()
641 .unwrap()
642 .quant
643 .as_ref()
644 .unwrap()
645 .family
646 .as_deref();
647 assert_eq!(fam, Some("none"));
648 }
649
650 #[test]
655 fn missing_fkc_block_is_rejected() {
656 let src = "\
657---
658fkc_version: 1
659provider:
660 name: p
661 backend: Cpu
662 kernel_source: \"c\"
663---
664
665## demo
666
667Just prose, no fkc block.
668";
669 let err = parse_file(src).expect_err("section with no fkc block must error");
670 assert!(
671 matches!(err, FkcError::MissingFkcBlock { ref section } if section == "demo"),
672 "got {err:?}"
673 );
674 }
675
676 #[test]
677 fn duplicate_fkc_block_is_rejected() {
678 let src = "\
679---
680fkc_version: 1
681provider:
682 name: p
683 backend: Cpu
684 kernel_source: \"c\"
685---
686
687## demo
688
689b
690
691```fkc
692kernel: demo
693```
694
695more prose
696
697```fkc
698kernel: demo2
699```
700";
701 let err = parse_file(src).expect_err("section with two fkc blocks must error");
702 assert!(
703 matches!(err, FkcError::MultipleFkcBlocks { ref section, count } if section == "demo" && count == 2),
704 "got {err:?}"
705 );
706 }
707
708 #[test]
709 fn missing_front_matter_is_rejected() {
710 let src = "## demo\n\n```fkc\nkernel: demo\n```\n";
711 let err = parse_file(src).expect_err("file without front-matter must error");
712 assert!(
713 matches!(err, FkcError::MalformedFrontMatter(_)),
714 "got {err:?}"
715 );
716 }
717
718 #[test]
726 fn orphan_fkc_block_outside_a_section_is_rejected() {
727 let src = "---\nfkc_version: 1\nprovider:\n name: p\n backend: Cpu\n kernel_source: \"ks\"\n---\n\n# Title\n\n```fkc\nkernel: add_f32\nop_kind: AddElementwise\n```\n";
729 let err = parse_file(src).expect_err(
730 "an fkc block outside any `## ` section must be rejected, not silently dropped",
731 );
732 assert!(
733 matches!(err, FkcError::OrphanFkcBlock { .. }),
734 "got {err:?}"
735 );
736 }
737
738 #[test]
739 fn fkc_block_with_no_sections_at_all_is_rejected() {
740 let src = "---\nfkc_version: 1\nprovider:\n name: p\n backend: Cpu\n kernel_source: \"ks\"\n---\n\n```fkc\nkernel: add_f32\nop_kind: AddElementwise\n```\n";
741 assert!(matches!(
742 parse_file(src).unwrap_err(),
743 FkcError::OrphanFkcBlock { .. }
744 ));
745 }
746
747 #[test]
748 fn non_fkc_fence_before_first_heading_is_not_flagged() {
749 let src = "---\nfkc_version: 1\nprovider:\n name: p\n backend: Cpu\n kernel_source: \"ks\"\n---\n\n# Title\n\n```yaml\nexample: true\n```\n\n## add_f32\n\n```fkc\nkernel: add_f32\nop_kind: AddElementwise\n```\n";
751 parse_file(src).expect("a non-fkc intro fence + a real section parses fine");
752 }
753
754 #[test]
755 fn indented_pseudo_heading_does_not_hide_a_following_orphan_fkc_block() {
756 let src = "---\nfkc_version: 1\nprovider:\n name: p\n backend: Cpu\n kernel_source: \"ks\"\n---\n\n# Title\n\n ## not-a-real-heading (indented)\n\n```fkc\nkernel: add_f32\nop_kind: AddElementwise\n```\n";
760 let err = parse_file(src)
761 .expect_err("an indented pseudo-heading must not hide a following orphan fkc block");
762 assert!(
763 matches!(err, FkcError::OrphanFkcBlock { .. }),
764 "got {err:?}"
765 );
766 }
767}