Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions crates/onnx-genai-engine/src/engine/load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1156,10 +1165,24 @@ fn resolve_metadata_and_decode_path(
shared_kv: crate::decode::SharedKvOffer,
capture_requested: bool,
) -> anyhow::Result<MetadataResolution> {
// 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)?;
Expand Down
4 changes: 2 additions & 2 deletions crates/onnx-genai-metadata/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
53 changes: 46 additions & 7 deletions crates/onnx-genai-metadata/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>> {
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<String>,
/// Capabilities the package declares that this runtime does not implement.
pub unsupported_capabilities: Vec<String>,
}

/// 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,
}
}

Expand Down
40 changes: 40 additions & 0 deletions crates/onnx-genai-metadata/tests/metadata_fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Loading