feat: add notebook: cell type for multi-notebook composition. - #867
Conversation
|
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
c0b4228 to
21f78e1
Compare
|
You can watch the full demo of this feature on YouTube |
4353cf1 to
2f64059
Compare
2f64059 to
3fdda3c
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds Phase 2 (backend) support for composable notebooks by introducing a notebook: cell type that lets a “composition notebook” reference other notebooks and compile them into a single KFP pipeline while preserving per-cell step visibility via sub-pipelines.
Changes:
- Adds a multi-notebook composition compiler (
kale.processors.workflow) that infers cross-notebook data flow and generates one DSL module per notebook plus an orchestrator DSL. - Extends the notebook tag language to recognize
notebook:<name>andstep:notebook:<name>and updateskale --nbto route to composition when references are present. - Adds unit tests and an
examples/composition/demo notebook set.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| kale/tests/unit_tests/test_workflow.py | Adds unit coverage for topo sort, cycle detection, boundary inference, name collisions, and wiring semantics. |
| kale/templates/workflow_template.jinja2 | New orchestrator DSL template that imports per-notebook modules and wires sub-pipelines. |
| kale/templates/subpipeline_template.jinja2 | New per-notebook DSL module template defining components + sub-pipeline + standalone compile hook. |
| kale/processors/workflow.py | Implements composition: reference extraction, dependency inference, boundary variable wiring, and DSL generation. |
| kale/processors/nbprocessor.py | Extends tag parsing with NOTEBOOK_TAG and records notebook reference metadata. |
| kale/common/kfputils.py | Updates pipeline compilation to copy sibling modules so orchestrator imports resolve in a temp dir. |
| kale/cli.py | Routes kale --nb to composition when notebook: cells exist; prints concise user-facing errors. |
| examples/composition/main.ipynb | Adds a composition notebook that references A/B/C as sub-pipelines. |
| examples/composition/notebook_a.ipynb | Adds example “producer” notebook for the demo. |
| examples/composition/notebook_b.ipynb | Adds example intermediate notebook for the demo. |
| examples/composition/notebook_c.ipynb | Adds example “consumer” notebook for the demo. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
3fdda3c to
263fb3c
Compare
|
moved the kfp import above the sys.path mutation so no generated module can shadow it, and the composer now rejects notebook names that collide with kfp/kale/stdlib. Kept insert(0) rather than append deliberately: the generated modules must win resolution of their own names, otherwise a same named module elsewhere on sys.path would silently shadow them |
|
New updated DSL structure thank you @jesuino for the suggestion! |
1c7f786 to
749057f
Compare
Signed-off-by: Ya-shh <yashh.real@gmail.com>
Signed-off-by: Yash <yashh.real@gmail.com>
Signed-off-by: Yash <yashh.real@gmail.com>
749057f to
0f7e2d5
Compare
StefanoFioravanzo
left a comment
There was a problem hiding this comment.
@Ya-shh I went through the code. For of all, congrats for submitting this first version. It's a lot of work and I really appreciate the lengths you went to understand the code deeply and implement a sophisticated mechanism.
I didn't run it myself, but I saw the demo and I know the approach work, practically. So we are in a very good position, because you proved this can be done.
I added some comments inline, but to me the bottom line is that we should try to rethink a bit how you put together the whole logic inside workflow.py. When thinking about an elegant, fluid, and well architected solution, I imagine a fully recursive implementation inside NotebookProcessor, instead of having to build a separate dedicated logic inside a separate module (workflow.py). The function compose_notebooks_as_subpipelines is very long and a bit hard to follow as well.
I'd like to hear what @ederign thinkgs as well, because what I am proposing would mean changing this code A LOT, but at the same time would allows us to have code that anyone can understand and feel like it was built for multi-notebook orchestration from day 1. As it is now, it still feels like it was built afterwards, with bits and pieces conneting sometimes in an awkward way
Also - mind you - I know it's hard to build a fully recursive loop, but that's also why it will be so much more elegant and beautiful :)
Signed-off-by: Ya-shh <yashh.real@gmail.com>
|
@StefanoFioravanzo @ederign step 3 of the rework is landed in Composition now happens inside R1 is fixed, The panel's compile RPC was the path that silently dropped references, It now produces a root DAG where each referenced notebook is a nested sub-DAG holding its own cell-level steps, and a mixed root adds its own steps as top-level components, verified against the reproducers in the gist Two things I want to flag :
A |
Signed-off-by: Ya-shh <yashh.real@gmail.com>
|
Step 4 is landed too, in The per-task config block is now a single shared macro imported by every emission path, so a step's Single notebook output is unchanged, verified byte for byte against the golden DSL fixtures in One correction to my note above: |
Signed-off-by: Ya-shh <yashh.real@gmail.com>
Signed-off-by: Ya-shh <yashh.real@gmail.com>
bbc63b7 to
656cbe6
Compare
Signed-off-by: Ya-shh <yashh.real@gmail.com>
# Conflicts: # kale/compiler.py # kale/templates/pipeline_template.jinja2
|
Template: done What is genuinely sub-pipeline specific turned out to be four things, not the
separately, @ederign's second round of reproducers is at six of seven passing |
|
@Ya-shh congrats on this amazing work, and thanks for sticking with us through all the rounds of reviews! |
This notebook is a leftover from local development that was swept into the merge commit dc0088b — it exists in neither of that merge's parents, so it never appeared in a reviewable diff. It does not belong in the curated examples: it has no `notebook:` cell, so it demonstrates nothing about composition (it is a plain two-step pipeline already covered by examples/base), and its metadata is scratch-quality — pipeline_name 'mixing', experiment 'new-testing', a pipeline_description copy-pasted from notebook_a, and a hardcoded base_image 'kale-runtime:2.1.0' that the other five composition notebooks do not set. Nothing references it: no CI job, no docs, no other notebook. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Eder Ignatowicz <ignatowicz@gmail.com>
ea6a200 to
f9a4b50
Compare
|
@Ya-shh merging this. Congrats, it's a big one for Kale. It will open a sea of new possibilities. The part I want to call out isn't the feature, it's that you took "restructure most of it" without starting over. Exactly as we proposed in the beginning. Follow-ups, none blocking:
Thanks for taking the feedback well, and the error messages and comments explaining why rather than what is not a common thing to see. Keep pushing!! |
|
/approve |
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: ederign The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
Backend and labextension for the composable-notebooks KEP (KEP-0812). Compose multiple Kale notebooks into one Kubeflow pipeline through the existing
kale --nbpath and the Kale panel, with no new command or flag.Demo of the composition flow on YouTube: https://youtu.be/rJEnyU6t3S8?si=LTkmlLsmMPCjdKlh
What it does
A
notebook:cell type lets a notebook reference other notebooks. Two shapes are supported:Sequence of notebooks (Story 1): the root notebook holds only
notebook:references. Each referenced notebook becomes a KFP sub-pipeline (GraphComponent), so cell-level step visibility is preserved. Data flow between notebooks is inferred by matching variable names, the same AST and PyFlakes mechanism Kale already uses between cells, applied one level up.Mixed steps and notebooks (Story 2): the root notebook can also carry its own
step:,importsandfunctionscells alongside the references. Each such step becomes a top-level component, and ordering and data cross the step/notebook boundary the same way they cross a step/step one. A root step runs after every unit above it and before every unit below it (reading order), while independent references between steps still run in parallel.Whether a notebook compiles as a plain pipeline, a sequence, or a mixed composition is decided entirely by the cells it contains.
How it works (reworked after review)
The first version of this PR put composition in a separate
processors/workflow.pybehind a branch incli.py. Two review findings retired that design:NotebookProcessor, the way a compiler recurses.workflow.pyat all, so composing from the UI silently produced a pipeline with the referenced notebooks missing.Composition now lives inside
NotebookProcessor, andworkflow.pyand thecli.pybranch are deleted. Every notebook goes through one compile path, which is what fixes the UI case:notebook:cell it recurses into a childNotebookProcessorand attaches the resulting pipeline as aSubPipelinenode, a peer ofStepin the same graph.dependencies_detectionresolves only along edges that already exist, and a composition has noprev:tags to create them. So the edges are inferred first: data flow between units, plus reading order for the root notebook's own steps.dependencies_detectionthen runs unchanged, so a variable crossing a notebook boundary is resolved by exactly the code that resolves one crossing a cell boundary.Compilerwalks the mixed graph and writes one DSL module per referenced notebook plus the orchestrator.DSL file structure (per review feedback)
Each notebook produces its own independent DSL module (
.kale/kale_notebook_<name>.py) and the orchestrating DSL file imports those modules as dependencies. This mirrors the recursive shape of the processing: each notebook is an independent module with no circular references, each owns its own configuration, and each is compilable standalone. In a mixed composition the root notebook's own steps live in the orchestrator, which is that notebook's own module. Thekale_notebook_prefix means a notebook namedjson.ipynb,class.ipynbor2train.ipynbcompiles rather than erroring.What is in the PR
Backend
processors/nbprocessor.py: thenotebook:<name>tag (referenced path in cell metadata) added to the tag language; recursive parsing of references intoSubPipelinenodes;link_composition_units()for edge inference;propagate_subpipeline_boundaries()to hand each boundary variable to the inner step that produces or consumes it. Anotebook:cell breaks the code merge chain (KEP-0812 caveat 5): untagged cells after a reference attach to the nextstep:cell, and orphaned code raises rather than being dropped.step.py:SubPipeline(Step), a referenced notebook as a node of the same graph, carrying the samename,ins,outsandconfigso dependency detection and code generation treat a reference and a step uniformly.compiler.py:generate_composition()and the per-notebook module emission. Each referenced notebook's module is generated by a compiler bound to that notebook's own pipeline, so its pipeline parameters and imports are the ones its steps are built with, exactly as when it is compiled on its own.templates/subpipeline_template.jinja2andtemplates/composition_template.jinja2: the per-notebook module (components, typed sub-pipeline, standalone compile hook) and the orchestrator that imports the modules, holds any root-step components, and wires everything.common/kfputils.py:compile_pipelinecarries the DSL script's sibling modules into its temp dir so the orchestrator's imports resolve.Frontend (labextension)
A
Notebookcell type in the Kale cell editor: pick it, enter the notebook path, and it writes thenotebook:tag plus thenotebook_pathmetadata. Reference cells render an inlinenotebook: <name>chip and are excluded fromprev:dependency choices and the merge-chain notice.Examples and tests
examples/composition/holdsmain.ipynb(three references, dataset to model to prediction) andmain_mixed.ipynb(references plus a rootstep:cell).test_composition.pycovers reference-to-node conversion, inferred order and data, variables crossing the step/notebook boundary both ways, reading order, preserved parallelism, the merge rule, orphan code, missing paths, reference cycles, name collisions, per-module output, safe module naming, parameter survival, nested-reference rejection, reported step config, and two realkfp.compilercompilations.The single-notebook path is untouched: a notebook with no
notebook:cells compiles exactly as before, and no existing cell type or API changes.Testing
kale --nb examples/composition/main.ipynbcompiles to a root DAG of three sub-DAGs, each holding its notebook's own cell-level steps;main_mixed.ipynbadds the root's step as a top-level component wired in reading order.Known limitations
notebook:cell inside a referenced notebook raises rather than compiling to something that flattens a level of nesting. Nested composition is not part of the KEP stories, so this is a deliberate rejection rather than a partial implementation. Happy to change it if reviewers would prefer it supported.limits,annotations,labels, retries or timeouts. Compiling reports exactly which step and which field is affected rather than dropping it quietly. Restoring this is the remaining step of the agreed rework plan.