Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5b0a046
start kernels card generation.
sayakpaul Jan 14, 2026
f0c87c2
up
sayakpaul Jan 14, 2026
8b0aa5d
address review feedback.
sayakpaul Jan 14, 2026
d5ee2e5
support backend logging.
sayakpaul Jan 14, 2026
c5deecd
black
sayakpaul Jan 14, 2026
00f970a
up
sayakpaul Jan 14, 2026
23adda1
make mypy happy.
sayakpaul Jan 14, 2026
15d1382
compat tomllib
sayakpaul Jan 14, 2026
6c2109e
implement force_update_content flag
sayakpaul Jan 16, 2026
667c14c
implement utility for license update
sayakpaul Jan 16, 2026
8988ab6
implement.
sayakpaul Jan 16, 2026
da5a7ac
format
sayakpaul Jan 16, 2026
68638fb
fix adding license.
sayakpaul Jan 19, 2026
081fdcf
fix kernel card backend
sayakpaul Jan 19, 2026
3ea7d88
black
sayakpaul Jan 19, 2026
ff155a5
up
sayakpaul Jan 19, 2026
f2cae73
resolve conflicts,.
sayakpaul Jan 22, 2026
4e4c327
make mypy ignore
sayakpaul Jan 22, 2026
8f1ca72
Merge branch 'main' into repocard-utils
sayakpaul Jan 24, 2026
15d26fe
Merge branch 'main' into repocard-utils
sayakpaul Jan 27, 2026
5e91faa
up.
sayakpaul Jan 27, 2026
32ddf29
include template in the build.
sayakpaul Jan 27, 2026
39a4f09
empty
sayakpaul Jan 27, 2026
7ba3db2
Merge branch 'main' into repocard-utils
sayakpaul Feb 3, 2026
b2bfb13
Merge branch 'main' into repocard-utils
sayakpaul Feb 9, 2026
81e0f93
address review feedback.
sayakpaul Feb 9, 2026
c7f8147
remove unneeded script.
sayakpaul Feb 9, 2026
f121e72
add a simple test suote.
sayakpaul Feb 9, 2026
589ed4c
revert readme delete.
sayakpaul Feb 12, 2026
1a0e3f5
resolve conflicts.
sayakpaul Feb 12, 2026
4c1612c
resolve conflicts.
sayakpaul Feb 14, 2026
6f35afc
Update kernels/src/kernels/card_template.md
sayakpaul Feb 16, 2026
6a5f0c7
Merge branch 'main' into repocard-utils
sayakpaul Feb 16, 2026
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
6 changes: 6 additions & 0 deletions docs/source/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ your kernel builds to the Hub. To know the supported arguments run: `kernels upl
being uploaded, it will attempt to delete the files existing under it.
- Make sure to be authenticated (run `hf auth login` if not) to be able to perform uploads to the Hub.

### kernels create-and-upload-card

Use `kernels create-and-upload-card <kernel_source_dir> --card-path README.md` to generate a basic homepage
for the kernel. Find an example [here](https://hf.co/kernels-community/kernel-card-template). You can
optionally push it to the Hub by specifying a `--repo-id`.

### kernels skills add

Use `kernels skills add` to install the skills for AI coding assistants like Claude, Codex, and OpenCode. For now, only the `cuda-kernels` skill is supported. Skill files are downloaded from the `huggingface/kernels` directory in this [repository](https://github.com/huggingface/kernels/tree/main/skills).
Expand Down
2 changes: 1 addition & 1 deletion kernels/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ kernels = "kernels.cli:main"
"kernels.lock" = "kernels.lockfile:write_egg_lockfile"

[tool.setuptools.package-data]
kernels = ["python_depends.json"]
kernels = ["python_depends.json", "card_template.md"]

[tool.isort]
profile = "black"
Expand Down
34 changes: 34 additions & 0 deletions kernels/src/kernels/card_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
{{ card_data }}
---

<!-- This model card has automatically been generated. You
should probably proofread and complete it, then remove this comment. -->

{{ model_description }}

## How to use

```python
# TODO: add an example code snippet for running this kernel
```

## Available functions

[TODO: add the functions available through this kernel]

## Supported backends

[TODO: add the backends this kernel supports]

## Benchmarks

[TODO: provide benchmarks if available]

## Source code

[TODO: provide original source code and other relevant citations if available]

## Notes

[TODO: provide additional notes about this kernel if needed]
69 changes: 69 additions & 0 deletions kernels/src/kernels/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@
from kernels.cli.skills import add_skill
from kernels.cli.versions import print_kernel_versions
from kernels.cli.doc import generate_readme_for_kernel
from kernels.kernel_card_utils import (
_load_or_create_kernel_card,
_update_benchmark,
_update_kernel_card_available_funcs,
_update_kernel_card_license,
_update_kernel_card_backends,
_update_kernel_card_usage,
)


def main():
Expand Down Expand Up @@ -239,6 +247,37 @@ def main():
)
init_parser.set_defaults(func=run_init)

repocard_parser = subparsers.add_parser(
"create-and-upload-card",
help="Create and optionally upload a kernel card.",
)
repocard_parser.add_argument(
"kernel_dir",
type=str,
help="Path to the kernels source.",
)
repocard_parser.add_argument(
"--card-path", type=str, required=True, help="Path to save the card to."
)
repocard_parser.add_argument(
"--description",
type=str,
default=None,
help="Description to introduce the kernel.",
)
repocard_parser.add_argument(
"--repo-id",
type=str,
default=None,
help="If specified it will be pushed to a repository on the Hub.",
)
repocard_parser.add_argument(
"--create-pr",
action="store_true",
help="If specified it will create a PR on the `repo_id`.",
)
repocard_parser.set_defaults(func=create_and_upload_card)

args = parser.parse_args()
args.func(args)

Expand Down Expand Up @@ -307,6 +346,36 @@ def upload_kernels(args):
)


def create_and_upload_card(args):
if not args.repo_id and args.create_pr:
raise ValueError("`create_pr` cannot be True when `repo_id` is None.")

kernel_dir = Path(args.kernel_dir).resolve()
kernel_card = _load_or_create_kernel_card(
kernel_description=args.description, license="apache-2.0"
)

updated_card = _update_kernel_card_usage(
kernel_card=kernel_card, local_path=kernel_dir
)
updated_card = _update_kernel_card_available_funcs(
kernel_card=kernel_card, local_path=kernel_dir
)
updated_card = _update_kernel_card_backends(
kernel_card=kernel_card, local_path=kernel_dir
)
updated_card = _update_benchmark(kernel_card=kernel_card, local_path=kernel_dir)
updated_card = _update_kernel_card_license(
kernel_card=kernel_card, local_path=kernel_dir
)

card_path = args.card_path
updated_card.save(card_path)

if args.repo_id:
updated_card.push_to_hub(repo_id=args.repo_id, create_pr=args.create_pr)


class _JSONEncoder(json.JSONEncoder):
def default(self, o):
if dataclasses.is_dataclass(o):
Expand Down
246 changes: 246 additions & 0 deletions kernels/src/kernels/kernel_card_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import ast
import re
from pathlib import Path

from .compat import tomllib
from typing import Any
from huggingface_hub import ModelCard, ModelCardData
from huggingface_hub.errors import EntryNotFoundError, RepositoryNotFoundError

KERNEL_CARD_TEMPLATE_PATH = Path(__file__).parent / "card_template.md"
DESCRIPTION = """
This is the repository card of {repo_id} that has been pushed on the Hub. It was built to be used with the [`kernels` library](https://github.com/huggingface/kernels). This card was automatically generated.
"""
EXAMPLE_CODE = """```python
# make sure `kernels` is installed: `pip install -U kernels`
from kernels import get_kernel

