Repository navigation
feat(trainer): add interactive TrainJob sessions - #886
nothariharan wants to merge 3 commits into
Conversation
The Trainer SDK could only submit training jobs, so running a command in a training job meant creating a TrainJob with the "sleep infinity" command and exec-ing into the launcher Pod with kubectl, as described in the Kubeflow website guide (kubeflow/website#4492). TrainerClient.connect() does that in the SDK. It creates the TrainJob, waits for the trainer Pod and returns a TrainJobSession to run commands in: from kubeflow.trainer import CustomTrainerContainer, TrainerClient client = TrainerClient() runtime = client.get_runtime("mpi-interactive") with client.connect( runtime=runtime, trainer=CustomTrainerContainer(image=runtime.trainer.image, num_nodes=4), ) as session: result = session.execute(["mpirun", "hostname"]) print(result.stdout) The trainer Pod keeps running with the default "sleep infinity" command, unless the TrainerCommand option overrides it. The commands run through the Kubernetes pods/exec API and their output, error and exit code come back in an ExecResult. Closing the session deletes the TrainJob and waits for its Pods to be deleted, so that the job name can be reused right away. Signed-off-by: nothariharan <nothariharan@gmail.com>
The CONTRIBUTING guide asks for a KEP with every significant change to the Kubeflow Trainer. The KEP describes the connect() API, the session and the ExecResult, the design decisions, and the test plan. Signed-off-by: nothariharan <nothariharan@gmail.com>
|
🎉 Welcome to the Kubeflow SDK! 🎉 Thanks for opening your first PR! We're happy to have you as part of our community 🚀 Here's what happens next:
Join the community:
Feel free to ask questions in the comments if you need any help or clarification! |
Signed-off-by: nothariharan <nothariharan@gmail.com>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
|
/hold This will first be discussed in a KEP, see #868 (comment) |
|
Thanks for this POC @nothariharan, but I will close it for now, since we need to gather more user stories and use-cases in the Trainer KEP first. |
|
@andreyvelich: Closed this PR. DetailsIn response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. |
|
Thanks @andreyvelich and @christian-heusel, makes sense. We were not aware of the KEP flow, so apologies for jumping ahead with the code. The branch stays up in the fork, and we are happy to help with the KEP and the user stories whenever you need them. |
What this PR does / why we need it:
This PR adds a
connect()API to the Kubeflow Trainer SDK.TrainerClient.connect()creates a TrainJob that keeps running and returns aTrainJobSessionthat runs commands in the trainer Pod of that job.Today the only way to run a command in a training job is to do it by hand. You create a TrainJob whose trainer command keeps the Pod alive, for example
sleep infinity, you wait for the job, you find the launcher Pod with the JobSet labels, youkubectl execinto that Pod, and you delete the job afterwards. That flow is documented in the Kubeflow Trainer integration guide in the website repository (kubeflow/website#4492). It works, but it needskubectl, it needs knowledge of the JobSet labels, and it cannot be done from a notebook.The API follows the same shape as
SparkClient.connect()that was added for Kubeflow Spark, so both SDKs feel the same for users. The design is described in the KEP that is included in this PR.The API
TrainerClient.connect(runtime, initializer, trainer, options, timeout, polling_interval)takes the sameruntime,initializer,trainerandoptionsarguments astrain(). It creates the TrainJob, waits for the trainer Pod to run and returns aTrainJobSession.TrainJobSessionhas three public members:nameexecute(command, *, container=None)ExecResult. A string command runs withsh -c, a list of arguments runs directly, the same waykubectl execruns a command.close(timeout=180)ExecResultholds thestdout, thestderrand thereturncodeof the command. ThereturncodeisNonewhen the Kubernetes API server did not report an exit code.How it works
Creating the job.
connect()callstrain()with the arguments of the caller. When the caller did not pass aTrainerCommandoption, the SDK injects one with thesleep infinitycommand, so the trainer Pod stays alive. The command lives inkubeflow.trainer.constants.constantsasINTERACTIVE_COMMAND. When the caller passes their ownTrainerCommand, that command is used and nothing is injected.Selecting the trainer Pod. The Pod that runs the trainer command is the Pod with the
trainer.kubeflow.org/trainjob-ancestor-step: trainerlabel. For the MPI runtimes it is the launcher Pod, for the other runtimes it is the node Pod. Every runtime that ships with Kubeflow Trainer sets that label. The label matters for the MPI runtimes, because their node Pods only run the SSH server, and the launcher Pod is created only after the node Pods are ready. Without the label, a session could be created on a node Pod before the launcher Pod exists, and the commands would run on the wrong Pod. When no Pod has the label, the launcher Pod is used as a fallback.Waiting for the session.
connect()polls the Pods of the TrainJob until the trainer Pod is in theRunningphase. A Pod in theSucceededor theFailedphase raises aRuntimeError, because the job cannot run any command in that state. When the Pod does not start within the timeout,connect()raises aTimeoutErrorand the message tells the user that the job was created and how to delete it.Running the commands. The commands run through the Kubernetes
pods/execAPI. The client useskubernetes.stream.streamwith_preload_content=False, which returns the WebSocket client that streams the output of the command. The client reads the standard output and the standard error channels until the connection is closed, and then it reads the exit code from the error channel. Executing commands in a Pod requires thepods/execpermission in the namespace of the TrainJob.Closing the session.
close()deletes the TrainJob and polls the Pods until they are gone. Waiting for the Pods matters, because the SSH keys of the MPI runtimes are mounted from a secret that is named after the job, so the old Pods have to be gone before a new job with the same name can start. When the Pods are still there after the timeout, the client logs a warning and returns instead of raising.Backends.
connect()is part of theRuntimeBackendinterface. The Kubernetes backend implements it. The LocalProcess and the Container backends raise aNotImplementedError, the same way they do for the other Kubernetes only APIs.What was tested
Unit tests
The new unit tests are in
kubeflow/trainer/backends/kubernetes/backend_test.py,kubeflow/trainer/types/types_test.py,kubeflow/trainer/api/trainer_client_test.py,kubeflow/trainer/backends/localprocess/backend_test.pyandkubeflow/trainer/backends/container/backend_test.py. They cover:connect()creates the TrainJob with thesleep infinitycommand.connect()does not override aTrainerCommandthat the caller passed.connect()rejects a trainer that is not aCustomTrainerContainer.connect()waits for the trainer Pod and never connects to the node Pods of the MPI runtimes.connect()times out when the Pod does not start, and it fails when the Pod has already finished.connect()falls back to the launcher Pod when the ancestor label is not set.execute()runs the command in the trainer Pod with thepods/execAPI, with the right container, and with the command as a list and as a string.execute()returns the output, the error output and the exit code. It reports an empty command, a missing Pod, a Pod that is not running and a failure of the exec call.close()deletes the TrainJob, waits for the Pods and is idempotent.execute()raises aRuntimeErrorin a closed session.NotImplementedError.The full suite of the repository passes, and the pre-commit hooks pass:
The documentation builds without new warnings, and the new classes are in the API reference.
End to end test on a real cluster
The end to end test ran against a real cluster, not against mocks:
mpi-interactiveClusterTrainingRuntime of the Kubeflow website guide. It has one launcher Pod and three node Pods, with OpenMPI and the SSH server of the MPI Operator, andrunLauncherAsNode: trueso the launcher counts as one of the MPI nodes.The test created a session with four nodes and checked the following:
So the checks were:
connect()returned once the launcher Pod was running, and the steps of the TrainJob were the launcher and the three nodes.mpirun hostnamereturned the hostnames of the launcher Pod and of the three node Pods, so the MPI setup works and the command ran in the right Pod.$(hostname)substitution worked.RuntimeError.The end to end test found two problems in the first version of the implementation, and both of them are fixed and covered by a unit test now:
stream()is called without_preload_content=False. The client has to ask for the WebSocket client, otherwise there is nothing to read the output from.job-indexlabel of the node Pods, and it connected to a node Pod of the MPI runtime before the launcher Pod existed. The commands then ran on the SSH server Pod instead of the launcher Pod. The selection now uses thetrainjob-ancestor-steplabel of the trainer Pod, andconnect()waits for that Pod.One note about the test environment. The image of the guide,
ghcr.io/kubeflow/trainer/deepspeed-runtime:v2.3.0, is 27.7 GB because it is based on CUDA, and it did not fit on the machine that ran the test. The test used a smaller image with the same OpenMPI version, the same SSH setup and the same Pod layout, and only the base image is different. The runtime manifest of the guide is otherwise unchanged.Compatibility and limitations
CustomTrainerContainer. TheCustomTrainerand theBuiltinTrainergenerate their own command, so their training process exits when the training finishes.connect(). The LocalProcess and the Container backends raise aNotImplementedError.ExecResultinstead of being streamed while the command runs.pods/execpermission in the namespace of the TrainJob.Documentation
kubeflow.trainer.TrainJobSessionandkubeflow.trainer.ExecResult.docs/source/train/interactive-session.rstwith the usage, the keep alive command and the requirements.proposals/868-interactive-trainjob-sessions.Which issue(s) this PR fixes (optional, in
Fixes #<issue number>, #<issue number>, ...format, will close the issue(s) when PR gets merged):Fixes #868
Checklist:
proposals/868-interactive-trainjob-sessions