Skip to content

Commit 495f1f3

Browse files
authored
Merge pull request #3920 from zenml-io/feature/served-pipelines
Deployed pipelines
2 parents fecb121 + 5d2184d commit 495f1f3

File tree

109 files changed

+14799
-323
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+14799
-323
lines changed

docs/book/getting-started/core-concepts.md

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ As seen in the image, a step might use the outputs from a previous step and thus
6060

6161
Pipelines and steps are defined in code using Python _decorators_ or _classes_. This is where the core business logic and value of your work live, and you will spend most of your time defining these two things.
6262

63-
Even though pipelines are simple Python functions, you are only allowed to call steps within this function. The inputs for steps called within a pipeline can either be the outputs of previous steps or alternatively, you can pass in values directly (as long as they're JSON-serializable).
63+
Even though pipelines are simple Python functions, you are only allowed to call steps within this function. The inputs for steps called within a pipeline can either be the outputs of previous steps or alternatively, you can pass in values directly or map them onto pipeline parameters (as long as they're JSON-serializable). Similarly, you can return values from a pipeline that are step outputs as long as they are JSON-serializable.
6464

6565
```python
6666
from zenml import pipeline
@@ -71,19 +71,19 @@ def my_pipeline():
7171
step_2(input_one="hello", input_two=output_step_one)
7272

7373
@pipeline
74-
def agent_evaluation_pipeline():
74+
def agent_evaluation_pipeline(query: str = "What is machine learning?") -> str:
7575
"""An AI agent evaluation pipeline."""
7676
prompt = "You are a helpful assistant. Please answer: {query}"
77-
test_query = "What is machine learning?"
78-
evaluation_result = evaluate_agent_response(prompt, test_query)
77+
evaluation_result = evaluate_agent_response(prompt, query)
78+
return evaluation_result
7979
```
8080

8181
Executing the Pipeline is as easy as calling the function that you decorated with the `@pipeline` decorator.
8282

8383
```python
8484
if __name__ == "__main__":
8585
my_pipeline()
86-
agent_evaluation_pipeline()
86+
agent_evaluation_pipeline(query="What is an LLM?")
8787
```
8888

8989
#### Artifacts
@@ -118,9 +118,11 @@ Once you have implemented your workflow by using the concepts described above, y
118118

119119
#### Stacks & Components
120120

121-
When you want to execute a pipeline run with ZenML, **Stacks** come into play. A **Stack** is a collection of **stack components**, where each component represents the respective configuration regarding a particular function in your MLOps pipeline, such as orchestration systems, artifact repositories, and model deployment platforms.
121+
When you want to execute a pipeline run with ZenML, **Stacks** come into play. A **Stack** is a collection of **stack components**, where each component represents the respective configuration regarding a particular function in your MLOps pipeline, such as pipeline orchestration or deployment systems, artifact repositories and container registries.
122122

123-
For instance, if you take a close look at the default local stack of ZenML, you will see two components that are **required** in every stack in ZenML, namely an _orchestrator_ and an _artifact store_.
123+
Pipelines can be executed in two ways: in **batch mode** (traditional execution through an orchestrator) or in **online mode** (long-running HTTP servers that can be invoked via REST API calls). Deploying pipelines for online mode execution allows you to serve your ML workflows as real-time endpoints, making them accessible for live inference and interactive use cases.
124+
125+
For instance, if you take a close look at the default local stack of ZenML, you will see two components that are **required** in every stack in ZenML, namely an _orchestrator_ and an _artifact store_. Additional components like _deployers_ can be added to enable specific functionality such as deploying pipelines as HTTP endpoints.
124126

125127
![ZenML running code on the Local Stack.](../.gitbook/assets/02_pipeline_local_stack.png)
126128

@@ -130,16 +132,30 @@ Keep in mind that each one of these components is built on top of base abstracti
130132

131133
#### Orchestrator
132134

133-
An **Orchestrator** is a workhorse that coordinates all the steps to run in a pipeline. Since pipelines can be set up with complex combinations of steps with various asynchronous dependencies between them, the orchestrator acts as the component that decides what steps to run and when to run them.
135+
An **Orchestrator** is a workhorse that coordinates all the steps to run in a pipeline in batch mode. Since pipelines can be set up with complex combinations of steps with various asynchronous dependencies between them, the orchestrator acts as the component that decides what steps to run and when to run them.
134136

135137
ZenML comes with a default _local orchestrator_ designed to run on your local machine. This is useful, especially during the exploration phase of your project. You don't have to rent a cloud instance just to try out basic things.
136138

139+
#### Deployer
140+
141+
A **Deployer** is a stack component that manages the deployment of pipelines as long-running HTTP servers useful for online mode execution. Unlike orchestrators that execute pipelines in batch mode, deployers can create and manage persistent services that wrap your pipeline in a web application, usually containerized, allowing it to be invoked through HTTP requests.
142+
143+
ZenML comes with a _Docker deployer_ that can run deployments on your local machine as Docker containers, making it easy to test and develop real-time pipeline endpoints before moving to production infrastructure.
144+
145+
#### Pipeline Run
146+
147+
A **Pipeline Run** is a record of a pipeline execution. When you run a pipeline using an orchestrator, a pipeline run is created tracking information about the execution such as the status, the artifacts and metadata produced by the pipeline and all its steps. When a pipeline is deployed for online mode execution, a pipeline run is similarly created for every HTTP request made to it.
148+
137149
#### Artifact Store
138150

139151
An **Artifact Store** is a component that houses all data that passes through the pipeline as inputs and outputs. Each artifact that gets stored in the artifact store is tracked and versioned and this allows for extremely useful features like data caching, which speeds up your workflows.
140152

141153
Similar to the orchestrator, ZenML comes with a default _local artifact store_ designed to run on your local machine. This is useful, especially during the exploration phase of your project. You don't have to set up a cloud storage system to try out basic things.
142154

155+
#### Deployment
156+
157+
A **Deployment** is a running instance of a pipeline deployed as an HTTP endpoint. When you deploy a pipeline using a deployer, it becomes a long-running service that can be invoked through REST API calls. Each HTTP request to a deployment triggers a new pipeline run, creating the same artifacts and metadata tracking as traditional batch pipeline executions. This enables real-time inference, interactive ML workflows, and seamless integration with web applications and external services.
158+
143159
#### Flavor
144160

145161
ZenML provides a dedicated base abstraction for each stack component type. These abstractions are used to develop solutions, called **Flavors**, tailored to specific use cases/tools. With ZenML installed, you get access to a variety of built-in and integrated Flavors for each component type, but users can also leverage the base abstractions to create their own custom flavors.

docs/book/how-to/steps-pipelines/advanced_features.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -628,8 +628,8 @@ This is particularly useful for steps that interact with external services or re
628628
Hooks allow you to execute custom code at specific points in the pipeline or step lifecycle:
629629

630630
```python
631-
def success_hook(step_name, step_output):
632-
print(f"Step {step_name} completed successfully with output: {step_output}")
631+
def success_hook():
632+
print(f"Step completed successfully")
633633

634634
def failure_hook(exception: BaseException):
635635
print(f"Step failed with error: {str(exception)}")
@@ -639,6 +639,11 @@ def my_step():
639639
return 42
640640
```
641641

642+
The following conventions apply to hooks:
643+
644+
* the success hook takes no arguments
645+
* the failure hook optionally takes a single `BaseException` typed argument
646+
642647
You can also define hooks at the pipeline level to apply to all steps:
643648

644649
```python

docs/book/toc.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
* [Templates](how-to/templates/templates.md)
5656
* [Dashboard](how-to/dashboard/dashboard-features.md)
5757

58+
5859
## Reference
5960

6061
* [Community & content](reference/community-and-content.md)

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies = [
3333
"distro>=1.6.0,<2.0.0",
3434
"docker~=7.1.0",
3535
"gitpython>=3.1.18,<4.0.0",
36+
"jsonref",
3637
"packaging>=24.1",
3738
"psutil>=5.0.0",
3839
"pydantic>=2.0,<=2.11.9",
@@ -368,5 +369,6 @@ module = [
368369
"numba.*",
369370
"uvloop.*",
370371
"litellm",
372+
"jsonref",
371373
]
372374
ignore_missing_imports = true

scripts/install-zenml-dev.sh

Lines changed: 106 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,40 @@
22

33
INTEGRATIONS=no
44
PIP_ARGS=
5+
UPGRADE_ALL=no
6+
7+
show_help() {
8+
cat << EOF
9+
Usage: $0 [OPTIONS]
10+
11+
Install ZenML in development mode with optional integrations.
12+
13+
OPTIONS:
14+
-i, --integrations yes|no Install integrations (default: no)
15+
-s, --system Install packages system-wide instead of in virtual environment
16+
-u, --upgrade-all Uninstall existing ZenML, clear caches, and install latest versions
17+
-h, --help Show this help message
18+
19+
EXAMPLES:
20+
# Basic installation
21+
$0
22+
23+
# Install with integrations
24+
$0 --integrations yes
25+
26+
# Force reinstall with latest versions of all dependencies
27+
$0 --upgrade-all --integrations yes
28+
29+
# System-wide installation with latest versions
30+
$0 --system --upgrade-all
31+
32+
NOTES:
33+
- The --upgrade-all flag will uninstall existing ZenML installation and clear all caches
34+
- This ensures you get the latest compatible versions of all dependencies
35+
- Use this when you want to refresh your environment with the newest packages
36+
37+
EOF
38+
}
539

640
parse_args () {
741
while [ $# -gt 0 ]; do
@@ -15,8 +49,17 @@ parse_args () {
1549
PIP_ARGS="--system"
1650
shift # past argument
1751
;;
52+
-u|--upgrade-all)
53+
UPGRADE_ALL="yes"
54+
shift # past argument
55+
;;
56+
-h|--help)
57+
show_help
58+
exit 0
59+
;;
1860
-*|--*)
1961
echo "Unknown option $1"
62+
show_help
2063
exit 1
2164
;;
2265
*)
@@ -26,12 +69,39 @@ parse_args () {
2669
done
2770
}
2871

72+
clean_and_uninstall() {
73+
echo "🧹 Cleaning existing ZenML installation and clearing caches..."
74+
75+
# Uninstall ZenML (if installed) and clear pip cache
76+
uv pip uninstall $PIP_ARGS zenml || true
77+
78+
# Clear uv cache to ensure fresh downloads
79+
uv cache clean || true
80+
81+
# Clear pip cache as well (in case pip was used previously)
82+
python -m pip cache purge 2>/dev/null || true
83+
84+
echo "✅ Cleanup completed"
85+
}
86+
2987
install_zenml() {
88+
echo "📦 Installing ZenML in editable mode..."
89+
90+
# Build upgrade arguments based on UPGRADE_ALL flag
91+
upgrade_args=""
92+
if [ "$UPGRADE_ALL" = "yes" ]; then
93+
upgrade_args="--upgrade --force-reinstall"
94+
echo "🔄 Using --upgrade --force-reinstall to get latest versions"
95+
fi
96+
3097
# install ZenML in editable mode
31-
uv pip install $PIP_ARGS -e ".[server,templates,terraform,secrets-aws,secrets-gcp,secrets-azure,secrets-hashicorp,s3fs,gcsfs,adlfs,dev,connectors-aws,connectors-gcp,connectors-azure,azureml,sagemaker,vertex]"
98+
uv pip install $PIP_ARGS $upgrade_args -e ".[server,templates,terraform,secrets-aws,secrets-gcp,secrets-azure,secrets-hashicorp,s3fs,gcsfs,adlfs,dev,connectors-aws,connectors-gcp,connectors-azure,azureml,sagemaker,vertex]"
99+
100+
echo "✅ ZenML installation completed"
32101
}
33102

34103
install_integrations() {
104+
echo "🔌 Installing ZenML integrations..."
35105

36106
# figure out the python version
37107
python_version=$(python -c "import sys; print('.'.join(map(str, sys.version_info[:2])))")
@@ -54,18 +124,37 @@ install_integrations() {
54124
--output-file integration-requirements.txt \
55125
$ignore_integrations_args
56126

57-
# pin pyyaml>=6.0.1
58-
echo "" >> integration-requirements.txt
59-
echo "pyyaml>=6.0.1" >> integration-requirements.txt
60-
echo "pyopenssl" >> integration-requirements.txt
61-
echo "typing-extensions" >> integration-requirements.txt
127+
# Handle package pins based on upgrade mode
128+
if [ "$UPGRADE_ALL" = "yes" ]; then
129+
echo "🔄 Using latest versions for integration dependencies"
130+
# When upgrading, use minimum versions to allow latest compatible
131+
echo "" >> integration-requirements.txt
132+
echo "pyyaml>=6.0.1" >> integration-requirements.txt
133+
echo "pyopenssl" >> integration-requirements.txt
134+
echo "typing-extensions" >> integration-requirements.txt
135+
echo "maison<2" >> integration-requirements.txt
136+
else
137+
# Original behavior with specific pins
138+
echo "" >> integration-requirements.txt
139+
echo "pyyaml>=6.0.1" >> integration-requirements.txt
140+
echo "pyopenssl" >> integration-requirements.txt
141+
echo "typing-extensions" >> integration-requirements.txt
142+
echo "maison<2" >> integration-requirements.txt
143+
fi
144+
62145
echo "-e .[server,templates,terraform,secrets-aws,secrets-gcp,secrets-azure,secrets-hashicorp,s3fs,gcsfs,adlfs,dev,connectors-aws,connectors-gcp,connectors-azure,azureml,sagemaker,vertex]" >> integration-requirements.txt
63146

64-
# workaround to make yamlfix work
65-
echo "maison<2" >> integration-requirements.txt
147+
# Build upgrade arguments based on UPGRADE_ALL flag
148+
upgrade_args=""
149+
if [ "$UPGRADE_ALL" = "yes" ]; then
150+
upgrade_args="--upgrade --force-reinstall"
151+
echo "🔄 Using --upgrade --force-reinstall for integration dependencies"
152+
fi
66153

67-
uv pip install $PIP_ARGS -r integration-requirements.txt
154+
uv pip install $PIP_ARGS $upgrade_args -r integration-requirements.txt
68155
rm integration-requirements.txt
156+
157+
echo "✅ Integration installation completed"
69158

70159
# https://github.com/Kludex/python-multipart/pull/166
71160
# There is an install conflict between multipart and python_multipart
@@ -83,7 +172,14 @@ export ZENML_ANALYTICS_OPT_IN=false
83172

84173
parse_args "$@"
85174

86-
python -m pip install --upgrade wheel pip uv
175+
# Clean and upgrade tooling packages if upgrading all
176+
if [ "$UPGRADE_ALL" = "yes" ]; then
177+
echo "🚀 Upgrading all dependencies to latest versions..."
178+
clean_and_uninstall
179+
python -m pip install --upgrade --force-reinstall wheel pip uv
180+
else
181+
python -m pip install --upgrade wheel pip uv
182+
fi
87183

88184
install_zenml
89185

src/zenml/analytics/enums.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,9 @@ class AnalyticsEvent(str, Enum):
9191

9292
# Server Settings
9393
SERVER_SETTINGS_UPDATED = "Server Settings Updated"
94+
95+
# Deployment
96+
DEPLOY_PIPELINE = "Pipeline deployed"
97+
CREATE_DEPLOYMENT = "Deployment created"
98+
STOP_DEPLOYMENT = "Deployment stopped"
99+
DELETE_DEPLOYMENT = "Deployment deleted"

src/zenml/artifact_stores/base_artifact_store.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ def _validate_path(self, path: str) -> None:
106106
IllegalOperationError: If the path is a local file and the server
107107
is not configured to allow local file access.
108108
"""
109+
# Skip validation for memory:// URIs used in serving mode
110+
if path.startswith("memory://"):
111+
return
112+
109113
if not self.allow_local_file_access and not io_utils.is_remote(path):
110114
raise IllegalOperationError(
111115
"Files in a local artifact store cannot be accessed from the "
@@ -139,6 +143,11 @@ def _sanitize_potential_path(self, potential_path: Any) -> Any:
139143
# Neither string nor bytes, this is not a path
140144
return potential_path
141145

146+
# Preserve special in-memory scheme used by serving mode as-is
147+
# to avoid treating it as a local filesystem path.
148+
if isinstance(path, str) and path.startswith("memory://"):
149+
return path
150+
142151
if io_utils.is_remote(path):
143152
# If we have a remote path, replace windows path separators with
144153
# slashes

src/zenml/artifacts/utils.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,21 @@ def _store_artifact_data_and_prepare_request(
152152
Artifact version request for the artifact data that was stored.
153153
"""
154154
artifact_store = Client().active_stack.artifact_store
155-
artifact_store.makedirs(uri)
155+
156+
# Detect in-memory materializer to avoid touching the artifact store.
157+
# Local import to minimize import-time dependencies.
158+
from zenml.materializers.in_memory_materializer import (
159+
InMemoryMaterializer,
160+
)
161+
162+
is_in_memory = issubclass(materializer_class, InMemoryMaterializer)
163+
164+
if not is_in_memory:
165+
artifact_store.makedirs(uri)
166+
else:
167+
# Ensure URI clearly indicates in-memory storage and not the artifact store
168+
if not uri.startswith("memory://"):
169+
uri = f"memory://custom_artifacts/{name}/{uuid4()}"
156170

157171
materializer = materializer_class(uri=uri, artifact_store=artifact_store)
158172
materializer.uri = materializer.uri.replace("\\", "/")
@@ -190,7 +204,7 @@ def _store_artifact_data_and_prepare_request(
190204
data_type=source_utils.resolve(data_type),
191205
content_hash=content_hash,
192206
project=Client().active_project.id,
193-
artifact_store_id=artifact_store.id,
207+
artifact_store_id=None if is_in_memory else artifact_store.id,
194208
visualizations=visualizations,
195209
has_custom_name=has_custom_name,
196210
save_type=save_type,

src/zenml/cli/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2506,6 +2506,7 @@ def my_pipeline(...):
25062506
from zenml.cli.base import * # noqa
25072507
from zenml.cli.code_repository import * # noqa
25082508
from zenml.cli.config import * # noqa
2509+
from zenml.cli.deployment import * # noqa
25092510
from zenml.cli.downgrade import * # noqa
25102511
from zenml.cli.feature import * # noqa
25112512
from zenml.cli.integration import * # noqa

0 commit comments

Comments
 (0)