From dc9fc8b5ee5b5b97ae73b695e2cdc91875c7a603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20de=20Kok?= Date: Fri, 7 Aug 2026 13:47:11 +0000 Subject: [PATCH] Add `kernel-depends` option to `general` options in `build.toml` Also support writing the kernel dependencies in the kernel metadata. --- kernels-data/bindings/python/kernels_data.pyi | 46 ++++++ kernels-data/bindings/python/src/lib.rs | 86 +++++++++- .../bindings/python/stubtest-allowlist.txt | 1 + .../python/tests/test_kernels_data.py | 47 +++++- kernels-data/src/config/kernel_deps.rs | 153 ++++++++++++++++++ kernels-data/src/config/mod.rs | 24 +++ kernels-data/src/config/v3.rs | 4 + kernels-data/src/config/v4.rs | 4 + kernels-data/src/config/v5.rs | 15 +- kernels-data/src/metadata.rs | 20 ++- 10 files changed, 395 insertions(+), 5 deletions(-) create mode 100644 kernels-data/src/config/kernel_deps.rs diff --git a/kernels-data/bindings/python/kernels_data.pyi b/kernels-data/bindings/python/kernels_data.pyi index 43b7af6a6..1a23f240b 100644 --- a/kernels-data/bindings/python/kernels_data.pyi +++ b/kernels-data/bindings/python/kernels_data.pyi @@ -11,7 +11,9 @@ __all__ = [ "DigestAlgorithm", "GitHash", "KernelBuilderVersion", + "KernelDependency", "KernelName", + "KernelVersion", "Metadata", "Digest", "DigestViolation", @@ -280,6 +282,48 @@ class DigestValidationError(Exception): """The individual digest violations.""" ... +class KernelVersion: + """A kernel version: either a numeric version or a git revision string.""" + + @final + class Version(KernelVersion): + """A numeric kernel version.""" + + version: int + __match_args__ = ("version",) + def __new__(cls, version: int) -> "KernelVersion.Version": ... + + @final + class Revision(KernelVersion): + """A git revision (e.g. commit SHA or branch/tag name).""" + + revision: str + __match_args__ = ("revision",) + def __new__(cls, revision: str) -> "KernelVersion.Revision": ... + + def __repr__(self) -> str: ... + def __eq__(self, value: object, /) -> bool: ... + def __hash__(self) -> int: ... + +@final +class KernelDependency: + """A dependency on another kernel.""" + + def __new__(cls, repo_id: str, version: KernelVersion) -> "KernelDependency": ... + @property + def repo_id(self) -> str: + """Identifier of the kernel repository this dependency points to.""" + ... + + @property + def version(self) -> KernelVersion: + """Version specifier for the dependency.""" + ... + + def __repr__(self) -> str: ... + def __eq__(self, value: object, /) -> bool: ... + def __hash__(self) -> int: ... + @final class Metadata: """Parsed `metadata.json` for a kernel build variant.""" @@ -317,6 +361,8 @@ class Metadata: @property def python_depends(self) -> list[str]: ... @property + def kernel_depends(self) -> list[KernelDependency]: ... + @property def backend(self) -> BackendInfo: ... @property def digest(self) -> Optional[Digest]: ... diff --git a/kernels-data/bindings/python/src/lib.rs b/kernels-data/bindings/python/src/lib.rs index 3cfcae33e..67dc990be 100644 --- a/kernels-data/bindings/python/src/lib.rs +++ b/kernels-data/bindings/python/src/lib.rs @@ -4,7 +4,7 @@ use std::io::BufReader; use std::path::PathBuf; use std::str::FromStr; -use kernels_data::config::{Backend, KernelName}; +use kernels_data::config::{Backend, KernelDependency, KernelName, KernelVersion}; use kernels_data::digest::{Digest, DigestAlgorithm, DigestViolation}; use kernels_data::metadata::{BackendInfo, GitHash, KernelBuilderVersion, Metadata, Provenance}; use kernels_data::version::Version; @@ -312,6 +312,78 @@ impl PyProvenance { } } +/// A kernel version: either a numeric version or a git revision string. +#[pyclass(name = "KernelVersion", frozen, eq, hash)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +enum PyKernelVersion { + Version { version: usize }, + Revision { revision: String }, +} + +impl From for PyKernelVersion { + fn from(v: KernelVersion) -> Self { + match v { + KernelVersion::Version(n) => Self::Version { version: n }, + KernelVersion::Revision(s) => Self::Revision { revision: s }, + } + } +} + +#[pymethods] +impl PyKernelVersion { + fn __repr__(&self) -> String { + match self { + Self::Version { version } => format!("KernelVersion.Version(version={version})"), + Self::Revision { revision } => { + format!("KernelVersion.Revision(revision={revision:?})") + } + } + } +} + +/// A dependency on another kernel. +#[pyclass(name = "KernelDependency", frozen, eq, hash)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +struct PyKernelDependency { + repo_id: String, + version: PyKernelVersion, +} + +impl From for PyKernelDependency { + fn from(d: KernelDependency) -> Self { + Self { + repo_id: d.repo_id, + version: d.version.into(), + } + } +} + +#[pymethods] +impl PyKernelDependency { + #[new] + fn new(repo_id: String, version: PyKernelVersion) -> Self { + Self { repo_id, version } + } + + #[getter] + fn repo_id(&self) -> &str { + &self.repo_id + } + + #[getter] + fn version(&self) -> PyKernelVersion { + self.version.clone() + } + + fn __repr__(&self) -> String { + format!( + "KernelDependency(repo_id={:?}, version={})", + self.repo_id, + self.version.__repr__() + ) + } +} + /// Parsed `metadata.json` for a kernel build variant. #[pyclass(name = "Metadata", frozen)] #[derive(Clone, Debug)] @@ -323,6 +395,7 @@ struct PyMetadata { upstream: Option, source: Option, python_depends: Vec, + kernel_depends: Vec, backend: PyBackendInfo, digest: Option, provenance: Option, @@ -338,6 +411,7 @@ impl From for PyMetadata { upstream: m.upstream.map(|u| u.as_url().to_string()), source: m.source.map(|u| u.as_url().to_string()), python_depends: m.python_depends, + kernel_depends: m.kernel_depends.into_iter().map(Into::into).collect(), backend: m.backend.into(), digest: m.digest.map(Into::into), provenance: m.provenance.map(Into::into), @@ -409,6 +483,11 @@ impl PyMetadata { &self.python_depends } + #[getter] + fn kernel_depends(&self) -> Vec { + self.kernel_depends.clone() + } + #[getter] fn backend(&self) -> PyBackendInfo { self.backend.clone() @@ -426,7 +505,7 @@ impl PyMetadata { fn __repr__(&self) -> String { format!( - "Metadata(id={}, name={:?}, version={:?}, license={:?}, upstream={:?}, source={:?}, python_depends={:?}, backend={}, digest={}, provenance={})", + "Metadata(id={}, name={:?}, version={:?}, license={:?}, upstream={:?}, source={:?}, python_depends={:?}, kernel_depends={:?}, backend={}, digest={}, provenance={})", self.id, self.name, self.version, @@ -434,6 +513,7 @@ impl PyMetadata { self.upstream, self.source, self.python_depends, + self.kernel_depends, self.backend.__repr__(), self.digest .as_ref() @@ -668,6 +748,8 @@ fn kernels_data_py(m: &PyBound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/kernels-data/bindings/python/stubtest-allowlist.txt b/kernels-data/bindings/python/stubtest-allowlist.txt index 38be35603..0bd36ed3a 100644 --- a/kernels-data/bindings/python/stubtest-allowlist.txt +++ b/kernels-data/bindings/python/stubtest-allowlist.txt @@ -6,6 +6,7 @@ kernels_data\.kernels_data # `@disjoint_base` marker only lives in `typing_extensions`, which this # dependency-free extension package does not pull in, so the stub omits it. kernels_data\.DigestViolation +kernels_data\.KernelVersion # `DigestValidationError.violations` is attached as a per-instance attribute by # the `Digest.validate` binding (via `setattr`), so it is not visible on the diff --git a/kernels-data/bindings/python/tests/test_kernels_data.py b/kernels-data/bindings/python/tests/test_kernels_data.py index 87f9d1119..92f09c237 100644 --- a/kernels-data/bindings/python/tests/test_kernels_data.py +++ b/kernels-data/bindings/python/tests/test_kernels_data.py @@ -2,7 +2,14 @@ import pytest -from kernels_data import Backend, KernelName, Metadata, Version +from kernels_data import ( + Backend, + KernelDependency, + KernelName, + KernelVersion, + Metadata, + Version, +) def _write_metadata(path, **fields): @@ -200,3 +207,41 @@ def test_metadata_load(tmp_path): def test_metadata_load_missing_file(tmp_path): with pytest.raises(OSError): Metadata.read_from_file(tmp_path / "does-not-exist.json") + + +def test_kernel_version_eq_and_hash(): + assert KernelVersion.Version(1) == KernelVersion.Version(1) + assert KernelVersion.Revision("abc") == KernelVersion.Revision("abc") + assert KernelVersion.Version(1) != KernelVersion.Version(2) + assert KernelVersion.Version(1) != KernelVersion.Revision("1") + assert hash(KernelVersion.Version(1)) == hash(KernelVersion.Version(1)) + assert hash(KernelVersion.Revision("abc")) == hash(KernelVersion.Revision("abc")) + assert {KernelVersion.Version(1), KernelVersion.Version(1)} == { + KernelVersion.Version(1) + } + + +def test_kernel_dependency_eq_and_hash(): + a = KernelDependency(repo_id="foo/bar", version=KernelVersion.Version(1)) + b = KernelDependency(repo_id="foo/bar", version=KernelVersion.Version(1)) + c = KernelDependency(repo_id="foo/bar", version=KernelVersion.Revision("deadbeef")) + d = KernelDependency(repo_id="other/repo", version=KernelVersion.Version(1)) + + assert a == b + assert hash(a) == hash(b) + assert a != c + assert a != d + + # Distinct instances with equal content collapse in sets/dicts. + assert {a, b, c, d} == {a, c, d} + + cache: dict[KernelDependency, int] = {a: 1} + assert cache[b] == 1 + + +def test_kernel_dependency_is_immutable(): + dep = KernelDependency(repo_id="foo/bar", version=KernelVersion.Version(1)) + with pytest.raises(AttributeError): + dep.repo_id = "other/repo" + with pytest.raises(AttributeError): + dep.version = KernelVersion.Version(2) diff --git a/kernels-data/src/config/kernel_deps.rs b/kernels-data/src/config/kernel_deps.rs new file mode 100644 index 000000000..a00c72000 --- /dev/null +++ b/kernels-data/src/config/kernel_deps.rs @@ -0,0 +1,153 @@ +use serde::{Deserialize, Serialize, de}; + +/// Kernel version (numeric or Git revision). +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(untagged, rename_all = "kebab-case")] +pub enum KernelVersion { + Version(usize), + Revision(String), +} + +/// A kernel dependency. +#[derive(Clone, Debug)] +pub struct KernelDependency { + pub repo_id: String, + pub version: KernelVersion, +} + +// Note: enum flattening + denying unknown fields is recommended against: +// +// https://serde.rs/field-attrs.html#flatten +// +// So we roll a bit of extra serde code to support both +// +// { repo-id = "foo/bar", version = 1 } +// { repo-id = "foo/bar", revision = "somerevision" } +#[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +struct KernelDependencyRepr { + repo_id: String, + version: Option, + revision: Option, +} + +impl<'de> Deserialize<'de> for KernelDependency { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let repr = KernelDependencyRepr::deserialize(deserializer)?; + let version = match (repr.version, repr.revision) { + (Some(v), None) => KernelVersion::Version(v), + (None, Some(r)) => KernelVersion::Revision(r), + (Some(_), Some(_)) => { + return Err(de::Error::custom( + "`version` and `revision` are mutually exclusive", + )); + } + (None, None) => { + return Err(de::Error::custom( + "either `version` or `revision` must be specified", + )); + } + }; + Ok(KernelDependency { + repo_id: repr.repo_id, + version, + }) + } +} + +impl Serialize for KernelDependency { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let repr = match &self.version { + KernelVersion::Version(v) => KernelDependencyRepr { + repo_id: self.repo_id.clone(), + version: Some(*v), + revision: None, + }, + KernelVersion::Revision(r) => KernelDependencyRepr { + repo_id: self.repo_id.clone(), + version: None, + revision: Some(r.clone()), + }, + }; + repr.serialize(serializer) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn roundtrip_version() { + let dep = KernelDependency { + repo_id: "kernels-staging/einops".to_string(), + version: KernelVersion::Version(1), + }; + let serialized = toml::to_string(&dep).unwrap(); + assert!(!serialized.contains("revision")); + + let parsed: KernelDependency = toml::from_str(&serialized).unwrap(); + assert_eq!(parsed.repo_id, dep.repo_id); + assert!(matches!(parsed.version, KernelVersion::Version(1))); + } + + #[test] + fn roundtrip_revision() { + let dep = KernelDependency { + repo_id: "kernels-staging/einops".to_string(), + version: KernelVersion::Revision("34fa".to_string()), + }; + let serialized = toml::to_string(&dep).unwrap(); + assert!(!serialized.contains("version")); + + let parsed: KernelDependency = toml::from_str(&serialized).unwrap(); + assert_eq!(parsed.repo_id, dep.repo_id); + assert!(matches!( + parsed.version, + KernelVersion::Revision(ref r) if r == "34fa" + )); + } + + #[test] + fn version_string_is_rejected() { + let toml_str = r#" +repo-id = "x" +version = "notanint" +"#; + assert!(toml::from_str::(toml_str).is_err()); + } + + #[test] + fn both_version_and_revision_is_rejected() { + let toml_str = r#" +repo-id = "x" +version = 1 +revision = "abc" +"#; + assert!(toml::from_str::(toml_str).is_err()); + } + + #[test] + fn neither_version_nor_revision_is_rejected() { + let toml_str = r#" +repo-id = "x" +"#; + assert!(toml::from_str::(toml_str).is_err()); + } + + #[test] + fn unknown_field_is_rejected() { + let toml_str = r#" +repo-id = "x" +version = 1 +frobnicate = true +"#; + assert!(toml::from_str::(toml_str).is_err()); + } +} diff --git a/kernels-data/src/config/mod.rs b/kernels-data/src/config/mod.rs index 93ba480b0..861c79390 100644 --- a/kernels-data/src/config/mod.rs +++ b/kernels-data/src/config/mod.rs @@ -18,6 +18,9 @@ pub use compat::BuildCompat; mod git_url; pub use git_url::GitUrl; +mod kernel_deps; +pub use kernel_deps::{KernelDependency, KernelVersion}; + mod name; pub use name::KernelName; @@ -109,6 +112,7 @@ pub struct General { pub backends: Vec, pub hub: Option, + pub kernel_depends: Option>, pub python_depends: Option>, pub cuda: Option, @@ -171,6 +175,7 @@ impl General { })) } + /// Get the general + backend-specific Python dependencies for the given backend. pub fn all_python_depends(&self, backend: Backend) -> Result> { self.general_python_depends() .map(|deps| Ok(deps?.0.to_owned())) @@ -180,19 +185,36 @@ impl General { ) .collect::>>() } + + /// Get the general + backend-specific kernel dependencies for the given backend. + pub fn all_kernel_depends(&self, backend: Backend) -> Vec { + let general = self.kernel_depends.iter().flatten().cloned(); + let backend_depends = match backend { + Backend::Cuda => self.cuda.as_ref().and_then(|c| c.kernel_depends.as_ref()), + Backend::Neuron => self.neuron.as_ref().and_then(|n| n.kernel_depends.as_ref()), + Backend::Xpu => self.xpu.as_ref().and_then(|x| x.kernel_depends.as_ref()), + _ => None, + }; + general + .chain(backend_depends.into_iter().flatten().cloned()) + .collect() + } } pub struct CudaGeneral { pub minver: Option, pub maxver: Option, + pub kernel_depends: Option>, pub python_depends: Option>, } pub struct XpuGeneral { + pub kernel_depends: Option>, pub python_depends: Option>, } pub struct NeuronGeneral { + pub kernel_depends: Option>, pub python_depends: Option>, } @@ -472,10 +494,12 @@ mod tests { source: None, backends: vec![Backend::Tpu], hub: None, + kernel_depends: None, python_depends: None, cuda: Some(CudaGeneral { minver: None, maxver: None, + kernel_depends: None, python_depends: Some(vec!["nvidia-cutlass-dsl".to_string()]), }), neuron: None, diff --git a/kernels-data/src/config/v3.rs b/kernels-data/src/config/v3.rs index b9aa400a6..a77fe7bd1 100644 --- a/kernels-data/src/config/v3.rs +++ b/kernels-data/src/config/v3.rs @@ -204,6 +204,7 @@ impl TryFrom for super::General { backends: general.backends.into_iter().map(Into::into).collect(), cuda: general.cuda.map(Into::into), hub: general.hub.map(Into::into), + kernel_depends: None, neuron: general.neuron.map(Into::into), python_depends: general.python_depends, tpu: None, @@ -217,6 +218,7 @@ impl From for super::CudaGeneral { Self { minver: cuda.minver, maxver: cuda.maxver, + kernel_depends: None, python_depends: cuda.python_depends, } } @@ -225,6 +227,7 @@ impl From for super::CudaGeneral { impl From for super::NeuronGeneral { fn from(neuron: NeuronGeneral) -> Self { Self { + kernel_depends: None, python_depends: neuron.python_depends, } } @@ -233,6 +236,7 @@ impl From for super::NeuronGeneral { impl From for super::XpuGeneral { fn from(xpu: XpuGeneral) -> Self { Self { + kernel_depends: None, python_depends: xpu.python_depends, } } diff --git a/kernels-data/src/config/v4.rs b/kernels-data/src/config/v4.rs index 1624cf983..418e9add8 100644 --- a/kernels-data/src/config/v4.rs +++ b/kernels-data/src/config/v4.rs @@ -202,6 +202,7 @@ impl From for super::General { backends: general.backends.into_iter().map(Into::into).collect(), cuda: general.cuda.map(Into::into), hub: general.hub.map(Into::into), + kernel_depends: None, neuron: general.neuron.map(Into::into), python_depends: general.python_depends, tpu: None, @@ -227,6 +228,7 @@ impl From for super::CudaGeneral { Self { minver: cuda.minver, maxver: cuda.maxver, + kernel_depends: None, python_depends: cuda.python_depends, } } @@ -235,6 +237,7 @@ impl From for super::CudaGeneral { impl From for super::NeuronGeneral { fn from(neuron: NeuronGeneral) -> Self { Self { + kernel_depends: None, python_depends: neuron.python_depends, } } @@ -243,6 +246,7 @@ impl From for super::NeuronGeneral { impl From for super::XpuGeneral { fn from(xpu: XpuGeneral) -> Self { Self { + kernel_depends: None, python_depends: xpu.python_depends, } } diff --git a/kernels-data/src/config/v5.rs b/kernels-data/src/config/v5.rs index 3e8a99960..314289a85 100644 --- a/kernels-data/src/config/v5.rs +++ b/kernels-data/src/config/v5.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use monostate::MustBe; use serde::{Deserialize, Serialize}; -use super::{Dependency, GitUrl, KernelName}; +use super::{Dependency, GitUrl, KernelDependency, KernelName}; use crate::version::Version; // `monostate` validates the edition on read but provides no `Serialize` impl for it. @@ -58,6 +58,8 @@ pub struct General { pub hub: Option, + pub kernel_depends: Option>, + pub neuron: Option, pub python_depends: Option>, @@ -72,12 +74,14 @@ pub struct General { pub struct CudaGeneral { pub minver: Option, pub maxver: Option, + pub kernel_depends: Option>, pub python_depends: Option>, } #[derive(Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] pub struct NeuronGeneral { + pub kernel_depends: Option>, pub python_depends: Option>, } @@ -90,6 +94,7 @@ pub struct TpuGeneral { #[derive(Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] pub struct XpuGeneral { + pub kernel_depends: Option>, pub python_depends: Option>, } @@ -224,6 +229,7 @@ impl From for super::General { backends: general.backends.into_iter().map(Into::into).collect(), cuda: general.cuda.map(Into::into), hub: general.hub.map(Into::into), + kernel_depends: general.kernel_depends, neuron: general.neuron.map(Into::into), python_depends: general.python_depends, tpu: general.tpu.map(Into::into), @@ -249,6 +255,7 @@ impl From for super::CudaGeneral { Self { minver: cuda.minver, maxver: cuda.maxver, + kernel_depends: cuda.kernel_depends, python_depends: cuda.python_depends, } } @@ -257,6 +264,7 @@ impl From for super::CudaGeneral { impl From for super::NeuronGeneral { fn from(neuron: NeuronGeneral) -> Self { Self { + kernel_depends: neuron.kernel_depends, python_depends: neuron.python_depends, } } @@ -273,6 +281,7 @@ impl From for super::TpuGeneral { impl From for super::XpuGeneral { fn from(xpu: XpuGeneral) -> Self { Self { + kernel_depends: xpu.kernel_depends, python_depends: xpu.python_depends, } } @@ -437,6 +446,7 @@ impl From for General { backends: general.backends.into_iter().map(Into::into).collect(), cuda: general.cuda.map(Into::into), hub: general.hub.map(Into::into), + kernel_depends: general.kernel_depends, neuron: general.neuron.map(Into::into), python_depends: general.python_depends, tpu: general.tpu.map(Into::into), @@ -462,6 +472,7 @@ impl From for CudaGeneral { Self { minver: cuda.minver, maxver: cuda.maxver, + kernel_depends: cuda.kernel_depends, python_depends: cuda.python_depends, } } @@ -470,6 +481,7 @@ impl From for CudaGeneral { impl From for NeuronGeneral { fn from(neuron: super::NeuronGeneral) -> Self { Self { + kernel_depends: neuron.kernel_depends, python_depends: neuron.python_depends, } } @@ -486,6 +498,7 @@ impl From for TpuGeneral { impl From for XpuGeneral { fn from(xpu: super::XpuGeneral) -> Self { Self { + kernel_depends: xpu.kernel_depends, python_depends: xpu.python_depends, } } diff --git a/kernels-data/src/metadata.rs b/kernels-data/src/metadata.rs index 3da04dc46..51fa93b99 100644 --- a/kernels-data/src/metadata.rs +++ b/kernels-data/src/metadata.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use eyre::Result; use serde::{Deserialize, Serialize}; -use crate::config::{Backend, Build, GitUrl, KernelName}; +use crate::config::{Backend, Build, GitUrl, KernelDependency, KernelName}; use crate::digest::Digest; #[derive(Debug, Deserialize, Serialize)] @@ -67,6 +67,8 @@ pub struct Metadata { #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, pub python_depends: Vec, + #[serde(default)] + pub kernel_depends: Vec, pub backend: BackendInfo, #[serde(skip_serializing_if = "Option::is_none")] pub digest: Option, @@ -84,6 +86,7 @@ impl Metadata { /// the archs need to be computed at build time for arch frameworks. pub fn for_backend(build: &Build, id: String, backend: Backend) -> Result { let python_depends = build.general.all_python_depends(backend)?; + let kernel_depends = build.general.all_kernel_depends(backend); let archs = build.framework.precomputable_backend_archs(backend); Ok(Self { @@ -94,6 +97,7 @@ impl Metadata { upstream: build.general.upstream.clone(), source: build.general.source.clone(), python_depends, + kernel_depends, backend: BackendInfo { archs, backend_type: backend, @@ -156,6 +160,7 @@ mod tests { source: None, backends: vec![Backend::Cuda, Backend::Rocm, Backend::Cpu], hub: None, + kernel_depends: None, python_depends: None, cuda: None, neuron: None, @@ -212,6 +217,7 @@ mod tests { source: None, backends: vec![Backend::Cuda], hub: None, + kernel_depends: None, python_depends: None, cuda: None, neuron: None, @@ -252,6 +258,18 @@ mod tests { assert!(parsed.provenance.is_none()); } + #[test] + fn metadata_always_emits_kernel_depends() { + let build = torch_noarch_build(); + let metadata = Metadata::for_backend(&build, "test-id".to_string(), Backend::Cuda).unwrap(); + + let json = serde_json::to_string(&metadata).unwrap(); + assert!(json.contains("\"kernel-depends\":[]")); + + let parsed: Metadata = serde_json::from_str(&json).unwrap(); + assert!(parsed.kernel_depends.is_empty()); + } + #[test] fn metadata_round_trips_provenance_in_kebab_case() { let build = torch_noarch_build();