kernel_module = get_kernel("{repo_id}") # <- change the ID if needed
{func_name} = kernel_module.{func_name}

{func_name}(...)
```"""
LIBRARY_NAME = "kernels"

is_jinja_available = False
try:
import jinja2 # noqa

is_jinja_available = True
except ImportError:
pass


def _load_or_create_kernel_card(
repo_id_or_path: str = "REPO_ID",
token: str | None = None,
kernel_description: str | None = None,
license: str | None = None,
force_update_content: bool = False,
) -> ModelCard:
if not is_jinja_available:
raise ValueError(
"Modelcard rendering is based on Jinja templates."
" Please make sure to have `jinja` installed before using `load_or_create_model_card`."
" To install it, please run `pip install Jinja2`."
)

kernel_card = None

if not force_update_content:
try:
kernel_card = ModelCard.load(repo_id_or_path, token=token)
except (EntryNotFoundError, RepositoryNotFoundError):
pass # Will create from template below

if kernel_card is None:
kernel_description = kernel_description or DESCRIPTION
kernel_card = ModelCard.from_template(
card_data=ModelCardData(license=license, library_name=LIBRARY_NAME),
template_path=str(KERNEL_CARD_TEMPLATE_PATH),
model_description=kernel_description,
)

return kernel_card


def _parse_build_toml(local_path: str | Path) -> dict | None:
local_path = Path(local_path)
build_toml_path = local_path / "build.toml"

if not build_toml_path.exists():
return None

try:
with open(build_toml_path, "rb") as f:
return tomllib.load(f)
except Exception:
return None


def _find_torch_ext_init(local_path: str | Path) -> Path | None:
local_path = Path(local_path)

config = _parse_build_toml(local_path)
if not config:
return None

try:
kernel_name = config.get("general", {}).get("name")
if not kernel_name:
return None

module_name = kernel_name.replace("-", "_")
init_file = local_path / "torch-ext" / module_name / "__init__.py"

if init_file.exists():
return init_file

return None
except Exception:
return None


def _extract_functions_from_all(init_file_path: Path) -> list[str] | None:
try:
content = init_file_path.read_text()

tree = ast.parse(content)

for node in ast.walk(tree):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == "__all__":
if isinstance(node.value, ast.List):
functions = []
for elt in node.value.elts:
if isinstance(elt, ast.Constant):
func_name = str(elt.value)
functions.append(func_name)
return functions if functions else None
return None
except Exception:
return None
Comment on lines +122 to +123
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Can raise here instead as well.



def _update_kernel_card_usage(
kernel_card: ModelCard,
local_path: str | Path,
repo_id: str = "REPO_ID",
) -> ModelCard:
init_file = _find_torch_ext_init(local_path)

if not init_file:
return kernel_card

func_names = _extract_functions_from_all(init_file)

if not func_names:
return kernel_card

func_name = func_names[0]
example_code = EXAMPLE_CODE.format(repo_id=repo_id, func_name=func_name)

card_content = str(kernel_card.content)
pattern = r"(## How to use\s*\n\n)```python\n# TODO: add an example code snippet for running this kernel\n```"

