Skip to content

feat: prefer recipe ported einops - #1092

Open
drbh wants to merge 9 commits into
mainfrom
port-einops
Open

feat: prefer recipe ported einops#1092
drbh wants to merge 9 commits into
mainfrom
port-einops

Conversation

@drbh

@drbh drbh commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

*updated to match latest changeset

this pr is an example of using the kernel porting tool to port the upstream einops into the kernel builder format

first we clone the upstream source*

# first clone the repo locally
git clone https://github.com/arogozhnikov/einops.git /tmp/einops-src

# next we pin to a specific commit (same used in the recipe)
git -C /tmp/einops-src checkout ad2c8d67d08cbebb45e1aff2be21dc88635fc695

next we

# run the tool directly from the branch (could later be built into the cli tool)
nix run 'github:huggingface/kernels/pull/776/head#kernel-port' -- \
einops/port/port.kdl \ # the recipe to port
--dir /tmp/einops-src \ # the location of the source we cloned
--out ~/Projects/kernels-community/einops # the output location

running the command will show how each line of the recipe impacted the transformation and outputs all of the files added and deleted in the process.

[line   4] source              verified https://github.com/arogozhnikov/einops.git @ ad2c8d67d08c
[line   9] prune               removed 57 file(s), kept 27
[line  12] delete              removed 1 file(s)
[line  15] move                moved 8 file(s) to "tests"
[line  20] move                moved 18 file(s) to "torch-ext/einops"
[line  25] relativize_imports  rewrote 7 import(s) in 4 file(s)
[line  30] ensure_import       added 1 import(s) in 1 file(s)
[line  36] remap_module        rewrote 7 import(s) in 6 file(s)
[line  42] relativize_imports  rewrote 7 import(s) in 6 file(s)
[line  47] kernelize_imports   kernelized 26 import(s) in 8 file(s)
[line  54] replace             1 replacement(s) in 1 file(s)
[line  59] replace             1 replacement(s) in 1 file(s)
[line  64] replace             7 replacement(s) in 7 file(s)
[line  70] replace             1 replacement(s) in 1 file(s)
[line  75] manifest            wrote build.toml (noarch)

check-ports.yaml reruns the port in ci and fails if einops/src does not match the output, so the generated tree is still reviewable but cannot drift from the recipe. prs only check the ports they touch

