Set up scheduler simulation for Alfred Unreleased: coming in v1.3

Load your schedulers' configurations into Alfred, so that it predicts where the replacement pods of each move would land.

Scheduler simulation lets Alfred predict where the replacement pods of each move would land, by running your scheduler's configuration against a copy of the cluster. In recommend-only mode, the prediction shows in Alfred's recommendations. To let Alfred migrate Instances, you need it: Alfred sends a request only when the simulation places every replacement pod. Alfred is alpha.

Alfred runs each simulation in a worker: the /alfred-simulator binary in Alfred's image, loaded with the configuration of one scheduler. You list the workers in an immutable ConfigMap, and give Alfred the identities that the workers report. Alfred checks the workers when it starts, and exits when one fails to load or reports another identity.

Step 1: Pin Alfred's image

A worker's identity includes a hash of Alfred's build, so the profiles you print in Step 3 must come from the image that Alfred runs. Find the digest that your Alfred pods run:

Shell
kubectl get pods -n ome -l control-plane=ome-alfred \
  -o jsonpath='{range .items[*]}{.status.containerStatuses[0].imageID}{"\n"}{end}'
Output
registry.example.com/ome/alfred@sha256:<digest>
registry.example.com/ome/alfred@sha256:<digest>
registry.example.com/ome/alfred@sha256:<digest>

Each line is one pod's image. When the pods show different digests, the tag moved between their pulls: pick the build you want. Add its digest to the tag in your values.yaml, so that all Alfred pods run that build:

values.yaml
global:
  hub: registry.example.com/ome
image:
  tag: v1.3.0@sha256:<digest>

The chart then runs registry.example.com/ome/alfred:v1.3.0@sha256:<digest>. You apply this change in Step 5.

Step 2: Write the scheduler configurations

Each worker simulates one scheduler, from a KubeSchedulerConfiguration file. Write one file for each scheduler that your OMENative pods name in schedulerName. For pods that name default-scheduler, or no scheduler:

default-scheduler.yaml
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
  - schedulerName: default-scheduler

For the OME scheduler, copy the configuration that it runs from its ConfigMap:

Shell
kubectl get configmap ome-scheduler-config -n ome -o jsonpath='{.data.config\.yaml}' > ome-scheduler.yaml

The command prints nothing. ome-scheduler.yaml now holds the OME scheduler's one profile, ome-scheduler, which runs the OMEGangPack plugin. Without the OME scheduler, leave out everything below that names ome-scheduler: its --from-file flags, its container in the Pod, its worker in workers.json and its profile in values.yaml.

The file above and the OME scheduler's configuration, as its chart renders it, meet the rules that the simulator checks, which any other file must meet too. The simulator runs these configurations on the scheduler of Kubernetes v1.35.4, whatever version your cluster runs, so a prediction can differ from what your scheduler does.

With --print-profile, the simulator prints a worker's profile, which holds the identity that Alfred checks. Put both files in a ConfigMap:

Shell
kubectl create configmap alfred-print-profile -n ome \
  --from-file=default-scheduler.yaml --from-file=ome-scheduler.yaml
Output
configmap/alfred-print-profile created

Then run each file through the simulator from the image you pinned. This Pod runs one container per file, with the same mount path and security settings as Alfred. --backend is a name that you choose for the worker, and becomes part of its identity:

alfred-print-profile.yaml
apiVersion: v1
kind: Pod
metadata:
  name: alfred-print-profile
  namespace: ome
spec:
  restartPolicy: Never
  automountServiceAccountToken: false
  enableServiceLinks: false
  securityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: default-scheduler
      image: registry.example.com/ome/alfred:v1.3.0@sha256:<digest>
      command: ["/alfred-simulator"]
      args:
        - --backend=kube-v135
        - --scheduler-config=/etc/alfred-simulation/default-scheduler.yaml
        - --print-profile
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
      volumeMounts:
        - name: config
          mountPath: /etc/alfred-simulation
          readOnly: true
    - name: ome-scheduler
      image: registry.example.com/ome/alfred:v1.3.0@sha256:<digest>
      command: ["/alfred-simulator"]
      args:
        - --backend=ome-v135
        - --scheduler-config=/etc/alfred-simulation/ome-scheduler.yaml
        - --print-profile
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
      volumeMounts:
        - name: config
          mountPath: /etc/alfred-simulation
          readOnly: true
  volumes:
    - name: config
      configMap:
        name: alfred-print-profile

