From bf629cc37ca4df1395657ae556cc89678c550b1e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Singh <60318530+SahilKumar75@users.noreply.github.com> Date: Sat, 22 Aug 2026 18:04:18 +0530 Subject: [PATCH] fix(trainer): unpack train_func_parameters as kwargs in localprocess backend The LocalProcess backend embedded train_func_parameters as a single positional dict argument instead of unpacking it as keyword arguments, unlike the Kubernetes backend which already does this correctly. Any CustomTrainer using func_args with LocalProcessBackend would fail with a TypeError since the training function receives one dict positional argument instead of its named parameters. Signed-off-by: Sahil Kumar Singh <60318530+SahilKumar75@users.noreply.github.com> --- .../trainer/backends/localprocess/utils.py | 2 +- .../backends/localprocess/utils_test.py | 86 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 kubeflow/trainer/backends/localprocess/utils_test.py diff --git a/kubeflow/trainer/backends/localprocess/utils.py b/kubeflow/trainer/backends/localprocess/utils.py index ef01fb501..9d61b25d3 100644 --- a/kubeflow/trainer/backends/localprocess/utils.py +++ b/kubeflow/trainer/backends/localprocess/utils.py @@ -216,7 +216,7 @@ def get_command_using_train_func( if train_func_parameters is None: func_code = f"{func_code}\n{train_func.__name__}()\n" else: - func_code = f"{func_code}\n{train_func.__name__}({train_func_parameters})\n" + func_code = f"{func_code}\n{train_func.__name__}(**{train_func_parameters})\n" with open(func_file, "w") as f: f.write(func_code) diff --git a/kubeflow/trainer/backends/localprocess/utils_test.py b/kubeflow/trainer/backends/localprocess/utils_test.py new file mode 100644 index 000000000..853a65582 --- /dev/null +++ b/kubeflow/trainer/backends/localprocess/utils_test.py @@ -0,0 +1,86 @@ +# Copyright 2025 The Kubeflow Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from kubeflow.trainer.backends.localprocess import utils +from kubeflow.trainer.backends.localprocess.constants import LOCAL_RUNTIME_IMAGE +from kubeflow.trainer.backends.localprocess.types import LocalRuntimeTrainer +from kubeflow.trainer.test.common import SUCCESS, TestCase +from kubeflow.trainer.types import types + + +def _build_runtime() -> types.Runtime: + return types.Runtime( + name="test-runtime", + trainer=LocalRuntimeTrainer( + trainer_type=types.TrainerType.CUSTOM_TRAINER, + framework="torch", + image=LOCAL_RUNTIME_IMAGE, + ), + kind=types.RuntimeKind.TRAINING_RUNTIME, + ) + + +def train_with_kwargs(lr: float, num_epochs: int): + print(f"Training with lr={lr}, num_epochs={num_epochs}") + + +def train_with_no_args(): + print("Training with no arguments") + + +@pytest.mark.parametrize( + "test_case", + [ + TestCase( + name="train func with parameters is called with kwargs unpacking", + expected_status=SUCCESS, + config={ + "train_func": train_with_kwargs, + "train_func_parameters": {"lr": 0.01, "num_epochs": 5}, + }, + expected_output=f"{train_with_kwargs.__name__}(**{{'lr': 0.01, 'num_epochs': 5}})", + ), + TestCase( + name="train func without parameters is called with no arguments", + expected_status=SUCCESS, + config={ + "train_func": train_with_no_args, + "train_func_parameters": None, + }, + expected_output=f"{train_with_no_args.__name__}()", + ), + ], +) +def test_get_command_using_train_func_generates_valid_call(test_case, tmp_path): + runtime = _build_runtime() + + utils.get_command_using_train_func( + runtime=runtime, + train_func=test_case.config["train_func"], + train_func_parameters=test_case.config["train_func_parameters"], + venv_dir=str(tmp_path), + train_job_name="test-job", + ) + + func_file = tmp_path / "train_test-job.py" + generated_code = func_file.read_text() + + assert test_case.expected_output in generated_code + # The generated call must be valid Python that we can actually compile and execute, + # proving the training function receives its arguments as named parameters rather + # than as a single positional dict. + namespace: dict = {} + exec(generated_code, namespace)