feat(docs): KEP- Spark Client for Kubeflow SDK#163
Conversation
dfdb297 to
8ad3b6e
Compare
b458571 to
30f3336
Compare
Pull Request Test Coverage Report for Build 20839855538Details
💛 - Coveralls |
| # Custom backend implementation | ||
| from kubeflow.spark.backends.base import SparkBackend | ||
|
|
||
| class CustomBackend(SparkBackend): |
There was a problem hiding this comment.
Users can extend the backend, if they want to have any specific changes or different way to connect or submit spark job
andreyvelich
left a comment
There was a problem hiding this comment.
Thanks for this great effort @Shekharrajak!
I left my initial thoughts.
| ### Goals | ||
|
|
||
| - Design a unified, Pythonic SDK for managing Spark applications on Kubernetes | ||
| - Support multiple backends (Kubernetes Operator, REST Gateway, Spark Connect) following the Trainer pattern |
There was a problem hiding this comment.
For Trainer, backends represent various job submission (local subprocess, container, and Kubernetes). I am not sure if we can replicate it for Spark.
There was a problem hiding this comment.
Here we have job submission using K8S Operator backend, Spark Connector backend , Gateway backend (not implemented - we just have abstract class).
There was a problem hiding this comment.
I am wondering what is the main motivation to separate SessionClient() and BatchClient()?
Alternatively, we can just have unified SparkClient() which has sessions and batch APIs:
submit_job() <-- to create Spark Application and submit batch job
connect() <-- to create session and connect to existing Spark clusterThere was a problem hiding this comment.
- BatchSparkClient users never see connect(), create_session() methods
- SparkSessionClient users never see submit_application(), wait_for_job_status() methods
- This prevents runtime errors: Can't call wait_for_job_status() on a session object.
There was a problem hiding this comment.
This helps user to know which methods are available clearly :
client = SparkClient()
# User sees BOTH batch AND session methods
client.submit_job(...) # For batch
client.connect(...) # For session
client.get_job(...) # Works with connect() or batch() ?
# event if we take arg in config:
client = SparkClient(mode="batch")
client.create_session(...) # IDE will not show error, but runtime error
There was a problem hiding this comment.
Do we know how the Spark session management will work with the new CRD: SparkCluster proposed by @ChenYi015 here: kubeflow/spark-operator#2744 ?
There was a problem hiding this comment.
Added my view: kubeflow/spark-operator#2744 (comment)
There was a problem hiding this comment.
If we want to create the spark cluster through the SDK then we need API like :
from kubeflow.spark import SparkClusterManager
manager = SparkClusterManager()
# Create persistent cluster
cluster = manager.create_cluster(
name="prod",
spark_version="4.0.1",
init_workers=5,
min_workers=2,
max_workers=10,
enable_hpa=True,
)
# Cluster url to use for spark job submission: cluster.master_url
| ┌───────────┴─────────────┬──────────────────┬────────────────┐ | ||
| ▼ ▼ ▼ ▼ | ||
| ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ | ||
| │OperatorBackend │ │ GatewayBackend │ │ ConnectBackend │ │ LocalBackend │ | ||
| │(Spark Operator │ │ (REST Gateway) │ │ (Spark Connect/ │ │ (Future) │ | ||
| │ CRDs on K8s) │ │ │ │ Interactive) │ │ │ | ||
| └──────────────────┘ └──────────────────┘ └──────────────────┘ └──────────────────┘ | ||
| Batch Jobs Batch Jobs Sessions Local Dev |
There was a problem hiding this comment.
Can you explain reason of creating various backends? Can we just have an API: SparkClient().connect() which creates SparkConnect CR and connects to the existing cluster as we discussed?
There was a problem hiding this comment.
The batch backend will have APIs like submit_application, wait_for_completion, get_logs, .. where user will just submit the job and can check the logs/results.
example: https://github.com/kubeflow/sdk/pull/158/files#diff-e692a5819ee6b1dc00cba3b58e91f058c0022d3ca9aa6f3ee468126f245eef89R89
But with interactive session spark client user will be able to run interactive SQL queries and DataFrame operations.
example: https://github.com/kubeflow/sdk/pull/158/files#diff-a5011f48c9d6d16ff6ddd65588f65a7c78abf5fbeb121cccb693c0892ce3a5aeR275
| # Submit a Spark application | ||
| response = client.submit_application( | ||
| app_name="spark-pi", | ||
| main_application_file="local:///opt/spark/examples/src/main/python/pi.py", |
There was a problem hiding this comment.
I am wondering if there is a way to allow users bypass function to SparkApplication similar to Trainer API: https://github.com/kubeflow/sdk?tab=readme-ov-file#run-your-first-pytorch-distributed-job
That might be interesting to explore how we can allow to submit SparkApplication without building an image.
There was a problem hiding this comment.
Are we thinking an API like this ?
def my_job(input_path: str, output_path: str):
from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
df = spark.read.csv(input_path)
df.groupBy("col").count().write.parquet(output_path)
spark.stop()
client.submit_function(
func=my_job,
func_args={"input_path": "s3://in", "output_path": "s3://out"}
)
There was a problem hiding this comment.
Yeah, that might be interesting to explore.
We can also extend submit_job() API to accept main_application_file or function.
There was a problem hiding this comment.
Let me add to KEP and will work on it.
There was a problem hiding this comment.
Spark requires pre-allocated cluster resources (driver/executor specifications), so I could not find such way for running spark job. But we can explore in next version.
There was a problem hiding this comment.
@vara-bonthu @ChenYi015 @nabuskey Do you have any ideas on how we can dynamically inject the users' code into Spark driver without re-building an image or put Python file into Pod's filesystem?
I have one idea if we have can leverage PodTemplate spec.
Let's say we define InitContainer that simply creates file in the Pod's filesystem with the Spark code in the pre-defined location (e.g. local:///opt/spark/spark_job.py). This initContainer can share emptyDir with the driver container, so driver container can see the file. To create the file, we can use the same script as for Trainer:
container:
command:
- bash
- -c
- |
read -r -d '' SCRIPT << EOM
def my_job(input_path: str, output_path: str):
from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
df = spark.read.csv(input_path)
df.groupBy("col").count().write.parquet(output_path)
spark.stop()
EOM
printf "%s" "$SCRIPT" > "/opt/spark/spark_job.py"WDYT ?
cc @shravan-achar @akshaychitneni @bigsur0 for feedback on this.
There was a problem hiding this comment.
Sorry to interrupt. I implemented a decorator that uses inspect to get the function's body and form an executable file. It also parses the function's signature into submit parameters (they must be JSON-serializable). I could implement this if you're interested.
Example
@spark_submit
def my_task(first, second):
...There was a problem hiding this comment.
@aagumin Yeah, this looks similar to how we do this in TrainerClient(): https://github.com/kubeflow/sdk/blob/main/kubeflow/trainer/backends/kubernetes/utils.py#L297-L301
I think, having the consistent UX across Data Processing (e.g. SparkJobs) and Training (e.g. TrainJob) workloads would be really cool.
As long as SparkApplication CRD can support initContainer via PodTemplate, that should be relatively easy to implement.
cc @franciscojavierarceo @kubeflow/kubeflow-sdk-team
| # Step 1: Interactive development with ConnectBackend | ||
| connect_config = ConnectBackendConfig(connect_url="sc://dev-cluster:15002") | ||
| dev_client = SparkClient(backend_config=connect_config) | ||
|
|
||
| with dev_client.create_session("dev") as session: | ||
| # Test and validate query | ||
| test_df = session.sql("SELECT * FROM data LIMIT 1000") | ||
| test_df.show() | ||
| # Iterate and refine... | ||
|
|
||
| # Step 2: Production batch job with OperatorBackend | ||
| prod_config = OperatorBackendConfig(namespace="production") | ||
| prod_client = SparkClient(backend_config=prod_config) |
There was a problem hiding this comment.
This is pretty interesting experience for dev -> prod Spark lifecycle.
cc @shravan-achar @akshaychitneni @bigsur0 to explore.
| ) | ||
| ``` | ||
|
|
||
| #### Integration with Pipelines |
There was a problem hiding this comment.
cc @kubeflow/wg-pipeline-leads to explore
f428a9d to
33a9ad1
Compare
| config = ConnectBackendConfig( | ||
| connect_url="sc://spark-cluster:15002", | ||
| use_ssl=True, | ||
| ) | ||
|
|
||
| # Create client and session | ||
| client = SparkSessionClient(backend_config=config) | ||
| session = client.create_session(app_name="analysis") |
There was a problem hiding this comment.
How will this be better than a native SparkConnect call? I can see that you can’t replace a Connect client with a regular one without changing interfaces, considering that the options for configuring a Connect session are significantly more limited.
There was a problem hiding this comment.
We will have more kubeflow compatible APIs and infra.
|
|
||
| Implements the backend for managed Spark gateways: | ||
|
|
||
| - Submits applications via REST endpoints |
There was a problem hiding this comment.
If a PR is added for gRPC submit support and the ability to implement a custom submit mechanism, then this becomes relevant.
e7dc708 to
92cb005
Compare
andreyvelich
left a comment
There was a problem hiding this comment.
Thanks for the update @Shekharrajak!
I left a few comments.
| client = ( | ||
| SparkClient.builder() |
There was a problem hiding this comment.
I like the idea of builder(), but shall this comes in the 2nd phase?
We have similar ideas with @kubeflow/kubeflow-sdk-team for TrainerClient and OptimizerClient, so maybe we can unify the experience?
Initially, we can just allow users to specify minimal number of parameters in the connect() API.
But I am happy to debate it during our next SDK call on Dec 17th: https://docs.google.com/document/d/1jH2WAX2ePxOfI4JuiVK9nPlesDMiyg67xzLwhpR7wTQ/edit?tab=t.0#heading=h.jarjwwjkqmul
There was a problem hiding this comment.
Agree, I'd like us to start with constructor and config objects aligning with existing clients and later expand it with builder approach
| ```python | ||
| class SparkClientBuilder: | ||
| """Builder for SparkClient configuration.""" |
There was a problem hiding this comment.
Will SparkApplication and SparkConnect CRD always have the same APIs? I am wondering if we can re-use the same builder for them.
cc @ChenYi015
There was a problem hiding this comment.
This would be internal changes, we can take care of it later as well.
| # Kubernetes settings | ||
| def namespace(self, ns: str) -> "SparkClientBuilder": | ||
| """Set Kubernetes namespace.""" |
There was a problem hiding this comment.
I think, namespace should be part of KubernetesBackendConfig in SparkClient to align with other clients: https://github.com/kubeflow/sdk/blob/main/kubeflow/trainer/backends/kubernetes/backend.py#L42-L43
| from kfp import dsl | ||
| from kubeflow.spark.pipelines import SparkJobOp | ||
|
|
||
| @dsl.pipeline(name="ml-pipeline") | ||
| def ml_pipeline(): | ||
| # Spark ETL step | ||
| etl = SparkJobOp( | ||
| name="feature-etl", | ||
| main_file="s3a://ml/etl.py", | ||
| executor_instances=20, | ||
| executor_memory="8g", | ||
| ) | ||
|
|
||
| # Training step depends on ETL completion | ||
| train = TrainOp( | ||
| dataset_path=etl.outputs["output_path"], | ||
| ) | ||
| train.after(etl) |
There was a problem hiding this comment.
We should whether we need to move it under: kubeflow.pipelines.components.
We can also leverage KFP lightweight Python tasks to submit such pipelines.
cc @franciscojavierarceo @kubeflow/wg-pipeline-leads
|
I am missing a bit the CPU and memory requests vs Limit . I only see which is somehow mixing up requests with limits. Furthermore we should make sure that it also works with the integrated seaweedfs S3. |
|
|
||
| client = ( | ||
| SparkClient.builder() | ||
| .namespace("spark-jobs") |
There was a problem hiding this comment.
Will this integrate well with our current multi-tenancy architecture ? https://github.com/kubeflow/manifests#architecture
There was a problem hiding this comment.
So if i execute this from a jupyterlab will it just use the current namespace?
| # Resource configuration | ||
| def driver(self, cores: int = 1, memory: str = "1g") -> "SparkClientBuilder": | ||
| """Configure driver resources.""" | ||
|
|
||
| def executor(self, cores: int = 1, memory: str = "1g") -> "SparkClientBuilder": | ||
| """Configure executor resources.""" | ||
|
|
There was a problem hiding this comment.
I am missing a bit the CPU and memory requests vs Limit . I only see
.driver(cores=2, memory="4g")
.executor(cores=4, memory="8g")
which is somehow mixing up requests with limits. Furthermore we should make sure that it also works with the integrated seaweedfs S3.
There was a problem hiding this comment.
Thanks for pointing it out. There are lot of conf is spark so anyways we need wider key value pair set.
Signed-off-by: shekharrajak <[email protected]>
6d12de7 to
c785109
Compare
andreyvelich
left a comment
There was a problem hiding this comment.
Thanks @Shekharrajak!
Mostly lgtm, I suggest that we move some topics to phase 2 as we discussed.
/assign @kramaranya @kubeflow/kubeflow-sdk-team for the final feedback.
| } | ||
| ``` | ||
|
|
||
| ### Structured Types |
There was a problem hiding this comment.
I guess, we can remove this from the version 1, right?
And implement this in the 2nd phase if needed.
| ``` | ||
|
|
||
| ```python | ||
| class SparkClientBuilder: |
There was a problem hiding this comment.
Can you move Builder to the future plans, since we will implement it later if needed.
There was a problem hiding this comment.
Yeah, we will start simpler in first MVP
| token: Optional authentication token for existing server. | ||
| use_ssl: Whether to use SSL for connection (only for existing servers). |
There was a problem hiding this comment.
Does SparkConnect CRD support ssl and auth?
There was a problem hiding this comment.
No built-in SSL/TLS or authentication support in the SparkConnect CRD. I was thinking for external servers or submitting spark job. But let me remove it for now - we will think later on this.
| token: Optional authentication token for existing server. | ||
| use_ssl: Whether to use SSL for connection (only for existing servers). | ||
| name: Optional session name. Auto-generated if not provided (create mode only). | ||
| app_name: Optional Spark application name (create mode only). |
There was a problem hiding this comment.
We don't need this, right ?
There was a problem hiding this comment.
If user want to provide specific name - let's park it for phase 2
| If provided, connects to existing server. If None, creates new session. | ||
| token: Optional authentication token for existing server. | ||
| use_ssl: Whether to use SSL for connection (only for existing servers). | ||
| name: Optional session name. Auto-generated if not provided (create mode only). |
There was a problem hiding this comment.
WIll name be equal to the SparkConnect CR name? Can we move it to options for now, to be consistent with Trainer.
| def delete_session(self, name: str) -> None: | ||
| """Delete a Spark Connect session.""" | ||
|
|
||
| def submit_job( |
There was a problem hiding this comment.
Let's say that job submission APIs will be implemented in the phase 2.
|
I understand there will be bunch of iterations on the API design and to improve user experience. But definitely we will focus on first MVP as simple as possible with strong foundation and phase wise implement features. The more important point is to start the journey otherwise we will see no progress. |
Co-authored-by: Andrey Velichkevich <[email protected]> Signed-off-by: Shekhar Prasad Rajak <[email protected]>
Signed-off-by: shekharrajak <[email protected]>
andreyvelich
left a comment
There was a problem hiding this comment.
Yeah, I agree with you.
Thank you for this work @Shekharrajak, excited to see this moving forward!
/lgtm
/approve
/hold in case @kramaranya @astefanutti @szaher @Fiona-Waters wanted to give more comments.
/assign @kramaranya @astefanutti @szaher @Fiona-Waters
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: andreyvelich 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 |
kramaranya
left a comment
There was a problem hiding this comment.
Looks good to me! Thanks @Shekharrajak for your work
/lgtm
|
/hold cancel |
* docs(KEP-107): Add Spark Client SDK proposal Signed-off-by: shekharrajak <[email protected]> * Update docs/proposals/107-spark-client/README.md Co-authored-by: Andrey Velichkevich <[email protected]> Signed-off-by: Shekhar Prasad Rajak <[email protected]> * docs(KEP-107): Remove SSL/TLS support from Spark Client API Signed-off-by: shekharrajak <[email protected]> --------- Signed-off-by: shekharrajak <[email protected]> Signed-off-by: Shekhar Prasad Rajak <[email protected]> Co-authored-by: Andrey Velichkevich <[email protected]>
Ref https://docs.google.com/document/d/1l57bBlpxrW4gLgAGnoq9Bg7Shre7Cglv4OLCox7ER_s/edit?tab=t.0
PR: #158