If your registry needs credentials, add the pull secrets from global.imagePullSecrets to the Pod, under spec.imagePullSecrets.

Shell
kubectl apply -f alfred-print-profile.yaml
Output
pod/alfred-print-profile created

Wait for both containers to finish:

Shell
kubectl wait pod/alfred-print-profile -n ome --for=jsonpath='{.status.phase}'=Succeeded --timeout=90s
Output
pod/alfred-print-profile condition met

Each container's log holds its worker's profile:

Shell
kubectl logs alfred-print-profile -n ome -c default-scheduler
Output
{"identity":{"schedulerName":"default-scheduler","backend":"kube-v135","schedulerVersion":"v1.35.4","configurationID":"sha256:<default-id>"},"gangScheduling":false}
Shell
kubectl logs alfred-print-profile -n ome -c ome-scheduler
Output
{"identity":{"schedulerName":"ome-scheduler","backend":"ome-v135","schedulerVersion":"v1.35.4","configurationID":"sha256:<ome-id>"},"gangScheduling":true}

Copy both profiles exactly. The configurationID changes when the configuration or Alfred's image changes, and gangScheduling is true when OMEGangPack runs.

When the simulator rejects a configuration, its container fails, the Pod ends Failed and the wait times out: that container's log says what's wrong. When the Pod stays Pending, kubectl describe pod alfred-print-profile -n ome shows why, for example an image pull error.

Once you've copied the profiles, remove the Pod and its ConfigMap:

Shell
kubectl delete pod,configmap alfred-print-profile -n ome
Output
pod "alfred-print-profile" deleted from ome namespace
configmap "alfred-print-profile" deleted from ome namespace

Step 4: Create the simulation ConfigMap

List the workers in workers.json, with the profiles you printed and the paths where Alfred mounts the ConfigMap, under /etc/alfred-simulation:

workers.json
{
  "workers": [
    {
      "binaryPath": "/alfred-simulator",
      "schedulerConfigPath": "/etc/alfred-simulation/default-scheduler.yaml",
      "identity": {
        "schedulerName": "default-scheduler",
        "backend": "kube-v135",
        "schedulerVersion": "v1.35.4",
        "configurationID": "sha256:<default-id>"
      },
      "gangScheduling": false
    },
    {
      "binaryPath": "/alfred-simulator",
      "schedulerConfigPath": "/etc/alfred-simulation/ome-scheduler.yaml",
      "identity": {
        "schedulerName": "ome-scheduler",
        "backend": "ome-v135",
        "schedulerVersion": "v1.35.4",
        "configurationID": "sha256:<ome-id>"
      },
      "gangScheduling": true
    }
  ]
}

Alfred reads this file strictly: an unknown field or a repeated identity stops it at startup.

Create the simulation ConfigMap from the three files, then make it immutable, so that it can't change under a running Alfred:

Shell
kubectl create configmap alfred-simulation-1 -n ome \
  --from-file=workers.json --from-file=default-scheduler.yaml --from-file=ome-scheduler.yaml
Output
configmap/alfred-simulation-1 created
Shell
kubectl patch configmap alfred-simulation-1 -n ome -p '{"immutable":true}'
Output
configmap/alfred-simulation-1 patched

When you later change a scheduler's configuration or Alfred's image, print the profiles again, create a ConfigMap with a new name, such as alfred-simulation-2, and point Alfred at it.

Step 5: Point Alfred at the workers

Add the ConfigMap and the profiles to your values.yaml, next to the image you pinned. alfredConfig.scheduling.profiles is keyed by the scheduler name, and each entry repeats that worker's identity:

