From 7a773da168ffb5088fdc52c7de48966f8d4751f3 Mon Sep 17 00:00:00 2001 From: justinchuby <223556219+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 20:18:18 -0700 Subject: [PATCH] fix(metadata): ignore capabilities the decode path never exercises A package built by current mobius failed to load with Invalid inference metadata: ["bounded_state_recurrence", "emit_valid_length", "linear_effects", "loop_induction_values", "nested_control_flow", "serving_service_contract", "typed_emit", "workflow_ssa"] on a bare decoder that runs correctly once those declarations are removed. The model was fine; the loader was refusing over workflow features it would never reach. `validate` folded two different questions into one Vec. A structural defect means the document does not describe a runnable model and nobody can proceed. An unsupported capability means the package asks for a runtime feature this build lacks, which only matters if the caller would exercise it. Merged, every caller had to treat both as fatal. Split them: `validate_structure_and_capabilities` returns a `CapabilityReport` with the two kinds apart, and the engine fails on structural defects while logging unsupported capabilities at info and continuing. `validate` keeps its all-or-nothing contract for callers that want it. Verified against the package that motivated this: the mobius-emitted workflow metadata now loads and decodes (7.233 ms/token) where it previously failed at load. Note: `recurrent_state::replace_state_rejects_a_sequence_axis` fails on origin/main before this change and is untouched here. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624 --- crates/onnx-genai-engine/src/engine/load.rs | 33 ++++++++++-- crates/onnx-genai-metadata/src/lib.rs | 4 +- crates/onnx-genai-metadata/src/validation.rs | 53 ++++++++++++++++--- .../tests/metadata_fixtures.rs | 40 ++++++++++++++ 4 files changed, 116 insertions(+), 14 deletions(-) diff --git a/crates/onnx-genai-engine/src/engine/load.rs b/crates/onnx-genai-engine/src/engine/load.rs index 93b40840d6..316153d813 100644 --- a/crates/onnx-genai-engine/src/engine/load.rs +++ b/crates/onnx-genai-engine/src/engine/load.rs @@ -368,8 +368,17 @@ impl Engine { // existing load path unchanged. See #384 and the qwen3.5-27B enablement. maybe_fill_hybrid_io_from_graph(&mut metadata, &model_directory.model_path); let runtime_caps = onnx_genai_metadata::RuntimeCapabilities::default(); - if let Err(errors) = onnx_genai_metadata::validate(&metadata, &runtime_caps) { - anyhow::bail!("Invalid inference metadata: {errors:?}"); + let report = + onnx_genai_metadata::validate_structure_and_capabilities(&metadata, &runtime_caps); + if !report.structural.is_empty() { + anyhow::bail!("Invalid inference metadata: {:?}", report.structural); + } + if !report.unsupported_capabilities.is_empty() { + tracing::info!( + "inference metadata declares capabilities this runtime does not implement: {}; \ + continuing because the decode path does not exercise them", + report.unsupported_capabilities.join(", ") + ); } // Native MTP self-speculation seeds its draft head from a target hidden // output. The native decode session only records that hidden state when @@ -1156,10 +1165,24 @@ fn resolve_metadata_and_decode_path( shared_kv: crate::decode::SharedKvOffer, capture_requested: bool, ) -> anyhow::Result { - // Validate capabilities + // Structural defects mean the document does not describe a runnable model, + // so they stay fatal. Unsupported capabilities are a different question: + // this is the bare-decoder decode path, which never reaches workflow + // features like `workflow_ssa` or `serving_service_contract`, so refusing + // to load over them rejects packages that would run correctly. Report them + // and continue. let runtime_caps = onnx_genai_metadata::RuntimeCapabilities::default(); - if let Err(errors) = onnx_genai_metadata::validate(&metadata, &runtime_caps) { - anyhow::bail!("Invalid inference metadata: {errors:?}"); + let report = + onnx_genai_metadata::validate_structure_and_capabilities(&metadata, &runtime_caps); + if !report.structural.is_empty() { + anyhow::bail!("Invalid inference metadata: {:?}", report.structural); + } + if !report.unsupported_capabilities.is_empty() { + tracing::info!( + "inference metadata declares capabilities this runtime does not implement: {}; \ + continuing because the decode path does not exercise them", + report.unsupported_capabilities.join(", ") + ); } let sliding_window = crate::decode::sliding_window_from_metadata(&metadata)?; diff --git a/crates/onnx-genai-metadata/src/lib.rs b/crates/onnx-genai-metadata/src/lib.rs index 4d0d91b712..87a3a860ef 100644 --- a/crates/onnx-genai-metadata/src/lib.rs +++ b/crates/onnx-genai-metadata/src/lib.rs @@ -54,8 +54,8 @@ pub use parser::{ }; pub use schema::*; pub use validation::{ - PipelineValidationError, RuntimeCapabilities, derived_capabilities, validate, - validate_metadata, validate_pipeline_spec, + CapabilityReport, PipelineValidationError, RuntimeCapabilities, derived_capabilities, + validate, validate_metadata, validate_pipeline_spec, validate_structure_and_capabilities, }; /// Generates the inference-metadata JSON Schema with deterministic object-key ordering. diff --git a/crates/onnx-genai-metadata/src/validation.rs b/crates/onnx-genai-metadata/src/validation.rs index 4ffd21b8f1..a6be2d9fef 100644 --- a/crates/onnx-genai-metadata/src/validation.rs +++ b/crates/onnx-genai-metadata/src/validation.rs @@ -84,22 +84,61 @@ impl Default for RuntimeCapabilities { } /// Validate the metadata document and required runtime capabilities. +/// +/// Reports structural defects and unsupported capabilities together, which is +/// what a strict caller wants. A caller that can *proceed* without a capability +/// — a bare decoder does not need `workflow_ssa` to be honoured — should use +/// [`validate_structure_and_capabilities`] and decide for itself, rather than +/// treating a capability it will never exercise as a malformed document. pub fn validate( metadata: &InferenceMetadata, runtime: &RuntimeCapabilities, ) -> Result<(), Vec> { - let mut errors = validate_metadata(metadata).err().unwrap_or_default(); + let report = validate_structure_and_capabilities(metadata, runtime); + let mut errors = report.structural; + errors.extend(report.unsupported_capabilities); + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } +} + +/// Structural defects and unsupported capabilities, kept apart. +/// +/// These answer different questions. A structural defect means the document +/// does not describe a runnable model and no caller can proceed. An unsupported +/// capability means the package asks for a runtime feature this build lacks, +/// which only matters if the caller would actually exercise it. Merging them +/// into one list forces every caller to treat both as fatal, which is why a +/// bare decoder used to be rejected for declaring workflow capabilities it +/// never reaches. +#[derive(Debug, Default, Clone)] +pub struct CapabilityReport { + /// The document is malformed or self-inconsistent. Always fatal. + pub structural: Vec, + /// Capabilities the package declares that this runtime does not implement. + pub unsupported_capabilities: Vec, +} + +/// Validate the document, reporting structural defects separately from +/// capabilities this runtime does not implement. +pub fn validate_structure_and_capabilities( + metadata: &InferenceMetadata, + runtime: &RuntimeCapabilities, +) -> CapabilityReport { + let structural = validate_metadata(metadata).err().unwrap_or_default(); let required = metadata .required_capabilities .iter() .cloned() .chain(derived_capabilities(metadata)); - errors.extend(required.filter(|capability| !runtime.supported.contains(capability))); - - if errors.is_empty() { - Ok(()) - } else { - Err(errors) + let unsupported_capabilities = required + .filter(|capability| !runtime.supported.contains(capability)) + .collect(); + CapabilityReport { + structural, + unsupported_capabilities, } } diff --git a/crates/onnx-genai-metadata/tests/metadata_fixtures.rs b/crates/onnx-genai-metadata/tests/metadata_fixtures.rs index f7cc61feeb..40802aa78c 100644 --- a/crates/onnx-genai-metadata/tests/metadata_fixtures.rs +++ b/crates/onnx-genai-metadata/tests/metadata_fixtures.rs @@ -1085,3 +1085,43 @@ pipeline: "{errors:?}" ); } + +/// A package may declare workflow capabilities a given runtime does not +/// implement. Those are a capability-negotiation question, not a malformed +/// document, and must be reported apart from structural defects so a caller +/// that never exercises them can still load the model. +/// +/// Regression guard for the bare decoder that was rejected with +/// `Invalid inference metadata: ["bounded_state_recurrence", "emit_valid_length", +/// "linear_effects", ...]` — eight capability names presented as validation +/// errors on a model that decoded correctly once they were dropped. +#[test] +fn unsupported_capabilities_are_reported_apart_from_structural_defects() { + use onnx_genai_metadata::{RuntimeCapabilities, validate_structure_and_capabilities}; + + let metadata: InferenceMetadata = serde_yaml::from_str( + r#" +required_capabilities: [kv_cache, grouped_query_attention, some_future_feature] +"#, + ) + .expect("parse metadata"); + + let report = + validate_structure_and_capabilities(&metadata, &RuntimeCapabilities::default()); + + assert!( + report.structural.is_empty(), + "a well-formed document must report no structural defects, got {:?}", + report.structural + ); + assert_eq!( + report.unsupported_capabilities, + vec!["some_future_feature".to_string()], + "only the capability this runtime lacks should be listed; kv_cache and \ + grouped_query_attention are supported" + ); + + // The strict entry point still folds both together, so callers that want + // all-or-nothing keep their behaviour. + assert!(onnx_genai_metadata::validate(&metadata, &RuntimeCapabilities::default()).is_err()); +}