Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/e2e-test-train-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:

- name: Run tests
run: |
pip install pytest
pip install pytest "kubernetes<36"
python3 -m pip install -e sdk/python[huggingface]
pytest -s sdk/python/test/e2e-fine-tune-llm/test_e2e_pytorch_fine_tune_llm.py --log-cli-level=debug
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/integration-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:

- name: Run tests
run: |
pip install pytest
pip install pytest "kubernetes<36"
python3 -m pip install -e sdk/python; pytest -s sdk/python/test/e2e --log-cli-level=debug --namespace=default
env:
GANG_SCHEDULER_NAME: ${{ matrix.gang-scheduler-name }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-example-notebooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:

- name: Install Python Dependencies
run: |
pip install papermill==2.6.0 jupyter==1.1.1 ipykernel==6.29.5
pip install papermill==2.6.0 jupyter==1.1.1 ipykernel==6.29.5 "kubernetes<36" "fastjsonschema<2.22"

- name: Run Jupyter Notebook with Papermill
shell: bash
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/test-python.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ jobs:

- name: Install dependencies
run: |
pip install pytest python-dateutil urllib3 kubernetes
# TODO: kubernetes 36.0.0 switched to Pydantic models, breaking the
# FakeResponse deserialization hack in the SDK unit tests. To remove
# this pin, either adapt FakeResponse in
# sdk/python/kubeflow/training/utils/utils.py for Pydantic or mock
# ApiClient.deserialize directly in the test suite.
pip install pytest python-dateutil urllib3 "kubernetes<36"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This pin, and the matching ones in integration-tests.yaml, e2e-test-train-api.yaml and test-example-notebooks.yaml, fixes a real breakage, and the TODO explaining the Pydantic switch is the kind of comment I wish more pins had. It just has nothing to do with MPI RBAC. Bundled like this, reverting the feature also reverts the CI fix. Worth its own PR so the two can land and be rolled back independently.

pip install -U './sdk/python[huggingface]'

- name: Run unit test for training sdk
Expand Down
3 changes: 3 additions & 0 deletions cmd/training-operator.v1/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ func main() {
// MPI related flags
flag.StringVar(&config.Config.MPIKubectlDeliveryImage, "mpi-kubectl-delivery-image",
config.MPIKubectlDeliveryImageDefault, "The image for mpi launcher init container")
flag.BoolVar(&config.Config.DisableMPIRBACManagement, "disable-mpi-rbac-management", false,
Comment thread
vicoooo26 marked this conversation as resolved.
"When set to true, disables the MPIJob controller's management of launcher RBAC resources (ServiceAccount, Role, RoleBinding) and ServiceAccountName injection. "+
"Users must provide their own ServiceAccount with a Role granting get, list, watch on pods and create on pods/exec.")

// Cert generation flags
flag.IntVar(&webhookServerPort, "webhook-server-port", 9443, "Endpoint port for the webhook server.")
Expand Down
1 change: 1 addition & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ var Config struct {
PyTorchInitContainerImage string
MPIKubectlDeliveryImage string
PyTorchInitContainerMaxTries int
DisableMPIRBACManagement bool
}

const (
Expand Down
75 changes: 42 additions & 33 deletions pkg/controller.v1/mpi/mpijob_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ import (
)

const (
FailedDeleteJobReason = "FailedDeleteJob"
SuccessfulDeleteJobReason = "SuccessfulDeleteJob"
FailedDeleteJobReason = "FailedDeleteJob"
SuccessfulDeleteJobReason = "SuccessfulDeleteJob"
NoServiceAccountNameReason = "NoServiceAccountName"

controllerName = "mpijob-controller"
labelMPIJobName = "mpi-job-name"
Expand Down Expand Up @@ -214,23 +215,25 @@ func (jc *MPIJobReconciler) SetupWithManager(mgr ctrl.Manager, controllerThreads
util.OnDependentFuncs[*corev1.ConfigMap](jc.Scheme, jc.Expectations, &jc.JobController))); err != nil {
return err
}
// inject watching for job related Role
if err = c.Watch(source.Kind[*rbacv1.Role](mgr.GetCache(), &rbacv1.Role{},
handler.TypedEnqueueRequestForOwner[*rbacv1.Role](mgr.GetScheme(), mgr.GetRESTMapper(), &kubeflowv1.MPIJob{}, handler.OnlyControllerOwner()),
util.OnDependentFuncs[*rbacv1.Role](jc.Scheme, jc.Expectations, &jc.JobController))); err != nil {
return err
}
// inject watching for job related RoleBinding
if err = c.Watch(source.Kind[*rbacv1.RoleBinding](mgr.GetCache(), &rbacv1.RoleBinding{},
handler.TypedEnqueueRequestForOwner[*rbacv1.RoleBinding](mgr.GetScheme(), mgr.GetRESTMapper(), &kubeflowv1.MPIJob{}, handler.OnlyControllerOwner()),
util.OnDependentFuncs[*rbacv1.RoleBinding](jc.Scheme, jc.Expectations, &jc.JobController))); err != nil {
return err
}
// inject watching for job related ServiceAccount
if err = c.Watch(source.Kind[*corev1.ServiceAccount](mgr.GetCache(), &corev1.ServiceAccount{},
handler.TypedEnqueueRequestForOwner[*corev1.ServiceAccount](mgr.GetScheme(), mgr.GetRESTMapper(), &kubeflowv1.MPIJob{}, handler.OnlyControllerOwner()),
util.OnDependentFuncs[*corev1.ServiceAccount](jc.Scheme, jc.Expectations, &jc.JobController))); err != nil {
return err
if !ctlrconfig.Config.DisableMPIRBACManagement {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No test fails when this guard is removed, as noted on the watch spec. It is also the one part of the feature whose effect never shows up in the API: a stray RBAC watch just means the operator still needs list and watch on Roles and RoleBindings cluster-wide, which is the thing someone turning this flag on is trying to get rid of. That makes it the piece most likely to regress unnoticed.

// inject watching for job related Role
if err = c.Watch(source.Kind[*rbacv1.Role](mgr.GetCache(), &rbacv1.Role{},
handler.TypedEnqueueRequestForOwner[*rbacv1.Role](mgr.GetScheme(), mgr.GetRESTMapper(), &kubeflowv1.MPIJob{}, handler.OnlyControllerOwner()),
util.OnDependentFuncs[*rbacv1.Role](jc.Scheme, jc.Expectations, &jc.JobController))); err != nil {
return err
}
// inject watching for job related RoleBinding
if err = c.Watch(source.Kind[*rbacv1.RoleBinding](mgr.GetCache(), &rbacv1.RoleBinding{},
handler.TypedEnqueueRequestForOwner[*rbacv1.RoleBinding](mgr.GetScheme(), mgr.GetRESTMapper(), &kubeflowv1.MPIJob{}, handler.OnlyControllerOwner()),
util.OnDependentFuncs[*rbacv1.RoleBinding](jc.Scheme, jc.Expectations, &jc.JobController))); err != nil {
return err
}
// inject watching for job related ServiceAccount
if err = c.Watch(source.Kind[*corev1.ServiceAccount](mgr.GetCache(), &corev1.ServiceAccount{},
handler.TypedEnqueueRequestForOwner[*corev1.ServiceAccount](mgr.GetScheme(), mgr.GetRESTMapper(), &kubeflowv1.MPIJob{}, handler.OnlyControllerOwner()),
util.OnDependentFuncs[*corev1.ServiceAccount](jc.Scheme, jc.Expectations, &jc.JobController))); err != nil {
return err
}
}
// skip watching volcano PodGroup if volcano PodGroup is not installed
if _, err = mgr.GetRESTMapper().RESTMapping(schema.GroupKind{Group: v1beta1.GroupName, Kind: "PodGroup"},
Expand Down Expand Up @@ -367,24 +370,25 @@ func (jc *MPIJobReconciler) ReconcilePods(
}
isGPULauncher := isGPULauncher(mpiJob)

// Get the launcher ServiceAccount for this MPIJob.
if sa, err := jc.getOrCreateLauncherServiceAccount(mpiJob); sa == nil || err != nil {
return err
}

// Get the ConfigMap for this MPIJob.
if config, err := jc.getOrCreateConfigMap(mpiJob, workerReplicas, isGPULauncher); config == nil || err != nil {
return err
}

// Get the launcher Role for this MPIJob.
if r, err := jc.getOrCreateLauncherRole(mpiJob, workerReplicas); r == nil || err != nil {
return err
}
if !ctlrconfig.Config.DisableMPIRBACManagement {
// Get the launcher ServiceAccount for this MPIJob.
if sa, err := jc.getOrCreateLauncherServiceAccount(mpiJob); sa == nil || err != nil {
return err
}
// Get the launcher Role for this MPIJob.
if r, err := jc.getOrCreateLauncherRole(mpiJob, workerReplicas); r == nil || err != nil {
return err
}

// Get the launcher RoleBinding for this MPIJob.
if rb, err := jc.getLauncherRoleBinding(mpiJob); rb == nil || err != nil {
return err
// Get the launcher RoleBinding for this MPIJob.
if rb, err := jc.getLauncherRoleBinding(mpiJob); rb == nil || err != nil {
return err
}
}

worker, err = jc.getOrCreateWorker(mpiJob)
Expand Down Expand Up @@ -1034,8 +1038,13 @@ func (jc *MPIJobReconciler) newLauncher(mpiJob *kubeflowv1.MPIJob, kubectlDelive
jc.PodGroupControl.DecoratePodTemplateSpec(podSpec, mpiJob, rt)
}

if len(mpiJob.Spec.MPIReplicaSpecs[kubeflowv1.MPIJobReplicaTypeLauncher].Template.Spec.ServiceAccountName) == 0 {
podSpec.Spec.ServiceAccountName = launcherName
if !ctlrconfig.Config.DisableMPIRBACManagement {
Comment thread
vicoooo26 marked this conversation as resolved.
if len(mpiJob.Spec.MPIReplicaSpecs[kubeflowv1.MPIJobReplicaTypeLauncher].Template.Spec.ServiceAccountName) == 0 {
podSpec.Spec.ServiceAccountName = launcherName
}
} else if len(mpiJob.Spec.MPIReplicaSpecs[kubeflowv1.MPIJobReplicaTypeLauncher].Template.Spec.ServiceAccountName) == 0 {
jc.Recorder.Eventf(mpiJob, corev1.EventTypeWarning, NoServiceAccountNameReason,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

With the flag on and no serviceAccountName on the launcher, this branch emits a warning but leaves podSpec.Spec.ServiceAccountName empty. The launcher then runs under the namespace default SA, which on a stock cluster cannot do what it needs. I checked on ACK v1.36.1-aliyun.1 with kubectl auth can-i --as=system:serviceaccount:default:default: get, list and watch on pods all return no, and so does create on pods/exec. So the MPIJob is admitted happily and then dies at runtime when the launcher tries to exec into the workers.

Two things make the warning weaker than it first looks.

First, newLauncher is the only caller (line 400, when the launcher Pod is created), so the event fires exactly once. After it ages out of the API server, or if the operator is upgraded while a launcher already exists, nothing points at the misconfiguration any more. There is no status condition, and the Pod spec looks unremarkable.

Second, and this is the part that surprised me: re-emitting the same warning on every reconcile would not fix it either. client-go's event correlator keys on (involvedObject, reason, message) and patches or suppresses instead of creating a new object. I patched the controller to re-emit on every reconcile to check. With an identical message no new event ever appeared; with a unique message per reconcile it did.

So whatever the durable signal ends up being, it has to be a status condition on the MPIJob or a fail-fast (reject in a validating webhook, or return a reconcile error). A repeated event will just get deduped. Either way, it would help to spell out the permissions a user-supplied SA needs: get, list and watch on pods, plus create on pods/exec.

Test logs and the mutation matrix behind this: https://github.com/cheyang/trainer/tree/verify/aliyun-trainer-prs/docs/verification/aliyun-trainer-prs

"Launcher has no ServiceAccountName; ensure the namespace default ServiceAccount has get, list, watch pods and create pods/exec permissions")
}

podSpec.Spec.InitContainers = append(podSpec.Spec.InitContainers, corev1.Container{
Expand Down
Loading
Loading