if re.search(pattern, card_content):
updated_content = re.sub(pattern, r"\1" + example_code, card_content)
kernel_card.content = updated_content

return kernel_card


def _update_kernel_card_available_funcs(
kernel_card: ModelCard, local_path: str | Path
) -> ModelCard:
init_file = _find_torch_ext_init(local_path)

if not init_file:
return kernel_card

func_names = _extract_functions_from_all(init_file)

if not func_names:
return kernel_card

functions_list = "\n".join(f"- `{func}`" for func in func_names)

card_content = str(kernel_card.content)
pattern = r"(## Available functions\s*\n\n)\[TODO: add the functions available through this kernel\]"

if re.search(pattern, card_content):
updated_content = re.sub(pattern, r"\1" + functions_list, card_content)
kernel_card.content = updated_content

return kernel_card


def _update_kernel_card_backends(
kernel_card: ModelCard, local_path: str | Path
) -> ModelCard:
config = _parse_build_toml(local_path)
if not config:
return kernel_card

general_config = config.get("general", {})

card_content = str(kernel_card.content)

backends = general_config.get("backends")
if backends:
backends_list = "\n".join(f"- {backend}" for backend in backends)
pattern = r"(## Supported backends\s*\n\n)\[TODO: add the backends this kernel supports\]"
if re.search(pattern, card_content):
card_content = re.sub(pattern, r"\1" + backends_list, card_content)

# TODO: should we consider making it a separate utility?
kernel_configs = config.get("kernel", {})
cuda_capabilities = []
if kernel_configs:
for k in kernel_configs:
cuda_cap_for_config = kernel_configs[k].get("cuda-capabilities")
if cuda_cap_for_config:
cuda_capabilities.extend(cuda_cap_for_config)
cuda_capabilities: set[Any] = set(cuda_capabilities) # type: ignore[no-redef]
if cuda_capabilities:
cuda_list = "\n".join(f"- {cap}" for cap in cuda_capabilities)
cuda_section = f"## CUDA Capabilities\n\n{cuda_list}\n\n"
pattern = r"(## Benchmarks)"
if re.search(pattern, card_content):
card_content = re.sub(pattern, cuda_section + r"\1", card_content)

kernel_card.content = card_content
return kernel_card


def _update_kernel_card_license(
kernel_card: ModelCard, local_path: str | Path
) -> ModelCard:
config = _parse_build_toml(local_path)
if not config:
return kernel_card

existing_license = kernel_card.data.get("license", None)
license_from_config = config.get("general", {}).get("license", None)
final_license = license_from_config or existing_license
kernel_card.data["license"] = final_license
return kernel_card


def _update_benchmark(kernel_card: ModelCard, local_path: str | Path):
local_path = Path(local_path)

benchmark_file = local_path / "benchmarks" / "benchmark.py"
if not benchmark_file.exists():
return kernel_card

card_content = str(kernel_card.content)
benchmark_text = '\n\nBenchmarking script is available for this kernel. Make sure to run `kernels benchmark org-id/repo-id` (replace "org-id" and "repo-id" with actual values).'

pattern = r"(## Benchmarks)"
if re.search(pattern, card_content):
updated_content = re.sub(pattern, r"\1" + benchmark_text, card_content)
kernel_card.content = updated_content

return kernel_card
Loading
Loading