values.yaml
simulation:
  configMapName: alfred-simulation-1
alfredConfig:
  scheduling:
    profiles:
      default-scheduler:
        backend: kube-v135
        schedulerVersion: v1.35.4
        configurationID: sha256:<default-id>
      ome-scheduler:
        backend: ome-v135
        schedulerVersion: v1.35.4
        configurationID: sha256:<ome-id>
        gangScheduling: true

Upgrade the release:

Shell
helm upgrade ome-alfred oci://ghcr.io/moirai-internal/charts/ome-alfred \
  --version 1.3.0 --namespace ome -f values.yaml

Helm upgrades the release and prints its status. Wait for the new pods:

Shell
kubectl rollout status deployment/ome-alfred -n ome
Output
deployment "ome-alfred" successfully rolled out

kubectl can print Waiting for deployment "ome-alfred" rollout to finish lines first. Alfred checks the workers before its pods become ready, so a finished rollout means that the workers loaded. When the rollout doesn't finish, see Alfred exits at startup.

Step 6: Check the predictions

After the next decision pass, 5 minutes at most by default, read the predictions from the recommendation record:

Shell
kubectl get configmap alfred-recommendations -n ome -o jsonpath='{.data.last-cycle\.json}' \
  | jq '.recommendations[] | select(.scheduling) | {workload, component, instance, advisoryReason, scheduling: (.scheduling | {schedulerName, status, reason})}'
Output
{
  "workload": "prod/chat",
  "component": "engine",
  "instance": 3,
  "advisoryReason": "OMENativeUnavailable",
  "scheduling": {
    "schedulerName": "default-scheduler",
    "status": "Feasible",
    "reason": "PlacementFound"
  }
}

The command prints nothing until a policy proposes to move an OMENative Instance. Each move stays advisory, with OMENativeUnavailable, until you turn on migration. Its scheduling.status is Feasible, with the reason PlacementFound, when the simulation placed every replacement pod, and Infeasible when it didn't. Otherwise, scheduling.reason says why there's no prediction:

Reason Meaning
ProfileNotConfigured No profile has the scheduler name that the pods use. Add a worker and a profile for that scheduler.
GangUnsupported The Instance has more than one pod, and its profile doesn't have gangScheduling: true. Make its pods use the OME scheduler.
WorkerFailed The worker didn't answer, for example because the profile differs from its identity in workers.json. Print the profiles again.
CaptureFailed Alfred couldn't copy the cluster, for example because the PodGroup CRD is missing: see Before you begin.
SimulationBudgetExceeded The move was past the 8 that Alfred simulates in a pass, or the pass ran out of its 30 seconds for simulation.

Scheduling results lists every reason.

Troubleshooting

Alfred exits at startup

A worker that fails to load, or reports another identity, makes Alfred exit, and the rollout doesn't finish. Read the logs of the Alfred pods:

Shell
kubectl logs -n ome -l control-plane=ome-alfred --tail=-1 --prefix

A pod that's restarting may print nothing yet: add --previous to read its last run. Look for the error unable to configure recommendation simulation, and the cause after it:

Cause What to do
worker profile identity does not match registry entry The identity in workers.json isn't the one that the worker prints. Print the profiles again from the digest that Alfred runs, and create a new ConfigMap.
worker gang scheduling claim does not match registry entry Fix the worker's gangScheduling in workers.json, in a new ConfigMap.
scheduler worker exited unsuccessfully The worker couldn't load its configuration. Run it with --print-profile, as in Step 3, to see why.

The error invalid startup configuration means that a chart value is out of range, and the cause names its flag: see Helm chart values.

Clean up

If you turned on migration, turn it off first: see Clean up in Let Alfred migrate Instances. Then remove simulation and alfredConfig.scheduling from your values.yaml, upgrade the release as in Step 5, and delete the ConfigMap:

Shell
kubectl delete configmap alfred-simulation-1 -n ome
Output
configmap "alfred-simulation-1" deleted from ome namespace

Next steps