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
5 changes: 5 additions & 0 deletions docs/source/kernel-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ metadata. Currently the following top-level keys are supported:
- `name` (`str`, required): then name of the kernel. Replacing dashes
by underscores should result in the module name of the kernel.
- `version` (`int`, required): the kernel version number.
- `kernels-minver` (`str`, optional): the minimum version of the `kernels`
Python library required to load the kernel (e.g. `"0.17.0"`). This key is
determined by the kernel builder from the features that the kernel uses,
it is not set by kernel authors. The `kernels` library warns when it loads
a kernel that requires a newer version than the one that is installed.
- `license` (`str`, required): the kernel license in. Refer to the
list of [supported license identifiers](https://huggingface.co/docs/hub/repositories-licenses).
- `upstream` (`str`, optional): Git-compatible URL (passable to `git clone`)
Expand Down
9 changes: 8 additions & 1 deletion kernels-data/bindings/python/kernels_data.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ class Provenance:
class Version:
"""A dotted numeric version (e.g. `12.8.0`).

Trailing zeros are stripped during normalization.
Versions that only differ in trailing zeros compare as equal.
"""

@staticmethod
Expand Down Expand Up @@ -659,6 +659,13 @@ class Metadata:
@property
def version(self) -> Optional[int]: ...
@property
def kernels_minver(self) -> Optional[Version]:
"""Minimum `kernels` library version required to load this variant.

`None` for metadata written before this key existed.
"""
...
@property
def license(self) -> Optional[str]: ...
@property
def upstream(self) -> Optional[str]: ...
Expand Down
10 changes: 9 additions & 1 deletion kernels-data/bindings/python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ struct PyMetadata {
id: String,
name: PyKernelName,
version: usize,
kernels_minver: Option<PyVersion>,
license: String,
upstream: Option<String>,
source: Option<String>,
Expand All @@ -409,6 +410,7 @@ impl From<Metadata> for PyMetadata {
id: m.id,
name: PyKernelName { inner: m.name },
version: m.version,
kernels_minver: m.kernels_minver.map(Into::into),
license: m.license,
upstream: m.upstream.map(|u| u.as_url().to_string()),
source: m.source.map(|u| u.as_url().to_string()),
Expand Down Expand Up @@ -465,6 +467,11 @@ impl PyMetadata {
self.version
}

#[getter]
fn kernels_minver(&self) -> Option<PyVersion> {
self.kernels_minver.clone()
}

#[getter]
fn license(&self) -> &str {
&self.license
Expand Down Expand Up @@ -507,10 +514,11 @@ impl PyMetadata {

fn __repr__(&self) -> String {
format!(
"Metadata(id={}, name={:?}, version={:?}, license={:?}, upstream={:?}, source={:?}, python_depends={:?}, kernel_depends={:?}, backend={}, digest={}, provenance={})",
"Metadata(id={}, name={:?}, version={:?}, kernels_minver={:?}, license={:?}, upstream={:?}, source={:?}, python_depends={:?}, kernel_depends={:?}, backend={}, digest={}, provenance={})",
self.id,
self.name,
self.version,
self.kernels_minver,
self.license,
self.upstream,
self.source,
Expand Down
65 changes: 58 additions & 7 deletions kernels-data/bindings/python/src/version.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,70 @@
use std::cmp::Ordering;
use std::hash::{Hash, Hasher};

use kernels_data::version::Version;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;

/// A dotted numeric version (e.g. `12.8.0`). Trailing zeros are stripped
/// during normalization.
/// A dotted numeric version (e.g. `12.8.0`). Versions that only differ in
/// trailing zeros compare as equal.
#[pyclass(name = "Version", frozen, eq, ord, hash)]
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[derive(Clone, Debug)]
pub(crate) struct PyVersion {
// We are storing this as a `Box<[usize]>` to allow storing kernels-data
// `Version` in directly in `PyVersion` as well.
inner: Box<[usize]>,
}

impl<const N: usize> From<Version<N>> for PyVersion {
fn from(version: Version<N>) -> Self {
Self {
inner: Box::from(&*version),
}
}
}

// A parsed version string does not carry `Version<N>`'s guarantee that both
// sides of a comparison have the same number of components, so compare
// component-wise with missing components counting as zero (`0.17` and
// `0.17.0` are the same version).
impl Ord for PyVersion {
fn cmp(&self, other: &Self) -> Ordering {
let n = self.inner.len().max(other.inner.len());
for i in 0..n {
let lhs = self.inner.get(i).copied().unwrap_or(0);
let rhs = other.inner.get(i).copied().unwrap_or(0);
match lhs.cmp(&rhs) {
Ordering::Equal => (),
ordering => return ordering,
}
}
Ordering::Equal
}
}

impl PartialOrd for PyVersion {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}

impl PartialEq for PyVersion {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}

impl Eq for PyVersion {}

impl Hash for PyVersion {
fn hash<H: Hasher>(&self, state: &mut H) {
// Skip trailing zeros so that versions that compare as equal hash
// identically.
let trailing_zeros = self.inner.iter().rev().take_while(|&&x| x == 0).count();
self.inner[..self.inner.len() - trailing_zeros].hash(state);
}
Comment on lines +19 to +23

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Directly put the Version<N> in the box. We keep all the guarantees of it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, From<Version<N>> now boxes the components directly through the [usize] deref, no copy into a Vec first

}

#[pymethods]
impl PyVersion {
/// Parse a version string of the form `X`, `X.Y`, `X.Y.Z`, ...
Expand All @@ -32,10 +86,7 @@ impl PyVersion {
})?;
parts.push(component);
}
// Remove trailing zeros for normalization.
let trailing_zeros = parts.iter().rev().take_while(|&&x| x == 0).count();
parts.truncate(parts.len() - trailing_zeros);
Ok(Self {
Ok(PyVersion {
inner: parts.into_boxed_slice(),
})
}
Expand Down
4 changes: 4 additions & 0 deletions kernels-data/bindings/python/tests/test_kernels_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
KernelName,
KernelVersion,
Metadata,
Version,
)


Expand Down Expand Up @@ -80,6 +81,7 @@ def test_metadata_load_full(tmp_path):
"version": 1,
"name": "my-kernel",
"license": "Apache-2.0",
"kernels-minver": "0.17.0",
"upstream": "https://github.com/example/kernel",
"source": "https://github.com/example/kernel-builder",
"python-depends": ["torch"],
Expand All @@ -91,6 +93,7 @@ def test_metadata_load_full(tmp_path):
assert m.id == "_my_kernel_8a3be8f"
assert m.name == KernelName("my-kernel")
assert m.version == 1
assert m.kernels_minver == Version.from_str("0.17.0")
assert m.license == "Apache-2.0"
assert m.upstream == "https://github.com/example/kernel"
assert m.source == "https://github.com/example/kernel-builder"
Expand All @@ -115,6 +118,7 @@ def test_metadata_load_minimal(tmp_path):
)
m = Metadata.read_from_file(path)
assert m.version == 1
assert m.kernels_minver is None
assert m.license == "Apache-2.0"
assert m.upstream is None
assert m.source is None
Expand Down
10 changes: 8 additions & 2 deletions kernels-data/bindings/python/tests/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from kernels_data import Version


def test_version_parse_and_normalize():
assert str(Version.from_str("12.8.0")) == "12.8"
def test_version_parse():
assert str(Version.from_str("12.8.0")) == "12.8.0"
assert str(Version.from_str("1")) == "1"
assert str(Version.from_str("1.2.3")) == "1.2.3"

Expand All @@ -19,6 +19,12 @@ def test_version_ordering_and_hash():
assert {v1, v2, v3} == {v1, v3}


def test_version_trailing_zeros_compare_equal():
assert Version.from_str("0.17") == Version.from_str("0.17.0")
assert Version.from_str("0.17") < Version.from_str("0.17.1")
assert Version.from_str("0.17.0.1") > Version.from_str("0.17")


def test_version_invalid():
with pytest.raises(ValueError):
Version.from_str("abc")
Expand Down
46 changes: 46 additions & 0 deletions kernels-data/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ use crate::version::Version;
pub type CurrentConfig = v5::Build;
pub const CURRENT_EDITION: usize = 5;

/// Baseline `kernels` version that can load kernels built with the current
/// metadata format.
const KERNELS_VERSION_BASELINE: Version<3> = Version::new([0, 14, 0]);

/// First `kernels` version that can resolve kernel dependencies.
const KERNELS_VERSION_KERNEL_DEPENDS: Version<3> = Version::new([0, 17, 0]);

pub struct Build {
pub general: General,
pub kernels: HashMap<String, Kernel>,
Expand All @@ -60,6 +67,24 @@ impl Build {
pub fn repo_id(&self) -> Option<&str> {
self.general.hub.as_ref().and_then(|h| h.repo_id.as_deref())
}

/// Minimum version of the `kernels` Python library required to load a
/// build variant for `backend`.
///
/// The version is derived from the features that the kernel uses, rather
/// than being specified by the kernel author, who would have to keep track
/// of which `kernels` version introduced support for which feature.
pub fn required_kernels_version(&self, backend: Backend) -> Version<3> {
let mut required = KERNELS_VERSION_BASELINE;

// Kernel dependencies are only fetched by newer versions of `kernels`,
// older versions would fail to import the kernel.
if !self.general.all_kernel_depends(backend).is_empty() {
required = required.max(KERNELS_VERSION_KERNEL_DEPENDS);
}

required
}
}

pub enum Framework {
Expand Down Expand Up @@ -545,4 +570,25 @@ mod tests {
.is_none()
);
}

/// The minimum `kernels` version is derived from the features that a
/// kernel uses, so kernel authors must not be able to set it themselves.
#[test]
fn general_minver_is_rejected() {
let toml = r#"
[general]
name = "test-kernel"
version = 1
edition = 5
license = "apache-2.0"
backends = ["cuda"]
minver = "0.11.0"

[torch]
src = []
"#;

let err = toml::from_str::<v5::Build>(toml).unwrap_err().to_string();
assert!(err.contains("unknown field `minver`"), "{err}");
}
}
49 changes: 48 additions & 1 deletion kernels-data/src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use serde::{Deserialize, Deserializer, Serialize};
use crate::config::{Backend, Build, GitUrl, KernelDependency, KernelName};
use crate::digest::Digest;
use crate::git::{GitStatus, Oid};
use crate::version::Version;

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
Expand Down Expand Up @@ -93,6 +94,10 @@ pub struct Metadata {
pub name: KernelName,
pub id: String,
pub version: usize,
/// Minimum version of the `kernels` Python library required to load this
/// build variant. Absent in metadata written before this key existed.
#[serde(skip_serializing_if = "Option::is_none")]
pub kernels_minver: Option<Version<3>>,
pub license: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub upstream: Option<GitUrl>,
Expand Down Expand Up @@ -125,6 +130,7 @@ impl Metadata {
id,
name: build.general.name.clone(),
version: build.general.version,
kernels_minver: Some(build.required_kernels_version(backend)),
license: build.general.license.clone(),
upstream: build.general.upstream.clone(),
source: build.general.source.clone(),
Expand Down Expand Up @@ -163,8 +169,12 @@ mod tests {
use std::collections::HashMap;
use std::str::FromStr;

use crate::config::{Backend, Build, Framework, General, KernelName, TorchNoarch, TvmFfi};
use crate::config::{
Backend, Build, Framework, General, KernelDependency, KernelName, KernelVersion,
TorchNoarch, TvmFfi,
};
use crate::git::{GitStatus, Oid};
use crate::version::Version;

use super::{KernelBuilderVersion, Metadata, Provenance};

Expand Down Expand Up @@ -303,6 +313,43 @@ mod tests {
assert!(parsed.provenance.is_none());
}

#[test]
fn metadata_kernels_minver_defaults_to_baseline() {
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(r#""kernels-minver":"0.14.0""#));

let parsed: Metadata = serde_json::from_str(&json).unwrap();
assert_eq!(
parsed.kernels_minver,
Some(Version::from_str("0.14.0").unwrap())
);
}

#[test]
fn metadata_kernels_minver_bumped_for_kernel_depends() {
let mut build = torch_noarch_build();
build.general.kernel_depends = Some(vec![KernelDependency {
repo_id: "kernels-community/activation".to_string(),
version: KernelVersion::Version(1),
}]);
let metadata = Metadata::for_backend(&build, "test-id".to_string(), Backend::Cuda).unwrap();

assert_eq!(
metadata.kernels_minver,
Some(Version::from_str("0.17.0").unwrap())
);
}

#[test]
fn metadata_without_kernels_minver_parses() {
let json = r#"{"name":"test-kernel","id":"test-id","version":1,"license":"apache-2.0","python-depends":[],"backend":{"type":"cuda"}}"#;
let parsed: Metadata = serde_json::from_str(json).unwrap();
assert!(parsed.kernels_minver.is_none());
}

#[test]
fn metadata_always_emits_kernel_depends() {
let build = torch_noarch_build();
Expand Down
7 changes: 7 additions & 0 deletions kernels-data/src/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Version<const N: usize>([usize; N]);

impl<const N: usize> Version<N> {
/// Construct a version from its components.
pub const fn new(parts: [usize; N]) -> Self {
Version(parts)
}
}

impl<'de, const N: usize> Deserialize<'de> for Version<N> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
Expand Down
2 changes: 2 additions & 0 deletions kernels/src/kernels/cli/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def _metadata_info(metadata: Metadata | None, variants: list[Variant]) -> dict:
{
"name": str(metadata.name),
"version": metadata.version,
"kernels_minver": (str(metadata.kernels_minver) if metadata.kernels_minver is not None else None),
"license": metadata.license,
"upstream": metadata.upstream,
"source": metadata.source,
Expand All @@ -144,6 +145,7 @@ def value(v) -> str:

print(f"Name: {value(info.get('name'))}")
print(f"Version: {value(info.get('version'))}")
print(f"Minimum kernels version: {value(info.get('kernels_minver'))}")
print(f"License: {value(info.get('license'))}")
print(f"Upstream: {value(info.get('upstream'))}")
print(f"Source: {value(info.get('source'))}")
Expand Down
Loading
Loading