Skip to content

Commit 7dbc786

Browse files
sayakpaulMekkCyber
andauthored
add utilities to generate template repo cards (#210)
* start kernels card generation. * up * address review feedback. * support backend logging. * black * up * make mypy happy. * compat tomllib * implement force_update_content flag * implement utility for license update * implement. * format * fix adding license. * fix kernel card backend * black * up * make mypy ignore * up. * include template in the build. * empty * address review feedback. * remove unneeded script. * add a simple test suote. * revert readme delete. * Update kernels/src/kernels/card_template.md Co-authored-by: Mohamed Mekkouri <93391238+MekkCyber@users.noreply.github.com> --------- Co-authored-by: Mohamed Mekkouri <93391238+MekkCyber@users.noreply.github.com>
1 parent 7676263 commit 7dbc786

6 files changed

Lines changed: 631 additions & 1 deletion

File tree

docs/source/cli.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ your kernel builds to the Hub. To know the supported arguments run: `kernels upl
3232
being uploaded, it will attempt to delete the files existing under it.
3333
- Make sure to be authenticated (run `hf auth login` if not) to be able to perform uploads to the Hub.
3434

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

3743
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).

kernels/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ kernels = "kernels.cli:main"
5353
"kernels.lock" = "kernels.lockfile:write_egg_lockfile"
5454

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

5858
[tool.isort]
5959
profile = "black"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
{{ card_data }}
3+
---
4+
5+
<!-- This model card has automatically been generated. You
6+
should probably proofread and complete it, then remove this comment. -->
7+
8+
{{ model_description }}
9+
10+
## How to use
11+
12+
```python
13+
# TODO: add an example code snippet for running this kernel
14+
```
15+
16+
## Available functions
17+
18+
[TODO: add the functions available through this kernel]
19+
20+
## Supported backends
21+
22+
[TODO: add the backends this kernel supports]
23+
24+
## Benchmarks
25+
26+
[TODO: provide benchmarks if available]
27+
28+
## Source code
29+
30+
[TODO: provide original source code and other relevant citations if available]
31+
32+
## Notes
33+
34+
[TODO: provide additional notes about this kernel if needed]

kernels/src/kernels/cli/__init__.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@
1616
from kernels.cli.skills import add_skill
1717
from kernels.cli.versions import print_kernel_versions
1818
from kernels.cli.doc import generate_readme_for_kernel
19+
from kernels.kernel_card_utils import (
20+
_load_or_create_kernel_card,
21+
_update_benchmark,
22+
_update_kernel_card_available_funcs,
23+
_update_kernel_card_license,
24+
_update_kernel_card_backends,
25+
_update_kernel_card_usage,
26+
)
1927

2028

2129
def main():
@@ -239,6 +247,37 @@ def main():
239247
)
240248
init_parser.set_defaults(func=run_init)
241249

250+
repocard_parser = subparsers.add_parser(
251+
"create-and-upload-card",
252+
help="Create and optionally upload a kernel card.",
253+
)
254+
repocard_parser.add_argument(
255+
"kernel_dir",
256+
type=str,
257+
help="Path to the kernels source.",
258+
)
259+
repocard_parser.add_argument(
260+
"--card-path", type=str, required=True, help="Path to save the card to."
261+
)
262+
repocard_parser.add_argument(
263+
"--description",
264+
type=str,
265+
default=None,
266+
help="Description to introduce the kernel.",
267+
)
268+
repocard_parser.add_argument(
269+
"--repo-id",
270+
type=str,
271+
default=None,
272+
help="If specified it will be pushed to a repository on the Hub.",
273+
)
274+
repocard_parser.add_argument(
275+
"--create-pr",
276+
action="store_true",
277+
help="If specified it will create a PR on the `repo_id`.",
278+
)
279+
repocard_parser.set_defaults(func=create_and_upload_card)
280+
242281
args = parser.parse_args()
243282
args.func(args)
244283

@@ -307,6 +346,36 @@ def upload_kernels(args):
307346
)
308347

309348

349+
def create_and_upload_card(args):
350+
if not args.repo_id and args.create_pr:
351+
raise ValueError("`create_pr` cannot be True when `repo_id` is None.")
352+
353+
kernel_dir = Path(args.kernel_dir).resolve()
354+
kernel_card = _load_or_create_kernel_card(
355+
kernel_description=args.description, license="apache-2.0"
356+
)
357+
358+
updated_card = _update_kernel_card_usage(
359+
kernel_card=kernel_card, local_path=kernel_dir
360+
)
361+
updated_card = _update_kernel_card_available_funcs(
362+
kernel_card=kernel_card, local_path=kernel_dir
363+
)
364+
updated_card = _update_kernel_card_backends(
365+
kernel_card=kernel_card, local_path=kernel_dir
366+
)
367+
updated_card = _update_benchmark(kernel_card=kernel_card, local_path=kernel_dir)
368+
updated_card = _update_kernel_card_license(
369+
kernel_card=kernel_card, local_path=kernel_dir
370+
)
371+
372+
card_path = args.card_path
373+
updated_card.save(card_path)
374+
375+
if args.repo_id:
376+
updated_card.push_to_hub(repo_id=args.repo_id, create_pr=args.create_pr)
377+
378+
310379
class _JSONEncoder(json.JSONEncoder):
311380
def default(self, o):
312381
if dataclasses.is_dataclass(o):
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import ast
2+
import re
3+
from pathlib import Path
4+
5+
from .compat import tomllib
6+
from typing import Any
7+
from huggingface_hub import ModelCard, ModelCardData
8+
from huggingface_hub.errors import EntryNotFoundError, RepositoryNotFoundError
9+
10+
KERNEL_CARD_TEMPLATE_PATH = Path(__file__).parent / "card_template.md"
11+
DESCRIPTION = """
12+
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.
13+
"""
14+
EXAMPLE_CODE = """```python
15+
# make sure `kernels` is installed: `pip install -U kernels`
16+
from kernels import get_kernel
17+
18+
kernel_module = get_kernel("{repo_id}") # <- change the ID if needed
19+
{func_name} = kernel_module.{func_name}
20+
21+
{func_name}(...)
22+
```"""
23+
LIBRARY_NAME = "kernels"
24+
25+
is_jinja_available = False
26+
try:
27+
import jinja2 # noqa
28+
29+
is_jinja_available = True
30+
except ImportError:
31+
pass
32+
33+
34+
def _load_or_create_kernel_card(
35+
repo_id_or_path: str = "REPO_ID",
36+
token: str | None = None,
37+
kernel_description: str | None = None,
38+
license: str | None = None,
39+
force_update_content: bool = False,
40+
) -> ModelCard:
41+
if not is_jinja_available:
42+
raise ValueError(
43+
"Modelcard rendering is based on Jinja templates."
44+
" Please make sure to have `jinja` installed before using `load_or_create_model_card`."
45+
" To install it, please run `pip install Jinja2`."
46+
)
47+
48+
kernel_card = None
49+
50+
if not force_update_content:
51+
try:
52+
kernel_card = ModelCard.load(repo_id_or_path, token=token)
53+
except (EntryNotFoundError, RepositoryNotFoundError):
54+
pass # Will create from template below
55+
56+
if kernel_card is None:
57+
kernel_description = kernel_description or DESCRIPTION
58+
kernel_card = ModelCard.from_template(
59+
card_data=ModelCardData(license=license, library_name=LIBRARY_NAME),
60+
template_path=str(KERNEL_CARD_TEMPLATE_PATH),
61+
model_description=kernel_description,
62+
)
63+
64+
return kernel_card
65+
66+
67+
def _parse_build_toml(local_path: str | Path) -> dict | None:
68+
local_path = Path(local_path)
69+
build_toml_path = local_path / "build.toml"
70+
71+
if not build_toml_path.exists():
72+
return None
73+
74+
try:
75+
with open(build_toml_path, "rb") as f:
76+
return tomllib.load(f)
77+
except Exception:
78+
return None
79+
80+
81+
def _find_torch_ext_init(local_path: str | Path) -> Path | None:
82+
local_path = Path(local_path)
83+
84+
config = _parse_build_toml(local_path)
85+
if not config:
86+
return None
87+
88+
try:
89+
kernel_name = config.get("general", {}).get("name")
90+
if not kernel_name:
91+
return None
92+
93+
module_name = kernel_name.replace("-", "_")
94+
init_file = local_path / "torch-ext" / module_name / "__init__.py"
95+
96+
if init_file.exists():
97+
return init_file
98+
99+
return None
100+
except Exception:
101+
return None
102+
103+
104+
def _extract_functions_from_all(init_file_path: Path) -> list[str] | None:
105+
try:
106+
content = init_file_path.read_text()
107+
108+
tree = ast.parse(content)
109+
110+
for node in ast.walk(tree):
111+
if isinstance(node, ast.Assign):
112+
for target in node.targets:
113+
if isinstance(target, ast.Name) and target.id == "__all__":
114+
if isinstance(node.value, ast.List):
115+
functions = []
116+
for elt in node.value.elts:
117+
if isinstance(elt, ast.Constant):
118+
func_name = str(elt.value)
119+
functions.append(func_name)
120+
return functions if functions else None
121+
return None
122+
except Exception:
123+
return None
124+
125+
126+
def _update_kernel_card_usage(
127+
kernel_card: ModelCard,
128+
local_path: str | Path,
129+
repo_id: str = "REPO_ID",
130+
) -> ModelCard:
131+
init_file = _find_torch_ext_init(local_path)
132+
133+
if not init_file:
134+
return kernel_card
135+
136+
func_names = _extract_functions_from_all(init_file)
137+
138+
if not func_names:
139+
return kernel_card
140+
141+
func_name = func_names[0]
142+
example_code = EXAMPLE_CODE.format(repo_id=repo_id, func_name=func_name)
143+
144+
card_content = str(kernel_card.content)
145+
pattern = r"(## How to use\s*\n\n)```python\n# TODO: add an example code snippet for running this kernel\n```"
146+
147+
if re.search(pattern, card_content):
148+
updated_content = re.sub(pattern, r"\1" + example_code, card_content)
149+
kernel_card.content = updated_content
150+
151+
return kernel_card
152+
153+
154+
def _update_kernel_card_available_funcs(
155+
kernel_card: ModelCard, local_path: str | Path
156+
) -> ModelCard:
157+
init_file = _find_torch_ext_init(local_path)
158+
159+
if not init_file:
160+
return kernel_card
161+
162+
func_names = _extract_functions_from_all(init_file)
163+
164+
if not func_names:
165+
return kernel_card
166+
167+
functions_list = "\n".join(f"- `{func}`" for func in func_names)
168+
169+
card_content = str(kernel_card.content)
170+
pattern = r"(## Available functions\s*\n\n)\[TODO: add the functions available through this kernel\]"
171+
172+
if re.search(pattern, card_content):
173+
updated_content = re.sub(pattern, r"\1" + functions_list, card_content)
174+
kernel_card.content = updated_content
175+
176+
return kernel_card
177+
178+
179+
def _update_kernel_card_backends(
180+
kernel_card: ModelCard, local_path: str | Path
181+
) -> ModelCard:
182+
config = _parse_build_toml(local_path)
183+
if not config:
184+
return kernel_card
185+
186+
general_config = config.get("general", {})
187+
188+
card_content = str(kernel_card.content)
189+
190+
backends = general_config.get("backends")
191+
if backends:
192+
backends_list = "\n".join(f"- {backend}" for backend in backends)
193+
pattern = r"(## Supported backends\s*\n\n)\[TODO: add the backends this kernel supports\]"
194+
if re.search(pattern, card_content):
195+
card_content = re.sub(pattern, r"\1" + backends_list, card_content)
196+
197+
# TODO: should we consider making it a separate utility?
198+
kernel_configs = config.get("kernel", {})
199+
cuda_capabilities = []
200+
if kernel_configs:
201+
for k in kernel_configs:
202+
cuda_cap_for_config = kernel_configs[k].get("cuda-capabilities")
203+
if cuda_cap_for_config:
204+
cuda_capabilities.extend(cuda_cap_for_config)
205+
cuda_capabilities: set[Any] = set(cuda_capabilities) # type: ignore[no-redef]
206+
if cuda_capabilities:
207+
cuda_list = "\n".join(f"- {cap}" for cap in cuda_capabilities)
208+
cuda_section = f"## CUDA Capabilities\n\n{cuda_list}\n\n"
209+
pattern = r"(## Benchmarks)"
210+
if re.search(pattern, card_content):
211+
card_content = re.sub(pattern, cuda_section + r"\1", card_content)
212+
213+
kernel_card.content = card_content
214+
return kernel_card
215+
216+
217+
def _update_kernel_card_license(
218+
kernel_card: ModelCard, local_path: str | Path
219+
) -> ModelCard:
220+
config = _parse_build_toml(local_path)
221+
if not config:
222+
return kernel_card
223+
224+
existing_license = kernel_card.data.get("license", None)
225+
license_from_config = config.get("general", {}).get("license", None)
226+
final_license = license_from_config or existing_license
227+
kernel_card.data["license"] = final_license
228+
return kernel_card
229+
230+
231+
def _update_benchmark(kernel_card: ModelCard, local_path: str | Path):
232+
local_path = Path(local_path)
233+
234+
benchmark_file = local_path / "benchmarks" / "benchmark.py"
235+
if not benchmark_file.exists():
236+
return kernel_card
237+
238+
card_content = str(kernel_card.content)
239+
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).'
240+
241+
pattern = r"(## Benchmarks)"
242+
if re.search(pattern, card_content):
243+
updated_content = re.sub(pattern, r"\1" + benchmark_text, card_content)
244+
kernel_card.content = updated_content
245+
246+
return kernel_card

0 commit comments

Comments
 (0)