Skip to content

feat(docs): KEP- Spark Client for Kubeflow SDK#163

Merged
google-oss-prow[bot] merged 3 commits into
kubeflow:mainfrom
Shekharrajak:feat/kep-spark-client
Jan 12, 2026
Merged

feat(docs): KEP- Spark Client for Kubeflow SDK#163
google-oss-prow[bot] merged 3 commits into
kubeflow:mainfrom
Shekharrajak:feat/kep-spark-client

Conversation

@Shekharrajak

@Shekharrajak Shekharrajak commented Nov 17, 2025

Copy link
Copy Markdown
Member

@coveralls

coveralls commented Nov 17, 2025

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 20839855538

Details

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage remained the same at 66.607%

Totals Coverage Status
Change from base Build 20780172530: 0.0%
Covered Lines: 2599
Relevant Lines: 3902

💛 - Coveralls

# Custom backend implementation
from kubeflow.spark.backends.base import SparkBackend

class CustomBackend(SparkBackend):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users can extend the backend, if they want to have any specific changes or different way to connect or submit spark job

@andreyvelich andreyvelich 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 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

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.

For Trainer, backends represent various job submission (local subprocess, container, and Kubernetes). I am not sure if we can replicate it for Spark.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we have job submission using K8S Operator backend, Spark Connector backend , Gateway backend (not implemented - we just have abstract class).

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 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 cluster

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 

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.

Do we know how the Spark session management will work with the new CRD: SparkCluster proposed by @ChenYi015 here: kubeflow/spark-operator#2744 ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +67 to +74
┌───────────┴─────────────┬──────────────────┬────────────────┐
▼ ▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│OperatorBackend │ │ GatewayBackend │ │ ConnectBackend │ │ LocalBackend │
│(Spark Operator │ │ (REST Gateway) │ │ (Spark Connect/ │ │ (Future) │
│ CRDs on K8s) │ │ │ │ Interactive) │ │ │
└──────────────────┘ └──────────────────┘ └──────────────────┘ └──────────────────┘
Batch Jobs Batch Jobs Sessions Local Dev

@andreyvelich andreyvelich Nov 23, 2025

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.

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?

@Shekharrajak Shekharrajak Nov 23, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"}                                     
) 

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.

Yeah, that might be interesting to explore.
We can also extend submit_job() API to accept main_application_file or function.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me add to KEP and will work on it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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.

@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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andreyvelich

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

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.

@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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Comment thread docs/proposals/107-spark-client/README.md Outdated
Comment thread docs/proposals/107-spark-client/README.md Outdated
Comment thread docs/proposals/107-spark-client/README.md Outdated
Comment on lines +476 to +488
# 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)

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 is pretty interesting experience for dev -> prod Spark lifecycle.
cc @shravan-achar @akshaychitneni @bigsur0 to explore.

Comment thread docs/proposals/107-spark-client/README.md Outdated
Comment thread docs/proposals/107-spark-client/README.md Outdated
)
```

#### Integration with Pipelines

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.

cc @kubeflow/wg-pipeline-leads to explore

Comment thread docs/proposals/107-spark-client/README.md Outdated
Comment on lines +295 to +302
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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will have more kubeflow compatible APIs and infra.

Comment thread docs/proposals/107-spark-client/README.md Outdated

Implements the backend for managed Spark gateways:

- Submits applications via REST endpoints

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a PR is added for gRPC submit support and the ability to implement a custom submit mechanism, then this becomes relevant.

@andreyvelich andreyvelich 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 for the update @Shekharrajak!
I left a few comments.

Comment on lines +86 to +87
client = (
SparkClient.builder()

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 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, I'd like us to start with constructor and config objects aligning with existing clients and later expand it with builder approach

Comment thread docs/proposals/107-spark-client/README.md Outdated
Comment on lines +113 to +115
```python
class SparkClientBuilder:
"""Builder for SparkClient configuration."""

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.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be internal changes, we can take care of it later as well.

Comment on lines +117 to +119
# Kubernetes settings
def namespace(self, ns: str) -> "SparkClientBuilder":
"""Set Kubernetes namespace."""

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.

Comment thread docs/proposals/107-spark-client/README.md Outdated
Comment thread docs/proposals/107-spark-client/README.md
Comment thread docs/proposals/107-spark-client/README.md Outdated
Comment on lines +541 to +558
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)

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 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

@juliusvonkohout

Copy link
Copy Markdown
Member

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.


client = (
SparkClient.builder()
.namespace("spark-jobs")

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.

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 if i execute this from a jupyterlab will it just use the current namespace?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Comment on lines +124 to +130
# 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."""

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing it out. There are lot of conf is spark so anyways we need wider key value pair set.

@Shekharrajak Shekharrajak force-pushed the feat/kep-spark-client branch from 6d12de7 to c785109 Compare January 8, 2026 14:54

@andreyvelich andreyvelich 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 @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

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 guess, we can remove this from the version 1, right?
And implement this in the 2nd phase if needed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes +1

Comment thread docs/proposals/107-spark-client/README.md Outdated
```

```python
class SparkClientBuilder:

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.

Can you move Builder to the future plans, since we will implement it later if needed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we will start simpler in first MVP

Comment on lines +323 to +324
token: Optional authentication token for existing server.
use_ssl: Whether to use SSL for connection (only for existing servers).

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.

Does SparkConnect CRD support ssl and auth?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed it for now a2a4837

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

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 don't need this, right ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

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.

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(

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 say that job submission APIs will be implemented in the phase 2.

@andreyvelich

Copy link
Copy Markdown
Member

/cc @ChenYi015 @nabuskey @vara-bonthu

@Shekharrajak

Copy link
Copy Markdown
Member Author

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.

Shekharrajak and others added 2 commits January 9, 2026 08:25
Co-authored-by: Andrey Velichkevich <[email protected]>
Signed-off-by: Shekhar Prasad Rajak <[email protected]>

@andreyvelich andreyvelich 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.

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

@google-oss-prow

Copy link
Copy Markdown
Contributor

[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

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kramaranya kramaranya left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! Thanks @Shekharrajak for your work
/lgtm

@andreyvelich

Copy link
Copy Markdown
Member

/hold cancel

@google-oss-prow google-oss-prow Bot merged commit 9ef0b9c into kubeflow:main Jan 12, 2026
14 of 15 checks passed
@google-oss-prow google-oss-prow Bot added this to the v0.3 milestone Jan 12, 2026
Shekharrajak added a commit to Shekharrajak/sdk that referenced this pull request Jan 13, 2026
* 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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.