note: flake.nix is not generated so it stays at einops/flake.nix and points nix at ./src, build.yaml already handles this layout (#1109)

@github-actions github-actions Bot added the feature New functionality / capability label Aug 21, 2026
@drbh

drbh commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

for completeness, we can bump the commit hash from ad2c8d67d08cbebb45e1aff2be21dc88635fc695 to the latest head e0d5eb4fd535945ff65d309206d0f1754a926821

and trying to rerun the command shows

nix run 'github:huggingface/kernels/pull/776/head#kernel-port' -- .ports/einops/port.kdl --dir /tmp/einops-src --out einops
[line  14] source              verified https://github.com/arogozhnikov/einops.git @ e0d5eb4fd535
[line  18] prune               removed 56 file(s), kept 27
[line  19] delete              removed 10 file(s)
[line  21] move                moved 17 file(s) to "torch-ext/einops"
error: recipe line 25: relativize_imports: expected exactly 7 change(s) but made 13 - upstream drifted; review the new rewrites and update changes=

at this point I simply asked and llm to fix the recipe so the port works correctly and copy pasted the command and error output above.

the exact output of this process can be seen in #1093

@sayakpaul

Copy link
Copy Markdown
Member

I think we should have a single standard to start this. I would prefer if we required users to specify the source and the commit in the recipe itself instead of having them locally clone and checkout to a particular branch first. Or, that should be a better default behaviour, IMO.

9 of the 15 files in the overlay are tests we added that are not ported from upstream

We should port tests, too. Then it will be down on us to make modifications there e.g., for running specific tests on our CI, etc. Cc: @danieldk here as well.

@sayakpaul

Copy link
Copy Markdown
Member

Going to run the commands from the PR description and share my findings here as well.

Comment thread einops/src/port-provenance.json
Comment thread .ports/einops/port.kdl Outdated
find="from .packing import pack, unpack\n" \
with="from .packing import pack, unpack\nfrom . import array_api\n"

// The rewritten tests and the flake.

@sayakpaul sayakpaul Aug 25, 2026

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.

We should not have to rewrite the tests much though. I think in the tests we would like to ensure that they use get_kernel() which already seems to be the case. So, I think we should rather provide similar instructions in the recipe to account for this.

pytest.mark.kernels_ci should probably be left to us because it's better if we decided which tests are lightweight and important enough to run on our CI.

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.

And also, I think tests should not be overlays no? Currently, we include them inside of the actual kernel source.

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.

the tests actually come from upstream and the port was updated so we rewrite the import to use get_kernels, this added some more lines to the port - but now we do not have any tests in the overlay file.

however if we were to add tests that were not originally upstream and rewritable - they would belong in the overlays folder - since that should contains all of the code that is not upstream. the "kernel source" which is now in the einops/src file should not contain any code that is not output from the port too. we keep this clear separation so the einops/src only contains generated code (we never mix non generated code with the tools output)

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.

I agree. Those tests (that we modify heavily) should probably be reserved for just CI-level testing. Those are fine to have in overlays.

Comment thread .ports/einops/port.kdl Outdated
Comment thread .ports/einops/overlay/tests/test_other.py
Comment thread .ports/einops/port.kdl Outdated
@sayakpaul

Copy link
Copy Markdown
Member

#1100 -- used the tool on flash-attn-ops. Major changes are in a layer norm file and I provided some comments there.

@drbh

drbh commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

/kernel-bot build-and-stage einops

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown

Build request processed.

Command: /kernel-bot build-and-stage einops
Mode: build and stage
Target branch: pr-1092
PR head SHA: 26b94d072a85a3609dea42a667ffe368126ddfc8
Workflows: build.yaml, build-mac.yaml, build-windows.yaml

Dispatched (2):

@drbh
drbh changed the base branch from main to nested-build-toml-support August 27, 2026 00:20
@sayakpaul

Copy link
Copy Markdown
Member

@drbh can we build using the bot without the PR not having landed in main?

Comment thread einops/port/port.kdl Outdated

replace in="tests/test_einsum.py" count=1 \
find=#"""
from einops.tests import collect_test_backends

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.

This seems quite verbose? It would be nicer to have something less literal and more declarative that is "for every import of the kernel of the shape import kernel replace by kernel = get_kernel(...) and every import of the shape from kernel import foo replace by foo = kernel.foo. All these full snippets are going to be very fragile for active upstream projects.

Base automatically changed from nested-build-toml-support to main August 27, 2026 14:24
@drbh

drbh commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

/kernel-bot build-and-stage einops

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown

Build request processed.

Command: /kernel-bot build-and-stage einops
Mode: build and stage
Target branch: pr-1092
PR head SHA: 2f5d4fe7d964a2ed5a7d55b89041dc2be17966e4
Workflows: build.yaml, build-mac.yaml, build-windows.yaml

Dispatched (2):

Hub uploads:

@sayakpaul sayakpaul left a comment

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.

Thanks, left some comments. I like the idea of validating a port, really cool!

Comment thread .github/workflows/check-ports.yaml Outdated
Comment on lines +3 to +6
# A ported kernel's <kernel>/src is generated from <kernel>/port/port.kdl and
# committed. Regenerate it here and fail if it drifted, so that a stale overlay
# silently reverting an in-tree fix, a hand-edited build.toml field the manifest
# op drops, or an overlay file edited without its generated twin are all loud.

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.

What do we consider to be a drift here?

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.

drift here is just einops/src not matching what the recipe outputs. the cases this catches are an overlay file going stale and undoing a fix that was made directly in src, a manual edit to build.toml that the manifest op drops on the next run, and the recipe being edited without rerunning the port (port-provenance.json stores the recipe hash so that one shows up as a diff in the provenance file). updated the comment in the workflow to list these

types: [opened, synchronize, reopened]
paths:
- "*/port/**"
- "*/src/**"

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.

So for kernels containing valid porting recipes, we expect their sources to be present under src, which is as expected 👍

Comment thread .github/workflows/check-ports.yaml Outdated
env:
# kernel-port still lives on the kernel-port-tool branch; point this at
# github:huggingface/kernels#kernel-port once that branch merges.
KERNEL_PORT: "nix run github:huggingface/kernels/kernel-port-tool#kernel-port --"

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.

Let's add a TODO here to update when the PR is eventually merged.

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.

Also, once this is shipped do we expect users to run with nix run or through the kernel-builder CLI?

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.

Let's add a TODO here to update when the PR is eventually merged.

done, also pinned the runner to a commit on the kernel-port-tool branch instead of the branch head so changes there dont affect open prs

Also, once this is shipped do we expect users to run with nix run or through the kernel-builder CLI?

nix run for now since the tool only exists on the branch. once it merges I think we can expose it as a kernel-builder subcommand and update KERNEL_PORT in the workflow, the script just reads that env var so either works

@@ -0,0 +1,12 @@
{
"format": 1,

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.

Self TODO: review the provenance generation logic.

Comment thread einops/port/port.kdl
@@ -0,0 +1,80 @@
recipe version=1

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.

Okay so IIUC we didn't need any overlay in this case? I thought the flake.nix would go to the overlay directory? How are we handling that?

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.

yea no overlay is needed for einops since everything comes from upstream. flake.nix is not generated so it stays at einops/flake.nix with path = ./src and only the tool output lives under src/. build.yaml already picks up <kernel>/src/build.toml when it exists

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.

flake.nix is not generated so it stays at einops/flake.nix

Oh okay. Earlier, I think we wanted to keep the external files in the overlay. What is the general guidelines behind putting stuff under an overlay folder then?

Comment thread scripts/check_ports.py
drbh added a commit to huggingface/kernels that referenced this pull request Sep 2, 2026
this pr fixes the macos ci failures where `get-kernel-check` rejects a
kernel that built fine. first hit on the einops port in
huggingface/kernels-community#1092

tldr; the check handed `$out` to the resolver and let it pick a variant
by detecting the backend at runtime. metal is the only backend detected
from live hardware (`torch.backends.mps.is_available()`, everything else
is baked into the torch build). the sandboxed macos build cannot see the
gpu, so the `torch-metal` variant is never selected and the check fails
even though the artifact is fine

repro (no kernel build needed)

```bash
mkdir -p /tmp/fake-kernel/torch-metal && touch /tmp/fake-kernel/torch-metal/metadata.json
kernels/.venv/bin/python -c "from pathlib import Path; import kernels; kernels.get_local_kernel(Path('/tmp/fake-kernel'))"
```

```
ValueError: Cannot parse metadata from `"/tmp/fake-kernel/torch-metal/metadata.json"`: EOF while parsing a value at line 1 column 0
```

on a mac with a visible gpu the variant is selected fine (it gets as far
as reading the empty `metadata.json`). same command inside a seatbelt
sandbox

```bash
sandbox-exec -p '(version 1)(allow default)(deny iokit-open)' \
    kernels/.venv/bin/python -c "from pathlib import Path; import kernels; kernels.get_local_kernel(Path('/tmp/fake-kernel'))"
```

```
FileNotFoundError: Cannot find a build variant for this system in /tmp/fake-kernel:
```

`sandbox-exec` is the same seatbelt mechanism the nix darwin sandbox
uses. denying iokit hides the gpu, `mps.is_available()` returns false,
and the same directory no longer resolves

note* this is also why only some metal builds fail in ci. arch metal
builds set `__noChroot = metalSupport` since they need the host metal
toolchain, so the gpu is still visible during the check. noarch metal
builds like einops run fully sandboxed and always fail. the first
command also fails on linux since there is no mps at all

the check now globs `$out` for `*/metadata.json` and points the resolver
at that exact variant dir instead of auto detecting from the build host.
each extension derivation produces exactly one variant, so zero or
multiple variants still fail the check
drbh added 4 commits September 2, 2026 15:30
Signed-off-by: David Holtz <david.richard.holtz@gmail.com>
Signed-off-by: David Holtz <david.richard.holtz@gmail.com>
Signed-off-by: David Holtz <david.richard.holtz@gmail.com>
Signed-off-by: David Holtz <david.richard.holtz@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New functionality / capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants